@mentra/bluetooth-sdk 3.2.0-dev.265 → 3.2.1-beta.312

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 (54) hide show
  1. package/README.md +28 -0
  2. package/android/lc3Lib/build.gradle +5 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +35 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +34 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +30 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +20 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  8. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +12 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +7 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +4 -5
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +345 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +13 -0
  14. package/build/BluetoothSdk.types.d.ts +93 -1
  15. package/build/BluetoothSdk.types.d.ts.map +1 -1
  16. package/build/BluetoothSdk.types.js.map +1 -1
  17. package/build/_private/BluetoothSdkModule.d.ts +32 -1
  18. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  19. package/build/_private/BluetoothSdkModule.js +15 -1
  20. package/build/_private/BluetoothSdkModule.js.map +1 -1
  21. package/build/generated/changelogCatalog.d.ts +5 -2
  22. package/build/generated/changelogCatalog.d.ts.map +1 -1
  23. package/build/generated/changelogCatalog.js +6 -2
  24. package/build/generated/changelogCatalog.js.map +1 -1
  25. package/build/generated/releaseMetadata.js +6 -6
  26. package/build/generated/releaseMetadata.js.map +1 -1
  27. package/build/index.d.ts.map +1 -1
  28. package/build/index.js +1 -0
  29. package/build/index.js.map +1 -1
  30. package/ios/BluetoothSdkModule.swift +60 -0
  31. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  32. package/ios/Source/Bridge.swift +27 -0
  33. package/ios/Source/DeviceManager.swift +52 -50
  34. package/ios/Source/DeviceStore.swift +6 -0
  35. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  36. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  37. package/ios/Source/MentraBluetoothSDK.swift +11 -0
  38. package/ios/Source/audio/PcmStreamPlayer.swift +12 -0
  39. package/ios/Source/events/BluetoothEvents.swift +15 -0
  40. package/ios/Source/services/PhoneMic.swift +1 -3
  41. package/ios/Source/sgcs/MentraLive.swift +247 -1
  42. package/ios/Source/sgcs/SGCManager.swift +32 -0
  43. package/ios/Source/utils/AudioRouteObservation.swift +54 -0
  44. package/ios/Source/utils/AudioSessionMonitor.swift +20 -6
  45. package/ios/Tests/AudioRouteObservationTests.swift +142 -0
  46. package/ios/Tests/BluetoothAudioRouteTests.swift +37 -0
  47. package/ios/Tests/GalleryServerCommandTests.swift +87 -0
  48. package/ios/Tests/WearTuningDispatchTests.swift +51 -0
  49. package/package.json +2 -2
  50. package/src/BluetoothSdk.types.ts +99 -0
  51. package/src/_private/BluetoothSdkModule.ts +64 -1
  52. package/src/generated/changelogCatalog.ts +6 -2
  53. package/src/generated/releaseMetadata.ts +6 -6
  54. package/src/index.ts +1 -0
@@ -0,0 +1,54 @@
1
+ import Combine
2
+ import Foundation
3
+
4
+ /// Keeps audio readiness observable before microphone capture has been started.
5
+ final class AudioRouteObservation {
6
+ private var subscriptions = Set<AnyCancellable>()
7
+ private var pendingRecheck: DispatchWorkItem?
8
+ private var generation = 0
9
+ private let settlingDelays: [TimeInterval]
10
+ private let onChange: () -> Void
11
+
12
+ init(
13
+ center: NotificationCenter = .default,
14
+ names: [Notification.Name],
15
+ settlingDelays: [TimeInterval] = [0.2, 0.3, 0.5],
16
+ onChange: @escaping () -> Void
17
+ ) {
18
+ self.settlingDelays = settlingDelays
19
+ self.onChange = onChange
20
+ for name in names {
21
+ center.publisher(for: name)
22
+ .receive(on: DispatchQueue.main)
23
+ .sink { [weak self] _ in self?.refresh() }
24
+ .store(in: &subscriptions)
25
+ }
26
+ }
27
+
28
+ deinit {
29
+ pendingRecheck?.cancel()
30
+ }
31
+
32
+ /// Start a new settling window for a route, foreground, or selected-device
33
+ /// change. The callback must read the current target and route each time.
34
+ func refresh() {
35
+ dispatchPrecondition(condition: .onQueue(.main))
36
+ pendingRecheck?.cancel()
37
+ generation += 1
38
+ reconcile(generation: generation, nextDelay: 0)
39
+ }
40
+
41
+ private func reconcile(generation: Int, nextDelay: Int) {
42
+ guard generation == self.generation else { return }
43
+ onChange()
44
+ // availableInputs can lag the notification, including after removal.
45
+ // Reconcile at 200, 500, and 1000 ms even if the first read was ready.
46
+ // A reentrant refresh also supersedes this window.
47
+ guard generation == self.generation, nextDelay < settlingDelays.count else { return }
48
+ let recheck = DispatchWorkItem { [weak self] in
49
+ self?.reconcile(generation: generation, nextDelay: nextDelay + 1)
50
+ }
51
+ pendingRecheck = recheck
52
+ DispatchQueue.main.asyncAfter(deadline: .now() + settlingDelays[nextDelay], execute: recheck)
53
+ }
54
+ }
@@ -6,9 +6,10 @@
6
6
  // Used to detect when Mentra Live glasses are paired/connected for audio
