@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.
Files changed (69) hide show
  1. package/README.md +30 -11
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +47 -12
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +75 -29
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -5
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +208 -45
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +80 -14
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -16
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +4 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3831 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -28
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +514 -561
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8674 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +5 -17
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +9 -20
  20. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  21. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
  22. package/build/BluetoothSdk.types.d.ts +48 -17
  23. package/build/BluetoothSdk.types.d.ts.map +1 -1
  24. package/build/BluetoothSdk.types.js.map +1 -1
  25. package/build/_private/BluetoothSdkModule.d.ts +9 -9
  26. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  27. package/build/_private/BluetoothSdkModule.js +0 -6
  28. package/build/_private/BluetoothSdkModule.js.map +1 -1
  29. package/build/_private/photoRequestPayload.d.ts +3 -1
  30. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  31. package/build/_private/photoRequestPayload.js +46 -2
  32. package/build/_private/photoRequestPayload.js.map +1 -1
  33. package/build/debug.d.ts +3 -0
  34. package/build/debug.d.ts.map +1 -0
  35. package/build/debug.js +8 -0
  36. package/build/debug.js.map +1 -0
  37. package/build/index.d.ts +1 -1
  38. package/build/index.d.ts.map +1 -1
  39. package/build/index.js +8 -8
  40. package/build/index.js.map +1 -1
  41. package/ios/BluetoothSdkModule.swift +44 -68
  42. package/ios/Source/BluetoothSdkDefaults.swift +23 -3
  43. package/ios/Source/DeviceManager.swift +38 -33
  44. package/ios/Source/DeviceStore.swift +5 -5
  45. package/ios/Source/MentraBluetoothSDK.swift +217 -50
  46. package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
  47. package/ios/Source/ObservableStore.swift +11 -0
  48. package/ios/Source/OtaManifest.swift +170 -0
  49. package/ios/Source/camera/CameraModels.swift +219 -13
  50. package/ios/Source/controllers/ControllerManager.swift +3 -7
  51. package/ios/Source/controllers/R1.swift +3 -7
  52. package/ios/Source/sgcs/Frame.swift +0 -2
  53. package/ios/Source/sgcs/G1.swift +7 -8
  54. package/ios/Source/sgcs/G2.swift +21 -20
  55. package/ios/Source/sgcs/Mach1.swift +7 -8
  56. package/ios/Source/sgcs/MentraLive.swift +132 -63
  57. package/ios/Source/sgcs/MentraNex.swift +7 -8
  58. package/ios/Source/sgcs/SGCManager.swift +7 -10
  59. package/ios/Source/sgcs/Simulated.swift +8 -8
  60. package/ios/Source/status/DeviceStatus.swift +1 -9
  61. package/ios/Source/streaming/StreamModels.swift +7 -32
  62. package/package.json +6 -1
  63. package/src/BluetoothSdk.types.ts +49 -17
  64. package/src/_private/BluetoothSdkModule.ts +9 -17
  65. package/src/_private/photoRequestPayload.ts +48 -3
  66. package/src/debug.ts +9 -0
  67. package/src/index.ts +19 -10
  68. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
  69. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7434
@@ -1,28 +1,46 @@
1
1
  package com.mentra.bluetoothsdk
2
2
 
3
+ import org.json.JSONObject
4
+
3
5
  enum class PhotoSize(val value: String) {
4
- SMALL("small"),
6
+ LOW("low"),
5
7
  MEDIUM("medium"),
6
- LARGE("large"),
7
- FULL("full");
8
+ HIGH("high"),
9
+ MAX("max");
8
10
 
9
11
  companion object {
10
12
  @JvmStatic
11
- fun fromValue(value: String?): PhotoSize =
12
- values().firstOrNull { it.value == value } ?: MEDIUM
13
+ fun normalizeLegacy(value: String?): String =
14
+ when (value) {
15
+ "small" -> LOW.value
16
+ "large" -> HIGH.value
17
+ "full" -> MAX.value
18
+ else -> value ?: MEDIUM.value
19
+ }
20
+
21
+ @JvmStatic
22
+ fun fromValue(value: String?): PhotoSize {
23
+ val normalized = normalizeLegacy(value)
24
+ return values().firstOrNull { it.value == normalized } ?: MEDIUM
25
+ }
13
26
  }
14
27
  }
