@onekeyfe/hd-transport-web-device 1.0.34-alpha.0

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,709 @@
1
+ import transport, { COMMON_HEADER_SIZE, LogBlockCommand } from '@onekeyfe/hd-transport'; // COMMON_HEADER_SIZE, // LogBlockCommand, // AcquireInput,
2
+ import {
3
+ ERRORS,
4
+ HardwareErrorCode,
5
+ Deferred,
6
+ createDeferred,
7
+ ONEKEY_SERVICE_UUID,
8
+ ONEKEY_WRITE_CHARACTERISTIC_UUID,
9
+ ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
10
+ isHeaderChunk,
11
+ } from '@onekeyfe/hd-shared';
12
+ import ByteBuffer from 'bytebuffer';
13
+ import type EventEmitter from 'events';
14
+
15
+ const { parseConfigure, buildBuffers, receiveOne, check } = transport;
16
+
17
+ // Add type declaration for desktopApi
18
+ declare global {
19
+ interface Window {
20
+ desktopApi?: {
21
+ onBleSelect: (callback: (devices: Array<{ id: string; name: string }>) => void) => () => void;
22
+ stopBleScan: () => void;
23
+ selectBleDevice: (deviceId: string) => void;
24
+ preSelectDevice?: (uuid: string) => void;
25
+ clearPreSelect?: () => void;
26
+ };
27
+ }
28
+ }
29
+
30
+ interface BleTransport {
31
+ device: BluetoothDevice;
32
+ server: BluetoothRemoteGATTServer;
33
+ service: BluetoothRemoteGATTService;
34
+ writeCharacteristic: BluetoothRemoteGATTCharacteristic;
35
+ notifyCharacteristic: BluetoothRemoteGATTCharacteristic;
36
+ notifySubscription: null | ((event: Event) => void);
37
+ }
38
+
39
+ export type BleAcquireInput = {
40
+ uuid: string;
41
+ forceCleanRunPromise?: boolean;
42
+ };
43
+
44
+ export default class ElectronBleTransport {
45
+ _messages: ReturnType<typeof transport.parseConfigure> | undefined;
46
+
47
+ name = 'ElectronBleTransport';
48
+
49
+ configured = false;
50
+
51
+ runPromise: Deferred<any> | null = null;
52
+
53
+ Log?: any;
54
+
55
+ emitter?: EventEmitter;
56
+
57
+ // Cache discovered devices and transport objects
58
+ private deviceList: Array<{ id: string; name: string; device: BluetoothDevice }> = [];
59
+
60
+ private transportCache: Record<string, BleTransport> = {};
61
+
62
+ // Track last logged device list to avoid duplicate logs
63
+ private lastLoggedDevices = '';
64
+
65
+ init(logger: any, emitter?: EventEmitter) {
66
+ this.Log = logger;
67
+ this.emitter = emitter;
68
+
69
+ if (!navigator.bluetooth) {
70
+ throw ERRORS.TypedError(
71
+ HardwareErrorCode.RuntimeError,
72
+ 'Web Bluetooth is not supported by current browser'
73
+ );
74
+ }
75
+ }
76
+
77
+ configure(signedData: any) {
78
+ const messages = parseConfigure(signedData);
79
+ this.configured = true;
80
+ this._messages = messages;
81
+ }
82
+
83
+ listen() {}
84
+
85
+ // Helper function to handle onBleSelect logging with deduplication
86
+ private logDeviceListIfChanged(
87
+ devices: Array<{ id: string; name: string }>,
88
+ scenario: 'enumerate' | 'acquire',
89
+ additionalInfo?: string
90
+ ): boolean {
91
+ // Sort devices by id to ensure consistent comparison regardless of order
92
+ const sortedDevices = [...devices].sort((a, b) => a.id.localeCompare(b.id));
93
+ const deviceListString = JSON.stringify(sortedDevices.map(d => ({ id: d.id, name: d.name })));
94
+
95
+ // Only log if the device list has changed
96
+ if (deviceListString !== this.lastLoggedDevices) {
97
+ const prefix =
98
+ scenario === 'enumerate'
99
+ ? '[Transport] Received new devices'
100
+ : '[Transport] Received devices in acquire';
101
+
102
+ const message = additionalInfo ? `${prefix} (${additionalInfo}):` : `${prefix}:`;
103
+
104
+ this.Log?.debug(message, devices);
105
+
106
+ // Update the last logged devices string
107
+ this.lastLoggedDevices = deviceListString;
108
+
109
+ return true; // Logged
110
+ }
111
+
112
+ return false; // Not logged (duplicate)
113
+ }
114
+
115
+ async enumerate(): Promise<{ id: string; name: string }[]> {
116
+ this.Log?.debug('[Transport] Starting enumerate');
117
+
118
+ try {
119
+ // Check window.desktopApi
120
+ this.Log?.debug('[Transport] Checking desktopApi:', window.desktopApi);
121
+ if (!window.desktopApi?.onBleSelect) {
122
+ console.error('[Transport] desktopApi.onBleSelect not available');
123
+ throw new Error('desktopApi.onBleSelect not available');
124
+ }
125
+
126
+ // Clear previous pre-selection state
127
+ window.desktopApi?.clearPreSelect?.();
128
+
129
+ // Use Set for deduplication, store discovered devices
130
+ const deviceSet = new Set<string>();
131
+ const devices: Array<{ id: string; name: string }> = [];
132
+
133
+ // Reset last logged devices for this operation
134
+ this.lastLoggedDevices = '';
135
+
136
+ return await new Promise(resolve => {
137
+ this.Log?.debug('[Transport] Setting up device scanning with 3s timeout');
138
+
139
+ // Cleanup function
140
+ const cleanupAll = () => {
141
+ this.Log?.debug('[Transport] Cleaning up resources');
142
+ clearTimeout(timeoutId);
143
+ cleanup?.();
144
+ // Stop BLE scanning through desktopApi
145
+ window.desktopApi?.stopBleScan();
146
+ };
147
+
148
+ // Set 3 second timeout
149
+ const timeoutId = setTimeout(() => {
150
+ this.Log?.debug('[Transport] Scan timeout, returning devices:', devices);
151
+ cleanupAll();
152
+ resolve(devices);
153
+ }, 3000);
154
+
155
+ // Listen for device discovery events
156
+ const cleanup = window.desktopApi?.onBleSelect(newDevices => {
157
+ // Use helper function to log only if device list changed
158
+ this.logDeviceListIfChanged(newDevices, 'enumerate');
159
+
160
+ // Add new devices to list (with deduplication)
161
+ newDevices.forEach(device => {
162
+ const deviceKey = `${device.id}-${device.name}`;
163
+ if (!deviceSet.has(deviceKey)) {
164
+ deviceSet.add(deviceKey);
165
+ devices.push(device);
166
+ this.Log?.debug('[Transport] Added new device:', device);
167
+ }
168
+ });
169
+ });
170
+
171
+ // Trigger device search
172
+ navigator.bluetooth
173
+ .requestDevice({
174
+ filters: [{ services: [ONEKEY_SERVICE_UUID] }],
175
+ optionalServices: [ONEKEY_SERVICE_UUID],
176
+ })
177
+ .catch(error => {
178
+ // Ignore user cancellation errors
179
+ this.Log?.debug('[Transport] RequestDevice error (expected):', error);
180
+ });
181
+ });
182
+ } catch (error) {
183
+ // Make sure to stop scanning even if there's an error
184
+ window.desktopApi?.stopBleScan();
185
+ console.error('[Transport] Error in enumerate:', error);
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ // Add device disconnect listener
191
+ private addDisconnectListener(device: BluetoothDevice) {
192
+ this.Log?.debug('[Transport] Adding disconnect listener for device:', device.id);
193
+ device.addEventListener('gattserverdisconnected', () => {
194
+ this.Log?.debug('[Transport] Device disconnected:', device.id);
195
+ // Clean up cache
196
+ delete this.transportCache[device.id];
197
+ // Trigger disconnect event
198
+ this.emitter?.emit('device-disconnect', {
199
+ name: device.name,
200
+ id: device.id,
201
+ connectId: device.id,
202
+ });
203
+ });
204
+ }
205
+
206
+ // Monitor characteristic value changes
207
+ private _monitorCharacteristic(
208
+ characteristic: BluetoothRemoteGATTCharacteristic,
209
+ deviceId: string
210
+ ) {
211
+ let bufferLength = 0;
212
+ let buffer: number[] = [];
213
+
214
+ const subscription = (event: Event) => {
215
+ const { value } = event.target as BluetoothRemoteGATTCharacteristic;
216
+ if (!value) return;
217
+
218
+ this.Log?.debug('[Transport] Received notification from device:', deviceId, value);
219
+
220
+ try {
221
+ // Convert DataView to Buffer-like Uint8Array
222
+ const data = new Uint8Array(value.buffer);
223
+ this.Log?.debug('[Transport] Received a packet, buffer:', data);
224
+
225
+ if (isHeaderChunk(data)) {
226
+ // Read buffer length from header (big-endian 32-bit integer at offset 5)
227
+ const dataView = new DataView(value.buffer);
228
+ bufferLength = dataView.getInt32(5, false); // false = big-endian
229
+ buffer = [...data.subarray(3)];
230
+ } else {
231
+ buffer = buffer.concat([...data]);
232
+ }
233
+
234
+ if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
235
+ // 6 is COMMON_HEADER_SIZE
236
+ const completeBuffer = new Uint8Array(buffer);
237
+ this.Log?.debug('[Transport] Received complete packet, resolving Promise');
238
+ bufferLength = 0;
239
+ buffer = [];
240
+
241
+ // Convert to hex string for processing
242
+ const hexString = Array.from(completeBuffer)
243
+ .map(b => b.toString(16).padStart(2, '0'))
244
+ .join('');
245
+
246
+ if (this.runPromise) {
247
+ this.runPromise.resolve(hexString);
248
+ }
249
+ }
250
+ } catch (error) {
251
+ console.error('[Transport] Monitor data error:', error);
252
+ if (this.runPromise) {
253
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
254
+ }
255
+ }
256
+ };
257
+
258
+ characteristic.addEventListener('characteristicvaluechanged', subscription);
259
+ return subscription;
260
+ }
261
+
262
+ // Find cached device
263
+ private findDevice(id: string) {
264
+ return this.deviceList.find(d => d.id === id);
265
+ }
266
+
267
+ async acquire(input: BleAcquireInput) {
268
+ const { uuid, forceCleanRunPromise } = input;
269
+
270
+ if (!uuid) {
271
+ throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
272
+ }
273
+
274
+ this.Log?.debug('[Transport] Acquiring device:', uuid);
275
+
276
+ // Reset last logged devices for this operation
277
+ this.lastLoggedDevices = '';
278
+
279
+ // Check existing connection status and clean up invalid connections
280
+ const existingTransport = this.transportCache[uuid];
281
+ if (existingTransport) {
282
+ const { server, device } = existingTransport;
283
+ this.Log?.debug('[Transport] Found existing transport, checking connection status');
284
+ this.Log?.debug('[Transport] Device GATT connected:', device.gatt?.connected);
285
+ this.Log?.debug('[Transport] Server connected:', server?.connected);
286
+
287
+ // If connection is disconnected, clean up cache
288
+ if (!device.gatt?.connected || !server?.connected) {
289
+ this.Log?.debug('[Transport] Connection is stale, cleaning up...');
290
+ await this.release(uuid);
291
+ } else {
292
+ this.Log?.debug('[Transport] Connection is still active, reusing existing transport');
293
+ return { uuid, path: uuid };
294
+ }
295
+ }
296
+
297
+ // Force clean running Promise
298
+ if (forceCleanRunPromise && this.runPromise) {
299
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
300
+ this.Log?.debug('[Transport] Force clean Bluetooth run promise:', forceCleanRunPromise);
301
+ }
302
+
303
+ try {
304
+ // 1. Find device or trigger device selection process
305
+ let deviceInfo = this.findDevice(uuid);
306
+
307
+ if (!deviceInfo) {
308
+ this.Log?.debug('[Transport] Device not found in cache, requesting user to select device');
309
+
310
+ // Check window.desktopApi
311
+ if (!window.desktopApi?.onBleSelect) {
312
+ throw new Error('desktopApi.onBleSelect not available');
313
+ }
314
+
315
+ deviceInfo = await new Promise((resolve, reject) => {
316
+ let resolved = false;
317
+ let targetDeviceName: string | null = null;
318
+
319
+ // Pre-select device first
320
+ window.desktopApi?.preSelectDevice?.(uuid);
321
+
322
+ // Cleanup functions
323
+ const cleanupAndResolve = (deviceInfo: any) => {
324
+ if (resolved) return;
325
+ resolved = true;
326
+ cleanup?.();
327
+ clearTimeout(timeoutId);
328
+ window.desktopApi?.clearPreSelect?.();
329
+ resolve(deviceInfo);
330
+ };
331
+
332
+ const cleanupAndReject = (error: any) => {
333
+ if (resolved) return;
334
+ resolved = true;
335
+ cleanup?.();
336
+ clearTimeout(timeoutId);
337
+ window.desktopApi?.stopBleScan();
338
+ window.desktopApi?.clearPreSelect?.();
339
+ reject(error);
340
+ };
341
+
342
+ // Listen for device selection events - get target device name for matching
343
+ const cleanup = window.desktopApi?.onBleSelect(devices => {
344
+ // Use helper function to log only if device list changed
345
+ this.logDeviceListIfChanged(devices, 'acquire', `target: ${uuid}`);
346
+
347
+ // Find the target device by UUID to get its name
348
+ const targetDevice = devices.find(device => device.id === uuid);
349
+ if (targetDevice) {
350
+ targetDeviceName = targetDevice.name;
351
+ this.Log?.debug('[Transport] Target device name for matching:', targetDeviceName);
352
+ }
353
+ });
354
+
355
+ // Wait for browser's requestDevice to be triggered (automatically selected by main process)
356
+ navigator.bluetooth
357
+ .requestDevice({
358
+ filters: [{ services: [ONEKEY_SERVICE_UUID] }],
359
+ optionalServices: [ONEKEY_SERVICE_UUID],
360
+ })
361
+ .then(selectedDevice => {
362
+ this.Log?.debug(
363
+ '[Transport] Device selected from browser:',
364
+ selectedDevice.id,
365
+ selectedDevice.name,
366
+ 'Target UUID:',
367
+ uuid,
368
+ 'Target name:',
369
+ targetDeviceName
370
+ );
371
+
372
+ // Verify the selected device matches by name
373
+ if (targetDeviceName && selectedDevice.name !== targetDeviceName) {
374
+ this.Log?.debug(
375
+ '[Transport] Selected device name does not match target:',
376
+ selectedDevice.name,
377
+ 'vs',
378
+ targetDeviceName
379
+ );
380
+ cleanupAndReject(
381
+ new Error(
382
+ `Selected device name "${selectedDevice.name}" does not match target name "${targetDeviceName}"`
383
+ )
384
+ );
385
+ return;
386
+ }
387
+
388
+ // Device name matches or no target name available, proceed
389
+ cleanupAndResolve({
390
+ id: selectedDevice.id,
391
+ name: selectedDevice.name || 'Unknown Device',
392
+ device: selectedDevice,
393
+ originalUuid: uuid, // Keep track of the original UUID for reference
394
+ });
395
+ })
396
+ .catch(error => {
397
+ console.error('[Transport] RequestDevice error:', error);
398
+ // cleanupAndReject(error);
399
+ });
400
+
401
+ // Set timeout
402
+ const timeoutId = setTimeout(() => {
403
+ this.Log?.debug('[Transport] Acquire timeout - v2 - waiting for device selection');
404
+ cleanupAndReject(new Error('Acquire device timeout'));
405
+ }, 5000);
406
+ });
407
+
408
+ if (!deviceInfo) {
409
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
410
+ }
411
+
412
+ // Clear pre-selection state
413
+ window.desktopApi?.clearPreSelect?.();
414
+ }
415
+
416
+ const { device } = deviceInfo;
417
+
418
+ // 2. Add disconnect listener for device
419
+ this.addDisconnectListener(device);
420
+
421
+ // 3. Connect to device
422
+ let server;
423
+ try {
424
+ this.Log?.debug('[Transport] Start connecting to device:', device.id);
425
+ server = await device.gatt?.connect();
426
+ this.Log?.debug('[Transport] Device gatt available:', !!device.gatt);
427
+ this.Log?.debug('[Transport] Device gatt connected:', device.gatt?.connected);
428
+ this.Log?.debug('[Transport] Connected to device:', server);
429
+ } catch (e: any) {
430
+ this.Log?.debug('[Transport] Connect to device error:', e);
431
+ throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, e.message || e);
432
+ }
433
+
434
+ if (!server) {
435
+ throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Unable to connect to device');
436
+ }
437
+
438
+ // 4. Get service
439
+ let service;
440
+ try {
441
+ this.Log?.debug('[Transport] Start getting service:', ONEKEY_SERVICE_UUID);
442
+ service = await server.getPrimaryService(ONEKEY_SERVICE_UUID);
443
+ this.Log?.debug('[Transport] Got service:', service);
444
+ } catch (e: any) {
445
+ this.Log?.debug('[Transport] Get service error:', e);
446
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
447
+ }
448
+
449
+ // 5. Get characteristics
450
+ let writeCharacteristic;
451
+ let notifyCharacteristic;
452
+ try {
453
+ this.Log?.debug(
454
+ '[Transport] Start getting write characteristic:',
455
+ ONEKEY_WRITE_CHARACTERISTIC_UUID
456
+ );
457
+ writeCharacteristic = await service.getCharacteristic(ONEKEY_WRITE_CHARACTERISTIC_UUID);
458
+ this.Log?.debug('[Transport] Got write characteristic:', writeCharacteristic);
459
+ this.Log?.debug(
460
+ '[Transport] Start getting notify characteristic:',
461
+ ONEKEY_NOTIFY_CHARACTERISTIC_UUID
462
+ );
463
+ notifyCharacteristic = await service.getCharacteristic(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
464
+ this.Log?.debug('[Transport] Got notify characteristic:', notifyCharacteristic);
465
+ } catch (e: any) {
466
+ this.Log?.debug('[Transport] Get characteristic error:', e);
467
+ throw ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound);
468
+ }
469
+
470
+ // 6. Check if characteristics support write and notify
471
+ if (
472
+ !writeCharacteristic.properties.write &&
473
+ !writeCharacteristic.properties.writeWithoutResponse
474
+ ) {
475
+ throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
476
+ }
477
+
478
+ if (!notifyCharacteristic.properties.notify) {
479
+ throw ERRORS.TypedError(
480
+ 'BLECharacteristicNotNotifiable: notify characteristic not notifiable'
481
+ );
482
+ }
483
+
484
+ // 7. Create transport object
485
+ const transport: BleTransport = {
486
+ device,
487
+ server,
488
+ service,
489
+ writeCharacteristic,
490
+ notifyCharacteristic,
491
+ notifySubscription: null,
492
+ };
493
+
494
+ this.Log?.debug('[Transport] Created transport:', transport);
495
+
496
+ // 8. Start notifications
497
+ try {
498
+ this.Log?.debug('[Transport] Start notifications:', notifyCharacteristic);
499
+ await notifyCharacteristic.startNotifications();
500
+ this.Log?.debug('[Transport] Started notifications:', notifyCharacteristic);
501
+ transport.notifySubscription = this._monitorCharacteristic(notifyCharacteristic, uuid);
502
+ } catch (e: any) {
503
+ this.Log?.debug('[Transport] Start notifications error:', e);
504
+ throw ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyError);
505
+ }
506
+
507
+ // 9. Cache transport object and device info
508
+ this.transportCache[uuid] = transport;
509
+ if (!this.findDevice(uuid)) {
510
+ this.deviceList.push(deviceInfo);
511
+ }
512
+
513
+ // 10. Trigger device connect event
514
+ this.emitter?.emit('device-connect', {
515
+ name: device.name,
516
+ id: device.id,
517
+ connectId: device.id,
518
+ });
519
+
520
+ return { uuid, path: uuid };
521
+ } catch (error) {
522
+ console.error('[Transport] Error acquiring device:', error);
523
+ // Make sure to stop scanning
524
+ window.desktopApi?.stopBleScan();
525
+ throw error;
526
+ }
527
+ }
528
+
529
+ async release(id: string) {
530
+ const transport = this.transportCache[id];
531
+ if (!transport) return;
532
+
533
+ const { notifyCharacteristic, notifySubscription } = transport;
534
+
535
+ try {
536
+ // Stop notification subscription
537
+ if (notifyCharacteristic && notifySubscription) {
538
+ this.Log?.debug('[Transport] Removing notification listener for device:', id);
539
+ notifyCharacteristic.removeEventListener('characteristicvaluechanged', notifySubscription);
540
+ try {
541
+ await notifyCharacteristic.stopNotifications();
542
+ } catch (e) {
543
+ this.Log?.debug('[Transport] Stop notifications error (ignored):', e);
544
+ }
545
+ }
546
+
547
+ // Clean up cache - don't actively disconnect, let system manage connection state
548
+ delete this.transportCache[id];
549
+
550
+ this.Log?.debug('[Transport] Device released (connection kept alive):', id);
551
+ } catch (error) {
552
+ console.error('[Transport] Error releasing device:', error);
553
+ // Clean up cache even if error occurs
554
+ delete this.transportCache[id];
555
+ }
556
+ }
557
+
558
+ async call(uuid: string, name: string, data: Record<string, unknown>) {
559
+ if (this._messages == null) {
560
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
561
+ }
562
+
563
+ const forceRun = name === 'Initialize' || name === 'Cancel';
564
+
565
+ this.Log?.debug('electron-ble-transport call this.runPromise', this.runPromise);
566
+ if (this.runPromise && !forceRun) {
567
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
568
+ }
569
+
570
+ const transport = this.transportCache[uuid];
571
+ if (!transport) {
572
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
573
+ }
574
+
575
+ // Check connection status
576
+ const { device, server } = transport;
577
+ if (!device.gatt?.connected || !server?.connected) {
578
+ this.Log?.debug('[Transport] Connection lost during call, device needs to be re-acquired');
579
+ throw ERRORS.TypedError(
580
+ HardwareErrorCode.BleDeviceNotBonded,
581
+ 'Device connection lost, please re-acquire device'
582
+ );
583
+ }
584
+
585
+ this.runPromise = createDeferred();
586
+ const messages = this._messages;
587
+
588
+ // Log different types of commands appropriately
589
+ if (name === 'ResourceUpdate' || name === 'ResourceAck') {
590
+ this.Log?.debug('electron-ble-transport', 'call-', ' name: ', name, ' data: ', {
591
+ file_name: data?.file_name,
592
+ hash: data?.hash,
593
+ });
594
+ } else if (LogBlockCommand.has(name)) {
595
+ this.Log?.debug('electron-ble-transport', 'call-', ' name: ', name);
596
+ } else {
597
+ this.Log?.debug('electron-ble-transport', 'call-', ' name: ', name, ' data: ', data);
598
+ }
599
+
600
+ const buffers = buildBuffers(messages, name, data) as Array<ByteBuffer>;
601
+
602
+ // Helper function to write chunked data
603
+ async function writeChunkedData(
604
+ buffers: ByteBuffer[],
605
+ writeFunction: (data: ArrayBuffer) => Promise<void>,
606
+ onError: (e: any) => void
607
+ ) {
608
+ // Web Bluetooth typically supports larger packets than mobile
609
+ const packetCapacity = 512; // Adjust based on your device's MTU
610
+ let index = 0;
611
+ let chunk = ByteBuffer.allocate(packetCapacity);
612
+
613
+ while (index < buffers.length) {
614
+ const buffer = buffers[index].toBuffer();
615
+ chunk.append(buffer);
616
+ index += 1;
617
+
618
+ if (chunk.offset === packetCapacity || index >= buffers.length) {
619
+ chunk.reset();
620
+ try {
621
+ // Convert ByteBuffer to ArrayBuffer for Web Bluetooth
622
+ const arrayBuffer = chunk.toArrayBuffer();
623
+ await writeFunction(arrayBuffer);
624
+ chunk = ByteBuffer.allocate(packetCapacity);
625
+ } catch (e) {
626
+ onError(e);
627
+ throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ try {
634
+ if (name === 'EmmcFileWrite') {
635
+ // For file write operations, use chunked writing with retry
636
+ await writeChunkedData(
637
+ buffers,
638
+ async (data: ArrayBuffer) => {
639
+ // Implement retry logic for file writes
640
+ let retries = 3;
641
+ while (retries > 0) {
642
+ try {
643
+ await transport.writeCharacteristic.writeValueWithoutResponse(data);
644
+ break;
645
+ } catch (e) {
646
+ retries--;
647
+ if (retries === 0) throw e;
648
+ // eslint-disable-next-line no-promise-executor-return
649
+ await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms before retry
650
+ }
651
+ }
652
+ },
653
+ e => {
654
+ this.runPromise = null;
655
+ this.Log?.debug('writeCharacteristic write error: ', e);
656
+ }
657
+ );
658
+ } else if (name === 'FirmwareUpload') {
659
+ // For firmware upload, use writeWithoutResponse for better performance
660
+ await writeChunkedData(
661
+ buffers,
662
+ async (data: ArrayBuffer) => {
663
+ await transport.writeCharacteristic.writeValueWithoutResponse(data);
664
+ },
665
+ e => {
666
+ this.runPromise = null;
667
+ this.Log?.debug('writeCharacteristic write error: ', e);
668
+ }
669
+ );
670
+ } else {
671
+ // For regular commands, write each buffer directly
672
+ for (const buffer of buffers) {
673
+ const arrayBuffer = buffer.toArrayBuffer();
674
+ try {
675
+ await transport.writeCharacteristic.writeValueWithoutResponse(arrayBuffer);
676
+ } catch (e: any) {
677
+ this.Log?.debug('writeCharacteristic write error: ', e);
678
+ this.runPromise = null;
679
+
680
+ // Map Web Bluetooth errors to our error codes
681
+ if (e.name === 'NetworkError' || e.message?.includes('disconnected')) {
682
+ throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
683
+ } else if (e.name === 'NotSupportedError') {
684
+ throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError, e.message);
685
+ } else {
686
+ throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ // Wait for response
693
+ const response = await this.runPromise.promise;
694
+
695
+ if (typeof response !== 'string') {
696
+ throw new Error('Returning data is not string.');
697
+ }
698
+
699
+ this.Log?.debug('receive data: ', response);
700
+ const jsonData = receiveOne(messages, response);
701
+ return check.call(jsonData);
702
+ } catch (e) {
703
+ this.Log?.debug('call error: ', e);
704
+ throw e;
705
+ } finally {
706
+ this.runPromise = null;
707
+ }
708
+ }
709
+ }