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

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/webusb.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /// <reference types="w3c-web-usb" />
2
+ /// <reference types="node" />
2
3
  import transport, { ProtocolV2UsbTransportBase } from '@onekeyfe/hd-transport';
4
+ import type EventEmitter from 'events';
3
5
  import type { AcquireInput, OneKeyDeviceInfoBase, ProtocolType, ProtocolV2CallContext, ProtocolV2Schemas, TransportCallOptions } from '@onekeyfe/hd-transport';
4
6
  export interface DeviceInfo extends OneKeyDeviceInfoBase {
5
7
  path: string;
@@ -11,19 +13,26 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
11
13
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
12
14
  private protocolV2SchemaSource;
13
15
  private deviceProtocol;
16
+ private confirmedDeviceProtocols;
14
17
  private deviceProtocolHints;
18
+ private staleProtocolPaths;
19
+ private acquiredPaths;
20
+ private probedDeviceObjects;
15
21
  private deviceEndpoints;
16
22
  name: string;
17
23
  stopped: boolean;
18
24
  configured: boolean;
19
25
  Log?: any;
20
26
  usb?: USB;
27
+ emitter?: EventEmitter;
21
28
  deviceList: Array<DeviceInfo>;
22
29
  configurationId: number;
23
30
  endpointId: number;
24
31
  interfaceId: number;
25
32
  constructor();
26
- init(logger: any): void;
33
+ init(logger: any, emitter?: EventEmitter): void;
34
+ emitDeviceDisconnect(path: string, device?: USBDevice): void;
35
+ markProtocolStale(path: string): void;
27
36
  configure(signedData: any): void;
28
37
  configureProtocolV2(signedData: any): void;
29
38
  promptDeviceAccess(): Promise<USBDevice | null>;
@@ -40,6 +49,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
40
49
  connectToDevice(path: string, first: boolean): Promise<void>;
41
50
  private closeOpenDevice;
42
51
  private clearEndpointHalt;
52
+ private assertAcquired;
43
53
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
44
54
  private getErrorMessage;
45
55
  private isRetryablePacketIoError;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.0-alpha.99",
3
+ "version": "1.2.0",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.99",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.99"
24
+ "@onekeyfe/hd-shared": "1.2.0",
25
+ "@onekeyfe/hd-transport": "1.2.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.0-alpha.99",
28
+ "@onekeyfe/hd-transport-electron": "1.2.0",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "8924c0e509f84675a2c33c0062166967f5bcb2d0"
32
+ "gitHead": "cc51df0a415bead21304ad1d8bc92b5312c91344"
33
33
  }
@@ -6,19 +6,23 @@ import transport, {
6
6
  ProtocolV2LinkManager,
7
7
  TRANSPORT_EVENT,
8
8
  bytesToHex,
9
+ detectProtocolV2LinkDisabledError,
9
10
  hexToBytes,
11
+ isProtocolV2LinkDisabledError,
10
12
  probeProtocolV2 as probeProtocolV2Helper,
11
13
  writeProtocolV2BleFrame,
12
14
  } from '@onekeyfe/hd-transport';
13
15
  import {
16
+ EBleDisconnectReason,
14
17
  ERRORS,
15
18
  HardwareErrorCode,
16
19
  HardwareErrorCodeMessage,
17
20
  createDeferred,
21
+ isBleStaleBondErrorText,
22
+ isBleStaleBondHardwareError,
18
23
  isHeaderChunk,
19
24
  } from '@onekeyfe/hd-shared';
20
25
 
21
- import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
22
26
  import { resolveBlePacketCapacity } from './ble-packet-capacity';
23
27
 
24
28
  import type { Deferred } from '@onekeyfe/hd-shared';
@@ -52,10 +56,6 @@ interface PacketProcessResult {
52
56
  error?: string;
53
57
  }
54
58
 
