@bota-dev/react-native-sdk 0.0.7 → 0.0.9

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 (46) hide show
  1. package/README.md +30 -2
  2. package/lib/commonjs/ble/constants.js +28 -2
  3. package/lib/commonjs/ble/constants.js.map +1 -1
  4. package/lib/commonjs/ble/parsers.js +128 -0
  5. package/lib/commonjs/ble/parsers.js.map +1 -1
  6. package/lib/commonjs/managers/DeviceManager.js +179 -0
  7. package/lib/commonjs/managers/DeviceManager.js.map +1 -1
  8. package/lib/commonjs/protocol/ProtocolHandler.js +6 -0
  9. package/lib/commonjs/protocol/ProtocolHandler.js.map +1 -1
  10. package/lib/commonjs/utils/crypto.js +169 -0
  11. package/lib/commonjs/utils/crypto.js.map +1 -0
  12. package/lib/commonjs/utils/index.js +12 -0
  13. package/lib/commonjs/utils/index.js.map +1 -1
  14. package/lib/module/ble/constants.js +26 -0
  15. package/lib/module/ble/constants.js.map +1 -1
  16. package/lib/module/ble/parsers.js +124 -1
  17. package/lib/module/ble/parsers.js.map +1 -1
  18. package/lib/module/managers/DeviceManager.js +181 -2
  19. package/lib/module/managers/DeviceManager.js.map +1 -1
  20. package/lib/module/protocol/ProtocolHandler.js +6 -0
  21. package/lib/module/protocol/ProtocolHandler.js.map +1 -1
  22. package/lib/module/utils/crypto.js +160 -0
  23. package/lib/module/utils/crypto.js.map +1 -0
  24. package/lib/module/utils/index.js +1 -0
  25. package/lib/module/utils/index.js.map +1 -1
  26. package/lib/typescript/src/ble/constants.d.ts +18 -0
  27. package/lib/typescript/src/ble/constants.d.ts.map +1 -1
  28. package/lib/typescript/src/ble/parsers.d.ts +34 -1
  29. package/lib/typescript/src/ble/parsers.d.ts.map +1 -1
  30. package/lib/typescript/src/managers/DeviceManager.d.ts +73 -1
  31. package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -1
  32. package/lib/typescript/src/models/Device.d.ts +65 -0
  33. package/lib/typescript/src/models/Device.d.ts.map +1 -1
  34. package/lib/typescript/src/protocol/ProtocolHandler.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/crypto.d.ts +83 -0
  36. package/lib/typescript/src/utils/crypto.d.ts.map +1 -0
  37. package/lib/typescript/src/utils/index.d.ts +1 -0
  38. package/lib/typescript/src/utils/index.d.ts.map +1 -1
  39. package/package.json +6 -2
  40. package/src/ble/constants.ts +26 -0
  41. package/src/ble/parsers.ts +131 -0
  42. package/src/managers/DeviceManager.ts +244 -0
  43. package/src/models/Device.ts +76 -0
  44. package/src/protocol/ProtocolHandler.ts +6 -0
  45. package/src/utils/crypto.ts +221 -0
  46. package/src/utils/index.ts +1 -0
@@ -11,6 +11,10 @@ import type {
11
11
  DeviceStatus,
12
12
  DeviceFlags,
13
13
  StorageInfo,
14
+ DeviceCapabilities,
15
+ WiFiStatus,
16
+ WiFiStatusInfo,
17
+ WiFiConfigResult,
14
18
  } from '../models/Device';
15
19
  import type { DeviceRecording, AudioCodec, TransferPacket } from '../models/Recording';
