@onekeyfe/hd-transport-web-device 1.2.0-alpha.7 → 1.2.0-alpha.71
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/__tests__/electron-ble-transport.test.ts +450 -12
- package/__tests__/webusb-protocol-v2-timeout.test.ts +385 -0
- package/dist/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/electron-ble-transport.d.ts +14 -5
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +107 -14
- package/dist/index.js +387 -325
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/webusb.d.ts +15 -10
- package/dist/webusb.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/ble-packet-capacity.ts +13 -0
- package/src/electron-ble-transport.ts +279 -175
- package/src/transportLog.ts +1 -0
- package/src/webusb.ts +184 -217
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import transport, {
|
|
2
|
-
LogBlockCommand,
|
|
3
2
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
3
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
4
4
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
5
5
|
ProtocolV2FrameAssembler,
|
|
6
|
-
|
|
6
|
+
ProtocolV2LinkManager,
|
|
7
|
+
TRANSPORT_EVENT,
|
|
7
8
|
bytesToHex,
|
|
8
9
|
hexToBytes,
|
|
9
10
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
|
+
writeProtocolV2BleFrame,
|
|
10
12
|
} from '@onekeyfe/hd-transport';
|
|
11
13
|
import {
|
|
12
14
|
ERRORS,
|
|
@@ -14,25 +16,21 @@ import {
|
|
|
14
16
|
HardwareErrorCodeMessage,
|
|
15
17
|
createDeferred,
|
|
16
18
|
isHeaderChunk,
|
|
17
|
-
wait,
|
|
18
19
|
} from '@onekeyfe/hd-shared';
|
|
19
20
|
|
|
21
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
22
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
23
|
+
|
|
20
24
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
21
25
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
22
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
OneKeyDeviceInfo,
|
|
28
|
+
ProtocolType,
|
|
29
|
+
ProtocolV2CallContext,
|
|
30
|
+
TransportCallOptions,
|
|
31
|
+
} from '@onekeyfe/hd-transport';
|
|
23
32
|
import type EventEmitter from 'events';
|
|
24
33
|
|
|
25
|
-
const FILE_WRITE_LOG_BLOCK_PATTERN = /(?:^|[^a-z])(?:raw)?(?:filesystem|emmc)?filewrite$/i;
|
|
26
|
-
|
|
27
|
-
function shouldSuppressHighVolumeCallLog(name: string) {
|
|
28
|
-
const normalized = name.replace(/[_\s-]/g, '');
|
|
29
|
-
return FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isLogBlockCommand(name: string) {
|
|
33
|
-
return (LogBlockCommand as Set<string> | undefined)?.has?.(name) ?? false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
34
|
const { parseConfigure, ProtocolV1, check } = transport;
|
|
37
35
|
|
|
38
36
|
declare global {
|
|
@@ -45,6 +43,7 @@ export type BleAcquireInput = {
|
|
|
45
43
|
uuid: string;
|
|
46
44
|
forceCleanRunPromise?: boolean;
|
|
47
45
|
expectedProtocol?: ProtocolType;
|
|
46
|
+
protocolHint?: ProtocolType;
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
interface PacketProcessResult {
|
|
@@ -70,9 +69,9 @@ const toBleDescriptor = (
|
|
|
70
69
|
...(protocolType ? { protocolType } : {}),
|
|
71
70
|
} as OneKeyDeviceInfo);
|
|
72
71
|
|
|
73
|
-
const
|
|
72
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
73
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
74
74
|
const BLE_WRITE_DELAY_MS = 5;
|
|
75
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
76
75
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
77
76
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
78
77
|
|
|
@@ -80,19 +79,23 @@ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
|
80
79
|
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
81
80
|
*
|
|
82
81
|
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
83
|
-
* after a Protocol V1
|
|
82
|
+
* after a Protocol V1 GetFeatures timeout by probing Protocol V2 Ping.
|
|
84
83
|
*/
|
|
85
84
|
export default class ElectronBleTransport {
|
|
86
85
|
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
87
86
|
|
|
88
87
|
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
89
88
|
|
|
89
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
90
|
+
|
|
90
91
|
name = 'ElectronBleTransport';
|
|
91
92
|
|
|
92
93
|
configured = false;
|
|
93
94
|
|
|
94
95
|
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
95
96
|
|
|
97
|
+
private runPromiseDeviceId: string | null = null;
|
|
98
|
+
|
|
96
99
|
Log?: any;
|
|
97
100
|
|
|
98
101
|
emitter?: EventEmitter;
|
|
@@ -103,6 +106,10 @@ export default class ElectronBleTransport {
|
|
|
103
106
|
|
|
104
107
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
105
108
|
|
|
109
|
+
private deviceMtus: Map<string, number> = new Map();
|
|
110
|
+
|
|
111
|
+
private devicePacketCapacities: Map<string, number> = new Map();
|
|
112
|
+
|
|
106
113
|
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
107
114
|
|
|
108
115
|
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -111,12 +118,31 @@ export default class ElectronBleTransport {
|
|
|
111
118
|
|
|
112
119
|
private v2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
113
120
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
122
|
+
getSchemas: () => {
|
|
123
|
+
if (!this._messages || !this._messagesV2) {
|
|
124
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
protocolV1: this._messages,
|
|
128
|
+
protocolV2: this._messagesV2,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
classifyError: () => 'link-fatal',
|
|
132
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
133
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
134
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
135
|
+
this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
|
|
136
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
137
|
+
await this.releaseNative(uuid);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
117
141
|
|
|
118
142
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
119
143
|
|
|
144
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
145
|
+
|
|
120
146
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
121
147
|
|
|
122
148
|
private notificationTokens: Map<string, number> = new Map();
|
|
@@ -155,17 +181,24 @@ export default class ElectronBleTransport {
|
|
|
155
181
|
}
|
|
156
182
|
|
|
157
183
|
private cleanupDeviceState(deviceId: string): void {
|
|
184
|
+
this.protocolV2Links
|
|
185
|
+
.invalidateLink(deviceId, 'Electron BLE device state cleaned')
|
|
186
|
+
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
158
187
|
this.connectedDevices.delete(deviceId);
|
|
159
188
|
this.deviceProtocol.delete(deviceId);
|
|
189
|
+
this.deviceMtus.delete(deviceId);
|
|
190
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
160
191
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
161
192
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
162
193
|
this.v1Buffers.delete(deviceId);
|
|
163
194
|
this.v2Assemblers.delete(deviceId);
|
|
164
195
|
this.resetProtocolV2Frames(deviceId);
|
|
165
|
-
if (this.activeProtocolV2Call?.uuid === deviceId) {
|
|
166
|
-
this.activeProtocolV2Call = null;
|
|
167
|
-
}
|
|
168
196
|
this.notificationTokens.delete(deviceId);
|
|
197
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
198
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
199
|
+
this.runPromise = null;
|
|
200
|
+
this.runPromiseDeviceId = null;
|
|
201
|
+
}
|
|
169
202
|
|
|
170
203
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
171
204
|
if (notifyCleanup) {
|
|
@@ -173,6 +206,12 @@ export default class ElectronBleTransport {
|
|
|
173
206
|
this.notificationCleanups.delete(deviceId);
|
|
174
207
|
}
|
|
175
208
|
|
|
209
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
210
|
+
if (mtuCleanup) {
|
|
211
|
+
mtuCleanup();
|
|
212
|
+
this.mtuCleanups.delete(deviceId);
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
177
216
|
if (disconnectCleanup) {
|
|
178
217
|
disconnectCleanup();
|
|
@@ -200,8 +239,19 @@ export default class ElectronBleTransport {
|
|
|
200
239
|
}
|
|
201
240
|
|
|
202
241
|
configureProtocolV2(signedData: any) {
|
|
242
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
243
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
203
248
|
this._messagesV2 = parseConfigure(signedData);
|
|
204
|
-
this.
|
|
249
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
250
|
+
if (isReconfiguration) {
|
|
251
|
+
this.protocolV2Links
|
|
252
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
253
|
+
.catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
|
|
254
|
+
}
|
|
205
255
|
}
|
|
206
256
|
|
|
207
257
|
async listen() {
|
|
@@ -236,12 +286,15 @@ export default class ElectronBleTransport {
|
|
|
236
286
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
237
287
|
}
|
|
238
288
|
|
|
289
|
+
if (this.connectedDevices.has(uuid)) {
|
|
290
|
+
await this.release(uuid);
|
|
291
|
+
}
|
|
292
|
+
|
|
239
293
|
if (forceCleanRunPromise && this.runPromise) {
|
|
240
294
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
241
295
|
this.runPromise.reject(error);
|
|
242
|
-
this.rejectAllProtocolV2Frames(error);
|
|
243
296
|
this.runPromise = null;
|
|
244
|
-
this.
|
|
297
|
+
this.runPromiseDeviceId = null;
|
|
245
298
|
}
|
|
246
299
|
|
|
247
300
|
try {
|
|
@@ -255,7 +308,9 @@ export default class ElectronBleTransport {
|
|
|
255
308
|
}
|
|
256
309
|
const protocolHint = expectedProtocol
|
|
257
310
|
? undefined
|
|
258
|
-
:
|
|
311
|
+
: input.protocolHint ??
|
|
312
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
313
|
+
inferProtocolHintFromDeviceName(device.name);
|
|
259
314
|
if (protocolHint) {
|
|
260
315
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
261
316
|
}
|
|
@@ -267,19 +322,38 @@ export default class ElectronBleTransport {
|
|
|
267
322
|
this.handleBluetoothError(error);
|
|
268
323
|
}
|
|
269
324
|
|
|
325
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
326
|
+
if (mtuCleanup) {
|
|
327
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
328
|
+
}
|
|
329
|
+
|
|
270
330
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
271
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
331
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
272
332
|
|
|
273
333
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
334
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
274
335
|
|
|
275
336
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
276
337
|
this.notificationCleanups.set(uuid, cleanup);
|
|
338
|
+
const connectionToken = this.notificationTokens.get(uuid);
|
|
339
|
+
|
|
340
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
341
|
+
if (protocolType === 'V2') {
|
|
342
|
+
this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
|
|
343
|
+
writeMode: 'withoutResponse',
|
|
344
|
+
negotiatedMtu: this.deviceMtus.get(uuid),
|
|
345
|
+
packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
277
348
|
|
|
278
349
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
279
350
|
(disconnectedDevice: any) => {
|
|
280
|
-
if (
|
|
351
|
+
if (
|
|
352
|
+
disconnectedDevice.id === uuid &&
|
|
353
|
+
this.notificationTokens.get(uuid) === connectionToken
|
|
354
|
+
) {
|
|
281
355
|
this.cleanupDeviceState(uuid);
|
|
282
|
-
this.emitter?.emit(
|
|
356
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
283
357
|
name: disconnectedDevice.name,
|
|
284
358
|
id: disconnectedDevice.id,
|
|
285
359
|
connectId: disconnectedDevice.id,
|
|
@@ -289,14 +363,6 @@ export default class ElectronBleTransport {
|
|
|
289
363
|
);
|
|
290
364
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
291
365
|
|
|
292
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
293
|
-
|
|
294
|
-
this.emitter?.emit('device-connect', {
|
|
295
|
-
name: device.name,
|
|
296
|
-
id: device.id,
|
|
297
|
-
connectId: device.id,
|
|
298
|
-
});
|
|
299
|
-
|
|
300
366
|
return {
|
|
301
367
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
302
368
|
uuid,
|
|
@@ -317,6 +383,16 @@ export default class ElectronBleTransport {
|
|
|
317
383
|
}
|
|
318
384
|
|
|
319
385
|
async release(id: string) {
|
|
386
|
+
try {
|
|
387
|
+
await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
|
|
388
|
+
await this.releaseNative(id);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
391
|
+
this.cleanupDeviceState(id);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async releaseNative(id: string) {
|
|
320
396
|
try {
|
|
321
397
|
if (this.connectedDevices.has(id)) {
|
|
322
398
|
if (window.desktopApi?.nobleBle) {
|
|
@@ -341,7 +417,7 @@ export default class ElectronBleTransport {
|
|
|
341
417
|
private createProtocolDetectionError() {
|
|
342
418
|
return ERRORS.TypedError(
|
|
343
419
|
HardwareErrorCode.BleTimeoutError,
|
|
344
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
420
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
345
421
|
);
|
|
346
422
|
}
|
|
347
423
|
|
|
@@ -366,23 +442,23 @@ export default class ElectronBleTransport {
|
|
|
366
442
|
}
|
|
367
443
|
|
|
368
444
|
if (expectedProtocol === 'V2') {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
445
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
446
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
447
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
448
|
+
return 'V2';
|
|
449
|
+
}
|
|
450
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
374
451
|
}
|
|
375
452
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// 不能作为最终结论。
|
|
453
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
454
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
379
455
|
const probeOrder: ProtocolType[] =
|
|
380
456
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
381
457
|
|
|
382
458
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
383
459
|
const protocol = probeOrder[i];
|
|
384
460
|
if (i > 0) {
|
|
385
|
-
//
|
|
461
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
386
462
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
387
463
|
}
|
|
388
464
|
const detected =
|
|
@@ -415,16 +491,18 @@ export default class ElectronBleTransport {
|
|
|
415
491
|
}
|
|
416
492
|
|
|
417
493
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
494
|
+
await this.protocolV2Links.invalidateLink(
|
|
495
|
+
uuid,
|
|
496
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
497
|
+
);
|
|
418
498
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
419
499
|
this.v2Assemblers.get(uuid)?.reset();
|
|
420
500
|
this.resetProtocolV2Frames(uuid);
|
|
421
|
-
if (this.
|
|
422
|
-
this.activeProtocolV2Call = null;
|
|
423
|
-
}
|
|
424
|
-
if (this.runPromise) {
|
|
501
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
425
502
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
426
503
|
this.runPromise.reject(error);
|
|
427
504
|
this.runPromise = null;
|
|
505
|
+
this.runPromiseDeviceId = null;
|
|
428
506
|
}
|
|
429
507
|
|
|
430
508
|
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
@@ -440,9 +518,18 @@ export default class ElectronBleTransport {
|
|
|
440
518
|
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
441
519
|
}
|
|
442
520
|
try {
|
|
521
|
+
await window.desktopApi?.nobleBle?.disconnect(uuid);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
|
|
524
|
+
}
|
|
525
|
+
this.connectedDevices.delete(uuid);
|
|
526
|
+
try {
|
|
527
|
+
await window.desktopApi?.nobleBle?.connect(uuid);
|
|
528
|
+
this.connectedDevices.add(uuid);
|
|
443
529
|
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
530
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
444
531
|
} catch (error) {
|
|
445
|
-
this.Log?.debug(`[Electron BLE]
|
|
532
|
+
this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
|
|
446
533
|
throw error;
|
|
447
534
|
}
|
|
448
535
|
|
|
@@ -457,11 +544,13 @@ export default class ElectronBleTransport {
|
|
|
457
544
|
|
|
458
545
|
try {
|
|
459
546
|
this.deviceProtocol.set(uuid, 'V1');
|
|
460
|
-
|
|
547
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
548
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
549
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
461
550
|
return true;
|
|
462
551
|
} catch (error) {
|
|
463
552
|
this.clearProbeProtocol(uuid, 'V1');
|
|
464
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
553
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
465
554
|
return false;
|
|
466
555
|
}
|
|
467
556
|
}
|
|
@@ -489,44 +578,73 @@ export default class ElectronBleTransport {
|
|
|
489
578
|
return detected;
|
|
490
579
|
}
|
|
491
580
|
|
|
492
|
-
private async
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
497
|
-
await this.writeOnce(uuid, hexData);
|
|
498
|
-
return;
|
|
581
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
582
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
583
|
+
if (!nobleBle) {
|
|
584
|
+
throw new Error('Noble BLE API not available');
|
|
499
585
|
}
|
|
500
586
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
504
|
-
offset += chunkHexLen;
|
|
587
|
+
await nobleBle.write(uuid, hexData);
|
|
588
|
+
}
|
|
505
589
|
|
|
506
|
-
|
|
590
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
591
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
592
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
593
|
+
}
|
|
507
594
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
595
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
596
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
597
|
+
mtu,
|
|
598
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
599
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
600
|
+
);
|
|
601
|
+
if (typeof mtu === 'number') {
|
|
602
|
+
this.deviceMtus.set(uuid, mtu);
|
|
603
|
+
} else {
|
|
604
|
+
this.deviceMtus.delete(uuid);
|
|
511
605
|
}
|
|
606
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
512
607
|
}
|
|
513
608
|
|
|
514
|
-
private
|
|
515
|
-
const
|
|
516
|
-
if (!
|
|
517
|
-
|
|
518
|
-
|
|
609
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
610
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
611
|
+
if (!onMtuChanged) return undefined;
|
|
612
|
+
return onMtuChanged(device => {
|
|
613
|
+
if (device.id === uuid) {
|
|
614
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
519
618
|
|
|
520
|
-
|
|
619
|
+
private writeProtocolV2Frame(
|
|
620
|
+
uuid: string,
|
|
621
|
+
frame: Uint8Array,
|
|
622
|
+
context: ProtocolV2CallContext,
|
|
623
|
+
assertCurrentGeneration: () => void
|
|
624
|
+
) {
|
|
625
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
626
|
+
const shouldPace = !context.highVolume;
|
|
627
|
+
return writeProtocolV2BleFrame({
|
|
628
|
+
frame,
|
|
629
|
+
packetCapacity,
|
|
630
|
+
assertActive: assertCurrentGeneration,
|
|
631
|
+
signal: context.signal,
|
|
632
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
633
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
634
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
635
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
636
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
637
|
+
});
|
|
521
638
|
}
|
|
522
639
|
|
|
523
640
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
524
641
|
if (hexData === 'PAIRING_REJECTED') {
|
|
525
642
|
this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId);
|
|
526
|
-
|
|
527
|
-
|
|
643
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
644
|
+
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
645
|
+
this.rejectProtocolV2Frames(deviceId, error);
|
|
646
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
528
647
|
this.runPromise.reject(error);
|
|
529
|
-
this.rejectAllProtocolV2Frames(error);
|
|
530
648
|
}
|
|
531
649
|
return;
|
|
532
650
|
}
|
|
@@ -545,12 +663,6 @@ export default class ElectronBleTransport {
|
|
|
545
663
|
|
|
546
664
|
private handleProtocolV2Notification(deviceId: string, hexData: string): void {
|
|
547
665
|
try {
|
|
548
|
-
if (!this.runPromise || this.activeProtocolV2Call?.uuid !== deviceId) {
|
|
549
|
-
this.v2Assemblers.get(deviceId)?.reset();
|
|
550
|
-
this.resetProtocolV2Frames(deviceId);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
666
|
const bytes = hexToBytes(hexData);
|
|
555
667
|
if (bytes.length === 0) return;
|
|
556
668
|
|
|
@@ -562,11 +674,8 @@ export default class ElectronBleTransport {
|
|
|
562
674
|
}
|
|
563
675
|
} catch (error) {
|
|
564
676
|
this.Log?.error('[Electron BLE] Protocol V2 notification error:', error);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.runPromise.reject(notifyError);
|
|
568
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
569
|
-
}
|
|
677
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
678
|
+
this.rejectProtocolV2Frames(deviceId, notifyError);
|
|
570
679
|
}
|
|
571
680
|
}
|
|
572
681
|
|
|
@@ -589,21 +698,17 @@ export default class ElectronBleTransport {
|
|
|
589
698
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
590
699
|
}
|
|
591
700
|
|
|
592
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
593
|
-
this.v2FrameQueues.clear();
|
|
594
|
-
for (const framePromise of this.v2FramePromises.values()) {
|
|
595
|
-
framePromise.reject(error);
|
|
596
|
-
}
|
|
597
|
-
this.v2FramePromises.clear();
|
|
598
|
-
}
|
|
599
|
-
|
|
600
701
|
private resetProtocolV2Frames(uuid: string) {
|
|
601
|
-
this.
|
|
602
|
-
this.v2FramePromises.delete(uuid);
|
|
702
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
603
703
|
}
|
|
604
704
|
|
|
605
|
-
private
|
|
606
|
-
|
|
705
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
706
|
+
this.v2FrameQueues.delete(uuid);
|
|
707
|
+
const framePromise = this.v2FramePromises.get(uuid);
|
|
708
|
+
if (framePromise) {
|
|
709
|
+
this.v2FramePromises.delete(uuid);
|
|
710
|
+
framePromise.reject(error);
|
|
711
|
+
}
|
|
607
712
|
}
|
|
608
713
|
|
|
609
714
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -628,13 +733,18 @@ export default class ElectronBleTransport {
|
|
|
628
733
|
|
|
629
734
|
if (result.error) {
|
|
630
735
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
631
|
-
if (this.runPromise) {
|
|
736
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
632
737
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
633
738
|
}
|
|
634
739
|
return;
|
|
635
740
|
}
|
|
636
741
|
|
|
637
|
-
if (
|
|
742
|
+
if (
|
|
743
|
+
result.isComplete &&
|
|
744
|
+
result.completePacket &&
|
|
745
|
+
this.runPromise &&
|
|
746
|
+
this.runPromiseDeviceId === deviceId
|
|
747
|
+
) {
|
|
638
748
|
this.runPromise.resolve(result.completePacket);
|
|
639
749
|
}
|
|
640
750
|
}
|
|
@@ -660,12 +770,8 @@ export default class ElectronBleTransport {
|
|
|
660
770
|
`Device protocol has not been detected for ${uuid}`
|
|
661
771
|
);
|
|
662
772
|
}
|
|
663
|
-
if (shouldSuppressHighVolumeCallLog(name)) {
|
|
664
|
-
|
|
665
|
-
} else if (isLogBlockCommand(name)) {
|
|
666
|
-
this.Log?.debug('[Electron BLE] call', 'name:', name, 'protocol:', protocol);
|
|
667
|
-
} else {
|
|
668
|
-
this.Log?.debug('[Electron BLE] call', 'name:', name, 'data:', data, 'protocol:', protocol);
|
|
773
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
774
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
669
775
|
}
|
|
670
776
|
|
|
671
777
|
if (protocol === 'V2') {
|
|
@@ -692,6 +798,7 @@ export default class ElectronBleTransport {
|
|
|
692
798
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
693
799
|
runPromise.promise.catch(() => undefined);
|
|
694
800
|
this.runPromise = runPromise;
|
|
801
|
+
this.runPromiseDeviceId = uuid;
|
|
695
802
|
const messages = this._messages;
|
|
696
803
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
697
804
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -736,11 +843,24 @@ export default class ElectronBleTransport {
|
|
|
736
843
|
return check.call(jsonData);
|
|
737
844
|
} catch (e) {
|
|
738
845
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
846
|
+
const isProbeTimeout =
|
|
847
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
848
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
849
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
850
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
851
|
+
notifyCleanup?.();
|
|
852
|
+
this.notificationCleanups.delete(uuid);
|
|
853
|
+
this.notificationTokens.delete(uuid);
|
|
854
|
+
if (!isProbeTimeout) {
|
|
855
|
+
await this.releaseNative(uuid);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
739
858
|
throw e;
|
|
740
859
|
} finally {
|
|
741
860
|
if (timeout) clearTimeout(timeout);
|
|
742
861
|
if (this.runPromise === runPromise) {
|
|
743
862
|
this.runPromise = null;
|
|
863
|
+
this.runPromiseDeviceId = null;
|
|
744
864
|
}
|
|
745
865
|
}
|
|
746
866
|
}
|
|
@@ -755,77 +875,61 @@ export default class ElectronBleTransport {
|
|
|
755
875
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
756
876
|
}
|
|
757
877
|
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
878
|
+
const callOptions = options;
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
return await this.protocolV2Links.call(
|
|
882
|
+
uuid,
|
|
883
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
884
|
+
name,
|
|
885
|
+
data,
|
|
886
|
+
callOptions
|
|
887
|
+
);
|
|
888
|
+
} catch (e) {
|
|
889
|
+
this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
|
|
890
|
+
throw e;
|
|
768
891
|
}
|
|
892
|
+
}
|
|
769
893
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
this.resetProtocolV2Frames(uuid);
|
|
777
|
-
let completed = false;
|
|
778
|
-
const callOptions = {
|
|
779
|
-
...options,
|
|
780
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
894
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
895
|
+
const generation = this.notificationTokens.get(uuid) ?? 0;
|
|
896
|
+
const assertCurrentGeneration = () => {
|
|
897
|
+
if (this.notificationTokens.get(uuid) !== generation) {
|
|
898
|
+
throw new Error(`Protocol V2 notification generation changed for ${uuid}`);
|
|
899
|
+
}
|
|
781
900
|
};
|
|
782
901
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
790
|
-
writeFrame: (frame: Uint8Array) => this.writeWithChunking(uuid, bytesToHex(frame)),
|
|
791
|
-
readFrame: async () => {
|
|
792
|
-
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
793
|
-
if (!(rxFrame instanceof Uint8Array)) {
|
|
794
|
-
throw new Error('Response is not Uint8Array');
|
|
795
|
-
}
|
|
796
|
-
return rxFrame;
|
|
797
|
-
},
|
|
798
|
-
logger: this.Log,
|
|
799
|
-
logPrefix: 'ProtocolV2 BLE',
|
|
800
|
-
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
801
|
-
ERRORS.TypedError(
|
|
802
|
-
HardwareErrorCode.BleTimeoutError,
|
|
803
|
-
`BLE response timeout after ${timeout}ms for ${name}`
|
|
804
|
-
),
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
const result = await session.call(name, data, callOptions);
|
|
808
|
-
completed = true;
|
|
809
|
-
return result;
|
|
810
|
-
} catch (e) {
|
|
811
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
902
|
+
return {
|
|
903
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
904
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
905
|
+
generation,
|
|
906
|
+
prepareCall: () => {
|
|
907
|
+
assertCurrentGeneration();
|
|
812
908
|
this.v2Assemblers.get(uuid)?.reset();
|
|
813
909
|
this.resetProtocolV2Frames(uuid);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
910
|
+
},
|
|
911
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
912
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
913
|
+
readFrame: async () => {
|
|
914
|
+
assertCurrentGeneration();
|
|
915
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
916
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
917
|
+
throw new Error('Response is not Uint8Array');
|
|
821
918
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
this.
|
|
827
|
-
}
|
|
828
|
-
|
|
919
|
+
return rxFrame;
|
|
920
|
+
},
|
|
921
|
+
reset: (reason: string) => {
|
|
922
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
923
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
924
|
+
},
|
|
925
|
+
logger: this.Log,
|
|
926
|
+
logPrefix: 'ProtocolV2 BLE',
|
|
927
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
928
|
+
ERRORS.TypedError(
|
|
929
|
+
HardwareErrorCode.BleTimeoutError,
|
|
930
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
931
|
+
),
|
|
932
|
+
};
|
|
829
933
|
}
|
|
830
934
|
|
|
831
935
|
private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult {
|