@onekeyfe/hd-transport-lowlevel 1.2.0-alpha.3 → 1.2.0-alpha.30

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
  },
@@ -118,6 +122,26 @@ const splitFrame = (frame, index) => [
118
122
  ];
119
123
 
120
124
  describe('LowlevelTransport protocol framing', () => {
125
+ test('keeps active links when the Protocol V2 schema is configured repeatedly', () => {
126
+ const lowlevel = new LowlevelTransport();
127
+ const invalidateAllLinks = jest.fn().mockResolvedValue(undefined);
128
+ lowlevel.protocolV2Links.invalidateAllLinks = invalidateAllLinks;
129
+
130
+ lowlevel.configureProtocolV2(protocolV2Schema);
131
+ lowlevel.configureProtocolV2(protocolV2Schema);
132
+
133
+ expect(invalidateAllLinks).not.toHaveBeenCalled();
134
+
135
+ lowlevel.configureProtocolV2({
136
+ nested: {
137
+ ...protocolV2Schema.nested,
138
+ ExtraMessage: { fields: {} },
139
+ },
140
+ });
141
+
142
+ expect(invalidateAllLinks).toHaveBeenCalledWith('Protocol V2 schema reconfigured');
143
+ });
144
+
121
145
  test('keeps Protocol V1 raw notification chunks compatible', async () => {
122
146
  const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', {
123
147
  message: 'ok',
@@ -138,6 +162,23 @@ describe('LowlevelTransport protocol framing', () => {
138
162
  });
139
163
  });
140
164
 
165
+ test('uses the Protocol V2 BLE writer with the lowlevel compatibility packet size', async () => {
166
+ const plugin = createPlugin({ devices: [], responses: [] });
167
+ const lowlevel = configureTransport(plugin);
168
+ const context = {
169
+ messageName: 'Ping',
170
+ timeoutMs: 1000,
171
+ highVolume: false,
172
+ generation: 1,
173
+ signal: new AbortController().signal,
174
+ };
175
+
176
+ await lowlevel.writeProtocolV2Frame('pro2-id', new Uint8Array(130), context, jest.fn());
177
+
178
+ expect(plugin.send).toHaveBeenCalledTimes(3);
179
+ expect(plugin.send.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([64, 64, 2]);
180
+ });
181
+
141
182
  test('rejects calls before protocol detection', async () => {
142
183
  const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', {
143
184
  message: 'ok',
@@ -162,13 +203,13 @@ describe('LowlevelTransport protocol framing', () => {
162
203
  );
163
204
  const callResponse = ProtocolV2.encodeFrame(
164
205
  schemas,
165
- 'ProtoVersion',
206
+ 'ProtocolInfo',
166
207
  {
167
- major_version: 2,
168
- minor_version: 1,
169
- patch_version: 3,
208
+ version: 1,
209
+ supported_messages: [60200, 60201, 60206, 60207],
210
+ protobuf_definition: 'onekey-protocol-v2',
170
211
  },
171
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
212
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: 2 }
172
213
  );
173
214
  const plugin = createPlugin({
174
215
  devices: [{ id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
@@ -183,15 +224,19 @@ describe('LowlevelTransport protocol framing', () => {
183
224
  uuid: 'pro2-id',
184
225
  protocolType: 'V2',
185
226
  });
186
- await expect(lowlevel.call('pro2-id', 'GetProtoVersion', {})).resolves.toEqual({
187
- type: 'ProtoVersion',
227
+ await expect(lowlevel.call('pro2-id', 'ProtocolInfoRequest', {})).resolves.toEqual({
228
+ type: 'ProtocolInfo',
188
229
  message: {
189
- major_version: 2,
190
- minor_version: 1,
191
- patch_version: 3,
230
+ version: 1,
231
+ supported_messages: [60200, 60201, 60206, 60207],
232
+ protobuf_definition: 'onekey-protocol-v2',
192
233
  },
193
234
  });
194
235
  expect(plugin.send).toHaveBeenCalled();
236
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
237
+ Number.parseInt(hex.slice(12, 14), 16)
238
+ );
239
+ expect(sentSeqs).toEqual([1, 2]);
195
240
  });
196
241
 
197
242
  test('falls back to Protocol V2 probe for unnamed Protocol V2 devices', async () => {
@@ -214,6 +259,97 @@ describe('LowlevelTransport protocol framing', () => {
214
259
  expect(lowlevel.getProtocolType('unknown-pro2-id')).toBe('V2');
215
260
  });
216
261
 
262
+ test('retains the Protocol V2 hint and sequence cursor across release and reacquire', async () => {
263
+ const probeResponse = ProtocolV2.encodeFrame(
264
+ schemas,
265
+ 'Success',
266
+ { message: 'ok' },
267
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
268
+ );
269
+ const plugin = createPlugin({
270
+ devices: [{ id: 'reconnect-pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
271
+ responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)],
272
+ });
273
+ const lowlevel = configureTransport(plugin);
274
+
275
+ await lowlevel.enumerate();
276
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
277
+ uuid: 'reconnect-pro2-id',
278
+ protocolType: 'V2',
279
+ });
280
+ await lowlevel.release('reconnect-pro2-id');
281
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
282
+ uuid: 'reconnect-pro2-id',
283
+ protocolType: 'V2',
284
+ });
285
+
286
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
287
+ Number.parseInt(hex.slice(12, 14), 16)
288
+ );
289
+ expect(sentSeqs).toEqual([1, 2]);
290
+ });
291
+
292
+ test('reuses the active generation when Core acquires the same BLE connection again', async () => {
293
+ const responses = [1, 2, 3, 4].map(seq =>
294
+ bytesToHex(
295
+ ProtocolV2.encodeFrame(
296
+ schemas,
297
+ 'Success',
298
+ { message: 'ok' },
299
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq }
300
+ )
301
+ )
302
+ );
303
+ const plugin = createPlugin({
304
+ devices: [{ id: 'repeated-acquire-id', name: 'OneKey Pro 2', commType: 'ble' }],
305
+ responses,
306
+ });
307
+ const lowlevel = configureTransport(plugin);
308
+
309
+ await expect(
310
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
311
+ ).resolves.toEqual({
312
+ uuid: 'repeated-acquire-id',
313
+ protocolType: 'V2',
314
+ });
315
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'first-acquire' });
316
+ await expect(
317
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
318
+ ).resolves.toEqual({
319
+ uuid: 'repeated-acquire-id',
320
+ protocolType: 'V2',
321
+ });
322
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'second-acquire' });
323
+
324
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
325
+ Number.parseInt(hex.slice(12, 14), 16)
326
+ );
327
+ expect(sentSeqs).toEqual([1, 2, 3, 4]);
328
+ });
329
+
330
+ test('actively probes explicit Protocol V2 during bootloader reconnect', async () => {
331
+ const probeResponse = ProtocolV2.encodeFrame(
332
+ schemas,
333
+ 'Success',
334
+ { message: 'ok' },
335
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
336
+ );
337
+ const plugin = createPlugin({
338
+ devices: [{ id: 'bootloader-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
339
+ responses: [bytesToHex(probeResponse)],
340
+ });
341
+ const lowlevel = configureTransport(plugin);
342
+
343
+ await expect(
344
+ lowlevel.acquire({ uuid: 'bootloader-v2-id', expectedProtocol: 'V2' })
345
+ ).resolves.toEqual({
346
+ uuid: 'bootloader-v2-id',
347
+ protocolType: 'V2',
348
+ });
349
+ expect(plugin.send).toHaveBeenCalledTimes(1);
350
+ expect(plugin.receive).toHaveBeenCalledTimes(1);
351
+ });
352
+
217
353
  test('resets the lowlevel connection before probing Protocol V2 after a V1 timeout', async () => {
218
354
  const probeResponse = ProtocolV2.encodeFrame(
219
355
  schemas,
@@ -252,6 +388,57 @@ describe('LowlevelTransport protocol framing', () => {
252
388
  expect(plugin.connect).toHaveBeenCalledTimes(2);
253
389
  });
254
390
 
391
+ test('disconnects a tainted Protocol V2 link after a response timeout', async () => {
392
+ const probeResponse = ProtocolV2.encodeFrame(
393
+ schemas,
394
+ 'Success',
395
+ { message: 'ok' },
396
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
397
+ );
398
+ const plugin = createPlugin({
399
+ devices: [{ id: 'timeout-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
400
+ responses: [bytesToHex(probeResponse)],
401
+ });
402
+ let receiveCount = 0;
403
+ plugin.receive.mockImplementation(() => {
404
+ receiveCount += 1;
405
+ return receiveCount === 1
406
+ ? Promise.resolve(bytesToHex(probeResponse))
407
+ : new Promise(() => {});
408
+ });
409
+ const lowlevel = configureTransport(plugin);
410
+
411
+ await lowlevel.acquire({ uuid: 'timeout-v2-id', expectedProtocol: 'V2' });
412
+ plugin.disconnect.mockClear();
413
+ await expect(
414
+ lowlevel.call('timeout-v2-id', 'Ping', { message: 'timeout' }, { timeoutMs: 10 })
415
+ ).rejects.toThrow('Lowlevel response timeout after 10ms for Ping');
416
+
417
+ expect(plugin.disconnect).toHaveBeenCalledWith('timeout-v2-id');
418
+ });
419
+
420
+ test('preserves an undefined business timeout outside explicit probes', async () => {
421
+ const plugin = createPlugin({
422
+ devices: [{ id: 'v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
423
+ responses: [],
424
+ });
425
+ const lowlevel = configureTransport(plugin);
426
+ lowlevel.deviceProtocol.set('v2-id', 'V2');
427
+ const linkCall = jest
428
+ .spyOn(lowlevel.protocolV2Links, 'call')
429
+ .mockResolvedValue({ type: 'Success', message: {} });
430
+
431
+ await lowlevel.call('v2-id', 'Ping', { message: 'no-business-timeout' });
432
+
433
+ expect(linkCall).toHaveBeenCalledWith(
434
+ 'v2-id',
435
+ expect.any(Function),
436
+ 'Ping',
437
+ { message: 'no-business-timeout' },
438
+ undefined
439
+ );
440
+ });
441
+
255
442
  test('verifies expected Protocol V1 instead of trusting the requested protocol', async () => {
256
443
  const plugin = createPlugin({
257
444
  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,10 @@ declare class LowlevelTransport {
17
26
  private deviceProtocol;
18
27
  private deviceProtocolHints;
19
28
  private protocolV2Assemblers;
29
+ private protocolV2Generations;
30
+ private connectedDevices;
31
+ private protocolV2Links;
32
+ private protocolV2SchemaConfiguration;
20
33
  getProtocolType(path: string): ProtocolType | undefined;
21
34
  init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin): void;
22
35
  configure(signedData: any): void;
@@ -43,7 +56,9 @@ declare class LowlevelTransport {
43
56
  private readProtocolV2Frame;
44
57
  private writeProtocolV2Frame;
45
58
  private callProtocolV2;
59
+ private createProtocolV2Adapter;
60
+ private advanceProtocolV2Generation;
46
61
  cancel(): void;
47
62
  }
48
63
 
49
- export { LowlevelTransport as default };
64
+ 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,SAYN,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,6BAA6B,EAE7B,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,OAAO,CAAC,6BAA6B,CAAqB;IAE1D,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;IAiBnC,MAAM;IAIA,SAAS;IAWT,OAAO,CAAC,KAAK,EAAE,oBAAoB;;;;IAgCnC,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;IA6E5B,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;YAgBpB,cAAc;IAwB5B,OAAO,CAAC,uBAAuB;IAgC/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);
@@ -74,7 +117,18 @@ class LowlevelTransport {
74
117
  this._messages = messages;
75
118
  }
76
119
  configureProtocolV2(signedData) {
120
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
121
+ if (this.protocolV2SchemaConfiguration === configuration) {
122
+ return;
123
+ }
124
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
77
125
  this._messagesV2 = parseConfigure(signedData);
126
+ this.protocolV2SchemaConfiguration = configuration;
127
+ if (isReconfiguration) {
128
+ this.protocolV2Links
129
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
130
+ .catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('Protocol V2 schema link cleanup failed:', error); });
131
+ }
78
132
  }
79
133
  listen() {
80
134
  }
@@ -93,14 +147,20 @@ class LowlevelTransport {
93
147
  acquire(input) {
94
148
  var _a;
95
149
  return __awaiter(this, void 0, void 0, function* () {
150
+ const alreadyConnected = this.connectedDevices.has(input.uuid);
96
151
  try {
97
152
  yield this.plugin.connect(input.uuid);
153
+ if (!alreadyConnected) {
154
+ this.connectedDevices.add(input.uuid);
155
+ this.advanceProtocolV2Generation(input.uuid);
156
+ }
98
157
  }
99
158
  catch (error) {
159
+ this.connectedDevices.delete(input.uuid);
100
160
  this.Log.debug('lowlelvel transport connect error: ', error);
101
161
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.LowlevelTrasnportConnectError, (_a = error.message) !== null && _a !== void 0 ? _a : error);
102
162
  }
103
- this.protocolV2Assemblers.set(input.uuid, new transport.ProtocolV2FrameAssembler());
163
+ this.protocolV2Assemblers.set(input.uuid, new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES));
104
164
  const protocolHint = input.expectedProtocol
105
165
  ? undefined
106
166
  : this.deviceProtocolHints.get(input.uuid);
@@ -111,9 +171,10 @@ class LowlevelTransport {
111
171
  release(uuid) {
112
172
  return __awaiter(this, void 0, void 0, function* () {
113
173
  try {
174
+ yield this.protocolV2Links.invalidateLink(uuid, 'Lowlevel transport released');
114
175
  yield this.plugin.disconnect(uuid);
176
+ this.connectedDevices.delete(uuid);
115
177
  this.deviceProtocol.delete(uuid);
116
- this.deviceProtocolHints.delete(uuid);
117
178
  this.protocolV2Assemblers.delete(uuid);
118
179
  return true;
119
180
  }
@@ -132,12 +193,7 @@ class LowlevelTransport {
132
193
  if (!protocol) {
133
194
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
134
195
  }
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
- }
196
+ this.Log.debug('transport call', { name, protocol });
141
197
  if (protocol === 'V2') {
142
198
  return this.callProtocolV2(uuid, name, data, options);
143
199
  }
@@ -145,17 +201,42 @@ class LowlevelTransport {
145
201
  });
146
202
  }
147
203
  callProtocolV1(uuid, name, data, options) {
204
+ var _a;
148
205
  return __awaiter(this, void 0, void 0, function* () {
149
206
  if (!this._messages) {
150
207
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
151
208
  }
152
209
  const messages = this._messages;
153
210
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
154
- for (const o of buffers) {
211
+ const isFirmwareUpload = name === 'FirmwareUpload';
212
+ const uploadStartedAt = Date.now();
213
+ const totalBytes = buffers.reduce((sum, buffer) => sum + buffer.limit, 0);
214
+ let sentBytes = 0;
215
+ let lastLoggedPercent = 0;
216
+ let lastLoggedAt = uploadStartedAt;
217
+ for (const [index, o] of buffers.entries()) {
155
218
  const outData = o.toString('hex');
156
- this.Log.debug('send hex strting: ', outData);
157
219
  try {
158
- yield this.plugin.send(uuid, outData);
220
+ yield this.plugin.send(uuid, outData, getProtocolV1SendOptions(name));
221
+ sentBytes += o.limit;
222
+ if (isFirmwareUpload) {
223
+ const now = Date.now();
224
+ const percent = Math.floor(((index + 1) / buffers.length) * 100);
225
+ if (shouldLogFirmwareUploadProgress({
226
+ percent,
227
+ lastLoggedPercent,
228
+ now,
229
+ lastLoggedAt,
230
+ })) {
231
+ const elapsedSeconds = Math.max((now - uploadStartedAt) / 1000, 0.001);
232
+ const kibPerSecond = sentBytes / 1024 / elapsedSeconds;
233
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[LowlevelTransport] FirmwareUpload progress: ${percent}% ` +
234
+ `(${index + 1}/${buffers.length} packets, ${sentBytes}/${totalBytes} bytes, ` +
235
+ `${elapsedSeconds.toFixed(1)}s, ${kibPerSecond.toFixed(1)} KiB/s)`);
236
+ lastLoggedPercent = percent;
237
+ lastLoggedAt = now;
238
+ }
239
+ }
159
240
  }
160
241
  catch (e) {
161
242
  this.Log.debug('lowlevel transport send error: ', e);
@@ -163,12 +244,20 @@ class LowlevelTransport {
163
244
  }
164
245
  }
165
246
  try {
166
- const response = yield this.readProtocolV1Message(options === null || options === void 0 ? void 0 : options.timeoutMs);
167
- this.Log.debug('receive data: ', response);
247
+ const response = yield this.readProtocolV1Message(uuid, options === null || options === void 0 ? void 0 : options.timeoutMs);
168
248
  const jsonData = ProtocolV1.decodeMessage(messages, response);
169
249
  return check.call(jsonData);
170
250
  }
171
251
  catch (e) {
252
+ if ((e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError &&
253
+ (options === null || options === void 0 ? void 0 : options.timeoutMs) !== PROTOCOL_PROBE_TIMEOUT_MS) {
254
+ try {
255
+ yield this.resetConnectionAfterProbe(uuid, 'V1');
256
+ }
257
+ catch (resetError) {
258
+ this.Log.debug('[LowlevelTransport] reset after Protocol V1 timeout failed:', resetError);
259
+ }
260
+ }
172
261
  if (name === 'Initialize' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS) {
173
262
  this.Log.debug('[LowlevelTransport] Protocol V1 Initialize probe call failed:', e);
174
263
  }
@@ -242,8 +331,10 @@ class LowlevelTransport {
242
331
  resetConnectionAfterProbe(uuid, protocol) {
243
332
  var _a, _b, _c, _d;
244
333
  return __awaiter(this, void 0, void 0, function* () {
334
+ yield this.protocolV2Links.invalidateLink(uuid, `Reset connection after Protocol ${protocol} probe`);
245
335
  (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
246
336
  try {
337
+ this.connectedDevices.delete(uuid);
247
338
  yield this.plugin.disconnect(uuid);
248
339
  }
249
340
  catch (error) {
@@ -251,6 +342,8 @@ class LowlevelTransport {
251
342
  }
252
343
  try {
253
344
  yield this.plugin.connect(uuid);
345
+ this.connectedDevices.add(uuid);
346
+ this.advanceProtocolV2Generation(uuid);
254
347
  }
255
348
  catch (error) {
256
349
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`, error);
@@ -307,18 +400,18 @@ class LowlevelTransport {
307
400
  }
308
401
  });
309
402
  }
310
- receiveHex(timeoutMs, commandName) {
403
+ receiveHex(uuid, timeoutMs, commandName) {
311
404
  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));
405
+ const response = yield transport.withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () => this.createProtocolTimeoutError(commandName, timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : 0));
313
406
  if (typeof response !== 'string') {
314
407
  throw new Error('Returning data is not string');
315
408
  }
316
409
  return response;
317
410
  });
318
411
  }
319
- readProtocolV1Message(timeoutMs) {
412
+ readProtocolV1Message(uuid, timeoutMs) {
320
413
  return __awaiter(this, void 0, void 0, function* () {
321
- const first = yield this.receiveHex(timeoutMs, 'ProtocolV1');
414
+ const first = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
322
415
  const firstData = transport.hexToBytes(first);
323
416
  if (!isProtocolV1TransportChunk(firstData)) {
324
417
  return first;
@@ -327,17 +420,17 @@ class LowlevelTransport {
327
420
  let buffer = firstData.slice(3);
328
421
  const expectedLength = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
329
422
  while (buffer.length < expectedLength) {
330
- const next = yield this.receiveHex(timeoutMs, 'ProtocolV1');
423
+ const next = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
331
424
  buffer = transport.concatUint8Arrays([buffer, transport.hexToBytes(next)]);
332
425
  }
333
426
  return transport.bytesToHex(buffer.slice(0, expectedLength));
334
427
  });
335
428
  }
336
- readProtocolV2Frame(uuid, timeoutMs) {
429
+ readProtocolV2Frame(uuid, timeoutMs, commandName = 'ProtocolV2') {
337
430
  return __awaiter(this, void 0, void 0, function* () {
338
431
  let assembler = this.protocolV2Assemblers.get(uuid);
339
432
  if (!assembler) {
340
- assembler = new transport.ProtocolV2FrameAssembler();
433
+ assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
341
434
  this.protocolV2Assemblers.set(uuid, assembler);
342
435
  }
343
436
  const queuedFrame = assembler.push(new Uint8Array(0));
@@ -345,7 +438,7 @@ class LowlevelTransport {
345
438
  return queuedFrame;
346
439
  let frame;
347
440
  while (!frame) {
348
- const response = yield this.receiveHex(timeoutMs, 'ProtocolV2');
441
+ const response = yield this.receiveHex(uuid, timeoutMs, commandName);
349
442
  const chunk = transport.hexToBytes(response);
350
443
  if (chunk.length > 0) {
351
444
  frame = assembler.push(chunk);
@@ -354,47 +447,74 @@ class LowlevelTransport {
354
447
  return frame;
355
448
  });
356
449
  }
357
- writeProtocolV2Frame(uuid, frame) {
450
+ writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration) {
358
451
  return __awaiter(this, void 0, void 0, function* () {
359
- for (let offset = 0; offset < frame.length; offset += LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH) {
360
- const chunk = frame.slice(offset, offset + LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH);
361
- yield this.plugin.send(uuid, transport.bytesToHex(chunk));
362
- }
452
+ yield transport.writeProtocolV2BleFrame({
453
+ frame,
454
+ packetCapacity: LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH,
455
+ assertActive: assertCurrentGeneration,
456
+ signal: context.signal,
457
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
458
+ writePacket: packet => this.plugin.send(uuid, transport.bytesToHex(packet)),
459
+ });
363
460
  });
364
461
  }
365
462
  callProtocolV2(uuid, name, data, options) {
366
- var _a, _b, _c;
367
463
  return __awaiter(this, void 0, void 0, function* () {
368
464
  if (!this._messages || !this._messagesV2) {
369
465
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
370
466
  }
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
467
  try {
386
- return yield session.call(name, data, Object.assign(Object.assign({}, options), { timeoutMs }));
468
+ return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, options);
387
469
  }
388
470
  catch (e) {
389
- (_c = this.protocolV2Assemblers.get(uuid)) === null || _c === void 0 ? void 0 : _c.reset();
390
471
  this.Log.error('lowlevel Protocol V2 call error: ', e);
391
472
  throw e;
392
473
  }
393
474
  });
394
475
  }
476
+ createProtocolV2Adapter(uuid) {
477
+ var _a;
478
+ const generation = (_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0;
479
+ const assertCurrentGeneration = () => {
480
+ if (this.protocolV2Generations.get(uuid) !== generation) {
481
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
482
+ }
483
+ };
484
+ return {
485
+ router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
486
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
487
+ generation,
488
+ prepareCall: () => {
489
+ var _a;
490
+ assertCurrentGeneration();
491
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
492
+ },
493
+ writeFrame: (frame, context) => this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
494
+ readFrame: (context) => {
495
+ assertCurrentGeneration();
496
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
497
+ },
498
+ reset: () => {
499
+ var _a;
500
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
501
+ },
502
+ logger: this.Log,
503
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
504
+ createTimeoutError: (messageName, timeout) => this.createProtocolTimeoutError(messageName, timeout),
505
+ };
506
+ }
507
+ advanceProtocolV2Generation(uuid) {
508
+ var _a;
509
+ const nextGeneration = ((_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
510
+ this.protocolV2Generations.set(uuid, nextGeneration);
511
+ return nextGeneration;
512
+ }
395
513
  cancel() {
396
514
  this.Log.debug('lowlevel-transport', 'cancel');
397
515
  }
398
516
  }
399
517
 
400
- module.exports = LowlevelTransport;
518
+ exports["default"] = LowlevelTransport;
519
+ exports.getProtocolV1SendOptions = getProtocolV1SendOptions;
520
+ 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.3",
3
+ "version": "1.2.0-alpha.30",
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.3",
24
- "@onekeyfe/hd-transport": "1.2.0-alpha.3"
23
+ "@onekeyfe/hd-shared": "1.2.0-alpha.30",
24
+ "@onekeyfe/hd-transport": "1.2.0-alpha.30"
25
25
  },
26
- "gitHead": "54a4a40baee7e376b9a810dade60007d2c9b00c9"
26
+ "gitHead": "07405d2e3dcdd0ba351ae98b7cf4f61690ef2913"
27
27
  }
package/src/index.ts CHANGED
@@ -1,21 +1,23 @@
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,
11
11
  probeProtocolV2 as probeProtocolV2Helper,
12
12
  withProtocolTimeout,
13
+ writeProtocolV2BleFrame,
13
14
  } from '@onekeyfe/hd-transport';
14
15
 
15
16
  import type EventEmitter from 'events';
16
17
  import type {
17
18
  LowLevelDevice,
18
19
  LowlevelTransportSharedPlugin,
20
+ ProtocolV2CallContext,
19
21
  ProtocolType,
20
22
  TransportCallOptions,
21
23
  } from '@onekeyfe/hd-transport';
@@ -25,8 +27,31 @@ const { check, ProtocolV1, parseConfigure } = transport;
25
27
 
26
28
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
27
29
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
28
- const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30_000;
29
30
  const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64;
31
+ const FIRMWARE_UPLOAD_LOG_PERCENT_STEP = 5;
32
+ const FIRMWARE_UPLOAD_LOG_INTERVAL_MS = 10_000;
33
+
34
+ export function shouldLogFirmwareUploadProgress({
35
+ percent,
36
+ lastLoggedPercent,
37
+ now,
38
+ lastLoggedAt,
39
+ }: {
40
+ percent: number;
41
+ lastLoggedPercent: number;
42
+ now: number;
43
+ lastLoggedAt: number;
44
+ }) {
45
+ return (
46
+ percent === 100 ||
47
+ percent - lastLoggedPercent >= FIRMWARE_UPLOAD_LOG_PERCENT_STEP ||
48
+ now - lastLoggedAt >= FIRMWARE_UPLOAD_LOG_INTERVAL_MS
49
+ );
50
+ }
51
+
52
+ export function getProtocolV1SendOptions(name: string) {
53
+ return name === 'FirmwareUpload' ? { withoutResponse: false } : undefined;
54
+ }
30
55
 
31
56
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
32
57
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
@@ -59,6 +84,43 @@ export default class LowlevelTransport {
59
84
 
60
85
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
61
86
 
87
+ private protocolV2Generations: Map<string, number> = new Map();
88
+
89
+ private connectedDevices: Set<string> = new Set();
90
+
91
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
92
+ getSchemas: () => {
93
+ if (!this._messages || !this._messagesV2) {
94
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
95
+ }
96
+ return {
97
+ protocolV1: this._messages,
98
+ protocolV2: this._messagesV2,
99
+ };
100
+ },
101
+ classifyError: () => 'link-fatal',
102
+ onLinkInvalidated: async (uuid, reason) => {
103
+ this.protocolV2Assemblers.get(uuid)?.reset();
104
+ this.Log?.debug(`[LowlevelTransport] Protocol V2 link invalidated: ${uuid}`, reason);
105
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
106
+ this.deviceProtocol.delete(uuid);
107
+ this.advanceProtocolV2Generation(uuid);
108
+ try {
109
+ await this.plugin.disconnect(uuid);
110
+ } catch (error) {
111
+ this.Log?.debug(
112
+ `[LowlevelTransport] disconnect tainted Protocol V2 link failed: ${uuid}`,
113
+ error
114
+ );
115
+ } finally {
116
+ this.connectedDevices.delete(uuid);
117
+ }
118
+ }
119
+ },
120
+ });
121
+
122
+ private protocolV2SchemaConfiguration: string | undefined;
123
+
62
124
  getProtocolType(path: string): ProtocolType | undefined {
63
125
  return this.deviceProtocol.get(path);
64
126
  }
@@ -77,7 +139,20 @@ export default class LowlevelTransport {
77
139
  }
78
140
 
79
141
  configureProtocolV2(signedData: any) {
142
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
143
+ if (this.protocolV2SchemaConfiguration === configuration) {
144
+ return;
145
+ }
146
+
147
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
80
148
  this._messagesV2 = parseConfigure(signedData);
149
+ this.protocolV2SchemaConfiguration = configuration;
150
+
151
+ if (isReconfiguration) {
152
+ this.protocolV2Links
153
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
154
+ .catch(error => this.Log?.debug('Protocol V2 schema link cleanup failed:', error));
155
+ }
81
156
  }
82
157
 
83
158
  listen() {
@@ -96,9 +171,15 @@ export default class LowlevelTransport {
96
171
  }
97
172
 
98
173
  async acquire(input: LowLevelAcquireInput) {
174
+ const alreadyConnected = this.connectedDevices.has(input.uuid);
99
175
  try {
100
176
  await this.plugin.connect(input.uuid);
177
+ if (!alreadyConnected) {
178
+ this.connectedDevices.add(input.uuid);
179
+ this.advanceProtocolV2Generation(input.uuid);
180
+ }
101
181
  } catch (error) {
182
+ this.connectedDevices.delete(input.uuid);
102
183
  this.Log.debug('lowlelvel transport connect error: ', error);
103
184
  throw ERRORS.TypedError(
104
185
  HardwareErrorCode.LowlevelTrasnportConnectError,
@@ -106,7 +187,10 @@ export default class LowlevelTransport {
106
187
  );
107
188
  }
108
189
 
109
- this.protocolV2Assemblers.set(input.uuid, new ProtocolV2FrameAssembler());
190
+ this.protocolV2Assemblers.set(
191
+ input.uuid,
192
+ new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
193
+ );
110
194
  const protocolHint = input.expectedProtocol
111
195
  ? undefined
112
196
  : this.deviceProtocolHints.get(input.uuid);
@@ -120,9 +204,12 @@ export default class LowlevelTransport {
120
204
 
121
205
  async release(uuid: string) {
122
206
  try {
207
+ await this.protocolV2Links.invalidateLink(uuid, 'Lowlevel transport released');
123
208
  await this.plugin.disconnect(uuid);
209
+ this.connectedDevices.delete(uuid);
124
210
  this.deviceProtocol.delete(uuid);
125
- this.deviceProtocolHints.delete(uuid);
211
+ // A name-derived protocol hint survives disconnect and lets fast reconnect probe
212
+ // Protocol V2 first without sending a redundant V1 Initialize.
126
213
  this.protocolV2Assemblers.delete(uuid);
127
214
  return true;
128
215
  } catch (error) {
@@ -148,20 +235,7 @@ export default class LowlevelTransport {
148
235
  `Device protocol has not been detected for ${uuid}`
149
236
  );
150
237
  }
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
- }
238
+ this.Log.debug('transport call', { name, protocol });
165
239
 
166
240
  if (protocol === 'V2') {
167
241
  return this.callProtocolV2(uuid, name, data, options);
@@ -182,12 +256,41 @@ export default class LowlevelTransport {
182
256
 
183
257
  const messages = this._messages;
184
258
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
185
- for (const o of buffers) {
259
+ const isFirmwareUpload = name === 'FirmwareUpload';
260
+ const uploadStartedAt = Date.now();
261
+ const totalBytes = buffers.reduce((sum, buffer) => sum + buffer.limit, 0);
262
+ let sentBytes = 0;
263
+ let lastLoggedPercent = 0;
264
+ let lastLoggedAt = uploadStartedAt;
265
+
266
+ for (const [index, o] of buffers.entries()) {
186
267
  const outData = o.toString('hex');
187
- // Upload resources on low-end phones may OOM
188
- this.Log.debug('send hex strting: ', outData);
189
268
  try {
190
- await this.plugin.send(uuid, outData);
269
+ await this.plugin.send(uuid, outData, getProtocolV1SendOptions(name));
270
+ sentBytes += o.limit;
271
+
272
+ if (isFirmwareUpload) {
273
+ const now = Date.now();
274
+ const percent = Math.floor(((index + 1) / buffers.length) * 100);
275
+ if (
276
+ shouldLogFirmwareUploadProgress({
277
+ percent,
278
+ lastLoggedPercent,
279
+ now,
280
+ lastLoggedAt,
281
+ })
282
+ ) {
283
+ const elapsedSeconds = Math.max((now - uploadStartedAt) / 1000, 0.001);
284
+ const kibPerSecond = sentBytes / 1024 / elapsedSeconds;
285
+ this.Log?.debug(
286
+ `[LowlevelTransport] FirmwareUpload progress: ${percent}% ` +
287
+ `(${index + 1}/${buffers.length} packets, ${sentBytes}/${totalBytes} bytes, ` +
288
+ `${elapsedSeconds.toFixed(1)}s, ${kibPerSecond.toFixed(1)} KiB/s)`
289
+ );
290
+ lastLoggedPercent = percent;
291
+ lastLoggedAt = now;
292
+ }
293
+ }
191
294
  } catch (e) {
192
295
  this.Log.debug('lowlevel transport send error: ', e);
193
296
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError, e.reason);
@@ -195,11 +298,20 @@ export default class LowlevelTransport {
195
298
  }
196
299
 
197
300
  try {
198
- const response = await this.readProtocolV1Message(options?.timeoutMs);
199
- this.Log.debug('receive data: ', response);
301
+ const response = await this.readProtocolV1Message(uuid, options?.timeoutMs);
200
302
  const jsonData = ProtocolV1.decodeMessage(messages, response);
201
303
  return check.call(jsonData);
202
304
  } catch (e) {
305
+ if (
306
+ e?.errorCode === HardwareErrorCode.BleTimeoutError &&
307
+ options?.timeoutMs !== PROTOCOL_PROBE_TIMEOUT_MS
308
+ ) {
309
+ try {
310
+ await this.resetConnectionAfterProbe(uuid, 'V1');
311
+ } catch (resetError) {
312
+ this.Log.debug('[LowlevelTransport] reset after Protocol V1 timeout failed:', resetError);
313
+ }
314
+ }
203
315
  if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
204
316
  this.Log.debug('[LowlevelTransport] Protocol V1 Initialize probe call failed:', e);
205
317
  } else {
@@ -291,9 +403,14 @@ export default class LowlevelTransport {
291
403
  }
292
404
 
293
405
  private async resetConnectionAfterProbe(uuid: string, protocol: ProtocolType) {
406
+ await this.protocolV2Links.invalidateLink(
407
+ uuid,
408
+ `Reset connection after Protocol ${protocol} probe`
409
+ );
294
410
  this.protocolV2Assemblers.get(uuid)?.reset();
295
411
 
296
412
  try {
413
+ this.connectedDevices.delete(uuid);
297
414
  await this.plugin.disconnect(uuid);
298
415
  } catch (error) {
299
416
  this.Log?.debug(
@@ -304,6 +421,8 @@ export default class LowlevelTransport {
304
421
 
305
422
  try {
306
423
  await this.plugin.connect(uuid);
424
+ this.connectedDevices.add(uuid);
425
+ this.advanceProtocolV2Generation(uuid);
307
426
  } catch (error) {
308
427
  this.Log?.debug(
309
428
  `[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`,
@@ -361,8 +480,8 @@ export default class LowlevelTransport {
361
480
  }
362
481
  }
363
482
 
364
- private async receiveHex(timeoutMs: number | undefined, commandName: string) {
365
- const response = await withProtocolTimeout(this.plugin.receive(), timeoutMs, () =>
483
+ private async receiveHex(uuid: string, timeoutMs: number | undefined, commandName: string) {
484
+ const response = await withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () =>
366
485
  this.createProtocolTimeoutError(commandName, timeoutMs ?? 0)
367
486
  );
368
487
  if (typeof response !== 'string') {
@@ -371,8 +490,8 @@ export default class LowlevelTransport {
371
490
  return response;
372
491
  }
373
492
 
374
- private async readProtocolV1Message(timeoutMs?: number) {
375
- const first = await this.receiveHex(timeoutMs, 'ProtocolV1');
493
+ private async readProtocolV1Message(uuid: string, timeoutMs?: number) {
494
+ const first = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
376
495
  const firstData = hexToBytes(first);
377
496
  if (!isProtocolV1TransportChunk(firstData)) {
378
497
  return first;
@@ -383,17 +502,17 @@ export default class LowlevelTransport {
383
502
  const expectedLength = PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
384
503
 
385
504
  while (buffer.length < expectedLength) {
386
- const next = await this.receiveHex(timeoutMs, 'ProtocolV1');
505
+ const next = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
387
506
  buffer = concatUint8Arrays([buffer, hexToBytes(next)]);
388
507
  }
389
508
 
390
509
  return bytesToHex(buffer.slice(0, expectedLength));
391
510
  }
392
511
 
393
- private async readProtocolV2Frame(uuid: string, timeoutMs?: number) {
512
+ private async readProtocolV2Frame(uuid: string, timeoutMs?: number, commandName = 'ProtocolV2') {
394
513
  let assembler = this.protocolV2Assemblers.get(uuid);
395
514
  if (!assembler) {
396
- assembler = new ProtocolV2FrameAssembler();
515
+ assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
397
516
  this.protocolV2Assemblers.set(uuid, assembler);
398
517
  }
399
518
 
@@ -402,7 +521,7 @@ export default class LowlevelTransport {
402
521
 
403
522
  let frame: Uint8Array | undefined;
404
523
  while (!frame) {
405
- const response = await this.receiveHex(timeoutMs, 'ProtocolV2');
524
+ const response = await this.receiveHex(uuid, timeoutMs, commandName);
406
525
  const chunk = hexToBytes(response);
407
526
  if (chunk.length > 0) {
408
527
  frame = assembler.push(chunk);
@@ -411,11 +530,20 @@ export default class LowlevelTransport {
411
530
  return frame;
412
531
  }
413
532
 
414
- private async writeProtocolV2Frame(uuid: string, frame: Uint8Array) {
415
- for (let offset = 0; offset < frame.length; offset += LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH) {
416
- const chunk = frame.slice(offset, offset + LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH);
417
- await this.plugin.send(uuid, bytesToHex(chunk));
418
- }
533
+ private async writeProtocolV2Frame(
534
+ uuid: string,
535
+ frame: Uint8Array,
536
+ context: ProtocolV2CallContext,
537
+ assertCurrentGeneration: () => void
538
+ ) {
539
+ await writeProtocolV2BleFrame({
540
+ frame,
541
+ packetCapacity: LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH,
542
+ assertActive: assertCurrentGeneration,
543
+ signal: context.signal,
544
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
545
+ writePacket: packet => this.plugin.send(uuid, bytesToHex(packet)),
546
+ });
419
547
  }
420
548
 
421
549
  private async callProtocolV2(
@@ -428,34 +556,58 @@ export default class LowlevelTransport {
428
556
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
429
557
  }
430
558
 
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
559
  try {
448
- return await session.call(name, data, {
449
- ...options,
450
- timeoutMs,
451
- });
560
+ return await this.protocolV2Links.call(
561
+ uuid,
562
+ () => this.createProtocolV2Adapter(uuid),
563
+ name,
564
+ data,
565
+ options
566
+ );
452
567
  } catch (e) {
453
- this.protocolV2Assemblers.get(uuid)?.reset();
454
568
  this.Log.error('lowlevel Protocol V2 call error: ', e);
455
569
  throw e;
456
570
  }
457
571
  }
458
572
 
573
+ private createProtocolV2Adapter(uuid: string) {
574
+ const generation = this.protocolV2Generations.get(uuid) ?? 0;
575
+ const assertCurrentGeneration = () => {
576
+ if (this.protocolV2Generations.get(uuid) !== generation) {
577
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
578
+ }
579
+ };
580
+
581
+ return {
582
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
583
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
584
+ generation,
585
+ prepareCall: () => {
586
+ assertCurrentGeneration();
587
+ this.protocolV2Assemblers.get(uuid)?.reset();
588
+ },
589
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
590
+ this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
591
+ readFrame: (context: { messageName: string; timeoutMs?: number }) => {
592
+ assertCurrentGeneration();
593
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
594
+ },
595
+ reset: () => {
596
+ this.protocolV2Assemblers.get(uuid)?.reset();
597
+ },
598
+ logger: this.Log,
599
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
600
+ createTimeoutError: (messageName: string, timeout: number) =>
601
+ this.createProtocolTimeoutError(messageName, timeout),
602
+ };
603
+ }
604
+
605
+ private advanceProtocolV2Generation(uuid: string) {
606
+ const nextGeneration = (this.protocolV2Generations.get(uuid) ?? 0) + 1;
607
+ this.protocolV2Generations.set(uuid, nextGeneration);
608
+ return nextGeneration;
609
+ }
610
+
459
611
  cancel() {
460
612
  this.Log.debug('lowlevel-transport', 'cancel');
461
613
  }