@onekeyfe/hd-transport-web-device 1.2.0-alpha.8 → 1.2.0-alpha.80
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 +453 -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 +18 -6
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +112 -15
- package/dist/index.js +424 -327
- 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 +321 -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,34 @@ export default class ElectronBleTransport {
|
|
|
111
118
|
|
|
112
119
|
private v2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
113
120
|
|
|
114
|
-
private
|
|
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
|
+
});
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
/** One-shot guard so a missing preload `release` bridge is reported once. */
|
|
143
|
+
private warnedMissingRelease = false;
|
|
117
144
|
|
|
118
145
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
119
146
|
|
|
147
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
148
|
+
|
|
120
149
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
121
150
|
|
|
122
151
|
private notificationTokens: Map<string, number> = new Map();
|
|
@@ -155,17 +184,24 @@ export default class ElectronBleTransport {
|
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
private cleanupDeviceState(deviceId: string): void {
|
|
187
|
+
this.protocolV2Links
|
|
188
|
+
.invalidateLink(deviceId, 'Electron BLE device state cleaned')
|
|
189
|
+
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
158
190
|
this.connectedDevices.delete(deviceId);
|
|
159
191
|
this.deviceProtocol.delete(deviceId);
|
|
192
|
+
this.deviceMtus.delete(deviceId);
|
|
193
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
160
194
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
161
195
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
162
196
|
this.v1Buffers.delete(deviceId);
|
|
163
197
|
this.v2Assemblers.delete(deviceId);
|
|
164
198
|
this.resetProtocolV2Frames(deviceId);
|
|
165
|
-
if (this.activeProtocolV2Call?.uuid === deviceId) {
|
|
166
|
-
this.activeProtocolV2Call = null;
|
|
167
|
-
}
|
|
168
199
|
this.notificationTokens.delete(deviceId);
|
|
200
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
201
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
202
|
+
this.runPromise = null;
|
|
203
|
+
this.runPromiseDeviceId = null;
|
|
204
|
+
}
|
|
169
205
|
|
|
170
206
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
171
207
|
if (notifyCleanup) {
|
|
@@ -173,6 +209,12 @@ export default class ElectronBleTransport {
|
|
|
173
209
|
this.notificationCleanups.delete(deviceId);
|
|
174
210
|
}
|
|
175
211
|
|
|
212
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
213
|
+
if (mtuCleanup) {
|
|
214
|
+
mtuCleanup();
|
|
215
|
+
this.mtuCleanups.delete(deviceId);
|
|
216
|
+
}
|
|
217
|
+
|
|
176
218
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
177
219
|
if (disconnectCleanup) {
|
|
178
220
|
disconnectCleanup();
|
|
@@ -200,8 +242,19 @@ export default class ElectronBleTransport {
|
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
configureProtocolV2(signedData: any) {
|
|
245
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
246
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
203
251
|
this._messagesV2 = parseConfigure(signedData);
|
|
204
|
-
this.
|
|
252
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
253
|
+
if (isReconfiguration) {
|
|
254
|
+
this.protocolV2Links
|
|
255
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
256
|
+
.catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
|
|
257
|
+
}
|
|
205
258
|
}
|
|
206
259
|
|
|
207
260
|
async listen() {
|
|
@@ -236,12 +289,15 @@ export default class ElectronBleTransport {
|
|
|
236
289
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
237
290
|
}
|
|
238
291
|
|
|
292
|
+
if (this.connectedDevices.has(uuid)) {
|
|
293
|
+
await this.release(uuid);
|
|
294
|
+
}
|
|
295
|
+
|
|
239
296
|
if (forceCleanRunPromise && this.runPromise) {
|
|
240
297
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
241
298
|
this.runPromise.reject(error);
|
|
242
|
-
this.rejectAllProtocolV2Frames(error);
|
|
243
299
|
this.runPromise = null;
|
|
244
|
-
this.
|
|
300
|
+
this.runPromiseDeviceId = null;
|
|
245
301
|
}
|
|
246
302
|
|
|
247
303
|
try {
|
|
@@ -255,7 +311,9 @@ export default class ElectronBleTransport {
|
|
|
255
311
|
}
|
|
256
312
|
const protocolHint = expectedProtocol
|
|
257
313
|
? undefined
|
|
258
|
-
:
|
|
314
|
+
: input.protocolHint ??
|
|
315
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
316
|
+
inferProtocolHintFromDeviceName(device.name);
|
|
259
317
|
if (protocolHint) {
|
|
260
318
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
261
319
|
}
|
|
@@ -267,19 +325,38 @@ export default class ElectronBleTransport {
|
|
|
267
325
|
this.handleBluetoothError(error);
|
|
268
326
|
}
|
|
269
327
|
|
|
328
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
329
|
+
if (mtuCleanup) {
|
|
330
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
331
|
+
}
|
|
332
|
+
|
|
270
333
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
271
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
334
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
272
335
|
|
|
273
336
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
337
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
274
338
|
|
|
275
339
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
276
340
|
this.notificationCleanups.set(uuid, cleanup);
|
|
341
|
+
const connectionToken = this.notificationTokens.get(uuid);
|
|
342
|
+
|
|
343
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
344
|
+
if (protocolType === 'V2') {
|
|
345
|
+
this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
|
|
346
|
+
writeMode: 'withoutResponse',
|
|
347
|
+
negotiatedMtu: this.deviceMtus.get(uuid),
|
|
348
|
+
packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
277
351
|
|
|
278
352
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
279
353
|
(disconnectedDevice: any) => {
|
|
280
|
-
if (
|
|
354
|
+
if (
|
|
355
|
+
disconnectedDevice.id === uuid &&
|
|
356
|
+
this.notificationTokens.get(uuid) === connectionToken
|
|
357
|
+
) {
|
|
281
358
|
this.cleanupDeviceState(uuid);
|
|
282
|
-
this.emitter?.emit(
|
|
359
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
283
360
|
name: disconnectedDevice.name,
|
|
284
361
|
id: disconnectedDevice.id,
|
|
285
362
|
connectId: disconnectedDevice.id,
|
|
@@ -289,14 +366,6 @@ export default class ElectronBleTransport {
|
|
|
289
366
|
);
|
|
290
367
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
291
368
|
|
|
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
369
|
return {
|
|
301
370
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
302
371
|
uuid,
|
|
@@ -316,7 +385,24 @@ export default class ElectronBleTransport {
|
|
|
316
385
|
}
|
|
317
386
|
}
|
|
318
387
|
|
|
319
|
-
async release(id: string) {
|
|
388
|
+
async release(id: string, _onclose?: boolean, keepSession?: boolean) {
|
|
389
|
+
try {
|
|
390
|
+
await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
|
|
391
|
+
await this.releaseLogical(id, keepSession);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
394
|
+
this.cleanupDeviceState(id);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// DeviceConnector.disconnect feature-detects this; without it REQUIRE_DISCONNECT
|
|
399
|
+
// recovery is a no-op and keep-alive hands the wedged link to every retry.
|
|
400
|
+
async disconnect(id: string) {
|
|
401
|
+
return this.releaseNative(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Hard teardown, error paths only: a link presumed dead must not be reused.
|
|
405
|
+
private async releaseNative(id: string) {
|
|
320
406
|
try {
|
|
321
407
|
if (this.connectedDevices.has(id)) {
|
|
322
408
|
if (window.desktopApi?.nobleBle) {
|
|
@@ -331,6 +417,32 @@ export default class ElectronBleTransport {
|
|
|
331
417
|
}
|
|
332
418
|
}
|
|
333
419
|
|
|
420
|
+
// Logical release: link and subscription stay up for the next call. Renderer
|
|
421
|
+
// listeners are still torn down, or fresh ones would double-process packets.
|
|
422
|
+
private async releaseLogical(id: string, keepSession?: boolean) {
|
|
423
|
+
try {
|
|
424
|
+
if (!this.connectedDevices.has(id)) return;
|
|
425
|
+
this.cleanupDeviceState(id);
|
|
426
|
+
|
|
427
|
+
const release = window.desktopApi?.nobleBle?.release;
|
|
428
|
+
if (!release) {
|
|
429
|
+
// Degraded, not broken: say it once, it just looks like a slow device.
|
|
430
|
+
if (!this.warnedMissingRelease) {
|
|
431
|
+
this.warnedMissingRelease = true;
|
|
432
|
+
this.Log?.error(
|
|
433
|
+
'[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
|
|
434
|
+
'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload'
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await release(id, keepSession);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.Log?.error('[Electron BLE] logical release failed:', error);
|
|
442
|
+
this.cleanupDeviceState(id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
334
446
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
335
447
|
return ERRORS.TypedError(
|
|
336
448
|
HardwareErrorCode.RuntimeError,
|
|
@@ -341,7 +453,7 @@ export default class ElectronBleTransport {
|
|
|
341
453
|
private createProtocolDetectionError() {
|
|
342
454
|
return ERRORS.TypedError(
|
|
343
455
|
HardwareErrorCode.BleTimeoutError,
|
|
344
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
456
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
345
457
|
);
|
|
346
458
|
}
|
|
347
459
|
|
|
@@ -366,23 +478,23 @@ export default class ElectronBleTransport {
|
|
|
366
478
|
}
|
|
367
479
|
|
|
368
480
|
if (expectedProtocol === 'V2') {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
481
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
482
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
483
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
484
|
+
return 'V2';
|
|
485
|
+
}
|
|
486
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
374
487
|
}
|
|
375
488
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// 不能作为最终结论。
|
|
489
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
490
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
379
491
|
const probeOrder: ProtocolType[] =
|
|
380
492
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
381
493
|
|
|
382
494
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
383
495
|
const protocol = probeOrder[i];
|
|
384
496
|
if (i > 0) {
|
|
385
|
-
//
|
|
497
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
386
498
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
387
499
|
}
|
|
388
500
|
const detected =
|
|
@@ -415,16 +527,18 @@ export default class ElectronBleTransport {
|
|
|
415
527
|
}
|
|
416
528
|
|
|
417
529
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
530
|
+
await this.protocolV2Links.invalidateLink(
|
|
531
|
+
uuid,
|
|
532
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
533
|
+
);
|
|
418
534
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
419
535
|
this.v2Assemblers.get(uuid)?.reset();
|
|
420
536
|
this.resetProtocolV2Frames(uuid);
|
|
421
|
-
if (this.
|
|
422
|
-
this.activeProtocolV2Call = null;
|
|
423
|
-
}
|
|
424
|
-
if (this.runPromise) {
|
|
537
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
425
538
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
426
539
|
this.runPromise.reject(error);
|
|
427
540
|
this.runPromise = null;
|
|
541
|
+
this.runPromiseDeviceId = null;
|
|
428
542
|
}
|
|
429
543
|
|
|
430
544
|
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
@@ -440,9 +554,18 @@ export default class ElectronBleTransport {
|
|
|
440
554
|
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
441
555
|
}
|
|
442
556
|
try {
|
|
557
|
+
await window.desktopApi?.nobleBle?.disconnect(uuid);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
|
|
560
|
+
}
|
|
561
|
+
this.connectedDevices.delete(uuid);
|
|
562
|
+
try {
|
|
563
|
+
await window.desktopApi?.nobleBle?.connect(uuid);
|
|
564
|
+
this.connectedDevices.add(uuid);
|
|
443
565
|
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
566
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
444
567
|
} catch (error) {
|
|
445
|
-
this.Log?.debug(`[Electron BLE]
|
|
568
|
+
this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
|
|
446
569
|
throw error;
|
|
447
570
|
}
|
|
448
571
|
|
|
@@ -457,11 +580,13 @@ export default class ElectronBleTransport {
|
|
|
457
580
|
|
|
458
581
|
try {
|
|
459
582
|
this.deviceProtocol.set(uuid, 'V1');
|
|
460
|
-
|
|
583
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
584
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
585
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
461
586
|
return true;
|
|
462
587
|
} catch (error) {
|
|
463
588
|
this.clearProbeProtocol(uuid, 'V1');
|
|
464
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
589
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
465
590
|
return false;
|
|
466
591
|
}
|
|
467
592
|
}
|
|
@@ -489,44 +614,73 @@ export default class ElectronBleTransport {
|
|
|
489
614
|
return detected;
|
|
490
615
|
}
|
|
491
616
|
|
|
492
|
-
private async
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
497
|
-
await this.writeOnce(uuid, hexData);
|
|
498
|
-
return;
|
|
617
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
618
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
619
|
+
if (!nobleBle) {
|
|
620
|
+
throw new Error('Noble BLE API not available');
|
|
499
621
|
}
|
|
500
622
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
504
|
-
offset += chunkHexLen;
|
|
623
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
624
|
+
}
|
|
505
625
|
|
|
506
|
-
|
|
626
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
627
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
628
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
629
|
+
}
|
|
507
630
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
631
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
632
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
633
|
+
mtu,
|
|
634
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
635
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
636
|
+
);
|
|
637
|
+
if (typeof mtu === 'number') {
|
|
638
|
+
this.deviceMtus.set(uuid, mtu);
|
|
639
|
+
} else {
|
|
640
|
+
this.deviceMtus.delete(uuid);
|
|
511
641
|
}
|
|
642
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
512
643
|
}
|
|
513
644
|
|
|
514
|
-
private
|
|
515
|
-
const
|
|
516
|
-
if (!
|
|
517
|
-
|
|
518
|
-
|
|
645
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
646
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
647
|
+
if (!onMtuChanged) return undefined;
|
|
648
|
+
return onMtuChanged(device => {
|
|
649
|
+
if (device.id === uuid) {
|
|
650
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
519
654
|
|
|
520
|
-
|
|
655
|
+
private writeProtocolV2Frame(
|
|
656
|
+
uuid: string,
|
|
657
|
+
frame: Uint8Array,
|
|
658
|
+
context: ProtocolV2CallContext,
|
|
659
|
+
assertCurrentGeneration: () => void
|
|
660
|
+
) {
|
|
661
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
662
|
+
const shouldPace = !context.highThroughput;
|
|
663
|
+
return writeProtocolV2BleFrame({
|
|
664
|
+
frame,
|
|
665
|
+
packetCapacity,
|
|
666
|
+
assertActive: assertCurrentGeneration,
|
|
667
|
+
signal: context.signal,
|
|
668
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
669
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
670
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
671
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
672
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
673
|
+
});
|
|
521
674
|
}
|
|
522
675
|
|
|
523
676
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
524
677
|
if (hexData === 'PAIRING_REJECTED') {
|
|
525
678
|
this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId);
|
|
526
|
-
|
|
527
|
-
|
|
679
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
680
|
+
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
681
|
+
this.rejectProtocolV2Frames(deviceId, error);
|
|
682
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
528
683
|
this.runPromise.reject(error);
|
|
529
|
-
this.rejectAllProtocolV2Frames(error);
|
|
530
684
|
}
|
|
531
685
|
return;
|
|
532
686
|
}
|
|
@@ -545,12 +699,6 @@ export default class ElectronBleTransport {
|
|
|
545
699
|
|
|
546
700
|
private handleProtocolV2Notification(deviceId: string, hexData: string): void {
|
|
547
701
|
try {
|
|
548
|
-
if (!this.runPromise || this.activeProtocolV2Call?.uuid !== deviceId) {
|
|
549
|
-
this.v2Assemblers.get(deviceId)?.reset();
|
|
550
|
-
this.resetProtocolV2Frames(deviceId);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
702
|
const bytes = hexToBytes(hexData);
|
|
555
703
|
if (bytes.length === 0) return;
|
|
556
704
|
|
|
@@ -562,11 +710,8 @@ export default class ElectronBleTransport {
|
|
|
562
710
|
}
|
|
563
711
|
} catch (error) {
|
|
564
712
|
this.Log?.error('[Electron BLE] Protocol V2 notification error:', error);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.runPromise.reject(notifyError);
|
|
568
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
569
|
-
}
|
|
713
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
714
|
+
this.rejectProtocolV2Frames(deviceId, notifyError);
|
|
570
715
|
}
|
|
571
716
|
}
|
|
572
717
|
|
|
@@ -589,21 +734,17 @@ export default class ElectronBleTransport {
|
|
|
589
734
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
590
735
|
}
|
|
591
736
|
|
|
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
737
|
private resetProtocolV2Frames(uuid: string) {
|
|
601
|
-
this.
|
|
602
|
-
this.v2FramePromises.delete(uuid);
|
|
738
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
603
739
|
}
|
|
604
740
|
|
|
605
|
-
private
|
|
606
|
-
|
|
741
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
742
|
+
this.v2FrameQueues.delete(uuid);
|
|
743
|
+
const framePromise = this.v2FramePromises.get(uuid);
|
|
744
|
+
if (framePromise) {
|
|
745
|
+
this.v2FramePromises.delete(uuid);
|
|
746
|
+
framePromise.reject(error);
|
|
747
|
+
}
|
|
607
748
|
}
|
|
608
749
|
|
|
609
750
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -628,13 +769,18 @@ export default class ElectronBleTransport {
|
|
|
628
769
|
|
|
629
770
|
if (result.error) {
|
|
630
771
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
631
|
-
if (this.runPromise) {
|
|
772
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
632
773
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
633
774
|
}
|
|
634
775
|
return;
|
|
635
776
|
}
|
|
636
777
|
|
|
637
|
-
if (
|
|
778
|
+
if (
|
|
779
|
+
result.isComplete &&
|
|
780
|
+
result.completePacket &&
|
|
781
|
+
this.runPromise &&
|
|
782
|
+
this.runPromiseDeviceId === deviceId
|
|
783
|
+
) {
|
|
638
784
|
this.runPromise.resolve(result.completePacket);
|
|
639
785
|
}
|
|
640
786
|
}
|
|
@@ -660,12 +806,8 @@ export default class ElectronBleTransport {
|
|
|
660
806
|
`Device protocol has not been detected for ${uuid}`
|
|
661
807
|
);
|
|
662
808
|
}
|
|
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);
|
|
809
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
810
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
669
811
|
}
|
|
670
812
|
|
|
671
813
|
if (protocol === 'V2') {
|
|
@@ -692,6 +834,7 @@ export default class ElectronBleTransport {
|
|
|
692
834
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
693
835
|
runPromise.promise.catch(() => undefined);
|
|
694
836
|
this.runPromise = runPromise;
|
|
837
|
+
this.runPromiseDeviceId = uuid;
|
|
695
838
|
const messages = this._messages;
|
|
696
839
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
697
840
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -736,11 +879,24 @@ export default class ElectronBleTransport {
|
|
|
736
879
|
return check.call(jsonData);
|
|
737
880
|
} catch (e) {
|
|
738
881
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
882
|
+
const isProbeTimeout =
|
|
883
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
884
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
885
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
886
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
887
|
+
notifyCleanup?.();
|
|
888
|
+
this.notificationCleanups.delete(uuid);
|
|
889
|
+
this.notificationTokens.delete(uuid);
|
|
890
|
+
if (!isProbeTimeout) {
|
|
891
|
+
await this.releaseNative(uuid);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
739
894
|
throw e;
|
|
740
895
|
} finally {
|
|
741
896
|
if (timeout) clearTimeout(timeout);
|
|
742
897
|
if (this.runPromise === runPromise) {
|
|
743
898
|
this.runPromise = null;
|
|
899
|
+
this.runPromiseDeviceId = null;
|
|
744
900
|
}
|
|
745
901
|
}
|
|
746
902
|
}
|
|
@@ -755,77 +911,61 @@ export default class ElectronBleTransport {
|
|
|
755
911
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
756
912
|
}
|
|
757
913
|
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
914
|
+
const callOptions = options;
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
return await this.protocolV2Links.call(
|
|
918
|
+
uuid,
|
|
919
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
920
|
+
name,
|
|
921
|
+
data,
|
|
922
|
+
callOptions
|
|
923
|
+
);
|
|
924
|
+
} catch (e) {
|
|
925
|
+
this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
|
|
926
|
+
throw e;
|
|
768
927
|
}
|
|
928
|
+
}
|
|
769
929
|
|
|
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,
|
|
930
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
931
|
+
const generation = this.notificationTokens.get(uuid) ?? 0;
|
|
932
|
+
const assertCurrentGeneration = () => {
|
|
933
|
+
if (this.notificationTokens.get(uuid) !== generation) {
|
|
934
|
+
throw new Error(`Protocol V2 notification generation changed for ${uuid}`);
|
|
935
|
+
}
|
|
781
936
|
};
|
|
782
937
|
|
|
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)) {
|
|
938
|
+
return {
|
|
939
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
940
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
941
|
+
generation,
|
|
942
|
+
prepareCall: () => {
|
|
943
|
+
assertCurrentGeneration();
|
|
812
944
|
this.v2Assemblers.get(uuid)?.reset();
|
|
813
945
|
this.resetProtocolV2Frames(uuid);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
946
|
+
},
|
|
947
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
948
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
949
|
+
readFrame: async () => {
|
|
950
|
+
assertCurrentGeneration();
|
|
951
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
952
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
953
|
+
throw new Error('Response is not Uint8Array');
|
|
821
954
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
this.
|
|
827
|
-
}
|
|
828
|
-
|
|
955
|
+
return rxFrame;
|
|
956
|
+
},
|
|
957
|
+
reset: (reason: string) => {
|
|
958
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
959
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
960
|
+
},
|
|
961
|
+
logger: this.Log,
|
|
962
|
+
logPrefix: 'ProtocolV2 BLE',
|
|
963
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
964
|
+
ERRORS.TypedError(
|
|
965
|
+
HardwareErrorCode.BleTimeoutError,
|
|
966
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
967
|
+
),
|
|
968
|
+
};
|
|
829
969
|
}
|
|
830
970
|
|
|
831
971
|
private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult {
|
|
@@ -849,6 +989,12 @@ export default class ElectronBleTransport {
|
|
|
849
989
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
850
990
|
bufferState.buffer = [...data.subarray(3)];
|
|
851
991
|
} else {
|
|
992
|
+
if (bufferState.buffer.length === 0) {
|
|
993
|
+
// Tail of a cancelled call, arriving after the buffer was reset. Not an
|
|
994
|
+
// `error`: the caller rejects the in-flight call on that field.
|
|
995
|
+
this.Log?.debug('[Electron BLE] Orphan continuation chunk discarded');
|
|
996
|
+
return { isComplete: false };
|
|
997
|
+
}
|
|
852
998
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
853
999
|
}
|
|
854
1000
|
|