@onekeyfe/hd-transport-web-device 1.2.0-alpha.17 → 1.2.0-alpha.171

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,88 @@ 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, _g;
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.skipProtocolProbe) {
194
+ if (!input.expectedProtocol) {
195
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'skipProtocolProbe requires an expected protocol');
196
+ }
197
+ this.staleProtocolPaths.delete(input.path);
198
+ this.deviceProtocol.set(input.path, input.expectedProtocol);
199
+ const currentDevice = (_b = this.deviceList.find(d => d.path === input.path)) === null || _b === void 0 ? void 0 : _b.device;
200
+ if (currentDevice) {
201
+ this.probedDeviceObjects.set(input.path, currentDevice);
202
+ }
203
+ }
204
+ else if (input.forceProtocolDetection) {
205
+ this.staleProtocolPaths.delete(input.path);
206
+ this.deviceProtocol.delete(input.path);
207
+ this.deviceProtocolHints.delete(input.path);
208
+ }
209
+ if (!input.skipProtocolProbe && this.staleProtocolPaths.has(input.path)) {
210
+ this.staleProtocolPaths.delete(input.path);
211
+ this.deviceProtocol.delete(input.path);
174
212
  }
175
- yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
213
+ if (!input.skipProtocolProbe && this.deviceProtocol.has(input.path)) {
214
+ const currentDevice = (_c = this.deviceList.find(d => d.path === input.path)) === null || _c === void 0 ? void 0 : _c.device;
215
+ if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
216
+ this.deviceProtocol.delete(input.path);
217
+ }
218
+ }
219
+ const cachedProtocol = this.deviceProtocol.get(input.path);
220
+ if (!input.skipProtocolProbe &&
221
+ cachedProtocol &&
222
+ input.expectedProtocol &&
223
+ cachedProtocol !== input.expectedProtocol) {
224
+ this.deviceProtocol.delete(input.path);
225
+ }
226
+ if (!this.deviceProtocol.has(input.path)) {
227
+ const usbDevice = (_d = this.deviceList.find(device => device.path === input.path)) === null || _d === void 0 ? void 0 : _d.device;
228
+ const protocolHint = input.expectedProtocol
229
+ ? undefined
230
+ : (_f = (_e = input.protocolHint) !== null && _e !== void 0 ? _e : this.deviceProtocolHints.get(input.path)) !== null && _f !== void 0 ? _f : hdShared.inferProtocolHintFromUsbId(usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.vendorId, usbDevice === null || usbDevice === void 0 ? void 0 : usbDevice.productId);
231
+ if (protocolHint) {
232
+ this.deviceProtocolHints.set(input.path, protocolHint);
233
+ }
234
+ yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
235
+ const probedDevice = (_g = this.deviceList.find(d => d.path === input.path)) === null || _g === void 0 ? void 0 : _g.device;
236
+ if (probedDevice) {
237
+ this.probedDeviceObjects.set(input.path, probedDevice);
238
+ }
239
+ }
240
+ this.acquiredPaths.add(input.path);
176
241
  return yield Promise.resolve(input.path);
177
242
  }
178
243
  catch (e) {
179
244
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
245
+ this.acquiredPaths.delete(input.path);
246
+ yield this.closeOpenDevice(input.path);
180
247
  throw e;
181
248
  }
182
249
  });
@@ -184,29 +251,53 @@ class WebUsbTransport {
184
251
  createProtocolMismatchError(expected) {
185
252
  return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
186
253
  }
254
+ createProtocolProbeTimeoutError(expected, attempts) {
255
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInitializeFailed, `Protocol ${expected} probe timeout after ${attempts} attempts`);
256
+ }
187
257
  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');
258
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
189
259
  }
