@mentra/bluetooth-sdk 3.4.0-dev.993 → 3.4.0-dev.998

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 (68) hide show
  1. package/MIGRATION.md +61 -0
  2. package/README.md +47 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +8 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +48 -4
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +21 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +5 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicy.kt +64 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +788 -134
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicy.kt +227 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +19 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicy.kt +94 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachine.kt +411 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateStore.kt +124 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +50 -0
  18. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerOwnerLossTest.kt +146 -0
  19. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusAudioSetupTest.kt +51 -0
  20. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +2 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicyTest.kt +65 -0
  22. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveAudioSetupReadinessTest.kt +231 -0
  23. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicyTest.kt +368 -0
  24. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +20 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicyTest.kt +136 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachineTest.kt +346 -0
  27. package/build/BluetoothSdk.types.d.ts +101 -0
  28. package/build/BluetoothSdk.types.d.ts.map +1 -1
  29. package/build/BluetoothSdk.types.js.map +1 -1
  30. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  31. package/build/_private/BluetoothSdkModule.js +1 -0
  32. package/build/_private/BluetoothSdkModule.js.map +1 -1
  33. package/build/generated/releaseMetadata.js +5 -5
  34. package/build/generated/releaseMetadata.js.map +1 -1
  35. package/build/index.d.ts +1 -1
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +3 -0
  38. package/build/index.js.map +1 -1
  39. package/ios/BluetoothSdkModule.swift +8 -2
  40. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  41. package/ios/Source/Bridge.swift +11 -3
  42. package/ios/Source/DeviceManager.swift +80 -6
  43. package/ios/Source/DeviceStore.swift +27 -0
  44. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  45. package/ios/Source/MentraBluetoothSDK.swift +10 -4
  46. package/ios/Source/errors/BluetoothSdkError.swift +5 -1
  47. package/ios/Source/sgcs/GlassesAudioSetupPolicy.swift +181 -0
  48. package/ios/Source/sgcs/MentraLive.swift +1053 -166
  49. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +260 -1
  50. package/ios/Source/sgcs/MentraLivePairingStateMachine.swift +385 -0
  51. package/ios/Source/sgcs/MentraLivePairingStateStore.swift +121 -0
  52. package/ios/Source/sgcs/SGCManager.swift +7 -0
  53. package/ios/Source/sgcs/Simulated.swift +2 -0
  54. package/ios/Source/status/DeviceStatus.swift +35 -0
  55. package/ios/Source/types/DeviceModels.swift +8 -1
  56. package/ios/Source/utils/AudioSessionMonitor.swift +158 -5
  57. package/ios/Tests/BluetoothAudioRouteTests.swift +82 -0
  58. package/ios/Tests/GlassesAudioSetupPolicyTests.swift +191 -0
  59. package/ios/Tests/MentraLiveAudioSetupTests.swift +215 -0
  60. package/ios/Tests/MentraLiveConnectionOptionsTests.swift +55 -0
  61. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +327 -0
  62. package/ios/Tests/MentraLivePairingStateMachineTests.swift +402 -0
  63. package/ios/Tests/MentraLiveSessionReadinessTests.swift +328 -26
  64. package/package.json +3 -2
  65. package/src/BluetoothSdk.types.ts +97 -0
  66. package/src/_private/BluetoothSdkModule.ts +1 -0
  67. package/src/generated/releaseMetadata.ts +5 -5
  68. package/src/index.ts +10 -0
