@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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Manager - Handles device discovery, connection, and provisioning
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Buffer } from 'buffer';
|
|
6
|
+
import { Subscription } from 'react-native-ble-plx';
|
|
7
|
+
import EventEmitter from 'eventemitter3';
|
|
8
|
+
|
|
9
|
+
import { getBleManager, BleManager } from '../ble/BleManager';
|
|
10
|
+
import {
|
|
11
|
+
SERVICE_DEVICE_INFO,
|
|
12
|
+
SERVICE_BOTA_PROVISIONING,
|
|
13
|
+
SERVICE_BOTA_CONTROL,
|
|
14
|
+
CHAR_SERIAL_NUMBER,
|
|
15
|
+
CHAR_FIRMWARE_REVISION,
|
|
16
|
+
CHAR_HARDWARE_REVISION,
|
|
17
|
+
CHAR_PAIRING_STATE,
|
|
18
|
+
CHAR_DEVICE_TOKEN,
|
|
19
|
+
CHAR_API_ENDPOINT,
|
|
20
|
+
CHAR_PROVISIONING_RESULT,
|
|
21
|
+
CHAR_DEVICE_STATUS,
|
|
22
|
+
CHAR_TIME_SYNC,
|
|
23
|
+
API_ENDPOINT_PRODUCTION,
|
|
24
|
+
API_ENDPOINT_SANDBOX,
|
|
25
|
+
PROVISIONING_SUCCESS,
|
|
26
|
+
PROVISIONING_INVALID_TOKEN,
|
|
27
|
+
PROVISIONING_STORAGE_ERROR,
|
|
28
|
+
PROVISIONING_CHUNK_ERROR,
|
|
29
|
+
OPERATION_TIMEOUT,
|
|
30
|
+
} from '../ble/constants';
|
|
31
|
+
import {
|
|
32
|
+
parsePairingState,
|
|
33
|
+
parseDeviceStatus,
|
|
34
|
+
createTimeSyncData,
|
|
35
|
+
} from '../ble/parsers';
|
|
36
|
+
import type {
|
|
37
|
+
DiscoveredDevice,
|
|
38
|
+
ConnectedDevice,
|
|
39
|
+
DeviceStatus,
|
|
40
|
+
ScanOptions,
|
|
41
|
+
Environment,
|
|
42
|
+
ProvisioningResult,
|
|
43
|
+
} from '../models/Device';
|
|
44
|
+
import type { DeviceManagerEvents } from '../models/Status';
|
|
45
|
+
import {
|
|
46
|
+
DeviceError,
|
|
47
|
+
ProvisioningError,
|
|
48
|
+
} from '../utils/errors';
|
|
49
|
+
import { logger } from '../utils/logger';
|
|
50
|
+
|
|
51
|
+
const log = logger.tag('DeviceManager');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Device Manager class
|
|
55
|
+
*/
|
|
56
|
+
export class DeviceManager extends EventEmitter<DeviceManagerEvents> {
|
|
57
|
+
private bleManager: BleManager;
|
|
58
|
+
private connectedDevices: Map<string, ConnectedDevice> = new Map();
|
|
59
|
+
private statusSubscriptions: Map<string, Subscription> = new Map();
|
|
60
|
+
private isInitialized = false;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
this.bleManager = getBleManager();
|
|
65
|
+
this.setupBleListeners();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the device manager
|
|
70
|
+
*/
|
|
71
|
+
initialize(): void {
|
|
72
|
+
if (this.isInitialized) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.isInitialized = true;
|
|
76
|
+
log.info('DeviceManager initialized');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set up BLE event listeners
|
|
81
|
+
*/
|
|
82
|
+
private setupBleListeners(): void {
|
|
83
|
+
this.bleManager.on('deviceDiscovered', (device) => {
|
|
84
|
+
this.emit('deviceDiscovered', device);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.bleManager.on('deviceConnected', (deviceId) => {
|
|
88
|
+
const device = this.connectedDevices.get(deviceId);
|
|
89
|
+
if (device) {
|
|
90
|
+
this.emit('deviceConnected', device);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.bleManager.on('deviceDisconnected', (deviceId, error) => {
|
|
95
|
+
const device = this.connectedDevices.get(deviceId);
|
|
96
|
+
if (device) {
|
|
97
|
+
// Update connection state
|
|
98
|
+
device.connectionState = 'disconnected';
|
|
99
|
+
}
|
|
100
|
+
this.connectedDevices.delete(deviceId);
|
|
101
|
+
|
|
102
|
+
// Clean up status subscription
|
|
103
|
+
this.statusSubscriptions.get(deviceId)?.remove();
|
|
104
|
+
this.statusSubscriptions.delete(deviceId);
|
|
105
|
+
|
|
106
|
+
this.emit('deviceDisconnected', deviceId, error);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Start scanning for Bota devices
|
|
112
|
+
*/
|
|
113
|
+
async startScan(options: ScanOptions = {}): Promise<void> {
|
|
114
|
+
log.info('Starting device scan', options as Record<string, unknown>);
|
|
115
|
+
this.emit('scanStarted');
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await this.bleManager.startScan(options);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.emit('scanError', error as Error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Stop scanning for devices
|
|
127
|
+
*/
|
|
128
|
+
stopScan(): void {
|
|
129
|
+
log.info('Stopping device scan');
|
|
130
|
+
this.bleManager.stopScan();
|
|
131
|
+
this.emit('scanStopped');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get list of discovered devices
|
|
136
|
+
*/
|
|
137
|
+
getDiscoveredDevices(): DiscoveredDevice[] {
|
|
138
|
+
return this.bleManager.getDiscoveredDevices();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get list of connected devices
|
|
143
|
+
*/
|
|
144
|
+
getConnectedDevices(): ConnectedDevice[] {
|
|
145
|
+
return Array.from(this.connectedDevices.values());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Connect to a discovered device
|
|
150
|
+
*/
|
|
151
|
+
async connect(device: DiscoveredDevice): Promise<ConnectedDevice> {
|
|
152
|
+
log.info('Connecting to device', { deviceId: device.id, name: device.name });
|
|
153
|
+
|
|
154
|
+
// Check if already connected
|
|
155
|
+
const existing = this.connectedDevices.get(device.id);
|
|
156
|
+
if (existing && existing.connectionState === 'connected') {
|
|
157
|
+
log.debug('Device already connected', { deviceId: device.id });
|
|
158
|
+
return existing;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Emit connecting state
|
|
162
|
+
this.emit('connectionStateChanged', device.id, 'connecting');
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Connect via BLE manager
|
|
166
|
+
await this.bleManager.connect(device.id);
|
|
167
|
+
|
|
168
|
+
// Read device information
|
|
169
|
+
const serialNumber = await this.readSerialNumber(device.id);
|
|
170
|
+
const firmwareVersion = await this.readFirmwareVersion(device.id);
|
|
171
|
+
const hardwareRevision = await this.readHardwareRevision(device.id);
|
|
172
|
+
const pairingState = await this.readPairingState(device.id);
|
|
173
|
+
const mtu = await this.bleManager.getMtu(device.id);
|
|
174
|
+
|
|
175
|
+
const connectedDevice: ConnectedDevice = {
|
|
176
|
+
id: device.id,
|
|
177
|
+
serialNumber,
|
|
178
|
+
deviceType: device.deviceType,
|
|
179
|
+
firmwareVersion,
|
|
180
|
+
hardwareRevision,
|
|
181
|
+
isProvisioned: pairingState === 'paired',
|
|
182
|
+
connectionState: 'connected',
|
|
183
|
+
mtu,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.connectedDevices.set(device.id, connectedDevice);
|
|
187
|
+
|
|
188
|
+
log.info('Device connected successfully', {
|
|
189
|
+
deviceId: device.id,
|
|
190
|
+
serialNumber,
|
|
191
|
+
isProvisioned: connectedDevice.isProvisioned,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.emit('connectionStateChanged', device.id, 'connected');
|
|
195
|
+
|
|
196
|
+
return connectedDevice;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.emit('connectionStateChanged', device.id, 'disconnected');
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Disconnect from a device
|
|
205
|
+
*/
|
|
206
|
+
async disconnect(device: ConnectedDevice): Promise<void> {
|
|
207
|
+
log.info('Disconnecting from device', { deviceId: device.id });
|
|
208
|
+
|
|
209
|
+
this.emit('connectionStateChanged', device.id, 'disconnecting');
|
|
210
|
+
|
|
211
|
+
// Clean up status subscription
|
|
212
|
+
this.statusSubscriptions.get(device.id)?.remove();
|
|
213
|
+
this.statusSubscriptions.delete(device.id);
|
|
214
|
+
|
|
215
|
+
await this.bleManager.disconnect(device.id);
|
|
216
|
+
this.connectedDevices.delete(device.id);
|
|
217
|
+
|
|
218
|
+
this.emit('connectionStateChanged', device.id, 'disconnected');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a device is connected
|
|
223
|
+
*/
|
|
224
|
+
isConnected(deviceId: string): boolean {
|
|
225
|
+
return this.bleManager.isConnected(deviceId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Provision a device with a token
|
|
230
|
+
*/
|
|
231
|
+
async provision(
|
|
232
|
+
device: ConnectedDevice,
|
|
233
|
+
deviceToken: string,
|
|
234
|
+
environment: Environment = 'production'
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
log.info('Provisioning device', {
|
|
237
|
+
deviceId: device.id,
|
|
238
|
+
environment,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!this.isConnected(device.id)) {
|
|
242
|
+
throw DeviceError.notConnected(device.id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check current pairing state
|
|
246
|
+
const pairingState = await this.readPairingState(device.id);
|
|
247
|
+
if (pairingState === 'paired') {
|
|
248
|
+
log.warn('Device is already provisioned', { deviceId: device.id });
|
|
249
|
+
throw ProvisioningError.alreadyProvisioned(device.id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Set up provisioning result listener
|
|
253
|
+
const resultPromise = this.waitForProvisioningResult(device.id);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
// Write API endpoint
|
|
257
|
+
await this.writeApiEndpoint(device.id, environment);
|
|
258
|
+
|
|
259
|
+
// Write device token (chunked)
|
|
260
|
+
await this.writeDeviceToken(device.id, deviceToken);
|
|
261
|
+
|
|
262
|
+
// Wait for provisioning result
|
|
263
|
+
const result = await resultPromise;
|
|
264
|
+
|
|
265
|
+
if (!result.success) {
|
|
266
|
+
switch (result.error) {
|
|
267
|
+
case 'invalid_token':
|
|
268
|
+
throw ProvisioningError.invalidToken(device.id);
|
|
269
|
+
case 'storage_error':
|
|
270
|
+
throw ProvisioningError.storageError(device.id);
|
|
271
|
+
case 'chunk_error':
|
|
272
|
+
throw ProvisioningError.chunkError(device.id);
|
|
273
|
+
default:
|
|
274
|
+
throw new ProvisioningError(
|
|
275
|
+
'Provisioning failed',
|
|
276
|
+
'PROVISIONING_FAILED',
|
|
277
|
+
device.id
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sync time to device
|
|
283
|
+
await this.syncTime(device.id);
|
|
284
|
+
|
|
285
|
+
// Update device state
|
|
286
|
+
device.isProvisioned = true;
|
|
287
|
+
|
|
288
|
+
log.info('Device provisioned successfully', { deviceId: device.id });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
log.error('Provisioning failed', error as Error, { deviceId: device.id });
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a device is provisioned
|
|
297
|
+
*/
|
|
298
|
+
async isProvisioned(device: ConnectedDevice): Promise<boolean> {
|
|
299
|
+
const pairingState = await this.readPairingState(device.id);
|
|
300
|
+
return pairingState === 'paired';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get device status
|
|
305
|
+
*/
|
|
306
|
+
async getStatus(device: ConnectedDevice): Promise<DeviceStatus> {
|
|
307
|
+
if (!this.isConnected(device.id)) {
|
|
308
|
+
throw DeviceError.notConnected(device.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const data = await this.bleManager.readCharacteristic(
|
|
312
|
+
device.id,
|
|
313
|
+
SERVICE_BOTA_CONTROL,
|
|
314
|
+
CHAR_DEVICE_STATUS
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return parseDeviceStatus(data);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Subscribe to device status updates
|
|
322
|
+
*/
|
|
323
|
+
subscribeToStatus(
|
|
324
|
+
device: ConnectedDevice,
|
|
325
|
+
callback: (status: DeviceStatus) => void
|
|
326
|
+
): () => void {
|
|
327
|
+
if (!this.isConnected(device.id)) {
|
|
328
|
+
throw DeviceError.notConnected(device.id);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Remove existing subscription
|
|
332
|
+
this.statusSubscriptions.get(device.id)?.remove();
|
|
333
|
+
|
|
334
|
+
const subscription = this.bleManager.subscribeToCharacteristic(
|
|
335
|
+
device.id,
|
|
336
|
+
SERVICE_BOTA_CONTROL,
|
|
337
|
+
CHAR_DEVICE_STATUS,
|
|
338
|
+
(data) => {
|
|
339
|
+
try {
|
|
340
|
+
const status = parseDeviceStatus(data);
|
|
341
|
+
this.emit('deviceStatusUpdated', device.id, status);
|
|
342
|
+
callback(status);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
log.error('Failed to parse status update', error as Error);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
(error) => {
|
|
348
|
+
log.error('Status subscription error', error);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
this.statusSubscriptions.set(device.id, subscription);
|
|
353
|
+
|
|
354
|
+
return () => {
|
|
355
|
+
subscription.remove();
|
|
356
|
+
this.statusSubscriptions.delete(device.id);
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Sync time to device
|
|
362
|
+
*/
|
|
363
|
+
async syncTime(deviceId: string): Promise<void> {
|
|
364
|
+
log.debug('Syncing time to device', { deviceId });
|
|
365
|
+
|
|
366
|
+
const timeSyncData = createTimeSyncData();
|
|
367
|
+
|
|
368
|
+
await this.bleManager.writeCharacteristic(
|
|
369
|
+
deviceId,
|
|
370
|
+
SERVICE_BOTA_CONTROL,
|
|
371
|
+
CHAR_TIME_SYNC,
|
|
372
|
+
timeSyncData
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Private helper methods
|
|
377
|
+
|
|
378
|
+
private async readSerialNumber(deviceId: string): Promise<string> {
|
|
379
|
+
const data = await this.bleManager.readCharacteristic(
|
|
380
|
+
deviceId,
|
|
381
|
+
SERVICE_DEVICE_INFO,
|
|
382
|
+
CHAR_SERIAL_NUMBER
|
|
383
|
+
);
|
|
384
|
+
return data.toString('utf8').replace(/\0/g, '');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async readFirmwareVersion(deviceId: string): Promise<string> {
|
|
388
|
+
const data = await this.bleManager.readCharacteristic(
|
|
389
|
+
deviceId,
|
|
390
|
+
SERVICE_DEVICE_INFO,
|
|
391
|
+
CHAR_FIRMWARE_REVISION
|
|
392
|
+
);
|
|
393
|
+
return data.toString('utf8').replace(/\0/g, '');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async readHardwareRevision(deviceId: string): Promise<string | undefined> {
|
|
397
|
+
try {
|
|
398
|
+
const data = await this.bleManager.readCharacteristic(
|
|
399
|
+
deviceId,
|
|
400
|
+
SERVICE_DEVICE_INFO,
|
|
401
|
+
CHAR_HARDWARE_REVISION
|
|
402
|
+
);
|
|
403
|
+
return data.toString('utf8').replace(/\0/g, '');
|
|
404
|
+
} catch {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async readPairingState(deviceId: string): Promise<'unpaired' | 'pairing' | 'paired' | 'error'> {
|
|
410
|
+
const data = await this.bleManager.readCharacteristic(
|
|
411
|
+
deviceId,
|
|
412
|
+
SERVICE_BOTA_PROVISIONING,
|
|
413
|
+
CHAR_PAIRING_STATE
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
if (data.length < 1) {
|
|
417
|
+
return 'unpaired';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return parsePairingState(data[0]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async writeApiEndpoint(
|
|
424
|
+
deviceId: string,
|
|
425
|
+
environment: Environment
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const endpointByte =
|
|
428
|
+
environment === 'production' ? API_ENDPOINT_PRODUCTION : API_ENDPOINT_SANDBOX;
|
|
429
|
+
|
|
430
|
+
await this.bleManager.writeCharacteristic(
|
|
431
|
+
deviceId,
|
|
432
|
+
SERVICE_BOTA_PROVISIONING,
|
|
433
|
+
CHAR_API_ENDPOINT,
|
|
434
|
+
Buffer.from([endpointByte])
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async writeDeviceToken(
|
|
439
|
+
deviceId: string,
|
|
440
|
+
token: string
|
|
441
|
+
): Promise<void> {
|
|
442
|
+
const mtu = await this.bleManager.getMtu(deviceId);
|
|
443
|
+
const maxChunkSize = mtu - 5; // Account for BLE overhead + chunk header
|
|
444
|
+
|
|
445
|
+
const tokenBuffer = Buffer.from(token, 'utf8');
|
|
446
|
+
const totalChunks = Math.ceil(tokenBuffer.length / (maxChunkSize - 2));
|
|
447
|
+
|
|
448
|
+
log.debug('Writing device token', {
|
|
449
|
+
deviceId,
|
|
450
|
+
tokenLength: token.length,
|
|
451
|
+
totalChunks,
|
|
452
|
+
mtu,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
456
|
+
const start = i * (maxChunkSize - 2);
|
|
457
|
+
const end = Math.min(start + (maxChunkSize - 2), tokenBuffer.length);
|
|
458
|
+
const chunkData = tokenBuffer.slice(start, end);
|
|
459
|
+
|
|
460
|
+
// Create chunk with header: [chunk_index, total_chunks, ...data]
|
|
461
|
+
const chunk = Buffer.alloc(2 + chunkData.length);
|
|
462
|
+
chunk.writeUInt8(i, 0);
|
|
463
|
+
chunk.writeUInt8(totalChunks, 1);
|
|
464
|
+
chunkData.copy(chunk, 2);
|
|
465
|
+
|
|
466
|
+
await this.bleManager.writeCharacteristic(
|
|
467
|
+
deviceId,
|
|
468
|
+
SERVICE_BOTA_PROVISIONING,
|
|
469
|
+
CHAR_DEVICE_TOKEN,
|
|
470
|
+
chunk
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
log.debug('Wrote token chunk', { chunk: i + 1, total: totalChunks });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private waitForProvisioningResult(deviceId: string): Promise<ProvisioningResult> {
|
|
478
|
+
return new Promise((resolve, reject) => {
|
|
479
|
+
const timeout = setTimeout(() => {
|
|
480
|
+
subscription.remove();
|
|
481
|
+
reject(ProvisioningError.timeout(deviceId));
|
|
482
|
+
}, OPERATION_TIMEOUT);
|
|
483
|
+
|
|
484
|
+
const subscription = this.bleManager.subscribeToCharacteristic(
|
|
485
|
+
deviceId,
|
|
486
|
+
SERVICE_BOTA_PROVISIONING,
|
|
487
|
+
CHAR_PROVISIONING_RESULT,
|
|
488
|
+
(data) => {
|
|
489
|
+
clearTimeout(timeout);
|
|
490
|
+
subscription.remove();
|
|
491
|
+
|
|
492
|
+
if (data.length < 1) {
|
|
493
|
+
resolve({ success: false, error: 'unknown' });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const resultCode = data[0];
|
|
498
|
+
switch (resultCode) {
|
|
499
|
+
case PROVISIONING_SUCCESS:
|
|
500
|
+
resolve({ success: true });
|
|
501
|
+
break;
|
|
502
|
+
case PROVISIONING_INVALID_TOKEN:
|
|
503
|
+
resolve({ success: false, error: 'invalid_token' });
|
|
504
|
+
break;
|
|
505
|
+
case PROVISIONING_STORAGE_ERROR:
|
|
506
|
+
resolve({ success: false, error: 'storage_error' });
|
|
507
|
+
break;
|
|
508
|
+
case PROVISIONING_CHUNK_ERROR:
|
|
509
|
+
resolve({ success: false, error: 'chunk_error' });
|
|
510
|
+
break;
|
|
511
|
+
default:
|
|
512
|
+
resolve({ success: false, error: 'unknown' });
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
(error) => {
|
|
516
|
+
clearTimeout(timeout);
|
|
517
|
+
subscription.remove();
|
|
518
|
+
reject(new ProvisioningError(
|
|
519
|
+
`Provisioning notification error: ${error.message}`,
|
|
520
|
+
'NOTIFICATION_ERROR',
|
|
521
|
+
deviceId,
|
|
522
|
+
error
|
|
523
|
+
));
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Clean up resources
|
|
531
|
+
*/
|
|
532
|
+
destroy(): void {
|
|
533
|
+
log.info('Destroying DeviceManager');
|
|
534
|
+
|
|
535
|
+
// Clean up all status subscriptions
|
|
536
|
+
for (const sub of this.statusSubscriptions.values()) {
|
|
537
|
+
sub.remove();
|
|
538
|
+
}
|
|
539
|
+
this.statusSubscriptions.clear();
|
|
540
|
+
|
|
541
|
+
this.connectedDevices.clear();
|
|
542
|
+
this.removeAllListeners();
|
|
543
|
+
this.isInitialized = false;
|
|
544
|
+
}
|
|
545
|
+
}
|