190
260
  detectProtocol(path, expectedProtocol, protocolHint) {
261
+ var _a;
191
262
  return __awaiter(this, void 0, void 0, function* () {
192
263
  if (expectedProtocol === 'V1') {
193
264
  if (yield this.probeProtocolV1(path)) {
194
265
  this.deviceProtocol.set(path, 'V1');
195
266
  return 'V1';
196
267
  }
268
+ yield this.closeConnectionAfterProbe(path);
197
269
  throw this.createProtocolMismatchError(expectedProtocol);
198
270
  }
199
271
  if (expectedProtocol === 'V2') {
200
- this.deviceProtocol.set(path, 'V2');
201
- return 'V2';
272
+ for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
273
+ if (yield this.probeProtocolV2(path)) {
274
+ this.deviceProtocol.set(path, 'V2');
275
+ return 'V2';
276
+ }
277
+ if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
278
+ yield this.resetConnectionAfterProbe(path);
279
+ (_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}`);
280
+ }
281
+ else {
282
+ yield this.closeConnectionAfterProbe(path);
283
+ }
284
+ }
285
+ this.deviceProtocol.delete(path);
286
+ throw this.createProtocolProbeTimeoutError(expectedProtocol, EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS);
202
287
  }
203
288
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
204
- for (const protocol of probeOrder) {
289
+ for (const [index, protocol] of probeOrder.entries()) {
205
290
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
206
291
  if (detected) {
207
292
  this.deviceProtocol.set(path, protocol);
208
293
  return protocol;
209
294
  }
295
+ if (index < probeOrder.length - 1) {
296
+ yield this.resetConnectionAfterProbe(path);
297
+ }
298
+ else {
299
+ yield this.closeConnectionAfterProbe(path);
300
+ }
210
301
  }
211
302
  this.deviceProtocol.delete(path);
212
303
  throw this.createProtocolDetectionError();
@@ -273,7 +364,7 @@ class WebUsbTransport {
273
364
  };
274
365
  }
275
366
  connectToDevice(path, first) {
276
- var _a, _b;
367
+ var _a;
277
368
  return __awaiter(this, void 0, void 0, function* () {
278
369
  let device = yield this.findDevice(path);
279
370
  if (!device.opened) {
@@ -297,33 +388,30 @@ class WebUsbTransport {
297
388
  }
298
389
  const endpoints = this.discoverEndpoints(device);
299
390
  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
391
  yield device.claimInterface(endpoints.interfaceNumber);
303
392
  yield this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
304
393
  yield this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
305
394
  });
306
395
  }
307
396
  closeOpenDevice(path) {
308
- var _a, _b, _c, _d, _e;
397
+ var _a, _b, _c, _d;
309
398
  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;
399
+ const current = (_a = this.deviceList.find(device => device.path === path)) === null || _a === void 0 ? void 0 : _a.device;
312
400
  if (!(current === null || current === void 0 ? void 0 : current.opened))
313
401
  return;
314
402
  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;
403
+ const ifaceNum = (_b = endpoints === null || endpoints === void 0 ? void 0 : endpoints.interfaceNumber) !== null && _b !== void 0 ? _b : this.interfaceId;
316
404
  try {
317
405
  yield current.releaseInterface(ifaceNum);
318
406
  }
319
407
  catch (error) {
320
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
408
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
321
409
  }
322
410
  try {
323
411
  yield current.close();
324
412
  }
325
413
  catch (error) {
326
- (_e = this.Log) === null || _e === void 0 ? void 0 : _e.debug('[WebUsbTransport] close before reconnect failed:', error);
414
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug('[WebUsbTransport] close before reconnect failed:', error);
327
415
  }
328
416
  });
329
417
  }
@@ -338,8 +426,18 @@ class WebUsbTransport {
338
426
  }
339
427
  });
340
428
  }
429
+ assertAcquired(path) {
430
+ if (!this.acquiredPaths.has(path)) {
431
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
432
+ }
433
+ }
341
434
  post(session, name, data) {
342
435
  return __awaiter(this, void 0, void 0, function* () {
436
+ this.assertAcquired(session);
437
+ if (this.deviceProtocol.get(session) === 'V2') {
438
+ yield this.sendProtocolV2UsbFlowControl(session, name, data);
439
+ return;
440
+ }
343
441
  yield this.call(session, name, data);
344
442
  });
345
443
  }
@@ -368,6 +466,7 @@ class WebUsbTransport {
368
466
  var _a;
369
467
  return __awaiter(this, void 0, void 0, function* () {
370
468
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
469
+ this.staleProtocolPaths.add(path);
371
470
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
372
471
  try {
373
472
  const currentDevice = yield this.findDevice(path);
@@ -485,34 +584,33 @@ class WebUsbTransport {
485
584
  throw lastError;
486
585
  });
487
586
  }
488
- resetConnectionAfterProbe(path) {
489
- var _a, _b;
587
+ transferInOnce(path, length) {
588
+ var _a;
490
589
  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);
590
+ const device = yield this.findDevice(path);
591
+ if (!device.opened) {
592
+ throw new Error('USBDevice is not open for transferIn');
510
593
  }
594
+ const endpoints = this.deviceEndpoints.get(path);
595
+ const endpointIn = (_a = endpoints === null || endpoints === void 0 ? void 0 : endpoints.endpointIn) !== null && _a !== void 0 ? _a : this.endpointId;
596
+ const result = yield device.transferIn(endpointIn, length);
597
+ return this.getTransferInData(result);
598
+ });
599
+ }
600
+ closeConnectionAfterProbe(path) {
601
+ return __awaiter(this, void 0, void 0, function* () {
602
+ yield this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
603
+ yield this.closeOpenDevice(path);
604
+ });
605
+ }
606
+ resetConnectionAfterProbe(path) {
607
+ return __awaiter(this, void 0, void 0, function* () {
608
+ yield this.closeConnectionAfterProbe(path);
511
609
  yield this.getConnectedDevices();
512
610
  yield this.connect(path, false);
513
611
  });
514
612
  }
515
- withProtocolReadTimeout(_path, promise, timeoutMs, protocol, onTimeout) {
613
+ withProtocolReadTimeout(path, promise, timeoutMs, protocol, onTimeout) {
516
614
  return __awaiter(this, void 0, void 0, function* () {
517
615
  let timer;
518
616
  let timedOut = false;
@@ -527,11 +625,19 @@ class WebUsbTransport {
527
625
  return yield Promise.race([
528
626
  guardedPromise,
529
627
  new Promise((_, reject) => {
530
- timer = setTimeout(() => {
628
+ timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
531
629
  timedOut = true;
532
630
  onTimeout === null || onTimeout === void 0 ? void 0 : onTimeout();
631
+ if (protocol === 'V1') {
632
+ try {
633
+ yield this.resetConnectionAfterProbe(path);
634
+ }
635
+ catch (error) {
636
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
637
+ }
638
+ }
533
639
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
534
- }, timeoutMs);
640
+ }), timeoutMs);
535
641
  }),
536
642
  ]);
537
643
  }
@@ -547,7 +653,9 @@ class WebUsbTransport {
547
653
  return false;
548
654
  }
549
655
  try {
550
- yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
656
+ yield this.callProtocolV1(path, 'Initialize', {}, {
657
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
658
+ });
551
659
  return true;
552
660
  }
553
661
  catch (_error) {
@@ -562,10 +670,9 @@ class WebUsbTransport {
562
670
  }
563
671
  return transport.probeProtocolV2({
564
672
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
565
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
673
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
566
674
  logger: this.Log,
567
675
  logPrefix: 'ProtocolV2 WebUSB',
568
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
569
676
  });
570
677
  });
571
678
  }
@@ -574,6 +681,7 @@ class WebUsbTransport {
574
681
  if (this.messages == null) {
575
682
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
576
683
  }
684
+ this.assertAcquired(path);
577
685
  const device = yield this.findDevice(path);
578
686
  if (!device) {
579
687
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
@@ -582,9 +690,6 @@ class WebUsbTransport {
582
690
  if (!protocol) {
583
691
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
584
692
  }
585
- if (!shouldSuppressHighVolumeCallLog(name)) {
586
- this.Log.debug('transport call', createTransportCallLog(name, protocol));
587
- }
588
693
  if (protocol === 'V2') {
589
694
  return this.callProtocolV2(path, name, data, options);
590
695
  }
@@ -613,79 +718,8 @@ class WebUsbTransport {
613
718
  });
614
719
  }
615
720
  callProtocolV2(path, name, data, options) {
616
- var _a;
617
- 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
721
  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;
722
+ return this.callProtocolV2Usb(path, name, data, options);
689
723
  });
690
724
  }
691
725
  receiveData(path, timeoutMs) {
@@ -726,45 +760,81 @@ class WebUsbTransport {
726
760
  });
727
761
  }
728
762
  release(path) {
729
- var _a, _b;
730
763
  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);
764
+ this.acquiredPaths.delete(path);
765
+ yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
766
+ yield this.closeOpenDevice(path);
742
767
  this.deviceEndpoints.delete(path);
743
768
  });
744
769
  }
770
+ getProtocolV2UsbSchemas() {
771
+ if (!this.messages || !this.messagesV2) {
772
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
773
+ }
774
+ return {
775
+ protocolV1: this.messages,
776
+ protocolV2: this.messagesV2,
777
+ };
778
+ }
779
+ getProtocolV2UsbLogger() {
780
+ return this.Log;
781
+ }
782
+ writeProtocolV2UsbPacket(path, frame, _context) {
783
+ return __awaiter(this, void 0, void 0, function* () {
784
+ yield this.transferOutOnce(path, frame);
785
+ });
786
+ }
787
+ readProtocolV2UsbPacket(path, _context) {
788
+ return __awaiter(this, void 0, void 0, function* () {
789
+ const dataView = yield this.transferInOnce(path, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
790
+ return new Uint8Array(this.toArrayBuffer(dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)));
791
+ });
792
+ }
793
+ resetProtocolV2UsbNativeLink(path, _reason) {
794
+ return __awaiter(this, void 0, void 0, function* () {
795
+ yield this.closeOpenDevice(path);
796
+ });
797
+ }
798
+ onProtocolV2UsbLinkInvalidated(path, reason) {
799
+ var _a;
800
+ this.deviceProtocol.delete(path);
801
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
802
+ }
803
+ createProtocolV2UsbTimeoutError(name, timeoutMs) {
804
+ return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
805
+ }
745
806
  getProtocolType(path) {
746
807
  return this.deviceProtocol.get(path);
747
808
  }
748
809
  }
749
810
 
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;
811
+ const BLE_ATT_HEADER_BYTES = 3;
812
+ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
813
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
814
+ return fallbackPacketCapacity;
815
+ }
816
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
753
817
  }
818
+
819
+ const { parseConfigure, ProtocolV1, check } = transport__default["default"];
754
820
  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;
821
+ const BLE_PACKET_SIZE_FALLBACK = 192;
822
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
756
823
  const BLE_WRITE_DELAY_MS = 5;
757
- const BLE_RESPONSE_TIMEOUT_MS = 30000;
758
- const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
824
+ const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
759
825
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
760
826
  class ElectronBleTransport {
761
827
  constructor() {
762
828
  this.name = 'ElectronBleTransport';
763
829
  this.configured = false;
764
830
  this.runPromise = null;
831
+ this.runPromiseDeviceId = null;
765
832
  this.connectedDevices = new Set();
766
833
  this.deviceProtocol = new Map();
767
834
  this.deviceProtocolHints = new Map();
835
+ this.confirmedProtocolV2 = new Set();
836
+ this.deviceMtus = new Map();
837
+ this.devicePacketCapacities = new Map();
768
838
  this.v1Buffers = new Map();
769
839
  this.v2Assemblers = new Map();
770
840
  this.v2FrameQueues = new Map();
@@ -786,12 +856,13 @@ class ElectronBleTransport {
786
856
  this.rejectProtocolV2Frames(uuid, new Error(reason));
787
857
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason);
788
858
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
789
- yield this.release(uuid);
859
+ yield this.releaseNative(uuid);
790
860
  }
791
861
  }),
792
862
  });
863
+ this.warnedMissingRelease = false;
793
864
  this.notificationCleanups = new Map();
794
- this.disconnectCleanups = new Map();
865
+ this.mtuCleanups = new Map();
795
866
  this.notificationTokens = new Map();
796
867
  this.nextNotificationToken = 1;
797
868
  }
@@ -830,19 +901,26 @@ class ElectronBleTransport {
830
901
  .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] link cleanup failed:', error); });
831
902
  this.connectedDevices.delete(deviceId);
832
903
  this.deviceProtocol.delete(deviceId);
904
+ this.deviceMtus.delete(deviceId);
905
+ this.devicePacketCapacities.delete(deviceId);
833
906
  this.v1Buffers.delete(deviceId);
834
907
  this.v2Assemblers.delete(deviceId);
835
908
  this.resetProtocolV2Frames(deviceId);
836
909
  this.notificationTokens.delete(deviceId);
910
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
911
+ this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
912
+ this.runPromise = null;
913
+ this.runPromiseDeviceId = null;
914
+ }
837
915
  const notifyCleanup = this.notificationCleanups.get(deviceId);
838
916
  if (notifyCleanup) {
839
917
  notifyCleanup();
840
918
  this.notificationCleanups.delete(deviceId);
841
919
  }
842
- const disconnectCleanup = this.disconnectCleanups.get(deviceId);
843
- if (disconnectCleanup) {
844
- disconnectCleanup();
845
- this.disconnectCleanups.delete(deviceId);
920
+ const mtuCleanup = this.mtuCleanups.get(deviceId);
921
+ if (mtuCleanup) {
922
+ mtuCleanup();
923
+ this.mtuCleanups.delete(deviceId);
846
924
  }
847
925
  }
848
926
  init(logger, emitter) {
@@ -852,17 +930,43 @@ class ElectronBleTransport {
852
930
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
853
931
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble BLE API is not available. Please ensure you are running in Electron with Noble support.');
854
932
  }
933
+ this.subscribeHostDisconnects();
855
934
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[Electron BLE] Transport initialized');
856
935
  }
936
+ subscribeHostDisconnects() {
937
+ var _a, _b, _c;
938
+ (_a = this.hostDisconnectCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
939
+ this.hostDisconnectCleanup = (_c = (_b = window.desktopApi) === null || _b === void 0 ? void 0 : _b.nobleBle) === null || _c === void 0 ? void 0 : _c.onDeviceDisconnected((disconnectedDevice) => {
940
+ var _a, _b, _c;
941
+ const uuid = disconnectedDevice === null || disconnectedDevice === void 0 ? void 0 : disconnectedDevice.id;
942
+ if (!uuid)
943
+ return;
944
+ this.cleanupDeviceState(uuid);
945
+ (_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);
946
+ (_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit(transport.TRANSPORT_EVENT.DEVICE_DISCONNECT, {
947
+ name: disconnectedDevice.name,
948
+ id: uuid,
949
+ connectId: uuid,
950
+ });
951
+ });
952
+ }
857
953
  configure(signedData) {
858
954
  this._messages = parseConfigure(signedData);
859
955
  this.configured = true;
860
956
  }
861
957
  configureProtocolV2(signedData) {
958
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
959
+ if (this.protocolV2SchemaConfiguration === configuration) {
960
+ return;
961
+ }
962
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
862
963
  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); });
964
+ this.protocolV2SchemaConfiguration = configuration;
965
+ if (isReconfiguration) {
966
+ this.protocolV2Links
967
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
968
+ .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] schema link cleanup failed:', error); });
969
+ }
866
970
  }
867
971
  listen() {
868
972
  return __awaiter(this, void 0, void 0, function* () {
@@ -880,10 +984,6 @@ class ElectronBleTransport {
880
984
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
881
985
  for (const dev of devices) {
882
986
  (_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
987
  }
888
988
  return devices.map(device => toBleDescriptor(device));
889
989
  }
@@ -894,7 +994,7 @@ class ElectronBleTransport {
894
994
  });
895
995
  }
896
996
  acquire(input) {
897
- var _a, _b, _c, _d, _e, _f;
997
+ var _a, _b, _c, _d, _e, _f, _g;
898
998
  return __awaiter(this, void 0, void 0, function* () {
899
999
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
900
1000
  if (!uuid) {
@@ -906,8 +1006,8 @@ class ElectronBleTransport {
906
1006
  if (forceCleanRunPromise && this.runPromise) {
907
1007
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
908
1008
  this.runPromise.reject(error);
909
- this.rejectAllProtocolV2Frames(error);
910
1009
  this.runPromise = null;
1010
+ this.runPromiseDeviceId = null;
911
1011
  }
912
1012
  try {
913
1013
  if (!((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle)) {
@@ -919,7 +1019,7 @@ class ElectronBleTransport {
919
1019
  }
920
1020
  const protocolHint = expectedProtocol
921
1021
  ? undefined
922
- : (_b = this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(device.name);
1022
+ : (_b = input.protocolHint) !== null && _b !== void 0 ? _b : this.deviceProtocolHints.get(uuid);
923
1023
  if (protocolHint) {
924
1024
  this.deviceProtocolHints.set(uuid, protocolHint);
925
1025
  }
@@ -930,52 +1030,64 @@ class ElectronBleTransport {
930
1030
  catch (error) {
931
1031
  this.handleBluetoothError(error);
932
1032
  }
1033
+ const mtuCleanup = this.createMtuSubscription(uuid);
1034
+ if (mtuCleanup) {
1035
+ this.mtuCleanups.set(uuid, mtuCleanup);
1036
+ }
933
1037
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
934
- this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler());
1038
+ this.v2Assemblers.set(uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
935
1039
  yield window.desktopApi.nobleBle.subscribe(uuid);
1040
+ yield this.refreshBlePacketCapacity(uuid);
936
1041
  const cleanup = this.createNotificationSubscription(uuid);
937
1042
  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
1043
  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
- });
1044
+ if (protocolType === 'V2') {
1045
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('[Electron BLE] Protocol V2 write configured', {
1046
+ writeMode: 'withoutResponse',
1047
+ negotiatedMtu: this.deviceMtus.get(uuid),
1048
+ packetCapacity: (_d = this.devicePacketCapacities.get(uuid)) !== null && _d !== void 0 ? _d : BLE_PACKET_SIZE_FALLBACK,
1049
+ });
1050
+ }
956
1051
  return Object.assign(Object.assign({}, toBleDescriptor({ id: device.id, name: device.name }, protocolType)), { uuid });
957
1052
  }
958
1053
  catch (error) {
959
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] acquire failed:', error);
1054
+ (_e = this.Log) === null || _e === void 0 ? void 0 : _e.error('[Electron BLE] acquire failed:', error);
960
1055
  try {
961
- if (((_e = window.desktopApi) === null || _e === void 0 ? void 0 : _e.nobleBle) && this.connectedDevices.has(uuid)) {
1056
+ if (((_f = window.desktopApi) === null || _f === void 0 ? void 0 : _f.nobleBle) && this.connectedDevices.has(uuid)) {
962
1057
  yield window.desktopApi.nobleBle.unsubscribe(uuid);
963
1058
  yield window.desktopApi.nobleBle.disconnect(uuid);
964
1059
  }
965
1060
  }
966
1061
  catch (cleanupError) {
967
- (_f = this.Log) === null || _f === void 0 ? void 0 : _f.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
1062
+ (_g = this.Log) === null || _g === void 0 ? void 0 : _g.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
968
1063
  }
969
1064
  this.cleanupDeviceState(uuid);
970
1065
  throw error;
971
1066
  }
972
1067
  });
973
1068
  }
974
- release(id) {
975
- var _a, _b;
1069
+ release(id, _onclose, keepSession) {
1070
+ var _a;
976
1071
  return __awaiter(this, void 0, void 0, function* () {
977
1072
  try {
978
1073
  yield this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released');
1074
+ yield this.releaseLogical(id, keepSession);
1075
+ }
1076
+ catch (error) {
1077
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] release failed:', error);
1078
+ this.cleanupDeviceState(id);
1079
+ }
1080
+ });
1081
+ }
1082
+ disconnect(id) {
1083
+ return __awaiter(this, void 0, void 0, function* () {
1084
+ return this.releaseNative(id);
1085
+ });
1086
+ }
1087
+ releaseNative(id) {
1088
+ var _a, _b;
1089
+ return __awaiter(this, void 0, void 0, function* () {
1090
+ try {
979
1091
  if (this.connectedDevices.has(id)) {
980
1092
  if ((_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) {
981
1093
  yield window.desktopApi.nobleBle.unsubscribe(id);
@@ -990,11 +1102,36 @@ class ElectronBleTransport {
990
1102
  }
991
1103
  });
992
1104
  }
993
- createProtocolMismatchError(expected) {
994
- return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
1105
+ releaseLogical(id, keepSession) {
1106
+ var _a, _b, _c, _d;
1107
+ return __awaiter(this, void 0, void 0, function* () {
1108
+ try {
1109
+ if (!this.connectedDevices.has(id))
1110
+ return;
1111
+ this.cleanupDeviceState(id);
1112
+ const release = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.release;
1113
+ if (!release) {
1114
+ if (!this.warnedMissingRelease) {
1115
+ this.warnedMissingRelease = true;
1116
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.error('[Electron BLE] desktopApi.nobleBle.release is missing from the preload bridge; ' +
1117
+ 'keep-alive idle countdown will never start — map NOBLE_BLE_RELEASE in the desktop preload');
1118
+ }
1119
+ return;
1120
+ }
1121
+ yield release(id, keepSession);
1122
+ }
1123
+ catch (error) {
1124
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.error('[Electron BLE] logical release failed:', error);
1125
+ this.cleanupDeviceState(id);
1126
+ }
1127
+ });
1128
+ }
1129
+ createProtocolMismatchError(expected, uuid) {
1130
+ const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
1131
+ 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
1132
  }
996
1133
  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');
1134
+ 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
1135
  }
999
1136
  clearProbeProtocol(uuid, protocol) {
1000
1137
  if (this.deviceProtocol.get(uuid) === protocol) {
@@ -1010,12 +1147,16 @@ class ElectronBleTransport {
1010
1147
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1011
1148
  return 'V1';
1012
1149
  }
1013
- throw this.createProtocolMismatchError(expectedProtocol);
1150
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1014
1151
  }
1015
1152
  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';
1153
+ if (yield this.probeProtocolV2(uuid)) {
1154
+ this.deviceProtocol.set(uuid, 'V2');
1155
+ this.confirmedProtocolV2.add(uuid);
1156
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1157
+ return 'V2';
1158
+ }
1159
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
1019
1160
  }
1020
1161
  const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1021
1162
  for (let i = 0; i < probeOrder.length; i += 1) {
@@ -1026,6 +1167,9 @@ class ElectronBleTransport {
1026
1167
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1027
1168
  if (detected) {
1028
1169
  this.deviceProtocol.set(uuid, protocol);
1170
+ if (protocol === 'V2') {
1171
+ this.confirmedProtocolV2.add(uuid);
1172
+ }
1029
1173
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
1030
1174
  return protocol;
1031
1175
  }
@@ -1049,38 +1193,22 @@ class ElectronBleTransport {
1049
1193
  });
1050
1194
  }
1051
1195
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
1052
- var _a, _b, _c, _d, _e, _f, _g;
1196
+ var _a;
1053
1197
  return __awaiter(this, void 0, void 0, function* () {
1054
1198
  yield this.protocolV2Links.invalidateLink(uuid, `Reset notify state after Protocol ${protocol} probe`);
1055
1199
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1056
1200
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1057
1201
  this.resetProtocolV2Frames(uuid);
1058
- if (this.runPromise) {
1202
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
1059
1203
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
1060
1204
  this.runPromise.reject(error);
1061
1205
  this.runPromise = null;
1206
+ this.runPromiseDeviceId = null;
1062
1207
  }
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;
1208
+ if (!this.notificationCleanups.has(uuid)) {
1209
+ const cleanup = this.createNotificationSubscription(uuid);
1210
+ this.notificationCleanups.set(uuid, cleanup);
1081
1211
  }
1082
- const cleanup = this.createNotificationSubscription(uuid);
1083
- this.notificationCleanups.set(uuid, cleanup);
1084
1212
  });
1085
1213
  }
1086
1214
  probeProtocolV1(uuid) {
@@ -1091,12 +1219,12 @@ class ElectronBleTransport {
1091
1219
  }
1092
1220
  try {
1093
1221
  this.deviceProtocol.set(uuid, 'V1');
1094
- yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1222
+ yield this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1095
1223
  return true;
1096
1224
  }
1097
1225
  catch (error) {
1098
1226
  this.clearProbeProtocol(uuid, 'V1');
1099
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
1227
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
1100
1228
  return false;
1101
1229
  }
1102
1230
  });
@@ -1126,25 +1254,6 @@ class ElectronBleTransport {
1126
1254
  return detected;
1127
1255
  });
1128
1256
  }
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
1257
  writeOnce(uuid, hexData) {
1149
1258
  var _a;
1150
1259
  return __awaiter(this, void 0, void 0, function* () {
@@ -1152,7 +1261,51 @@ class ElectronBleTransport {
1152
1261
  if (!nobleBle) {
1153
1262
  throw new Error('Noble BLE API not available');
1154
1263
  }
1155
- yield nobleBle.write(uuid, hexData);
1264
+ yield nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
1265
+ });
1266
+ }
1267
+ refreshBlePacketCapacity(uuid) {
1268
+ var _a, _b;
1269
+ return __awaiter(this, void 0, void 0, function* () {
1270
+ const device = yield ((_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.getDevice(uuid));
1271
+ this.updateBlePacketCapacity(uuid, device === null || device === void 0 ? void 0 : device.mtu);
1272
+ });
1273
+ }
1274
+ updateBlePacketCapacity(uuid, mtu) {
1275
+ const packetCapacity = resolveBlePacketCapacity(mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
1276
+ if (typeof mtu === 'number') {
1277
+ this.deviceMtus.set(uuid, mtu);
1278
+ }
1279
+ else {
1280
+ this.deviceMtus.delete(uuid);
1281
+ }
1282
+ this.devicePacketCapacities.set(uuid, packetCapacity);
1283
+ }
1284
+ createMtuSubscription(uuid) {
1285
+ var _a, _b;
1286
+ const onMtuChanged = (_b = (_a = window.desktopApi) === null || _a === void 0 ? void 0 : _a.nobleBle) === null || _b === void 0 ? void 0 : _b.onMtuChanged;
1287
+ if (!onMtuChanged)
1288
+ return undefined;
1289
+ return onMtuChanged(device => {
1290
+ if (device.id === uuid) {
1291
+ this.updateBlePacketCapacity(uuid, device.mtu);
1292
+ }
1293
+ });
1294
+ }
1295
+ writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration) {
1296
+ var _a;
1297
+ const packetCapacity = (_a = this.devicePacketCapacities.get(uuid)) !== null && _a !== void 0 ? _a : BLE_PACKET_SIZE_FALLBACK;
1298
+ const shouldPace = !context.highThroughput;
1299
+ return transport.writeProtocolV2BleFrame({
1300
+ frame,
1301
+ packetCapacity,
1302
+ assertActive: assertCurrentGeneration,
1303
+ signal: context.signal,
1304
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1305
+ initialDelayMs: shouldPace && frame.length <= packetCapacity ? BLE_WRITE_DELAY_MS : 0,
1306
+ burstSize: shouldPace ? 1 : undefined,
1307
+ burstPauseMs: shouldPace ? BLE_WRITE_DELAY_MS : 0,
1308
+ writePacket: packet => this.writeOnce(uuid, transport.bytesToHex(packet)),
1156
1309
  });
1157
1310
  }
1158
1311
  handleNotification(deviceId, hexData) {
@@ -1163,7 +1316,7 @@ class ElectronBleTransport {
1163
1316
  if (this.deviceProtocol.get(deviceId) === 'V2') {
1164
1317
  this.rejectProtocolV2Frames(deviceId, error);
1165
1318
  }
1166
- else if (this.runPromise) {
1319
+ else if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1167
1320
  this.runPromise.reject(error);
1168
1321
  }
1169
1322
  return;
@@ -1215,16 +1368,8 @@ class ElectronBleTransport {
1215
1368
  }
1216
1369
  this.getProtocolV2FrameQueue(uuid).push(frame);
1217
1370
  }
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
1371
  resetProtocolV2Frames(uuid) {
1226
- this.v2FrameQueues.delete(uuid);
1227
- this.v2FramePromises.delete(uuid);
1372
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1228
1373
  }
1229
1374
  rejectProtocolV2Frames(uuid, error) {
1230
1375
  this.v2FrameQueues.delete(uuid);
@@ -1257,17 +1402,19 @@ class ElectronBleTransport {
1257
1402
  const result = this.processProtocolV1Notification(deviceId, hexData);
1258
1403
  if (result.error) {
1259
1404
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
1260
- if (this.runPromise) {
1405
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
1261
1406
  this.runPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError));
1262
1407
  }
1263
1408
  return;
1264
1409
  }
1265
- if (result.isComplete && result.completePacket && this.runPromise) {
1410
+ if (result.isComplete &&
1411
+ result.completePacket &&
1412
+ this.runPromise &&
1413
+ this.runPromiseDeviceId === deviceId) {
1266
1414
  this.runPromise.resolve(result.completePacket);
1267
1415
  }
1268
1416
  }
1269
1417
  call(uuid, name, data, options) {
1270
- var _a;
1271
1418
  return __awaiter(this, void 0, void 0, function* () {
1272
1419
  if (!this._messages) {
1273
1420
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
@@ -1279,15 +1426,21 @@ class ElectronBleTransport {
1279
1426
  if (!protocol) {
1280
1427
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
1281
1428
  }
1282
- if (!shouldSuppressHighVolumeCallLog(name)) {
1283
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', createTransportCallLog(name, protocol));
1284
- }
1285
1429
  if (protocol === 'V2') {
1286
1430
  return this.callProtocolV2(uuid, name, data, options);
1287
1431
  }
1288
1432
  return this.callProtocolV1(uuid, name, data, options);
1289
1433
  });
1290
1434
  }
1435
+ post(uuid, name, data) {
1436
+ return __awaiter(this, void 0, void 0, function* () {
1437
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1438
+ yield this.protocolV2Links.sendFlowControl(uuid, () => this.createProtocolV2Adapter(uuid), name, data);
1439
+ return;
1440
+ }
1441
+ yield this.callProtocolV1(uuid, name, data);
1442
+ });
1443
+ }
1291
1444
  callProtocolV1(uuid, name, data, options) {
1292
1445
  var _a, _b;
1293
1446
  return __awaiter(this, void 0, void 0, function* () {
@@ -1301,6 +1454,7 @@ class ElectronBleTransport {
1301
1454
  const runPromise = hdShared.createDeferred();
1302
1455
  runPromise.promise.catch(() => undefined);
1303
1456
  this.runPromise = runPromise;
1457
+ this.runPromiseDeviceId = uuid;
1304
1458
  const messages = this._messages;
1305
1459
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1306
1460
  let timeout;
@@ -1339,6 +1493,17 @@ class ElectronBleTransport {
1339
1493
  }
1340
1494
  catch (e) {
1341
1495
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V1 call error:', e);
1496
+ const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1497
+ if ((e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError) {
1498
+ this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
1499
+ const notifyCleanup = this.notificationCleanups.get(uuid);
1500
+ notifyCleanup === null || notifyCleanup === void 0 ? void 0 : notifyCleanup();
1501
+ this.notificationCleanups.delete(uuid);
1502
+ this.notificationTokens.delete(uuid);
1503
+ if (!isProbeTimeout) {
1504
+ yield this.releaseNative(uuid);
1505
+ }
1506
+ }
1342
1507
  throw e;
1343
1508
  }
1344
1509
  finally {
@@ -1346,22 +1511,23 @@ class ElectronBleTransport {
1346
1511
  clearTimeout(timeout);
1347
1512
  if (this.runPromise === runPromise) {
1348
1513
  this.runPromise = null;
1514
+ this.runPromiseDeviceId = null;
1349
1515
  }
1350
1516
  }
1351
1517
  });
1352
1518
  }
1353
1519
  callProtocolV2(uuid, name, data, options) {
1354
- var _a, _b;
1520
+ var _a;
1355
1521
  return __awaiter(this, void 0, void 0, function* () {
1356
1522
  if (!this._messages || !this._messagesV2) {
1357
1523
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
1358
1524
  }
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 });
1525
+ const callOptions = options;
1360
1526
  try {
1361
1527
  return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, callOptions);
1362
1528
  }
1363
1529
  catch (e) {
1364
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.error('[Electron BLE] Protocol V2 call error:', e);
1530
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.error('[Electron BLE] Protocol V2 call error:', e);
1365
1531
  throw e;
1366
1532
  }
1367
1533
  });
@@ -1376,6 +1542,7 @@ class ElectronBleTransport {
1376
1542
  };
1377
1543
  return {
1378
1544
  router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
1545
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1379
1546
  generation,
1380
1547
  prepareCall: () => {
1381
1548
  var _a;
@@ -1383,10 +1550,7 @@ class ElectronBleTransport {
1383
1550
  (_a = this.v2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
1384
1551
  this.resetProtocolV2Frames(uuid);
1385
1552
  },
1386
- writeFrame: (frame) => {
1387
- assertCurrentGeneration();
1388
- return this.writeWithChunking(uuid, transport.bytesToHex(frame));
1389
- },
1553
+ writeFrame: (frame, context) => this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
1390
1554
  readFrame: () => __awaiter(this, void 0, void 0, function* () {
1391
1555
  assertCurrentGeneration();
1392
1556
  const rxFrame = yield this.readProtocolV2Frame(uuid);
@@ -1406,6 +1570,7 @@ class ElectronBleTransport {
1406
1570
  };
1407
1571
  }
1408
1572
  processProtocolV1Notification(deviceId, hexData) {
1573
+ var _a;
1409
1574
  try {
1410
1575
  if (typeof hexData !== 'string') {
1411
1576
  return { isComplete: false, error: 'Invalid hexData type' };
@@ -1424,6 +1589,10 @@ class ElectronBleTransport {
1424
1589
  bufferState.buffer = [...data.subarray(3)];
1425
1590
  }
1426
1591
  else {
1592
+ if (bufferState.buffer.length === 0) {
1593
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[Electron BLE] Orphan continuation chunk discarded');
1594
+ return { isComplete: false };
1595
+ }
1427
1596
  bufferState.buffer = bufferState.buffer.concat([...data]);
1428
1597
  }
1429
1598
  if (bufferState.buffer.length - transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) {