@onekeyfe/hd-transport-web-device 1.2.0-alpha.16 → 1.2.0-alpha.161
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 +551 -27
- package/__tests__/webusb-protocol-cache.test.ts +244 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +363 -11
- 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 +19 -4
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +169 -15
- package/dist/index.js +451 -296
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/webusb.d.ts +25 -11
- 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 +291 -130
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +327 -205
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import transport, {
|
|
2
2
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
3
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
3
4
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
4
5
|
ProtocolV2FrameAssembler,
|
|
5
6
|
ProtocolV2LinkManager,
|
|
7
|
+
TRANSPORT_EVENT,
|
|
6
8
|
bytesToHex,
|
|
7
9
|
hexToBytes,
|
|
8
10
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
11
|
+
writeProtocolV2BleFrame,
|
|
9
12
|
} from '@onekeyfe/hd-transport';
|
|
10
13
|
import {
|
|
14
|
+
EBleDisconnectReason,
|
|
11
15
|
ERRORS,
|
|
12
16
|
HardwareErrorCode,
|
|
13
17
|
HardwareErrorCodeMessage,
|
|
14
18
|
createDeferred,
|
|
15
19
|
isHeaderChunk,
|
|
16
|
-
wait,
|
|
17
20
|
} from '@onekeyfe/hd-shared';
|
|
18
21
|
|
|
19
|
-
import {
|
|
22
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
20
23
|
|
|
21
24
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
22
25
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
23
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
OneKeyDeviceInfo,
|
|
28
|
+
ProtocolType,
|
|
29
|
+
ProtocolV2CallContext,
|
|
30
|
+
TransportCallOptions,
|
|
31
|
+
} from '@onekeyfe/hd-transport';
|
|
24
32
|
import type EventEmitter from 'events';
|
|
25
33
|
|
|
26
34
|
const { parseConfigure, ProtocolV1, check } = transport;
|
|
@@ -35,6 +43,7 @@ export type BleAcquireInput = {
|
|
|
35
43
|
uuid: string;
|
|
36
44
|
forceCleanRunPromise?: boolean;
|
|
37
45
|
expectedProtocol?: ProtocolType;
|
|
46
|
+
protocolHint?: ProtocolType;
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
interface PacketProcessResult {
|
|
@@ -43,10 +52,6 @@ interface PacketProcessResult {
|
|
|
43
52
|
error?: string;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
47
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
55
|
const toBleDescriptor = (
|
|
51
56
|
device: { id: string; name: string | null },
|
|
52
57
|
protocolType?: ProtocolType
|
|
@@ -60,29 +65,33 @@ const toBleDescriptor = (
|
|
|
60
65
|
...(protocolType ? { protocolType } : {}),
|
|
61
66
|
} as OneKeyDeviceInfo);
|
|
62
67
|
|
|
63
|
-
const
|
|
68
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
69
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
64
70
|
const BLE_WRITE_DELAY_MS = 5;
|
|
65
|
-
const
|
|
66
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
71
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
67
72
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
75
|
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
71
76
|
*
|
|
72
77
|
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
73
|
-
* after a Protocol V1
|
|
78
|
+
* after a Protocol V1 GetFeatures timeout by probing Protocol V2 Ping.
|
|
74
79
|
*/
|
|
75
80
|
export default class ElectronBleTransport {
|
|
76
81
|
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
77
82
|
|
|
78
83
|
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
79
84
|
|
|
85
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
86
|
+
|
|
80
87
|
name = 'ElectronBleTransport';
|
|
81
88
|
|
|
82
89
|
configured = false;
|
|
83
90
|
|
|
84
91
|
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
85
92
|
|
|
93
|
+
private runPromiseDeviceId: string | null = null;
|
|
94
|
+
|
|
86
95
|
Log?: any;
|
|
87
96
|
|
|
88
97
|
emitter?: EventEmitter;
|
|
@@ -93,6 +102,13 @@ export default class ElectronBleTransport {
|
|
|
93
102
|
|
|
94
103
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
95
104
|
|
|
105
|
+
/** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
|
|
106
|
+
private confirmedProtocolV2 = new Set<string>();
|
|
107
|
+
|
|
108
|
+
private deviceMtus: Map<string, number> = new Map();
|
|
109
|
+
|
|
110
|
+
private devicePacketCapacities: Map<string, number> = new Map();
|
|
111
|
+
|
|
96
112
|
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
97
113
|
|
|
98
114
|
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -117,14 +133,27 @@ export default class ElectronBleTransport {
|
|
|
117
133
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
118
134
|
this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
|
|
119
135
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
120
|
-
await this.
|
|
136
|
+
await this.releaseNative(uuid);
|
|
121
137
|
}
|
|
122
138
|
},
|
|
123
139
|
});
|
|
124
140
|
|
|
141
|
+
/** One-shot guard so a missing preload `release` bridge is reported once. */
|
|
142
|
+
private warnedMissingRelease = false;
|
|
143
|
+
|
|
125
144
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
126
145
|
|
|
127
|
-
private
|
|
146
|
+
private mtuCleanups: Map<string, () => void> = new Map();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Transport-lifetime subscription to host BLE disconnects.
|
|
150
|
+
*
|
|
151
|
+
* This must NOT be scoped to acquire()/release(): a logical release keeps the
|
|
152
|
+
* native link alive for the keep-alive window, so a device that drops while
|
|
153
|
+
* idle would otherwise go unobserved and consumers would never learn it left
|
|
154
|
+
* (OK-60486).
|
|
155
|
+
*/
|
|
156
|
+
private hostDisconnectCleanup?: () => void;
|
|
128
157
|
|
|
129
158
|
private notificationTokens: Map<string, number> = new Map();
|
|
130
159
|
|
|
@@ -167,12 +196,19 @@ export default class ElectronBleTransport {
|
|
|
167
196
|
.catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error));
|
|
168
197
|
this.connectedDevices.delete(deviceId);
|
|
169
198
|
this.deviceProtocol.delete(deviceId);
|
|
199
|
+
this.deviceMtus.delete(deviceId);
|
|
200
|
+
this.devicePacketCapacities.delete(deviceId);
|
|
170
201
|
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
171
202
|
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
172
203
|
this.v1Buffers.delete(deviceId);
|
|
173
204
|
this.v2Assemblers.delete(deviceId);
|
|
174
205
|
this.resetProtocolV2Frames(deviceId);
|
|
175
206
|
this.notificationTokens.delete(deviceId);
|
|
207
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
208
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
209
|
+
this.runPromise = null;
|
|
210
|
+
this.runPromiseDeviceId = null;
|
|
211
|
+
}
|
|
176
212
|
|
|
177
213
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
178
214
|
if (notifyCleanup) {
|
|
@@ -180,10 +216,10 @@ export default class ElectronBleTransport {
|
|
|
180
216
|
this.notificationCleanups.delete(deviceId);
|
|
181
217
|
}
|
|
182
218
|
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
this.
|
|
219
|
+
const mtuCleanup = this.mtuCleanups.get(deviceId);
|
|
220
|
+
if (mtuCleanup) {
|
|
221
|
+
mtuCleanup();
|
|
222
|
+
this.mtuCleanups.delete(deviceId);
|
|
187
223
|
}
|
|
188
224
|
}
|
|
189
225
|
|
|
@@ -198,19 +234,67 @@ export default class ElectronBleTransport {
|
|
|
198
234
|
);
|
|
199
235
|
}
|
|
200
236
|
|
|
237
|
+
this.subscribeHostDisconnects();
|
|
238
|
+
|
|
201
239
|
this.Log?.debug('[Electron BLE] Transport initialized');
|
|
202
240
|
}
|
|
203
241
|
|
|
242
|
+
/**
|
|
243
|
+
* One host subscription for the whole transport lifetime. init() can run
|
|
244
|
+
* again after an SDK reset, so drop the previous listener first rather than
|
|
245
|
+
* stacking duplicates.
|
|
246
|
+
*/
|
|
247
|
+
private subscribeHostDisconnects() {
|
|
248
|
+
this.hostDisconnectCleanup?.();
|
|
249
|
+
this.hostDisconnectCleanup = window.desktopApi?.nobleBle?.onDeviceDisconnected(
|
|
250
|
+
(disconnectedDevice: { id: string; name: string; reason?: EBleDisconnectReason }) => {
|
|
251
|
+
const uuid = disconnectedDevice?.id;
|
|
252
|
+
if (!uuid) return;
|
|
253
|
+
|
|
254
|
+
// The link is gone, so renderer-side state must go with it; the next
|
|
255
|
+
// acquire reconnects from scratch.
|
|
256
|
+
this.cleanupDeviceState(uuid);
|
|
257
|
+
|
|
258
|
+
// Every link drop is reported, including the main process reclaiming
|
|
259
|
+
// an idle link on its keep-alive timer: consumers track whether a BLE
|
|
260
|
+
// link is live, not whether the peripheral is theoretically in range,
|
|
261
|
+
// and a link we closed ourselves is still a closed link. Nothing
|
|
262
|
+
// reconnects on its own, so this settles once until the user acts.
|
|
263
|
+
// `reason` is carried for diagnostics only — behaviour is uniform.
|
|
264
|
+
this.Log?.debug(
|
|
265
|
+
'[Electron BLE] Device link dropped:',
|
|
266
|
+
uuid,
|
|
267
|
+
disconnectedDevice.reason ?? EBleDisconnectReason.DeviceDisconnected
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
271
|
+
name: disconnectedDevice.name,
|
|
272
|
+
id: uuid,
|
|
273
|
+
connectId: uuid,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
204
279
|
configure(signedData: any) {
|
|
205
280
|
this._messages = parseConfigure(signedData);
|
|
206
281
|
this.configured = true;
|
|
207
282
|
}
|
|
208
283
|
|
|
209
284
|
configureProtocolV2(signedData: any) {
|
|
285
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
286
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
210
291
|
this._messagesV2 = parseConfigure(signedData);
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
292
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
293
|
+
if (isReconfiguration) {
|
|
294
|
+
this.protocolV2Links
|
|
295
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
296
|
+
.catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error));
|
|
297
|
+
}
|
|
214
298
|
}
|
|
215
299
|
|
|
216
300
|
async listen() {
|
|
@@ -226,10 +310,6 @@ export default class ElectronBleTransport {
|
|
|
226
310
|
this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
|
|
227
311
|
for (const dev of devices) {
|
|
228
312
|
this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
|
|
229
|
-
const protocolHint = inferProtocolHintFromDeviceName(dev.name);
|
|
230
|
-
if (protocolHint) {
|
|
231
|
-
this.deviceProtocolHints.set(dev.id, protocolHint);
|
|
232
|
-
}
|
|
233
313
|
}
|
|
234
314
|
return devices.map(device => toBleDescriptor(device));
|
|
235
315
|
} catch (error) {
|
|
@@ -252,8 +332,8 @@ export default class ElectronBleTransport {
|
|
|
252
332
|
if (forceCleanRunPromise && this.runPromise) {
|
|
253
333
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
254
334
|
this.runPromise.reject(error);
|
|
255
|
-
this.rejectAllProtocolV2Frames(error);
|
|
256
335
|
this.runPromise = null;
|
|
336
|
+
this.runPromiseDeviceId = null;
|
|
257
337
|
}
|
|
258
338
|
|
|
259
339
|
try {
|
|
@@ -267,7 +347,7 @@ export default class ElectronBleTransport {
|
|
|
267
347
|
}
|
|
268
348
|
const protocolHint = expectedProtocol
|
|
269
349
|
? undefined
|
|
270
|
-
: this.deviceProtocolHints.get(uuid)
|
|
350
|
+
: input.protocolHint ?? this.deviceProtocolHints.get(uuid);
|
|
271
351
|
if (protocolHint) {
|
|
272
352
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
273
353
|
}
|
|
@@ -279,35 +359,28 @@ export default class ElectronBleTransport {
|
|
|
279
359
|
this.handleBluetoothError(error);
|
|
280
360
|
}
|
|
281
361
|
|
|
362
|
+
const mtuCleanup = this.createMtuSubscription(uuid);
|
|
363
|
+
if (mtuCleanup) {
|
|
364
|
+
this.mtuCleanups.set(uuid, mtuCleanup);
|
|
365
|
+
}
|
|
366
|
+
|
|
282
367
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
283
|
-
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
368
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
|
|
284
369
|
|
|
285
370
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
371
|
+
await this.refreshBlePacketCapacity(uuid);
|
|
286
372
|
|
|
287
373
|
const cleanup = this.createNotificationSubscription(uuid);
|
|
288
374
|
this.notificationCleanups.set(uuid, cleanup);
|
|
289
375
|
|
|
290
|
-
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
291
|
-
(disconnectedDevice: any) => {
|
|
292
|
-
if (disconnectedDevice.id === uuid) {
|
|
293
|
-
this.cleanupDeviceState(uuid);
|
|
294
|
-
this.emitter?.emit('device-disconnect', {
|
|
295
|
-
name: disconnectedDevice.name,
|
|
296
|
-
id: disconnectedDevice.id,
|
|
297
|
-
connectId: disconnectedDevice.id,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
);
|
|
302
|
-
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
303
|
-
|
|
304
376
|
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
377
|
+
if (protocolType === 'V2') {
|
|
378
|
+
this.Log?.debug('[Electron BLE] Protocol V2 write configured', {
|
|
379
|
+
writeMode: 'withoutResponse',
|
|
380
|
+
negotiatedMtu: this.deviceMtus.get(uuid),
|
|
381
|
+
packetCapacity: this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
311
384
|
|
|
312
385
|
return {
|
|
313
386
|
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
@@ -328,9 +401,25 @@ export default class ElectronBleTransport {
|
|
|
328
401
|
}
|
|
329
402
|
}
|
|
330
403
|
|
|
331
|
-
async release(id: string) {
|
|
404
|
+
async release(id: string, _onclose?: boolean, keepSession?: boolean) {
|
|
332
405
|
try {
|
|
333
406
|
await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
|
|
407
|
+
await this.releaseLogical(id, keepSession);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
410
|
+
this.cleanupDeviceState(id);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// DeviceConnector.disconnect feature-detects this; without it REQUIRE_DISCONNECT
|
|
415
|
+
// recovery is a no-op and keep-alive hands the wedged link to every retry.
|
|
416
|
+
async disconnect(id: string) {
|
|
417
|
+
return this.releaseNative(id);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Hard teardown, error paths only: a link presumed dead must not be reused.
|
|
421
|
+
private async releaseNative(id: string) {
|
|
422
|
+
try {
|
|
334
423
|
if (this.connectedDevices.has(id)) {
|
|
335
424
|
if (window.desktopApi?.nobleBle) {
|
|
336
425
|
await window.desktopApi.nobleBle.unsubscribe(id);
|
|
@@ -344,9 +433,38 @@ export default class ElectronBleTransport {
|
|
|
344
433
|
}
|
|
345
434
|
}
|
|
346
435
|
|
|
347
|
-
|
|
436
|
+
// Logical release: link and subscription stay up for the next call. Renderer
|
|
437
|
+
// listeners are still torn down, or fresh ones would double-process packets.
|
|
438
|
+
private async releaseLogical(id: string, keepSession?: boolean) {
|
|
439
|
+
try {
|
|
440
|
+
if (!this.connectedDevices.has(id)) return;
|
|
441
|
+
this.cleanupDeviceState(id);
|
|
442
|
+
|
|
443
|
+
const release = window.desktopApi?.nobleBle?.release;
|
|
444
|
+
if (!release) {
|
|
445
|
+
// Degraded, not broken: say it once, it just looks like a slow device.
|
|
446
|
+
if (!this.warnedMissingRelease) {
|
|
447
|
+
this.warnedMissingRelease = true;
|
|
448
|
+
this.Log?.error(
|
|
449
|
+
'[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
|
|
450
|
+
'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload'
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await release(id, keepSession);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
this.Log?.error('[Electron BLE] logical release failed:', error);
|
|
458
|
+
this.cleanupDeviceState(id);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
|
|
463
|
+
// A generic Ping miss is not a bond failure. Only a later miss after this
|
|
464
|
+
// endpoint already answered V2, or a native encryption/pairing error, is.
|
|
465
|
+
const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
|
|
348
466
|
return ERRORS.TypedError(
|
|
349
|
-
HardwareErrorCode.RuntimeError,
|
|
467
|
+
isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
|
|
350
468
|
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
351
469
|
);
|
|
352
470
|
}
|
|
@@ -354,7 +472,7 @@ export default class ElectronBleTransport {
|
|
|
354
472
|
private createProtocolDetectionError() {
|
|
355
473
|
return ERRORS.TypedError(
|
|
356
474
|
HardwareErrorCode.BleTimeoutError,
|
|
357
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
475
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
358
476
|
);
|
|
359
477
|
}
|
|
360
478
|
|
|
@@ -375,33 +493,38 @@ export default class ElectronBleTransport {
|
|
|
375
493
|
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
|
|
376
494
|
return 'V1';
|
|
377
495
|
}
|
|
378
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
496
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
379
497
|
}
|
|
380
498
|
|
|
381
499
|
if (expectedProtocol === 'V2') {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
500
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
501
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
502
|
+
this.confirmedProtocolV2.add(uuid);
|
|
503
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
504
|
+
return 'V2';
|
|
505
|
+
}
|
|
506
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
387
507
|
}
|
|
388
508
|
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// 不能作为最终结论。
|
|
509
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
510
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
392
511
|
const probeOrder: ProtocolType[] =
|
|
393
512
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
394
513
|
|
|
395
514
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
396
515
|
const protocol = probeOrder[i];
|
|
397
516
|
if (i > 0) {
|
|
398
|
-
//
|
|
517
|
+
// Keep the physical BLE link while switching probes. Reconnecting here can
|
|
518
|
+
// summon a second OS pairing prompt for a device that is still onboarding.
|
|
399
519
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
400
520
|
}
|
|
401
521
|
const detected =
|
|
402
522
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
403
523
|
if (detected) {
|
|
404
524
|
this.deviceProtocol.set(uuid, protocol);
|
|
525
|
+
if (protocol === 'V2') {
|
|
526
|
+
this.confirmedProtocolV2.add(uuid);
|
|
527
|
+
}
|
|
405
528
|
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
406
529
|
return protocol;
|
|
407
530
|
}
|
|
@@ -435,33 +558,20 @@ export default class ElectronBleTransport {
|
|
|
435
558
|
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
436
559
|
this.v2Assemblers.get(uuid)?.reset();
|
|
437
560
|
this.resetProtocolV2Frames(uuid);
|
|
438
|
-
if (this.runPromise) {
|
|
561
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
439
562
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
440
563
|
this.runPromise.reject(error);
|
|
441
564
|
this.runPromise = null;
|
|
565
|
+
this.runPromiseDeviceId = null;
|
|
442
566
|
}
|
|
443
567
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
await window.desktopApi?.nobleBle?.unsubscribe(uuid);
|
|
453
|
-
} catch (error) {
|
|
454
|
-
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
455
|
-
}
|
|
456
|
-
try {
|
|
457
|
-
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
458
|
-
} catch (error) {
|
|
459
|
-
this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
|
|
460
|
-
throw error;
|
|
568
|
+
// A timed-out V1 probe retires its renderer notification token so a late V1
|
|
569
|
+
// response cannot satisfy the V2 probe. Install a fresh listener without
|
|
570
|
+
// touching the native GATT subscription or physical connection.
|
|
571
|
+
if (!this.notificationCleanups.has(uuid)) {
|
|
572
|
+
const cleanup = this.createNotificationSubscription(uuid);
|
|
573
|
+
this.notificationCleanups.set(uuid, cleanup);
|
|
461
574
|
}
|
|
462
|
-
|
|
463
|
-
const cleanup = this.createNotificationSubscription(uuid);
|
|
464
|
-
this.notificationCleanups.set(uuid, cleanup);
|
|
465
575
|
}
|
|
466
576
|
|
|
467
577
|
private async probeProtocolV1(uuid: string) {
|
|
@@ -471,11 +581,13 @@ export default class ElectronBleTransport {
|
|
|
471
581
|
|
|
472
582
|
try {
|
|
473
583
|
this.deviceProtocol.set(uuid, 'V1');
|
|
474
|
-
|
|
584
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
585
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
586
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
475
587
|
return true;
|
|
476
588
|
} catch (error) {
|
|
477
589
|
this.clearProbeProtocol(uuid, 'V1');
|
|
478
|
-
this.Log?.debug('[Electron BLE] Protocol V1
|
|
590
|
+
this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
|
|
479
591
|
return false;
|
|
480
592
|
}
|
|
481
593
|
}
|
|
@@ -503,35 +615,63 @@ export default class ElectronBleTransport {
|
|
|
503
615
|
return detected;
|
|
504
616
|
}
|
|
505
617
|
|
|
506
|
-
private async
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
await wait(BLE_WRITE_DELAY_MS);
|
|
511
|
-
await this.writeOnce(uuid, hexData);
|
|
512
|
-
return;
|
|
618
|
+
private async writeOnce(uuid: string, hexData: string): Promise<void> {
|
|
619
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
620
|
+
if (!nobleBle) {
|
|
621
|
+
throw new Error('Noble BLE API not available');
|
|
513
622
|
}
|
|
514
623
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
518
|
-
offset += chunkHexLen;
|
|
624
|
+
await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
|
|
625
|
+
}
|
|
519
626
|
|
|
520
|
-
|
|
627
|
+
private async refreshBlePacketCapacity(uuid: string): Promise<void> {
|
|
628
|
+
const device = await window.desktopApi?.nobleBle?.getDevice(uuid);
|
|
629
|
+
this.updateBlePacketCapacity(uuid, device?.mtu);
|
|
630
|
+
}
|
|
521
631
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
632
|
+
private updateBlePacketCapacity(uuid: string, mtu?: number): void {
|
|
633
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
634
|
+
mtu,
|
|
635
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
636
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
637
|
+
);
|
|
638
|
+
if (typeof mtu === 'number') {
|
|
639
|
+
this.deviceMtus.set(uuid, mtu);
|
|
640
|
+
} else {
|
|
641
|
+
this.deviceMtus.delete(uuid);
|
|
525
642
|
}
|
|
643
|
+
this.devicePacketCapacities.set(uuid, packetCapacity);
|
|
526
644
|
}
|
|
527
645
|
|
|
528
|
-
private
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
531
|
-
|
|
532
|
-
|
|
646
|
+
private createMtuSubscription(uuid: string): (() => void) | undefined {
|
|
647
|
+
const onMtuChanged = window.desktopApi?.nobleBle?.onMtuChanged;
|
|
648
|
+
if (!onMtuChanged) return undefined;
|
|
649
|
+
return onMtuChanged(device => {
|
|
650
|
+
if (device.id === uuid) {
|
|
651
|
+
this.updateBlePacketCapacity(uuid, device.mtu);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
533
655
|
|
|
534
|
-
|
|
656
|
+
private writeProtocolV2Frame(
|
|
657
|
+
uuid: string,
|
|
658
|
+
frame: Uint8Array,
|
|
659
|
+
context: ProtocolV2CallContext,
|
|
660
|
+
assertCurrentGeneration: () => void
|
|
661
|
+
) {
|
|
662
|
+
const packetCapacity = this.devicePacketCapacities.get(uuid) ?? BLE_PACKET_SIZE_FALLBACK;
|
|
663
|
+
const shouldPace = !context.highThroughput;
|
|
664
|
+
return writeProtocolV2BleFrame({
|
|
665
|
+
frame,
|
|
666
|
+
packetCapacity,
|
|
667
|
+
assertActive: assertCurrentGeneration,
|
|
668
|
+
signal: context.signal,
|
|
669
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
670
|
+
initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
|
|
671
|
+
burstSize: shouldPace ? 1 : undefined,
|
|
672
|
+
burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
|
|
673
|
+
writePacket: packet => this.writeOnce(uuid, bytesToHex(packet)),
|
|
674
|
+
});
|
|
535
675
|
}
|
|
536
676
|
|
|
537
677
|
private handleNotification(deviceId: string, hexData: string): void {
|
|
@@ -540,7 +680,7 @@ export default class ElectronBleTransport {
|
|
|
540
680
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
541
681
|
if (this.deviceProtocol.get(deviceId) === 'V2') {
|
|
542
682
|
this.rejectProtocolV2Frames(deviceId, error);
|
|
543
|
-
} else if (this.runPromise) {
|
|
683
|
+
} else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
544
684
|
this.runPromise.reject(error);
|
|
545
685
|
}
|
|
546
686
|
return;
|
|
@@ -595,17 +735,8 @@ export default class ElectronBleTransport {
|
|
|
595
735
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
596
736
|
}
|
|
597
737
|
|
|
598
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
599
|
-
this.v2FrameQueues.clear();
|
|
600
|
-
for (const framePromise of this.v2FramePromises.values()) {
|
|
601
|
-
framePromise.reject(error);
|
|
602
|
-
}
|
|
603
|
-
this.v2FramePromises.clear();
|
|
604
|
-
}
|
|
605
|
-
|
|
606
738
|
private resetProtocolV2Frames(uuid: string) {
|
|
607
|
-
this.
|
|
608
|
-
this.v2FramePromises.delete(uuid);
|
|
739
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
609
740
|
}
|
|
610
741
|
|
|
611
742
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -639,13 +770,18 @@ export default class ElectronBleTransport {
|
|
|
639
770
|
|
|
640
771
|
if (result.error) {
|
|
641
772
|
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
642
|
-
if (this.runPromise) {
|
|
773
|
+
if (this.runPromise && this.runPromiseDeviceId === deviceId) {
|
|
643
774
|
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
644
775
|
}
|
|
645
776
|
return;
|
|
646
777
|
}
|
|
647
778
|
|
|
648
|
-
if (
|
|
779
|
+
if (
|
|
780
|
+
result.isComplete &&
|
|
781
|
+
result.completePacket &&
|
|
782
|
+
this.runPromise &&
|
|
783
|
+
this.runPromiseDeviceId === deviceId
|
|
784
|
+
) {
|
|
649
785
|
this.runPromise.resolve(result.completePacket);
|
|
650
786
|
}
|
|
651
787
|
}
|
|
@@ -671,16 +807,25 @@ export default class ElectronBleTransport {
|
|
|
671
807
|
`Device protocol has not been detected for ${uuid}`
|
|
672
808
|
);
|
|
673
809
|
}
|
|
674
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
675
|
-
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
676
|
-
}
|
|
677
|
-
|
|
678
810
|
if (protocol === 'V2') {
|
|
679
811
|
return this.callProtocolV2(uuid, name, data, options);
|
|
680
812
|
}
|
|
681
813
|
return this.callProtocolV1(uuid, name, data, options);
|
|
682
814
|
}
|
|
683
815
|
|
|
816
|
+
async post(uuid: string, name: string, data: Record<string, unknown>) {
|
|
817
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
818
|
+
await this.protocolV2Links.sendFlowControl(
|
|
819
|
+
uuid,
|
|
820
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
821
|
+
name,
|
|
822
|
+
data
|
|
823
|
+
);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
await this.callProtocolV1(uuid, name, data);
|
|
827
|
+
}
|
|
828
|
+
|
|
684
829
|
private async callProtocolV1(
|
|
685
830
|
uuid: string,
|
|
686
831
|
name: string,
|
|
@@ -699,6 +844,7 @@ export default class ElectronBleTransport {
|
|
|
699
844
|
const runPromise = createDeferred<Uint8Array | string>();
|
|
700
845
|
runPromise.promise.catch(() => undefined);
|
|
701
846
|
this.runPromise = runPromise;
|
|
847
|
+
this.runPromiseDeviceId = uuid;
|
|
702
848
|
const messages = this._messages;
|
|
703
849
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
704
850
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -743,11 +889,24 @@ export default class ElectronBleTransport {
|
|
|
743
889
|
return check.call(jsonData);
|
|
744
890
|
} catch (e) {
|
|
745
891
|
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
892
|
+
const isProbeTimeout =
|
|
893
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
894
|
+
if ((e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError) {
|
|
895
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
896
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
897
|
+
notifyCleanup?.();
|
|
898
|
+
this.notificationCleanups.delete(uuid);
|
|
899
|
+
this.notificationTokens.delete(uuid);
|
|
900
|
+
if (!isProbeTimeout) {
|
|
901
|
+
await this.releaseNative(uuid);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
746
904
|
throw e;
|
|
747
905
|
} finally {
|
|
748
906
|
if (timeout) clearTimeout(timeout);
|
|
749
907
|
if (this.runPromise === runPromise) {
|
|
750
908
|
this.runPromise = null;
|
|
909
|
+
this.runPromiseDeviceId = null;
|
|
751
910
|
}
|
|
752
911
|
}
|
|
753
912
|
}
|
|
@@ -762,10 +921,7 @@ export default class ElectronBleTransport {
|
|
|
762
921
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
763
922
|
}
|
|
764
923
|
|
|
765
|
-
const callOptions =
|
|
766
|
-
...options,
|
|
767
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
768
|
-
};
|
|
924
|
+
const callOptions = options;
|
|
769
925
|
|
|
770
926
|
try {
|
|
771
927
|
return await this.protocolV2Links.call(
|
|
@@ -791,16 +947,15 @@ export default class ElectronBleTransport {
|
|
|
791
947
|
|
|
792
948
|
return {
|
|
793
949
|
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
950
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
794
951
|
generation,
|
|
795
952
|
prepareCall: () => {
|
|
796
953
|
assertCurrentGeneration();
|
|
797
954
|
this.v2Assemblers.get(uuid)?.reset();
|
|
798
955
|
this.resetProtocolV2Frames(uuid);
|
|
799
956
|
},
|
|
800
|
-
writeFrame: (frame: Uint8Array) =>
|
|
801
|
-
assertCurrentGeneration
|
|
802
|
-
return this.writeWithChunking(uuid, bytesToHex(frame));
|
|
803
|
-
},
|
|
957
|
+
writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
|
|
958
|
+
this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
|
|
804
959
|
readFrame: async () => {
|
|
805
960
|
assertCurrentGeneration();
|
|
806
961
|
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
@@ -844,6 +999,12 @@ export default class ElectronBleTransport {
|
|
|
844
999
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
845
1000
|
bufferState.buffer = [...data.subarray(3)];
|
|
846
1001
|
} else {
|
|
1002
|
+
if (bufferState.buffer.length === 0) {
|
|
1003
|
+
// Tail of a cancelled call, arriving after the buffer was reset. Not an
|
|
1004
|
+
// `error`: the caller rejects the in-flight call on that field.
|
|
1005
|
+
this.Log?.debug('[Electron BLE] Orphan continuation chunk discarded');
|
|
1006
|
+
return { isComplete: false };
|
|
1007
|
+
}
|
|
847
1008
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
848
1009
|
}
|
|
849
1010
|
|