@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.
- package/README.md +29 -0
- package/dist/electron-ble-transport.d.ts +51 -0
- package/dist/electron-ble-transport.d.ts.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +766 -0
- package/dist/webble-utils.d.ts +4 -0
- package/dist/webble-utils.d.ts.map +1 -0
- package/dist/webusb.d.ts +33 -0
- package/dist/webusb.d.ts.map +1 -0
- package/package.json +31 -0
- package/src/electron-ble-transport.ts +709 -0
- package/src/index.ts +4 -0
- package/src/webble-utils.ts +30 -0
- package/src/webusb.ts +298 -0
- package/tsconfig.json +11 -0
|
@@ -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
|
+
}
|