@mentra/bluetooth-sdk 3.2.1-beta.455 → 3.2.1-beta.459

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.
@@ -1378,7 +1378,8 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
1378
1378
  Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
1379
1379
  sgc?.cleanup()
1380
1380
  sgc = null
1381
- resetSystemTimeSync()
1381
+ DeviceStore.apply("glasses", "micEnabled", false)
1382
+ resetConnectionReadyState()
1382
1383
  }
1383
1384
 
1384
1385
  if (sgc != null) {
@@ -1574,10 +1575,11 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
1574
1575
  handleMach1Ready() // Z100 uses same initialization as Mach1
1575
1576
  }
1576
1577
 
1577
- // Re-apply microphone settings after reconnection
1578
- // Cache was cleared on disconnect, so this will definitely send commands
1578
+ // Disconnect clears micEnabled but preserves the consumers' audio requests.
1579
+ // Recompute that derived flag before selecting a microphone; replaying an
1580
+ // unchanged should_send_* setting is deduplicated by DeviceStore.apply().
1579
1581
  Bridge.log("MAN: Re-applying microphone settings after reconnection")
1580
- updateMicState()
1582
+ setMicState()
1581
1583
 
1582
1584
  // send to the server our battery status:
1583
1585
  Bridge.sendBatteryStatus(sgc?.batteryLevel ?: -1, false)
@@ -1617,7 +1619,11 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
1617
1619
  mainHandler.postDelayed(sync, 3000)
1618
1620
  }
1619
1621
 
