@onekeyfe/hd-transport-lowlevel 1.2.0-alpha.8 → 1.2.0-alpha.81

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
+ });
@@ -122,6 +122,49 @@ const splitFrame = (frame, index) => [
122
122
  ];
123
123
 
124
124
  describe('LowlevelTransport protocol framing', () => {
125
+ test('falls back to Protocol V2 when a cached V1 hint is stale', async () => {
126
+ const plugin = createPlugin({ devices: [], responses: [] });
127
+ const lowlevel = configureTransport(plugin);
128
+ const events = [];
129
+ lowlevel.probeProtocolV1 = jest.fn().mockImplementation(() => {
130
+ events.push('probe-v1');
131
+ return Promise.resolve(false);
132
+ });
133
+ lowlevel.resetConnectionAfterProbe = jest.fn().mockImplementation(() => {
134
+ events.push('reset');
135
+ return Promise.resolve();
136
+ });
137
+ lowlevel.probeProtocolV2 = jest.fn().mockImplementation(() => {
138
+ events.push('probe-v2');
139
+ return Promise.resolve(true);
140
+ });
141
+
142
+ await expect(lowlevel.detectProtocol('pro-lowlevel', undefined, 'V1')).resolves.toBe('V2');
143
+
144
+ expect(events).toEqual(['probe-v1', 'reset', 'probe-v2']);
145
+ expect(lowlevel.getProtocolType('pro-lowlevel')).toBe('V2');
146
+ });
147
+
148
+ test('keeps active links when the Protocol V2 schema is configured repeatedly', () => {
149
+ const lowlevel = new LowlevelTransport();
150
+ const invalidateAllLinks = jest.fn().mockResolvedValue(undefined);
151
+ lowlevel.protocolV2Links.invalidateAllLinks = invalidateAllLinks;
152
+
153
+ lowlevel.configureProtocolV2(protocolV2Schema);
154
+ lowlevel.configureProtocolV2(protocolV2Schema);
155
+
156
+ expect(invalidateAllLinks).not.toHaveBeenCalled();
157
+
158
+ lowlevel.configureProtocolV2({
159
+ nested: {
160
+ ...protocolV2Schema.nested,
161
+ ExtraMessage: { fields: {} },
162
+ },
163
+ });
164
+
165
+ expect(invalidateAllLinks).toHaveBeenCalledWith('Protocol V2 schema reconfigured');
166
+ });
167
+
125
168
  test('keeps Protocol V1 raw notification chunks compatible', async () => {
126
169
  const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', {
127
170
  message: 'ok',
@@ -142,6 +185,23 @@ describe('LowlevelTransport protocol framing', () => {
142
185
  });
143
186
  });
144
187
 
188
+ test('uses the Protocol V2 BLE writer with the lowlevel compatibility packet size', async () => {
189
+ const plugin = createPlugin({ devices: [], responses: [] });
190
+ const lowlevel = configureTransport(plugin);
191
+ const context = {
192
+ messageName: 'Ping',
193
+ timeoutMs: 1000,
194
+ highThroughput: false,
195
+ generation: 1,
196
+ signal: new AbortController().signal,
197
+ };
198
+
199
+ await lowlevel.writeProtocolV2Frame('pro2-id', new Uint8Array(130), context, jest.fn());
200
+
201
+ expect(plugin.send).toHaveBeenCalledTimes(3);
202
+ expect(plugin.send.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([64, 64, 2]);
203
+ });
204
+
145
205
  test('rejects calls before protocol detection', async () => {
146
206
  const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', {
147
207
  message: 'ok',
@@ -172,7 +232,7 @@ describe('LowlevelTransport protocol framing', () => {
172
232
  supported_messages: [60200, 60201, 60206, 60207],
173
233
  protobuf_definition: 'onekey-protocol-v2',
174
234
  },
175
- { router: PROTOCOL_V2_CHANNEL_BLE_UART }
235
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: 2 }
176
236
  );
177
237
  const plugin = createPlugin({
178
238
  devices: [{ id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
@@ -196,6 +256,10 @@ describe('LowlevelTransport protocol framing', () => {
196
256
  },
197
257
  });
198
258
  expect(plugin.send).toHaveBeenCalled();
259
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
260
+ Number.parseInt(hex.slice(12, 14), 16)
261
+ );
262
+ expect(sentSeqs).toEqual([1, 2]);
199
263
  });
200
264
 
201
265
  test('falls back to Protocol V2 probe for unnamed Protocol V2 devices', async () => {
@@ -218,6 +282,97 @@ describe('LowlevelTransport protocol framing', () => {
218
282
  expect(lowlevel.getProtocolType('unknown-pro2-id')).toBe('V2');
219
283
  });
220
284
 
285
+ test('retains the Protocol V2 hint and sequence cursor across release and reacquire', async () => {
286
+ const probeResponse = ProtocolV2.encodeFrame(
287
+ schemas,
288
+ 'Success',
289
+ { message: 'ok' },
290
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
291
+ );
292
+ const plugin = createPlugin({
293
+ devices: [{ id: 'reconnect-pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
294
+ responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)],
295
+ });
296
+ const lowlevel = configureTransport(plugin);
297
+
298
+ await lowlevel.enumerate();
299
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
300
+ uuid: 'reconnect-pro2-id',
301
+ protocolType: 'V2',
302
+ });
303
+ await lowlevel.release('reconnect-pro2-id');
304
+ await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({
305
+ uuid: 'reconnect-pro2-id',
306
+ protocolType: 'V2',
307
+ });
308
+
309
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
310
+ Number.parseInt(hex.slice(12, 14), 16)
311
+ );
312
+ expect(sentSeqs).toEqual([1, 2]);
313
+ });
314
+
315
+ test('reuses the active generation when Core acquires the same BLE connection again', async () => {
316
+ const responses = [1, 2, 3, 4].map(seq =>
317
+ bytesToHex(
318
+ ProtocolV2.encodeFrame(
319
+ schemas,
320
+ 'Success',
321
+ { message: 'ok' },
322
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART, seq }
323
+ )
324
+ )
325
+ );
326
+ const plugin = createPlugin({
327
+ devices: [{ id: 'repeated-acquire-id', name: 'OneKey Pro 2', commType: 'ble' }],
328
+ responses,
329
+ });
330
+ const lowlevel = configureTransport(plugin);
331
+
332
+ await expect(
333
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
334
+ ).resolves.toEqual({
335
+ uuid: 'repeated-acquire-id',
336
+ protocolType: 'V2',
337
+ });
338
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'first-acquire' });
339
+ await expect(
340
+ lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' })
341
+ ).resolves.toEqual({
342
+ uuid: 'repeated-acquire-id',
343
+ protocolType: 'V2',
344
+ });
345
+ await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'second-acquire' });
346
+
347
+ const sentSeqs = plugin.send.mock.calls.map(([, hex]) =>
348
+ Number.parseInt(hex.slice(12, 14), 16)
349
+ );
350
+ expect(sentSeqs).toEqual([1, 2, 3, 4]);
351
+ });
352
+
353
+ test('actively probes explicit Protocol V2 during bootloader reconnect', async () => {
354
+ const probeResponse = ProtocolV2.encodeFrame(
355
+ schemas,
356
+ 'Success',
357
+ { message: 'ok' },
358
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
359
+ );
360
+ const plugin = createPlugin({
361
+ devices: [{ id: 'bootloader-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
362
+ responses: [bytesToHex(probeResponse)],
363
+ });
364
+ const lowlevel = configureTransport(plugin);
365
+
366
+ await expect(
367
+ lowlevel.acquire({ uuid: 'bootloader-v2-id', expectedProtocol: 'V2' })
368
+ ).resolves.toEqual({
369
+ uuid: 'bootloader-v2-id',
370
+ protocolType: 'V2',
371
+ });
372
+ expect(plugin.send).toHaveBeenCalledTimes(1);
373
+ expect(plugin.receive).toHaveBeenCalledTimes(1);
374
+ });
375
+
221
376
  test('resets the lowlevel connection before probing Protocol V2 after a V1 timeout', async () => {
222
377
  const probeResponse = ProtocolV2.encodeFrame(
223
378
  schemas,
@@ -256,6 +411,57 @@ describe('LowlevelTransport protocol framing', () => {
256
411
  expect(plugin.connect).toHaveBeenCalledTimes(2);
257
412
  });
258
413
 
414
+ test('disconnects a tainted Protocol V2 link after a response timeout', async () => {
415
+ const probeResponse = ProtocolV2.encodeFrame(
416
+ schemas,
417
+ 'Success',
418
+ { message: 'ok' },
419
+ { router: PROTOCOL_V2_CHANNEL_BLE_UART }
420
+ );
421
+ const plugin = createPlugin({
422
+ devices: [{ id: 'timeout-v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
423
+ responses: [bytesToHex(probeResponse)],
424
+ });
425
+ let receiveCount = 0;
426
+ plugin.receive.mockImplementation(() => {
427
+ receiveCount += 1;
428
+ return receiveCount === 1
429
+ ? Promise.resolve(bytesToHex(probeResponse))
430
+ : new Promise(() => {});
431
+ });
432
+ const lowlevel = configureTransport(plugin);
433
+
434
+ await lowlevel.acquire({ uuid: 'timeout-v2-id', expectedProtocol: 'V2' });
435
+ plugin.disconnect.mockClear();
436
+ await expect(
437
+ lowlevel.call('timeout-v2-id', 'Ping', { message: 'timeout' }, { timeoutMs: 10 })
438
+ ).rejects.toThrow('Lowlevel response timeout after 10ms for Ping');
439
+
440
+ expect(plugin.disconnect).toHaveBeenCalledWith('timeout-v2-id');
441
+ });
442
+
443
+ test('preserves an undefined business timeout outside explicit probes', async () => {
444
+ const plugin = createPlugin({
445
+ devices: [{ id: 'v2-id', name: 'OneKey Pro 2', commType: 'ble' }],
446
+ responses: [],
447
+ });
448
+ const lowlevel = configureTransport(plugin);
449
+ lowlevel.deviceProtocol.set('v2-id', 'V2');
450
+ const linkCall = jest
451
+ .spyOn(lowlevel.protocolV2Links, 'call')
452
+ .mockResolvedValue({ type: 'Success', message: {} });
453
+
454
+ await lowlevel.call('v2-id', 'Ping', { message: 'no-business-timeout' });
455
+
456
+ expect(linkCall).toHaveBeenCalledWith(
457
+ 'v2-id',
458
+ expect.any(Function),
459
+ 'Ping',
460
+ { message: 'no-business-timeout' },
461
+ undefined
462
+ );
463
+ });
464
+
259
465
  test('verifies expected Protocol V1 instead of trusting the requested protocol', async () => {
260
466
  const plugin = createPlugin({
261
467
  devices: [{ id: 'v2-id', name: 'Unknown BLE Device', commType: 'ble' }],
package/dist/index.d.ts CHANGED
@@ -5,8 +5,18 @@ import EventEmitter from 'events';
5
5
  type LowLevelAcquireInput = {
6
6
  uuid: string;
7
7
  expectedProtocol?: ProtocolType;
8
+ protocolHint?: ProtocolType;
8
9
  };
9
10
 
11
+ declare function shouldLogFirmwareUploadProgress({ percent, lastLoggedPercent, now, lastLoggedAt, }: {
12
+ percent: number;
13
+ lastLoggedPercent: number;
14
+ now: number;
15
+ lastLoggedAt: number;
16
+ }): boolean;
17
+ declare function getProtocolV1SendOptions(name: string): {
18
+ withoutResponse: boolean;
19
+ } | undefined;
10
20
  declare class LowlevelTransport {
11
21
  _messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
12
22
  _messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
@@ -17,6 +27,10 @@ declare class LowlevelTransport {
17
27
  private deviceProtocol;
18
28
  private deviceProtocolHints;
19
29
  private protocolV2Assemblers;
30
+ private protocolV2Generations;
31
+ private connectedDevices;
32
+ private protocolV2Links;
33
+ private protocolV2SchemaConfiguration;
20
34
  getProtocolType(path: string): ProtocolType | undefined;
21
35
  init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin): void;
22
36
  configure(signedData: any): void;
@@ -43,7 +57,9 @@ declare class LowlevelTransport {
43
57
  private readProtocolV2Frame;
44
58
  private writeProtocolV2Frame;
45
59
  private callProtocolV2;
60
+ private createProtocolV2Adapter;
61
+ private advanceProtocolV2Generation;
46
62
  cancel(): void;
47
63
  }
48
64
 
49
- export { LowlevelTransport as default };
65
+ 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,EAC7B,YAAY,EAEZ,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;YA2Cd,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
  }
@@ -91,19 +145,25 @@ class LowlevelTransport {
91
145
  });
92
146
  }
93
147
  acquire(input) {
94
- var _a;
148
+ var _a, _b;
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
- : this.deviceProtocolHints.get(input.uuid);
166
+ : (_b = input.protocolHint) !== null && _b !== void 0 ? _b : this.deviceProtocolHints.get(input.uuid);
107
167
  const protocolType = yield this.detectProtocol(input.uuid, input.expectedProtocol, protocolHint);
108
168
  return { uuid: input.uuid, protocolType };
109
169
  });
@@ -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
  }
@@ -194,7 +283,7 @@ class LowlevelTransport {
194
283
  }
195
284
  }
196
285
  detectProtocol(uuid, expectedProtocol, protocolHint) {
197
- var _a, _b, _c, _d, _e, _f;
286
+ var _a, _b, _c;
198
287
  return __awaiter(this, void 0, void 0, function* () {
199
288
  if (expectedProtocol === 'V2') {
200
289
  if (yield this.probeProtocolV2(uuid)) {
@@ -212,28 +301,17 @@ class LowlevelTransport {
212
301
  }
213
302
  throw this.createProtocolMismatchError(expectedProtocol);
214
303
  }
215
- if (protocolHint === 'V2' && (yield this.probeProtocolV2(uuid))) {
216
- this.deviceProtocol.set(uuid, 'V2');
217
- (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (hint)`);
218
- return 'V2';
219
- }
220
- const cachedProtocol = this.deviceProtocol.get(uuid);
221
- if (cachedProtocol === 'V2' && (yield this.probeProtocolV2(uuid))) {
222
- this.deviceProtocol.set(uuid, 'V2');
223
- (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
224
- return 'V2';
225
- }
226
- const protocolV1Detected = yield this.probeProtocolV1(uuid);
227
- if (protocolV1Detected) {
228
- this.deviceProtocol.set(uuid, 'V1');
229
- (_e = this.Log) === null || _e === void 0 ? void 0 : _e.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1`);
230
- return 'V1';
231
- }
232
- yield this.resetConnectionAfterProbe(uuid, 'V1');
233
- if (yield this.probeProtocolV2(uuid)) {
234
- this.deviceProtocol.set(uuid, 'V2');
235
- (_f = this.Log) === null || _f === void 0 ? void 0 : _f.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2`);
236
- return 'V2';
304
+ const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
305
+ for (const [index, protocol] of probeOrder.entries()) {
306
+ if (index > 0) {
307
+ yield this.resetConnectionAfterProbe(uuid, probeOrder[index - 1]);
308
+ }
309
+ const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
310
+ if (detected) {
311
+ this.deviceProtocol.set(uuid, protocol);
312
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
313
+ return protocol;
314
+ }
237
315
  }
238
316
  this.deviceProtocol.delete(uuid);
239
317
  throw this.createProtocolDetectionError();
@@ -242,8 +320,10 @@ class LowlevelTransport {
242
320
  resetConnectionAfterProbe(uuid, protocol) {
243
321
  var _a, _b, _c, _d;
244
322
  return __awaiter(this, void 0, void 0, function* () {
323
+ yield this.protocolV2Links.invalidateLink(uuid, `Reset connection after Protocol ${protocol} probe`);
245
324
  (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
246
325
  try {
326
+ this.connectedDevices.delete(uuid);
247
327
  yield this.plugin.disconnect(uuid);
248
328
  }
249
329
  catch (error) {
@@ -251,6 +331,8 @@ class LowlevelTransport {
251
331
  }
252
332
  try {
253
333
  yield this.plugin.connect(uuid);
334
+ this.connectedDevices.add(uuid);
335
+ this.advanceProtocolV2Generation(uuid);
254
336
  }
255
337
  catch (error) {
256
338
  (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`, error);
@@ -307,18 +389,18 @@ class LowlevelTransport {
307
389
  }
308
390
  });
309
391
  }
310
- receiveHex(timeoutMs, commandName) {
392
+ receiveHex(uuid, timeoutMs, commandName) {
311
393
  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));
394
+ const response = yield transport.withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () => this.createProtocolTimeoutError(commandName, timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : 0));
313
395
  if (typeof response !== 'string') {
314
396
  throw new Error('Returning data is not string');
315
397
  }
316
398
  return response;
317
399
  });
318
400
  }
319
- readProtocolV1Message(timeoutMs) {
401
+ readProtocolV1Message(uuid, timeoutMs) {
320
402
  return __awaiter(this, void 0, void 0, function* () {
321
- const first = yield this.receiveHex(timeoutMs, 'ProtocolV1');
403
+ const first = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
322
404
  const firstData = transport.hexToBytes(first);
323
405
  if (!isProtocolV1TransportChunk(firstData)) {
324
406
  return first;
@@ -327,17 +409,17 @@ class LowlevelTransport {
327
409
  let buffer = firstData.slice(3);
328
410
  const expectedLength = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
329
411
  while (buffer.length < expectedLength) {
330
- const next = yield this.receiveHex(timeoutMs, 'ProtocolV1');
412
+ const next = yield this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
331
413
  buffer = transport.concatUint8Arrays([buffer, transport.hexToBytes(next)]);
332
414
  }
333
415
  return transport.bytesToHex(buffer.slice(0, expectedLength));
334
416
  });
335
417
  }
336
- readProtocolV2Frame(uuid, timeoutMs) {
418
+ readProtocolV2Frame(uuid, timeoutMs, commandName = 'ProtocolV2') {
337
419
  return __awaiter(this, void 0, void 0, function* () {
338
420
  let assembler = this.protocolV2Assemblers.get(uuid);
339
421
  if (!assembler) {
340
- assembler = new transport.ProtocolV2FrameAssembler();
422
+ assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
341
423
  this.protocolV2Assemblers.set(uuid, assembler);
342
424
  }
343
425
  const queuedFrame = assembler.push(new Uint8Array(0));
@@ -345,7 +427,7 @@ class LowlevelTransport {
345
427
  return queuedFrame;
346
428
  let frame;
347
429
  while (!frame) {
348
- const response = yield this.receiveHex(timeoutMs, 'ProtocolV2');
430
+ const response = yield this.receiveHex(uuid, timeoutMs, commandName);
349
431
  const chunk = transport.hexToBytes(response);
350
432
  if (chunk.length > 0) {
351
433
  frame = assembler.push(chunk);
@@ -354,47 +436,74 @@ class LowlevelTransport {
354
436
  return frame;
355
437
  });
356
438
  }
357
- writeProtocolV2Frame(uuid, frame) {
439
+ writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration) {
358
440
  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
- }
441
+ yield transport.writeProtocolV2BleFrame({
442
+ frame,
443
+ packetCapacity: LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH,
444
+ assertActive: assertCurrentGeneration,
445
+ signal: context.signal,
446
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
447
+ writePacket: packet => this.plugin.send(uuid, transport.bytesToHex(packet)),
448
+ });
363
449
  });
364
450
  }
365
451
  callProtocolV2(uuid, name, data, options) {
366
- var _a, _b, _c;
367
452
  return __awaiter(this, void 0, void 0, function* () {
368
453
  if (!this._messages || !this._messagesV2) {
369
454
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
370
455
  }
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
456
  try {
386
- return yield session.call(name, data, Object.assign(Object.assign({}, options), { timeoutMs }));
457
+ return yield this.protocolV2Links.call(uuid, () => this.createProtocolV2Adapter(uuid), name, data, options);
387
458
  }
388
459
  catch (e) {
389
- (_c = this.protocolV2Assemblers.get(uuid)) === null || _c === void 0 ? void 0 : _c.reset();
390
460
  this.Log.error('lowlevel Protocol V2 call error: ', e);
391
461
  throw e;
392
462
  }
393
463
  });
394
464
  }
465
+ createProtocolV2Adapter(uuid) {
466
+ var _a;
467
+ const generation = (_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0;
468
+ const assertCurrentGeneration = () => {
469
+ if (this.protocolV2Generations.get(uuid) !== generation) {
470
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
471
+ }
472
+ };
473
+ return {
474
+ router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
475
+ maxFrameBytes: transport.PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
476
+ generation,
477
+ prepareCall: () => {
478
+ var _a;
479
+ assertCurrentGeneration();
480
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
481
+ },
482
+ writeFrame: (frame, context) => this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
483
+ readFrame: (context) => {
484
+ assertCurrentGeneration();
485
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
486
+ },
487
+ reset: () => {
488
+ var _a;
489
+ (_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
490
+ },
491
+ logger: this.Log,
492
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
493
+ createTimeoutError: (messageName, timeout) => this.createProtocolTimeoutError(messageName, timeout),
494
+ };
495
+ }
496
+ advanceProtocolV2Generation(uuid) {
497
+ var _a;
498
+ const nextGeneration = ((_a = this.protocolV2Generations.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
499
+ this.protocolV2Generations.set(uuid, nextGeneration);
500
+ return nextGeneration;
501
+ }
395
502
  cancel() {
396
503
  this.Log.debug('lowlevel-transport', 'cancel');
397
504
  }
398
505
  }
399
506
 
400
- module.exports = LowlevelTransport;
507
+ exports["default"] = LowlevelTransport;
508
+ exports.getProtocolV1SendOptions = getProtocolV1SendOptions;
509
+ exports.shouldLogFirmwareUploadProgress = shouldLogFirmwareUploadProgress;
package/dist/types.d.ts CHANGED
@@ -2,5 +2,6 @@ import type { ProtocolType } from '@onekeyfe/hd-transport';
2
2
  export type LowLevelAcquireInput = {
3
3
  uuid: string;
4
4
  expectedProtocol?: ProtocolType;
5
+ protocolHint?: ProtocolType;
5
6
  };
6
7
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,YAAY,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-lowlevel",
3
- "version": "1.2.0-alpha.8",
3
+ "version": "1.2.0-alpha.81",
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.8",
24
- "@onekeyfe/hd-transport": "1.2.0-alpha.8"
23
+ "@onekeyfe/hd-shared": "1.2.0-alpha.81",
24
+ "@onekeyfe/hd-transport": "1.2.0-alpha.81"
25
25
  },
26
- "gitHead": "d2b1323e5ad9c373e72d4d092f35c9f274c1fd74"
26
+ "gitHead": "8f6b4dca4ab97dc0a83f7ace61c24a0f0e193016"
27
27
  }
package/src/index.ts CHANGED
@@ -1,15 +1,16 @@
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';
@@ -17,6 +18,7 @@ import type {
17
18
  LowLevelDevice,
18
19
  LowlevelTransportSharedPlugin,
19
20
  ProtocolType,
21
+ ProtocolV2CallContext,
20
22
  TransportCallOptions,
21
23
  } from '@onekeyfe/hd-transport';
22
24
  import type { LowLevelAcquireInput } from './types';
@@ -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,10 +187,13 @@ 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
- : this.deviceProtocolHints.get(input.uuid);
196
+ : input.protocolHint ?? this.deviceProtocolHints.get(input.uuid);
113
197
  const protocolType = await this.detectProtocol(
114
198
  input.uuid,
115
199
  input.expectedProtocol,
@@ -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 {
@@ -259,31 +371,20 @@ export default class LowlevelTransport {
259
371
  throw this.createProtocolMismatchError(expectedProtocol);
260
372
  }
261
373
 
262
- if (protocolHint === 'V2' && (await this.probeProtocolV2(uuid))) {
263
- this.deviceProtocol.set(uuid, 'V2');
264
- this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (hint)`);
265
- return 'V2';
266
- }
267
-
268
- const cachedProtocol = this.deviceProtocol.get(uuid);
269
- if (cachedProtocol === 'V2' && (await this.probeProtocolV2(uuid))) {
270
- this.deviceProtocol.set(uuid, 'V2');
271
- this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
272
- return 'V2';
273
- }
274
-
275
- const protocolV1Detected = await this.probeProtocolV1(uuid);
276
- if (protocolV1Detected) {
277
- this.deviceProtocol.set(uuid, 'V1');
278
- this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1`);
279
- return 'V1';
280
- }
374
+ const probeOrder: ProtocolType[] =
375
+ protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
281
376
 
282
- await this.resetConnectionAfterProbe(uuid, 'V1');
283
- if (await this.probeProtocolV2(uuid)) {
284
- this.deviceProtocol.set(uuid, 'V2');
285
- this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2`);
286
- return 'V2';
377
+ for (const [index, protocol] of probeOrder.entries()) {
378
+ if (index > 0) {
379
+ await this.resetConnectionAfterProbe(uuid, probeOrder[index - 1]);
380
+ }
381
+ const detected =
382
+ protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
383
+ if (detected) {
384
+ this.deviceProtocol.set(uuid, protocol);
385
+ this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
386
+ return protocol;
387
+ }
287
388
  }
288
389
 
289
390
  this.deviceProtocol.delete(uuid);
@@ -291,9 +392,14 @@ export default class LowlevelTransport {
291
392
  }
292
393
 
293
394
  private async resetConnectionAfterProbe(uuid: string, protocol: ProtocolType) {
395
+ await this.protocolV2Links.invalidateLink(
396
+ uuid,
397
+ `Reset connection after Protocol ${protocol} probe`
398
+ );
294
399
  this.protocolV2Assemblers.get(uuid)?.reset();
295
400
 
296
401
  try {
402
+ this.connectedDevices.delete(uuid);
297
403
  await this.plugin.disconnect(uuid);
298
404
  } catch (error) {
299
405
  this.Log?.debug(
@@ -304,6 +410,8 @@ export default class LowlevelTransport {
304
410
 
305
411
  try {
306
412
  await this.plugin.connect(uuid);
413
+ this.connectedDevices.add(uuid);
414
+ this.advanceProtocolV2Generation(uuid);
307
415
  } catch (error) {
308
416
  this.Log?.debug(
309
417
  `[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`,
@@ -361,8 +469,8 @@ export default class LowlevelTransport {
361
469
  }
362
470
  }
363
471
 
364
- private async receiveHex(timeoutMs: number | undefined, commandName: string) {
365
- const response = await withProtocolTimeout(this.plugin.receive(), timeoutMs, () =>
472
+ private async receiveHex(uuid: string, timeoutMs: number | undefined, commandName: string) {
473
+ const response = await withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () =>
366
474
  this.createProtocolTimeoutError(commandName, timeoutMs ?? 0)
367
475
  );
368
476
  if (typeof response !== 'string') {
@@ -371,8 +479,8 @@ export default class LowlevelTransport {
371
479
  return response;
372
480
  }
373
481
 
374
- private async readProtocolV1Message(timeoutMs?: number) {
375
- const first = await this.receiveHex(timeoutMs, 'ProtocolV1');
482
+ private async readProtocolV1Message(uuid: string, timeoutMs?: number) {
483
+ const first = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
376
484
  const firstData = hexToBytes(first);
377
485
  if (!isProtocolV1TransportChunk(firstData)) {
378
486
  return first;
@@ -383,17 +491,17 @@ export default class LowlevelTransport {
383
491
  const expectedLength = PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
384
492
 
385
493
  while (buffer.length < expectedLength) {
386
- const next = await this.receiveHex(timeoutMs, 'ProtocolV1');
494
+ const next = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1');
387
495
  buffer = concatUint8Arrays([buffer, hexToBytes(next)]);
388
496
  }
389
497
 
390
498
  return bytesToHex(buffer.slice(0, expectedLength));
391
499
  }
392
500
 
393
- private async readProtocolV2Frame(uuid: string, timeoutMs?: number) {
501
+ private async readProtocolV2Frame(uuid: string, timeoutMs?: number, commandName = 'ProtocolV2') {
394
502
  let assembler = this.protocolV2Assemblers.get(uuid);
395
503
  if (!assembler) {
396
- assembler = new ProtocolV2FrameAssembler();
504
+ assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES);
397
505
  this.protocolV2Assemblers.set(uuid, assembler);
398
506
  }
399
507
 
@@ -402,7 +510,7 @@ export default class LowlevelTransport {
402
510
 
403
511
  let frame: Uint8Array | undefined;
404
512
  while (!frame) {
405
- const response = await this.receiveHex(timeoutMs, 'ProtocolV2');
513
+ const response = await this.receiveHex(uuid, timeoutMs, commandName);
406
514
  const chunk = hexToBytes(response);
407
515
  if (chunk.length > 0) {
408
516
  frame = assembler.push(chunk);
@@ -411,11 +519,20 @@ export default class LowlevelTransport {
411
519
  return frame;
412
520
  }
413
521
 
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
- }
522
+ private async writeProtocolV2Frame(
523
+ uuid: string,
524
+ frame: Uint8Array,
525
+ context: ProtocolV2CallContext,
526
+ assertCurrentGeneration: () => void
527
+ ) {
528
+ await writeProtocolV2BleFrame({
529
+ frame,
530
+ packetCapacity: LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH,
531
+ assertActive: assertCurrentGeneration,
532
+ signal: context.signal,
533
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
534
+ writePacket: packet => this.plugin.send(uuid, bytesToHex(packet)),
535
+ });
419
536
  }
420
537
 
421
538
  private async callProtocolV2(
@@ -428,34 +545,58 @@ export default class LowlevelTransport {
428
545
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
429
546
  }
430
547
 
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
548
  try {
448
- return await session.call(name, data, {
449
- ...options,
450
- timeoutMs,
451
- });
549
+ return await this.protocolV2Links.call(
550
+ uuid,
551
+ () => this.createProtocolV2Adapter(uuid),
552
+ name,
553
+ data,
554
+ options
555
+ );
452
556
  } catch (e) {
453
- this.protocolV2Assemblers.get(uuid)?.reset();
454
557
  this.Log.error('lowlevel Protocol V2 call error: ', e);
455
558
  throw e;
456
559
  }
457
560
  }
458
561
 
562
+ private createProtocolV2Adapter(uuid: string) {
563
+ const generation = this.protocolV2Generations.get(uuid) ?? 0;
564
+ const assertCurrentGeneration = () => {
565
+ if (this.protocolV2Generations.get(uuid) !== generation) {
566
+ throw new Error(`Protocol V2 connection generation changed for ${uuid}`);
567
+ }
568
+ };
569
+
570
+ return {
571
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
572
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
573
+ generation,
574
+ prepareCall: () => {
575
+ assertCurrentGeneration();
576
+ this.protocolV2Assemblers.get(uuid)?.reset();
577
+ },
578
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) =>
579
+ this.writeProtocolV2Frame(uuid, frame, context, assertCurrentGeneration),
580
+ readFrame: (context: { messageName: string; timeoutMs?: number }) => {
581
+ assertCurrentGeneration();
582
+ return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName);
583
+ },
584
+ reset: () => {
585
+ this.protocolV2Assemblers.get(uuid)?.reset();
586
+ },
587
+ logger: this.Log,
588
+ logPrefix: 'ProtocolV2 Lowlevel-BLE',
589
+ createTimeoutError: (messageName: string, timeout: number) =>
590
+ this.createProtocolTimeoutError(messageName, timeout),
591
+ };
592
+ }
593
+
594
+ private advanceProtocolV2Generation(uuid: string) {
595
+ const nextGeneration = (this.protocolV2Generations.get(uuid) ?? 0) + 1;
596
+ this.protocolV2Generations.set(uuid, nextGeneration);
597
+ return nextGeneration;
598
+ }
599
+
459
600
  cancel() {
460
601
  this.Log.debug('lowlevel-transport', 'cancel');
461
602
  }
package/src/types.ts CHANGED
@@ -3,4 +3,5 @@ import type { ProtocolType } from '@onekeyfe/hd-transport';
3
3
  export type LowLevelAcquireInput = {
4
4
  uuid: string;
5
5
  expectedProtocol?: ProtocolType;
6
+ protocolHint?: ProtocolType;
6
7
  };