package/MIGRATION.md ADDED
@@ -0,0 +1,61 @@
1
+ # Bluetooth SDK migration: Mentra Live pairing state
2
+
3
+ This note covers the public pairing API in `@mentra/bluetooth-sdk` `3.3.0` relative to the published `3.2.1` package. `3.3.0` is the current release family in `package.json` and is not on npm yet.
4
+
5
+ ## Version
6
+
7
+ The SDK version is `package.json` `"version"`. The iOS podspec (`ios/MentraBluetoothSDK.podspec`) and the Android Gradle module (`android/build.gradle`) read that same value. Swift package metadata is injected from it at pack time.
8
+
9
+ This repository keeps the Bluetooth SDK version aligned with the matching Mentra Live software release. It does not cut an independent major for a breaking SDK change. The break below ships in the unpublished `3.3.0` family. No further version bump was applied in this change. Integrators on `3.2.1` pick it up by moving to `3.3.0` when that release is published.
10
+
11
+ ## Removed
12
+
13
+ - `confirmReplaceOwner()` is removed.
14
+ - The `confirm_replace_owner_result` event and its types are removed.
15
+ - `owner_replaced` is deprecated and is never emitted. Remove listeners. Use `pairing_state_changed` or `GlassesStatus.pairingState`.
16
+ - `pairing_info` no longer includes `awaiting_confirm` or `retained_owner_link`.
17
+
18
+ ## Added
19
+
20
+ `GlassesStatus.pairingState` (absent on models other than Mentra Live):
21
+
22
+ ```ts
23
+ type MentraLivePairingState =
24
+ | {state: "normal"}
25
+ | {state: "pairing_mode"; windowId: number; endsAtMs: number}
26
+ | {state: "pairing_rejected"}
27
+ ```
28
+
29
+ | State | What to do |
30
+ | --- | --- |
31
+ | `normal` | Automatic reconnect is allowed. |
32
+ | `pairing_mode` | A pairing window is open. Leave reconnect paused until `endsAtMs`. That timestamp is the window plus a 30-second margin. `windowId` identifies the window and survives a cold start. |
33
+ | `pairing_rejected` | The glasses no longer accept this phone (another phone paired, or repeated authentication rejections on secure-pairing firmware). Reconnect has stopped. Show **Pair Again**. This clears only after a successful explicit `connect()` / `connectDefault()`, or after `forget()`. |
34
+
35
+ `useMentraBluetooth().glasses` does not include `pairingState`. Read `getGlassesStatus()`, `subscribeGlassesStatus()`, or the `pairing_state_changed` event (`{state: MentraLivePairingState}`).
36
+
37
+ `GlassesStatus.audioSetup` (every model) says whether the user has to do anything for glasses audio:
38
+
39
+ ```ts
40
+ type GlassesAudioSetupState =
41
+ | {state: "idle"}
42
+ | {state: "not_applicable"}
43
+ | {state: "connecting"}
44
+ | {state: "connected"}
45
+ | {state: "needs_user"; action: "connect_in_settings"}
46
+ ```
47
+
48
+ Render `needs_user` as a step that sends the user to Settings > Bluetooth, and treat `connected` and `not_applicable` as done. The SDK owns the waits (on iPhone: up to 8 seconds after a pair on firmware with `auto_audio`, none on older firmware, 10 seconds after a reconnect). Remove any app-side audio timers or `pairing_info.auto_audio` checks. Changes also arrive as `audio_setup_changed` (`{state: GlassesAudioSetupState}`).
49
+
50
+ Other additions:
51
+
52
+ - `entering_pairing_mode` includes `window_id` (same window as `pairingState.windowId`).
53
+ - `pairing_mode_ended` fires when the glasses close the window and this phone is still accepted. `reason` is `"paired"`, `"cancelled"`, or `"timeout"`. Optional `window_id`. The previous phone does not get this event when another phone pairs; it moves to `pairing_rejected`.
54
+ - `pairing_info.auto_audio` is true when the glasses derive Classic audio from the LE pair (CTKD). The SDK already uses it for `audioSetup`.
55
+ - `pair_failure.code` may be `"ios_forget_pairing"`: iOS holds a stale Bluetooth pairing for these glasses during an explicit attempt. The user must open Settings > Bluetooth, choose Forget This Device, then pair again. On Android and iOS native apps this arrives as the pairing-failure error's `failureCode`.
56
+
57
+ ## Behavior
58
+
59
+ On supported Mentra Live firmware, three presses of the power button open one 120-second pairing window. The glasses speak a 4-character code. Three presses again cancel the window. After a timeout or a cancel, the phone that was already paired reconnects automatically. If another phone pairs, that previous phone gets `pairing_rejected`. Pairing readiness needs no app-side `pairing_info` wait: the SDK reports the glasses ready only after secure firmware commits the bond, and treats glasses that send no `pairing_info` within 5 seconds as older firmware.
60
+
61
+ User-facing sample copy should say "Pair Again". Do not say "owner" in that copy. API fields such as `owner_retained`, `owner_committed`, and `ownership_transfer` are unchanged identifiers on `entering_pairing_mode` and `pairing_info`.
package/README.md CHANGED
@@ -448,6 +448,51 @@ await BluetoothSdk.clearDefaultDevice()
448
448
  await saveDeviceToYourAppStorage(null)
