@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/dist/index.js CHANGED
@@ -41,14 +41,6 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
41
41
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
42
42
  };
43
43
 
44
- const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']);
45
- function shouldSuppressHighVolumeCallLog(name) {
46
- return HIGH_VOLUME_CALLS.has(name);
47
- }
48
- function createTransportCallLog(name, protocol) {
49
- return { name, protocol };
50
- }
51
-
52
44
  const { parseConfigure: parseConfigure$1, check: check$1, ProtocolV1: ProtocolV1$1 } = transport__default["default"];
53
45
  const CONFIGURATION_ID = 1;
54
46
  const INTERFACE_ID = 0;
@@ -59,20 +51,39 @@ const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
59
51
  const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
60
52
  const PACKET_IO_MAX_RETRIES = 3;
61
53
  const PACKET_IO_RETRY_DELAY = 300;
62
- const PROTOCOL_PROBE_TIMEOUT = 1000;
63
- function inferProtocolHintFromDeviceName$1(name) {
64
- return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
54
+ const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
55
+ const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
56
+ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
57
+ let activeWebUsbTransport;
58
+ let usbDisconnectListenerAttached = false;
59
+ function registerActiveWebUsbTransport(instance, usb) {
60
+ activeWebUsbTransport = instance;
61
+ if (usbDisconnectListenerAttached)
62
+ return;
63
+ usbDisconnectListenerAttached = true;
64
+ usb.addEventListener('disconnect', event => {
65
+ if (!event.device)
66
+ return;
67
+ const path = hdShared.resolveOneKeyUsbDevicePath(event.device);
68
+ if (!path)
69
+ return;
70
+ activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.markProtocolStale(path);
71
+ activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.emitDeviceDisconnect(path, event.device);
72
+ });
65
73
  }
66
- class WebUsbTransport {
74
+ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
67
75
  constructor() {
76
+ super({
77
+ router: transport.PROTOCOL_V2_CHANNEL_USB,
78
+ maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
79
+ logPrefix: 'ProtocolV2 WebUSB',
80
+ });
68
81
  this.deviceProtocol = new Map();
69
82
  this.deviceProtocolHints = new Map();
70
- this.protocolV2Assemblers = new Map();
71
- this.protocolV2Sessions = new Map();
72
- this.protocolV2ReadTimeouts = new Map();
83
+ this.staleProtocolPaths = new Set();
84
+ this.acquiredPaths = new Set();
85
+ this.probedDeviceObjects = new Map();
73
86
  this.deviceEndpoints = new Map();
74
- this.mockSerialPaths = new WeakMap();
75
- this.mockSerialCounter = 0;
76
87
  this.name = 'WebUsbTransport';
77
88
  this.stopped = false;
78
89
  this.configured = false;
@@ -81,13 +92,27 @@ class WebUsbTransport {
81
92
  this.endpointId = ENDPOINT_ID;
82
93
  this.interfaceId = INTERFACE_ID;
83
94
  }
84
- init(logger) {
95
+ init(logger, emitter) {
85
96
  this.Log = logger;
97
+ this.emitter = emitter;
86
98
  const { usb } = navigator;
87
99
  if (!usb) {
88
100
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
89
101
  }
90
102
  this.usb = usb;
103
+ registerActiveWebUsbTransport(this, usb);
104
+ }
105
+ emitDeviceDisconnect(path, device) {
106
+ var _a, _b;
107
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
108
+ name: (_b = device === null || device === void 0 ? void 0 : device.productName) !== null && _b !== void 0 ? _b : '',
109
+ id: path,
110
+ connectId: path,
111
+ });
112
+ }
113
+ markProtocolStale(path) {
114
+ this.staleProtocolPaths.add(path);
115
+ this.probedDeviceObjects.delete(path);
91
116
  }
92
117
  configure(signedData) {
93
118
  const messages = parseConfigure$1(signedData);
@@ -95,9 +120,18 @@ class WebUsbTransport {
95
120
  this.messages = messages;
96
121
  }
97
122
  configureProtocolV2(signedData) {
123
+ var _a;
124
+ const schemaSource = typeof signedData === 'string'
125
+ ? signedData
126
+ : (_a = JSON.stringify(signedData)) !== null && _a !== void 0 ? _a : String(signedData);
127
+ if (schemaSource === this.protocolV2SchemaSource)
128
+ return;
129
+ const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
98
130
  this.messagesV2 = parseConfigure$1(signedData);
99
- this.protocolV2Sessions.clear();
100
- this.protocolV2ReadTimeouts.clear();
131
+ this.protocolV2SchemaSource = schemaSource;
132
+ if (!hadProtocolV2Schema)
133
+ return;
134
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[WebUsbTransport] schema link cleanup failed:', error); });
101
135
  }
102
136
  promptDeviceAccess() {
103
137
  return __awaiter(this, void 0, void 0, function* () {
@@ -119,20 +153,6 @@ class WebUsbTransport {
119
153
  return this.deviceList;
120
154
  });
121
155
  }
122
- getDevicePath(device) {
123
- var _a;
124
- if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
125
- return device.serialNumber;
126
- }
127
- let path = this.mockSerialPaths.get(device);
128
- if (!path) {
129
- this.mockSerialCounter += 1;
130
- path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${this.mockSerialCounter}`;
131
- this.mockSerialPaths.set(device, path);
132
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUSB] device has no serial number, using mock path: ${path}`);
133
- }
134
- return path;
135
- }
136
156
  getConnectedDevices() {
137
157
  return __awaiter(this, void 0, void 0, function* () {
138
158
  if (!this.usb)
@@ -142,41 +162,74 @@ class WebUsbTransport {
142
162
  const isOneKey = hdShared.ONEKEY_WEBUSB_FILTER.some((desc) => dev.vendorId === desc.vendorId && dev.productId === desc.productId);
143
163
  return isOneKey && !hdShared.isKnownTrezorWebUsbDevice(dev);
144
164
  });
145
- this.deviceList = onekeyDevices.map(device => {
146
- const path = this.getDevicePath(device);
147
- const protocolHint = inferProtocolHintFromDeviceName$1(device.productName);
165
+ this.deviceList = onekeyDevices.flatMap(device => {
166
+ const path = hdShared.resolveOneKeyUsbDevicePath(device);
167
+ if (!path)
168
+ return [];
169
+ const protocolHint = hdShared.inferProtocolHintFromUsbId(device.vendorId, device.productId);
148
170
  if (protocolHint) {
149
171
  this.deviceProtocolHints.set(path, protocolHint);
150
172
  }
151
- return {
152
- path,
153
- device,
154
- commType: 'webusb',
155
- };
173
+ return [
174
+ {
175
+ path,
176
+ device,
177
+ commType: 'webusb',
178
+ },
179
+ ];
156
180
  });
157
181
  return this.deviceList;
158
182
  });
159
183
  }
160
184
  acquire(input) {
161
- var _a, _b, _c;
185
+ var _a, _b, _c, _d, _e, _f;
162
186
  return __awaiter(this, void 0, void 0, function* () {
163
187
  if (!input.path)
164
188
  return;
165
189
  try {
190
+ yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
166
191
  yield this.closeOpenDevice(input.path);
167
192
  yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
168
- const deviceName = (_b = this.deviceList.find(device => device.path === input.path)) === null || _b === void 0 ? void 0 : _b.device.productName;
169
- const protocolHint = input.expectedProtocol
170
- ? undefined
171
- : (_c = this.deviceProtocolHints.get(input.path)) !== null && _c !== void 0 ? _c : inferProtocolHintFromDeviceName$1(deviceName);
172
- if (protocolHint) {
173
- this.deviceProtocolHints.set(input.path, protocolHint);
193
+ if (input.forceProtocolDetection) {
194
+ this.staleProtocolPaths.delete(input.path);
195
+ this.deviceProtocol.delete(input.path);
196
+ this.deviceProtocolHints.delete(input.path);
197
+ }
198
+ if (this.staleProtocolPaths.has(input.path)) {
199
+ this.staleProtocolPaths.delete(input.path);
200
+ this.deviceProtocol.delete(input.path);
201
+ }
202
+ if (this.deviceProtocol.has(input.path)) {
203
+ const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
204
+ if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
205
+ this.deviceProtocol.delete(input.path);
206
+ }
207
+ }
208
+ const cachedProtocol = this.deviceProtocol.get(input.path);
209
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
210
+ this.deviceProtocol.delete(input.path);
174
211
  }
175
- yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
212
+ if (!this.deviceProtocol.has(input.path)) {
213
+ const usbDevice = (_c = this.deviceList.find(device => device.path === input.path)) === null || _c === void 0 ? void 0 : _c.device;
214
+ const protocolHint = input.expectedProtocol
215
+ ? undefined
216
+ : (_e = (_d = input.protocolHint) !== null && _d !== void 0 ? _d : this.deviceProtocolHints.get(input.path)) !== null && _e !== void 0 ? _e : hdShared.inferProtocolHintFromUsbId(usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.vendorId, usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.productId);
217
+ if (protocolHint) {
218
+ this.deviceProtocolHints.set(input.path, protocolHint);
219
+ }
220
+ yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
221
+ const probedDevice = (_f = this.deviceList.find(d => d.path === input.path)) === null || _f === void 0 ? void 0 : _f.device;
222
+ if (probedDevice) {
223
+ this.probedDeviceObjects.set(input.path, probedDevice);
224
+ }
225
+ }
226
+ this.acquiredPaths.add(input.path);
176
227
  return yield Promise.resolve(input.path);
177
228
  }
178
229
  catch (e) {
179
230
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
231
+ this.acquiredPaths.delete(input.path);
232
+ yield this.closeOpenDevice(input.path);
180
233
  throw e;
181
234
  }
182
235
  });
@@ -184,29 +237,53 @@ class WebUsbTransport {
184
237
  createProtocolMismatchError(expected) {
185
238
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
186
239
  }
240
+ createProtocolProbeTimeoutError(expected, attempts) {
241
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInitializeFailed, `Protocol ${expected} probe timeout after ${attempts} attempts`);
242
+ }
187
243
  createProtocolDetectionError() {
188
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping');
244
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
189
245
  }
190
246
  detectProtocol(path, expectedProtocol, protocolHint) {
247
+ var _a;
191
248
  return __awaiter(this, void 0, void 0, function* () {
192
249
  if (expectedProtocol === 'V1') {
193
250
  if (yield this.probeProtocolV1(path)) {
194
251
  this.deviceProtocol.set(path, 'V1');
195
252
  return 'V1';
196
253
  }
254
+ yield this.closeConnectionAfterProbe(path);
197
255
  throw this.createProtocolMismatchError(expectedProtocol);
198
256
  }
199
257
  if (expectedProtocol === 'V2') {
200
- this.deviceProtocol.set(path, 'V2');
201
- return 'V2';
258
+ for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
259
+ if (yield this.probeProtocolV2(path)) {
260
+ this.deviceProtocol.set(path, 'V2');
261
+ return 'V2';
262
+ }
263
+ if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
264
+ yield this.resetConnectionAfterProbe(path);
265
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] Protocol V2 probe timed out, retrying ${attempt + 1}/${EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS}`);
266
+ }
267
+ else {
268
+ yield this.closeConnectionAfterProbe(path);
269
+ }
270
+ }
271
+ this.deviceProtocol.delete(path);
272
+ throw this.createProtocolProbeTimeoutError(expectedProtocol, EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS);
202
273
  }
203
274
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
204
- for (const protocol of probeOrder) {
275
+ for (const [index, protocol] of probeOrder.entries()) {
205
276
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
206
277
  if (detected) {
207
278
  this.deviceProtocol.set(path, protocol);
208
279
  return protocol;
209
280
  }
281
+ if (index < probeOrder.length - 1) {
282
+ yield this.resetConnectionAfterProbe(path);
283
+ }
284
+ else {
285
+ yield this.closeConnectionAfterProbe(path);
286
+ }
210
287
  }
211
288
  this.deviceProtocol.delete(path);
212
289
  throw this.createProtocolDetectionError();
@@ -273,7 +350,7 @@ class WebUsbTransport {
273
350
  };
274
351
  }
275
352
  connectToDevice(path, first) {
276
- var _a, _b;
353
+ var _a;
277
354
  return __awaiter(this, void 0, void 0, function* () {
278
355
  let device = yield this.findDevice(path);
279
356
  if (!device.opened) {
@@ -297,33 +374,30 @@ class WebUsbTransport {
297
374
  }
298
375
  const endpoints = this.discoverEndpoints(device);
299
376
  this.deviceEndpoints.set(path, endpoints);
300
- (_b = this.protocolV2Assemblers.get(path)) === null || _b === void 0 ? void 0 : _b.reset();
301
- this.protocolV2Assemblers.set(path, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_FRAME_MAX_BYTES));
302
377
  yield device.claimInterface(endpoints.interfaceNumber);
303
378
  yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
304
379
  yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
305
380
  });
306
381
  }
307
382
  closeOpenDevice(path) {
308
- var _a, _b, _c, _d, _e;
383
+ var _a, _b, _c, _d;
309
384
  return __awaiter(this, void 0, void 0, function* () {
310
- (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
311
- const current = (_b = this.deviceList.find(device => device.path === path)) === null || _b === void 0 ? void 0 : _b.device;
385
+ const current = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
312
386
  if (!(current === null || current === void 0 ? void 0 : current.opened))
313
387
  return;
314
388
  const endpoints = this.deviceEndpoints.get(path);
315
- const ifaceNum = (_c = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _c !== void 0 ? _c : this.interfaceId;
389
+ const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
316
390
  try {
317
391
  yield current.releaseInterface(ifaceNum);
318
392
  }
319
393
  catch (error) {
320
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
394
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
321
395
  }
322
396
  try {
323
397
  yield current.close();
324
398
  }
325
399
  catch (error) {
326
- (_e = this.Log) === null || _e === void 0 ? void 0 : _e.debug('[WebUsbTransport] close before reconnect failed:', error);
400
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
327
401
  }
328
402
  });
329
403
  }
@@ -338,8 +412,18 @@ class WebUsbTransport {
338
412
  }
339
413
  });
340
414
  }
415
+ assertAcquired(path) {
416
+ if (!this.acquiredPaths.has(path)) {
417
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
418
+ }
419
+ }
341
420
  post(session, name, data) {
342
421
  return __awaiter(this, void 0, void 0, function* () {
422
+ this.assertAcquired(session);
423
+ if (this.deviceProtocol.get(session) === 'V2') {
424
+ yield this.sendProtocolV2UsbFlowControl(session, name, data);
425
+ return;
426
+ }
343
427
  yield this.call(session, name, data);
344
428
  });
345
429
  }
@@ -368,6 +452,7 @@ class WebUsbTransport {
368
452
  var _a;
369
453
  return __awaiter(this, void 0, void 0, function* () {
370
454
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
455
+ this.staleProtocolPaths.add(path);
371
456
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
372
457
  try {
373
458
  const currentDevice = yield this.findDevice(path);
@@ -485,34 +570,33 @@ class WebUsbTransport {
485
570
  throw lastError;
486
571
  });
487
572
  }
488
- resetConnectionAfterProbe(path) {
489
- var _a, _b;
573
+ transferInOnce(path, length) {
574
+ var _a;
490
575
  return __awaiter(this, void 0, void 0, function* () {
491
- (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
492
- this.protocolV2Sessions.delete(path);
493
- this.protocolV2ReadTimeouts.delete(path);
494
- try {
495
- const device = yield this.findDevice(path);
496
- if (device.opened) {
497
- const endpoints = this.deviceEndpoints.get(path);
498
- const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
499
- try {
500
- yield device.releaseInterface(ifaceNum);
501
- }
502
- catch (error) {
503
- this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
504
- }
505
- yield device.close();
506
- }
507
- }
508
- catch (error) {
509
- this.Log.debug('[WebUsbTransport] close after protocol probe error:', error);
576
+ const device = yield this.findDevice(path);
577
+ if (!device.opened) {
578
+ throw new Error('USBDevice is not open for transferIn');
510
579
  }
580
+ const endpoints = this.deviceEndpoints.get(path);
581
+ const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
582
+ const result = yield device.transferIn(endpointIn, length);
583
+ return this.getTransferInData(result);
584
+ });
585
+ }
586
+ closeConnectionAfterProbe(path) {
587
+ return __awaiter(this, void 0, void 0, function* () {
588
+ yield this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
589
+ yield this.closeOpenDevice(path);
590
+ });
591
+ }
592
+ resetConnectionAfterProbe(path) {
593
+ return __awaiter(this, void 0, void 0, function* () {
594
+ yield this.closeConnectionAfterProbe(path);
511
595
  yield this.getConnectedDevices();
512
596
  yield this.connect(path, false);
513
597
  });
514
598
  }
515
- withProtocolReadTimeout(_path, promise, timeoutMs, protocol, onTimeout) {
599
+ withProtocolReadTimeout(path, promise, timeoutMs, protocol, onTimeout) {
516
600
  return __awaiter(this, void 0, void 0, function* () {
517
601
  let timer;
518
602
  let timedOut = false;
@@ -527,11 +611,19 @@ class WebUsbTransport {
527
611
  return yield Promise.race([
528
612
  guardedPromise,
529
613
  new Promise((_, reject) => {
530
- timer = setTimeout(() => {
614
+ timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
531
615
  timedOut = true;
532
616
  onTimeout === null || onTimeout === void 0 ? void 0 : onTimeout();
617
+ if (protocol === 'V1') {
618
+ try {
619
+ yield this.resetConnectionAfterProbe(path);
620
+ }
621
+ catch (error) {
622
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
623
+ }
624
+ }
533
625
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
534
- }, timeoutMs);
626
+ }), timeoutMs);
535
627
  }),
536
628
  ]);
537
629
  }
@@ -547,7 +639,9 @@ class WebUsbTransport {
547
639
  return false;
548
640
  }
549
641
  try {
550
- yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
642
+ yield this.callProtocolV1(path, 'Initialize', {}, {
643
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
644
+ });
551
645
  return true;
552
646
  }
553
647
  catch (_error) {
@@ -562,10 +656,9 @@ class WebUsbTransport {
562
656
  }
563
657
  return transport.probeProtocolV2({
564
658
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
565
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
659
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
566
660
  logger: this.Log,
567
661
  logPrefix: 'ProtocolV2 WebUSB',
568
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
569
662
  });
570
663
  });
571
664
  }
@@ -574,6 +667,7 @@ class WebUsbTransport {
574
667
  if (this.messages == null) {
575
668
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
576
669
  }
670
+ this.assertAcquired(path);
577
671
  const device = yield this.findDevice(path);
578
672
  if (!device) {
579
673
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
@@ -582,9 +676,6 @@ class WebUsbTransport {
582
676
  if (!protocol) {
583
677
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
584
678
  }
585
- if (!shouldSuppressHighVolumeCallLog(name)) {
586
- this.Log.debug('transport call', createTransportCallLog(name, protocol));
587
- }
588
679
  if (protocol === 'V2') {
589
680
  return this.callProtocolV2(path, name, data, options);
590
681
  }
@@ -613,79 +704,8 @@ class WebUsbTransport {
613
704
  });
614
705
  }
615
706
  callProtocolV2(path, name, data, options) {
616
- var _a;
617
707
  return __awaiter(this, void 0, void 0, function* () {
618
- const protocolV1Messages = this.messages;
619
- if (!this.messagesV2) {
620
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured, 'Protocol V2 schema not configured');
621
- }
622
- if (!protocolV1Messages) {
623
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
624
- }
625
- let session = this.protocolV2Sessions.get(path);
626
- if (!session) {
627
- session = new transport.ProtocolV2Session({
628
- schemas: {
629
- protocolV1: protocolV1Messages,
630
- protocolV2: this.messagesV2,
631
- },
632
- router: transport.PROTOCOL_V2_CHANNEL_USB,
633
- writeFrame: (frame) => this.transferOutOnce(path, frame),
634
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
635
- logger: this.Log,
636
- logPrefix: 'ProtocolV2 WebUSB',
637
- createTimeoutError: (messageName, timeoutMs) => new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
638
- });
639
- this.protocolV2Sessions.set(path, session);
640
- }
641
- this.protocolV2ReadTimeouts.set(path, options === null || options === void 0 ? void 0 : options.timeoutMs);
642
- (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
643
- try {
644
- return yield session.call(name, data, options);
645
- }
646
- catch (error) {
647
- const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
648
- if (message.includes('protocol v2 read timeout') || message.includes('response timeout')) {
649
- try {
650
- yield this.resetConnectionAfterProbe(path);
651
- }
652
- catch (resetError) {
653
- this.Log.debug('[WebUsbTransport] Protocol V2 timeout reset failed:', resetError);
654
- }
655
- }
656
- throw error;
657
- }
658
- finally {
659
- this.protocolV2ReadTimeouts.delete(path);
660
- }
661
- });
662
- }
663
- receiveProtocolV2Frame(path, timeoutMs) {
664
- return __awaiter(this, void 0, void 0, function* () {
665
- let assembler = this.protocolV2Assemblers.get(path);
666
- if (!assembler) {
667
- assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_FRAME_MAX_BYTES);
668
- this.protocolV2Assemblers.set(path, assembler);
669
- }
670
- let frame = assembler.push(new Uint8Array(0));
671
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
672
- while (!frame) {
673
- const cancelToken = { cancelled: false };
674
- const transferIn = this.transferInWithRetry(path, transport.PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
675
- const dataView = deadline
676
- ? yield this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V2', () => {
677
- cancelToken.cancelled = true;
678
- })
679
- : yield transferIn;
680
- const bytes = new Uint8Array(this.toArrayBuffer(dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)));
681
- try {
682
- frame = assembler.push(bytes);
683
- }
684
- catch (error) {
685
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, error instanceof Error ? error.message : String(error));
686
- }
687
- }
688
- return frame;
708
+ return this.callProtocolV2Usb(path, name, data, options);
689
709
  });
690
710
  }
691
711
  receiveData(path, timeoutMs) {
@@ -726,45 +746,81 @@ class WebUsbTransport {
726
746
  });
727
747
  }
728
748
  release(path) {
729
- var _a, _b;
730
749
  return __awaiter(this, void 0, void 0, function* () {
731
- const device = yield this.findDevice(path);
732
- const endpoints = this.deviceEndpoints.get(path);
733
- const ifaceNum = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _a !== void 0 ? _a : this.interfaceId;
734
- yield device.releaseInterface(ifaceNum);
735
- yield device.close();
736
- this.deviceProtocol.delete(path);
737
- this.deviceProtocolHints.delete(path);
738
- (_b = this.protocolV2Assemblers.get(path)) === null || _b === void 0 ? void 0 : _b.reset();
739
- this.protocolV2Assemblers.delete(path);
740
- this.protocolV2Sessions.delete(path);
741
- this.protocolV2ReadTimeouts.delete(path);
750
+ this.acquiredPaths.delete(path);
751
+ yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
752
+ yield this.closeOpenDevice(path);
742
753
  this.deviceEndpoints.delete(path);
743
754
  });
744
755
  }
756
+ getProtocolV2UsbSchemas() {
757
+ if (!this.messages || !this.messagesV2) {
758
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
759
+ }
760
+ return {
761
+ protocolV1: this.messages,
762
+ protocolV2: this.messagesV2,
763
+ };
764
+ }
765
+ getProtocolV2UsbLogger() {
766
+ return this.Log;
767
+ }
768
+ writeProtocolV2UsbPacket(path, frame, _context) {
769
+ return __awaiter(this, void 0, void 0, function* () {
770
+ yield this.transferOutOnce(path, frame);
771
+ });
772
+ }
773
+ readProtocolV2UsbPacket(path, _context) {
774
+ return __awaiter(this, void 0, void 0, function* () {
775
+ const dataView = yield this.transferInOnce(path, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
776
+ return new Uint8Array(this.toArrayBuffer(dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)));
777
+ });
778
+ }
779
+ resetProtocolV2UsbNativeLink(path, _reason) {
780
+ return __awaiter(this, void 0, void 0, function* () {
781
+ yield this.closeOpenDevice(path);
782
+ });
783
+ }
784
+ onProtocolV2UsbLinkInvalidated(path, reason) {
785
+ var _a;
786
+ this.deviceProtocol.delete(path);
787
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
788
+ }
789
+ createProtocolV2UsbTimeoutError(name, timeoutMs) {
790
+ return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
791
+ }
745
792
  getProtocolType(path) {
746
793
  return this.deviceProtocol.get(path);
747
794
  }
748
795
  }
749
796
 
750
- const { parseConfigure, ProtocolV1, check } = transport__default["default"];
751
- function inferProtocolHintFromDeviceName(name) {
752
- return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
797
+ const BLE_ATT_HEADER_BYTES = 3;
798
+ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
799
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
800
+ return fallbackPacketCapacity;
801
+ }
802
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
753
803
  }
804
+
805
+ const { parseConfigure, ProtocolV1, check } = transport__default["default"];
754
806
  const toBleDescriptor = (device, protocolType) => (Object.assign({ id: device.id, name: device.name, path: device.id, debug: false, commType: 'electron-ble' }, (protocolType ? { protocolType } : {})));
755
- const BLE_PACKET_SIZE = 192;
807
+ const BLE_PACKET_SIZE_FALLBACK = 192;
808
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
756
809
  const BLE_WRITE_DELAY_MS = 5;
757
- const BLE_RESPONSE_TIMEOUT_MS = 30000;
758
- const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
810
+ const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
759
811
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
760
812
  class ElectronBleTransport {
761
813
  constructor() {
762
814
  this.name = 'ElectronBleTransport';
763
815
  this.configured = false;
764
816
  this.runPromise = null;
817
+ this.runPromiseDeviceId = null;
765
818
  this.connectedDevices = new Set();
766
819
  this.deviceProtocol = new Map();
767
820
  this.deviceProtocolHints = new Map();
821
+ this.confirmedProtocolV2 = new Set();
822
+ this.deviceMtus = new Map();
823
+ this.devicePacketCapacities = new Map();
768
824
  this.v1Buffers = new Map();
769
825
  this.v2Assemblers = new Map();
770
826
  this.v2FrameQueues = new Map();
@@ -786,12 +842,13 @@ class ElectronBleTransport {
786
842
  this.rejectProtocolV2Frames(uuid, new Error(reason));
787
843
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
788
844
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
789
- yield this.release(uuid);
845
+ yield this.releaseNative(uuid);
790
846
  }
791
847
  }),
792
848
  });
849
+ this.warnedMissingRelease = false;
793
850
  this.notificationCleanups = new Map();
794
- this.disconnectCleanups = new Map();
851
+ this.mtuCleanups = new Map();
795
852
  this.notificationTokens = new Map();
796
853
  this.nextNotificationToken = 1;
797
854
  }
@@ -830,19 +887,26 @@ class ElectronBleTransport {
830
887
  .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] link cleanup failed:', error); });
831
888
  this.connectedDevices.delete(deviceId);
832
889
  this.deviceProtocol.delete(deviceId);
890
+ this.deviceMtus.delete(deviceId);
891
+ this.devicePacketCapacities.delete(deviceId);
833
892
  this.v1Buffers.delete(deviceId);
834
893
  this.v2Assemblers.delete(deviceId);
835
894
  this.resetProtocolV2Frames(deviceId);
836
895
  this.notificationTokens.delete(deviceId);
896
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
897
+ this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
898
+ this.runPromise = null;
899
+ this.runPromiseDeviceId = null;
900
+ }
837
901
  const notifyCleanup = this.notificationCleanups.get(deviceId);
838
902
  if (notifyCleanup) {
839
903
  notifyCleanup();
840
904
  this.notificationCleanups.delete(deviceId);
841
905
  }
842
- const disconnectCleanup = this.disconnectCleanups.get(deviceId);
843
- if (disconnectCleanup) {
844
- disconnectCleanup();
845
- this.disconnectCleanups.delete(deviceId);
906
+ const mtuCleanup = this.mtuCleanups.get(deviceId);
907
+ if (mtuCleanup) {
908
+ mtuCleanup();
909
+ this.mtuCleanups.delete(deviceId);
846
910
  }
847
911
  }
848
912
  init(logger, emitter) {
@@ -852,17 +916,43 @@ class ElectronBleTransport {
852
916
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
853
917
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble BLE API is not available. Please ensure you are running in Electron with Noble support.');
854
918
  }
919
+ this.subscribeHostDisconnects();
855
920
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Transport initialized');
856
921
  }
922
+ subscribeHostDisconnects() {
923
+ var _a, _b, _c;
924
+ (_a = this.hostDisconnectCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
925
+ this.hostDisconnectCleanup = (_c = (_b = window.desktopApi) === null || _b === void 0 ? void 0 : _b.nobleBle) === null || _c === void 0 ? void 0 : _c.onDeviceDisconnected((disconnectedDevice) => {
926
+ var _a, _b, _c;
927
+ const uuid = disconnectedDevice === null || disconnectedDevice === void 0 ? void 0 : disconnectedDevice.id;
928
+ if (!uuid)
929
+ return;
930
+ this.cleanupDeviceState(uuid);
931
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Device link dropped:', uuid, (_b = disconnectedDevice.reason) !== null && _b !== void 0 ? _b : hdShared.EBleDisconnectReason.DeviceDisconnected);
932
+ (_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
933
+ name: disconnectedDevice.name,
934
+ id: uuid,
935
+ connectId: uuid,
936
+ });
937
+ });
938
+ }
857
939
  configure(signedData) {
858
940
  this._messages = parseConfigure(signedData);
859
941
  this.configured = true;
860
942
  }
861
943
  configureProtocolV2(signedData) {
944
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
945
+ if (this.protocolV2SchemaConfiguration === configuration) {
946
+ return;
947
+ }
948
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
862
949
  this._messagesV2 = parseConfigure(signedData);
863
- this.protocolV2Links
864
- .invalidateAllLinks('Protocol V2 schema reconfigured')
865
- .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] schema link cleanup failed:', error); });
950
+ this.protocolV2SchemaConfiguration = configuration;
951
+ if (isReconfiguration) {
952
+ this.protocolV2Links
953
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
954
+ .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] schema link cleanup failed:', error); });
955
+ }
866
956
  }
867
957
  listen() {
868
958
  return __awaiter(this, void 0, void 0, function* () {
@@ -880,10 +970,6 @@ class ElectronBleTransport {
880
970
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
881
971
  for (const dev of devices) {
882
972
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
883
- const protocolHint = inferProtocolHintFromDeviceName(dev.name);
884
- if (protocolHint) {
885
- this.deviceProtocolHints.set(dev.id, protocolHint);
886
- }
887
973
  }
888
974
  return devices.map(device => toBleDescriptor(device));
889
975
  }
@@ -894,7 +980,7 @@ class ElectronBleTransport {
894
980
  });
895
981
  }
896
982
  acquire(input) {
897
- var _a, _b, _c, _d, _e, _f;
983
+ var _a, _b, _c, _d, _e, _f, _g;
898
984
  return __awaiter(this, void 0, void 0, function* () {
899
985
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
900
986
  if (!uuid) {
@@ -906,8 +992,8 @@ class ElectronBleTransport {
906
992
  if (forceCleanRunPromise && this.runPromise) {
907
993
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
908
994
  this.runPromise.reject(error);
909
- this.rejectAllProtocolV2Frames(error);
910
995
  this.runPromise = null;
996
+ this.runPromiseDeviceId = null;
911
997
  }
912
998
  try {
913
999
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
@@ -919,7 +1005,7 @@ class ElectronBleTransport {
919
1005
  }
920
1006
  const protocolHint = expectedProtocol
921
1007
  ? undefined
922
- : (_b = this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(device.name);
1008
+ : (_b = input.protocolHint) !== null && _b !== void 0 ? _b : this.deviceProtocolHints.get(uuid);
923
1009
  if (protocolHint) {
924
1010
  this.deviceProtocolHints.set(uuid, protocolHint);
925
1011
  }
@@ -930,52 +1016,64 @@ class ElectronBleTransport {
930
1016
  catch (error) {
931
1017
  this.handleBluetoothError(error);
932
1018
  }
1019
+ const mtuCleanup = this.createMtuSubscription(uuid);
1020
+ if (mtuCleanup) {
1021
+ this.mtuCleanups.set(uuid, mtuCleanup);
1022
+ }
933
1023
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
934
- this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler());
1024
+ this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
935
1025
  yield window.desktopApi.nobleBle.subscribe(uuid);
1026
+ yield this.refreshBlePacketCapacity(uuid);
936
1027
  const cleanup = this.createNotificationSubscription(uuid);
937
1028
  this.notificationCleanups.set(uuid, cleanup);
938
- const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected((disconnectedDevice) => {
939
- var _a;
940
- if (disconnectedDevice.id === uuid) {
941
- this.cleanupDeviceState(uuid);
942
- (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('device-disconnect', {
943
- name: disconnectedDevice.name,
944
- id: disconnectedDevice.id,
945
- connectId: disconnectedDevice.id,
946
- });
947
- }
948
- });
949
- this.disconnectCleanups.set(uuid, disconnectCleanup);
950
1029
  const protocolType = yield this.detectProtocol(uuid, expectedProtocol, protocolHint);
951
- (_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit('device-connect', {
952
- name: device.name,
953
- id: device.id,
954
- connectId: device.id,
955
- });
1030
+ if (protocolType === 'V2') {
1031
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[Electron BLE] Protocol V2 write configured', {
1032
+ writeMode: 'withoutResponse',
1033
+ negotiatedMtu: this.deviceMtus.get(uuid),
1034
+ packetCapacity: (_d = this.devicePacketCapacities.get(uuid)) !== null && _d !== void 0 ? _d : BLE_PACKET_SIZE_FALLBACK,
1035
+ });
1036
+ }
956
1037
  return Object.assign(Object.assign({}, toBleDescriptor({ id: device.id, name: device.name }, protocolType)), { uuid });
957
1038
  }
958
1039
  catch (error) {
959
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] acquire failed:', error);
1040
+ (_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
960
1041
  try {
961
- if (((_e = window.desktopApi) === null || _e === void 0 ? void 0 : _e.nobleBle) && this.connectedDevices.has(uuid)) {
1042
+ if (((_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle) && this.connectedDevices.has(uuid)) {
962
1043
  yield window.desktopApi.nobleBle.unsubscribe(uuid);
963
1044
  yield window.desktopApi.nobleBle.disconnect(uuid);
964
1045
  }
965
1046
  }
966
1047
  catch (cleanupError) {
967
- (_f = this.Log) === null || _f === void 0 ? void 0 : _f.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
1048
+ (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
968
1049
  }
969
1050
  this.cleanupDeviceState(uuid);
970
1051
  throw error;
971
1052
  }
972
1053
  });
973
1054
  }
974
- release(id) {
975
- var _a, _b;
1055
+ release(id, _onclose, keepSession) {
1056
+ var _a;
976
1057
  return __awaiter(this, void 0, void 0, function* () {
977
1058
  try {
978
1059
  yield this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
1060
+ yield this.releaseLogical(id, keepSession);
1061
+ }
1062
+ catch (error) {
1063
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] release failed:', error);
1064
+ this.cleanupDeviceState(id);
1065
+ }
1066
+ });
1067
+ }
1068
+ disconnect(id) {
1069
+ return __awaiter(this, void 0, void 0, function* () {
1070
+ return this.releaseNative(id);
1071
+ });
1072
+ }
1073
+ releaseNative(id) {
1074
+ var _a, _b;
1075
+ return __awaiter(this, void 0, void 0, function* () {
1076
+ try {
979
1077
  if (this.connectedDevices.has(id)) {
980
1078
  if ((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) {
981
1079
  yield window.desktopApi.nobleBle.unsubscribe(id);
@@ -990,11 +1088,36 @@ class ElectronBleTransport {
990
1088
  }
991
1089
  });
992
1090
  }
993
- createProtocolMismatchError(expected) {
994
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1091
+ releaseLogical(id, keepSession) {
1092
+ var _a, _b, _c, _d;
1093
+ return __awaiter(this, void 0, void 0, function* () {
1094
+ try {
1095
+ if (!this.connectedDevices.has(id))
1096
+ return;
1097
+ this.cleanupDeviceState(id);
1098
+ const release = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.release;
1099
+ if (!release) {
1100
+ if (!this.warnedMissingRelease) {
1101
+ this.warnedMissingRelease = true;
1102
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.error('[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
1103
+ 'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload');
1104
+ }
1105
+ return;
1106
+ }
1107
+ yield release(id, keepSession);
1108
+ }
1109
+ catch (error) {
1110
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
1111
+ this.cleanupDeviceState(id);
1112
+ }
1113
+ });
1114
+ }
1115
+ createProtocolMismatchError(expected, uuid) {
1116
+ const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1117
+ return hdShared.ERRORS.TypedError(isStaleV2Bond ? hdShared.HardwareErrorCode.BleDeviceBondError : hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
995
1118
  }
996
1119
  createProtocolDetectionError() {
997
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping');
1120
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
998
1121
  }
999
1122
  clearProbeProtocol(uuid, protocol) {
1000
1123
  if (this.deviceProtocol.get(uuid) === protocol) {
@@ -1010,12 +1133,16 @@ class ElectronBleTransport {
1010
1133
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1011
1134
  return 'V1';
1012
1135
  }
1013
- throw this.createProtocolMismatchError(expectedProtocol);
1136
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1014
1137
  }
1015
1138
  if (expectedProtocol === 'V2') {
1016
- this.deviceProtocol.set(uuid, 'V2');
1017
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1018
- return 'V2';
1139
+ if (yield this.probeProtocolV2(uuid)) {
1140
+ this.deviceProtocol.set(uuid, 'V2');
1141
+ this.confirmedProtocolV2.add(uuid);
1142
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1143
+ return 'V2';
1144
+ }
1145
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1019
1146
  }
1020
1147
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1021
1148
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1026,6 +1153,9 @@ class ElectronBleTransport {
1026
1153
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1027
1154
  if (detected) {
1028
1155
  this.deviceProtocol.set(uuid, protocol);
1156
+ if (protocol === 'V2') {
1157
+ this.confirmedProtocolV2.add(uuid);
1158
+ }
1029
1159
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1030
1160
  return protocol;
1031
1161
  }
@@ -1049,38 +1179,22 @@ class ElectronBleTransport {
1049
1179
  });
1050
1180
  }
1051
1181
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
1052
- var _a, _b, _c, _d, _e, _f, _g;
1182
+ var _a;
1053
1183
  return __awaiter(this, void 0, void 0, function* () {
1054
1184
  yield this.protocolV2Links.invalidateLink(uuid, `Reset notify state after Protocol ${protocol} probe`);
1055
1185
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1056
1186
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1057
1187
  this.resetProtocolV2Frames(uuid);
1058
- if (this.runPromise) {
1188
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
1059
1189
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
1060
1190
  this.runPromise.reject(error);
1061
1191
  this.runPromise = null;
1192
+ this.runPromiseDeviceId = null;
1062
1193
  }
1063
- const notifyCleanup = this.notificationCleanups.get(uuid);
1064
- if (notifyCleanup) {
1065
- notifyCleanup();
1066
- this.notificationCleanups.delete(uuid);
1067
- }
1068
- this.notificationTokens.delete(uuid);
1069
- try {
1070
- yield ((_c = (_b = window.desktopApi) === null || _b === void 0 ? void 0 : _b.nobleBle) === null || _c === void 0 ? void 0 : _c.unsubscribe(uuid));
1071
- }
1072
- catch (error) {
1073
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
1074
- }
1075
- try {
1076
- yield ((_f = (_e = window.desktopApi) === null || _e === void 0 ? void 0 : _e.nobleBle) === null || _f === void 0 ? void 0 : _f.subscribe(uuid));
1077
- }
1078
- catch (error) {
1079
- (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
1080
- throw error;
1194
+ if (!this.notificationCleanups.has(uuid)) {
1195
+ const cleanup = this.createNotificationSubscription(uuid);
1196
+ this.notificationCleanups.set(uuid, cleanup);
1081
1197
  }
1082
- const cleanup = this.createNotificationSubscription(uuid);
1083
- this.notificationCleanups.set(uuid, cleanup);
1084
1198
  });
1085
1199
  }
1086
1200
  probeProtocolV1(uuid) {
@@ -1091,12 +1205,12 @@ class ElectronBleTransport {
1091
1205
  }
1092
1206
  try {
1093
1207
  this.deviceProtocol.set(uuid, 'V1');
1094
- yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1208
+ yield this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1095
1209
  return true;
1096
1210
  }
1097
1211
  catch (error) {
1098
1212
  this.clearProbeProtocol(uuid, 'V1');
1099
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
1213
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
1100
1214
  return false;
1101
1215
  }
1102
1216
  });
@@ -1126,25 +1240,6 @@ class ElectronBleTransport {
1126
1240
  return detected;
1127
1241
  });
1128
1242
  }
1129
- writeWithChunking(uuid, hexData) {
1130
- return __awaiter(this, void 0, void 0, function* () {
1131
- const totalBytes = hexData.length / 2;
1132
- if (totalBytes <= BLE_PACKET_SIZE) {
1133
- yield hdShared.wait(BLE_WRITE_DELAY_MS);
1134
- yield this.writeOnce(uuid, hexData);
1135
- return;
1136
- }
1137
- for (let offset = 0; offset < hexData.length;) {
1138
- const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset);
1139
- const chunkHex = hexData.substring(offset, offset + chunkHexLen);
1140
- offset += chunkHexLen;
1141
- yield this.writeOnce(uuid, chunkHex);
1142
- if (offset < hexData.length) {
1143
- yield hdShared.wait(BLE_WRITE_DELAY_MS);
1144
- }
1145
- }
1146
- });
1147
- }
1148
1243
  writeOnce(uuid, hexData) {
1149
1244
  var _a;
1150
1245
  return __awaiter(this, void 0, void 0, function* () {
@@ -1152,7 +1247,51 @@ class ElectronBleTransport {
1152
1247
  if (!nobleBle) {
1153
1248
  throw new Error('Noble BLE API not available');
1154
1249
  }
1155
- yield nobleBle.write(uuid, hexData);
1250
+ yield nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
1251
+ });
1252
+ }
1253
+ refreshBlePacketCapacity(uuid) {
1254
+ var _a, _b;
1255
+ return __awaiter(this, void 0, void 0, function* () {
1256
+ const device = yield ((_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.getDevice(uuid));
1257
+ this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
1258
+ });
1259
+ }
1260
+ updateBlePacketCapacity(uuid, mtu) {
1261
+ const packetCapacity = resolveBlePacketCapacity(mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
1262
+ if (typeof mtu === 'number') {
1263
+ this.deviceMtus.set(uuid, mtu);
1264
+ }
1265
+ else {
1266
+ this.deviceMtus.delete(uuid);
1267
+ }
1268
+ this.devicePacketCapacities.set(uuid, packetCapacity);
1269
+ }
1270
+ createMtuSubscription(uuid) {
1271
+ var _a, _b;
1272
+ const onMtuChanged = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.onMtuChanged;
1273
+ if (!onMtuChanged)
1274
+ return undefined;
1275
+ return onMtuChanged(device => {
1276
+ if (device.id === uuid) {
1277
+ this.updateBlePacketCapacity(uuid, device.mtu);
1278
+ }
1279
+ });
1280
+ }
1281
+ writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration) {
1282
+ var _a;
1283
+ const packetCapacity = (_a = this.devicePacketCapacities.get(uuid)) !== null && _a !== void 0 ? _a : BLE_PACKET_SIZE_FALLBACK;
1284
+ const shouldPace = !context.highThroughput;
1285
+ return transport.writeProtocolV2BleFrame({
1286
+ frame,
1287
+ packetCapacity,
1288
+ assertActive: assertCurrentGeneration,
1289
+ signal: context.signal,
1290
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1291
+ initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
1292
+ burstSize: shouldPace ? 1 : undefined,
1293
+ burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
1294
+ writePacket: packet => this.writeOnce(uuid, transport.bytesToHex(packet)),
1156
1295
  });
1157
1296
  }
1158
1297
  handleNotification(deviceId, hexData) {
@@ -1163,7 +1302,7 @@ class ElectronBleTransport {
1163
1302
  if (this.deviceProtocol.get(deviceId) === 'V2') {
1164
1303
  this.rejectProtocolV2Frames(deviceId, error);
1165
1304
  }
1166
- else if (this.runPromise) {
1305
+ else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1167
1306
  this.runPromise.reject(error);
1168
1307
  }
1169
1308
  return;
@@ -1215,16 +1354,8 @@ class ElectronBleTransport {
1215
1354
  }
1216
1355
  this.getProtocolV2FrameQueue(uuid).push(frame);
1217
1356
  }
1218
- rejectAllProtocolV2Frames(error) {
1219
- this.v2FrameQueues.clear();
1220
- for (const framePromise of this.v2FramePromises.values()) {
1221
- framePromise.reject(error);
1222
- }
1223
- this.v2FramePromises.clear();
1224
- }
1225
1357
  resetProtocolV2Frames(uuid) {
1226
- this.v2FrameQueues.delete(uuid);
1227
- this.v2FramePromises.delete(uuid);
1358
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1228
1359
  }
1229
1360
  rejectProtocolV2Frames(uuid, error) {
1230
1361
  this.v2FrameQueues.delete(uuid);
@@ -1257,17 +1388,19 @@ class ElectronBleTransport {
1257
1388
  const result = this.processProtocolV1Notification(deviceId, hexData);
1258
1389
  if (result.error) {
1259
1390
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
1260
- if (this.runPromise) {
1391
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1261
1392
  this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError));
1262
1393
  }
1263
1394
  return;
1264
1395
  }
1265
- if (result.isComplete && result.completePacket && this.runPromise) {
1396
+ if (result.isComplete &&
1397
+ result.completePacket &&
1398
+ this.runPromise &&
1399
+ this.runPromiseDeviceId === deviceId) {
1266
1400
  this.runPromise.resolve(result.completePacket);
1267
1401
  }
1268
1402
  }
1269
1403
  call(uuid, name, data, options) {
1270
- var _a;
1271
1404
  return __awaiter(this, void 0, void 0, function* () {
1272
1405
  if (!this._messages) {
1273
1406
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
@@ -1279,15 +1412,21 @@ class ElectronBleTransport {
1279
1412
  if (!protocol) {
1280
1413
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
1281
1414
  }
1282
- if (!shouldSuppressHighVolumeCallLog(name)) {
1283
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', createTransportCallLog(name, protocol));
1284
- }
1285
1415
  if (protocol === 'V2') {
1286
1416
  return this.callProtocolV2(uuid, name, data, options);
1287
1417
  }
1288
1418
  return this.callProtocolV1(uuid, name, data, options);
1289
1419
  });
1290
1420
  }
1421
+ post(uuid, name, data) {
1422
+ return __awaiter(this, void 0, void 0, function* () {
1423
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1424
+ yield this.protocolV2Links.sendFlowControl(uuid, () => this.createProtocolV2Adapter(uuid), name, data);
1425
+ return;
1426
+ }
1427
+ yield this.callProtocolV1(uuid, name, data);
1428
+ });
1429
+ }
1291
1430
  callProtocolV1(uuid, name, data, options) {
1292
1431
  var _a, _b;
1293
1432
  return __awaiter(this, void 0, void 0, function* () {
@@ -1301,6 +1440,7 @@ class ElectronBleTransport {
1301
1440
  const runPromise = hdShared.createDeferred();
1302
1441
  runPromise.promise.catch(() => undefined);
1303
1442
  this.runPromise = runPromise;
1443
+ this.runPromiseDeviceId = uuid;
1304
1444
  const messages = this._messages;
1305
1445
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1306
1446
  let timeout;
@@ -1339,6 +1479,17 @@ class ElectronBleTransport {
1339
1479
  }
1340
1480
  catch (e) {
1341
1481
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V1 call error:', e);
1482
+ const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1483
+ if ((e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError) {
1484
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1485
+ const notifyCleanup = this.notificationCleanups.get(uuid);
1486
+ notifyCleanup === null || notifyCleanup === void 0 ? void 0 : notifyCleanup();
1487
+ this.notificationCleanups.delete(uuid);
1488
+ this.notificationTokens.delete(uuid);
1489
+ if (!isProbeTimeout) {
1490
+ yield this.releaseNative(uuid);
1491
+ }
1492
+ }
1342
1493
  throw e;
1343
1494
  }
1344
1495
  finally {
@@ -1346,22 +1497,23 @@ class ElectronBleTransport {
1346
1497
  clearTimeout(timeout);
1347
1498
  if (this.runPromise === runPromise) {
1348
1499
  this.runPromise = null;
1500
+ this.runPromiseDeviceId = null;
1349
1501
  }
1350
1502
  }
1351
1503
  });
1352
1504
  }
1353
1505
  callProtocolV2(uuid, name, data, options) {
1354
- var _a, _b;
1506
+ var _a;
1355
1507
  return __awaiter(this, void 0, void 0, function* () {
1356
1508
  if (!this._messages || !this._messagesV2) {
1357
1509
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
1358
1510
  }
1359
- const callOptions = Object.assign(Object.assign({}, options), { timeoutMs: (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : BLE_RESPONSE_TIMEOUT_MS });
1511
+ const callOptions = options;
1360
1512
  try {
1361
1513
  return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, callOptions);
1362
1514
  }
1363
1515
  catch (e) {
1364
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V2 call error:', e);
1516
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V2 call error:', e);
1365
1517
  throw e;
1366
1518
  }
1367
1519
  });
@@ -1376,6 +1528,7 @@ class ElectronBleTransport {
1376
1528
  };
1377
1529
  return {
1378
1530
  router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
1531
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1379
1532
  generation,
1380
1533
  prepareCall: () => {
1381
1534
  var _a;
@@ -1383,10 +1536,7 @@ class ElectronBleTransport {
1383
1536
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1384
1537
  this.resetProtocolV2Frames(uuid);
1385
1538
  },
1386
- writeFrame: (frame) => {
1387
- assertCurrentGeneration();
1388
- return this.writeWithChunking(uuid, transport.bytesToHex(frame));
1389
- },
1539
+ writeFrame: (frame, context) => this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
1390
1540
  readFrame: () => __awaiter(this, void 0, void 0, function* () {
1391
1541
  assertCurrentGeneration();
1392
1542
  const rxFrame = yield this.readProtocolV2Frame(uuid);
@@ -1406,6 +1556,7 @@ class ElectronBleTransport {
1406
1556
  };
1407
1557
  }
1408
1558
  processProtocolV1Notification(deviceId, hexData) {
1559
+ var _a;
1409
1560
  try {
1410
1561
  if (typeof hexData !== 'string') {
1411
1562
  return { isComplete: false, error: 'Invalid hexData type' };
@@ -1424,6 +1575,10 @@ class ElectronBleTransport {
1424
1575
  bufferState.buffer = [...data.subarray(3)];
1425
1576
  }
1426
1577
  else {
1578
+ if (bufferState.buffer.length === 0) {
1579
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Orphan continuation chunk discarded');
1580
+ return { isComplete: false };
1581
+ }
1427
1582
  bufferState.buffer = bufferState.buffer.concat([...data]);
1428
1583
  }
1429
1584
  if (bufferState.buffer.length - transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) {