@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
@@ -368,6 +368,10 @@ class BluetoothSdkModule : Module() {
368
368
  "receive_command_from_ble",
369
369
  "swipe_volume_status",
370
370
  "switch_status",
371
+ "mic_tuning_state",
372
+ "mic_rms",
373
+ "wear_state",
374
+ "wear_tuning",
371
375
  "rgb_led_control_response",
372
376
  "settings_ack",
373
377
  "version_info",
@@ -582,6 +586,33 @@ class BluetoothSdkModule : Module() {
582
586
  // Stub on Android — iOS uses this for the jetsam stress test.
583
587
  Function("getMemoryMB") { -> 0.0 }
584
588
 
589
+ // MARK: - Mic tuning (internal SDK surface only)
590
+
591
+ AsyncFunction("setMicRmsTelemetry") { enabled: Boolean ->
592
+ deviceManager?.sgc?.setMicRmsTelemetry(enabled)
593
+ }
594
+
595
+ // Fire-and-forget: the answer arrives as a mic_tuning_state event, which
596
+ // the screen is subscribed to anyway.
597
+ AsyncFunction("requestMicTuningState") { deviceManager?.sgc?.requestMicTuningState() }
598
+
599
+ // MARK: - Wear detection (internal SDK surface only)
600
+
601
+ // All fire-and-forget; answers arrive as wear_state / wear_tuning events.
602
+ AsyncFunction("queryWearState") { deviceManager?.queryWearState() }
603
+
604
+ AsyncFunction("setWearReporting") { enabled: Boolean ->
605
+ deviceManager?.setWearReporting(enabled)
606
+ }
607
+
608
+ AsyncFunction("setWearTuning") { intervalMs: Int, count: Int, majority: Int ->
609
+ deviceManager?.setWearTuning(intervalMs, count, majority)
610
+ }
611
+
612
+ AsyncFunction("requestWearTuning") { deviceManager?.requestWearTuning() }
613
+
614
+ AsyncFunction("resetWearTuning") { deviceManager?.resetWearTuning() }
615
+
585
616
  // MARK: - Incident Reporting
586
617
 
587
618
  SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
@@ -391,6 +391,40 @@ public class Bridge private constructor() {
391
391
  sendTypedMessage("switch_status", body)
392
392
  }
393
393
 
394
+ /**
395
+ * Mic tuning values the glasses report as actually in force (post-clamp),
396
+ * from sr_mictun / sr_micst.
397
+ */
398
+ @JvmStatic
399
+ fun sendMicTuningState(state: Map<String, Any>) {
400
+ sendTypedMessage("mic_tuning_state", HashMap(state))
401
+ }
402
+
403
+ /** Wear state from sr_wrst, or from a wear transition on the glasses. */
404
+ @JvmStatic
405
+ fun sendWearState(worn: Boolean) {
406
+ val body = HashMap<String, Any>()
407
+ body["worn"] = worn
408
+ sendTypedMessage("wear_state", body)
409
+ }
410
+
411
+ /** Wear vote config the glasses report as in force, from sr_weartun. */
412
+ @JvmStatic
413
+ fun sendWearTuningState(state: Map<String, Any>) {
414
+ sendTypedMessage("wear_tuning", HashMap(state))
415
+ }
416
+
417
+ /** Disposable mic RMS sample from sr_micrms. */
418
+ @JvmStatic
419
+ fun sendMicRms(rms: Int, gateOpen: Boolean, speakerElevated: Boolean, generation: Int) {
420
+ val body = HashMap<String, Any>()
421
+ body["rms"] = rms
422
+ body["gateOpen"] = gateOpen
423
+ body["speakerElevated"] = speakerElevated
424
+ body["generation"] = generation
425
+ sendTypedMessage("mic_rms", body)
426
+ }
427
+
394
428
  @JvmStatic
395
429
  fun sendPhotoError(requestId: String, errorCode: String, errorMessage: String) {
396
430
  val timestamp = System.currentTimeMillis()
@@ -1883,6 +1883,31 @@ class DeviceManager {
1883
1883
  sgc?.dbg2()
1884
1884
  }
1885
1885
 
1886
+ fun queryWearState() {
1887
+ Bridge.log("MAN: queryWearState()")
1888
+ sgc?.queryWearState()
1889
+ }
1890
+
1891
+ fun setWearReporting(enabled: Boolean) {
1892
+ Bridge.log("MAN: setWearReporting($enabled)")
1893
+ sgc?.setWearReporting(enabled)
1894
+ }
1895
+
1896
+ fun setWearTuning(intervalMs: Int, count: Int, majority: Int) {
1897
+ Bridge.log("MAN: setWearTuning($intervalMs, $count, $majority)")
1898
+ sgc?.setWearTuning(intervalMs, count, majority)
1899
+ }
1900
+
1901
+ fun requestWearTuning() {
1902
+ Bridge.log("MAN: requestWearTuning()")
1903
+ sgc?.requestWearTuning()
1904
+ }
1905
+
1906
+ fun resetWearTuning() {
1907
+ Bridge.log("MAN: resetWearTuning()")
1908
+ sgc?.resetWearTuning()
1909
+ }
1910
+
1886
1911
  fun startStream(message: MutableMap<String, Any>) {
1887
1912
  Bridge.log("MAN: startStream")
1888
1913
  sgc?.startStream(message)
@@ -31,6 +31,16 @@ object DeviceStore {
31
31
  JSONObject(mapOf("v" to newValue)).toString()
32
32
  }
33
33
 
34
+ /**
35
+ * Empty/`reset` mic tuning must still be written to the glasses. Native
36
+ * `update` drops null, and equal empty maps would otherwise skip BLE.
37
+ */
38
+ private fun isMicTuningReset(value: Any): Boolean {
39
+ val map = value as? Map<*, *> ?: return true
40
+ if (map.isEmpty()) return true
41
+ return map.entries.none { (key, field) -> key != "reset" && field is Number }
42
+ }
43
+
34
44
  private fun scheduleDashboardHeightToGlasses() {
35
45
  pendingDashboardHeightRunnable?.let { dashboardBleHandler.removeCallbacks(it) }
36
46
  val r = Runnable {
@@ -164,7 +174,10 @@ object DeviceStore {
164
174
  fun apply(category: String, key: String, value: Any) {
165
175
  val oldValue = store.get(category, key)
166
176
  store.set(category, key, value)
167
- if (observableStoreWouldHaveSkipped(oldValue, value)) {
177
+ val skipped = observableStoreWouldHaveSkipped(oldValue, value)
178
+ val forceMicReset =
179
+ category == ObservableStore.BLUETOOTH_CATEGORY && key == "mic_tuning" && isMicTuningReset(value)
180
+ if (skipped && !forceMicReset) {
168
181
  return
169
182
  }
170
183
 
@@ -278,6 +291,12 @@ object DeviceStore {
278
291
  "bluetooth" to "loudness_gate_enabled" -> {
279
292
  DeviceManager.getInstance().sgc?.sendLoudnessGateSetting()
280
293
  }
294
+ // Deliberately has no seeded default above: the key starts absent so
295
+ // that a session where the engine has not authorized tuning can only
296
+ // ever send a reset.
297
+ "bluetooth" to "mic_tuning" -> {
298
+ DeviceManager.getInstance().sgc?.sendMicTuningSetting()
299
+ }
281
300
  "bluetooth" to "nex_lc3_audio_playback" -> {
282
301
  (value as? Boolean)?.let { enabled ->
283
302
  Bridge.log("DeviceStore: nex_lc3_audio_playback changed to $enabled")
@@ -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.2.1"
6
- const val RELEASE_IDENTITY: String = "3.2.1-dev.277"
7
- const val RELEASE_SET_ID: String = "mentra-3.2.1-dev.277"
8
- const val SOURCE_COMMIT: String = "2668275c49728812fea5356f6df3e78e73f1ed87"
9
- const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.277.json"
10
- const val OTA_MANIFEST_SHA256: String = "527d30a6400d5accdbf83e2c4316ec6e30e2b7ef1d70b6fd54e7abcec1bcc183"
6
+ const val RELEASE_IDENTITY: String = "3.2.1-dev.279"
7
+ const val RELEASE_SET_ID: String = "mentra-3.2.1-dev.279"
8
+ const val SOURCE_COMMIT: String = "b89b07413c06947f1a2f4a321ce23927eaebd4e4"
9
+ const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.279.json"
10
+ const val OTA_MANIFEST_SHA256: String = "725cccaec20e39be8f75dbe1e511092aa4812ab46cade44a583843282f0c0eb3"
11
11
  }
@@ -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 &&
@@ -8879,6 +8984,9 @@ class MentraLive : SGCManager() {
8879
8984
  peerK900Le = false
8880
8985
  peerWireCapsBinary = false
8881
8986
  peerFilePayloadV2 = false
8987
+ peerWearTuning = false
8988
+ peerMicTuning = false
8989
+ micTuningGeneration = 0
8882
8990
  BleJsonCompact.resetSession()
8883
8991
  wireHandshakeSentGeneration = -1
8884
8992
  }
@@ -8954,6 +9062,32 @@ class MentraLive : SGCManager() {
8954
9062
  if (caps.has("file_payload_v2")) {
8955
9063
  peerFilePayloadV2 = caps.optBoolean("file_payload_v2", false)
8956
9064
  }
9065
+ if (caps.has("wear_tuning") && !peerWearTuning) {
9066
+ // Firmware advertises this as a JSON bool (`true`), not `1`.
9067
+ // optInt("wear_tuning") returns 0 for a boolean and used to
9068
+ // no-op every Super Mode wear button.
9069
+ peerWearTuning = jsonFlagOn(caps, "wear_tuning")
9070
+ if (peerWearTuning) {
9071
+ Bridge.log("LIVE: wire_caps wear_tuning supported")
9072
+ // Nothing to push: wear reporting starts off on the glasses
9073
+ // and stays off until the tuning screen asks for it. One read
9074
+ // per link: the flag is cleared on every wire epoch, but the
9075
+ // glasses only forget tuning on BLE disconnect.
9076
+ if (!wearTuningQueriedThisLink) {
9077
+ wearTuningQueriedThisLink = true
9078
+ requestWearTuning()
9079
+ }
9080
+ }
9081
+ }
9082
+ if (caps.has("mic_tuning") && !peerMicTuning) {
9083
+ peerMicTuning = jsonFlagOn(caps, "mic_tuning")
9084
+ if (peerMicTuning) {
9085
+ // Caps can arrive after the on-connect batch already ran, in
9086
+ // which case the tuning send was skipped. Do it now.
9087
+ Bridge.log("LIVE: wire_caps mic_tuning supported")
9088
+ sendMicTuningSetting()
9089
+ }
9090
+ }
8957
9091
  }
8958
9092
 
8959
9093
  private fun advertiseFilePayloadCapabilityToBes() {
@@ -10682,6 +10816,209 @@ class MentraLive : SGCManager() {
10682
10816
 
10683
10817
  // Send glasses-side loudness / Barrier gate setting.
10684
10818
  sendLoudnessGateSetting()
10819
+
10820
+ // Send mic tuning. With nothing authorized this sends a reset, which is
10821
+ // what returns a freshly connected pair of glasses to stock behaviour.
10822
+ sendMicTuningSetting()
10823
+ }
10824
+
10825
+ /**
10826
+ * Push the effective mic tuning to the glasses.
10827
+ *
10828
+ * The store holds only what the engine has authorized for this process; a
10829
+ * missing value means "no tuning", which is sent as an explicit reset
10830
+ * rather than silently skipped. That is what keeps a persisted super-mode
10831
+ * value from surviving into a session where super mode is off.
10832
+ */
10833
+ override fun sendMicTuningSetting() {
10834
+ if (!isConnected) {
10835
+ Bridge.log("LIVE: Cannot send mic tuning - not connected")
10836
+ return
10837
+ }
10838
+ if (!peerMicTuning) {
10839
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_mictun anyway")
10840
+ }
10841
+
10842
+ try {
10843
+ val tuning = DeviceStore.get("bluetooth", "mic_tuning")
10844
+ val body = JSONObject()
10845
+ @Suppress("UNCHECKED_CAST")
10846
+ val fields = tuning as? Map<String, Any>
10847
+ var sent = 0
10848
+ if (fields != null) {
10849
+ for (name in MIC_TUNING_FIELDS) {
10850
+ val raw = fields[name]
10851
+ val number = raw as? Number ?: continue
10852
+ body.put(name, number.toInt())
10853
+ sent++
10854
+ }
10855
+ }
10856
+ if (sent == 0) {
10857
+ body.put("reset", 1)
10858
+ }
10859
+
10860
+ val serialized = body.toString()
10861
+ if (serialized == lastSentMicTuningBody) {
10862
+ Bridge.log("LIVE: 🎚️ Mic tuning unchanged this link, not resending: " + serialized)
10863
+ return
10864
+ }
10865
+
10866
+ Bridge.log("LIVE: 🎚️ Sending mic tuning to glasses: " + serialized)
10867
+
10868
+ val cmdObject = JSONObject()
10869
+ cmdObject.put("C", "cs_mictun")
10870
+ cmdObject.put("V", 1)
10871
+ cmdObject.put("B", body.toString())
10872
+
10873
+ val packedData =
10874
+ K900ProtocolUtils.packDataToK900(
10875
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
10876
+ K900ProtocolUtils.CMD_TYPE_STRING,
10877
+ k900LengthEndian()
10878
+ )
10879
+ if (packedData == null) {
10880
+ Bridge.log("LIVE: Failed to pack mic tuning command")
10881
+ return
10882
+ }
10883
+ queueData(packedData)
10884
+ lastSentMicTuningBody = serialized
10885
+ } catch (e: JSONException) {
10886
+ Log.e(TAG, "Error creating mic tuning command", e)
10887
+ }
10888
+ }
10889
+
10890
+ /** Ask the glasses what tuning they are actually running (sr_micst). */
10891
+ override fun requestMicTuningState() {
10892
+ if (!isConnected) {
10893
+ Bridge.log("LIVE: Cannot send cs_micst - not connected")
10894
+ return
10895
+ }
10896
+ if (!peerMicTuning) {
10897
+ Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_micst anyway")
10898
+ }
10899
+ sendMicTuningCommand("cs_micst", JSONObject())
10900
+ }
10901
+
10902
+ /** Read the current wear state (sr_wrst). Always available. */
10903
+ override fun queryWearState() {
10904
+ sendWearCommandIfReady("cs_wrst", JSONObject())
10905
+ }
10906
+
10907
+ /**
10908
+ * Turn wear reporting on or off for this session.
10909
+ *
10910
+ * Deliberately not the NV-backed cs_swit type 1: the glasses must forget
10911
+ * this on disconnect, and any later switch write would re-persist a wear
10912
+ * bit that had been enabled once.
10913
+ */
10914
+ override fun setWearReporting(enabled: Boolean) {
10915
+ val body = JSONObject()
10916
+ body.put("enabled", if (enabled) 1 else 0)
10917
+ sendWearCommandIfReady("cs_weartun", body)
10918
+ }
10919
+
10920
+ /** Move the debounce vote. Negative means "leave this one alone". */
10921
+ override fun setWearTuning(intervalMs: Int, count: Int, majority: Int) {
10922
+ val body = JSONObject()
10923
+ if (intervalMs >= 0) body.put("interval", intervalMs)
10924
+ if (count >= 0) body.put("count", count)
10925
+ if (majority >= 0) body.put("majority", majority)
10926
+ if (body.length() == 0) return
10927
+ sendWearCommandIfReady("cs_weartun", body)
10928
+ }
10929
+
10930
+ /** Ask what the poll loop is actually running (sr_weartun). */
10931
+ override fun requestWearTuning() {
10932
+ sendWearCommandIfReady("cs_wearst", JSONObject())
10933
+ }
10934
+
10935
+ /**
10936
+ * Restore firmware defaults and disable reporting. Distinct from sending
10937
+ * the default vote values, which would leave reporting on.
10938
+ */
10939
+ override fun resetWearTuning() {
10940
+ val body = JSONObject()
10941
+ body.put("reset", 1)
10942
+ sendWearCommandIfReady("cs_weartun", body)
10943
+ }
10944
+
10945
+ /**
10946
+ * JSON wire_caps flags arrive as bools (`true`) or ints (`1`).
10947
+ * [JSONObject.optInt] returns 0 for a boolean, which used to hide
10948
+ * advertised capabilities.
10949
+ */
10950
+ private fun jsonFlagOn(obj: JSONObject, key: String): Boolean {
10951
+ if (!obj.has(key) || obj.isNull(key)) return false
10952
+ return when (val value = obj.opt(key)) {
10953
+ is Boolean -> value
10954
+ is Number -> value.toInt() != 0
10955
+ is String -> value == "1" || value.equals("true", ignoreCase = true)
10956
+ else -> false
10957
+ }
10958
+ }
10959
+
10960
+ private fun sendWearCommandIfReady(command: String, body: JSONObject) {
10961
+ if (!isConnected) {
10962
+ Bridge.log("LIVE: Cannot send $command - not connected")
10963
+ return
10964
+ }
10965
+ if (!peerWearTuning && command != "cs_wrst") {
10966
+ Bridge.log("LIVE: wear_tuning cap not advertised; sending $command anyway")
10967
+ }
10968
+ sendWearCommand(command, body)
10969
+ }
10970
+
10971
+ private fun sendWearCommand(command: String, body: JSONObject) {
10972
+ try {
10973
+ val cmdObject = JSONObject()
10974
+ cmdObject.put("C", command)
10975
+ cmdObject.put("V", 1)
10976
+ cmdObject.put("B", body.toString())
10977
+ val packedData =
10978
+ K900ProtocolUtils.packDataToK900(
10979
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
10980
+ K900ProtocolUtils.CMD_TYPE_STRING,
10981
+ k900LengthEndian()
10982
+ )
10983
+ if (packedData == null) {
10984
+ Bridge.log("LIVE: Failed to pack " + command)
10985
+ return
10986
+ }
10987
+ Bridge.log("LIVE: 👓 " + command + " " + body.toString())
10988
+ queueData(packedData)
10989
+ } catch (e: JSONException) {
10990
+ Log.e(TAG, "Error creating " + command, e)
10991
+ }
10992
+ }
10993
+
10994
+ /** Enable or disable the sr_micrms readout. */
10995
+ override fun setMicRmsTelemetry(enabled: Boolean) {
10996
+ if (!isConnected || !peerMicTuning) return
10997
+ val body = JSONObject()
10998
+ body.put("on", if (enabled) 1 else 0)
10999
+ sendMicTuningCommand("cs_micrms", body)
11000
+ }
11001
+
11002
+ private fun sendMicTuningCommand(command: String, body: JSONObject) {
11003
+ try {
11004
+ val cmdObject = JSONObject()
11005
+ cmdObject.put("C", command)
11006
+ cmdObject.put("V", 1)
11007
+ cmdObject.put("B", body.toString())
11008
+ val packedData =
11009
+ K900ProtocolUtils.packDataToK900(
11010
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
11011
+ K900ProtocolUtils.CMD_TYPE_STRING,
11012
+ k900LengthEndian()
11013
+ )
11014
+ if (packedData == null) {
11015
+ Bridge.log("LIVE: Failed to pack " + command)
11016
+ return
11017
+ }
11018
+ queueData(packedData)
11019
+ } catch (e: JSONException) {
11020
+ Log.e(TAG, "Error creating " + command, e)
11021
+ }
10685
11022
  }
10686
11023
 
10687
11024
  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