@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,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Handler - Implements Device-App Protocol for recording transfer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Buffer } from 'buffer';
|
|
6
|
+
import { Subscription } from 'react-native-ble-plx';
|
|
7
|
+
|
|
8
|
+
import { getBleManager, BleManager } from '../ble/BleManager';
|
|
9
|
+
import {
|
|
10
|
+
SERVICE_BOTA_STORAGE,
|
|
11
|
+
CHAR_STORAGE_INFO,
|
|
12
|
+
CHAR_RECORDING_LIST,
|
|
13
|
+
CHAR_RECORDING_TRANSFER,
|
|
14
|
+
CHAR_TRANSFER_CONTROL,
|
|
15
|
+
TRANSFER_PACKET_TIMEOUT,
|
|
16
|
+
} from '../ble/constants';
|
|
17
|
+
import {
|
|
18
|
+
parseStorageInfo,
|
|
19
|
+
parseRecordingList,
|
|
20
|
+
parseTransferPacket,
|
|
21
|
+
createAckPacket,
|
|
22
|
+
createTransferCommand,
|
|
23
|
+
} from '../ble/parsers';
|
|
24
|
+
import type { StorageInfo } from '../models/Device';
|
|
25
|
+
import type { DeviceRecording, TransferPacket } from '../models/Recording';
|
|
26
|
+
import { TransferError, DeviceError } from '../utils/errors';
|
|
27
|
+
import { logger } from '../utils/logger';
|
|
28
|
+
|
|
29
|
+
const log = logger.tag('ProtocolHandler');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Transfer state for tracking ongoing transfers
|
|
33
|
+
*/
|
|
34
|
+
interface TransferState {
|
|
35
|
+
recordingUuid: string;
|
|
36
|
+
expectedSequence: number;
|
|
37
|
+
receivedPackets: Map<number, Buffer>;
|
|
38
|
+
totalBytes: number;
|
|
39
|
+
isComplete: boolean;
|
|
40
|
+
checksum?: number;
|
|
41
|
+
subscription?: Subscription;
|
|
42
|
+
timeoutId?: NodeJS.Timeout;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Transfer progress callback
|
|
47
|
+
*/
|
|
48
|
+
export type TransferProgressCallback = (
|
|
49
|
+
bytesReceived: number,
|
|
50
|
+
totalBytes?: number
|
|
51
|
+
) => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Protocol Handler class
|
|
55
|
+
*/
|
|
56
|
+
export class ProtocolHandler {
|
|
57
|
+
private bleManager: BleManager;
|
|
58
|
+
private activeTransfers: Map<string, TransferState> = new Map();
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
this.bleManager = getBleManager();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get storage info from device
|
|
66
|
+
*/
|
|
67
|
+
async getStorageInfo(deviceId: string): Promise<StorageInfo> {
|
|
68
|
+
if (!this.bleManager.isConnected(deviceId)) {
|
|
69
|
+
throw DeviceError.notConnected(deviceId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await this.bleManager.readCharacteristic(
|
|
73
|
+
deviceId,
|
|
74
|
+
SERVICE_BOTA_STORAGE,
|
|
75
|
+
CHAR_STORAGE_INFO
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return parseStorageInfo(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* List recordings on device
|
|
83
|
+
*/
|
|
84
|
+
async listRecordings(deviceId: string): Promise<DeviceRecording[]> {
|
|
85
|
+
if (!this.bleManager.isConnected(deviceId)) {
|
|
86
|
+
throw DeviceError.notConnected(deviceId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log.debug('Listing recordings', { deviceId });
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
let recordings: DeviceRecording[] = [];
|
|
93
|
+
let subscription: Subscription | undefined;
|
|
94
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
95
|
+
|
|
96
|
+
const cleanup = () => {
|
|
97
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
98
|
+
subscription?.remove();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Set up timeout
|
|
102
|
+
timeoutId = setTimeout(() => {
|
|
103
|
+
cleanup();
|
|
104
|
+
// If we received some data, return it; otherwise error
|
|
105
|
+
if (recordings.length > 0) {
|
|
106
|
+
resolve(recordings);
|
|
107
|
+
} else {
|
|
108
|
+
reject(new TransferError(
|
|
109
|
+
'Timeout waiting for recording list',
|
|
110
|
+
'LIST_TIMEOUT'
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
}, 5000);
|
|
114
|
+
|
|
115
|
+
// Subscribe to recording list notifications
|
|
116
|
+
subscription = this.bleManager.subscribeToCharacteristic(
|
|
117
|
+
deviceId,
|
|
118
|
+
SERVICE_BOTA_STORAGE,
|
|
119
|
+
CHAR_RECORDING_LIST,
|
|
120
|
+
(data) => {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = parseRecordingList(data);
|
|
123
|
+
recordings = recordings.concat(parsed);
|
|
124
|
+
|
|
125
|
+
// Check if this is the last packet (could check for end marker)
|
|
126
|
+
// For now, wait for timeout or explicit end
|
|
127
|
+
} catch (error) {
|
|
128
|
+
log.error('Failed to parse recording list', error as Error);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
(error) => {
|
|
132
|
+
cleanup();
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Send list command
|
|
138
|
+
const command = createTransferCommand('list');
|
|
139
|
+
this.bleManager
|
|
140
|
+
.writeCharacteristic(
|
|
141
|
+
deviceId,
|
|
142
|
+
SERVICE_BOTA_STORAGE,
|
|
143
|
+
CHAR_TRANSFER_CONTROL,
|
|
144
|
+
command
|
|
145
|
+
)
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
cleanup();
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Transfer a recording from device
|
|
155
|
+
* Returns the audio data as a Buffer
|
|
156
|
+
*/
|
|
157
|
+
async transferRecording(
|
|
158
|
+
deviceId: string,
|
|
159
|
+
recordingUuid: string,
|
|
160
|
+
onProgress?: TransferProgressCallback
|
|
161
|
+
): Promise<Buffer> {
|
|
162
|
+
if (!this.bleManager.isConnected(deviceId)) {
|
|
163
|
+
throw DeviceError.notConnected(deviceId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if transfer already in progress
|
|
167
|
+
if (this.activeTransfers.has(recordingUuid)) {
|
|
168
|
+
throw new TransferError(
|
|
169
|
+
'Transfer already in progress for this recording',
|
|
170
|
+
'TRANSFER_IN_PROGRESS',
|
|
171
|
+
recordingUuid
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
log.info('Starting recording transfer', { deviceId, recordingUuid });
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const state: TransferState = {
|
|
179
|
+
recordingUuid,
|
|
180
|
+
expectedSequence: 0,
|
|
181
|
+
receivedPackets: new Map(),
|
|
182
|
+
totalBytes: 0,
|
|
183
|
+
isComplete: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.activeTransfers.set(recordingUuid, state);
|
|
187
|
+
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
190
|
+
state.subscription?.remove();
|
|
191
|
+
this.activeTransfers.delete(recordingUuid);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const resetTimeout = () => {
|
|
195
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
196
|
+
state.timeoutId = setTimeout(() => {
|
|
197
|
+
cleanup();
|
|
198
|
+
reject(TransferError.timeout(recordingUuid));
|
|
199
|
+
}, TRANSFER_PACKET_TIMEOUT);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Subscribe to transfer data notifications
|
|
203
|
+
state.subscription = this.bleManager.subscribeToCharacteristic(
|
|
204
|
+
deviceId,
|
|
205
|
+
SERVICE_BOTA_STORAGE,
|
|
206
|
+
CHAR_RECORDING_TRANSFER,
|
|
207
|
+
async (data) => {
|
|
208
|
+
try {
|
|
209
|
+
resetTimeout();
|
|
210
|
+
|
|
211
|
+
const packet = parseTransferPacket(data);
|
|
212
|
+
await this.handleTransferPacket(
|
|
213
|
+
deviceId,
|
|
214
|
+
state,
|
|
215
|
+
packet,
|
|
216
|
+
onProgress
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (state.isComplete) {
|
|
220
|
+
cleanup();
|
|
221
|
+
|
|
222
|
+
// Assemble the audio data
|
|
223
|
+
const audioData = this.assembleAudioData(state);
|
|
224
|
+
|
|
225
|
+
// Verify checksum if available
|
|
226
|
+
if (state.checksum !== undefined) {
|
|
227
|
+
const calculatedChecksum = this.calculateCrc32(audioData);
|
|
228
|
+
if (calculatedChecksum !== state.checksum) {
|
|
229
|
+
reject(TransferError.checksumMismatch(recordingUuid));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log.info('Transfer completed', {
|
|
235
|
+
recordingUuid,
|
|
236
|
+
size: audioData.length,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
resolve(audioData);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
cleanup();
|
|
243
|
+
reject(error);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
(error) => {
|
|
247
|
+
cleanup();
|
|
248
|
+
reject(TransferError.interrupted(recordingUuid, error));
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Send start transfer command
|
|
253
|
+
const command = createTransferCommand('start', recordingUuid);
|
|
254
|
+
this.bleManager
|
|
255
|
+
.writeCharacteristic(
|
|
256
|
+
deviceId,
|
|
257
|
+
SERVICE_BOTA_STORAGE,
|
|
258
|
+
CHAR_TRANSFER_CONTROL,
|
|
259
|
+
command
|
|
260
|
+
)
|
|
261
|
+
.then(() => {
|
|
262
|
+
resetTimeout();
|
|
263
|
+
})
|
|
264
|
+
.catch((error) => {
|
|
265
|
+
cleanup();
|
|
266
|
+
reject(error);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Handle a transfer packet from device
|
|
273
|
+
*/
|
|
274
|
+
private async handleTransferPacket(
|
|
275
|
+
deviceId: string,
|
|
276
|
+
state: TransferState,
|
|
277
|
+
packet: TransferPacket,
|
|
278
|
+
onProgress?: TransferProgressCallback
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
switch (packet.type) {
|
|
281
|
+
case 'data':
|
|
282
|
+
if (packet.data) {
|
|
283
|
+
// Store packet data
|
|
284
|
+
state.receivedPackets.set(packet.sequenceNumber, Buffer.from(packet.data));
|
|
285
|
+
state.totalBytes += packet.data.length;
|
|
286
|
+
|
|
287
|
+
// Send ACK
|
|
288
|
+
await this.sendAck(deviceId, 'ack', packet.sequenceNumber);
|
|
289
|
+
|
|
290
|
+
// Report progress
|
|
291
|
+
onProgress?.(state.totalBytes);
|
|
292
|
+
|
|
293
|
+
log.debug('Received data packet', {
|
|
294
|
+
seq: packet.sequenceNumber,
|
|
295
|
+
size: packet.data.length,
|
|
296
|
+
total: state.totalBytes,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'eof':
|
|
302
|
+
state.checksum = packet.checksum;
|
|
303
|
+
state.isComplete = true;
|
|
304
|
+
|
|
305
|
+
// Send final ACK
|
|
306
|
+
await this.sendAck(deviceId, 'ack', packet.sequenceNumber);
|
|
307
|
+
|
|
308
|
+
log.debug('Received EOF packet', {
|
|
309
|
+
seq: packet.sequenceNumber,
|
|
310
|
+
checksum: packet.checksum,
|
|
311
|
+
});
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
case 'error':
|
|
315
|
+
throw TransferError.deviceError(
|
|
316
|
+
state.recordingUuid,
|
|
317
|
+
packet.errorCode ?? 0xff
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Send an ACK packet to device
|
|
324
|
+
*/
|
|
325
|
+
private async sendAck(
|
|
326
|
+
deviceId: string,
|
|
327
|
+
type: 'ack' | 'nack' | 'abort',
|
|
328
|
+
sequenceNumber: number
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
const ackPacket = createAckPacket(type, sequenceNumber);
|
|
331
|
+
|
|
332
|
+
await this.bleManager.writeCharacteristic(
|
|
333
|
+
deviceId,
|
|
334
|
+
SERVICE_BOTA_STORAGE,
|
|
335
|
+
CHAR_RECORDING_TRANSFER,
|
|
336
|
+
ackPacket,
|
|
337
|
+
false // Write without response for speed
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Assemble audio data from received packets
|
|
343
|
+
*/
|
|
344
|
+
private assembleAudioData(state: TransferState): Buffer {
|
|
345
|
+
// Sort packets by sequence number and concatenate
|
|
346
|
+
const sortedSequences = Array.from(state.receivedPackets.keys()).sort(
|
|
347
|
+
(a, b) => a - b
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const chunks: Buffer[] = [];
|
|
351
|
+
for (const seq of sortedSequences) {
|
|
352
|
+
const data = state.receivedPackets.get(seq);
|
|
353
|
+
if (data) {
|
|
354
|
+
chunks.push(data);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return Buffer.concat(chunks);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Calculate CRC32 checksum
|
|
363
|
+
*/
|
|
364
|
+
private calculateCrc32(data: Buffer): number {
|
|
365
|
+
let crc = 0xffffffff;
|
|
366
|
+
const table = this.getCrc32Table();
|
|
367
|
+
|
|
368
|
+
for (let i = 0; i < data.length; i++) {
|
|
369
|
+
crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get CRC32 lookup table (lazy initialized)
|
|
377
|
+
*/
|
|
378
|
+
private crc32Table: number[] | null = null;
|
|
379
|
+
private getCrc32Table(): number[] {
|
|
380
|
+
if (this.crc32Table) {
|
|
381
|
+
return this.crc32Table;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const table: number[] = [];
|
|
385
|
+
for (let i = 0; i < 256; i++) {
|
|
386
|
+
let crc = i;
|
|
387
|
+
for (let j = 0; j < 8; j++) {
|
|
388
|
+
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
389
|
+
}
|
|
390
|
+
table.push(crc >>> 0);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.crc32Table = table;
|
|
394
|
+
return table;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Confirm sync to device (allows device to delete local copy)
|
|
399
|
+
*/
|
|
400
|
+
async confirmSync(deviceId: string, recordingUuid: string): Promise<void> {
|
|
401
|
+
if (!this.bleManager.isConnected(deviceId)) {
|
|
402
|
+
throw DeviceError.notConnected(deviceId);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
log.debug('Confirming sync', { deviceId, recordingUuid });
|
|
406
|
+
|
|
407
|
+
const command = createTransferCommand('confirm', recordingUuid);
|
|
408
|
+
|
|
409
|
+
await this.bleManager.writeCharacteristic(
|
|
410
|
+
deviceId,
|
|
411
|
+
SERVICE_BOTA_STORAGE,
|
|
412
|
+
CHAR_TRANSFER_CONTROL,
|
|
413
|
+
command
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Cancel an ongoing transfer
|
|
419
|
+
*/
|
|
420
|
+
async cancelTransfer(deviceId: string, recordingUuid: string): Promise<void> {
|
|
421
|
+
const state = this.activeTransfers.get(recordingUuid);
|
|
422
|
+
if (!state) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
log.info('Cancelling transfer', { recordingUuid });
|
|
427
|
+
|
|
428
|
+
// Send abort
|
|
429
|
+
try {
|
|
430
|
+
await this.sendAck(deviceId, 'abort', state.expectedSequence);
|
|
431
|
+
} catch {
|
|
432
|
+
// Ignore errors during cancellation
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Clean up
|
|
436
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
437
|
+
state.subscription?.remove();
|
|
438
|
+
this.activeTransfers.delete(recordingUuid);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Check if a transfer is in progress
|
|
443
|
+
*/
|
|
444
|
+
isTransferInProgress(recordingUuid: string): boolean {
|
|
445
|
+
return this.activeTransfers.has(recordingUuid);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Clean up resources
|
|
450
|
+
*/
|
|
451
|
+
destroy(): void {
|
|
452
|
+
// Cancel all active transfers
|
|
453
|
+
for (const [, state] of this.activeTransfers) {
|
|
454
|
+
if (state.timeoutId) clearTimeout(state.timeoutId);
|
|
455
|
+
state.subscription?.remove();
|
|
456
|
+
}
|
|
457
|
+
this.activeTransfers.clear();
|
|
458
|
+
}
|
|
459
|
+
}
|