@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 +67 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
|
@@ -247,6 +247,7 @@ class DeviceManager {
|
|
|
247
247
|
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
248
248
|
private var lastLc3Event: Long? = null
|
|
249
249
|
private var micReinitRunnable: Runnable? = null
|
|
250
|
+
private var systemMicAvailabilityRecheckRunnable: Runnable? = null
|
|
250
251
|
|
|
251
252
|
// VAD
|
|
252
253
|
private val vadBuffer = mutableListOf<ByteArray>()
|
|
@@ -358,6 +359,37 @@ class DeviceManager {
|
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
private fun scheduleSystemMicAvailabilityRecheck(reason: String) {
|
|
363
|
+
if (systemMicAvailabilityRecheckRunnable != null) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
val recheck =
|
|
368
|
+
object : Runnable {
|
|
369
|
+
override fun run() {
|
|
370
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
371
|
+
|
|
372
|
+
if (!micEnabled || !systemMicUnavailable) {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
val stillBlocked = phoneMic?.hasBlockingMicInterruption() ?: true
|
|
377
|
+
if (stillBlocked) {
|
|
378
|
+
scheduleSystemMicAvailabilityRecheck(reason)
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
systemMicUnavailable = false
|
|
383
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
384
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
385
|
+
updateMicState()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
systemMicAvailabilityRecheckRunnable = recheck
|
|
390
|
+
mainHandler.postDelayed(recheck, 2_000)
|
|
391
|
+
}
|
|
392
|
+
|
|
361
393
|
// MARK: - Unique (Android)
|
|
362
394
|
private fun setupPermissionMonitoring() {
|
|
363
395
|
val context = Bridge.getContext()
|
|
@@ -947,6 +979,7 @@ class DeviceManager {
|
|
|
947
979
|
systemMicUnavailable = true
|
|
948
980
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
949
981
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
982
|
+
scheduleSystemMicAvailabilityRecheck("external_app_recording")
|
|
950
983
|
}
|
|
951
984
|
"audio_focus_available" -> {
|
|
952
985
|
// Audio focus is available again
|
|
@@ -965,6 +998,7 @@ class DeviceManager {
|
|
|
965
998
|
systemMicUnavailable = true
|
|
966
999
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
967
1000
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1001
|
+
scheduleSystemMicAvailabilityRecheck("phone_call_interruption")
|
|
968
1002
|
}
|
|
969
1003
|
"phone_call_ended" -> {
|
|
970
1004
|
// Phone call ended - mark mic as available again
|
|
@@ -977,12 +1011,14 @@ class DeviceManager {
|
|
|
977
1011
|
systemMicUnavailable = true
|
|
978
1012
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
979
1013
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1014
|
+
scheduleSystemMicAvailabilityRecheck("phone_call_active")
|
|
980
1015
|
}
|
|
981
1016
|
"audio_focus_denied" -> {
|
|
982
1017
|
// Another app has audio focus
|
|
983
1018
|
systemMicUnavailable = true
|
|
984
1019
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
985
1020
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1021
|
+
scheduleSystemMicAvailabilityRecheck("audio_focus_denied")
|
|
986
1022
|
}
|
|
987
1023
|
"permission_denied" -> {
|
|
988
1024
|
// Microphone permission not granted
|
|
@@ -1010,6 +1046,9 @@ class DeviceManager {
|
|
|
1010
1046
|
// systemMicUnavailable = false
|
|
1011
1047
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1012
1048
|
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1049
|
+
if (systemMicUnavailable) {
|
|
1050
|
+
scheduleSystemMicAvailabilityRecheck("recording_stopped")
|
|
1051
|
+
}
|
|
1013
1052
|
}
|
|
1014
1053
|
else -> {
|
|
1015
1054
|
// Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
|
|
@@ -1380,9 +1419,9 @@ class DeviceManager {
|
|
|
1380
1419
|
* Send OTA start command to glasses. Called when user approves an update (onboarding or
|
|
1381
1420
|
* background mode). Triggers glasses to begin download and installation.
|
|
1382
1421
|
*/
|
|
1383
|
-
fun sendOtaStart() {
|
|
1422
|
+
fun sendOtaStart(otaVersionUrl: String? = null) {
|
|
1384
1423
|
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1385
|
-
(sgc as? MentraLive)?.sendOtaStart()
|
|
1424
|
+
(sgc as? MentraLive)?.sendOtaStart(otaVersionUrl)
|
|
1386
1425
|
}
|
|
1387
1426
|
|
|
1388
1427
|
fun sendOtaQueryStatus() {
|
|
@@ -1396,8 +1435,26 @@ class DeviceManager {
|
|
|
1396
1435
|
}
|
|
1397
1436
|
|
|
1398
1437
|
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1438
|
+
sendButtonPhotoSettings(requestId, ButtonPhotoSettings(ButtonPhotoSize.fromValue(size)))
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
fun sendButtonPhotoSettings(requestId: String, settings: ButtonPhotoSettings) {
|
|
1399
1442
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1400
|
-
live.sendButtonPhotoSettings(
|
|
1443
|
+
live.sendButtonPhotoSettings(
|
|
1444
|
+
requestId,
|
|
1445
|
+
settings.size?.value,
|
|
1446
|
+
settings.mfnr,
|
|
1447
|
+
settings.zsl,
|
|
1448
|
+
settings.noiseReduction,
|
|
1449
|
+
settings.edgeEnhancement,
|
|
1450
|
+
settings.ispDigitalGain,
|
|
1451
|
+
settings.ispAnalogGain,
|
|
1452
|
+
settings.aeExposureDivisor,
|
|
1453
|
+
settings.isoCap,
|
|
1454
|
+
settings.compress,
|
|
1455
|
+
settings.sound,
|
|
1456
|
+
settings.resetCaptureTuning == true,
|
|
1457
|
+
)
|
|
1401
1458
|
}
|
|
1402
1459
|
|
|
1403
1460
|
fun sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) {
|
|
@@ -1505,17 +1562,22 @@ class DeviceManager {
|
|
|
1505
1562
|
width: Int = 0,
|
|
1506
1563
|
height: Int = 0,
|
|
1507
1564
|
fps: Int = 0,
|
|
1565
|
+
maxRecordingTimeMinutes: Int = 0,
|
|
1508
1566
|
) {
|
|
1509
1567
|
Bridge.log(
|
|
1510
1568
|
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=true, sound=$sound, " +
|
|
1511
|
-
"resolution=${width}x${height}@${fps}fps"
|
|
1569
|
+
"resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
|
|
1512
1570
|
)
|
|
1513
|
-
sgc?.startVideoRecording(
|
|
1571
|
+
sgc?.startVideoRecording(
|
|
1572
|
+
requestId, save, true, sound, width, height, fps, maxRecordingTimeMinutes)
|
|
1514
1573
|
}
|
|
1515
1574
|
|
|
1516
|
-
fun stopVideoRecording(requestId: String) {
|
|
1517
|
-
Bridge.log(
|
|
1518
|
-
|
|
1575
|
+
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
1576
|
+
Bridge.log(
|
|
1577
|
+
"MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
|
|
1578
|
+
if (webhookUrl.isNullOrEmpty()) "none" else "set"
|
|
1579
|
+
)
|
|
1580
|
+
sgc?.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1519
1581
|
}
|
|
1520
1582
|
|
|
1521
1583
|
fun setMicState() {
|
|
@@ -1533,38 +1595,31 @@ class DeviceManager {
|
|
|
1533
1595
|
updateMicState()
|
|
1534
1596
|
}
|
|
1535
1597
|
|
|
1536
|
-
fun requestPhoto(
|
|
1537
|
-
requestId: String,
|
|
1538
|
-
appId: String,
|
|
1539
|
-
size: String,
|
|
1540
|
-
webhookUrl: String,
|
|
1541
|
-
authToken: String?,
|
|
1542
|
-
compress: String,
|
|
1543
|
-
flash: Boolean,
|
|
1544
|
-
save: Boolean,
|
|
1545
|
-
sound: Boolean,
|
|
1546
|
-
exposureTimeNs: Double? = null,
|
|
1547
|
-
iso: Int? = null,
|
|
1548
|
-
) {
|
|
1598
|
+
fun requestPhoto(request: PhotoRequest) {
|
|
1549
1599
|
val exposureNs: Long? =
|
|
1550
|
-
exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1600
|
+
request.exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1551
1601
|
when {
|
|
1552
1602
|
v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
1553
1603
|
else -> v.toLong()
|
|
1554
1604
|
}
|
|
1555
1605
|
}
|
|
1556
|
-
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1606
|
+
val manualIso = if (exposureNs != null) request.iso?.takeIf { it > 0 } else null
|
|
1607
|
+
val routed =
|
|
1608
|
+
request.copy(
|
|
1609
|
+
exposureTimeNs = exposureNs?.toDouble(),
|
|
1610
|
+
iso = manualIso,
|
|
1611
|
+
)
|
|
1557
1612
|
Bridge.log(
|
|
1558
|
-
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash save=$save sound=$sound exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1613
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} appId=${routed.appId} size=${routed.size.value} compress=${routed.compress.value} flash=${routed.flash} save=${routed.save} sound=${routed.sound} exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} aeDivisor=${routed.aeExposureDivisor} isoCap=${routed.isoCap} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1559
1614
|
)
|
|
1560
1615
|
val activeSgc = sgc
|
|
1561
1616
|
if (activeSgc == null) {
|
|
1562
1617
|
Bridge.log(
|
|
1563
|
-
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=$requestId"
|
|
1618
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
|
|
1564
1619
|
)
|
|
1565
1620
|
return
|
|
1566
1621
|
}
|
|
1567
|
-
activeSgc.requestPhoto(
|
|
1622
|
+
activeSgc.requestPhoto(routed)
|
|
1568
1623
|
}
|
|
1569
1624
|
|
|
1570
1625
|
fun rgbLedControl(
|
|
@@ -1776,6 +1831,8 @@ class DeviceManager {
|
|
|
1776
1831
|
|
|
1777
1832
|
micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1778
1833
|
micReinitRunnable = null
|
|
1834
|
+
systemMicAvailabilityRecheckRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1835
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
1779
1836
|
|
|
1780
1837
|
// Clean up transcriber resources
|
|
1781
1838
|
transcriber?.shutdown()
|
|
@@ -126,7 +126,7 @@ object DeviceStore {
|
|
|
126
126
|
store.set("bluetooth", "gallery_mode", true)
|
|
127
127
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
128
128
|
store.set("bluetooth", "screen_disabled", false)
|
|
129
|
-
store.set("bluetooth", "button_photo_size", "
|
|
129
|
+
store.set("bluetooth", "button_photo_size", "max")
|
|
130
130
|
store.set("bluetooth", "button_camera_led", true)
|
|
131
131
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
132
132
|
store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
|
|
@@ -23,6 +23,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
23
23
|
private val deviceManager: DeviceManager
|
|
24
24
|
private val listeners =
|
|
25
25
|
Collections.synchronizedSet(mutableSetOf<MentraBluetoothSdkListener>())
|
|
26
|
+
private val analytics = BluetoothSdkAnalytics(appContext, config.analytics)
|
|
26
27
|
private val discoveredDeviceNames = mutableSetOf<String>()
|
|
27
28
|
private val bridgeEventSinkId: String
|
|
28
29
|
private val storeListenerId: String
|
|
@@ -44,13 +45,20 @@ class MentraBluetoothSdk private constructor(
|
|
|
44
45
|
private var pendingWifiStatus: PendingWifiStatusRequest? = null
|
|
45
46
|
private var pendingHotspotStatus: PendingHotspotStatusRequest? = null
|
|
46
47
|
private var pendingVersionInfo: PendingResponse<VersionInfoResult>? = null
|
|
48
|
+
@Volatile private var configuredOtaVersionUrl: String? = null
|
|
47
49
|
|
|
48
50
|
init {
|
|
49
51
|
listeners.add(listener)
|
|
50
52
|
Bridge.initialize(appContext)
|
|
51
53
|
deviceManager = DeviceManager.getInstance()
|
|
52
54
|
bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
|
|
55
|
+
// Baseline the analytics connection state before subscribing to the store:
|
|
56
|
+
// store updates invoke listeners synchronously on the updating thread, so a
|
|
57
|
+
// connected status observed before the baseline would be reported as a fresh
|
|
58
|
+
// bluetooth_sdk_glasses_connected transition.
|
|
59
|
+
analytics.initializeGlassesStatus(getRawGlassesStatus())
|
|
53
60
|
storeListenerId = DeviceStore.store.addListener { category, changes -> dispatchStoreUpdate(category, changes) }
|
|
61
|
+
analytics.captureStarted()
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
companion object {
|
|
@@ -58,8 +66,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
58
66
|
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
59
67
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
60
68
|
private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
|
|
69
|
+
private const val VIDEO_UPLOAD_STOP_TIMEOUT_MS = 10 * 60 * 1000L
|
|
61
70
|
private const val STREAM_START_TIMEOUT_MS = 30_000L
|
|
62
71
|
private const val STREAM_STOP_TIMEOUT_MS = 15_000L
|
|
72
|
+
private const val OTA_BES_VERSION_WAIT_MS = 5_000L
|
|
73
|
+
private const val OTA_MTK_VERSION_WAIT_MS = 2_000L
|
|
74
|
+
private const val OTA_VERSION_POLL_MS = 100L
|
|
63
75
|
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
64
76
|
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
65
77
|
|
|
@@ -97,6 +109,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
97
109
|
private data class PendingVideoRecordingRequest(
|
|
98
110
|
val expectedStatus: String,
|
|
99
111
|
val pending: PendingResponse<VideoRecordingStatusEvent>,
|
|
112
|
+
val waitForUpload: Boolean = false,
|
|
113
|
+
var stoppedEvent: VideoRecordingStatusEvent? = null,
|
|
114
|
+
var uploadSucceeded: Boolean = false,
|
|
100
115
|
)
|
|
101
116
|
|
|
102
117
|
private data class PendingWifiScan(
|
|
@@ -184,6 +199,15 @@ class MentraBluetoothSdk private constructor(
|
|
|
184
199
|
|
|
185
200
|
fun getDefaultDevice(): Device? = currentDefaultDevice()
|
|
186
201
|
|
|
202
|
+
private fun requireGlassesConnected(operation: String) {
|
|
203
|
+
if (!getRawGlassesStatus().connected) {
|
|
204
|
+
throw BluetoothException(
|
|
205
|
+
"glasses_not_connected",
|
|
206
|
+
"Cannot $operation because glasses are not connected.",
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
187
211
|
fun setDefaultDevice(device: Device?) {
|
|
188
212
|
if (device == null) {
|
|
189
213
|
clearDefaultDevice()
|
|
@@ -450,16 +474,59 @@ class MentraBluetoothSdk private constructor(
|
|
|
450
474
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
451
475
|
}
|
|
452
476
|
|
|
453
|
-
fun setButtonPhotoSettings(
|
|
477
|
+
fun setButtonPhotoSettings(settings: ButtonPhotoSettings): SettingsAckEvent =
|
|
454
478
|
performSettingsCommand(
|
|
455
479
|
setting = "button_photo",
|
|
456
|
-
updateStore = { _ ->
|
|
457
|
-
|
|
480
|
+
updateStore = { _ ->
|
|
481
|
+
if (settings.resetCaptureTuning) {
|
|
482
|
+
// Clear all stored scan-tuning keys from phone cache
|
|
483
|
+
listOf(
|
|
484
|
+
"button_photo_mfnr",
|
|
485
|
+
"button_photo_zsl",
|
|
486
|
+
"button_photo_noise_reduction",
|
|
487
|
+
"button_photo_edge_enhancement",
|
|
488
|
+
"button_photo_isp_digital_gain",
|
|
489
|
+
"button_photo_isp_analog_gain",
|
|
490
|
+
"button_photo_ae_exposure_divisor",
|
|
491
|
+
"button_photo_iso_cap",
|
|
492
|
+
"button_photo_compress",
|
|
493
|
+
"button_photo_sound",
|
|
494
|
+
).forEach { key ->
|
|
495
|
+
DeviceStore.store.remove(ObservableStore.BLUETOOTH_CATEGORY, key)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Only update size if explicitly provided; omitted size = leave stored value unchanged
|
|
499
|
+
settings.size?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_size", it.value) }
|
|
500
|
+
settings.mfnr?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_mfnr", it) }
|
|
501
|
+
settings.zsl?.let { DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_zsl", it) }
|
|
502
|
+
settings.noiseReduction?.let {
|
|
503
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_noise_reduction", it)
|
|
504
|
+
}
|
|
505
|
+
settings.edgeEnhancement?.let {
|
|
506
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_edge_enhancement", it)
|
|
507
|
+
}
|
|
508
|
+
settings.ispDigitalGain?.let {
|
|
509
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_isp_digital_gain", it)
|
|
510
|
+
}
|
|
511
|
+
settings.ispAnalogGain?.let {
|
|
512
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_isp_analog_gain", it)
|
|
513
|
+
}
|
|
514
|
+
settings.aeExposureDivisor?.let {
|
|
515
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_ae_exposure_divisor", it)
|
|
516
|
+
}
|
|
517
|
+
settings.isoCap?.let {
|
|
518
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_iso_cap", it)
|
|
519
|
+
}
|
|
520
|
+
settings.compress?.let {
|
|
521
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_compress", it)
|
|
522
|
+
}
|
|
523
|
+
settings.sound?.let {
|
|
524
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_sound", it)
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
send = { requestId -> deviceManager.sendButtonPhotoSettings(requestId, settings) },
|
|
458
528
|
)
|
|
459
529
|
|
|
460
|
-
fun setButtonPhotoSettings(settings: ButtonPhotoSettings): SettingsAckEvent =
|
|
461
|
-
setButtonPhotoSettings(size = settings.size)
|
|
462
|
-
|
|
463
530
|
fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int): SettingsAckEvent =
|
|
464
531
|
performSettingsCommand(
|
|
465
532
|
setting = "button_video_recording",
|
|
@@ -657,19 +724,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
657
724
|
val pending = PendingResponse<PhotoResponseEvent>("photo request ${request.requestId}")
|
|
658
725
|
pendingPhotoRequests[request.requestId] = pending
|
|
659
726
|
try {
|
|
660
|
-
deviceManager.requestPhoto(
|
|
661
|
-
request.requestId,
|
|
662
|
-
request.appId,
|
|
663
|
-
request.size.value,
|
|
664
|
-
request.webhookUrl,
|
|
665
|
-
request.authToken,
|
|
666
|
-
request.compress.value,
|
|
667
|
-
request.flash,
|
|
668
|
-
request.save,
|
|
669
|
-
request.sound,
|
|
670
|
-
request.exposureTimeNs,
|
|
671
|
-
request.iso,
|
|
672
|
-
)
|
|
727
|
+
deviceManager.requestPhoto(request)
|
|
673
728
|
return pending.await()
|
|
674
729
|
} finally {
|
|
675
730
|
pendingPhotoRequests.remove(request.requestId, pending)
|
|
@@ -771,6 +826,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
771
826
|
|
|
772
827
|
fun startVideoRecording(request: VideoRecordingRequest): VideoRecordingStatusEvent {
|
|
773
828
|
require(request.requestId.isNotBlank()) { "requestId is required to start video recording." }
|
|
829
|
+
requireGlassesConnected("start video recording")
|
|
774
830
|
val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
|
|
775
831
|
val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
|
|
776
832
|
if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
|
|
@@ -787,6 +843,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
787
843
|
request.width,
|
|
788
844
|
request.height,
|
|
789
845
|
request.fps,
|
|
846
|
+
request.maxRecordingTimeMinutes,
|
|
790
847
|
)
|
|
791
848
|
return pending.await()
|
|
792
849
|
} finally {
|
|
@@ -794,10 +851,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
794
851
|
}
|
|
795
852
|
}
|
|
796
853
|
|
|
797
|
-
|
|
854
|
+
@JvmOverloads
|
|
855
|
+
fun stopVideoRecording(
|
|
856
|
+
requestId: String,
|
|
857
|
+
webhookUrl: String? = null,
|
|
858
|
+
authToken: String? = null,
|
|
859
|
+
): VideoRecordingStatusEvent {
|
|
798
860
|
require(requestId.isNotBlank()) { "requestId is required to stop video recording." }
|
|
861
|
+
requireGlassesConnected("stop video recording")
|
|
799
862
|
val pending = PendingResponse<VideoRecordingStatusEvent>("stop video recording")
|
|
800
|
-
val
|
|
863
|
+
val waitForUpload = !webhookUrl.isNullOrBlank()
|
|
864
|
+
val pendingRequest =
|
|
865
|
+
PendingVideoRecordingRequest(
|
|
866
|
+
expectedStatus = "recording_stopped",
|
|
867
|
+
pending = pending,
|
|
868
|
+
waitForUpload = waitForUpload,
|
|
869
|
+
)
|
|
801
870
|
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
802
871
|
throw BluetoothException(
|
|
803
872
|
"request_in_flight",
|
|
@@ -805,8 +874,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
805
874
|
)
|
|
806
875
|
}
|
|
807
876
|
try {
|
|
808
|
-
deviceManager.stopVideoRecording(requestId)
|
|
809
|
-
|
|
877
|
+
deviceManager.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
878
|
+
val timeoutMs =
|
|
879
|
+
if (waitForUpload) VIDEO_UPLOAD_STOP_TIMEOUT_MS else DEFAULT_REQUEST_TIMEOUT_MS
|
|
880
|
+
return pending.await(timeoutMs)
|
|
810
881
|
} finally {
|
|
811
882
|
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
812
883
|
}
|
|
@@ -835,8 +906,41 @@ class MentraBluetoothSdk private constructor(
|
|
|
835
906
|
}
|
|
836
907
|
}
|
|
837
908
|
|
|
838
|
-
|
|
839
|
-
|
|
909
|
+
internal fun setOtaVersionUrl(otaVersionUrl: String) {
|
|
910
|
+
configuredOtaVersionUrl = OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
internal fun getOtaVersionUrl(): String = configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
914
|
+
|
|
915
|
+
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
916
|
+
fun checkForOtaUpdate(): Boolean {
|
|
917
|
+
val status = getFreshGlassesStatus()
|
|
918
|
+
if (!status.connected) {
|
|
919
|
+
throw BluetoothException(
|
|
920
|
+
"glasses_not_connected",
|
|
921
|
+
"Cannot check OTA update because glasses are not connected.",
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
if (status.buildNumber.isBlank()) {
|
|
925
|
+
throw BluetoothException(
|
|
926
|
+
"missing_glasses_version",
|
|
927
|
+
"Cannot check OTA update because glasses build number is unavailable.",
|
|
928
|
+
)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
val manifestUrl = resolveOtaVersionUrl(status)
|
|
932
|
+
val manifest = OtaManifestChecker.fetch(manifestUrl)
|
|
933
|
+
val otaStatus = waitForOtaManifestStatus(status, manifest)
|
|
934
|
+
return OtaManifestChecker.hasUpdate(
|
|
935
|
+
otaStatus.buildNumber,
|
|
936
|
+
otaStatus.mtkFirmwareVersion,
|
|
937
|
+
otaStatus.besFirmwareVersion,
|
|
938
|
+
manifest,
|
|
939
|
+
)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Ask connected Mentra Live glasses to report the current OTA install/session status. */
|
|
943
|
+
private fun queryOtaStatus(): OtaQueryResult =
|
|
840
944
|
performOtaQuery("OTA status query") {
|
|
841
945
|
deviceManager.sendOtaQueryStatus()
|
|
842
946
|
}
|
|
@@ -869,6 +973,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
869
973
|
|
|
870
974
|
/** Start the OTA flow after your app has presented the available update to the user. */
|
|
871
975
|
fun startOtaUpdate(): OtaStartAckEvent {
|
|
976
|
+
val otaVersionUrl = resolveOtaVersionUrl(getFreshGlassesStatus())
|
|
977
|
+
return startOtaUpdate(otaVersionUrl)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private fun startOtaCommand(otaVersionUrl: String): OtaStartAckEvent {
|
|
872
981
|
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
873
982
|
synchronized(oneShotLock) {
|
|
874
983
|
if (pendingOtaStart != null) {
|
|
@@ -880,7 +989,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
880
989
|
pendingOtaStart = pending
|
|
881
990
|
}
|
|
882
991
|
try {
|
|
883
|
-
deviceManager.sendOtaStart()
|
|
992
|
+
deviceManager.sendOtaStart(otaVersionUrl)
|
|
884
993
|
return pending.await()
|
|
885
994
|
} finally {
|
|
886
995
|
synchronized(oneShotLock) {
|
|
@@ -891,15 +1000,98 @@ class MentraBluetoothSdk private constructor(
|
|
|
891
1000
|
}
|
|
892
1001
|
}
|
|
893
1002
|
|
|
894
|
-
|
|
895
|
-
|
|
1003
|
+
internal fun startOtaUpdate(otaVersionUrl: String): OtaStartAckEvent =
|
|
1004
|
+
startOtaCommand(otaVersionUrl)
|
|
1005
|
+
|
|
1006
|
+
internal fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1007
|
+
|
|
1008
|
+
/** Re-run the glasses-side OTA version check after an internal clock-skew recovery. */
|
|
1009
|
+
@JvmSynthetic
|
|
1010
|
+
internal fun retryOtaVersionCheck(): OtaQueryResult =
|
|
896
1011
|
performOtaQuery("OTA version retry") {
|
|
897
1012
|
deviceManager.retryOtaVersionCheck()
|
|
898
1013
|
}
|
|
899
1014
|
|
|
900
|
-
|
|
1015
|
+
private fun getFreshGlassesStatus(): GlassesStatus {
|
|
1016
|
+
val status = getRawGlassesStatus()
|
|
1017
|
+
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
1018
|
+
return status
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return try {
|
|
1022
|
+
val versionInfo = requestVersionInfo()
|
|
1023
|
+
status.copy(
|
|
1024
|
+
androidVersion = versionInfo.androidVersion.ifBlank { status.androidVersion },
|
|
1025
|
+
firmwareVersion = versionInfo.firmwareVersion.ifBlank { status.firmwareVersion },
|
|
1026
|
+
besFirmwareVersion = versionInfo.besFirmwareVersion.ifBlank { status.besFirmwareVersion },
|
|
1027
|
+
mtkFirmwareVersion = versionInfo.mtkFirmwareVersion.ifBlank { status.mtkFirmwareVersion },
|
|
1028
|
+
buildNumber = versionInfo.buildNumber.ifBlank { status.buildNumber },
|
|
1029
|
+
systemTimeMs = versionInfo.systemTimeMs ?: status.systemTimeMs,
|
|
1030
|
+
otaVersionUrl = versionInfo.otaVersionUrl.ifBlank { status.otaVersionUrl },
|
|
1031
|
+
appVersion = versionInfo.appVersion.ifBlank { status.appVersion },
|
|
1032
|
+
)
|
|
1033
|
+
} catch (_: Throwable) {
|
|
1034
|
+
status
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private fun waitForOtaManifestStatus(
|
|
1039
|
+
initialStatus: GlassesStatus,
|
|
1040
|
+
manifest: org.json.JSONObject,
|
|
1041
|
+
): GlassesStatus {
|
|
1042
|
+
var status = initialStatus
|
|
1043
|
+
if (OtaManifestChecker.hasBesFirmware(manifest) && status.besFirmwareVersion.isBlank()) {
|
|
1044
|
+
status = waitForGlassesStatus(status, OTA_BES_VERSION_WAIT_MS) {
|
|
1045
|
+
!it.connected || it.besFirmwareVersion.isNotBlank()
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (OtaManifestChecker.hasMtkPatches(manifest) && status.mtkFirmwareVersion.isBlank()) {
|
|
1050
|
+
status = waitForGlassesStatus(status, OTA_MTK_VERSION_WAIT_MS) {
|
|
1051
|
+
!it.connected || it.mtkFirmwareVersion.isNotBlank()
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (!status.connected) {
|
|
1056
|
+
throw BluetoothException(
|
|
1057
|
+
"glasses_not_connected",
|
|
1058
|
+
"Cannot check OTA update because glasses disconnected.",
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
return status
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private fun waitForGlassesStatus(
|
|
1065
|
+
initialStatus: GlassesStatus,
|
|
1066
|
+
timeoutMs: Long,
|
|
1067
|
+
isReady: (GlassesStatus) -> Boolean,
|
|
1068
|
+
): GlassesStatus {
|
|
1069
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
1070
|
+
var status = initialStatus
|
|
1071
|
+
while (System.currentTimeMillis() < deadline) {
|
|
1072
|
+
status = getRawGlassesStatus()
|
|
1073
|
+
if (isReady(status)) return status
|
|
1074
|
+
val remainingMs = deadline - System.currentTimeMillis()
|
|
1075
|
+
if (remainingMs <= 0L) break
|
|
1076
|
+
Thread.sleep(minOf(OTA_VERSION_POLL_MS, remainingMs))
|
|
1077
|
+
}
|
|
1078
|
+
return getRawGlassesStatus()
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private fun resolveOtaVersionUrl(status: GlassesStatus): String {
|
|
1082
|
+
val deviceUrl = status.otaVersionUrl.trim()
|
|
1083
|
+
if (isLegacyAsgOtaStartBuild(status.buildNumber)) {
|
|
1084
|
+
return deviceUrl.ifBlank { OtaManifestDefaults.PROD_OTA_VERSION_URL }
|
|
1085
|
+
}
|
|
1086
|
+
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1087
|
+
// A future glasses-advertised URL should not silently change that pairing.
|
|
1088
|
+
return configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1089
|
+
}
|
|
901
1090
|
|
|
902
|
-
|
|
1091
|
+
private fun isLegacyAsgOtaStartBuild(buildNumber: String): Boolean {
|
|
1092
|
+
val parsed = buildNumber.toIntOrNull()
|
|
1093
|
+
return parsed != null && parsed < 100_000
|
|
1094
|
+
}
|
|
903
1095
|
|
|
904
1096
|
internal fun sendShutdown() {
|
|
905
1097
|
deviceManager.sendShutdown()
|
|
@@ -917,12 +1109,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
917
1109
|
stopStreamKeepAliveMonitor()
|
|
918
1110
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
919
1111
|
DeviceStore.store.removeListener(storeListenerId)
|
|
1112
|
+
analytics.shutdown()
|
|
920
1113
|
listeners.clear()
|
|
921
1114
|
}
|
|
922
1115
|
|
|
923
1116
|
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
924
1117
|
when (ObservableStore.normalizeCategory(category)) {
|
|
925
1118
|
"glasses" -> {
|
|
1119
|
+
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
926
1120
|
val state = getState()
|
|
927
1121
|
dispatchToListeners {
|
|
928
1122
|
it.onStateChanged(state)
|
|
@@ -1112,6 +1306,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1112
1306
|
handleVideoRecordingStatusForRequests(event)
|
|
1113
1307
|
dispatchToListeners { it.onVideoRecordingStatus(event) }
|
|
1114
1308
|
}
|
|
1309
|
+
"media_success", "media_error" -> {
|
|
1310
|
+
val event = MediaUploadEvent(data)
|
|
1311
|
+
handleMediaUploadForRequests(event)
|
|
1312
|
+
dispatchToListeners { it.onMediaUpload(event) }
|
|
1313
|
+
}
|
|
1115
1314
|
"rgb_led_control_response" -> {
|
|
1116
1315
|
val event = RgbLedControlResponseEvent(data)
|
|
1117
1316
|
handleRgbLedResponseForRequests(event)
|
|
@@ -1290,6 +1489,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1290
1489
|
return streamId to pending
|
|
1291
1490
|
}
|
|
1292
1491
|
if (pendingStreamStarts.size == 1) {
|
|
1492
|
+
// A streamId-less STOPPED is the glasses' stop-ack for a PREVIOUS
|
|
1493
|
+
// stream (their stop ack carries no streamId), not a verdict on the
|
|
1494
|
+
// pending start — a start_stream that replaces a running publisher
|
|
1495
|
+
// emits exactly this sequence (stopped -> initializing -> streaming).
|
|
1496
|
+
// Attributing it here would reject a start that is about to succeed.
|
|
1497
|
+
if (event.state == StreamState.STOPPED) {
|
|
1498
|
+
return null
|
|
1499
|
+
}
|
|
1293
1500
|
val entry = pendingStreamStarts.entries.first()
|
|
1294
1501
|
return entry.key to entry.value
|
|
1295
1502
|
}
|
|
@@ -1341,7 +1548,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1341
1548
|
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
1342
1549
|
if (event.success) {
|
|
1343
1550
|
if (event.status == request.expectedStatus) {
|
|
1344
|
-
request.
|
|
1551
|
+
if (request.waitForUpload) {
|
|
1552
|
+
request.stoppedEvent = event
|
|
1553
|
+
if (request.uploadSucceeded) {
|
|
1554
|
+
request.pending.resolve(event)
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
request.pending.resolve(event)
|
|
1558
|
+
}
|
|
1345
1559
|
}
|
|
1346
1560
|
} else {
|
|
1347
1561
|
request.pending.reject(
|
|
@@ -1353,6 +1567,27 @@ class MentraBluetoothSdk private constructor(
|
|
|
1353
1567
|
}
|
|
1354
1568
|
}
|
|
1355
1569
|
|
|
1570
|
+
private fun handleMediaUploadForRequests(event: MediaUploadEvent) {
|
|
1571
|
+
if (!event.isVideo) return
|
|
1572
|
+
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
1573
|
+
if (!request.waitForUpload) return
|
|
1574
|
+
if (event.isSuccess) {
|
|
1575
|
+
val stoppedEvent = request.stoppedEvent
|
|
1576
|
+
if (stoppedEvent != null) {
|
|
1577
|
+
request.pending.resolve(stoppedEvent)
|
|
1578
|
+
} else {
|
|
1579
|
+
request.uploadSucceeded = true
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
request.pending.reject(
|
|
1583
|
+
BluetoothException(
|
|
1584
|
+
"video_upload_failed",
|
|
1585
|
+
event.errorMessage ?: "Video upload failed.",
|
|
1586
|
+
)
|
|
1587
|
+
)
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1356
1591
|
private fun handleRgbLedResponseForRequests(event: RgbLedControlResponseEvent) {
|
|
1357
1592
|
val pending = pendingRgbLedRequests[event.requestId] ?: return
|
|
1358
1593
|
if (event.state == "success") {
|