@aepyornis/fastboot.ts 0.0.1

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.
@@ -0,0 +1 @@
1
+ dist
@@ -0,0 +1,3 @@
1
+ {
2
+ "semi": false
3
+ }
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # fastboot.ts
2
+
3
+ ```sh
4
+ pnpm install
5
+ pnpm run build
6
+ ```
7
+
8
+ src/device.ts handles interfacing with WebUSB and implements fastboot protocol
9
+ src/client.ts implements higher level API, similar to fastboot cli tool
10
+ src/flasher.ts flashes zip image from a list of instructions
11
+ src/sparse.ts sparse image utilities Copyright (c) 2021 Danny Lin <danny@kdrag0n.dev>
12
+
13
+ TODO
14
+
15
+ --apply-vbmeta
16
+ repack_ramdisk ?
17
+
18
+ ```js
19
+ import { FastbootClient, FashbootFlasher } from "@aepyornis/fastboot.ts"
20
+
21
+ const client = await FastbootClient.create()
22
+
23
+ // run commands
24
+ await client.unlock()
25
+ await client.getVar("product")
26
+
27
+ // flash CalyxOS
28
+ import OpfsBlobStore from "@aepyornis/opfs_blob_store"
29
+ const bs = await OpfsBlobStore.create()
30
+ const hash = "a4434edb21e5e12a00ab9949f48f06c48169adcaeb4dce644857e1528b275274"
31
+ const url = "https://release.calyxinstitute.org/lynx-factory-25605200.zip"
32
+ await bs.fetch(hash, url)
33
+ const file = await bs.get(hash)
34
+
35
+ const instructions = `
36
+ fastboot --set-active=other reboot-bootloader
37
+ sleep 5
38
+ fastboot flash --slot=other bootloader bootloader-lynx-lynx-15.2-12878710.img
39
+ fastboot --set-active=other reboot-bootloader
40
+ sleep 5
41
+ fastboot flash --slot=other radio radio-lynx-g5300q-241205-250127-B-12973597.img
42
+ fastboot --set-active=other reboot-bootloader
43
+ sleep 5
44
+ fastboot flash --slot=other radio radio-lynx-g5300q-241205-250127-B-12973597.img
45
+ fastboot --set-active=other reboot-bootloader
46
+ sleep 5
47
+ fastboot erase avb_custom_key
48
+ fastboot flash avb_custom_key avb_custom_key.img
49
+ fastboot --skip-reboot -w update image-lynx-bp1a.250305.019.zip
50
+ fastboot reboot-bootloader
51
+ `
52
+
53
+ const client = await FastbootClient.create()
54
+ const deviceFlasher = new FastbootFlasher()
55
+ await deviceFlasher.run(instructions)
56
+ ```
@@ -0,0 +1,10 @@
1
+ // @ts-check
2
+
3
+ import eslint from "@eslint/js"
4
+ import tseslint from "typescript-eslint"
5
+
6
+ export default tseslint.config(
7
+ { ignores: ["dist/"] },
8
+ eslint.configs.recommended,
9
+ tseslint.configs.recommended,
10
+ )
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@aepyornis/fastboot.ts",
3
+ "version": "0.0.1",
4
+ "description": "fastboot again",
5
+ "main": "src/index.ts",
6
+ "scripts": {
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"
9
+ },
10
+ "keywords": [],
11
+ "author": "Ziggy, Calyx Institute",
12
+ "license": "MIT",
13
+ "packageManager": "pnpm@10.8.0",
14
+ "devDependencies": {
15
+ "@eslint/js": "^9.24.0",
16
+ "@types/w3c-web-usb": "^1.0.10",
17
+ "esbuild": "^0.25.2",
18
+ "eslint": "^9.24.0",
19
+ "globals": "^16.0.0",
20
+ "prettier": "^3.5.3",
21
+ "typescript": "^5.8.3",
22
+ "typescript-eslint": "^8.29.1"
23
+ },
24
+ "dependencies": {
25
+ "@zip.js/zip.js": "^2.7.60"
26
+ }
27
+ }
package/src/client.ts ADDED
@@ -0,0 +1,344 @@
1
+ import { BlobWriter, Entry } from "@zip.js/zip.js"
2
+ import { IMAGES } from "./images"
3
+ import { parseFileHeader, splitBlob, fromRaw } from "./sparse"
4
+ import { FastbootDevice } from "./device"
5
+
6
+ export class FastbootError extends Error {}
7
+
8
+ const FastbootUSBDeviceFilter = {
9
+ classCode: 0xff,
10
+ subclassCode: 0x42,
11
+ protocolCode: 0x03,
12
+ }
13
+
14
+ interface Logger {
15
+ log(message: string): void
16
+ }
17
+
18
+ // higher level API to interact with fastboot device
19
+ // translates CLI commands to
20
+ export class FastbootClient {
21
+ fd: FastbootDevice
22
+ logger: Logger
23
+
24
+ constructor(usb_device: USBDevice, logger: Logger = window.console) {
25
+ this.fd = new FastbootDevice(usb_device, logger)
26
+ this.logger = logger
27
+ }
28
+
29
+ async getVar(variable: string) {
30
+ return this.fd.getVar(variable)
31
+ }
32
+
33
+ async lock() {
34
+ await this.flashing("lock")
35
+ await this.fd.waitForReconnect()
36
+ if (await this.unlocked()) {
37
+ throw new FastbootError("failed to lock device")
38
+ }
39
+ }
40
+
41
+ async unlock() {
42
+ await this.flashing("unlock")
43
+ await this.fd.waitForReconnect()
44
+ if (await this.locked()) {
45
+ throw new FastbootError("failed to unlock device")
46
+ }
47
+ }
48
+
49
+ async reboot() {
50
+ this.logger.log("rebooting")
51
+ await this.fd.exec("reboot")
52
+ }
53
+
54
+ async rebootBootloader() {
55
+ this.logger.log("rebooting into bootloader")
56
+ this.fd.exec("reboot-bootloader")
57
+ await this.fd.waitForReconnect()
58
+ }
59
+
60
+ async rebootFastboot() {
61
+ this.logger.log("rebooting into fastboot")
62
+ this.fd.exec("reboot-fastboot")
63
+ await this.fd.waitForReconnect()
64
+ }
65
+
66
+ async doFlash(
67
+ partition: string,
68
+ blob: Blob,
69
+ slot: "current" | "other" | "a" | "b" = "current",
70
+ applyVbmeta: boolean = false,
71
+ ) {
72
+ // add _a or _b
73
+ // !(await this.isUserspace()) ?
74
+ if (
75
+ partition !== "avb_custom_key" &&
76
+ (await this.getVar(`has-slot:${partition}`)) === "yes"
77
+ ) {
78
+ if (slot === "current") {
79
+ partition += "_" + (await this.currentSlot())
80
+ } else if (slot === "other") {
81
+ partition += "_" + (await this.otherSlot())
82
+ } else if (slot === "a" || slot === "b") {
83
+ partition += "_" + slot
84
+ } else {
85
+ throw new FastbootError(`Unknown Slot: ${slot}`)
86
+ }
87
+ }
88
+
89
+ const { blobSize, totalBytes, isSparse } = await parseBlobHeader(blob)
90
+
91
+ // should_flash_in_userspace() ?
92
+ // Logical partitions need to be resized before flashing because
93
+ // they're sized perfectly to the payload.
94
+ if (
95
+ (await this.isUserspace()) &&
96
+ (await this.getVar(`is-logical:${partition}`)) === "yes"
97
+ ) {
98
+ await this.resizePartition(partition, totalBytes)
99
+ }
100
+
101
+ const max = await this.maxDownloadSize()
102
+
103
+ if (blobSize > max && !isSparse) {
104
+ this.logger.log(`${partition} image is raw, converting to sparse`)
105
+ blob = await fromRaw(blob)
106
+ }
107
+
108
+ this.logger.log(
109
+ `Flashing ${totalBytes} bytes to ${partition} w/ max ${max} bytes per split`,
110
+ )
111
+
112
+ let splits = 0
113
+ let sentBytes = 0
114
+ for await (const split of splitBlob(blob, max)) {
115
+ await this.fd.transferData(split.data)
116
+ this.logger.log(`run command flash:${partition}`)
117
+ await this.fd.sendCommand(`flash:${partition}`)
118
+ sentBytes += split.bytes
119
+ splits += 1
120
+ this.logger.log(
121
+ `${partition} #${splits}) sent ${split.bytes} bytes. ${sentBytes}/${blobSize}`,
122
+ )
123
+ }
124
+ this.logger.log(
125
+ `Flashed ${partition} with ${splits} split(s). Bytes sent: ${sentBytes}`,
126
+ )
127
+ }
128
+
129
+ // fb->ResizePartition ?
130
+ async resizePartition(name: string, totalBytes: number) {
131
+ // As per AOSP fastboot, we reset the partition to 0 bytes first
132
+ // to optimize extent allocation before setting the actual size.
133
+ await this.fd.sendCommand(`resize-logical-partition:${name}:0`)
134
+ await this.fd.sendCommand(`resize-logical-partition:${name}:${totalBytes}`)
135
+ }
136
+
137
+ async flashing(command: "unlock" | "lock") {
138
+ if (
139
+ (command === "unlock" && (await this.unlocked())) ||
140
+ (command === "lock" && (await this.locked()))
141
+ ) {
142
+ return true
143
+ }
144
+
145
+ this.logger.log(`ACTION NEEDED: flashing ${command}`)
146
+ await this.fd.exec(`flashing ${command}`)
147
+ await this.fd.waitForReconnect()
148
+ }
149
+
150
+ // run directions, typically the contents of fastboot-info.txt
151
+ // retriving the flashing data from files in entries
152
+ async fastbootInfo(
153
+ entries: Entry[],
154
+ directions: string,
155
+ wipe: boolean = false,
156
+ ) {
157
+ const lines = directions
158
+ .split("\n")
159
+ .map((x) => x.trim())
160
+ .filter((l) => !(l == "" || l[0] == "#" || l.slice(0, 7) == "version"))
161
+
162
+ for (const line of lines) {
163
+ this.logger.log(`fastboot-info: ${line}`)
164
+ const parts = line.split(" ").map((x) => x.trim())
165
+ const command = parts.shift()
166
+
167
+ switch (command) {
168
+ case "flash": {
169
+ let slot = "current"
170
+ let applyVbmeta = false
171
+ let partition = null
172
+ let filename = null
173
+
174
+ for (const arg of parts) {
175
+ if (arg === "--slot-other") {
176
+ slot = "other"
177
+ } else if (arg === "--apply-vbmeta") {
178
+ applyVbmeta = true
179
+ } else if (partition == null) {
180
+ partition = arg
181
+ } else {
182
+ filename = arg
183
+ }
184
+ }
185
+
186
+ if (filename === null) {
187
+ filename = IMAGES.find(
188
+ (img) => img["nickname"] === partition,
189
+ )?.img_name
190
+ if (!filename) {
191
+ throw new Error(`Unknown partition: ${partition}`)
192
+ }
193
+ }
194
+
195
+ const entry = entries.find((e) => e.filename === filename)
196
+ if (!entry) {
197
+ throw new Error(
198
+ `partition ${partition} with filename ${filename} not found in zipfile.`,
199
+ )
200
+ }
201
+ this.logger.log(`Extracting ${filename}`)
202
+ const blob = await entry.getData(
203
+ new BlobWriter("application/octet-stream"),
204
+ )
205
+ this.logger.log(
206
+ `flashing partition ${partition} with ${filename} from nested zip`,
207
+ )
208
+ await this.doFlash(partition, blob, slot, applyVbmeta)
209
+ break
210
+ }
211
+ case "reboot":
212
+ if (parts[0] === "fastboot") {
213
+ await this.rebootFastboot()
214
+ } else {
215
+ await this.rebootBootloader()
216
+ }
217
+ break
218
+ case "update-super": {
219
+ await this.updateSuper(entries, wipe)
220
+ break
221
+ }
222
+ case "if-wipe":
223
+ if (wipe && parts[0] === "erase" && parts[1]) {
224
+ await this.erase(parts[1])
225
+ }
226
+ break
227
+ case "erase":
228
+ await this.erase(parts[0])
229
+ break
230
+ default:
231
+ throw new Error(`unknown command ${command}`)
232
+ }
233
+ }
234
+ }
235
+
236
+ async updateSuper(entries: Entry[], wipe: boolean) {
237
+ const superEmptyImage = entries.find(
238
+ (e) => e.filename === "super_empty.img",
239
+ )
240
+ if (!superEmptyImage) {
241
+ throw new FastbootError(`super_empty.img not found`)
242
+ }
243
+
244
+ let superName = "super"
245
+ try {
246
+ superName = await this.getVar("super-partition-name")
247
+ } catch (err) {}
248
+
249
+ // fastboot-info does this
250
+ // await this.rebootFastboot()
251
+
252
+ const blob = await superEmptyImage.getData(
253
+ new BlobWriter("application/octet-stream"),
254
+ )
255
+ const buffer = await blob.arrayBuffer()
256
+
257
+ await this.fd.transferData(buffer)
258
+ await this.fd.sendCommand(`update-super:${superName}${wipe ? ":wipe" : ""}`)
259
+ }
260
+
261
+ async erase(partition: string) {
262
+ return this.fd.exec(`erase:${partition}`)
263
+ }
264
+
265
+ async setActiveOtherSlot() {
266
+ const otherSlot = await this.otherSlot()
267
+ return await this.fd.exec(`set_active:${otherSlot}`)
268
+ }
269
+
270
+ async maxDownloadSize() {
271
+ try {
272
+ const deviceMax = parseInt(await this.getVar("max-download-size"), 16)
273
+ const upperLimit = 1024 * 1024 * 1024 * 1 // 1 GiB
274
+ return Math.min(deviceMax, upperLimit)
275
+ } catch (err) {
276
+ console.debug(err)
277
+ }
278
+ // FAIL or empty variable means no max, set a reasonable limit to conserve memory
279
+ return 512 * 1024 * 1024 // 512 MiB
280
+ }
281
+
282
+ async unlocked() {
283
+ return (await this.getVar("unlocked")) === "yes"
284
+ }
285
+
286
+ async locked() {
287
+ return (await this.getVar("unlocked")) === "no"
288
+ }
289
+
290
+ async currentSlot() {
291
+ return this.getVar("current-slot")
292
+ }
293
+
294
+ async otherSlot() {
295
+ const currentSlot = await this.getVar("current-slot")
296
+ if (currentSlot === "a") {
297
+ return "b"
298
+ } else if (currentSlot === "b") {
299
+ return "a"
300
+ } else {
301
+ throw new Error(
302
+ `Unable to determine other slot, current slot: ${currentSlot}`,
303
+ )
304
+ }
305
+ }
306
+
307
+ async isUserspace() {
308
+ return (await this.getVar("is-userspace")) === "yes"
309
+ }
310
+
311
+ static async create() {
312
+ return new FastbootClient(await this.requestUsbDevice(), window.console)
313
+ }
314
+
315
+ static requestUsbDevice(): Promise<USBDevice> {
316
+ return window.navigator.usb.requestDevice({
317
+ filters: [FastbootUSBDeviceFilter],
318
+ })
319
+ }
320
+ }
321
+
322
+ async function parseBlobHeader(blob: Blob): {
323
+ blobSize: number
324
+ totalBytes: number
325
+ isSparse: boolean
326
+ } {
327
+ const FILE_HEADER_SIZE = 28
328
+ const blobSize = blob.size
329
+ let totalBytes = blobSize
330
+ let isSparse = false
331
+
332
+ try {
333
+ const fileHeader = await blob.slice(0, FILE_HEADER_SIZE).arrayBuffer()
334
+ const sparseHeader = parseFileHeader(fileHeader)
335
+ if (sparseHeader !== null) {
336
+ totalBytes = sparseHeader.blocks * sparseHeader.blockSize
337
+ isSparse = true
338
+ }
339
+ } catch (error) {
340
+ console.debug(error)
341
+ // ImageError = invalid, so keep blob.size
342
+ }
343
+ return { blobSize, totalBytes, isSparse }
344
+ }
package/src/device.ts ADDED
@@ -0,0 +1,258 @@
1
+ type CommandPacket = {
2
+ command: string
3
+ }
4
+
5
+ type ResponsePacket = {
6
+ status: "OKAY" | "FAIL" | "DATA" | "INFO" | "TEXT"
7
+ message?: string
8
+ dataLength?: number
9
+ }
10
+
11
+ type FastbootSession = {
12
+ phase: 1 | 2 | 3 | 4 | 5
13
+ status: null | "OKAY" | "FAIL"
14
+ packets: (CommandPacket | ResponsePacket)[]
15
+ }
16
+
17
+ interface Logger {
18
+ log(message: string): void
19
+ }
20
+
21
+ export class FastbootDeviceError extends Error {
22
+ status: string
23
+
24
+ constructor(status: string, message: string) {
25
+ super(`Bootloader replied with ${status}: ${message}`)
26
+ this.status = status
27
+ this.name = "FastbootDeviceError"
28
+ }
29
+ }
30
+
31
+ // implements the fastboot protocol over WebUSB
32
+ export class FastbootDevice {
33
+ device: USBDevice
34
+ serialNumber: string;
35
+ in: USBEndpoint
36
+ out: USBEndpoint
37
+ session: FastbootSession
38
+ sessions: FastbootSession[]
39
+ logger: Logger
40
+
41
+ constructor(device, logger = window.console) {
42
+ this.device = device
43
+ this.serialNumber = this.device.serialNumber
44
+ this.session = null
45
+ this.sessions = []
46
+ this.logger = logger
47
+ this.setup()
48
+ }
49
+
50
+ // validate device and assign endpoints attributes
51
+ setup() {
52
+ if (this.device.configurations.length > 1) {
53
+ console.warn(
54
+ `device has ${device.configurations.length} configurations. Using the first one.`,
55
+ )
56
+ }
57
+
58
+ // this.in = this.device.configurations[0].interfaces[0].alternate.endpoints[0]
59
+ const endpoints =
60
+ this.device.configurations[0].interfaces[0].alternate.endpoints
61
+
62
+ if (endpoints.length !== 2) {
63
+ throw new Error("USB Interface must have only 2 endpoints")
64
+ }
65
+
66
+ for (const endpoint of endpoints) {
67
+ if (endpoint.direction === "in") {
68
+ this.in = endpoint
69
+ } else if (endpoint.direction === "out") {
70
+ this.out = endpoint
71
+ } else {
72
+ throw new Error(`Endpoint error: ${endpoint}`)
73
+ }
74
+ }
75
+ }
76
+
77
+ async connect() {
78
+ if (!this.device.opened) {
79
+ await this.device.open()
80
+ }
81
+ await this.device.selectConfiguration(1)
82
+ await this.device.claimInterface(0)
83
+ }
84
+
85
+ // some commands (like "flashing lock") will disconnect the device
86
+ // we have to re-assign this.device after it reconnects
87
+ async reconnect() {
88
+ const devices = await navigator.usb.getDevices()
89
+ for (const device of devices) {
90
+ if (device.serialNumber === this.serialNumber) {
91
+ this.device = device
92
+ this.setup()
93
+ await this.connect()
94
+ return true
95
+ }
96
+ }
97
+ throw new Error("Could not find device in navigator.usb.getDevices()")
98
+ }
99
+
100
+ waitForReconnect(): Promise<boolean> {
101
+ return new Promise((resolve, reject) => {
102
+ navigator.usb.addEventListener(
103
+ "connect",
104
+ async () => {
105
+ try {
106
+ await this.reconnect()
107
+ resolve(true)
108
+ } catch (e) {
109
+ reject(e)
110
+ }
111
+ },
112
+ { once: true },
113
+ )
114
+ })
115
+ }
116
+
117
+ async getPacket(): ResponsePacket {
118
+ this.logger.log(`receiving packet from endpoint ${this.in.endpointNumber}`)
119
+ const inPacket = await this.device.transferIn(this.in.endpointNumber, 256)
120
+ const inPacketText = new TextDecoder().decode(inPacket.data)
121
+ const status = inPacketText.substring(0, 4)
122
+ const message = inPacketText.slice(4).trim()
123
+ switch (status) {
124
+ case "INFO":
125
+ return { status, message: `(bootloader) ${message}` }
126
+ case "TEXT":
127
+ return { status, message }
128
+ case "FAIL":
129
+ return { status, message }
130
+ case "OKAY":
131
+ return { status, message }
132
+ case "DATA": {
133
+ const dataLength = parseInt(inPacketText.slice(4, 12), 16)
134
+ return {
135
+ status,
136
+ dataLength,
137
+ message: `ready to transfer ${dataLength} bytes`,
138
+ }
139
+ }
140
+ default:
141
+ throw new Error(`invalid packet: ${inPacketText}`)
142
+ }
143
+ }
144
+
145
+ async getPackets() {
146
+ let response
147
+ do {
148
+ response = await this.getPacket()
149
+ this.session.packets.push(response)
150
+ this.logger.log(`[${response.status}] ${response.message}`)
151
+ } while (["INFO", "TEXT"].includes(response.status))
152
+ }
153
+
154
+ async sendCommand(text): ResponsePacket {
155
+ this.session.packets.push({ command: text } as CommandPacket)
156
+ const outPacket = new TextEncoder().encode(text)
157
+ this.logger.log(
158
+ `transfering "${text}" to endpoint ${this.out.endpointNumber}`,
159
+ )
160
+ const result: USBOutTransferResult = await this.device.transferOut(
161
+ this.out.endpointNumber,
162
+ outPacket,
163
+ )
164
+ await this.getPackets()
165
+
166
+ if (this.lastPacket.status === "FAIL") {
167
+ this.session.status = "FAIL"
168
+ throw new FastbootDeviceError(
169
+ this.lastPacket.status,
170
+ this.lastPacket.message,
171
+ )
172
+ } else {
173
+ return this.lastPacket
174
+ }
175
+ }
176
+
177
+ async exec(command): ResponsePacket {
178
+ if (this.isActive) {
179
+ throw new Error("fastboot device is busy")
180
+ } else if (!this.device.opened) {
181
+ await this.connect()
182
+ }
183
+
184
+ if (this.session) {
185
+ this.sessions.push(this.session)
186
+ }
187
+
188
+ this.session = { status: null, packets: [] }
189
+
190
+ return this.sendCommand(command)
191
+ }
192
+
193
+ async getVar(variable: FastbootClientVariables): Promise<string> {
194
+ await this.exec(`getvar:${variable}`)
195
+ return this.lastPacket.message
196
+ }
197
+
198
+ get lastPacket() {
199
+ return this.session.packets[this.session.packets.length - 1]
200
+ }
201
+
202
+ get isActive() {
203
+ if (!this.session || this.session.packets.length === 0) {
204
+ return false
205
+ }
206
+
207
+ return !["FAIL", "OKAY"].includes(this.lastPacket.status)
208
+ }
209
+
210
+ // send buffer to phone
211
+ // download:00001234 -> "DATA" -> transferOut -> "OKAY"
212
+ async transferData(buffer: ArrayBuffer) {
213
+ // Bootloader requires an 8-digit hex number
214
+ const xferHex = buffer.byteLength.toString(16).padStart(8, "0")
215
+ if (xferHex.length !== 8) {
216
+ throw new FastbootDeviceError(
217
+ "FAIL",
218
+ `Transfer size overflow: ${xferHex} is more than 8 digits`,
219
+ )
220
+ }
221
+
222
+ this.logger.log(`Sending command download:${xferHex}.`)
223
+ const response = await this.sendCommand(`download:${xferHex}`)
224
+
225
+ if (response.status !== "DATA") {
226
+ throw new FastbootDeviceError(
227
+ "FAIL",
228
+ `response to download:${xferHex} is ${response.status}. Expected DATA.`,
229
+ )
230
+ } else if (response.dataLength !== buffer.byteLength) {
231
+ throw new FastbootDeviceError(
232
+ "FAIL",
233
+ `Bootloader wants ${response.dataLength} bytes, requested to send ${buffer.byteLength} bytes`,
234
+ )
235
+ }
236
+
237
+ const BULK_TRANSFER_SIZE = 16384
238
+ this.logger.log(`Sending payload: ${buffer.byteLength} bytes`)
239
+ let i = 0
240
+ let remainingBytes = buffer.byteLength
241
+ while (remainingBytes > 0) {
242
+ const chunk = buffer.slice(
243
+ i * BULK_TRANSFER_SIZE,
244
+ (i + 1) * BULK_TRANSFER_SIZE,
245
+ )
246
+ if (i % 1000 === 0) {
247
+ this.logger.log(
248
+ `Sending ${chunk.byteLength} bytes to endpoint, ${remainingBytes} remaining, i=${i}`,
249
+ )
250
+ }
251
+ await this.device.transferOut(this.out.endpointNumber, chunk)
252
+ remainingBytes -= chunk.byteLength
253
+ i += 1
254
+ }
255
+ this.logger.log("Payload sent, waiting for response...")
256
+ await this.getPackets()
257
+ }
258
+ }