@bota-dev/react-native-sdk 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/lib/commonjs/ble/BleManager.js +16 -1
  2. package/lib/commonjs/ble/BleManager.js.map +1 -1
  3. package/lib/commonjs/ble/constants.js +16 -2
  4. package/lib/commonjs/ble/constants.js.map +1 -1
  5. package/lib/commonjs/managers/DeviceManager.js +216 -0
  6. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  7. package/lib/commonjs/managers/RecordingManager.js +2 -1
  8. package/lib/commonjs/managers/RecordingManager.js.map +1 -1
  9. package/lib/commonjs/upload/UploadQueue.js +2 -0
  10. package/lib/commonjs/upload/UploadQueue.js.map +1 -1
  11. package/lib/module/ble/BleManager.js +19 -2
  12. package/lib/module/ble/BleManager.js.map +1 -1
  13. package/lib/module/ble/constants.js +14 -0
  14. package/lib/module/ble/constants.js.map +1 -1
  15. package/lib/module/managers/DeviceManager.js +217 -1
  16. package/lib/module/managers/DeviceManager.js.map +1 -1
  17. package/lib/module/managers/RecordingManager.js +2 -1
  18. package/lib/module/managers/RecordingManager.js.map +1 -1
  19. package/lib/module/upload/UploadQueue.js +2 -0
  20. package/lib/module/upload/UploadQueue.js.map +1 -1
  21. package/lib/typescript/src/ble/BleManager.d.ts.map +1 -1
  22. package/lib/typescript/src/ble/constants.d.ts +10 -0
  23. package/lib/typescript/src/ble/constants.d.ts.map +1 -1
  24. package/lib/typescript/src/managers/DeviceManager.d.ts +43 -1
  25. package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -1
  26. package/lib/typescript/src/managers/RecordingManager.d.ts.map +1 -1
  27. package/lib/typescript/src/models/Device.d.ts +75 -0
  28. package/lib/typescript/src/models/Device.d.ts.map +1 -1
  29. package/lib/typescript/src/models/Recording.d.ts +4 -0
  30. package/lib/typescript/src/models/Recording.d.ts.map +1 -1
  31. package/lib/typescript/src/upload/UploadQueue.d.ts +1 -0
  32. package/lib/typescript/src/upload/UploadQueue.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/ble/BleManager.ts +12 -2
  35. package/src/ble/constants.ts +14 -0
  36. package/src/managers/DeviceManager.ts +250 -0
  37. package/src/managers/RecordingManager.ts +1 -0
  38. package/src/models/Device.ts +93 -0
  39. package/src/models/Recording.ts +4 -0
  40. package/src/upload/UploadQueue.ts +3 -0
@@ -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
  */
@@ -188,6 +188,7 @@ export class RecordingManager extends EventEmitter<RecordingManagerEvents> {
188
188
  uploadUrl: uploadInfo.uploadUrl,
189
189
  uploadToken: uploadInfo.uploadToken,
190
190
  completeUrl: uploadInfo.completeUrl,
191
+ contentType: uploadInfo.contentType,
191
192
  });
192
193
 
193
194
  // Wait for upload to complete
@@ -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
+ }
@@ -38,6 +38,8 @@ export interface UploadInfo {
38
38
  completeUrl: string;
39
39
  /** Expiration time of the upload URL */
40
40
  expiresAt: Date;
41
+ /** Content type for S3 upload (e.g., 'audio/opus', 'audio/wav') */
42
+ contentType?: string;
41
43
  }
42
44
 
43
45
  /**
@@ -94,6 +96,8 @@ export interface UploadTask {
94
96
  uploadToken: string;
95
97
  /** Complete URL to call after upload */
96
98
  completeUrl: string;
99
+ /** Content type for S3 upload (e.g., 'audio/opus', 'audio/wav') */
100
+ contentType?: string;
97
101
  /** Current status */
98
102
  status: UploadTaskStatus;
99
103
  /** Number of retry attempts */
@@ -71,6 +71,7 @@ export class UploadQueue extends EventEmitter<UploadQueueEvents> {
71
71
  uploadUrl: string;
72
72
  uploadToken: string;
73
73
  completeUrl: string;
74
+ contentType?: string;
74
75
  }): Promise<UploadTask> {
75
76
  const task: UploadTask = {
76
77
  id: generateTaskId(),
@@ -80,6 +81,7 @@ export class UploadQueue extends EventEmitter<UploadQueueEvents> {
80
81
  uploadUrl: params.uploadUrl,
81
82
  uploadToken: params.uploadToken,
82
83
  completeUrl: params.completeUrl,
84
+ contentType: params.contentType,
83
85
  status: 'pending',
84
86
  retryCount: 0,
85
87
  createdAt: new Date(),
@@ -242,6 +244,7 @@ export class UploadQueue extends EventEmitter<UploadQueueEvents> {
242
244
 
243
245
  // Upload to S3
244
246
  await this.uploader.upload(audioData, task.uploadUrl, {
247
+ contentType: task.contentType,
245
248
  onProgress: (progress) => {
246
249
  this.emit('uploadProgress', task.id, progress);
247
250
  },