@onekeyfe/hd-transport-web-device 1.1.27 → 1.2.0-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/__tests__/electron-ble-transport.test.ts +231 -0
- package/dist/electron-ble-transport.d.ts +50 -13
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +80 -17
- package/dist/index.js +927 -115
- package/dist/webusb.d.ts +29 -3
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +612 -133
- package/src/webusb.ts +595 -47
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
import transport, {
|
|
1
|
+
import transport, {
|
|
2
|
+
LogBlockCommand,
|
|
3
|
+
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
4
|
+
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
5
|
+
ProtocolV2FrameAssembler,
|
|
6
|
+
ProtocolV2Session,
|
|
7
|
+
bytesToHex,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
probeProtocolV2 as probeProtocolV2Helper,
|
|
10
|
+
} from '@onekeyfe/hd-transport';
|
|
2
11
|
import {
|
|
3
12
|
ERRORS,
|
|
4
13
|
HardwareErrorCode,
|
|
5
14
|
HardwareErrorCodeMessage,
|
|
6
15
|
createDeferred,
|
|
7
16
|
isHeaderChunk,
|
|
17
|
+
wait,
|
|
8
18
|
} from '@onekeyfe/hd-shared';
|
|
9
19
|
|
|
10
20
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
11
|
-
import type EventEmitter from 'events';
|
|
12
|
-
// Import DesktopAPI type from hd-transport-electron
|
|
13
21
|
import type { DesktopAPI } from '@onekeyfe/hd-transport-electron';
|
|
22
|
+
import type { OneKeyDeviceInfo, ProtocolType, TransportCallOptions } from '@onekeyfe/hd-transport';
|
|
23
|
+
import type EventEmitter from 'events';
|
|
24
|
+
|
|
25
|
+
const FILE_WRITE_LOG_BLOCK_PATTERN = /(?:^|[^a-z])(?:raw)?(?:filesystem|emmc)?filewrite$/i;
|
|
26
|
+
|
|
27
|
+
function shouldSuppressHighVolumeCallLog(name: string) {
|
|
28
|
+
const normalized = name.replace(/[_\s-]/g, '');
|
|
29
|
+
return FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isLogBlockCommand(name: string) {
|
|
33
|
+
return (LogBlockCommand as Set<string> | undefined)?.has?.(name) ?? false;
|
|
34
|
+
}
|
|
14
35
|
|
|
15
|
-
const { parseConfigure,
|
|
36
|
+
const { parseConfigure, ProtocolV1, check } = transport;
|
|
16
37
|
|
|
17
|
-
// Noble BLE specific API interface
|
|
18
38
|
declare global {
|
|
19
39
|
interface Window {
|
|
20
40
|
desktopApi?: DesktopAPI;
|
|
@@ -24,44 +44,89 @@ declare global {
|
|
|
24
44
|
export type BleAcquireInput = {
|
|
25
45
|
uuid: string;
|
|
26
46
|
forceCleanRunPromise?: boolean;
|
|
47
|
+
expectedProtocol?: ProtocolType;
|
|
27
48
|
};
|
|
28
49
|
|
|
29
|
-
// Packet processing result interface
|
|
30
50
|
interface PacketProcessResult {
|
|
31
51
|
isComplete: boolean;
|
|
32
52
|
completePacket?: string;
|
|
33
53
|
error?: string;
|
|
34
54
|
}
|
|
35
55
|
|
|
56
|
+
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
57
|
+
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const toBleDescriptor = (
|
|
61
|
+
device: { id: string; name: string | null },
|
|
62
|
+
protocolType?: ProtocolType
|
|
63
|
+
): OneKeyDeviceInfo =>
|
|
64
|
+
({
|
|
65
|
+
id: device.id,
|
|
66
|
+
name: device.name,
|
|
67
|
+
path: device.id,
|
|
68
|
+
debug: false,
|
|
69
|
+
commType: 'electron-ble',
|
|
70
|
+
...(protocolType ? { protocolType } : {}),
|
|
71
|
+
} as OneKeyDeviceInfo);
|
|
72
|
+
|
|
73
|
+
const BLE_PACKET_SIZE = 192;
|
|
74
|
+
const BLE_WRITE_DELAY_MS = 5;
|
|
75
|
+
const BLE_WRITE_MAX_RETRIES = 3;
|
|
76
|
+
const BLE_WRITE_RETRY_DELAY_MS = 300;
|
|
77
|
+
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
78
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
79
|
+
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Desktop Electron BLE transport with automatic Protocol V1/V2 detection.
|
|
83
|
+
*
|
|
84
|
+
* Protocol V1 devices continue using chunked packets. Protocol V2 is detected
|
|
85
|
+
* after a Protocol V1 Initialize timeout by probing Protocol V2 Ping.
|
|
86
|
+
*/
|
|
36
87
|
export default class ElectronBleTransport {
|
|
37
|
-
_messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
88
|
+
private _messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
89
|
+
|
|
90
|
+
private _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
38
91
|
|
|
39
92
|
name = 'ElectronBleTransport';
|
|
40
93
|
|
|
41
94
|
configured = false;
|
|
42
95
|
|
|
43
|
-
runPromise: Deferred<
|
|
96
|
+
runPromise: Deferred<Uint8Array | string> | null = null;
|
|
44
97
|
|
|
45
98
|
Log?: any;
|
|
46
99
|
|
|
47
100
|
emitter?: EventEmitter;
|
|
48
101
|
|
|
49
|
-
// Cache for connected devices
|
|
50
102
|
private connectedDevices: Set<string> = new Set();
|
|
51
103
|
|
|
52
|
-
|
|
53
|
-
|
|
104
|
+
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
105
|
+
|
|
106
|
+
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
107
|
+
|
|
108
|
+
private v1Buffers: Map<string, { buffer: number[]; bufferLength: number }> = new Map();
|
|
109
|
+
|
|
110
|
+
private v2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
111
|
+
|
|
112
|
+
private v2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
113
|
+
|
|
114
|
+
private v2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
115
|
+
|
|
116
|
+
private activeProtocolV2Call: { uuid: string; token: number } | null = null;
|
|
117
|
+
|
|
118
|
+
private nextProtocolV2CallToken = 1;
|
|
54
119
|
|
|
55
|
-
// Notification cleanup functions
|
|
56
120
|
private notificationCleanups: Map<string, () => void> = new Map();
|
|
57
121
|
|
|
58
|
-
// Disconnect listener cleanup functions
|
|
59
122
|
private disconnectCleanups: Map<string, () => void> = new Map();
|
|
60
123
|
|
|
61
|
-
|
|
124
|
+
private notificationTokens: Map<string, number> = new Map();
|
|
125
|
+
|
|
126
|
+
private nextNotificationToken = 1;
|
|
127
|
+
|
|
62
128
|
private handleBluetoothError(error: any): never {
|
|
63
129
|
if (error && typeof error === 'object') {
|
|
64
|
-
// Check for specific bluetooth error codes
|
|
65
130
|
if ('code' in error) {
|
|
66
131
|
if (error.code === HardwareErrorCode.BlePoweredOff) {
|
|
67
132
|
throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff);
|
|
@@ -73,7 +138,6 @@ export default class ElectronBleTransport {
|
|
|
73
138
|
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
74
139
|
}
|
|
75
140
|
}
|
|
76
|
-
// Check for error message containing bluetooth state related text using predefined messages
|
|
77
141
|
const errorMessage = error.message || String(error);
|
|
78
142
|
const poweredOffMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePoweredOff];
|
|
79
143
|
const unsupportedMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleUnsupported];
|
|
@@ -89,23 +153,28 @@ export default class ElectronBleTransport {
|
|
|
89
153
|
throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError);
|
|
90
154
|
}
|
|
91
155
|
}
|
|
92
|
-
|
|
93
156
|
throw error;
|
|
94
157
|
}
|
|
95
158
|
|
|
96
|
-
// Clean up all device state and listeners - unified cleanup function
|
|
97
159
|
private cleanupDeviceState(deviceId: string): void {
|
|
98
160
|
this.connectedDevices.delete(deviceId);
|
|
99
|
-
this.
|
|
161
|
+
this.deviceProtocol.delete(deviceId);
|
|
162
|
+
// Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2)
|
|
163
|
+
// and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect.
|
|
164
|
+
this.v1Buffers.delete(deviceId);
|
|
165
|
+
this.v2Assemblers.delete(deviceId);
|
|
166
|
+
this.resetProtocolV2Frames(deviceId);
|
|
167
|
+
if (this.activeProtocolV2Call?.uuid === deviceId) {
|
|
168
|
+
this.activeProtocolV2Call = null;
|
|
169
|
+
}
|
|
170
|
+
this.notificationTokens.delete(deviceId);
|
|
100
171
|
|
|
101
|
-
// Clean up notification listener
|
|
102
172
|
const notifyCleanup = this.notificationCleanups.get(deviceId);
|
|
103
173
|
if (notifyCleanup) {
|
|
104
174
|
notifyCleanup();
|
|
105
175
|
this.notificationCleanups.delete(deviceId);
|
|
106
176
|
}
|
|
107
177
|
|
|
108
|
-
// Clean up disconnect listener
|
|
109
178
|
const disconnectCleanup = this.disconnectCleanups.get(deviceId);
|
|
110
179
|
if (disconnectCleanup) {
|
|
111
180
|
disconnectCleanup();
|
|
@@ -117,7 +186,6 @@ export default class ElectronBleTransport {
|
|
|
117
186
|
this.Log = logger;
|
|
118
187
|
this.emitter = emitter;
|
|
119
188
|
|
|
120
|
-
// Check if Noble BLE API is available
|
|
121
189
|
if (!window.desktopApi?.nobleBle) {
|
|
122
190
|
throw ERRORS.TypedError(
|
|
123
191
|
HardwareErrorCode.RuntimeError,
|
|
@@ -125,41 +193,57 @@ export default class ElectronBleTransport {
|
|
|
125
193
|
);
|
|
126
194
|
}
|
|
127
195
|
|
|
128
|
-
this.Log?.debug('[
|
|
196
|
+
this.Log?.debug('[Electron BLE] Transport initialized');
|
|
129
197
|
}
|
|
130
198
|
|
|
131
199
|
configure(signedData: any) {
|
|
132
|
-
|
|
200
|
+
this._messages = parseConfigure(signedData);
|
|
133
201
|
this.configured = true;
|
|
134
|
-
this._messages = messages;
|
|
135
202
|
}
|
|
136
203
|
|
|
137
|
-
|
|
204
|
+
configureProtocolV2(signedData: any) {
|
|
205
|
+
this._messagesV2 = parseConfigure(signedData);
|
|
206
|
+
this.Log?.debug('[Electron BLE] Protocol V2 schema configured');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async listen() {
|
|
210
|
+
return this.enumerate();
|
|
211
|
+
}
|
|
138
212
|
|
|
139
|
-
async enumerate(): Promise<
|
|
213
|
+
async enumerate(): Promise<OneKeyDeviceInfo[]> {
|
|
140
214
|
try {
|
|
141
215
|
if (!window.desktopApi?.nobleBle) {
|
|
142
216
|
throw new Error('Noble BLE API not available');
|
|
143
217
|
}
|
|
144
|
-
|
|
145
218
|
const devices = await window.desktopApi.nobleBle.enumerate();
|
|
146
|
-
|
|
219
|
+
this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`);
|
|
220
|
+
for (const dev of devices) {
|
|
221
|
+
this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`);
|
|
222
|
+
const protocolHint = inferProtocolHintFromDeviceName(dev.name);
|
|
223
|
+
if (protocolHint) {
|
|
224
|
+
this.deviceProtocolHints.set(dev.id, protocolHint);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return devices.map(device => toBleDescriptor(device));
|
|
147
228
|
} catch (error) {
|
|
148
|
-
this.Log?.error('[
|
|
229
|
+
this.Log?.error('[Electron BLE] enumerate failed:', error);
|
|
149
230
|
this.handleBluetoothError(error);
|
|
150
231
|
}
|
|
151
232
|
}
|
|
152
233
|
|
|
153
234
|
async acquire(input: BleAcquireInput) {
|
|
154
|
-
const { uuid, forceCleanRunPromise } = input;
|
|
235
|
+
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
155
236
|
|
|
156
237
|
if (!uuid) {
|
|
157
238
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
158
239
|
}
|
|
159
240
|
|
|
160
|
-
// Force clean running Promise
|
|
161
241
|
if (forceCleanRunPromise && this.runPromise) {
|
|
162
|
-
|
|
242
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
243
|
+
this.runPromise.reject(error);
|
|
244
|
+
this.rejectAllProtocolV2Frames(error);
|
|
245
|
+
this.runPromise = null;
|
|
246
|
+
this.activeProtocolV2Call = null;
|
|
163
247
|
}
|
|
164
248
|
|
|
165
249
|
try {
|
|
@@ -167,13 +251,17 @@ export default class ElectronBleTransport {
|
|
|
167
251
|
throw new Error('Noble BLE API not available');
|
|
168
252
|
}
|
|
169
253
|
|
|
170
|
-
// Check if device is available
|
|
171
254
|
const device = await window.desktopApi.nobleBle.getDevice(uuid);
|
|
172
255
|
if (!device) {
|
|
173
256
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`);
|
|
174
257
|
}
|
|
258
|
+
const protocolHint = expectedProtocol
|
|
259
|
+
? undefined
|
|
260
|
+
: this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name);
|
|
261
|
+
if (protocolHint) {
|
|
262
|
+
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
263
|
+
}
|
|
175
264
|
|
|
176
|
-
// Connect to device
|
|
177
265
|
try {
|
|
178
266
|
await window.desktopApi.nobleBle.connect(uuid);
|
|
179
267
|
this.connectedDevices.add(uuid);
|
|
@@ -181,29 +269,18 @@ export default class ElectronBleTransport {
|
|
|
181
269
|
this.handleBluetoothError(error);
|
|
182
270
|
}
|
|
183
271
|
|
|
184
|
-
|
|
185
|
-
this.
|
|
272
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
273
|
+
this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
186
274
|
|
|
187
|
-
// Subscribe to notifications
|
|
188
275
|
await window.desktopApi.nobleBle.subscribe(uuid);
|
|
189
276
|
|
|
190
|
-
|
|
191
|
-
const cleanup = window.desktopApi.nobleBle.onNotification(
|
|
192
|
-
(deviceId: string, data: string) => {
|
|
193
|
-
if (deviceId === uuid) {
|
|
194
|
-
this.handleNotificationData(uuid, data);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
);
|
|
277
|
+
const cleanup = this.createNotificationSubscription(uuid);
|
|
198
278
|
this.notificationCleanups.set(uuid, cleanup);
|
|
199
279
|
|
|
200
|
-
// Set up disconnect listener
|
|
201
280
|
const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected(
|
|
202
281
|
(disconnectedDevice: any) => {
|
|
203
282
|
if (disconnectedDevice.id === uuid) {
|
|
204
283
|
this.cleanupDeviceState(uuid);
|
|
205
|
-
|
|
206
|
-
// Trigger disconnect event
|
|
207
284
|
this.emitter?.emit('device-disconnect', {
|
|
208
285
|
name: disconnectedDevice.name,
|
|
209
286
|
id: disconnectedDevice.id,
|
|
@@ -214,16 +291,29 @@ export default class ElectronBleTransport {
|
|
|
214
291
|
);
|
|
215
292
|
this.disconnectCleanups.set(uuid, disconnectCleanup);
|
|
216
293
|
|
|
217
|
-
|
|
294
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
295
|
+
|
|
218
296
|
this.emitter?.emit('device-connect', {
|
|
219
297
|
name: device.name,
|
|
220
298
|
id: device.id,
|
|
221
299
|
connectId: device.id,
|
|
222
300
|
});
|
|
223
301
|
|
|
224
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
...toBleDescriptor({ id: device.id, name: device.name }, protocolType),
|
|
304
|
+
uuid,
|
|
305
|
+
};
|
|
225
306
|
} catch (error) {
|
|
226
|
-
this.Log?.error('[
|
|
307
|
+
this.Log?.error('[Electron BLE] acquire failed:', error);
|
|
308
|
+
try {
|
|
309
|
+
if (window.desktopApi?.nobleBle && this.connectedDevices.has(uuid)) {
|
|
310
|
+
await window.desktopApi.nobleBle.unsubscribe(uuid);
|
|
311
|
+
await window.desktopApi.nobleBle.disconnect(uuid);
|
|
312
|
+
}
|
|
313
|
+
} catch (cleanupError) {
|
|
314
|
+
this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError);
|
|
315
|
+
}
|
|
316
|
+
this.cleanupDeviceState(uuid);
|
|
227
317
|
throw error;
|
|
228
318
|
}
|
|
229
319
|
}
|
|
@@ -231,157 +321,550 @@ export default class ElectronBleTransport {
|
|
|
231
321
|
async release(id: string) {
|
|
232
322
|
try {
|
|
233
323
|
if (this.connectedDevices.has(id)) {
|
|
234
|
-
// Unsubscribe from notifications
|
|
235
324
|
if (window.desktopApi?.nobleBle) {
|
|
236
325
|
await window.desktopApi.nobleBle.unsubscribe(id);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Disconnect device
|
|
240
|
-
if (window.desktopApi?.nobleBle) {
|
|
241
326
|
await window.desktopApi.nobleBle.disconnect(id);
|
|
242
327
|
}
|
|
243
|
-
|
|
244
|
-
// Clean up all device state
|
|
245
328
|
this.cleanupDeviceState(id);
|
|
246
329
|
}
|
|
247
330
|
} catch (error) {
|
|
248
|
-
this.Log?.error('[
|
|
249
|
-
// Clean up local state even if release fails
|
|
331
|
+
this.Log?.error('[Electron BLE] release failed:', error);
|
|
250
332
|
this.cleanupDeviceState(id);
|
|
251
333
|
}
|
|
252
334
|
}
|
|
253
335
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
336
|
+
private createProtocolMismatchError(expected: ProtocolType) {
|
|
337
|
+
return ERRORS.TypedError(
|
|
338
|
+
HardwareErrorCode.RuntimeError,
|
|
339
|
+
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private createProtocolDetectionError() {
|
|
344
|
+
return ERRORS.TypedError(
|
|
345
|
+
HardwareErrorCode.BleTimeoutError,
|
|
346
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
351
|
+
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
352
|
+
this.deviceProtocol.delete(uuid);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async detectProtocol(
|
|
357
|
+
uuid: string,
|
|
358
|
+
expectedProtocol?: ProtocolType,
|
|
359
|
+
protocolHint?: ProtocolType
|
|
360
|
+
): Promise<ProtocolType> {
|
|
361
|
+
if (expectedProtocol === 'V1') {
|
|
362
|
+
if (await this.probeProtocolV1(uuid)) {
|
|
363
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
364
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`);
|
|
365
|
+
return 'V1';
|
|
366
|
+
}
|
|
367
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (expectedProtocol === 'V2') {
|
|
371
|
+
// 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
|
|
372
|
+
// 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
|
|
373
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
374
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
375
|
+
return 'V2';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
|
|
379
|
+
// 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
|
|
380
|
+
// 不能作为最终结论。
|
|
381
|
+
const probeOrder: ProtocolType[] =
|
|
382
|
+
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
385
|
+
const protocol = probeOrder[i];
|
|
386
|
+
if (i > 0) {
|
|
387
|
+
// 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
|
|
388
|
+
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
389
|
+
}
|
|
390
|
+
const detected =
|
|
391
|
+
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
392
|
+
if (detected) {
|
|
393
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
394
|
+
this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
395
|
+
return protocol;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.deviceProtocol.delete(uuid);
|
|
400
|
+
throw this.createProtocolDetectionError();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private createNotificationSubscription(uuid: string) {
|
|
404
|
+
if (!window.desktopApi?.nobleBle) {
|
|
405
|
+
throw new Error('Noble BLE API not available');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const notificationToken = this.nextNotificationToken;
|
|
409
|
+
this.nextNotificationToken += 1;
|
|
410
|
+
this.notificationTokens.set(uuid, notificationToken);
|
|
411
|
+
|
|
412
|
+
return window.desktopApi.nobleBle.onNotification((deviceId: string, data: string) => {
|
|
413
|
+
if (deviceId === uuid && this.notificationTokens.get(uuid) === notificationToken) {
|
|
414
|
+
this.handleNotification(uuid, data);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
420
|
+
this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 });
|
|
421
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
422
|
+
this.resetProtocolV2Frames(uuid);
|
|
423
|
+
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
424
|
+
this.activeProtocolV2Call = null;
|
|
425
|
+
}
|
|
426
|
+
if (this.runPromise) {
|
|
427
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
428
|
+
this.runPromise.reject(error);
|
|
429
|
+
this.runPromise = null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const notifyCleanup = this.notificationCleanups.get(uuid);
|
|
433
|
+
if (notifyCleanup) {
|
|
434
|
+
notifyCleanup();
|
|
435
|
+
this.notificationCleanups.delete(uuid);
|
|
436
|
+
}
|
|
437
|
+
this.notificationTokens.delete(uuid);
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await window.desktopApi?.nobleBle?.unsubscribe(uuid);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error);
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await window.desktopApi?.nobleBle?.subscribe(uuid);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error);
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const cleanup = this.createNotificationSubscription(uuid);
|
|
452
|
+
this.notificationCleanups.set(uuid, cleanup);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private async probeProtocolV1(uuid: string) {
|
|
456
|
+
if (!this._messages) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
462
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
463
|
+
return true;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
466
|
+
this.Log?.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async probeProtocolV2(uuid: string) {
|
|
472
|
+
if (!this._messages || !this._messagesV2) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
477
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
478
|
+
const detected = await probeProtocolV2Helper({
|
|
479
|
+
call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
|
|
480
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
|
|
481
|
+
logger: this.Log,
|
|
482
|
+
logPrefix: 'ProtocolV2 BLE',
|
|
483
|
+
onProbeFailed: () => {
|
|
484
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
485
|
+
this.resetProtocolV2Frames(uuid);
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
if (!detected) {
|
|
489
|
+
this.clearProbeProtocol(uuid, 'V2');
|
|
490
|
+
}
|
|
491
|
+
return detected;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private async writeWithChunking(uuid: string, hexData: string): Promise<void> {
|
|
495
|
+
const totalBytes = hexData.length / 2;
|
|
496
|
+
|
|
497
|
+
if (totalBytes <= BLE_PACKET_SIZE) {
|
|
498
|
+
await wait(BLE_WRITE_DELAY_MS);
|
|
499
|
+
await this.writeWithRetry(uuid, hexData);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (let offset = 0; offset < hexData.length; ) {
|
|
504
|
+
const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset);
|
|
505
|
+
const chunkHex = hexData.substring(offset, offset + chunkHexLen);
|
|
506
|
+
offset += chunkHexLen;
|
|
507
|
+
|
|
508
|
+
await this.writeWithRetry(uuid, chunkHex);
|
|
509
|
+
|
|
510
|
+
if (offset < hexData.length) {
|
|
511
|
+
await wait(BLE_WRITE_DELAY_MS);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private async writeWithRetry(uuid: string, hexData: string): Promise<void> {
|
|
517
|
+
let lastError: any;
|
|
518
|
+
const nobleBle = window.desktopApi?.nobleBle;
|
|
519
|
+
if (!nobleBle) {
|
|
520
|
+
throw new Error('Noble BLE API not available');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
for (let attempt = 1; attempt <= BLE_WRITE_MAX_RETRIES; attempt++) {
|
|
524
|
+
try {
|
|
525
|
+
await nobleBle.write(uuid, hexData);
|
|
526
|
+
return;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
lastError = error;
|
|
529
|
+
this.Log?.error(
|
|
530
|
+
`[Electron BLE] write failed (attempt ${attempt}/${BLE_WRITE_MAX_RETRIES}):`,
|
|
531
|
+
error
|
|
532
|
+
);
|
|
533
|
+
if (attempt < BLE_WRITE_MAX_RETRIES) {
|
|
534
|
+
await wait(BLE_WRITE_RETRY_DELAY_MS);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
throw ERRORS.TypedError(
|
|
539
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
540
|
+
`BLE write failed after ${BLE_WRITE_MAX_RETRIES} attempts: ${lastError?.message ?? lastError}`
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private handleNotification(deviceId: string, hexData: string): void {
|
|
257
545
|
if (hexData === 'PAIRING_REJECTED') {
|
|
258
|
-
this.Log?.debug('[
|
|
546
|
+
this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId);
|
|
259
547
|
if (this.runPromise) {
|
|
260
|
-
|
|
548
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled);
|
|
549
|
+
this.runPromise.reject(error);
|
|
550
|
+
this.rejectAllProtocolV2Frames(error);
|
|
261
551
|
}
|
|
262
552
|
return;
|
|
263
553
|
}
|
|
264
554
|
|
|
265
|
-
const
|
|
555
|
+
const protocol = this.deviceProtocol.get(deviceId);
|
|
556
|
+
if (!protocol) {
|
|
557
|
+
this.Log?.debug('[Electron BLE] Ignore notification before protocol detection:', deviceId);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (protocol === 'V2') {
|
|
561
|
+
this.handleProtocolV2Notification(deviceId, hexData);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.handleProtocolV1Notification(deviceId, hexData);
|
|
565
|
+
}
|
|
266
566
|
|
|
267
|
-
|
|
268
|
-
|
|
567
|
+
private handleProtocolV2Notification(deviceId: string, hexData: string): void {
|
|
568
|
+
try {
|
|
569
|
+
if (!this.runPromise || this.activeProtocolV2Call?.uuid !== deviceId) {
|
|
570
|
+
this.v2Assemblers.get(deviceId)?.reset();
|
|
571
|
+
this.resetProtocolV2Frames(deviceId);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const bytes = hexToBytes(hexData);
|
|
576
|
+
if (bytes.length === 0) return;
|
|
577
|
+
|
|
578
|
+
const assembler = this.v2Assemblers.get(deviceId);
|
|
579
|
+
if (!assembler) return;
|
|
580
|
+
|
|
581
|
+
for (const frameData of assembler.drain(bytes)) {
|
|
582
|
+
this.resolveProtocolV2Frame(deviceId, frameData);
|
|
583
|
+
}
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.Log?.error('[Electron BLE] Protocol V2 notification error:', error);
|
|
269
586
|
if (this.runPromise) {
|
|
270
|
-
|
|
587
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
588
|
+
this.runPromise.reject(notifyError);
|
|
589
|
+
this.rejectAllProtocolV2Frames(notifyError);
|
|
271
590
|
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private getProtocolV2FrameQueue(uuid: string) {
|
|
595
|
+
let queue = this.v2FrameQueues.get(uuid);
|
|
596
|
+
if (!queue) {
|
|
597
|
+
queue = [];
|
|
598
|
+
this.v2FrameQueues.set(uuid, queue);
|
|
599
|
+
}
|
|
600
|
+
return queue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
|
|
604
|
+
const framePromise = this.v2FramePromises.get(uuid);
|
|
605
|
+
if (framePromise) {
|
|
606
|
+
framePromise.resolve(frame);
|
|
607
|
+
this.v2FramePromises.delete(uuid);
|
|
272
608
|
return;
|
|
273
609
|
}
|
|
610
|
+
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
611
|
+
}
|
|
274
612
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
613
|
+
private rejectAllProtocolV2Frames(error: Error) {
|
|
614
|
+
this.v2FrameQueues.clear();
|
|
615
|
+
for (const framePromise of this.v2FramePromises.values()) {
|
|
616
|
+
framePromise.reject(error);
|
|
617
|
+
}
|
|
618
|
+
this.v2FramePromises.clear();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private resetProtocolV2Frames(uuid: string) {
|
|
622
|
+
this.v2FrameQueues.delete(uuid);
|
|
623
|
+
this.v2FramePromises.delete(uuid);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private isActiveProtocolV2Call(uuid: string, token: number) {
|
|
627
|
+
return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async readProtocolV2Frame(uuid: string) {
|
|
631
|
+
const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
|
|
632
|
+
if (queuedFrame) {
|
|
633
|
+
return queuedFrame;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const framePromise = createDeferred<Uint8Array>();
|
|
637
|
+
this.v2FramePromises.set(uuid, framePromise);
|
|
638
|
+
try {
|
|
639
|
+
return await framePromise.promise;
|
|
640
|
+
} finally {
|
|
641
|
+
if (this.v2FramePromises.get(uuid) === framePromise) {
|
|
642
|
+
this.v2FramePromises.delete(uuid);
|
|
278
643
|
}
|
|
279
644
|
}
|
|
280
645
|
}
|
|
281
646
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
647
|
+
private handleProtocolV1Notification(deviceId: string, hexData: string): void {
|
|
648
|
+
const result = this.processProtocolV1Notification(deviceId, hexData);
|
|
649
|
+
|
|
650
|
+
if (result.error) {
|
|
651
|
+
this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error);
|
|
652
|
+
if (this.runPromise) {
|
|
653
|
+
this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
285
656
|
}
|
|
286
657
|
|
|
287
|
-
|
|
658
|
+
if (result.isComplete && result.completePacket && this.runPromise) {
|
|
659
|
+
this.runPromise.resolve(result.completePacket);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
288
662
|
|
|
289
|
-
|
|
290
|
-
|
|
663
|
+
async call(
|
|
664
|
+
uuid: string,
|
|
665
|
+
name: string,
|
|
666
|
+
data: Record<string, unknown>,
|
|
667
|
+
options?: TransportCallOptions
|
|
668
|
+
) {
|
|
669
|
+
if (!this._messages) {
|
|
670
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
291
671
|
}
|
|
292
672
|
|
|
293
673
|
if (!this.connectedDevices.has(uuid)) {
|
|
294
674
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound, `Device ${uuid} not connected`);
|
|
295
675
|
}
|
|
296
676
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} else if (
|
|
307
|
-
this.Log?.debug('[
|
|
677
|
+
const protocol = this.deviceProtocol.get(uuid);
|
|
678
|
+
if (!protocol) {
|
|
679
|
+
throw ERRORS.TypedError(
|
|
680
|
+
HardwareErrorCode.RuntimeError,
|
|
681
|
+
`Device protocol has not been detected for ${uuid}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (shouldSuppressHighVolumeCallLog(name)) {
|
|
685
|
+
// 高频文件写入不要逐包发 debug 事件,否则调试日志会反向拖慢传输。
|
|
686
|
+
} else if (isLogBlockCommand(name)) {
|
|
687
|
+
this.Log?.debug('[Electron BLE] call', 'name:', name, 'protocol:', protocol);
|
|
308
688
|
} else {
|
|
309
|
-
this.Log?.debug('[
|
|
689
|
+
this.Log?.debug('[Electron BLE] call', 'name:', name, 'data:', data, 'protocol:', protocol);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (protocol === 'V2') {
|
|
693
|
+
return this.callProtocolV2(uuid, name, data, options);
|
|
310
694
|
}
|
|
695
|
+
return this.callProtocolV1(uuid, name, data, options);
|
|
696
|
+
}
|
|
311
697
|
|
|
312
|
-
|
|
698
|
+
private async callProtocolV1(
|
|
699
|
+
uuid: string,
|
|
700
|
+
name: string,
|
|
701
|
+
data: Record<string, unknown>,
|
|
702
|
+
options?: TransportCallOptions
|
|
703
|
+
) {
|
|
704
|
+
if (!this._messages) {
|
|
705
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
709
|
+
if (this.runPromise && !forceRun) {
|
|
710
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const runPromise = createDeferred<Uint8Array | string>();
|
|
714
|
+
runPromise.promise.catch(() => undefined);
|
|
715
|
+
this.runPromise = runPromise;
|
|
716
|
+
const messages = this._messages;
|
|
717
|
+
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
718
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
313
719
|
|
|
314
720
|
try {
|
|
315
721
|
if (!window.desktopApi?.nobleBle) {
|
|
316
722
|
throw new Error('Noble BLE write API not available');
|
|
317
723
|
}
|
|
318
724
|
|
|
319
|
-
// Write each buffer to the device
|
|
320
725
|
for (let i = 0; i < buffers.length; i++) {
|
|
321
726
|
const buffer = buffers[i];
|
|
322
|
-
|
|
323
727
|
if (!buffer || typeof buffer.toString !== 'function') {
|
|
324
|
-
this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} is invalid:`, buffer);
|
|
325
728
|
throw new Error(`Buffer ${i + 1} is invalid`);
|
|
326
729
|
}
|
|
327
|
-
|
|
328
|
-
// Use ByteBuffer's toString('hex') method directly, similar to other transports
|
|
329
730
|
const hexString = buffer.toString('hex');
|
|
330
|
-
|
|
331
731
|
if (hexString.length === 0) {
|
|
332
|
-
this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} generated empty hex string`);
|
|
333
732
|
throw new Error(`Buffer ${i + 1} is empty`);
|
|
334
733
|
}
|
|
335
|
-
|
|
336
734
|
await window.desktopApi.nobleBle.write(uuid, hexString);
|
|
337
735
|
}
|
|
338
736
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
737
|
+
const response = await Promise.race([
|
|
738
|
+
runPromise.promise,
|
|
739
|
+
new Promise<never>((_, reject) => {
|
|
740
|
+
if (options?.timeoutMs) {
|
|
741
|
+
timeout = setTimeout(() => {
|
|
742
|
+
const error = ERRORS.TypedError(
|
|
743
|
+
HardwareErrorCode.BleTimeoutError,
|
|
744
|
+
`BLE response timeout after ${options.timeoutMs}ms for ${name}`
|
|
745
|
+
);
|
|
746
|
+
runPromise.reject(error);
|
|
747
|
+
reject(error);
|
|
748
|
+
}, options.timeoutMs);
|
|
749
|
+
}
|
|
750
|
+
}),
|
|
751
|
+
]);
|
|
342
752
|
if (typeof response !== 'string') {
|
|
343
753
|
throw new Error('Returning data is not string.');
|
|
344
754
|
}
|
|
345
755
|
|
|
346
|
-
const jsonData =
|
|
756
|
+
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
347
757
|
return check.call(jsonData);
|
|
348
758
|
} catch (e) {
|
|
349
|
-
this.Log?.error('[
|
|
759
|
+
this.Log?.error('[Electron BLE] Protocol V1 call error:', e);
|
|
350
760
|
throw e;
|
|
351
761
|
} finally {
|
|
762
|
+
if (timeout) clearTimeout(timeout);
|
|
763
|
+
if (this.runPromise === runPromise) {
|
|
764
|
+
this.runPromise = null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private async callProtocolV2(
|
|
770
|
+
uuid: string,
|
|
771
|
+
name: string,
|
|
772
|
+
data: Record<string, unknown>,
|
|
773
|
+
options?: TransportCallOptions
|
|
774
|
+
) {
|
|
775
|
+
if (!this._messages || !this._messagesV2) {
|
|
776
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
|
|
780
|
+
if (this.runPromise) {
|
|
781
|
+
if (!forceRun) {
|
|
782
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
783
|
+
}
|
|
784
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
785
|
+
this.runPromise.reject(error);
|
|
786
|
+
this.rejectAllProtocolV2Frames(error);
|
|
352
787
|
this.runPromise = null;
|
|
788
|
+
this.activeProtocolV2Call = null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const runPromise = createDeferred<Uint8Array | string>();
|
|
792
|
+
runPromise.promise.catch(() => undefined);
|
|
793
|
+
this.runPromise = runPromise;
|
|
794
|
+
const callToken = this.nextProtocolV2CallToken++;
|
|
795
|
+
this.activeProtocolV2Call = { uuid, token: callToken };
|
|
796
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
797
|
+
this.resetProtocolV2Frames(uuid);
|
|
798
|
+
let completed = false;
|
|
799
|
+
const callOptions = {
|
|
800
|
+
...options,
|
|
801
|
+
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const session = new ProtocolV2Session({
|
|
806
|
+
schemas: {
|
|
807
|
+
protocolV1: this._messages,
|
|
808
|
+
protocolV2: this._messagesV2,
|
|
809
|
+
},
|
|
810
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
811
|
+
writeFrame: (frame: Uint8Array) => this.writeWithChunking(uuid, bytesToHex(frame)),
|
|
812
|
+
readFrame: async () => {
|
|
813
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
814
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
815
|
+
throw new Error('Response is not Uint8Array');
|
|
816
|
+
}
|
|
817
|
+
return rxFrame;
|
|
818
|
+
},
|
|
819
|
+
logger: this.Log,
|
|
820
|
+
logPrefix: 'ProtocolV2 BLE',
|
|
821
|
+
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
822
|
+
ERRORS.TypedError(
|
|
823
|
+
HardwareErrorCode.BleTimeoutError,
|
|
824
|
+
`BLE response timeout after ${timeout}ms for ${name}`
|
|
825
|
+
),
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const result = await session.call(name, data, callOptions);
|
|
829
|
+
completed = true;
|
|
830
|
+
return result;
|
|
831
|
+
} catch (e) {
|
|
832
|
+
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
833
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
834
|
+
this.resetProtocolV2Frames(uuid);
|
|
835
|
+
}
|
|
836
|
+
this.Log?.error('[Electron BLE] Protocol V2 call error:', e);
|
|
837
|
+
throw e;
|
|
838
|
+
} finally {
|
|
839
|
+
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
840
|
+
if (!completed) {
|
|
841
|
+
this.v2Assemblers.get(uuid)?.reset();
|
|
842
|
+
}
|
|
843
|
+
this.resetProtocolV2Frames(uuid);
|
|
844
|
+
this.activeProtocolV2Call = null;
|
|
845
|
+
}
|
|
846
|
+
if (this.runPromise === runPromise) {
|
|
847
|
+
this.runPromise = null;
|
|
848
|
+
}
|
|
353
849
|
}
|
|
354
850
|
}
|
|
355
851
|
|
|
356
|
-
|
|
357
|
-
private processNotificationPacket(deviceId: string, hexData: string): PacketProcessResult {
|
|
852
|
+
private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult {
|
|
358
853
|
try {
|
|
359
|
-
// Validate input
|
|
360
854
|
if (typeof hexData !== 'string') {
|
|
361
855
|
return { isComplete: false, error: 'Invalid hexData type' };
|
|
362
856
|
}
|
|
363
857
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return { isComplete: false, error: 'Invalid hex data format' };
|
|
858
|
+
const data = hexToBytes(hexData);
|
|
859
|
+
if (data.length === 0) {
|
|
860
|
+
return { isComplete: false, error: 'Empty or invalid hex data' };
|
|
368
861
|
}
|
|
369
862
|
|
|
370
|
-
|
|
371
|
-
const hexMatch = cleanHexData.match(/.{1,2}/g);
|
|
372
|
-
if (!hexMatch) {
|
|
373
|
-
return { isComplete: false, error: 'Failed to parse hex data' };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const data = new Uint8Array(hexMatch.map(byte => parseInt(byte, 16)));
|
|
377
|
-
|
|
378
|
-
// Get buffer state
|
|
379
|
-
const bufferState = this.dataBuffers.get(deviceId);
|
|
863
|
+
const bufferState = this.v1Buffers.get(deviceId);
|
|
380
864
|
if (!bufferState) {
|
|
381
865
|
return { isComplete: false, error: 'No buffer state for device' };
|
|
382
866
|
}
|
|
383
867
|
|
|
384
|
-
// Process header or data chunk
|
|
385
868
|
if (isHeaderChunk(data)) {
|
|
386
869
|
const dataView = new DataView(data.buffer);
|
|
387
870
|
bufferState.bufferLength = dataView.getInt32(5, false);
|
|
@@ -390,20 +873,12 @@ export default class ElectronBleTransport {
|
|
|
390
873
|
bufferState.buffer = bufferState.buffer.concat([...data]);
|
|
391
874
|
}
|
|
392
875
|
|
|
393
|
-
|
|
394
|
-
if (bufferState.buffer.length - COMMON_HEADER_SIZE >= bufferState.bufferLength) {
|
|
876
|
+
if (bufferState.buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) {
|
|
395
877
|
const completeBuffer = new Uint8Array(bufferState.buffer);
|
|
396
|
-
|
|
397
|
-
// Reset buffer state
|
|
398
878
|
bufferState.bufferLength = 0;
|
|
399
879
|
bufferState.buffer = [];
|
|
400
880
|
|
|
401
|
-
|
|
402
|
-
const hexString = Array.from(completeBuffer)
|
|
403
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
404
|
-
.join('');
|
|
405
|
-
|
|
406
|
-
return { isComplete: true, completePacket: hexString };
|
|
881
|
+
return { isComplete: true, completePacket: bytesToHex(completeBuffer) };
|
|
407
882
|
}
|
|
408
883
|
|
|
409
884
|
return { isComplete: false };
|
|
@@ -411,4 +886,8 @@ export default class ElectronBleTransport {
|
|
|
411
886
|
return { isComplete: false, error: `Packet processing error: ${error}` };
|
|
412
887
|
}
|
|
413
888
|
}
|
|
889
|
+
|
|
890
|
+
getProtocolType(path: string): ProtocolType | undefined {
|
|
891
|
+
return this.deviceProtocol.get(path);
|
|
892
|
+
}
|
|
414
893
|
}
|