@mentra/bluetooth-sdk 3.2.1-dev.277 → 3.2.1-dev.279

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 (29) hide show
  1. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +31 -0
  2. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +34 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +20 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  6. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +337 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +13 -0
  8. package/build/BluetoothSdk.types.d.ts +82 -0
  9. package/build/BluetoothSdk.types.d.ts.map +1 -1
  10. package/build/BluetoothSdk.types.js.map +1 -1
  11. package/build/_private/BluetoothSdkModule.d.ts +31 -1
  12. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  13. package/build/_private/BluetoothSdkModule.js +14 -0
  14. package/build/_private/BluetoothSdkModule.js.map +1 -1
  15. package/build/generated/releaseMetadata.js +5 -5
  16. package/build/generated/releaseMetadata.js.map +1 -1
  17. package/ios/BluetoothSdkModule.swift +55 -0
  18. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  19. package/ios/Source/Bridge.swift +27 -0
  20. package/ios/Source/DeviceManager.swift +20 -0
  21. package/ios/Source/DeviceStore.swift +6 -0
  22. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  23. package/ios/Source/sgcs/MentraLive.swift +237 -0
  24. package/ios/Source/sgcs/SGCManager.swift +32 -0
  25. package/ios/Tests/WearTuningDispatchTests.swift +51 -0
  26. package/package.json +2 -2
  27. package/src/BluetoothSdk.types.ts +88 -0
  28. package/src/_private/BluetoothSdkModule.ts +62 -0
  29. package/src/generated/releaseMetadata.ts +5 -5