7
7
  //
8
8
 
9
+ import Foundation
10
+
9
11
  #if !os(macOS)
10
12
  import AVFoundation
11
- import Foundation
12
13
  import UIKit
13
14
 
14
15
  class AudioSessionMonitor {
@@ -57,11 +58,14 @@ class AudioSessionMonitor {
57
58
  Bridge.log("AudioMonitor: Checking active route, output count: \(outputs.count)")
58
59
  for output in outputs {
59
60
  Bridge.log("AudioMonitor: - \(output.portName) (type: \(output.portType.rawValue))")
60
- if output.portType == .bluetoothHFP || output.portType == .bluetoothA2DP {
61
- if output.portName.localizedCaseInsensitiveContains(devicePattern) {
62
- Bridge.log("AudioMonitor: ✅ Found active audio device: \(output.portName)")
63
- return true
64
- }
61
+ if BluetoothAudioRoute.matches(
62
+ name: output.portName,
63
+ portType: output.portType.rawValue,
64
+ target: devicePattern,
65
+ isIOSAppOnMac: ProcessInfo.processInfo.isiOSAppOnMac
66
+ ) {
67
+ Bridge.log("AudioMonitor: ✅ Found active audio device: \(output.portName)")
68
+ return true
65
69
  }
66
70
  }
67
71
 
@@ -283,3 +287,13 @@ class AudioSessionMonitor {
283
287
  }
284
288
  }
285
289
  #endif
290
+
291
+ /// AVAudioSession preserves the route name on iOS-on-Mac but reports the
292
+ /// CoreAudio transport "Bluetooth" instead of iPhone's profile-specific port.
293
+ enum BluetoothAudioRoute {
294
+ static func matches(name: String, portType: String, target: String, isIOSAppOnMac: Bool) -> Bool {
295
+ guard !target.isEmpty, name.localizedCaseInsensitiveContains(target) else { return false }
296
+ return portType == "BluetoothHFP" || portType == "BluetoothA2DPOutput"
297
+ || (isIOSAppOnMac && portType == "Bluetooth")
298
+ }
299
+ }
@@ -0,0 +1,142 @@
1
+ import Foundation
2
+ @testable import MentraBluetoothSDK
3
+ import XCTest
4
+
5
+ final class AudioRouteObservationTests: XCTestCase {
6
+ @MainActor
7
+ func testInputsBecomingAvailableAfterNotificationAreReconciled() async {
8
+ let center = NotificationCenter()
9
+ let route = Notification.Name("test.audio.route")
10
+ var inputAvailable = false
11
+ var ready = false
12
+ let initialRead = expectation(description: "Initial unavailable input observed")
13
+ let changed = expectation(description: "Delayed input becomes ready without another event")
14
+ var firstRead = true
15
+ let observer = AudioRouteObservation(center: center, names: [route]) {
16
+ if firstRead {
17
+ firstRead = false
18
+ initialRead.fulfill()
19
+ }
20
+ if inputAvailable, !ready { changed.fulfill() }
21
+ ready = inputAvailable
22
+ }
23
+
24
+ center.post(name: route, object: nil)
25
+ await fulfillment(of: [initialRead], timeout: 2)
26
+ XCTAssertFalse(ready)
27
+ inputAvailable = true
28
+ await fulfillment(of: [changed], timeout: 1)
29
+ XCTAssertTrue(ready)
30
+ withExtendedLifetime(observer) {}
31
+ }
32
+
33
+ @MainActor
34
+ func testInitiallyReadyInputCanDisappearDuringSettling() async {
35
+ var inputAvailable = true
36
+ var ready = false
37
+ let disappeared = expectation(description: "Input removal observed during settling")
38
+ let observer = AudioRouteObservation(names: [], settlingDelays: [0.05, 0.05]) {
39
+ if ready, !inputAvailable { disappeared.fulfill() }
40
+ ready = inputAvailable
41
+ }
42
+ observer.refresh()
43
+ XCTAssertTrue(ready)
44
+ inputAvailable = false
45
+ await fulfillment(of: [disappeared], timeout: 2)
46
+ XCTAssertFalse(ready)
47
+ withExtendedLifetime(observer) {}
48
+ }
49
+
50
+ @MainActor
51
+ func testDeviceChangeReplacesPendingChecksAndReadsOnlyCurrentTarget() async {
52
+ var target = "first"
53
+ var availableInputs = Set<String>()
54
+ var observations: [(String, Bool)] = []
55
+ var secondChecks = 0
56
+ let settled = expectation(description: "Replacement window completed")
57
+ let observer = AudioRouteObservation(names: [], settlingDelays: [0.05, 0.05, 0.1]) {
58
+ observations.append((target, availableInputs.contains(target)))
59
+ if target == "second" {
60
+ secondChecks += 1
61
+ if secondChecks == 2 {
62
+ // The old target is available, but its readiness must not leak.
63
+ XCTAssertFalse(observations.contains { $0.1 })
64
+ availableInputs.insert("second")
65
+ }
66
+ if secondChecks == 4 { settled.fulfill() }
67
+ }
68
+ }
69
+ observer.refresh()
70
+ target = "second"
71
+ observer.refresh() // Same entry point used by DeviceStore's device_name change.
72
+ availableInputs.insert("first")
73
+ await fulfillment(of: [settled], timeout: 2)
74
+ XCTAssertEqual(observations.filter { $0.0 == "first" }.count, 1)
75
+ XCTAssertEqual(observations.filter { $0.0 == "second" }.count, 4)
76
+ XCTAssertEqual(observations.last?.0, "second")
77
+ XCTAssertEqual(observations.last?.1, true)
78
+ withExtendedLifetime(observer) {}
79
+ }
80
+
81
+ @MainActor
82
+ func testUnavailableInputStopsRecheckingAfterBoundedWindow() async {
83
+ var checks = 0
84
+ let settled = expectation(description: "Immediate read and two scheduled rechecks completed")
85
+ let extraCheck = expectation(description: "No rechecks beyond the configured window")
86
+ extraCheck.isInverted = true
87
+ let observer = AudioRouteObservation(names: [], settlingDelays: [0.02, 0.02]) {
88
+ checks += 1
89
+ if checks == 3 { settled.fulfill() }
90
+ if checks > 3 { extraCheck.fulfill() }
91
+ }
92
+ observer.refresh()
93
+ await fulfillment(of: [settled], timeout: 2)
94
+ XCTAssertEqual(checks, 3)
95
+ await fulfillment(of: [extraCheck], timeout: 0.1)
96
+ XCTAssertEqual(checks, 3)
97
+ withExtendedLifetime(observer) {}
98
+ }
99
+
100
+ @MainActor
101
+ func testReleasingObserverCancelsPendingRechecks() async throws {
102
+ var checks = 0
103
+ var observer: AudioRouteObservation? = AudioRouteObservation(names: [], settlingDelays: [0.02]) {
104
+ checks += 1
105
+ }
106
+ observer?.refresh()
107
+ XCTAssertEqual(checks, 1)
108
+ observer = nil
109
+ try await Task.sleep(nanoseconds: 100_000_000)
110
+ XCTAssertEqual(checks, 1)
111
+ }
112
+
113
+ func testObservesRouteAndForegroundWithoutStartingAMicrophone() async {
114
+ let center = NotificationCenter()
115
+ let route = Notification.Name("test.audio.route")
116
+ let foreground = Notification.Name("test.audio.foreground")
117
+ let changed = expectation(description: "Both readiness events delivered")
118
+ changed.expectedFulfillmentCount = 2
119
+ let observer = AudioRouteObservation(center: center, names: [route, foreground]) {
120
+ XCTAssertTrue(Thread.isMainThread)
121
+ changed.fulfill()
122
+ }
123
+ center.post(name: route, object: nil)
124
+ center.post(name: foreground, object: nil)
125
+ await fulfillment(of: [changed], timeout: 1)
126
+ withExtendedLifetime(observer) {}
127
+ }
128
+
129
+ func testReleasingObserverStopsReadinessCallbacks() async {
130
+ let center = NotificationCenter()
131
+ let route = Notification.Name("test.audio.route")
132
+ let changed = expectation(description: "Released observer is silent")
133
+ changed.isInverted = true
134
+ var observer: AudioRouteObservation? = AudioRouteObservation(center: center, names: [route]) {
135
+ changed.fulfill()
136
+ }
137
+ XCTAssertNotNil(observer)
138
+ observer = nil
139
+ center.post(name: route, object: nil)
140
+ await fulfillment(of: [changed], timeout: 0.1)
141
+ }
142
+ }
@@ -0,0 +1,37 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ final class BluetoothAudioRouteTests: XCTestCase {
5
+ func testRecognizesIPhoneProfiles() {
6
+ for port in ["BluetoothHFP", "BluetoothA2DPOutput"] {
7
+ XCTAssertTrue(BluetoothAudioRoute.matches(
8
+ name: "Mentra_Live_03BE", portType: port, target: "03BE", isIOSAppOnMac: false
9
+ ))
10
+ }
11
+ }
12
+
13
+ func testRecognizesObservedMacTransportOnlyOnMac() {
14
+ XCTAssertTrue(BluetoothAudioRoute.matches(
15
+ name: "Mentra_Live_03BE", portType: "Bluetooth", target: "mentra_live_03be", isIOSAppOnMac: true
16
+ ))
17
+ XCTAssertFalse(BluetoothAudioRoute.matches(
18
+ name: "Mentra_Live_03BE", portType: "Bluetooth", target: "03BE", isIOSAppOnMac: false
19
+ ))
20
+ }
21
+
22
+ func testRejectsDifferentGlassesAndMissingIdentity() {
23
+ for target in ["023B", ""] {
24
+ XCTAssertFalse(BluetoothAudioRoute.matches(
25
+ name: "Mentra_Live_03BE", portType: "Bluetooth", target: target, isIOSAppOnMac: true
26
+ ))
27
+ }
28
+ }
29
+
30
+ func testRejectsNonBluetoothDeviceWithMatchingName() {
31
+ for port in ["Speaker", "USBAudio", "BuiltInMic", ""] {
32
+ XCTAssertFalse(BluetoothAudioRoute.matches(
33
+ name: "Mentra_Live_03BE", portType: port, target: "03BE", isIOSAppOnMac: true
34
+ ))
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,87 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ /// Replace only the radio handoff; exercise the public SDK, device dispatch,
5
+ /// command serialization, receive parser, event bridge, and pending request resolver.
6
+ @MainActor
7
+ private final class GalleryCommandTransport: MentraLive {
8
+ var respond: (([String: Any]) -> [[String: Any]])?
9
+ var requestIds: [String] = []
10
+
11
+ override func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool, requireAck: Bool) {
12
+ XCTAssertEqual(jsonOriginal["type"] as? String, "set_gallery_server_enabled")
13
+ XCTAssertTrue(wakeUp)
14
+ XCTAssertTrue(requireAck)
15
+ do {
16
+ let bytes = try JSONSerialization.data(withJSONObject: jsonOriginal)
17
+ let command = try XCTUnwrap(JSONSerialization.jsonObject(with: bytes) as? [String: Any])
18
+ try requestIds.append(XCTUnwrap(command["request_id"] as? String))
19
+ for response in respond?(command) ?? [] {
20
+ try processReceivedData(JSONSerialization.data(withJSONObject: response))
21
+ }
22
+ } catch {
23
+ XCTFail("Could not replay gallery command: \(error)")
24
+ }
25
+ }
26
+ }
27
+
28
+ @MainActor
29
+ final class GalleryServerCommandTests: XCTestCase {
30
+ func testEnableOfflineEnableAndDisableRoundTripThroughNativeBridge() async throws {
31
+ let previous = DeviceManager.shared.sgc
32
+ let transport = GalleryCommandTransport()
33
+ DeviceManager.shared.sgc = transport
34
+ let sdk = MentraBluetoothSDK()
35
+ defer {
36
+ sdk.invalidate()
37
+ DeviceManager.shared.sgc = previous
38
+ }
39
+
40
+ for (enabled, listening) in [(true, true), (true, false), (false, false)] {
41
+ transport.respond = { command in
42
+ XCTAssertEqual(command["enabled"] as? Bool, enabled)
43
+ var response: [String: Any] = [
44
+ "type": "settings_ack", "setting": "gallery_server", "status": "applied",
45
+ "request_id": command["request_id"]!, "timestamp": 123,
46
+ "enabled": enabled, "listening": listening,
47
+ ]
48
+ if listening { response["url"] = "http://10.1.2.3:8089" }
49
+ // A response to another request must not resolve this call.
50
+ var unrelated = response
51
+ unrelated["request_id"] = "unrelated-request"
52
+ unrelated["enabled"] = !enabled
53
+ return [unrelated, response]
54
+ }
55
+ let ack = try await sdk.setGalleryServerEnabled(enabled)
56
+ XCTAssertEqual(ack.enabled, enabled)
57
+ XCTAssertEqual(ack.listening, listening)
58
+ XCTAssertEqual(ack.url, listening ? "http://10.1.2.3:8089" : nil)
59
+ XCTAssertEqual(ack.requestId, transport.requestIds.last)
60
+ }
61
+ XCTAssertEqual(Set(transport.requestIds).count, 3)
62
+ }
63
+
64
+ func testStorageErrorRejectsThePublicSdkRequest() async {
65
+ let previous = DeviceManager.shared.sgc
66
+ let transport = GalleryCommandTransport()
67
+ DeviceManager.shared.sgc = transport
68
+ let sdk = MentraBluetoothSDK()
69
+ defer {
70
+ sdk.invalidate()
71
+ DeviceManager.shared.sgc = previous
72
+ }
73
+ transport.respond = { command in
74
+ [["type": "settings_ack", "setting": "gallery_server", "status": "error",
75
+ "request_id": command["request_id"]!, "error_code": "settings_unavailable",
76
+ "error_message": "Could not persist the gallery server setting."]]
77
+ }
78
+ do {
79
+ _ = try await sdk.setGalleryServerEnabled(true)
80
+ XCTFail("A failed save must reject the SDK request")
81
+ } catch let error as BluetoothSdkError {
82
+ XCTAssertEqual(error.code, "settings_unavailable")
83
+ } catch {
84
+ XCTFail("Unexpected error: \(error)")
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,51 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ /// Recording subclass used to prove wear methods dispatched through an
5
+ /// `SGCManager?` hit MentraLive, not the extension no-op.
6
+ @MainActor
7
+ private final class RecordingMentraLive: MentraLive {
8
+ private(set) var calls: [String] = []
9
+
10
+ override func queryWearState() {
11
+ calls.append("queryWearState")
12
+ }
13
+
14
+ override func setWearReporting(_ enabled: Bool) {
15
+ calls.append("setWearReporting:\(enabled)")
16
+ }
17
+
18
+ override func setWearTuning(intervalMs: Int, count: Int, majority: Int) {
19
+ calls.append("setWearTuning:\(intervalMs),\(count),\(majority)")
20
+ }
21
+
22
+ override func requestWearTuning() {
23
+ calls.append("requestWearTuning")
24
+ }
25
+
26
+ override func resetWearTuning() {
27
+ calls.append("resetWearTuning")
28
+ }
29
+ }
30
+
31
+ @MainActor
32
+ final class WearTuningDispatchTests: XCTestCase {
33
+ func testWearMethodsDispatchThroughSGCManagerReference() {
34
+ let live = RecordingMentraLive()
35
+ let sgc: (any SGCManager)? = live
36
+
37
+ sgc?.queryWearState()
38
+ sgc?.setWearReporting(true)
39
+ sgc?.setWearTuning(intervalMs: 300, count: 5, majority: 4)
40
+ sgc?.requestWearTuning()
41
+ sgc?.resetWearTuning()
42
+
43
+ XCTAssertEqual(live.calls, [
44
+ "queryWearState",
45
+ "setWearReporting:true",
46
+ "setWearTuning:300,5,4",
47
+ "requestWearTuning",
48
+ "resetWearTuning",
49
+ ])
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.2.0-dev.265",
3
+ "version": "3.2.1-beta.312",
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.0-dev.265"
117
+ "@mentra/cloud-protocol": "3.2.1-beta.312"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@types/node": "^25.9.3",
@@ -481,6 +481,84 @@ export type SwitchStatusEvent = {
481
481
  timestamp: number
482
482
  }
483
483
 
484
+ /**
485
+ * Mentra Live center-mic gate + ADC gain overrides. Internal SDK surface: this
486
+ * is a super-mode tuning aid, not a public capability.
487
+ *
488
+ * Every field is optional; an omitted field keeps the firmware default. The
489
+ * glasses hold these in RAM only and drop them on disconnect, so the phone
490
+ * re-sends on every connect.
491
+ */
492
+ export type MicTuning = {
493
+ /** codec_adc_vol index, 0-15. 15 is +32 dB, the top of the table. */
494
+ gain?: number
495
+ /** Gate open / close thresholds, linear RMS on int16 samples. */
496
+ open?: number
497
+ close?: number
498
+ /** Frames of 10 ms. */
499
+ attack?: number
500
+ hang?: number
501
+ /** Thresholds used while the speaker is active or within its hold-off. */
502
+ sp_open?: number
503
+ sp_close?: number
504
+ sp_hold?: number
505
+ }
506
+
507
+ /**
508
+ * What the glasses report as actually in force, after their own clamping. Use
509
+ * this rather than the requested value when showing applied settings.
510
+ */
511
+ export type MicTuningStateEvent = MicTuning & {
512
+ type: "mic_tuning_state"
513
+ /** Tuning revision; increments on every accepted set or reset. */
514
+ generation: number
515
+ /** True when any override is in force. */
516
+ overridden: boolean
517
+ }
518
+
519
+ /** Live center-mic level. Disposable: samples are dropped under any pressure. */
520
+ export type MicRmsEvent = {
521
+ type: "mic_rms"
522
+ rms: number
523
+ gateOpen: boolean
524
+ speakerElevated: boolean
525
+ /** Tuning revision the sample was measured under. */
526
+ generation: number
527
+ }
528
+
529
+ /**
530
+ * Mentra Live wear-detection vote. Internal SDK surface: Super Mode only.
531
+ * Values live in RAM on the glasses and reset on disconnect.
532
+ */
533
+ export type WearTuning = {
534
+ /** Poll period in milliseconds. Firmware clamp: 50–2000. */
535
+ interval?: number
536
+ /** Sliding-window length. Firmware clamp: 3–15. */
537
+ count?: number
538
+ /** Votes needed to flip, either direction. Must be > count/2 and ≤ count. */
539
+ majority?: number
540
+ }
541
+
542
+ /** Current wear vote from sr_wrst, or an unsolicited wear transition. */
543
+ export type WearStateEvent = {
544
+ type: "wear_state"
545
+ worn: boolean
546
+ }
547
+
548
+ /**
549
+ * What the wear poll loop is actually running. A rejected patch echoes the
550
+ * unchanged values with `accepted: false`.
551
+ */
552
+ export type WearTuningEvent = WearTuning & {
553
+ type: "wear_tuning"
554
+ enabled: boolean
555
+ interval: number
556
+ count: number
557
+ majority: number
558
+ generation: number
559
+ accepted: boolean
560
+ }
561
+
484
562
  export type RgbLedControlResponseEvent =
485
563
  | {
486
564
  type: "rgb_led_control_response"
@@ -499,6 +577,7 @@ export type RgbLedControlSuccessResponseEvent = Extract<RgbLedControlResponseEve
499
577
  export type SettingsAckStatus = "applied" | "ready" | "error" | "failed" | "failure" | "rejected"
500
578
 
501
579
  export type SettingsAckSetting =
580
+ | "gallery_server"
502
581
  | "gallery_mode"
503
582
  | "button_photo"
504
583
  | "button_video_recording"
@@ -524,6 +603,10 @@ export type SettingsAckEvent = {
524
603
  fps?: number
525
604
  enabled?: boolean
526
605
  minutes?: number
606
+ /** Current site-network listener state; present for gallery_server acknowledgements. */
607
+ listening?: boolean
608
+ /** Current HTTP base URL; present for gallery_server only while listening. */
609
+ url?: string
527
610
  /** ANR enabled flag; present when setting === "camera_tuning" */
528
611
  anr?: boolean
529
612
  /** Stock-gain flag; present when setting === "camera_tuning" */
@@ -1066,6 +1149,10 @@ export type BluetoothSdkModuleEvents = {
1066
1149
  heartbeat_received: (event: HeartbeatReceivedEvent) => void
1067
1150
  swipe_volume_status: (event: SwipeVolumeStatusEvent) => void
1068
1151
  switch_status: (event: SwitchStatusEvent) => void
1152
+ mic_tuning_state: (event: MicTuningStateEvent) => void
1153
+ mic_rms: (event: MicRmsEvent) => void
1154
+ wear_state: (event: WearStateEvent) => void
1155
+ wear_tuning: (event: WearTuningEvent) => void
1069
1156
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void
1070
1157
  settings_ack: (event: SettingsAckEvent) => void
1071
1158
  pair_failure: (event: PairFailureEvent) => void
@@ -1226,6 +1313,10 @@ export type BluetoothSdkEventMap = {
1226
1313
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
1227
1314
  swipe_volume_status: SwipeVolumeStatusEvent
1228
1315
  switch_status: SwitchStatusEvent
1316
+ mic_tuning_state: MicTuningStateEvent
1317
+ mic_rms: MicRmsEvent
1318
+ wear_state: WearStateEvent
1319
+ wear_tuning: WearTuningEvent
1229
1320
  rgb_led_control_response: RgbLedControlResponseEvent
1230
1321
  settings_ack: SettingsAckEvent
1231
1322
  pair_failure: PairFailureEvent
@@ -1317,6 +1408,12 @@ export interface BluetoothSdkPublicModule {
1317
1408
  /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1318
1409
  setWifiAdbState(enabled: boolean): Promise<void>
1319
1410
 
1411
+ /**
1412
+ * Persistently enable/disable unauthenticated HTTP gallery access on the glasses' Wi-Fi.
1413
+ * Default off; survives reconnects/restarts until disabled. Only use on trusted networks.
1414
+ * Returns saved enabled state plus current listening/url. No Wi-Fi means listening=false.
1415
+ */
1416
+ setGalleryServerEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1320
1417
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1321
1418
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
1322
1419
  setLoudnessGateEnabled(enabled: boolean): Promise<void>
@@ -1701,6 +1798,8 @@ export type BluetoothSettingsUpdate = Partial<{
1701
1798
  gallery_mode: boolean
1702
1799
  voice_activity_detection_enabled: boolean
1703
1800
  loudness_gate_enabled: boolean
1801
+ /** Effective mic tuning only. `{}` means "reset to firmware defaults". */
1802
+ mic_tuning: MicTuning
1704
1803
  button_photo_size: ButtonPhotoSize
1705
1804
  button_video_settings: {width: number; height: number; fps: number}
1706
1805
  button_video_width: number
@@ -33,6 +33,7 @@ import {
33
33
  NativePhoneNotification,
34
34
  NativeNotificationConfig,
35
35
  NativeNotificationStatus,
36
+ MicTuning,
36
37
  ObservableStoreCategory,
37
38
  OtaQueryResult,
38
39
  OtaStartAckEvent,
@@ -139,10 +140,41 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
139
140
  logCurrentWifiFrequency(): Promise<void>
140
141
 
141
142
  // Gallery Commands
143
+ setGalleryServerEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
142
144
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
143
145
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
144
146
  /** Mentra Live center-mic loudness / Barrier gate (cs_swit type 10). */
145
147
  setLoudnessGateEnabled(enabled: boolean): Promise<void>
148
+ /**
149
+ * Mentra Live mic tuning. `null` clears every override and returns the
150
+ * glasses to firmware defaults.
151
+ *
152
+ * Deliberately absent from the public SDK surface: it is a super-mode
153
+ * calibration aid whose values only make sense next to a live RMS readout.
154
+ */
155
+ setMicTuning(tuning: MicTuning | null): Promise<void>
156
+ /**
157
+ * Ask the glasses to report the tuning they are actually running. The answer
158
+ * arrives as a `mic_tuning_state` event, which is also emitted unprompted
159
+ * after every set, so a screen that subscribes on mount and calls this on
160
+ * focus never has to guess.
161
+ */
162
+ requestMicTuningState(): Promise<void>
163
+ /** Enable or disable the `mic_rms` readout. Auto-disabled on disconnect. */
164
+ setMicRmsTelemetry(enabled: boolean): Promise<void>
165
+ /**
166
+ * Mentra Live wear detection. Internal / Super Mode only.
167
+ *
168
+ * Reporting is RAM-only on the glasses and must not go through `cs_swit`
169
+ * type 1 (that bit is NV-backed). `resetWearTuning` both restores the
170
+ * firmware vote defaults and turns reporting off.
171
+ */
172
+ queryWearState(): Promise<void>
173
+ setWearReporting(enabled: boolean): Promise<void>
174
+ /** Negative values leave that field unchanged. */
175
+ setWearTuning(intervalMs: number, count: number, majority: number): Promise<void>
176
+ requestWearTuning(): Promise<void>
177
+ resetWearTuning(): Promise<void>
146
178
  /**
147
179
  * @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
148
180
  * `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop).
@@ -546,6 +578,22 @@ NativeBluetoothSdkModule.setLoudnessGateEnabled = function (enabled: boolean) {
546
578
  return this.updateBluetoothSettings({loudness_gate_enabled: enabled})
547
579
  }
548
580
 
581
+ NativeBluetoothSdkModule.setMicTuning = function (tuning: MicTuning | null) {
582
+ // `{}` rather than null: the store drops null writes, and the native side
583
+ // reads an empty object as "no overrides" and sends an explicit reset.
584
+ return this.updateBluetoothSettings({mic_tuning: tuning ?? {}})
585
+ }
586
+
587
+ const nativeMicModule = NativeBluetoothSdkModule as unknown as Record<string, unknown>
588
+ NativeBluetoothSdkModule.requestMicTuningState = bindNativeMethod<() => Promise<void>>(
589
+ nativeMicModule,
590
+ "requestMicTuningState",
591
+ )
592
+ NativeBluetoothSdkModule.setMicRmsTelemetry = bindNativeMethod<(enabled: boolean) => Promise<void>>(
593
+ nativeMicModule,
594
+ "setMicRmsTelemetry",
595
+ )
596
+
549
597
  const nativeSetCameraFov = bindNativeMethod<(fov: CameraFovSetting) => MaybePromise<CameraFovResult>>(
550
598
  NativeBluetoothSdkModule as unknown as Record<string, unknown>,
551
599
  "setCameraFov",
@@ -630,7 +678,7 @@ const nativeStopScan = NativeBluetoothSdkModule.stopScan.bind(NativeBluetoothSdk
630
678
  const activeScanCancellations = new Set<() => Promise<void>>()
631
679
  NativeBluetoothSdkModule.stopScan = async function () {
632
680
  if (activeScanCancellations.size === 0) return nativeStopScan()
633
- await Promise.all([...activeScanCancellations].map(cancel => cancel()))
681
+ await Promise.all([...activeScanCancellations].map((cancel) => cancel()))
634
682
  }
635
683
 
636
684
  NativeBluetoothSdkModule.scan = async function (modelOrOptions: DeviceModel | ScanOptions, options?: ScanModelOptions) {
@@ -678,5 +726,20 @@ NativeBluetoothSdkModule.startStream = function (params: StreamStartRequest) {
678
726
  return nativeStartStream(streamRequestParamsForNative(params) as unknown as StreamStartRequest)
679
727
  }
680
728
 
729
+ const nativeWearModule = NativeBluetoothSdkModule as unknown as Record<string, unknown>
730
+ NativeBluetoothSdkModule.queryWearState = bindNativeMethod<() => Promise<void>>(nativeWearModule, "queryWearState")
731
+ NativeBluetoothSdkModule.setWearReporting = bindNativeMethod<(enabled: boolean) => Promise<void>>(
732
+ nativeWearModule,
733
+ "setWearReporting",
734
+ )
735
+ NativeBluetoothSdkModule.setWearTuning = bindNativeMethod<
736
+ (intervalMs: number, count: number, majority: number) => Promise<void>
737
+ >(nativeWearModule, "setWearTuning")
738
+ NativeBluetoothSdkModule.requestWearTuning = bindNativeMethod<() => Promise<void>>(
739
+ nativeWearModule,
740
+ "requestWearTuning",
741
+ )
742
+ NativeBluetoothSdkModule.resetWearTuning = bindNativeMethod<() => Promise<void>>(nativeWearModule, "resetWearTuning")
743
+
681
744
  export default NativeBluetoothSdkModule
682
745
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkInternalModule