@onekeyfe/hd-transport-lowlevel 1.1.26-alpha.9 → 1.1.27-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/__tests__/protocol-v2.test.js +199 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -15
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +294 -17
- package/src/types.ts +3 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const LowlevelTransport = require('../src').default;
|
|
3
|
+
const { parseConfigure } = require('../../hd-transport/src/serialization/protobuf/messages');
|
|
4
|
+
const { ProtocolV1, ProtocolV2 } = require('../../hd-transport/src/protocols');
|
|
5
|
+
const { bytesToHex } = require('../../hd-transport/src/protocols/v2/session');
|
|
6
|
+
const { PROTOCOL_V2_CHANNEL_BLE_UART } = require('../../hd-transport/src/constants');
|
|
7
|
+
|
|
8
|
+
const protocolV1Schema = {
|
|
9
|
+
nested: {
|
|
10
|
+
Initialize: {
|
|
11
|
+
fields: {},
|
|
12
|
+
},
|
|
13
|
+
Success: {
|
|
14
|
+
fields: {
|
|
15
|
+
message: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
id: 1,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
MessageType: {
|
|
22
|
+
values: {
|
|
23
|
+
MessageType_Initialize: 1,
|
|
24
|
+
MessageType_Success: 2,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const protocolV2Schema = {
|
|
31
|
+
nested: {
|
|
32
|
+
GetProtoVersion: {
|
|
33
|
+
fields: {},
|
|
34
|
+
},
|
|
35
|
+
ProtoVersion: {
|
|
36
|
+
fields: {
|
|
37
|
+
major_version: {
|
|
38
|
+
type: 'uint32',
|
|
39
|
+
id: 1,
|
|
40
|
+
},
|
|
41
|
+
minor_version: {
|
|
42
|
+
type: 'uint32',
|
|
43
|
+
id: 2,
|
|
44
|
+
},
|
|
45
|
+
patch_version: {
|
|
46
|
+
type: 'uint32',
|
|
47
|
+
id: 3,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
MessageType: {
|
|
52
|
+
values: {
|
|
53
|
+
MessageType_GetProtoVersion: 60200,
|
|
54
|
+
MessageType_ProtoVersion: 60201,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const schemas = {
|
|
61
|
+
protocolV1: parseConfigure(protocolV1Schema),
|
|
62
|
+
protocolV2: parseConfigure(protocolV2Schema),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const createLogger = () => ({
|
|
66
|
+
debug: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const createPlugin = ({ devices, responses }) => ({
|
|
71
|
+
enumerate: jest.fn(() => Promise.resolve(devices)),
|
|
72
|
+
connect: jest.fn(() => Promise.resolve()),
|
|
73
|
+
disconnect: jest.fn(() => Promise.resolve()),
|
|
74
|
+
init: jest.fn(() => Promise.resolve()),
|
|
75
|
+
send: jest.fn(() => Promise.resolve()),
|
|
76
|
+
receive: jest.fn(() => {
|
|
77
|
+
const next = responses.shift();
|
|
78
|
+
if (next instanceof Error) {
|
|
79
|
+
return Promise.reject(next);
|
|
80
|
+
}
|
|
81
|
+
if (!next) {
|
|
82
|
+
return Promise.reject(new Error('No queued response'));
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve(next);
|
|
85
|
+
}),
|
|
86
|
+
version: 'test-plugin',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const configureTransport = plugin => {
|
|
90
|
+
const lowlevel = new LowlevelTransport();
|
|
91
|
+
lowlevel.init(createLogger(), undefined, plugin);
|
|
92
|
+
lowlevel.configure(protocolV1Schema);
|
|
93
|
+
lowlevel.configureProtocolV2(protocolV2Schema);
|
|
94
|
+
return lowlevel;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const splitFrame = (frame, index) => [
|
|
98
|
+
bytesToHex(frame.slice(0, index)),
|
|
99
|
+
bytesToHex(frame.slice(index)),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
describe('LowlevelTransport protocol framing', () => {
|
|
103
|
+
test('keeps Protocol V1 raw notification chunks compatible', async () => {
|
|
104
|
+
const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', {
|
|
105
|
+
message: 'ok',
|
|
106
|
+
}).map(chunk => chunk.toString('hex'));
|
|
107
|
+
const plugin = createPlugin({
|
|
108
|
+
devices: [{ id: 'classic-id', name: 'OneKey Classic', commType: 'ble' }],
|
|
109
|
+
responses: responseChunks,
|
|
110
|
+
});
|
|
111
|
+
const lowlevel = configureTransport(plugin);
|
|
112
|
+
|
|
113
|
+
await expect(lowlevel.call('classic-id', 'Initialize', {})).resolves.toEqual({
|
|
114
|
+
type: 'Success',
|
|
115
|
+
message: { message: 'ok' },
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('detects Protocol V2 devices and reassembles split Protocol V2 notifications', async () => {
|
|
120
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
121
|
+
schemas,
|
|
122
|
+
'ProtoVersion',
|
|
123
|
+
{
|
|
124
|
+
major_version: 2,
|
|
125
|
+
minor_version: 0,
|
|
126
|
+
patch_version: 0,
|
|
127
|
+
},
|
|
128
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
129
|
+
);
|
|
130
|
+
const callResponse = ProtocolV2.encodeFrame(
|
|
131
|
+
schemas,
|
|
132
|
+
'ProtoVersion',
|
|
133
|
+
{
|
|
134
|
+
major_version: 2,
|
|
135
|
+
minor_version: 1,
|
|
136
|
+
patch_version: 3,
|
|
137
|
+
},
|
|
138
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
139
|
+
);
|
|
140
|
+
const plugin = createPlugin({
|
|
141
|
+
devices: [{ id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble' }],
|
|
142
|
+
responses: [...splitFrame(probeResponse, 4), ...splitFrame(callResponse, 5)],
|
|
143
|
+
});
|
|
144
|
+
const lowlevel = configureTransport(plugin);
|
|
145
|
+
|
|
146
|
+
await expect(lowlevel.enumerate()).resolves.toEqual([
|
|
147
|
+
{ id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble', protocolType: 'V2' },
|
|
148
|
+
]);
|
|
149
|
+
await expect(lowlevel.acquire({ uuid: 'pro2-id' })).resolves.toEqual({
|
|
150
|
+
uuid: 'pro2-id',
|
|
151
|
+
protocolType: 'V2',
|
|
152
|
+
});
|
|
153
|
+
await expect(lowlevel.call('pro2-id', 'GetProtoVersion', {})).resolves.toEqual({
|
|
154
|
+
type: 'ProtoVersion',
|
|
155
|
+
message: {
|
|
156
|
+
major_version: 2,
|
|
157
|
+
minor_version: 1,
|
|
158
|
+
patch_version: 3,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
expect(plugin.send).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('falls back to Protocol V2 probe for unnamed Protocol V2 devices', async () => {
|
|
165
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
166
|
+
schemas,
|
|
167
|
+
'ProtoVersion',
|
|
168
|
+
{
|
|
169
|
+
major_version: 2,
|
|
170
|
+
minor_version: 0,
|
|
171
|
+
patch_version: 0,
|
|
172
|
+
},
|
|
173
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
174
|
+
);
|
|
175
|
+
const plugin = createPlugin({
|
|
176
|
+
devices: [{ id: 'unknown-pro2-id', name: 'Unknown BLE Device', commType: 'ble' }],
|
|
177
|
+
responses: [new Error('Protocol V1 probe timed out'), bytesToHex(probeResponse)],
|
|
178
|
+
});
|
|
179
|
+
const lowlevel = configureTransport(plugin);
|
|
180
|
+
|
|
181
|
+
await expect(lowlevel.acquire({ uuid: 'unknown-pro2-id' })).resolves.toEqual({
|
|
182
|
+
uuid: 'unknown-pro2-id',
|
|
183
|
+
protocolType: 'V2',
|
|
184
|
+
});
|
|
185
|
+
expect(lowlevel.getProtocolType('unknown-pro2-id')).toBe('V2');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('verifies expected Protocol V1 instead of trusting the requested protocol', async () => {
|
|
189
|
+
const plugin = createPlugin({
|
|
190
|
+
devices: [{ id: 'v2-id', name: 'Unknown BLE Device', commType: 'ble' }],
|
|
191
|
+
responses: [new Error('Protocol V1 probe timed out')],
|
|
192
|
+
});
|
|
193
|
+
const lowlevel = configureTransport(plugin);
|
|
194
|
+
|
|
195
|
+
await expect(lowlevel.acquire({ uuid: 'v2-id', expectedProtocol: 'V1' })).rejects.toThrow(
|
|
196
|
+
'Device protocol mismatch: expected V1'
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,26 +1,49 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
1
|
+
import * as transport from '@onekeyfe/hd-transport';
|
|
2
|
+
import transport__default, { ProtocolType, LowlevelTransportSharedPlugin, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
|
|
5
5
|
type LowLevelAcquireInput = {
|
|
6
6
|
uuid: string;
|
|
7
|
+
expectedProtocol?: ProtocolType;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
declare class LowlevelTransport {
|
|
10
|
-
_messages: ReturnType<typeof
|
|
11
|
+
_messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
12
|
+
_messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
11
13
|
configured: boolean;
|
|
12
14
|
Log?: any;
|
|
13
15
|
emitter?: EventEmitter;
|
|
14
16
|
plugin: LowlevelTransportSharedPlugin;
|
|
17
|
+
private deviceProtocol;
|
|
18
|
+
private protocolV2Assemblers;
|
|
19
|
+
getProtocolType(path: string): ProtocolType;
|
|
15
20
|
init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin): void;
|
|
16
21
|
configure(signedData: any): void;
|
|
22
|
+
configureProtocolV2(signedData: any): void;
|
|
17
23
|
listen(): void;
|
|
18
|
-
enumerate(): Promise<
|
|
24
|
+
enumerate(): Promise<{
|
|
25
|
+
protocolType?: ProtocolType | undefined;
|
|
26
|
+
commType: transport.OneKeyDeviceCommType;
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}[]>;
|
|
19
30
|
acquire(input: LowLevelAcquireInput): Promise<{
|
|
20
31
|
uuid: string;
|
|
32
|
+
protocolType: ProtocolType;
|
|
21
33
|
}>;
|
|
22
34
|
release(uuid: string): Promise<boolean>;
|
|
23
|
-
call(uuid: string, name: string, data: Record<string, unknown
|
|
35
|
+
call(uuid: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
36
|
+
private callProtocolV1;
|
|
37
|
+
private createProtocolTimeoutError;
|
|
38
|
+
private createProtocolMismatchError;
|
|
39
|
+
private detectProtocol;
|
|
40
|
+
private probeProtocolV1;
|
|
41
|
+
private probeProtocolV2;
|
|
42
|
+
private receiveHex;
|
|
43
|
+
private readProtocolV1Message;
|
|
44
|
+
private readProtocolV2Frame;
|
|
45
|
+
private writeProtocolV2Frame;
|
|
46
|
+
private callProtocolV2;
|
|
24
47
|
cancel(): void;
|
|
25
48
|
}
|
|
26
49
|
|
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,
|
|
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,EAEV,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,oBAAoB,CAAoD;IAEhF,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY;IAI3C,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;;;;;;IAcT,OAAO,CAAC,KAAK,EAAE,oBAAoB;;;;IAiBnC,OAAO,CAAC,IAAI,EAAE,MAAM;IAYpB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA6BlB,cAAc;IAuC5B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,2BAA2B;YAOrB,cAAc;YAsCd,eAAe;YAef,eAAe;YAmBf,UAAU;YAUV,qBAAqB;YAmBrB,mBAAmB;YAqBnB,oBAAoB;YAOpB,cAAc;IAsC5B,MAAM;CAGP"}
|
package/dist/index.js
CHANGED
|
@@ -37,11 +37,30 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
37
37
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const { check,
|
|
40
|
+
const { check, ProtocolV1, parseConfigure } = transport__default["default"];
|
|
41
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
42
|
+
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
43
|
+
const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30000;
|
|
44
|
+
const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64;
|
|
45
|
+
function inferProtocolTypeFromDeviceName(name) {
|
|
46
|
+
return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
|
|
47
|
+
}
|
|
48
|
+
function isProtocolV1TransportChunk(data) {
|
|
49
|
+
return data.length >= 9 && data[0] === 0x3f && data[1] === 0x23 && data[2] === 0x23;
|
|
50
|
+
}
|
|
51
|
+
function readProtocolV1PayloadLength(data) {
|
|
52
|
+
return data[5] * 0x1000000 + data[6] * 0x10000 + data[7] * 0x100 + data[8];
|
|
53
|
+
}
|
|
41
54
|
class LowlevelTransport {
|
|
42
55
|
constructor() {
|
|
43
56
|
this.configured = false;
|
|
44
57
|
this.plugin = {};
|
|
58
|
+
this.deviceProtocol = new Map();
|
|
59
|
+
this.protocolV2Assemblers = new Map();
|
|
60
|
+
}
|
|
61
|
+
getProtocolType(path) {
|
|
62
|
+
var _a;
|
|
63
|
+
return (_a = this.deviceProtocol.get(path)) !== null && _a !== void 0 ? _a : 'V1';
|
|
45
64
|
}
|
|
46
65
|
init(logger, emitter, plugin) {
|
|
47
66
|
this.Log = logger;
|
|
@@ -54,28 +73,45 @@ class LowlevelTransport {
|
|
|
54
73
|
this.configured = true;
|
|
55
74
|
this._messages = messages;
|
|
56
75
|
}
|
|
76
|
+
configureProtocolV2(signedData) {
|
|
77
|
+
this._messagesV2 = parseConfigure(signedData);
|
|
78
|
+
}
|
|
57
79
|
listen() {
|
|
58
80
|
}
|
|
59
81
|
enumerate() {
|
|
60
|
-
return this
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
const devices = yield this.plugin.enumerate();
|
|
84
|
+
return devices.map((device) => {
|
|
85
|
+
const protocolType = inferProtocolTypeFromDeviceName(device.name);
|
|
86
|
+
if (protocolType) {
|
|
87
|
+
this.deviceProtocol.set(device.id, protocolType);
|
|
88
|
+
}
|
|
89
|
+
return Object.assign(Object.assign({}, device), (protocolType ? { protocolType } : {}));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
61
92
|
}
|
|
62
93
|
acquire(input) {
|
|
63
|
-
var _a;
|
|
94
|
+
var _a, _b;
|
|
64
95
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
96
|
try {
|
|
66
97
|
yield this.plugin.connect(input.uuid);
|
|
67
|
-
return { uuid: input.uuid };
|
|
68
98
|
}
|
|
69
99
|
catch (error) {
|
|
70
100
|
this.Log.debug('lowlelvel transport connect error: ', error);
|
|
71
101
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.LowlevelTrasnportConnectError, (_a = error.message) !== null && _a !== void 0 ? _a : error);
|
|
72
102
|
}
|
|
103
|
+
this.protocolV2Assemblers.set(input.uuid, new transport.ProtocolV2FrameAssembler());
|
|
104
|
+
const expectedProtocol = (_b = input.expectedProtocol) !== null && _b !== void 0 ? _b : this.deviceProtocol.get(input.uuid);
|
|
105
|
+
const protocolType = yield this.detectProtocol(input.uuid, expectedProtocol);
|
|
106
|
+
return { uuid: input.uuid, protocolType };
|
|
73
107
|
});
|
|
74
108
|
}
|
|
75
109
|
release(uuid) {
|
|
76
110
|
return __awaiter(this, void 0, void 0, function* () {
|
|
77
111
|
try {
|
|
78
112
|
yield this.plugin.disconnect(uuid);
|
|
113
|
+
this.deviceProtocol.delete(uuid);
|
|
114
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
79
115
|
return true;
|
|
80
116
|
}
|
|
81
117
|
catch (error) {
|
|
@@ -84,19 +120,31 @@ class LowlevelTransport {
|
|
|
84
120
|
}
|
|
85
121
|
});
|
|
86
122
|
}
|
|
87
|
-
call(uuid, name, data) {
|
|
123
|
+
call(uuid, name, data, options) {
|
|
88
124
|
return __awaiter(this, void 0, void 0, function* () {
|
|
89
125
|
if (this._messages === null || !this._messages) {
|
|
90
126
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
91
127
|
}
|
|
92
|
-
const
|
|
128
|
+
const protocol = this.getProtocolType(uuid);
|
|
93
129
|
if (transport.LogBlockCommand.has(name)) {
|
|
94
|
-
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name);
|
|
130
|
+
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
95
131
|
}
|
|
96
132
|
else {
|
|
97
|
-
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' data: ', data);
|
|
133
|
+
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
|
|
134
|
+
}
|
|
135
|
+
if (protocol === 'V2') {
|
|
136
|
+
return this.callProtocolV2(uuid, name, data, options);
|
|
137
|
+
}
|
|
138
|
+
return this.callProtocolV1(uuid, name, data, options);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
callProtocolV1(uuid, name, data, options) {
|
|
142
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
if (!this._messages) {
|
|
144
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
98
145
|
}
|
|
99
|
-
const
|
|
146
|
+
const messages = this._messages;
|
|
147
|
+
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
100
148
|
for (const o of buffers) {
|
|
101
149
|
const outData = o.toString('hex');
|
|
102
150
|
this.Log.debug('send hex strting: ', outData);
|
|
@@ -109,16 +157,180 @@ class LowlevelTransport {
|
|
|
109
157
|
}
|
|
110
158
|
}
|
|
111
159
|
try {
|
|
112
|
-
const response = yield this.
|
|
113
|
-
if (typeof response !== 'string') {
|
|
114
|
-
throw new Error('Returning data is not string');
|
|
115
|
-
}
|
|
160
|
+
const response = yield this.readProtocolV1Message(options === null || options === void 0 ? void 0 : options.timeoutMs);
|
|
116
161
|
this.Log.debug('receive data: ', response);
|
|
117
|
-
const jsonData =
|
|
162
|
+
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
118
163
|
return check.call(jsonData);
|
|
119
164
|
}
|
|
120
165
|
catch (e) {
|
|
121
|
-
|
|
166
|
+
if (name === 'Initialize' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
167
|
+
this.Log.debug('[LowlevelTransport] Protocol V1 Initialize probe call failed:', e);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.Log.error('lowlevel call error: ', e);
|
|
171
|
+
}
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
createProtocolTimeoutError(name, timeout) {
|
|
177
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, `Lowlevel response timeout after ${timeout}ms for ${name}`);
|
|
178
|
+
}
|
|
179
|
+
createProtocolMismatchError(expected) {
|
|
180
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
|
|
181
|
+
}
|
|
182
|
+
detectProtocol(uuid, expectedProtocol) {
|
|
183
|
+
var _a, _b, _c, _d;
|
|
184
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
185
|
+
if (expectedProtocol === 'V2') {
|
|
186
|
+
if (yield this.probeProtocolV2(uuid)) {
|
|
187
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
188
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
189
|
+
return 'V2';
|
|
190
|
+
}
|
|
191
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
192
|
+
}
|
|
193
|
+
if (expectedProtocol === 'V1') {
|
|
194
|
+
if (yield this.probeProtocolV1(uuid)) {
|
|
195
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
196
|
+
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
|
|
197
|
+
return 'V1';
|
|
198
|
+
}
|
|
199
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
200
|
+
}
|
|
201
|
+
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
202
|
+
if (cachedProtocol === 'V2' && (yield this.probeProtocolV2(uuid))) {
|
|
203
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
204
|
+
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
|
|
205
|
+
return 'V2';
|
|
206
|
+
}
|
|
207
|
+
let protocol = 'V1';
|
|
208
|
+
if (!(yield this.probeProtocolV1(uuid)) && (yield this.probeProtocolV2(uuid))) {
|
|
209
|
+
protocol = 'V2';
|
|
210
|
+
}
|
|
211
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
212
|
+
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
213
|
+
return protocol;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
probeProtocolV1(uuid) {
|
|
217
|
+
var _a;
|
|
218
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
219
|
+
if (!this._messages) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
224
|
+
yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[LowlevelTransport] Protocol V1 Initialize probe failed:', error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
probeProtocolV2(uuid) {
|
|
234
|
+
var _a;
|
|
235
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
236
|
+
if (!this._messages || !this._messagesV2) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
240
|
+
(_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
|
|
241
|
+
return transport.probeProtocolV2({
|
|
242
|
+
call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
|
|
243
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
|
|
244
|
+
logger: this.Log,
|
|
245
|
+
logPrefix: 'ProtocolV2 Lowlevel-BLE',
|
|
246
|
+
onProbeFailed: () => {
|
|
247
|
+
var _a;
|
|
248
|
+
(_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
receiveHex(timeoutMs, commandName) {
|
|
254
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
const response = yield transport.withProtocolTimeout(this.plugin.receive(), timeoutMs, () => this.createProtocolTimeoutError(commandName, timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : 0));
|
|
256
|
+
if (typeof response !== 'string') {
|
|
257
|
+
throw new Error('Returning data is not string');
|
|
258
|
+
}
|
|
259
|
+
return response;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
readProtocolV1Message(timeoutMs) {
|
|
263
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
264
|
+
const first = yield this.receiveHex(timeoutMs, 'ProtocolV1');
|
|
265
|
+
const firstData = transport.hexToBytes(first);
|
|
266
|
+
if (!isProtocolV1TransportChunk(firstData)) {
|
|
267
|
+
return first;
|
|
268
|
+
}
|
|
269
|
+
const payloadLength = readProtocolV1PayloadLength(firstData);
|
|
270
|
+
let buffer = firstData.slice(3);
|
|
271
|
+
const expectedLength = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
|
|
272
|
+
while (buffer.length < expectedLength) {
|
|
273
|
+
const next = yield this.receiveHex(timeoutMs, 'ProtocolV1');
|
|
274
|
+
buffer = transport.concatUint8Arrays([buffer, transport.hexToBytes(next)]);
|
|
275
|
+
}
|
|
276
|
+
return transport.bytesToHex(buffer.slice(0, expectedLength));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
readProtocolV2Frame(uuid, timeoutMs) {
|
|
280
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
281
|
+
let assembler = this.protocolV2Assemblers.get(uuid);
|
|
282
|
+
if (!assembler) {
|
|
283
|
+
assembler = new transport.ProtocolV2FrameAssembler();
|
|
284
|
+
this.protocolV2Assemblers.set(uuid, assembler);
|
|
285
|
+
}
|
|
286
|
+
const queuedFrame = assembler.push(new Uint8Array(0));
|
|
287
|
+
if (queuedFrame)
|
|
288
|
+
return queuedFrame;
|
|
289
|
+
let frame;
|
|
290
|
+
while (!frame) {
|
|
291
|
+
const response = yield this.receiveHex(timeoutMs, 'ProtocolV2');
|
|
292
|
+
const chunk = transport.hexToBytes(response);
|
|
293
|
+
if (chunk.length > 0) {
|
|
294
|
+
frame = assembler.push(chunk);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return frame;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
writeProtocolV2Frame(uuid, frame) {
|
|
301
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
for (let offset = 0; offset < frame.length; offset += LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH) {
|
|
303
|
+
const chunk = frame.slice(offset, offset + LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH);
|
|
304
|
+
yield this.plugin.send(uuid, transport.bytesToHex(chunk));
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
callProtocolV2(uuid, name, data, options) {
|
|
309
|
+
var _a, _b, _c;
|
|
310
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
if (!this._messages || !this._messagesV2) {
|
|
312
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
313
|
+
}
|
|
314
|
+
const timeoutMs = (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : LOWLEVEL_PROTOCOL_TIMEOUT_MS;
|
|
315
|
+
(_b = this.protocolV2Assemblers.get(uuid)) === null || _b === void 0 ? void 0 : _b.reset();
|
|
316
|
+
const session = new transport.ProtocolV2Session({
|
|
317
|
+
schemas: {
|
|
318
|
+
protocolV1: this._messages,
|
|
319
|
+
protocolV2: this._messagesV2,
|
|
320
|
+
},
|
|
321
|
+
router: transport.PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
322
|
+
writeFrame: (frame) => this.writeProtocolV2Frame(uuid, frame),
|
|
323
|
+
readFrame: () => this.readProtocolV2Frame(uuid, timeoutMs),
|
|
324
|
+
logger: this.Log,
|
|
325
|
+
logPrefix: 'ProtocolV2 Lowlevel-BLE',
|
|
326
|
+
createTimeoutError: (_messageName, timeout) => this.createProtocolTimeoutError(name, timeout),
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
return yield session.call(name, data, Object.assign(Object.assign({}, options), { timeoutMs }));
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
(_c = this.protocolV2Assemblers.get(uuid)) === null || _c === void 0 ? void 0 : _c.reset();
|
|
333
|
+
this.Log.error('lowlevel Protocol V2 call error: ', e);
|
|
122
334
|
throw e;
|
|
123
335
|
}
|
|
124
336
|
});
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,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;CACjC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-lowlevel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.27-alpha.30",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"lint:fix": "eslint . --fix"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@onekeyfe/hd-shared": "1.1.
|
|
23
|
-
"@onekeyfe/hd-transport": "1.1.
|
|
22
|
+
"@onekeyfe/hd-shared": "1.1.27-alpha.30",
|
|
23
|
+
"@onekeyfe/hd-transport": "1.1.27-alpha.30"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "f1e3a93e766463007436eaa9aff4a271ffd8830c"
|
|
26
26
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
-
import transport, {
|
|
2
|
+
import transport, {
|
|
3
|
+
LogBlockCommand,
|
|
4
|
+
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
5
|
+
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
6
|
+
ProtocolV2FrameAssembler,
|
|
7
|
+
ProtocolV2Session,
|
|
8
|
+
bytesToHex,
|
|
9
|
+
concatUint8Arrays,
|
|
10
|
+
hexToBytes,
|
|
11
|
+
probeProtocolV2 as probeProtocolV2Helper,
|
|
12
|
+
withProtocolTimeout,
|
|
13
|
+
} from '@onekeyfe/hd-transport';
|
|
3
14
|
|
|
4
15
|
import type EventEmitter from 'events';
|
|
5
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
LowLevelDevice,
|
|
18
|
+
LowlevelTransportSharedPlugin,
|
|
19
|
+
ProtocolType,
|
|
20
|
+
TransportCallOptions,
|
|
21
|
+
} from '@onekeyfe/hd-transport';
|
|
6
22
|
import type { LowLevelAcquireInput } from './types';
|
|
7
23
|
|
|
8
|
-
const { check,
|
|
24
|
+
const { check, ProtocolV1, parseConfigure } = transport;
|
|
25
|
+
|
|
26
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
27
|
+
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
28
|
+
const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30_000;
|
|
29
|
+
const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64;
|
|
30
|
+
|
|
31
|
+
function inferProtocolTypeFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
32
|
+
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isProtocolV1TransportChunk(data: Uint8Array) {
|
|
36
|
+
return data.length >= 9 && data[0] === 0x3f && data[1] === 0x23 && data[2] === 0x23;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readProtocolV1PayloadLength(data: Uint8Array) {
|
|
40
|
+
return data[5] * 0x1000000 + data[6] * 0x10000 + data[7] * 0x100 + data[8];
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
export default class LowlevelTransport {
|
|
11
44
|
_messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
12
45
|
|
|
46
|
+
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
47
|
+
|
|
13
48
|
configured = false;
|
|
14
49
|
|
|
15
50
|
Log?: any;
|
|
@@ -18,6 +53,14 @@ export default class LowlevelTransport {
|
|
|
18
53
|
|
|
19
54
|
plugin: LowlevelTransportSharedPlugin = {} as LowlevelTransportSharedPlugin;
|
|
20
55
|
|
|
56
|
+
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
57
|
+
|
|
58
|
+
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
59
|
+
|
|
60
|
+
getProtocolType(path: string): ProtocolType {
|
|
61
|
+
return this.deviceProtocol.get(path) ?? 'V1';
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin) {
|
|
22
65
|
this.Log = logger;
|
|
23
66
|
this.emitter = emitter;
|
|
@@ -31,18 +74,31 @@ export default class LowlevelTransport {
|
|
|
31
74
|
this._messages = messages;
|
|
32
75
|
}
|
|
33
76
|
|
|
77
|
+
configureProtocolV2(signedData: any) {
|
|
78
|
+
this._messagesV2 = parseConfigure(signedData);
|
|
79
|
+
}
|
|
80
|
+
|
|
34
81
|
listen() {
|
|
35
82
|
// empty
|
|
36
83
|
}
|
|
37
84
|
|
|
38
|
-
enumerate() {
|
|
39
|
-
|
|
85
|
+
async enumerate() {
|
|
86
|
+
const devices = await this.plugin.enumerate();
|
|
87
|
+
return devices.map((device: LowLevelDevice) => {
|
|
88
|
+
const protocolType = inferProtocolTypeFromDeviceName(device.name);
|
|
89
|
+
if (protocolType) {
|
|
90
|
+
this.deviceProtocol.set(device.id, protocolType);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...device,
|
|
94
|
+
...(protocolType ? { protocolType } : {}),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
40
97
|
}
|
|
41
98
|
|
|
42
99
|
async acquire(input: LowLevelAcquireInput) {
|
|
43
100
|
try {
|
|
44
101
|
await this.plugin.connect(input.uuid);
|
|
45
|
-
return { uuid: input.uuid };
|
|
46
102
|
} catch (error) {
|
|
47
103
|
this.Log.debug('lowlelvel transport connect error: ', error);
|
|
48
104
|
throw ERRORS.TypedError(
|
|
@@ -50,11 +106,18 @@ export default class LowlevelTransport {
|
|
|
50
106
|
error.message ?? error
|
|
51
107
|
);
|
|
52
108
|
}
|
|
109
|
+
|
|
110
|
+
this.protocolV2Assemblers.set(input.uuid, new ProtocolV2FrameAssembler());
|
|
111
|
+
const expectedProtocol = input.expectedProtocol ?? this.deviceProtocol.get(input.uuid);
|
|
112
|
+
const protocolType = await this.detectProtocol(input.uuid, expectedProtocol);
|
|
113
|
+
return { uuid: input.uuid, protocolType };
|
|
53
114
|
}
|
|
54
115
|
|
|
55
116
|
async release(uuid: string) {
|
|
56
117
|
try {
|
|
57
118
|
await this.plugin.disconnect(uuid);
|
|
119
|
+
this.deviceProtocol.delete(uuid);
|
|
120
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
58
121
|
return true;
|
|
59
122
|
} catch (error) {
|
|
60
123
|
this.Log.debug('lowlelvel transport disconnect error: ', error);
|
|
@@ -62,19 +125,51 @@ export default class LowlevelTransport {
|
|
|
62
125
|
}
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
async call(
|
|
128
|
+
async call(
|
|
129
|
+
uuid: string,
|
|
130
|
+
name: string,
|
|
131
|
+
data: Record<string, unknown>,
|
|
132
|
+
options?: TransportCallOptions
|
|
133
|
+
) {
|
|
66
134
|
if (this._messages === null || !this._messages) {
|
|
67
135
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
68
136
|
}
|
|
69
137
|
|
|
70
|
-
const
|
|
138
|
+
const protocol = this.getProtocolType(uuid);
|
|
71
139
|
if (LogBlockCommand.has(name)) {
|
|
72
|
-
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name);
|
|
140
|
+
this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
73
141
|
} else {
|
|
74
|
-
this.Log.debug(
|
|
142
|
+
this.Log.debug(
|
|
143
|
+
'lowlevel-transport',
|
|
144
|
+
'call-',
|
|
145
|
+
' name: ',
|
|
146
|
+
name,
|
|
147
|
+
' data: ',
|
|
148
|
+
data,
|
|
149
|
+
' protocol: ',
|
|
150
|
+
protocol
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (protocol === 'V2') {
|
|
155
|
+
return this.callProtocolV2(uuid, name, data, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this.callProtocolV1(uuid, name, data, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async callProtocolV1(
|
|
162
|
+
uuid: string,
|
|
163
|
+
name: string,
|
|
164
|
+
data: Record<string, unknown>,
|
|
165
|
+
options?: TransportCallOptions
|
|
166
|
+
) {
|
|
167
|
+
if (!this._messages) {
|
|
168
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
75
169
|
}
|
|
76
170
|
|
|
77
|
-
const
|
|
171
|
+
const messages = this._messages;
|
|
172
|
+
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
78
173
|
for (const o of buffers) {
|
|
79
174
|
const outData = o.toString('hex');
|
|
80
175
|
// Upload resources on low-end phones may OOM
|
|
@@ -88,15 +183,197 @@ export default class LowlevelTransport {
|
|
|
88
183
|
}
|
|
89
184
|
|
|
90
185
|
try {
|
|
91
|
-
const response = await this.
|
|
92
|
-
if (typeof response !== 'string') {
|
|
93
|
-
throw new Error('Returning data is not string');
|
|
94
|
-
}
|
|
186
|
+
const response = await this.readProtocolV1Message(options?.timeoutMs);
|
|
95
187
|
this.Log.debug('receive data: ', response);
|
|
96
|
-
const jsonData =
|
|
188
|
+
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
97
189
|
return check.call(jsonData);
|
|
98
190
|
} catch (e) {
|
|
99
|
-
|
|
191
|
+
if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
192
|
+
this.Log.debug('[LowlevelTransport] Protocol V1 Initialize probe call failed:', e);
|
|
193
|
+
} else {
|
|
194
|
+
this.Log.error('lowlevel call error: ', e);
|
|
195
|
+
}
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private createProtocolTimeoutError(name: string, timeout: number) {
|
|
201
|
+
return ERRORS.TypedError(
|
|
202
|
+
HardwareErrorCode.BleTimeoutError,
|
|
203
|
+
`Lowlevel response timeout after ${timeout}ms for ${name}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private createProtocolMismatchError(expected: ProtocolType) {
|
|
208
|
+
return ERRORS.TypedError(
|
|
209
|
+
HardwareErrorCode.RuntimeError,
|
|
210
|
+
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async detectProtocol(
|
|
215
|
+
uuid: string,
|
|
216
|
+
expectedProtocol?: ProtocolType
|
|
217
|
+
): Promise<ProtocolType> {
|
|
218
|
+
if (expectedProtocol === 'V2') {
|
|
219
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
220
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
221
|
+
this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
222
|
+
return 'V2';
|
|
223
|
+
}
|
|
224
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (expectedProtocol === 'V1') {
|
|
228
|
+
if (await this.probeProtocolV1(uuid)) {
|
|
229
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
230
|
+
this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
|
|
231
|
+
return 'V1';
|
|
232
|
+
}
|
|
233
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
237
|
+
if (cachedProtocol === 'V2' && (await this.probeProtocolV2(uuid))) {
|
|
238
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
239
|
+
this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
|
|
240
|
+
return 'V2';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let protocol: ProtocolType = 'V1';
|
|
244
|
+
if (!(await this.probeProtocolV1(uuid)) && (await this.probeProtocolV2(uuid))) {
|
|
245
|
+
protocol = 'V2';
|
|
246
|
+
}
|
|
247
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
248
|
+
this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
249
|
+
return protocol;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async probeProtocolV1(uuid: string) {
|
|
253
|
+
if (!this._messages) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
259
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
260
|
+
return true;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.Log?.debug('[LowlevelTransport] Protocol V1 Initialize probe failed:', error);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async probeProtocolV2(uuid: string) {
|
|
268
|
+
if (!this._messages || !this._messagesV2) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
273
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
274
|
+
return probeProtocolV2Helper({
|
|
275
|
+
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
276
|
+
this.callProtocolV2(uuid, name, data, options),
|
|
277
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
|
|
278
|
+
logger: this.Log,
|
|
279
|
+
logPrefix: 'ProtocolV2 Lowlevel-BLE',
|
|
280
|
+
onProbeFailed: () => {
|
|
281
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async receiveHex(timeoutMs: number | undefined, commandName: string) {
|
|
287
|
+
const response = await withProtocolTimeout(this.plugin.receive(), timeoutMs, () =>
|
|
288
|
+
this.createProtocolTimeoutError(commandName, timeoutMs ?? 0)
|
|
289
|
+
);
|
|
290
|
+
if (typeof response !== 'string') {
|
|
291
|
+
throw new Error('Returning data is not string');
|
|
292
|
+
}
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async readProtocolV1Message(timeoutMs?: number) {
|
|
297
|
+
const first = await this.receiveHex(timeoutMs, 'ProtocolV1');
|
|
298
|
+
const firstData = hexToBytes(first);
|
|
299
|
+
if (!isProtocolV1TransportChunk(firstData)) {
|
|
300
|
+
return first;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const payloadLength = readProtocolV1PayloadLength(firstData);
|
|
304
|
+
let buffer = firstData.slice(3);
|
|
305
|
+
const expectedLength = PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength;
|
|
306
|
+
|
|
307
|
+
while (buffer.length < expectedLength) {
|
|
308
|
+
const next = await this.receiveHex(timeoutMs, 'ProtocolV1');
|
|
309
|
+
buffer = concatUint8Arrays([buffer, hexToBytes(next)]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return bytesToHex(buffer.slice(0, expectedLength));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async readProtocolV2Frame(uuid: string, timeoutMs?: number) {
|
|
316
|
+
let assembler = this.protocolV2Assemblers.get(uuid);
|
|
317
|
+
if (!assembler) {
|
|
318
|
+
assembler = new ProtocolV2FrameAssembler();
|
|
319
|
+
this.protocolV2Assemblers.set(uuid, assembler);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const queuedFrame = assembler.push(new Uint8Array(0));
|
|
323
|
+
if (queuedFrame) return queuedFrame;
|
|
324
|
+
|
|
325
|
+
let frame: Uint8Array | undefined;
|
|
326
|
+
while (!frame) {
|
|
327
|
+
const response = await this.receiveHex(timeoutMs, 'ProtocolV2');
|
|
328
|
+
const chunk = hexToBytes(response);
|
|
329
|
+
if (chunk.length > 0) {
|
|
330
|
+
frame = assembler.push(chunk);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return frame;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async writeProtocolV2Frame(uuid: string, frame: Uint8Array) {
|
|
337
|
+
for (let offset = 0; offset < frame.length; offset += LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH) {
|
|
338
|
+
const chunk = frame.slice(offset, offset + LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH);
|
|
339
|
+
await this.plugin.send(uuid, bytesToHex(chunk));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async callProtocolV2(
|
|
344
|
+
uuid: string,
|
|
345
|
+
name: string,
|
|
346
|
+
data: Record<string, unknown>,
|
|
347
|
+
options?: TransportCallOptions
|
|
348
|
+
) {
|
|
349
|
+
if (!this._messages || !this._messagesV2) {
|
|
350
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const timeoutMs = options?.timeoutMs ?? LOWLEVEL_PROTOCOL_TIMEOUT_MS;
|
|
354
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
355
|
+
const session = new ProtocolV2Session({
|
|
356
|
+
schemas: {
|
|
357
|
+
protocolV1: this._messages,
|
|
358
|
+
protocolV2: this._messagesV2,
|
|
359
|
+
},
|
|
360
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
361
|
+
writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(uuid, frame),
|
|
362
|
+
readFrame: () => this.readProtocolV2Frame(uuid, timeoutMs),
|
|
363
|
+
logger: this.Log,
|
|
364
|
+
logPrefix: 'ProtocolV2 Lowlevel-BLE',
|
|
365
|
+
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
366
|
+
this.createProtocolTimeoutError(name, timeout),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
return await session.call(name, data, {
|
|
371
|
+
...options,
|
|
372
|
+
timeoutMs,
|
|
373
|
+
});
|
|
374
|
+
} catch (e) {
|
|
375
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
376
|
+
this.Log.error('lowlevel Protocol V2 call error: ', e);
|
|
100
377
|
throw e;
|
|
101
378
|
}
|
|
102
379
|
}
|