16
20
  import {
@@ -48,6 +52,20 @@ import {
48
52
  PACKET_TYPE_DATA,
49
53
  PACKET_TYPE_EOF,
50
54
  PACKET_TYPE_ERROR,
55
+ CAP_BLE_SYNC,
56
+ CAP_WIFI_UPLOAD,
57
+ CAP_LTE_UPLOAD,
58
+ CAP_REMOTE_RECORD,
59
+ WIFI_STATUS_IDLE,
60
+ WIFI_STATUS_CONNECTING,
61
+ WIFI_STATUS_CONNECTED,
62
+ WIFI_STATUS_FAILED,
63
+ WIFI_STATUS_DISCONNECTED,
64
+ WIFI_CONFIG_SUCCESS,
65
+ WIFI_CONFIG_INVALID_GRANT,
66
+ WIFI_CONFIG_GRANT_EXPIRED,
67
+ WIFI_CONFIG_DECRYPTION_ERROR,
68
+ WIFI_CONFIG_STORAGE_ERROR,
51
69
  } from './constants';
52
70
 
53
71
  /**
@@ -393,3 +411,116 @@ export function createTransferCommand(
393
411
 
394
412
  return Buffer.from([cmdByte]);
395
413
  }
414
+
415
+ // ============================================================================
416
+ // WiFi Upload Configuration Parsers
417
+ // ============================================================================
418
+
419
+ /**
420
+ * Parse device capabilities from byte value
421
+ */
422
+ export function parseDeviceCapabilities(byte: number): DeviceCapabilities {
423
+ return {
424
+ bleSync: (byte & CAP_BLE_SYNC) !== 0,
425
+ wifiUpload: (byte & CAP_WIFI_UPLOAD) !== 0,
426
+ lteUpload: (byte & CAP_LTE_UPLOAD) !== 0,
427
+ remoteRecord: (byte & CAP_REMOTE_RECORD) !== 0,
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Parse WiFi status from byte value
433
+ */
434
+ export function parseWiFiStatus(byte: number): WiFiStatus {
435
+ switch (byte) {
436
+ case WIFI_STATUS_IDLE:
437
+ return 'idle';
438
+ case WIFI_STATUS_CONNECTING:
439
+ return 'connecting';
440
+ case WIFI_STATUS_CONNECTED:
441
+ return 'connected';
442
+ case WIFI_STATUS_FAILED:
443
+ return 'failed';
444
+ case WIFI_STATUS_DISCONNECTED:
445
+ return 'disconnected';
446
+ default:
447
+ return 'idle';
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Parse WiFi status information from characteristic value
453
+ *
454
+ * Format:
455
+ * Byte 0: Status (0x00=idle, 0x01=connecting, 0x02=connected, 0x03=failed, 0x04=disconnected)
456
+ * Byte 1: Signal strength (0-100)
457
+ * Byte 2: SSID length
458
+ * Bytes 3-34: SSID (max 32 bytes)
459
+ * Bytes 35+: Error message (if status=failed)
460
+ */
461
+ export function parseWiFiStatusInfo(data: Buffer): WiFiStatusInfo {
462
+ if (data.length < 3) {
463
+ throw new Error(`Invalid WiFi status data length: ${data.length}`);
464
+ }
465
+
466
+ const status = parseWiFiStatus(data.readUInt8(0));
467
+ const signalStrength = data.readUInt8(1);
468
+ const ssidLength = data.readUInt8(2);
469
+
470
+ let ssid: string | undefined;
471
+ let lastError: string | undefined;
472
+
473
+ if (ssidLength > 0 && data.length >= 3 + ssidLength) {
474
+ ssid = data.slice(3, 3 + ssidLength).toString('utf-8');
475
+ }
476
+
477
+ if (status === 'failed' && data.length > 3 + ssidLength) {
478
+ lastError = data.slice(3 + ssidLength).toString('utf-8');
479
+ }
480
+
481
+ return {
482
+ status,
483
+ signalStrength: signalStrength > 0 ? signalStrength : undefined,
484
+ ssid,
485
+ lastError,
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Parse WiFi configuration result from characteristic value
491
+ *
492
+ * Format:
493
+ * Byte 0: Result code (0x00=success, 0x01=invalid_grant, 0x02=grant_expired, 0x03=decryption_error, 0x04=storage_error)
494
+ */
495
+ export function parseWiFiConfigResult(data: Buffer): WiFiConfigResult {
496
+ if (data.length < 1) {
497
+ throw new Error(`Invalid WiFi config result length: ${data.length}`);
498
+ }
499
+
500
+ const code = data.readUInt8(0);
501
+
502
+ switch (code) {
503
+ case WIFI_CONFIG_SUCCESS:
504
+ return { success: true };
505
+ case WIFI_CONFIG_INVALID_GRANT:
506
+ return { success: false, error: 'invalid_grant' };
507
+ case WIFI_CONFIG_GRANT_EXPIRED:
508
+ return { success: false, error: 'grant_expired' };
509
+ case WIFI_CONFIG_DECRYPTION_ERROR:
510
+ return { success: false, error: 'decryption_error' };
511
+ case WIFI_CONFIG_STORAGE_ERROR:
512
+ return { success: false, error: 'storage_error' };
513
+ default:
514
+ return { success: false, error: 'unknown' };
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Create WiFi grant submission packet
520
+ *
521
+ * Format:
522
+ * Bytes 0-N: Grant blob (JWT token as UTF-8 string)
523
+ */
524
+ export function createWiFiGrantPacket(grantBlob: string): Buffer {
525
+ return Buffer.from(grantBlob, 'utf-8');
526
+ }
@@ -11,6 +11,7 @@ import {
11
11
  SERVICE_DEVICE_INFO,
12
12
  SERVICE_BOTA_PROVISIONING,
13
13
  SERVICE_BOTA_CONTROL,
14
+ SERVICE_BOTA_WIFI_CONFIG,
14
15
  CHAR_SERIAL_NUMBER,
15
16
  CHAR_FIRMWARE_REVISION,
16
17
  CHAR_HARDWARE_REVISION,
@@ -22,6 +23,9 @@ import {
22
23
  CHAR_RECORDING_CONTROL,
23
24
  CHAR_RECORDING_STATUS,
24
25
  CHAR_TIME_SYNC,
26
+ CHAR_WIFI_GRANT,
27
+ CHAR_WIFI_CREDENTIAL,
28
+ CHAR_WIFI_STATUS,
25
29
  API_ENDPOINT_PRODUCTION,
26
30
  API_ENDPOINT_SANDBOX,
27
31
  PROVISIONING_SUCCESS,
@@ -41,6 +45,9 @@ import {
41
45
  parsePairingState,
42
46
  parseDeviceStatus,
43
47
  createTimeSyncData,
48
+ parseWiFiStatusInfo,
49
+ parseWiFiConfigResult,
50
+ createWiFiGrantPacket,
44
51
  } from '../ble/parsers';
45
52
  import type {
46
53
  DiscoveredDevice,
@@ -53,6 +60,10 @@ import type {
53
60
  // RecordingCommand, // TODO: Re-enable when used
54
61
  StartRecordingOptions,
55
62
  StopRecordingOptions,
63
+ WiFiConfigGrant,
64
+ WiFiCredentials,
65
+ WiFiConfigResult,
66
+ WiFiStatusInfo,
56
67
  } from '../models/Device';
57
68
  import type { DeviceManagerEvents } from '../models/Status';
58
69
  import {
@@ -60,6 +71,11 @@ import {
60
71
  ProvisioningError,
61
72
  } from '../utils/errors';
62
73
  import { logger } from '../utils/logger';
74
+ import {
75
+ deriveSessionKey,
76
+ encryptWiFiCredentials,
77
+ formatWiFiCredentialPacket,
78
+ } from '../utils/crypto';
63
79
 
64
80
  const log = logger.tag('DeviceManager');
65
81
 
@@ -776,6 +792,234 @@ export class DeviceManager extends EventEmitter<DeviceManagerEvents> {
776
792
  };
777
793
  }
778
794
 
795
+ // ============================================================================
796
+ // WiFi Upload Configuration
797
+ // ============================================================================
798
+
799
+ /**
800
+ * Configure WiFi credentials on device via BLE.
801
+ * Requires a WiFi configuration grant from backend.
802
+ *
803
+ * @param deviceId - Connected device ID
804
+ * @param credentials - WiFi network credentials
805
+ * @param grant - WiFi config grant from backend
806
+ * @returns Configuration result
807
+ *
808
+ * @example
809
+ * ```typescript
810
+ * // 1. Get grant from backend
811
+ * const grant = await api.createWiFiConfigGrant(deviceId);
812
+ *
813
+ * // 2. Configure device via BLE
814
+ * const result = await deviceManager.configureWiFi(
815
+ * deviceId,
816
+ * { ssid: 'MyNetwork', password: 'password123', securityType: 'WPA2' },
817
+ * grant
818
+ * );
819
+ *
820
+ * if (result.success) {
821
+ * console.log('WiFi configured successfully');
822
+ * }
823
+ * ```
824
+ */
825
+ async configureWiFi(
826
+ deviceId: string,
827
+ credentials: WiFiCredentials,
828
+ grant: WiFiConfigGrant
829
+ ): Promise<WiFiConfigResult> {
830
+ log.info('Configuring WiFi on device', { deviceId, ssid: credentials.ssid });
831
+
832
+ try {
833
+ // Step 1: Submit WiFi config grant to device
834
+ const grantPacket = createWiFiGrantPacket(grant.grantBlob);
835
+ await this.bleManager.writeCharacteristic(
836
+ deviceId,
837
+ SERVICE_BOTA_WIFI_CONFIG,
838
+ CHAR_WIFI_GRANT,
839
+ grantPacket
840
+ );
841
+
842
+ log.debug('WiFi grant submitted');
843
+
844
+ // Step 2: Derive K_session from grant
845
+ const sessionKey = deriveSessionKey(grant.grantBlob);
846
+
847
+ // Step 3: Encrypt WiFi credentials with K_session
848
+ const encrypted = encryptWiFiCredentials(
849
+ credentials.ssid,
850
+ credentials.password,
851
+ sessionKey
852
+ );
853
+
854
+ // Step 4: Format credential packet for BLE transmission
855
+ const credentialPacket = formatWiFiCredentialPacket(encrypted);
856
+
857
+ log.debug('Sending encrypted WiFi credentials', {
858
+ packetSize: credentialPacket.length,
859
+ });
860
+
861
+ // Step 5: Write encrypted credentials to device
862
+ await this.bleManager.writeCharacteristic(
863
+ deviceId,
864
+ SERVICE_BOTA_WIFI_CONFIG,
865
+ CHAR_WIFI_CREDENTIAL,
866
+ credentialPacket
867
+ );
868
+
869
+ // Step 6: Wait for configuration result
870
+ const result = await this.waitForWiFiConfigResult(deviceId);
871
+
872
+ if (result.success) {
873
+ log.info('WiFi configuration successful', { deviceId });
874
+ } else {
875
+ log.warn('WiFi configuration failed', { deviceId, error: result.error });
876
+ }
877
+
878
+ return result;
879
+ } catch (error) {
880
+ log.error('WiFi configuration error', error instanceof Error ? error : undefined);
881
+ throw new DeviceError(
882
+ `Failed to configure WiFi: ${error instanceof Error ? error.message : 'Unknown error'}`,
883
+ 'WIFI_CONFIG_ERROR',
884
+ deviceId,
885
+ error instanceof Error ? error : undefined
886
+ );
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Get WiFi connection status from device.
892
+ *
893
+ * @param deviceId - Connected device ID
894
+ * @returns WiFi status information
895
+ *
896
+ * @example
897
+ * ```typescript
898
+ * const status = await deviceManager.getWiFiStatus(deviceId);
899
+ * console.log('WiFi status:', status.status);
900
+ * if (status.status === 'connected') {
901
+ * console.log('Connected to:', status.ssid);
902
+ * console.log('Signal strength:', status.signalStrength);
903
+ * }
904
+ * ```
905
+ */
906
+ async getWiFiStatus(deviceId: string): Promise<WiFiStatusInfo> {
907
+ log.debug('Reading WiFi status', { deviceId });
908
+
909
+ try {
910
+ const data = await this.bleManager.readCharacteristic(
911
+ deviceId,
912
+ SERVICE_BOTA_WIFI_CONFIG,
913
+ CHAR_WIFI_STATUS
914
+ );
915
+
916
+ const status = parseWiFiStatusInfo(data);
917
+
918
+ log.debug('WiFi status', { deviceId, status: status.status, ssid: status.ssid });
919
+
920
+ return status;
921
+ } catch (error) {
922
+ log.error('Failed to read WiFi status', error instanceof Error ? error : undefined);
923
+ throw new DeviceError(
924
+ `Failed to read WiFi status: ${error instanceof Error ? error.message : 'Unknown error'}`,
925
+ 'WIFI_STATUS_ERROR',
926
+ deviceId,
927
+ error instanceof Error ? error : undefined
928
+ );
929
+ }
930
+ }
931
+
932
+ /**
933
+ * Subscribe to WiFi status updates from device.
934
+ *
935
+ * @param deviceId - Connected device ID
936
+ * @param callback - Callback function for status updates
937
+ * @returns Subscription object (call .remove() to unsubscribe)
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * const subscription = deviceManager.subscribeToWiFiStatus(deviceId, (status) => {
942
+ * console.log('WiFi status update:', status.status);
943
+ * if (status.status === 'connected') {
944
+ * console.log('Connected to:', status.ssid);
945
+ * } else if (status.status === 'failed') {
946
+ * console.error('Connection failed:', status.lastError);
947
+ * }
948
+ * });
949
+ *
950
+ * // Later: unsubscribe
951
+ * subscription.remove();
952
+ * ```
953
+ */
954
+ subscribeToWiFiStatus(
955
+ deviceId: string,
956
+ callback: (status: WiFiStatusInfo) => void
957
+ ): Subscription {
958
+ log.debug('Subscribing to WiFi status', { deviceId });
959
+
960
+ return this.bleManager.subscribeToCharacteristic(
961
+ deviceId,
962
+ SERVICE_BOTA_WIFI_CONFIG,
963
+ CHAR_WIFI_STATUS,
964
+ (data) => {
965
+ try {
966
+ const status = parseWiFiStatusInfo(data);
967
+ callback(status);
968
+ } catch (error) {
969
+ log.error('Failed to parse WiFi status', error instanceof Error ? error : undefined);
970
+ }
971
+ },
972
+ (error) => {
973
+ log.error('WiFi status subscription error', error);
974
+ }
975
+ );
976
+ }
977
+
978
+ /**
979
+ * Wait for WiFi configuration result from device.
980
+ */
981
+ private waitForWiFiConfigResult(deviceId: string): Promise<WiFiConfigResult> {
982
+ return new Promise((resolve, reject) => {
983
+ const timeout = setTimeout(() => {
984
+ subscription.remove();
985
+ reject(new DeviceError(
986
+ 'WiFi configuration timeout',
987
+ 'WIFI_CONFIG_TIMEOUT',
988
+ deviceId
989
+ ));
990
+ }, OPERATION_TIMEOUT);
991
+
992
+ const subscription = this.bleManager.subscribeToCharacteristic(
993
+ deviceId,
994
+ SERVICE_BOTA_WIFI_CONFIG,
995
+ CHAR_WIFI_STATUS,
996
+ (data) => {
997
+ try {
998
+ // Configuration result comes via status updates
999
+ const result = parseWiFiConfigResult(data);
1000
+
1001
+ clearTimeout(timeout);
1002
+ subscription.remove();
1003
+ resolve(result);
1004
+ } catch (error) {
1005
+ // Ignore parse errors, wait for valid result
1006
+ log.debug('Ignoring invalid WiFi config result', { error: error instanceof Error ? error.message : String(error) });
1007
+ }
1008
+ },
1009
+ (error) => {
1010
+ clearTimeout(timeout);
1011
+ subscription.remove();
1012
+ reject(new DeviceError(
1013
+ `WiFi config result error: ${error.message}`,
1014
+ 'WIFI_CONFIG_RESULT_ERROR',
1015
+ deviceId,
1016
+ error
1017
+ ));
1018
+ }
1019
+ );
1020
+ });
1021
+ }
1022
+
779
1023
  /**
780
1024
  * Clean up resources
781
1025
  */
@@ -76,6 +76,20 @@ export interface DiscoveredDevice {
76
76
  discoveredAt: Date;
77
77
  }
78
78
 
79
+ /**
80
+ * Device capabilities (bitmask)
81
+ */
82
+ export interface DeviceCapabilities {
83
+ /** Supports Bluetooth Sync (BLE transfer) */
84
+ bleSync: boolean;
85
+ /** Supports WiFi Upload */
86
+ wifiUpload: boolean;
87
+ /** Supports Cellular Upload (4G/LTE) */
88
+ lteUpload: boolean;
89
+ /** Supports Remote Recording Control */
90
+ remoteRecord: boolean;
91
+ }
92
+
79
93
  /**
80
94
  * Device after successful BLE connection
81
95
  */
@@ -96,6 +110,8 @@ export interface ConnectedDevice {
96
110
  connectionState: ConnectionState;
97
111
  /** Negotiated MTU size */
98
112
  mtu: number;
113
+ /** Device capabilities */
114
+ capabilities?: DeviceCapabilities;
99
115
  }
100
116
 
101
117
  /**
@@ -255,3 +271,63 @@ export interface RecordingState {
255
271
  /** Who initiated the recording */
256
272
  initiatedBy?: 'local' | 'remote';
257
273
  }
274
+
275
+ // ============================================================================
276
+ // WiFi Upload Configuration Types
277
+ // ============================================================================
278
+
279
+ /**
280
+ * WiFi security type
281
+ */
282
+ export type WiFiSecurityType = 'WPA2' | 'WPA3' | 'WEP' | 'OPEN';
283
+
284
+ /**
285
+ * WiFi connection status
286
+ */
287
+ export type WiFiStatus = 'idle' | 'connecting' | 'connected' | 'failed' | 'disconnected';
288
+
289
+ /**
290
+ * WiFi configuration grant from backend
291
+ */
292
+ export interface WiFiConfigGrant {
293
+ /** Grant blob (JWT token) */
294
+ grantBlob: string;
295
+ /** Expiration timestamp */
296
+ expiresAt: Date;
297
+ }
298
+
299
+ /**
300
+ * WiFi credentials to configure
301
+ */
302
+ export interface WiFiCredentials {
303
+ /** WiFi network SSID */
304
+ ssid: string;
305
+ /** WiFi password */
306
+ password: string;
307
+ /** Security type (default: WPA2) */
308
+ securityType?: WiFiSecurityType;
309
+ }
310
+
311
+ /**
312
+ * WiFi configuration result from device
313
+ */
314
+ export interface WiFiConfigResult {
315
+ /** Whether configuration was successful */
316
+ success: boolean;
317
+ /** Error code if failed */
318
+ error?: 'invalid_grant' | 'grant_expired' | 'decryption_error' | 'storage_error' | 'unknown';
319
+ }
320
+
321
+ /**
322
+ * WiFi status information from device
323
+ */
324
+ export interface WiFiStatusInfo {
325
+ /** Current WiFi connection status */
326
+ status: WiFiStatus;
327
+ /** Connected SSID (if connected) */
328
+ ssid?: string;
329
+ /** Signal strength (0-100) */
330
+ signalStrength?: number;
331
+ /** Last connection error (if failed) */
332
+ lastError?: string;
333
+ }
@@ -208,6 +208,12 @@ export class ProtocolHandler {
208
208
  try {
209
209
  resetTimeout();
210
210
 
211
+ // Skip ACK echo-back packets (App→Device only, may be echoed by BLE stack)
212
+ const firstByte = data.readUInt8(0);
213
+ if (firstByte >= 0x10 && firstByte <= 0x12) {
214
+ return;
215
+ }
216
+
211
217
  const packet = parseTransferPacket(data);
212
218
  await this.handleTransferPacket(
213
219
  deviceId,