@aepyornis/fastboot.ts 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mise.toml CHANGED
@@ -1,2 +1,2 @@
1
1
  [tools]
2
- node = "22"
2
+ node = "24"
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@aepyornis/fastboot.ts",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Fastboot using WebUSB",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
8
- "build": "esbuild src/index.ts --bundle --sourcemap --format=esm --platform=browser --target=chrome121 --outfile=dist/fastboot.js"
8
+ "build": "esbuild src/index.ts --bundle --sourcemap --format=esm --platform=browser --target=chrome132 --outfile=dist/fastboot.js"
9
9
  },
10
10
  "keywords": [],
11
11
  "author": "Ziggy, Calyx Institute",
@@ -13,9 +13,9 @@
13
13
  "devDependencies": {
14
14
  "@eslint/js": "^9.24.0",
15
15
  "@types/w3c-web-usb": "^1.0.10",
16
- "esbuild": "^0.25.2",
16
+ "esbuild": "^0.27.2",
17
17
  "eslint": "^9.24.0",
18
- "globals": "^16.0.0",
18
+ "globals": "^17.0.0",
19
19
  "prettier": "^3.5.3",
20
20
  "typescript": "^5.8.3",
21
21
  "typescript-eslint": "^8.29.1"
package/src/client.ts CHANGED
@@ -17,17 +17,21 @@ interface Logger {
17
17
  log(message: string): void
18
18
  }
19
19
 
20
+ interface KeyValueDict {
21
+ [key: string]: string
22
+ }
23
+
20
24
  // higher level API to interact with fastboot device
21
25
  // translates CLI commands to
22
26
  export class FastbootClient {
23
27
  fd: FastbootDevice
24
28
  logger: Logger
25
- var_cache: Object
29
+ var_cache: KeyValueDict
26
30
 
27
31
  constructor(usb_device: USBDevice, logger: Logger = window.console) {
28
32
  this.fd = new FastbootDevice(usb_device, logger)
29
33
  this.logger = logger
30
- this.var_cache = {}
34
+ this.var_cache = {} as KeyValueDict
31
35
  }
32
36
 
33
37
  async getVar(variable: string) {
@@ -72,7 +76,6 @@ export class FastbootClient {
72
76
  }
73
77
 
74
78
  async rebootFastboot() {
75
- const serialNumber = this.fd.serialNumber
76
79
  this.logger.log("rebooting into fastboot")
77
80
  await this.fd.exec("reboot-fastboot")
78
81
  await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -363,13 +366,12 @@ export class FastbootClient {
363
366
  })
364
367
  }
365
368
 
366
- static async findOrRequestDevice(serialNumber: string): Promise<Void> {
367
- for (const device of (await navigator.usb.getDevices())) {
369
+ static async findOrRequestDevice(serialNumber: string): Promise<USBDevice> {
370
+ for (const device of await navigator.usb.getDevices()) {
368
371
  if (device.serialNumber === serialNumber) {
369
- return device
372
+ return device
370
373
  }
371
374
  }
372
375
  return await FastbootClient.requestUsbDevice()
373
376
  }
374
-
375
377
  }
package/src/device.ts CHANGED
@@ -9,7 +9,7 @@ type ResponsePacket = {
9
9
  }
10
10
 
11
11
  type FastbootSession = {
12
- phase: 1 | 2 | 3 | 4 | 5
12
+ // phase: 1 | 2 | 3 | 4 | 5
13
13
  status: null | "OKAY" | "FAIL"
14
14
  packets: (CommandPacket | ResponsePacket)[]
15
15
  }
@@ -18,6 +18,15 @@ interface Logger {
18
18
  log(message: string): void
19
19
  }
20
20
 
21
+ export class FastbootUsbConnectionError extends Error {
22
+ constructor(
23
+ message: string = "Could not find device in navigator.usb.getDevices()",
24
+ ) {
25
+ super(message)
26
+ this.name = "FastbootUsbConnectionError"
27
+ }
28
+ }
29
+
21
30
  export class FastbootDeviceError extends Error {
22
31
  status: string
23
32
 
@@ -34,14 +43,20 @@ export class FastbootDevice {
34
43
  serialNumber: string;
35
44
  in: USBEndpoint
36
45
  out: USBEndpoint
37
- session: FastbootSession | null
46
+ session: FastbootSession
38
47
  sessions: FastbootSession[]
39
48
  logger: Logger
40
49
 
41
50
  constructor(device: USBDevice, logger: Logger = window.console) {
51
+ if (!device.serialNumber) {
52
+ throw new Error(
53
+ "Access to USBDevice#serialNumber is necessary but stored only in temporary memory.",
54
+ )
55
+ }
56
+
42
57
  this.device = device
43
- this.serialNumber = this.device.serialNumber
44
- this.session = null
58
+ this.serialNumber = device.serialNumber
59
+ this.session = { status: null, packets: [] }
45
60
  this.sessions = []
46
61
  this.logger = logger
47
62
  this.setup()
@@ -51,7 +66,7 @@ export class FastbootDevice {
51
66
  setup() {
52
67
  if (this.device.configurations.length > 1) {
53
68
  console.warn(
54
- `device has ${device.configurations.length} configurations. Using the first one.`,
69
+ `device has ${this.device.configurations.length} configurations. Using the first one.`,
55
70
  )
56
71
  }
57
72
 
@@ -84,49 +99,67 @@ export class FastbootDevice {
84
99
 
85
100
  // some commands (like "flashing lock") will disconnect the device
86
101
  // we have to re-assign this.device after it reconnects
87
- async reconnect() {
102
+ async reconnect(): Promise<boolean> {
88
103
  const devices = await navigator.usb.getDevices()
89
104
  for (const device of devices) {
90
105
  if (device.serialNumber === this.serialNumber) {
106
+ this.logger.log(`reconnect: Found device ${device.serialNumber}`)
91
107
  this.device = device
92
108
  this.setup()
93
109
  await this.connect()
94
110
  return true
95
111
  }
96
112
  }
97
- throw new Error("Could not find device in navigator.usb.getDevices()")
113
+ throw new FastbootUsbConnectionError()
98
114
  }
99
115
 
116
+ // The install requires reboots and it's important we pause the
117
+ // install and reset the usb device after it reconnects
100
118
  async waitForReconnect(): Promise<boolean> {
101
- const devices = await navigator.usb.getDevices()
119
+ try {
120
+ this.logger("waitForReconnect try reconnect()")
121
+ return await this.reconnect()
122
+ } catch (e) {
123
+ this.logger.log("waitForReconnect wait 3 seconds")
124
+ await new Promise((resolve) => setTimeout(resolve, 3000))
125
+ }
102
126
 
103
- if (devices.some(device => device.serialNumber === this.serialNumber)) {
104
- this.logger.log(`waitForReconnect: Found device ${this.serialNumber}`)
105
- await this.reconnect()
106
- return Promise.resolve(true)
107
- } else {
108
- this.logger.log(`waitForReconnect: Adding navigator.usb event listener`)
109
- return new Promise((resolve, reject) => {
110
- navigator.usb.addEventListener(
111
- "connect",
112
- async (event) => {
113
- this.logger.log(
114
- `waitForReconnect: Device connected ${event.device.productName}`,
115
- )
116
- try {
117
- await this.reconnect()
118
- resolve(true)
119
- } catch (e) {
120
- reject(e)
121
- }
122
- },
123
- { once: true },
124
- )
125
- })
127
+ try {
128
+ return await this.reconnect()
129
+ } catch (e) {
130
+ if (e instanceof FastbootUsbConnectionError) {
131
+ this.logger.log("waitForReconnect wait 30 seconds")
132
+ await new Promise((resolve) => setTimeout(resolve, 30000))
133
+ } else {
134
+ throw e
135
+ }
136
+ }
137
+
138
+ // try once more then wait for navigator.usb connect event
139
+ try {
140
+ return await this.reconnect()
141
+ } catch (e) {
142
+ if (e instanceof FastbootUsbConnectionError) {
143
+ return new Promise((resolve, reject) => {
144
+ this.logger.log("adding navigator.usb connect listener")
145
+ navigator.usb.addEventListener(
146
+ "connect",
147
+ async () => {
148
+ try {
149
+ await this.reconnect()
150
+ resolve(true)
151
+ } catch (e) {
152
+ reject(e)
153
+ }
154
+ },
155
+ { once: true },
156
+ )
157
+ })
158
+ }
126
159
  }
127
160
  }
128
161
 
129
- async getPacket(): ResponsePacket {
162
+ async getPacket(): Promise<ResponsePacket> {
130
163
  this.logger.log(`receiving packet from endpoint ${this.in.endpointNumber}`)
131
164
  const inPacket = await this.device.transferIn(this.in.endpointNumber, 256)
132
165
  const inPacketText = new TextDecoder().decode(inPacket.data)
@@ -163,7 +196,7 @@ export class FastbootDevice {
163
196
  } while (["INFO", "TEXT"].includes(response.status))
164
197
  }
165
198
 
166
- async sendCommand(text): ResponsePacket {
199
+ async sendCommand(text: string): Promise<ResponsePacket> {
167
200
  this.session.packets.push({ command: text } as CommandPacket)
168
201
  const outPacket = new TextEncoder().encode(text)
169
202
  this.logger.log(
@@ -173,6 +206,7 @@ export class FastbootDevice {
173
206
  this.out.endpointNumber,
174
207
  outPacket,
175
208
  )
209
+
176
210
  await this.getPackets()
177
211
 
178
212
  if (this.lastPacket.status === "FAIL") {
@@ -186,17 +220,14 @@ export class FastbootDevice {
186
220
  }
187
221
  }
188
222
 
189
- async exec(command): ResponsePacket {
223
+ async exec(command: string): Promise<ResponsePacket> {
190
224
  if (this.isActive) {
191
225
  throw new Error("fastboot device is busy")
192
226
  } else if (!this.device.opened) {
193
227
  await this.connect()
194
228
  }
195
229
 
196
- if (this.session) {
197
- this.sessions.push(this.session)
198
- }
199
-
230
+ this.sessions.push(this.session)
200
231
  this.session = { status: null, packets: [] }
201
232
 
202
233
  return this.sendCommand(command)
@@ -208,18 +239,19 @@ export class FastbootDevice {
208
239
  }
209
240
 
210
241
  get lastPacket() {
211
- if (!this.session || this.session.packets.length === 0) {
242
+ if (this.session.packets.length === 0) {
212
243
  return null
244
+ } else {
245
+ return this.session.packets[this.session.packets.length - 1]
213
246
  }
214
- return this.session.packets[this.session.packets.length - 1]
215
247
  }
216
248
 
217
249
  get isActive() {
218
- if (!this.session || this.session.packets.length === 0) {
250
+ if (this.session.packets.length === 0) {
219
251
  return false
252
+ } else {
253
+ return !["FAIL", "OKAY"].includes(this.lastPacket.status)
220
254
  }
221
-
222
- return !["FAIL", "OKAY"].includes(this.lastPacket.status)
223
255
  }
224
256
 
225
257
  // send buffer to phone
package/src/flasher.ts CHANGED
@@ -58,7 +58,7 @@ function parseInstruction(text: string): Instruction {
58
58
  } else if (word === "--set-active=other") {
59
59
  options.setActive = "other"
60
60
  } else if (word === "--set-active=a" || word === "--set-active=b") {
61
- options.setActive = word.slice(-1)
61
+ options.setActive = word.slice(-1)
62
62
  } else if (word === "--slot-other") {
63
63
  options.slot = "other"
64
64
  } else if (word.slice(0, 6) === "--slot") {
package/src/sparse.ts CHANGED
@@ -65,6 +65,12 @@ export interface SparseChunk {
65
65
  data: Blob | null // to be populated by consumer
66
66
  }
67
67
 
68
+ export interface BlobHeader {
69
+ blobSize: number
70
+ totalBytes: number
71
+ isSparse: boolean
72
+ }
73
+
68
74
  class BlobBuilder {
69
75
  private blob: Blob
70
76
  private type: string
@@ -383,11 +389,7 @@ export async function* splitBlob(blob: Blob, splitSize: number) {
383
389
  }
384
390
  }
385
391
 
386
- export async function parseBlobHeader(blob: Blob): {
387
- blobSize: number
388
- totalBytes: number
389
- isSparse: boolean
390
- } {
392
+ export async function parseBlobHeader(blob: Blob): Promise<BlobHeader> {
391
393
  const FILE_HEADER_SIZE = 28
392
394
  const blobSize = blob.size
393
395
  let totalBytes = blobSize
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "es6",
4
- "lib": ["es6", "dom"],
3
+ "target": "es2016",
4
+ "lib": ["es2016", "dom"],
5
5
  "module": "es6",
6
6
  "sourceMap": true,
7
7
  "allowJs": false,