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