@mentra/bluetooth-sdk 0.1.5 → 0.1.7

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.
Files changed (92) hide show
  1. package/README.md +45 -27
  2. package/android/build.gradle +2 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +53 -36
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +22 -9
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -36
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +9 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +68 -56
  8. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +125 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +209 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -3
  12. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
  13. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +72 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +132 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +39 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +2 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +168 -10
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -1
  23. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +607 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +201 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +173 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +348 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
  28. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +75 -0
  29. package/build/BluetoothSdk.types.d.ts +46 -20
  30. package/build/BluetoothSdk.types.d.ts.map +1 -1
  31. package/build/BluetoothSdk.types.js +1 -0
  32. package/build/BluetoothSdk.types.js.map +1 -1
  33. package/build/_private/BluetoothSdkModule.d.ts +6 -5
  34. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  35. package/build/_private/BluetoothSdkModule.js +40 -5
  36. package/build/_private/BluetoothSdkModule.js.map +1 -1
  37. package/build/index.d.ts +1 -1
  38. package/build/index.d.ts.map +1 -1
  39. package/build/index.js +4 -2
  40. package/build/index.js.map +1 -1
  41. package/build/react/useMentraBluetooth.d.ts +5 -3
  42. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  43. package/build/react/useMentraBluetooth.js +15 -4
  44. package/build/react/useMentraBluetooth.js.map +1 -1
  45. package/ios/BluetoothSdkModule.swift +45 -28
  46. package/ios/MentraBluetoothSDK.podspec +0 -3
  47. package/ios/Source/Audio/AudioModels.swift +159 -0
  48. package/ios/Source/Bridge.swift +22 -11
  49. package/ios/Source/Camera/CameraModels.swift +248 -0
  50. package/ios/Source/Connection/ScanSession.swift +27 -0
  51. package/ios/Source/DeviceManager.swift +20 -97
  52. package/ios/Source/DeviceStore.swift +13 -4
  53. package/ios/Source/Errors/BluetoothError.swift +19 -0
  54. package/ios/Source/Events/BluetoothEvents.swift +149 -0
  55. package/ios/Source/Internal/BluetoothAvailability.swift +58 -0
  56. package/ios/Source/Internal/ValueParsing.swift +90 -0
  57. package/ios/Source/MentraBluetoothSDK.swift +50 -2150
  58. package/ios/Source/Requests/DisplayRequests.swift +58 -0
  59. package/ios/Source/Status/DeviceStatus.swift +471 -0
  60. package/ios/Source/Status/RuntimeState.swift +359 -0
  61. package/ios/Source/Status/WifiHotspotStatus.swift +326 -0
  62. package/ios/Source/Streaming/StreamModels.swift +436 -0
  63. package/ios/Source/Types/DeviceModels.swift +134 -0
  64. package/ios/Source/controllers/ControllerManager.swift +3 -3
  65. package/ios/Source/controllers/R1.swift +1 -1
  66. package/ios/Source/sgcs/G1.swift +1 -1
  67. package/ios/Source/sgcs/G2.swift +99 -39
  68. package/ios/Source/sgcs/Mach1.swift +1 -1
  69. package/ios/Source/sgcs/MentraLive.swift +109 -12
  70. package/ios/Source/sgcs/MentraNex.swift +1 -1
  71. package/ios/Source/sgcs/SGCManager.swift +11 -3
  72. package/ios/Source/sgcs/Simulated.swift +2 -2
  73. package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
  74. package/package.json +1 -1
  75. package/src/BluetoothSdk.types.ts +48 -28
  76. package/src/_private/BluetoothSdkModule.ts +47 -21
  77. package/src/index.ts +6 -4
  78. package/src/react/useMentraBluetooth.ts +19 -7
  79. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +0 -1810
  80. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
  81. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
  82. package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
  83. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
  84. package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
  85. package/ios/Packages/VAD/Data/Configuration.swift +0 -68
  86. package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
  87. package/ios/Packages/VAD/Data/Record.swift +0 -13
  88. package/ios/Packages/VAD/Data/Result.swift +0 -22
  89. package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
  90. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  91. package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
  92. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +0 -64
