@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.
Files changed (177) hide show
  1. package/README.md +279 -0
  2. package/lib/commonjs/BotaClient.js +223 -0
  3. package/lib/commonjs/BotaClient.js.map +1 -0
  4. package/lib/commonjs/ble/BleManager.js +494 -0
  5. package/lib/commonjs/ble/BleManager.js.map +1 -0
  6. package/lib/commonjs/ble/constants.js +166 -0
  7. package/lib/commonjs/ble/constants.js.map +1 -0
  8. package/lib/commonjs/ble/index.js +54 -0
  9. package/lib/commonjs/ble/index.js.map +1 -0
  10. package/lib/commonjs/ble/parsers.js +345 -0
  11. package/lib/commonjs/ble/parsers.js.map +1 -0
  12. package/lib/commonjs/index.js +81 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/managers/DeviceManager.js +437 -0
  15. package/lib/commonjs/managers/DeviceManager.js.map +1 -0
  16. package/lib/commonjs/managers/OTAManager.js +227 -0
  17. package/lib/commonjs/managers/OTAManager.js.map +1 -0
  18. package/lib/commonjs/managers/RecordingManager.js +384 -0
  19. package/lib/commonjs/managers/RecordingManager.js.map +1 -0
  20. package/lib/commonjs/managers/index.js +27 -0
  21. package/lib/commonjs/managers/index.js.map +1 -0
  22. package/lib/commonjs/models/Device.js +2 -0
  23. package/lib/commonjs/models/Device.js.map +1 -0
  24. package/lib/commonjs/models/Recording.js +2 -0
  25. package/lib/commonjs/models/Recording.js.map +1 -0
  26. package/lib/commonjs/models/Status.js +6 -0
  27. package/lib/commonjs/models/Status.js.map +1 -0
  28. package/lib/commonjs/models/index.js +39 -0
  29. package/lib/commonjs/models/index.js.map +1 -0
  30. package/lib/commonjs/protocol/ProtocolHandler.js +343 -0
  31. package/lib/commonjs/protocol/ProtocolHandler.js.map +1 -0
  32. package/lib/commonjs/protocol/index.js +13 -0
  33. package/lib/commonjs/protocol/index.js.map +1 -0
  34. package/lib/commonjs/storage/StorageManager.js +333 -0
  35. package/lib/commonjs/storage/StorageManager.js.map +1 -0
  36. package/lib/commonjs/storage/index.js +19 -0
  37. package/lib/commonjs/storage/index.js.map +1 -0
  38. package/lib/commonjs/upload/S3Uploader.js +133 -0
  39. package/lib/commonjs/upload/S3Uploader.js.map +1 -0
  40. package/lib/commonjs/upload/UploadQueue.js +280 -0
  41. package/lib/commonjs/upload/UploadQueue.js.map +1 -0
  42. package/lib/commonjs/upload/index.js +20 -0
  43. package/lib/commonjs/upload/index.js.map +1 -0
  44. package/lib/commonjs/utils/errors.js +187 -0
  45. package/lib/commonjs/utils/errors.js.map +1 -0
  46. package/lib/commonjs/utils/index.js +40 -0
  47. package/lib/commonjs/utils/index.js.map +1 -0
  48. package/lib/commonjs/utils/logger.js +135 -0
  49. package/lib/commonjs/utils/logger.js.map +1 -0
  50. package/lib/commonjs/utils/retry.js +160 -0
  51. package/lib/commonjs/utils/retry.js.map +1 -0
  52. package/lib/module/BotaClient.js +216 -0
  53. package/lib/module/BotaClient.js.map +1 -0
  54. package/lib/module/ble/BleManager.js +484 -0
  55. package/lib/module/ble/BleManager.js.map +1 -0
  56. package/lib/module/ble/constants.js +159 -0
  57. package/lib/module/ble/constants.js.map +1 -0
  58. package/lib/module/ble/index.js +8 -0
  59. package/lib/module/ble/index.js.map +1 -0
  60. package/lib/module/ble/parsers.js +328 -0
  61. package/lib/module/ble/parsers.js.map +1 -0
  62. package/lib/module/index.js +22 -0
  63. package/lib/module/index.js.map +1 -0
  64. package/lib/module/managers/DeviceManager.js +429 -0
  65. package/lib/module/managers/DeviceManager.js.map +1 -0
  66. package/lib/module/managers/OTAManager.js +219 -0
  67. package/lib/module/managers/OTAManager.js.map +1 -0
  68. package/lib/module/managers/RecordingManager.js +376 -0
  69. package/lib/module/managers/RecordingManager.js.map +1 -0
  70. package/lib/module/managers/index.js +8 -0
  71. package/lib/module/managers/index.js.map +1 -0
  72. package/lib/module/models/Device.js +2 -0
  73. package/lib/module/models/Device.js.map +1 -0
  74. package/lib/module/models/Recording.js +2 -0
  75. package/lib/module/models/Recording.js.map +1 -0
  76. package/lib/module/models/Status.js +2 -0
  77. package/lib/module/models/Status.js.map +1 -0
  78. package/lib/module/models/index.js +8 -0
  79. package/lib/module/models/index.js.map +1 -0
  80. package/lib/module/protocol/ProtocolHandler.js +336 -0
  81. package/lib/module/protocol/ProtocolHandler.js.map +1 -0
  82. package/lib/module/protocol/index.js +6 -0
  83. package/lib/module/protocol/index.js.map +1 -0
  84. package/lib/module/storage/StorageManager.js +324 -0
  85. package/lib/module/storage/StorageManager.js.map +1 -0
  86. package/lib/module/storage/index.js +6 -0
  87. package/lib/module/storage/index.js.map +1 -0
  88. package/lib/module/upload/S3Uploader.js +126 -0
  89. package/lib/module/upload/S3Uploader.js.map +1 -0
  90. package/lib/module/upload/UploadQueue.js +272 -0
  91. package/lib/module/upload/UploadQueue.js.map +1 -0
  92. package/lib/module/upload/index.js +7 -0
  93. package/lib/module/upload/index.js.map +1 -0
  94. package/lib/module/utils/errors.js +173 -0
  95. package/lib/module/utils/errors.js.map +1 -0
  96. package/lib/module/utils/index.js +8 -0
  97. package/lib/module/utils/index.js.map +1 -0
  98. package/lib/module/utils/logger.js +129 -0
  99. package/lib/module/utils/logger.js.map +1 -0
  100. package/lib/module/utils/retry.js +149 -0
  101. package/lib/module/utils/retry.js.map +1 -0
  102. package/lib/typescript/src/BotaClient.d.ts +77 -0
  103. package/lib/typescript/src/BotaClient.d.ts.map +1 -0
  104. package/lib/typescript/src/ble/BleManager.d.ts +111 -0
  105. package/lib/typescript/src/ble/BleManager.d.ts.map +1 -0
  106. package/lib/typescript/src/ble/constants.d.ts +111 -0
  107. package/lib/typescript/src/ble/constants.d.ts.map +1 -0
  108. package/lib/typescript/src/ble/index.d.ts +7 -0
  109. package/lib/typescript/src/ble/index.d.ts.map +1 -0
  110. package/lib/typescript/src/ble/parsers.d.ts +100 -0
  111. package/lib/typescript/src/ble/parsers.d.ts.map +1 -0
  112. package/lib/typescript/src/index.d.ts +16 -0
  113. package/lib/typescript/src/index.d.ts.map +1 -0
  114. package/lib/typescript/src/managers/DeviceManager.d.ts +84 -0
  115. package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -0
  116. package/lib/typescript/src/managers/OTAManager.d.ts +78 -0
  117. package/lib/typescript/src/managers/OTAManager.d.ts.map +1 -0
  118. package/lib/typescript/src/managers/RecordingManager.d.ts +90 -0
  119. package/lib/typescript/src/managers/RecordingManager.d.ts.map +1 -0
  120. package/lib/typescript/src/managers/index.d.ts +7 -0
  121. package/lib/typescript/src/managers/index.d.ts.map +1 -0
  122. package/lib/typescript/src/models/Device.d.ts +139 -0
  123. package/lib/typescript/src/models/Device.d.ts.map +1 -0
  124. package/lib/typescript/src/models/Recording.d.ts +110 -0
  125. package/lib/typescript/src/models/Recording.d.ts.map +1 -0
  126. package/lib/typescript/src/models/Status.d.ts +104 -0
  127. package/lib/typescript/src/models/Status.d.ts.map +1 -0
  128. package/lib/typescript/src/models/index.d.ts +7 -0
  129. package/lib/typescript/src/models/index.d.ts.map +1 -0
  130. package/lib/typescript/src/protocol/ProtocolHandler.d.ts +69 -0
  131. package/lib/typescript/src/protocol/ProtocolHandler.d.ts.map +1 -0
  132. package/lib/typescript/src/protocol/index.d.ts +5 -0
  133. package/lib/typescript/src/protocol/index.d.ts.map +1 -0
  134. package/lib/typescript/src/storage/StorageManager.d.ts +116 -0
  135. package/lib/typescript/src/storage/StorageManager.d.ts.map +1 -0
  136. package/lib/typescript/src/storage/index.d.ts +5 -0
  137. package/lib/typescript/src/storage/index.d.ts.map +1 -0
  138. package/lib/typescript/src/upload/S3Uploader.d.ts +38 -0
  139. package/lib/typescript/src/upload/S3Uploader.d.ts.map +1 -0
  140. package/lib/typescript/src/upload/UploadQueue.d.ts +95 -0
  141. package/lib/typescript/src/upload/UploadQueue.d.ts.map +1 -0
  142. package/lib/typescript/src/upload/index.d.ts +6 -0
  143. package/lib/typescript/src/upload/index.d.ts.map +1 -0
  144. package/lib/typescript/src/utils/errors.d.ts +82 -0
  145. package/lib/typescript/src/utils/errors.d.ts.map +1 -0
  146. package/lib/typescript/src/utils/index.d.ts +7 -0
  147. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  148. package/lib/typescript/src/utils/logger.d.ts +68 -0
  149. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  150. package/lib/typescript/src/utils/retry.d.ts +53 -0
  151. package/lib/typescript/src/utils/retry.d.ts.map +1 -0
  152. package/package.json +95 -0
  153. package/src/BotaClient.ts +238 -0
  154. package/src/ble/BleManager.ts +573 -0
  155. package/src/ble/constants.ts +158 -0
  156. package/src/ble/index.ts +7 -0
  157. package/src/ble/parsers.ts +395 -0
  158. package/src/index.ts +64 -0
  159. package/src/managers/DeviceManager.ts +545 -0
  160. package/src/managers/OTAManager.ts +263 -0
  161. package/src/managers/RecordingManager.ts +434 -0
  162. package/src/managers/index.ts +12 -0
  163. package/src/models/Device.ts +164 -0
  164. package/src/models/Recording.ts +123 -0
  165. package/src/models/Status.ts +126 -0
  166. package/src/models/index.ts +7 -0
  167. package/src/protocol/ProtocolHandler.ts +459 -0
  168. package/src/protocol/index.ts +5 -0
  169. package/src/storage/StorageManager.ts +343 -0
  170. package/src/storage/index.ts +5 -0
  171. package/src/upload/S3Uploader.ts +164 -0
  172. package/src/upload/UploadQueue.ts +310 -0
  173. package/src/upload/index.ts +6 -0
  174. package/src/utils/errors.ts +310 -0
  175. package/src/utils/index.ts +7 -0
  176. package/src/utils/logger.ts +137 -0
  177. 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
+ }