@bota-dev/react-native-sdk 0.0.2
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/README.md +279 -0
- package/lib/commonjs/BotaClient.js +223 -0
- package/lib/commonjs/BotaClient.js.map +1 -0
- package/lib/commonjs/ble/BleManager.js +494 -0
- package/lib/commonjs/ble/BleManager.js.map +1 -0
- package/lib/commonjs/ble/constants.js +166 -0
- package/lib/commonjs/ble/constants.js.map +1 -0
- package/lib/commonjs/ble/index.js +54 -0
- package/lib/commonjs/ble/index.js.map +1 -0
- package/lib/commonjs/ble/parsers.js +345 -0
- package/lib/commonjs/ble/parsers.js.map +1 -0
- package/lib/commonjs/index.js +81 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/managers/DeviceManager.js +437 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -0
- package/lib/commonjs/managers/OTAManager.js +227 -0
- package/lib/commonjs/managers/OTAManager.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +384 -0
- package/lib/commonjs/managers/RecordingManager.js.map +1 -0
- package/lib/commonjs/managers/index.js +27 -0
- package/lib/commonjs/managers/index.js.map +1 -0
- package/lib/commonjs/models/Device.js +2 -0
- package/lib/commonjs/models/Device.js.map +1 -0
- package/lib/commonjs/models/Recording.js +2 -0
- package/lib/commonjs/models/Recording.js.map +1 -0
- package/lib/commonjs/models/Status.js +6 -0
- package/lib/commonjs/models/Status.js.map +1 -0
- package/lib/commonjs/models/index.js +39 -0
- package/lib/commonjs/models/index.js.map +1 -0
- package/lib/commonjs/protocol/ProtocolHandler.js +343 -0
- package/lib/commonjs/protocol/ProtocolHandler.js.map +1 -0
- package/lib/commonjs/protocol/index.js +13 -0
- package/lib/commonjs/protocol/index.js.map +1 -0
- package/lib/commonjs/storage/StorageManager.js +333 -0
- package/lib/commonjs/storage/StorageManager.js.map +1 -0
- package/lib/commonjs/storage/index.js +19 -0
- package/lib/commonjs/storage/index.js.map +1 -0
- package/lib/commonjs/upload/S3Uploader.js +133 -0
- package/lib/commonjs/upload/S3Uploader.js.map +1 -0
- package/lib/commonjs/upload/UploadQueue.js +280 -0
- package/lib/commonjs/upload/UploadQueue.js.map +1 -0
- package/lib/commonjs/upload/index.js +20 -0
- package/lib/commonjs/upload/index.js.map +1 -0
- package/lib/commonjs/utils/errors.js +187 -0
- package/lib/commonjs/utils/errors.js.map +1 -0
- package/lib/commonjs/utils/index.js +40 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/commonjs/utils/logger.js +135 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/commonjs/utils/retry.js +160 -0
- package/lib/commonjs/utils/retry.js.map +1 -0
- package/lib/module/BotaClient.js +216 -0
- package/lib/module/BotaClient.js.map +1 -0
- package/lib/module/ble/BleManager.js +484 -0
- package/lib/module/ble/BleManager.js.map +1 -0
- package/lib/module/ble/constants.js +159 -0
- package/lib/module/ble/constants.js.map +1 -0
- package/lib/module/ble/index.js +8 -0
- package/lib/module/ble/index.js.map +1 -0
- package/lib/module/ble/parsers.js +328 -0
- package/lib/module/ble/parsers.js.map +1 -0
- package/lib/module/index.js +22 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/managers/DeviceManager.js +429 -0
- package/lib/module/managers/DeviceManager.js.map +1 -0
- package/lib/module/managers/OTAManager.js +219 -0
- package/lib/module/managers/OTAManager.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +376 -0
- package/lib/module/managers/RecordingManager.js.map +1 -0
- package/lib/module/managers/index.js +8 -0
- package/lib/module/managers/index.js.map +1 -0
- package/lib/module/models/Device.js +2 -0
- package/lib/module/models/Device.js.map +1 -0
- package/lib/module/models/Recording.js +2 -0
- package/lib/module/models/Recording.js.map +1 -0
- package/lib/module/models/Status.js +2 -0
- package/lib/module/models/Status.js.map +1 -0
- package/lib/module/models/index.js +8 -0
- package/lib/module/models/index.js.map +1 -0
- package/lib/module/protocol/ProtocolHandler.js +336 -0
- package/lib/module/protocol/ProtocolHandler.js.map +1 -0
- package/lib/module/protocol/index.js +6 -0
- package/lib/module/protocol/index.js.map +1 -0
- package/lib/module/storage/StorageManager.js +324 -0
- package/lib/module/storage/StorageManager.js.map +1 -0
- package/lib/module/storage/index.js +6 -0
- package/lib/module/storage/index.js.map +1 -0
- package/lib/module/upload/S3Uploader.js +126 -0
- package/lib/module/upload/S3Uploader.js.map +1 -0
- package/lib/module/upload/UploadQueue.js +272 -0
- package/lib/module/upload/UploadQueue.js.map +1 -0
- package/lib/module/upload/index.js +7 -0
- package/lib/module/upload/index.js.map +1 -0
- package/lib/module/utils/errors.js +173 -0
- package/lib/module/utils/errors.js.map +1 -0
- package/lib/module/utils/index.js +8 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/logger.js +129 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/retry.js +149 -0
- package/lib/module/utils/retry.js.map +1 -0
- package/lib/typescript/src/BotaClient.d.ts +77 -0
- package/lib/typescript/src/BotaClient.d.ts.map +1 -0
- package/lib/typescript/src/ble/BleManager.d.ts +111 -0
- package/lib/typescript/src/ble/BleManager.d.ts.map +1 -0
- package/lib/typescript/src/ble/constants.d.ts +111 -0
- package/lib/typescript/src/ble/constants.d.ts.map +1 -0
- package/lib/typescript/src/ble/index.d.ts +7 -0
- package/lib/typescript/src/ble/index.d.ts.map +1 -0
- package/lib/typescript/src/ble/parsers.d.ts +100 -0
- package/lib/typescript/src/ble/parsers.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/managers/DeviceManager.d.ts +84 -0
- package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/OTAManager.d.ts +78 -0
- package/lib/typescript/src/managers/OTAManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/RecordingManager.d.ts +90 -0
- package/lib/typescript/src/managers/RecordingManager.d.ts.map +1 -0
- package/lib/typescript/src/managers/index.d.ts +7 -0
- package/lib/typescript/src/managers/index.d.ts.map +1 -0
- package/lib/typescript/src/models/Device.d.ts +139 -0
- package/lib/typescript/src/models/Device.d.ts.map +1 -0
- package/lib/typescript/src/models/Recording.d.ts +110 -0
- package/lib/typescript/src/models/Recording.d.ts.map +1 -0
- package/lib/typescript/src/models/Status.d.ts +104 -0
- package/lib/typescript/src/models/Status.d.ts.map +1 -0
- package/lib/typescript/src/models/index.d.ts +7 -0
- package/lib/typescript/src/models/index.d.ts.map +1 -0
- package/lib/typescript/src/protocol/ProtocolHandler.d.ts +69 -0
- package/lib/typescript/src/protocol/ProtocolHandler.d.ts.map +1 -0
- package/lib/typescript/src/protocol/index.d.ts +5 -0
- package/lib/typescript/src/protocol/index.d.ts.map +1 -0
- package/lib/typescript/src/storage/StorageManager.d.ts +116 -0
- package/lib/typescript/src/storage/StorageManager.d.ts.map +1 -0
- package/lib/typescript/src/storage/index.d.ts +5 -0
- package/lib/typescript/src/storage/index.d.ts.map +1 -0
- package/lib/typescript/src/upload/S3Uploader.d.ts +38 -0
- package/lib/typescript/src/upload/S3Uploader.d.ts.map +1 -0
- package/lib/typescript/src/upload/UploadQueue.d.ts +95 -0
- package/lib/typescript/src/upload/UploadQueue.d.ts.map +1 -0
- package/lib/typescript/src/upload/index.d.ts +6 -0
- package/lib/typescript/src/upload/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/errors.d.ts +82 -0
- package/lib/typescript/src/utils/errors.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +7 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +68 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/lib/typescript/src/utils/retry.d.ts +53 -0
- package/lib/typescript/src/utils/retry.d.ts.map +1 -0
- package/package.json +95 -0
- package/src/BotaClient.ts +238 -0
- package/src/ble/BleManager.ts +573 -0
- package/src/ble/constants.ts +158 -0
- package/src/ble/index.ts +7 -0
- package/src/ble/parsers.ts +395 -0
- package/src/index.ts +64 -0
- package/src/managers/DeviceManager.ts +545 -0
- package/src/managers/OTAManager.ts +263 -0
- package/src/managers/RecordingManager.ts +434 -0
- package/src/managers/index.ts +12 -0
- package/src/models/Device.ts +164 -0
- package/src/models/Recording.ts +123 -0
- package/src/models/Status.ts +126 -0
- package/src/models/index.ts +7 -0
- package/src/protocol/ProtocolHandler.ts +459 -0
- package/src/protocol/index.ts +5 -0
- package/src/storage/StorageManager.ts +343 -0
- package/src/storage/index.ts +5 -0
- package/src/upload/S3Uploader.ts +164 -0
- package/src/upload/UploadQueue.ts +310 -0
- package/src/upload/index.ts +6 -0
- package/src/utils/errors.ts +310 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/logger.ts +137 -0
- package/src/utils/retry.ts +177 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLE Manager - Abstraction layer over react-native-ble-plx
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BleManager as RNBleManager,
|
|
7
|
+
Device,
|
|
8
|
+
State,
|
|
9
|
+
Subscription,
|
|
10
|
+
BleError,
|
|
11
|
+
} from 'react-native-ble-plx';
|
|
12
|
+
import { Buffer } from 'buffer';
|
|
13
|
+
import EventEmitter from 'eventemitter3';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
DEVICE_NAME_PREFIX,
|
|
17
|
+
DEFAULT_MTU,
|
|
18
|
+
MAX_MTU,
|
|
19
|
+
CONNECTION_TIMEOUT,
|
|
20
|
+
SCAN_TIMEOUT,
|
|
21
|
+
} from './constants';
|
|
22
|
+
import type {
|
|
23
|
+
DiscoveredDevice,
|
|
24
|
+
DeviceType,
|
|
25
|
+
PairingState,
|
|
26
|
+
ScanOptions,
|
|
27
|
+
} from '../models/Device';
|
|
28
|
+
import { BluetoothError, DeviceError } from '../utils/errors';
|
|
29
|
+
import { logger } from '../utils/logger';
|
|
30
|
+
import {
|
|
31
|
+
parseDeviceType,
|
|
32
|
+
parsePairingState,
|
|
33
|
+
parseFirmwareVersion,
|
|
34
|
+
} from './parsers';
|
|
35
|
+
|
|
36
|
+
const log = logger.tag('BleManager');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Events emitted by BleManager
|
|
40
|
+
*/
|
|
41
|
+
interface BleManagerEvents {
|
|
42
|
+
stateChange: (state: State) => void;
|
|
43
|
+
deviceDiscovered: (device: DiscoveredDevice) => void;
|
|
44
|
+
deviceConnected: (deviceId: string) => void;
|
|
45
|
+
deviceDisconnected: (deviceId: string, error?: Error) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* BLE Manager class - singleton wrapper around react-native-ble-plx
|
|
50
|
+
*/
|
|
51
|
+
export class BleManager extends EventEmitter<BleManagerEvents> {
|
|
52
|
+
private manager: RNBleManager;
|
|
53
|
+
private stateSubscription: Subscription | null = null;
|
|
54
|
+
private disconnectSubscriptions: Map<string, Subscription> = new Map();
|
|
55
|
+
private connectedDevices: Map<string, Device> = new Map();
|
|
56
|
+
private discoveredDevices: Map<string, DiscoveredDevice> = new Map();
|
|
57
|
+
private isScanning = false;
|
|
58
|
+
private cachedState: State = State.Unknown;
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super();
|
|
62
|
+
this.manager = new RNBleManager();
|
|
63
|
+
this.setupStateListener();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set up Bluetooth state change listener
|
|
68
|
+
*/
|
|
69
|
+
private setupStateListener(): void {
|
|
70
|
+
this.stateSubscription = this.manager.onStateChange((state) => {
|
|
71
|
+
log.debug('Bluetooth state changed', { state });
|
|
72
|
+
this.cachedState = state;
|
|
73
|
+
this.emit('stateChange', state);
|
|
74
|
+
}, true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get current Bluetooth state
|
|
79
|
+
*/
|
|
80
|
+
async getState(): Promise<State> {
|
|
81
|
+
const state = await this.manager.state();
|
|
82
|
+
this.cachedState = state;
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get cached Bluetooth state (synchronous)
|
|
88
|
+
*/
|
|
89
|
+
getCachedState(): State {
|
|
90
|
+
return this.cachedState;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if Bluetooth is ready for operations
|
|
95
|
+
*/
|
|
96
|
+
async isReady(): Promise<boolean> {
|
|
97
|
+
const state = await this.getState();
|
|
98
|
+
return state === State.PoweredOn;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Wait for Bluetooth to be ready
|
|
103
|
+
*/
|
|
104
|
+
async waitForReady(timeoutMs: number = 10000): Promise<void> {
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
108
|
+
const state = await this.getState();
|
|
109
|
+
|
|
110
|
+
if (state === State.PoweredOn) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (state === State.Unsupported) {
|
|
115
|
+
throw BluetoothError.unavailable();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (state === State.Unauthorized) {
|
|
119
|
+
throw BluetoothError.unauthorized();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (state === State.PoweredOff) {
|
|
123
|
+
throw BluetoothError.poweredOff();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Wait a bit before checking again
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new BluetoothError('Bluetooth did not become ready in time', 'TIMEOUT');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start scanning for Bota devices
|
|
135
|
+
*/
|
|
136
|
+
async startScan(options: ScanOptions = {}): Promise<void> {
|
|
137
|
+
if (this.isScanning) {
|
|
138
|
+
log.warn('Scan already in progress');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this.waitForReady();
|
|
143
|
+
|
|
144
|
+
const {
|
|
145
|
+
timeout = SCAN_TIMEOUT,
|
|
146
|
+
deviceTypes,
|
|
147
|
+
pairingState,
|
|
148
|
+
minRssi,
|
|
149
|
+
allowDuplicates = false,
|
|
150
|
+
} = options;
|
|
151
|
+
|
|
152
|
+
log.info('Starting BLE scan', { timeout, deviceTypes, pairingState });
|
|
153
|
+
|
|
154
|
+
this.discoveredDevices.clear();
|
|
155
|
+
this.isScanning = true;
|
|
156
|
+
|
|
157
|
+
// Set up timeout
|
|
158
|
+
const scanTimeout = setTimeout(() => {
|
|
159
|
+
this.stopScan();
|
|
160
|
+
}, timeout);
|
|
161
|
+
|
|
162
|
+
// Start scanning
|
|
163
|
+
this.manager.startDeviceScan(
|
|
164
|
+
null, // Scan all services
|
|
165
|
+
{ allowDuplicates },
|
|
166
|
+
(error, device) => {
|
|
167
|
+
if (error) {
|
|
168
|
+
log.error('Scan error', error);
|
|
169
|
+
this.stopScan();
|
|
170
|
+
clearTimeout(scanTimeout);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!device || !device.name?.startsWith(DEVICE_NAME_PREFIX)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse device information from advertisement
|
|
179
|
+
const discovered = this.parseDiscoveredDevice(device);
|
|
180
|
+
if (!discovered) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Apply filters
|
|
185
|
+
if (deviceTypes && !deviceTypes.includes(discovered.deviceType)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (pairingState && discovered.pairingState !== pairingState) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (minRssi !== undefined && discovered.rssi < minRssi) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for duplicates
|
|
198
|
+
const existingDevice = this.discoveredDevices.get(device.id);
|
|
199
|
+
if (existingDevice && !allowDuplicates) {
|
|
200
|
+
// Update RSSI
|
|
201
|
+
existingDevice.rssi = discovered.rssi;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Store and emit
|
|
206
|
+
this.discoveredDevices.set(device.id, discovered);
|
|
207
|
+
log.debug('Device discovered', {
|
|
208
|
+
id: discovered.id,
|
|
209
|
+
name: discovered.name,
|
|
210
|
+
type: discovered.deviceType,
|
|
211
|
+
});
|
|
212
|
+
this.emit('deviceDiscovered', discovered);
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Stop scanning for devices
|
|
219
|
+
*/
|
|
220
|
+
stopScan(): void {
|
|
221
|
+
if (!this.isScanning) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
log.info('Stopping BLE scan');
|
|
226
|
+
this.manager.stopDeviceScan();
|
|
227
|
+
this.isScanning = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get list of discovered devices
|
|
232
|
+
*/
|
|
233
|
+
getDiscoveredDevices(): DiscoveredDevice[] {
|
|
234
|
+
return Array.from(this.discoveredDevices.values());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse a discovered device from BLE advertisement data
|
|
239
|
+
*/
|
|
240
|
+
private parseDiscoveredDevice(device: Device): DiscoveredDevice | null {
|
|
241
|
+
if (!device.name) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Parse manufacturer data if available
|
|
246
|
+
let deviceType: DeviceType = 'bota_pin';
|
|
247
|
+
let firmwareVersion = '0.0.0';
|
|
248
|
+
let pairingState: PairingState = 'unpaired';
|
|
249
|
+
let manufacturerData: Uint8Array | undefined;
|
|
250
|
+
|
|
251
|
+
if (device.manufacturerData) {
|
|
252
|
+
try {
|
|
253
|
+
manufacturerData = Buffer.from(device.manufacturerData, 'base64');
|
|
254
|
+
if (manufacturerData.length >= 4) {
|
|
255
|
+
deviceType = parseDeviceType(manufacturerData[0]);
|
|
256
|
+
firmwareVersion = parseFirmwareVersion(
|
|
257
|
+
manufacturerData[1],
|
|
258
|
+
manufacturerData[2]
|
|
259
|
+
);
|
|
260
|
+
pairingState = parsePairingState(manufacturerData[3]);
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
log.warn('Failed to parse manufacturer data', { deviceId: device.id });
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// Infer device type from name
|
|
267
|
+
if (device.name.includes('Pin4G')) {
|
|
268
|
+
deviceType = 'bota_pin_4g';
|
|
269
|
+
} else if (device.name.includes('Note')) {
|
|
270
|
+
deviceType = 'bota_note';
|
|
271
|
+
} else if (device.name.includes('Pin')) {
|
|
272
|
+
deviceType = 'bota_pin';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
id: device.id,
|
|
278
|
+
name: device.name,
|
|
279
|
+
deviceType,
|
|
280
|
+
firmwareVersion,
|
|
281
|
+
pairingState,
|
|
282
|
+
rssi: device.rssi ?? -100,
|
|
283
|
+
manufacturerData,
|
|
284
|
+
discoveredAt: new Date(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Connect to a device
|
|
290
|
+
*/
|
|
291
|
+
async connect(deviceId: string): Promise<Device> {
|
|
292
|
+
log.info('Connecting to device', { deviceId });
|
|
293
|
+
|
|
294
|
+
// Check if already connected
|
|
295
|
+
if (this.connectedDevices.has(deviceId)) {
|
|
296
|
+
log.debug('Device already connected', { deviceId });
|
|
297
|
+
return this.connectedDevices.get(deviceId)!;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Connect with timeout
|
|
302
|
+
const device = await this.manager.connectToDevice(deviceId, {
|
|
303
|
+
timeout: CONNECTION_TIMEOUT,
|
|
304
|
+
requestMTU: MAX_MTU,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
log.debug('Device connected, discovering services', { deviceId });
|
|
308
|
+
|
|
309
|
+
// Discover services and characteristics
|
|
310
|
+
await device.discoverAllServicesAndCharacteristics();
|
|
311
|
+
|
|
312
|
+
// Store connected device
|
|
313
|
+
this.connectedDevices.set(deviceId, device);
|
|
314
|
+
|
|
315
|
+
// Set up disconnect listener
|
|
316
|
+
const disconnectSub = device.onDisconnected((error, disconnectedDevice) => {
|
|
317
|
+
log.info('Device disconnected', {
|
|
318
|
+
deviceId: disconnectedDevice.id,
|
|
319
|
+
error: error?.message,
|
|
320
|
+
});
|
|
321
|
+
this.connectedDevices.delete(disconnectedDevice.id);
|
|
322
|
+
this.disconnectSubscriptions.get(disconnectedDevice.id)?.remove();
|
|
323
|
+
this.disconnectSubscriptions.delete(disconnectedDevice.id);
|
|
324
|
+
this.emit(
|
|
325
|
+
'deviceDisconnected',
|
|
326
|
+
disconnectedDevice.id,
|
|
327
|
+
error ? new Error(error.message) : undefined
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
this.disconnectSubscriptions.set(deviceId, disconnectSub);
|
|
331
|
+
|
|
332
|
+
this.emit('deviceConnected', deviceId);
|
|
333
|
+
return device;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const bleError = error as BleError;
|
|
336
|
+
log.error('Connection failed', new Error(bleError.message), { deviceId });
|
|
337
|
+
throw DeviceError.connectionFailed(deviceId, new Error(bleError.message));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Disconnect from a device
|
|
343
|
+
*/
|
|
344
|
+
async disconnect(deviceId: string): Promise<void> {
|
|
345
|
+
log.info('Disconnecting from device', { deviceId });
|
|
346
|
+
|
|
347
|
+
const device = this.connectedDevices.get(deviceId);
|
|
348
|
+
if (!device) {
|
|
349
|
+
log.warn('Device not found in connected devices', { deviceId });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await device.cancelConnection();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const bleError = error as BleError;
|
|
357
|
+
log.warn('Disconnect error (may be expected)', { error: bleError.message });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.connectedDevices.delete(deviceId);
|
|
361
|
+
this.disconnectSubscriptions.get(deviceId)?.remove();
|
|
362
|
+
this.disconnectSubscriptions.delete(deviceId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if a device is connected
|
|
367
|
+
*/
|
|
368
|
+
isConnected(deviceId: string): boolean {
|
|
369
|
+
return this.connectedDevices.has(deviceId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get a connected device
|
|
374
|
+
*/
|
|
375
|
+
getConnectedDevice(deviceId: string): Device | undefined {
|
|
376
|
+
return this.connectedDevices.get(deviceId);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get negotiated MTU for a device
|
|
381
|
+
*/
|
|
382
|
+
async getMtu(deviceId: string): Promise<number> {
|
|
383
|
+
const device = this.connectedDevices.get(deviceId);
|
|
384
|
+
if (!device) {
|
|
385
|
+
return DEFAULT_MTU;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
// mtu is a property, not a method in react-native-ble-plx
|
|
390
|
+
return device.mtu ?? DEFAULT_MTU;
|
|
391
|
+
} catch {
|
|
392
|
+
return DEFAULT_MTU;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Read a characteristic value
|
|
398
|
+
*/
|
|
399
|
+
async readCharacteristic(
|
|
400
|
+
deviceId: string,
|
|
401
|
+
serviceUuid: string,
|
|
402
|
+
characteristicUuid: string
|
|
403
|
+
): Promise<Buffer> {
|
|
404
|
+
const device = this.connectedDevices.get(deviceId);
|
|
405
|
+
if (!device) {
|
|
406
|
+
throw DeviceError.notConnected(deviceId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const characteristic = await device.readCharacteristicForService(
|
|
411
|
+
serviceUuid,
|
|
412
|
+
characteristicUuid
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (!characteristic.value) {
|
|
416
|
+
return Buffer.alloc(0);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return Buffer.from(characteristic.value, 'base64');
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const bleError = error as BleError;
|
|
422
|
+
log.error('Read characteristic failed', new Error(bleError.message), {
|
|
423
|
+
deviceId,
|
|
424
|
+
serviceUuid,
|
|
425
|
+
characteristicUuid,
|
|
426
|
+
});
|
|
427
|
+
throw new DeviceError(
|
|
428
|
+
`Failed to read characteristic: ${bleError.message}`,
|
|
429
|
+
'READ_FAILED',
|
|
430
|
+
deviceId,
|
|
431
|
+
new Error(bleError.message)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Write a characteristic value
|
|
438
|
+
*/
|
|
439
|
+
async writeCharacteristic(
|
|
440
|
+
deviceId: string,
|
|
441
|
+
serviceUuid: string,
|
|
442
|
+
characteristicUuid: string,
|
|
443
|
+
data: Buffer,
|
|
444
|
+
withResponse: boolean = true
|
|
445
|
+
): Promise<void> {
|
|
446
|
+
const device = this.connectedDevices.get(deviceId);
|
|
447
|
+
if (!device) {
|
|
448
|
+
throw DeviceError.notConnected(deviceId);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const base64Data = data.toString('base64');
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
if (withResponse) {
|
|
455
|
+
await device.writeCharacteristicWithResponseForService(
|
|
456
|
+
serviceUuid,
|
|
457
|
+
characteristicUuid,
|
|
458
|
+
base64Data
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
await device.writeCharacteristicWithoutResponseForService(
|
|
462
|
+
serviceUuid,
|
|
463
|
+
characteristicUuid,
|
|
464
|
+
base64Data
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
const bleError = error as BleError;
|
|
469
|
+
log.error('Write characteristic failed', new Error(bleError.message), {
|
|
470
|
+
deviceId,
|
|
471
|
+
serviceUuid,
|
|
472
|
+
characteristicUuid,
|
|
473
|
+
});
|
|
474
|
+
throw new DeviceError(
|
|
475
|
+
`Failed to write characteristic: ${bleError.message}`,
|
|
476
|
+
'WRITE_FAILED',
|
|
477
|
+
deviceId,
|
|
478
|
+
new Error(bleError.message)
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Subscribe to characteristic notifications
|
|
485
|
+
*/
|
|
486
|
+
subscribeToCharacteristic(
|
|
487
|
+
deviceId: string,
|
|
488
|
+
serviceUuid: string,
|
|
489
|
+
characteristicUuid: string,
|
|
490
|
+
onData: (data: Buffer) => void,
|
|
491
|
+
onError?: (error: Error) => void
|
|
492
|
+
): Subscription {
|
|
493
|
+
const device = this.connectedDevices.get(deviceId);
|
|
494
|
+
if (!device) {
|
|
495
|
+
throw DeviceError.notConnected(deviceId);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return device.monitorCharacteristicForService(
|
|
499
|
+
serviceUuid,
|
|
500
|
+
characteristicUuid,
|
|
501
|
+
(error, characteristic) => {
|
|
502
|
+
if (error) {
|
|
503
|
+
log.error('Notification error', new Error(error.message), {
|
|
504
|
+
deviceId,
|
|
505
|
+
characteristicUuid,
|
|
506
|
+
});
|
|
507
|
+
onError?.(new Error(error.message));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (characteristic?.value) {
|
|
512
|
+
const data = Buffer.from(characteristic.value, 'base64');
|
|
513
|
+
onData(data);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Destroy the BLE manager and clean up resources
|
|
521
|
+
*/
|
|
522
|
+
destroy(): void {
|
|
523
|
+
log.info('Destroying BLE manager');
|
|
524
|
+
|
|
525
|
+
this.stopScan();
|
|
526
|
+
|
|
527
|
+
// Remove all disconnect subscriptions
|
|
528
|
+
for (const sub of this.disconnectSubscriptions.values()) {
|
|
529
|
+
sub.remove();
|
|
530
|
+
}
|
|
531
|
+
this.disconnectSubscriptions.clear();
|
|
532
|
+
|
|
533
|
+
// Disconnect all devices
|
|
534
|
+
for (const deviceId of this.connectedDevices.keys()) {
|
|
535
|
+
this.disconnect(deviceId).catch(() => {});
|
|
536
|
+
}
|
|
537
|
+
this.connectedDevices.clear();
|
|
538
|
+
|
|
539
|
+
// Remove state subscription
|
|
540
|
+
this.stateSubscription?.remove();
|
|
541
|
+
this.stateSubscription = null;
|
|
542
|
+
|
|
543
|
+
// Destroy the manager
|
|
544
|
+
this.manager.destroy();
|
|
545
|
+
|
|
546
|
+
this.removeAllListeners();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Singleton instance
|
|
552
|
+
*/
|
|
553
|
+
let instance: BleManager | null = null;
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get or create the BLE manager singleton
|
|
557
|
+
*/
|
|
558
|
+
export function getBleManager(): BleManager {
|
|
559
|
+
if (!instance) {
|
|
560
|
+
instance = new BleManager();
|
|
561
|
+
}
|
|
562
|
+
return instance;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Reset the BLE manager singleton (for testing)
|
|
567
|
+
*/
|
|
568
|
+
export function resetBleManager(): void {
|
|
569
|
+
if (instance) {
|
|
570
|
+
instance.destroy();
|
|
571
|
+
instance = null;
|
|
572
|
+
}
|
|
573
|
+
}
|