@mentra/bluetooth-sdk 0.1.21-beta.3 → 3.1.0-dev.4
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 +38 -2
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +209 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1381 -284
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +63 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +47 -3
- package/ios/Source/DeviceManager.swift +146 -16
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +498 -77
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +74 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
|
@@ -89,6 +89,14 @@ class DeviceManager {
|
|
|
89
89
|
get() = DeviceStore.store.get("bluetooth", "pending_wearable") as? String ?: ""
|
|
90
90
|
set(value) = DeviceStore.apply("bluetooth", "pending_wearable", value)
|
|
91
91
|
|
|
92
|
+
private var pendingDeviceName: String
|
|
93
|
+
get() = DeviceStore.store.get("bluetooth", "pending_device_name") as? String ?: ""
|
|
94
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_device_name", value)
|
|
95
|
+
|
|
96
|
+
private var pendingDeviceAddress: String
|
|
97
|
+
get() = DeviceStore.store.get("bluetooth", "pending_device_address") as? String ?: ""
|
|
98
|
+
set(value) = DeviceStore.apply("bluetooth", "pending_device_address", value)
|
|
99
|
+
|
|
92
100
|
public var deviceName: String
|
|
93
101
|
get() = DeviceStore.store.get("bluetooth", "device_name") as? String ?: ""
|
|
94
102
|
set(value) = DeviceStore.apply("bluetooth", "device_name", value)
|
|
@@ -205,6 +213,7 @@ class DeviceManager {
|
|
|
205
213
|
private var lastReadyHandledAtMs: Long = 0L
|
|
206
214
|
private var lastReadyHandledKey: String = ""
|
|
207
215
|
private var lastSystemTimeSyncConnectionKey: String = ""
|
|
216
|
+
private var pendingSystemTimeSync: Runnable? = null
|
|
208
217
|
|
|
209
218
|
private var systemMicUnavailable: Boolean
|
|
210
219
|
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
@@ -250,6 +259,12 @@ class DeviceManager {
|
|
|
250
259
|
// Audio output format - defaults to LC3 for bandwidth savings
|
|
251
260
|
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
252
261
|
private var lastLc3Event: Long? = null
|
|
262
|
+
private val micHealthLock = Any()
|
|
263
|
+
private var sequenceGapEvents = 0L
|
|
264
|
+
private var decodeFailures = 0L
|
|
265
|
+
private var lastLc3ReceivedAt: Long? = null
|
|
266
|
+
private var lastPcmProducedAt: Long? = null
|
|
267
|
+
private var lastLc3Sequence: Int? = null
|
|
253
268
|
private var micReinitRunnable: Runnable? = null
|
|
254
269
|
private var systemMicAvailabilityRecheckRunnable: Runnable? = null
|
|
255
270
|
|
|
@@ -742,7 +757,69 @@ class DeviceManager {
|
|
|
742
757
|
* this per uplink packet — otherwise the watchdog keeps re-enabling a working mic.
|
|
743
758
|
*/
|
|
744
759
|
fun reportGlassesAudioActivity() {
|
|
745
|
-
|
|
760
|
+
val now = System.currentTimeMillis()
|
|
761
|
+
lastLc3Event = now
|
|
762
|
+
synchronized(micHealthLock) {
|
|
763
|
+
lastPcmProducedAt = now
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private fun micHealthSnapshotLocked(): MicHealth =
|
|
768
|
+
MicHealth(
|
|
769
|
+
sequenceGapEvents = sequenceGapEvents,
|
|
770
|
+
decodeFailures = decodeFailures,
|
|
771
|
+
lastLc3ReceivedAt = lastLc3ReceivedAt,
|
|
772
|
+
lastPcmProducedAt = lastPcmProducedAt,
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
private fun resetMicHealth() {
|
|
776
|
+
synchronized(micHealthLock) {
|
|
777
|
+
sequenceGapEvents = 0
|
|
778
|
+
decodeFailures = 0
|
|
779
|
+
lastLc3ReceivedAt = null
|
|
780
|
+
lastPcmProducedAt = null
|
|
781
|
+
lastLc3Sequence = null
|
|
782
|
+
}
|
|
783
|
+
lastLc3Event = null
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
fun resetMicSequenceBaseline() {
|
|
787
|
+
synchronized(micHealthLock) { lastLc3Sequence = null }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private fun recordLc3Packet(sequenceNumber: Int?) {
|
|
791
|
+
var gap = false
|
|
792
|
+
val health = synchronized(micHealthLock) {
|
|
793
|
+
val now = System.currentTimeMillis()
|
|
794
|
+
lastLc3ReceivedAt = now
|
|
795
|
+
lastLc3Event = now
|
|
796
|
+
if (sequenceNumber != null) {
|
|
797
|
+
val normalized = sequenceNumber and 0xff
|
|
798
|
+
lastLc3Sequence?.let { previous ->
|
|
799
|
+
val expected = (previous + 1) and 0xff
|
|
800
|
+
if (normalized != expected) {
|
|
801
|
+
gap = true
|
|
802
|
+
sequenceGapEvents += 1
|
|
803
|
+
Bridge.log("MAN: LC3 packet sequence mismatch. Expected: $expected, Got: $normalized")
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
lastLc3Sequence = normalized
|
|
807
|
+
}
|
|
808
|
+
micHealthSnapshotLocked()
|
|
809
|
+
}
|
|
810
|
+
if (gap) Bridge.sendMicHealth(health, "sequence_gap")
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private fun recordMicDecodeFailure() {
|
|
814
|
+
val health = synchronized(micHealthLock) {
|
|
815
|
+
decodeFailures += 1
|
|
816
|
+
micHealthSnapshotLocked()
|
|
817
|
+
}
|
|
818
|
+
Bridge.sendMicHealth(health, "decode_failure")
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private fun recordMicPcmProduced() {
|
|
822
|
+
synchronized(micHealthLock) { lastPcmProducedAt = System.currentTimeMillis() }
|
|
746
823
|
}
|
|
747
824
|
|
|
748
825
|
/**
|
|
@@ -750,12 +827,14 @@ class DeviceManager {
|
|
|
750
827
|
* canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
|
|
751
828
|
* phone→cloud encoding.
|
|
752
829
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
830
|
+
@JvmOverloads
|
|
831
|
+
fun handleGlassesMicData(rawLC3Data: ByteArray, frameSize: Int = 40, sequenceNumber: Int? = null) {
|
|
832
|
+
recordLc3Packet(sequenceNumber)
|
|
755
833
|
val pcmData: ByteArray?
|
|
756
834
|
synchronized(lc3Lock) {
|
|
757
835
|
if (lc3DecoderPtr == 0L) {
|
|
758
836
|
Bridge.log("MAN: LC3 decoder not initialized, cannot process glasses audio")
|
|
837
|
+
recordMicDecodeFailure()
|
|
759
838
|
return
|
|
760
839
|
}
|
|
761
840
|
|
|
@@ -764,14 +843,17 @@ class DeviceManager {
|
|
|
764
843
|
pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, rawLC3Data, frameSize)
|
|
765
844
|
} catch (e: Exception) {
|
|
766
845
|
Bridge.log("MAN: Failed to decode glasses LC3: ${e.message}")
|
|
846
|
+
recordMicDecodeFailure()
|
|
767
847
|
return
|
|
768
848
|
}
|
|
769
849
|
}
|
|
770
850
|
if (pcmData != null && pcmData.isNotEmpty()) {
|
|
771
851
|
// Re-encode to canonical LC3 via handlePcm (outside lock to avoid deadlock)
|
|
852
|
+
recordMicPcmProduced()
|
|
772
853
|
handlePcm(pcmData)
|
|
773
854
|
} else {
|
|
774
855
|
Bridge.log("MAN: LC3 decode returned empty data")
|
|
856
|
+
recordMicDecodeFailure()
|
|
775
857
|
}
|
|
776
858
|
}
|
|
777
859
|
|
|
@@ -948,15 +1030,20 @@ class DeviceManager {
|
|
|
948
1030
|
|
|
949
1031
|
when (currentViewState.layoutType) {
|
|
950
1032
|
"text_wall" -> {
|
|
951
|
-
sgc?.sendTextWall(currentViewState.text)
|
|
1033
|
+
sgc?.sendTextWall(parsePlaceholders(currentViewState.text))
|
|
952
1034
|
}
|
|
953
1035
|
|
|
954
1036
|
"double_text_wall" -> {
|
|
955
|
-
sgc?.sendDoubleTextWall(
|
|
1037
|
+
sgc?.sendDoubleTextWall(
|
|
1038
|
+
parsePlaceholders(currentViewState.topText),
|
|
1039
|
+
parsePlaceholders(currentViewState.bottomText)
|
|
1040
|
+
)
|
|
956
1041
|
}
|
|
957
1042
|
|
|
958
1043
|
"reference_card" -> {
|
|
959
|
-
|
|
1044
|
+
val title = parsePlaceholders(currentViewState.title)
|
|
1045
|
+
val text = parsePlaceholders(currentViewState.text)
|
|
1046
|
+
sgc?.sendTextWall("$title\n\n$text")
|
|
960
1047
|
}
|
|
961
1048
|
|
|
962
1049
|
"bitmap_view" -> {
|
|
@@ -973,7 +1060,7 @@ class DeviceManager {
|
|
|
973
1060
|
|
|
974
1061
|
"positioned_text" -> {
|
|
975
1062
|
sgc?.sendPositionedText(
|
|
976
|
-
currentViewState.text,
|
|
1063
|
+
parsePlaceholders(currentViewState.text),
|
|
977
1064
|
currentViewState.bmpX ?: 0,
|
|
978
1065
|
currentViewState.bmpY ?: 0,
|
|
979
1066
|
currentViewState.bmpWidth ?: 576,
|
|
@@ -994,6 +1081,8 @@ class DeviceManager {
|
|
|
994
1081
|
}
|
|
995
1082
|
|
|
996
1083
|
private fun parsePlaceholders(text: String): String {
|
|
1084
|
+
if (!text.contains('$')) return text
|
|
1085
|
+
|
|
997
1086
|
val dateFormatter = SimpleDateFormat("M/dd, h:mm", Locale.getDefault())
|
|
998
1087
|
val formattedDate = dateFormatter.format(Date())
|
|
999
1088
|
|
|
@@ -1141,7 +1230,7 @@ class DeviceManager {
|
|
|
1141
1230
|
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
1142
1231
|
sgc?.cleanup()
|
|
1143
1232
|
sgc = null
|
|
1144
|
-
|
|
1233
|
+
resetSystemTimeSync()
|
|
1145
1234
|
}
|
|
1146
1235
|
|
|
1147
1236
|
if (sgc != null) {
|
|
@@ -1264,6 +1353,16 @@ class DeviceManager {
|
|
|
1264
1353
|
return
|
|
1265
1354
|
}
|
|
1266
1355
|
|
|
1356
|
+
if (pendingDeviceName.isNotEmpty()) {
|
|
1357
|
+
deviceName = pendingDeviceName
|
|
1358
|
+
}
|
|
1359
|
+
if (pendingDeviceAddress.isNotEmpty()) {
|
|
1360
|
+
deviceAddress = pendingDeviceAddress
|
|
1361
|
+
}
|
|
1362
|
+
pendingDeviceName = ""
|
|
1363
|
+
pendingDeviceAddress = ""
|
|
1364
|
+
pendingWearable = ""
|
|
1365
|
+
|
|
1267
1366
|
val readyKey = "${sgc?.type}:${deviceName}"
|
|
1268
1367
|
val now = System.currentTimeMillis()
|
|
1269
1368
|
if (readyKey == lastReadyHandledKey && now - lastReadyHandledAtMs < 2000) {
|
|
@@ -1274,7 +1373,7 @@ class DeviceManager {
|
|
|
1274
1373
|
lastReadyHandledAtMs = now
|
|
1275
1374
|
|
|
1276
1375
|
Bridge.log("MAN: handleDeviceReady() ${sgc?.type}")
|
|
1277
|
-
|
|
1376
|
+
resetMicHealth()
|
|
1278
1377
|
defaultWearable = sgc?.type ?: ""
|
|
1279
1378
|
searching = false
|
|
1280
1379
|
|
|
@@ -1344,9 +1443,25 @@ class DeviceManager {
|
|
|
1344
1443
|
}
|
|
1345
1444
|
|
|
1346
1445
|
lastSystemTimeSyncConnectionKey = connectionKey
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1446
|
+
pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
|
|
1447
|
+
val sync =
|
|
1448
|
+
Runnable {
|
|
1449
|
+
pendingSystemTimeSync = null
|
|
1450
|
+
if (lastSystemTimeSyncConnectionKey != connectionKey || sgc !== activeSgc) {
|
|
1451
|
+
return@Runnable
|
|
1452
|
+
}
|
|
1453
|
+
val timestampMs = System.currentTimeMillis()
|
|
1454
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: $timestampMs")
|
|
1455
|
+
activeSgc.sendSetSystemTime(timestampMs)
|
|
1456
|
+
}
|
|
1457
|
+
pendingSystemTimeSync = sync
|
|
1458
|
+
mainHandler.postDelayed(sync, 3000)
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
private fun resetSystemTimeSync() {
|
|
1462
|
+
pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
|
|
1463
|
+
pendingSystemTimeSync = null
|
|
1464
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1350
1465
|
}
|
|
1351
1466
|
|
|
1352
1467
|
private fun handleG1Ready() {
|
|
@@ -1361,7 +1476,8 @@ class DeviceManager {
|
|
|
1361
1476
|
|
|
1362
1477
|
fun handleDeviceDisconnected() {
|
|
1363
1478
|
Bridge.log("MAN: Device disconnected")
|
|
1364
|
-
|
|
1479
|
+
resetSystemTimeSync()
|
|
1480
|
+
resetMicHealth()
|
|
1365
1481
|
DeviceStore.apply("glasses", "headUp", false)
|
|
1366
1482
|
DeviceStore.apply(
|
|
1367
1483
|
"glasses",
|
|
@@ -1634,6 +1750,11 @@ class DeviceManager {
|
|
|
1634
1750
|
sgc?.sendHotspotState(enabled)
|
|
1635
1751
|
}
|
|
1636
1752
|
|
|
1753
|
+
fun setWifiAdbState(enabled: Boolean) {
|
|
1754
|
+
Bridge.log("MAN: Setting glasses Wi-Fi ADB state: $enabled")
|
|
1755
|
+
sgc?.sendWifiAdbState(enabled)
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1637
1758
|
fun setSystemTime(timestampMs: Long) {
|
|
1638
1759
|
Bridge.log("MAN: Setting glasses system time: $timestampMs")
|
|
1639
1760
|
sgc?.sendSetSystemTime(timestampMs)
|
|
@@ -1868,6 +1989,11 @@ class DeviceManager {
|
|
|
1868
1989
|
)
|
|
1869
1990
|
}
|
|
1870
1991
|
|
|
1992
|
+
fun queryVideoRecordingStatus(requestId: String) {
|
|
1993
|
+
Bridge.log("MAN: Querying video recording status: requestId=$requestId")
|
|
1994
|
+
sgc?.queryVideoRecordingStatus(requestId)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1871
1997
|
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
1872
1998
|
Bridge.log(
|
|
1873
1999
|
"MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
|
|
@@ -1954,6 +2080,11 @@ class DeviceManager {
|
|
|
1954
2080
|
return
|
|
1955
2081
|
}
|
|
1956
2082
|
initSGC(defaultWearable)
|
|
2083
|
+
val live = sgc as? MentraLive
|
|
2084
|
+
if (live?.isPairingYieldActive() == true) {
|
|
2085
|
+
Bridge.log("MAN: connectDefault skipped — Mentra Live pairing yield active")
|
|
2086
|
+
return
|
|
2087
|
+
}
|
|
1957
2088
|
searching = true
|
|
1958
2089
|
sgc?.connectById(reconnectTarget)
|
|
1959
2090
|
connectDefaultController()
|
|
@@ -2009,10 +2140,10 @@ class DeviceManager {
|
|
|
2009
2140
|
disconnect()
|
|
2010
2141
|
Thread.sleep(100)
|
|
2011
2142
|
searching = true
|
|
2012
|
-
|
|
2143
|
+
pendingDeviceName = name
|
|
2013
2144
|
|
|
2014
2145
|
initSGC(pendingWearable)
|
|
2015
|
-
sgc?.connectById(
|
|
2146
|
+
sgc?.connectById(name)
|
|
2016
2147
|
}
|
|
2017
2148
|
|
|
2018
2149
|
fun connectDevice(deviceModel: String, deviceName: String) {
|
|
@@ -2043,7 +2174,8 @@ class DeviceManager {
|
|
|
2043
2174
|
sgc?.clearDisplay()
|
|
2044
2175
|
sgc?.disconnect()
|
|
2045
2176
|
sgc = null // Clear the SGC reference after disconnect
|
|
2046
|
-
|
|
2177
|
+
resetSystemTimeSync()
|
|
2178
|
+
resetMicHealth()
|
|
2047
2179
|
searching = false
|
|
2048
2180
|
micEnabled = false
|
|
2049
2181
|
updateMicState()
|
|
@@ -2054,6 +2186,7 @@ class DeviceManager {
|
|
|
2054
2186
|
// previously connected pair can never be reported for the next connection.
|
|
2055
2187
|
DeviceStore.apply("glasses", "serialNumber", "")
|
|
2056
2188
|
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
2189
|
+
DeviceStore.apply("glasses", "wifiMacAddress", "")
|
|
2057
2190
|
DeviceStore.apply("glasses", "leftMacAddress", "")
|
|
2058
2191
|
DeviceStore.apply("glasses", "rightMacAddress", "")
|
|
2059
2192
|
DeviceStore.apply("glasses", "macAddress", "")
|
|
@@ -2082,20 +2215,75 @@ class DeviceManager {
|
|
|
2082
2215
|
fun forget() {
|
|
2083
2216
|
Bridge.log("MAN: Forgetting smart glasses")
|
|
2084
2217
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2218
|
+
val live = sgc as? MentraLive
|
|
2219
|
+
if (live != null) {
|
|
2220
|
+
// MentraLive.forget() disconnects GATT then removeBond. Do not refuse
|
|
2221
|
+
// during CTKD bonding — Unpair is an explicit user request to drop the pair.
|
|
2222
|
+
live.forget()
|
|
2223
|
+
// MentraLive.forget() already destroyed the SGC. Clear the manager reference and
|
|
2224
|
+
// session state without calling disconnect() again (that would hit a dead instance
|
|
2225
|
+
// and leave a destroyed MentraLive retained for the next scan).
|
|
2226
|
+
sgc = null
|
|
2227
|
+
resetSystemTimeSync()
|
|
2228
|
+
searching = false
|
|
2229
|
+
micEnabled = false
|
|
2230
|
+
updateMicState()
|
|
2231
|
+
shouldSendBootingMessage = true
|
|
2232
|
+
DeviceStore.apply("glasses", "deviceModel", "")
|
|
2233
|
+
DeviceStore.apply("glasses", "serialNumber", "")
|
|
2234
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
2235
|
+
DeviceStore.apply("glasses", "leftMacAddress", "")
|
|
2236
|
+
DeviceStore.apply("glasses", "rightMacAddress", "")
|
|
2237
|
+
DeviceStore.apply("glasses", "macAddress", "")
|
|
2238
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2239
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
2240
|
+
DeviceStore.apply(
|
|
2241
|
+
"glasses",
|
|
2242
|
+
"voiceActivityDetectionEnabled",
|
|
2243
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
2244
|
+
)
|
|
2245
|
+
searchingController = false
|
|
2246
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2247
|
+
controller?.disconnect()
|
|
2248
|
+
controller = null
|
|
2249
|
+
} else {
|
|
2250
|
+
// Typical abandonAttempt path: disconnect() already destroyed SGC and nulled
|
|
2251
|
+
// this.sgc, so MentraLive.forget() never ran and Classic bonds stayed up —
|
|
2252
|
+
// glasses keep IBRT ACL and stop BLE advertising. Only tear Live bonds when
|
|
2253
|
+
// the selected/pending wearable is Mentra Live — forgetting G1/G2 must not
|
|
2254
|
+
// unpair an unrelated Live unit.
|
|
2255
|
+
val liveTarget =
|
|
2256
|
+
defaultWearable == DeviceTypes.LIVE || pendingWearable == DeviceTypes.LIVE
|
|
2257
|
+
if (liveTarget) {
|
|
2258
|
+
val extraAddress =
|
|
2259
|
+
when {
|
|
2260
|
+
defaultWearable == DeviceTypes.LIVE ->
|
|
2261
|
+
deviceAddress.takeIf { it.isNotEmpty() }
|
|
2262
|
+
pendingWearable == DeviceTypes.LIVE ->
|
|
2263
|
+
pendingDeviceAddress.takeIf { it.isNotEmpty() }
|
|
2264
|
+
else -> null
|
|
2265
|
+
}
|
|
2266
|
+
MentraLive.unbondBondedMentraLiveDevices(
|
|
2267
|
+
Bridge.getContext(),
|
|
2268
|
+
extraAddress,
|
|
2269
|
+
)
|
|
2270
|
+
}
|
|
2271
|
+
sgc?.forget()
|
|
2272
|
+
disconnect()
|
|
2273
|
+
}
|
|
2090
2274
|
|
|
2091
2275
|
// Clear state
|
|
2092
2276
|
defaultWearable = ""
|
|
2093
2277
|
deviceName = ""
|
|
2094
2278
|
deviceAddress = ""
|
|
2279
|
+
pendingDeviceName = ""
|
|
2280
|
+
pendingDeviceAddress = ""
|
|
2281
|
+
pendingWearable = ""
|
|
2095
2282
|
Bridge.saveSetting("default_wearable", "")
|
|
2096
2283
|
Bridge.saveSetting("device_name", "")
|
|
2097
2284
|
Bridge.saveSetting("device_address", "")
|
|
2098
2285
|
Bridge.saveSetting("project_name", "")
|
|
2286
|
+
Bridge.saveSetting("pending_wearable", "")
|
|
2099
2287
|
}
|
|
2100
2288
|
|
|
2101
2289
|
fun forgetController() {
|
|
@@ -65,6 +65,7 @@ object DeviceStore {
|
|
|
65
65
|
store.set("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
66
66
|
store.set("glasses", "deviceModel", "")
|
|
67
67
|
store.set("glasses", "firmwareVersion", "")
|
|
68
|
+
store.set("glasses", "hotspotOtaVersion", 0)
|
|
68
69
|
store.set("glasses", "micEnabled", false)
|
|
69
70
|
store.set("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
70
71
|
store.set("glasses", "bluetoothClassicConnected", false)
|
|
@@ -74,6 +75,7 @@ object DeviceStore {
|
|
|
74
75
|
store.set("glasses", "caseBatteryLevel", -1)
|
|
75
76
|
store.set("glasses", "headUp", false)
|
|
76
77
|
store.set("glasses", "bluetoothMacAddress", "")
|
|
78
|
+
store.set("glasses", "wifiMacAddress", "")
|
|
77
79
|
store.set("glasses", "leftMacAddress", "")
|
|
78
80
|
store.set("glasses", "rightMacAddress", "")
|
|
79
81
|
store.set("glasses", "macAddress", "")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
|
+
internal object GeneratedReleaseMetadata {
|
|
5
|
+
const val FAMILY_BASE_VERSION: String = "3.1.0"
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.1.0-dev.4"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.4"
|
|
8
|
+
const val SOURCE_COMMIT: String = "43b81aea1c84675fade024077063e45fddde4861"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.4/mentra-live-ota-3.1.0-dev.4.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "8fbea7589345658dc5dffcb394229ffc5c52fbe75352acb8f6827d83290ce750"
|
|
11
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
3
5
|
import android.bluetooth.BluetoothManager
|
|
4
6
|
import android.content.Context
|
|
7
|
+
import android.os.Bundle
|
|
5
8
|
import android.os.Handler
|
|
6
9
|
import android.os.Looper
|
|
7
10
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
@@ -30,6 +33,23 @@ class MentraBluetoothSdk private constructor(
|
|
|
30
33
|
private val discoveredDeviceNames = mutableSetOf<String>()
|
|
31
34
|
private val bridgeEventSinkId: String
|
|
32
35
|
private val storeListenerId: String
|
|
36
|
+
private val activityLifecycleCallbacks =
|
|
37
|
+
object : Application.ActivityLifecycleCallbacks {
|
|
38
|
+
override fun onActivityResumed(activity: Activity) {
|
|
39
|
+
// Match the Mentra App's OnActivityEntersForeground hook. This runs after
|
|
40
|
+
// runtime permission dialogs in native host apps, allowing the service to
|
|
41
|
+
// replace its temporary dataSync type with connectedDevice/microphone/location.
|
|
42
|
+
deviceManager.refreshForegroundServiceTypes()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
|
46
|
+
override fun onActivityStarted(activity: Activity) = Unit
|
|
47
|
+
override fun onActivityPaused(activity: Activity) = Unit
|
|
48
|
+
override fun onActivityStopped(activity: Activity) = Unit
|
|
49
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
50
|
+
override fun onActivityDestroyed(activity: Activity) = Unit
|
|
51
|
+
}
|
|
52
|
+
private var activityLifecycleCallbacksRegistered = false
|
|
33
53
|
private var suppressDefaultDeviceEvents = false
|
|
34
54
|
private val streamKeepAliveLock = Any()
|
|
35
55
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
@@ -57,6 +77,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
57
77
|
listeners.add(listener)
|
|
58
78
|
Bridge.initialize(appContext)
|
|
59
79
|
deviceManager = DeviceManager.getInstance()
|
|
80
|
+
(appContext as? Application)?.let { application ->
|
|
81
|
+
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
|
82
|
+
activityLifecycleCallbacksRegistered = true
|
|
83
|
+
}
|
|
60
84
|
bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
|
|
61
85
|
// Baseline the analytics connection state before subscribing to the store:
|
|
62
86
|
// store updates invoke listeners synchronously on the updating thread, so a
|
|
@@ -258,6 +282,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
258
282
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", device.name)
|
|
259
283
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", device.address ?: "")
|
|
260
284
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", device.projectName ?: "")
|
|
285
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
286
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
261
287
|
} finally {
|
|
262
288
|
suppressDefaultDeviceEvents = false
|
|
263
289
|
}
|
|
@@ -271,6 +297,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
271
297
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", "")
|
|
272
298
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", "")
|
|
273
299
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", "")
|
|
300
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
301
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
274
302
|
} finally {
|
|
275
303
|
suppressDefaultDeviceEvents = false
|
|
276
304
|
}
|
|
@@ -383,8 +411,24 @@ class MentraBluetoothSdk private constructor(
|
|
|
383
411
|
cancelConnectionAttempt()
|
|
384
412
|
}
|
|
385
413
|
}
|
|
386
|
-
if (
|
|
387
|
-
|
|
414
|
+
if (!isController) {
|
|
415
|
+
if (options.saveAsDefault) {
|
|
416
|
+
setDefaultDevice(device)
|
|
417
|
+
} else {
|
|
418
|
+
// Pairing uses saveAsDefault=false so default identity stays on the
|
|
419
|
+
// previous owner until handleDeviceReady. Stash the GATT target separately
|
|
420
|
+
// so MentraLive can connect by MAC without promoting getDefaultDevice().
|
|
421
|
+
DeviceStore.apply(
|
|
422
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
423
|
+
"pending_device_name",
|
|
424
|
+
device.name,
|
|
425
|
+
)
|
|
426
|
+
DeviceStore.apply(
|
|
427
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
428
|
+
"pending_device_address",
|
|
429
|
+
device.address ?: "",
|
|
430
|
+
)
|
|
431
|
+
}
|
|
388
432
|
}
|
|
389
433
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_wearable", device.model.deviceType)
|
|
390
434
|
deviceManager.connectByName(device.name)
|
|
@@ -408,6 +452,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
408
452
|
}
|
|
409
453
|
|
|
410
454
|
fun cancelConnectionAttempt() {
|
|
455
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
456
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
411
457
|
deviceManager.disconnect()
|
|
412
458
|
}
|
|
413
459
|
|
|
@@ -416,6 +462,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
416
462
|
}
|
|
417
463
|
|
|
418
464
|
fun disconnect() {
|
|
465
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
466
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
419
467
|
deviceManager.disconnect()
|
|
420
468
|
}
|
|
421
469
|
|
|
@@ -856,6 +904,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
856
904
|
}
|
|
857
905
|
}
|
|
858
906
|
|
|
907
|
+
fun setWifiAdbState(enabled: Boolean) {
|
|
908
|
+
deviceManager.setWifiAdbState(enabled)
|
|
909
|
+
}
|
|
910
|
+
|
|
859
911
|
fun setSystemTime(timestampMs: Long) {
|
|
860
912
|
deviceManager.setSystemTime(timestampMs)
|
|
861
913
|
}
|
|
@@ -1087,6 +1139,25 @@ class MentraBluetoothSdk private constructor(
|
|
|
1087
1139
|
}
|
|
1088
1140
|
}
|
|
1089
1141
|
|
|
1142
|
+
suspend fun queryVideoRecordingStatus(requestId: String): VideoRecordingStatusEvent {
|
|
1143
|
+
require(requestId.isNotBlank()) { "requestId is required to query video recording status." }
|
|
1144
|
+
requireGlassesConnected("query video recording status")
|
|
1145
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("query video recording status")
|
|
1146
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_status", pending)
|
|
1147
|
+
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
1148
|
+
throw BluetoothSdkException(
|
|
1149
|
+
"request_in_flight",
|
|
1150
|
+
"A video recording command is already waiting for requestId $requestId.",
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
deviceManager.queryVideoRecordingStatus(requestId)
|
|
1155
|
+
return pending.await()
|
|
1156
|
+
} finally {
|
|
1157
|
+
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1090
1161
|
suspend fun requestVersionInfo(): VersionInfoResult {
|
|
1091
1162
|
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
1092
1163
|
synchronized(oneShotLock) {
|
|
@@ -1110,11 +1181,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
1110
1181
|
}
|
|
1111
1182
|
}
|
|
1112
1183
|
|
|
1113
|
-
|
|
1184
|
+
/** Select the HTTP(S) manifest used by subsequent OTA checks and installs. */
|
|
1185
|
+
fun setOtaVersionUrl(otaVersionUrl: String) {
|
|
1114
1186
|
configuredOtaVersionUrl = OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
1115
1187
|
}
|
|
1116
1188
|
|
|
1117
|
-
|
|
1189
|
+
/** Return the configured OTA manifest, or this SDK release's default manifest. */
|
|
1190
|
+
fun getOtaVersionUrl(): String = configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1118
1191
|
|
|
1119
1192
|
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
1120
1193
|
suspend fun checkForOtaUpdate(): Boolean {
|
|
@@ -1240,6 +1313,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1240
1313
|
systemTimeMs = versionInfo.systemTimeMs ?: status.systemTimeMs,
|
|
1241
1314
|
otaVersionUrl = versionInfo.otaVersionUrl.ifBlank { status.otaVersionUrl },
|
|
1242
1315
|
appVersion = versionInfo.appVersion.ifBlank { status.appVersion },
|
|
1316
|
+
hotspotOtaVersion =
|
|
1317
|
+
if (versionInfo.hotspotOtaVersion > 0) {
|
|
1318
|
+
versionInfo.hotspotOtaVersion
|
|
1319
|
+
} else {
|
|
1320
|
+
status.hotspotOtaVersion
|
|
1321
|
+
},
|
|
1243
1322
|
)
|
|
1244
1323
|
} catch (cancellation: CancellationException) {
|
|
1245
1324
|
// Never swallow cancellation into the stale-status fallback: callers
|
|
@@ -1323,6 +1402,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1323
1402
|
|
|
1324
1403
|
override fun close() {
|
|
1325
1404
|
stopStreamKeepAliveMonitor()
|
|
1405
|
+
if (activityLifecycleCallbacksRegistered) {
|
|
1406
|
+
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1407
|
+
activityLifecycleCallbacks,
|
|
1408
|
+
)
|
|
1409
|
+
activityLifecycleCallbacksRegistered = false
|
|
1410
|
+
}
|
|
1326
1411
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
1327
1412
|
DeviceStore.store.removeListener(storeListenerId)
|
|
1328
1413
|
analytics.shutdown()
|
|
@@ -1520,6 +1605,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1520
1605
|
}
|
|
1521
1606
|
dispatchToListeners { it.onGalleryStatus(event) }
|
|
1522
1607
|
}
|
|
1608
|
+
"pairing_info" -> {
|
|
1609
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1610
|
+
}
|
|
1523
1611
|
"photo_response" -> {
|
|
1524
1612
|
val event = PhotoResponseEvent(data)
|
|
1525
1613
|
handlePhotoResponseForRequests(event)
|
|
@@ -1567,10 +1655,19 @@ class MentraBluetoothSdk private constructor(
|
|
|
1567
1655
|
}
|
|
1568
1656
|
"ota_status" -> {
|
|
1569
1657
|
val resultValues = data + mapOf("type" to "ota_status")
|
|
1658
|
+
val event = OtaStatusEvent.fromMap(resultValues)
|
|
1570
1659
|
synchronized(oneShotLock) {
|
|
1571
1660
|
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1661
|
+
otaStartRejectionErrorCode(event)?.let { errorCode ->
|
|
1662
|
+
pendingOtaStart?.reject(
|
|
1663
|
+
BluetoothSdkException(errorCode, "Glasses rejected OTA start: $errorCode")
|
|
1664
|
+
)
|
|
1665
|
+
}
|
|
1572
1666
|
}
|
|
1573
|
-
dispatchToListeners { it.onOtaStatus(
|
|
1667
|
+
dispatchToListeners { it.onOtaStatus(event) }
|
|
1668
|
+
}
|
|
1669
|
+
"mic_health" -> {
|
|
1670
|
+
dispatchToListeners { it.onMicHealth(MicHealthEvent.fromMap(data)) }
|
|
1574
1671
|
}
|
|
1575
1672
|
"settings_ack" -> {
|
|
1576
1673
|
val event = SettingsAckEvent(data)
|
|
@@ -2161,6 +2258,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
2161
2258
|
}
|
|
2162
2259
|
}
|
|
2163
2260
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2261
|
+
/** OTA status messages are not request-correlated, so only known pre-ack failures settle a start. */
|
|
2262
|
+
internal fun otaStartRejectionErrorCode(event: OtaStatusEvent): String? =
|
|
2263
|
+
if (
|
|
2264
|
+
event.status.equals("failed", ignoreCase = true) &&
|
|
2265
|
+
event.errorMessage.equals("battery_low", ignoreCase = true)
|
|
2266
|
+
) {
|
|
2267
|
+
"battery_low"
|
|
2268
|
+
} else {
|
|
2269
|
+
null
|
|
2270
|
+
}
|