449
449
  ```
450
450
 
451
+ ## Mentra Live pairing
452
+
453
+ `confirmReplaceOwner()` and `confirm_replace_owner_result` are removed. `owner_replaced` is deprecated and is never emitted. The replacement is `GlassesStatus.pairingState`, also delivered as `pairing_state_changed`. Full steps are in [MIGRATION.md](./MIGRATION.md). The version stays `3.3.0` in `package.json` (podspec and Gradle read it). npm still publishes `3.2.1`.
454
+
455
+ On supported Mentra Live firmware, three power-button presses open one 120-second window and the glasses speak a 4-character code. Three presses again cancel it. The phone that was already paired pauses automatic reconnect for that window plus a 30-second margin (`pairingState.endsAtMs`), then reconnects after `pairing_mode_ended` with `reason` `"timeout"` or `"cancelled"`. If the glasses stop accepting this phone (another phone paired, or repeated authentication rejections on secure-pairing firmware), this phone's state becomes `pairing_rejected`: reconnect stops, and the UI should show Pair Again. That state clears only after a successful explicit pair or `forget()`.
456
+
457
+ `useMentraBluetooth().glasses` does not include `pairingState`. Read `getGlassesStatus()`, `subscribeGlassesStatus()`, or:
458
+
459
+ ```ts
460
+ BluetoothSdk.addListener("pairing_state_changed", (event) => {
461
+ if (event.state.state === "pairing_rejected") showPairAgain()
462
+ })
463
+
464
+ BluetoothSdk.addListener("pair_failure", (event) => {
465
+ if (event.code === "ios_forget_pairing") {
466
+ // iOS still has a Bluetooth pairing for these glasses.
467
+ // Settings > Bluetooth > Forget This Device, then pair again.
468
+ }
469
+ })
470
+ ```
471
+
472
+ `entering_pairing_mode` includes `window_id`. `pairing_info` includes `auto_audio` and no longer includes `awaiting_confirm` or `retained_owner_link`. Do not use the word "owner" in user-facing sample copy.
473
+
474
+ ## Glasses audio setup
475
+
476
+ `GlassesStatus.audioSetup`, also delivered as `audio_setup_changed` (`{state}`), says whether the user has to do anything to get glasses audio working. The SDK decides it for every glasses model. Render it; do not add your own audio timers or platform checks.
477
+
478
+ | `audioSetup.state` | Meaning | What to show |
479
+ | --- | --- | --- |
480
+ | `idle` | No glasses session | Nothing |
481
+ | `not_applicable` | No audio step (Android attaches audio from the BLE bond; glasses without Classic audio) | Nothing |
482
+ | `connecting` | Glasses connected, audio expected to come up on its own | A "connecting" state |
483
+ | `connected` | Classic audio (A2DP or HFP) is up | Nothing |
484
+ | `needs_user` | Audio did not come up on its own (`action: "connect_in_settings"`) | Ask the user to connect the glasses in Settings > Bluetooth |
485
+
486
+ On iPhone, after a pair the SDK waits up to 8 seconds on firmware that brings audio up from the BLE pair and reports `needs_user` at once on older firmware. After a reconnect it waits 10 seconds. The state moves to `connected` as soon as audio comes up. Mentra Live firmware that reports its own audio link (`classic_link`) is preferred over the phone's audio route, which is wrong while another Bluetooth audio device holds it.
487
+
488
+ ```ts
489
+ BluetoothSdk.addListener("audio_setup_changed", (event) => {
490
+ if (event.state.state === "needs_user") showConnectAudioInSettings()
491
+ })
492
+ ```
493
+
494
+ `useMentraBluetooth().glasses` does not include `audioSetup`. Read `getGlassesStatus()`, `subscribeGlassesStatus()`, or the event.
495
+
451
496
  ## Native notification controls
452
497
 
453
498
  G2 exposes firmware-owned notification history and popups. `getNativeNotificationStatus()` reports support, source (`phone` on Android or `ancs` on iOS), authorization, the desired controls, and submission/failure state. `submitted` means the controls were queued for the glasses, not that firmware acknowledged them. Unsupported drivers reject configuration instead of silently succeeding.
@@ -751,6 +796,8 @@ export function HardwareEventLogger() {
751
796
 
752
797
  For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscription API. Keep the returned subscription and call `remove()` when the listener is no longer needed.
753
798
 
799
+ Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `ota_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `mic_health`, `local_transcription`, `rgb_led_control_response`, `pair_failure`, `pairing_info`, `entering_pairing_mode`, `pairing_mode_ended`, `pairing_state_changed`, `audio_connected`, `audio_disconnected`, and `log`. `owner_replaced` remains in the type map and is never emitted.
800
+
754
801
  ### Claiming touchpad gestures
