@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.1

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 (114) hide show
  1. package/README.md +27 -12
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +3 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +169 -26
  7. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +72 -16
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +127 -8
  9. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +400 -99
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
  13. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +18 -2
  14. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +342 -42
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -2
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  31. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  32. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +52 -2
  33. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  34. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  37. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  40. package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  41. package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  42. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +45 -0
  43. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
  45. package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  46. package/build/BluetoothSdk.types.d.ts +100 -22
  47. package/build/BluetoothSdk.types.d.ts.map +1 -1
  48. package/build/BluetoothSdk.types.js +2 -1
  49. package/build/BluetoothSdk.types.js.map +1 -1
  50. package/build/_internal.d.ts +3 -0
  51. package/build/_internal.d.ts.map +1 -1
  52. package/build/_internal.js +3 -0
  53. package/build/_internal.js.map +1 -1
  54. package/build/_private/BluetoothSdkModule.d.ts +31 -4
  55. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  56. package/build/_private/BluetoothSdkModule.js +26 -3
  57. package/build/_private/BluetoothSdkModule.js.map +1 -1
  58. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  59. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  60. package/build/_private/MentraLocalNetworkModule.js +3 -0
  61. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  62. package/build/_private/cameraRequestPayload.d.ts +1 -1
  63. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  64. package/build/_private/cameraRequestPayload.js +7 -0
  65. package/build/_private/cameraRequestPayload.js.map +1 -1
  66. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  67. package/build/_private/photoRequestPayload.js +10 -0
  68. package/build/_private/photoRequestPayload.js.map +1 -1
  69. package/build/_private/sdkOtaManifest.d.ts +20 -0
  70. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  71. package/build/_private/sdkOtaManifest.js +28 -0
  72. package/build/_private/sdkOtaManifest.js.map +1 -0
  73. package/build/index.d.ts +1 -1
  74. package/build/index.d.ts.map +1 -1
  75. package/build/index.js +11 -1
  76. package/build/index.js.map +1 -1
  77. package/expo-module.config.json +1 -0
  78. package/ios/BluetoothSdkModule.swift +113 -2
  79. package/ios/Source/BluetoothSdkDefaults.swift +2 -1
  80. package/ios/Source/Bridge.swift +87 -21
  81. package/ios/Source/DeviceManager.swift +118 -20
  82. package/ios/Source/DeviceStore.swift +4 -0
  83. package/ios/Source/MentraBluetoothSDK.swift +326 -33
  84. package/ios/Source/OtaManifest.swift +47 -4
  85. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  86. package/ios/Source/camera/CameraModels.swift +26 -6
  87. package/ios/Source/internal/BleTraceLogger.swift +1 -1
  88. package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
  89. package/ios/Source/sgcs/Ar99.swift +2451 -0
  90. package/ios/Source/sgcs/G1.swift +1 -1
  91. package/ios/Source/sgcs/G2.swift +19 -7
  92. package/ios/Source/sgcs/Mach1.swift +1 -1
  93. package/ios/Source/sgcs/MentraLive.swift +478 -61
  94. package/ios/Source/sgcs/MentraNex.swift +1 -1
  95. package/ios/Source/sgcs/Nimo.swift +1 -1
  96. package/ios/Source/sgcs/SGCManager.swift +13 -2
  97. package/ios/Source/sgcs/Simulated.swift +1 -1
  98. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  99. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  100. package/ios/Source/streaming/StreamModels.swift +68 -2
  101. package/ios/Source/tts/TTSTools.swift +1 -1
  102. package/ios/Source/types/DeviceModels.swift +13 -0
  103. package/ios/Source/utils/BleJsonCompact.swift +8 -0
  104. package/ios/Source/utils/Constants.swift +2 -0
  105. package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
  106. package/package.json +1 -2
  107. package/src/BluetoothSdk.types.ts +106 -24
  108. package/src/_internal.ts +3 -0
  109. package/src/_private/BluetoothSdkModule.ts +83 -18
  110. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  111. package/src/_private/cameraRequestPayload.ts +9 -2
  112. package/src/_private/photoRequestPayload.ts +13 -4
  113. package/src/_private/sdkOtaManifest.ts +31 -0
  114. package/src/index.ts +13 -1