@@ -1060,6 +1060,10 @@ extension MentraLive: CBCentralManagerDelegate {
1060
1060
  self.readinessCompletedThisBleSession = false
1061
1061
  self.updateConnectionState(ConnTypes.DISCONNECTED)
1062
1062
  self.rgbLedAuthorityClaimed = false
1063
+ // The glasses reset mic and wear tuning on BLE disconnect, so the
1064
+ // next link must be allowed to send them again.
1065
+ self.lastSentMicTuningBody = nil
1066
+ self.wearTuningQueriedThisLink = false
1063
1067
 
1064
1068
  self.stopAllTimers()
1065
1069
  self.closeL2capFileChannel()
@@ -1698,6 +1702,21 @@ class MentraLive: NSObject, SGCManager {
1698
1702
  private var ancsRelayEnableRequested = false
1699
1703
  private var peerWireCapsBinary = false
1700
1704
  private var peerFilePayloadV2 = false
1705
+ /// Firmware understands cs_mictun / cs_micst / cs_micrms. Nothing mic-tuning
1706
+ /// related goes on the wire until this is seen.
1707
+ private var peerMicTuning = false
1708
+ /// Firmware understands cs_weartun / cs_wearst. Wear reporting stays off
1709
+ /// until Super Mode asks for it; this flag only gates those commands.
1710
+ private var peerWearTuning = false
1711
+ /// Tuning generation echoed by the last sr_mictun / sr_micst. An sr_micrms
1712
+ /// measured before that revision describes a config we already replaced.
1713
+ private var micTuningGeneration = 0
1714
+ /// Connect-time dedupe, mirroring Android. Several paths push mic tuning
1715
+ /// when a link comes up and the wire_caps parse re-runs per glasses_ready;
1716
+ /// the glasses only forget tuning on BLE disconnect, so an identical body
1717
+ /// within one link is dropped. Both reset in didDisconnectPeripheral.
1718
+ private var lastSentMicTuningBody: String?
1719
+ private var wearTuningQueriedThisLink = false
1701
1720
  /// Last observed glasses process session id (`sid` in glasses_ready / version_info_1).
1702
1721
  /// The BES keeps the BLE link alive across asg_client restarts, so transport state
1703
1722
  /// cannot signal a restart - a CHANGED (or newly appearing) sid is the restart signal.
@@ -3427,6 +3446,35 @@ class MentraLive: NSObject, SGCManager {
3427
3446
  )
3428
3447
  }
3429
3448
 
3449
+ case "sr_mictun", "sr_micst":
3450
+ if let body = k900ParseBody(json["B"]) {
3451
+ handleMicTuningState(body)
3452
+ }
3453
+
3454
+ case "sr_weartun":
3455
+ if let body = k900ParseBody(json["B"]) {
3456
+ handleWearTuningState(json, body)
3457
+ }
3458
+
3459
+ case "sr_wrst":
3460
+ if let body = k900ParseBody(json["B"]) {
3461
+ Bridge.sendWearState(worn: (k900JsonInt(body, "on") ?? 0) != 0)
3462
+ }
3463
+
3464
+ case "sr_micrms":
3465
+ if let body = k900ParseBody(json["B"]) {
3466
+ let generation = k900JsonInt(body, "gen") ?? 0
3467
+ // Measured under a config we have already replaced.
3468
+ if generation >= micTuningGeneration {
3469
+ Bridge.sendMicRms(
3470
+ rms: k900JsonInt(body, "rms") ?? 0,
3471
+ gateOpen: (k900JsonInt(body, "gate") ?? 0) != 0,
3472
+ speakerElevated: (k900JsonInt(body, "sp") ?? 0) != 0,
3473
+ generation: generation
3474
+ )
3475
+ }
3476
+ }
3477
+
3430
3478
  case "sr_shut":
3431
3479
  Bridge.log("K900 shutdown command received - glasses shutting down")
3432
3480
  // Mark as killed to prevent reconnection attempts
@@ -6049,6 +6097,9 @@ extension MentraLive {
6049
6097
  peerK900Le = false
6050
6098
  peerWireCapsBinary = false
6051
6099
  peerFilePayloadV2 = false
6100
+ peerWearTuning = false
6101
+ peerMicTuning = false
6102
+ micTuningGeneration = 0
6052
6103
  BleJsonCompact.resetSession()
6053
6104
  wireHandshakeSentGeneration = -1
6054
6105
  }
@@ -6118,6 +6169,33 @@ extension MentraLive {
6118
6169
  if caps.keys.contains("file_payload_v2") {
6119
6170
  peerFilePayloadV2 = (caps["file_payload_v2"] as? Bool) == true
6120
6171
  }
6172
+ if caps.keys.contains("wear_tuning"), !peerWearTuning {
6173
+ let supported = (caps["wear_tuning"] as? Bool) == true
6174
+ || ((caps["wear_tuning"] as? NSNumber)?.intValue ?? 0) != 0
6175
+ if supported {
6176
+ peerWearTuning = true
6177
+ Bridge.log("LIVE: wire_caps wear_tuning supported")
6178
+ // Nothing to push: wear reporting starts off on the glasses
6179
+ // and stays off until the tuning screen asks for it. One read
6180
+ // per link: the flag is cleared on every wire epoch, but the
6181
+ // glasses only forget tuning on BLE disconnect.
6182
+ if !wearTuningQueriedThisLink {
6183
+ wearTuningQueriedThisLink = true
6184
+ requestWearTuning()
6185
+ }
6186
+ }
6187
+ }
6188
+ if caps.keys.contains("mic_tuning"), !peerMicTuning {
6189
+ let supported = (caps["mic_tuning"] as? Bool) == true
6190
+ || ((caps["mic_tuning"] as? NSNumber)?.intValue ?? 0) != 0
6191
+ if supported {
6192
+ peerMicTuning = true
6193
+ // Caps can land after the on-connect batch already ran, which
6194
+ // would have skipped the tuning send.
6195
+ Bridge.log("LIVE: wire_caps mic_tuning supported")
6196
+ sendMicTuningSetting()
6197
+ }
6198
+ }
6121
6199
  }
6122
6200
 
6123
6201
  private func advertiseFilePayloadCapabilityToBes() {
@@ -6786,6 +6864,165 @@ extension MentraLive {
6786
6864
 
6787
6865
  // Send glasses-side loudness / Barrier gate setting.
6788
6866
  sendLoudnessGateSetting()
6867
+
6868
+ // Send mic tuning. With nothing authorized this sends a reset, which is
6869
+ // what returns a freshly connected pair of glasses to stock behaviour.
6870
+ sendMicTuningSetting()
6871
+ }
6872
+
6873
+ /// Mic tuning field names, matching the BES cs_mictun body.
6874
+ private static let micTuningFields = [
6875
+ "gain", "open", "close", "attack", "hang", "sp_open", "sp_close", "sp_hold",
6876
+ ]
6877
+
6878
+ /// Push the effective mic tuning to the glasses.
6879
+ ///
6880
+ /// The store holds only what the engine has authorized for this process; a
6881
+ /// missing value means "no tuning", which is sent as an explicit reset
6882
+ /// rather than silently skipped. That is what keeps a persisted super-mode
6883
+ /// value from surviving into a session where super mode is off.
6884
+ func sendMicTuningSetting() {
6885
+ guard connectedPeripheral != nil, txCharacteristic != nil else {
6886
+ Bridge.log("LIVE: Cannot send mic tuning - BLE write path not ready")
6887
+ return
6888
+ }
6889
+ if !peerMicTuning {
6890
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_mictun anyway")
6891
+ }
6892
+
6893
+ var body: [String: Any] = [:]
6894
+ if let fields = DeviceStore.shared.get("bluetooth", "mic_tuning") as? [String: Any] {
6895
+ for name in Self.micTuningFields {
6896
+ if let number = fields[name] as? NSNumber {
6897
+ body[name] = number.intValue
6898
+ }
6899
+ }
6900
+ }
6901
+ if body.isEmpty {
6902
+ body["reset"] = 1
6903
+ }
6904
+
6905
+ // Key order is fixed so equal bodies serialize identically.
6906
+ let serialized = body.keys.sorted().map { "\($0)=\(body[$0]!)" }.joined(separator: ",")
6907
+ if serialized == lastSentMicTuningBody {
6908
+ Bridge.log("LIVE: 🎚️ Mic tuning unchanged this link, not resending: \(serialized)")
6909
+ return
6910
+ }
6911
+
6912
+ Bridge.log("LIVE: 🎚️ Sending mic tuning to glasses: \(body)")
6913
+ sendMicTuningCommand("cs_mictun", body: body)
6914
+ lastSentMicTuningBody = serialized
6915
+ }
6916
+
6917
+ /// Ask the glasses what tuning they are actually running (sr_micst).
6918
+ func requestMicTuningState() {
6919
+ guard connectedPeripheral != nil, txCharacteristic != nil else {
6920
+ Bridge.log("LIVE: Cannot send cs_micst - BLE write path not ready")
6921
+ return
6922
+ }
6923
+ if !peerMicTuning {
6924
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_micst anyway")
6925
+ }
6926
+ sendMicTuningCommand("cs_micst", body: [:])
6927
+ }
6928
+
6929
+ /// Enable or disable the sr_micrms readout.
6930
+ func setMicRmsTelemetry(_ enabled: Bool) {
6931
+ guard connectedPeripheral != nil, txCharacteristic != nil, peerMicTuning else { return }
6932
+ sendMicTuningCommand("cs_micrms", body: ["on": enabled ? 1 : 0])
6933
+ }
6934
+
6935
+ /// Read the current wear state (sr_wrst). Always available.
6936
+ @objc func queryWearState() {
6937
+ sendWearCommandIfReady("cs_wrst", body: [:])
6938
+ }
6939
+
6940
+ /// Turn wear reporting on or off for this session.
6941
+ ///
6942
+ /// Deliberately not the NV-backed cs_swit type 1: the glasses must forget
6943
+ /// this on disconnect, and any later switch write would re-persist a wear
6944
+ /// bit that had been enabled once.
6945
+ @objc func setWearReporting(_ enabled: Bool) {
6946
+ sendWearCommandIfReady("cs_weartun", body: ["enabled": enabled ? 1 : 0])
6947
+ }
6948
+
6949
+ /// Move the debounce vote. Negative means "leave this one alone".
6950
+ @objc func setWearTuning(intervalMs: Int, count: Int, majority: Int) {
6951
+ var body: [String: Any] = [:]
6952
+ if intervalMs >= 0 { body["interval"] = intervalMs }
6953
+ if count >= 0 { body["count"] = count }
6954
+ if majority >= 0 { body["majority"] = majority }
6955
+ guard !body.isEmpty else { return }
6956
+ sendWearCommandIfReady("cs_weartun", body: body)
6957
+ }
6958
+
6959
+ /// Ask what the poll loop is actually running (sr_weartun).
6960
+ @objc func requestWearTuning() {
6961
+ sendWearCommandIfReady("cs_wearst", body: [:])
6962
+ }
6963
+
6964
+ /// Restore firmware defaults and disable reporting. Distinct from sending
6965
+ /// the default vote values, which would leave reporting on.
6966
+ @objc func resetWearTuning() {
6967
+ sendWearCommandIfReady("cs_weartun", body: ["reset": 1])
6968
+ }
6969
+
6970
+ private func sendWearCommandIfReady(_ name: String, body: [String: Any]) {
6971
+ guard connectedPeripheral != nil, txCharacteristic != nil else {
6972
+ Bridge.log("LIVE: Cannot send \(name) - BLE write path not ready")
6973
+ return
6974
+ }
6975
+ if !peerWearTuning && name != "cs_wrst" {
6976
+ Bridge.log("LIVE: wear_tuning cap not advertised; sending \(name) anyway")
6977
+ }
6978
+ sendMicTuningCommand(name, body: body)
6979
+ }
6980
+
6981
+ private func sendMicTuningCommand(_ name: String, body: [String: Any]) {
6982
+ do {
6983
+ let bodyData = try JSONSerialization.data(withJSONObject: body)
6984
+ guard let bodyString = String(data: bodyData, encoding: .utf8) else {
6985
+ Bridge.log("LIVE: Failed to encode \(name) payload")
6986
+ return
6987
+ }
6988
+ let command: [String: Any] = ["C": name, "V": 1, "B": bodyString]
6989
+ if !sendRawK900Command(command, wakeUp: true) {
6990
+ Bridge.log("LIVE: Failed to send \(name)")
6991
+ }
6992
+ } catch {
6993
+ Bridge.log("LIVE: Error encoding \(name) payload: \(error)")
6994
+ }
6995
+ }
6996
+
6997
+ /// Post-clamp tuning the glasses report as in force. Forwarded verbatim so
6998
+ /// the screen can show the applied value next to the requested one.
6999
+ private func handleMicTuningState(_ body: [String: Any]) {
7000
+ var state: [String: Any] = [:]
7001
+ for name in Self.micTuningFields {
7002
+ if let number = body[name] as? NSNumber {
7003
+ state[name] = number.intValue
7004
+ }
7005
+ }
7006
+ let generation = (body["gen"] as? NSNumber)?.intValue ?? 0
7007
+ state["generation"] = generation
7008
+ state["overridden"] = ((body["ovr"] as? NSNumber)?.intValue ?? 0) != 0
7009
+ micTuningGeneration = generation
7010
+ Bridge.sendMicTuningState(state)
7011
+ }
7012
+
7013
+ /// What the wear poll loop is actually running. A rejected patch comes back
7014
+ /// with the unchanged values and a non-zero result code, so the screen can
7015
+ /// show that the request did not take.
7016
+ private func handleWearTuningState(_ json: [String: Any], _ body: [String: Any]) {
7017
+ let state: [String: Any] = [
7018
+ "enabled": (k900JsonInt(body, "enabled") ?? 0) != 0,
7019
+ "interval": k900JsonInt(body, "interval") ?? 0,
7020
+ "count": k900JsonInt(body, "count") ?? 0,
7021
+ "majority": k900JsonInt(body, "majority") ?? 0,
7022
+ "generation": k900JsonInt(body, "gen") ?? 0,
7023
+ "accepted": (k900JsonInt(json, "S") ?? 0) == 0,
7024
+ ]
7025
+ Bridge.sendWearTuningState(state)
6789
7026
  }
6790
7027
 
6791
7028
  func sendVoiceActivityDetectionSetting() {
@@ -228,6 +228,24 @@ protocol SGCManager {
228
228
 
229
229
  func sendLoudnessGateSetting()
230
230
 
231
+ // MARK: - Mic tuning (super-mode only)
232
+
233
+ func sendMicTuningSetting()
234
+ func requestMicTuningState()
235
+ func setMicRmsTelemetry(_ enabled: Bool)
236
+
237
+ // MARK: - Wear detection (super-mode only)
238
+
239
+ // These are protocol requirements, not extension-only members, on purpose:
240
+ // DeviceManager holds an `SGCManager?`, and a method that exists only in
241
+ // the extension is statically dispatched through that reference, so the
242
+ // no-op below would win over MentraLive's implementation.
243
+ func queryWearState()
244
+ func setWearReporting(_ enabled: Bool)
245
+ func setWearTuning(intervalMs: Int, count: Int, majority: Int)
246
+ func requestWearTuning()
247
+ func resetWearTuning()
248
+
231
249
  // MARK: - Version Info
232
250
 
233
251
  func requestVersionInfo()
@@ -417,6 +435,20 @@ extension SGCManager {
417
435
 
418
436
  func sendLoudnessGateSetting() {}
419
437
 
438
+ // MARK: - Mic tuning (default no-op — Mentra Live supports this)
439
+
440
+ func sendMicTuningSetting() {}
441
+ func requestMicTuningState() {}
442
+ func setMicRmsTelemetry(_: Bool) {}
443
+
444
+ // MARK: - Wear detection (default no-op — Mentra Live supports this)
445
+
446
+ func queryWearState() {}
447
+ func setWearReporting(_: Bool) {}
448
+ func setWearTuning(intervalMs _: Int, count _: Int, majority _: Int) {}
449
+ func requestWearTuning() {}
450
+ func resetWearTuning() {}
451
+
420
452
  /// Default no-op; Mentra Live and G2 override to handle phone-detected clock skew.
421
453
  func sendSetSystemTime(_: Int64) {
422
454
  Bridge.log("SGC: sendSetSystemTime not supported")
@@ -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.1-dev.277",
3
+ "version": "3.2.1-dev.279",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -114,7 +114,7 @@
114
114
  "registry": "https://registry.npmjs.org/"
115
115
  },
116
116
  "dependencies": {
117
- "@mentra/cloud-protocol": "3.2.1-dev.277"
117
+ "@mentra/cloud-protocol": "3.2.1-dev.279"
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"
@@ -1066,6 +1144,10 @@ export type BluetoothSdkModuleEvents = {
1066
1144
  heartbeat_received: (event: HeartbeatReceivedEvent) => void
1067
1145
  swipe_volume_status: (event: SwipeVolumeStatusEvent) => void
1068
1146
  switch_status: (event: SwitchStatusEvent) => void
1147
+ mic_tuning_state: (event: MicTuningStateEvent) => void
1148
+ mic_rms: (event: MicRmsEvent) => void
1149
+ wear_state: (event: WearStateEvent) => void
1150
+ wear_tuning: (event: WearTuningEvent) => void
1069
1151
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void
1070
1152
  settings_ack: (event: SettingsAckEvent) => void
1071
1153
  pair_failure: (event: PairFailureEvent) => void
@@ -1226,6 +1308,10 @@ export type BluetoothSdkEventMap = {
1226
1308
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
1227
1309
  swipe_volume_status: SwipeVolumeStatusEvent
1228
1310
  switch_status: SwitchStatusEvent
1311
+ mic_tuning_state: MicTuningStateEvent
1312
+ mic_rms: MicRmsEvent
1313
+ wear_state: WearStateEvent
1314
+ wear_tuning: WearTuningEvent
1229
1315
  rgb_led_control_response: RgbLedControlResponseEvent
1230
1316
  settings_ack: SettingsAckEvent
1231
1317
  pair_failure: PairFailureEvent
@@ -1701,6 +1787,8 @@ export type BluetoothSettingsUpdate = Partial<{
1701
1787
  gallery_mode: boolean
1702
1788
  voice_activity_detection_enabled: boolean
1703
1789
  loudness_gate_enabled: boolean
1790
+ /** Effective mic tuning only. `{}` means "reset to firmware defaults". */
1791
+ mic_tuning: MicTuning
1704
1792
  button_photo_size: ButtonPhotoSize
1705
1793
  button_video_settings: {width: number; height: number; fps: number}
1706
1794
  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,
@@ -143,6 +144,36 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
143
144
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
144
145
  /** Mentra Live center-mic loudness / Barrier gate (cs_swit type 10). */
145
146
  setLoudnessGateEnabled(enabled: boolean): Promise<void>
147
+ /**
148
+ * Mentra Live mic tuning. `null` clears every override and returns the
149
+ * glasses to firmware defaults.
150
+ *
151
+ * Deliberately absent from the public SDK surface: it is a super-mode
152
+ * calibration aid whose values only make sense next to a live RMS readout.
153
+ */
154
+ setMicTuning(tuning: MicTuning | null): Promise<void>
155
+ /**
156
+ * Ask the glasses to report the tuning they are actually running. The answer
157
+ * arrives as a `mic_tuning_state` event, which is also emitted unprompted
158
+ * after every set, so a screen that subscribes on mount and calls this on
159
+ * focus never has to guess.
160
+ */
161
+ requestMicTuningState(): Promise<void>
162
+ /** Enable or disable the `mic_rms` readout. Auto-disabled on disconnect. */
163
+ setMicRmsTelemetry(enabled: boolean): Promise<void>
164
+ /**
165
+ * Mentra Live wear detection. Internal / Super Mode only.
166
+ *
167
+ * Reporting is RAM-only on the glasses and must not go through `cs_swit`
168
+ * type 1 (that bit is NV-backed). `resetWearTuning` both restores the
169
+ * firmware vote defaults and turns reporting off.
170
+ */
171
+ queryWearState(): Promise<void>
172
+ setWearReporting(enabled: boolean): Promise<void>
173
+ /** Negative values leave that field unchanged. */
174
+ setWearTuning(intervalMs: number, count: number, majority: number): Promise<void>
175
+ requestWearTuning(): Promise<void>
176
+ resetWearTuning(): Promise<void>
146
177
  /**
147
178
  * @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
148
179
  * `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop).
@@ -546,6 +577,22 @@ NativeBluetoothSdkModule.setLoudnessGateEnabled = function (enabled: boolean) {
546
577
  return this.updateBluetoothSettings({loudness_gate_enabled: enabled})
547
578
  }
548
579
 
580
+ NativeBluetoothSdkModule.setMicTuning = function (tuning: MicTuning | null) {
581
+ // `{}` rather than null: the store drops null writes, and the native side
582
+ // reads an empty object as "no overrides" and sends an explicit reset.
583
+ return this.updateBluetoothSettings({mic_tuning: tuning ?? {}})
584
+ }
585
+
586
+ const nativeMicModule = NativeBluetoothSdkModule as unknown as Record<string, unknown>
587
+ NativeBluetoothSdkModule.requestMicTuningState = bindNativeMethod<() => Promise<void>>(
588
+ nativeMicModule,
589
+ "requestMicTuningState",
590
+ )
591
+ NativeBluetoothSdkModule.setMicRmsTelemetry = bindNativeMethod<(enabled: boolean) => Promise<void>>(
592
+ nativeMicModule,
593
+ "setMicRmsTelemetry",
594
+ )
595
+
549
596
  const nativeSetCameraFov = bindNativeMethod<(fov: CameraFovSetting) => MaybePromise<CameraFovResult>>(
550
597
  NativeBluetoothSdkModule as unknown as Record<string, unknown>,
551
598
  "setCameraFov",
@@ -678,5 +725,20 @@ NativeBluetoothSdkModule.startStream = function (params: StreamStartRequest) {
678
725
  return nativeStartStream(streamRequestParamsForNative(params) as unknown as StreamStartRequest)
679
726
  }
680
727
 
728
+ const nativeWearModule = NativeBluetoothSdkModule as unknown as Record<string, unknown>
729
+ NativeBluetoothSdkModule.queryWearState = bindNativeMethod<() => Promise<void>>(nativeWearModule, "queryWearState")
730
+ NativeBluetoothSdkModule.setWearReporting = bindNativeMethod<(enabled: boolean) => Promise<void>>(
731
+ nativeWearModule,
732
+ "setWearReporting",
733
+ )
734
+ NativeBluetoothSdkModule.setWearTuning = bindNativeMethod<
735
+ (intervalMs: number, count: number, majority: number) => Promise<void>
736
+ >(nativeWearModule, "setWearTuning")
737
+ NativeBluetoothSdkModule.requestWearTuning = bindNativeMethod<() => Promise<void>>(
738
+ nativeWearModule,
739
+ "requestWearTuning",
740
+ )
741
+ NativeBluetoothSdkModule.resetWearTuning = bindNativeMethod<() => Promise<void>>(nativeWearModule, "resetWearTuning")
742
+
681
743
  export default NativeBluetoothSdkModule
682
744
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkInternalModule
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.1",
15
- "releaseIdentity": "3.2.1-dev.277",
16
- "releaseSetId": "mentra-3.2.1-dev.277",
17
- "sourceCommit": "2668275c49728812fea5356f6df3e78e73f1ed87",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.277.json",
19
- "otaManifestSha256": "527d30a6400d5accdbf83e2c4316ec6e30e2b7ef1d70b6fd54e7abcec1bcc183"
15
+ "releaseIdentity": "3.2.1-dev.279",
16
+ "releaseSetId": "mentra-3.2.1-dev.279",
17
+ "sourceCommit": "b89b07413c06947f1a2f4a321ce23927eaebd4e4",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.279.json",
19
+ "otaManifestSha256": "725cccaec20e39be8f75dbe1e511092aa4812ab46cade44a583843282f0c0eb3"
20
20
  })