755
802
 
756
803
  On Mentra Live, disable a gesture's stock action before using it in your app:
@@ -310,7 +310,9 @@ class BluetoothSdkModule : Module() {
310
310
  }
311
311
 
312
312
  override fun onError(error: BluetoothError) {
313
- sendEvent("pair_failure", mapOf("error" to error.message))
313
+ val body = mutableMapOf<String, Any>("error" to error.message)
314
+ error.failureCode?.let { body["code"] = it }
315
+ sendEvent("pair_failure", body)
314
316
  }
315
317
 
316
318
  override fun onRawEvent(eventName: String, values: Map<String, Any>) {
@@ -379,6 +381,9 @@ class BluetoothSdkModule : Module() {
379
381
  "pair_failure",
380
382
  "pairing_info",
381
383
  "entering_pairing_mode",
384
+ "pairing_mode_ended",
385
+ "pairing_state_changed",
386
+ "audio_setup_changed",
382
387
  "owner_replaced",
383
388
  "audio_pairing_needed",
384
389
  "audio_connected",
@@ -125,9 +125,10 @@ public class Bridge private constructor() {
125
125
 
126
126
  /** Send pair failure event */
127
127
  @JvmStatic
128
- fun sendPairFailureEvent(error: String) {
128
+ fun sendPairFailureEvent(error: String, code: String? = null) {
129
129
  val data = HashMap<String, Any>()
130
130
  data["error"] = error
131
+ if (code != null) data["code"] = code
131
132
  sendTypedMessage("pair_failure", data as Map<String, Any>)
132
133
  }
133
134
 
@@ -138,6 +139,9 @@ public class Bridge private constructor() {
138
139
  classicBondReady: Boolean = false,
139
140
  securePairingCapable: Boolean = true,
140
141
  protocolVersion: Int = 1,
142
+ ownershipTransfer: Boolean = false,
143
+ ownerCommitted: Boolean = false,
144
+ autoAudio: Boolean = false,
141
145
  ) {
142
146
  val data = HashMap<String, Any>()
143
147
  data["had_previous_bond"] = hadPreviousBond
@@ -145,6 +149,9 @@ public class Bridge private constructor() {
145
149
  data["classic_bond_ready"] = classicBondReady
146
150
  data["secure_pairing_capable"] = securePairingCapable
147
151
  data["protocol_version"] = protocolVersion
152
+ data["ownership_transfer"] = ownershipTransfer
153
+ data["owner_committed"] = ownerCommitted
154
+ data["auto_audio"] = autoAudio
148
155
  sendTypedMessage("pairing_info", data as Map<String, Any>)
149
156
  }
150
157
 
@@ -23,6 +23,7 @@ import com.mentra.bluetoothsdk.sgcs.SceneElement
23
23
  import com.mentra.bluetoothsdk.sgcs.SceneFrame
24
24
  import com.mentra.bluetoothsdk.sgcs.Mach1
25
25
  import com.mentra.bluetoothsdk.sgcs.MentraLive
26
+ import com.mentra.bluetoothsdk.sgcs.MentraLivePairingStateStore
26
27
  import com.mentra.bluetoothsdk.sgcs.MentraNex
27
28
  import com.mentra.bluetoothsdk.sgcs.Nimo
28
29
  import com.mentra.bluetoothsdk.sgcs.NimoResourceDiagnostics
@@ -571,8 +572,10 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
571
572
 
572
573
  BluetoothAdapter.STATE_ON -> {
573
574
  Bridge.log("MAN: Bluetooth turned ON")
574
- // Auto-reconnect to last known device if we have one
575
- if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
575
+ // Auto-reconnect to last known device if we have one.
576
+ // After owner loss, connectById watches for the next pairing
577
+ // mode instead of opening GATT.
578
+ if (shouldReconnectAfterBluetoothOn()) {
576
579
  Bridge.log(
577
580
  "MAN: Bluetooth restored, attempting reconnect to: $deviceName"
578
581
  )
@@ -1509,6 +1512,8 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
1509
1512
  Bridge.log("MAN: SGC is null, returning")
1510
1513
  return
1511
1514
  }
1515
+ // Before clearPendingConnection(): the pending target is what marks an explicit pair.
1516
+ notePairingSessionReady()
1512
1517
 
1513
1518
  // A new identity must never inherit the previous glasses' address when
1514
1519
  // pairing supplied only a name. Same-device reconnects retain their cache.
@@ -2350,6 +2355,37 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
2350
2355
  sgc?.sendRgbLedControl(requestId, packageName, action, color, onDurationMs, offDurationMs, count)
2351
2356
  }
2352
2357
 
2358
+ internal fun shouldReconnectAfterBluetoothOn(): Boolean =
2359
+ defaultWearable.isNotEmpty() && deviceName.isNotEmpty()
2360
+
2361
+ /**
2362
+ * A ready Mentra Live session. Only an explicit pair (a pending target) clears
2363
+ * `pairing_rejected`; an automatic session only ends a pause and resets the streak.
2364
+ */
2365
+ internal fun notePairingSessionReady() {
2366
+ if (sgc !is MentraLive) return
2367
+ val explicitAttempt = pendingDeviceName.isNotEmpty() || pendingDeviceAddress.isNotEmpty()
2368
+ MentraLivePairingStateStore.update("device_ready") { it.onSessionReady(explicitAttempt) }
2369
+ }
2370
+
2371
+ /** Forget ends the pairing record. Also drops the pre-v4 owner-lost setting. */
2372
+ internal fun clearPairingStateOnForget() {
2373
+ MentraLivePairingStateStore.update("forget") { it.onForget() }
2374
+ if (DeviceStore.store.get("bluetooth", "mentra_live_owner_lost") as? Boolean != true) return
2375
+ DeviceStore.apply("bluetooth", "mentra_live_owner_lost", false)
2376
+ Bridge.saveSetting("mentra_live_owner_lost", false)
2377
+ }
2378
+
2379
+ /**
2380
+ * The saved glasses' firmware is no longer known once they are forgotten. The next glasses
2381
+ * report it again with their first `pairing_info`.
2382
+ */
2383
+ internal fun clearSecureFirmware() {
2384
+ if (DeviceStore.store.get("bluetooth", "mentra_live_secure_firmware") as? Boolean != true) return
2385
+ DeviceStore.apply("bluetooth", "mentra_live_secure_firmware", false)
2386
+ Bridge.saveSetting("mentra_live_secure_firmware", false)
2387
+ }
2388
+
2353
2389
  fun connectDefault() {
2354
2390
  if (NimoResourceDiagnostics.dispatchCapturedEntry { connectDefault() }) return
2355
2391
  if (defaultWearable.isEmpty()) {
@@ -2376,8 +2412,14 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
2376
2412
  if (!NimoResourceDiagnostics.allowManagerConnect(defaultWearable, reconnectTarget)) return
2377
2413
  initSGC(defaultWearable)
2378
2414
  val live = sgc as? MentraLive
2379
- if (live?.isPairingYieldActive() == true) {
2380
- Bridge.log("MAN: connectDefault skipped — Mentra Live pairing yield active")
2415
+ if (live != null && MentraLivePairingStateStore.isRejected) {
2416
+ Bridge.log("MAN: connectDefault skipped — Mentra Live pairing_rejected (Pair Again or forget)")
2417
+ return
2418
+ }
2419
+ if (live != null && MentraLivePairingStateStore.isPaused) {
2420
+ // connectById only records the target and watches for the window to end.
2421
+ Bridge.log("MAN: connectDefault deferred — Mentra Live pairing_mode")
2422
+ live.connectById(reconnectTarget)
2381
2423
  return
2382
2424
  }
2383
2425
  searching = true
@@ -2570,6 +2612,8 @@ class DeviceManager internal constructor(initializeHardware: Boolean) {
2570
2612
  connectionRecovery.clear()
2571
2613
  }
2572
2614
  Bridge.log("MAN: Forgetting smart glasses")
2615
+ clearPairingStateOnForget()
2616
+ clearSecureFirmware()
2573
2617
 
2574
2618
  val live = sgc as? MentraLive
2575
2619
  if (live != null) {
@@ -2,6 +2,7 @@ package com.mentra.bluetoothsdk
2
2
 
3
3
  import android.os.Handler
4
4
  import android.os.Looper
5
+ import com.mentra.bluetoothsdk.sgcs.GlassesAudioSetupPolicy
5
6
  import com.mentra.bluetoothsdk.utils.DeviceTypes
6
7
  import com.mentra.bluetoothsdk.utils.ConnTypes
7
8
  import com.mentra.bluetoothsdk.utils.MicMap
@@ -79,6 +80,8 @@ object DeviceStore {
79
80
  store.set("glasses", "micEnabled", false)
80
81
  store.set("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
81
82
  store.set("glasses", "bluetoothClassicConnected", false)
83
+ store.set("glasses", "bluetoothClassicProfilesConnected", false)
84
+ store.set("glasses", "bluetoothAudioRouted", false)
82
85
  store.set("glasses", "caseRemoved", true)
83
86
  store.set("glasses", "caseOpen", true)
84
87
  store.set("glasses", "caseCharging", false)
@@ -167,6 +170,16 @@ object DeviceStore {
167
170
  return store.get(category, key)
168
171
  }
169
172
 
173
+ /**
174
+ * `GlassesStatus.audioSetup` follows readiness on every model ([GlassesAudioSetupPolicy]), so
175
+ * the snapshot that changed `fullyBooted` already carries the new state. Announce it like
176
+ * `pairing_state_changed`, only when it changed.
177
+ */
178
+ private fun publishAudioSetupChange(wasReady: Boolean, ready: Boolean) {
179
+ val state = GlassesAudioSetupPolicy.change(wasReady, ready) ?: return
180
+ Bridge.sendTypedMessage("audio_setup_changed", mapOf("state" to state.toMap()))
181
+ }
182
+
170
183
  fun set(category: String, key: String, value: Any) {
171
184
  store.set(category, key, value)
172
185
  }
@@ -175,12 +188,20 @@ object DeviceStore {
175
188
  fun apply(category: String, key: String, value: Any) {
176
189
  val oldValue = store.get(category, key)
177
190
  store.set(category, key, value)
191
+ if (category == "glasses" && key == "bluetoothClassicConnected") {
192
+ // Android has no separate profile/route signal; keep both equal to the Classic audio state.
193
+ store.set(category, "bluetoothClassicProfilesConnected", value)
194
+ store.set(category, "bluetoothAudioRouted", value)
195
+ }
178
196
  val skipped = observableStoreWouldHaveSkipped(oldValue, value)
179
197
  val forceMicReset =
180
198
  category == ObservableStore.BLUETOOTH_CATEGORY && key == "mic_tuning" && isMicTuningReset(value)
181
199
  if (skipped && !forceMicReset) {
182
200
  return
183
201
  }
202
+ if (category == "glasses" && key == "fullyBooted" && value is Boolean) {
203
+ publishAudioSetupChange(oldValue as? Boolean ?: false, value)
204
+ }
184
205
 
185
206
  // Trigger hardware updates based on setting changes
186
207
  when (category to key) {
@@ -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.4.0"
6
- const val RELEASE_IDENTITY: String = "3.4.0-dev.993"
7
- const val RELEASE_SET_ID: String = "mentra-3.4.0-dev.993"
8
- const val SOURCE_COMMIT: String = "7fd0a5cfd9fb3db878f18f3975121c613a4b8fcc"
9
- const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.993.json"
10
- const val OTA_MANIFEST_SHA256: String = "fb9e951467dcb09ceca5e7b3c244f6e7c21f0fd61efadbf3c06d59134e272345"
6
+ const val RELEASE_IDENTITY: String = "3.4.0-dev.998"
7
+ const val RELEASE_SET_ID: String = "mentra-3.4.0-dev.998"
8
+ const val SOURCE_COMMIT: String = "c77108af50fcaabe3f2c1464b2a5d649a30e337b"
9
+ const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.998.json"
10
+ const val OTA_MANIFEST_SHA256: String = "8c797478019499beb9a637bf418ff6586fd974115f712dfb51c7a96f6e08151c"
11
11
  }
@@ -9,6 +9,7 @@ import android.os.Bundle
9
9
  import android.os.Handler
10
10
  import android.os.Looper
11
11
  import android.util.Log
12
+ import com.mentra.bluetoothsdk.sgcs.MentraLive
12
13
  import com.mentra.bluetoothsdk.sgcs.NimoResourceDiagnostics
13
14
  import com.mentra.bluetoothsdk.streaming.StreamSessionState
14
15
  import com.mentra.bluetoothsdk.utils.ControllerTypes
@@ -2110,6 +2111,7 @@ class MentraBluetoothSdk private constructor(
2110
2111
  BluetoothError(
2111
2112
  code = "pair_failure",
2112
2113
  message = data["error"] as? String ?: data.toString(),
2114
+ failureCode = data["code"] as? String,
2113
2115
  )
2114
2116
  )
2115
2117
  }
@@ -45,6 +45,11 @@ data class BluetoothError(
45
45
  val code: String,
46
46
  val message: String,
47
47
  val cause: Throwable? = null,
48
+ /**
49
+ * Stable machine-readable cause within [code], when the SDK can classify it. For
50
+ * `pair_failure`: `ios_forget_pairing` (iOS only).
51
+ */
52
+ val failureCode: String? = null,
48
53
  )
49
54
 
50
55
  enum class ScanStopReason {
@@ -0,0 +1,64 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ /**
4
+ * `GlassesStatus.audioSetup`: whether the user has to do anything to get glasses audio working.
5
+ * Native code encodes it as a map, `{"state": "idle"}` through
6
+ * `{"state": "needs_user", "action": "connect_in_settings"}`, the same shape as the TypeScript
7
+ * `GlassesAudioSetupState` and iOS.
8
+ */
9
+ internal enum class GlassesAudioSetupState(val state: String, val action: String? = null) {
10
+ /** No glasses session. */
11
+ IDLE("idle"),
12
+ /** No user step for audio on this platform and glasses. */
13
+ NOT_APPLICABLE("not_applicable"),
14
+ /** The link is up, audio is not, and the SDK still expects audio to come up on its own. */
15
+ CONNECTING("connecting"),
16
+ /** Classic audio to these glasses is up. */
17
+ CONNECTED("connected"),
18
+ /** Audio did not come up on its own. The user connects it in system Bluetooth settings. */
19
+ NEEDS_USER("needs_user", "connect_in_settings");
20
+
21
+ fun toMap(): Map<String, Any> =
22
+ if (action == null) mapOf("state" to state) else mapOf("state" to state, "action" to action)
23
+
24
+ companion object {
25
+ fun fromMap(raw: Any?): GlassesAudioSetupState? {
26
+ val state = (raw as? Map<*, *>)?.get("state") as? String ?: return null
27
+ return values().firstOrNull { it.state == state }
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The Android audio setup rule, for every glasses model including Mentra Live. Android attaches
34
+ * Classic audio from the BLE bond (CTKD on secure Mentra Live firmware, `createBond` on older
35
+ * firmware), and glasses without Classic audio have nothing to set up, so there is never a user
36
+ * step to offer: `not_applicable` while the glasses are ready, `idle` without a session. The
37
+ * shared status layer applies it, so the update that marks the glasses ready carries
38
+ * `not_applicable` with it. iOS keeps its timed Mentra Live rules in `GlassesAudioSetupPolicy.swift`.
39
+ */
40
+ internal object GlassesAudioSetupPolicy {
41
+ fun state(ready: Boolean): GlassesAudioSetupState =
42
+ if (ready) GlassesAudioSetupState.NOT_APPLICABLE else GlassesAudioSetupState.IDLE
43
+
44
+ /** The state `audio_setup_changed` announces when readiness moves, or null when it is unchanged. */
45
+ fun change(wasReady: Boolean, ready: Boolean): GlassesAudioSetupState? {
46
+ val after = state(ready)
47
+ return if (state(wasReady) == after) null else after
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Mentra Live firmware `{"type":"classic_link","a2dp":<bool>,"hfp":<bool>}`: whether the phone's
53
+ * A2DP and HFP (service-level) links to the glasses are up. iOS feeds it into its audio setup
54
+ * rules; Android's `audioSetup` does not depend on it. A missing or malformed field reads false.
55
+ */
56
+ internal data class MentraLiveClassicLink(val a2dp: Boolean, val hfp: Boolean) {
57
+ companion object {
58
+ fun parse(json: org.json.JSONObject): MentraLiveClassicLink =
59
+ MentraLiveClassicLink(
60
+ a2dp = json.optBoolean("a2dp", false),
61
+ hfp = json.optBoolean("hfp", false),
62
+ )
63
+ }
64
+ }