@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTA Manager - Handles firmware over-the-air updates
|
|
3
|
+
*
|
|
4
|
+
* Note: Full Nordic DFU implementation requires native modules.
|
|
5
|
+
* This is a placeholder that handles version checking and update preparation.
|
|
6
|
+
* Actual DFU transfer would need react-native-nordic-dfu or similar.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import EventEmitter from 'eventemitter3';
|
|
10
|
+
|
|
11
|
+
import { getBleManager } from '../ble/BleManager';
|
|
12
|
+
import {
|
|
13
|
+
SERVICE_BOTA_CONTROL,
|
|
14
|
+
CHAR_DEVICE_COMMAND,
|
|
15
|
+
DEVICE_CMD_ENTER_DFU,
|
|
16
|
+
} from '../ble/constants';
|
|
17
|
+
import type { ConnectedDevice } from '../models/Device';
|
|
18
|
+
import { DeviceError } from '../utils/errors';
|
|
19
|
+
import { logger } from '../utils/logger';
|
|
20
|
+
import { Buffer } from 'buffer';
|
|
21
|
+
|
|
22
|
+
const log = logger.tag('OTAManager');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Firmware info
|
|
26
|
+
*/
|
|
27
|
+
export interface FirmwareInfo {
|
|
28
|
+
version: string;
|
|
29
|
+
url: string;
|
|
30
|
+
checksum: string;
|
|
31
|
+
releaseNotes?: string;
|
|
32
|
+
size: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* OTA progress stages
|
|
37
|
+
*/
|
|
38
|
+
export type OtaStage =
|
|
39
|
+
| 'checking'
|
|
40
|
+
| 'downloading'
|
|
41
|
+
| 'preparing'
|
|
42
|
+
| 'updating'
|
|
43
|
+
| 'verifying'
|
|
44
|
+
| 'completed'
|
|
45
|
+
| 'failed';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* OTA progress
|
|
49
|
+
*/
|
|
50
|
+
export interface OtaProgress {
|
|
51
|
+
stage: OtaStage;
|
|
52
|
+
progress: number;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Events emitted by OTAManager
|
|
58
|
+
*/
|
|
59
|
+
interface OTAManagerEvents {
|
|
60
|
+
updateAvailable: (firmware: FirmwareInfo) => void;
|
|
61
|
+
progress: (deviceId: string, progress: OtaProgress) => void;
|
|
62
|
+
completed: (deviceId: string, version: string) => void;
|
|
63
|
+
failed: (deviceId: string, error: Error) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* OTA Manager class
|
|
68
|
+
*/
|
|
69
|
+
export class OTAManager extends EventEmitter<OTAManagerEvents> {
|
|
70
|
+
private firmwareCdnUrl: string;
|
|
71
|
+
|
|
72
|
+
constructor(firmwareCdnUrl: string = 'https://cdn.bota.dev/firmware') {
|
|
73
|
+
super();
|
|
74
|
+
this.firmwareCdnUrl = firmwareCdnUrl;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check for available firmware updates
|
|
79
|
+
*/
|
|
80
|
+
async checkForUpdate(device: ConnectedDevice): Promise<FirmwareInfo | null> {
|
|
81
|
+
log.info('Checking for firmware update', {
|
|
82
|
+
deviceId: device.id,
|
|
83
|
+
currentVersion: device.firmwareVersion,
|
|
84
|
+
deviceType: device.deviceType,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.emit('progress', device.id, { stage: 'checking', progress: 0 });
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Fetch latest firmware info from CDN
|
|
91
|
+
const response = await fetch(
|
|
92
|
+
`${this.firmwareCdnUrl}/latest?device_type=${device.deviceType}¤t=${device.firmwareVersion}`
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
log.info('No update available', { deviceId: device.id });
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Failed to check for updates: ${response.status}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const firmware = (await response.json()) as FirmwareInfo;
|
|
104
|
+
|
|
105
|
+
// Compare versions
|
|
106
|
+
if (this.isNewerVersion(firmware.version, device.firmwareVersion)) {
|
|
107
|
+
log.info('Update available', {
|
|
108
|
+
deviceId: device.id,
|
|
109
|
+
currentVersion: device.firmwareVersion,
|
|
110
|
+
newVersion: firmware.version,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.emit('updateAvailable', firmware);
|
|
114
|
+
return firmware;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log.info('Device is up to date', {
|
|
118
|
+
deviceId: device.id,
|
|
119
|
+
version: device.firmwareVersion,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
log.error('Failed to check for update', error as Error, {
|
|
125
|
+
deviceId: device.id,
|
|
126
|
+
});
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Download firmware package
|
|
133
|
+
*/
|
|
134
|
+
async downloadFirmware(firmware: FirmwareInfo): Promise<ArrayBuffer> {
|
|
135
|
+
log.info('Downloading firmware', {
|
|
136
|
+
version: firmware.version,
|
|
137
|
+
size: firmware.size,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const response = await fetch(firmware.url);
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`Failed to download firmware: ${response.status}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = await response.arrayBuffer();
|
|
147
|
+
|
|
148
|
+
// Verify checksum
|
|
149
|
+
// Note: In production, implement proper checksum verification
|
|
150
|
+
log.info('Firmware downloaded', { size: data.byteLength });
|
|
151
|
+
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Prepare device for DFU mode
|
|
157
|
+
*/
|
|
158
|
+
async enterDfuMode(device: ConnectedDevice): Promise<void> {
|
|
159
|
+
if (!getBleManager().isConnected(device.id)) {
|
|
160
|
+
throw DeviceError.notConnected(device.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
log.info('Entering DFU mode', { deviceId: device.id });
|
|
164
|
+
|
|
165
|
+
// Send DFU command to device
|
|
166
|
+
await getBleManager().writeCharacteristic(
|
|
167
|
+
device.id,
|
|
168
|
+
SERVICE_BOTA_CONTROL,
|
|
169
|
+
CHAR_DEVICE_COMMAND,
|
|
170
|
+
Buffer.from([DEVICE_CMD_ENTER_DFU])
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Device will disconnect and reboot into DFU mode
|
|
174
|
+
log.info('DFU command sent, device will reboot', { deviceId: device.id });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Perform firmware update
|
|
179
|
+
*
|
|
180
|
+
* Note: This is a placeholder. Full implementation requires:
|
|
181
|
+
* 1. react-native-nordic-dfu for actual DFU transfer
|
|
182
|
+
* 2. Native module integration
|
|
183
|
+
* 3. DFU package preparation
|
|
184
|
+
*/
|
|
185
|
+
async performUpdate(
|
|
186
|
+
device: ConnectedDevice,
|
|
187
|
+
firmware: FirmwareInfo
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
log.info('Starting firmware update', {
|
|
190
|
+
deviceId: device.id,
|
|
191
|
+
version: firmware.version,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.emit('progress', device.id, { stage: 'downloading', progress: 0 });
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Download firmware
|
|
198
|
+
await this.downloadFirmware(firmware);
|
|
199
|
+
|
|
200
|
+
this.emit('progress', device.id, { stage: 'downloading', progress: 1 });
|
|
201
|
+
this.emit('progress', device.id, { stage: 'preparing', progress: 0 });
|
|
202
|
+
|
|
203
|
+
// Enter DFU mode
|
|
204
|
+
await this.enterDfuMode(device);
|
|
205
|
+
|
|
206
|
+
this.emit('progress', device.id, { stage: 'preparing', progress: 1 });
|
|
207
|
+
|
|
208
|
+
// Note: At this point, we would use react-native-nordic-dfu
|
|
209
|
+
// to perform the actual firmware transfer.
|
|
210
|
+
// For now, we throw an error indicating this needs native implementation.
|
|
211
|
+
|
|
212
|
+
throw new Error(
|
|
213
|
+
'Full DFU implementation requires react-native-nordic-dfu native module. ' +
|
|
214
|
+
'Device has entered DFU mode and is advertising as "BotaDFU-xxx".'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// After DFU completes:
|
|
218
|
+
// this.emit('progress', device.id, { stage: 'completed', progress: 1 });
|
|
219
|
+
// this.emit('completed', device.id, firmware.version);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const err = error as Error;
|
|
222
|
+
log.error('Firmware update failed', err, { deviceId: device.id });
|
|
223
|
+
|
|
224
|
+
this.emit('progress', device.id, {
|
|
225
|
+
stage: 'failed',
|
|
226
|
+
progress: 0,
|
|
227
|
+
error: err.message,
|
|
228
|
+
});
|
|
229
|
+
this.emit('failed', device.id, err);
|
|
230
|
+
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Compare semantic versions
|
|
237
|
+
*/
|
|
238
|
+
private isNewerVersion(newVersion: string, currentVersion: string): boolean {
|
|
239
|
+
const parseVersion = (v: string): number[] => {
|
|
240
|
+
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const newParts = parseVersion(newVersion);
|
|
244
|
+
const currentParts = parseVersion(currentVersion);
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < Math.max(newParts.length, currentParts.length); i++) {
|
|
247
|
+
const newPart = newParts[i] || 0;
|
|
248
|
+
const currentPart = currentParts[i] || 0;
|
|
249
|
+
|
|
250
|
+
if (newPart > currentPart) return true;
|
|
251
|
+
if (newPart < currentPart) return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clean up resources
|
|
259
|
+
*/
|
|
260
|
+
destroy(): void {
|
|
261
|
+
this.removeAllListeners();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording Manager - Handles recording sync and upload operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import EventEmitter from 'eventemitter3';
|
|
6
|
+
|
|
7
|
+
import { ProtocolHandler } from '../protocol/ProtocolHandler';
|
|
8
|
+
import { StorageManager } from '../storage/StorageManager';
|
|
9
|
+
import { UploadQueue } from '../upload/UploadQueue';
|
|
10
|
+
import type { ConnectedDevice, StorageInfo } from '../models/Device';
|
|
11
|
+
import type {
|
|
12
|
+
DeviceRecording,
|
|
13
|
+
UploadInfo,
|
|
14
|
+
SyncProgress,
|
|
15
|
+
UploadTask,
|
|
16
|
+
} from '../models/Recording';
|
|
17
|
+
import type { RecordingManagerEvents } from '../models/Status';
|
|
18
|
+
import { DeviceError } from '../utils/errors';
|
|
19
|
+
import { logger } from '../utils/logger';
|
|
20
|
+
import { getBleManager } from '../ble/BleManager';
|
|
21
|
+
|
|
22
|
+
const log = logger.tag('RecordingManager');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Upload info provider callback type
|
|
26
|
+
*/
|
|
27
|
+
export type UploadInfoProvider = (
|
|
28
|
+
recording: DeviceRecording
|
|
29
|
+
) => Promise<UploadInfo>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recording Manager class
|
|
33
|
+
*/
|
|
34
|
+
export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
|
|
35
|
+
private protocolHandler: ProtocolHandler;
|
|
36
|
+
private storage: StorageManager;
|
|
37
|
+
private uploadQueue: UploadQueue;
|
|
38
|
+
private isInitialized = false;
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super();
|
|
42
|
+
this.protocolHandler = new ProtocolHandler();
|
|
43
|
+
this.storage = new StorageManager();
|
|
44
|
+
this.uploadQueue = new UploadQueue(this.storage, { autoStart: false });
|
|
45
|
+
|
|
46
|
+
this.setupUploadQueueListeners();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize the recording manager
|
|
51
|
+
*/
|
|
52
|
+
async initialize(): Promise<void> {
|
|
53
|
+
if (this.isInitialized) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await this.storage.initialize();
|
|
58
|
+
this.isInitialized = true;
|
|
59
|
+
|
|
60
|
+
// Start processing any pending uploads
|
|
61
|
+
this.uploadQueue.resume();
|
|
62
|
+
|
|
63
|
+
log.info('RecordingManager initialized');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set up upload queue event listeners
|
|
68
|
+
*/
|
|
69
|
+
private setupUploadQueueListeners(): void {
|
|
70
|
+
this.uploadQueue.on('taskAdded', () => {
|
|
71
|
+
this.emit('queueUpdated', this.uploadQueue.getAllTasks());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.uploadQueue.on('taskUpdated', () => {
|
|
75
|
+
this.emit('queueUpdated', this.uploadQueue.getAllTasks());
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.uploadQueue.on('taskCompleted', (taskId, recordingId) => {
|
|
79
|
+
this.emit('uploadCompleted', taskId, recordingId);
|
|
80
|
+
this.emit('queueUpdated', this.uploadQueue.getAllTasks());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.uploadQueue.on('taskFailed', (taskId, error) => {
|
|
84
|
+
this.emit('uploadFailed', taskId, error);
|
|
85
|
+
this.emit('queueUpdated', this.uploadQueue.getAllTasks());
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.uploadQueue.on('uploadProgress', (taskId, progress) => {
|
|
89
|
+
this.emit('uploadProgress', taskId, progress);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get storage info from device
|
|
95
|
+
*/
|
|
96
|
+
async getStorageInfo(device: ConnectedDevice): Promise<StorageInfo> {
|
|
97
|
+
if (!getBleManager().isConnected(device.id)) {
|
|
98
|
+
throw DeviceError.notConnected(device.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this.protocolHandler.getStorageInfo(device.id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List recordings on a device
|
|
106
|
+
*/
|
|
107
|
+
async listRecordings(device: ConnectedDevice): Promise<DeviceRecording[]> {
|
|
108
|
+
if (!getBleManager().isConnected(device.id)) {
|
|
109
|
+
throw DeviceError.notConnected(device.id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log.info('Listing recordings', { deviceId: device.id });
|
|
113
|
+
|
|
114
|
+
const recordings = await this.protocolHandler.listRecordings(device.id);
|
|
115
|
+
|
|
116
|
+
log.info('Found recordings', {
|
|
117
|
+
deviceId: device.id,
|
|
118
|
+
count: recordings.length,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return recordings;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sync a single recording from device
|
|
126
|
+
* Transfers from device, saves locally, and queues for upload
|
|
127
|
+
*/
|
|
128
|
+
async *syncRecording(
|
|
129
|
+
device: ConnectedDevice,
|
|
130
|
+
recording: DeviceRecording,
|
|
131
|
+
uploadInfo: UploadInfo
|
|
132
|
+
): AsyncGenerator<SyncProgress> {
|
|
133
|
+
if (!getBleManager().isConnected(device.id)) {
|
|
134
|
+
throw DeviceError.notConnected(device.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
log.info('Starting recording sync', {
|
|
138
|
+
deviceId: device.id,
|
|
139
|
+
recordingUuid: recording.uuid,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.emit('syncStarted', recording.uuid);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Stage: Preparing
|
|
146
|
+
yield {
|
|
147
|
+
stage: 'preparing',
|
|
148
|
+
progress: 0,
|
|
149
|
+
totalBytes: recording.fileSizeBytes,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Stage: Transferring from device
|
|
153
|
+
const audioData = await this.protocolHandler.transferRecording(
|
|
154
|
+
device.id,
|
|
155
|
+
recording.uuid,
|
|
156
|
+
(_bytesReceived, _totalBytes) => {
|
|
157
|
+
// Progress callback - can be used for real-time progress updates
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
yield {
|
|
162
|
+
stage: 'transferring',
|
|
163
|
+
progress: 1,
|
|
164
|
+
bytesTransferred: audioData.length,
|
|
165
|
+
totalBytes: recording.fileSizeBytes,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Save locally
|
|
169
|
+
const localPath = await this.storage.saveRecordingData(
|
|
170
|
+
device.id,
|
|
171
|
+
recording.uuid,
|
|
172
|
+
audioData
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Stage: Uploading
|
|
176
|
+
yield {
|
|
177
|
+
stage: 'uploading',
|
|
178
|
+
progress: 0,
|
|
179
|
+
bytesUploaded: 0,
|
|
180
|
+
totalBytes: audioData.length,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Queue for upload (this will process in background)
|
|
184
|
+
const task = await this.uploadQueue.enqueue({
|
|
185
|
+
recordingId: uploadInfo.recordingId,
|
|
186
|
+
deviceId: device.id,
|
|
187
|
+
localPath,
|
|
188
|
+
uploadUrl: uploadInfo.uploadUrl,
|
|
189
|
+
uploadToken: uploadInfo.uploadToken,
|
|
190
|
+
completeUrl: uploadInfo.completeUrl,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Wait for upload to complete
|
|
194
|
+
await this.waitForUpload(task.id);
|
|
195
|
+
|
|
196
|
+
// Stage: Completing - Confirm sync to device
|
|
197
|
+
yield {
|
|
198
|
+
stage: 'completing',
|
|
199
|
+
progress: 0.5,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await this.protocolHandler.confirmSync(device.id, recording.uuid);
|
|
203
|
+
|
|
204
|
+
// Update last sync time
|
|
205
|
+
await this.storage.setLastSyncTime(device.id);
|
|
206
|
+
|
|
207
|
+
// Stage: Completed
|
|
208
|
+
yield {
|
|
209
|
+
stage: 'completed',
|
|
210
|
+
progress: 1,
|
|
211
|
+
recordingId: uploadInfo.recordingId,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
log.info('Recording sync completed', {
|
|
215
|
+
deviceId: device.id,
|
|
216
|
+
recordingUuid: recording.uuid,
|
|
217
|
+
recordingId: uploadInfo.recordingId,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.emit('syncCompleted', recording.uuid, uploadInfo.recordingId);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const err = error as Error;
|
|
223
|
+
log.error('Recording sync failed', err, {
|
|
224
|
+
deviceId: device.id,
|
|
225
|
+
recordingUuid: recording.uuid,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
yield {
|
|
229
|
+
stage: 'failed',
|
|
230
|
+
progress: 0,
|
|
231
|
+
error: err.message,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
this.emit('syncFailed', recording.uuid, err);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wait for an upload task to complete
|
|
241
|
+
*/
|
|
242
|
+
private async waitForUpload(taskId: string): Promise<void> {
|
|
243
|
+
return new Promise<void>((resolve, reject) => {
|
|
244
|
+
const checkTask = () => {
|
|
245
|
+
const task = this.storage.getUploadTask(taskId);
|
|
246
|
+
if (!task) {
|
|
247
|
+
reject(new Error('Upload task not found'));
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (task.status === 'completed') {
|
|
252
|
+
resolve();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (task.status === 'failed') {
|
|
257
|
+
reject(new Error(task.errorMessage || 'Upload failed'));
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Check immediately
|
|
265
|
+
if (checkTask()) return;
|
|
266
|
+
|
|
267
|
+
// Poll for completion
|
|
268
|
+
const interval = setInterval(() => {
|
|
269
|
+
if (checkTask()) {
|
|
270
|
+
clearInterval(interval);
|
|
271
|
+
}
|
|
272
|
+
}, 500);
|
|
273
|
+
|
|
274
|
+
// Also listen for events
|
|
275
|
+
const onComplete = (completedTaskId: string) => {
|
|
276
|
+
if (completedTaskId === taskId) {
|
|
277
|
+
clearInterval(interval);
|
|
278
|
+
this.uploadQueue.off('taskCompleted', onComplete);
|
|
279
|
+
this.uploadQueue.off('taskFailed', onFailed);
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const onFailed = (failedTaskId: string, error: Error) => {
|
|
285
|
+
if (failedTaskId === taskId) {
|
|
286
|
+
clearInterval(interval);
|
|
287
|
+
this.uploadQueue.off('taskCompleted', onComplete);
|
|
288
|
+
this.uploadQueue.off('taskFailed', onFailed);
|
|
289
|
+
reject(error);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
this.uploadQueue.on('taskCompleted', onComplete);
|
|
294
|
+
this.uploadQueue.on('taskFailed', onFailed);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Sync all recordings from a device
|
|
300
|
+
*/
|
|
301
|
+
async *syncAllRecordings(
|
|
302
|
+
device: ConnectedDevice,
|
|
303
|
+
uploadInfoProvider: UploadInfoProvider
|
|
304
|
+
): AsyncGenerator<SyncProgress & { recordingIndex?: number; totalRecordings?: number }> {
|
|
305
|
+
if (!getBleManager().isConnected(device.id)) {
|
|
306
|
+
throw DeviceError.notConnected(device.id);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
log.info('Syncing all recordings', { deviceId: device.id });
|
|
310
|
+
|
|
311
|
+
// List recordings
|
|
312
|
+
const recordings = await this.listRecordings(device);
|
|
313
|
+
|
|
314
|
+
if (recordings.length === 0) {
|
|
315
|
+
log.info('No recordings to sync', { deviceId: device.id });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
log.info('Starting sync of recordings', {
|
|
320
|
+
deviceId: device.id,
|
|
321
|
+
count: recordings.length,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Sync each recording
|
|
325
|
+
for (let i = 0; i < recordings.length; i++) {
|
|
326
|
+
const recording = recordings[i];
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Get upload info from customer backend
|
|
330
|
+
const uploadInfo = await uploadInfoProvider(recording);
|
|
331
|
+
|
|
332
|
+
// Sync the recording
|
|
333
|
+
for await (const progress of this.syncRecording(
|
|
334
|
+
device,
|
|
335
|
+
recording,
|
|
336
|
+
uploadInfo
|
|
337
|
+
)) {
|
|
338
|
+
yield {
|
|
339
|
+
...progress,
|
|
340
|
+
recordingIndex: i,
|
|
341
|
+
totalRecordings: recordings.length,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const err = error as Error;
|
|
346
|
+
log.error('Failed to sync recording', err, {
|
|
347
|
+
deviceId: device.id,
|
|
348
|
+
recordingUuid: recording.uuid,
|
|
349
|
+
index: i,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
yield {
|
|
353
|
+
stage: 'failed',
|
|
354
|
+
progress: 0,
|
|
355
|
+
error: err.message,
|
|
356
|
+
recordingIndex: i,
|
|
357
|
+
totalRecordings: recordings.length,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Continue with next recording
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get pending uploads
|
|
367
|
+
*/
|
|
368
|
+
getPendingUploads(): UploadTask[] {
|
|
369
|
+
return this.uploadQueue.getPendingTasks();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get all uploads
|
|
374
|
+
*/
|
|
375
|
+
getAllUploads(): UploadTask[] {
|
|
376
|
+
return this.uploadQueue.getAllTasks();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Cancel a pending upload
|
|
381
|
+
*/
|
|
382
|
+
async cancelUpload(taskId: string): Promise<void> {
|
|
383
|
+
await this.uploadQueue.cancel(taskId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Retry failed uploads
|
|
388
|
+
*/
|
|
389
|
+
async retryFailedUploads(): Promise<void> {
|
|
390
|
+
await this.uploadQueue.retryFailed();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Clear completed uploads from queue
|
|
395
|
+
*/
|
|
396
|
+
async clearCompletedUploads(): Promise<void> {
|
|
397
|
+
await this.storage.clearCompletedTasks();
|
|
398
|
+
this.emit('queueUpdated', this.uploadQueue.getAllTasks());
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Clear all uploads
|
|
403
|
+
*/
|
|
404
|
+
async clearAllUploads(): Promise<void> {
|
|
405
|
+
await this.uploadQueue.cancelAll();
|
|
406
|
+
this.emit('queueUpdated', []);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Pause upload processing
|
|
411
|
+
*/
|
|
412
|
+
pauseUploads(): void {
|
|
413
|
+
this.uploadQueue.pause();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resume upload processing
|
|
418
|
+
*/
|
|
419
|
+
resumeUploads(): void {
|
|
420
|
+
this.uploadQueue.resume();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Clean up resources
|
|
425
|
+
*/
|
|
426
|
+
destroy(): void {
|
|
427
|
+
log.info('Destroying RecordingManager');
|
|
428
|
+
this.protocolHandler.destroy();
|
|
429
|
+
this.uploadQueue.destroy();
|
|
430
|
+
this.storage.destroy();
|
|
431
|
+
this.removeAllListeners();
|
|
432
|
+
this.isInitialized = false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managers module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { DeviceManager } from './DeviceManager';
|
|
6
|
+
export { RecordingManager, type UploadInfoProvider } from './RecordingManager';
|
|
7
|
+
export {
|
|
8
|
+
OTAManager,
|
|
9
|
+
type FirmwareInfo,
|
|
10
|
+
type OtaStage,
|
|
11
|
+
type OtaProgress,
|
|
12
|
+
} from './OTAManager';
|