@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.
- package/MIGRATION.md +61 -0
- package/README.md +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +8 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +48 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +21 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicy.kt +64 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +788 -134
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicy.kt +227 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +19 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicy.kt +94 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachine.kt +411 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateStore.kt +124 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +50 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerOwnerLossTest.kt +146 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusAudioSetupTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +2 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicyTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveAudioSetupReadinessTest.kt +231 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicyTest.kt +368 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicyTest.kt +136 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachineTest.kt +346 -0
- package/build/BluetoothSdk.types.d.ts +101 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +11 -3
- package/ios/Source/DeviceManager.swift +80 -6
- package/ios/Source/DeviceStore.swift +27 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +10 -4
- package/ios/Source/errors/BluetoothSdkError.swift +5 -1
- package/ios/Source/sgcs/GlassesAudioSetupPolicy.swift +181 -0
- package/ios/Source/sgcs/MentraLive.swift +1053 -166
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +260 -1
- package/ios/Source/sgcs/MentraLivePairingStateMachine.swift +385 -0
- package/ios/Source/sgcs/MentraLivePairingStateStore.swift +121 -0
- package/ios/Source/sgcs/SGCManager.swift +7 -0
- package/ios/Source/sgcs/Simulated.swift +2 -0
- package/ios/Source/status/DeviceStatus.swift +35 -0
- package/ios/Source/types/DeviceModels.swift +8 -1
- package/ios/Source/utils/AudioSessionMonitor.swift +158 -5
- package/ios/Tests/BluetoothAudioRouteTests.swift +82 -0
- package/ios/Tests/GlassesAudioSetupPolicyTests.swift +191 -0
- package/ios/Tests/MentraLiveAudioSetupTests.swift +215 -0
- package/ios/Tests/MentraLiveConnectionOptionsTests.swift +55 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +327 -0
- package/ios/Tests/MentraLivePairingStateMachineTests.swift +402 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +328 -26
- package/package.json +3 -2
- package/src/BluetoothSdk.types.ts +97 -0
- package/src/_private/BluetoothSdkModule.ts +1 -0
- package/src/generated/releaseMetadata.ts +5 -5
- 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
|
-
|
|
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
|
-
|
|
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
|
|
2380
|
-
Bridge.log("MAN: connectDefault skipped — Mentra Live
|
|
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.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.4.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
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.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
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
|
+
}
|