@mentra/bluetooth-sdk 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +47 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +75 -29
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +208 -45
- 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 +80 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +4 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3831 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -28
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +514 -561
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8674 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +5 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +9 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
- package/build/BluetoothSdk.types.d.ts +48 -17
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -9
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -6
- 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 +46 -2
- 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 +8 -8
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +44 -68
- package/ios/Source/BluetoothSdkDefaults.swift +23 -3
- package/ios/Source/DeviceManager.swift +38 -33
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +217 -50
- 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 +219 -13
- package/ios/Source/controllers/ControllerManager.swift +3 -7
- package/ios/Source/controllers/R1.swift +3 -7
- package/ios/Source/sgcs/Frame.swift +0 -2
- package/ios/Source/sgcs/G1.swift +7 -8
- package/ios/Source/sgcs/G2.swift +21 -20
- package/ios/Source/sgcs/Mach1.swift +7 -8
- package/ios/Source/sgcs/MentraLive.swift +132 -63
- package/ios/Source/sgcs/MentraNex.swift +7 -8
- package/ios/Source/sgcs/SGCManager.swift +7 -10
- package/ios/Source/sgcs/Simulated.swift +8 -8
- package/ios/Source/status/DeviceStatus.swift +1 -9
- package/ios/Source/streaming/StreamModels.swift +7 -32
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +49 -17
- package/src/_private/BluetoothSdkModule.ts +9 -17
- package/src/_private/photoRequestPayload.ts +48 -3
- package/src/debug.ts +9 -0
- package/src/index.ts +19 -10
- 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 -7434
|
@@ -45,6 +45,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
45
45
|
private var pendingWifiStatus: PendingWifiStatusRequest? = null
|
|
46
46
|
private var pendingHotspotStatus: PendingHotspotStatusRequest? = null
|
|
47
47
|
private var pendingVersionInfo: PendingResponse<VersionInfoResult>? = null
|
|
48
|
+
@Volatile private var configuredOtaVersionUrl: String? = null
|
|
48
49
|
|
|
49
50
|
init {
|
|
50
51
|
listeners.add(listener)
|
|
@@ -68,6 +69,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
68
69
|
private const val VIDEO_UPLOAD_STOP_TIMEOUT_MS = 10 * 60 * 1000L
|
|
69
70
|
private const val STREAM_START_TIMEOUT_MS = 30_000L
|
|
70
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
|
|
71
75
|
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
72
76
|
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
73
77
|
|
|
@@ -470,40 +474,73 @@ class MentraBluetoothSdk private constructor(
|
|
|
470
474
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
471
475
|
}
|
|
472
476
|
|
|
473
|
-
fun
|
|
477
|
+
fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
|
|
474
478
|
performSettingsCommand(
|
|
475
479
|
setting = "button_photo",
|
|
476
|
-
updateStore = { _ ->
|
|
477
|
-
|
|
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) },
|
|
478
528
|
)
|
|
479
529
|
|
|
480
|
-
fun
|
|
481
|
-
setButtonPhotoSettings(size = settings.size)
|
|
482
|
-
|
|
483
|
-
fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int): SettingsAckEvent =
|
|
530
|
+
fun setVideoRecordingDefaults(defaults: VideoRecordingDefaults): SettingsAckEvent =
|
|
484
531
|
performSettingsCommand(
|
|
485
532
|
setting = "button_video_recording",
|
|
486
533
|
updateStore = { _ ->
|
|
487
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", width)
|
|
488
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", height)
|
|
489
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", fps)
|
|
534
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", defaults.width)
|
|
535
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", defaults.height)
|
|
536
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", defaults.fps)
|
|
490
537
|
},
|
|
491
538
|
send = { requestId ->
|
|
492
|
-
deviceManager.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
539
|
+
deviceManager.sendButtonVideoRecordingSettings(requestId, defaults.width, defaults.height, defaults.fps)
|
|
493
540
|
},
|
|
494
541
|
)
|
|
495
542
|
|
|
496
|
-
fun
|
|
497
|
-
setButtonVideoRecordingSettings(width = settings.width, height = settings.height, fps = settings.fps)
|
|
498
|
-
|
|
499
|
-
fun setButtonCameraLed(enabled: Boolean): SettingsAckEvent =
|
|
500
|
-
performSettingsCommand(
|
|
501
|
-
setting = "button_camera_led",
|
|
502
|
-
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_camera_led", enabled) },
|
|
503
|
-
send = { requestId -> deviceManager.sendButtonCameraLedSetting(requestId, enabled) },
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
fun setButtonMaxRecordingTime(minutes: Int): SettingsAckEvent =
|
|
543
|
+
fun setMaxVideoRecordingDuration(minutes: Int): SettingsAckEvent =
|
|
507
544
|
performSettingsCommand(
|
|
508
545
|
setting = "button_max_recording_time",
|
|
509
546
|
updateStore = { _ ->
|
|
@@ -677,19 +714,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
677
714
|
val pending = PendingResponse<PhotoResponseEvent>("photo request ${request.requestId}")
|
|
678
715
|
pendingPhotoRequests[request.requestId] = pending
|
|
679
716
|
try {
|
|
680
|
-
deviceManager.requestPhoto(
|
|
681
|
-
request.requestId,
|
|
682
|
-
request.appId,
|
|
683
|
-
request.size.value,
|
|
684
|
-
request.webhookUrl,
|
|
685
|
-
request.authToken,
|
|
686
|
-
request.compress.value,
|
|
687
|
-
request.flash,
|
|
688
|
-
request.save,
|
|
689
|
-
request.sound,
|
|
690
|
-
request.exposureTimeNs,
|
|
691
|
-
request.iso,
|
|
692
|
-
)
|
|
717
|
+
deviceManager.requestPhoto(request)
|
|
693
718
|
return pending.await()
|
|
694
719
|
} finally {
|
|
695
720
|
pendingPhotoRequests.remove(request.requestId, pending)
|
|
@@ -719,7 +744,16 @@ class MentraBluetoothSdk private constructor(
|
|
|
719
744
|
}
|
|
720
745
|
}
|
|
721
746
|
|
|
722
|
-
fun startStream(request: StreamRequest): StreamStatusEvent
|
|
747
|
+
fun startStream(request: StreamRequest): StreamStatusEvent =
|
|
748
|
+
startStream(request, startSdkKeepAlive = true)
|
|
749
|
+
|
|
750
|
+
internal fun startExternallyManagedStream(request: StreamRequest): StreamStatusEvent =
|
|
751
|
+
startStream(request, startSdkKeepAlive = false)
|
|
752
|
+
|
|
753
|
+
private fun startStream(
|
|
754
|
+
request: StreamRequest,
|
|
755
|
+
startSdkKeepAlive: Boolean,
|
|
756
|
+
): StreamStatusEvent {
|
|
723
757
|
val message = request.toMap().toMutableMap()
|
|
724
758
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
725
759
|
?: "sdk-${UUID.randomUUID()}"
|
|
@@ -730,8 +764,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
730
764
|
try {
|
|
731
765
|
deviceManager.startStream(message)
|
|
732
766
|
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
733
|
-
if (
|
|
734
|
-
startStreamKeepAliveMonitor(streamId,
|
|
767
|
+
if (startSdkKeepAlive) {
|
|
768
|
+
startStreamKeepAliveMonitor(streamId, DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS)
|
|
735
769
|
}
|
|
736
770
|
return event
|
|
737
771
|
} finally {
|
|
@@ -871,8 +905,41 @@ class MentraBluetoothSdk private constructor(
|
|
|
871
905
|
}
|
|
872
906
|
}
|
|
873
907
|
|
|
874
|
-
|
|
875
|
-
|
|
908
|
+
internal fun setOtaVersionUrl(otaVersionUrl: String) {
|
|
909
|
+
configuredOtaVersionUrl = OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
internal fun getOtaVersionUrl(): String = configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
913
|
+
|
|
914
|
+
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
915
|
+
fun checkForOtaUpdate(): Boolean {
|
|
916
|
+
val status = getFreshGlassesStatus()
|
|
917
|
+
if (!status.connected) {
|
|
918
|
+
throw BluetoothException(
|
|
919
|
+
"glasses_not_connected",
|
|
920
|
+
"Cannot check OTA update because glasses are not connected.",
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
if (status.buildNumber.isBlank()) {
|
|
924
|
+
throw BluetoothException(
|
|
925
|
+
"missing_glasses_version",
|
|
926
|
+
"Cannot check OTA update because glasses build number is unavailable.",
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
val manifestUrl = resolveOtaVersionUrl(status)
|
|
931
|
+
val manifest = OtaManifestChecker.fetch(manifestUrl)
|
|
932
|
+
val otaStatus = waitForOtaManifestStatus(status, manifest)
|
|
933
|
+
return OtaManifestChecker.hasUpdate(
|
|
934
|
+
otaStatus.buildNumber,
|
|
935
|
+
otaStatus.mtkFirmwareVersion,
|
|
936
|
+
otaStatus.besFirmwareVersion,
|
|
937
|
+
manifest,
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Ask connected Mentra Live glasses to report the current OTA install/session status. */
|
|
942
|
+
private fun queryOtaStatus(): OtaQueryResult =
|
|
876
943
|
performOtaQuery("OTA status query") {
|
|
877
944
|
deviceManager.sendOtaQueryStatus()
|
|
878
945
|
}
|
|
@@ -905,6 +972,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
905
972
|
|
|
906
973
|
/** Start the OTA flow after your app has presented the available update to the user. */
|
|
907
974
|
fun startOtaUpdate(): OtaStartAckEvent {
|
|
975
|
+
val otaVersionUrl = resolveOtaVersionUrl(getFreshGlassesStatus())
|
|
976
|
+
return startOtaUpdate(otaVersionUrl)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private fun startOtaCommand(otaVersionUrl: String): OtaStartAckEvent {
|
|
908
980
|
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
909
981
|
synchronized(oneShotLock) {
|
|
910
982
|
if (pendingOtaStart != null) {
|
|
@@ -916,7 +988,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
916
988
|
pendingOtaStart = pending
|
|
917
989
|
}
|
|
918
990
|
try {
|
|
919
|
-
deviceManager.sendOtaStart()
|
|
991
|
+
deviceManager.sendOtaStart(otaVersionUrl)
|
|
920
992
|
return pending.await()
|
|
921
993
|
} finally {
|
|
922
994
|
synchronized(oneShotLock) {
|
|
@@ -927,15 +999,98 @@ class MentraBluetoothSdk private constructor(
|
|
|
927
999
|
}
|
|
928
1000
|
}
|
|
929
1001
|
|
|
930
|
-
|
|
931
|
-
|
|
1002
|
+
internal fun startOtaUpdate(otaVersionUrl: String): OtaStartAckEvent =
|
|
1003
|
+
startOtaCommand(otaVersionUrl)
|
|
1004
|
+
|
|
1005
|
+
internal fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1006
|
+
|
|
1007
|
+
/** Re-run the glasses-side OTA version check after an internal clock-skew recovery. */
|
|
1008
|
+
@JvmSynthetic
|
|
1009
|
+
internal fun retryOtaVersionCheck(): OtaQueryResult =
|
|
932
1010
|
performOtaQuery("OTA version retry") {
|
|
933
1011
|
deviceManager.retryOtaVersionCheck()
|
|
934
1012
|
}
|
|
935
1013
|
|
|
936
|
-
|
|
1014
|
+
private fun getFreshGlassesStatus(): GlassesStatus {
|
|
1015
|
+
val status = getRawGlassesStatus()
|
|
1016
|
+
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
1017
|
+
return status
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return try {
|
|
1021
|
+
val versionInfo = requestVersionInfo()
|
|
1022
|
+
status.copy(
|
|
1023
|
+
androidVersion = versionInfo.androidVersion.ifBlank { status.androidVersion },
|
|
1024
|
+
firmwareVersion = versionInfo.firmwareVersion.ifBlank { status.firmwareVersion },
|
|
1025
|
+
besFirmwareVersion = versionInfo.besFirmwareVersion.ifBlank { status.besFirmwareVersion },
|
|
1026
|
+
mtkFirmwareVersion = versionInfo.mtkFirmwareVersion.ifBlank { status.mtkFirmwareVersion },
|
|
1027
|
+
buildNumber = versionInfo.buildNumber.ifBlank { status.buildNumber },
|
|
1028
|
+
systemTimeMs = versionInfo.systemTimeMs ?: status.systemTimeMs,
|
|
1029
|
+
otaVersionUrl = versionInfo.otaVersionUrl.ifBlank { status.otaVersionUrl },
|
|
1030
|
+
appVersion = versionInfo.appVersion.ifBlank { status.appVersion },
|
|
1031
|
+
)
|
|
1032
|
+
} catch (_: Throwable) {
|
|
1033
|
+
status
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private fun waitForOtaManifestStatus(
|
|
1038
|
+
initialStatus: GlassesStatus,
|
|
1039
|
+
manifest: org.json.JSONObject,
|
|
1040
|
+
): GlassesStatus {
|
|
1041
|
+
var status = initialStatus
|
|
1042
|
+
if (OtaManifestChecker.hasBesFirmware(manifest) && status.besFirmwareVersion.isBlank()) {
|
|
1043
|
+
status = waitForGlassesStatus(status, OTA_BES_VERSION_WAIT_MS) {
|
|
1044
|
+
!it.connected || it.besFirmwareVersion.isNotBlank()
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (OtaManifestChecker.hasMtkPatches(manifest) && status.mtkFirmwareVersion.isBlank()) {
|
|
1049
|
+
status = waitForGlassesStatus(status, OTA_MTK_VERSION_WAIT_MS) {
|
|
1050
|
+
!it.connected || it.mtkFirmwareVersion.isNotBlank()
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!status.connected) {
|
|
1055
|
+
throw BluetoothException(
|
|
1056
|
+
"glasses_not_connected",
|
|
1057
|
+
"Cannot check OTA update because glasses disconnected.",
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
return status
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
private fun waitForGlassesStatus(
|
|
1064
|
+
initialStatus: GlassesStatus,
|
|
1065
|
+
timeoutMs: Long,
|
|
1066
|
+
isReady: (GlassesStatus) -> Boolean,
|
|
1067
|
+
): GlassesStatus {
|
|
1068
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
1069
|
+
var status = initialStatus
|
|
1070
|
+
while (System.currentTimeMillis() < deadline) {
|
|
1071
|
+
status = getRawGlassesStatus()
|
|
1072
|
+
if (isReady(status)) return status
|
|
1073
|
+
val remainingMs = deadline - System.currentTimeMillis()
|
|
1074
|
+
if (remainingMs <= 0L) break
|
|
1075
|
+
Thread.sleep(minOf(OTA_VERSION_POLL_MS, remainingMs))
|
|
1076
|
+
}
|
|
1077
|
+
return getRawGlassesStatus()
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private fun resolveOtaVersionUrl(status: GlassesStatus): String {
|
|
1081
|
+
val deviceUrl = status.otaVersionUrl.trim()
|
|
1082
|
+
if (isLegacyAsgOtaStartBuild(status.buildNumber)) {
|
|
1083
|
+
return deviceUrl.ifBlank { OtaManifestDefaults.PROD_OTA_VERSION_URL }
|
|
1084
|
+
}
|
|
1085
|
+
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1086
|
+
// A future glasses-advertised URL should not silently change that pairing.
|
|
1087
|
+
return configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1088
|
+
}
|
|
937
1089
|
|
|
938
|
-
|
|
1090
|
+
private fun isLegacyAsgOtaStartBuild(buildNumber: String): Boolean {
|
|
1091
|
+
val parsed = buildNumber.toIntOrNull()
|
|
1092
|
+
return parsed != null && parsed < 100_000
|
|
1093
|
+
}
|
|
939
1094
|
|
|
940
1095
|
internal fun sendShutdown() {
|
|
941
1096
|
deviceManager.sendShutdown()
|
|
@@ -1333,6 +1488,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1333
1488
|
return streamId to pending
|
|
1334
1489
|
}
|
|
1335
1490
|
if (pendingStreamStarts.size == 1) {
|
|
1491
|
+
// A streamId-less STOPPED is the glasses' stop-ack for a PREVIOUS
|
|
1492
|
+
// stream (their stop ack carries no streamId), not a verdict on the
|
|
1493
|
+
// pending start — a start_stream that replaces a running publisher
|
|
1494
|
+
// emits exactly this sequence (stopped -> initializing -> streaming).
|
|
1495
|
+
// Attributing it here would reject a start that is about to succeed.
|
|
1496
|
+
if (event.state == StreamState.STOPPED) {
|
|
1497
|
+
return null
|
|
1498
|
+
}
|
|
1336
1499
|
val entry = pendingStreamStarts.entries.first()
|
|
1337
1500
|
return entry.key to entry.value
|
|
1338
1501
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
object MentraBluetoothSdkDebug {
|
|
4
|
+
@JvmStatic
|
|
5
|
+
fun setOtaVersionUrl(
|
|
6
|
+
sdk: MentraBluetoothSdk,
|
|
7
|
+
otaVersionUrl: String,
|
|
8
|
+
) {
|
|
9
|
+
sdk.setOtaVersionUrl(otaVersionUrl)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@JvmStatic
|
|
13
|
+
fun getOtaVersionUrl(sdk: MentraBluetoothSdk): String = sdk.getOtaVersionUrl()
|
|
14
|
+
}
|
|
@@ -53,6 +53,26 @@ class ObservableStore {
|
|
|
53
53
|
listeners.forEach { it(normalizedCategory, mapOf(key to value)) }
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
fun remove(category: String, key: String) {
|
|
57
|
+
val normalizedCategory: String
|
|
58
|
+
val updatedSnapshot: Map<String, Any>
|
|
59
|
+
val listeners: List<(String, Map<String, Any>) -> Unit>
|
|
60
|
+
|
|
61
|
+
synchronized(this) {
|
|
62
|
+
normalizedCategory = normalizeCategory(category)
|
|
63
|
+
val fullKey = "$normalizedCategory.$key"
|
|
64
|
+
if (!values.containsKey(fullKey)) return // already absent, no notification needed
|
|
65
|
+
values.remove(fullKey)
|
|
66
|
+
val prefix = "$normalizedCategory."
|
|
67
|
+
updatedSnapshot = values.filterKeys { it.startsWith(prefix) }
|
|
68
|
+
.mapKeys { it.key.removePrefix(prefix) }
|
|
69
|
+
listeners = emitListeners.values.toList()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Emit the full updated category snapshot so UI listeners can clear the removed key.
|
|
73
|
+
listeners.forEach { it(normalizedCategory, updatedSnapshot) }
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
@Synchronized
|
|
57
77
|
fun get(category: String, key: String): Any? = values["${normalizeCategory(category)}.$key"]
|
|
58
78
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import java.net.HttpURLConnection
|
|
4
|
+
import java.net.URI
|
|
5
|
+
import java.net.URL
|
|
6
|
+
import org.json.JSONArray
|
|
7
|
+
import org.json.JSONObject
|
|
8
|
+
|
|
9
|
+
internal object OtaManifestDefaults {
|
|
10
|
+
private const val SDK_OTA_RELEASE_BASE_URL =
|
|
11
|
+
"https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
12
|
+
// Keep prod as the legacy-device fallback: pre-override ASG builds ignore
|
|
13
|
+
// ota_start.ota_version_url and use their compiled MentraOS default.
|
|
14
|
+
const val PROD_OTA_VERSION_URL = "https://ota.mentraglass.com/prod_live_version.json"
|
|
15
|
+
|
|
16
|
+
fun defaultOtaVersionUrl(): String {
|
|
17
|
+
val sdkVersion = BuildConfig.SDK_VERSION.trim()
|
|
18
|
+
if (sdkVersion.isBlank() || sdkVersion == "unspecified") {
|
|
19
|
+
throw BluetoothException(
|
|
20
|
+
"missing_sdk_version",
|
|
21
|
+
"Cannot determine Bluetooth SDK version for the default OTA manifest URL.",
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return "$SDK_OTA_RELEASE_BASE_URL/bluetooth-sdk-$sdkVersion-version.json"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
internal object OtaManifestChecker {
|
|
29
|
+
private const val ASG_CLIENT_PACKAGE = "com.mentra.asg_client"
|
|
30
|
+
|
|
31
|
+
fun normalizeHttpUrl(value: String): String {
|
|
32
|
+
val trimmed = value.trim()
|
|
33
|
+
if (trimmed.isEmpty()) {
|
|
34
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be a non-empty http(s) URL.")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val uri =
|
|
38
|
+
try {
|
|
39
|
+
URI(trimmed)
|
|
40
|
+
} catch (error: Exception) {
|
|
41
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be a valid http(s) URL.", error)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val scheme = uri.scheme?.lowercase()
|
|
45
|
+
if ((scheme != "http" && scheme != "https") || uri.host.isNullOrBlank()) {
|
|
46
|
+
throw BluetoothException("invalid_ota_url", "OTA version URL must be an http(s) URL.")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return uri.toString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun fetch(otaVersionUrl: String): JSONObject {
|
|
53
|
+
val connection = URL(otaVersionUrl).openConnection() as HttpURLConnection
|
|
54
|
+
connection.connectTimeout = 15_000
|
|
55
|
+
connection.readTimeout = 15_000
|
|
56
|
+
connection.requestMethod = "GET"
|
|
57
|
+
return try {
|
|
58
|
+
val status = connection.responseCode
|
|
59
|
+
if (status !in 200..299) {
|
|
60
|
+
throw BluetoothException("ota_manifest_request_failed", "OTA manifest request failed with HTTP $status.")
|
|
61
|
+
}
|
|
62
|
+
JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
|
63
|
+
} finally {
|
|
64
|
+
connection.disconnect()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun hasUpdate(
|
|
69
|
+
currentBuildNumber: String,
|
|
70
|
+
currentMtkVersion: String,
|
|
71
|
+
currentBesVersion: String,
|
|
72
|
+
manifest: JSONObject,
|
|
73
|
+
): Boolean =
|
|
74
|
+
hasApkUpdate(currentBuildNumber, manifest) ||
|
|
75
|
+
hasMtkUpdate(manifest.optJSONArray("mtk_patches"), currentMtkVersion) ||
|
|
76
|
+
hasBesUpdate(manifest.optJSONObject("bes_firmware"), currentBesVersion)
|
|
77
|
+
|
|
78
|
+
fun hasMtkPatches(manifest: JSONObject): Boolean =
|
|
79
|
+
(manifest.optJSONArray("mtk_patches")?.length() ?: 0) > 0
|
|
80
|
+
|
|
81
|
+
fun hasBesFirmware(manifest: JSONObject): Boolean =
|
|
82
|
+
manifest.optJSONObject("bes_firmware") != null
|
|
83
|
+
|
|
84
|
+
private fun latestAppInfo(manifest: JSONObject): JSONObject {
|
|
85
|
+
val apps = manifest.optJSONObject("apps")
|
|
86
|
+
val app = apps?.optJSONObject(ASG_CLIENT_PACKAGE)
|
|
87
|
+
if (app != null && app.hasNumber("versionCode")) {
|
|
88
|
+
return app
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (manifest.hasNumber("versionCode")) {
|
|
92
|
+
return manifest
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw BluetoothException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun hasApkUpdate(currentBuildNumber: String, manifest: JSONObject): Boolean {
|
|
99
|
+
val currentVersion =
|
|
100
|
+
currentBuildNumber.toLongOrNull()
|
|
101
|
+
?: throw BluetoothException(
|
|
102
|
+
"invalid_glasses_version",
|
|
103
|
+
"Cannot check OTA update because glasses build number is invalid.",
|
|
104
|
+
)
|
|
105
|
+
val serverVersion = latestAppInfo(manifest).requiredLong("versionCode")
|
|
106
|
+
return serverVersion > currentVersion
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun hasMtkUpdate(patches: JSONArray?, currentVersion: String): Boolean {
|
|
110
|
+
if (patches == null || patches.length() == 0) return false
|
|
111
|
+
if (currentVersion.isBlank()) return false
|
|
112
|
+
|
|
113
|
+
for (index in 0 until patches.length()) {
|
|
114
|
+
val patch = patches.optJSONObject(index) ?: continue
|
|
115
|
+
val startFirmware = patch.optString("start_firmware", "")
|
|
116
|
+
if (startFirmware == currentVersion) return true
|
|
117
|
+
val serverDate = if (startFirmware.contains("_")) startFirmware.substringAfterLast("_") else startFirmware
|
|
118
|
+
if (serverDate == currentVersion) return true
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun hasBesUpdate(besFirmware: JSONObject?, currentVersion: String): Boolean {
|
|
124
|
+
if (besFirmware == null) return false
|
|
125
|
+
val serverVersion = besFirmware.optString("version", "")
|
|
126
|
+
if (serverVersion.isBlank()) {
|
|
127
|
+
throw BluetoothException("invalid_ota_manifest", "OTA manifest bes_firmware.version is missing.")
|
|
128
|
+
}
|
|
129
|
+
if (currentVersion.isBlank()) return true
|
|
130
|
+
return compareVersions(serverVersion, currentVersion) > 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun compareVersions(version1: String, version2: String): Int {
|
|
134
|
+
if (version1.contains(".") && version2.contains(".")) {
|
|
135
|
+
val parts1 = version1.split(".")
|
|
136
|
+
val parts2 = version2.split(".")
|
|
137
|
+
val maxLength = maxOf(parts1.size, parts2.size)
|
|
138
|
+
for (index in 0 until maxLength) {
|
|
139
|
+
val value1 = parts1.getOrNull(index)?.toIntOrNull() ?: 0
|
|
140
|
+
val value2 = parts2.getOrNull(index)?.toIntOrNull() ?: 0
|
|
141
|
+
if (value1 != value2) return value1 - value2
|
|
142
|
+
}
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
return version1.compareTo(version2)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fun JSONObject.hasNumber(key: String): Boolean = !isNull(key) && opt(key) is Number
|
|
149
|
+
|
|
150
|
+
private fun JSONObject.requiredLong(key: String): Long =
|
|
151
|
+
(opt(key) as? Number)?.toLong()
|
|
152
|
+
?: throw BluetoothException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
153
|
+
}
|