@onekeyfe/hd-transport-lowlevel 1.2.0-alpha.2 → 1.2.0-alpha.20

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.
@@ -0,0 +1,42 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const { getProtocolV1SendOptions, shouldLogFirmwareUploadProgress } = require('../src');
3
+
4
+ describe('firmware upload progress logging', () => {
5
+ test('按 5% 进度限流打印', () => {
6
+ expect(
7
+ shouldLogFirmwareUploadProgress({
8
+ percent: 9,
9
+ lastLoggedPercent: 5,
10
+ now: 5_000,
11
+ lastLoggedAt: 0,
12
+ })
13
+ ).toBe(false);
14
+
15
+ expect(
16
+ shouldLogFirmwareUploadProgress({
17
+ percent: 10,
18
+ lastLoggedPercent: 5,
19
+ now: 5_000,
20
+ lastLoggedAt: 0,
21
+ })
22
+ ).toBe(true);
23
+ });
24
+
25
+ test('进度不足 5% 时最长每 10 秒打印一次心跳', () => {
26
+ expect(
27
+ shouldLogFirmwareUploadProgress({
28
+ percent: 7,
29
+ lastLoggedPercent: 5,
30
+ now: 10_000,
31
+ lastLoggedAt: 0,
32
+ })
33
+ ).toBe(true);
34
+ });
35
+ });
36
+
37
+ describe('firmware upload write mode', () => {
38
+ test('固件上传使用带响应写入,普通命令保持默认模式', () => {
39
+ expect(getProtocolV1SendOptions('FirmwareUpload')).toEqual({ withoutResponse: false });
40
+ expect(getProtocolV1SendOptions('Initialize')).toBeUndefined();
41
+ });
42
+ });
@@ -29,21 +29,25 @@ const protocolV1Schema = {
29
29
 
30
30
  const protocolV2Schema = {
31
31
  nested: {
32
- GetProtoVersion: {
32
+ ProtocolInfoRequest: {
33
33
  fields: {},
34
34
  },
35
- ProtoVersion: {
35
+ ProtocolInfo: {
36
36
  fields: {
37
- major_version: {
37
+ version: {
38
38
  type: 'uint32',
39
39
  id: 1,
40
40
  },
41
- minor_version: {
41
+ supported_messages: {
42
+ rule: 'repeated',
42
43
  type: 'uint32',
43
44
  id: 2,
45
+ options: {
46
+ packed: false,
47
+ },
44
48
  },
45
- patch_version: {
46
- type: 'uint32',
49
+ protobuf_definition: {
50
+ type: 'string',
47
51
  id: 3,
48
52
  },
49
53
  },
@@ -66,8 +70,8 @@ const protocolV2Schema = {
66
70
  },
67
71
  MessageType: {
68
72
  values: {
69
- MessageType_GetProtoVersion: 60200,
70
- MessageType_ProtoVersion: 60201,
73
+ MessageType_ProtocolInfoRequest: 60200,
74
+ MessageType_ProtocolInfo: 60201,
71
75
  MessageType_Ping: 60206,
72
76
  MessageType_Success: 60207,
73
77
  },
@@ -162,11 +166,11 @@ describe('LowlevelTransport protocol framing', () => {
162
166
  );
163
167
  const callResponse = ProtocolV2.encodeFrame(
164
168
  schemas,
165
- 'ProtoVersion',
169
+ 'ProtocolInfo',
166
170
  {
167
- major_version: 2,
168
- minor_version: 1,
169
- patch_version: 3,
171
+ version: 1,
172
+ supported_messages: [60200, 60201, 60206, 60207],
173
+ protobuf_definition: 'onekey-protocol-v2',
170
174
  },
171
175
  { router: PROTOCOL_V2_CHANNEL_BLE_UART }
172
176
  );
@@ -183,15 +187,19 @@ describe('LowlevelTransport protocol framing', () => {
183
187
  uuid: 'pro2-id',
184
188
  protocolType: 'V2',
185
189
  });
186
- await expect(lowlevel.call('pro2-id', 'GetProtoVersion', {})).resolves.toEqual({
187
- type: 'ProtoVersion',
190
+ await expect(lowlevel.call('pro2-id', 'ProtocolInfoRequest', {})).resolves.toEqual({
191
+ type: 'ProtocolInfo',
188
192
  message: {
189
- major_version: 2,
190
- minor_version: 1,
191
- patch_version: 3,
193
+ version: 1,
194
+ supported_messages: [60200, 60201, 60206, 60207],
195
+ protobuf_definition: 'onekey-protocol-v2',
192
196
  },
193
197
  });
194
198
  expect(plugin.send).toHaveBeenCalled();
199
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
200
+ Number.parseInt(hex.slice(12, 14), 16)
201
+ );
202
+ expect(sentSeqs).toEqual([1, 2]);
195
203
  });
196
204
 
197
205
  test('falls back to Protocol V2 probe for unnamed Protocol V2 devices', async () => {
@@ -214,6 +222,87 @@ describe('LowlevelTransport protocol framing', () => {
214
222
  expect(lowlevel.getProtocolType('unknown-pro2-id')).toBe('V2');
215
223
  });
216
224
 
225
+ test('retains the Protocol V2 hint and sequence cursor across release and reacquire', async () => {
226
+ const probeResponse = ProtocolV2.encodeFrame(
227
+ schemas,
228
+ 'Success',
229
+ { message: 'ok' },
230
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
231
+ );
232
+ const plugin = createPlugin({
233
+ devices: [{ id: 'reconnect-pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
234
+ responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)],
235
+ });
236
+ const lowlevel = configureTransport(plugin);
237
+
238
+ await lowlevel.enumerate();
239
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
240
+ uuid: 'reconnect-pro2-id',
241
+ protocolType: 'V2',
242
+ });
243
+ await lowlevel.release('reconnect-pro2-id');
244
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
245
+ uuid: 'reconnect-pro2-id',
246
+ protocolType: 'V2',
247
+ });
248
+
249
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
250
+ Number.parseInt(hex.slice(12, 14), 16)
251
+ );
252
+ expect(sentSeqs).toEqual([1, 2]);
253
+ });
254
+
255
+ test('reuses the active generation when Core acquires the same BLE connection again', async () => {
256
+ const probeResponse = ProtocolV2.encodeFrame(
257
+ schemas,
258
+ 'Success',
259
+ { message: 'ok' },
260
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
261
+ );
262
+ const plugin = createPlugin({
263
+ devices: [{ id: 'repeated-acquire-id', name: 'OneKey Pro 2', commType: 'ble' }],
264
+ responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)],
265
+ });
266
+ const lowlevel = configureTransport(plugin);
267
+
268
+ await expect(
269
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
270
+ ).resolves.toEqual({
271
+ uuid: 'repeated-acquire-id',
272
+ protocolType: 'V2',
273
+ });
274
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'first-acquire' });
275
+ await expect(
276
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
277
+ ).resolves.toEqual({
278
+ uuid: 'repeated-acquire-id',
279
+ protocolType: 'V2',
280
+ });
281
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'second-acquire' });
282
+
283
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
284
+ Number.parseInt(hex.slice(12, 14), 16)
285
+ );
286
+ expect(sentSeqs).toEqual([1, 2]);
287
+ });
288
+
289
+ test('trusts explicit Protocol V2 during bootloader reconnect without probing Ping', async () => {
290
+ const plugin = createPlugin({
291
+ devices: [{ id: 'bootloader-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
292
+ responses: [],
293
+ });
294
+ const lowlevel = configureTransport(plugin);
295
+
296
+ await expect(
297
+ lowlevel.acquire({ uuid: 'bootloader-v2-id', expectedProtocol: 'V2' })
298
+ ).resolves.toEqual({
299
+ uuid: 'bootloader-v2-id',
300
+ protocolType: 'V2',
301
+ });
302
+ expect(plugin.send).not.toHaveBeenCalled();
303
+ expect(plugin.receive).not.toHaveBeenCalled();
304
+ });
305
+
217
306
  test('resets the lowlevel connection before probing Protocol V2 after a V1 timeout', async () => {
218
307
  const probeResponse = ProtocolV2.encodeFrame(
219
308
  schemas,
@@ -252,6 +341,45 @@ describe('LowlevelTransport protocol framing', () => {
252
341
  expect(plugin.connect).toHaveBeenCalledTimes(2);
253
342
  });
254
343
 
344
+ test('disconnects a tainted Protocol V2 link after a response timeout', async () => {
345
+ const plugin = createPlugin({
346
+ devices: [{ id: 'timeout-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
347
+ responses: [],
348
+ });
349
+ plugin.receive.mockImplementation(() => new Promise(() => {}));
350
+ const lowlevel = configureTransport(plugin);
351
+
352
+ await lowlevel.acquire({ uuid: 'timeout-v2-id', expectedProtocol: 'V2' });
353
+ plugin.disconnect.mockClear();
354
+ await expect(
355
+ lowlevel.call('timeout-v2-id', 'Ping', { message: 'timeout' }, { timeoutMs: 10 })
356
+ ).rejects.toThrow('Lowlevel response timeout after 10ms for Ping');
357
+
358
+ expect(plugin.disconnect).toHaveBeenCalledWith('timeout-v2-id');
359
+ });
360
+
361
+ test('preserves an undefined business timeout outside explicit probes', async () => {
362
+ const plugin = createPlugin({
363
+ devices: [{ id: 'v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
364
+ responses: [],
365
+ });
366
+ const lowlevel = configureTransport(plugin);
367
+ lowlevel.deviceProtocol.set('v2-id', 'V2');
368
+ const linkCall = jest
369
+ .spyOn(lowlevel.protocolV2Links, 'call')
370
+ .mockResolvedValue({ type: 'Success', message: {} });
371
+
372
+ await lowlevel.call('v2-id', 'Ping', { message: 'no-business-timeout' });
373
+
374
+ expect(linkCall).toHaveBeenCalledWith(
375
+ 'v2-id',
376
+ expect.any(Function),
377
+ 'Ping',
378
+ { message: 'no-business-timeout' },
379
+ undefined
380
+ );
381
+ });
382
+
255
383
  test('verifies expected Protocol V1 instead of trusting the requested protocol', async () => {
256
384
  const plugin = createPlugin({
257
385
  devices: [{ id: 'v2-id', name: 'Unknown BLE Device', commType: 'ble' }],
package/dist/index.d.ts CHANGED
@@ -7,6 +7,15 @@ type LowLevelAcquireInput = {
7
7
  expectedProtocol?: ProtocolType;
8
8
  };
9
9
 
10
+ declare function shouldLogFirmwareUploadProgress({ percent, lastLoggedPercent, now, lastLoggedAt, }: {
11
+ percent: number;
12
+ lastLoggedPercent: number;
13
+ now: number;
14
+ lastLoggedAt: number;
15
+ }): boolean;
16
+ declare function getProtocolV1SendOptions(name: string): {
17
+ withoutResponse: boolean;
18
+ } | undefined;
10
19
  declare class LowlevelTransport {
11
20
  _messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
12
21
  _messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
@@ -17,6 +26,9 @@ declare class LowlevelTransport {
17
26
  private deviceProtocol;
18
27
  private deviceProtocolHints;
19
28
  private protocolV2Assemblers;
29
+ private protocolV2Generations;
30
+ private connectedDevices;
31
+ private protocolV2Links;
20
32
  getProtocolType(path: string): ProtocolType | undefined;
21
33
  init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin): void;
22
34
  configure(signedData: any): void;
@@ -43,7 +55,9 @@ declare class LowlevelTransport {
43
55
  private readProtocolV2Frame;
44
56
  private writeProtocolV2Frame;
45
57
  private callProtocolV2;
58
+ private createProtocolV2Adapter;
59
+ private advanceProtocolV2Generation;
46
60
  cancel(): void;
47
61
  }
48
62
 
49
- export { LowlevelTransport as default };
63
+ export { LowlevelTransport as default, getProtocolV1SendOptions, shouldLogFirmwareUploadProgress };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,SAWN,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,6BAA6B,EAC7B,YAAY,EACZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAqBpD,MAAM,CAAC,OAAO,OAAO,iBAAiB;IACpC,SAAS,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEnE,WAAW,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAErE,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,MAAM,EAAE,6BAA6B,CAAuC;IAE5E,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,mBAAmB,CAAwC;IAEnE,OAAO,CAAC,oBAAoB,CAAoD;IAEhF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIvD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,6BAA6B;IAO9E,SAAS,CAAC,UAAU,EAAE,GAAG;IAMzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAInC,MAAM;IAIA,SAAS;IAWT,OAAO,CAAC,KAAK,EAAE,oBAAoB;;;;IAuBnC,OAAO,CAAC,IAAI,EAAE,MAAM;IAapB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YAmClB,cAAc;IAuC5B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,kBAAkB;YAMZ,cAAc;YAsDd,yBAAyB;YA0BzB,eAAe;YAgBf,eAAe;YA6Bf,UAAU;YAUV,qBAAqB;YAmBrB,mBAAmB;YAqBnB,oBAAoB;YAOpB,cAAc;IAsC5B,MAAM;CAGP"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,SAWN,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,6BAA6B,EAC7B,YAAY,EACZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAUpD,wBAAgB,+BAA+B,CAAC,EAC9C,OAAO,EACP,iBAAiB,EACjB,GAAG,EACH,YAAY,GACb,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACtB,WAMA;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM;;cAEpD;AAcD,MAAM,CAAC,OAAO,OAAO,iBAAiB;IACpC,SAAS,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEnE,WAAW,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAErE,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,MAAM,EAAE,6BAA6B,CAAuC;IAE5E,OAAO,CAAC,cAAc,CAAwC;IAE9D,OAAO,CAAC,mBAAmB,CAAwC;IAEnE,OAAO,CAAC,oBAAoB,CAAoD;IAEhF,OAAO,CAAC,qBAAqB,CAAkC;IAE/D,OAAO,CAAC,gBAAgB,CAA0B;IAElD,OAAO,CAAC,eAAe,CA6BpB;IAEH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIvD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,6BAA6B;IAO9E,SAAS,CAAC,UAAU,EAAE,GAAG;IAMzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAOnC,MAAM;IAIA,SAAS;IAWT,OAAO,CAAC,KAAK,EAAE,oBAAoB;;;;IA6BnC,OAAO,CAAC,IAAI,EAAE,MAAM;IAgBpB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YAsBlB,cAAc;IAmE5B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,kBAAkB;YAMZ,cAAc;YAsDd,yBAAyB;YAiCzB,eAAe;YAgBf,eAAe;YA6Bf,UAAU;YAUV,qBAAqB;YAmBrB,mBAAmB;YAqBnB,oBAAoB;YAOpB,cAAc;IAwB5B,OAAO,CAAC,uBAAuB;IAkC/B,OAAO,CAAC,2BAA2B;IAMnC,MAAM;CAGP"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
3
5
  var hdShared = require('@onekeyfe/hd-shared');
4
6
  var transport = require('@onekeyfe/hd-transport');
5
7
 
@@ -40,8 +42,17 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
40
42
  const { check, ProtocolV1, parseConfigure } = transport__default["default"];
41
43
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
42
44
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
43
- const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30000;
44
45
  const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64;
46
+ const FIRMWARE_UPLOAD_LOG_PERCENT_STEP = 5;
47
+ const FIRMWARE_UPLOAD_LOG_INTERVAL_MS = 10000;
48
+ function shouldLogFirmwareUploadProgress({ percent, lastLoggedPercent, now, lastLoggedAt, }) {
49
+ return (percent === 100 ||
50
+ percent - lastLoggedPercent >= FIRMWARE_UPLOAD_LOG_PERCENT_STEP ||
51
+ now - lastLoggedAt >= FIRMWARE_UPLOAD_LOG_INTERVAL_MS);
52
+ }
53
+ function getProtocolV1SendOptions(name) {
54
+ return name === 'FirmwareUpload' ? { withoutResponse: false } : undefined;
55
+ }
45
56
  function inferProtocolHintFromDeviceName(name) {
46
57
  return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
47
58
  }
@@ -58,6 +69,38 @@ class LowlevelTransport {
58
69
  this.deviceProtocol = new Map();
59
70
  this.deviceProtocolHints = new Map();
60
71
  this.protocolV2Assemblers = new Map();
72
+ this.protocolV2Generations = new Map();
73
+ this.connectedDevices = new Set();
74
+ this.protocolV2Links = new transport.ProtocolV2LinkManager({
75
+ getSchemas: () => {
76
+ if (!this._messages || !this._messagesV2) {
77
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
78
+ }
79
+ return {
80
+ protocolV1: this._messages,
81
+ protocolV2: this._messagesV2,
82
+ };
83
+ },
84
+ classifyError: () => 'link-fatal',
85
+ onLinkInvalidated: (uuid, reason) => __awaiter(this, void 0, void 0, function* () {
86
+ var _a, _b, _c;
87
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
88
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[LowlevelTransport] Protocol V2 link invalidated: ${uuid}`, reason);
89
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
90
+ this.deviceProtocol.delete(uuid);
91
+ this.advanceProtocolV2Generation(uuid);
92
+ try {
93
+ yield this.plugin.disconnect(uuid);
94
+ }
95
+ catch (error) {
96
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] disconnect tainted Protocol V2 link failed: ${uuid}`, error);
97
+ }
98
+ finally {
99
+ this.connectedDevices.delete(uuid);
100
+ }
101
+ }
102
+ }),
103
+ });
61
104
  }
62
105
  getProtocolType(path) {
63
106
  return this.deviceProtocol.get(path);
@@ -75,6 +118,9 @@ class LowlevelTransport {
75
118
  }
76
119
  configureProtocolV2(signedData) {
77
120
  this._messagesV2 = parseConfigure(signedData);
121
+ this.protocolV2Links
122
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
123
+ .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('Protocol V2 schema link cleanup failed:', error); });
78
124
  }
79
125
  listen() {
80
126
  }
@@ -93,10 +139,16 @@ class LowlevelTransport {
93
139
  acquire(input) {
94
140
  var _a;
95
141
  return __awaiter(this, void 0, void 0, function* () {
142
+ const alreadyConnected = this.connectedDevices.has(input.uuid);
96
143
  try {
97
144
  yield this.plugin.connect(input.uuid);
145
+ if (!alreadyConnected) {
146
+ this.connectedDevices.add(input.uuid);
147
+ this.advanceProtocolV2Generation(input.uuid);
148
+ }
98
149
  }
99
150
  catch (error) {
151
+ this.connectedDevices.delete(input.uuid);
100
152
  this.Log.debug('lowlelvel transport connect error: ', error);
101
153
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.LowlevelTrasnportConnectError, (_a = error.message) !== null && _a !== void 0 ? _a : error);
102
154
  }
@@ -111,9 +163,10 @@ class LowlevelTransport {
111
163
  release(uuid) {
112
164
  return __awaiter(this, void 0, void 0, function* () {
113
165
  try {
166
+ yield this.protocolV2Links.invalidateLink(uuid, 'Lowlevel transport released');
114
167
  yield this.plugin.disconnect(uuid);
168
+ this.connectedDevices.delete(uuid);
115
169
  this.deviceProtocol.delete(uuid);
116
- this.deviceProtocolHints.delete(uuid);
117
170
  this.protocolV2Assemblers.delete(uuid);
118
171
  return true;
119
172
  }
@@ -132,12 +185,7 @@ class LowlevelTransport {
132
185
  if (!protocol) {
133
186
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
134
187
  }
135
- if (transport.LogBlockCommand.has(name)) {
136
- this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' protocol: ', protocol);
137
- }
138
- else {
139
- this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
140
- }
188
+ this.Log.debug('transport call', { name, protocol });
141
189
  if (protocol === 'V2') {
142
190
  return this.callProtocolV2(uuid, name, data, options);
143
191
  }
@@ -145,17 +193,42 @@ class LowlevelTransport {
145
193
  });
146
194
  }
147
195
  callProtocolV1(uuid, name, data, options) {
196
+ var _a;
148
197
  return __awaiter(this, void 0, void 0, function* () {
149
198
  if (!this._messages) {
150
199
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
151
200
  }
152
201
  const messages = this._messages;
153
202
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
154
- for (const o of buffers) {
203
+ const isFirmwareUpload = name === 'FirmwareUpload';
204
+ const uploadStartedAt = Date.now();
205
+ const totalBytes = buffers.reduce((sum, buffer) => sum + buffer.limit, 0);
206
+ let sentBytes = 0;
207
+ let lastLoggedPercent = 0;
208
+ let lastLoggedAt = uploadStartedAt;
209
+ for (const [index, o] of buffers.entries()) {
155
210
  const outData = o.toString('hex');
156
- this.Log.debug('send hex strting: ', outData);
157
211
  try {
158
- yield this.plugin.send(uuid, outData);
212
+ yield this.plugin.send(uuid, outData, getProtocolV1SendOptions(name));
213
+ sentBytes += o.limit;
214
+ if (isFirmwareUpload) {
215
+ const now = Date.now();
216
+ const percent = Math.floor(((index + 1) / buffers.length) * 100);
217
+ if (shouldLogFirmwareUploadProgress({
218
+ percent,
219
+ lastLoggedPercent,
220
+ now,
221
+ lastLoggedAt,
222
+ })) {
223
+ const elapsedSeconds = Math.max((now - uploadStartedAt) / 1000, 0.001);
224
+ const kibPerSecond = sentBytes / 1024 / elapsedSeconds;
225
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[LowlevelTransport] FirmwareUpload progress: ${percent}% ` +
226
+ `(${index + 1}/${buffers.length} packets, ${sentBytes}/${totalBytes} bytes, ` +
227
+ `${elapsedSeconds.toFixed(1)}s, ${kibPerSecond.toFixed(1)} KiB/s)`);
228
+ lastLoggedPercent = percent;
229
+ lastLoggedAt = now;
230
+ }
231
+ }
159
232
  }
160
233
  catch (e) {
161
234
  this.Log.debug('lowlevel transport send error: ', e);
@@ -163,8 +236,7 @@ class LowlevelTransport {
163
236
  }
164
237
  }
165
238
  try {
166
- const response = yield this.readProtocolV1Message(options === null || options === void 0 ? void 0 : options.timeoutMs);
167
- this.Log.debug('receive data: ', response);
239
+ const response = yield this.readProtocolV1Message(uuid, options === null || options === void 0 ? void 0 : options.timeoutMs);
168
240
  const jsonData = ProtocolV1.decodeMessage(messages, response);
169
241
  return check.call(jsonData);
170
242
  }
@@ -197,12 +269,9 @@ class LowlevelTransport {
197
269
  var _a, _b, _c, _d, _e, _f;
198
270
  return __awaiter(this, void 0, void 0, function* () {
199
271
  if (expectedProtocol === 'V2') {
200
- if (yield this.probeProtocolV2(uuid)) {
201
- this.deviceProtocol.set(uuid, 'V2');
202
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
203
- return 'V2';
204
- }
205
- throw this.createProtocolMismatchError(expectedProtocol);
272
+ this.deviceProtocol.set(uuid, 'V2');
273
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
274
+ return 'V2';
206
275
  }
207
276
  if (expectedProtocol === 'V1') {
208
277
  if (yield this.probeProtocolV1(uuid)) {
@@ -242,8 +311,10 @@ class LowlevelTransport {
242
311
  resetConnectionAfterProbe(uuid, protocol) {
243
312
  var _a, _b, _c, _d;
244
313
  return __awaiter(this, void 0, void 0, function* () {
314
+ yield this.protocolV2Links.invalidateLink(uuid, `Reset connection after Protocol ${protocol} probe`);
245
315
  (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
246
316
  try {
317
+ this.connectedDevices.delete(uuid);
247
318
  yield this.plugin.disconnect(uuid);
248
319
  }
249
320
  catch (error) {
@@ -251,6 +322,8 @@ class LowlevelTransport {
251
322
  }
252
323
  try {
253
324
  yield this.plugin.connect(uuid);
325
+ this.connectedDevices.add(uuid);
326
+ this.advanceProtocolV2Generation(uuid);
254
327
  }
255
328
  catch (error) {
256
329
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`, error);
@@ -307,18 +380,18 @@ class LowlevelTransport {
307
380
  }
308
381
  });
309
382
  }
310
- receiveHex(timeoutMs, commandName) {
383
+ receiveHex(uuid, timeoutMs, commandName) {
311
384
  return __awaiter(this, void 0, void 0, function* () {
312
- const response = yield transport.withProtocolTimeout(this.plugin.receive(), timeoutMs, () => this.createProtocolTimeoutError(commandName, timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : 0));
385
+ const response = yield transport.withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () => this.createProtocolTimeoutError(commandName, timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : 0));
313
386
  if (typeof response !== 'string') {
314
387
  throw new Error('Returning data is not string');
315
388
  }
316
389
  return response;
317
390
  });
318
391
  }
319
- readProtocolV1Message(timeoutMs) {
392
+ readProtocolV1Message(uuid, timeoutMs) {
320
393
  return __awaiter(this, void 0, void 0, function* () {
321
- const first = yield this.receiveHex(timeoutMs, 'ProtocolV1');
394
+ const first = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
322
395
  const firstData = transport.hexToBytes(first);
323
396
  if (!isProtocolV1TransportChunk(firstData)) {
324
397
  return first;
@@ -327,13 +400,13 @@ class LowlevelTransport {
327
400
  let buffer = firstData.slice(3);
328
401
  const expectedLength = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
329
402
  while (buffer.length < expectedLength) {
330
- const next = yield this.receiveHex(timeoutMs, 'ProtocolV1');
403
+ const next = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
331
404
  buffer = transport.concatUint8Arrays([buffer, transport.hexToBytes(next)]);
332
405
  }
333
406
  return transport.bytesToHex(buffer.slice(0, expectedLength));
334
407
  });
335
408
  }
336
- readProtocolV2Frame(uuid, timeoutMs) {
409
+ readProtocolV2Frame(uuid, timeoutMs, commandName = 'ProtocolV2') {
337
410
  return __awaiter(this, void 0, void 0, function* () {
338
411
  let assembler = this.protocolV2Assemblers.get(uuid);
339
412
  if (!assembler) {
@@ -345,7 +418,7 @@ class LowlevelTransport {
345
418
  return queuedFrame;
346
419
  let frame;
347
420
  while (!frame) {
348
- const response = yield this.receiveHex(timeoutMs, 'ProtocolV2');
421
+ const response = yield this.receiveHex(uuid, timeoutMs, commandName);
349
422
  const chunk = transport.hexToBytes(response);
350
423
  if (chunk.length > 0) {
351
424
  frame = assembler.push(chunk);
@@ -363,38 +436,64 @@ class LowlevelTransport {
363
436
  });
364
437
  }
365
438
  callProtocolV2(uuid, name, data, options) {
366
- var _a, _b, _c;
367
439
  return __awaiter(this, void 0, void 0, function* () {
368
440
  if (!this._messages || !this._messagesV2) {
369
441
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
370
442
  }
371
- const timeoutMs = (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : LOWLEVEL_PROTOCOL_TIMEOUT_MS;
372
- (_b = this.protocolV2Assemblers.get(uuid)) === null || _b === void 0 ? void 0 : _b.reset();
373
- const session = new transport.ProtocolV2Session({
374
- schemas: {
375
- protocolV1: this._messages,
376
- protocolV2: this._messagesV2,
377
- },
378
- router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
379
- writeFrame: (frame) => this.writeProtocolV2Frame(uuid, frame),
380
- readFrame: () => this.readProtocolV2Frame(uuid, timeoutMs),
381
- logger: this.Log,
382
- logPrefix: 'ProtocolV2 Lowlevel-BLE',
383
- createTimeoutError: (_messageName, timeout) => this.createProtocolTimeoutError(name, timeout),
384
- });
385
443
  try {
386
- return yield session.call(name, data, Object.assign(Object.assign({}, options), { timeoutMs }));
444
+ return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, options);
387
445
  }
388
446
  catch (e) {
389
- (_c = this.protocolV2Assemblers.get(uuid)) === null || _c === void 0 ? void 0 : _c.reset();
390
447
  this.Log.error('lowlevel Protocol V2 call error: ', e);
391
448
  throw e;
392
449
  }
393
450
  });
394
451
  }
452
+ createProtocolV2Adapter(uuid) {
453
+ var _a;
454
+ const generation = (_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0;
455
+ const assertCurrentGeneration = () => {
456
+ if (this.protocolV2Generations.get(uuid) !== generation) {
457
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
458
+ }
459
+ };
460
+ return {
461
+ router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
462
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
463
+ generation,
464
+ prepareCall: () => {
465
+ var _a;
466
+ assertCurrentGeneration();
467
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
468
+ },
469
+ writeFrame: (frame) => {
470
+ assertCurrentGeneration();
471
+ return this.writeProtocolV2Frame(uuid, frame);
472
+ },
473
+ readFrame: (context) => {
474
+ assertCurrentGeneration();
475
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
476
+ },
477
+ reset: () => {
478
+ var _a;
479
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
480
+ },
481
+ logger: this.Log,
482
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
483
+ createTimeoutError: (messageName, timeout) => this.createProtocolTimeoutError(messageName, timeout),
484
+ };
485
+ }
486
+ advanceProtocolV2Generation(uuid) {
487
+ var _a;
488
+ const nextGeneration = ((_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
489
+ this.protocolV2Generations.set(uuid, nextGeneration);
490
+ return nextGeneration;
491
+ }
395
492
  cancel() {
396
493
  this.Log.debug('lowlevel-transport', 'cancel');
397
494
  }
398
495
  }
399
496
 
400
- module.exports = LowlevelTransport;
497
+ exports["default"] = LowlevelTransport;
498
+ exports.getProtocolV1SendOptions = getProtocolV1SendOptions;
499
+ exports.shouldLogFirmwareUploadProgress = shouldLogFirmwareUploadProgress;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-lowlevel",
3
- "version": "1.2.0-alpha.2",
3
+ "version": "1.2.0-alpha.20",
4
4
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,8 +20,8 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-shared": "1.2.0-alpha.2",
24
- "@onekeyfe/hd-transport": "1.2.0-alpha.2"
23
+ "@onekeyfe/hd-shared": "1.2.0-alpha.20",
24
+ "@onekeyfe/hd-transport": "1.2.0-alpha.20"
25
25
  },
26
- "gitHead": "6c183b7f7027f4b4d3db4f427f6360351a6c6680"
26
+ "gitHead": "5fbc1ada90fd3cfda7e5ef08be368cb452018733"
27
27
  }
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
  import transport, {
3
- LogBlockCommand,
4
3
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
4
+ PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
5
5
  PROTOCOL_V2_CHANNEL_BLE_UART,
6
6
  ProtocolV2FrameAssembler,
7
- ProtocolV2Session,
7
+ ProtocolV2LinkManager,
8
8
  bytesToHex,
9
9
  concatUint8Arrays,
10
10
  hexToBytes,
@@ -25,8 +25,31 @@ const { check, ProtocolV1, parseConfigure } = transport;
25
25
 
26
26
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
27
27
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
28
- const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30_000;
29
28
  const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64;
29
+ const FIRMWARE_UPLOAD_LOG_PERCENT_STEP = 5;
30
+ const FIRMWARE_UPLOAD_LOG_INTERVAL_MS = 10_000;
31
+
32
+ export function shouldLogFirmwareUploadProgress({
33
+ percent,
34
+ lastLoggedPercent,
35
+ now,
36
+ lastLoggedAt,
37
+ }: {
38
+ percent: number;
39
+ lastLoggedPercent: number;
40
+ now: number;
41
+ lastLoggedAt: number;
42
+ }) {
43
+ return (
44
+ percent === 100 ||
45
+ percent - lastLoggedPercent >= FIRMWARE_UPLOAD_LOG_PERCENT_STEP ||
46
+ now - lastLoggedAt >= FIRMWARE_UPLOAD_LOG_INTERVAL_MS
47
+ );
48
+ }
49
+
50
+ export function getProtocolV1SendOptions(name: string) {
51
+ return name === 'FirmwareUpload' ? { withoutResponse: false } : undefined;
52
+ }
30
53
 
31
54
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
32
55
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
@@ -59,6 +82,41 @@ export default class LowlevelTransport {
59
82
 
60
83
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
61
84
 
85
+ private protocolV2Generations: Map<string, number> = new Map();
86
+
87
+ private connectedDevices: Set<string> = new Set();
88
+
89
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
90
+ getSchemas: () => {
91
+ if (!this._messages || !this._messagesV2) {
92
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
93
+ }
94
+ return {
95
+ protocolV1: this._messages,
96
+ protocolV2: this._messagesV2,
97
+ };
98
+ },
99
+ classifyError: () => 'link-fatal',
100
+ onLinkInvalidated: async (uuid, reason) => {
101
+ this.protocolV2Assemblers.get(uuid)?.reset();
102
+ this.Log?.debug(`[LowlevelTransport] Protocol V2 link invalidated: ${uuid}`, reason);
103
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
104
+ this.deviceProtocol.delete(uuid);
105
+ this.advanceProtocolV2Generation(uuid);
106
+ try {
107
+ await this.plugin.disconnect(uuid);
108
+ } catch (error) {
109
+ this.Log?.debug(
110
+ `[LowlevelTransport] disconnect tainted Protocol V2 link failed: ${uuid}`,
111
+ error
112
+ );
113
+ } finally {
114
+ this.connectedDevices.delete(uuid);
115
+ }
116
+ }
117
+ },
118
+ });
119
+
62
120
  getProtocolType(path: string): ProtocolType | undefined {
63
121
  return this.deviceProtocol.get(path);
64
122
  }
@@ -78,6 +136,9 @@ export default class LowlevelTransport {
78
136
 
79
137
  configureProtocolV2(signedData: any) {
80
138
  this._messagesV2 = parseConfigure(signedData);
139
+ this.protocolV2Links
140
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
141
+ .catch(error => this.Log?.debug('Protocol V2 schema link cleanup failed:', error));
81
142
  }
82
143
 
83
144
  listen() {
@@ -96,9 +157,15 @@ export default class LowlevelTransport {
96
157
  }
97
158
 
98
159
  async acquire(input: LowLevelAcquireInput) {
160
+ const alreadyConnected = this.connectedDevices.has(input.uuid);
99
161
  try {
100
162
  await this.plugin.connect(input.uuid);
163
+ if (!alreadyConnected) {
164
+ this.connectedDevices.add(input.uuid);
165
+ this.advanceProtocolV2Generation(input.uuid);
166
+ }
101
167
  } catch (error) {
168
+ this.connectedDevices.delete(input.uuid);
102
169
  this.Log.debug('lowlelvel transport connect error: ', error);
103
170
  throw ERRORS.TypedError(
104
171
  HardwareErrorCode.LowlevelTrasnportConnectError,
@@ -120,9 +187,12 @@ export default class LowlevelTransport {
120
187
 
121
188
  async release(uuid: string) {
122
189
  try {
190
+ await this.protocolV2Links.invalidateLink(uuid, 'Lowlevel transport released');
123
191
  await this.plugin.disconnect(uuid);
192
+ this.connectedDevices.delete(uuid);
124
193
  this.deviceProtocol.delete(uuid);
125
- this.deviceProtocolHints.delete(uuid);
194
+ // A name-derived protocol hint survives disconnect and lets fast reconnect probe
195
+ // Protocol V2 first without sending a redundant V1 Initialize.
126
196
  this.protocolV2Assemblers.delete(uuid);
127
197
  return true;
128
198
  } catch (error) {
@@ -148,20 +218,7 @@ export default class LowlevelTransport {
148
218
  `Device protocol has not been detected for ${uuid}`
149
219
  );
150
220
  }
151
- if (LogBlockCommand.has(name)) {
152
- this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' protocol: ', protocol);
153
- } else {
154
- this.Log.debug(
155
- 'lowlevel-transport',
156
- 'call-',
157
- ' name: ',
158
- name,
159
- ' data: ',
160
- data,
161
- ' protocol: ',
162
- protocol
163
- );
164
- }
221
+ this.Log.debug('transport call', { name, protocol });
165
222
 
166
223
  if (protocol === 'V2') {
167
224
  return this.callProtocolV2(uuid, name, data, options);
@@ -182,12 +239,41 @@ export default class LowlevelTransport {
182
239
 
183
240
  const messages = this._messages;
184
241
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
185
- for (const o of buffers) {
242
+ const isFirmwareUpload = name === 'FirmwareUpload';
243
+ const uploadStartedAt = Date.now();
244
+ const totalBytes = buffers.reduce((sum, buffer) => sum + buffer.limit, 0);
245
+ let sentBytes = 0;
246
+ let lastLoggedPercent = 0;
247
+ let lastLoggedAt = uploadStartedAt;
248
+
249
+ for (const [index, o] of buffers.entries()) {
186
250
  const outData = o.toString('hex');
187
- // Upload resources on low-end phones may OOM
188
- this.Log.debug('send hex strting: ', outData);
189
251
  try {
190
- await this.plugin.send(uuid, outData);
252
+ await this.plugin.send(uuid, outData, getProtocolV1SendOptions(name));
253
+ sentBytes += o.limit;
254
+
255
+ if (isFirmwareUpload) {
256
+ const now = Date.now();
257
+ const percent = Math.floor(((index + 1) / buffers.length) * 100);
258
+ if (
259
+ shouldLogFirmwareUploadProgress({
260
+ percent,
261
+ lastLoggedPercent,
262
+ now,
263
+ lastLoggedAt,
264
+ })
265
+ ) {
266
+ const elapsedSeconds = Math.max((now - uploadStartedAt) / 1000, 0.001);
267
+ const kibPerSecond = sentBytes / 1024 / elapsedSeconds;
268
+ this.Log?.debug(
269
+ `[LowlevelTransport] FirmwareUpload progress: ${percent}% ` +
270
+ `(${index + 1}/${buffers.length} packets, ${sentBytes}/${totalBytes} bytes, ` +
271
+ `${elapsedSeconds.toFixed(1)}s, ${kibPerSecond.toFixed(1)} KiB/s)`
272
+ );
273
+ lastLoggedPercent = percent;
274
+ lastLoggedAt = now;
275
+ }
276
+ }
191
277
  } catch (e) {
192
278
  this.Log.debug('lowlevel transport send error: ', e);
193
279
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError, e.reason);
@@ -195,8 +281,7 @@ export default class LowlevelTransport {
195
281
  }
196
282
 
197
283
  try {
198
- const response = await this.readProtocolV1Message(options?.timeoutMs);
199
- this.Log.debug('receive data: ', response);
284
+ const response = await this.readProtocolV1Message(uuid, options?.timeoutMs);
200
285
  const jsonData = ProtocolV1.decodeMessage(messages, response);
201
286
  return check.call(jsonData);
202
287
  } catch (e) {
@@ -242,12 +327,12 @@ export default class LowlevelTransport {
242
327
  protocolHint?: ProtocolType
243
328
  ): Promise<ProtocolType> {
244
329
  if (expectedProtocol === 'V2') {
245
- if (await this.probeProtocolV2(uuid)) {
246
- this.deviceProtocol.set(uuid, 'V2');
247
- this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
248
- return 'V2';
249
- }
250
- throw this.createProtocolMismatchError(expectedProtocol);
330
+ // Bootloader may not answer Ping after an update reboot. An explicit V2 hint means
331
+ // the protocol was already confirmed, so establish the link and let the first
332
+ // business command validate it, matching RN and Electron BLE.
333
+ this.deviceProtocol.set(uuid, 'V2');
334
+ this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
335
+ return 'V2';
251
336
  }
252
337
 
253
338
  if (expectedProtocol === 'V1') {
@@ -291,9 +376,14 @@ export default class LowlevelTransport {
291
376
  }
292
377
 
293
378
  private async resetConnectionAfterProbe(uuid: string, protocol: ProtocolType) {
379
+ await this.protocolV2Links.invalidateLink(
380
+ uuid,
381
+ `Reset connection after Protocol ${protocol} probe`
382
+ );
294
383
  this.protocolV2Assemblers.get(uuid)?.reset();
295
384
 
296
385
  try {
386
+ this.connectedDevices.delete(uuid);
297
387
  await this.plugin.disconnect(uuid);
298
388
  } catch (error) {
299
389
  this.Log?.debug(
@@ -304,6 +394,8 @@ export default class LowlevelTransport {
304
394
 
305
395
  try {
306
396
  await this.plugin.connect(uuid);
397
+ this.connectedDevices.add(uuid);
398
+ this.advanceProtocolV2Generation(uuid);
307
399
  } catch (error) {
308
400
  this.Log?.debug(
309
401
  `[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`,
@@ -361,8 +453,8 @@ export default class LowlevelTransport {
361
453
  }
362
454
  }
363
455
 
364
- private async receiveHex(timeoutMs: number | undefined, commandName: string) {
365
- const response = await withProtocolTimeout(this.plugin.receive(), timeoutMs, () =>
456
+ private async receiveHex(uuid: string, timeoutMs: number | undefined, commandName: string) {
457
+ const response = await withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () =>
366
458
  this.createProtocolTimeoutError(commandName, timeoutMs ?? 0)
367
459
  );
368
460
  if (typeof response !== 'string') {
@@ -371,8 +463,8 @@ export default class LowlevelTransport {
371
463
  return response;
372
464
  }
373
465
 
374
- private async readProtocolV1Message(timeoutMs?: number) {
375
- const first = await this.receiveHex(timeoutMs, 'ProtocolV1');
466
+ private async readProtocolV1Message(uuid: string, timeoutMs?: number) {
467
+ const first = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
376
468
  const firstData = hexToBytes(first);
377
469
  if (!isProtocolV1TransportChunk(firstData)) {
378
470
  return first;
@@ -383,14 +475,14 @@ export default class LowlevelTransport {
383
475
  const expectedLength = PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
384
476
 
385
477
  while (buffer.length < expectedLength) {
386
- const next = await this.receiveHex(timeoutMs, 'ProtocolV1');
478
+ const next = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
387
479
  buffer = concatUint8Arrays([buffer, hexToBytes(next)]);
388
480
  }
389
481
 
390
482
  return bytesToHex(buffer.slice(0, expectedLength));
391
483
  }
392
484
 
393
- private async readProtocolV2Frame(uuid: string, timeoutMs?: number) {
485
+ private async readProtocolV2Frame(uuid: string, timeoutMs?: number, commandName = 'ProtocolV2') {
394
486
  let assembler = this.protocolV2Assemblers.get(uuid);
395
487
  if (!assembler) {
396
488
  assembler = new ProtocolV2FrameAssembler();
@@ -402,7 +494,7 @@ export default class LowlevelTransport {
402
494
 
403
495
  let frame: Uint8Array | undefined;
404
496
  while (!frame) {
405
- const response = await this.receiveHex(timeoutMs, 'ProtocolV2');
497
+ const response = await this.receiveHex(uuid, timeoutMs, commandName);
406
498
  const chunk = hexToBytes(response);
407
499
  if (chunk.length > 0) {
408
500
  frame = assembler.push(chunk);
@@ -428,34 +520,60 @@ export default class LowlevelTransport {
428
520
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
429
521
  }
430
522
 
431
- const timeoutMs = options?.timeoutMs ?? LOWLEVEL_PROTOCOL_TIMEOUT_MS;
432
- this.protocolV2Assemblers.get(uuid)?.reset();
433
- const session = new ProtocolV2Session({
434
- schemas: {
435
- protocolV1: this._messages,
436
- protocolV2: this._messagesV2,
437
- },
438
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
439
- writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(uuid, frame),
440
- readFrame: () => this.readProtocolV2Frame(uuid, timeoutMs),
441
- logger: this.Log,
442
- logPrefix: 'ProtocolV2 Lowlevel-BLE',
443
- createTimeoutError: (_messageName: string, timeout: number) =>
444
- this.createProtocolTimeoutError(name, timeout),
445
- });
446
-
447
523
  try {
448
- return await session.call(name, data, {
449
- ...options,
450
- timeoutMs,
451
- });
524
+ return await this.protocolV2Links.call(
525
+ uuid,
526
+ () => this.createProtocolV2Adapter(uuid),
527
+ name,
528
+ data,
529
+ options
530
+ );
452
531
  } catch (e) {
453
- this.protocolV2Assemblers.get(uuid)?.reset();
454
532
  this.Log.error('lowlevel Protocol V2 call error: ', e);
455
533
  throw e;
456
534
  }
457
535
  }
458
536
 
537
+ private createProtocolV2Adapter(uuid: string) {
538
+ const generation = this.protocolV2Generations.get(uuid) ?? 0;
539
+ const assertCurrentGeneration = () => {
540
+ if (this.protocolV2Generations.get(uuid) !== generation) {
541
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
542
+ }
543
+ };
544
+
545
+ return {
546
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
547
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
548
+ generation,
549
+ prepareCall: () => {
550
+ assertCurrentGeneration();
551
+ this.protocolV2Assemblers.get(uuid)?.reset();
552
+ },
553
+ writeFrame: (frame: Uint8Array) => {
554
+ assertCurrentGeneration();
555
+ return this.writeProtocolV2Frame(uuid, frame);
556
+ },
557
+ readFrame: (context: { messageName: string; timeoutMs?: number }) => {
558
+ assertCurrentGeneration();
559
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
560
+ },
561
+ reset: () => {
562
+ this.protocolV2Assemblers.get(uuid)?.reset();
563
+ },
564
+ logger: this.Log,
565
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
566
+ createTimeoutError: (messageName: string, timeout: number) =>
567
+ this.createProtocolTimeoutError(messageName, timeout),
568
+ };
569
+ }
570
+
571
+ private advanceProtocolV2Generation(uuid: string) {
572
+ const nextGeneration = (this.protocolV2Generations.get(uuid) ?? 0) + 1;
573
+ this.protocolV2Generations.set(uuid, nextGeneration);
574
+ return nextGeneration;
575
+ }
576
+
459
577
  cancel() {
460
578
  this.Log.debug('lowlevel-transport', 'cancel');
461
579
  }