@onekeyfe/hd-transport-react-native 1.2.0-alpha.16 → 1.2.0-alpha.160
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/bleStaleBond.d.ts +5 -0
- package/dist/bleStaleBond.d.ts.map +1 -0
- package/dist/bleStrategy.d.ts +7 -0
- 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 +136 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +993 -326
- package/dist/transportLog.d.ts +1 -12
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +6 -5
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStaleBond.test.ts +31 -0
- package/src/__tests__/bleStrategy.test.ts +93 -5
- package/src/__tests__/connectTimeout.test.ts +303 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +150 -0
- package/src/__tests__/protocolReprobe.test.ts +156 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +916 -32
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStaleBond.ts +63 -0
- package/src/bleStrategy.ts +26 -11
- package/src/constants.ts +9 -14
- package/src/index.ts +1242 -295
- package/src/transportLog.ts +1 -18
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -1,43 +1,61 @@
|
|
|
1
1
|
import { PermissionsAndroid, Platform } from 'react-native';
|
|
2
2
|
import { Buffer } from 'buffer';
|
|
3
3
|
import {
|
|
4
|
-
BleATTErrorCode,
|
|
5
4
|
BleError,
|
|
6
5
|
BleErrorCode,
|
|
7
6
|
BleManager as BlePlxManager,
|
|
7
|
+
ConnectionPriority,
|
|
8
8
|
ScanMode,
|
|
9
9
|
} from 'react-native-ble-plx';
|
|
10
10
|
import ByteBuffer from 'bytebuffer';
|
|
11
11
|
import transport, {
|
|
12
|
-
LogBlockCommand,
|
|
13
12
|
type OneKeyDeviceInfoBase,
|
|
14
13
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
14
|
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
15
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
17
16
|
type ProtocolType,
|
|
17
|
+
type ProtocolV2CallContext,
|
|
18
18
|
ProtocolV2FrameAssembler,
|
|
19
19
|
ProtocolV2LinkManager,
|
|
20
|
+
TRANSPORT_EVENT,
|
|
20
21
|
type TransportCallOptions,
|
|
22
|
+
isProtocolV2HighThroughputCall,
|
|
21
23
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
24
|
+
writeProtocolV2BleFrame,
|
|
22
25
|
} from '@onekeyfe/hd-transport';
|
|
23
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
ERRORS,
|
|
28
|
+
HardwareErrorCode,
|
|
29
|
+
createDeferred,
|
|
30
|
+
isOnekeyBluetoothDevice,
|
|
31
|
+
} from '@onekeyfe/hd-shared';
|
|
24
32
|
|
|
25
33
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
26
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
hasWritableCapability,
|
|
36
|
+
resolveProtocolV2PacketCapacity,
|
|
37
|
+
shouldRefreshNegotiatedMtu,
|
|
38
|
+
shouldWriteProtocolV2WithResponse,
|
|
39
|
+
} from './bleStrategy';
|
|
27
40
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
28
41
|
import {
|
|
29
42
|
ANDROID_PACKET_LENGTH,
|
|
43
|
+
ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
30
44
|
IOS_PACKET_LENGTH,
|
|
31
|
-
|
|
45
|
+
IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
32
46
|
getBluetoothServiceUuids,
|
|
33
47
|
getInfosForServiceUuid,
|
|
34
48
|
isSameBleUuid,
|
|
35
49
|
} from './constants';
|
|
50
|
+
import {
|
|
51
|
+
isBleStaleBondHardwareError,
|
|
52
|
+
isNativeBleStaleBondError,
|
|
53
|
+
toBleStaleBondHardwareError,
|
|
54
|
+
} from './bleStaleBond';
|
|
36
55
|
import { isHeaderChunk } from './utils/validateNotify';
|
|
37
56
|
import BleTransport from './BleTransport';
|
|
38
57
|
import timer from './utils/timer';
|
|
39
58
|
import { bleLogger, setBleLogger } from './logger';
|
|
40
|
-
import { createTransportCallLog } from './transportLog';
|
|
41
59
|
|
|
42
60
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
43
61
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -53,24 +71,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
53
71
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
54
72
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
55
73
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
56
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
57
74
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
58
75
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
59
76
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
60
77
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
61
78
|
|
|
62
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
79
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
63
80
|
type ResolvedBleCharacteristics = {
|
|
64
81
|
writeCharacteristic: Characteristic;
|
|
65
82
|
notifyCharacteristic: Characteristic;
|
|
66
83
|
};
|
|
67
84
|
|
|
85
|
+
const isAsciiWhitespace = (code: number) =>
|
|
86
|
+
code === 0x09 ||
|
|
87
|
+
code === 0x0a ||
|
|
88
|
+
code === 0x0b ||
|
|
89
|
+
code === 0x0c ||
|
|
90
|
+
code === 0x0d ||
|
|
91
|
+
code === 0x20;
|
|
92
|
+
|
|
93
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
94
|
+
let searchFrom = 0;
|
|
95
|
+
while (searchFrom < text.length) {
|
|
96
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
97
|
+
if (statusIndex < 0) return false;
|
|
98
|
+
|
|
99
|
+
let cursor = statusIndex + 'status'.length;
|
|
100
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
101
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
102
|
+
cursor += 1;
|
|
103
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
104
|
+
}
|
|
105
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
106
|
+
|
|
107
|
+
searchFrom = statusIndex + 'status'.length;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
};
|
|
111
|
+
|
|
68
112
|
const delay = (ms: number) =>
|
|
69
113
|
new Promise<void>(resolve => {
|
|
70
114
|
setTimeout(resolve, ms);
|
|
71
115
|
});
|
|
72
116
|
|
|
73
|
-
const getFirmwareUploadWriteRetryType = (
|
|
117
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
118
|
+
error: unknown
|
|
119
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
74
120
|
if (!error || typeof error !== 'object') return null;
|
|
75
121
|
const bleWriteError = error as {
|
|
76
122
|
androidErrorCode?: unknown;
|
|
@@ -81,13 +127,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
81
127
|
name?: unknown;
|
|
82
128
|
};
|
|
83
129
|
|
|
84
|
-
if (
|
|
85
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
86
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
87
|
-
) {
|
|
88
|
-
return 'reconnectable';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
130
|
if (
|
|
92
131
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
93
132
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -98,15 +137,31 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
98
137
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
99
138
|
.filter(value => typeof value === 'string')
|
|
100
139
|
.join(' ');
|
|
101
|
-
return
|
|
140
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
102
141
|
};
|
|
103
142
|
|
|
104
143
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
105
144
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
106
|
-
const
|
|
107
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
145
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
108
146
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
149
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
150
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
151
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
152
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
153
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
154
|
+
*/
|
|
155
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
156
|
+
export const BLE_NATIVE_TEARDOWN_TIMEOUT_MS = 3_000;
|
|
157
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
158
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
159
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
160
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
161
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
162
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
163
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
164
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
110
165
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
111
166
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
112
167
|
export type ProtocolV2BleTuning = {
|
|
@@ -117,8 +172,8 @@ export type ProtocolV2BleTuning = {
|
|
|
117
172
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
118
173
|
|
|
119
174
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
120
|
-
iosPacketLength:
|
|
121
|
-
androidPacketLength:
|
|
175
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
176
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
122
177
|
};
|
|
123
178
|
|
|
124
179
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -152,32 +207,71 @@ export function getProtocolV2BleTuning() {
|
|
|
152
207
|
return { ...protocolV2BleTuning };
|
|
153
208
|
}
|
|
154
209
|
|
|
155
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
156
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
210
|
function getDeviceDisplayName(device?: Device | null) {
|
|
160
211
|
return device?.name || device?.localName || null;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
214
|
+
const IOS_REQUEST_MTU = 247;
|
|
215
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
216
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
217
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
170
|
-
);
|
|
171
|
-
}
|
|
219
|
+
const getRequestedBleMtu = () =>
|
|
220
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
172
221
|
|
|
173
|
-
const
|
|
222
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
174
223
|
|
|
175
224
|
const connectOptions: Record<string, unknown> = {
|
|
176
|
-
requestMTU:
|
|
177
|
-
timeout:
|
|
225
|
+
requestMTU: getRequestedBleMtu(),
|
|
226
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
178
227
|
refreshGatt: 'OnConnected',
|
|
179
228
|
};
|
|
180
229
|
|
|
230
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
231
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
232
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
237
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
238
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
239
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
240
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
241
|
+
*/
|
|
242
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
243
|
+
/**
|
|
244
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
245
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
246
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
247
|
+
* budget.
|
|
248
|
+
*/
|
|
249
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
250
|
+
/**
|
|
251
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
252
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
253
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
254
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
255
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
256
|
+
*/
|
|
257
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
258
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
259
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
260
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
261
|
+
export const BLE_SETUP_WEDGED_MESSAGE = 'BLE setup wedged repeatedly';
|
|
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 isWedgedBleSetupError = (error: unknown): boolean =>
|
|
267
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.PollingTimeout &&
|
|
268
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
269
|
+
(error as { message: string }).message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
|
|
270
|
+
const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
271
|
+
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
272
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
273
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
274
|
+
|
|
181
275
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
182
276
|
|
|
183
277
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -189,34 +283,38 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
189
283
|
return infos;
|
|
190
284
|
};
|
|
191
285
|
|
|
192
|
-
const
|
|
193
|
-
|
|
286
|
+
const requestNegotiatedMtu = async (
|
|
287
|
+
device: Device,
|
|
288
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
289
|
+
attempt: number
|
|
290
|
+
) => {
|
|
291
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
194
292
|
|
|
195
293
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
requested: ANDROID_REQUEST_MTU,
|
|
200
|
-
actual: mtuDevice.mtu,
|
|
201
|
-
});
|
|
294
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
295
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
296
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
202
297
|
return mtuDevice;
|
|
203
298
|
} catch (error) {
|
|
204
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
299
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
300
|
+
platform: Platform.OS,
|
|
301
|
+
stage,
|
|
302
|
+
attempt,
|
|
303
|
+
actual: device.mtu,
|
|
304
|
+
error: error instanceof Error ? error.message : String(error),
|
|
305
|
+
});
|
|
205
306
|
return device;
|
|
206
307
|
}
|
|
207
308
|
};
|
|
208
309
|
|
|
310
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
311
|
+
|
|
209
312
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
210
313
|
|
|
211
314
|
function remapError(error: IOBleErrorRemap) {
|
|
212
315
|
if (error instanceof BleError) {
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
// @ts-expect-error
|
|
216
|
-
error.iosErrorCode === BleATTErrorCode.UnlikelyError ||
|
|
217
|
-
error.reason === 'Peer removed pairing information'
|
|
218
|
-
) {
|
|
219
|
-
throw ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
|
|
316
|
+
if (isNativeBleStaleBondError(error)) {
|
|
317
|
+
throw toBleStaleBondHardwareError(error);
|
|
220
318
|
}
|
|
221
319
|
|
|
222
320
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -245,6 +343,8 @@ export default class ReactNativeBleTransport {
|
|
|
245
343
|
|
|
246
344
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
247
345
|
|
|
346
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
347
|
+
|
|
248
348
|
name = 'ReactNativeBleTransport';
|
|
249
349
|
|
|
250
350
|
configured = false;
|
|
@@ -255,6 +355,8 @@ export default class ReactNativeBleTransport {
|
|
|
255
355
|
|
|
256
356
|
runPromise: Deferred<any> | null = null;
|
|
257
357
|
|
|
358
|
+
private runPromiseDeviceId: string | null = null;
|
|
359
|
+
|
|
258
360
|
emitter?: EventEmitter;
|
|
259
361
|
|
|
260
362
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -262,8 +364,38 @@ export default class ReactNativeBleTransport {
|
|
|
262
364
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
263
365
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
264
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
369
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
370
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
371
|
+
* transport that was never validated.
|
|
372
|
+
*/
|
|
373
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
374
|
+
|
|
375
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
376
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
377
|
+
|
|
378
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
379
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
380
|
+
|
|
265
381
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
266
382
|
|
|
383
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
384
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
385
|
+
|
|
386
|
+
/** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
|
|
387
|
+
private confirmedProtocolV2 = new Set<string>();
|
|
388
|
+
|
|
389
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
390
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Native encryption/pairing failures seen before Protocol V2 probe starts.
|
|
394
|
+
* Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
|
|
395
|
+
* fails with ATT 14/15. Remember it so detectProtocol fails immediately.
|
|
396
|
+
*/
|
|
397
|
+
private staleBondErrors: Map<string, Error> = new Map();
|
|
398
|
+
|
|
267
399
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
268
400
|
|
|
269
401
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -286,15 +418,26 @@ export default class ReactNativeBleTransport {
|
|
|
286
418
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
287
419
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
288
420
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
289
|
-
await this.
|
|
421
|
+
await this.releaseNative(uuid, true);
|
|
290
422
|
}
|
|
291
423
|
},
|
|
292
424
|
});
|
|
293
425
|
|
|
294
426
|
private monitorTokens: Map<string, number> = new Map();
|
|
295
427
|
|
|
428
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
429
|
+
|
|
430
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
431
|
+
|
|
432
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
433
|
+
|
|
434
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
435
|
+
|
|
296
436
|
private nextMonitorToken = 1;
|
|
297
437
|
|
|
438
|
+
/** Serializes transport lifecycle changes for the same physical device. */
|
|
439
|
+
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
440
|
+
|
|
298
441
|
constructor(options: TransportOptions) {
|
|
299
442
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
300
443
|
}
|
|
@@ -311,10 +454,19 @@ export default class ReactNativeBleTransport {
|
|
|
311
454
|
}
|
|
312
455
|
|
|
313
456
|
configureProtocolV2(signedData: any) {
|
|
457
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
458
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
314
463
|
this._messagesV2 = parseConfigure(signedData);
|
|
315
|
-
this.
|
|
316
|
-
|
|
317
|
-
.
|
|
464
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
465
|
+
if (isReconfiguration) {
|
|
466
|
+
this.protocolV2Links
|
|
467
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
468
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
469
|
+
}
|
|
318
470
|
}
|
|
319
471
|
|
|
320
472
|
listen() {
|
|
@@ -344,29 +496,15 @@ export default class ReactNativeBleTransport {
|
|
|
344
496
|
}
|
|
345
497
|
}
|
|
346
498
|
|
|
347
|
-
let fallbackServiceUuid: string | undefined;
|
|
348
|
-
|
|
349
499
|
if (!infos) {
|
|
350
500
|
const services = await device.services();
|
|
351
501
|
Log?.debug(
|
|
352
502
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
353
503
|
services?.map(service => service.uuid)
|
|
354
504
|
);
|
|
355
|
-
|
|
356
|
-
const knownService = services.find(service =>
|
|
357
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
358
|
-
);
|
|
359
|
-
const fallbackService =
|
|
360
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
361
|
-
|
|
362
|
-
if (fallbackService) {
|
|
363
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
364
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
365
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
366
|
-
}
|
|
367
505
|
}
|
|
368
506
|
|
|
369
|
-
if (!infos
|
|
507
|
+
if (!infos) {
|
|
370
508
|
try {
|
|
371
509
|
Log?.debug('cancel connection when service not found');
|
|
372
510
|
await device.cancelConnection();
|
|
@@ -376,9 +514,7 @@ export default class ReactNativeBleTransport {
|
|
|
376
514
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
377
515
|
}
|
|
378
516
|
|
|
379
|
-
const serviceUuid = infos
|
|
380
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
381
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
517
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
382
518
|
|
|
383
519
|
if (!serviceUuid) {
|
|
384
520
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -428,6 +564,7 @@ export default class ReactNativeBleTransport {
|
|
|
428
564
|
|
|
429
565
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
430
566
|
transport.disconnectSubscription?.remove();
|
|
567
|
+
const { monitorToken } = transport;
|
|
431
568
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
432
569
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
433
570
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -437,18 +574,17 @@ export default class ReactNativeBleTransport {
|
|
|
437
574
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
438
575
|
return;
|
|
439
576
|
}
|
|
577
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
578
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
440
581
|
|
|
441
582
|
try {
|
|
442
583
|
Log?.debug('device disconnect: ', device?.id);
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
id: device?.id,
|
|
446
|
-
connectId: device?.id,
|
|
447
|
-
});
|
|
448
|
-
if (this.runPromise) {
|
|
584
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
585
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
449
586
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
450
587
|
this.runPromise.reject(error);
|
|
451
|
-
this.rejectAllProtocolV2Frames(error);
|
|
452
588
|
}
|
|
453
589
|
} catch (e) {
|
|
454
590
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -458,6 +594,22 @@ export default class ReactNativeBleTransport {
|
|
|
458
594
|
});
|
|
459
595
|
}
|
|
460
596
|
|
|
597
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
598
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
602
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
606
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
607
|
+
name,
|
|
608
|
+
id: uuid,
|
|
609
|
+
connectId: uuid,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
461
613
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
462
614
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
463
615
|
try {
|
|
@@ -470,22 +622,21 @@ export default class ReactNativeBleTransport {
|
|
|
470
622
|
const isConnected = await device.isConnected().catch(() => false);
|
|
471
623
|
if (!isConnected) {
|
|
472
624
|
try {
|
|
473
|
-
device = await device.connect(connectOptions);
|
|
625
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
474
626
|
} catch (e) {
|
|
475
627
|
if (
|
|
476
628
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
477
629
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
478
630
|
) {
|
|
479
|
-
device = await device.connect();
|
|
631
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
480
632
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
481
633
|
throw e;
|
|
482
634
|
}
|
|
483
635
|
}
|
|
484
636
|
}
|
|
485
637
|
|
|
486
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
487
|
-
device
|
|
488
|
-
);
|
|
638
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
639
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
489
640
|
|
|
490
641
|
transport.device = device;
|
|
491
642
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -575,10 +726,19 @@ export default class ReactNativeBleTransport {
|
|
|
575
726
|
}
|
|
576
727
|
|
|
577
728
|
const displayName = getDeviceDisplayName(device);
|
|
729
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
730
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
731
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
578
732
|
const isOneKey =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
733
|
+
!isUnnamedIOSPeripheral &&
|
|
734
|
+
isOnekeyBluetoothDevice({
|
|
735
|
+
id: device?.id,
|
|
736
|
+
name: device?.name,
|
|
737
|
+
localName: device?.localName,
|
|
738
|
+
// The native scan is already restricted to the OneKey communication service,
|
|
739
|
+
// but ble-plx permits the returned advertisement field to be null.
|
|
740
|
+
serviceUuids: device?.serviceUUIDs ?? getBluetoothServiceUuids(),
|
|
741
|
+
});
|
|
582
742
|
if (isOneKey) {
|
|
583
743
|
addDevice(device as unknown as Device);
|
|
584
744
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -595,10 +755,18 @@ export default class ReactNativeBleTransport {
|
|
|
595
755
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
596
756
|
devices => {
|
|
597
757
|
for (const device of devices) {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
758
|
+
const localName =
|
|
759
|
+
'localName' in device && typeof device.localName === 'string'
|
|
760
|
+
? device.localName
|
|
761
|
+
: null;
|
|
762
|
+
if (
|
|
763
|
+
isOnekeyBluetoothDevice({
|
|
764
|
+
id: device.id,
|
|
765
|
+
name: device.name,
|
|
766
|
+
localName,
|
|
767
|
+
serviceUuids: device.serviceUUIDs,
|
|
768
|
+
})
|
|
769
|
+
) {
|
|
602
770
|
Log?.debug('search connected peripheral: ', device.id);
|
|
603
771
|
addDevice(device as unknown as Device);
|
|
604
772
|
}
|
|
@@ -609,10 +777,7 @@ export default class ReactNativeBleTransport {
|
|
|
609
777
|
const addDevice = (device: Device) => {
|
|
610
778
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
611
779
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
612
|
-
|
|
613
|
-
if (protocolHint) {
|
|
614
|
-
this.deviceProtocolHints.set(device.id, protocolHint);
|
|
615
|
-
}
|
|
780
|
+
|
|
616
781
|
deviceList.push({
|
|
617
782
|
...device,
|
|
618
783
|
name: displayName,
|
|
@@ -622,7 +787,6 @@ export default class ReactNativeBleTransport {
|
|
|
622
787
|
deviceId: device.id,
|
|
623
788
|
name: displayName,
|
|
624
789
|
serviceUUIDs: device.serviceUUIDs,
|
|
625
|
-
protocolHint,
|
|
626
790
|
});
|
|
627
791
|
}
|
|
628
792
|
};
|
|
@@ -634,13 +798,92 @@ export default class ReactNativeBleTransport {
|
|
|
634
798
|
});
|
|
635
799
|
}
|
|
636
800
|
|
|
801
|
+
private async installTransportForAcquire(
|
|
802
|
+
uuid: string,
|
|
803
|
+
device: Device,
|
|
804
|
+
characteristics?: ResolvedBleCharacteristics
|
|
805
|
+
) {
|
|
806
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
807
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
808
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
809
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
810
|
+
const monitorToken = this.nextMonitorToken;
|
|
811
|
+
this.nextMonitorToken += 1;
|
|
812
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
813
|
+
transport.monitorToken = monitorToken;
|
|
814
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
815
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
816
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
817
|
+
transport.notifyCharacteristic,
|
|
818
|
+
uuid,
|
|
819
|
+
monitorToken,
|
|
820
|
+
notifyTransactionId
|
|
821
|
+
);
|
|
822
|
+
transportCache[uuid] = transport;
|
|
823
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
824
|
+
this.protocolV2Assemblers.set(
|
|
825
|
+
uuid,
|
|
826
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
if (Platform.OS === 'ios') {
|
|
830
|
+
await new Promise<void>(resolve => {
|
|
831
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
832
|
+
});
|
|
833
|
+
} else if (Platform.OS === 'android') {
|
|
834
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const initialMtu = transport.mtuSize;
|
|
838
|
+
let refreshAttempts = 0;
|
|
839
|
+
if (
|
|
840
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
841
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
842
|
+
) {
|
|
843
|
+
refreshAttempts += 1;
|
|
844
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
845
|
+
transport.device,
|
|
846
|
+
'servicesAndNotifyReady',
|
|
847
|
+
1
|
|
848
|
+
);
|
|
849
|
+
transport.device = refreshedDevice;
|
|
850
|
+
transport.mtuSize =
|
|
851
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
852
|
+
|
|
853
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
854
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
855
|
+
refreshAttempts += 1;
|
|
856
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
857
|
+
transport.device = refreshedDevice;
|
|
858
|
+
transport.mtuSize =
|
|
859
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
864
|
+
platform: Platform.OS,
|
|
865
|
+
requested: getRequestedBleMtu(),
|
|
866
|
+
initial: initialMtu,
|
|
867
|
+
actual: transport.mtuSize,
|
|
868
|
+
refreshAttempts,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
return transport;
|
|
872
|
+
}
|
|
873
|
+
|
|
637
874
|
async acquire(input: BleAcquireInput) {
|
|
638
|
-
const { uuid
|
|
875
|
+
const { uuid } = input;
|
|
639
876
|
|
|
640
877
|
if (!uuid) {
|
|
641
878
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
642
879
|
}
|
|
643
880
|
|
|
881
|
+
return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
private async acquireUnlocked(input: BleAcquireInput) {
|
|
885
|
+
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
886
|
+
|
|
644
887
|
const cachedTransport = transportCache[uuid];
|
|
645
888
|
if (cachedTransport) {
|
|
646
889
|
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
@@ -659,7 +902,7 @@ export default class ReactNativeBleTransport {
|
|
|
659
902
|
* connection, clean it up before creating a new transport instance.
|
|
660
903
|
*/
|
|
661
904
|
Log?.debug('transport not reusable, will release: ', uuid);
|
|
662
|
-
await this.
|
|
905
|
+
await this.releaseUnlocked(uuid, true);
|
|
663
906
|
}
|
|
664
907
|
|
|
665
908
|
let device: Device | null = null;
|
|
@@ -667,8 +910,8 @@ export default class ReactNativeBleTransport {
|
|
|
667
910
|
if (forceCleanRunPromise && this.runPromise) {
|
|
668
911
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
669
912
|
this.runPromise.reject(error);
|
|
670
|
-
this.rejectAllProtocolV2Frames(error);
|
|
671
913
|
this.runPromise = null;
|
|
914
|
+
this.runPromiseDeviceId = null;
|
|
672
915
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
673
916
|
}
|
|
674
917
|
|
|
@@ -704,15 +947,22 @@ export default class ReactNativeBleTransport {
|
|
|
704
947
|
if (!device) {
|
|
705
948
|
Log?.debug('try to connect to device: ', uuid);
|
|
706
949
|
try {
|
|
707
|
-
device = await
|
|
950
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
951
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
952
|
+
);
|
|
708
953
|
} catch (e) {
|
|
709
954
|
Log?.debug('try to connect to device has error: ', e);
|
|
955
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
956
|
+
throw e;
|
|
957
|
+
}
|
|
710
958
|
if (
|
|
711
959
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
712
960
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
713
961
|
) {
|
|
714
962
|
Log?.debug('first try to reconnect without params');
|
|
715
|
-
device = await
|
|
963
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
964
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
965
|
+
);
|
|
716
966
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
717
967
|
Log?.debug('device already connected');
|
|
718
968
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -728,26 +978,36 @@ export default class ReactNativeBleTransport {
|
|
|
728
978
|
|
|
729
979
|
if (!(await device.isConnected())) {
|
|
730
980
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
981
|
+
const disconnectedDevice = device;
|
|
731
982
|
|
|
732
983
|
try {
|
|
733
|
-
device = await
|
|
984
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
985
|
+
disconnectedDevice.connect(connectOptions)
|
|
986
|
+
);
|
|
734
987
|
} catch (e) {
|
|
735
988
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
989
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
990
|
+
throw e;
|
|
991
|
+
}
|
|
736
992
|
if (
|
|
737
993
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
738
994
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
739
995
|
) {
|
|
740
996
|
Log?.debug('second try to reconnect without params');
|
|
741
997
|
try {
|
|
742
|
-
device = await
|
|
998
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
999
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1000
|
+
);
|
|
743
1001
|
} catch (e) {
|
|
744
1002
|
Log?.debug('last try to reconnect error: ', e);
|
|
745
1003
|
// last try to reconnect device if this issue exists
|
|
746
1004
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
747
1005
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
748
1006
|
Log?.debug('last try to reconnect');
|
|
749
|
-
await
|
|
750
|
-
device = await
|
|
1007
|
+
await disconnectedDevice.cancelConnection();
|
|
1008
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1009
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1010
|
+
);
|
|
751
1011
|
}
|
|
752
1012
|
}
|
|
753
1013
|
} else {
|
|
@@ -756,59 +1016,49 @@ export default class ReactNativeBleTransport {
|
|
|
756
1016
|
}
|
|
757
1017
|
}
|
|
758
1018
|
|
|
759
|
-
device = await
|
|
760
|
-
const
|
|
1019
|
+
device = await resolveNegotiatedMtu(device);
|
|
1020
|
+
const acquiredDevice = device;
|
|
1021
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1022
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
761
1023
|
|
|
762
1024
|
const protocolHint = expectedProtocol
|
|
763
1025
|
? undefined
|
|
764
|
-
: this.deviceProtocolHints.get(uuid)
|
|
765
|
-
inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
|
|
1026
|
+
: input.protocolHint ?? this.deviceProtocolHints.get(uuid);
|
|
766
1027
|
|
|
767
1028
|
// release transport before new transport instance
|
|
768
|
-
await this.
|
|
1029
|
+
await this.releaseUnlocked(uuid, true);
|
|
769
1030
|
if (protocolHint) {
|
|
770
1031
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
771
1032
|
}
|
|
772
1033
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
const monitorToken = this.nextMonitorToken;
|
|
778
|
-
this.nextMonitorToken += 1;
|
|
779
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
780
|
-
transport.monitorToken = monitorToken;
|
|
781
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
782
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
783
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
784
|
-
transport.notifyCharacteristic,
|
|
785
|
-
uuid,
|
|
786
|
-
monitorToken,
|
|
787
|
-
notifyTransactionId
|
|
788
|
-
);
|
|
789
|
-
transportCache[uuid] = transport;
|
|
790
|
-
|
|
791
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
792
|
-
|
|
793
|
-
if (Platform.OS === 'ios') {
|
|
794
|
-
await new Promise<void>(resolve => {
|
|
795
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
796
|
-
});
|
|
797
|
-
} else if (Platform.OS === 'android') {
|
|
798
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
802
|
-
|
|
803
|
-
this.emitter?.emit('device-connect', {
|
|
804
|
-
name: device.name,
|
|
805
|
-
id: device.id,
|
|
806
|
-
connectId: device.id,
|
|
1034
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1035
|
+
writeCharacteristic,
|
|
1036
|
+
notifyCharacteristic,
|
|
807
1037
|
});
|
|
808
1038
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1039
|
+
try {
|
|
1040
|
+
const protocolType = await this.detectProtocol(
|
|
1041
|
+
uuid,
|
|
1042
|
+
expectedProtocol,
|
|
1043
|
+
protocolHint,
|
|
1044
|
+
async () => {
|
|
1045
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1046
|
+
}
|
|
1047
|
+
);
|
|
1048
|
+
const currentTransport = transportCache[uuid];
|
|
1049
|
+
if (!currentTransport) {
|
|
1050
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1051
|
+
}
|
|
1052
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1053
|
+
return { uuid, protocolType };
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (isBleStaleBondHardwareError(error)) {
|
|
1056
|
+
await this.disconnectUnlocked(uuid);
|
|
1057
|
+
} else {
|
|
1058
|
+
await this.releaseUnlocked(uuid, true);
|
|
1059
|
+
}
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
812
1062
|
}
|
|
813
1063
|
|
|
814
1064
|
_monitorCharacteristic(
|
|
@@ -835,17 +1085,18 @@ export default class ReactNativeBleTransport {
|
|
|
835
1085
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
836
1086
|
return;
|
|
837
1087
|
}
|
|
838
|
-
if (
|
|
1088
|
+
if (isNativeBleStaleBondError(error)) {
|
|
1089
|
+
this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
839
1093
|
let errorCode:
|
|
840
|
-
| typeof HardwareErrorCode.BleDeviceBondError
|
|
841
1094
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
842
1095
|
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
843
1096
|
| typeof HardwareErrorCode.BleTimeoutError =
|
|
844
1097
|
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
845
1098
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
846
1099
|
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
847
|
-
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
848
|
-
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
849
1100
|
} else if (
|
|
850
1101
|
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
851
1102
|
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
@@ -858,18 +1109,14 @@ export default class ReactNativeBleTransport {
|
|
|
858
1109
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
859
1110
|
return;
|
|
860
1111
|
}
|
|
861
|
-
if (this.runPromise) {
|
|
1112
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
862
1113
|
let ERROR:
|
|
863
|
-
| typeof HardwareErrorCode.BleDeviceBondError
|
|
864
1114
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
865
1115
|
| typeof HardwareErrorCode.BleTimeoutError =
|
|
866
1116
|
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
867
1117
|
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
868
1118
|
ERROR = HardwareErrorCode.BleTimeoutError;
|
|
869
1119
|
}
|
|
870
|
-
if (error.reason?.includes('Encryption is insufficient')) {
|
|
871
|
-
ERROR = HardwareErrorCode.BleDeviceBondError;
|
|
872
|
-
}
|
|
873
1120
|
if (
|
|
874
1121
|
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
875
1122
|
error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
|
|
@@ -881,7 +1128,6 @@ export default class ReactNativeBleTransport {
|
|
|
881
1128
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
882
1129
|
);
|
|
883
1130
|
this.runPromise.reject(notifyError);
|
|
884
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
885
1131
|
Log?.debug(
|
|
886
1132
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
887
1133
|
);
|
|
@@ -889,7 +1135,6 @@ export default class ReactNativeBleTransport {
|
|
|
889
1135
|
}
|
|
890
1136
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
891
1137
|
this.runPromise.reject(notifyError);
|
|
892
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
893
1138
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
894
1139
|
}
|
|
895
1140
|
|
|
@@ -907,7 +1152,7 @@ export default class ReactNativeBleTransport {
|
|
|
907
1152
|
|
|
908
1153
|
try {
|
|
909
1154
|
const data = Buffer.from(c.value as string, 'base64');
|
|
910
|
-
const protocol = this.
|
|
1155
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
911
1156
|
if (!protocol) {
|
|
912
1157
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
913
1158
|
return;
|
|
@@ -934,14 +1179,16 @@ export default class ReactNativeBleTransport {
|
|
|
934
1179
|
// );
|
|
935
1180
|
bufferLength = 0;
|
|
936
1181
|
buffer = [];
|
|
937
|
-
this.
|
|
1182
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1183
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1184
|
+
}
|
|
938
1185
|
}
|
|
939
1186
|
} catch (error) {
|
|
940
1187
|
Log?.debug('monitor data error: ', error);
|
|
941
1188
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
942
|
-
if (this.
|
|
1189
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
943
1190
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
944
|
-
} else {
|
|
1191
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
945
1192
|
this.runPromise?.reject(notifyError);
|
|
946
1193
|
}
|
|
947
1194
|
}
|
|
@@ -951,13 +1198,23 @@ export default class ReactNativeBleTransport {
|
|
|
951
1198
|
}
|
|
952
1199
|
|
|
953
1200
|
async release(uuid: string, onclose = false) {
|
|
954
|
-
|
|
1201
|
+
return this.runLifecycleOperation(uuid, () => this.releaseUnlocked(uuid, onclose));
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private async releaseUnlocked(uuid: string, onclose = false) {
|
|
955
1205
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
956
|
-
|
|
1206
|
+
return this.releaseNative(uuid, onclose);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
1210
|
+
const transport = transportCache[uuid];
|
|
1211
|
+
const manager = this.blePlxManager;
|
|
1212
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
957
1213
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
958
1214
|
this.runPromise.reject(error);
|
|
959
1215
|
this.runPromise = null;
|
|
960
|
-
this.
|
|
1216
|
+
this.runPromiseDeviceId = null;
|
|
1217
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
961
1218
|
} else {
|
|
962
1219
|
this.resetProtocolV2Frames(uuid);
|
|
963
1220
|
}
|
|
@@ -985,34 +1242,57 @@ export default class ReactNativeBleTransport {
|
|
|
985
1242
|
);
|
|
986
1243
|
transport.notifySubscription?.remove();
|
|
987
1244
|
transport.notifySubscription = undefined;
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
try {
|
|
991
|
-
await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
|
|
992
|
-
} catch (e) {
|
|
993
|
-
Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
|
|
994
|
-
}
|
|
1245
|
+
if (transportCache[uuid] === transport) {
|
|
1246
|
+
delete transportCache[uuid];
|
|
995
1247
|
}
|
|
996
|
-
|
|
997
|
-
delete transportCache[uuid];
|
|
998
1248
|
}
|
|
999
1249
|
|
|
1250
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1251
|
+
|
|
1000
1252
|
this.deviceProtocol.delete(uuid);
|
|
1001
|
-
|
|
1253
|
+
this.probingProtocols.delete(uuid);
|
|
1254
|
+
this.staleBondErrors.delete(uuid);
|
|
1255
|
+
// Confirmed protocol and caller hints stay in deviceProtocol / protocolHint.
|
|
1002
1256
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1003
1257
|
this.protocolV2Assemblers.delete(uuid);
|
|
1004
1258
|
this.resetProtocolV2Frames(uuid);
|
|
1005
1259
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1260
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
1261
|
+
const operations: Promise<unknown>[] = [
|
|
1262
|
+
this.runBestEffortNativeOperation('release: restore connection priority', () =>
|
|
1263
|
+
this.restoreAndroidConnectionPriority(uuid, transport)
|
|
1264
|
+
),
|
|
1265
|
+
];
|
|
1266
|
+
if (transport?.notifyTransactionId && manager) {
|
|
1267
|
+
operations.push(
|
|
1268
|
+
this.runBestEffortNativeOperation('release: cancel notify transaction', () =>
|
|
1269
|
+
manager.cancelTransaction(transport.notifyTransactionId as string)
|
|
1270
|
+
)
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
if (manager) {
|
|
1274
|
+
operations.push(
|
|
1275
|
+
this.runBestEffortNativeOperation('release: cancel transaction', () =>
|
|
1276
|
+
manager.cancelTransaction(uuid)
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
await Promise.all(operations);
|
|
1281
|
+
});
|
|
1011
1282
|
|
|
1012
1283
|
return Promise.resolve(true);
|
|
1013
1284
|
}
|
|
1014
1285
|
|
|
1015
1286
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
1287
|
+
if (this.getProtocolType(session) === 'V2') {
|
|
1288
|
+
await this.protocolV2Links.sendFlowControl(
|
|
1289
|
+
session,
|
|
1290
|
+
() => this.createProtocolV2Adapter(session),
|
|
1291
|
+
name,
|
|
1292
|
+
data
|
|
1293
|
+
);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1016
1296
|
await this.call(session, name, data);
|
|
1017
1297
|
}
|
|
1018
1298
|
|
|
@@ -1037,8 +1317,6 @@ export default class ReactNativeBleTransport {
|
|
|
1037
1317
|
`Device protocol has not been detected for ${uuid}`
|
|
1038
1318
|
);
|
|
1039
1319
|
}
|
|
1040
|
-
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1041
|
-
|
|
1042
1320
|
if (protocol === 'V2') {
|
|
1043
1321
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1044
1322
|
}
|
|
@@ -1064,7 +1342,25 @@ export default class ReactNativeBleTransport {
|
|
|
1064
1342
|
const transport = this.getCachedTransport(uuid);
|
|
1065
1343
|
const runPromise = createDeferred<string>();
|
|
1066
1344
|
runPromise.promise.catch(() => undefined);
|
|
1345
|
+
const supersededRunPromise = this.runPromise;
|
|
1346
|
+
if (supersededRunPromise) {
|
|
1347
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1348
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1349
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1350
|
+
// tear down the shared connection while another call is using it.
|
|
1351
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1352
|
+
}
|
|
1067
1353
|
this.runPromise = runPromise;
|
|
1354
|
+
this.runPromiseDeviceId = uuid;
|
|
1355
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1356
|
+
// only the call that still owns the slot may release it.
|
|
1357
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1358
|
+
if (this.runPromise === runPromise) {
|
|
1359
|
+
this.runPromise = null;
|
|
1360
|
+
this.runPromiseDeviceId = null;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1068
1364
|
const messages = this._messages;
|
|
1069
1365
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1070
1366
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1090,6 +1386,12 @@ export default class ReactNativeBleTransport {
|
|
|
1090
1386
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1091
1387
|
} catch (e) {
|
|
1092
1388
|
onError(e);
|
|
1389
|
+
if (isWedgedWriteError(e)) {
|
|
1390
|
+
throw e;
|
|
1391
|
+
}
|
|
1392
|
+
if (isNativeBleStaleBondError(e) || isBleStaleBondHardwareError(e)) {
|
|
1393
|
+
throw toBleStaleBondHardwareError(e);
|
|
1394
|
+
}
|
|
1093
1395
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1094
1396
|
}
|
|
1095
1397
|
}
|
|
@@ -1121,6 +1423,12 @@ export default class ReactNativeBleTransport {
|
|
|
1121
1423
|
}
|
|
1122
1424
|
} catch (e) {
|
|
1123
1425
|
onError(e);
|
|
1426
|
+
if (isWedgedWriteError(e)) {
|
|
1427
|
+
throw e;
|
|
1428
|
+
}
|
|
1429
|
+
if (isNativeBleStaleBondError(e) || isBleStaleBondHardwareError(e)) {
|
|
1430
|
+
throw toBleStaleBondHardwareError(e);
|
|
1431
|
+
}
|
|
1124
1432
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1125
1433
|
}
|
|
1126
1434
|
}
|
|
@@ -1134,9 +1442,15 @@ export default class ReactNativeBleTransport {
|
|
|
1134
1442
|
if (name === 'EmmcFileWrite') {
|
|
1135
1443
|
await writeChunkedData(
|
|
1136
1444
|
buffers,
|
|
1137
|
-
data =>
|
|
1445
|
+
data =>
|
|
1446
|
+
this.writeBlePacket(
|
|
1447
|
+
uuid,
|
|
1448
|
+
data,
|
|
1449
|
+
payload => transport.writeWithRetry(payload),
|
|
1450
|
+
isCurrentOwner
|
|
1451
|
+
),
|
|
1138
1452
|
e => {
|
|
1139
|
-
|
|
1453
|
+
releaseOwnershipIfCurrent();
|
|
1140
1454
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1141
1455
|
}
|
|
1142
1456
|
);
|
|
@@ -1157,43 +1471,31 @@ export default class ReactNativeBleTransport {
|
|
|
1157
1471
|
// eslint-disable-next-line no-constant-condition
|
|
1158
1472
|
while (true) {
|
|
1159
1473
|
try {
|
|
1160
|
-
await
|
|
1474
|
+
await this.writeBlePacket(
|
|
1475
|
+
uuid,
|
|
1476
|
+
data,
|
|
1477
|
+
payload => transport.writeWithRetry(payload),
|
|
1478
|
+
isCurrentOwner
|
|
1479
|
+
);
|
|
1161
1480
|
return;
|
|
1162
1481
|
} catch (error) {
|
|
1163
1482
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1164
1483
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1165
1484
|
throw error;
|
|
1166
1485
|
}
|
|
1167
|
-
const
|
|
1168
|
-
const delayMs = shouldReconnect
|
|
1169
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1170
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1486
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1171
1487
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1172
1488
|
attempt: attempt + 1,
|
|
1173
1489
|
delayMs,
|
|
1174
|
-
reconnect: shouldReconnect,
|
|
1175
1490
|
error,
|
|
1176
1491
|
});
|
|
1177
|
-
if (shouldReconnect) {
|
|
1178
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1179
|
-
}
|
|
1180
1492
|
await delay(delayMs);
|
|
1181
1493
|
attempt += 1;
|
|
1182
|
-
if (shouldReconnect) {
|
|
1183
|
-
try {
|
|
1184
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1185
|
-
} catch (e) {
|
|
1186
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1187
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1188
|
-
throw e;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
1494
|
}
|
|
1193
1495
|
}
|
|
1194
1496
|
},
|
|
1195
1497
|
e => {
|
|
1196
|
-
|
|
1498
|
+
releaseOwnershipIfCurrent();
|
|
1197
1499
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1198
1500
|
}
|
|
1199
1501
|
);
|
|
@@ -1202,10 +1504,28 @@ export default class ReactNativeBleTransport {
|
|
|
1202
1504
|
const outData = o.toString('base64');
|
|
1203
1505
|
// Upload resources on low-end phones may OOM
|
|
1204
1506
|
try {
|
|
1205
|
-
|
|
1507
|
+
const shouldUseWriteWithResponse =
|
|
1508
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1509
|
+
await this.writeBlePacket(
|
|
1510
|
+
uuid,
|
|
1511
|
+
outData,
|
|
1512
|
+
payload =>
|
|
1513
|
+
shouldUseWriteWithResponse
|
|
1514
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1515
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1516
|
+
isCurrentOwner
|
|
1517
|
+
);
|
|
1206
1518
|
} catch (e) {
|
|
1207
1519
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1208
|
-
|
|
1520
|
+
releaseOwnershipIfCurrent();
|
|
1521
|
+
if (isWedgedWriteError(e)) {
|
|
1522
|
+
throw e;
|
|
1523
|
+
}
|
|
1524
|
+
if (isNativeBleStaleBondError(e) || isBleStaleBondHardwareError(e)) {
|
|
1525
|
+
const bondError = toBleStaleBondHardwareError(e);
|
|
1526
|
+
this.rememberStaleBondError(uuid, bondError);
|
|
1527
|
+
throw bondError;
|
|
1528
|
+
}
|
|
1209
1529
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1210
1530
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1211
1531
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1241,16 +1561,30 @@ export default class ReactNativeBleTransport {
|
|
|
1241
1561
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1242
1562
|
return check.call(jsonData);
|
|
1243
1563
|
} catch (e) {
|
|
1244
|
-
if (name === '
|
|
1245
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1564
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1565
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1246
1566
|
} else {
|
|
1247
1567
|
Log?.error('call error: ', e);
|
|
1248
1568
|
}
|
|
1569
|
+
const isProbeTimeout =
|
|
1570
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1571
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1572
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1573
|
+
// call is actively using.
|
|
1574
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1575
|
+
if (
|
|
1576
|
+
!isProbeTimeout &&
|
|
1577
|
+
!isStaleCall &&
|
|
1578
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1579
|
+
) {
|
|
1580
|
+
await this.disconnect(uuid);
|
|
1581
|
+
}
|
|
1249
1582
|
throw e;
|
|
1250
1583
|
} finally {
|
|
1251
1584
|
if (timeout) clearTimeout(timeout);
|
|
1252
1585
|
if (this.runPromise === runPromise) {
|
|
1253
1586
|
this.runPromise = null;
|
|
1587
|
+
this.runPromiseDeviceId = null;
|
|
1254
1588
|
}
|
|
1255
1589
|
}
|
|
1256
1590
|
}
|
|
@@ -1260,8 +1594,14 @@ export default class ReactNativeBleTransport {
|
|
|
1260
1594
|
}
|
|
1261
1595
|
|
|
1262
1596
|
async disconnect(session: string) {
|
|
1597
|
+
return this.runLifecycleOperation(session, () => this.disconnectUnlocked(session));
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
private async disconnectUnlocked(session: string) {
|
|
1263
1601
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1264
1602
|
const transport = transportCache[session];
|
|
1603
|
+
const manager = this.blePlxManager;
|
|
1604
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1265
1605
|
|
|
1266
1606
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1267
1607
|
// from being triggered when we cancel the connection below
|
|
@@ -1289,60 +1629,255 @@ export default class ReactNativeBleTransport {
|
|
|
1289
1629
|
}
|
|
1290
1630
|
}
|
|
1291
1631
|
|
|
1292
|
-
// cancel the ble transaction
|
|
1293
|
-
if (session) {
|
|
1294
|
-
try {
|
|
1295
|
-
await this.blePlxManager?.cancelTransaction(session);
|
|
1296
|
-
} catch (e) {
|
|
1297
|
-
Log?.debug('resetSession: cancel transaction error (ignored): ', e?.message || e);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// disconnect the device via the device object
|
|
1302
|
-
if (transport?.device) {
|
|
1303
|
-
try {
|
|
1304
|
-
await transport.device.cancelConnection();
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
Log?.debug('resetSession: device.cancelConnection error (ignored): ', e?.message || e);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// disconnect the device via the ble manager
|
|
1311
|
-
try {
|
|
1312
|
-
await this.blePlxManager?.cancelDeviceConnection(session);
|
|
1313
|
-
} catch (e) {
|
|
1314
|
-
Log?.debug('resetSession: manager.cancelDeviceConnection error (ignored): ', e?.message || e);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
1632
|
// clear the transport cache
|
|
1318
|
-
if (transportCache[session]) {
|
|
1633
|
+
if (!transport || transportCache[session] === transport) {
|
|
1319
1634
|
delete transportCache[session];
|
|
1320
1635
|
}
|
|
1321
1636
|
this.deviceProtocol.delete(session);
|
|
1637
|
+
this.probingProtocols.delete(session);
|
|
1638
|
+
this.staleBondErrors.delete(session);
|
|
1322
1639
|
this.deviceProtocolHints.delete(session);
|
|
1640
|
+
this.sessionProtocols.delete(session);
|
|
1641
|
+
this.protocolReprobeFailures.delete(session);
|
|
1323
1642
|
this.protocolV2Assemblers.delete(session);
|
|
1324
1643
|
this.resetProtocolV2Frames(session);
|
|
1325
1644
|
|
|
1326
1645
|
// emit the disconnect event
|
|
1327
1646
|
try {
|
|
1328
|
-
this.
|
|
1329
|
-
name: transport?.device?.name,
|
|
1330
|
-
id: session,
|
|
1331
|
-
connectId: session,
|
|
1332
|
-
});
|
|
1647
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1333
1648
|
} catch (e) {
|
|
1334
1649
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1335
1650
|
}
|
|
1651
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1652
|
+
this.monitorTokens.delete(session);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
await this.runNativeTeardown(session, manager, async () => {
|
|
1656
|
+
const operations: Promise<unknown>[] = [];
|
|
1657
|
+
if (manager) {
|
|
1658
|
+
operations.push(
|
|
1659
|
+
this.runBestEffortNativeOperation('disconnect: cancel transaction', () =>
|
|
1660
|
+
manager.cancelTransaction(session)
|
|
1661
|
+
)
|
|
1662
|
+
);
|
|
1663
|
+
operations.push(
|
|
1664
|
+
this.runBestEffortNativeOperation('disconnect: cancel device connection', () =>
|
|
1665
|
+
manager.cancelDeviceConnection(session)
|
|
1666
|
+
)
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (transport?.device) {
|
|
1670
|
+
operations.push(
|
|
1671
|
+
this.runBestEffortNativeOperation('disconnect: device cancel connection', () =>
|
|
1672
|
+
transport.device.cancelConnection()
|
|
1673
|
+
)
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
await Promise.all(operations);
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1336
1679
|
// eslint-disable-next-line no-promise-executor-return
|
|
1337
1680
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1338
1681
|
}
|
|
1339
1682
|
|
|
1683
|
+
private async runNativeTeardown(
|
|
1684
|
+
uuid: string,
|
|
1685
|
+
manager: BlePlxManager | undefined,
|
|
1686
|
+
teardown: () => Promise<void>
|
|
1687
|
+
) {
|
|
1688
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1689
|
+
let timedOut = false;
|
|
1690
|
+
const pending = Promise.resolve()
|
|
1691
|
+
.then(teardown)
|
|
1692
|
+
.catch(error => {
|
|
1693
|
+
Log?.debug('BLE native teardown error (ignored): ', error?.message || error);
|
|
1694
|
+
});
|
|
1695
|
+
try {
|
|
1696
|
+
await Promise.race([
|
|
1697
|
+
pending,
|
|
1698
|
+
new Promise<void>(resolve => {
|
|
1699
|
+
timer = setTimeout(() => {
|
|
1700
|
+
timedOut = true;
|
|
1701
|
+
resolve();
|
|
1702
|
+
}, BLE_NATIVE_TEARDOWN_TIMEOUT_MS);
|
|
1703
|
+
}),
|
|
1704
|
+
]);
|
|
1705
|
+
} finally {
|
|
1706
|
+
if (timer) clearTimeout(timer);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (timedOut) {
|
|
1710
|
+
Log?.error('[ReactNativeBleTransport] BLE native teardown timed out:', uuid);
|
|
1711
|
+
if (this.blePlxManager === manager) {
|
|
1712
|
+
this.resetPlxManager();
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
private runBestEffortNativeOperation(label: string, operation: () => Promise<unknown>) {
|
|
1718
|
+
return Promise.resolve()
|
|
1719
|
+
.then(operation)
|
|
1720
|
+
.catch(error => {
|
|
1721
|
+
Log?.debug(`${label} error (ignored): `, error?.message || error);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
private async runLifecycleOperation<T>(uuid: string, operation: () => Promise<T>): Promise<T> {
|
|
1726
|
+
const previousOperation = this.lifecycleOperations.get(uuid) ?? Promise.resolve();
|
|
1727
|
+
let completeOperation!: () => void;
|
|
1728
|
+
const operationGate = new Promise<void>(resolve => {
|
|
1729
|
+
completeOperation = resolve;
|
|
1730
|
+
});
|
|
1731
|
+
const operationTail = previousOperation.catch(() => undefined).then(() => operationGate);
|
|
1732
|
+
this.lifecycleOperations.set(uuid, operationTail);
|
|
1733
|
+
|
|
1734
|
+
await previousOperation.catch(() => undefined);
|
|
1735
|
+
try {
|
|
1736
|
+
return await operation();
|
|
1737
|
+
} finally {
|
|
1738
|
+
completeOperation();
|
|
1739
|
+
if (this.lifecycleOperations.get(uuid) === operationTail) {
|
|
1740
|
+
this.lifecycleOperations.delete(uuid);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1340
1745
|
cancel() {
|
|
1341
1746
|
Log?.debug('transport-react-native transport cancel');
|
|
1342
1747
|
if (this.runPromise) {
|
|
1343
1748
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1344
1749
|
}
|
|
1345
1750
|
this.runPromise = null;
|
|
1751
|
+
this.runPromiseDeviceId = null;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1755
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1756
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1757
|
+
let timedOut = false;
|
|
1758
|
+
const pending = connect();
|
|
1759
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1760
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1761
|
+
pending.catch(() => undefined);
|
|
1762
|
+
try {
|
|
1763
|
+
const result = await Promise.race([
|
|
1764
|
+
pending,
|
|
1765
|
+
new Promise<never>((_, reject) => {
|
|
1766
|
+
timer = setTimeout(() => {
|
|
1767
|
+
timedOut = true;
|
|
1768
|
+
reject(
|
|
1769
|
+
ERRORS.TypedError(
|
|
1770
|
+
HardwareErrorCode.BleConnectedError,
|
|
1771
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1772
|
+
)
|
|
1773
|
+
);
|
|
1774
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1775
|
+
}),
|
|
1776
|
+
]);
|
|
1777
|
+
return result;
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1780
|
+
const resetManager = this.abandonStalledConnection(
|
|
1781
|
+
uuid,
|
|
1782
|
+
timedOut ? 'connect-backstop' : 'connect-native'
|
|
1783
|
+
);
|
|
1784
|
+
if (resetManager) {
|
|
1785
|
+
throw this.createWedgedBleSetupError();
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
throw error;
|
|
1789
|
+
} finally {
|
|
1790
|
+
if (timer) clearTimeout(timer);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1795
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1796
|
+
uuid: string,
|
|
1797
|
+
device: Device
|
|
1798
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1799
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1800
|
+
let timedOut = false;
|
|
1801
|
+
const pending = this.resolveCharacteristics(device);
|
|
1802
|
+
pending.catch(() => undefined);
|
|
1803
|
+
try {
|
|
1804
|
+
const result = await Promise.race([
|
|
1805
|
+
pending,
|
|
1806
|
+
new Promise<never>((_, reject) => {
|
|
1807
|
+
timer = setTimeout(() => {
|
|
1808
|
+
timedOut = true;
|
|
1809
|
+
reject(
|
|
1810
|
+
ERRORS.TypedError(
|
|
1811
|
+
HardwareErrorCode.BleConnectedError,
|
|
1812
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1816
|
+
}),
|
|
1817
|
+
]);
|
|
1818
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1819
|
+
return result;
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1822
|
+
const resetManager = this.abandonStalledConnection(
|
|
1823
|
+
uuid,
|
|
1824
|
+
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
1825
|
+
);
|
|
1826
|
+
if (resetManager) {
|
|
1827
|
+
throw this.createWedgedBleSetupError();
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
throw error;
|
|
1831
|
+
} finally {
|
|
1832
|
+
if (timer) clearTimeout(timer);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1838
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1839
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1840
|
+
* Returns true when the manager itself was reset so the caller can stop Core retries.
|
|
1841
|
+
*/
|
|
1842
|
+
private abandonStalledConnection(
|
|
1843
|
+
uuid: string,
|
|
1844
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1845
|
+
): boolean {
|
|
1846
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1847
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1848
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1849
|
+
stage,
|
|
1850
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1854
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1855
|
+
});
|
|
1856
|
+
const stalled = transportCache[uuid];
|
|
1857
|
+
if (stalled) {
|
|
1858
|
+
delete transportCache[uuid];
|
|
1859
|
+
}
|
|
1860
|
+
this.deviceProtocol.delete(uuid);
|
|
1861
|
+
this.probingProtocols.delete(uuid);
|
|
1862
|
+
this.staleBondErrors.delete(uuid);
|
|
1863
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1864
|
+
this.resetProtocolV2Frames(uuid);
|
|
1865
|
+
|
|
1866
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1867
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1868
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1869
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1870
|
+
this.resetPlxManager();
|
|
1871
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private createWedgedBleSetupError() {
|
|
1878
|
+
// PollingTimeout is not retried by connectDeviceForBle and already maps
|
|
1879
|
+
// to the App "connection failed" help text.
|
|
1880
|
+
return ERRORS.TypedError(HardwareErrorCode.PollingTimeout, BLE_SETUP_WEDGED_MESSAGE);
|
|
1346
1881
|
}
|
|
1347
1882
|
|
|
1348
1883
|
private getCachedTransport(uuid: string) {
|
|
@@ -1353,9 +1888,146 @@ export default class ReactNativeBleTransport {
|
|
|
1353
1888
|
return transport;
|
|
1354
1889
|
}
|
|
1355
1890
|
|
|
1356
|
-
|
|
1891
|
+
/**
|
|
1892
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1893
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1894
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1895
|
+
* cached and every later call would hang on it again.
|
|
1896
|
+
*/
|
|
1897
|
+
private async writeBlePacket(
|
|
1898
|
+
uuid: string,
|
|
1899
|
+
data: string,
|
|
1900
|
+
write: (payload: string) => Promise<unknown>,
|
|
1901
|
+
isCurrentOwner?: () => boolean
|
|
1902
|
+
) {
|
|
1903
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1904
|
+
let timedOut = false;
|
|
1905
|
+
try {
|
|
1906
|
+
await Promise.race([
|
|
1907
|
+
write(data),
|
|
1908
|
+
new Promise<never>((_, reject) => {
|
|
1909
|
+
timer = setTimeout(() => {
|
|
1910
|
+
timedOut = true;
|
|
1911
|
+
reject(
|
|
1912
|
+
ERRORS.TypedError(
|
|
1913
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1914
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1915
|
+
)
|
|
1916
|
+
);
|
|
1917
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1918
|
+
}),
|
|
1919
|
+
]);
|
|
1920
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (timedOut) {
|
|
1923
|
+
// A superseded call's late write must not tear down the link the current
|
|
1924
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1925
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1926
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1927
|
+
} else {
|
|
1928
|
+
this.tearDownWedgedLink(uuid);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
throw error;
|
|
1932
|
+
} finally {
|
|
1933
|
+
if (timer) clearTimeout(timer);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1939
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1940
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1941
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1942
|
+
*/
|
|
1943
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1944
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1945
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1946
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1947
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
const wedged = transportCache[uuid];
|
|
1951
|
+
this.disconnect(uuid).catch(error => {
|
|
1952
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1953
|
+
});
|
|
1954
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1955
|
+
delete transportCache[uuid];
|
|
1956
|
+
}
|
|
1957
|
+
this.deviceProtocol.delete(uuid);
|
|
1958
|
+
this.probingProtocols.delete(uuid);
|
|
1959
|
+
this.staleBondErrors.delete(uuid);
|
|
1960
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1961
|
+
this.resetProtocolV2Frames(uuid);
|
|
1962
|
+
|
|
1963
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1964
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1965
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1966
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1967
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1968
|
+
this.resetPlxManager();
|
|
1969
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
private resetPlxManager() {
|
|
1974
|
+
const manager = this.blePlxManager;
|
|
1975
|
+
this.blePlxManager = undefined;
|
|
1976
|
+
const reason = 'React Native BLE manager reset';
|
|
1977
|
+
// Destroying the shared manager invalidates every peripheral it owns. Notify
|
|
1978
|
+
// each cached session before clearing generations so Core cannot retain a
|
|
1979
|
+
// silently stale connection for an unrelated device.
|
|
1980
|
+
Object.entries(transportCache).forEach(([uuid, cachedTransport]) => {
|
|
1981
|
+
try {
|
|
1982
|
+
cachedTransport.disconnectSubscription?.remove();
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
Log?.debug('BLE manager reset disconnect subscription removal failed:', error);
|
|
1985
|
+
}
|
|
1986
|
+
cachedTransport.disconnectSubscription = undefined;
|
|
1987
|
+
try {
|
|
1988
|
+
cachedTransport.notifySubscription?.remove();
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
Log?.debug('BLE manager reset notify subscription removal failed:', error);
|
|
1991
|
+
}
|
|
1992
|
+
cachedTransport.notifySubscription = undefined;
|
|
1993
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1994
|
+
try {
|
|
1995
|
+
this.emitDeviceDisconnect(
|
|
1996
|
+
uuid,
|
|
1997
|
+
cachedTransport.device?.name,
|
|
1998
|
+
cachedTransport.monitorToken ?? this.monitorTokens.get(uuid)
|
|
1999
|
+
);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
Log?.debug('BLE manager reset disconnect event failed:', error);
|
|
2002
|
+
}
|
|
2003
|
+
delete transportCache[uuid];
|
|
2004
|
+
});
|
|
2005
|
+
this.protocolV2Links.invalidateAllLinks(reason).catch(error => {
|
|
2006
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager link invalidation failed:', error);
|
|
2007
|
+
});
|
|
2008
|
+
this.deviceProtocol.clear();
|
|
2009
|
+
this.probingProtocols.clear();
|
|
2010
|
+
this.staleBondErrors.clear();
|
|
2011
|
+
this.sessionProtocols.clear();
|
|
2012
|
+
this.confirmedProtocolV2.clear();
|
|
2013
|
+
this.protocolReprobeFailures.clear();
|
|
2014
|
+
this.writeTimeoutCounts.clear();
|
|
2015
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
2016
|
+
this.monitorTokens.clear();
|
|
2017
|
+
this.protocolV2Assemblers.clear();
|
|
2018
|
+
try {
|
|
2019
|
+
manager?.destroy();
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
|
|
2026
|
+
// A generic Ping miss is not a bond failure. Only a later miss after this
|
|
2027
|
+
// endpoint already answered V2, or a native encryption/pairing error, is.
|
|
2028
|
+
const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
|
|
1357
2029
|
return ERRORS.TypedError(
|
|
1358
|
-
HardwareErrorCode.RuntimeError,
|
|
2030
|
+
isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
|
|
1359
2031
|
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
1360
2032
|
);
|
|
1361
2033
|
}
|
|
@@ -1363,24 +2035,51 @@ export default class ReactNativeBleTransport {
|
|
|
1363
2035
|
private createProtocolDetectionError() {
|
|
1364
2036
|
return ERRORS.TypedError(
|
|
1365
2037
|
HardwareErrorCode.BleTimeoutError,
|
|
1366
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
2038
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1367
2039
|
);
|
|
1368
2040
|
}
|
|
1369
2041
|
|
|
1370
2042
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
2043
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
2044
|
+
this.probingProtocols.delete(uuid);
|
|
2045
|
+
}
|
|
1371
2046
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1372
2047
|
this.deviceProtocol.delete(uuid);
|
|
1373
2048
|
}
|
|
1374
2049
|
}
|
|
1375
2050
|
|
|
2051
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
2052
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
2053
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1376
2056
|
private async detectProtocol(
|
|
1377
2057
|
uuid: string,
|
|
1378
2058
|
expectedProtocol?: ProtocolType,
|
|
1379
|
-
protocolHint?: ProtocolType
|
|
2059
|
+
protocolHint?: ProtocolType,
|
|
2060
|
+
rebuildTransport?: () => Promise<void>
|
|
1380
2061
|
): Promise<ProtocolType> {
|
|
2062
|
+
// iOS still skips an extra V1 Initialize during acquire. Expected V2 must
|
|
2063
|
+
// Ping so USB-priority `link disabled` can surface instead of a later
|
|
2064
|
+
// unmapped RuntimeError.
|
|
2065
|
+
if (Platform.OS === 'ios' && expectedProtocol === 'V1') {
|
|
2066
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
2067
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
2068
|
+
deviceId: uuid,
|
|
2069
|
+
protocol: expectedProtocol,
|
|
2070
|
+
source: 'expected',
|
|
2071
|
+
});
|
|
2072
|
+
return expectedProtocol;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// iOS Classic/Touch/Pro skip probing above. Pro2/Neo still probe, and a stale
|
|
2076
|
+
// OS bond must not wait for Ping/GetFeatures timeouts after ATT 14/15.
|
|
2077
|
+
this.throwIfStaleBondError(uuid);
|
|
2078
|
+
|
|
1381
2079
|
if (expectedProtocol === 'V1') {
|
|
1382
2080
|
if (await this.probeProtocolV1(uuid)) {
|
|
1383
2081
|
this.deviceProtocol.set(uuid, 'V1');
|
|
2082
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1384
2083
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1385
2084
|
deviceId: uuid,
|
|
1386
2085
|
protocol: 'V1',
|
|
@@ -1388,37 +2087,60 @@ export default class ReactNativeBleTransport {
|
|
|
1388
2087
|
});
|
|
1389
2088
|
return 'V1';
|
|
1390
2089
|
}
|
|
1391
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
2090
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
1392
2091
|
}
|
|
1393
2092
|
|
|
1394
2093
|
if (expectedProtocol === 'V2') {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
2094
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
2095
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
2096
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
2097
|
+
this.confirmedProtocolV2.add(uuid);
|
|
2098
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2099
|
+
deviceId: uuid,
|
|
2100
|
+
protocol: 'V2',
|
|
2101
|
+
source: 'expected',
|
|
2102
|
+
});
|
|
2103
|
+
return 'V2';
|
|
2104
|
+
}
|
|
2105
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
1404
2106
|
}
|
|
1405
2107
|
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1409
|
-
const
|
|
2108
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
2109
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
2110
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
2111
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
2112
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1410
2113
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
2114
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
2115
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
2116
|
+
// only adds its timeout to every poll.
|
|
2117
|
+
const trustSessionProtocol =
|
|
2118
|
+
sessionProtocol !== undefined &&
|
|
2119
|
+
!protocolHint &&
|
|
2120
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2121
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1411
2122
|
|
|
1412
2123
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1413
2124
|
const protocol = probeOrder[i];
|
|
1414
2125
|
if (i > 0) {
|
|
1415
|
-
//
|
|
2126
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1416
2127
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
2128
|
+
if (!transportCache[uuid]) {
|
|
2129
|
+
if (!rebuildTransport) {
|
|
2130
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
2131
|
+
}
|
|
2132
|
+
await rebuildTransport();
|
|
2133
|
+
}
|
|
1417
2134
|
}
|
|
1418
2135
|
const detected =
|
|
1419
2136
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1420
2137
|
if (detected) {
|
|
1421
2138
|
this.deviceProtocol.set(uuid, protocol);
|
|
2139
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
2140
|
+
if (protocol === 'V2') {
|
|
2141
|
+
this.confirmedProtocolV2.add(uuid);
|
|
2142
|
+
}
|
|
2143
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1422
2144
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1423
2145
|
deviceId: uuid,
|
|
1424
2146
|
protocol,
|
|
@@ -1428,7 +2150,16 @@ export default class ReactNativeBleTransport {
|
|
|
1428
2150
|
}
|
|
1429
2151
|
}
|
|
1430
2152
|
|
|
2153
|
+
if (trustSessionProtocol) {
|
|
2154
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2155
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
2156
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
2157
|
+
} else {
|
|
2158
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1431
2161
|
this.deviceProtocol.delete(uuid);
|
|
2162
|
+
this.probingProtocols.delete(uuid);
|
|
1432
2163
|
throw this.createProtocolDetectionError();
|
|
1433
2164
|
}
|
|
1434
2165
|
|
|
@@ -1490,12 +2221,20 @@ export default class ReactNativeBleTransport {
|
|
|
1490
2221
|
}
|
|
1491
2222
|
|
|
1492
2223
|
try {
|
|
1493
|
-
this.
|
|
1494
|
-
|
|
2224
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2225
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
2226
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
2227
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2228
|
+
this.probingProtocols.delete(uuid);
|
|
1495
2229
|
return true;
|
|
1496
2230
|
} catch (error) {
|
|
1497
2231
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1498
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
2232
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2233
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2234
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2235
|
+
if (isWedgedWriteError(error) || isBleStaleBondHardwareError(error)) {
|
|
2236
|
+
throw error;
|
|
2237
|
+
}
|
|
1499
2238
|
return false;
|
|
1500
2239
|
}
|
|
1501
2240
|
}
|
|
@@ -1505,8 +2244,9 @@ export default class ReactNativeBleTransport {
|
|
|
1505
2244
|
return false;
|
|
1506
2245
|
}
|
|
1507
2246
|
|
|
1508
|
-
this.
|
|
2247
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1509
2248
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2249
|
+
this.throwIfStaleBondError(uuid);
|
|
1510
2250
|
const detected = await probeProtocolV2Helper({
|
|
1511
2251
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
1512
2252
|
this.callProtocolV2(uuid, name, data, options),
|
|
@@ -1517,9 +2257,12 @@ export default class ReactNativeBleTransport {
|
|
|
1517
2257
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1518
2258
|
this.resetProtocolV2Frames(uuid);
|
|
1519
2259
|
},
|
|
2260
|
+
shouldRethrow: isBleStaleBondHardwareError,
|
|
1520
2261
|
});
|
|
1521
2262
|
if (!detected) {
|
|
1522
2263
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2264
|
+
} else {
|
|
2265
|
+
this.probingProtocols.delete(uuid);
|
|
1523
2266
|
}
|
|
1524
2267
|
return detected;
|
|
1525
2268
|
}
|
|
@@ -1570,17 +2313,8 @@ export default class ReactNativeBleTransport {
|
|
|
1570
2313
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1571
2314
|
}
|
|
1572
2315
|
|
|
1573
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1574
|
-
this.protocolV2FrameQueues.clear();
|
|
1575
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1576
|
-
framePromise.reject(error);
|
|
1577
|
-
}
|
|
1578
|
-
this.protocolV2FramePromises.clear();
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
2316
|
private resetProtocolV2Frames(uuid: string) {
|
|
1582
|
-
this.
|
|
1583
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2317
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1584
2318
|
}
|
|
1585
2319
|
|
|
1586
2320
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -1592,6 +2326,21 @@ export default class ReactNativeBleTransport {
|
|
|
1592
2326
|
}
|
|
1593
2327
|
}
|
|
1594
2328
|
|
|
2329
|
+
private rememberStaleBondError(uuid: string, error: Error) {
|
|
2330
|
+
this.staleBondErrors.set(uuid, error);
|
|
2331
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
2332
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
2333
|
+
this.runPromise.reject(error);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
private throwIfStaleBondError(uuid: string) {
|
|
2338
|
+
const error = this.staleBondErrors.get(uuid);
|
|
2339
|
+
if (error) {
|
|
2340
|
+
throw error;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
1595
2344
|
private async readProtocolV2Frame(uuid: string) {
|
|
1596
2345
|
const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
|
|
1597
2346
|
if (queuedFrame) {
|
|
@@ -1609,19 +2358,100 @@ export default class ReactNativeBleTransport {
|
|
|
1609
2358
|
}
|
|
1610
2359
|
}
|
|
1611
2360
|
|
|
1612
|
-
private async
|
|
2361
|
+
private async writeProtocolV2Packet(
|
|
2362
|
+
uuid: string,
|
|
2363
|
+
transport: BleTransport,
|
|
2364
|
+
base64: string,
|
|
2365
|
+
context: ProtocolV2CallContext,
|
|
2366
|
+
assertCurrentGeneration: () => void
|
|
2367
|
+
) {
|
|
2368
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2369
|
+
platform: Platform.OS,
|
|
2370
|
+
highThroughput: context.highThroughput,
|
|
2371
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2372
|
+
characteristic: transport.writeCharacteristic,
|
|
2373
|
+
});
|
|
2374
|
+
let attempt = 0;
|
|
2375
|
+
for (;;) {
|
|
2376
|
+
assertCurrentGeneration();
|
|
2377
|
+
if (context.signal.aborted) {
|
|
2378
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
await this.writeBlePacket(
|
|
2382
|
+
uuid,
|
|
2383
|
+
base64,
|
|
2384
|
+
payload =>
|
|
2385
|
+
shouldUseWriteWithResponse
|
|
2386
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2387
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2388
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2389
|
+
// tear down the link that the current generation is using.
|
|
2390
|
+
() => {
|
|
2391
|
+
try {
|
|
2392
|
+
assertCurrentGeneration();
|
|
2393
|
+
return !context.signal.aborted;
|
|
2394
|
+
} catch {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
);
|
|
2399
|
+
assertCurrentGeneration();
|
|
2400
|
+
return;
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
if (isNativeBleStaleBondError(error) || isBleStaleBondHardwareError(error)) {
|
|
2403
|
+
const bondError = toBleStaleBondHardwareError(error);
|
|
2404
|
+
this.rememberStaleBondError(uuid, bondError);
|
|
2405
|
+
throw bondError;
|
|
2406
|
+
}
|
|
2407
|
+
if (
|
|
2408
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2409
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2410
|
+
) {
|
|
2411
|
+
throw error;
|
|
2412
|
+
}
|
|
2413
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2414
|
+
attempt += 1;
|
|
2415
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2416
|
+
name: context.messageName,
|
|
2417
|
+
attempt,
|
|
2418
|
+
delayMs,
|
|
2419
|
+
});
|
|
2420
|
+
await delay(delayMs);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
private async writeProtocolV2Frame(
|
|
2426
|
+
uuid: string,
|
|
2427
|
+
transport: BleTransport,
|
|
2428
|
+
frame: Uint8Array,
|
|
2429
|
+
context: ProtocolV2CallContext,
|
|
2430
|
+
assertCurrentGeneration: () => void
|
|
2431
|
+
) {
|
|
1613
2432
|
const tuning = getProtocolV2BleTuning();
|
|
1614
2433
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1615
2434
|
platform: Platform.OS,
|
|
1616
2435
|
iosPacketLength: tuning.iosPacketLength,
|
|
1617
2436
|
androidPacketLength: tuning.androidPacketLength,
|
|
1618
|
-
mtu:
|
|
2437
|
+
mtu: transport.mtuSize,
|
|
2438
|
+
});
|
|
2439
|
+
await writeProtocolV2BleFrame({
|
|
2440
|
+
frame,
|
|
2441
|
+
packetCapacity,
|
|
2442
|
+
assertActive: assertCurrentGeneration,
|
|
2443
|
+
signal: context.signal,
|
|
2444
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2445
|
+
wait: delay,
|
|
2446
|
+
writePacket: packet =>
|
|
2447
|
+
this.writeProtocolV2Packet(
|
|
2448
|
+
uuid,
|
|
2449
|
+
transport,
|
|
2450
|
+
Buffer.from(packet).toString('base64'),
|
|
2451
|
+
context,
|
|
2452
|
+
assertCurrentGeneration
|
|
2453
|
+
),
|
|
1619
2454
|
});
|
|
1620
|
-
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1621
|
-
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1622
|
-
const base64 = Buffer.from(chunk).toString('base64');
|
|
1623
|
-
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1624
|
-
}
|
|
1625
2455
|
}
|
|
1626
2456
|
|
|
1627
2457
|
private async callProtocolV2(
|
|
@@ -1634,19 +2464,45 @@ export default class ReactNativeBleTransport {
|
|
|
1634
2464
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1635
2465
|
}
|
|
1636
2466
|
|
|
1637
|
-
const callOptions =
|
|
1638
|
-
|
|
1639
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1640
|
-
};
|
|
1641
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2467
|
+
const callOptions = options;
|
|
2468
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
1642
2469
|
|
|
1643
|
-
if (
|
|
2470
|
+
if (highThroughputWrite) {
|
|
2471
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
1644
2472
|
const tuning = getProtocolV2BleTuning();
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2473
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2474
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2475
|
+
platform: Platform.OS,
|
|
2476
|
+
highThroughput: true,
|
|
2477
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2478
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
2479
|
+
});
|
|
2480
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2481
|
+
platform: Platform.OS,
|
|
2482
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2483
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2484
|
+
mtu: currentTransport.mtuSize,
|
|
1649
2485
|
});
|
|
2486
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2487
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2488
|
+
currentTransport.mtuSize
|
|
2489
|
+
)}:${packetCapacity}`;
|
|
2490
|
+
const loggedSignatures =
|
|
2491
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2492
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2493
|
+
loggedSignatures.add(logSignature);
|
|
2494
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2495
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2496
|
+
name,
|
|
2497
|
+
writeMode,
|
|
2498
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2499
|
+
packetCapacity,
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
if (highThroughputWrite) {
|
|
2505
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
1650
2506
|
}
|
|
1651
2507
|
|
|
1652
2508
|
try {
|
|
@@ -1660,6 +2516,90 @@ export default class ReactNativeBleTransport {
|
|
|
1660
2516
|
} catch (e) {
|
|
1661
2517
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1662
2518
|
throw e;
|
|
2519
|
+
} finally {
|
|
2520
|
+
if (highThroughputWrite) {
|
|
2521
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2527
|
+
const transport = this.getCachedTransport(uuid);
|
|
2528
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2529
|
+
|
|
2530
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2531
|
+
transport.device = refreshedDevice;
|
|
2532
|
+
transport.mtuSize =
|
|
2533
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2534
|
+
|
|
2535
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2536
|
+
throw ERRORS.TypedError(
|
|
2537
|
+
HardwareErrorCode.BleConnectedError,
|
|
2538
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2544
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2545
|
+
if (timerId !== undefined) {
|
|
2546
|
+
clearTimeout(timerId);
|
|
2547
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2552
|
+
if (Platform.OS !== 'android') return;
|
|
2553
|
+
|
|
2554
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2555
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2556
|
+
|
|
2557
|
+
const transport = transportCache[uuid];
|
|
2558
|
+
if (!transport) return;
|
|
2559
|
+
|
|
2560
|
+
try {
|
|
2561
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2562
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2563
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2564
|
+
priority: 'high',
|
|
2565
|
+
});
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2568
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2574
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2575
|
+
|
|
2576
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2577
|
+
const timerId = setTimeout(() => {
|
|
2578
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2579
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2580
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2581
|
+
);
|
|
2582
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2583
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2587
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2588
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
try {
|
|
2593
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2594
|
+
ConnectionPriority.Balanced
|
|
2595
|
+
);
|
|
2596
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2597
|
+
priority: 'balanced',
|
|
2598
|
+
});
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2601
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2602
|
+
});
|
|
1663
2603
|
}
|
|
1664
2604
|
}
|
|
1665
2605
|
|
|
@@ -1680,10 +2620,16 @@ export default class ReactNativeBleTransport {
|
|
|
1680
2620
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1681
2621
|
this.resetProtocolV2Frames(uuid);
|
|
1682
2622
|
},
|
|
1683
|
-
writeFrame: async (frame: Uint8Array) => {
|
|
2623
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1684
2624
|
assertCurrentGeneration();
|
|
1685
2625
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1686
|
-
await this.writeProtocolV2Frame(
|
|
2626
|
+
await this.writeProtocolV2Frame(
|
|
2627
|
+
uuid,
|
|
2628
|
+
currentTransport,
|
|
2629
|
+
frame,
|
|
2630
|
+
context,
|
|
2631
|
+
assertCurrentGeneration
|
|
2632
|
+
);
|
|
1687
2633
|
},
|
|
1688
2634
|
readFrame: async () => {
|
|
1689
2635
|
assertCurrentGeneration();
|
|
@@ -1694,6 +2640,7 @@ export default class ReactNativeBleTransport {
|
|
|
1694
2640
|
return rxFrame;
|
|
1695
2641
|
},
|
|
1696
2642
|
reset: (reason: string) => {
|
|
2643
|
+
if (this.monitorTokens.get(uuid) !== generation) return;
|
|
1697
2644
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1698
2645
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1699
2646
|
},
|
|
@@ -1708,6 +2655,6 @@ export default class ReactNativeBleTransport {
|
|
|
1708
2655
|
}
|
|
1709
2656
|
|
|
1710
2657
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1711
|
-
return this.
|
|
2658
|
+
return this.getActiveProtocol(path);
|
|
1712
2659
|
}
|
|
1713
2660
|
}
|