@onekeyfe/hd-transport-react-native 1.2.0-alpha.1 → 1.2.0-alpha.100
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/dist/BleTransport.d.ts +2 -4
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +7 -2
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +117 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +866 -421
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +7 -6
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStrategy.test.ts +89 -6
- package/src/__tests__/connectTimeout.test.ts +292 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +949 -0
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +26 -36
- package/src/constants.ts +9 -14
- package/src/index.ts +1109 -428
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +1 -0
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -5,33 +5,46 @@ import {
|
|
|
5
5
|
BleError,
|
|
6
6
|
BleErrorCode,
|
|
7
7
|
BleManager as BlePlxManager,
|
|
8
|
+
ConnectionPriority,
|
|
8
9
|
ScanMode,
|
|
9
10
|
} from 'react-native-ble-plx';
|
|
10
11
|
import ByteBuffer from 'bytebuffer';
|
|
11
12
|
import transport, {
|
|
12
|
-
LogBlockCommand,
|
|
13
13
|
type OneKeyDeviceInfoBase,
|
|
14
14
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
15
16
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
16
17
|
type ProtocolType,
|
|
18
|
+
type ProtocolV2CallContext,
|
|
17
19
|
ProtocolV2FrameAssembler,
|
|
18
|
-
|
|
20
|
+
ProtocolV2LinkManager,
|
|
21
|
+
TRANSPORT_EVENT,
|
|
19
22
|
type TransportCallOptions,
|
|
23
|
+
isProtocolV2HighThroughputCall,
|
|
20
24
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
25
|
+
writeProtocolV2BleFrame,
|
|
21
26
|
} from '@onekeyfe/hd-transport';
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ERRORS,
|
|
29
|
+
HardwareErrorCode,
|
|
30
|
+
createDeferred,
|
|
31
|
+
isOnekeyBluetoothDevice,
|
|
32
|
+
isPro2FindMyAdvertisementName,
|
|
33
|
+
} from '@onekeyfe/hd-shared';
|
|
23
34
|
|
|
24
35
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
25
36
|
import {
|
|
26
37
|
hasWritableCapability,
|
|
27
|
-
resolveBleWriteMode,
|
|
28
38
|
resolveProtocolV2PacketCapacity,
|
|
39
|
+
shouldRefreshNegotiatedMtu,
|
|
40
|
+
shouldWriteProtocolV2WithResponse,
|
|
29
41
|
} from './bleStrategy';
|
|
30
42
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
31
43
|
import {
|
|
32
44
|
ANDROID_PACKET_LENGTH,
|
|
45
|
+
ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
33
46
|
IOS_PACKET_LENGTH,
|
|
34
|
-
|
|
47
|
+
IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
35
48
|
getBluetoothServiceUuids,
|
|
36
49
|
getInfosForServiceUuid,
|
|
37
50
|
isSameBleUuid,
|
|
@@ -40,6 +53,7 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
40
53
|
import BleTransport from './BleTransport';
|
|
41
54
|
import timer from './utils/timer';
|
|
42
55
|
import { bleLogger, setBleLogger } from './logger';
|
|
56
|
+
import { createTransportCallLog } from './transportLog';
|
|
43
57
|
|
|
44
58
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
45
59
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -55,24 +69,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
55
69
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
56
70
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
57
71
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
58
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
59
72
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
60
73
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
61
74
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
62
75
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
63
76
|
|
|
64
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
77
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
65
78
|
type ResolvedBleCharacteristics = {
|
|
66
79
|
writeCharacteristic: Characteristic;
|
|
67
80
|
notifyCharacteristic: Characteristic;
|
|
68
81
|
};
|
|
69
82
|
|
|
83
|
+
const isAsciiWhitespace = (code: number) =>
|
|
84
|
+
code === 0x09 ||
|
|
85
|
+
code === 0x0a ||
|
|
86
|
+
code === 0x0b ||
|
|
87
|
+
code === 0x0c ||
|
|
88
|
+
code === 0x0d ||
|
|
89
|
+
code === 0x20;
|
|
90
|
+
|
|
91
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
92
|
+
let searchFrom = 0;
|
|
93
|
+
while (searchFrom < text.length) {
|
|
94
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
95
|
+
if (statusIndex < 0) return false;
|
|
96
|
+
|
|
97
|
+
let cursor = statusIndex + 'status'.length;
|
|
98
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
99
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
100
|
+
cursor += 1;
|
|
101
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
102
|
+
}
|
|
103
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
104
|
+
|
|
105
|
+
searchFrom = statusIndex + 'status'.length;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
|
|
70
110
|
const delay = (ms: number) =>
|
|
71
111
|
new Promise<void>(resolve => {
|
|
72
112
|
setTimeout(resolve, ms);
|
|
73
113
|
});
|
|
74
114
|
|
|
75
|
-
const getFirmwareUploadWriteRetryType = (
|
|
115
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
116
|
+
error: unknown
|
|
117
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
76
118
|
if (!error || typeof error !== 'object') return null;
|
|
77
119
|
const bleWriteError = error as {
|
|
78
120
|
androidErrorCode?: unknown;
|
|
@@ -83,13 +125,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
83
125
|
name?: unknown;
|
|
84
126
|
};
|
|
85
127
|
|
|
86
|
-
if (
|
|
87
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
88
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
89
|
-
) {
|
|
90
|
-
return 'reconnectable';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
128
|
if (
|
|
94
129
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
95
130
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -100,39 +135,42 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
100
135
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
101
136
|
.filter(value => typeof value === 'string')
|
|
102
137
|
.join(' ');
|
|
103
|
-
return
|
|
138
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
104
139
|
};
|
|
105
140
|
|
|
106
141
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
107
142
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
108
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
109
143
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
110
144
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
111
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
147
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
148
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
149
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
150
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
151
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
152
|
+
*/
|
|
153
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
154
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
155
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
156
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
157
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
158
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
159
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
160
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
161
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
112
162
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
113
163
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
114
|
-
const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
|
|
115
|
-
const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
|
|
116
|
-
const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
|
|
117
|
-
|
|
118
164
|
export type ProtocolV2BleTuning = {
|
|
119
165
|
iosPacketLength?: number;
|
|
120
166
|
androidPacketLength?: number;
|
|
121
|
-
highVolumeWriteBurstSize?: number;
|
|
122
|
-
highVolumeWritePauseMs?: number;
|
|
123
|
-
highVolumeWriteFlushDelayMs?: number;
|
|
124
|
-
highVolumeWriteWithResponse?: boolean;
|
|
125
167
|
};
|
|
126
168
|
|
|
127
169
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
128
170
|
|
|
129
171
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
130
|
-
iosPacketLength:
|
|
131
|
-
androidPacketLength:
|
|
132
|
-
highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
|
|
133
|
-
highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
|
|
134
|
-
highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
|
|
135
|
-
highVolumeWriteWithResponse: false,
|
|
172
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
173
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
136
174
|
};
|
|
137
175
|
|
|
138
176
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -153,27 +191,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
|
153
191
|
tuning.androidPacketLength,
|
|
154
192
|
protocolV2BleTuning.androidPacketLength
|
|
155
193
|
),
|
|
156
|
-
highVolumeWriteBurstSize: normalizePositiveInteger(
|
|
157
|
-
tuning.highVolumeWriteBurstSize,
|
|
158
|
-
protocolV2BleTuning.highVolumeWriteBurstSize
|
|
159
|
-
),
|
|
160
|
-
highVolumeWritePauseMs: normalizePositiveInteger(
|
|
161
|
-
tuning.highVolumeWritePauseMs,
|
|
162
|
-
protocolV2BleTuning.highVolumeWritePauseMs
|
|
163
|
-
),
|
|
164
|
-
highVolumeWriteFlushDelayMs: normalizePositiveInteger(
|
|
165
|
-
tuning.highVolumeWriteFlushDelayMs,
|
|
166
|
-
protocolV2BleTuning.highVolumeWriteFlushDelayMs
|
|
167
|
-
),
|
|
168
|
-
highVolumeWriteWithResponse:
|
|
169
|
-
tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
|
|
170
194
|
};
|
|
171
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
195
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
export function resetProtocolV2BleTuning() {
|
|
175
199
|
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
176
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
200
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
export function getProtocolV2BleTuning() {
|
|
@@ -188,24 +212,60 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
188
212
|
return device?.name || device?.localName || null;
|
|
189
213
|
}
|
|
190
214
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
const IOS_REQUEST_MTU = 247;
|
|
216
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
217
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
218
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
198
|
-
);
|
|
199
|
-
}
|
|
220
|
+
const getRequestedBleMtu = () =>
|
|
221
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
200
222
|
|
|
201
|
-
const
|
|
223
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
202
224
|
|
|
203
225
|
const connectOptions: Record<string, unknown> = {
|
|
204
|
-
requestMTU:
|
|
205
|
-
timeout:
|
|
226
|
+
requestMTU: getRequestedBleMtu(),
|
|
227
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
206
228
|
refreshGatt: 'OnConnected',
|
|
207
229
|
};
|
|
208
230
|
|
|
231
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
232
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
233
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
238
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
239
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
240
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
241
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
242
|
+
*/
|
|
243
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
244
|
+
/**
|
|
245
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
246
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
247
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
248
|
+
* budget.
|
|
249
|
+
*/
|
|
250
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
251
|
+
/**
|
|
252
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
253
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
254
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
255
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
256
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
257
|
+
*/
|
|
258
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
259
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
260
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
261
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
262
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
263
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
264
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
265
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
266
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
267
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
268
|
+
|
|
209
269
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
210
270
|
|
|
211
271
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -217,22 +277,32 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
217
277
|
return infos;
|
|
218
278
|
};
|
|
219
279
|
|
|
220
|
-
const
|
|
221
|
-
|
|
280
|
+
const requestNegotiatedMtu = async (
|
|
281
|
+
device: Device,
|
|
282
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
283
|
+
attempt: number
|
|
284
|
+
) => {
|
|
285
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
222
286
|
|
|
223
287
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
mtu: mtuDevice.mtu,
|
|
228
|
-
});
|
|
288
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
289
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
290
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
229
291
|
return mtuDevice;
|
|
230
292
|
} catch (error) {
|
|
231
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
293
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
294
|
+
platform: Platform.OS,
|
|
295
|
+
stage,
|
|
296
|
+
attempt,
|
|
297
|
+
actual: device.mtu,
|
|
298
|
+
error: error instanceof Error ? error.message : String(error),
|
|
299
|
+
});
|
|
232
300
|
return device;
|
|
233
301
|
}
|
|
234
302
|
};
|
|
235
303
|
|
|
304
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
305
|
+
|
|
236
306
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
237
307
|
|
|
238
308
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -272,6 +342,8 @@ export default class ReactNativeBleTransport {
|
|
|
272
342
|
|
|
273
343
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
274
344
|
|
|
345
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
346
|
+
|
|
275
347
|
name = 'ReactNativeBleTransport';
|
|
276
348
|
|
|
277
349
|
configured = false;
|
|
@@ -282,6 +354,8 @@ export default class ReactNativeBleTransport {
|
|
|
282
354
|
|
|
283
355
|
runPromise: Deferred<any> | null = null;
|
|
284
356
|
|
|
357
|
+
private runPromiseDeviceId: string | null = null;
|
|
358
|
+
|
|
285
359
|
emitter?: EventEmitter;
|
|
286
360
|
|
|
287
361
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -289,20 +363,65 @@ export default class ReactNativeBleTransport {
|
|
|
289
363
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
290
364
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
291
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
368
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
369
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
370
|
+
* transport that was never validated.
|
|
371
|
+
*/
|
|
372
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
373
|
+
|
|
374
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
375
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
376
|
+
|
|
377
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
378
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
379
|
+
|
|
292
380
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
293
381
|
|
|
382
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
383
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
384
|
+
|
|
385
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
386
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
387
|
+
|
|
294
388
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
295
389
|
|
|
296
390
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
297
391
|
|
|
298
392
|
private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
299
393
|
|
|
300
|
-
private
|
|
301
|
-
|
|
302
|
-
|
|
394
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
395
|
+
getSchemas: () => {
|
|
396
|
+
if (!this._messages || !this._messagesV2) {
|
|
397
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
protocolV1: this._messages,
|
|
401
|
+
protocolV2: this._messagesV2,
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
classifyError: () => 'link-fatal',
|
|
405
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
406
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
407
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
408
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
409
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
410
|
+
await this.releaseNative(uuid, true);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
});
|
|
303
414
|
|
|
304
415
|
private monitorTokens: Map<string, number> = new Map();
|
|
305
416
|
|
|
417
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
418
|
+
|
|
419
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
420
|
+
|
|
421
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
422
|
+
|
|
423
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
424
|
+
|
|
306
425
|
private nextMonitorToken = 1;
|
|
307
426
|
|
|
308
427
|
constructor(options: TransportOptions) {
|
|
@@ -321,8 +440,19 @@ export default class ReactNativeBleTransport {
|
|
|
321
440
|
}
|
|
322
441
|
|
|
323
442
|
configureProtocolV2(signedData: any) {
|
|
443
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
444
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
324
449
|
this._messagesV2 = parseConfigure(signedData);
|
|
325
|
-
|
|
450
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
451
|
+
if (isReconfiguration) {
|
|
452
|
+
this.protocolV2Links
|
|
453
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
454
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
455
|
+
}
|
|
326
456
|
}
|
|
327
457
|
|
|
328
458
|
listen() {
|
|
@@ -352,29 +482,15 @@ export default class ReactNativeBleTransport {
|
|
|
352
482
|
}
|
|
353
483
|
}
|
|
354
484
|
|
|
355
|
-
let fallbackServiceUuid: string | undefined;
|
|
356
|
-
|
|
357
485
|
if (!infos) {
|
|
358
486
|
const services = await device.services();
|
|
359
487
|
Log?.debug(
|
|
360
488
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
361
489
|
services?.map(service => service.uuid)
|
|
362
490
|
);
|
|
363
|
-
|
|
364
|
-
const knownService = services.find(service =>
|
|
365
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
366
|
-
);
|
|
367
|
-
const fallbackService =
|
|
368
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
369
|
-
|
|
370
|
-
if (fallbackService) {
|
|
371
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
372
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
373
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
374
|
-
}
|
|
375
491
|
}
|
|
376
492
|
|
|
377
|
-
if (!infos
|
|
493
|
+
if (!infos) {
|
|
378
494
|
try {
|
|
379
495
|
Log?.debug('cancel connection when service not found');
|
|
380
496
|
await device.cancelConnection();
|
|
@@ -384,9 +500,7 @@ export default class ReactNativeBleTransport {
|
|
|
384
500
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
385
501
|
}
|
|
386
502
|
|
|
387
|
-
const serviceUuid = infos
|
|
388
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
389
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
503
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
390
504
|
|
|
391
505
|
if (!serviceUuid) {
|
|
392
506
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -436,6 +550,7 @@ export default class ReactNativeBleTransport {
|
|
|
436
550
|
|
|
437
551
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
438
552
|
transport.disconnectSubscription?.remove();
|
|
553
|
+
const { monitorToken } = transport;
|
|
439
554
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
440
555
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
441
556
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -445,18 +560,17 @@ export default class ReactNativeBleTransport {
|
|
|
445
560
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
446
561
|
return;
|
|
447
562
|
}
|
|
563
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
564
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
448
567
|
|
|
449
568
|
try {
|
|
450
569
|
Log?.debug('device disconnect: ', device?.id);
|
|
451
|
-
this.
|
|
452
|
-
|
|
453
|
-
id: device?.id,
|
|
454
|
-
connectId: device?.id,
|
|
455
|
-
});
|
|
456
|
-
if (this.runPromise) {
|
|
570
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
571
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
457
572
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
458
573
|
this.runPromise.reject(error);
|
|
459
|
-
this.rejectAllProtocolV2Frames(error);
|
|
460
574
|
}
|
|
461
575
|
} catch (e) {
|
|
462
576
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -466,6 +580,22 @@ export default class ReactNativeBleTransport {
|
|
|
466
580
|
});
|
|
467
581
|
}
|
|
468
582
|
|
|
583
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
584
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
588
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
592
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
593
|
+
name,
|
|
594
|
+
id: uuid,
|
|
595
|
+
connectId: uuid,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
469
599
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
470
600
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
471
601
|
try {
|
|
@@ -478,22 +608,21 @@ export default class ReactNativeBleTransport {
|
|
|
478
608
|
const isConnected = await device.isConnected().catch(() => false);
|
|
479
609
|
if (!isConnected) {
|
|
480
610
|
try {
|
|
481
|
-
device = await device.connect(connectOptions);
|
|
611
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
482
612
|
} catch (e) {
|
|
483
613
|
if (
|
|
484
614
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
485
615
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
486
616
|
) {
|
|
487
|
-
device = await device.connect();
|
|
617
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
488
618
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
489
619
|
throw e;
|
|
490
620
|
}
|
|
491
621
|
}
|
|
492
622
|
}
|
|
493
623
|
|
|
494
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
495
|
-
device
|
|
496
|
-
);
|
|
624
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
625
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
497
626
|
|
|
498
627
|
transport.device = device;
|
|
499
628
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -553,14 +682,13 @@ export default class ReactNativeBleTransport {
|
|
|
553
682
|
}
|
|
554
683
|
|
|
555
684
|
blePlxManager.startDeviceScan(
|
|
556
|
-
|
|
685
|
+
getBluetoothServiceUuids(),
|
|
557
686
|
{
|
|
558
687
|
allowDuplicates: true,
|
|
559
688
|
scanMode: ScanMode.LowLatency,
|
|
560
689
|
},
|
|
561
690
|
(error, device) => {
|
|
562
691
|
if (error) {
|
|
563
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
564
692
|
Log?.debug('ble scan error: ', error);
|
|
565
693
|
if (
|
|
566
694
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -584,33 +712,23 @@ export default class ReactNativeBleTransport {
|
|
|
584
712
|
}
|
|
585
713
|
|
|
586
714
|
const displayName = getDeviceDisplayName(device);
|
|
715
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
716
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
717
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
718
|
+
const isFindMyPeripheral =
|
|
719
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
720
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
587
721
|
const isOneKey =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
!!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
|
|
593
|
-
|
|
594
|
-
if (shouldTraceCandidate) {
|
|
595
|
-
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
722
|
+
!isUnnamedIOSPeripheral &&
|
|
723
|
+
!isFindMyPeripheral &&
|
|
724
|
+
isOnekeyBluetoothDevice({
|
|
725
|
+
id: device?.id,
|
|
596
726
|
name: device?.name,
|
|
597
727
|
localName: device?.localName,
|
|
598
|
-
|
|
599
|
-
serviceUUIDs: device?.serviceUUIDs,
|
|
600
|
-
accepted: isOneKey,
|
|
728
|
+
serviceUuids: device?.serviceUUIDs,
|
|
601
729
|
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
730
|
if (isOneKey) {
|
|
605
|
-
Log?.debug('search device start ======================');
|
|
606
|
-
const { name, localName, id, serviceUUIDs } = device ?? {};
|
|
607
|
-
Log?.debug(
|
|
608
|
-
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
609
|
-
id ?? ''
|
|
610
|
-
}\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
|
|
611
|
-
);
|
|
612
731
|
addDevice(device as unknown as Device);
|
|
613
|
-
Log?.debug('search device end ======================\n');
|
|
614
732
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
615
733
|
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
616
734
|
name: device?.name,
|
|
@@ -622,12 +740,32 @@ export default class ReactNativeBleTransport {
|
|
|
622
740
|
}
|
|
623
741
|
);
|
|
624
742
|
|
|
625
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
743
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
744
|
+
devices => {
|
|
745
|
+
for (const device of devices) {
|
|
746
|
+
const localName =
|
|
747
|
+
'localName' in device && typeof device.localName === 'string'
|
|
748
|
+
? device.localName
|
|
749
|
+
: null;
|
|
750
|
+
const isFindMyPeripheral =
|
|
751
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
752
|
+
isPro2FindMyAdvertisementName(localName);
|
|
753
|
+
|
|
754
|
+
if (
|
|
755
|
+
!isFindMyPeripheral &&
|
|
756
|
+
isOnekeyBluetoothDevice({
|
|
757
|
+
id: device.id,
|
|
758
|
+
name: device.name,
|
|
759
|
+
localName,
|
|
760
|
+
serviceUuids: device.serviceUUIDs,
|
|
761
|
+
})
|
|
762
|
+
) {
|
|
763
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
764
|
+
addDevice(device as unknown as Device);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
629
767
|
}
|
|
630
|
-
|
|
768
|
+
);
|
|
631
769
|
|
|
632
770
|
const addDevice = (device: Device) => {
|
|
633
771
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -641,6 +779,12 @@ export default class ReactNativeBleTransport {
|
|
|
641
779
|
name: displayName,
|
|
642
780
|
commType: 'ble',
|
|
643
781
|
} as IOneKeyDevice);
|
|
782
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
783
|
+
deviceId: device.id,
|
|
784
|
+
name: displayName,
|
|
785
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
786
|
+
protocolHint,
|
|
787
|
+
});
|
|
644
788
|
}
|
|
645
789
|
};
|
|
646
790
|
|
|
@@ -651,6 +795,79 @@ export default class ReactNativeBleTransport {
|
|
|
651
795
|
});
|
|
652
796
|
}
|
|
653
797
|
|
|
798
|
+
private async installTransportForAcquire(
|
|
799
|
+
uuid: string,
|
|
800
|
+
device: Device,
|
|
801
|
+
characteristics?: ResolvedBleCharacteristics
|
|
802
|
+
) {
|
|
803
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
804
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
805
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
806
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
807
|
+
const monitorToken = this.nextMonitorToken;
|
|
808
|
+
this.nextMonitorToken += 1;
|
|
809
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
810
|
+
transport.monitorToken = monitorToken;
|
|
811
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
812
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
813
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
814
|
+
transport.notifyCharacteristic,
|
|
815
|
+
uuid,
|
|
816
|
+
monitorToken,
|
|
817
|
+
notifyTransactionId
|
|
818
|
+
);
|
|
819
|
+
transportCache[uuid] = transport;
|
|
820
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
821
|
+
this.protocolV2Assemblers.set(
|
|
822
|
+
uuid,
|
|
823
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (Platform.OS === 'ios') {
|
|
827
|
+
await new Promise<void>(resolve => {
|
|
828
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
829
|
+
});
|
|
830
|
+
} else if (Platform.OS === 'android') {
|
|
831
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const initialMtu = transport.mtuSize;
|
|
835
|
+
let refreshAttempts = 0;
|
|
836
|
+
if (
|
|
837
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
838
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
839
|
+
) {
|
|
840
|
+
refreshAttempts += 1;
|
|
841
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
842
|
+
transport.device,
|
|
843
|
+
'servicesAndNotifyReady',
|
|
844
|
+
1
|
|
845
|
+
);
|
|
846
|
+
transport.device = refreshedDevice;
|
|
847
|
+
transport.mtuSize =
|
|
848
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
849
|
+
|
|
850
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
851
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
852
|
+
refreshAttempts += 1;
|
|
853
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
854
|
+
transport.device = refreshedDevice;
|
|
855
|
+
transport.mtuSize =
|
|
856
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
861
|
+
platform: Platform.OS,
|
|
862
|
+
requested: getRequestedBleMtu(),
|
|
863
|
+
initial: initialMtu,
|
|
864
|
+
actual: transport.mtuSize,
|
|
865
|
+
refreshAttempts,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
return transport;
|
|
869
|
+
}
|
|
870
|
+
|
|
654
871
|
async acquire(input: BleAcquireInput) {
|
|
655
872
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
656
873
|
|
|
@@ -684,9 +901,8 @@ export default class ReactNativeBleTransport {
|
|
|
684
901
|
if (forceCleanRunPromise && this.runPromise) {
|
|
685
902
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
686
903
|
this.runPromise.reject(error);
|
|
687
|
-
this.rejectAllProtocolV2Frames(error);
|
|
688
904
|
this.runPromise = null;
|
|
689
|
-
this.
|
|
905
|
+
this.runPromiseDeviceId = null;
|
|
690
906
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
691
907
|
}
|
|
692
908
|
|
|
@@ -722,15 +938,22 @@ export default class ReactNativeBleTransport {
|
|
|
722
938
|
if (!device) {
|
|
723
939
|
Log?.debug('try to connect to device: ', uuid);
|
|
724
940
|
try {
|
|
725
|
-
device = await
|
|
941
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
942
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
943
|
+
);
|
|
726
944
|
} catch (e) {
|
|
727
945
|
Log?.debug('try to connect to device has error: ', e);
|
|
946
|
+
if (isConnectTimeoutError(e)) {
|
|
947
|
+
throw e;
|
|
948
|
+
}
|
|
728
949
|
if (
|
|
729
950
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
730
951
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
731
952
|
) {
|
|
732
953
|
Log?.debug('first try to reconnect without params');
|
|
733
|
-
device = await
|
|
954
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
955
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
956
|
+
);
|
|
734
957
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
735
958
|
Log?.debug('device already connected');
|
|
736
959
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -746,26 +969,36 @@ export default class ReactNativeBleTransport {
|
|
|
746
969
|
|
|
747
970
|
if (!(await device.isConnected())) {
|
|
748
971
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
972
|
+
const disconnectedDevice = device;
|
|
749
973
|
|
|
750
974
|
try {
|
|
751
|
-
device = await
|
|
975
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
976
|
+
disconnectedDevice.connect(connectOptions)
|
|
977
|
+
);
|
|
752
978
|
} catch (e) {
|
|
753
979
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
980
|
+
if (isConnectTimeoutError(e)) {
|
|
981
|
+
throw e;
|
|
982
|
+
}
|
|
754
983
|
if (
|
|
755
984
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
756
985
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
757
986
|
) {
|
|
758
987
|
Log?.debug('second try to reconnect without params');
|
|
759
988
|
try {
|
|
760
|
-
device = await
|
|
989
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
990
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
991
|
+
);
|
|
761
992
|
} catch (e) {
|
|
762
993
|
Log?.debug('last try to reconnect error: ', e);
|
|
763
994
|
// last try to reconnect device if this issue exists
|
|
764
995
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
765
996
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
766
997
|
Log?.debug('last try to reconnect');
|
|
767
|
-
await
|
|
768
|
-
device = await
|
|
998
|
+
await disconnectedDevice.cancelConnection();
|
|
999
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1000
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1001
|
+
);
|
|
769
1002
|
}
|
|
770
1003
|
}
|
|
771
1004
|
} else {
|
|
@@ -774,13 +1007,16 @@ export default class ReactNativeBleTransport {
|
|
|
774
1007
|
}
|
|
775
1008
|
}
|
|
776
1009
|
|
|
777
|
-
device = await
|
|
778
|
-
const
|
|
1010
|
+
device = await resolveNegotiatedMtu(device);
|
|
1011
|
+
const acquiredDevice = device;
|
|
1012
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1013
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
779
1014
|
|
|
780
1015
|
const protocolHint = expectedProtocol
|
|
781
1016
|
? undefined
|
|
782
|
-
:
|
|
783
|
-
|
|
1017
|
+
: input.protocolHint ??
|
|
1018
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
1019
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
784
1020
|
|
|
785
1021
|
// release transport before new transport instance
|
|
786
1022
|
await this.release(uuid, true);
|
|
@@ -788,45 +1024,30 @@ export default class ReactNativeBleTransport {
|
|
|
788
1024
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
789
1025
|
}
|
|
790
1026
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
const monitorToken = this.nextMonitorToken;
|
|
796
|
-
this.nextMonitorToken += 1;
|
|
797
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
798
|
-
transport.monitorToken = monitorToken;
|
|
799
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
800
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
801
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
802
|
-
transport.notifyCharacteristic,
|
|
803
|
-
uuid,
|
|
804
|
-
monitorToken,
|
|
805
|
-
notifyTransactionId
|
|
806
|
-
);
|
|
807
|
-
transportCache[uuid] = transport;
|
|
808
|
-
|
|
809
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
810
|
-
|
|
811
|
-
if (Platform.OS === 'ios') {
|
|
812
|
-
await new Promise<void>(resolve => {
|
|
813
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
814
|
-
});
|
|
815
|
-
} else if (Platform.OS === 'android') {
|
|
816
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
820
|
-
|
|
821
|
-
this.emitter?.emit('device-connect', {
|
|
822
|
-
name: device.name,
|
|
823
|
-
id: device.id,
|
|
824
|
-
connectId: device.id,
|
|
1027
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1028
|
+
writeCharacteristic,
|
|
1029
|
+
notifyCharacteristic,
|
|
825
1030
|
});
|
|
826
1031
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1032
|
+
try {
|
|
1033
|
+
const protocolType = await this.detectProtocol(
|
|
1034
|
+
uuid,
|
|
1035
|
+
expectedProtocol,
|
|
1036
|
+
protocolHint,
|
|
1037
|
+
async () => {
|
|
1038
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
const currentTransport = transportCache[uuid];
|
|
1042
|
+
if (!currentTransport) {
|
|
1043
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1044
|
+
}
|
|
1045
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1046
|
+
return { uuid, protocolType };
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
await this.release(uuid, true);
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
830
1051
|
}
|
|
831
1052
|
|
|
832
1053
|
_monitorCharacteristic(
|
|
@@ -853,7 +1074,30 @@ export default class ReactNativeBleTransport {
|
|
|
853
1074
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
854
1075
|
return;
|
|
855
1076
|
}
|
|
856
|
-
if (this.
|
|
1077
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1078
|
+
let errorCode:
|
|
1079
|
+
| typeof HardwareErrorCode.BleDeviceBondError
|
|
1080
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
1081
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
1082
|
+
| typeof HardwareErrorCode.BleTimeoutError =
|
|
1083
|
+
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
1084
|
+
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1085
|
+
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1086
|
+
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
1087
|
+
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
1088
|
+
} else if (
|
|
1089
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1090
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1091
|
+
error.reason?.includes('The handle is invalid') ||
|
|
1092
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
1093
|
+
error.reason?.includes('notify change failed for device')
|
|
1094
|
+
) {
|
|
1095
|
+
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
1096
|
+
}
|
|
1097
|
+
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
857
1101
|
let ERROR:
|
|
858
1102
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
859
1103
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -876,7 +1120,6 @@ export default class ReactNativeBleTransport {
|
|
|
876
1120
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
877
1121
|
);
|
|
878
1122
|
this.runPromise.reject(notifyError);
|
|
879
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
880
1123
|
Log?.debug(
|
|
881
1124
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
882
1125
|
);
|
|
@@ -884,7 +1127,6 @@ export default class ReactNativeBleTransport {
|
|
|
884
1127
|
}
|
|
885
1128
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
886
1129
|
this.runPromise.reject(notifyError);
|
|
887
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
888
1130
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
889
1131
|
}
|
|
890
1132
|
|
|
@@ -902,13 +1144,13 @@ export default class ReactNativeBleTransport {
|
|
|
902
1144
|
|
|
903
1145
|
try {
|
|
904
1146
|
const data = Buffer.from(c.value as string, 'base64');
|
|
905
|
-
const protocol = this.
|
|
1147
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
906
1148
|
if (!protocol) {
|
|
907
1149
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
908
1150
|
return;
|
|
909
1151
|
}
|
|
910
1152
|
if (protocol === 'V2') {
|
|
911
|
-
this.handleProtocolV2Notification(uuid, new Uint8Array(data));
|
|
1153
|
+
this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
|
|
912
1154
|
return;
|
|
913
1155
|
}
|
|
914
1156
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
@@ -929,13 +1171,18 @@ export default class ReactNativeBleTransport {
|
|
|
929
1171
|
// );
|
|
930
1172
|
bufferLength = 0;
|
|
931
1173
|
buffer = [];
|
|
932
|
-
this.
|
|
1174
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1175
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1176
|
+
}
|
|
933
1177
|
}
|
|
934
1178
|
} catch (error) {
|
|
935
1179
|
Log?.debug('monitor data error: ', error);
|
|
936
1180
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
937
|
-
this.
|
|
938
|
-
|
|
1181
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1182
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1183
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
1184
|
+
this.runPromise?.reject(notifyError);
|
|
1185
|
+
}
|
|
939
1186
|
}
|
|
940
1187
|
}, notifyTransactionId);
|
|
941
1188
|
|
|
@@ -943,13 +1190,18 @@ export default class ReactNativeBleTransport {
|
|
|
943
1190
|
}
|
|
944
1191
|
|
|
945
1192
|
async release(uuid: string, onclose = false) {
|
|
1193
|
+
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
1194
|
+
return this.releaseNative(uuid, onclose);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
946
1198
|
const transport = transportCache[uuid];
|
|
947
|
-
if (this.runPromise) {
|
|
1199
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
948
1200
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
949
1201
|
this.runPromise.reject(error);
|
|
950
1202
|
this.runPromise = null;
|
|
951
|
-
this.
|
|
952
|
-
this.
|
|
1203
|
+
this.runPromiseDeviceId = null;
|
|
1204
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
953
1205
|
} else {
|
|
954
1206
|
this.resetProtocolV2Frames(uuid);
|
|
955
1207
|
}
|
|
@@ -957,12 +1209,11 @@ export default class ReactNativeBleTransport {
|
|
|
957
1209
|
if (Platform.OS === 'android' && !onclose && transport) {
|
|
958
1210
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
959
1211
|
this.resetProtocolV2Frames(uuid);
|
|
960
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
961
|
-
this.activeProtocolV2Call = null;
|
|
962
|
-
}
|
|
963
1212
|
return Promise.resolve(true);
|
|
964
1213
|
}
|
|
965
1214
|
|
|
1215
|
+
await this.restoreAndroidConnectionPriority(uuid, transport);
|
|
1216
|
+
|
|
966
1217
|
if (transport) {
|
|
967
1218
|
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
968
1219
|
this.monitorTokens.delete(uuid);
|
|
@@ -992,8 +1243,11 @@ export default class ReactNativeBleTransport {
|
|
|
992
1243
|
delete transportCache[uuid];
|
|
993
1244
|
}
|
|
994
1245
|
|
|
1246
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1247
|
+
|
|
995
1248
|
this.deviceProtocol.delete(uuid);
|
|
996
|
-
this.
|
|
1249
|
+
this.probingProtocols.delete(uuid);
|
|
1250
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
997
1251
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
998
1252
|
this.protocolV2Assemblers.delete(uuid);
|
|
999
1253
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1025,13 +1279,6 @@ export default class ReactNativeBleTransport {
|
|
|
1025
1279
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1026
1280
|
}
|
|
1027
1281
|
|
|
1028
|
-
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1029
|
-
|
|
1030
|
-
Log?.debug('transport-react-native call this.runPromise', this.runPromise);
|
|
1031
|
-
if (this.runPromise && !forceRun) {
|
|
1032
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1282
|
const protocol = this.getProtocolType(uuid);
|
|
1036
1283
|
if (!protocol) {
|
|
1037
1284
|
throw ERRORS.TypedError(
|
|
@@ -1039,31 +1286,17 @@ export default class ReactNativeBleTransport {
|
|
|
1039
1286
|
`Device protocol has not been detected for ${uuid}`
|
|
1040
1287
|
);
|
|
1041
1288
|
}
|
|
1042
|
-
|
|
1043
|
-
if (name === 'ResourceUpdate' || name === 'ResourceAck') {
|
|
1044
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
|
|
1045
|
-
file_name: data?.file_name,
|
|
1046
|
-
hash: data?.hash,
|
|
1047
|
-
});
|
|
1048
|
-
} else if (LogBlockCommand.has(name)) {
|
|
1049
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
1050
|
-
} else {
|
|
1051
|
-
Log?.debug(
|
|
1052
|
-
'transport-react-native',
|
|
1053
|
-
'call-',
|
|
1054
|
-
' name: ',
|
|
1055
|
-
name,
|
|
1056
|
-
' data: ',
|
|
1057
|
-
data,
|
|
1058
|
-
' protocol: ',
|
|
1059
|
-
protocol
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1289
|
+
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1062
1290
|
|
|
1063
1291
|
if (protocol === 'V2') {
|
|
1064
1292
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1065
1293
|
}
|
|
1066
1294
|
|
|
1295
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1296
|
+
if (this.runPromise && !forceRun) {
|
|
1297
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1067
1300
|
return this.callProtocolV1(uuid, name, data, options);
|
|
1068
1301
|
}
|
|
1069
1302
|
|
|
@@ -1080,7 +1313,25 @@ export default class ReactNativeBleTransport {
|
|
|
1080
1313
|
const transport = this.getCachedTransport(uuid);
|
|
1081
1314
|
const runPromise = createDeferred<string>();
|
|
1082
1315
|
runPromise.promise.catch(() => undefined);
|
|
1316
|
+
const supersededRunPromise = this.runPromise;
|
|
1317
|
+
if (supersededRunPromise) {
|
|
1318
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1319
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1320
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1321
|
+
// tear down the shared connection while another call is using it.
|
|
1322
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1323
|
+
}
|
|
1083
1324
|
this.runPromise = runPromise;
|
|
1325
|
+
this.runPromiseDeviceId = uuid;
|
|
1326
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1327
|
+
// only the call that still owns the slot may release it.
|
|
1328
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1329
|
+
if (this.runPromise === runPromise) {
|
|
1330
|
+
this.runPromise = null;
|
|
1331
|
+
this.runPromiseDeviceId = null;
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1084
1335
|
const messages = this._messages;
|
|
1085
1336
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1086
1337
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1106,6 +1357,9 @@ export default class ReactNativeBleTransport {
|
|
|
1106
1357
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1107
1358
|
} catch (e) {
|
|
1108
1359
|
onError(e);
|
|
1360
|
+
if (isWedgedWriteError(e)) {
|
|
1361
|
+
throw e;
|
|
1362
|
+
}
|
|
1109
1363
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1110
1364
|
}
|
|
1111
1365
|
}
|
|
@@ -1137,6 +1391,9 @@ export default class ReactNativeBleTransport {
|
|
|
1137
1391
|
}
|
|
1138
1392
|
} catch (e) {
|
|
1139
1393
|
onError(e);
|
|
1394
|
+
if (isWedgedWriteError(e)) {
|
|
1395
|
+
throw e;
|
|
1396
|
+
}
|
|
1140
1397
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1141
1398
|
}
|
|
1142
1399
|
}
|
|
@@ -1150,21 +1407,26 @@ export default class ReactNativeBleTransport {
|
|
|
1150
1407
|
if (name === 'EmmcFileWrite') {
|
|
1151
1408
|
await writeChunkedData(
|
|
1152
1409
|
buffers,
|
|
1153
|
-
data =>
|
|
1410
|
+
data =>
|
|
1411
|
+
this.writeBlePacket(
|
|
1412
|
+
uuid,
|
|
1413
|
+
data,
|
|
1414
|
+
payload => transport.writeWithRetry(payload),
|
|
1415
|
+
isCurrentOwner
|
|
1416
|
+
),
|
|
1154
1417
|
e => {
|
|
1155
|
-
|
|
1418
|
+
releaseOwnershipIfCurrent();
|
|
1156
1419
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1157
1420
|
}
|
|
1158
1421
|
);
|
|
1159
1422
|
} else if (name === 'FirmwareUpload') {
|
|
1160
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1423
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
1161
1424
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
1162
1425
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1163
1426
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1164
1427
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1165
1428
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
1166
1429
|
});
|
|
1167
|
-
|
|
1168
1430
|
await writeFirmwareUploadChunkedData(
|
|
1169
1431
|
buffers,
|
|
1170
1432
|
async data => {
|
|
@@ -1174,43 +1436,31 @@ export default class ReactNativeBleTransport {
|
|
|
1174
1436
|
// eslint-disable-next-line no-constant-condition
|
|
1175
1437
|
while (true) {
|
|
1176
1438
|
try {
|
|
1177
|
-
await
|
|
1439
|
+
await this.writeBlePacket(
|
|
1440
|
+
uuid,
|
|
1441
|
+
data,
|
|
1442
|
+
payload => transport.writeWithRetry(payload),
|
|
1443
|
+
isCurrentOwner
|
|
1444
|
+
);
|
|
1178
1445
|
return;
|
|
1179
1446
|
} catch (error) {
|
|
1180
1447
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1181
1448
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1182
1449
|
throw error;
|
|
1183
1450
|
}
|
|
1184
|
-
const
|
|
1185
|
-
const delayMs = shouldReconnect
|
|
1186
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1187
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1451
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1188
1452
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1189
1453
|
attempt: attempt + 1,
|
|
1190
1454
|
delayMs,
|
|
1191
|
-
reconnect: shouldReconnect,
|
|
1192
1455
|
error,
|
|
1193
1456
|
});
|
|
1194
|
-
if (shouldReconnect) {
|
|
1195
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1196
|
-
}
|
|
1197
1457
|
await delay(delayMs);
|
|
1198
1458
|
attempt += 1;
|
|
1199
|
-
if (shouldReconnect) {
|
|
1200
|
-
try {
|
|
1201
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1202
|
-
} catch (e) {
|
|
1203
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1204
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1205
|
-
throw e;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
1459
|
}
|
|
1210
1460
|
}
|
|
1211
1461
|
},
|
|
1212
1462
|
e => {
|
|
1213
|
-
|
|
1463
|
+
releaseOwnershipIfCurrent();
|
|
1214
1464
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1215
1465
|
}
|
|
1216
1466
|
);
|
|
@@ -1218,12 +1468,24 @@ export default class ReactNativeBleTransport {
|
|
|
1218
1468
|
for (const o of buffers) {
|
|
1219
1469
|
const outData = o.toString('base64');
|
|
1220
1470
|
// Upload resources on low-end phones may OOM
|
|
1221
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
1222
1471
|
try {
|
|
1223
|
-
|
|
1472
|
+
const shouldUseWriteWithResponse =
|
|
1473
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1474
|
+
await this.writeBlePacket(
|
|
1475
|
+
uuid,
|
|
1476
|
+
outData,
|
|
1477
|
+
payload =>
|
|
1478
|
+
shouldUseWriteWithResponse
|
|
1479
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1480
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1481
|
+
isCurrentOwner
|
|
1482
|
+
);
|
|
1224
1483
|
} catch (e) {
|
|
1225
1484
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1226
|
-
|
|
1485
|
+
releaseOwnershipIfCurrent();
|
|
1486
|
+
if (isWedgedWriteError(e)) {
|
|
1487
|
+
throw e;
|
|
1488
|
+
}
|
|
1227
1489
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1228
1490
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1229
1491
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1256,20 +1518,33 @@ export default class ReactNativeBleTransport {
|
|
|
1256
1518
|
throw new Error('Returning data is not string.');
|
|
1257
1519
|
}
|
|
1258
1520
|
|
|
1259
|
-
Log?.debug('receive data: ', response);
|
|
1260
1521
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1261
1522
|
return check.call(jsonData);
|
|
1262
1523
|
} catch (e) {
|
|
1263
|
-
if (name === '
|
|
1264
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1524
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1525
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1265
1526
|
} else {
|
|
1266
1527
|
Log?.error('call error: ', e);
|
|
1267
1528
|
}
|
|
1529
|
+
const isProbeTimeout =
|
|
1530
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1531
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1532
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1533
|
+
// call is actively using.
|
|
1534
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1535
|
+
if (
|
|
1536
|
+
!isProbeTimeout &&
|
|
1537
|
+
!isStaleCall &&
|
|
1538
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1539
|
+
) {
|
|
1540
|
+
await this.disconnect(uuid);
|
|
1541
|
+
}
|
|
1268
1542
|
throw e;
|
|
1269
1543
|
} finally {
|
|
1270
1544
|
if (timeout) clearTimeout(timeout);
|
|
1271
1545
|
if (this.runPromise === runPromise) {
|
|
1272
1546
|
this.runPromise = null;
|
|
1547
|
+
this.runPromiseDeviceId = null;
|
|
1273
1548
|
}
|
|
1274
1549
|
}
|
|
1275
1550
|
}
|
|
@@ -1279,8 +1554,9 @@ export default class ReactNativeBleTransport {
|
|
|
1279
1554
|
}
|
|
1280
1555
|
|
|
1281
1556
|
async disconnect(session: string) {
|
|
1282
|
-
|
|
1557
|
+
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1283
1558
|
const transport = transportCache[session];
|
|
1559
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1284
1560
|
|
|
1285
1561
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1286
1562
|
// from being triggered when we cancel the connection below
|
|
@@ -1338,23 +1614,22 @@ export default class ReactNativeBleTransport {
|
|
|
1338
1614
|
delete transportCache[session];
|
|
1339
1615
|
}
|
|
1340
1616
|
this.deviceProtocol.delete(session);
|
|
1617
|
+
this.probingProtocols.delete(session);
|
|
1341
1618
|
this.deviceProtocolHints.delete(session);
|
|
1619
|
+
this.sessionProtocols.delete(session);
|
|
1620
|
+
this.protocolReprobeFailures.delete(session);
|
|
1342
1621
|
this.protocolV2Assemblers.delete(session);
|
|
1343
1622
|
this.resetProtocolV2Frames(session);
|
|
1344
|
-
if (this.activeProtocolV2Call?.uuid === session) {
|
|
1345
|
-
this.activeProtocolV2Call = null;
|
|
1346
|
-
}
|
|
1347
1623
|
|
|
1348
1624
|
// emit the disconnect event
|
|
1349
1625
|
try {
|
|
1350
|
-
this.
|
|
1351
|
-
name: transport?.device?.name,
|
|
1352
|
-
id: session,
|
|
1353
|
-
connectId: session,
|
|
1354
|
-
});
|
|
1626
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1355
1627
|
} catch (e) {
|
|
1356
1628
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1357
1629
|
}
|
|
1630
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1631
|
+
this.monitorTokens.delete(session);
|
|
1632
|
+
}
|
|
1358
1633
|
// eslint-disable-next-line no-promise-executor-return
|
|
1359
1634
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1360
1635
|
}
|
|
@@ -1365,6 +1640,114 @@ export default class ReactNativeBleTransport {
|
|
|
1365
1640
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1366
1641
|
}
|
|
1367
1642
|
this.runPromise = null;
|
|
1643
|
+
this.runPromiseDeviceId = null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1647
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1648
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1649
|
+
let timedOut = false;
|
|
1650
|
+
const pending = connect();
|
|
1651
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1652
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1653
|
+
pending.catch(() => undefined);
|
|
1654
|
+
try {
|
|
1655
|
+
const result = await Promise.race([
|
|
1656
|
+
pending,
|
|
1657
|
+
new Promise<never>((_, reject) => {
|
|
1658
|
+
timer = setTimeout(() => {
|
|
1659
|
+
timedOut = true;
|
|
1660
|
+
reject(
|
|
1661
|
+
ERRORS.TypedError(
|
|
1662
|
+
HardwareErrorCode.BleConnectedError,
|
|
1663
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1664
|
+
)
|
|
1665
|
+
);
|
|
1666
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1667
|
+
}),
|
|
1668
|
+
]);
|
|
1669
|
+
return result;
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1672
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1673
|
+
}
|
|
1674
|
+
throw error;
|
|
1675
|
+
} finally {
|
|
1676
|
+
if (timer) clearTimeout(timer);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1681
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1682
|
+
uuid: string,
|
|
1683
|
+
device: Device
|
|
1684
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1685
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1686
|
+
let timedOut = false;
|
|
1687
|
+
const pending = this.resolveCharacteristics(device);
|
|
1688
|
+
pending.catch(() => undefined);
|
|
1689
|
+
try {
|
|
1690
|
+
const result = await Promise.race([
|
|
1691
|
+
pending,
|
|
1692
|
+
new Promise<never>((_, reject) => {
|
|
1693
|
+
timer = setTimeout(() => {
|
|
1694
|
+
timedOut = true;
|
|
1695
|
+
reject(
|
|
1696
|
+
ERRORS.TypedError(
|
|
1697
|
+
HardwareErrorCode.BleConnectedError,
|
|
1698
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1702
|
+
}),
|
|
1703
|
+
]);
|
|
1704
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1705
|
+
return result;
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1708
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1709
|
+
}
|
|
1710
|
+
throw error;
|
|
1711
|
+
} finally {
|
|
1712
|
+
if (timer) clearTimeout(timer);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1718
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1719
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1720
|
+
*/
|
|
1721
|
+
private abandonStalledConnection(
|
|
1722
|
+
uuid: string,
|
|
1723
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1724
|
+
) {
|
|
1725
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1726
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1727
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1728
|
+
stage,
|
|
1729
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1733
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1734
|
+
});
|
|
1735
|
+
const stalled = transportCache[uuid];
|
|
1736
|
+
if (stalled) {
|
|
1737
|
+
delete transportCache[uuid];
|
|
1738
|
+
}
|
|
1739
|
+
this.deviceProtocol.delete(uuid);
|
|
1740
|
+
this.probingProtocols.delete(uuid);
|
|
1741
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1742
|
+
this.resetProtocolV2Frames(uuid);
|
|
1743
|
+
|
|
1744
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1745
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1746
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1747
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1748
|
+
this.resetPlxManager();
|
|
1749
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1750
|
+
}
|
|
1368
1751
|
}
|
|
1369
1752
|
|
|
1370
1753
|
private getCachedTransport(uuid: string) {
|
|
@@ -1375,6 +1758,109 @@ export default class ReactNativeBleTransport {
|
|
|
1375
1758
|
return transport;
|
|
1376
1759
|
}
|
|
1377
1760
|
|
|
1761
|
+
/**
|
|
1762
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1763
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1764
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1765
|
+
* cached and every later call would hang on it again.
|
|
1766
|
+
*/
|
|
1767
|
+
private async writeBlePacket(
|
|
1768
|
+
uuid: string,
|
|
1769
|
+
data: string,
|
|
1770
|
+
write: (payload: string) => Promise<unknown>,
|
|
1771
|
+
isCurrentOwner?: () => boolean
|
|
1772
|
+
) {
|
|
1773
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1774
|
+
let timedOut = false;
|
|
1775
|
+
try {
|
|
1776
|
+
await Promise.race([
|
|
1777
|
+
write(data),
|
|
1778
|
+
new Promise<never>((_, reject) => {
|
|
1779
|
+
timer = setTimeout(() => {
|
|
1780
|
+
timedOut = true;
|
|
1781
|
+
reject(
|
|
1782
|
+
ERRORS.TypedError(
|
|
1783
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1784
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1785
|
+
)
|
|
1786
|
+
);
|
|
1787
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1788
|
+
}),
|
|
1789
|
+
]);
|
|
1790
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (timedOut) {
|
|
1793
|
+
// A superseded call's late write must not tear down the link the current
|
|
1794
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1795
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1796
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1797
|
+
} else {
|
|
1798
|
+
this.tearDownWedgedLink(uuid);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
throw error;
|
|
1802
|
+
} finally {
|
|
1803
|
+
if (timer) clearTimeout(timer);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1809
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1810
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1811
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1812
|
+
*/
|
|
1813
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1814
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1815
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1816
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1817
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
const wedged = transportCache[uuid];
|
|
1821
|
+
this.disconnect(uuid).catch(error => {
|
|
1822
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1823
|
+
});
|
|
1824
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1825
|
+
delete transportCache[uuid];
|
|
1826
|
+
}
|
|
1827
|
+
this.deviceProtocol.delete(uuid);
|
|
1828
|
+
this.probingProtocols.delete(uuid);
|
|
1829
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1830
|
+
this.resetProtocolV2Frames(uuid);
|
|
1831
|
+
|
|
1832
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1833
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1834
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1835
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1836
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1837
|
+
this.resetPlxManager();
|
|
1838
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
private resetPlxManager() {
|
|
1843
|
+
const manager = this.blePlxManager;
|
|
1844
|
+
this.blePlxManager = undefined;
|
|
1845
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1846
|
+
Object.keys(transportCache).forEach(key => {
|
|
1847
|
+
delete transportCache[key];
|
|
1848
|
+
});
|
|
1849
|
+
this.deviceProtocol.clear();
|
|
1850
|
+
this.probingProtocols.clear();
|
|
1851
|
+
this.sessionProtocols.clear();
|
|
1852
|
+
this.protocolReprobeFailures.clear();
|
|
1853
|
+
this.writeTimeoutCounts.clear();
|
|
1854
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
1855
|
+
this.monitorTokens.clear();
|
|
1856
|
+
this.protocolV2Assemblers.clear();
|
|
1857
|
+
try {
|
|
1858
|
+
manager?.destroy();
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1378
1864
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1379
1865
|
return ERRORS.TypedError(
|
|
1380
1866
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1385,70 +1871,131 @@ export default class ReactNativeBleTransport {
|
|
|
1385
1871
|
private createProtocolDetectionError() {
|
|
1386
1872
|
return ERRORS.TypedError(
|
|
1387
1873
|
HardwareErrorCode.BleTimeoutError,
|
|
1388
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1874
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1389
1875
|
);
|
|
1390
1876
|
}
|
|
1391
1877
|
|
|
1392
1878
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1879
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1880
|
+
this.probingProtocols.delete(uuid);
|
|
1881
|
+
}
|
|
1393
1882
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1394
1883
|
this.deviceProtocol.delete(uuid);
|
|
1395
1884
|
}
|
|
1396
1885
|
}
|
|
1397
1886
|
|
|
1887
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1888
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1889
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1398
1892
|
private async detectProtocol(
|
|
1399
1893
|
uuid: string,
|
|
1400
1894
|
expectedProtocol?: ProtocolType,
|
|
1401
|
-
protocolHint?: ProtocolType
|
|
1895
|
+
protocolHint?: ProtocolType,
|
|
1896
|
+
rebuildTransport?: () => Promise<void>
|
|
1402
1897
|
): Promise<ProtocolType> {
|
|
1898
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1899
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1900
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1901
|
+
deviceId: uuid,
|
|
1902
|
+
protocol: expectedProtocol,
|
|
1903
|
+
source: 'expected',
|
|
1904
|
+
});
|
|
1905
|
+
return expectedProtocol;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1403
1908
|
if (expectedProtocol === 'V1') {
|
|
1404
1909
|
if (await this.probeProtocolV1(uuid)) {
|
|
1405
1910
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1406
|
-
|
|
1911
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1912
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1913
|
+
deviceId: uuid,
|
|
1914
|
+
protocol: 'V1',
|
|
1915
|
+
source: 'expected',
|
|
1916
|
+
});
|
|
1407
1917
|
return 'V1';
|
|
1408
1918
|
}
|
|
1409
1919
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1410
1920
|
}
|
|
1411
1921
|
|
|
1412
1922
|
if (expectedProtocol === 'V2') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1923
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1924
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1925
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1926
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1927
|
+
deviceId: uuid,
|
|
1928
|
+
protocol: 'V2',
|
|
1929
|
+
source: 'expected',
|
|
1930
|
+
});
|
|
1931
|
+
return 'V2';
|
|
1932
|
+
}
|
|
1933
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1418
1934
|
}
|
|
1419
1935
|
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1423
|
-
const
|
|
1936
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1937
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1938
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1939
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1940
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1424
1941
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1942
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1943
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1944
|
+
// only adds its timeout to every poll.
|
|
1945
|
+
const trustSessionProtocol =
|
|
1946
|
+
sessionProtocol !== undefined &&
|
|
1947
|
+
!protocolHint &&
|
|
1948
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1949
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1425
1950
|
|
|
1426
1951
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1427
1952
|
const protocol = probeOrder[i];
|
|
1428
1953
|
if (i > 0) {
|
|
1429
|
-
//
|
|
1954
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1430
1955
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1956
|
+
if (!transportCache[uuid]) {
|
|
1957
|
+
if (!rebuildTransport) {
|
|
1958
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1959
|
+
}
|
|
1960
|
+
await rebuildTransport();
|
|
1961
|
+
}
|
|
1431
1962
|
}
|
|
1432
1963
|
const detected =
|
|
1433
1964
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1434
1965
|
if (detected) {
|
|
1435
1966
|
this.deviceProtocol.set(uuid, protocol);
|
|
1436
|
-
|
|
1967
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1968
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1969
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1970
|
+
deviceId: uuid,
|
|
1971
|
+
protocol,
|
|
1972
|
+
source: 'probe',
|
|
1973
|
+
});
|
|
1437
1974
|
return protocol;
|
|
1438
1975
|
}
|
|
1439
1976
|
}
|
|
1440
1977
|
|
|
1978
|
+
if (trustSessionProtocol) {
|
|
1979
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1980
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1981
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1982
|
+
} else {
|
|
1983
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1441
1986
|
this.deviceProtocol.delete(uuid);
|
|
1987
|
+
this.probingProtocols.delete(uuid);
|
|
1442
1988
|
throw this.createProtocolDetectionError();
|
|
1443
1989
|
}
|
|
1444
1990
|
|
|
1445
1991
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
1446
1992
|
const transport = transportCache[uuid];
|
|
1993
|
+
await this.protocolV2Links.invalidateLink(
|
|
1994
|
+
uuid,
|
|
1995
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
1996
|
+
);
|
|
1447
1997
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1448
1998
|
this.resetProtocolV2Frames(uuid);
|
|
1449
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
1450
|
-
this.activeProtocolV2Call = null;
|
|
1451
|
-
}
|
|
1452
1999
|
if (this.runPromise) {
|
|
1453
2000
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1454
2001
|
this.runPromise.reject(error);
|
|
@@ -1499,12 +2046,20 @@ export default class ReactNativeBleTransport {
|
|
|
1499
2046
|
}
|
|
1500
2047
|
|
|
1501
2048
|
try {
|
|
1502
|
-
this.
|
|
1503
|
-
|
|
2049
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2050
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
2051
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
2052
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2053
|
+
this.probingProtocols.delete(uuid);
|
|
1504
2054
|
return true;
|
|
1505
2055
|
} catch (error) {
|
|
1506
2056
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1507
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
2057
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2058
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2059
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2060
|
+
if (isWedgedWriteError(error)) {
|
|
2061
|
+
throw error;
|
|
2062
|
+
}
|
|
1508
2063
|
return false;
|
|
1509
2064
|
}
|
|
1510
2065
|
}
|
|
@@ -1514,7 +2069,7 @@ export default class ReactNativeBleTransport {
|
|
|
1514
2069
|
return false;
|
|
1515
2070
|
}
|
|
1516
2071
|
|
|
1517
|
-
this.
|
|
2072
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1518
2073
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1519
2074
|
const detected = await probeProtocolV2Helper({
|
|
1520
2075
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1529,17 +2084,15 @@ export default class ReactNativeBleTransport {
|
|
|
1529
2084
|
});
|
|
1530
2085
|
if (!detected) {
|
|
1531
2086
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2087
|
+
} else {
|
|
2088
|
+
this.probingProtocols.delete(uuid);
|
|
1532
2089
|
}
|
|
1533
2090
|
return detected;
|
|
1534
2091
|
}
|
|
1535
2092
|
|
|
1536
|
-
private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
|
|
2093
|
+
private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
|
|
1537
2094
|
try {
|
|
1538
|
-
if (
|
|
1539
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1540
|
-
this.resetProtocolV2Frames(uuid);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
2095
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) return;
|
|
1543
2096
|
|
|
1544
2097
|
if (data.length === 0) return;
|
|
1545
2098
|
|
|
@@ -1552,8 +2105,15 @@ export default class ReactNativeBleTransport {
|
|
|
1552
2105
|
} catch (error) {
|
|
1553
2106
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1554
2107
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
2108
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
2109
|
+
this.protocolV2Links
|
|
2110
|
+
.invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
|
|
2111
|
+
.catch(invalidateError =>
|
|
2112
|
+
Log?.debug(
|
|
2113
|
+
'[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
|
|
2114
|
+
invalidateError
|
|
2115
|
+
)
|
|
2116
|
+
);
|
|
1557
2117
|
}
|
|
1558
2118
|
}
|
|
1559
2119
|
|
|
@@ -1576,21 +2136,17 @@ export default class ReactNativeBleTransport {
|
|
|
1576
2136
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1577
2137
|
}
|
|
1578
2138
|
|
|
1579
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1580
|
-
this.protocolV2FrameQueues.clear();
|
|
1581
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1582
|
-
framePromise.reject(error);
|
|
1583
|
-
}
|
|
1584
|
-
this.protocolV2FramePromises.clear();
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
2139
|
private resetProtocolV2Frames(uuid: string) {
|
|
1588
|
-
this.
|
|
1589
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2140
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1590
2141
|
}
|
|
1591
2142
|
|
|
1592
|
-
private
|
|
1593
|
-
|
|
2143
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
2144
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
2145
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
2146
|
+
if (framePromise) {
|
|
2147
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
2148
|
+
framePromise.reject(error);
|
|
2149
|
+
}
|
|
1594
2150
|
}
|
|
1595
2151
|
|
|
1596
2152
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -1610,66 +2166,97 @@ export default class ReactNativeBleTransport {
|
|
|
1610
2166
|
}
|
|
1611
2167
|
}
|
|
1612
2168
|
|
|
1613
|
-
private async
|
|
2169
|
+
private async writeProtocolV2Packet(
|
|
2170
|
+
uuid: string,
|
|
1614
2171
|
transport: BleTransport,
|
|
1615
|
-
|
|
1616
|
-
|
|
2172
|
+
base64: string,
|
|
2173
|
+
context: ProtocolV2CallContext,
|
|
2174
|
+
assertCurrentGeneration: () => void
|
|
1617
2175
|
) {
|
|
1618
|
-
const
|
|
1619
|
-
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2176
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
1620
2177
|
platform: Platform.OS,
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2178
|
+
highThroughput: context.highThroughput,
|
|
2179
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2180
|
+
characteristic: transport.writeCharacteristic,
|
|
1624
2181
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2182
|
+
let attempt = 0;
|
|
2183
|
+
for (;;) {
|
|
2184
|
+
assertCurrentGeneration();
|
|
2185
|
+
if (context.signal.aborted) {
|
|
2186
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
await this.writeBlePacket(
|
|
2190
|
+
uuid,
|
|
2191
|
+
base64,
|
|
2192
|
+
payload =>
|
|
2193
|
+
shouldUseWriteWithResponse
|
|
2194
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2195
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2196
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2197
|
+
// tear down the link that the current generation is using.
|
|
2198
|
+
() => {
|
|
2199
|
+
try {
|
|
2200
|
+
assertCurrentGeneration();
|
|
2201
|
+
return !context.signal.aborted;
|
|
2202
|
+
} catch {
|
|
2203
|
+
return false;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
);
|
|
2207
|
+
assertCurrentGeneration();
|
|
2208
|
+
return;
|
|
2209
|
+
} catch (error) {
|
|
1645
2210
|
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
offset + packetCapacity < frame.length
|
|
2211
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2212
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
1649
2213
|
) {
|
|
1650
|
-
|
|
2214
|
+
throw error;
|
|
1651
2215
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
|
|
1659
|
-
Log?.debug(
|
|
1660
|
-
'[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
|
|
1661
|
-
error
|
|
1662
|
-
);
|
|
1663
|
-
await this.writeProtocolV2Frame(transport, frame, {
|
|
1664
|
-
highVolume: true,
|
|
1665
|
-
writeWithResponse: true,
|
|
2216
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2217
|
+
attempt += 1;
|
|
2218
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2219
|
+
name: context.messageName,
|
|
2220
|
+
attempt,
|
|
2221
|
+
delayMs,
|
|
1666
2222
|
});
|
|
1667
|
-
|
|
2223
|
+
await delay(delayMs);
|
|
1668
2224
|
}
|
|
1669
|
-
throw error;
|
|
1670
2225
|
}
|
|
1671
2226
|
}
|
|
1672
2227
|
|
|
2228
|
+
private async writeProtocolV2Frame(
|
|
2229
|
+
uuid: string,
|
|
2230
|
+
transport: BleTransport,
|
|
2231
|
+
frame: Uint8Array,
|
|
2232
|
+
context: ProtocolV2CallContext,
|
|
2233
|
+
assertCurrentGeneration: () => void
|
|
2234
|
+
) {
|
|
2235
|
+
const tuning = getProtocolV2BleTuning();
|
|
2236
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2237
|
+
platform: Platform.OS,
|
|
2238
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2239
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2240
|
+
mtu: transport.mtuSize,
|
|
2241
|
+
});
|
|
2242
|
+
await writeProtocolV2BleFrame({
|
|
2243
|
+
frame,
|
|
2244
|
+
packetCapacity,
|
|
2245
|
+
assertActive: assertCurrentGeneration,
|
|
2246
|
+
signal: context.signal,
|
|
2247
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2248
|
+
wait: delay,
|
|
2249
|
+
writePacket: packet =>
|
|
2250
|
+
this.writeProtocolV2Packet(
|
|
2251
|
+
uuid,
|
|
2252
|
+
transport,
|
|
2253
|
+
Buffer.from(packet).toString('base64'),
|
|
2254
|
+
context,
|
|
2255
|
+
assertCurrentGeneration
|
|
2256
|
+
),
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1673
2260
|
private async callProtocolV2(
|
|
1674
2261
|
uuid: string,
|
|
1675
2262
|
name: string,
|
|
@@ -1680,102 +2267,196 @@ export default class ReactNativeBleTransport {
|
|
|
1680
2267
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1681
2268
|
}
|
|
1682
2269
|
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2270
|
+
const callOptions = options;
|
|
2271
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
2272
|
+
|
|
2273
|
+
if (highThroughputWrite) {
|
|
2274
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
2275
|
+
const tuning = getProtocolV2BleTuning();
|
|
2276
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2277
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2278
|
+
platform: Platform.OS,
|
|
2279
|
+
highThroughput: true,
|
|
2280
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2281
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
2282
|
+
});
|
|
2283
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2284
|
+
platform: Platform.OS,
|
|
2285
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2286
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2287
|
+
mtu: currentTransport.mtuSize,
|
|
2288
|
+
});
|
|
2289
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2290
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2291
|
+
currentTransport.mtuSize
|
|
2292
|
+
)}:${packetCapacity}`;
|
|
2293
|
+
const loggedSignatures =
|
|
2294
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2295
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2296
|
+
loggedSignatures.add(logSignature);
|
|
2297
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2298
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2299
|
+
name,
|
|
2300
|
+
writeMode,
|
|
2301
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2302
|
+
packetCapacity,
|
|
2303
|
+
});
|
|
1687
2304
|
}
|
|
1688
|
-
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1689
|
-
this.runPromise.reject(error);
|
|
1690
|
-
this.rejectAllProtocolV2Frames(error);
|
|
1691
|
-
this.runPromise = null;
|
|
1692
|
-
this.activeProtocolV2Call = null;
|
|
1693
2305
|
}
|
|
1694
2306
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
this.runPromise = runPromise;
|
|
1699
|
-
const callToken = this.nextProtocolV2CallToken++;
|
|
1700
|
-
this.activeProtocolV2Call = { uuid, token: callToken };
|
|
1701
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1702
|
-
this.resetProtocolV2Frames(uuid);
|
|
1703
|
-
let completed = false;
|
|
1704
|
-
const callOptions = {
|
|
1705
|
-
...options,
|
|
1706
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1707
|
-
};
|
|
1708
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2307
|
+
if (highThroughputWrite) {
|
|
2308
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
2309
|
+
}
|
|
1709
2310
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
2311
|
+
try {
|
|
2312
|
+
return await this.protocolV2Links.call(
|
|
2313
|
+
uuid,
|
|
2314
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
1714
2315
|
name,
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1718
|
-
burstSize: tuning.highVolumeWriteBurstSize,
|
|
1719
|
-
pauseMs: tuning.highVolumeWritePauseMs,
|
|
1720
|
-
flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
|
|
1721
|
-
writeWithResponse: tuning.highVolumeWriteWithResponse,
|
|
1722
|
-
}
|
|
2316
|
+
data,
|
|
2317
|
+
callOptions
|
|
1723
2318
|
);
|
|
2319
|
+
} catch (e) {
|
|
2320
|
+
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
2321
|
+
throw e;
|
|
2322
|
+
} finally {
|
|
2323
|
+
if (highThroughputWrite) {
|
|
2324
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2325
|
+
}
|
|
1724
2326
|
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2330
|
+
const transport = this.getCachedTransport(uuid);
|
|
2331
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2332
|
+
|
|
2333
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2334
|
+
transport.device = refreshedDevice;
|
|
2335
|
+
transport.mtuSize =
|
|
2336
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2337
|
+
|
|
2338
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2339
|
+
throw ERRORS.TypedError(
|
|
2340
|
+
HardwareErrorCode.BleConnectedError,
|
|
2341
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2347
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2348
|
+
if (timerId !== undefined) {
|
|
2349
|
+
clearTimeout(timerId);
|
|
2350
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2355
|
+
if (Platform.OS !== 'android') return;
|
|
2356
|
+
|
|
2357
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2358
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2359
|
+
|
|
2360
|
+
const transport = transportCache[uuid];
|
|
2361
|
+
if (!transport) return;
|
|
1725
2362
|
|
|
1726
2363
|
try {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
},
|
|
1732
|
-
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
1733
|
-
writeFrame: async (frame: Uint8Array) => {
|
|
1734
|
-
await this.writeProtocolV2Frame(transport, frame, {
|
|
1735
|
-
highVolume: highVolumeWrite,
|
|
1736
|
-
});
|
|
1737
|
-
},
|
|
1738
|
-
readFrame: async () => {
|
|
1739
|
-
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
1740
|
-
if (!(rxFrame instanceof Uint8Array)) {
|
|
1741
|
-
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1742
|
-
}
|
|
1743
|
-
return rxFrame;
|
|
1744
|
-
},
|
|
1745
|
-
logger: Log,
|
|
1746
|
-
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1747
|
-
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
1748
|
-
ERRORS.TypedError(
|
|
1749
|
-
HardwareErrorCode.BleTimeoutError,
|
|
1750
|
-
`BLE response timeout after ${timeout}ms for ${name}`
|
|
1751
|
-
),
|
|
2364
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2365
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2366
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2367
|
+
priority: 'high',
|
|
1752
2368
|
});
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
1753
2375
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2376
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2377
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2378
|
+
|
|
2379
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2380
|
+
const timerId = setTimeout(() => {
|
|
2381
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2382
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2383
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2384
|
+
);
|
|
2385
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2386
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2390
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2391
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
try {
|
|
2396
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2397
|
+
ConnectionPriority.Balanced
|
|
2398
|
+
);
|
|
2399
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2400
|
+
priority: 'balanced',
|
|
2401
|
+
});
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2404
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
2410
|
+
const generation = this.monitorTokens.get(uuid) ?? 0;
|
|
2411
|
+
const assertCurrentGeneration = () => {
|
|
2412
|
+
if (this.monitorTokens.get(uuid) !== generation) {
|
|
2413
|
+
throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
return {
|
|
2418
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
2419
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
2420
|
+
generation,
|
|
2421
|
+
prepareCall: () => {
|
|
2422
|
+
assertCurrentGeneration();
|
|
1759
2423
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1760
2424
|
this.resetProtocolV2Frames(uuid);
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2425
|
+
},
|
|
2426
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2427
|
+
assertCurrentGeneration();
|
|
2428
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2429
|
+
await this.writeProtocolV2Frame(
|
|
2430
|
+
uuid,
|
|
2431
|
+
currentTransport,
|
|
2432
|
+
frame,
|
|
2433
|
+
context,
|
|
2434
|
+
assertCurrentGeneration
|
|
2435
|
+
);
|
|
2436
|
+
},
|
|
2437
|
+
readFrame: async () => {
|
|
2438
|
+
assertCurrentGeneration();
|
|
2439
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
2440
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
2441
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1768
2442
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
this.
|
|
1774
|
-
}
|
|
1775
|
-
|
|
2443
|
+
return rxFrame;
|
|
2444
|
+
},
|
|
2445
|
+
reset: (reason: string) => {
|
|
2446
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2447
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2448
|
+
},
|
|
2449
|
+
logger: Log,
|
|
2450
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
2451
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
2452
|
+
ERRORS.TypedError(
|
|
2453
|
+
HardwareErrorCode.BleTimeoutError,
|
|
2454
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
2455
|
+
),
|
|
2456
|
+
};
|
|
1776
2457
|
}
|
|
1777
2458
|
|
|
1778
2459
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1779
|
-
return this.
|
|
2460
|
+
return this.getActiveProtocol(path);
|
|
1780
2461
|
}
|
|
1781
2462
|
}
|