@@ -250,7 +250,8 @@ public class Bridge private constructor() {
250
250
  deviceModel: String,
251
251
  deviceName: String,
252
252
  deviceAddress: String = "",
253
- rssi: Int? = null
253
+ rssi: Int? = null,
254
+ projectName: String? = null
254
255
  ) {
255
256
  val searchResults =
256
257
  (DeviceStore.store.getCategory("bluetooth")["searchResults"] as? List<*>)
@@ -262,7 +263,7 @@ public class Bridge private constructor() {
262
263
  ?.toMap()
263
264
  }
264
265
  ?: emptyList()
265
- val id = "$deviceModel:$deviceName"
266
+ val id = listOfNotNull(deviceModel, projectName?.takeIf { it.isNotBlank() }, deviceName).joinToString(":")
266
267
  val newResult =
267
268
  buildMap<String, Any> {
268
269
  put("id", id)
@@ -271,6 +272,7 @@ public class Bridge private constructor() {
271
272
  if (deviceAddress.isNotBlank()) {
272
273
  put("address", deviceAddress)
273
274
  }
275
+ projectName?.takeIf { it.isNotBlank() }?.let { put("projectName", it) }
274
276
  rssi?.let { put("rssi", it) }
275
277
  }
276
278
  // Keep the public searchResults array stable as glasses are added or removed.
@@ -520,30 +522,81 @@ public class Bridge private constructor() {
520
522
  sendTypedMessage("glasses_serial_number", body as Map<String, Any>)
521
523
  }
522
524
 
523
- /** Send WiFi status change */
525
+ /**
526
+ * Send WiFi status change. [error] is the glasses' provisioning failure reason
527
+ * (e.g. "connect_timeout", "connected_to_other_network") when this status is the
528
+ * verdict of a failed connect attempt; null for routine link-state updates.
529
+ */
524
530
  @JvmStatic
525
- fun sendWifiStatusChange(connected: Boolean, ssid: String?, localIp: String?) {
531
+ @JvmOverloads
532
+ fun sendWifiStatusChange(
533
+ connected: Boolean,
534
+ ssid: String?,
535
+ localIp: String?,
536
+ error: String? = null
537
+ ) {
526
538
  val status = WifiStatus.fromStoreFields(connected, ssid, localIp) ?: return
527
- sendTypedMessage("wifi_status_change", status.toMap())
539
+ val payload =
540
+ if (error != null) status.toMap() + mapOf("error" to error)
541
+ else status.toMap()
542
+ sendTypedMessage("wifi_status_change", payload)
543
+ }
544
+
545
+ /**
546
+ * Claim the WiFi scan-results store for a newly requested scan. Called by the
547
+ * SDK when it generates the scanId, BEFORE the scan command goes out: store
548
+ * ownership is decided at request time, not by whichever chunk arrives first,
549
+ * so a delayed chunk from an older, abandoned scan can never reset or clobber
550
+ * the current scan's accumulator.
551
+ */
552
+ @JvmStatic
553
+ fun claimWifiScanResults(scanId: String) {
554
+ DeviceStore.apply("bluetooth", "wifiScanActiveScanId", scanId)
528
555
  }
529
556
 
530
557
  /** Send WiFi scan results */
531
558
  @JvmStatic
532
- fun updateWifiScanResults(networks: List<Map<String, Any>>, scanComplete: Boolean) {
533
- var storedNetworks: List<Map<String, Any>> =
534
- DeviceStore.get("bluetooth", "wifiScanResults") as? List<Map<String, Any>>
535
- ?: emptyList()
536
- // add the networks to the storedNetworks array, removing duplicates by ssid
537
- val updatedNetworks = storedNetworks.toMutableList()
538
- for (network in networks) {
539
- if (!updatedNetworks.any { it["ssid"] as? String == network["ssid"] as? String }) {
540
- updatedNetworks.add(network)
559
+ fun updateWifiScanResults(
560
+ networks: List<Map<String, Any>>,
561
+ scanComplete: Boolean,
562
+ scanId: String? = null
563
+ ) {
564
+ // Only chunks echoing the active scanId claimed at request time may mutate
565
+ // the store; foreign chunks are still forwarded to the SDK sink, which
566
+ // drops stale ids itself. Scan-id-less chunks (old firmware) keep the
567
+ // legacy accumulate-forever store behavior.
568
+ val ownsStore =
569
+ scanId == null || scanId == DeviceStore.get("bluetooth", "wifiScanActiveScanId")
570
+ var updatedNetworks: List<Map<String, Any>> = networks
571
+ if (ownsStore) {
572
+ var storedNetworks: List<Map<String, Any>> =
573
+ DeviceStore.get("bluetooth", "wifiScanResults") as? List<Map<String, Any>>
574
+ ?: emptyList()
575
+ val lastScanId = DeviceStore.get("bluetooth", "wifiScanResultsScanId")
576
+ if (scanId != null && scanId != lastScanId) {
577
+ // First chunk of a new scan: drop networks accumulated for a previous scan
578
+ // so stale entries never carry over into this scan's store.
579
+ storedNetworks = emptyList()
580
+ DeviceStore.apply("bluetooth", "wifiScanResultsScanId", scanId)
581
+ }
582
+ // add the networks to the storedNetworks array, removing duplicates by ssid
583
+ val merged = storedNetworks.toMutableList()
584
+ for (network in networks) {
585
+ if (!merged.any { it["ssid"] as? String == network["ssid"] as? String }) {
586
+ merged.add(network)
587
+ }
541
588
  }
589
+ DeviceStore.apply("bluetooth", "wifiScanResults", merged)
590
+ updatedNetworks = merged
542
591
  }
543
- DeviceStore.apply("bluetooth", "wifiScanResults", updatedNetworks)
544
592
  val body = HashMap<String, Any>()
545
- body["networks"] = updatedNetworks
593
+ // Correlated scans: the SDK accumulates and dedupes chunks per scanId itself,
594
+ // so forward only this chunk; the merged store list is for UI consumers.
595
+ body["networks"] = if (scanId != null) networks else updatedNetworks
546
596
  body["scanComplete"] = scanComplete
597
+ if (scanId != null) {
598
+ body["scanId"] = scanId
599
+ }
547
600
  sendTypedMessage("wifi_scan_result", body)
548
601
  }
549
602
 
@@ -821,3 +874,6 @@ public class Bridge private constructor() {
821
874
  }
822
875
  }
823
876
  }
877
+
878
+
879
+
@@ -14,6 +14,7 @@ import com.mentra.bluetoothsdk.controllers.ControllerManager
14
14
  import com.mentra.bluetoothsdk.controllers.R1
15
15
  import com.mentra.bluetoothsdk.services.ForegroundService
16
16
  import com.mentra.bluetoothsdk.services.PhoneMic
17
+ import com.mentra.bluetoothsdk.sgcs.Ar99
17
18
  import com.mentra.bluetoothsdk.sgcs.G1
18
19
  import com.mentra.bluetoothsdk.sgcs.G2
19
20
  import com.mentra.bluetoothsdk.sgcs.SceneElement
@@ -28,6 +29,7 @@ import com.mentra.bluetoothsdk.utils.ControllerTypes
28
29
  import com.mentra.bluetoothsdk.utils.DeviceTypes
29
30
  import com.mentra.bluetoothsdk.utils.MicMap
30
31
  import com.mentra.bluetoothsdk.utils.MicTypes
32
+ import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
31
33
  import com.mentra.lc3Lib.Lc3Cpp
32
34
  import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
33
35
  import kotlinx.coroutines.CoroutineScope
@@ -329,6 +331,16 @@ class DeviceManager {
329
331
  return
330
332
  }
331
333
 
334
+ if (sgc?.isMicSuspendedForAudio == true) {
335
+ Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
336
+ return
337
+ }
338
+
339
+ if (PhoneAudioMonitor.getInstance(Bridge.getContext()).isOwnAppAudioPlaying()) {
340
+ Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
341
+ return
342
+ }
343
+
332
344
  // When no frame has ever been received, treat elapsed as "forever" so we
333
345
  // actually attempt recovery (was 0 before, which made the watchdog a no-op).
334
346
  val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
@@ -571,6 +583,36 @@ class DeviceManager {
571
583
  }
572
584
  }
573
585
 
586
+ /**
587
+ * Re-evaluate the active foreground-service types while the host Activity is visible.
588
+ *
589
+ * Location is a while-in-use permission, so Android 14+ only allows the service to add
590
+ * the location type from the foreground. Sending a command to the existing service
591
+ * updates its type mask without tearing down the glasses connection.
592
+ */
593
+ fun refreshForegroundServiceTypes() {
594
+ val context = Bridge.getContext()
595
+
596
+ try {
597
+ Bridge.log("MAN: Refreshing foreground service types")
598
+ val serviceIntent =
599
+ Intent(context, ForegroundService::class.java).apply {
600
+ action = ForegroundService.ACTION_REFRESH_TYPES
601
+ }
602
+
603
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
604
+ context.startForegroundService(serviceIntent)
605
+ } else {
606
+ context.startService(serviceIntent)
607
+ }
608
+
609
+ serviceStarted = true
610
+ Bridge.log("MAN: Foreground service types refreshed")
611
+ } catch (e: Exception) {
612
+ Bridge.log("MAN: Failed to refresh foreground service types: ${e.message}")
613
+ }
614
+ }
615
+
574
616
  private fun restartForegroundService() {
575
617
  val context = Bridge.getContext()
576
618
 
@@ -1117,6 +1159,8 @@ class DeviceManager {
1117
1159
  sgc = MentraLive()
1118
1160
  } else if (wearable.contains(DeviceTypes.NEX)) {
1119
1161
  sgc = MentraNex()
1162
+ } else if (wearable.contains(DeviceTypes.AR99)) {
1163
+ sgc = Ar99()
1120
1164
  } else if (wearable.contains(DeviceTypes.MACH1)) {
1121
1165
  sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
1122
1166
  } else if (wearable.contains(DeviceTypes.Z100)) {
@@ -1194,6 +1238,10 @@ class DeviceManager {
1194
1238
  }
1195
1239
  )
1196
1240
  nextTranscriber.initialize()
1241
+ if (!nextTranscriber.isInitialized()) {
1242
+ Bridge.log("SherpaOnnxTranscriber initialize() returned without becoming ready")
1243
+ return false
1244
+ }
1197
1245
  transcriber = nextTranscriber
1198
1246
  Bridge.log("SherpaOnnxTranscriber fully initialized")
1199
1247
  true
@@ -1280,6 +1328,10 @@ class DeviceManager {
1280
1328
  Bridge.saveSetting("default_wearable", defaultWearable)
1281
1329
  Bridge.saveSetting("device_name", deviceName)
1282
1330
  Bridge.saveSetting("device_address", deviceAddress)
1331
+ if (defaultWearable.contains(DeviceTypes.AR99)) {
1332
+ val projectName = (DeviceStore.store.get("bluetooth", "project_name") as? String)?.trim().orEmpty()
1333
+ Bridge.saveSetting("project_name", projectName)
1334
+ }
1283
1335
  }
1284
1336
 
1285
1337
  private fun syncSystemTimeOnceForConnection(connectionKey: String) {
@@ -1556,10 +1608,10 @@ class DeviceManager {
1556
1608
  sgc?.sendStreamKeepAlive(message)
1557
1609
  }
1558
1610
 
1559
- fun requestWifiScan() {
1611
+ fun requestWifiScan(scanId: String? = null) {
1560
1612
  Bridge.log("MAN: Requesting wifi scan")
1561
1613
  DeviceStore.apply("bluetooth", "wifiScanResults", emptyList<Any>())
1562
- sgc?.requestWifiScan()
1614
+ sgc?.requestWifiScan(scanId)
1563
1615
  }
1564
1616
 
1565
1617
  fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
@@ -1606,6 +1658,20 @@ class DeviceManager {
1606
1658
  (sgc as? MentraLive)?.sendOtaQueryStatus()
1607
1659
  }
1608
1660
 
1661
+ fun startAr99OtaFromFile(path: String): Boolean {
1662
+ val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
1663
+ return ar99.startOtaFromFile(path)
1664
+ }
1665
+
1666
+ fun cancelAr99Ota() {
1667
+ (sgc as? Ar99)?.cancelAr99Ota()
1668
+ }
1669
+
1670
+ fun sendAr99FactoryReset() {
1671
+ val ar99 = sgc as? Ar99 ?: throw IllegalStateException("unsupported_device")
1672
+ ar99.sendFactoryReset()
1673
+ }
1674
+
1609
1675
  fun sendGalleryMode(requestId: String, enabled: Boolean) {
1610
1676
  val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1611
1677
  live.sendGalleryMode(requestId, enabled)
@@ -1649,6 +1715,38 @@ class DeviceManager {
1649
1715
  live.sendCameraFovSetting(requestId, fov, roiPosition)
1650
1716
  }
1651
1717
 
1718
+ /** Sends the pre-lease FOV command used by ASG clients that do not send an acknowledgement. */
1719
+ fun sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) {
1720
+ val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
1721
+ if (!glassesConnected) throw IllegalStateException("not_connected")
1722
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1723
+ live.sendCameraFovSetting(null, fov, roiPosition)
1724
+ }
1725
+
1726
+ /** Replays the persistent base FOV without waiting for an acknowledgement. */
1727
+ fun restoreLegacyCameraFovSetting() {
1728
+ val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
1729
+ if (!glassesConnected) throw IllegalStateException("not_connected")
1730
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1731
+ live.sendCameraFovSetting()
1732
+ }
1733
+
1734
+ fun sendCameraFovOverride(
1735
+ requestId: String,
1736
+ leaseId: String,
1737
+ fov: Int,
1738
+ roiPosition: Int,
1739
+ ttlMs: Int,
1740
+ ) {
1741
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1742
+ live.sendCameraFovOverride(requestId, leaseId, fov, roiPosition, ttlMs)
1743
+ }
1744
+
1745
+ fun releaseCameraFovOverride(requestId: String, leaseId: String) {
1746
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1747
+ live.releaseCameraFovOverride(requestId, leaseId)
1748
+ }
1749
+
1652
1750
  fun sendCameraTuningConfig(requestId: String, anrOn: Boolean, gainOn: Boolean) {
1653
1751
  val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1654
1752
  live.sendCameraTuningConfig(requestId, anrOn, gainOn)
@@ -1657,8 +1755,11 @@ class DeviceManager {
1657
1755
  fun warmUpCamera(
1658
1756
  requestId: String,
1659
1757
  size: PhotoSize,
1758
+ mode: PhotoMode = PhotoMode.PHOTO,
1660
1759
  exposureTimeNs: Long?,
1661
1760
  durationMs: Int,
1761
+ zsl: Boolean? = null,
1762
+ mfnr: Boolean? = null,
1662
1763
  ) {
1663
1764
  // Fail fast like other camera commands so the SDK promise rejects immediately instead of
1664
1765
  // hanging until the request timeout with no camera_status.
@@ -1668,11 +1769,16 @@ class DeviceManager {
1668
1769
  "unsupported_device",
1669
1770
  "This command requires Mentra Live glasses.",
1670
1771
  )
1671
- live.warmUpCamera(requestId, size, exposureTimeNs, durationMs)
1772
+ live.warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs, zsl, mfnr)
1773
+ }
1774
+
1775
+ fun stopCameraWarmUp(requestId: String) {
1776
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1777
+ live.stopCameraWarmUp(requestId)
1672
1778
  }
1673
1779
 
1674
1780
  /**
1675
- * Read glasses media step volume (0–15) via K900 on Mentra Live only. Blocks until response,
1781
+ * Read glasses media step volume (0—5) via K900 on Mentra Live only. Blocks until response,
1676
1782
  * error, or timeout (used from JS AsyncFunction on a worker thread).
1677
1783
  */
1678
1784
  fun getGlassesMediaVolumeBlocking(): Map<String, Any> {
@@ -1698,7 +1804,7 @@ class DeviceManager {
1698
1804
  return result ?: throw IllegalStateException("glasses_volume_empty")
1699
1805
  }
1700
1806
 
1701
- /** Set glasses media step volume (0–15) via K900 on Mentra Live only. */
1807
+ /** Set glasses media step volume (0—5) via K900 on Mentra Live only. */
1702
1808
  fun setGlassesMediaVolumeBlocking(level: Int): Map<String, Any> {
1703
1809
  val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1704
1810
  val latch = CountDownLatch(1)
@@ -1830,8 +1936,14 @@ class DeviceManager {
1830
1936
  Bridge.log("MAN: No default wearable, returning")
1831
1937
  return
1832
1938
  }
1833
- if (deviceName.isEmpty()) {
1834
- Bridge.log("MAN: No device name, returning")
1939
+ val reconnectTarget =
1940
+ if (defaultWearable.contains(DeviceTypes.AR99) && deviceAddress.isNotBlank()) {
1941
+ deviceAddress
1942
+ } else {
1943
+ deviceName
1944
+ }
1945
+ if (reconnectTarget.isEmpty()) {
1946
+ Bridge.log("MAN: No reconnect target, returning")
1835
1947
  return
1836
1948
  }
1837
1949
  if (!hasBluetoothPermissions()) {
@@ -1843,7 +1955,7 @@ class DeviceManager {
1843
1955
  }
1844
1956
  initSGC(defaultWearable)
1845
1957
  searching = true
1846
- sgc?.connectById(deviceName)
1958
+ sgc?.connectById(reconnectTarget)
1847
1959
  connectDefaultController()
1848
1960
  }
1849
1961
 
@@ -1938,6 +2050,11 @@ class DeviceManager {
1938
2050
  shouldSendBootingMessage = true // Reset for next first connect
1939
2051
  // clear glasses properties:
1940
2052
  DeviceStore.apply("glasses", "deviceModel", "")
2053
+ // A manufacturing serial is session-bound. Clear it on every disconnect so a
2054
+ // previously connected pair's serial can never be reported for the next
2055
+ // connection (e.g. switching from G1/Ar99, which populate it from the
2056
+ // advertisement, to a model that never writes it, like G2).
2057
+ DeviceStore.apply("glasses", "serialNumber", "")
1941
2058
  DeviceStore.apply("glasses", "fullyBooted", false)
1942
2059
  DeviceStore.apply("glasses", "connected", false)
1943
2060
  DeviceStore.apply(
@@ -1976,6 +2093,7 @@ class DeviceManager {
1976
2093
  Bridge.saveSetting("default_wearable", "")
1977
2094
  Bridge.saveSetting("device_name", "")
1978
2095
  Bridge.saveSetting("device_address", "")
2096
+ Bridge.saveSetting("project_name", "")
1979
2097
  }
1980
2098
 
1981
2099
  fun forgetController() {
@@ -2046,3 +2164,4 @@ class DeviceManager {
2046
2164
  }
2047
2165
  }
2048
2166
  }
2167
+
@@ -125,6 +125,7 @@ object DeviceStore {
125
125
  store.set("bluetooth", "contextual_dashboard", true)
126
126
  store.set("bluetooth", "gallery_mode", true)
127
127
  store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
128
+ store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.LOUDNESS_GATE_ENABLED)
128
129
  store.set("bluetooth", "screen_disabled", false)
129
130
  store.set("bluetooth", "button_photo_size", "max")
130
131
  store.set("bluetooth", "button_max_recording_time", 10)
@@ -268,6 +269,9 @@ object DeviceStore {
268
269
  "bluetooth" to "voice_activity_detection_enabled" -> {
269
270
  DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
270
271
  }
272
+ "bluetooth" to "loudness_gate_enabled" -> {
273
+ DeviceManager.getInstance().sgc?.sendLoudnessGateSetting()
274
+ }
271
275
  "bluetooth" to "nex_lc3_audio_playback" -> {
272
276
  (value as? Boolean)?.let { enabled ->
273
277
  Bridge.log("DeviceStore: nex_lc3_audio_playback changed to $enabled")