@@ -4,10 +4,10 @@ import android.bluetooth.BluetoothManager
4
4
  import android.content.Context
5
5
  import android.os.Handler
6
6
  import android.os.Looper
7
- import com.mentra.bluetoothsdk.utils.ConnTypes
8
7
  import com.mentra.bluetoothsdk.utils.ControllerTypes
9
8
  import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
10
9
  import java.util.Collections
10
+ import java.util.concurrent.atomic.AtomicBoolean
11
11
 
12
12
  class MentraBluetoothSdk private constructor(
13
13
  context: Context,
@@ -34,6 +34,7 @@ class MentraBluetoothSdk private constructor(
34
34
 
35
35
  companion object {
36
36
  private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address")
37
+ private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
37
38
  private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
38
39
 
39
40
  @JvmStatic
@@ -58,10 +59,22 @@ class MentraBluetoothSdk private constructor(
58
59
  listeners.remove(listener)
59
60
  }
60
61
 
61
- fun getGlassesStatus(): GlassesStatus =
62
+ fun getState(): MentraBluetoothState =
63
+ MentraBluetoothState.from(getRawGlassesStatus(), getRawBluetoothStatus())
64
+
65
+ fun getGlasses(): GlassesRuntimeState =
66
+ getState().glasses
67
+
68
+ fun getSdkState(): PhoneSdkRuntimeState =
69
+ getState().sdk
70
+
71
+ fun getScanState(): BluetoothScanState =
72
+ getState().scan
73
+
74
+ internal fun getRawGlassesStatus(): GlassesStatus =
62
75
  GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses"))
63
76
 
64
- fun getBluetoothStatus(): BluetoothStatus =
77
+ internal fun getRawBluetoothStatus(): BluetoothStatus =
65
78
  BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
66
79
 
67
80
  fun getDefaultDevice(): Device? = currentDefaultDevice()
@@ -144,7 +157,7 @@ class MentraBluetoothSdk private constructor(
144
157
  val latestResults = mutableListOf<Device>()
145
158
  lateinit var timeoutRunnable: Runnable
146
159
  lateinit var session: ScanSession
147
- var finished = false
160
+ val finished = AtomicBoolean(false)
148
161
 
149
162
  fun emitResults(devices: List<Device>) {
150
163
  latestResults.clear()
@@ -154,16 +167,13 @@ class MentraBluetoothSdk private constructor(
154
167
 
155
168
  val scanListener =
156
169
  object : MentraBluetoothSdkCallback() {
157
- override fun onBluetoothStatusChanged(status: BluetoothStatusUpdate) {
158
- status.searchResults?.let { results ->
159
- emitResults(results.filter { it.model == model })
160
- }
170
+ override fun onScanChanged(scan: BluetoothScanState) {
171
+ emitResults(scan.devices.filter { it.model == model })
161
172
  }
162
- }
173
+ }
163
174
 
164
175
  fun finish(reason: ScanStopReason) {
165
- if (finished) return
166
- finished = true
176
+ if (!finished.compareAndSet(false, true)) return
167
177
  removeListener(scanListener)
168
178
  mainHandler.removeCallbacks(timeoutRunnable)
169
179
  session.markStopped()
@@ -178,7 +188,7 @@ class MentraBluetoothSdk private constructor(
178
188
  try {
179
189
  emitResults(emptyList())
180
190
  startScan(model)
181
- emitResults(getBluetoothStatus().searchResults.filter { it.model == model })
191
+ emitResults(getRawBluetoothStatus().searchResults.filter { it.model == model })
182
192
  mainHandler.postDelayed(timeoutRunnable, normalizedTimeoutMs)
183
193
  return session
184
194
  } catch (error: Throwable) {
@@ -298,8 +308,12 @@ class MentraBluetoothSdk private constructor(
298
308
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "screen_disabled", disabled)
299
309
  }
300
310
 
301
- fun setGalleryMode(mode: GalleryMode) {
302
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "galleryModeAuto", mode == GalleryMode.AUTO)
311
+ fun setGalleryModeEnabled(enabled: Boolean) {
312
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "gallery_mode", enabled)
313
+ }
314
+
315
+ fun setVoiceActivityDetectionEnabled(enabled: Boolean) {
316
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
303
317
  }
304
318
 
305
319
  fun setButtonPhotoSettings(size: ButtonPhotoSize) {
@@ -310,14 +324,14 @@ class MentraBluetoothSdk private constructor(
310
324
  setButtonPhotoSettings(size = settings.size)
311
325
  }
312
326
 
313
- fun setButtonVideoRecordingSettings(width: Int, height: Int, frameRate: Int) {
327
+ fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int) {
314
328
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", width)
315
329
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", height)
316
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", frameRate)
330
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", fps)
317
331
  }
318
332
 
319
333
  fun setButtonVideoRecordingSettings(settings: ButtonVideoRecordingSettings) {
320
- setButtonVideoRecordingSettings(width = settings.width, height = settings.height, frameRate = settings.frameRate)
334
+ setButtonVideoRecordingSettings(width = settings.width, height = settings.height, fps = settings.fps)
321
335
  }
322
336
 
323
337
  fun setButtonCameraLed(enabled: Boolean) {
@@ -332,14 +346,13 @@ class MentraBluetoothSdk private constructor(
332
346
  DeviceStore.apply(
333
347
  ObservableStore.BLUETOOTH_CATEGORY,
334
348
  "camera_fov",
335
- mapOf("fov" to fov.fov, "roiPosition" to fov.roiPosition),
349
+ mapOf("fov" to fov.fov, "roi_position" to fov.roiPosition),
336
350
  )
337
351
  }
338
352
 
339
353
  fun setMicState(
340
354
  enabled: Boolean,
341
355
  useGlassesMic: Boolean = true,
342
- bypassVad: Boolean = true,
343
356
  sendTranscript: Boolean = false,
344
357
  sendLc3Data: Boolean = false,
345
358
  ) {
@@ -353,7 +366,6 @@ class MentraBluetoothSdk private constructor(
353
366
  applyMicState(
354
367
  sendPcmData = enabled,
355
368
  sendTranscript = enabled && sendTranscript,
356
- bypassVad = bypassVad,
357
369
  sendLc3Data = enabled && sendLc3Data,
358
370
  )
359
371
  }
@@ -361,13 +373,11 @@ class MentraBluetoothSdk private constructor(
361
373
  private fun applyMicState(
362
374
  sendPcmData: Boolean,
363
375
  sendTranscript: Boolean,
364
- bypassVad: Boolean,
365
376
  sendLc3Data: Boolean,
366
377
  ) {
367
378
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_pcm", sendPcmData)
368
379
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_lc3", sendLc3Data)
369
380
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_transcript", sendTranscript)
370
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "bypass_vad", bypassVad)
371
381
  deviceManager.setMicState()
372
382
  }
373
383
 
@@ -404,6 +414,9 @@ class MentraBluetoothSdk private constructor(
404
414
  }
405
415
 
406
416
  fun requestPhoto(request: PhotoRequest) {
417
+ Bridge.log(
418
+ "NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${request.requestId} appId=${request.appId}"
419
+ )
407
420
  deviceManager.requestPhoto(
408
421
  request.requestId,
409
422
  request.appId,
@@ -411,7 +424,9 @@ class MentraBluetoothSdk private constructor(
411
424
  request.webhookUrl,
412
425
  request.authToken,
413
426
  request.compress.value,
427
+ request.flash,
414
428
  request.sound,
429
+ request.exposureTimeNs,
415
430
  )
416
431
  }
417
432
 
@@ -483,13 +498,22 @@ class MentraBluetoothSdk private constructor(
483
498
 
484
499
  private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
485
500
  when (ObservableStore.normalizeCategory(category)) {
486
- "glasses" ->
501
+ "glasses" -> {
502
+ val state = getState()
487
503
  dispatchToListeners {
488
- it.onGlassesStatusChanged(GlassesStatusUpdate.fromMap(glassesStatusChanges(changes)))
504
+ it.onStateChanged(state)
505
+ it.onGlassesChanged(state.glasses)
489
506
  }
507
+ }
490
508
  ObservableStore.BLUETOOTH_CATEGORY -> {
509
+ val state = getState()
510
+ val scanChanged = changes.keys.any { it in SCAN_STATE_KEYS }
491
511
  dispatchToListeners {
492
- it.onBluetoothStatusChanged(BluetoothStatusUpdate.fromMap(changes))
512
+ it.onStateChanged(state)
513
+ it.onSdkStateChanged(state.sdk)
514
+ if (scanChanged) {
515
+ it.onScanChanged(state.scan)
516
+ }
493
517
  }
494
518
  if (!suppressDefaultDeviceEvents && changes.keys.any { it in DEFAULT_DEVICE_KEYS }) {
495
519
  dispatchDefaultDeviceChanged()
@@ -499,37 +523,6 @@ class MentraBluetoothSdk private constructor(
499
523
  }
500
524
  }
501
525
 
502
- private fun glassesStatusChanges(changes: Map<String, Any>): Map<String, Any> {
503
- var merged = changes
504
-
505
- if (changes.keys.any { it in setOf("wifiConnected", "wifiSsid", "wifiLocalIp") }) {
506
- merged =
507
- merged +
508
- mapOf(
509
- "wifiConnected" to ((DeviceStore.get("glasses", "wifiConnected") as? Boolean) ?: false),
510
- "wifiSsid" to ((DeviceStore.get("glasses", "wifiSsid") as? String) ?: ""),
511
- "wifiLocalIp" to ((DeviceStore.get("glasses", "wifiLocalIp") as? String) ?: ""),
512
- )
513
- }
514
-
515
- if (changes.keys.any { it in setOf("connected", "fullyBooted", "connectionState") }) {
516
- merged =
517
- merged +
518
- mapOf(
519
- "connected" to ((DeviceStore.get("glasses", "connected") as? Boolean) ?: false),
520
- "fullyBooted" to ((DeviceStore.get("glasses", "fullyBooted") as? Boolean) ?: false),
521
- "connectionState" to ((DeviceStore.get("glasses", "connectionState") as? String) ?: ConnTypes.DISCONNECTED),
522
- )
523
- }
524
-
525
- if (changes.containsKey("signalStrengthUpdatedAt") && !changes.containsKey("signalStrength")) {
526
- val signalStrength = (DeviceStore.get("glasses", "signalStrength") as? Number)?.toInt() ?: -1
527
- merged = merged + ("signalStrength" to signalStrength)
528
- }
529
-
530
- return merged
531
- }
532
-
533
526
  private fun dispatchDefaultDeviceChanged() {
534
527
  val defaultDevice = currentDefaultDevice()
535
528
  dispatchToListeners { it.onDefaultDeviceChanged(defaultDevice) }
@@ -617,6 +610,25 @@ class MentraBluetoothSdk private constructor(
617
610
  }
618
611
  }
619
612
  "head_up" -> dispatchToListeners { it.onHeadUpChanged(data["up"] as? Boolean ?: false) }
613
+ "voice_activity_detection_status" ->
614
+ dispatchToListeners {
615
+ it.onVoiceActivityDetectionStatus(
616
+ VoiceActivityDetectionStatusEvent(
617
+ voiceActivityDetectionEnabled =
618
+ data["voiceActivityDetectionEnabled"] as? Boolean ?: true,
619
+ values = data,
620
+ )
621
+ )
622
+ }
623
+ "speaking_status" ->
624
+ dispatchToListeners {
625
+ it.onSpeakingStatus(
626
+ SpeakingStatusEvent(
627
+ speaking = data["speaking"] as? Boolean ?: false,
628
+ values = data,
629
+ )
630
+ )
631
+ }
620
632
  "battery_status" ->
621
633
  dispatchToListeners {
622
634
  it.onBatteryStatus(
@@ -0,0 +1,125 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ enum class MicPreference(val value: String) {
4
+ AUTO("auto"),
5
+ PHONE("phone"),
6
+ GLASSES("glasses"),
7
+ BLUETOOTH("bluetooth"),
8
+ }
9
+
10
+ data class MicPcmEvent(
11
+ val pcm: ByteArray,
12
+ val sampleRate: Int,
13
+ val bitsPerSample: Int,
14
+ val channels: Int,
15
+ val encoding: String,
16
+ val voiceActivityDetectionEnabled: Boolean,
17
+ ) {
18
+ constructor(values: Map<String, Any>) : this(
19
+ pcm = values["pcm"] as? ByteArray ?: ByteArray(0),
20
+ sampleRate = numberValue(values, "sampleRate") ?: SAMPLE_RATE,
21
+ bitsPerSample = numberValue(values, "bitsPerSample") ?: BITS_PER_SAMPLE,
22
+ channels = numberValue(values, "channels") ?: CHANNELS,
23
+ encoding = stringValue(values, "encoding") ?: ENCODING,
24
+ voiceActivityDetectionEnabled = boolValue(values, "voiceActivityDetectionEnabled") ?: true,
25
+ )
26
+
27
+ fun toMap(): Map<String, Any> =
28
+ mapOf(
29
+ "type" to "mic_pcm",
30
+ "pcm" to pcm,
31
+ "sampleRate" to sampleRate,
32
+ "bitsPerSample" to bitsPerSample,
33
+ "channels" to channels,
34
+ "encoding" to encoding,
35
+ "voiceActivityDetectionEnabled" to voiceActivityDetectionEnabled,
36
+ )
37
+
38
+ companion object {
39
+ const val SAMPLE_RATE = 16_000
40
+ const val BITS_PER_SAMPLE = 16
41
+ const val CHANNELS = 1
42
+ const val ENCODING = "pcm_s16le"
43
+ }
44
+ }
45
+
46
+ data class MicLc3Event(
47
+ val lc3: ByteArray,
48
+ val sampleRate: Int,
49
+ val channels: Int,
50
+ val encoding: String,
51
+ val frameDurationMs: Int,
52
+ val frameSizeBytes: Int,
53
+ val bitrate: Int,
54
+ val packetizedFromGlasses: Boolean,
55
+ val voiceActivityDetectionEnabled: Boolean,
56
+ ) {
57
+ constructor(values: Map<String, Any>) : this(
58
+ lc3 = values["lc3"] as? ByteArray ?: ByteArray(0),
59
+ sampleRate = numberValue(values, "sampleRate") ?: SAMPLE_RATE,
60
+ channels = numberValue(values, "channels") ?: CHANNELS,
61
+ encoding = stringValue(values, "encoding") ?: ENCODING,
62
+ frameDurationMs = numberValue(values, "frameDurationMs") ?: FRAME_DURATION_MS,
63
+ frameSizeBytes = numberValue(values, "frameSizeBytes") ?: DEFAULT_FRAME_SIZE_BYTES,
64
+ bitrate = numberValue(values, "bitrate") ?: DEFAULT_BITRATE,
65
+ packetizedFromGlasses = boolValue(values, "packetizedFromGlasses") ?: false,
66
+ voiceActivityDetectionEnabled = boolValue(values, "voiceActivityDetectionEnabled") ?: true,
67
+ )
68
+
69
+ fun toMap(): Map<String, Any> =
70
+ mapOf(
71
+ "type" to "mic_lc3",
72
+ "lc3" to lc3,
73
+ "sampleRate" to sampleRate,
74
+ "channels" to channels,
75
+ "encoding" to encoding,
76
+ "frameDurationMs" to frameDurationMs,
77
+ "frameSizeBytes" to frameSizeBytes,
78
+ "bitrate" to bitrate,
79
+ "packetizedFromGlasses" to packetizedFromGlasses,
80
+ "voiceActivityDetectionEnabled" to voiceActivityDetectionEnabled,
81
+ )
82
+
83
+ companion object {
84
+ const val SAMPLE_RATE = 16_000
85
+ const val CHANNELS = 1
86
+ const val ENCODING = "lc3"
87
+ const val FRAME_DURATION_MS = 10
88
+ const val DEFAULT_FRAME_SIZE_BYTES = 60
89
+ const val DEFAULT_BITRATE = DEFAULT_FRAME_SIZE_BYTES * 8 * (1000 / FRAME_DURATION_MS)
90
+ }
91
+ }
92
+
93
+ data class LocalTranscriptionEvent(
94
+ val text: String,
95
+ val isFinal: Boolean,
96
+ val values: Map<String, Any>,
97
+ )
98
+
99
+ data class GlassesMediaVolumeGetResult(
100
+ val level: Int?,
101
+ val statusCode: Int?,
102
+ val values: Map<String, Any>,
103
+ ) {
104
+ companion object {
105
+ fun fromMap(values: Map<String, Any>): GlassesMediaVolumeGetResult =
106
+ GlassesMediaVolumeGetResult(
107
+ level = numberValue(values, "level"),
108
+ statusCode = (values["statusCode"] as? Number)?.toInt(),
109
+ values = values,
110
+ )
111
+ }
112
+ }
113
+
114
+ data class GlassesMediaVolumeSetResult(
115
+ val statusCode: Int?,
116
+ val values: Map<String, Any>,
117
+ ) {
118
+ companion object {
119
+ fun fromMap(values: Map<String, Any>): GlassesMediaVolumeSetResult =
120
+ GlassesMediaVolumeSetResult(
121
+ statusCode = (values["statusCode"] as? Number)?.toInt(),
122
+ values = values,
123
+ )
124
+ }
125
+ }
@@ -0,0 +1,209 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ enum class PhotoSize(val value: String) {
4
+ SMALL("small"),
5
+ MEDIUM("medium"),
6
+ LARGE("large"),
7
+ FULL("full");
8
+
9
+ companion object {
10
+ @JvmStatic
11
+ fun fromValue(value: String?): PhotoSize =
12
+ values().firstOrNull { it.value == value } ?: MEDIUM
13
+ }
14
+ }
15
+
16
+ enum class ButtonPhotoSize(val value: String) {
17
+ SMALL("small"),
18
+ MEDIUM("medium"),
19
+ LARGE("large");
20
+
21
+ companion object {
22
+ @JvmStatic
23
+ fun fromValue(value: String?): ButtonPhotoSize =
24
+ values().firstOrNull { it.value == value } ?: MEDIUM
25
+ }
26
+ }
27
+
28
+ enum class PhotoCompression(val value: String) {
29
+ NONE("none"),
30
+ MEDIUM("medium"),
31
+ HEAVY("heavy");
32
+
33
+ companion object {
34
+ @JvmStatic
35
+ fun fromValue(value: String?): PhotoCompression =
36
+ values().firstOrNull { it.value == value } ?: NONE
37
+ }
38
+ }
39
+
40
+ data class ButtonPhotoSettings(
41
+ val size: ButtonPhotoSize,
42
+ )
43
+
44
+ data class ButtonVideoRecordingSettings(
45
+ val width: Int,
46
+ val height: Int,
47
+ val fps: Int,
48
+ )
49
+
50
+ enum class CameraFov(val fov: Int, val roiPosition: Int) {
51
+ STANDARD(118, 0),
52
+ WIDE(118, 0),
53
+ }
54
+
55
+ data class PhotoRequest @JvmOverloads constructor(
56
+ val requestId: String,
57
+ val appId: String,
58
+ val size: PhotoSize,
59
+ val webhookUrl: String,
60
+ val authToken: String? = null,
61
+ val compress: PhotoCompression = PhotoCompression.MEDIUM,
62
+ val flash: Boolean = true,
63
+ val sound: Boolean = true,
64
+ /** Sensor exposure time for this capture only (ns), or null for auto exposure */
65
+ val exposureTimeNs: Double? = null,
66
+ ) {
67
+ companion object {
68
+ /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
69
+ @JvmStatic
70
+ fun fromMap(values: Map<String, Any>): PhotoRequest {
71
+ val rawExp = values["exposureTimeNs"] ?: values["exposure_time_ns"]
72
+ val exposureTimeNs: Double? =
73
+ when (rawExp) {
74
+ is Number -> rawExp.toDouble().takeIf { it.isFinite() && it > 0 }
75
+ else -> null
76
+ }
77
+ return PhotoRequest(
78
+ requestId = stringValue(values, "requestId", "request_id").orEmpty(),
79
+ appId = stringValue(values, "appId", "app_id").orEmpty(),
80
+ size = PhotoSize.fromValue(stringValue(values, "size") ?: "medium"),
81
+ webhookUrl = stringValue(values, "webhookUrl", "webhook_url").orEmpty(),
82
+ authToken = stringValue(values, "authToken", "auth_token")?.takeIf { it.isNotBlank() },
83
+ compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
84
+ flash = boolValue(values, "flash") ?: true,
85
+ sound = boolValue(values, "sound") ?: true,
86
+ exposureTimeNs = exposureTimeNs,
87
+ )
88
+ }
89
+ }
90
+ }
91
+
92
+ enum class RgbLedAction(val value: String) {
93
+ ON("on"),
94
+ OFF("off");
95
+
96
+ companion object {
97
+ @JvmStatic
98
+ fun fromValue(value: String?): RgbLedAction =
99
+ values().firstOrNull { it.value == value } ?: OFF
100
+ }
101
+ }
102
+
103
+ enum class RgbLedColor(val value: String) {
104
+ RED("red"),
105
+ GREEN("green"),
106
+ BLUE("blue"),
107
+ ORANGE("orange"),
108
+ WHITE("white");
109
+
110
+ companion object {
111
+ @JvmStatic
112
+ fun fromValue(value: String?): RgbLedColor? =
113
+ values().firstOrNull { it.value == value }
114
+ }
115
+ }
116
+
117
+ data class RgbLedRequest @JvmOverloads constructor(
118
+ val requestId: String,
119
+ val packageName: String?,
120
+ val action: RgbLedAction,
121
+ val color: RgbLedColor?,
122
+ val onDurationMs: Int,
123
+ val offDurationMs: Int,
124
+ val count: Int,
125
+ )
126
+
127
+ data class VideoRecordingRequest(
128
+ val requestId: String,
129
+ val save: Boolean,
130
+ val sound: Boolean,
131
+ )
132
+
133
+ data class GalleryStatusEvent(
134
+ val values: Map<String, Any>,
135
+ )
136
+
137
+ sealed interface PhotoResponse {
138
+ val state: String
139
+ val requestId: String
140
+ val timestamp: Long
141
+
142
+ fun toMap(): Map<String, Any> =
143
+ when (this) {
144
+ is Success -> mapOf(
145
+ "state" to state,
146
+ "requestId" to requestId,
147
+ "uploadUrl" to uploadUrl,
148
+ "timestamp" to timestamp,
149
+ )
150
+
151
+ is Error -> mutableMapOf<String, Any>(
152
+ "state" to state,
153
+ "requestId" to requestId,
154
+ "timestamp" to timestamp,
155
+ "errorMessage" to errorMessage,
156
+ ).apply {
157
+ if (!errorCode.isNullOrBlank()) {
158
+ this["errorCode"] = errorCode
159
+ }
160
+ }
161
+ }
162
+
163
+ fun toEventMap(): Map<String, Any> = toMap() + mapOf("type" to "photo_response")
164
+
165
+ data class Success(
166
+ override val requestId: String,
167
+ val uploadUrl: String,
168
+ override val timestamp: Long,
169
+ ) : PhotoResponse {
170
+ override val state: String = "success"
171
+ }
172
+
173
+ data class Error(
174
+ override val requestId: String,
175
+ val errorCode: String?,
176
+ val errorMessage: String,
177
+ override val timestamp: Long,
178
+ ) : PhotoResponse {
179
+ override val state: String = "error"
180
+ }
181
+
182
+ companion object {
183
+ fun fromMap(values: Map<String, Any>): PhotoResponse {
184
+ val requestId = stringValue(values, "requestId").orEmpty()
185
+ val timestamp = longValue(values, "timestamp") ?: System.currentTimeMillis()
186
+ val state = stringValue(values, "state")?.lowercase()
187
+ return if (state == "success") {
188
+ val uploadUrl = stringValue(values, "uploadUrl").orEmpty()
189
+ Success(requestId = requestId, uploadUrl = uploadUrl, timestamp = timestamp)
190
+ } else {
191
+ Error(
192
+ requestId = requestId,
193
+ errorCode = stringValue(values, "errorCode"),
194
+ errorMessage = stringValue(values, "errorMessage") ?: "Unknown photo error",
195
+ timestamp = timestamp,
196
+ )
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ data class PhotoResponseEvent(
203
+ val response: PhotoResponse,
204
+ ) {
205
+ constructor(values: Map<String, Any>) : this(PhotoResponse.fromMap(values))
206
+
207
+ val requestId: String get() = response.requestId
208
+ val values: Map<String, Any> get() = response.toEventMap()
209
+ }
@@ -0,0 +1,78 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import com.mentra.bluetoothsdk.utils.ConnTypes
4
+ import java.util.concurrent.atomic.AtomicBoolean
5
+
6
+ enum class GlassesConnectionState(val value: String) {
7
+ DISCONNECTED(ConnTypes.DISCONNECTED),
8
+ SCANNING(ConnTypes.SCANNING),
9
+ CONNECTING(ConnTypes.CONNECTING),
10
+ BONDING(ConnTypes.BONDING),
11
+ CONNECTED(ConnTypes.CONNECTED);
12
+
13
+ val isConnected: Boolean
14
+ get() = this == CONNECTED
15
+
16
+ val isBusy: Boolean
17
+ get() = this == SCANNING || this == CONNECTING || this == BONDING
18
+
19
+ internal fun toStatusMap(
20
+ connected: Boolean,
21
+ fullyBooted: Boolean,
22
+ ): Map<String, Any> =
23
+ when {
24
+ this == CONNECTED || connected || fullyBooted ->
25
+ mapOf("state" to "connected", "fullyBooted" to fullyBooted)
26
+ this == SCANNING -> mapOf("state" to "scanning")
27
+ this == CONNECTING -> mapOf("state" to "connecting")
28
+ this == BONDING -> mapOf("state" to "bonding")
29
+ else -> mapOf("state" to "disconnected")
30
+ }
31
+
32
+ companion object {
33
+ @JvmStatic
34
+ fun fromValue(value: String?): GlassesConnectionState =
35
+ optionalFromValue(value) ?: DISCONNECTED
36
+
37
+ internal fun optionalFromValue(value: String?): GlassesConnectionState? {
38
+ val normalized = value?.trim()?.takeIf { it.isNotEmpty() } ?: return null
39
+ return values().firstOrNull { it.value.equals(normalized, ignoreCase = true) }
40
+ }
41
+ }
42
+ }
43
+
44
+ data class BluetoothError(
45
+ val code: String,
46
+ val message: String,
47
+ val cause: Throwable? = null,
48
+ )
49
+
50
+ enum class ScanStopReason {
51
+ COMPLETED,
52
+ CANCELLED,
53
+ ERROR,
54
+ }
55
+
56
+ interface ScanCallback {
57
+ fun onResults(devices: List<Device>) {}
58
+ fun onComplete(devices: List<Device>) {}
59
+ fun onError(error: BluetoothError) {}
60
+ }
61
+
62
+ abstract class MentraBluetoothScanCallback : ScanCallback
63
+
64
+ class ScanSession internal constructor(
65
+ private val stopAction: () -> Unit,
66
+ ) {
67
+ private val stopped = AtomicBoolean(false)
68
+
69
+ fun stop() {
70
+ if (stopped.compareAndSet(false, true)) {
71
+ stopAction()
72
+ }
73
+ }
74
+
75
+ internal fun markStopped() {
76
+ stopped.set(true)
77
+ }
78
+ }
@@ -22,7 +22,8 @@ abstract class ControllerManager {
22
22
  authToken: String?,
23
23
  compress: String?,
24
24
  flash: Boolean,
25
- sound: Boolean
25
+ sound: Boolean,
26
+ exposureTimeNs: Long?,
26
27
  )
27
28
  abstract fun startStream(message: Map<String, Any>)
28
29
  abstract fun stopStream()
@@ -132,8 +133,8 @@ abstract class ControllerManager {
132
133
  val micEnabled: Boolean
133
134
  get() = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
134
135
 
135
- val vadEnabled: Boolean
136
- get() = DeviceStore.get("glasses", "vadEnabled") as? Boolean ?: false
136
+ val voiceActivityDetectionEnabled: Boolean
137
+ get() = DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean ?: true
137
138
 
138
139
  val batteryLevel: Int
139
140
  get() = DeviceStore.get("glasses", "batteryLevel") as? Int ?: -1