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