1620
- private fun resetSystemTimeSync() {
1622
+ private fun resetConnectionReadyState() {
1623
+ // Suppress duplicate readiness only within one connection, never across
1624
+ // a genuine disconnect/reconnect of the same glasses inside two seconds.
1625
+ lastReadyHandledKey = ""
1626
+ lastReadyHandledAtMs = 0L
1621
1627
  pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
1622
1628
  pendingSystemTimeSync = null
1623
1629
  lastSystemTimeSyncConnectionKey = ""
@@ -1635,7 +1641,7 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
1635
1641
 
1636
1642
  fun handleDeviceDisconnected() {
1637
1643
  Bridge.log("MAN: Device disconnected")
1638
- resetSystemTimeSync()
1644
+ resetConnectionReadyState()
1639
1645
  resetMicHealth()
1640
1646
  DeviceStore.apply("glasses", "headUp", false)
1641
1647
  DeviceStore.apply(
@@ -2445,7 +2451,10 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
2445
2451
  val device = sgc
2446
2452
  if (device is Nimo) device.cleanup() else device?.disconnect()
2447
2453
  sgc = null // Clear the SGC reference after disconnect
2448
- resetSystemTimeSync()
2454
+ // This cache belongs to the discarded connection. Keep consumer demand,
2455
+ // but require a new mic-enable command when replacement glasses are ready.
2456
+ DeviceStore.apply("glasses", "micEnabled", false)
2457
+ resetConnectionReadyState()
2449
2458
  resetMicHealth()
2450
2459
  searching = false
2451
2460
  micEnabled = false
@@ -2508,7 +2517,8 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
2508
2517
  // session state without calling disconnect() again (that would hit a dead instance
2509
2518
  // and leave a destroyed MentraLive retained for the next scan).
2510
2519
  sgc = null
2511
- resetSystemTimeSync()
2520
+ DeviceStore.apply("glasses", "micEnabled", false)
2521
+ resetConnectionReadyState()
2512
2522
  searching = false
2513
2523
  micEnabled = false
2514
2524
  updateMicState()
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
5
  const val FAMILY_BASE_VERSION: String = "3.2.1"
6
- const val RELEASE_IDENTITY: String = "3.2.1-beta.455"
7
- const val RELEASE_SET_ID: String = "mentra-3.2.1-beta.455"
8
- const val SOURCE_COMMIT: String = "07da1db49bab0ceacbcea480102309b8e8ae5bc6"
9
- const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.455.json"
10
- const val OTA_MANIFEST_SHA256: String = "0e3867266f9f3836f06b9ce574e6c3870f0864087e219002e424f54f194934e0"
6
+ const val RELEASE_IDENTITY: String = "3.2.1-beta.459"
7
+ const val RELEASE_SET_ID: String = "mentra-3.2.1-beta.459"
8
+ const val SOURCE_COMMIT: String = "4e485930e223459b6c6f4a4f30072be409611f3d"
9
+ const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.459.json"
10
+ const val OTA_MANIFEST_SHA256: String = "82457abc1aa4faa1aea0ff67b2e90ab5a5cb180a6ee1945e92465305124445e9"
11
11
  }
@@ -2117,6 +2117,10 @@ class Nimo : SGCManager() {
2117
2117
  canvasEncoder.invalidate()
2118
2118
  canvas.disconnected()
2119
2119
  audioClient.stop()
2120
+ // Nimo's decoder/stream belongs to this GATT session. Consumer demand
2121
+ // remains in bluetooth.should_send_*; readiness must start a fresh stream
2122
+ // even when reconnecting with this same adapter instance.
2123
+ DeviceStore.apply("glasses", "micEnabled", false)
2120
2124
  receiveAssembler.reset()
2121
2125
  writes.reset()
2122
2126
  negotiatedMtu = 23
@@ -24,6 +24,96 @@ import java.util.concurrent.TimeUnit
24
24
  class DeviceManagerSceneHandoffTest {
25
25
  @Before fun setup() { Bridge.initialize(ApplicationProvider.getApplicationContext()) }
26
26
 
27
+ @Test @LooperMode(LooperMode.Mode.PAUSED)
28
+ fun reconnectRestoresUnchangedAudioRequests() {
29
+ for (request in listOf("should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active")) {
30
+ withReadyRecordingDevice { manager, original, _ ->
31
+ for (key in listOf("should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active")) {
32
+ DeviceStore.set("bluetooth", key, key == request)
33
+ }
34
+ DeviceStore.set("bluetooth", "micRanking", listOf("glasses"))
35
+ DeviceStore.set("glasses", "micEnabled", false)
36
+ original.hasMic = true
37
+ manager.setMicState()
38
+ assertEquals(true, DeviceStore.get("bluetooth", "micEnabled"))
39
+ assertEquals(true, DeviceStore.get("glasses", "micEnabled"))
40
+
41
+ repeat(3) {
42
+ manager.disconnect()
43
+ assertEquals(false, DeviceStore.get("bluetooth", "micEnabled"))
44
+ assertEquals(false, DeviceStore.get("glasses", "micEnabled"))
45
+ assertEquals(true, DeviceStore.get("bluetooth", request))
46
+ val replacement = NimoRecordingSGC().apply { hasMic = true }
47
+ manager.sgc = replacement
48
+
49
+ // No test reset of glasses.micEnabled: production teardown must
50
+ // clear it. Replaying the same consumer request is deduplicated.
51
+ DeviceStore.apply("bluetooth", request, true)
52
+ assertEquals(false, DeviceStore.get("bluetooth", "micEnabled"))
53
+ DeviceStore.apply("glasses", "fullyBooted", true)
54
+
55
+ assertEquals("Reconnect must restore $request", true, DeviceStore.get("bluetooth", "micEnabled"))
56
+ assertEquals(listOf(true), replacement.micChanges)
57
+ assertEquals("glasses", manager.activeMicSource())
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ @Test @LooperMode(LooperMode.Mode.PAUSED)
64
+ fun linkDisconnectRetainsIntentForTheSameCommunicator() {
65
+ withReadyRecordingDevice { manager, original, _ ->
66
+ original.hasMic = true
67
+ DeviceStore.set("bluetooth", "micRanking", listOf("glasses"))
68
+ DeviceStore.set("glasses", "micEnabled", false)
69
+ DeviceStore.set("bluetooth", "should_send_lc3", true)
70
+ manager.setMicState()
71
+ assertEquals(listOf(true), original.micChanges)
72
+ DeviceStore.set("glasses", "fullyBooted", true)
73
+ DeviceStore.apply("glasses", "fullyBooted", false)
74
+ assertSame(original, manager.sgc)
75
+ assertEquals(true, DeviceStore.get("bluetooth", "should_send_lc3"))
76
+ assertEquals(true, DeviceStore.get("glasses", "micEnabled"))
77
+ }
78
+ }
79
+
80
+ @Test @LooperMode(LooperMode.Mode.PAUSED)
81
+ fun discardingCommunicatorInvalidatesMicCache() {
82
+ withReadyRecordingDevice { manager, original, _ ->
83
+ original.hasMic = true
84
+ DeviceStore.set("bluetooth", "micRanking", listOf("glasses"))
85
+ DeviceStore.set("glasses", "micEnabled", false)
86
+ DeviceStore.set("bluetooth", "should_send_lc3", true)
87
+ manager.setMicState()
88
+ manager.initSGC(original.type)
89
+ assertEquals(true, DeviceStore.get("glasses", "micEnabled"))
90
+
91
+ // An unsupported model exercises disposal without starting real BLE.
92
+ manager.initSGC("Unavailable test glasses")
93
+ assertNull(manager.sgc)
94
+ assertEquals(false, DeviceStore.get("glasses", "micEnabled"))
95
+ assertEquals(true, DeviceStore.get("bluetooth", "should_send_lc3"))
96
+ }
97
+ }
98
+
99
+ @Test @LooperMode(LooperMode.Mode.PAUSED)
100
+ fun reconnectWithoutAudioRequestsLeavesMicrophoneOff() {
101
+ withReadyRecordingDevice { manager, _, _ ->
102
+ for (key in listOf("should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active")) {
103
+ DeviceStore.set("bluetooth", key, false)
104
+ }
105
+ DeviceStore.set("glasses", "micEnabled", true)
106
+ manager.disconnect()
107
+ assertEquals(false, DeviceStore.get("glasses", "micEnabled"))
108
+ val replacement = NimoRecordingSGC().apply { hasMic = true }
109
+ manager.sgc = replacement
110
+ DeviceStore.apply("glasses", "fullyBooted", true)
111
+ assertEquals(false, DeviceStore.get("bluetooth", "micEnabled"))
112
+ assertEquals("", manager.activeMicSource())
113
+ assertTrue(replacement.micChanges.isEmpty())
114
+ }
115
+ }
116
+
27
117
  @Test @LooperMode(LooperMode.Mode.PAUSED)
28
118
  fun nimoBrightnessChangesDoNotReplaceTheSelectedScene() {
29
119
  for (headUp in listOf(false, true)) for ((key, value) in brightnessChanges()) {
@@ -499,6 +589,7 @@ class DeviceManagerSceneHandoffTest {
499
589
  }
500
590
 
501
591
  private open class RecordingSGC(override val sceneHandoffRequiresClear: Boolean) : SGCManager() {
592
+ val micChanges = mutableListOf<Boolean>()
502
593
  val calls = CopyOnWriteArrayList<String>()
503
594
  val brightnessCalls = mutableListOf<String>()
504
595
  val textSent = CountDownLatch(1)
@@ -511,7 +602,10 @@ class DeviceManagerSceneHandoffTest {
511
602
  override fun sendTextWall(text: String) { lastTextNanos = System.nanoTime(); calls += "text:$text"; textSent.countDown() }
512
603
  override fun disconnect() { calls += "disconnect" }
513
604
  override fun cleanup() { calls += "cleanup" }
514
- override fun setMicEnabled(enabled: Boolean) {}
605
+ override fun setMicEnabled(enabled: Boolean) {
606
+ micChanges += enabled
607
+ DeviceStore.set("glasses", "micEnabled", enabled)
608
+ }
515
609
  override fun sortMicRanking(list: MutableList<String>) = list
516
610
  override fun requestPhoto(request: PhotoRequest) {}
517
611
  override fun startStream(message: MutableMap<String, Any>) {}
@@ -0,0 +1,93 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.bluetooth.BluetoothGattCharacteristic
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import com.mentra.bluetoothsdk.Bridge
6
+ import com.mentra.bluetoothsdk.DeviceManager
7
+ import com.mentra.bluetoothsdk.DeviceStore
8
+ import org.junit.Assert.*
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.annotation.Config
13
+ import org.robolectric.annotation.LooperMode
14
+ import java.util.UUID
15
+
16
+ @RunWith(RobolectricTestRunner::class)
17
+ @Config(sdk = [33])
18
+ @LooperMode(LooperMode.Mode.PAUSED)
19
+ class NimoMicReconnectTest {
20
+ private val requests = listOf("should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active")
21
+
22
+ @Test fun sameAdapterReconnectRestartsTheStoppedAudioClientForRetainedConsumers() {
23
+ for (request in requests) withNimo { manager, device ->
24
+ DeviceStore.apply("bluetooth", request, true)
25
+ assertTrue(audioStarted(device))
26
+ repeat(3) {
27
+ resetLink(device)
28
+ assertFalse(audioStarted(device))
29
+ assertEquals(true, DeviceStore.get("bluetooth", request))
30
+ // This is the same adapter, not the replacement fake used by the manager tests.
31
+ DeviceStore.apply("glasses", "fullyBooted", true)
32
+ assertSame(device, manager.sgc)
33
+ assertTrue("Readiness must restart Nimo decoding without waiting for the watchdog", audioStarted(device))
34
+ }
35
+ }
36
+ }
37
+
38
+ @Test fun consumerStoppingDuringLinkLossDoesNotRestartAudio() = withNimo { _, device ->
39
+ DeviceStore.apply("bluetooth", "should_send_pcm", true)
40
+ assertTrue(audioStarted(device))
41
+ resetLink(device)
42
+ DeviceStore.apply("bluetooth", "should_send_pcm", false)
43
+ DeviceStore.apply("glasses", "fullyBooted", true)
44
+ assertFalse(audioStarted(device))
45
+ assertEquals(false, DeviceStore.get("glasses", "micEnabled"))
46
+ }
47
+
48
+ private fun resetLink(device: Nimo) {
49
+ // Exercise the production reset called by the GATT disconnect callback.
50
+ Nimo::class.java.getDeclaredMethod("resetSessionState").apply { isAccessible = true }.invoke(device)
51
+ DeviceStore.apply("glasses", "fullyBooted", false)
52
+ }
53
+
54
+ private fun audioStarted(device: Nimo): Boolean {
55
+ val audio = Nimo::class.java.getDeclaredField("audioClient").apply { isAccessible = true }.get(device)
56
+ return audio.javaClass.getDeclaredField("started").apply { isAccessible = true }.getBoolean(audio)
57
+ }
58
+
59
+ private fun withNimo(block: (DeviceManager, Nimo) -> Unit) {
60
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
61
+ val singleton = DeviceManager::class.java.getDeclaredField("_instance").apply { isAccessible = true }
62
+ val previous = singleton.get(null)
63
+ val saved = listOf("bluetooth", "glasses").associateWith { DeviceStore.store.getCategory(it) }
64
+ val manager = DeviceManager(initializeHardware = false)
65
+ singleton.set(null, manager)
66
+ val device = Nimo()
67
+ manager.sgc = device
68
+ // No radio/firmware: supply the discovered mic characteristic and inspect the real
69
+ // audio-client lifecycle. Codec output and BLE delivery still require a device test.
70
+ Nimo::class.java.getDeclaredField("micChar").apply { isAccessible = true }.set(
71
+ device, BluetoothGattCharacteristic(UUID.randomUUID(), BluetoothGattCharacteristic.PROPERTY_WRITE, 0)
72
+ )
73
+ requests.forEach { DeviceStore.set("bluetooth", it, false) }
74
+ DeviceStore.set("bluetooth", "micEnabled", false)
75
+ DeviceStore.set("bluetooth", "micRanking", listOf("glasses"))
76
+ DeviceStore.set("glasses", "micEnabled", false)
77
+ DeviceStore.set("glasses", "fullyBooted", false)
78
+ try {
79
+ block(manager, device)
80
+ } finally {
81
+ device.cleanup()
82
+ manager.sgc = null
83
+ manager.cleanup()
84
+ singleton.set(null, previous)
85
+ saved.forEach { (category, values) ->
86
+ DeviceStore.store.getCategory(category).keys.filter { it !in values }.forEach {
87
+ DeviceStore.store.remove(category, it)
88
+ }
89
+ values.forEach { (key, value) -> DeviceStore.set(category, key, value) }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -1,10 +1,10 @@
1
1
  export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "3.2.1",
4
- "releaseIdentity": "3.2.1-beta.455",
5
- "releaseSetId": "mentra-3.2.1-beta.455",
6
- "sourceCommit": "07da1db49bab0ceacbcea480102309b8e8ae5bc6",
7
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.455.json",
8
- "otaManifestSha256": "0e3867266f9f3836f06b9ce574e6c3870f0864087e219002e424f54f194934e0"
4
+ "releaseIdentity": "3.2.1-beta.459",
5
+ "releaseSetId": "mentra-3.2.1-beta.459",
6
+ "sourceCommit": "4e485930e223459b6c6f4a4f30072be409611f3d",
7
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.459.json",
8
+ "otaManifestSha256": "82457abc1aa4faa1aea0ff67b2e90ab5a5cb180a6ee1945e92465305124445e9"
9
9
  });
10
10
  //# sourceMappingURL=releaseMetadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,gBAAgB;IACnC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,kIAAkI;IACpJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.1\",\n \"releaseIdentity\": \"3.2.1-beta.455\",\n \"releaseSetId\": \"mentra-3.2.1-beta.455\",\n \"sourceCommit\": \"07da1db49bab0ceacbcea480102309b8e8ae5bc6\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.455.json\",\n \"otaManifestSha256\": \"0e3867266f9f3836f06b9ce574e6c3870f0864087e219002e424f54f194934e0\"\n})\n"]}
1
+ {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,gBAAgB;IACnC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,kIAAkI;IACpJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.1\",\n \"releaseIdentity\": \"3.2.1-beta.459\",\n \"releaseSetId\": \"mentra-3.2.1-beta.459\",\n \"sourceCommit\": \"4e485930e223459b6c6f4a4f30072be409611f3d\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.459.json\",\n \"otaManifestSha256\": \"82457abc1aa4faa1aea0ff67b2e90ab5a5cb180a6ee1945e92465305124445e9\"\n})\n"]}
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
16
16
  /// silently disable it on every pair the app touches.
17
17
  static let autoPowerOffEnabled = false
18
18
  private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
19
- private static let swiftPackageSdkVersion = "3.2.1-beta.455"
19
+ private static let swiftPackageSdkVersion = "3.2.1-beta.459"
20
20
  private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
21
21
 
22
22
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -318,9 +318,9 @@ struct ViewState {
318
318
  var lc3Converter: PcmConverter?
319
319
  /// Audio output format - defaults to LC3 for bandwidth savings
320
320
  private var audioOutputFormat: AudioOutputFormat = .lc3
321
- /// Last time we received an LC3 frame from the glasses (used by the mic
322
- /// inactivity watchdog).
323
- private var lastLc3Event: Date?
321
+ private var micWatchdog = GlassesMicWatchdog()
322
+ // Injectable monotonic clock so recovery deadlines can be tested without sleeping.
323
+ var micWatchdogNow: () -> TimeInterval = { ProcessInfo.processInfo.systemUptime }
324
324
  private var sequenceGapEvents: Int64 = 0
325
325
  private var decodeFailures: Int64 = 0
326
326
  private var lastLc3ReceivedAt: Int64?
@@ -472,10 +472,17 @@ struct ViewState {
472
472
  }
473
473
 
474
474
  func reportGlassesAudioActivity() {
475
- lastLc3Event = Date()
475
+ micWatchdog.receivedAudio(at: micWatchdogNow())
476
476
  lastPcmProducedAt = nowMs()
477
477
  }
478
478
 
479
+ /// Decoded glasses audio must update recovery health; phone PCM must not.
480
+ func handleGlassesPcm(_ pcmData: Data) {
481
+ guard !pcmData.isEmpty else { return }
482
+ reportGlassesAudioActivity()
483
+ handlePcm(pcmData)
484
+ }
485
+
479
486
  private func micHealth() -> MicHealth {
480
487
  MicHealth(
481
488
  sequenceGapEvents: sequenceGapEvents,
@@ -499,13 +506,13 @@ struct ViewState {
499
506
  lastLc3ReceivedAt = nil
500
507
  lastPcmProducedAt = nil
501
508
  lastLc3Sequence = nil
502
- lastLc3Event = nil
509
+ micWatchdog = GlassesMicWatchdog()
503
510
  }
504
511
 
505
512
  private func recordLc3Packet(sequenceNumber: Int?) {
506
513
  let receivedAt = nowMs()
507
514
  lastLc3ReceivedAt = receivedAt
508
- lastLc3Event = Date()
515
+ micWatchdog.receivedAudio(at: micWatchdogNow())
509
516
  var hasGap = false
510
517
  if let sequenceNumber {
511
518
  let normalized = sequenceNumber & 0xFF
@@ -605,6 +612,7 @@ struct ViewState {
605
612
  }
606
613
 
607
614
  currentMic = micUsed
615
+ updateGlassesMicExpectation()
608
616
 
609
617
  // log if no mic was found:
610
618
  if micUsed == "" && micEnabled {
@@ -723,6 +731,7 @@ struct ViewState {
723
731
  Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
724
732
  sgc?.cleanup()
725
733
  sgc = nil
734
+ DeviceStore.shared.apply("glasses", "micEnabled", false)
726
735
  resetSystemTimeSync()
727
736
  }
728
737
 
@@ -902,27 +911,20 @@ struct ViewState {
902
911
  return result
903
912
  }
904
913
 
905
- private func checkAndReinitGlassesMic() {
906
- // if the glasses mic is marked as enabled (and the glasses are connected), but our last known lc3 event is from > 5 seconds ago, reinitialize the mic:
907
- let glassesMicEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
908
- let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
909
- if !glassesMicEnabled || !glassesConnected {
910
- return
911
- }
912
-
913
- if sgc?.isMicSuspendedForAudio == true {
914
- Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
915
- return
916
- }
917
-
918
- if PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying() {
919
- Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
920
- return
921
- }
914
+ private func updateGlassesMicExpectation() {
915
+ // Demand/route and device readiness decide whether audio is expected. The
916
+ // shared micEnabled flag alone is not evidence of a running hardware stream.
917
+ let expected = micEnabled && currentMic == MicTypes.GLASSES_CUSTOM
918
+ && sgc?.fullyBooted == true && sgc?.hasMic == true
919
+ && sgc?.isMicSuspendedForAudio != true
920
+ && !PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying()
921
+ micWatchdog.expectAudio(expected, at: micWatchdogNow())
922
+ }
922
923
 
923
- let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
924
- if timeSinceLastLc3Event > 5 {
925
- Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
924
+ func checkAndReinitGlassesMic() {
925
+ updateGlassesMicExpectation()
926
+ if micWatchdog.shouldRetry(at: micWatchdogNow()) {
927
+ Bridge.log("MAN: Expected glasses audio missing for 5 seconds; retrying mic start")
926
928
  sgc?.setMicEnabled(true)
927
929
  }
928
930
  }
@@ -1066,6 +1068,11 @@ struct ViewState {
1066
1068
  #endif
1067
1069
  checkCurrentAudioDevice()
1068
1070
 
1071
+ // Disconnect clears micEnabled but preserves the consumers' audio requests.
1072
+ // Recompute demand before selecting a microphone; unchanged requests are
1073
+ // deduplicated by DeviceStore.apply().
1074
+ setMicState()
1075
+
1069
1076
  // save the default_wearable now that we're connected:
1070
1077
  Bridge.saveSetting("default_wearable", defaultWearable)
1071
1078
  Bridge.saveSetting("device_name", deviceName)
@@ -1960,6 +1967,9 @@ struct ViewState {
1960
1967
  sgc?.clearDisplay() // clear the screen
1961
1968
  sgc?.disconnect()
1962
1969
  sgc = nil // Clear the SGC reference after disconnect
1970
+ // This cache belongs to the discarded connection. Keep consumer demand,
1971
+ // but require a new mic-enable command when replacement glasses are ready.
1972
+ DeviceStore.shared.apply("glasses", "micEnabled", false)
1963
1973
  resetSystemTimeSync()
1964
1974
  resetMicHealth()
1965
1975
  searching = false
@@ -3,9 +3,9 @@ import Foundation
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
5
  static let familyBaseVersion = "3.2.1"
6
- static let releaseIdentity = "3.2.1-beta.455"
7
- static let releaseSetId = "mentra-3.2.1-beta.455"
8
- static let sourceCommit = "07da1db49bab0ceacbcea480102309b8e8ae5bc6"
9
- static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.455.json"
10
- static let otaManifestSha256 = "0e3867266f9f3836f06b9ce574e6c3870f0864087e219002e424f54f194934e0"
6
+ static let releaseIdentity = "3.2.1-beta.459"
7
+ static let releaseSetId = "mentra-3.2.1-beta.459"
8
+ static let sourceCommit = "4e485930e223459b6c6f4a4f30072be409611f3d"
9
+ static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.459.json"
10
+ static let otaManifestSha256 = "82457abc1aa4faa1aea0ff67b2e90ab5a5cb180a6ee1945e92465305124445e9"
11
11
  }
@@ -0,0 +1,33 @@
1
+ import Foundation
2
+
3
+ /// Tracks expected audio separately from observed audio. All times use a monotonic clock.
4
+ struct GlassesMicWatchdog {
5
+ private var expectedSince: TimeInterval?
6
+ private var lastActivity: TimeInterval?
7
+ private var lastRetry: TimeInterval?
8
+ private let timeout: TimeInterval = 5
9
+
10
+ mutating func expectAudio(_ expected: Bool, at now: TimeInterval) {
11
+ guard expected else {
12
+ self = GlassesMicWatchdog()
13
+ return
14
+ }
15
+ if expectedSince == nil {
16
+ expectedSince = now
17
+ }
18
+ }
19
+
20
+ mutating func receivedAudio(at now: TimeInterval) {
21
+ guard expectedSince != nil else { return }
22
+ lastActivity = now
23
+ }
24
+
25
+ mutating func shouldRetry(at now: TimeInterval) -> Bool {
26
+ guard let expectedSince else { return false }
27
+ let lastProgress = max(expectedSince, lastActivity ?? expectedSince, lastRetry ?? expectedSince)
28
+ guard now - lastProgress >= timeout else { return false }
29
+ // Rate-limit retries even when the device never produces its first packet.
30
+ lastRetry = now
31
+ return true
32
+ }
33
+ }
@@ -1850,8 +1850,7 @@ final class Ar99: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SGCM
1850
1850
  private func ensureOpusDecoder() {
1851
1851
  guard opusDecoder == nil else { return }
1852
1852
  opusDecoder = Ar99OpusPcmDecoder { pcm in
1853
- DeviceManager.shared.reportGlassesAudioActivity()
1854
- DeviceManager.shared.handlePcm(pcm)
1853
+ DeviceManager.shared.handleGlassesPcm(pcm)
1855
1854
  }
1856
1855
  if opusDecoder == nil {
1857
1856
  Bridge.log("AR99: system Opus decoder unavailable")
@@ -1543,7 +1543,7 @@ class Nimo: NSObject, SGCManager {
1543
1543
  guard let opusDecoder else { return }
1544
1544
  for opusFrame in packet.opusFrames {
1545
1545
  if let pcm = opusDecoder.decode(opusFrame), !pcm.isEmpty {
1546
- DeviceManager.shared.handlePcm(pcm)
1546
+ DeviceManager.shared.handleGlassesPcm(pcm)
1547
1547
  }
1548
1548
  }
1549
1549
  }
@@ -0,0 +1,337 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ @MainActor
5
+ final class DeviceManagerReconnectTests: XCTestCase {
6
+ private let requests = ["should_send_lc3", "should_send_pcm", "should_send_transcript", "local_stt_fallback_active"]
7
+
8
+ private func withRecordingDevice(_ body: (DeviceManager, DeviceStore) -> Void) {
9
+ let manager = DeviceManager.shared
10
+ let store = DeviceStore.shared
11
+ let saved = ["bluetooth", "glasses"].map { ($0, store.store.getCategory($0)) }
12
+ let previousDevice = manager.sgc
13
+ let previousController = manager.controller
14
+ let previousClock = manager.micWatchdogNow
15
+ manager.micWatchdogNow = { 0 }
16
+ defer {
17
+ store.set("bluetooth", "micEnabled", false)
18
+ manager.updateMicState()
19
+ manager.micWatchdogNow = previousClock
20
+ manager.sgc = previousDevice
21
+ manager.controller = previousController
22
+ for (category, values) in saved {
23
+ for key in store.store.getCategory(category).keys where values[key] == nil {
24
+ store.store.remove(category, key)
25
+ }
26
+ for (key, value) in values {
27
+ store.set(category, key, value)
28
+ }
29
+ }
30
+ }
31
+ manager.controller = nil
32
+ manager.sgc = ReconnectRecordingDevice()
33
+ for key in requests {
34
+ store.set("bluetooth", key, false)
35
+ }
36
+ store.set("bluetooth", "micEnabled", false)
37
+ store.set("bluetooth", "micRanking", [MicTypes.GLASSES_CUSTOM])
38
+ store.set("glasses", "micEnabled", false)
39
+ store.set("glasses", "fullyBooted", false)
40
+ body(manager, store)
41
+ }
42
+
43
+ func testReconnectRestoresUnchangedAudioRequests() {
44
+ for request in requests {
45
+ withRecordingDevice { manager, store in
46
+ store.apply("bluetooth", request, true)
47
+ XCTAssertEqual(store.get("bluetooth", "micEnabled") as? Bool, true)
48
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, true)
49
+ for _ in 0 ..< 3 {
50
+ manager.disconnect()
51
+ XCTAssertEqual(store.get("bluetooth", request) as? Bool, true)
52
+ XCTAssertEqual(store.get("bluetooth", "micEnabled") as? Bool, false)
53
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, false)
54
+ let reconnected = ReconnectRecordingDevice()
55
+ manager.sgc = reconnected
56
+
57
+ // Replaying the same consumer request is deduplicated. No test
58
+ // reset of glasses.micEnabled: production teardown must clear it.
59
+ store.apply("bluetooth", request, true)
60
+ XCTAssertEqual(store.get("bluetooth", "micEnabled") as? Bool, false)
61
+ store.apply("glasses", "fullyBooted", true)
62
+
63
+ XCTAssertEqual(store.get("bluetooth", "micEnabled") as? Bool, true, request)
64
+ XCTAssertEqual(reconnected.micChanges, [true], request)
65
+ XCTAssertEqual(store.get("bluetooth", "currentMic") as? String, MicTypes.GLASSES_CUSTOM, request)
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ func testLinkDisconnectRetainsIntentForTheSameCommunicator() {
72
+ withRecordingDevice { manager, store in
73
+ store.apply("glasses", "fullyBooted", true)
74
+ store.apply("bluetooth", "should_send_lc3", true)
75
+ let original = manager.sgc as! ReconnectRecordingDevice
76
+ store.apply("glasses", "fullyBooted", false)
77
+ XCTAssertTrue((manager.sgc as? ReconnectRecordingDevice) === original)
78
+ XCTAssertEqual(store.get("bluetooth", "should_send_lc3") as? Bool, true)
79
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, true)
80
+ }
81
+ }
82
+
83
+ func testDiscardingCommunicatorInvalidatesMicCache() {
84
+ withRecordingDevice { manager, store in
85
+ store.apply("bluetooth", "should_send_lc3", true)
86
+ manager.initSGC(manager.sgc!.type)
87
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, true)
88
+
89
+ // An unsupported model exercises disposal without starting real BLE.
90
+ manager.initSGC("Unavailable test glasses")
91
+ XCTAssertNil(manager.sgc)
92
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, false)
93
+ XCTAssertEqual(store.get("bluetooth", "should_send_lc3") as? Bool, true)
94
+ }
95
+ }
96
+
97
+ func testReconnectWithoutAudioRequestsLeavesMicOff() {
98
+ withRecordingDevice { manager, store in
99
+ store.set("glasses", "micEnabled", true)
100
+ manager.disconnect()
101
+ XCTAssertEqual(store.get("glasses", "micEnabled") as? Bool, false)
102
+ let reconnected = ReconnectRecordingDevice()
103
+ manager.sgc = reconnected
104
+ store.apply("glasses", "fullyBooted", true)
105
+ XCTAssertEqual(store.get("bluetooth", "micEnabled") as? Bool, false)
106
+ XCTAssertEqual(store.get("bluetooth", "currentMic") as? String, "")
107
+ XCTAssertTrue(reconnected.micChanges.isEmpty)
108
+ }
109
+ }
110
+
111
+ func testWatchdogRetriesWhenFirstPacketNeverArrivesAndLimitsRetries() {
112
+ withRecordingDevice { manager, store in
113
+ var now: TimeInterval = 0
114
+ manager.micWatchdogNow = { now }
115
+ store.apply("glasses", "fullyBooted", true)
116
+ store.apply("bluetooth", "should_send_pcm", true)
117
+ let device = manager.sgc as! ReconnectRecordingDevice
118
+ now = 4
119
+ manager.checkAndReinitGlassesMic()
120
+ XCTAssertEqual(device.micChanges, [true])
121
+ now = 5
122
+ manager.checkAndReinitGlassesMic()
123
+ XCTAssertEqual(device.micChanges, [true, true])
124
+ now = 5.1
125
+ manager.checkAndReinitGlassesMic()
126
+ XCTAssertEqual(device.micChanges, [true, true])
127
+ now = 10
128
+ manager.checkAndReinitGlassesMic()
129
+ XCTAssertEqual(device.micChanges, [true, true, true])
130
+ }
131
+ }
132
+
133
+ func testDecodedGlassesAudioKeepsWatchdogHealthyAndMissingPacketsAfterwardRetry() {
134
+ withRecordingDevice { manager, store in
135
+ var now: TimeInterval = 0
136
+ manager.micWatchdogNow = { now }
137
+ store.apply("glasses", "fullyBooted", true)
138
+ store.apply("bluetooth", "should_send_pcm", true)
139
+ let device = manager.sgc as! ReconnectRecordingDevice
140
+ // This is the decoded-audio entry point used by Nimo and AR99.
141
+ for tick in 1 ... 10 {
142
+ now = TimeInterval(tick * 10)
143
+ manager.handleGlassesPcm(Data([0, 0]))
144
+ manager.checkAndReinitGlassesMic()
145
+ }
146
+ XCTAssertEqual(device.micChanges, [true])
147
+ now = 104
148
+ manager.checkAndReinitGlassesMic()
149
+ XCTAssertEqual(device.micChanges, [true])
150
+ now = 105
151
+ manager.checkAndReinitGlassesMic()
152
+ XCTAssertEqual(device.micChanges, [true, true])
153
+ }
154
+ }
155
+
156
+ func testLc3GlassesAudioKeepsWatchdogHealthy() {
157
+ withRecordingDevice { manager, store in
158
+ var now: TimeInterval = 0
159
+ manager.micWatchdogNow = { now }
160
+ store.apply("glasses", "fullyBooted", true)
161
+ store.apply("bluetooth", "should_send_pcm", true)
162
+ let device = manager.sgc as! ReconnectRecordingDevice
163
+ let lc3 = manager.lc3Converter!.encode(Data(repeating: 0, count: 320), frameSize: 40) as Data
164
+ XCTAssertEqual(lc3.count, 40)
165
+ // G2 and Live deliver LC3 through this entry point, not decoded PCM.
166
+ for tick in 1 ... 10 {
167
+ now = TimeInterval(tick * 10)
168
+ manager.handleGlassesMicData(lc3, 40)
169
+ manager.checkAndReinitGlassesMic()
170
+ }
171
+ XCTAssertEqual(device.micChanges, [true])
172
+ }
173
+ }
174
+
175
+ func testPhonePcmAndEmptyGlassesPcmCannotHideAMissingGlassesStream() {
176
+ withRecordingDevice { manager, store in
177
+ var now: TimeInterval = 0
178
+ manager.micWatchdogNow = { now }
179
+ store.apply("glasses", "fullyBooted", true)
180
+ store.apply("bluetooth", "should_send_pcm", true)
181
+ let device = manager.sgc as! ReconnectRecordingDevice
182
+ now = 5
183
+ manager.handlePcm(Data([0, 0]))
184
+ manager.handleGlassesPcm(Data())
185
+ manager.checkAndReinitGlassesMic()
186
+ XCTAssertEqual(device.micChanges, [true, true])
187
+ }
188
+ }
189
+
190
+ func testWatchdogRespectsSuspensionAndGivesResumeAFreshDeadline() {
191
+ withRecordingDevice { manager, store in
192
+ var now: TimeInterval = 0
193
+ manager.micWatchdogNow = { now }
194
+ store.apply("glasses", "fullyBooted", true)
195
+ store.apply("bluetooth", "should_send_pcm", true)
196
+ let device = manager.sgc as! ReconnectRecordingDevice
197
+ device.isMicSuspendedForAudio = true
198
+ now = 100
199
+ manager.checkAndReinitGlassesMic()
200
+ XCTAssertEqual(device.micChanges, [true])
201
+ device.isMicSuspendedForAudio = false
202
+ now = 200
203
+ manager.checkAndReinitGlassesMic()
204
+ XCTAssertEqual(device.micChanges, [true])
205
+ now = 205
206
+ manager.checkAndReinitGlassesMic()
207
+ XCTAssertEqual(device.micChanges, [true, true])
208
+
209
+ store.apply("bluetooth", "should_send_pcm", false)
210
+ // A late firmware ACK can leave the shared cache true after demand ends.
211
+ store.set("glasses", "micEnabled", true)
212
+ now = 300
213
+ manager.checkAndReinitGlassesMic()
214
+ XCTAssertEqual(device.micChanges, [true, true, false])
215
+ }
216
+ }
217
+
218
+ func testWatchdogRespectsOwnAppPlaybackAndPhoneRoute() {
219
+ withRecordingDevice { manager, store in
220
+ var now: TimeInterval = 0
221
+ manager.micWatchdogNow = { now }
222
+ let monitor = PhoneAudioMonitor.getInstance()
223
+ let wasPlaying = monitor.isOwnAppAudioPlaying()
224
+ defer { monitor.setOwnAppAudioPlaying(wasPlaying) }
225
+ monitor.setOwnAppAudioPlaying(false)
226
+ store.apply("glasses", "fullyBooted", true)
227
+ store.apply("bluetooth", "should_send_pcm", true)
228
+ let device = manager.sgc as! ReconnectRecordingDevice
229
+ monitor.setOwnAppAudioPlaying(true)
230
+ now = 100
231
+ manager.checkAndReinitGlassesMic()
232
+ XCTAssertEqual(device.micChanges, [true])
233
+ monitor.setOwnAppAudioPlaying(false)
234
+ store.set("bluetooth", "currentMic", MicTypes.PHONE_INTERNAL)
235
+ now = 200
236
+ manager.checkAndReinitGlassesMic()
237
+ XCTAssertEqual(device.micChanges, [true])
238
+ }
239
+ }
240
+
241
+ func testWatchdogDeadlineBelongsToTheCurrentConnection() {
242
+ withRecordingDevice { manager, store in
243
+ var now: TimeInterval = 0
244
+ manager.micWatchdogNow = { now }
245
+ store.apply("glasses", "fullyBooted", true)
246
+ store.apply("bluetooth", "should_send_pcm", true)
247
+ let device = manager.sgc as! ReconnectRecordingDevice
248
+ store.apply("glasses", "fullyBooted", false)
249
+ now = 100
250
+ manager.checkAndReinitGlassesMic()
251
+ XCTAssertEqual(device.micChanges, [true])
252
+ store.apply("glasses", "fullyBooted", true)
253
+ manager.checkAndReinitGlassesMic()
254
+ XCTAssertEqual(device.micChanges, [true])
255
+ now = 105
256
+ manager.checkAndReinitGlassesMic()
257
+ XCTAssertEqual(device.micChanges, [true, true])
258
+ }
259
+ }
260
+ }
261
+
262
+ @MainActor
263
+ private final class ReconnectRecordingDevice: SGCManager {
264
+ var type = "Reconnect test glasses"
265
+ let hasMic = true
266
+ let showConnectionConfirmation = false
267
+ var micChanges: [Bool] = []
268
+ var isMicSuspendedForAudio = false
269
+
270
+ func clearSceneElements(_: [String]) async {}
271
+ func sendTextWall(_: String) async {}
272
+ func applySceneFrame(_: SceneFrame) async {}
273
+ func clearDisplay() {}
274
+
275
+ func setMicEnabled(_ enabled: Bool) {
276
+ micChanges.append(enabled)
277
+ DeviceStore.shared.set("glasses", "micEnabled", enabled)
278
+ }
279
+
280
+ func sortMicRanking(list: [String]) -> [String] {
281
+ list
282
+ }
283
+
284
+ func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
285
+ func requestPhoto(_: PhotoRequest) {}
286
+ func startStream(_: [String: Any]) {}
287
+ func stopStream() {}
288
+ func sendStreamKeepAlive(_: [String: Any]) {}
289
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
290
+ func stopVideoRecording(requestId _: String) {}
291
+ func sendButtonPhotoSettings() {}
292
+ func sendButtonVideoRecordingSettings() {}
293
+ func sendCameraFovSetting() {}
294
+ func sendButtonMaxRecordingTime() {}
295
+ func setBrightness(_: Int, autoMode _: Bool) {}
296
+ func sendText(_: String) async {}
297
+ func sendDoubleTextWall(_: String, _: String) async {}
298
+ func displayBitmap(base64ImageData _: String, x _: Int32?, y _: Int32?, width _: Int32?, height _: Int32?) async -> Bool {
299
+ false
300
+ }
301
+
302
+ func showDashboard() {}
303
+ func setDashboardPosition(_: Int, _: Int) {}
304
+ func setHeadUpAngle(_: Int) {}
305
+ func getBatteryStatus() {}
306
+ func setSilentMode(_: Bool) {}
307
+ func exit() {}
308
+ func sendShutdown() {}
309
+ func sendReboot() {}
310
+ func sendRgbLedControl(requestId _: String, packageName _: String?, action _: String, color _: String?, onDurationMs _: Int, offDurationMs _: Int, count _: Int) {}
311
+ func disconnect() {}
312
+ func forget() {}
313
+ func findCompatibleDevices() {}
314
+ func stopScan() {}
315
+ func connectById(_: String) {}
316
+ func getConnectedBluetoothName() -> String? {
317
+ nil
318
+ }
319
+
320
+ func cleanup() {}
321
+ func ping() {}
322
+ func dbg1() {}
323
+ func dbg2() {}
324
+ func connectController() {}
325
+ func disconnectController() {}
326
+ func requestWifiScan(scanId _: String?) {}
327
+ func sendWifiCredentials(_: String, _: String) {}
328
+ func forgetWifiNetwork(_: String) {}
329
+ func sendHotspotState(_: Bool) {}
330
+ func sendUserEmailToGlasses(_: String) {}
331
+ func sendOtaStart(otaVersionUrl _: String?) {}
332
+ func sendOtaQueryStatus() {}
333
+ func queryGalleryStatus() {}
334
+ func sendGalleryMode() {}
335
+ func requestVersionInfo() {}
336
+ func sendIncidentId(_: String, apiBaseUrl _: String?) {}
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.2.1-beta.455",
3
+ "version": "3.2.1-beta.459",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -114,7 +114,7 @@
114
114
  "registry": "https://registry.npmjs.org/"
115
115
  },
116
116
  "dependencies": {
117
- "@mentra/cloud-protocol": "3.2.1-beta.455"
117
+ "@mentra/cloud-protocol": "3.2.1-beta.459"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@types/node": "^25.9.3",
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.1",
15
- "releaseIdentity": "3.2.1-beta.455",
16
- "releaseSetId": "mentra-3.2.1-beta.455",
17
- "sourceCommit": "07da1db49bab0ceacbcea480102309b8e8ae5bc6",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.455.json",
19
- "otaManifestSha256": "0e3867266f9f3836f06b9ce574e6c3870f0864087e219002e424f54f194934e0"
15
+ "releaseIdentity": "3.2.1-beta.459",
16
+ "releaseSetId": "mentra-3.2.1-beta.459",
17
+ "sourceCommit": "4e485930e223459b6c6f4a4f30072be409611f3d",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.459.json",
19
+ "otaManifestSha256": "82457abc1aa4faa1aea0ff67b2e90ab5a5cb180a6ee1945e92465305124445e9"
20
20
  })