@bota.dev/react-native-app-sdk 2.0.0-beta.0
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/BotaDeviceSDK.podspec +55 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
- package/generated/codegen-contract.json +3593 -0
- package/ios/BotaDeviceSDK.mm +1300 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
- package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
- package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
- package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
- package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
- package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
- package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
- package/lib/commonjs/BotaClient.js +194 -0
- package/lib/commonjs/BotaClient.js.map +1 -0
- package/lib/commonjs/ble/constants.js +10 -0
- package/lib/commonjs/ble/constants.js.map +1 -0
- package/lib/commonjs/ble/deviceLogs.js +50 -0
- package/lib/commonjs/ble/deviceLogs.js.map +1 -0
- package/lib/commonjs/cache/DeviceStateCache.js +6 -0
- package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
- package/lib/commonjs/client.js +746 -0
- package/lib/commonjs/client.js.map +1 -0
- package/lib/commonjs/compatibility/runtime.js +44 -0
- package/lib/commonjs/compatibility/runtime.js.map +1 -0
- package/lib/commonjs/index.js +123 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/managers/DeviceManager.js +647 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -0
- package/lib/commonjs/managers/OTAManager.js +140 -0
- package/lib/commonjs/managers/OTAManager.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +444 -0
- package/lib/commonjs/managers/RecordingManager.js.map +1 -0
- package/lib/commonjs/managers/StreamingSession.js +175 -0
- package/lib/commonjs/managers/StreamingSession.js.map +1 -0
- package/lib/commonjs/managers/types.js +6 -0
- package/lib/commonjs/managers/types.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/package.json +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/commonjs/sync/syncStatus.js +178 -0
- package/lib/commonjs/sync/syncStatus.js.map +1 -0
- package/lib/commonjs/utils/errors.js +194 -0
- package/lib/commonjs/utils/errors.js.map +1 -0
- package/lib/commonjs/utils/logger.js +84 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/module/BotaClient.js +189 -0
- package/lib/module/BotaClient.js.map +1 -0
- package/lib/module/ble/constants.js +6 -0
- package/lib/module/ble/constants.js.map +1 -0
- package/lib/module/ble/deviceLogs.js +45 -0
- package/lib/module/ble/deviceLogs.js.map +1 -0
- package/lib/module/cache/DeviceStateCache.js +4 -0
- package/lib/module/cache/DeviceStateCache.js.map +1 -0
- package/lib/module/client.js +740 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/compatibility/runtime.js +35 -0
- package/lib/module/compatibility/runtime.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/managers/DeviceManager.js +641 -0
- package/lib/module/managers/DeviceManager.js.map +1 -0
- package/lib/module/managers/OTAManager.js +134 -0
- package/lib/module/managers/OTAManager.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +438 -0
- package/lib/module/managers/RecordingManager.js.map +1 -0
- package/lib/module/managers/StreamingSession.js +168 -0
- package/lib/module/managers/StreamingSession.js.map +1 -0
- package/lib/module/managers/types.js +4 -0
- package/lib/module/managers/types.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 +4 -0
- package/lib/module/models/Status.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/module/sync/syncStatus.js +174 -0
- package/lib/module/sync/syncStatus.js.map +1 -0
- package/lib/module/utils/errors.js +183 -0
- package/lib/module/utils/errors.js.map +1 -0
- package/lib/module/utils/logger.js +80 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/BotaClient.d.ts +40 -0
- package/lib/typescript/BotaClient.d.ts.map +1 -0
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -0
- package/lib/typescript/ble/deviceLogs.d.ts +10 -0
- package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +208 -0
- package/lib/typescript/client.d.ts.map +1 -0
- package/lib/typescript/compatibility/runtime.d.ts +11 -0
- package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/managers/DeviceManager.d.ts +117 -0
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/managers/OTAManager.d.ts +25 -0
- package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +54 -0
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
- package/lib/typescript/managers/StreamingSession.d.ts +35 -0
- package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
- package/lib/typescript/managers/types.d.ts +19 -0
- package/lib/typescript/managers/types.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +392 -0
- package/lib/typescript/models/Device.d.ts.map +1 -0
- package/lib/typescript/models/Recording.d.ts +259 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -0
- package/lib/typescript/models/Status.d.ts +106 -0
- package/lib/typescript/models/Status.d.ts.map +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
- package/lib/typescript/sync/syncStatus.d.ts +123 -0
- package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
- package/lib/typescript/utils/errors.d.ts +83 -0
- package/lib/typescript/utils/errors.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +36 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/package.json +100 -0
- package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
- package/src/BotaClient.ts +231 -0
- package/src/ble/constants.ts +3 -0
- package/src/ble/deviceLogs.ts +61 -0
- package/src/cache/DeviceStateCache.ts +20 -0
- package/src/client.ts +1410 -0
- package/src/compatibility/runtime.ts +55 -0
- package/src/index.ts +146 -0
- package/src/managers/DeviceManager.ts +927 -0
- package/src/managers/OTAManager.ts +195 -0
- package/src/managers/RecordingManager.ts +578 -0
- package/src/managers/StreamingSession.ts +212 -0
- package/src/managers/types.ts +36 -0
- package/src/models/Device.ts +484 -0
- package/src/models/Recording.ts +281 -0
- package/src/models/Status.ts +128 -0
- package/src/specs/NativeBotaDeviceSDK.ts +542 -0
- package/src/sync/syncStatus.ts +243 -0
- package/src/utils/errors.ts +327 -0
- package/src/utils/logger.ts +122 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized sync-status derivation.
|
|
3
|
+
*
|
|
4
|
+
* Apps display "what's happening with the recordings on the device right now"
|
|
5
|
+
* in multiple places — sync banners, pending-recordings rows, recording detail
|
|
6
|
+
* pages, list-item subtitles. Without a shared rule set every screen tends to
|
|
7
|
+
* compute its own label from raw inputs, leading to screens that disagree
|
|
8
|
+
* (one shows "Bluetooth syncing", another shows "4G uploading").
|
|
9
|
+
*
|
|
10
|
+
* This module owns the *rules* (pure function over a typed input shape). Apps
|
|
11
|
+
* own the *subscription* (a thin hook that reads from whatever stores/queries
|
|
12
|
+
* they use and calls `deriveSyncStatus`). The label text returned here is the
|
|
13
|
+
* canonical surface — every consumer should read `label` / `shortLabel`
|
|
14
|
+
* verbatim rather than re-deriving from `kind`/`channel`.
|
|
15
|
+
*
|
|
16
|
+
* Precedence policy (when multiple signals are true simultaneously):
|
|
17
|
+
* 1. Live recording — if the device is actively recording AND streaming is
|
|
18
|
+
* enabled, show "Streaming via <channel>" regardless of pending uploads.
|
|
19
|
+
* 2. Actual device transport — if `device.syncActive` is set and a radio
|
|
20
|
+
* (WiFi / 4G) is up, the label reflects *that* transport, even if the
|
|
21
|
+
* app initiated the sync via BLE. This matches what bytes are actually
|
|
22
|
+
* moving and avoids the smart-sync handoff flicker where the app's
|
|
23
|
+
* "BLE pull" state lags the device's "4G upload" reality.
|
|
24
|
+
* 3. App-driven BLE pull — `appDriving.active` (e.g. the consumer's
|
|
25
|
+
* `useSyncAllRecordings` global) shows as "Bluetooth syncing N/M".
|
|
26
|
+
* 4. Device claims sync but no radio is up — `'waiting_channel'`, best-guess
|
|
27
|
+
* channel from `wifiAttempting` / `lteAttempting`.
|
|
28
|
+
* 5. Otherwise — `'idle'`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type SyncKind =
|
|
32
|
+
| 'idle'
|
|
33
|
+
| 'ble_pull'
|
|
34
|
+
| 'wifi_upload'
|
|
35
|
+
| 'lte_upload'
|
|
36
|
+
| 'streaming'
|
|
37
|
+
| 'waiting_channel';
|
|
38
|
+
|
|
39
|
+
export type SyncChannel = 'Bluetooth' | 'WiFi' | '4G';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Inputs to `deriveSyncStatus`. Every field is what the *consumer app* already
|
|
43
|
+
* has on hand — the SDK does not subscribe to any state itself.
|
|
44
|
+
*
|
|
45
|
+
* `appDriving` — populated from the consumer's app-level sync state (e.g. the
|
|
46
|
+
* global `isSyncing` + `syncAllProgress` exposed by their `useSyncAll*`
|
|
47
|
+
* hook). Set `active: false` if the app isn't currently pulling.
|
|
48
|
+
*
|
|
49
|
+
* `device` — flags reported by the device. For BLE-connected devices these
|
|
50
|
+
* come from `BotaClient.devices.on('statusUpdate', ...)`. For backend-only
|
|
51
|
+
* visibility (no BLE) they come from the heartbeat-derived state on the
|
|
52
|
+
* server, merged at the app layer.
|
|
53
|
+
*
|
|
54
|
+
* `bleConnected` — whether the app currently has a live BLE link to this
|
|
55
|
+
* device. Used to disambiguate `'ble_pull'` from `'4g/wifi_upload'`.
|
|
56
|
+
*/
|
|
57
|
+
export interface SyncStatusInputs {
|
|
58
|
+
appDriving: {
|
|
59
|
+
/** True while the consumer's sync flow is actively pulling via BLE. */
|
|
60
|
+
active: boolean;
|
|
61
|
+
/** 1-indexed current item. Undefined if not tracking per-item progress. */
|
|
62
|
+
currentIndex?: number;
|
|
63
|
+
/** Total items being synced. Undefined if unknown. */
|
|
64
|
+
total?: number;
|
|
65
|
+
};
|
|
66
|
+
device: {
|
|
67
|
+
/** Device's own sync-active flag (bit 5 of the firmware status flags). */
|
|
68
|
+
syncActive: boolean;
|
|
69
|
+
/** Device is currently recording. */
|
|
70
|
+
isRecording: boolean;
|
|
71
|
+
/** Device has a usable WiFi connection. */
|
|
72
|
+
wifiConnected: boolean;
|
|
73
|
+
/** Device has a usable LTE/4G connection. */
|
|
74
|
+
lteConnected: boolean;
|
|
75
|
+
/** Device's WiFi radio is powered up and trying (connecting / scanning). */
|
|
76
|
+
wifiAttempting: boolean;
|
|
77
|
+
/** Device's LTE radio is powered up and trying (registered / searching). */
|
|
78
|
+
lteAttempting: boolean;
|
|
79
|
+
/** Streaming upload mode is enabled on the device. */
|
|
80
|
+
streamingEnabled: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Device's upload-channel preference order (highest priority first), e.g.
|
|
83
|
+
* `['wifi', 'ble', 'cellular']`. When provided, the streaming channel is
|
|
84
|
+
* resolved by walking this list and picking the first channel that is both
|
|
85
|
+
* enabled (`enabledConnections`) and currently up — so a device whose LTE
|
|
86
|
+
* radio is connected but whose preference ranks BLE higher (or has cellular
|
|
87
|
+
* disabled) is correctly shown as streaming via Bluetooth, not 4G.
|
|
88
|
+
* Omit to fall back to the legacy "first connected radio wins" heuristic.
|
|
89
|
+
*/
|
|
90
|
+
uploadPreference?: Array<'wifi' | 'ble' | 'cellular'>;
|
|
91
|
+
/** Which upload channels are enabled on the device. */
|
|
92
|
+
enabledConnections?: { wifi: boolean; cellular: boolean };
|
|
93
|
+
};
|
|
94
|
+
/** BLE link to the device is currently up. */
|
|
95
|
+
bleConnected: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the channel a live stream is actually using. When the device's upload
|
|
100
|
+
* preference + enabled-connection flags are known, walk the preference order and
|
|
101
|
+
* pick the first channel that is enabled AND up (BLE is always available while
|
|
102
|
+
* the link is live) — this matches the firmware/app channel-selection rule. When
|
|
103
|
+
* those inputs are absent, fall back to "first connected radio wins".
|
|
104
|
+
*/
|
|
105
|
+
function resolveStreamingChannel(device: SyncStatusInputs['device']): SyncChannel {
|
|
106
|
+
const { wifiConnected, lteConnected, uploadPreference, enabledConnections } = device;
|
|
107
|
+
|
|
108
|
+
if (uploadPreference && enabledConnections) {
|
|
109
|
+
for (const channel of uploadPreference) {
|
|
110
|
+
if (channel === 'ble') return 'Bluetooth';
|
|
111
|
+
if (channel === 'wifi' && enabledConnections.wifi && wifiConnected) return 'WiFi';
|
|
112
|
+
if (channel === 'cellular' && enabledConnections.cellular && lteConnected) return '4G';
|
|
113
|
+
}
|
|
114
|
+
return 'Bluetooth';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return wifiConnected ? 'WiFi' : lteConnected ? '4G' : 'Bluetooth';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SyncStatus {
|
|
121
|
+
kind: SyncKind;
|
|
122
|
+
/** Resolved channel ('Bluetooth' / 'WiFi' / '4G'), absent for idle. */
|
|
123
|
+
channel?: SyncChannel;
|
|
124
|
+
/** 1-indexed current item, if known. */
|
|
125
|
+
currentIndex?: number;
|
|
126
|
+
/** Total items, if known. */
|
|
127
|
+
total?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Full label suitable for banners. Examples:
|
|
130
|
+
* "Syncing 2/5 via Bluetooth..."
|
|
131
|
+
* "Uploading 1/3 via 4G..."
|
|
132
|
+
* "Streaming via WiFi"
|
|
133
|
+
* "Waiting for upload channel..."
|
|
134
|
+
* "" (when kind === 'idle')
|
|
135
|
+
*/
|
|
136
|
+
label: string;
|
|
137
|
+
/**
|
|
138
|
+
* Compact label for tight UI (table rows, badges). Examples:
|
|
139
|
+
* "Bluetooth syncing 2/5"
|
|
140
|
+
* "4G uploading 1/3"
|
|
141
|
+
* "Streaming"
|
|
142
|
+
* "" (when kind === 'idle')
|
|
143
|
+
*/
|
|
144
|
+
shortLabel: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const IDLE: SyncStatus = { kind: 'idle', label: '', shortLabel: '' };
|
|
148
|
+
|
|
149
|
+
function progressFraction(currentIndex?: number, total?: number): string {
|
|
150
|
+
if (currentIndex == null || total == null || total <= 0) return '';
|
|
151
|
+
return ` ${Math.min(currentIndex, total)}/${total}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Pure derivation: given a snapshot of app + device + link state, return the
|
|
156
|
+
* single canonical `SyncStatus`. Safe to call on every render (no I/O, no
|
|
157
|
+
* allocations beyond the return value and label strings).
|
|
158
|
+
*/
|
|
159
|
+
export function deriveSyncStatus(inputs: SyncStatusInputs): SyncStatus {
|
|
160
|
+
const { appDriving, device, bleConnected } = inputs;
|
|
161
|
+
|
|
162
|
+
// 1. Live streaming wins — the user is recording right now and bytes are
|
|
163
|
+
// flowing as they're captured, not from queued files.
|
|
164
|
+
if (device.isRecording && device.streamingEnabled) {
|
|
165
|
+
const channel = resolveStreamingChannel(device);
|
|
166
|
+
return {
|
|
167
|
+
kind: 'streaming',
|
|
168
|
+
channel,
|
|
169
|
+
label: `Streaming via ${channel}`,
|
|
170
|
+
shortLabel: `Streaming (${channel})`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Device says it's syncing AND a radio is up — trust the device's own
|
|
175
|
+
// transport. This wins over `appDriving` so smart-sync handoff (BLE
|
|
176
|
+
// trigger → device 4G upload) doesn't flicker on the label.
|
|
177
|
+
if (device.syncActive && device.wifiConnected) {
|
|
178
|
+
const frac = progressFraction(appDriving.currentIndex, appDriving.total);
|
|
179
|
+
return {
|
|
180
|
+
kind: 'wifi_upload',
|
|
181
|
+
channel: 'WiFi',
|
|
182
|
+
currentIndex: appDriving.currentIndex,
|
|
183
|
+
total: appDriving.total,
|
|
184
|
+
label: `Uploading${frac} via WiFi...`,
|
|
185
|
+
shortLabel: `WiFi uploading${frac}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (device.syncActive && device.lteConnected && !bleConnected) {
|
|
189
|
+
// When BLE is up the app is in control — see rule 3 — so we let
|
|
190
|
+
// `appDriving` take over if it's active even though LTE is also flagged.
|
|
191
|
+
const frac = progressFraction(appDriving.currentIndex, appDriving.total);
|
|
192
|
+
return {
|
|
193
|
+
kind: 'lte_upload',
|
|
194
|
+
channel: '4G',
|
|
195
|
+
currentIndex: appDriving.currentIndex,
|
|
196
|
+
total: appDriving.total,
|
|
197
|
+
label: `Uploading${frac} via 4G...`,
|
|
198
|
+
shortLabel: `4G uploading${frac}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 3. App-driven BLE pull.
|
|
203
|
+
if (appDriving.active) {
|
|
204
|
+
const frac = progressFraction(appDriving.currentIndex, appDriving.total);
|
|
205
|
+
return {
|
|
206
|
+
kind: 'ble_pull',
|
|
207
|
+
channel: 'Bluetooth',
|
|
208
|
+
currentIndex: appDriving.currentIndex,
|
|
209
|
+
total: appDriving.total,
|
|
210
|
+
label: `Syncing${frac} via Bluetooth...`,
|
|
211
|
+
shortLabel: `Bluetooth syncing${frac}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 4. Device says it's syncing but no radio is up yet — typically a 4G
|
|
216
|
+
// cold-start (PDP attach in progress) or WiFi reconnect.
|
|
217
|
+
if (device.syncActive) {
|
|
218
|
+
const guess: SyncChannel | undefined = device.wifiAttempting
|
|
219
|
+
? 'WiFi'
|
|
220
|
+
: device.lteAttempting
|
|
221
|
+
? '4G'
|
|
222
|
+
: undefined;
|
|
223
|
+
if (guess) {
|
|
224
|
+
const frac = progressFraction(appDriving.currentIndex, appDriving.total);
|
|
225
|
+
return {
|
|
226
|
+
kind: 'waiting_channel',
|
|
227
|
+
channel: guess,
|
|
228
|
+
currentIndex: appDriving.currentIndex,
|
|
229
|
+
total: appDriving.total,
|
|
230
|
+
label: `Uploading${frac} via ${guess}...`,
|
|
231
|
+
shortLabel: `${guess} uploading${frac}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
kind: 'waiting_channel',
|
|
236
|
+
label: 'Waiting for upload channel...',
|
|
237
|
+
shortLabel: 'Waiting for channel',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 5. Idle.
|
|
242
|
+
return IDLE;
|
|
243
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error types for the Bota SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BLE_ERROR_STORAGE_KEY_UNAVAILABLE } from '../ble/constants';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for all Bota SDK errors
|
|
9
|
+
*/
|
|
10
|
+
export class BotaError extends Error {
|
|
11
|
+
constructor(
|
|
12
|
+
message: string,
|
|
13
|
+
public readonly code: string,
|
|
14
|
+
public readonly cause?: Error
|
|
15
|
+
) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'BotaError';
|
|
18
|
+
Object.setPrototypeOf(this, BotaError.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bluetooth-related errors
|
|
24
|
+
*/
|
|
25
|
+
export class BluetoothError extends BotaError {
|
|
26
|
+
constructor(message: string, code: string, cause?: Error) {
|
|
27
|
+
super(message, code, cause);
|
|
28
|
+
this.name = 'BluetoothError';
|
|
29
|
+
Object.setPrototypeOf(this, BluetoothError.prototype);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static unavailable(): BluetoothError {
|
|
33
|
+
return new BluetoothError(
|
|
34
|
+
'Bluetooth is not available on this device',
|
|
35
|
+
'BLUETOOTH_UNAVAILABLE'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static unauthorized(): BluetoothError {
|
|
40
|
+
return new BluetoothError(
|
|
41
|
+
'Bluetooth permission not granted',
|
|
42
|
+
'BLUETOOTH_UNAUTHORIZED'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static poweredOff(): BluetoothError {
|
|
47
|
+
return new BluetoothError('Bluetooth is powered off', 'BLUETOOTH_OFF');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Device-related errors
|
|
53
|
+
*/
|
|
54
|
+
export class DeviceError extends BotaError {
|
|
55
|
+
constructor(
|
|
56
|
+
message: string,
|
|
57
|
+
code: string,
|
|
58
|
+
public readonly deviceId?: string,
|
|
59
|
+
cause?: Error
|
|
60
|
+
) {
|
|
61
|
+
super(message, code, cause);
|
|
62
|
+
this.name = 'DeviceError';
|
|
63
|
+
Object.setPrototypeOf(this, DeviceError.prototype);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static notFound(deviceId: string): DeviceError {
|
|
67
|
+
return new DeviceError(
|
|
68
|
+
`Device ${deviceId} not found`,
|
|
69
|
+
'DEVICE_NOT_FOUND',
|
|
70
|
+
deviceId
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static connectionFailed(deviceId: string, cause?: Error): DeviceError {
|
|
75
|
+
return new DeviceError(
|
|
76
|
+
`Failed to connect to device ${deviceId}`,
|
|
77
|
+
'CONNECTION_FAILED',
|
|
78
|
+
deviceId,
|
|
79
|
+
cause
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static connectionLost(deviceId: string, during?: string): DeviceError {
|
|
84
|
+
const message = during
|
|
85
|
+
? `Connection to device ${deviceId} lost during ${during}`
|
|
86
|
+
: `Connection to device ${deviceId} lost`;
|
|
87
|
+
return new DeviceError(message, 'CONNECTION_LOST', deviceId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static bondingFailed(deviceId: string, cause?: Error): DeviceError {
|
|
91
|
+
return new DeviceError(
|
|
92
|
+
`Bonding failed for device ${deviceId}`,
|
|
93
|
+
'BONDING_FAILED',
|
|
94
|
+
deviceId,
|
|
95
|
+
cause
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static notConnected(deviceId: string): DeviceError {
|
|
100
|
+
return new DeviceError(
|
|
101
|
+
`Device ${deviceId} is not connected`,
|
|
102
|
+
'NOT_CONNECTED',
|
|
103
|
+
deviceId
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static alreadyConnected(deviceId: string): DeviceError {
|
|
108
|
+
return new DeviceError(
|
|
109
|
+
`Device ${deviceId} is already connected`,
|
|
110
|
+
'ALREADY_CONNECTED',
|
|
111
|
+
deviceId
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Provisioning-related errors
|
|
118
|
+
*/
|
|
119
|
+
export class ProvisioningError extends BotaError {
|
|
120
|
+
constructor(
|
|
121
|
+
message: string,
|
|
122
|
+
code: string,
|
|
123
|
+
public readonly deviceId?: string,
|
|
124
|
+
cause?: Error
|
|
125
|
+
) {
|
|
126
|
+
super(message, code, cause);
|
|
127
|
+
this.name = 'ProvisioningError';
|
|
128
|
+
Object.setPrototypeOf(this, ProvisioningError.prototype);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static invalidToken(deviceId: string): ProvisioningError {
|
|
132
|
+
return new ProvisioningError(
|
|
133
|
+
'Device rejected the token as invalid',
|
|
134
|
+
'INVALID_TOKEN',
|
|
135
|
+
deviceId
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static storageError(deviceId: string): ProvisioningError {
|
|
140
|
+
return new ProvisioningError(
|
|
141
|
+
'Device failed to store the token',
|
|
142
|
+
'STORAGE_ERROR',
|
|
143
|
+
deviceId
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static chunkError(deviceId: string): ProvisioningError {
|
|
148
|
+
return new ProvisioningError(
|
|
149
|
+
'Token chunk transfer failed',
|
|
150
|
+
'CHUNK_ERROR',
|
|
151
|
+
deviceId
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static alreadyProvisioned(deviceId: string): ProvisioningError {
|
|
156
|
+
return new ProvisioningError(
|
|
157
|
+
'Device is already provisioned',
|
|
158
|
+
'ALREADY_PROVISIONED',
|
|
159
|
+
deviceId
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static alreadyPaired(deviceId: string): ProvisioningError {
|
|
164
|
+
return new ProvisioningError(
|
|
165
|
+
'Device is already paired with another credential. Factory reset the device before re-provisioning.',
|
|
166
|
+
'ALREADY_PAIRED',
|
|
167
|
+
deviceId
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static timeout(deviceId: string): ProvisioningError {
|
|
172
|
+
return new ProvisioningError(
|
|
173
|
+
'Provisioning timed out waiting for device response',
|
|
174
|
+
'PROVISIONING_TIMEOUT',
|
|
175
|
+
deviceId
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Transfer-related errors
|
|
182
|
+
*/
|
|
183
|
+
export class TransferError extends BotaError {
|
|
184
|
+
constructor(
|
|
185
|
+
message: string,
|
|
186
|
+
code: string,
|
|
187
|
+
public readonly recordingUuid?: string,
|
|
188
|
+
cause?: Error
|
|
189
|
+
) {
|
|
190
|
+
super(message, code, cause);
|
|
191
|
+
this.name = 'TransferError';
|
|
192
|
+
Object.setPrototypeOf(this, TransferError.prototype);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static recordingNotFound(uuid: string): TransferError {
|
|
196
|
+
return new TransferError(
|
|
197
|
+
`Recording ${uuid} not found on device`,
|
|
198
|
+
'RECORDING_NOT_FOUND',
|
|
199
|
+
uuid
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static checksumMismatch(uuid: string): TransferError {
|
|
204
|
+
return new TransferError(
|
|
205
|
+
`Checksum mismatch for recording ${uuid}`,
|
|
206
|
+
'CHECKSUM_MISMATCH',
|
|
207
|
+
uuid
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static interrupted(uuid: string, cause?: Error): TransferError {
|
|
212
|
+
return new TransferError(
|
|
213
|
+
`Transfer interrupted for recording ${uuid}`,
|
|
214
|
+
'TRANSFER_INTERRUPTED',
|
|
215
|
+
uuid,
|
|
216
|
+
cause
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
static timeout(uuid: string): TransferError {
|
|
221
|
+
return new TransferError(
|
|
222
|
+
`Transfer timed out for recording ${uuid}`,
|
|
223
|
+
'TRANSFER_TIMEOUT',
|
|
224
|
+
uuid
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static deviceError(uuid: string, errorCode: number): TransferError {
|
|
229
|
+
if (errorCode === BLE_ERROR_STORAGE_KEY_UNAVAILABLE) {
|
|
230
|
+
return new TransferError(
|
|
231
|
+
`Recording ${uuid} exists, but its device storage key is unavailable`,
|
|
232
|
+
'STORAGE_KEY_UNAVAILABLE',
|
|
233
|
+
uuid
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return new TransferError(
|
|
237
|
+
`Device error (code: ${errorCode}) during transfer of ${uuid}`,
|
|
238
|
+
'DEVICE_ERROR',
|
|
239
|
+
uuid
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Upload-related errors
|
|
246
|
+
*/
|
|
247
|
+
export class UploadError extends BotaError {
|
|
248
|
+
constructor(
|
|
249
|
+
message: string,
|
|
250
|
+
code: string,
|
|
251
|
+
public readonly taskId?: string,
|
|
252
|
+
cause?: Error
|
|
253
|
+
) {
|
|
254
|
+
super(message, code, cause);
|
|
255
|
+
this.name = 'UploadError';
|
|
256
|
+
Object.setPrototypeOf(this, UploadError.prototype);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static urlExpired(taskId: string): UploadError {
|
|
260
|
+
return new UploadError(
|
|
261
|
+
'Upload URL has expired',
|
|
262
|
+
'URL_EXPIRED',
|
|
263
|
+
taskId
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static networkUnavailable(): UploadError {
|
|
268
|
+
return new UploadError(
|
|
269
|
+
'Network is not available',
|
|
270
|
+
'NETWORK_UNAVAILABLE'
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static uploadFailed(taskId: string, cause?: Error): UploadError {
|
|
275
|
+
return new UploadError(
|
|
276
|
+
`Upload failed for task ${taskId}`,
|
|
277
|
+
'UPLOAD_FAILED',
|
|
278
|
+
taskId,
|
|
279
|
+
cause
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static completionFailed(taskId: string, cause?: Error): UploadError {
|
|
284
|
+
return new UploadError(
|
|
285
|
+
`Failed to notify completion for task ${taskId}`,
|
|
286
|
+
'COMPLETION_FAILED',
|
|
287
|
+
taskId,
|
|
288
|
+
cause
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* SDK state errors
|
|
295
|
+
*/
|
|
296
|
+
export class SdkError extends BotaError {
|
|
297
|
+
constructor(message: string, code: string, cause?: Error) {
|
|
298
|
+
super(message, code, cause);
|
|
299
|
+
this.name = 'SdkError';
|
|
300
|
+
Object.setPrototypeOf(this, SdkError.prototype);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static notInitialized(): SdkError {
|
|
304
|
+
return new SdkError(
|
|
305
|
+
'SDK has not been initialized. Call BotaClient.configure() first.',
|
|
306
|
+
'NOT_INITIALIZED'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
static invalidState(expected: string, actual: string): SdkError {
|
|
311
|
+
return new SdkError(
|
|
312
|
+
`Invalid state: expected ${expected}, got ${actual}`,
|
|
313
|
+
'INVALID_STATE'
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static timeout(operation: string): SdkError {
|
|
318
|
+
return new SdkError(`Operation timed out: ${operation}`, 'TIMEOUT');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Type guard to check if an error is a BotaError
|
|
324
|
+
*/
|
|
325
|
+
export function isBotaError(error: unknown): error is BotaError {
|
|
326
|
+
return error instanceof BotaError;
|
|
327
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { LogLevel } from '../models/Status';
|
|
2
|
+
|
|
3
|
+
export type SdkLogLevel = Exclude<LogLevel, 'none'>;
|
|
4
|
+
|
|
5
|
+
export interface SdkLogEntry {
|
|
6
|
+
level: SdkLogLevel;
|
|
7
|
+
message: string;
|
|
8
|
+
context?: Record<string, unknown>;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type LogHandler = (entry: SdkLogEntry) => void;
|
|
13
|
+
|
|
14
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
15
|
+
debug: 0,
|
|
16
|
+
info: 1,
|
|
17
|
+
warn: 2,
|
|
18
|
+
error: 3,
|
|
19
|
+
none: 4,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class Logger {
|
|
23
|
+
private level: LogLevel = 'warn';
|
|
24
|
+
private readonly prefix = '[Bota SDK]';
|
|
25
|
+
private handler: LogHandler | null = null;
|
|
26
|
+
|
|
27
|
+
setLevel(level: LogLevel): void {
|
|
28
|
+
this.level = level;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getLevel(): LogLevel {
|
|
32
|
+
return this.level;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setHandler(handler: LogHandler | null): void {
|
|
36
|
+
this.handler = handler;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
40
|
+
this.write('debug', message, context);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
44
|
+
this.write('info', message, context);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
48
|
+
this.write('warn', message, context);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
error(
|
|
52
|
+
message: string,
|
|
53
|
+
error?: Error,
|
|
54
|
+
context?: Record<string, unknown>
|
|
55
|
+
): void {
|
|
56
|
+
this.write(
|
|
57
|
+
'error',
|
|
58
|
+
message,
|
|
59
|
+
error
|
|
60
|
+
? { ...context, error: error.message, stack: error.stack }
|
|
61
|
+
: context
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
tag(tag: string): TaggedLogger {
|
|
66
|
+
return new TaggedLogger(this, tag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private write(
|
|
70
|
+
level: SdkLogLevel,
|
|
71
|
+
message: string,
|
|
72
|
+
context?: Record<string, unknown>
|
|
73
|
+
): void {
|
|
74
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
|
|
75
|
+
if (this.handler) {
|
|
76
|
+
this.handler({ level, message, context, timestamp: new Date() });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const output = this.format(level, message, context);
|
|
80
|
+
console[level](output);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private format(
|
|
84
|
+
level: SdkLogLevel,
|
|
85
|
+
message: string,
|
|
86
|
+
context?: Record<string, unknown>
|
|
87
|
+
): string {
|
|
88
|
+
const contextValue = context ? ` ${JSON.stringify(context)}` : '';
|
|
89
|
+
return `${new Date().toISOString()} ${this.prefix} [${level.toUpperCase()}] ${message}${contextValue}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class TaggedLogger {
|
|
94
|
+
constructor(
|
|
95
|
+
private readonly parent: Logger,
|
|
96
|
+
private readonly tagValue: string
|
|
97
|
+
) {}
|
|
98
|
+
|
|
99
|
+
debug(message: string, context?: Record<string, unknown>): void {
|
|
100
|
+
this.parent.debug(`[${this.tagValue}] ${message}`, context);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
104
|
+
this.parent.info(`[${this.tagValue}] ${message}`, context);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
108
|
+
this.parent.warn(`[${this.tagValue}] ${message}`, context);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
error(
|
|
112
|
+
message: string,
|
|
113
|
+
error?: Error,
|
|
114
|
+
context?: Record<string, unknown>
|
|
115
|
+
): void {
|
|
116
|
+
this.parent.error(`[${this.tagValue}] ${message}`, error, context);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const logger = new Logger();
|
|
121
|
+
|
|
122
|
+
export type { TaggedLogger };
|