55
- function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
56
- return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
57
- }
58
-
59
59
  const toBleDescriptor = (
60
60
  device: { id: string; name: string | null },
61
61
  protocolType?: ProtocolType
@@ -72,7 +72,7 @@ const toBleDescriptor = (
72
72
  const BLE_PACKET_SIZE_FALLBACK = 192;
73
73
  const BLE_PACKET_SIZE_MAXIMUM = 244;
74
74
  const BLE_WRITE_DELAY_MS = 5;
75
- const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
75
+ const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
76
76
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
77
77
 
78
78
  /**
@@ -106,6 +106,9 @@ export default class ElectronBleTransport {
106
106
 
107
107
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
108
108
 
109
+ /** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
110
+ private confirmedProtocolV2 = new Set<string>();
111
+
109
112
  private deviceMtus: Map<string, number> = new Map();
110
113
 
111
114
  private devicePacketCapacities: Map<string, number> = new Map();
@@ -146,13 +149,47 @@ export default class ElectronBleTransport {
146
149
 
147
150
  private mtuCleanups: Map<string, () => void> = new Map();
148
151
 
149
- private disconnectCleanups: Map<string, () => void> = new Map();
152
+ /**
153
+ * Transport-lifetime subscription to host BLE disconnects.
154
+ *
155
+ * This must NOT be scoped to acquire()/release(): a logical release keeps the
156
+ * native link alive for the keep-alive window, so a device that drops while
157
+ * idle would otherwise go unobserved and consumers would never learn it left
158
+ * (OK-60486).
159
+ */
160
+ private hostDisconnectCleanup?: () => void;
150
161
 
151
162
  private notificationTokens: Map<string, number> = new Map();
152
163
 
153
164
  private nextNotificationToken = 1;
154
165
 
155
- private handleBluetoothError(error: any): never {
166
+ private toStaleBondError(error: unknown): Error | null {
167
+ if (isBleStaleBondHardwareError(error)) {
168
+ return error as Error;
169
+ }
170
+ const errorMessage =
171
+ error && typeof error === 'object' && 'message' in error
172
+ ? String((error as { message?: unknown }).message ?? '')
173
+ : String(error ?? '');
174
+ if (!isBleStaleBondErrorText(errorMessage)) {
175
+ return null;
176
+ }
177
+ const normalizedErrorMessage = errorMessage.toLowerCase();
178
+ return ERRORS.TypedError(
179
+ normalizedErrorMessage.includes('peer removed pairing information')
180
+ ? HardwareErrorCode.BlePeerRemovedPairingInformation
181
+ : HardwareErrorCode.BleDeviceBondError,
182
+ errorMessage
183
+ );
184
+ }
185
+
186
+ private handleBluetoothError(error: any, mapProtocolV2StaleBond = false): never {
187
+ if (mapProtocolV2StaleBond) {
188
+ const staleBondError = this.toStaleBondError(error);
189
+ if (staleBondError) {
190
+ throw staleBondError;
191
+ }
192
+ }
156
193
  if (error && typeof error === 'object') {
157
194
  if ('code' in error) {
158
195
  if (error.code === HardwareErrorCode.BlePoweredOff) {
@@ -214,12 +251,6 @@ export default class ElectronBleTransport {
214
251
  mtuCleanup();
215
252
  this.mtuCleanups.delete(deviceId);
216
253
  }
217
-
218
- const disconnectCleanup = this.disconnectCleanups.get(deviceId);
219
- if (disconnectCleanup) {
220
- disconnectCleanup();
221
- this.disconnectCleanups.delete(deviceId);
222
- }
223
254
  }
224
255
 
225
256
  init(logger: any, emitter?: EventEmitter) {
@@ -233,9 +264,48 @@ export default class ElectronBleTransport {
233
264
  );
234
265
  }
235
266
 
267
+ this.subscribeHostDisconnects();
268
+
236
269
  this.Log?.debug('[Electron BLE] Transport initialized');
237
270
  }
238
271
 
272
+ /**
273
+ * One host subscription for the whole transport lifetime. init() can run
274
+ * again after an SDK reset, so drop the previous listener first rather than
275
+ * stacking duplicates.
276
+ */
277
+ private subscribeHostDisconnects() {
278
+ this.hostDisconnectCleanup?.();
279
+ this.hostDisconnectCleanup = window.desktopApi?.nobleBle?.onDeviceDisconnected(
280
+ (disconnectedDevice: { id: string; name: string; reason?: EBleDisconnectReason }) => {
281
+ const uuid = disconnectedDevice?.id;
282
+ if (!uuid) return;
283
+
284
+ // The link is gone, so renderer-side state must go with it; the next
285
+ // acquire reconnects from scratch.
286
+ this.cleanupDeviceState(uuid);
287
+
288
+ // Every link drop is reported, including the main process reclaiming
289
+ // an idle link on its keep-alive timer: consumers track whether a BLE
290
+ // link is live, not whether the peripheral is theoretically in range,
291
+ // and a link we closed ourselves is still a closed link. Nothing
292
+ // reconnects on its own, so this settles once until the user acts.
293
+ // `reason` is carried for diagnostics only — behaviour is uniform.
294
+ this.Log?.debug(
295
+ '[Electron BLE] Device link dropped:',
296
+ uuid,
297
+ disconnectedDevice.reason ?? EBleDisconnectReason.DeviceDisconnected
298
+ );
299
+
300
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
301
+ name: disconnectedDevice.name,
302
+ id: uuid,
303
+ connectId: uuid,
304
+ });
305
+ }
306
+ );
307
+ }
308
+
239
309
  configure(signedData: any) {
240
310
  this._messages = parseConfigure(signedData);
241
311
  this.configured = true;
@@ -270,10 +340,6 @@ export default class ElectronBleTransport {
270
340
  this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
271
341
  for (const dev of devices) {
272
342
  this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
273
- const protocolHint = inferProtocolHintFromDeviceName(dev.name);
274
- if (protocolHint) {
275
- this.deviceProtocolHints.set(dev.id, protocolHint);
276
- }
277
343
  }
278
344
  return devices.map(device => toBleDescriptor(device));
279
345
  } catch (error) {
@@ -284,6 +350,9 @@ export default class ElectronBleTransport {
284
350
 
285
351
  async acquire(input: BleAcquireInput) {
286
352
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
353
+ const shouldMapProtocolV2StaleBond = expectedProtocol
354
+ ? expectedProtocol === 'V2'
355
+ : this.confirmedProtocolV2.has(uuid);
287
356
 
288
357
  if (!uuid) {
289
358
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
@@ -311,9 +380,7 @@ export default class ElectronBleTransport {
311
380
  }
312
381
  const protocolHint = expectedProtocol
313
382
  ? undefined
314
- : input.protocolHint ??
315
- this.deviceProtocolHints.get(uuid) ??
316
- inferProtocolHintFromDeviceName(device.name);
383
+ : input.protocolHint ?? this.deviceProtocolHints.get(uuid);
317
384
  if (protocolHint) {
318
385
  this.deviceProtocolHints.set(uuid, protocolHint);
319
386
  }
@@ -322,7 +389,7 @@ export default class ElectronBleTransport {
322
389
  await window.desktopApi.nobleBle.connect(uuid);
323
390
  this.connectedDevices.add(uuid);
324
391
  } catch (error) {
325
- this.handleBluetoothError(error);
392
+ this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
326
393
  }
327
394
 
328
395
  const mtuCleanup = this.createMtuSubscription(uuid);
@@ -333,12 +400,15 @@ export default class ElectronBleTransport {
333
400
  this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
334
401
  this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
335
402
 
336
- await window.desktopApi.nobleBle.subscribe(uuid);
403
+ try {
404
+ await window.desktopApi.nobleBle.subscribe(uuid);
405
+ } catch (error) {
406
+ this.handleBluetoothError(error, shouldMapProtocolV2StaleBond);
407
+ }
337
408
  await this.refreshBlePacketCapacity(uuid);
338
409
 
339
410
  const cleanup = this.createNotificationSubscription(uuid);
340
411
  this.notificationCleanups.set(uuid, cleanup);
341
- const connectionToken = this.notificationTokens.get(uuid);
342
412
 
343
413
  const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
344
414
  if (protocolType === 'V2') {
@@ -349,23 +419,6 @@ export default class ElectronBleTransport {
349
419
  });
350
420
  }
351
421
 
352
- const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
353
- (disconnectedDevice: any) => {
354
- if (
355
- disconnectedDevice.id === uuid &&
356
- this.notificationTokens.get(uuid) === connectionToken
357
- ) {
358
- this.cleanupDeviceState(uuid);
359
- this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
360
- name: disconnectedDevice.name,
361
- id: disconnectedDevice.id,
362
- connectId: disconnectedDevice.id,
363
- });
364
- }
365
- }
366
- );
367
- this.disconnectCleanups.set(uuid, disconnectCleanup);
368
-
369
422
  return {
370
423
  ...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
371
424
  uuid,
@@ -443,9 +496,12 @@ export default class ElectronBleTransport {
443
496
  }
444
497
  }
445
498
 
446
- private createProtocolMismatchError(expected: ProtocolType) {
499
+ private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
500
+ // A generic Ping miss is not a bond failure. Only a later miss after this
501
+ // endpoint already answered V2, or a native encryption/pairing error, is.
502
+ const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
447
503
  return ERRORS.TypedError(
448
- HardwareErrorCode.RuntimeError,
504
+ isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
449
505
  `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
450
506
  );
451
507
  }
@@ -468,22 +524,30 @@ export default class ElectronBleTransport {
468
524
  expectedProtocol?: ProtocolType,
469
525
  protocolHint?: ProtocolType
470
526
  ): Promise<ProtocolType> {
527
+ // A declared V1 is taken at face value, as the React Native transport
528
+ // already does on iOS: the caller reads the protocol off its own device
529
+ // record, so probing re-asks a question that is already answered and adds a
530
+ // round trip to every cold connect. It also fails in a way that costs the
531
+ // session: a device whose protocol session has stalled ignores the probe
532
+ // frame, and the timeout became a protocol-mismatch error that stopped Core
533
+ // from ever sending Initialize — the one frame such a device still answers,
534
+ // and how it gets revived. A declared V2 keeps probing, matching iOS, so a
535
+ // USB-priority "link disabled" surfaces here rather than as an unmapped
536
+ // error later. An undeclared protocol still goes through full detection.
471
537
  if (expectedProtocol === 'V1') {
472
- if (await this.probeProtocolV1(uuid)) {
473
- this.deviceProtocol.set(uuid, 'V1');
474
- this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
475
- return 'V1';
476
- }
477
- throw this.createProtocolMismatchError(expectedProtocol);
538
+ this.deviceProtocol.set(uuid, 'V1');
539
+ this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected, no probe)`);
540
+ return 'V1';
478
541
  }
479
542
 
480
543
  if (expectedProtocol === 'V2') {
481
544
  if (await this.probeProtocolV2(uuid)) {
482
545
  this.deviceProtocol.set(uuid, 'V2');
546
+ this.confirmedProtocolV2.add(uuid);
483
547
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
484
548
  return 'V2';
485
549
  }
486
- throw this.createProtocolMismatchError(expectedProtocol);
550
+ throw this.createProtocolMismatchError(expectedProtocol, uuid);
487
551
  }
488
552
 
489
553
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
@@ -494,13 +558,17 @@ export default class ElectronBleTransport {
494
558
  for (let i = 0; i < probeOrder.length; i += 1) {
495
559
  const protocol = probeOrder[i];
496
560
  if (i > 0) {
497
- // Reset subscriptions and buffers after a failed probe before trying another protocol.
561
+ // Keep the physical BLE link while switching probes. Reconnecting here can
562
+ // summon a second OS pairing prompt for a device that is still onboarding.
498
563
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
499
564
  }
500
565
  const detected =
501
566
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
502
567
  if (detected) {
503
568
  this.deviceProtocol.set(uuid, protocol);
569
+ if (protocol === 'V2') {
570
+ this.confirmedProtocolV2.add(uuid);
571
+ }
504
572
  this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
505
573
  return protocol;
506
574
  }
@@ -541,36 +609,13 @@ export default class ElectronBleTransport {
541
609
  this.runPromiseDeviceId = null;
542
610
  }
543
611
 
544
- const notifyCleanup = this.notificationCleanups.get(uuid);
545
- if (notifyCleanup) {
546
- notifyCleanup();
547
- this.notificationCleanups.delete(uuid);
548
- }
549
- this.notificationTokens.delete(uuid);
550
-
551
- try {
552
- await window.desktopApi?.nobleBle?.unsubscribe(uuid);
553
- } catch (error) {
554
- this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
555
- }
556
- try {
557
- await window.desktopApi?.nobleBle?.disconnect(uuid);
558
- } catch (error) {
559
- this.Log?.debug(`[Electron BLE] disconnect after Protocol ${protocol} probe failed:`, error);
560
- }
561
- this.connectedDevices.delete(uuid);
562
- try {
563
- await window.desktopApi?.nobleBle?.connect(uuid);
564
- this.connectedDevices.add(uuid);
565
- await window.desktopApi?.nobleBle?.subscribe(uuid);
566
- await this.refreshBlePacketCapacity(uuid);
567
- } catch (error) {
568
- this.Log?.debug(`[Electron BLE] reconnect after Protocol ${protocol} probe failed:`, error);
569
- throw error;
612
+ // A timed-out V1 probe retires its renderer notification token so a late V1
613
+ // response cannot satisfy the V2 probe. Install a fresh listener without
614
+ // touching the native GATT subscription or physical connection.
615
+ if (!this.notificationCleanups.has(uuid)) {
616
+ const cleanup = this.createNotificationSubscription(uuid);
617
+ this.notificationCleanups.set(uuid, cleanup);
570
618
  }
571
-
572
- const cleanup = this.createNotificationSubscription(uuid);
573
- this.notificationCleanups.set(uuid, cleanup);
574
619
  }
575
620
 
576
621
  private async probeProtocolV1(uuid: string) {
@@ -587,6 +632,9 @@ export default class ElectronBleTransport {
587
632
  } catch (error) {
588
633
  this.clearProbeProtocol(uuid, 'V1');
589
634
  this.Log?.debug('[Electron BLE] Protocol V1 GetFeatures probe failed:', error);
635
+ if (isProtocolV2LinkDisabledError(error)) {
636
+ throw error;
637
+ }
590
638
  return false;
591
639
  }
592
640
  }
@@ -607,6 +655,8 @@ export default class ElectronBleTransport {
607
655
  this.v2Assemblers.get(uuid)?.reset();
608
656
  this.resetProtocolV2Frames(uuid);
609
657
  },
658
+ shouldRethrow: error =>
659
+ isBleStaleBondHardwareError(error) || isProtocolV2LinkDisabledError(error),
610
660
  });
611
661
  if (!detected) {
612
662
  this.clearProbeProtocol(uuid, 'V2');
@@ -620,7 +670,15 @@ export default class ElectronBleTransport {
620
670
  throw new Error('Noble BLE API not available');
621
671
  }
622
672
 
623
- await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
673
+ try {
674
+ await nobleBle.write(uuid, hexData, { pacingDelayMs: 0 });
675
+ } catch (error) {
676
+ const staleBondError = this.toStaleBondError(error);
677
+ if (staleBondError) {
678
+ throw staleBondError;
679
+ }
680
+ throw error;
681
+ }
624
682
  }
625
683
 
626
684
  private async refreshBlePacketCapacity(uuid: string): Promise<void> {
@@ -694,9 +752,29 @@ export default class ElectronBleTransport {
694
752
  this.handleProtocolV2Notification(deviceId, hexData);
695
753
  return;
696
754
  }
755
+ const linkDisabledError = this.readProtocolV2LinkDisabledFailure(deviceId, hexData);
756
+ if (linkDisabledError) {
757
+ if (this.runPromise && this.runPromiseDeviceId === deviceId) {
758
+ this.runPromise.reject(linkDisabledError);
759
+ }
760
+ return;
761
+ }
697
762
  this.handleProtocolV1Notification(deviceId, hexData);
698
763
  }
699
764
 
765
+ private readProtocolV2LinkDisabledFailure(deviceId: string, hexData: string) {
766
+ if (!this._messages || !this._messagesV2) return undefined;
767
+
768
+ const assembler = this.v2Assemblers.get(deviceId);
769
+ if (!assembler) return undefined;
770
+
771
+ return detectProtocolV2LinkDisabledError({
772
+ schemas: { protocolV1: this._messages, protocolV2: this._messagesV2 },
773
+ assembler,
774
+ bytes: hexToBytes(hexData),
775
+ });
776
+ }
777
+
700
778
  private handleProtocolV2Notification(deviceId: string, hexData: string): void {
701
779
  try {
702
780
  const bytes = hexToBytes(hexData);
@@ -806,16 +884,25 @@ export default class ElectronBleTransport {
806
884
  `Device protocol has not been detected for ${uuid}`
807
885
  );
808
886
  }
809
- if (!shouldSuppressHighVolumeCallLog(name)) {
810
- this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
811
- }
812
-
813
887
  if (protocol === 'V2') {
814
888
  return this.callProtocolV2(uuid, name, data, options);
815
889
  }
816
890
  return this.callProtocolV1(uuid, name, data, options);
817
891
  }
818
892
 
893
+ async post(uuid: string, name: string, data: Record<string, unknown>) {
894
+ if (this.deviceProtocol.get(uuid) === 'V2') {
895
+ await this.protocolV2Links.sendFlowControl(
896
+ uuid,
897
+ () => this.createProtocolV2Adapter(uuid),
898
+ name,
899
+ data
900
+ );
901
+ return;
902
+ }
903
+ await this.callProtocolV1(uuid, name, data);
904
+ }
905
+
819
906
  private async callProtocolV1(
820
907
  uuid: string,
821
908
  name: string,