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