@mentra/bluetooth-sdk 3.2.0-dev.266 → 3.2.1-beta.315
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/README.md +28 -0
- package/android/lc3Lib/build.gradle +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +30 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +20 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +4 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +345 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +13 -0
- package/build/BluetoothSdk.types.d.ts +93 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +32 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +15 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +60 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +27 -0
- package/ios/Source/DeviceManager.swift +52 -50
- package/ios/Source/DeviceStore.swift +6 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +11 -0
- package/ios/Source/audio/PcmStreamPlayer.swift +12 -0
- package/ios/Source/events/BluetoothEvents.swift +15 -0
- package/ios/Source/services/PhoneMic.swift +1 -3
- package/ios/Source/sgcs/MentraLive.swift +247 -1
- package/ios/Source/sgcs/SGCManager.swift +32 -0
- package/ios/Source/utils/AudioRouteObservation.swift +54 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +20 -6
- package/ios/Tests/AudioRouteObservationTests.swift +142 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +37 -0
- package/ios/Tests/GalleryServerCommandTests.swift +87 -0
- package/ios/Tests/WearTuningDispatchTests.swift +51 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +99 -0
- package/src/_private/BluetoothSdkModule.ts +64 -1
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- 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;
|