@bota-dev/react-native-sdk 0.0.3 → 0.0.4
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/lib/commonjs/ble/BleManager.js +16 -1
- package/lib/commonjs/ble/BleManager.js.map +1 -1
- package/lib/commonjs/ble/constants.js +16 -2
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +216 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/module/ble/BleManager.js +19 -2
- package/lib/module/ble/BleManager.js.map +1 -1
- package/lib/module/ble/constants.js +14 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +217 -1
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/typescript/src/ble/BleManager.d.ts.map +1 -1
- package/lib/typescript/src/ble/constants.d.ts +10 -0
- package/lib/typescript/src/ble/constants.d.ts.map +1 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts +43 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/src/models/Device.d.ts +75 -0
- package/lib/typescript/src/models/Device.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ble/BleManager.ts +12 -2
- package/src/ble/constants.ts +14 -0
- package/src/managers/DeviceManager.ts +250 -0
- package/src/models/Device.ts +93 -0
package/src/ble/constants.ts
CHANGED
|
@@ -102,6 +102,20 @@ export const ACK_TYPE_ABORT = 0x12;
|
|
|
102
102
|
// Device command values
|
|
103
103
|
export const DEVICE_CMD_ENTER_DFU = 0x03;
|
|
104
104
|
|
|
105
|
+
// Recording control opcodes (for remote start/stop)
|
|
106
|
+
export const RECORDING_CMD_LOCAL_START = 0x01;
|
|
107
|
+
export const RECORDING_CMD_LOCAL_STOP = 0x02;
|
|
108
|
+
export const RECORDING_CMD_GRANT_START = 0x10;
|
|
109
|
+
export const RECORDING_CMD_GRANT_STOP = 0x11;
|
|
110
|
+
|
|
111
|
+
// Recording control response codes
|
|
112
|
+
export const RECORDING_RESULT_SUCCESS = 0x00;
|
|
113
|
+
export const RECORDING_RESULT_ERROR = 0x01;
|
|
114
|
+
export const RECORDING_RESULT_ALREADY_RECORDING = 0x02;
|
|
115
|
+
export const RECORDING_RESULT_NOT_RECORDING = 0x03;
|
|
116
|
+
export const RECORDING_RESULT_INVALID_GRANT = 0x04;
|
|
117
|
+
export const RECORDING_RESULT_GRANT_EXPIRED = 0x05;
|
|
118
|
+
|
|
105
119
|
// Device state values (from status)
|
|
106
120
|
export const DEVICE_STATE_IDLE = 0x00;
|
|
107
121
|
export const DEVICE_STATE_RECORDING = 0x01;
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
CHAR_API_ENDPOINT,
|
|
20
20
|
CHAR_PROVISIONING_RESULT,
|
|
21
21
|
CHAR_DEVICE_STATUS,
|
|
22
|
+
CHAR_RECORDING_CONTROL,
|
|
23
|
+
CHAR_RECORDING_STATUS,
|
|
22
24
|
CHAR_TIME_SYNC,
|
|
23
25
|
API_ENDPOINT_PRODUCTION,
|
|
24
26
|
API_ENDPOINT_SANDBOX,
|
|
@@ -27,6 +29,13 @@ import {
|
|
|
27
29
|
PROVISIONING_STORAGE_ERROR,
|
|
28
30
|
PROVISIONING_CHUNK_ERROR,
|
|
29
31
|
OPERATION_TIMEOUT,
|
|
32
|
+
RECORDING_CMD_GRANT_START,
|
|
33
|
+
RECORDING_CMD_GRANT_STOP,
|
|
34
|
+
RECORDING_RESULT_SUCCESS,
|
|
35
|
+
RECORDING_RESULT_ALREADY_RECORDING,
|
|
36
|
+
RECORDING_RESULT_NOT_RECORDING,
|
|
37
|
+
RECORDING_RESULT_INVALID_GRANT,
|
|
38
|
+
RECORDING_RESULT_GRANT_EXPIRED,
|
|
30
39
|
} from '../ble/constants';
|
|
31
40
|
import {
|
|
32
41
|
parsePairingState,
|
|
@@ -40,6 +49,10 @@ import type {
|
|
|
40
49
|
ScanOptions,
|
|
41
50
|
Environment,
|
|
42
51
|
ProvisioningResult,
|
|
52
|
+
RecordingState,
|
|
53
|
+
// RecordingCommand, // TODO: Re-enable when used
|
|
54
|
+
StartRecordingOptions,
|
|
55
|
+
StopRecordingOptions,
|
|
43
56
|
} from '../models/Device';
|
|
44
57
|
import type { DeviceManagerEvents } from '../models/Status';
|
|
45
58
|
import {
|
|
@@ -526,6 +539,243 @@ export class DeviceManager extends EventEmitter<DeviceManagerEvents> {
|
|
|
526
539
|
});
|
|
527
540
|
}
|
|
528
541
|
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// Remote Recording Control (MVP)
|
|
544
|
+
// ============================================================================
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Request to start recording on a device remotely.
|
|
548
|
+
* This writes a signed grant token to the device via BLE.
|
|
549
|
+
*
|
|
550
|
+
* @param device - Connected device
|
|
551
|
+
* @param grantToken - Signed JWT grant token from backend
|
|
552
|
+
* @param _options - Optional recording options (for future use)
|
|
553
|
+
* @returns Recording command result
|
|
554
|
+
*/
|
|
555
|
+
async requestStartRecording(
|
|
556
|
+
device: ConnectedDevice,
|
|
557
|
+
grantToken: string,
|
|
558
|
+
_options?: StartRecordingOptions
|
|
559
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
560
|
+
log.info('Requesting start recording', { deviceId: device.id });
|
|
561
|
+
|
|
562
|
+
if (!this.isConnected(device.id)) {
|
|
563
|
+
throw DeviceError.notConnected(device.id);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
// Create payload: [opcode, grant_token_bytes]
|
|
568
|
+
const tokenBuffer = Buffer.from(grantToken, 'utf8');
|
|
569
|
+
const payload = Buffer.alloc(1 + tokenBuffer.length);
|
|
570
|
+
payload.writeUInt8(RECORDING_CMD_GRANT_START, 0);
|
|
571
|
+
tokenBuffer.copy(payload, 1);
|
|
572
|
+
|
|
573
|
+
// Set up response listener before writing
|
|
574
|
+
const resultPromise = this.waitForRecordingResult(device.id);
|
|
575
|
+
|
|
576
|
+
// Write to recording control characteristic
|
|
577
|
+
await this.bleManager.writeCharacteristic(
|
|
578
|
+
device.id,
|
|
579
|
+
SERVICE_BOTA_CONTROL,
|
|
580
|
+
CHAR_RECORDING_CONTROL,
|
|
581
|
+
payload
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// Wait for device response
|
|
585
|
+
const result = await resultPromise;
|
|
586
|
+
|
|
587
|
+
log.info('Start recording result', { deviceId: device.id, result });
|
|
588
|
+
|
|
589
|
+
return result;
|
|
590
|
+
} catch (error) {
|
|
591
|
+
log.error('Failed to start recording', error as Error, { deviceId: device.id });
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Request to stop recording on a device remotely.
|
|
598
|
+
* This writes a signed grant token to the device via BLE.
|
|
599
|
+
*
|
|
600
|
+
* @param device - Connected device
|
|
601
|
+
* @param grantToken - Signed JWT grant token from backend
|
|
602
|
+
* @param _options - Optional stop options (for future use)
|
|
603
|
+
* @returns Recording command result
|
|
604
|
+
*/
|
|
605
|
+
async requestStopRecording(
|
|
606
|
+
device: ConnectedDevice,
|
|
607
|
+
grantToken: string,
|
|
608
|
+
_options?: StopRecordingOptions
|
|
609
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
610
|
+
log.info('Requesting stop recording', { deviceId: device.id });
|
|
611
|
+
|
|
612
|
+
if (!this.isConnected(device.id)) {
|
|
613
|
+
throw DeviceError.notConnected(device.id);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
// Create payload: [opcode, grant_token_bytes]
|
|
618
|
+
const tokenBuffer = Buffer.from(grantToken, 'utf8');
|
|
619
|
+
const payload = Buffer.alloc(1 + tokenBuffer.length);
|
|
620
|
+
payload.writeUInt8(RECORDING_CMD_GRANT_STOP, 0);
|
|
621
|
+
tokenBuffer.copy(payload, 1);
|
|
622
|
+
|
|
623
|
+
// Set up response listener before writing
|
|
624
|
+
const resultPromise = this.waitForRecordingResult(device.id);
|
|
625
|
+
|
|
626
|
+
// Write to recording control characteristic
|
|
627
|
+
await this.bleManager.writeCharacteristic(
|
|
628
|
+
device.id,
|
|
629
|
+
SERVICE_BOTA_CONTROL,
|
|
630
|
+
CHAR_RECORDING_CONTROL,
|
|
631
|
+
payload
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// Wait for device response
|
|
635
|
+
const result = await resultPromise;
|
|
636
|
+
|
|
637
|
+
log.info('Stop recording result', { deviceId: device.id, result });
|
|
638
|
+
|
|
639
|
+
return result;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
log.error('Failed to stop recording', error as Error, { deviceId: device.id });
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Get current recording state from device
|
|
648
|
+
*/
|
|
649
|
+
async getRecordingState(device: ConnectedDevice): Promise<RecordingState> {
|
|
650
|
+
if (!this.isConnected(device.id)) {
|
|
651
|
+
throw DeviceError.notConnected(device.id);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const data = await this.bleManager.readCharacteristic(
|
|
655
|
+
device.id,
|
|
656
|
+
SERVICE_BOTA_CONTROL,
|
|
657
|
+
CHAR_RECORDING_STATUS
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
return this.parseRecordingState(data);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Subscribe to recording state changes
|
|
665
|
+
*/
|
|
666
|
+
subscribeToRecordingState(
|
|
667
|
+
device: ConnectedDevice,
|
|
668
|
+
callback: (state: RecordingState) => void
|
|
669
|
+
): () => void {
|
|
670
|
+
if (!this.isConnected(device.id)) {
|
|
671
|
+
throw DeviceError.notConnected(device.id);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const subscription = this.bleManager.subscribeToCharacteristic(
|
|
675
|
+
device.id,
|
|
676
|
+
SERVICE_BOTA_CONTROL,
|
|
677
|
+
CHAR_RECORDING_STATUS,
|
|
678
|
+
(data) => {
|
|
679
|
+
try {
|
|
680
|
+
const state = this.parseRecordingState(data);
|
|
681
|
+
callback(state);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
log.error('Failed to parse recording state', error as Error);
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
(error) => {
|
|
687
|
+
log.error('Recording state subscription error', error);
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return () => {
|
|
692
|
+
subscription.remove();
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Wait for recording control result from device
|
|
698
|
+
*/
|
|
699
|
+
private waitForRecordingResult(
|
|
700
|
+
deviceId: string
|
|
701
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
702
|
+
return new Promise((resolve, reject) => {
|
|
703
|
+
const timeout = setTimeout(() => {
|
|
704
|
+
subscription.remove();
|
|
705
|
+
resolve({ success: false, error: 'timeout' });
|
|
706
|
+
}, OPERATION_TIMEOUT);
|
|
707
|
+
|
|
708
|
+
const subscription = this.bleManager.subscribeToCharacteristic(
|
|
709
|
+
deviceId,
|
|
710
|
+
SERVICE_BOTA_CONTROL,
|
|
711
|
+
CHAR_RECORDING_STATUS,
|
|
712
|
+
(data) => {
|
|
713
|
+
clearTimeout(timeout);
|
|
714
|
+
subscription.remove();
|
|
715
|
+
|
|
716
|
+
if (data.length < 1) {
|
|
717
|
+
resolve({ success: false, error: 'invalid_response' });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Parse response: [state, timestamp(4), result_code, source]
|
|
722
|
+
const resultCode = data.length >= 6 ? data[5] : data[0];
|
|
723
|
+
|
|
724
|
+
switch (resultCode) {
|
|
725
|
+
case RECORDING_RESULT_SUCCESS:
|
|
726
|
+
resolve({ success: true });
|
|
727
|
+
break;
|
|
728
|
+
case RECORDING_RESULT_ALREADY_RECORDING:
|
|
729
|
+
resolve({ success: false, error: 'already_recording' });
|
|
730
|
+
break;
|
|
731
|
+
case RECORDING_RESULT_NOT_RECORDING:
|
|
732
|
+
resolve({ success: false, error: 'not_recording' });
|
|
733
|
+
break;
|
|
734
|
+
case RECORDING_RESULT_INVALID_GRANT:
|
|
735
|
+
resolve({ success: false, error: 'invalid_grant' });
|
|
736
|
+
break;
|
|
737
|
+
case RECORDING_RESULT_GRANT_EXPIRED:
|
|
738
|
+
resolve({ success: false, error: 'grant_expired' });
|
|
739
|
+
break;
|
|
740
|
+
default:
|
|
741
|
+
// State 0x01 = recording, 0x00 = idle
|
|
742
|
+
if (data[0] === 0x01 || data[0] === 0x00) {
|
|
743
|
+
resolve({ success: true });
|
|
744
|
+
} else {
|
|
745
|
+
resolve({ success: false, error: 'unknown_error' });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
(error) => {
|
|
750
|
+
clearTimeout(timeout);
|
|
751
|
+
subscription.remove();
|
|
752
|
+
reject(new DeviceError(
|
|
753
|
+
`Recording control error: ${error.message}`,
|
|
754
|
+
'RECORDING_CONTROL_ERROR',
|
|
755
|
+
deviceId,
|
|
756
|
+
error
|
|
757
|
+
));
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Parse recording state from BLE data
|
|
765
|
+
*/
|
|
766
|
+
private parseRecordingState(data: Buffer): RecordingState {
|
|
767
|
+
// Format: [state, timestamp(4), result_code, source]
|
|
768
|
+
const state = data.length >= 1 ? data[0] : 0;
|
|
769
|
+
const timestamp = data.length >= 5 ? data.readUInt32LE(1) : 0;
|
|
770
|
+
const source = data.length >= 7 ? data[6] : 0;
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
active: state === 0x01,
|
|
774
|
+
startedAt: timestamp > 0 ? new Date(timestamp * 1000) : undefined,
|
|
775
|
+
initiatedBy: source === 0x01 ? 'remote' : 'local',
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
529
779
|
/**
|
|
530
780
|
* Clean up resources
|
|
531
781
|
*/
|
package/src/models/Device.ts
CHANGED
|
@@ -162,3 +162,96 @@ export interface ProvisioningResult {
|
|
|
162
162
|
success: boolean;
|
|
163
163
|
error?: 'invalid_token' | 'storage_error' | 'chunk_error' | 'unknown';
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Remote Recording Control Types
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Recording command type
|
|
172
|
+
*/
|
|
173
|
+
export type RecordingCommandType = 'start_recording' | 'stop_recording';
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Recording command status
|
|
177
|
+
*/
|
|
178
|
+
export type RecordingCommandStatus =
|
|
179
|
+
| 'pending'
|
|
180
|
+
| 'delivered'
|
|
181
|
+
| 'executed'
|
|
182
|
+
| 'failed'
|
|
183
|
+
| 'expired'
|
|
184
|
+
| 'cancelled';
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Options for starting recording remotely
|
|
188
|
+
*/
|
|
189
|
+
export interface StartRecordingOptions {
|
|
190
|
+
/** Maximum recording duration in seconds (auto-stop) */
|
|
191
|
+
maxDurationSec?: number;
|
|
192
|
+
/** Metadata to attach to the recording */
|
|
193
|
+
metadata?: Record<string, unknown>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Options for stopping recording remotely
|
|
198
|
+
*/
|
|
199
|
+
export interface StopRecordingOptions {
|
|
200
|
+
/** Whether to trigger immediate upload after stopping */
|
|
201
|
+
uploadImmediately?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Recording command result
|
|
206
|
+
*/
|
|
207
|
+
export interface RecordingCommandResult {
|
|
208
|
+
/** Command ID from backend */
|
|
209
|
+
commandId: string;
|
|
210
|
+
/** Recording ID (for start_recording) */
|
|
211
|
+
recordingId?: string;
|
|
212
|
+
/** When recording started */
|
|
213
|
+
startedAt?: Date;
|
|
214
|
+
/** When recording stopped */
|
|
215
|
+
stoppedAt?: Date;
|
|
216
|
+
/** Recording duration in seconds */
|
|
217
|
+
durationSeconds?: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Recording command error
|
|
222
|
+
*/
|
|
223
|
+
export interface RecordingCommandError {
|
|
224
|
+
code: string;
|
|
225
|
+
message: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Full recording command response
|
|
230
|
+
*/
|
|
231
|
+
export interface RecordingCommand {
|
|
232
|
+
id: string;
|
|
233
|
+
deviceId: string;
|
|
234
|
+
type: RecordingCommandType;
|
|
235
|
+
status: RecordingCommandStatus;
|
|
236
|
+
grantToken: string;
|
|
237
|
+
result?: RecordingCommandResult;
|
|
238
|
+
error?: RecordingCommandError;
|
|
239
|
+
expiresAt?: Date;
|
|
240
|
+
createdAt: Date;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Current recording state of the device
|
|
245
|
+
*/
|
|
246
|
+
export interface RecordingState {
|
|
247
|
+
/** Whether device is currently recording */
|
|
248
|
+
active: boolean;
|
|
249
|
+
/** Current recording ID (if recording) */
|
|
250
|
+
recordingId?: string;
|
|
251
|
+
/** When recording started */
|
|
252
|
+
startedAt?: Date;
|
|
253
|
+
/** Duration in seconds (updated periodically) */
|
|
254
|
+
durationSeconds?: number;
|
|
255
|
+
/** Who initiated the recording */
|
|
256
|
+
initiatedBy?: 'local' | 'remote';
|
|
257
|
+
}
|