15
28
 
16
29
  enum class ButtonPhotoSize(val value: String) {
17
- SMALL("small"),
30
+ LOW("low"),
18
31
  MEDIUM("medium"),
19
- LARGE("large"),
32
+ HIGH("high"),
20
33
  MAX("max");
21
34
 
22
35
  companion object {
23
36
  @JvmStatic
24
- fun fromValue(value: String?): ButtonPhotoSize =
25
- values().firstOrNull { it.value == value } ?: MEDIUM
37
+ fun normalizeLegacy(value: String?): String = PhotoSize.normalizeLegacy(value)
38
+
39
+ @JvmStatic
40
+ fun fromValue(value: String?): ButtonPhotoSize {
41
+ val normalized = normalizeLegacy(value)
42
+ return values().firstOrNull { it.value == normalized } ?: MEDIUM
43
+ }
26
44
  }
27
45
  }
28
46
 
@@ -38,11 +56,22 @@ enum class PhotoCompression(val value: String) {
38
56
  }
39
57
  }
40
58
 
41
- data class ButtonPhotoSettings(
42
- val size: ButtonPhotoSize,
59
+ data class PhotoCaptureDefaults(
60
+ val size: PhotoSize? = null,
61
+ val mfnr: Boolean? = null,
62
+ val zsl: Boolean? = null,
63
+ val noiseReduction: Boolean? = null,
64
+ val edgeEnhancement: Boolean? = null,
65
+ val ispDigitalGain: Int? = null,
66
+ val ispAnalogGain: String? = null,
67
+ val aeExposureDivisor: Int? = null,
68
+ val isoCap: Int? = null,
69
+ val compress: String? = null,
70
+ val sound: Boolean? = null,
71
+ val resetCaptureTuning: Boolean = false,
43
72
  )
44
73
 
45
- data class ButtonVideoRecordingSettings(
74
+ data class VideoRecordingDefaults(
46
75
  val width: Int,
47
76
  val height: Int,
48
77
  val fps: Int,
@@ -138,13 +167,21 @@ data class PhotoRequest @JvmOverloads constructor(
138
167
  val webhookUrl: String,
139
168
  val authToken: String? = null,
140
169
  val compress: PhotoCompression = PhotoCompression.MEDIUM,
141
- val flash: Boolean = true,
142
170
  val save: Boolean = false,
143
171
  val sound: Boolean = true,
144
172
  /** Sensor exposure time for this capture only (ns), or null for auto exposure */
145
173
  val exposureTimeNs: Double? = null,
146
174
  /** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
147
175
  val iso: Int? = null,
176
+ val aeExposureDivisor: Int? = null,
177
+ val isoCap: Int? = null,
178
+ val noiseReduction: Boolean? = null,
179
+ val edgeEnhancement: Boolean? = null,
180
+ val mfnr: Boolean? = null,
181
+ val zsl: Boolean? = null,
182
+ val ispDigitalGain: Int? = null,
183
+ val ispAnalogGain: String? = null,
184
+ val resetCaptureTuning: Boolean? = null,
148
185
  ) {
149
186
  companion object {
150
187
  /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
@@ -167,6 +204,12 @@ data class PhotoRequest @JvmOverloads constructor(
167
204
  }
168
205
  else -> null
169
206
  }
207
+ val aeDivisor =
208
+ numberValue(values, "aeExposureDivisor")?.takeIf { it > 1 }
209
+ val isoCap = numberValue(values, "isoCap")?.takeIf { it > 0 }
210
+ val ispDigitalGain = numberValue(values, "ispDigitalGain")
211
+ val ispAnalogGain = stringValue(values, "ispAnalogGain")
212
+
170
213
  return PhotoRequest(
171
214
  requestId = stringValue(values, "requestId", "request_id").orEmpty(),
172
215
  appId = stringValue(values, "appId", "app_id").orEmpty(),
@@ -174,13 +217,36 @@ data class PhotoRequest @JvmOverloads constructor(
174
217
  webhookUrl = stringValue(values, "webhookUrl", "webhook_url").orEmpty(),
175
218
  authToken = stringValue(values, "authToken", "auth_token")?.takeIf { it.isNotBlank() },
176
219
  compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
177
- flash = boolValue(values, "flash") ?: true,
178
220
  save = boolValue(values, "save", "saveToGallery") ?: false,
179
221
  sound = boolValue(values, "sound") ?: true,
180
222
  exposureTimeNs = exposureTimeNs,
181
223
  iso = iso,
224
+ aeExposureDivisor = aeDivisor,
225
+ isoCap = isoCap,
226
+ noiseReduction = boolValue(values, "noiseReduction"),
227
+ edgeEnhancement = boolValue(values, "edgeEnhancement"),
228
+ mfnr = boolValue(values, "mfnr"),
229
+ zsl = boolValue(values, "zsl"),
230
+ ispDigitalGain = ispDigitalGain,
231
+ ispAnalogGain = ispAnalogGain,
182
232
  )
183
233
  }
234
+
235
+ /** Adds optional scan-mode tuning fields to a `take_photo` JSON payload. */
236
+ @JvmStatic
237
+ fun appendScanFields(
238
+ json: JSONObject,
239
+ request: PhotoRequest,
240
+ ) {
241
+ request.aeExposureDivisor?.let { json.put("aeExposureDivisor", it) }
242
+ request.isoCap?.let { json.put("isoCap", it) }
243
+ request.noiseReduction?.let { json.put("noiseReduction", it) }
244
+ request.edgeEnhancement?.let { json.put("edgeEnhancement", it) }
245
+ request.mfnr?.let { json.put("mfnr", it) }
246
+ request.zsl?.let { json.put("zsl", it) }
247
+ request.ispDigitalGain?.let { json.put("ispDigitalGain", it) }
248
+ request.ispAnalogGain?.let { json.put("ispAnalogGain", it) }
249
+ }
184
250
  }
185
251
  }
186
252
 
@@ -2,6 +2,7 @@ package com.mentra.bluetoothsdk.controllers
2
2
 
3
3
  import com.mentra.bluetoothsdk.BluetoothSdkDefaults
4
4
  import com.mentra.bluetoothsdk.DeviceStore
5
+ import com.mentra.bluetoothsdk.PhotoRequest
5
6
 
6
7
  abstract class ControllerManager {
7
8
  @JvmField var type: String = ""
@@ -15,30 +16,17 @@ abstract class ControllerManager {
15
16
  abstract fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean)
16
17
 
17
18
  // Camera & Media
18
- abstract fun requestPhoto(
19
- requestId: String,
20
- appId: String,
21
- size: String?,
22
- webhookUrl: String?,
23
- authToken: String?,
24
- compress: String?,
25
- flash: Boolean,
26
- save: Boolean,
27
- sound: Boolean,
28
- exposureTimeNs: Long?,
29
- iso: Int?,
30
- )
19
+ abstract fun requestPhoto(request: PhotoRequest)
31
20
  abstract fun startStream(message: Map<String, Any>)
32
21
  abstract fun stopStream()
33
22
  abstract fun sendStreamKeepAlive(message: Map<String, Any>)
34
- abstract fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean)
23
+ abstract fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean)
35
24
  abstract fun stopVideoRecording(requestId: String)
36
25
 
37
26
  // Button Settings
38
27
  abstract fun sendButtonPhotoSettings()
39
28
  abstract fun sendButtonVideoRecordingSettings()
40
29
  abstract fun sendButtonMaxRecordingTime()
41
- abstract fun sendButtonCameraLedSetting()
42
30
 
43
31
  // Display Control
44
32
  abstract fun setBrightness(level: Int, autoMode: Boolean)
@@ -87,7 +75,7 @@ abstract class ControllerManager {
87
75
  abstract fun sendWifiCredentials(ssid: String, password: String)
88
76
  abstract fun forgetWifiNetwork(ssid: String)
89
77
  abstract fun sendHotspotState(enabled: Boolean)
90
- abstract fun sendOtaStart()
78
+ abstract fun sendOtaStart(otaVersionUrl: String? = null)
91
79
 
92
80
  // User Context (for crash reporting)
93
81
  abstract fun sendUserEmailToGlasses(email: String)
@@ -1,5 +1,6 @@
1
1
  package com.mentra.bluetoothsdk.controllers
2
2
 
3
+ import com.mentra.bluetoothsdk.PhotoRequest
3
4
  import android.Manifest
4
5
  import android.bluetooth.BluetoothAdapter
5
6
  import android.bluetooth.BluetoothDevice
@@ -861,12 +862,8 @@ class R1 : ControllerManager() {
861
862
  override fun setMicEnabled(enabled: Boolean) {}
862
863
  override fun sortMicRanking(list: MutableList<String>): MutableList<String> = list
863
864
  override fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean) {}
864
- override fun requestPhoto(
865
- requestId: String, appId: String, size: String?, webhookUrl: String?,
866
- authToken: String?, compress: String?, flash: Boolean, save: Boolean, sound: Boolean,
867
- exposureTimeNs: Long?, iso: Int?,
868
- ) {}
869
- override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
865
+ override fun requestPhoto(request: PhotoRequest) {}
866
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {}
870
867
  override fun stopVideoRecording(requestId: String) {}
871
868
  override fun startStream(message: Map<String, Any>) {}
872
869
  override fun stopStream() {}
@@ -874,7 +871,6 @@ class R1 : ControllerManager() {
874
871
  override fun sendButtonPhotoSettings() {}
875
872
  override fun sendButtonVideoRecordingSettings() {}
876
873
  override fun sendButtonMaxRecordingTime() {}
877
- override fun sendButtonCameraLedSetting() {}
878
874
  override fun setBrightness(level: Int, autoMode: Boolean) {}
879
875
  override fun clearDisplay() {}
880
876
  override fun sendTextWall(text: String) {}
@@ -901,7 +897,7 @@ class R1 : ControllerManager() {
901
897
  override fun sendWifiCredentials(ssid: String, password: String) {}
902
898
  override fun forgetWifiNetwork(ssid: String) {}
903
899
  override fun sendHotspotState(enabled: Boolean) {}
904
- override fun sendOtaStart() {}
900
+ override fun sendOtaStart(otaVersionUrl: String?) {}
905
901
  override fun sendUserEmailToGlasses(email: String) {}
906
902
  override fun queryGalleryStatus() {}
907
903
  override fun sendGalleryMode() {}
@@ -49,6 +49,7 @@ class PhoneMic private constructor(private val context: Context) {
49
49
  private const val FOCUS_REGAIN_DELAY_MS = 500L
50
50
  private const val SAMSUNG_MIC_TEST_DELAY_MS = 500L
51
51
  private const val MIC_SWITCH_DELAY_MS = 300L // Time for DeviceManager to switch mics
52
+ private const val AUDIO_SOURCE_HOTWORD = 1999
52
53
  }
53
54
 
54
55
  // Audio recording components
@@ -185,7 +186,42 @@ class PhoneMic private constructor(private val context: Context) {
185
186
  }
186
187
 
187
188
  fun isRecordingWithMode(mode: String): Boolean {
188
- return isRecording.get() && currentMicMode == mode
189
+ if (!isRecording.get()) {
190
+ return false
191
+ }
192
+
193
+ val record = audioRecord
194
+ val activelyRecording =
195
+ try {
196
+ record?.recordingState == AudioRecord.RECORDSTATE_RECORDING
197
+ } catch (e: Exception) {
198
+ Bridge.log("MIC: Failed to read AudioRecord state: ${e.message}")
199
+ false
200
+ }
201
+
202
+ if (!activelyRecording) {
203
+ Bridge.log("MIC: Stale recording state detected, cleaning up")
204
+ cleanUpRecording()
205
+ abandonAudioFocus()
206
+ currentMicMode = ""
207
+ return false
208
+ }
209
+
210
+ return currentMicMode == mode
211
+ }
212
+
213
+ fun hasBlockingMicInterruption(): Boolean {
214
+ val callActive =
215
+ isPhoneCallActive ||
216
+ try {
217
+ (telephonyManager?.callState ?: TelephonyManager.CALL_STATE_IDLE) !=
218
+ TelephonyManager.CALL_STATE_IDLE
219
+ } catch (e: Exception) {
220
+ Bridge.log("MIC: Failed to read telephony call state: ${e.message}")
221
+ true
222
+ }
223
+
224
+ return callActive || isExternalAudioActive
189
225
  }
190
226
 
191
227
  /**
@@ -544,7 +580,23 @@ class PhoneMic private constructor(private val context: Context) {
544
580
  audioRecord?.let { ourAudioSessionIds.add(it.audioSessionId) }
545
581
 
546
582
  // Start recording
547
- audioRecord?.startRecording()
583
+ val record = audioRecord ?: return false
584
+ try {
585
+ record.startRecording()
586
+ } catch (e: Exception) {
587
+ Bridge.log("MIC: AudioRecord.startRecording() failed: ${e.message}")
588
+ audioRecord?.release()
589
+ audioRecord = null
590
+ return false
591
+ }
592
+
593
+ if (record.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
594
+ Bridge.log("MIC: AudioRecord did not enter recording state")
595
+ record.release()
596
+ audioRecord = null
597
+ return false
598
+ }
599
+
548
600
  isRecording.set(true)
549
601
 
550
602
  // Start recording thread
@@ -577,15 +629,22 @@ class PhoneMic private constructor(private val context: Context) {
577
629
 
578
630
  val audioBuffer = ShortArray(bufferSize / 2)
579
631
 
632
+ var unexpectedStopReason: String? = null
633
+
580
634
  while (isRecording.get()) {
581
635
  // Capture local reference to avoid use-after-free if cleanUpRecording()
582
636
  // releases the AudioRecord while we're in read()
583
- val record = audioRecord ?: break
637
+ val record = audioRecord
638
+ if (record == null) {
639
+ unexpectedStopReason = "AudioRecord missing"
640
+ break
641
+ }
584
642
 
585
643
  val readResult = try {
586
644
  record.read(audioBuffer, 0, audioBuffer.size)
587
645
  } catch (e: Exception) {
588
646
  Bridge.log("MIC: AudioRecord.read() exception: ${e.message}")
647
+ unexpectedStopReason = "AudioRecord.read() exception: ${e.message}"
589
648
  break
590
649
  }
591
650
 
@@ -603,9 +662,16 @@ class PhoneMic private constructor(private val context: Context) {
603
662
  } else if (readResult < 0) {
604
663
  // AudioRecord error (ERROR, ERROR_INVALID_OPERATION, ERROR_BAD_VALUE, ERROR_DEAD_OBJECT)
605
664
  Bridge.log("MIC: AudioRecord.read() returned error: $readResult")
665
+ unexpectedStopReason = "AudioRecord.read() error: $readResult"
606
666
  break
607
667
  }
608
668
  }
669
+
670
+ unexpectedStopReason?.let { reason ->
671
+ if (isRecording.get()) {
672
+ mainHandler.post { handleUnexpectedRecordingStop(reason) }
673
+ }
674
+ }
609
675
  }
610
676
  .apply {
611
677
  name = "AudioRecordingThread"
@@ -613,6 +679,25 @@ class PhoneMic private constructor(private val context: Context) {
613
679
  }
614
680
  }
615
681
 
682
+ private fun handleUnexpectedRecordingStop(reason: String) {
683
+ if (!isRecording.compareAndSet(true, false)) {
684
+ return
685
+ }
686
+
687
+ Bridge.log("MIC: Recording stopped unexpectedly: $reason")
688
+ cleanUpRecording()
689
+ abandonAudioFocus()
690
+
691
+ if (audioManager.isBluetoothScoOn) {
692
+ audioManager.stopBluetoothSco()
693
+ audioManager.isBluetoothScoOn = false
694
+ }
695
+
696
+ audioManager.mode = AudioManager.MODE_NORMAL
697
+ currentMicMode = ""
698
+ notifyDeviceManager("recording_stopped", getAvailableInputDevices().values.toList())
699
+ }
700
+
616
701
  private fun cleanUpRecording() {
617
702
  isRecording.set(false)
618
703
 
@@ -778,7 +863,8 @@ class PhoneMic private constructor(private val context: Context) {
778
863
  // Filter out our own recordings
779
864
  val otherAppRecordings =
780
865
  configs.filter { config ->
781
- !ourAudioSessionIds.contains(config.clientAudioSessionId)
866
+ !ourAudioSessionIds.contains(config.clientAudioSessionId) &&
867
+ config.clientAudioSource != AUDIO_SOURCE_HOTWORD
782
868
  }
783
869
 
784
870
  val wasExternalActive = isExternalAudioActive