@onekeyfe/hd-transport-web-device 1.0.34-alpha.0
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/README.md +29 -0
- package/dist/electron-ble-transport.d.ts +51 -0
- package/dist/electron-ble-transport.d.ts.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +766 -0
- package/dist/webble-utils.d.ts +4 -0
- package/dist/webble-utils.d.ts.map +1 -0
- package/dist/webusb.d.ts +33 -0
- package/dist/webusb.d.ts.map +1 -0
- package/package.json +31 -0
- package/src/electron-ble-transport.ts +709 -0
- package/src/index.ts +4 -0
- package/src/webble-utils.ts +30 -0
- package/src/webusb.ts +298 -0
- package/tsconfig.json +11 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MESSAGE_TOP_CHAR, MESSAGE_HEADER_BYTE } from '@onekeyfe/hd-transport';
|
|
2
|
+
|
|
3
|
+
export const isHeaderChunk = (chunk: Buffer | Uint8Array): boolean => {
|
|
4
|
+
if (chunk.length < 9) return false;
|
|
5
|
+
const [MagicQuestionMark, sharp1, sharp2] = chunk;
|
|
6
|
+
|
|
7
|
+
if (
|
|
8
|
+
String.fromCharCode(MagicQuestionMark) === String.fromCharCode(MESSAGE_TOP_CHAR) &&
|
|
9
|
+
String.fromCharCode(sharp1) === String.fromCharCode(MESSAGE_HEADER_BYTE) &&
|
|
10
|
+
String.fromCharCode(sharp2) === String.fromCharCode(MESSAGE_HEADER_BYTE)
|
|
11
|
+
) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const isOnekeyDevice = (name: string | null, id?: string): boolean => {
|
|
19
|
+
if (id?.startsWith?.('MI')) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 过滤 BixinKeyxxx 和 Kxxxx 和 Txxxx
|
|
24
|
+
// i 忽略大小写模式
|
|
25
|
+
const re = /(BixinKey\d{10})|(K\d{4})|(T\d{4})|(Touch\s\w{4})|(Pro\s\w{4})/i;
|
|
26
|
+
if (name && re.exec(name)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
};
|
package/src/webusb.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
import transport, { AcquireInput, LogBlockCommand } from '@onekeyfe/hd-transport';
|
|
3
|
+
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
4
|
+
import ByteBuffer from 'bytebuffer';
|
|
5
|
+
|
|
6
|
+
const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport;
|
|
7
|
+
|
|
8
|
+
const CONFIGURATION_ID = 1;
|
|
9
|
+
const INTERFACE_ID = 0;
|
|
10
|
+
const ENDPOINT_ID = 1;
|
|
11
|
+
const PACKET_SIZE = 64;
|
|
12
|
+
const HEADER_LENGTH = 6;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Device information with path and WebUSB device instance
|
|
16
|
+
*/
|
|
17
|
+
interface DeviceInfo {
|
|
18
|
+
path: string;
|
|
19
|
+
device: USBDevice;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class WebUsbTransport {
|
|
23
|
+
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
24
|
+
|
|
25
|
+
name = 'WebUsbTransport';
|
|
26
|
+
|
|
27
|
+
stopped = false;
|
|
28
|
+
|
|
29
|
+
configured = false;
|
|
30
|
+
|
|
31
|
+
Log?: any;
|
|
32
|
+
|
|
33
|
+
usb?: USB;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cached list of connected devices
|
|
37
|
+
* This is essential for maintaining device references between operations
|
|
38
|
+
*/
|
|
39
|
+
deviceList: Array<DeviceInfo> = [];
|
|
40
|
+
|
|
41
|
+
configurationId = CONFIGURATION_ID;
|
|
42
|
+
|
|
43
|
+
endpointId = ENDPOINT_ID;
|
|
44
|
+
|
|
45
|
+
interfaceId = INTERFACE_ID;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize WebUSB transport
|
|
49
|
+
*/
|
|
50
|
+
init(logger: any) {
|
|
51
|
+
this.Log = logger;
|
|
52
|
+
|
|
53
|
+
const { usb } = navigator;
|
|
54
|
+
if (!usb) {
|
|
55
|
+
throw ERRORS.TypedError(
|
|
56
|
+
HardwareErrorCode.RuntimeError,
|
|
57
|
+
'WebUSB is not supported by current browsers'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
this.usb = usb;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Configure transport protocol
|
|
65
|
+
*/
|
|
66
|
+
configure(signedData: any) {
|
|
67
|
+
const messages = parseConfigure(signedData);
|
|
68
|
+
this.configured = true;
|
|
69
|
+
this.messages = messages;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request user to select a device
|
|
74
|
+
* This method must be called in response to a user action
|
|
75
|
+
* to comply with WebUSB security requirements
|
|
76
|
+
*/
|
|
77
|
+
async promptDeviceAccess() {
|
|
78
|
+
if (!this.usb) return null;
|
|
79
|
+
try {
|
|
80
|
+
const device = await this.usb.requestDevice({ filters: ONEKEY_WEBUSB_FILTER });
|
|
81
|
+
return device;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
this.Log.debug('requestDevice error: ', e);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Enumerate already connected devices
|
|
90
|
+
* This method only returns devices that are already authorized by the browser
|
|
91
|
+
* It does NOT prompt the user to select a device
|
|
92
|
+
*/
|
|
93
|
+
async enumerate() {
|
|
94
|
+
await this.getConnectedDevices();
|
|
95
|
+
return this.deviceList;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get list of connected devices
|
|
100
|
+
*/
|
|
101
|
+
async getConnectedDevices() {
|
|
102
|
+
if (!this.usb) return [];
|
|
103
|
+
|
|
104
|
+
const devices = await this.usb.getDevices();
|
|
105
|
+
const onekeyDevices = devices.filter(dev => {
|
|
106
|
+
const isOneKey = ONEKEY_WEBUSB_FILTER.some(
|
|
107
|
+
desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
108
|
+
);
|
|
109
|
+
const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
|
|
110
|
+
return isOneKey && hasSerialNumber;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.deviceList = onekeyDevices.map(device => ({
|
|
114
|
+
path: device.serialNumber as string,
|
|
115
|
+
device,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
return this.deviceList;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Acquire device control
|
|
123
|
+
*/
|
|
124
|
+
async acquire(input: AcquireInput) {
|
|
125
|
+
if (!input.path) return;
|
|
126
|
+
try {
|
|
127
|
+
await this.connect(input.path ?? '', true);
|
|
128
|
+
return await Promise.resolve(input.path);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.Log.debug('acquire error: ', e);
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find device by path
|
|
137
|
+
*/
|
|
138
|
+
async findDevice(path: string) {
|
|
139
|
+
// If device list is empty, refresh it first
|
|
140
|
+
if (this.deviceList.length === 0) {
|
|
141
|
+
await this.getConnectedDevices();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let device = this.deviceList.find(d => d.path === path);
|
|
145
|
+
|
|
146
|
+
// If device not found after first attempt, try refreshing the list once more
|
|
147
|
+
if (device == null) {
|
|
148
|
+
await this.getConnectedDevices();
|
|
149
|
+
device = this.deviceList.find(d => d.path === path);
|
|
150
|
+
|
|
151
|
+
if (device == null) {
|
|
152
|
+
throw new Error('Action was interrupted.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return device.device;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Connect to device with retry mechanism
|
|
161
|
+
*/
|
|
162
|
+
async connect(path: string, first: boolean) {
|
|
163
|
+
const maxRetries = 5;
|
|
164
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
165
|
+
try {
|
|
166
|
+
return await this.connectToDevice(path, first);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (i === maxRetries - 1) {
|
|
169
|
+
throw e;
|
|
170
|
+
}
|
|
171
|
+
await wait(i * 200);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Connect to specific device
|
|
178
|
+
*/
|
|
179
|
+
async connectToDevice(path: string, first: boolean) {
|
|
180
|
+
const device: USBDevice = await this.findDevice(path);
|
|
181
|
+
await device.open();
|
|
182
|
+
|
|
183
|
+
if (first) {
|
|
184
|
+
await device.selectConfiguration(this.configurationId);
|
|
185
|
+
try {
|
|
186
|
+
await device.reset();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// Ignore reset errors
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await device.claimInterface(this.interfaceId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
196
|
+
await this.call(session, name, data);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Call device method
|
|
201
|
+
*/
|
|
202
|
+
async call(path: string, name: string, data: Record<string, unknown>) {
|
|
203
|
+
if (this.messages == null) {
|
|
204
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const device = await this.findDevice(path);
|
|
208
|
+
if (!device) {
|
|
209
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const { messages } = this;
|
|
213
|
+
if (LogBlockCommand.has(name)) {
|
|
214
|
+
this.Log.debug('call-', ' name: ', name);
|
|
215
|
+
} else {
|
|
216
|
+
this.Log.debug('call-', ' name: ', name, ' data: ', data);
|
|
217
|
+
}
|
|
218
|
+
const encodeBuffers = buildEncodeBuffers(messages, name, data);
|
|
219
|
+
|
|
220
|
+
for (const buffer of encodeBuffers) {
|
|
221
|
+
const newArray: Uint8Array = new Uint8Array(PACKET_SIZE);
|
|
222
|
+
newArray[0] = 63;
|
|
223
|
+
newArray.set(new Uint8Array(buffer), 1);
|
|
224
|
+
// console.log('send packet: ', newArray);
|
|
225
|
+
|
|
226
|
+
if (!device.opened) {
|
|
227
|
+
await this.connect(path, false);
|
|
228
|
+
}
|
|
229
|
+
await device.transferOut(this.endpointId, newArray);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const resData = await this.receiveData(path);
|
|
233
|
+
if (typeof resData !== 'string') {
|
|
234
|
+
throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
|
|
235
|
+
}
|
|
236
|
+
const jsonData = receiveOne(messages, resData);
|
|
237
|
+
return check.call(jsonData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Receive data from device
|
|
242
|
+
*/
|
|
243
|
+
async receiveData(path: string) {
|
|
244
|
+
const device: USBDevice = await this.findDevice(path);
|
|
245
|
+
if (!device.opened) {
|
|
246
|
+
await this.connect(path, false);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const firstPacket = await device.transferIn(this.endpointId, PACKET_SIZE);
|
|
250
|
+
const firstData = firstPacket.data?.buffer.slice(1);
|
|
251
|
+
console.log('receive first packet: ', firstPacket);
|
|
252
|
+
const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(firstData as ArrayBuffer);
|
|
253
|
+
|
|
254
|
+
console.log('chunk length: ', length);
|
|
255
|
+
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
257
|
+
const lengthWithHeader = Number(length + HEADER_LENGTH);
|
|
258
|
+
const decoded = new ByteBuffer(lengthWithHeader);
|
|
259
|
+
decoded.writeUint16(typeId);
|
|
260
|
+
decoded.writeUint32(length);
|
|
261
|
+
if (length) {
|
|
262
|
+
decoded.append(restBuffer);
|
|
263
|
+
}
|
|
264
|
+
console.log('first decoded: ', decoded);
|
|
265
|
+
|
|
266
|
+
while (decoded.offset < lengthWithHeader) {
|
|
267
|
+
const res = await device.transferIn(this.endpointId, PACKET_SIZE);
|
|
268
|
+
|
|
269
|
+
if (!res.data) {
|
|
270
|
+
throw new Error('no data');
|
|
271
|
+
}
|
|
272
|
+
if (res.data.byteLength === 0) {
|
|
273
|
+
// empty data
|
|
274
|
+
console.warn('empty data');
|
|
275
|
+
}
|
|
276
|
+
const buffer = res.data.buffer.slice(1);
|
|
277
|
+
if (lengthWithHeader - decoded.offset >= PACKET_SIZE) {
|
|
278
|
+
decoded.append(buffer as unknown as ArrayBuffer);
|
|
279
|
+
} else {
|
|
280
|
+
decoded.append(
|
|
281
|
+
buffer.slice(0, lengthWithHeader - decoded.offset) as unknown as ArrayBuffer
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
decoded.reset();
|
|
286
|
+
const result = decoded.toBuffer();
|
|
287
|
+
return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Release device
|
|
292
|
+
*/
|
|
293
|
+
async release(path: string) {
|
|
294
|
+
const device: USBDevice = await this.findDevice(path);
|
|
295
|
+
await device.releaseInterface(this.interfaceId);
|
|
296
|
+
await device.close();
|
|
297
|
+
}
|
|
298
|
+
}
|