@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.
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +31 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +20 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +337 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +13 -0
- package/build/BluetoothSdk.types.d.ts +82 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +31 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +14 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +55 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +27 -0
- package/ios/Source/DeviceManager.swift +20 -0
- package/ios/Source/DeviceStore.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/MentraLive.swift +237 -0
- package/ios/Source/sgcs/SGCManager.swift +32 -0
- package/ios/Tests/WearTuningDispatchTests.swift +51 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +88 -0
- package/src/_private/BluetoothSdkModule.ts +62 -0
- 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
|
-
|
|
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.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.2.1-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
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
|
|