@ukeyfe/hardware-transport-web-device 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,413 @@
1
+ import transport, { COMMON_HEADER_SIZE, LogBlockCommand } from '@ukeyfe/hardware-transport';
2
+ import {
3
+ ERRORS,
4
+ HardwareErrorCode,
5
+ HardwareErrorCodeMessage,
6
+ Deferred,
7
+ createDeferred,
8
+ isHeaderChunk,
9
+ } from '@ukeyfe/hardware-shared';
10
+ import type EventEmitter from 'events';
11
+ // Import DesktopAPI type from hd-transport-electron
12
+ import type { DesktopAPI } from '@ukeyfe/hardware-transport-electron';
13
+
14
+ const { parseConfigure, buildBuffers, receiveOne, check } = transport;
15
+
16
+ // Noble BLE specific API interface
17
+ declare global {
18
+ interface Window {
19
+ desktopApi?: DesktopAPI;
20
+ }
21
+ }
22
+
23
+ export type BleAcquireInput = {
24
+ uuid: string;
25
+ forceCleanRunPromise?: boolean;
26
+ };
27
+
28
+ // Packet processing result interface
29
+ interface PacketProcessResult {
30
+ isComplete: boolean;
31
+ completePacket?: string;
32
+ error?: string;
33
+ }
34
+
35
+ export default class ElectronBleTransport {
36
+ _messages: ReturnType<typeof transport.parseConfigure> | undefined;
37
+
38
+ name = 'ElectronBleTransport';
39
+
40
+ configured = false;
41
+
42
+ runPromise: Deferred<any> | null = null;
43
+
44
+ Log?: any;
45
+
46
+ emitter?: EventEmitter;
47
+
48
+ // Cache for connected devices
49
+ private connectedDevices: Set<string> = new Set();
50
+
51
+ // Data processing state
52
+ private dataBuffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
53
+
54
+ // Notification cleanup functions
55
+ private notificationCleanups: Map<string, () => void> = new Map();
56
+
57
+ // Disconnect listener cleanup functions
58
+ private disconnectCleanups: Map<string, () => void> = new Map();
59
+
60
+ // Handle bluetooth related errors with proper error code mapping
61
+ private handleBluetoothError(error: any): never {
62
+ if (error && typeof error === 'object') {
63
+ // Check for specific bluetooth error codes
64
+ if ('code' in error) {
65
+ if (error.code === HardwareErrorCode.BlePoweredOff) {
66
+ throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
67
+ }
68
+ if (error.code === HardwareErrorCode.BleUnsupported) {
69
+ throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
70
+ }
71
+ if (error.code === HardwareErrorCode.BlePermissionError) {
72
+ throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
73
+ }
74
+ }
75
+ // Check for error message containing bluetooth state related text using predefined messages
76
+ const errorMessage = error.message || String(error);
77
+ const poweredOffMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePoweredOff];
78
+ const unsupportedMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleUnsupported];
79
+ const permissionMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePermissionError];
80
+
81
+ if (errorMessage.includes(poweredOffMessage) || errorMessage.includes('poweredOff')) {
82
+ throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
83
+ }
84
+ if (errorMessage.includes(unsupportedMessage) || errorMessage.includes('unsupported')) {
85
+ throw ERRORS.TypedError(HardwareErrorCode.BleUnsupported);
86
+ }
87
+ if (errorMessage.includes(permissionMessage) || errorMessage.includes('unauthorized')) {
88
+ throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
89
+ }
90
+ }
91
+
92
+ throw error;
93
+ }
94
+
95
+ // Clean up all device state and listeners - unified cleanup function
96
+ private cleanupDeviceState(deviceId: string): void {
97
+ this.connectedDevices.delete(deviceId);
98
+ this.dataBuffers.delete(deviceId);
99
+
100
+ // Clean up notification listener
101
+ const notifyCleanup = this.notificationCleanups.get(deviceId);
102
+ if (notifyCleanup) {
103
+ notifyCleanup();
104
+ this.notificationCleanups.delete(deviceId);
105
+ }
106
+
107
+ // Clean up disconnect listener
108
+ const disconnectCleanup = this.disconnectCleanups.get(deviceId);
109
+ if (disconnectCleanup) {
110
+ disconnectCleanup();
111
+ this.disconnectCleanups.delete(deviceId);
112
+ }
113
+ }
114
+
115
+ init(logger: any, emitter?: EventEmitter) {
116
+ this.Log = logger;
117
+ this.emitter = emitter;
118
+
119
+ // Check if Noble BLE API is available
120
+ if (!window.desktopApi?.nobleBle) {
121
+ throw ERRORS.TypedError(
122
+ HardwareErrorCode.RuntimeError,
123
+ 'Noble BLE API is not available. Please ensure you are running in Electron with Noble support.'
124
+ );
125
+ }
126
+
127
+ this.Log?.debug('[Transport] Noble BLE Transport initialized');
128
+ }
129
+
130
+ configure(signedData: any) {
131
+ const messages = parseConfigure(signedData);
132
+ this.configured = true;
133
+ this._messages = messages;
134
+ }
135
+
136
+ listen() {}
137
+
138
+ async enumerate(): Promise<{ id: string; name: string }[]> {
139
+ try {
140
+ if (!window.desktopApi?.nobleBle) {
141
+ throw new Error('Noble BLE API not available');
142
+ }
143
+
144
+ const devices = await window.desktopApi.nobleBle.enumerate();
145
+ return devices;
146
+ } catch (error) {
147
+ this.Log?.error('[Transport] Noble BLE enumerate failed:', error);
148
+ this.handleBluetoothError(error);
149
+ }
150
+ }
151
+
152
+ async acquire(input: BleAcquireInput) {
153
+ const { uuid, forceCleanRunPromise } = input;
154
+
155
+ if (!uuid) {
156
+ throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
157
+ }
158
+
159
+ // Force clean running Promise
160
+ if (forceCleanRunPromise && this.runPromise) {
161
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
162
+ }
163
+
164
+ try {
165
+ if (!window.desktopApi?.nobleBle) {
166
+ throw new Error('Noble BLE API not available');
167
+ }
168
+
169
+ // Check if device is available
170
+ const device = await window.desktopApi.nobleBle.getDevice(uuid);
171
+ if (!device) {
172
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
173
+ }
174
+
175
+ // Connect to device
176
+ try {
177
+ await window.desktopApi.nobleBle.connect(uuid);
178
+ this.connectedDevices.add(uuid);
179
+ } catch (error) {
180
+ this.handleBluetoothError(error);
181
+ }
182
+
183
+ // Initialize data buffer for this device
184
+ this.dataBuffers.set(uuid, { buffer: [], bufferLength: 0 });
185
+
186
+ // Subscribe to notifications
187
+ await window.desktopApi.nobleBle.subscribe(uuid);
188
+
189
+ // Set up notification listener
190
+ const cleanup = window.desktopApi.nobleBle.onNotification(
191
+ (deviceId: string, data: string) => {
192
+ if (deviceId === uuid) {
193
+ this.handleNotificationData(uuid, data);
194
+ }
195
+ }
196
+ );
197
+ this.notificationCleanups.set(uuid, cleanup);
198
+
199
+ // Set up disconnect listener
200
+ const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
201
+ (disconnectedDevice: any) => {
202
+ if (disconnectedDevice.id === uuid) {
203
+ this.cleanupDeviceState(uuid);
204
+
205
+ // Trigger disconnect event
206
+ this.emitter?.emit('device-disconnect', {
207
+ name: disconnectedDevice.name,
208
+ id: disconnectedDevice.id,
209
+ connectId: disconnectedDevice.id,
210
+ });
211
+ }
212
+ }
213
+ );
214
+ this.disconnectCleanups.set(uuid, disconnectCleanup);
215
+
216
+ // Trigger connect event
217
+ this.emitter?.emit('device-connect', {
218
+ name: device.name,
219
+ id: device.id,
220
+ connectId: device.id,
221
+ });
222
+
223
+ return { uuid, path: uuid };
224
+ } catch (error) {
225
+ this.Log?.error('[Transport] Noble BLE acquire failed:', error);
226
+ throw error;
227
+ }
228
+ }
229
+
230
+ async release(id: string) {
231
+ try {
232
+ if (this.connectedDevices.has(id)) {
233
+ // Unsubscribe from notifications
234
+ if (window.desktopApi?.nobleBle) {
235
+ await window.desktopApi.nobleBle.unsubscribe(id);
236
+ }
237
+
238
+ // Disconnect device
239
+ if (window.desktopApi?.nobleBle) {
240
+ await window.desktopApi.nobleBle.disconnect(id);
241
+ }
242
+
243
+ // Clean up all device state
244
+ this.cleanupDeviceState(id);
245
+ }
246
+ } catch (error) {
247
+ this.Log?.error('[Transport] Noble BLE release failed:', error);
248
+ // Clean up local state even if release fails
249
+ this.cleanupDeviceState(id);
250
+ }
251
+ }
252
+
253
+ // Handle notification data from Noble BLE
254
+ private handleNotificationData(deviceId: string, hexData: string): void {
255
+ // Check for pairing rejection
256
+ if (hexData === 'PAIRING_REJECTED') {
257
+ this.Log?.debug('[Transport] Pairing rejection detected for device:', deviceId);
258
+ if (this.runPromise) {
259
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled));
260
+ }
261
+ return;
262
+ }
263
+
264
+ const result = this.processNotificationPacket(deviceId, hexData);
265
+
266
+ if (result.error) {
267
+ this.Log?.error('[Transport] Packet processing error:', result.error);
268
+ if (this.runPromise) {
269
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
270
+ }
271
+ return;
272
+ }
273
+
274
+ if (result.isComplete && result.completePacket) {
275
+ if (this.runPromise) {
276
+ this.runPromise.resolve(result.completePacket);
277
+ }
278
+ }
279
+ }
280
+
281
+ async call(uuid: string, name: string, data: Record<string, unknown>) {
282
+ if (this._messages == null) {
283
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
284
+ }
285
+
286
+ const forceRun = name === 'Initialize' || name === 'Cancel';
287
+
288
+ if (this.runPromise && !forceRun) {
289
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
290
+ }
291
+
292
+ if (!this.connectedDevices.has(uuid)) {
293
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound, `Device ${uuid} not connected`);
294
+ }
295
+
296
+ this.runPromise = createDeferred();
297
+ const messages = this._messages;
298
+
299
+ // Log different types of commands appropriately
300
+ if (name === 'ResourceUpdate' || name === 'ResourceAck') {
301
+ this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', {
302
+ file_name: data?.file_name,
303
+ hash: data?.hash,
304
+ });
305
+ } else if (LogBlockCommand.has(name)) {
306
+ this.Log?.debug('[Transport] Noble BLE call', 'name:', name);
307
+ } else {
308
+ this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', data);
309
+ }
310
+
311
+ const buffers = buildBuffers(messages, name, data);
312
+
313
+ try {
314
+ if (!window.desktopApi?.nobleBle) {
315
+ throw new Error('Noble BLE write API not available');
316
+ }
317
+
318
+ // Write each buffer to the device
319
+ for (let i = 0; i < buffers.length; i++) {
320
+ const buffer = buffers[i];
321
+
322
+ if (!buffer || typeof buffer.toString !== 'function') {
323
+ this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} is invalid:`, buffer);
324
+ throw new Error(`Buffer ${i + 1} is invalid`);
325
+ }
326
+
327
+ // Use ByteBuffer's toString('hex') method directly, similar to other transports
328
+ const hexString = buffer.toString('hex');
329
+
330
+ if (hexString.length === 0) {
331
+ this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} generated empty hex string`);
332
+ throw new Error(`Buffer ${i + 1} is empty`);
333
+ }
334
+
335
+ await window.desktopApi.nobleBle.write(uuid, hexString);
336
+ }
337
+
338
+ // Wait for response
339
+ const response = await this.runPromise.promise;
340
+
341
+ if (typeof response !== 'string') {
342
+ throw new Error('Returning data is not string.');
343
+ }
344
+
345
+ const jsonData = receiveOne(messages, response);
346
+ return check.call(jsonData);
347
+ } catch (e) {
348
+ this.Log?.error('[Transport] Noble BLE call error:', e);
349
+ throw e;
350
+ } finally {
351
+ this.runPromise = null;
352
+ }
353
+ }
354
+
355
+ // Process hex data from notification with validation and packet reassembly
356
+ private processNotificationPacket(deviceId: string, hexData: string): PacketProcessResult {
357
+ try {
358
+ // Validate input
359
+ if (typeof hexData !== 'string') {
360
+ return { isComplete: false, error: 'Invalid hexData type' };
361
+ }
362
+
363
+ // Clean and validate hex format
364
+ const cleanHexData = hexData.replace(/\s+/g, '');
365
+ if (!/^[0-9A-Fa-f]*$/.test(cleanHexData)) {
366
+ return { isComplete: false, error: 'Invalid hex data format' };
367
+ }
368
+
369
+ // Convert hex string to Uint8Array
370
+ const hexMatch = cleanHexData.match(/.{1,2}/g);
371
+ if (!hexMatch) {
372
+ return { isComplete: false, error: 'Failed to parse hex data' };
373
+ }
374
+
375
+ const data = new Uint8Array(hexMatch.map(byte => parseInt(byte, 16)));
376
+
377
+ // Get buffer state
378
+ const bufferState = this.dataBuffers.get(deviceId);
379
+ if (!bufferState) {
380
+ return { isComplete: false, error: 'No buffer state for device' };
381
+ }
382
+
383
+ // Process header or data chunk
384
+ if (isHeaderChunk(data)) {
385
+ const dataView = new DataView(data.buffer);
386
+ bufferState.bufferLength = dataView.getInt32(5, false);
387
+ bufferState.buffer = [...data.subarray(3)];
388
+ } else {
389
+ bufferState.buffer = bufferState.buffer.concat([...data]);
390
+ }
391
+
392
+ // Check if packet is complete
393
+ if (bufferState.buffer.length - COMMON_HEADER_SIZE >= bufferState.bufferLength) {
394
+ const completeBuffer = new Uint8Array(bufferState.buffer);
395
+
396
+ // Reset buffer state
397
+ bufferState.bufferLength = 0;
398
+ bufferState.buffer = [];
399
+
400
+ // Convert to hex string
401
+ const hexString = Array.from(completeBuffer)
402
+ .map(b => b.toString(16).padStart(2, '0'))
403
+ .join('');
404
+
405
+ return { isComplete: true, completePacket: hexString };
406
+ }
407
+
408
+ return { isComplete: false };
409
+ } catch (error) {
410
+ return { isComplete: false, error: `Packet processing error: ${error}` };
411
+ }
412
+ }
413
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import WebUsbTransport from './webusb';
2
+ import ElectronBleTransport from './electron-ble-transport';
3
+
4
+ export { WebUsbTransport, ElectronBleTransport };
package/src/webusb.ts ADDED
@@ -0,0 +1,298 @@
1
+ /* eslint-disable no-undef */
2
+ import transport, { AcquireInput, LogBlockCommand } from '@ukeyfe/hardware-transport';
3
+ import { ERRORS, HardwareErrorCode, UKEY_WEBUSB_FILTER, wait } from '@ukeyfe/hardware-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: UKEY_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 ukeyDevices = devices.filter(dev => {
106
+ const isUKey = UKEY_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 isUKey && hasSerialNumber;
111
+ });
112
+
113
+ this.deviceList = ukeyDevices.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
+ }