@bota-dev/react-native-sdk 0.0.6 → 0.0.8
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 +30 -2
- package/lib/commonjs/ble/constants.js +28 -2
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/ble/parsers.js +128 -0
- package/lib/commonjs/ble/parsers.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +179 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/upload/UploadQueue.js +9 -2
- package/lib/commonjs/upload/UploadQueue.js.map +1 -1
- package/lib/commonjs/utils/crypto.js +169 -0
- package/lib/commonjs/utils/crypto.js.map +1 -0
- package/lib/commonjs/utils/index.js +12 -0
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/module/ble/constants.js +26 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/ble/parsers.js +124 -1
- package/lib/module/ble/parsers.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +181 -2
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/upload/UploadQueue.js +9 -2
- package/lib/module/upload/UploadQueue.js.map +1 -1
- package/lib/module/utils/crypto.js +160 -0
- package/lib/module/utils/crypto.js.map +1 -0
- package/lib/module/utils/index.js +1 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/typescript/src/ble/constants.d.ts +18 -0
- package/lib/typescript/src/ble/constants.d.ts.map +1 -1
- package/lib/typescript/src/ble/parsers.d.ts +34 -1
- package/lib/typescript/src/ble/parsers.d.ts.map +1 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts +73 -1
- package/lib/typescript/src/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/src/models/Device.d.ts +65 -0
- package/lib/typescript/src/models/Device.d.ts.map +1 -1
- package/lib/typescript/src/models/Recording.d.ts +9 -9
- package/lib/typescript/src/models/Recording.d.ts.map +1 -1
- package/lib/typescript/src/upload/UploadQueue.d.ts +2 -2
- package/lib/typescript/src/upload/UploadQueue.d.ts.map +1 -1
- package/lib/typescript/src/utils/crypto.d.ts +83 -0
- package/lib/typescript/src/utils/crypto.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +1 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/ble/constants.ts +26 -0
- package/src/ble/parsers.ts +131 -0
- package/src/managers/DeviceManager.ts +244 -0
- package/src/models/Device.ts +76 -0
- package/src/models/Recording.ts +9 -9
- package/src/upload/UploadQueue.ts +15 -8
- package/src/utils/crypto.ts +221 -0
- package/src/utils/index.ts +1 -0
package/src/ble/parsers.ts
CHANGED
|
@@ -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
|
*/
|
package/src/models/Device.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/models/Recording.ts
CHANGED
|
@@ -30,14 +30,14 @@ export interface DeviceRecording {
|
|
|
30
30
|
export interface UploadInfo {
|
|
31
31
|
/** Pre-signed S3 URL for upload */
|
|
32
32
|
uploadUrl: string;
|
|
33
|
-
/** Upload token (up_*) for verification */
|
|
34
|
-
uploadToken: string;
|
|
35
33
|
/** Recording ID assigned by Bota API (rec_*) */
|
|
36
34
|
recordingId: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
35
|
+
/** Upload token (up_*) for verification - optional if using custom completion flow */
|
|
36
|
+
uploadToken?: string;
|
|
37
|
+
/** URL to call when upload is complete - optional if using custom completion flow */
|
|
38
|
+
completeUrl?: string;
|
|
39
39
|
/** Expiration time of the upload URL */
|
|
40
|
-
expiresAt
|
|
40
|
+
expiresAt?: Date;
|
|
41
41
|
/** Content type for S3 upload (e.g., 'audio/opus', 'audio/wav') */
|
|
42
42
|
contentType?: string;
|
|
43
43
|
}
|
|
@@ -92,10 +92,10 @@ export interface UploadTask {
|
|
|
92
92
|
localPath: string;
|
|
93
93
|
/** Pre-signed S3 upload URL */
|
|
94
94
|
uploadUrl: string;
|
|
95
|
-
/** Upload token */
|
|
96
|
-
uploadToken
|
|
97
|
-
/** Complete URL to call after upload */
|
|
98
|
-
completeUrl
|
|
95
|
+
/** Upload token - optional if using custom completion flow */
|
|
96
|
+
uploadToken?: string;
|
|
97
|
+
/** Complete URL to call after upload - optional if using custom completion flow */
|
|
98
|
+
completeUrl?: string;
|
|
99
99
|
/** Content type for S3 upload (e.g., 'audio/opus', 'audio/wav') */
|
|
100
100
|
contentType?: string;
|
|
101
101
|
/** Current status */
|
|
@@ -69,8 +69,8 @@ export class UploadQueue extends EventEmitter<UploadQueueEvents> {
|
|
|
69
69
|
deviceId: string;
|
|
70
70
|
localPath: string;
|
|
71
71
|
uploadUrl: string;
|
|
72
|
-
uploadToken
|
|
73
|
-
completeUrl
|
|
72
|
+
uploadToken?: string;
|
|
73
|
+
completeUrl?: string;
|
|
74
74
|
contentType?: string;
|
|
75
75
|
}): Promise<UploadTask> {
|
|
76
76
|
const task: UploadTask = {
|
|
@@ -251,12 +251,19 @@ export class UploadQueue extends EventEmitter<UploadQueueEvents> {
|
|
|
251
251
|
abortSignal: abortController.signal,
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
// Notify completion
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
// Notify completion (only if completeUrl and uploadToken are provided)
|
|
255
|
+
if (task.completeUrl && task.uploadToken) {
|
|
256
|
+
await this.uploader.notifyCompletion(
|
|
257
|
+
task.completeUrl,
|
|
258
|
+
task.recordingId,
|
|
259
|
+
task.uploadToken
|
|
260
|
+
);
|
|
261
|
+
} else {
|
|
262
|
+
log.debug('Skipping completion notification (custom completion flow)', {
|
|
263
|
+
taskId: task.id,
|
|
264
|
+
recordingId: task.recordingId,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
260
267
|
|
|
261
268
|
// Mark as completed
|
|
262
269
|
await this.storage.updateTaskStatus(task.id, 'completed');
|