@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
@@ -141,6 +141,9 @@ class MentraLive : SGCManager() {
141
141
  private const val LC3_FRAME_SIZE = 40
142
142
  private const val VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8
143
143
  private const val LOUDNESS_GATE_SWITCH_TYPE = 10
144
+ // Mic tuning field names, matching the BES cs_mictun body.
145
+ private val MIC_TUNING_FIELDS =
146
+ listOf("gain", "open", "close", "attack", "hang", "sp_open", "sp_close", "sp_hold")
144
147
  private const val BES2700_MTU_LIMIT = 509
145
148
  private const val A2DP_CONNECT_MAX_ATTEMPTS = 5
146
149
  private const val A2DP_CONNECT_RETRY_MS = 800L
@@ -532,6 +535,22 @@ class MentraLive : SGCManager() {
532
535
  private var peerK900Le = false
533
536
  private var peerWireCapsBinary = false
534
537
  private var peerFilePayloadV2 = false
538
+ // Firmware understands cs_mictun / cs_micst / cs_micrms. Nothing mic-tuning
539
+ // related goes on the wire until this is seen.
540
+ private var peerMicTuning = false
541
+ private var peerWearTuning = false
542
+ // Connect-time dedupe. Three paths push mic tuning when a link comes up
543
+ // (sendUserSettings, the wire_caps advertisement, the engine's settings
544
+ // replay) and the wire_caps parse re-runs on every glasses_ready, so a
545
+ // single connect used to cost two identical cs_mictun and several
546
+ // cs_wearst. The glasses only lose tuning state on BLE disconnect, so an
547
+ // identical body within one link is a no-op and is dropped here. Both
548
+ // reset when the link drops (updateConnectionState -> DISCONNECTED).
549
+ private var lastSentMicTuningBody: String? = null
550
+ private var wearTuningQueriedThisLink = false
551
+ // Tuning generation echoed by the last sr_mictun / sr_micst. An sr_micrms
552
+ // measured before that revision describes a config we already replaced.
553
+ private var micTuningGeneration = 0
535
554
  // Last observed glasses process session id (`sid` in glasses_ready / version_info_1).
536
555
  // The BES keeps the BLE link alive across asg_client restarts, so transport state
537
556
  // cannot signal a restart - a CHANGED (or newly appearing) sid is the restart signal.
@@ -1219,6 +1238,10 @@ class MentraLive : SGCManager() {
1219
1238
  DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
1220
1239
  resetWireNegotiationState()
1221
1240
  resetPendingAckState()
1241
+ // The glasses reset mic and wear tuning on BLE disconnect, so the
1242
+ // next link must be allowed to send them again.
1243
+ lastSentMicTuningBody = null
1244
+ wearTuningQueriedThisLink = false
1222
1245
  sendQueue.clear() // see the disconnect reset above: stale writes die with the session
1223
1246
 
1224
1247
  // Drop OTA caches when fully disconnected — avoids leaking session/step state
@@ -5531,6 +5554,55 @@ class MentraLive : SGCManager() {
5531
5554
  Log.e(TAG, "Error parsing sr_swit response", e)
5532
5555
  }
5533
5556
  }
5557
+ "sr_mictun", "sr_micst" -> {
5558
+ try {
5559
+ val bodyObj = optK900Body(json)
5560
+ if (bodyObj != null) {
5561
+ handleMicTuningState(bodyObj)
5562
+ }
5563
+ } catch (e: Exception) {
5564
+ Log.e(TAG, "Error parsing mic tuning state", e)
5565
+ }
5566
+ }
5567
+ "sr_weartun" -> {
5568
+ try {
5569
+ val bodyObj = optK900Body(json)
5570
+ if (bodyObj != null) {
5571
+ handleWearTuningState(json, bodyObj)
5572
+ }
5573
+ } catch (e: Exception) {
5574
+ Log.e(TAG, "Error parsing wear tuning state", e)
5575
+ }
5576
+ }
5577
+ "sr_wrst" -> {
5578
+ try {
5579
+ val bodyObj = optK900Body(json)
5580
+ if (bodyObj != null) {
5581
+ Bridge.sendWearState(bodyObj.optInt("on", 0) != 0)
5582
+ }
5583
+ } catch (e: Exception) {
5584
+ Log.e(TAG, "Error parsing sr_wrst response", e)
5585
+ }
5586
+ }
5587
+ "sr_micrms" -> {
5588
+ try {
5589
+ val bodyObj = optK900Body(json)
5590
+ if (bodyObj != null) {
5591
+ val generation = bodyObj.optInt("gen", 0)
5592
+ // Measured under a config we have already replaced.
5593
+ if (generation >= micTuningGeneration) {
5594
+ Bridge.sendMicRms(
5595
+ bodyObj.optInt("rms", 0),
5596
+ bodyObj.optInt("gate", 0) != 0,
5597
+ bodyObj.optInt("sp", 0) != 0,
5598
+ generation
5599
+ )
5600
+ }
5601
+ }
5602
+ } catch (e: Exception) {
5603
+ Log.e(TAG, "Error parsing sr_micrms response", e)
5604
+ }
5605
+ }
5534
5606
  "sr_shut" -> {
5535
5607
  Bridge.log("LIVE: K900 shutdown command received - glasses shutting down")
5536
5608
  lastShutdownTimeMs = System.currentTimeMillis()
@@ -5828,6 +5900,39 @@ class MentraLive : SGCManager() {
5828
5900
  return !(value is Boolean) || value
5829
5901
  }
5830
5902
 
5903
+ /**
5904
+ * Post-clamp tuning the glasses report as in force. Forwarded verbatim so
5905
+ * the screen can show the applied value next to the requested one.
5906
+ */
5907
+ private fun handleMicTuningState(body: JSONObject) {
5908
+ val state = HashMap<String, Any>()
5909
+ for (name in MIC_TUNING_FIELDS) {
5910
+ if (body.has(name)) state[name] = body.optInt(name, 0)
5911
+ }
5912
+ val generation = body.optInt("gen", 0)
5913
+ state["generation"] = generation
5914
+ state["overridden"] = body.optInt("ovr", 0) != 0
5915
+ micTuningGeneration = generation
5916
+ Bridge.sendMicTuningState(state)
5917
+ }
5918
+
5919
+ /**
5920
+ * What the wear poll loop is actually running. A rejected patch comes back
5921
+ * with the unchanged values and a non-zero result code, so the screen can
5922
+ * show that the request did not take.
5923
+ */
5924
+ private fun handleWearTuningState(json: JSONObject, body: JSONObject) {
5925
+ val state = HashMap<String, Any>()
5926
+ state["enabled"] = body.optInt("enabled", 0) != 0
5927
+ state["interval"] = body.optInt("interval", 0)
5928
+ state["count"] = body.optInt("count", 0)
5929
+ state["majority"] = body.optInt("majority", 0)
5930
+ state["generation"] = body.optInt("gen", 0)
5931
+ // K900 replies carry the result code in "S"; 0 is RC_SUCCESS.
5932
+ state["accepted"] = json.optInt("S", 0) == 0
5933
+ Bridge.sendWearTuningState(state)
5934
+ }
5935
+
5831
5936
  private fun handleSwitchStatus(switchType: Int, switchValue: Int, timestamp: Long) {
5832
5937
  Bridge.sendSwitchStatus(switchType, switchValue, timestamp)
5833
5938
  if (switchType == VOICE_ACTIVITY_DETECTION_SWITCH_TYPE &&
@@ -6042,6 +6147,14 @@ class MentraLive : SGCManager() {
6042
6147
  }
6043
6148
  }
6044
6149
 
6150
+ fun sendGalleryServerEnabled(requestId: String, enabled: Boolean) {
6151
+ val json = JSONObject()
6152
+ .put("type", "set_gallery_server_enabled")
6153
+ .put("request_id", requestId)
6154
+ .put("enabled", enabled)
6155
+ sendJson(json, true)
6156
+ }
6157
+
6045
6158
  override fun sendGalleryMode() {
6046
6159
  val active = DeviceStore.get("bluetooth", "gallery_mode") as Boolean
6047
6160
  sendGalleryMode(null, active)
@@ -8879,6 +8992,9 @@ class MentraLive : SGCManager() {
8879
8992
  peerK900Le = false
8880
8993
  peerWireCapsBinary = false
8881
8994
  peerFilePayloadV2 = false
8995
+ peerWearTuning = false
8996
+ peerMicTuning = false
8997
+ micTuningGeneration = 0
8882
8998
  BleJsonCompact.resetSession()
8883
8999
  wireHandshakeSentGeneration = -1
8884
9000
  }
@@ -8954,6 +9070,32 @@ class MentraLive : SGCManager() {
8954
9070
  if (caps.has("file_payload_v2")) {
8955
9071
  peerFilePayloadV2 = caps.optBoolean("file_payload_v2", false)
8956
9072
  }
9073
+ if (caps.has("wear_tuning") && !peerWearTuning) {
9074
+ // Firmware advertises this as a JSON bool (`true`), not `1`.
9075
+ // optInt("wear_tuning") returns 0 for a boolean and used to
9076
+ // no-op every Super Mode wear button.
9077
+ peerWearTuning = jsonFlagOn(caps, "wear_tuning")
9078
+ if (peerWearTuning) {
9079
+ Bridge.log("LIVE: wire_caps wear_tuning supported")
9080
+ // Nothing to push: wear reporting starts off on the glasses
9081
+ // and stays off until the tuning screen asks for it. One read
9082
+ // per link: the flag is cleared on every wire epoch, but the
9083
+ // glasses only forget tuning on BLE disconnect.
9084
+ if (!wearTuningQueriedThisLink) {
9085
+ wearTuningQueriedThisLink = true
9086
+ requestWearTuning()
9087
+ }
9088
+ }
9089
+ }
9090
+ if (caps.has("mic_tuning") && !peerMicTuning) {
9091
+ peerMicTuning = jsonFlagOn(caps, "mic_tuning")
9092
+ if (peerMicTuning) {
9093
+ // Caps can arrive after the on-connect batch already ran, in
9094
+ // which case the tuning send was skipped. Do it now.
9095
+ Bridge.log("LIVE: wire_caps mic_tuning supported")
9096
+ sendMicTuningSetting()
9097
+ }
9098
+ }
8957
9099
  }
8958
9100
 
8959
9101
  private fun advertiseFilePayloadCapabilityToBes() {
@@ -10682,6 +10824,209 @@ class MentraLive : SGCManager() {
10682
10824
 
10683
10825
  // Send glasses-side loudness / Barrier gate setting.
10684
10826
  sendLoudnessGateSetting()
10827
+
10828
+ // Send mic tuning. With nothing authorized this sends a reset, which is
10829
+ // what returns a freshly connected pair of glasses to stock behaviour.
10830
+ sendMicTuningSetting()
10831
+ }
10832
+
10833
+ /**
10834
+ * Push the effective mic tuning to the glasses.
10835
+ *
10836
+ * The store holds only what the engine has authorized for this process; a
10837
+ * missing value means "no tuning", which is sent as an explicit reset
10838
+ * rather than silently skipped. That is what keeps a persisted super-mode
10839
+ * value from surviving into a session where super mode is off.
10840
+ */
10841
+ override fun sendMicTuningSetting() {
10842
+ if (!isConnected) {
10843
+ Bridge.log("LIVE: Cannot send mic tuning - not connected")
10844
+ return
10845
+ }
10846
+ if (!peerMicTuning) {
10847
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_mictun anyway")
10848
+ }
10849
+
10850
+ try {
10851
+ val tuning = DeviceStore.get("bluetooth", "mic_tuning")
10852
+ val body = JSONObject()
10853
+ @Suppress("UNCHECKED_CAST")
10854
+ val fields = tuning as? Map<String, Any>
10855
+ var sent = 0
10856
+ if (fields != null) {
10857
+ for (name in MIC_TUNING_FIELDS) {
10858
+ val raw = fields[name]
10859
+ val number = raw as? Number ?: continue
10860
+ body.put(name, number.toInt())
10861
+ sent++
10862
+ }
10863
+ }
10864
+ if (sent == 0) {
10865
+ body.put("reset", 1)
10866
+ }
10867
+
10868
+ val serialized = body.toString()
10869
+ if (serialized == lastSentMicTuningBody) {
10870
+ Bridge.log("LIVE: 🎚️ Mic tuning unchanged this link, not resending: " + serialized)
10871
+ return
10872
+ }
10873
+
10874
+ Bridge.log("LIVE: 🎚️ Sending mic tuning to glasses: " + serialized)
10875
+
10876
+ val cmdObject = JSONObject()
10877
+ cmdObject.put("C", "cs_mictun")
10878
+ cmdObject.put("V", 1)
10879
+ cmdObject.put("B", body.toString())
10880
+
10881
+ val packedData =
10882
+ K900ProtocolUtils.packDataToK900(
10883
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
10884
+ K900ProtocolUtils.CMD_TYPE_STRING,
10885
+ k900LengthEndian()
10886
+ )
10887
+ if (packedData == null) {
10888
+ Bridge.log("LIVE: Failed to pack mic tuning command")
10889
+ return
10890
+ }
10891
+ queueData(packedData)
10892
+ lastSentMicTuningBody = serialized
10893
+ } catch (e: JSONException) {
10894
+ Log.e(TAG, "Error creating mic tuning command", e)
10895
+ }
10896
+ }
10897
+
10898
+ /** Ask the glasses what tuning they are actually running (sr_micst). */
10899
+ override fun requestMicTuningState() {
10900
+ if (!isConnected) {
10901
+ Bridge.log("LIVE: Cannot send cs_micst - not connected")
10902
+ return
10903
+ }
10904
+ if (!peerMicTuning) {
10905
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_micst anyway")
10906
+ }
10907
+ sendMicTuningCommand("cs_micst", JSONObject())
10908
+ }
10909
+
10910
+ /** Read the current wear state (sr_wrst). Always available. */
10911
+ override fun queryWearState() {
10912
+ sendWearCommandIfReady("cs_wrst", JSONObject())
10913
+ }
10914
+
10915
+ /**
10916
+ * Turn wear reporting on or off for this session.
10917
+ *
10918
+ * Deliberately not the NV-backed cs_swit type 1: the glasses must forget
10919
+ * this on disconnect, and any later switch write would re-persist a wear
10920
+ * bit that had been enabled once.
10921
+ */
10922
+ override fun setWearReporting(enabled: Boolean) {
10923
+ val body = JSONObject()
10924
+ body.put("enabled", if (enabled) 1 else 0)
10925
+ sendWearCommandIfReady("cs_weartun", body)
10926
+ }
10927
+
10928
+ /** Move the debounce vote. Negative means "leave this one alone". */
10929
+ override fun setWearTuning(intervalMs: Int, count: Int, majority: Int) {
10930
+ val body = JSONObject()
10931
+ if (intervalMs >= 0) body.put("interval", intervalMs)
10932
+ if (count >= 0) body.put("count", count)
10933
+ if (majority >= 0) body.put("majority", majority)
10934
+ if (body.length() == 0) return
10935
+ sendWearCommandIfReady("cs_weartun", body)
10936
+ }
10937
+
10938
+ /** Ask what the poll loop is actually running (sr_weartun). */
10939
+ override fun requestWearTuning() {
10940
+ sendWearCommandIfReady("cs_wearst", JSONObject())
10941
+ }
10942
+
10943
+ /**
10944
+ * Restore firmware defaults and disable reporting. Distinct from sending
10945
+ * the default vote values, which would leave reporting on.
10946
+ */
10947
+ override fun resetWearTuning() {
10948
+ val body = JSONObject()
10949
+ body.put("reset", 1)
10950
+ sendWearCommandIfReady("cs_weartun", body)
10951
+ }
10952
+
10953
+ /**
10954
+ * JSON wire_caps flags arrive as bools (`true`) or ints (`1`).
10955
+ * [JSONObject.optInt] returns 0 for a boolean, which used to hide
10956
+ * advertised capabilities.
10957
+ */
10958
+ private fun jsonFlagOn(obj: JSONObject, key: String): Boolean {
10959
+ if (!obj.has(key) || obj.isNull(key)) return false
10960
+ return when (val value = obj.opt(key)) {
10961
+ is Boolean -> value
10962
+ is Number -> value.toInt() != 0
10963
+ is String -> value == "1" || value.equals("true", ignoreCase = true)
10964
+ else -> false
10965
+ }
10966
+ }
10967
+
10968
+ private fun sendWearCommandIfReady(command: String, body: JSONObject) {
10969
+ if (!isConnected) {
10970
+ Bridge.log("LIVE: Cannot send $command - not connected")
10971
+ return
10972
+ }
10973
+ if (!peerWearTuning && command != "cs_wrst") {
10974
+ Bridge.log("LIVE: wear_tuning cap not advertised; sending $command anyway")
10975
+ }
10976
+ sendWearCommand(command, body)
10977
+ }
10978
+
10979
+ private fun sendWearCommand(command: String, body: JSONObject) {
10980
+ try {
10981
+ val cmdObject = JSONObject()
10982
+ cmdObject.put("C", command)
10983
+ cmdObject.put("V", 1)
10984
+ cmdObject.put("B", body.toString())
10985
+ val packedData =
10986
+ K900ProtocolUtils.packDataToK900(
10987
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
10988
+ K900ProtocolUtils.CMD_TYPE_STRING,
10989
+ k900LengthEndian()
10990
+ )
10991
+ if (packedData == null) {
10992
+ Bridge.log("LIVE: Failed to pack " + command)
10993
+ return
10994
+ }
10995
+ Bridge.log("LIVE: 👓 " + command + " " + body.toString())
10996
+ queueData(packedData)
10997
+ } catch (e: JSONException) {
10998
+ Log.e(TAG, "Error creating " + command, e)
10999
+ }
11000
+ }
11001
+
11002
+ /** Enable or disable the sr_micrms readout. */
11003
+ override fun setMicRmsTelemetry(enabled: Boolean) {
11004
+ if (!isConnected || !peerMicTuning) return
11005
+ val body = JSONObject()
11006
+ body.put("on", if (enabled) 1 else 0)
11007
+ sendMicTuningCommand("cs_micrms", body)
11008
+ }
11009
+
11010
+ private fun sendMicTuningCommand(command: String, body: JSONObject) {
11011
+ try {
11012
+ val cmdObject = JSONObject()
11013
+ cmdObject.put("C", command)
11014
+ cmdObject.put("V", 1)
11015
+ cmdObject.put("B", body.toString())
11016
+ val packedData =
11017
+ K900ProtocolUtils.packDataToK900(
11018
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
11019
+ K900ProtocolUtils.CMD_TYPE_STRING,
11020
+ k900LengthEndian()
11021
+ )
11022
+ if (packedData == null) {
11023
+ Bridge.log("LIVE: Failed to pack " + command)
11024
+ return
11025
+ }
11026
+ queueData(packedData)
11027
+ } catch (e: JSONException) {
11028
+ Log.e(TAG, "Error creating " + command, e)
11029
+ }
10685
11030
  }
10686
11031
 
10687
11032
  override fun sendVoiceActivityDetectionSetting() {
@@ -350,6 +350,19 @@ abstract class SGCManager {
350
350
  // Mentra Live center-mic loudness / Barrier gate
351
351
  open fun sendLoudnessGateSetting() {}
352
352
 
353
+ // Mentra Live mic tuning (super-mode only). No value authorized means the
354
+ // implementation sends an explicit reset rather than skipping the send.
355
+ open fun sendMicTuningSetting() {}
356
+ open fun requestMicTuningState() {}
357
+ open fun setMicRmsTelemetry(enabled: Boolean) {}
358
+
359
+ // Wear detection (Mentra Live only; super-mode tooling).
360
+ open fun queryWearState() {}
361
+ open fun setWearReporting(enabled: Boolean) {}
362
+ open fun setWearTuning(intervalMs: Int, count: Int, majority: Int) {}
363
+ open fun requestWearTuning() {}
364
+ open fun resetWearTuning() {}
365
+
353
366
  // Start/stop LC3 audio playback from glasses based on the nex_lc3_audio_playback flag.
354
367
  open fun applyNexAudioPlaybackSetting() {}
355
368
 
@@ -386,6 +386,78 @@ export type SwitchStatusEvent = {
386
386
  switchValue?: number;
387
387
  timestamp: number;
388
388
  };
389
+ /**
390
+ * Mentra Live center-mic gate + ADC gain overrides. Internal SDK surface: this
391
+ * is a super-mode tuning aid, not a public capability.
392
+ *
393
+ * Every field is optional; an omitted field keeps the firmware default. The
394
+ * glasses hold these in RAM only and drop them on disconnect, so the phone
395
+ * re-sends on every connect.
396
+ */
397
+ export type MicTuning = {
398
+ /** codec_adc_vol index, 0-15. 15 is +32 dB, the top of the table. */
399
+ gain?: number;
400
+ /** Gate open / close thresholds, linear RMS on int16 samples. */
401
+ open?: number;
402
+ close?: number;
403
+ /** Frames of 10 ms. */
404
+ attack?: number;
405
+ hang?: number;
406
+ /** Thresholds used while the speaker is active or within its hold-off. */
407
+ sp_open?: number;
408
+ sp_close?: number;
409
+ sp_hold?: number;
410
+ };
411
+ /**
412
+ * What the glasses report as actually in force, after their own clamping. Use
413
+ * this rather than the requested value when showing applied settings.
414
+ */
415
+ export type MicTuningStateEvent = MicTuning & {
416
+ type: "mic_tuning_state";
417
+ /** Tuning revision; increments on every accepted set or reset. */
418
+ generation: number;
419
+ /** True when any override is in force. */
420
+ overridden: boolean;
421
+ };
422
+ /** Live center-mic level. Disposable: samples are dropped under any pressure. */
423
+ export type MicRmsEvent = {
424
+ type: "mic_rms";
425
+ rms: number;
426
+ gateOpen: boolean;
427
+ speakerElevated: boolean;
428
+ /** Tuning revision the sample was measured under. */
429
+ generation: number;
430
+ };
431
+ /**
432
+ * Mentra Live wear-detection vote. Internal SDK surface: Super Mode only.
433
+ * Values live in RAM on the glasses and reset on disconnect.
434
+ */
435
+ export type WearTuning = {
436
+ /** Poll period in milliseconds. Firmware clamp: 50–2000. */
437
+ interval?: number;
438
+ /** Sliding-window length. Firmware clamp: 3–15. */
439
+ count?: number;
440
+ /** Votes needed to flip, either direction. Must be > count/2 and ≤ count. */
441
+ majority?: number;
442
+ };
443
+ /** Current wear vote from sr_wrst, or an unsolicited wear transition. */
444
+ export type WearStateEvent = {
445
+ type: "wear_state";
446
+ worn: boolean;
447
+ };
448
+ /**
449
+ * What the wear poll loop is actually running. A rejected patch echoes the
450
+ * unchanged values with `accepted: false`.
451
+ */
452
+ export type WearTuningEvent = WearTuning & {
453
+ type: "wear_tuning";
454
+ enabled: boolean;
455
+ interval: number;
456
+ count: number;
457
+ majority: number;
458
+ generation: number;
459
+ accepted: boolean;
460
+ };
389
461
  export type RgbLedControlResponseEvent = {
390
462
  type: "rgb_led_control_response";
391
463
  state: "success";
@@ -400,7 +472,7 @@ export type RgbLedControlSuccessResponseEvent = Extract<RgbLedControlResponseEve
400
472
  state: "success";
401
473
  }>;
402
474
  export type SettingsAckStatus = "applied" | "ready" | "error" | "failed" | "failure" | "rejected";
403
- export type SettingsAckSetting = "gallery_mode" | "button_photo" | "button_video_recording" | "button_max_recording_time" | "camera_fov" | "camera_fov_override" | "camera_tuning";
475
+ export type SettingsAckSetting = "gallery_server" | "gallery_mode" | "button_photo" | "button_video_recording" | "button_max_recording_time" | "camera_fov" | "camera_fov_override" | "camera_tuning";
404
476
  export type SettingsAckEvent = {
405
477
  type: "settings_ack";
406
478
  requestId: string;
@@ -418,6 +490,10 @@ export type SettingsAckEvent = {
418
490
  fps?: number;
419
491
  enabled?: boolean;
420
492
  minutes?: number;
493
+ /** Current site-network listener state; present for gallery_server acknowledgements. */
494
+ listening?: boolean;
495
+ /** Current HTTP base URL; present for gallery_server only while listening. */
496
+ url?: string;
421
497
  /** ANR enabled flag; present when setting === "camera_tuning" */
422
498
  anr?: boolean;
423
499
  /** Stock-gain flag; present when setting === "camera_tuning" */
@@ -900,6 +976,10 @@ export type BluetoothSdkModuleEvents = {
900
976
  heartbeat_received: (event: HeartbeatReceivedEvent) => void;
901
977
  swipe_volume_status: (event: SwipeVolumeStatusEvent) => void;
902
978
  switch_status: (event: SwitchStatusEvent) => void;
979
+ mic_tuning_state: (event: MicTuningStateEvent) => void;
980
+ mic_rms: (event: MicRmsEvent) => void;
981
+ wear_state: (event: WearStateEvent) => void;
982
+ wear_tuning: (event: WearTuningEvent) => void;
903
983
  rgb_led_control_response: (event: RgbLedControlResponseEvent) => void;
904
984
  settings_ack: (event: SettingsAckEvent) => void;
905
985
  pair_failure: (event: PairFailureEvent) => void;
@@ -1036,6 +1116,10 @@ export type BluetoothSdkEventMap = {
1036
1116
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent;
1037
1117
  swipe_volume_status: SwipeVolumeStatusEvent;
1038
1118
  switch_status: SwitchStatusEvent;
1119
+ mic_tuning_state: MicTuningStateEvent;
1120
+ mic_rms: MicRmsEvent;
1121
+ wear_state: WearStateEvent;
1122
+ wear_tuning: WearTuningEvent;
1039
1123
  rgb_led_control_response: RgbLedControlResponseEvent;
1040
1124
  settings_ack: SettingsAckEvent;
1041
1125
  pair_failure: PairFailureEvent;
@@ -1111,6 +1195,12 @@ export interface BluetoothSdkPublicModule {
1111
1195
  setSystemTime(timestampMs: number): Promise<void>;
1112
1196
  /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
1113
1197
  setWifiAdbState(enabled: boolean): Promise<void>;
1198
+ /**
1199
+ * Persistently enable/disable unauthenticated HTTP gallery access on the glasses' Wi-Fi.
1200
+ * Default off; survives reconnects/restarts until disabled. Only use on trusted networks.
1201
+ * Returns saved enabled state plus current listening/url. No Wi-Fi means listening=false.
1202
+ */
1203
+ setGalleryServerEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>;
1114
1204
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>;
1115
1205
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>;
1116
1206
  setLoudnessGateEnabled(enabled: boolean): Promise<void>;
@@ -1415,6 +1505,8 @@ export type BluetoothSettingsUpdate = Partial<{
1415
1505
  gallery_mode: boolean;
1416
1506
  voice_activity_detection_enabled: boolean;
1417
1507
  loudness_gate_enabled: boolean;
1508
+ /** Effective mic tuning only. `{}` means "reset to firmware defaults". */
1509
+ mic_tuning: MicTuning;
1418
1510
  button_photo_size: ButtonPhotoSize;
1419
1511
  button_video_settings: {
1420
1512
  width: number;