@onekeyfe/hd-transport-web-device 1.2.0-alpha.15 → 1.2.0-alpha.151

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,38 @@ 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
+ });
65
72
  }
66
- class WebUsbTransport {
73
+ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
67
74
  constructor() {
75
+ super({
76
+ router: transport.PROTOCOL_V2_CHANNEL_USB,
77
+ maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
78
+ logPrefix: 'ProtocolV2 WebUSB',
79
+ });
68
80
  this.deviceProtocol = new Map();
69
81
  this.deviceProtocolHints = new Map();
70
- this.protocolV2Assemblers = new Map();
71
- this.protocolV2Sessions = new Map();
72
- this.protocolV2ReadTimeouts = new Map();
82
+ this.staleProtocolPaths = new Set();
83
+ this.acquiredPaths = new Set();
84
+ this.probedDeviceObjects = new Map();
73
85
  this.deviceEndpoints = new Map();
74
- this.mockSerialPaths = new WeakMap();
75
- this.mockSerialCounter = 0;
76
86
  this.name = 'WebUsbTransport';
77
87
  this.stopped = false;
78
88
  this.configured = false;
@@ -88,6 +98,11 @@ class WebUsbTransport {
88
98
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
89
99
  }
90
100
  this.usb = usb;
101
+ registerActiveWebUsbTransport(this, usb);
102
+ }
103
+ markProtocolStale(path) {
104
+ this.staleProtocolPaths.add(path);
105
+ this.probedDeviceObjects.delete(path);
91
106
  }
92
107
  configure(signedData) {
93
108
  const messages = parseConfigure$1(signedData);
@@ -95,9 +110,18 @@ class WebUsbTransport {
95
110
  this.messages = messages;
96
111
  }
97
112
  configureProtocolV2(signedData) {
113
+ var _a;
114
+ const schemaSource = typeof signedData === 'string'
115
+ ? signedData
116
+ : (_a = JSON.stringify(signedData)) !== null && _a !== void 0 ? _a : String(signedData);
117
+ if (schemaSource === this.protocolV2SchemaSource)
118
+ return;
119
+ const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
98
120
  this.messagesV2 = parseConfigure$1(signedData);
99
- this.protocolV2Sessions.clear();
100
- this.protocolV2ReadTimeouts.clear();
121
+ this.protocolV2SchemaSource = schemaSource;
122
+ if (!hadProtocolV2Schema)
123
+ return;
124
+ 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
125
  }
102
126
  promptDeviceAccess() {
103
127
  return __awaiter(this, void 0, void 0, function* () {
@@ -119,20 +143,6 @@ class WebUsbTransport {
119
143
  return this.deviceList;
120
144
  });
121
145
  }
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
146
  getConnectedDevices() {
137
147
  return __awaiter(this, void 0, void 0, function* () {
138
148
  if (!this.usb)
@@ -142,41 +152,74 @@ class WebUsbTransport {
142
152
  const isOneKey = hdShared.ONEKEY_WEBUSB_FILTER.some((desc) => dev.vendorId === desc.vendorId && dev.productId === desc.productId);
143
153
  return isOneKey && !hdShared.isKnownTrezorWebUsbDevice(dev);
144
154
  });
145
- this.deviceList = onekeyDevices.map(device => {
146
- const path = this.getDevicePath(device);
147
- const protocolHint = inferProtocolHintFromDeviceName$1(device.productName);
155
+ this.deviceList = onekeyDevices.flatMap(device => {
156
+ const path = hdShared.resolveOneKeyUsbDevicePath(device);
157
+ if (!path)
158
+ return [];
159
+ const protocolHint = hdShared.inferProtocolHintFromUsbId(device.vendorId, device.productId);
148
160
  if (protocolHint) {
149
161
  this.deviceProtocolHints.set(path, protocolHint);
150
162
  }
151
- return {
152
- path,
153
- device,
154
- commType: 'webusb',
155
- };
163
+ return [
164
+ {
165
+ path,
166
+ device,
167
+ commType: 'webusb',
168
+ },
169
+ ];
156
170
  });
157
171
  return this.deviceList;
158
172
  });
159
173
  }
160
174
  acquire(input) {
161
- var _a, _b, _c;
175
+ var _a, _b, _c, _d, _e, _f;
162
176
  return __awaiter(this, void 0, void 0, function* () {
163
177
  if (!input.path)
164
178
  return;
165
179
  try {
180
+ yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
166
181
  yield this.closeOpenDevice(input.path);
167
182
  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);
183
+ if (input.forceProtocolDetection) {
184
+ this.staleProtocolPaths.delete(input.path);
185
+ this.deviceProtocol.delete(input.path);
186
+ this.deviceProtocolHints.delete(input.path);
187
+ }
188
+ if (this.staleProtocolPaths.has(input.path)) {
189
+ this.staleProtocolPaths.delete(input.path);
190
+ this.deviceProtocol.delete(input.path);
191
+ }
192
+ if (this.deviceProtocol.has(input.path)) {
193
+ const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
194
+ if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
195
+ this.deviceProtocol.delete(input.path);
196
+ }
197
+ }
198
+ const cachedProtocol = this.deviceProtocol.get(input.path);
199
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
200
+ this.deviceProtocol.delete(input.path);
201
+ }
202
+ if (!this.deviceProtocol.has(input.path)) {
203
+ const usbDevice = (_c = this.deviceList.find(device => device.path === input.path)) === null || _c === void 0 ? void 0 : _c.device;
204
+ const protocolHint = input.expectedProtocol
205
+ ? undefined
206
+ : (_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);
207
+ if (protocolHint) {
208
+ this.deviceProtocolHints.set(input.path, protocolHint);
209
+ }
210
+ yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
211
+ const probedDevice = (_f = this.deviceList.find(d => d.path === input.path)) === null || _f === void 0 ? void 0 : _f.device;
212
+ if (probedDevice) {
213
+ this.probedDeviceObjects.set(input.path, probedDevice);
214
+ }
174
215
  }
175
- yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
216
+ this.acquiredPaths.add(input.path);
176
217
  return yield Promise.resolve(input.path);
177
218
  }
178
219
  catch (e) {
179
220
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
221
+ this.acquiredPaths.delete(input.path);
222
+ yield this.closeOpenDevice(input.path);
180
223
  throw e;
181
224
  }
182
225
  });
@@ -184,29 +227,53 @@ class WebUsbTransport {
184
227
  createProtocolMismatchError(expected) {
185
228
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
186
229
  }
230
+ createProtocolProbeTimeoutError(expected, attempts) {
231
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInitializeFailed, `Protocol ${expected} probe timeout after ${attempts} attempts`);
232
+ }
187
233
  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');
234
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
189
235
  }
190
236
  detectProtocol(path, expectedProtocol, protocolHint) {
237
+ var _a;
191
238
  return __awaiter(this, void 0, void 0, function* () {
192
239
  if (expectedProtocol === 'V1') {
193
240
  if (yield this.probeProtocolV1(path)) {
194
241
  this.deviceProtocol.set(path, 'V1');
195
242
  return 'V1';
196
243
  }
244
+ yield this.closeConnectionAfterProbe(path);
197
245
  throw this.createProtocolMismatchError(expectedProtocol);
198
246
  }
199
247
  if (expectedProtocol === 'V2') {
200
- this.deviceProtocol.set(path, 'V2');
201
- return 'V2';
248
+ for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
249
+ if (yield this.probeProtocolV2(path)) {
250
+ this.deviceProtocol.set(path, 'V2');
251
+ return 'V2';
252
+ }
253
+ if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
254
+ yield this.resetConnectionAfterProbe(path);
255
+ (_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}`);
256
+ }
257
+ else {
258
+ yield this.closeConnectionAfterProbe(path);
259
+ }
260
+ }
261
+ this.deviceProtocol.delete(path);
262
+ throw this.createProtocolProbeTimeoutError(expectedProtocol, EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS);
202
263
  }
203
264
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
204
- for (const protocol of probeOrder) {
265
+ for (const [index, protocol] of probeOrder.entries()) {
205
266
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
206
267
  if (detected) {
207
268
  this.deviceProtocol.set(path, protocol);
208
269
  return protocol;
209
270
  }
271
+ if (index < probeOrder.length - 1) {
272
+ yield this.resetConnectionAfterProbe(path);
273
+ }
274
+ else {
275
+ yield this.closeConnectionAfterProbe(path);
276
+ }
210
277
  }
211
278
  this.deviceProtocol.delete(path);
212
279
  throw this.createProtocolDetectionError();
@@ -273,7 +340,7 @@ class WebUsbTransport {
273
340
  };
274
341
  }
275
342
  connectToDevice(path, first) {
276
- var _a, _b;
343
+ var _a;
277
344
  return __awaiter(this, void 0, void 0, function* () {
278
345
  let device = yield this.findDevice(path);
279
346
  if (!device.opened) {
@@ -297,33 +364,30 @@ class WebUsbTransport {
297
364
  }
298
365
  const endpoints = this.discoverEndpoints(device);
299
366
  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
367
  yield device.claimInterface(endpoints.interfaceNumber);
303
368
  yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
304
369
  yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
305
370
  });
306
371
  }
307
372
  closeOpenDevice(path) {
308
- var _a, _b, _c, _d, _e;
373
+ var _a, _b, _c, _d;
309
374
  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;
375
+ const current = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
312
376
  if (!(current === null || current === void 0 ? void 0 : current.opened))
313
377
  return;
314
378
  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;
379
+ const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
316
380
  try {
317
381
  yield current.releaseInterface(ifaceNum);
318
382
  }
319
383
  catch (error) {
320
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
384
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
321
385
  }
322
386
  try {
323
387
  yield current.close();
324
388
  }
325
389
  catch (error) {
326
- (_e = this.Log) === null || _e === void 0 ? void 0 : _e.debug('[WebUsbTransport] close before reconnect failed:', error);
390
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
327
391
  }
328
392
  });
329
393
  }
@@ -338,8 +402,18 @@ class WebUsbTransport {
338
402
  }
339
403
  });
340
404
  }
405
+ assertAcquired(path) {
406
+ if (!this.acquiredPaths.has(path)) {
407
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
408
+ }
409
+ }
341
410
  post(session, name, data) {
342
411
  return __awaiter(this, void 0, void 0, function* () {
412
+ this.assertAcquired(session);
413
+ if (this.deviceProtocol.get(session) === 'V2') {
414
+ yield this.sendProtocolV2UsbFlowControl(session, name, data);
415
+ return;
416
+ }
343
417
  yield this.call(session, name, data);
344
418
  });
345
419
  }
@@ -368,6 +442,7 @@ class WebUsbTransport {
368
442
  var _a;
369
443
  return __awaiter(this, void 0, void 0, function* () {
370
444
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
445
+ this.staleProtocolPaths.add(path);
371
446
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
372
447
  try {
373
448
  const currentDevice = yield this.findDevice(path);
@@ -485,34 +560,33 @@ class WebUsbTransport {
485
560
  throw lastError;
486
561
  });
487
562
  }
488
- resetConnectionAfterProbe(path) {
489
- var _a, _b;
563
+ transferInOnce(path, length) {
564
+ var _a;
490
565
  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);
566
+ const device = yield this.findDevice(path);
567
+ if (!device.opened) {
568
+ throw new Error('USBDevice is not open for transferIn');
510
569
  }
570
+ const endpoints = this.deviceEndpoints.get(path);
571
+ const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
572
+ const result = yield device.transferIn(endpointIn, length);
573
+ return this.getTransferInData(result);
574
+ });
575
+ }
576
+ closeConnectionAfterProbe(path) {
577
+ return __awaiter(this, void 0, void 0, function* () {
578
+ yield this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
579
+ yield this.closeOpenDevice(path);
580
+ });
581
+ }
582
+ resetConnectionAfterProbe(path) {
583
+ return __awaiter(this, void 0, void 0, function* () {
584
+ yield this.closeConnectionAfterProbe(path);
511
585
  yield this.getConnectedDevices();
512
586
  yield this.connect(path, false);
513
587
  });
514
588
  }
515
- withProtocolReadTimeout(_path, promise, timeoutMs, protocol, onTimeout) {
589
+ withProtocolReadTimeout(path, promise, timeoutMs, protocol, onTimeout) {
516
590
  return __awaiter(this, void 0, void 0, function* () {
517
591
  let timer;
518
592
  let timedOut = false;
@@ -527,11 +601,19 @@ class WebUsbTransport {
527
601
  return yield Promise.race([
528
602
  guardedPromise,
529
603
  new Promise((_, reject) => {
530
- timer = setTimeout(() => {
604
+ timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
531
605
  timedOut = true;
532
606
  onTimeout === null || onTimeout === void 0 ? void 0 : onTimeout();
607
+ if (protocol === 'V1') {
608
+ try {
609
+ yield this.resetConnectionAfterProbe(path);
610
+ }
611
+ catch (error) {
612
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
613
+ }
614
+ }
533
615
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
534
- }, timeoutMs);
616
+ }), timeoutMs);
535
617
  }),
536
618
  ]);
537
619
  }
@@ -547,7 +629,9 @@ class WebUsbTransport {
547
629
  return false;
548
630
  }
549
631
  try {
550
- yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
632
+ yield this.callProtocolV1(path, 'Initialize', {}, {
633
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
634
+ });
551
635
  return true;
552
636
  }
553
637
  catch (_error) {
@@ -562,10 +646,9 @@ class WebUsbTransport {
562
646
  }
563
647
  return transport.probeProtocolV2({
564
648
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
565
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
649
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
566
650
  logger: this.Log,
567
651
  logPrefix: 'ProtocolV2 WebUSB',
568
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
569
652
  });
570
653
  });
571
654
  }
@@ -574,6 +657,7 @@ class WebUsbTransport {
574
657
  if (this.messages == null) {
575
658
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
576
659
  }
660
+ this.assertAcquired(path);
577
661
  const device = yield this.findDevice(path);
578
662
  if (!device) {
579
663
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
@@ -582,9 +666,6 @@ class WebUsbTransport {
582
666
  if (!protocol) {
583
667
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
584
668
  }
585
- if (!shouldSuppressHighVolumeCallLog(name)) {
586
- this.Log.debug('transport call', createTransportCallLog(name, protocol));
587
- }
588
669
  if (protocol === 'V2') {
589
670
  return this.callProtocolV2(path, name, data, options);
590
671
  }
@@ -613,79 +694,8 @@ class WebUsbTransport {
613
694
  });
614
695
  }
615
696
  callProtocolV2(path, name, data, options) {
616
- var _a;
617
697
  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;
698
+ return this.callProtocolV2Usb(path, name, data, options);
689
699
  });
690
700
  }
691
701
  receiveData(path, timeoutMs) {
@@ -726,45 +736,81 @@ class WebUsbTransport {
726
736
  });
727
737
  }
728
738
  release(path) {
729
- var _a, _b;
730
739
  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);
740
+ this.acquiredPaths.delete(path);
741
+ yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
742
+ yield this.closeOpenDevice(path);
742
743
  this.deviceEndpoints.delete(path);
743
744
  });
744
745
  }
746
+ getProtocolV2UsbSchemas() {
747
+ if (!this.messages || !this.messagesV2) {
748
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
749
+ }
750
+ return {
751
+ protocolV1: this.messages,
752
+ protocolV2: this.messagesV2,
753
+ };
754
+ }
755
+ getProtocolV2UsbLogger() {
756
+ return this.Log;
757
+ }
758
+ writeProtocolV2UsbPacket(path, frame, _context) {
759
+ return __awaiter(this, void 0, void 0, function* () {
760
+ yield this.transferOutOnce(path, frame);
761
+ });
762
+ }
763
+ readProtocolV2UsbPacket(path, _context) {
764
+ return __awaiter(this, void 0, void 0, function* () {
765
+ const dataView = yield this.transferInOnce(path, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
766
+ return new Uint8Array(this.toArrayBuffer(dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)));
767
+ });
768
+ }
769
+ resetProtocolV2UsbNativeLink(path, _reason) {
770
+ return __awaiter(this, void 0, void 0, function* () {
771
+ yield this.closeOpenDevice(path);
772
+ });
773
+ }
774
+ onProtocolV2UsbLinkInvalidated(path, reason) {
775
+ var _a;
776
+ this.deviceProtocol.delete(path);
777
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
778
+ }
779
+ createProtocolV2UsbTimeoutError(name, timeoutMs) {
780
+ return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
781
+ }
745
782
  getProtocolType(path) {
746
783
  return this.deviceProtocol.get(path);
747
784
  }
748
785
  }
749
786
 
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;
787
+ const BLE_ATT_HEADER_BYTES = 3;
788
+ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
789
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
790
+ return fallbackPacketCapacity;
791
+ }
792
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
753
793
  }
794
+
795
+ const { parseConfigure, ProtocolV1, check } = transport__default["default"];
754
796
  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;
797
+ const BLE_PACKET_SIZE_FALLBACK = 192;
798
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
756
799
  const BLE_WRITE_DELAY_MS = 5;
757
- const BLE_RESPONSE_TIMEOUT_MS = 30000;
758
- const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
800
+ const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
759
801
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
760
802
  class ElectronBleTransport {
761
803
  constructor() {
762
804
  this.name = 'ElectronBleTransport';
763
805
  this.configured = false;
764
806
  this.runPromise = null;
807
+ this.runPromiseDeviceId = null;
765
808
  this.connectedDevices = new Set();
766
809
  this.deviceProtocol = new Map();
767
810
  this.deviceProtocolHints = new Map();
811
+ this.confirmedProtocolV2 = new Set();
812
+ this.deviceMtus = new Map();
813
+ this.devicePacketCapacities = new Map();
768
814
  this.v1Buffers = new Map();
769
815
  this.v2Assemblers = new Map();
770
816
  this.v2FrameQueues = new Map();
@@ -786,11 +832,13 @@ class ElectronBleTransport {
786
832
  this.rejectProtocolV2Frames(uuid, new Error(reason));
787
833
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
788
834
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
789
- yield this.release(uuid);
835
+ yield this.releaseNative(uuid);
790
836
  }
791
837
  }),
792
838
  });
839
+ this.warnedMissingRelease = false;
793
840
  this.notificationCleanups = new Map();
841
+ this.mtuCleanups = new Map();
794
842
  this.disconnectCleanups = new Map();
795
843
  this.notificationTokens = new Map();
796
844
  this.nextNotificationToken = 1;
@@ -830,15 +878,27 @@ class ElectronBleTransport {
830
878
  .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] link cleanup failed:', error); });
831
879
  this.connectedDevices.delete(deviceId);
832
880
  this.deviceProtocol.delete(deviceId);
881
+ this.deviceMtus.delete(deviceId);
882
+ this.devicePacketCapacities.delete(deviceId);
833
883
  this.v1Buffers.delete(deviceId);
834
884
  this.v2Assemblers.delete(deviceId);
835
885
  this.resetProtocolV2Frames(deviceId);
836
886
  this.notificationTokens.delete(deviceId);
887
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
888
+ this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
889
+ this.runPromise = null;
890
+ this.runPromiseDeviceId = null;
891
+ }
837
892
  const notifyCleanup = this.notificationCleanups.get(deviceId);
838
893
  if (notifyCleanup) {
839
894
  notifyCleanup();
840
895
  this.notificationCleanups.delete(deviceId);
841
896
  }
897
+ const mtuCleanup = this.mtuCleanups.get(deviceId);
898
+ if (mtuCleanup) {
899
+ mtuCleanup();
900
+ this.mtuCleanups.delete(deviceId);
901
+ }
842
902
  const disconnectCleanup = this.disconnectCleanups.get(deviceId);
843
903
  if (disconnectCleanup) {
844
904
  disconnectCleanup();
@@ -859,10 +919,18 @@ class ElectronBleTransport {
859
919
  this.configured = true;
860
920
  }
861
921
  configureProtocolV2(signedData) {
922
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
923
+ if (this.protocolV2SchemaConfiguration === configuration) {
924
+ return;
925
+ }
926
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
862
927
  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); });
928
+ this.protocolV2SchemaConfiguration = configuration;
929
+ if (isReconfiguration) {
930
+ this.protocolV2Links
931
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
932
+ .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] schema link cleanup failed:', error); });
933
+ }
866
934
  }
867
935
  listen() {
868
936
  return __awaiter(this, void 0, void 0, function* () {
@@ -880,10 +948,6 @@ class ElectronBleTransport {
880
948
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
881
949
  for (const dev of devices) {
882
950
  (_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
951
  }
888
952
  return devices.map(device => toBleDescriptor(device));
889
953
  }
@@ -894,7 +958,7 @@ class ElectronBleTransport {
894
958
  });
895
959
  }
896
960
  acquire(input) {
897
- var _a, _b, _c, _d, _e, _f;
961
+ var _a, _b, _c, _d, _e, _f, _g;
898
962
  return __awaiter(this, void 0, void 0, function* () {
899
963
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
900
964
  if (!uuid) {
@@ -906,8 +970,8 @@ class ElectronBleTransport {
906
970
  if (forceCleanRunPromise && this.runPromise) {
907
971
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
908
972
  this.runPromise.reject(error);
909
- this.rejectAllProtocolV2Frames(error);
910
973
  this.runPromise = null;
974
+ this.runPromiseDeviceId = null;
911
975
  }
912
976
  try {
913
977
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
@@ -919,7 +983,7 @@ class ElectronBleTransport {
919
983
  }
920
984
  const protocolHint = expectedProtocol
921
985
  ? undefined
922
- : (_b = this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(device.name);
986
+ : (_b = input.protocolHint) !== null && _b !== void 0 ? _b : this.deviceProtocolHints.get(uuid);
923
987
  if (protocolHint) {
924
988
  this.deviceProtocolHints.set(uuid, protocolHint);
925
989
  }
@@ -930,16 +994,31 @@ class ElectronBleTransport {
930
994
  catch (error) {
931
995
  this.handleBluetoothError(error);
932
996
  }
997
+ const mtuCleanup = this.createMtuSubscription(uuid);
998
+ if (mtuCleanup) {
999
+ this.mtuCleanups.set(uuid, mtuCleanup);
1000
+ }
933
1001
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
934
- this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler());
1002
+ this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
935
1003
  yield window.desktopApi.nobleBle.subscribe(uuid);
1004
+ yield this.refreshBlePacketCapacity(uuid);
936
1005
  const cleanup = this.createNotificationSubscription(uuid);
937
1006
  this.notificationCleanups.set(uuid, cleanup);
1007
+ const connectionToken = this.notificationTokens.get(uuid);
1008
+ const protocolType = yield this.detectProtocol(uuid, expectedProtocol, protocolHint);
1009
+ if (protocolType === 'V2') {
1010
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[Electron BLE] Protocol V2 write configured', {
1011
+ writeMode: 'withoutResponse',
1012
+ negotiatedMtu: this.deviceMtus.get(uuid),
1013
+ packetCapacity: (_d = this.devicePacketCapacities.get(uuid)) !== null && _d !== void 0 ? _d : BLE_PACKET_SIZE_FALLBACK,
1014
+ });
1015
+ }
938
1016
  const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected((disconnectedDevice) => {
939
1017
  var _a;
940
- if (disconnectedDevice.id === uuid) {
1018
+ if (disconnectedDevice.id === uuid &&
1019
+ this.notificationTokens.get(uuid) === connectionToken) {
941
1020
  this.cleanupDeviceState(uuid);
942
- (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('device-disconnect', {
1021
+ (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
943
1022
  name: disconnectedDevice.name,
944
1023
  id: disconnectedDevice.id,
945
1024
  connectId: disconnectedDevice.id,
@@ -947,35 +1026,46 @@ class ElectronBleTransport {
947
1026
  }
948
1027
  });
949
1028
  this.disconnectCleanups.set(uuid, disconnectCleanup);
950
- 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
- });
956
1029
  return Object.assign(Object.assign({}, toBleDescriptor({ id: device.id, name: device.name }, protocolType)), { uuid });
957
1030
  }
958
1031
  catch (error) {
959
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] acquire failed:', error);
1032
+ (_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
960
1033
  try {
961
- if (((_e = window.desktopApi) === null || _e === void 0 ? void 0 : _e.nobleBle) && this.connectedDevices.has(uuid)) {
1034
+ if (((_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle) && this.connectedDevices.has(uuid)) {
962
1035
  yield window.desktopApi.nobleBle.unsubscribe(uuid);
963
1036
  yield window.desktopApi.nobleBle.disconnect(uuid);
964
1037
  }
965
1038
  }
966
1039
  catch (cleanupError) {
967
- (_f = this.Log) === null || _f === void 0 ? void 0 : _f.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
1040
+ (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
968
1041
  }
969
1042
  this.cleanupDeviceState(uuid);
970
1043
  throw error;
971
1044
  }
972
1045
  });
973
1046
  }
974
- release(id) {
975
- var _a, _b;
1047
+ release(id, _onclose, keepSession) {
1048
+ var _a;
976
1049
  return __awaiter(this, void 0, void 0, function* () {
977
1050
  try {
978
1051
  yield this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
1052
+ yield this.releaseLogical(id, keepSession);
1053
+ }
1054
+ catch (error) {
1055
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] release failed:', error);
1056
+ this.cleanupDeviceState(id);
1057
+ }
1058
+ });
1059
+ }
1060
+ disconnect(id) {
1061
+ return __awaiter(this, void 0, void 0, function* () {
1062
+ return this.releaseNative(id);
1063
+ });
1064
+ }
1065
+ releaseNative(id) {
1066
+ var _a, _b;
1067
+ return __awaiter(this, void 0, void 0, function* () {
1068
+ try {
979
1069
  if (this.connectedDevices.has(id)) {
980
1070
  if ((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) {
981
1071
  yield window.desktopApi.nobleBle.unsubscribe(id);
@@ -990,11 +1080,36 @@ class ElectronBleTransport {
990
1080
  }
991
1081
  });
992
1082
  }
993
- createProtocolMismatchError(expected) {
994
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1083
+ releaseLogical(id, keepSession) {
1084
+ var _a, _b, _c, _d;
1085
+ return __awaiter(this, void 0, void 0, function* () {
1086
+ try {
1087
+ if (!this.connectedDevices.has(id))
1088
+ return;
1089
+ this.cleanupDeviceState(id);
1090
+ const release = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.release;
1091
+ if (!release) {
1092
+ if (!this.warnedMissingRelease) {
1093
+ this.warnedMissingRelease = true;
1094
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.error('[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
1095
+ 'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload');
1096
+ }
1097
+ return;
1098
+ }
1099
+ yield release(id, keepSession);
1100
+ }
1101
+ catch (error) {
1102
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
1103
+ this.cleanupDeviceState(id);
1104
+ }
1105
+ });
1106
+ }
1107
+ createProtocolMismatchError(expected, uuid) {
1108
+ const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1109
+ 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
1110
  }
996
1111
  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');
1112
+ 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
1113
  }
999
1114
  clearProbeProtocol(uuid, protocol) {
1000
1115
  if (this.deviceProtocol.get(uuid) === protocol) {
@@ -1010,12 +1125,16 @@ class ElectronBleTransport {
1010
1125
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1011
1126
  return 'V1';
1012
1127
  }
1013
- throw this.createProtocolMismatchError(expectedProtocol);
1128
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1014
1129
  }
1015
1130
  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';
1131
+ if (yield this.probeProtocolV2(uuid)) {
1132
+ this.deviceProtocol.set(uuid, 'V2');
1133
+ this.confirmedProtocolV2.add(uuid);
1134
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1135
+ return 'V2';
1136
+ }
1137
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1019
1138
  }
1020
1139
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1021
1140
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1026,6 +1145,9 @@ class ElectronBleTransport {
1026
1145
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1027
1146
  if (detected) {
1028
1147
  this.deviceProtocol.set(uuid, protocol);
1148
+ if (protocol === 'V2') {
1149
+ this.confirmedProtocolV2.add(uuid);
1150
+ }
1029
1151
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1030
1152
  return protocol;
1031
1153
  }
@@ -1049,38 +1171,22 @@ class ElectronBleTransport {
1049
1171
  });
1050
1172
  }
1051
1173
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
1052
- var _a, _b, _c, _d, _e, _f, _g;
1174
+ var _a;
1053
1175
  return __awaiter(this, void 0, void 0, function* () {
1054
1176
  yield this.protocolV2Links.invalidateLink(uuid, `Reset notify state after Protocol ${protocol} probe`);
1055
1177
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1056
1178
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1057
1179
  this.resetProtocolV2Frames(uuid);
1058
- if (this.runPromise) {
1180
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
1059
1181
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
1060
1182
  this.runPromise.reject(error);
1061
1183
  this.runPromise = null;
1184
+ this.runPromiseDeviceId = null;
1062
1185
  }
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;
1186
+ if (!this.notificationCleanups.has(uuid)) {
1187
+ const cleanup = this.createNotificationSubscription(uuid);
1188
+ this.notificationCleanups.set(uuid, cleanup);
1081
1189
  }
1082
- const cleanup = this.createNotificationSubscription(uuid);
1083
- this.notificationCleanups.set(uuid, cleanup);
1084
1190
  });
1085
1191
  }
1086
1192
  probeProtocolV1(uuid) {
@@ -1091,12 +1197,12 @@ class ElectronBleTransport {
1091
1197
  }
1092
1198
  try {
1093
1199
  this.deviceProtocol.set(uuid, 'V1');
1094
- yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1200
+ yield this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1095
1201
  return true;
1096
1202
  }
1097
1203
  catch (error) {
1098
1204
  this.clearProbeProtocol(uuid, 'V1');
1099
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
1205
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
1100
1206
  return false;
1101
1207
  }
1102
1208
  });
@@ -1126,25 +1232,6 @@ class ElectronBleTransport {
1126
1232
  return detected;
1127
1233
  });
1128
1234
  }
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
1235
  writeOnce(uuid, hexData) {
1149
1236
  var _a;
1150
1237
  return __awaiter(this, void 0, void 0, function* () {
@@ -1152,7 +1239,51 @@ class ElectronBleTransport {
1152
1239
  if (!nobleBle) {
1153
1240
  throw new Error('Noble BLE API not available');
1154
1241
  }
1155
- yield nobleBle.write(uuid, hexData);
1242
+ yield nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
1243
+ });
1244
+ }
1245
+ refreshBlePacketCapacity(uuid) {
1246
+ var _a, _b;
1247
+ return __awaiter(this, void 0, void 0, function* () {
1248
+ const device = yield ((_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.getDevice(uuid));
1249
+ this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
1250
+ });
1251
+ }
1252
+ updateBlePacketCapacity(uuid, mtu) {
1253
+ const packetCapacity = resolveBlePacketCapacity(mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
1254
+ if (typeof mtu === 'number') {
1255
+ this.deviceMtus.set(uuid, mtu);
1256
+ }
1257
+ else {
1258
+ this.deviceMtus.delete(uuid);
1259
+ }
1260
+ this.devicePacketCapacities.set(uuid, packetCapacity);
1261
+ }
1262
+ createMtuSubscription(uuid) {
1263
+ var _a, _b;
1264
+ const onMtuChanged = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.onMtuChanged;
1265
+ if (!onMtuChanged)
1266
+ return undefined;
1267
+ return onMtuChanged(device => {
1268
+ if (device.id === uuid) {
1269
+ this.updateBlePacketCapacity(uuid, device.mtu);
1270
+ }
1271
+ });
1272
+ }
1273
+ writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration) {
1274
+ var _a;
1275
+ const packetCapacity = (_a = this.devicePacketCapacities.get(uuid)) !== null && _a !== void 0 ? _a : BLE_PACKET_SIZE_FALLBACK;
1276
+ const shouldPace = !context.highThroughput;
1277
+ return transport.writeProtocolV2BleFrame({
1278
+ frame,
1279
+ packetCapacity,
1280
+ assertActive: assertCurrentGeneration,
1281
+ signal: context.signal,
1282
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1283
+ initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
1284
+ burstSize: shouldPace ? 1 : undefined,
1285
+ burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
1286
+ writePacket: packet => this.writeOnce(uuid, transport.bytesToHex(packet)),
1156
1287
  });
1157
1288
  }
1158
1289
  handleNotification(deviceId, hexData) {
@@ -1163,7 +1294,7 @@ class ElectronBleTransport {
1163
1294
  if (this.deviceProtocol.get(deviceId) === 'V2') {
1164
1295
  this.rejectProtocolV2Frames(deviceId, error);
1165
1296
  }
1166
- else if (this.runPromise) {
1297
+ else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1167
1298
  this.runPromise.reject(error);
1168
1299
  }
1169
1300
  return;
@@ -1215,16 +1346,8 @@ class ElectronBleTransport {
1215
1346
  }
1216
1347
  this.getProtocolV2FrameQueue(uuid).push(frame);
1217
1348
  }
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
1349
  resetProtocolV2Frames(uuid) {
1226
- this.v2FrameQueues.delete(uuid);
1227
- this.v2FramePromises.delete(uuid);
1350
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1228
1351
  }
1229
1352
  rejectProtocolV2Frames(uuid, error) {
1230
1353
  this.v2FrameQueues.delete(uuid);
@@ -1257,17 +1380,19 @@ class ElectronBleTransport {
1257
1380
  const result = this.processProtocolV1Notification(deviceId, hexData);
1258
1381
  if (result.error) {
1259
1382
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
1260
- if (this.runPromise) {
1383
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1261
1384
  this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError));
1262
1385
  }
1263
1386
  return;
1264
1387
  }
1265
- if (result.isComplete && result.completePacket && this.runPromise) {
1388
+ if (result.isComplete &&
1389
+ result.completePacket &&
1390
+ this.runPromise &&
1391
+ this.runPromiseDeviceId === deviceId) {
1266
1392
  this.runPromise.resolve(result.completePacket);
1267
1393
  }
1268
1394
  }
1269
1395
  call(uuid, name, data, options) {
1270
- var _a;
1271
1396
  return __awaiter(this, void 0, void 0, function* () {
1272
1397
  if (!this._messages) {
1273
1398
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
@@ -1279,15 +1404,21 @@ class ElectronBleTransport {
1279
1404
  if (!protocol) {
1280
1405
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
1281
1406
  }
1282
- if (!shouldSuppressHighVolumeCallLog(name)) {
1283
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', createTransportCallLog(name, protocol));
1284
- }
1285
1407
  if (protocol === 'V2') {
1286
1408
  return this.callProtocolV2(uuid, name, data, options);
1287
1409
  }
1288
1410
  return this.callProtocolV1(uuid, name, data, options);
1289
1411
  });
1290
1412
  }
1413
+ post(uuid, name, data) {
1414
+ return __awaiter(this, void 0, void 0, function* () {
1415
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1416
+ yield this.protocolV2Links.sendFlowControl(uuid, () => this.createProtocolV2Adapter(uuid), name, data);
1417
+ return;
1418
+ }
1419
+ yield this.callProtocolV1(uuid, name, data);
1420
+ });
1421
+ }
1291
1422
  callProtocolV1(uuid, name, data, options) {
1292
1423
  var _a, _b;
1293
1424
  return __awaiter(this, void 0, void 0, function* () {
@@ -1301,6 +1432,7 @@ class ElectronBleTransport {
1301
1432
  const runPromise = hdShared.createDeferred();
1302
1433
  runPromise.promise.catch(() => undefined);
1303
1434
  this.runPromise = runPromise;
1435
+ this.runPromiseDeviceId = uuid;
1304
1436
  const messages = this._messages;
1305
1437
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1306
1438
  let timeout;
@@ -1339,6 +1471,17 @@ class ElectronBleTransport {
1339
1471
  }
1340
1472
  catch (e) {
1341
1473
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V1 call error:', e);
1474
+ const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1475
+ if ((e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError) {
1476
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1477
+ const notifyCleanup = this.notificationCleanups.get(uuid);
1478
+ notifyCleanup === null || notifyCleanup === void 0 ? void 0 : notifyCleanup();
1479
+ this.notificationCleanups.delete(uuid);
1480
+ this.notificationTokens.delete(uuid);
1481
+ if (!isProbeTimeout) {
1482
+ yield this.releaseNative(uuid);
1483
+ }
1484
+ }
1342
1485
  throw e;
1343
1486
  }
1344
1487
  finally {
@@ -1346,22 +1489,23 @@ class ElectronBleTransport {
1346
1489
  clearTimeout(timeout);
1347
1490
  if (this.runPromise === runPromise) {
1348
1491
  this.runPromise = null;
1492
+ this.runPromiseDeviceId = null;
1349
1493
  }
1350
1494
  }
1351
1495
  });
1352
1496
  }
1353
1497
  callProtocolV2(uuid, name, data, options) {
1354
- var _a, _b;
1498
+ var _a;
1355
1499
  return __awaiter(this, void 0, void 0, function* () {
1356
1500
  if (!this._messages || !this._messagesV2) {
1357
1501
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
1358
1502
  }
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 });
1503
+ const callOptions = options;
1360
1504
  try {
1361
1505
  return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, callOptions);
1362
1506
  }
1363
1507
  catch (e) {
1364
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V2 call error:', e);
1508
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V2 call error:', e);
1365
1509
  throw e;
1366
1510
  }
1367
1511
  });
@@ -1376,6 +1520,7 @@ class ElectronBleTransport {
1376
1520
  };
1377
1521
  return {
1378
1522
  router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
1523
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1379
1524
  generation,
1380
1525
  prepareCall: () => {
1381
1526
  var _a;
@@ -1383,10 +1528,7 @@ class ElectronBleTransport {
1383
1528
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1384
1529
  this.resetProtocolV2Frames(uuid);
1385
1530
  },
1386
- writeFrame: (frame) => {
1387
- assertCurrentGeneration();
1388
- return this.writeWithChunking(uuid, transport.bytesToHex(frame));
1389
- },
1531
+ writeFrame: (frame, context) => this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
1390
1532
  readFrame: () => __awaiter(this, void 0, void 0, function* () {
1391
1533
  assertCurrentGeneration();
1392
1534
  const rxFrame = yield this.readProtocolV2Frame(uuid);
@@ -1406,6 +1548,7 @@ class ElectronBleTransport {
1406
1548
  };
1407
1549
  }
1408
1550
  processProtocolV1Notification(deviceId, hexData) {
1551
+ var _a;
1409
1552
  try {
1410
1553
  if (typeof hexData !== 'string') {
1411
1554
  return { isComplete: false, error: 'Invalid hexData type' };
@@ -1424,6 +1567,10 @@ class ElectronBleTransport {
1424
1567
  bufferState.buffer = [...data.subarray(3)];
1425
1568
  }
1426
1569
  else {
1570
+ if (bufferState.buffer.length === 0) {
1571
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Orphan continuation chunk discarded');
1572
+ return { isComplete: false };
1573
+ }
1427
1574
  bufferState.buffer = bufferState.buffer.concat([...data]);
1428
1575
  }
1429
1576
  if (bufferState.buffer.length - transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) {