@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 +1 -1
- package/package.json +4 -4
- package/src/client.ts +9 -7
- package/src/device.ts +75 -43
- package/src/flasher.ts +1 -1
- package/src/sparse.ts +7 -5
- package/tsconfig.json +2 -2
package/mise.toml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
[tools]
|
|
2
|
-
node = "
|
|
2
|
+
node = "24"
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aepyornis/fastboot.ts",
|
|
3
|
-
"version": "0.0.
|
|
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=
|
|
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.
|
|
16
|
+
"esbuild": "^0.27.2",
|
|
17
17
|
"eslint": "^9.24.0",
|
|
18
|
-
"globals": "^
|
|
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:
|
|
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<
|
|
367
|
-
for (const device of
|
|
369
|
+
static async findOrRequestDevice(serialNumber: string): Promise<USBDevice> {
|
|
370
|
+
for (const device of await navigator.usb.getDevices()) {
|
|
368
371
|
if (device.serialNumber === serialNumber) {
|
|
369
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|