@mentra/bluetooth-sdk 0.1.8 → 0.1.10
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +8 -2
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -8,6 +8,9 @@ import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
|
8
8
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
9
9
|
import java.util.Collections
|
|
10
10
|
import java.util.UUID
|
|
11
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
12
|
+
import java.util.concurrent.CountDownLatch
|
|
13
|
+
import java.util.concurrent.TimeUnit
|
|
11
14
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
12
15
|
|
|
13
16
|
class MentraBluetoothSdk private constructor(
|
|
@@ -26,6 +29,21 @@ class MentraBluetoothSdk private constructor(
|
|
|
26
29
|
private var suppressDefaultDeviceEvents = false
|
|
27
30
|
private val streamKeepAliveLock = Any()
|
|
28
31
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
32
|
+
private val pendingPhotoRequests = ConcurrentHashMap<String, PendingResponse<PhotoResponseEvent>>()
|
|
33
|
+
private val pendingVideoRecordingRequests =
|
|
34
|
+
ConcurrentHashMap<String, PendingVideoRecordingRequest>()
|
|
35
|
+
private val pendingRgbLedRequests = ConcurrentHashMap<String, PendingResponse<RgbLedControlResponseEvent>>()
|
|
36
|
+
private val pendingSettingsRequests = ConcurrentHashMap<String, PendingResponse<SettingsAckEvent>>()
|
|
37
|
+
private val pendingStreamStarts = ConcurrentHashMap<String, PendingResponse<StreamStatusEvent>>()
|
|
38
|
+
private val oneShotLock = Any()
|
|
39
|
+
private var pendingGalleryStatus: PendingResponse<GalleryStatusEvent>? = null
|
|
40
|
+
private var pendingOtaQuery: PendingResponse<OtaQueryResult>? = null
|
|
41
|
+
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>? = null
|
|
42
|
+
private var pendingStreamStop: PendingStreamStop? = null
|
|
43
|
+
private var pendingWifiScan: PendingWifiScan? = null
|
|
44
|
+
private var pendingWifiStatus: PendingWifiStatusRequest? = null
|
|
45
|
+
private var pendingHotspotStatus: PendingHotspotStatusRequest? = null
|
|
46
|
+
private var pendingVersionInfo: PendingResponse<VersionInfoResult>? = null
|
|
29
47
|
|
|
30
48
|
init {
|
|
31
49
|
listeners.add(listener)
|
|
@@ -39,6 +57,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
39
57
|
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address")
|
|
40
58
|
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
41
59
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
60
|
+
private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
|
|
61
|
+
private const val STREAM_START_TIMEOUT_MS = 30_000L
|
|
62
|
+
private const val STREAM_STOP_TIMEOUT_MS = 15_000L
|
|
42
63
|
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
43
64
|
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
44
65
|
|
|
@@ -62,8 +83,79 @@ class MentraBluetoothSdk private constructor(
|
|
|
62
83
|
var pendingAckId: String? = null,
|
|
63
84
|
var missedAckCount: Int = 0,
|
|
64
85
|
var nextTick: Runnable? = null,
|
|
86
|
+
// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a
|
|
87
|
+
// slow startup (glasses can't ACK until they reach starting/streaming) can't trip a
|
|
88
|
+
// false keep-alive timeout before the stream is ever up.
|
|
89
|
+
var armed: Boolean = false,
|
|
65
90
|
)
|
|
66
91
|
|
|
92
|
+
private data class PendingStreamStop(
|
|
93
|
+
val streamId: String?,
|
|
94
|
+
val pending: PendingResponse<StreamStatusEvent>,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
private data class PendingVideoRecordingRequest(
|
|
98
|
+
val expectedStatus: String,
|
|
99
|
+
val pending: PendingResponse<VideoRecordingStatusEvent>,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
private data class PendingWifiScan(
|
|
103
|
+
val pending: PendingResponse<List<WifiScanResult>>,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
private data class PendingWifiStatusRequest(
|
|
107
|
+
val operation: WifiStatusOperation,
|
|
108
|
+
val ssid: String,
|
|
109
|
+
val pending: PendingResponse<WifiStatusEvent>,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
private enum class WifiStatusOperation {
|
|
113
|
+
CONNECT,
|
|
114
|
+
FORGET,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private data class PendingHotspotStatusRequest(
|
|
118
|
+
val enabled: Boolean,
|
|
119
|
+
val pending: PendingResponse<HotspotStatusEvent>,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
private class PendingResponse<T>(
|
|
123
|
+
private val operation: String,
|
|
124
|
+
) {
|
|
125
|
+
private val latch = CountDownLatch(1)
|
|
126
|
+
private val completed = AtomicBoolean(false)
|
|
127
|
+
@Volatile private var result: T? = null
|
|
128
|
+
@Volatile private var error: Throwable? = null
|
|
129
|
+
|
|
130
|
+
fun resolve(value: T) {
|
|
131
|
+
if (completed.compareAndSet(false, true)) {
|
|
132
|
+
result = value
|
|
133
|
+
latch.countDown()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun reject(error: Throwable) {
|
|
138
|
+
if (completed.compareAndSet(false, true)) {
|
|
139
|
+
this.error = error
|
|
140
|
+
latch.countDown()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fun await(timeoutMs: Long = DEFAULT_REQUEST_TIMEOUT_MS): T {
|
|
145
|
+
if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
|
146
|
+
throw BluetoothException(
|
|
147
|
+
"request_timeout",
|
|
148
|
+
"$operation timed out waiting for glasses response.",
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
error?.let { throw it }
|
|
152
|
+
return result ?: throw BluetoothException(
|
|
153
|
+
"empty_response",
|
|
154
|
+
"$operation completed without a response payload.",
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
67
159
|
fun addListener(listener: MentraBluetoothSdkListener) {
|
|
68
160
|
listeners.add(listener)
|
|
69
161
|
}
|
|
@@ -313,6 +405,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
313
405
|
)
|
|
314
406
|
}
|
|
315
407
|
|
|
408
|
+
internal fun setCalendarEvents(events: List<CalendarEvent>) {
|
|
409
|
+
DeviceStore.apply(
|
|
410
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
411
|
+
"calendar_events",
|
|
412
|
+
events.map { it.toMap() },
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
316
416
|
fun setHeadUpAngle(angleDegrees: Int) {
|
|
317
417
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "head_up_angle", angleDegrees)
|
|
318
418
|
}
|
|
@@ -321,46 +421,90 @@ class MentraBluetoothSdk private constructor(
|
|
|
321
421
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "screen_disabled", disabled)
|
|
322
422
|
}
|
|
323
423
|
|
|
324
|
-
fun setGalleryModeEnabled(enabled: Boolean)
|
|
325
|
-
|
|
424
|
+
fun setGalleryModeEnabled(enabled: Boolean): SettingsAckEvent =
|
|
425
|
+
performSettingsCommand(
|
|
426
|
+
setting = "gallery_mode",
|
|
427
|
+
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "gallery_mode", enabled) },
|
|
428
|
+
send = { requestId -> deviceManager.sendGalleryMode(requestId, enabled) },
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
private fun performSettingsCommand(
|
|
432
|
+
setting: String,
|
|
433
|
+
updateStore: (SettingsAckEvent) -> Unit,
|
|
434
|
+
send: (String) -> Unit,
|
|
435
|
+
): SettingsAckEvent {
|
|
436
|
+
val requestId = "settings-$setting-${UUID.randomUUID()}"
|
|
437
|
+
val pending = PendingResponse<SettingsAckEvent>("set $setting")
|
|
438
|
+
pendingSettingsRequests[requestId] = pending
|
|
439
|
+
try {
|
|
440
|
+
send(requestId)
|
|
441
|
+
val ack = pending.await()
|
|
442
|
+
updateStore(ack)
|
|
443
|
+
return ack
|
|
444
|
+
} finally {
|
|
445
|
+
pendingSettingsRequests.remove(requestId, pending)
|
|
446
|
+
}
|
|
326
447
|
}
|
|
327
448
|
|
|
328
449
|
fun setVoiceActivityDetectionEnabled(enabled: Boolean) {
|
|
329
450
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
330
451
|
}
|
|
331
452
|
|
|
332
|
-
fun setButtonPhotoSettings(size: ButtonPhotoSize)
|
|
333
|
-
|
|
334
|
-
|
|
453
|
+
fun setButtonPhotoSettings(size: ButtonPhotoSize): SettingsAckEvent =
|
|
454
|
+
performSettingsCommand(
|
|
455
|
+
setting = "button_photo",
|
|
456
|
+
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_photo_size", size.value) },
|
|
457
|
+
send = { requestId -> deviceManager.sendButtonPhotoSettings(requestId, size.value) },
|
|
458
|
+
)
|
|
335
459
|
|
|
336
|
-
fun setButtonPhotoSettings(settings: ButtonPhotoSettings)
|
|
460
|
+
fun setButtonPhotoSettings(settings: ButtonPhotoSettings): SettingsAckEvent =
|
|
337
461
|
setButtonPhotoSettings(size = settings.size)
|
|
338
|
-
}
|
|
339
462
|
|
|
340
|
-
fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
463
|
+
fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int): SettingsAckEvent =
|
|
464
|
+
performSettingsCommand(
|
|
465
|
+
setting = "button_video_recording",
|
|
466
|
+
updateStore = { _ ->
|
|
467
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", width)
|
|
468
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", height)
|
|
469
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", fps)
|
|
470
|
+
},
|
|
471
|
+
send = { requestId ->
|
|
472
|
+
deviceManager.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
473
|
+
},
|
|
474
|
+
)
|
|
345
475
|
|
|
346
|
-
fun setButtonVideoRecordingSettings(settings: ButtonVideoRecordingSettings)
|
|
476
|
+
fun setButtonVideoRecordingSettings(settings: ButtonVideoRecordingSettings): SettingsAckEvent =
|
|
347
477
|
setButtonVideoRecordingSettings(width = settings.width, height = settings.height, fps = settings.fps)
|
|
348
|
-
}
|
|
349
478
|
|
|
350
|
-
fun setButtonCameraLed(enabled: Boolean)
|
|
351
|
-
|
|
352
|
-
|
|
479
|
+
fun setButtonCameraLed(enabled: Boolean): SettingsAckEvent =
|
|
480
|
+
performSettingsCommand(
|
|
481
|
+
setting = "button_camera_led",
|
|
482
|
+
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_camera_led", enabled) },
|
|
483
|
+
send = { requestId -> deviceManager.sendButtonCameraLedSetting(requestId, enabled) },
|
|
484
|
+
)
|
|
353
485
|
|
|
354
|
-
fun setButtonMaxRecordingTime(minutes: Int)
|
|
355
|
-
|
|
356
|
-
|
|
486
|
+
fun setButtonMaxRecordingTime(minutes: Int): SettingsAckEvent =
|
|
487
|
+
performSettingsCommand(
|
|
488
|
+
setting = "button_max_recording_time",
|
|
489
|
+
updateStore = { _ ->
|
|
490
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_max_recording_time", minutes)
|
|
491
|
+
},
|
|
492
|
+
send = { requestId -> deviceManager.sendButtonMaxRecordingTime(requestId, minutes) },
|
|
493
|
+
)
|
|
357
494
|
|
|
358
|
-
fun setCameraFov(fov: CameraFov) {
|
|
359
|
-
|
|
495
|
+
fun setCameraFov(fov: CameraFov): CameraFovResult {
|
|
496
|
+
val ack = performSettingsCommand(
|
|
497
|
+
setting = "camera_fov",
|
|
498
|
+
updateStore = { _ -> },
|
|
499
|
+
send = { requestId -> deviceManager.sendCameraFovSetting(requestId, fov.fov, fov.roiPosition.value) },
|
|
500
|
+
)
|
|
501
|
+
val result = CameraFovResult.fromAck(ack, fov)
|
|
502
|
+
DeviceStore.set(
|
|
360
503
|
ObservableStore.BLUETOOTH_CATEGORY,
|
|
361
504
|
"camera_fov",
|
|
362
|
-
mapOf("fov" to
|
|
505
|
+
mapOf("fov" to result.fov, "roi_position" to result.roiPosition.value),
|
|
363
506
|
)
|
|
507
|
+
return result
|
|
364
508
|
}
|
|
365
509
|
|
|
366
510
|
fun setMicState(
|
|
@@ -410,97 +554,353 @@ class MentraBluetoothSdk private constructor(
|
|
|
410
554
|
return GlassesMediaVolumeSetResult.fromMap(deviceManager.setGlassesMediaVolumeBlocking(level))
|
|
411
555
|
}
|
|
412
556
|
|
|
413
|
-
fun requestWifiScan() {
|
|
414
|
-
|
|
557
|
+
fun requestWifiScan(): List<WifiScanResult> {
|
|
558
|
+
val pending = PendingResponse<List<WifiScanResult>>("WiFi scan request")
|
|
559
|
+
synchronized(oneShotLock) {
|
|
560
|
+
if (pendingWifiScan != null) {
|
|
561
|
+
throw BluetoothException(
|
|
562
|
+
"request_in_flight",
|
|
563
|
+
"A WiFi scan is already waiting for a glasses response.",
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
pendingWifiScan = PendingWifiScan(pending)
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
deviceManager.requestWifiScan()
|
|
570
|
+
return pending.await()
|
|
571
|
+
} finally {
|
|
572
|
+
synchronized(oneShotLock) {
|
|
573
|
+
if (pendingWifiScan?.pending === pending) {
|
|
574
|
+
pendingWifiScan = null
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
415
578
|
}
|
|
416
579
|
|
|
417
|
-
fun sendWifiCredentials(ssid: String, password: String) {
|
|
418
|
-
|
|
580
|
+
fun sendWifiCredentials(ssid: String, password: String): WifiStatusEvent {
|
|
581
|
+
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
582
|
+
synchronized(oneShotLock) {
|
|
583
|
+
if (pendingWifiStatus != null) {
|
|
584
|
+
throw BluetoothException(
|
|
585
|
+
"request_in_flight",
|
|
586
|
+
"A WiFi status command is already waiting for a glasses response.",
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
pendingWifiStatus = PendingWifiStatusRequest(WifiStatusOperation.CONNECT, ssid, pending)
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
deviceManager.sendWifiCredentials(ssid, password)
|
|
593
|
+
return pending.await()
|
|
594
|
+
} finally {
|
|
595
|
+
synchronized(oneShotLock) {
|
|
596
|
+
if (pendingWifiStatus?.pending === pending) {
|
|
597
|
+
pendingWifiStatus = null
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
fun forgetWifiNetwork(ssid: String): WifiStatusEvent {
|
|
604
|
+
val pending = PendingResponse<WifiStatusEvent>("WiFi forget request")
|
|
605
|
+
synchronized(oneShotLock) {
|
|
606
|
+
if (pendingWifiStatus != null) {
|
|
607
|
+
throw BluetoothException(
|
|
608
|
+
"request_in_flight",
|
|
609
|
+
"A WiFi status command is already waiting for a glasses response.",
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
pendingWifiStatus = PendingWifiStatusRequest(WifiStatusOperation.FORGET, ssid, pending)
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
deviceManager.forgetWifiNetwork(ssid)
|
|
616
|
+
return pending.await()
|
|
617
|
+
} finally {
|
|
618
|
+
synchronized(oneShotLock) {
|
|
619
|
+
if (pendingWifiStatus?.pending === pending) {
|
|
620
|
+
pendingWifiStatus = null
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
419
624
|
}
|
|
420
625
|
|
|
421
|
-
fun
|
|
422
|
-
|
|
626
|
+
fun setHotspotState(enabled: Boolean): HotspotStatusEvent {
|
|
627
|
+
val pending = PendingResponse<HotspotStatusEvent>("hotspot ${if (enabled) "enable" else "disable"} request")
|
|
628
|
+
synchronized(oneShotLock) {
|
|
629
|
+
if (pendingHotspotStatus != null) {
|
|
630
|
+
throw BluetoothException(
|
|
631
|
+
"request_in_flight",
|
|
632
|
+
"A hotspot command is already waiting for a glasses response.",
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
pendingHotspotStatus = PendingHotspotStatusRequest(enabled, pending)
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
deviceManager.setHotspotState(enabled)
|
|
639
|
+
return pending.await()
|
|
640
|
+
} finally {
|
|
641
|
+
synchronized(oneShotLock) {
|
|
642
|
+
if (pendingHotspotStatus?.pending === pending) {
|
|
643
|
+
pendingHotspotStatus = null
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
423
647
|
}
|
|
424
648
|
|
|
425
|
-
fun
|
|
426
|
-
deviceManager.
|
|
649
|
+
fun setSystemTime(timestampMs: Long) {
|
|
650
|
+
deviceManager.setSystemTime(timestampMs)
|
|
427
651
|
}
|
|
428
652
|
|
|
429
|
-
fun requestPhoto(request: PhotoRequest) {
|
|
653
|
+
fun requestPhoto(request: PhotoRequest): PhotoResponseEvent {
|
|
430
654
|
Bridge.log(
|
|
431
655
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${request.requestId} appId=${request.appId}"
|
|
432
656
|
)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
657
|
+
val pending = PendingResponse<PhotoResponseEvent>("photo request ${request.requestId}")
|
|
658
|
+
pendingPhotoRequests[request.requestId] = pending
|
|
659
|
+
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
|
+
)
|
|
673
|
+
return pending.await()
|
|
674
|
+
} finally {
|
|
675
|
+
pendingPhotoRequests.remove(request.requestId, pending)
|
|
676
|
+
}
|
|
445
677
|
}
|
|
446
678
|
|
|
447
|
-
fun queryGalleryStatus() {
|
|
448
|
-
|
|
679
|
+
fun queryGalleryStatus(): GalleryStatusEvent {
|
|
680
|
+
val pending = PendingResponse<GalleryStatusEvent>("gallery status query")
|
|
681
|
+
synchronized(oneShotLock) {
|
|
682
|
+
if (pendingGalleryStatus != null) {
|
|
683
|
+
throw BluetoothException(
|
|
684
|
+
"request_in_flight",
|
|
685
|
+
"A gallery status query is already waiting for a glasses response.",
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
pendingGalleryStatus = pending
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
deviceManager.queryGalleryStatus()
|
|
692
|
+
return pending.await()
|
|
693
|
+
} finally {
|
|
694
|
+
synchronized(oneShotLock) {
|
|
695
|
+
if (pendingGalleryStatus === pending) {
|
|
696
|
+
pendingGalleryStatus = null
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
449
700
|
}
|
|
450
701
|
|
|
451
|
-
fun startStream(request: StreamRequest) {
|
|
702
|
+
fun startStream(request: StreamRequest): StreamStatusEvent {
|
|
452
703
|
val message = request.toMap().toMutableMap()
|
|
453
704
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
454
705
|
?: "sdk-${UUID.randomUUID()}"
|
|
455
706
|
message["streamId"] = streamId
|
|
707
|
+
val pending = PendingResponse<StreamStatusEvent>("start stream $streamId")
|
|
708
|
+
pendingStreamStarts[streamId] = pending
|
|
456
709
|
stopStreamKeepAliveMonitor()
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
710
|
+
try {
|
|
711
|
+
deviceManager.startStream(message)
|
|
712
|
+
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
713
|
+
if (request.keepAlive && !request.isExternallyManagedKeepAlive()) {
|
|
714
|
+
startStreamKeepAliveMonitor(streamId, request.keepAliveIntervalSeconds)
|
|
715
|
+
}
|
|
716
|
+
return event
|
|
717
|
+
} finally {
|
|
718
|
+
pendingStreamStarts.remove(streamId, pending)
|
|
460
719
|
}
|
|
461
720
|
}
|
|
462
721
|
|
|
463
|
-
internal fun
|
|
722
|
+
internal fun sendExternallyManagedStreamKeepAlive(request: StreamKeepAliveRequest) {
|
|
464
723
|
deviceManager.keepStreamAlive(request.toMap().toMutableMap())
|
|
465
724
|
}
|
|
466
725
|
|
|
467
|
-
fun rgbLedControl(request: RgbLedRequest) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
726
|
+
fun rgbLedControl(request: RgbLedRequest): RgbLedControlResponseEvent {
|
|
727
|
+
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}")
|
|
728
|
+
pendingRgbLedRequests[request.requestId] = pending
|
|
729
|
+
try {
|
|
730
|
+
deviceManager.rgbLedControl(
|
|
731
|
+
request.requestId,
|
|
732
|
+
request.packageName,
|
|
733
|
+
request.action.value,
|
|
734
|
+
request.color?.value,
|
|
735
|
+
request.onDurationMs,
|
|
736
|
+
request.offDurationMs,
|
|
737
|
+
request.count,
|
|
738
|
+
)
|
|
739
|
+
return pending.await()
|
|
740
|
+
} finally {
|
|
741
|
+
pendingRgbLedRequests.remove(request.requestId, pending)
|
|
742
|
+
}
|
|
477
743
|
}
|
|
478
744
|
|
|
479
|
-
fun stopStream() {
|
|
745
|
+
fun stopStream(): StreamStatusEvent {
|
|
746
|
+
val pending = PendingResponse<StreamStatusEvent>("stop stream")
|
|
747
|
+
val targetStreamId = synchronized(streamKeepAliveLock) {
|
|
748
|
+
activeStreamKeepAlive?.streamId
|
|
749
|
+
}
|
|
750
|
+
synchronized(oneShotLock) {
|
|
751
|
+
if (pendingStreamStop != null) {
|
|
752
|
+
throw BluetoothException(
|
|
753
|
+
"request_in_flight",
|
|
754
|
+
"A stream stop command is already waiting for a glasses response.",
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
pendingStreamStop = PendingStreamStop(targetStreamId, pending)
|
|
758
|
+
}
|
|
480
759
|
stopStreamKeepAliveMonitor()
|
|
481
|
-
|
|
760
|
+
try {
|
|
761
|
+
deviceManager.stopStream()
|
|
762
|
+
return pending.await(STREAM_STOP_TIMEOUT_MS)
|
|
763
|
+
} finally {
|
|
764
|
+
synchronized(oneShotLock) {
|
|
765
|
+
if (pendingStreamStop?.pending === pending) {
|
|
766
|
+
pendingStreamStop = null
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
482
770
|
}
|
|
483
771
|
|
|
484
|
-
fun startVideoRecording(request: VideoRecordingRequest) {
|
|
485
|
-
|
|
772
|
+
fun startVideoRecording(request: VideoRecordingRequest): VideoRecordingStatusEvent {
|
|
773
|
+
require(request.requestId.isNotBlank()) { "requestId is required to start video recording." }
|
|
774
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
|
|
775
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
|
|
776
|
+
if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
|
|
777
|
+
throw BluetoothException(
|
|
778
|
+
"request_in_flight",
|
|
779
|
+
"A video recording command is already waiting for requestId ${request.requestId}.",
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
deviceManager.startVideoRecording(
|
|
784
|
+
request.requestId,
|
|
785
|
+
request.save,
|
|
786
|
+
request.sound,
|
|
787
|
+
request.width,
|
|
788
|
+
request.height,
|
|
789
|
+
request.fps,
|
|
790
|
+
)
|
|
791
|
+
return pending.await()
|
|
792
|
+
} finally {
|
|
793
|
+
pendingVideoRecordingRequests.remove(request.requestId, pendingRequest)
|
|
794
|
+
}
|
|
486
795
|
}
|
|
487
796
|
|
|
488
|
-
fun stopVideoRecording(requestId: String) {
|
|
489
|
-
|
|
797
|
+
fun stopVideoRecording(requestId: String): VideoRecordingStatusEvent {
|
|
798
|
+
require(requestId.isNotBlank()) { "requestId is required to stop video recording." }
|
|
799
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("stop video recording")
|
|
800
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_stopped", pending)
|
|
801
|
+
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
802
|
+
throw BluetoothException(
|
|
803
|
+
"request_in_flight",
|
|
804
|
+
"A video recording command is already waiting for requestId $requestId.",
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
deviceManager.stopVideoRecording(requestId)
|
|
809
|
+
return pending.await()
|
|
810
|
+
} finally {
|
|
811
|
+
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
812
|
+
}
|
|
490
813
|
}
|
|
491
814
|
|
|
492
|
-
fun requestVersionInfo() {
|
|
493
|
-
|
|
815
|
+
fun requestVersionInfo(): VersionInfoResult {
|
|
816
|
+
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
817
|
+
synchronized(oneShotLock) {
|
|
818
|
+
if (pendingVersionInfo != null) {
|
|
819
|
+
throw BluetoothException(
|
|
820
|
+
"request_in_flight",
|
|
821
|
+
"A version info request is already waiting for a glasses response.",
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
pendingVersionInfo = pending
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
deviceManager.requestVersionInfo()
|
|
828
|
+
return pending.await()
|
|
829
|
+
} finally {
|
|
830
|
+
synchronized(oneShotLock) {
|
|
831
|
+
if (pendingVersionInfo === pending) {
|
|
832
|
+
pendingVersionInfo = null
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
494
836
|
}
|
|
495
837
|
|
|
496
|
-
|
|
497
|
-
|
|
838
|
+
/** Ask connected Mentra Live glasses to check/report OTA availability and status. */
|
|
839
|
+
fun checkForOtaUpdate(): OtaQueryResult =
|
|
840
|
+
performOtaQuery("OTA status query") {
|
|
841
|
+
deviceManager.sendOtaQueryStatus()
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private fun performOtaQuery(
|
|
845
|
+
operation: String,
|
|
846
|
+
sendRequest: () -> Unit,
|
|
847
|
+
): OtaQueryResult {
|
|
848
|
+
val pending = PendingResponse<OtaQueryResult>(operation)
|
|
849
|
+
synchronized(oneShotLock) {
|
|
850
|
+
if (pendingOtaQuery != null) {
|
|
851
|
+
throw BluetoothException(
|
|
852
|
+
"request_in_flight",
|
|
853
|
+
"An OTA status query is already waiting for a glasses response.",
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
pendingOtaQuery = pending
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
sendRequest()
|
|
860
|
+
return pending.await()
|
|
861
|
+
} finally {
|
|
862
|
+
synchronized(oneShotLock) {
|
|
863
|
+
if (pendingOtaQuery === pending) {
|
|
864
|
+
pendingOtaQuery = null
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
498
868
|
}
|
|
499
869
|
|
|
500
|
-
|
|
501
|
-
|
|
870
|
+
/** Start the OTA flow after your app has presented the available update to the user. */
|
|
871
|
+
fun startOtaUpdate(): OtaStartAckEvent {
|
|
872
|
+
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
873
|
+
synchronized(oneShotLock) {
|
|
874
|
+
if (pendingOtaStart != null) {
|
|
875
|
+
throw BluetoothException(
|
|
876
|
+
"request_in_flight",
|
|
877
|
+
"An OTA start command is already waiting for a glasses response.",
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
pendingOtaStart = pending
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
deviceManager.sendOtaStart()
|
|
884
|
+
return pending.await()
|
|
885
|
+
} finally {
|
|
886
|
+
synchronized(oneShotLock) {
|
|
887
|
+
if (pendingOtaStart === pending) {
|
|
888
|
+
pendingOtaStart = null
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
502
892
|
}
|
|
503
893
|
|
|
894
|
+
/** Re-run the glasses-side OTA version check, mainly after correcting clock skew/TLS failures. */
|
|
895
|
+
fun retryOtaVersionCheck(): OtaQueryResult =
|
|
896
|
+
performOtaQuery("OTA version retry") {
|
|
897
|
+
deviceManager.retryOtaVersionCheck()
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
internal fun sendOtaStart(): OtaStartAckEvent = startOtaUpdate()
|
|
901
|
+
|
|
902
|
+
internal fun sendOtaQueryStatus(): OtaQueryResult = checkForOtaUpdate()
|
|
903
|
+
|
|
504
904
|
internal fun sendShutdown() {
|
|
505
905
|
deviceManager.sendShutdown()
|
|
506
906
|
}
|
|
@@ -639,7 +1039,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
639
1039
|
it.onVoiceActivityDetectionStatus(
|
|
640
1040
|
VoiceActivityDetectionStatusEvent(
|
|
641
1041
|
voiceActivityDetectionEnabled =
|
|
642
|
-
data["voiceActivityDetectionEnabled"] as? Boolean
|
|
1042
|
+
data["voiceActivityDetectionEnabled"] as? Boolean
|
|
1043
|
+
?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
643
1044
|
values = data,
|
|
644
1045
|
)
|
|
645
1046
|
)
|
|
@@ -663,14 +1064,62 @@ class MentraBluetoothSdk private constructor(
|
|
|
663
1064
|
)
|
|
664
1065
|
)
|
|
665
1066
|
}
|
|
666
|
-
"wifi_status_change" ->
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1067
|
+
"wifi_status_change" -> {
|
|
1068
|
+
val event = WifiStatusEvent(data)
|
|
1069
|
+
handleWifiStatusForRequests(event)
|
|
1070
|
+
dispatchToListeners { it.onWifiStatusChanged(event) }
|
|
1071
|
+
}
|
|
1072
|
+
"wifi_scan_result" -> {
|
|
1073
|
+
val networks =
|
|
1074
|
+
(data["networks"] as? List<*>)
|
|
1075
|
+
?.mapNotNull { (it as? Map<*, *>)?.stringKeyedMap() }
|
|
1076
|
+
?.map(WifiScanResult::fromMap)
|
|
1077
|
+
?: emptyList()
|
|
1078
|
+
val hasCompletionFlag =
|
|
1079
|
+
data.containsKey("scanComplete") || data.containsKey("scan_complete")
|
|
1080
|
+
val scanComplete =
|
|
1081
|
+
(data["scanComplete"] as? Boolean) ?: (data["scan_complete"] as? Boolean) ?: false
|
|
1082
|
+
if (scanComplete || !hasCompletionFlag) {
|
|
1083
|
+
handleWifiScanResultsForRequests(networks)
|
|
1084
|
+
}
|
|
1085
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1086
|
+
}
|
|
1087
|
+
"hotspot_status_change" -> {
|
|
1088
|
+
val event = HotspotStatusEvent(data)
|
|
1089
|
+
handleHotspotStatusForRequests(event)
|
|
1090
|
+
dispatchToListeners { it.onHotspotStatusChanged(event) }
|
|
1091
|
+
}
|
|
1092
|
+
"hotspot_error" -> {
|
|
1093
|
+
val event = HotspotErrorEvent(data)
|
|
1094
|
+
handleHotspotErrorForRequests(event)
|
|
1095
|
+
dispatchToListeners { it.onHotspotError(event) }
|
|
1096
|
+
}
|
|
1097
|
+
"gallery_status" -> {
|
|
1098
|
+
val event = GalleryStatusEvent(data)
|
|
1099
|
+
synchronized(oneShotLock) {
|
|
1100
|
+
pendingGalleryStatus?.resolve(event)
|
|
1101
|
+
}
|
|
1102
|
+
dispatchToListeners { it.onGalleryStatus(event) }
|
|
1103
|
+
}
|
|
1104
|
+
"photo_response" -> {
|
|
1105
|
+
val event = PhotoResponseEvent(data)
|
|
1106
|
+
handlePhotoResponseForRequests(event)
|
|
1107
|
+
dispatchToListeners { it.onPhotoResponse(event) }
|
|
1108
|
+
}
|
|
671
1109
|
"photo_status" -> dispatchToListeners { it.onPhotoStatus(PhotoStatusEvent(data)) }
|
|
1110
|
+
"video_recording_status" -> {
|
|
1111
|
+
val event = VideoRecordingStatusEvent(data)
|
|
1112
|
+
handleVideoRecordingStatusForRequests(event)
|
|
1113
|
+
dispatchToListeners { it.onVideoRecordingStatus(event) }
|
|
1114
|
+
}
|
|
1115
|
+
"rgb_led_control_response" -> {
|
|
1116
|
+
val event = RgbLedControlResponseEvent(data)
|
|
1117
|
+
handleRgbLedResponseForRequests(event)
|
|
1118
|
+
dispatchToListeners { it.onRgbLedControlResponse(event) }
|
|
1119
|
+
}
|
|
672
1120
|
"stream_status" -> {
|
|
673
1121
|
val event = StreamStatusEvent(data)
|
|
1122
|
+
handleStreamStatusForRequests(event)
|
|
674
1123
|
handleStreamStatusForKeepAlive(event.status)
|
|
675
1124
|
dispatchToListeners { it.onStreamStatus(event) }
|
|
676
1125
|
}
|
|
@@ -680,6 +1129,39 @@ class MentraBluetoothSdk private constructor(
|
|
|
680
1129
|
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
681
1130
|
}
|
|
682
1131
|
}
|
|
1132
|
+
"ota_update_available" -> {
|
|
1133
|
+
val resultValues = data + mapOf("type" to "ota_update_available")
|
|
1134
|
+
synchronized(oneShotLock) {
|
|
1135
|
+
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1136
|
+
}
|
|
1137
|
+
dispatchToListeners { it.onOtaUpdateAvailable(OtaUpdateAvailableEvent.fromMap(resultValues)) }
|
|
1138
|
+
}
|
|
1139
|
+
"ota_start_ack" -> {
|
|
1140
|
+
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
1141
|
+
synchronized(oneShotLock) {
|
|
1142
|
+
pendingOtaStart?.resolve(event)
|
|
1143
|
+
}
|
|
1144
|
+
dispatchToListeners { it.onOtaStartAck(event) }
|
|
1145
|
+
}
|
|
1146
|
+
"ota_status" -> {
|
|
1147
|
+
val resultValues = data + mapOf("type" to "ota_status")
|
|
1148
|
+
synchronized(oneShotLock) {
|
|
1149
|
+
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1150
|
+
}
|
|
1151
|
+
dispatchToListeners { it.onOtaStatus(OtaStatusEvent.fromMap(resultValues)) }
|
|
1152
|
+
}
|
|
1153
|
+
"settings_ack" -> {
|
|
1154
|
+
val event = SettingsAckEvent(data)
|
|
1155
|
+
handleSettingsAckForRequests(event)
|
|
1156
|
+
dispatchToListeners { it.onSettingsAck(event) }
|
|
1157
|
+
}
|
|
1158
|
+
"version_info" -> {
|
|
1159
|
+
val event = VersionInfoResult.fromMap(data)
|
|
1160
|
+
synchronized(oneShotLock) {
|
|
1161
|
+
pendingVersionInfo?.resolve(event)
|
|
1162
|
+
}
|
|
1163
|
+
dispatchToListeners { it.onVersionInfo(event) }
|
|
1164
|
+
}
|
|
683
1165
|
"mic_pcm" -> {
|
|
684
1166
|
val event = MicPcmEvent(data)
|
|
685
1167
|
if (event.pcm.isNotEmpty()) {
|
|
@@ -758,6 +1240,212 @@ class MentraBluetoothSdk private constructor(
|
|
|
758
1240
|
tracker?.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
759
1241
|
}
|
|
760
1242
|
|
|
1243
|
+
private fun handleStreamStatusForRequests(event: StreamStatusEvent) {
|
|
1244
|
+
val startMatch = matchingStreamStart(event)
|
|
1245
|
+
if (startMatch != null) {
|
|
1246
|
+
val (streamId, pending) = startMatch
|
|
1247
|
+
when (event.state) {
|
|
1248
|
+
StreamState.STREAMING -> {
|
|
1249
|
+
pendingStreamStarts.remove(streamId, pending)
|
|
1250
|
+
pending.resolve(event)
|
|
1251
|
+
}
|
|
1252
|
+
StreamState.ERROR,
|
|
1253
|
+
StreamState.RECONNECT_FAILED,
|
|
1254
|
+
StreamState.STOPPED -> {
|
|
1255
|
+
pendingStreamStarts.remove(streamId, pending)
|
|
1256
|
+
pending.reject(streamStatusException(event, "stream_start_failed"))
|
|
1257
|
+
}
|
|
1258
|
+
else -> Unit
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
val stop = synchronized(oneShotLock) { pendingStreamStop }
|
|
1263
|
+
if (stop != null && streamStatusMatches(event, stop.streamId)) {
|
|
1264
|
+
when {
|
|
1265
|
+
isAlreadyStoppedStreamStatus(event) -> {
|
|
1266
|
+
synchronized(oneShotLock) {
|
|
1267
|
+
if (pendingStreamStop === stop) {
|
|
1268
|
+
pendingStreamStop = null
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
stop.pending.resolve(stoppedStreamEvent(event, stop.streamId))
|
|
1272
|
+
}
|
|
1273
|
+
event.state == StreamState.ERROR || event.state == StreamState.RECONNECT_FAILED -> {
|
|
1274
|
+
synchronized(oneShotLock) {
|
|
1275
|
+
if (pendingStreamStop === stop) {
|
|
1276
|
+
pendingStreamStop = null
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
stop.pending.reject(streamStatusException(event, "stream_stop_failed"))
|
|
1280
|
+
}
|
|
1281
|
+
else -> Unit
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
private fun matchingStreamStart(event: StreamStatusEvent): Pair<String, PendingResponse<StreamStatusEvent>>? {
|
|
1287
|
+
val streamId = event.streamId
|
|
1288
|
+
if (!streamId.isNullOrBlank()) {
|
|
1289
|
+
val pending = pendingStreamStarts[streamId] ?: return null
|
|
1290
|
+
return streamId to pending
|
|
1291
|
+
}
|
|
1292
|
+
if (pendingStreamStarts.size == 1) {
|
|
1293
|
+
val entry = pendingStreamStarts.entries.first()
|
|
1294
|
+
return entry.key to entry.value
|
|
1295
|
+
}
|
|
1296
|
+
return null
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private fun streamStatusMatches(event: StreamStatusEvent, streamId: String?): Boolean =
|
|
1300
|
+
streamId.isNullOrBlank() || event.streamId.isNullOrBlank() || event.streamId == streamId
|
|
1301
|
+
|
|
1302
|
+
private fun isAlreadyStoppedStreamStatus(event: StreamStatusEvent): Boolean {
|
|
1303
|
+
if (event.state == StreamState.STOPPED) {
|
|
1304
|
+
return true
|
|
1305
|
+
}
|
|
1306
|
+
val details = (event.status as? StreamStatus.Error)?.errorDetails?.lowercase()
|
|
1307
|
+
return details in setOf("not_streaming", "already_stopped", "not streaming")
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
private fun stoppedStreamEvent(event: StreamStatusEvent, streamId: String?): StreamStatusEvent =
|
|
1311
|
+
StreamStatusEvent(
|
|
1312
|
+
StreamStatus.Lifecycle(
|
|
1313
|
+
state = StreamState.STOPPED,
|
|
1314
|
+
streamId = event.streamId ?: streamId,
|
|
1315
|
+
timestamp = event.status.timestamp ?: System.currentTimeMillis(),
|
|
1316
|
+
resolvedConfig = event.resolvedConfig,
|
|
1317
|
+
)
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
private fun streamStatusException(event: StreamStatusEvent, code: String): BluetoothException {
|
|
1321
|
+
val details = (event.status as? StreamStatus.Error)?.errorDetails
|
|
1322
|
+
?: "Stream status ${event.state.value}"
|
|
1323
|
+
return BluetoothException(code, details)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
private fun handlePhotoResponseForRequests(event: PhotoResponseEvent) {
|
|
1327
|
+
val pending = pendingPhotoRequests[event.requestId] ?: return
|
|
1328
|
+
when (val response = event.response) {
|
|
1329
|
+
is PhotoResponse.Success -> pending.resolve(event)
|
|
1330
|
+
is PhotoResponse.Error ->
|
|
1331
|
+
pending.reject(
|
|
1332
|
+
BluetoothException(
|
|
1333
|
+
response.errorCode ?: "photo_request_failed",
|
|
1334
|
+
response.errorMessage,
|
|
1335
|
+
)
|
|
1336
|
+
)
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private fun handleVideoRecordingStatusForRequests(event: VideoRecordingStatusEvent) {
|
|
1341
|
+
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
1342
|
+
if (event.success) {
|
|
1343
|
+
if (event.status == request.expectedStatus) {
|
|
1344
|
+
request.pending.resolve(event)
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
request.pending.reject(
|
|
1348
|
+
BluetoothException(
|
|
1349
|
+
event.status.ifBlank { "video_recording_failed" },
|
|
1350
|
+
event.details ?: "Video recording command failed.",
|
|
1351
|
+
)
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
private fun handleRgbLedResponseForRequests(event: RgbLedControlResponseEvent) {
|
|
1357
|
+
val pending = pendingRgbLedRequests[event.requestId] ?: return
|
|
1358
|
+
if (event.state == "success") {
|
|
1359
|
+
pending.resolve(event)
|
|
1360
|
+
} else {
|
|
1361
|
+
pending.reject(
|
|
1362
|
+
BluetoothException(
|
|
1363
|
+
event.errorCode ?: "rgb_led_control_failed",
|
|
1364
|
+
event.errorCode ?: "RGB LED command failed.",
|
|
1365
|
+
)
|
|
1366
|
+
)
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private fun handleSettingsAckForRequests(event: SettingsAckEvent) {
|
|
1371
|
+
val pending = pendingSettingsRequests[event.requestId] ?: return
|
|
1372
|
+
if (isFailureStatus(event.status)) {
|
|
1373
|
+
pending.reject(
|
|
1374
|
+
BluetoothException(
|
|
1375
|
+
event.errorCode ?: "${event.setting.ifBlank { "settings" }}_failed",
|
|
1376
|
+
event.errorMessage ?: "Settings command ${event.setting.ifBlank { event.requestId }} failed.",
|
|
1377
|
+
)
|
|
1378
|
+
)
|
|
1379
|
+
} else {
|
|
1380
|
+
pending.resolve(event)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
private fun isFailureStatus(status: String): Boolean =
|
|
1385
|
+
status.lowercase() in setOf("error", "failed", "failure", "rejected")
|
|
1386
|
+
|
|
1387
|
+
private fun handleWifiScanResultsForRequests(results: List<WifiScanResult>) {
|
|
1388
|
+
val request = synchronized(oneShotLock) { pendingWifiScan } ?: return
|
|
1389
|
+
synchronized(oneShotLock) {
|
|
1390
|
+
if (pendingWifiScan === request) {
|
|
1391
|
+
pendingWifiScan = null
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
request.pending.resolve(results)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
1398
|
+
val request = synchronized(oneShotLock) { pendingWifiStatus } ?: return
|
|
1399
|
+
if (!wifiStatusMatches(event.status, request)) return
|
|
1400
|
+
synchronized(oneShotLock) {
|
|
1401
|
+
if (pendingWifiStatus === request) {
|
|
1402
|
+
pendingWifiStatus = null
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
request.pending.resolve(event)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private fun wifiStatusMatches(status: WifiStatus, request: PendingWifiStatusRequest): Boolean =
|
|
1409
|
+
when (request.operation) {
|
|
1410
|
+
WifiStatusOperation.CONNECT ->
|
|
1411
|
+
status is WifiStatus.Connected && status.ssid == request.ssid
|
|
1412
|
+
WifiStatusOperation.FORGET ->
|
|
1413
|
+
status == WifiStatus.Disconnected || (status is WifiStatus.Connected && status.ssid != request.ssid)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private fun handleHotspotStatusForRequests(event: HotspotStatusEvent) {
|
|
1417
|
+
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
1418
|
+
if (!hotspotStatusMatches(event.status, request.enabled)) return
|
|
1419
|
+
synchronized(oneShotLock) {
|
|
1420
|
+
if (pendingHotspotStatus === request) {
|
|
1421
|
+
pendingHotspotStatus = null
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
request.pending.resolve(event)
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private fun hotspotStatusMatches(status: HotspotStatus, enabled: Boolean): Boolean =
|
|
1428
|
+
if (enabled) {
|
|
1429
|
+
status is HotspotStatus.Enabled
|
|
1430
|
+
} else {
|
|
1431
|
+
status == HotspotStatus.Disabled
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
private fun handleHotspotErrorForRequests(event: HotspotErrorEvent) {
|
|
1435
|
+
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
1436
|
+
synchronized(oneShotLock) {
|
|
1437
|
+
if (pendingHotspotStatus === request) {
|
|
1438
|
+
pendingHotspotStatus = null
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
request.pending.reject(
|
|
1442
|
+
BluetoothException(
|
|
1443
|
+
"hotspot_command_failed",
|
|
1444
|
+
event.message ?: "Hotspot command failed.",
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
761
1449
|
private fun sendNextStreamKeepAlive(tracker: ActiveStreamKeepAlive) {
|
|
762
1450
|
var timeoutEvent: StreamStatusEvent? = null
|
|
763
1451
|
var request: StreamKeepAliveRequest? = null
|
|
@@ -767,7 +1455,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
767
1455
|
return
|
|
768
1456
|
}
|
|
769
1457
|
|
|
770
|
-
if (tracker.pendingAckId != null) {
|
|
1458
|
+
if (tracker.armed && tracker.pendingAckId != null) {
|
|
771
1459
|
tracker.missedAckCount += 1
|
|
772
1460
|
if (tracker.missedAckCount >= MAX_MISSED_STREAM_KEEP_ALIVE_ACKS) {
|
|
773
1461
|
activeStreamKeepAlive = null
|
|
@@ -796,6 +1484,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
796
1484
|
|
|
797
1485
|
timeoutEvent?.let { event ->
|
|
798
1486
|
dispatchToListeners { it.onStreamStatus(event) }
|
|
1487
|
+
stopStreamKeepAliveMonitor()
|
|
1488
|
+
deviceManager.stopStream()
|
|
799
1489
|
return
|
|
800
1490
|
}
|
|
801
1491
|
|
|
@@ -818,10 +1508,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
818
1508
|
|
|
819
1509
|
private fun handleStreamStatusForKeepAlive(status: StreamStatus) {
|
|
820
1510
|
val streamId = status.streamId
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId } != streamId
|
|
824
|
-
) {
|
|
1511
|
+
val activeStreamId = synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId }
|
|
1512
|
+
if (streamId == null || activeStreamId != streamId) {
|
|
825
1513
|
return
|
|
826
1514
|
}
|
|
827
1515
|
when (status.state) {
|
|
@@ -829,7 +1517,21 @@ class MentraBluetoothSdk private constructor(
|
|
|
829
1517
|
StreamState.STOPPING,
|
|
830
1518
|
StreamState.ERROR,
|
|
831
1519
|
StreamState.RECONNECT_FAILED -> stopStreamKeepAliveMonitor()
|
|
832
|
-
|
|
1520
|
+
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
1521
|
+
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
1522
|
+
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
1523
|
+
// pre-arm bookkeeping so a stale unacked id (sent before the glasses could ACK)
|
|
1524
|
+
// can't immediately count as a miss.
|
|
1525
|
+
else ->
|
|
1526
|
+
synchronized(streamKeepAliveLock) {
|
|
1527
|
+
activeStreamKeepAlive?.let {
|
|
1528
|
+
if (it.streamId == streamId && !it.armed) {
|
|
1529
|
+
it.armed = true
|
|
1530
|
+
it.pendingAckId = null
|
|
1531
|
+
it.missedAckCount = 0
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
833
1535
|
}
|
|
834
1536
|
}
|
|
835
1537
|
}
|