@mentra/bluetooth-sdk 3.2.0-dev.265 → 3.2.1-beta.312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -50,6 +50,8 @@ struct ViewState {
|
|
|
50
50
|
private var cancellables = Set<AnyCancellable>()
|
|
51
51
|
#if os(macOS)
|
|
52
52
|
private var audioRouteObserver: MacAudioRouteObserver?
|
|
53
|
+
#else
|
|
54
|
+
private var audioRouteObservation: AudioRouteObservation?
|
|
53
55
|
#endif
|
|
54
56
|
var sendStateWorkItem: DispatchWorkItem?
|
|
55
57
|
let sendStateQueue = DispatchQueue(label: "sendStateQueue", qos: .userInitiated)
|
|
@@ -366,6 +368,18 @@ struct ViewState {
|
|
|
366
368
|
Bridge.log("MAN: init()")
|
|
367
369
|
super.init()
|
|
368
370
|
|
|
371
|
+
#if !os(macOS)
|
|
372
|
+
// Pair Audio precedes BLE readiness and microphone capture. Its audio
|
|
373
|
+
// state must not depend on the lazily created PhoneMic singleton.
|
|
374
|
+
audioRouteObservation = AudioRouteObservation(names: [
|
|
375
|
+
AVAudioSession.routeChangeNotification,
|
|
376
|
+
UIApplication.didBecomeActiveNotification,
|
|
377
|
+
]) { [weak self] in
|
|
378
|
+
self?.refreshAudioDeviceState()
|
|
379
|
+
self?.updateMicState()
|
|
380
|
+
}
|
|
381
|
+
#endif
|
|
382
|
+
|
|
369
383
|
// Start memory monitoring (logs every 30s to help detect leaks)
|
|
370
384
|
// MemoryMonitor.start()
|
|
371
385
|
|
|
@@ -929,6 +943,16 @@ struct ViewState {
|
|
|
929
943
|
}
|
|
930
944
|
|
|
931
945
|
func checkCurrentAudioDevice() {
|
|
946
|
+
#if os(macOS)
|
|
947
|
+
refreshAudioDeviceState()
|
|
948
|
+
#else
|
|
949
|
+
// DeviceStore calls this when the selected glasses change. Replace any
|
|
950
|
+
// pending route checks so the new target gets its own settling window.
|
|
951
|
+
audioRouteObservation?.refresh()
|
|
952
|
+
#endif
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private func refreshAudioDeviceState() {
|
|
932
956
|
let audioDevicePattern = getAudioDevicePattern()
|
|
933
957
|
Bridge.log("MAN: checkCurrentAudioDevice: audioDevicePattern: \(audioDevicePattern)")
|
|
934
958
|
|
|
@@ -942,59 +966,13 @@ struct ViewState {
|
|
|
942
966
|
glassesBluetoothClassicConnected = AudioSessionMonitor.isAudioDeviceConnected(devicePattern: audioDevicePattern)
|
|
943
967
|
otherBtConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(devicePattern: audioDevicePattern)
|
|
944
968
|
#else
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
if !isConnected {
|
|
951
|
-
Bridge.log("MAN: Device '\(deviceName)' disconnected")
|
|
952
|
-
glassesBluetoothClassicConnected = false
|
|
953
|
-
|
|
954
|
-
let isOtherDeviceConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(
|
|
955
|
-
devicePattern: audioDevicePattern
|
|
956
|
-
)
|
|
957
|
-
if isOtherDeviceConnected {
|
|
958
|
-
Bridge.log("MAN: Other device connected, returning")
|
|
959
|
-
otherBtConnected = true
|
|
960
|
-
}
|
|
961
|
-
return
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
965
|
-
if isPaired {
|
|
966
|
-
Bridge.log("MAN: Successfully detected newly paired device '\(audioDevicePattern)'")
|
|
967
|
-
glassesBluetoothClassicConnected = true
|
|
968
|
-
} else {
|
|
969
|
-
glassesBluetoothClassicConnected = false
|
|
970
|
-
}
|
|
969
|
+
// A connected HFP input remains eligible even while another output is
|
|
970
|
+
// selected. Inspect available inputs as well as the active A2DP route.
|
|
971
|
+
glassesBluetoothClassicConnected = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
972
|
+
otherBtConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(devicePattern: audioDevicePattern)
|
|
971
973
|
#endif
|
|
972
974
|
}
|
|
973
975
|
|
|
974
|
-
#if !os(macOS)
|
|
975
|
-
func onRouteChange(
|
|
976
|
-
reason: AVAudioSession.RouteChangeReason, availableInputs: [AVAudioSessionPortDescription]
|
|
977
|
-
) {
|
|
978
|
-
Bridge.log("MAN: onRouteChange: reason: \(reason)")
|
|
979
|
-
Bridge.log("MAN: onRouteChange: inputs: \(availableInputs)")
|
|
980
|
-
|
|
981
|
-
// check if our deviceName is connected:
|
|
982
|
-
// (return if deviceName is empty):
|
|
983
|
-
if deviceName.isEmpty {
|
|
984
|
-
Bridge.log("MAN: Device name is empty, returning")
|
|
985
|
-
return
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Add small delay to let iOS populate availableInputs
|
|
989
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
990
|
-
guard let self = self else { return }
|
|
991
|
-
checkCurrentAudioDevice()
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
updateMicState()
|
|
995
|
-
}
|
|
996
|
-
#endif
|
|
997
|
-
|
|
998
976
|
func onInterruption(began: Bool) {
|
|
999
977
|
Bridge.log("MAN: Interruption: \(began)")
|
|
1000
978
|
systemMicUnavailable = began
|
|
@@ -1488,6 +1466,26 @@ struct ViewState {
|
|
|
1488
1466
|
|
|
1489
1467
|
func dbg2() {}
|
|
1490
1468
|
|
|
1469
|
+
func queryWearState() {
|
|
1470
|
+
sgc?.queryWearState()
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
func setWearReporting(_ enabled: Bool) {
|
|
1474
|
+
sgc?.setWearReporting(enabled)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
func setWearTuning(intervalMs: Int, count: Int, majority: Int) {
|
|
1478
|
+
sgc?.setWearTuning(intervalMs: intervalMs, count: count, majority: majority)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
func requestWearTuning() {
|
|
1482
|
+
sgc?.requestWearTuning()
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
func resetWearTuning() {
|
|
1486
|
+
sgc?.resetWearTuning()
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1491
1489
|
func startStream(_ message: [String: Any]) {
|
|
1492
1490
|
var message = message
|
|
1493
1491
|
Bridge.log("MAN: startStream: \(message)")
|
|
@@ -1586,6 +1584,10 @@ struct ViewState {
|
|
|
1586
1584
|
return live
|
|
1587
1585
|
}
|
|
1588
1586
|
|
|
1587
|
+
func sendGalleryServerEnabled(requestId: String, enabled: Bool) throws {
|
|
1588
|
+
try liveSgc().sendGalleryServerEnabled(requestId: requestId, enabled: enabled)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1589
1591
|
func sendGalleryMode(requestId: String, enabled: Bool) throws {
|
|
1590
1592
|
try liveSgc().sendGalleryMode(requestId: requestId, active: enabled)
|
|
1591
1593
|
}
|
|
@@ -254,6 +254,12 @@ class DeviceStore {
|
|
|
254
254
|
case ("bluetooth", "loudness_gate_enabled"):
|
|
255
255
|
DeviceManager.shared.sgc?.sendLoudnessGateSetting()
|
|
256
256
|
|
|
257
|
+
// Deliberately has no seeded default: the key starts absent so that a
|
|
258
|
+
// session where the engine has not authorized tuning can only ever
|
|
259
|
+
// send a reset.
|
|
260
|
+
case ("bluetooth", "mic_tuning"):
|
|
261
|
+
DeviceManager.shared.sgc?.sendMicTuningSetting()
|
|
262
|
+
|
|
257
263
|
case ("bluetooth", "screen_disabled"):
|
|
258
264
|
if let disabled = value as? Bool {
|
|
259
265
|
if disabled {
|
|
@@ -2,7 +2,8 @@ import Foundation
|
|
|
2
2
|
|
|
3
3
|
/// Generated from /changelogs. Do not edit directly.
|
|
4
4
|
let generatedReleaseChangelogs: [ReleaseChangelog] = [
|
|
5
|
+
ReleaseChangelog(version: "3.2.1", markdown: "This release is under active development. User-facing changes will be documented as they land."),
|
|
5
6
|
ReleaseChangelog(version: "3.2.0", markdown: "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."),
|
|
6
|
-
ReleaseChangelog(version: "3.1.1", markdown: "
|
|
7
|
-
ReleaseChangelog(version: "3.1.0", markdown: "
|
|
7
|
+
ReleaseChangelog(version: "3.1.1", markdown: "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access."),
|
|
8
|
+
ReleaseChangelog(version: "3.1.0", markdown: "**Mentra App and glasses**\n\n- Improved glasses pairing and reconnection, including on iPad.\n- Improved Android background reliability for captions and miniapps.\n- Added Xingyi AR99 support.\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\n- Added video thumbnails and smoother gallery browsing.\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\n\n**Miniapp updates**\n\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\n- **Translation:** Restored all 60 languages.\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\n- **Mentra Notes:** Smoother back navigation.\n- **Recorder:** Fixed audio sharing."),
|
|
8
9
|
]
|
|
@@ -2,10 +2,10 @@ import Foundation
|
|
|
2
2
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
|
-
static let familyBaseVersion = "3.2.
|
|
6
|
-
static let releaseIdentity = "3.2.
|
|
7
|
-
static let releaseSetId = "mentra-3.2.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://
|
|
10
|
-
static let otaManifestSha256 = "
|
|
5
|
+
static let familyBaseVersion = "3.2.1"
|
|
6
|
+
static let releaseIdentity = "3.2.1-beta.312"
|
|
7
|
+
static let releaseSetId = "mentra-3.2.1-beta.312"
|
|
8
|
+
static let sourceCommit = "0e549d30e1de230cc9f22e77b80ca1704833cceb"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.312.json"
|
|
10
|
+
static let otaManifestSha256 = "c7d7975d15ab271035e7e26431be200d91664cc4c85b761e373ed4bc4026a3ac"
|
|
11
11
|
}
|
|
@@ -639,6 +639,17 @@ public final class MentraBluetoothSDK {
|
|
|
639
639
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "screen_disabled", disabled)
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
/// Enable or disable persistent, unauthenticated HTTP gallery access on the glasses' Wi-Fi.
|
|
643
|
+
/// Defaults off and survives glasses restarts. Only enable on a trusted network.
|
|
644
|
+
/// The ack reports the saved `enabled` state and current `listening`/`url` separately.
|
|
645
|
+
public func setGalleryServerEnabled(_ enabled: Bool) async throws -> SettingsAckEvent {
|
|
646
|
+
try await performSettingsCommand(
|
|
647
|
+
setting: "gallery_server",
|
|
648
|
+
updateStore: { _ in },
|
|
649
|
+
send: { requestId in try DeviceManager.shared.sendGalleryServerEnabled(requestId: requestId, enabled: enabled) }
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
642
653
|
public func setGalleryModeEnabled(_ enabled: Bool) async throws -> SettingsAckEvent {
|
|
643
654
|
try await performSettingsCommand(
|
|
644
655
|
setting: "gallery_mode",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
import Foundation
|
|
3
|
+
import os
|
|
3
4
|
|
|
4
5
|
private enum PcmStreamError: LocalizedError {
|
|
5
6
|
case invalidArgument(String)
|
|
@@ -25,6 +26,7 @@ private enum PcmStreamError: LocalizedError {
|
|
|
25
26
|
/// recovery, close, and abort deterministic without blocking Expo's module
|
|
26
27
|
/// queue.
|
|
27
28
|
final class PcmStreamPlayer: @unchecked Sendable {
|
|
29
|
+
private let logger = Logger(subsystem: "com.mentra.pcm-stream", category: "playback")
|
|
28
30
|
private static let backpressureCeilingMs: Int64 = 2000
|
|
29
31
|
private static let maximumBacklogMs: Int64 = 10000
|
|
30
32
|
|
|
@@ -37,6 +39,7 @@ final class PcmStreamPlayer: @unchecked Sendable {
|
|
|
37
39
|
|
|
38
40
|
private var queuedFrames: Int64 = 0
|
|
39
41
|
private var playedFrames: Int64 = 0
|
|
42
|
+
private var writtenFrames: Int64 = 0
|
|
40
43
|
private var closing = false
|
|
41
44
|
private var aborted = false
|
|
42
45
|
private var finished = false
|
|
@@ -107,6 +110,10 @@ final class PcmStreamPlayer: @unchecked Sendable {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
let buffer = try makeBuffer(data: data, frameCount: frameCount)
|
|
113
|
+
if writtenFrames == 0 {
|
|
114
|
+
logger.info("PCM_PLAYBACK first_write frames=\(frameCount) sampleRate=\(self.sampleRate) engineRunning=\(self.engine.isRunning)")
|
|
115
|
+
}
|
|
116
|
+
writtenFrames += Int64(frameCount)
|
|
110
117
|
queuedFrames += Int64(frameCount)
|
|
111
118
|
player.scheduleBuffer(
|
|
112
119
|
buffer,
|
|
@@ -157,6 +164,7 @@ final class PcmStreamPlayer: @unchecked Sendable {
|
|
|
157
164
|
await withCheckedContinuation { continuation in
|
|
158
165
|
stateQueue.async { [self] in
|
|
159
166
|
if !aborted {
|
|
167
|
+
logger.info("PCM_PLAYBACK abort writtenFrames=\(self.writtenFrames) playedFrames=\(self.playedFrames) queuedFrames=\(self.queuedFrames)")
|
|
160
168
|
aborted = true
|
|
161
169
|
finished = true
|
|
162
170
|
player.stop()
|
|
@@ -201,6 +209,10 @@ final class PcmStreamPlayer: @unchecked Sendable {
|
|
|
201
209
|
|
|
202
210
|
private func didPlay(frameCount: Int) {
|
|
203
211
|
guard !aborted, terminalError == nil else { return }
|
|
212
|
+
if playedFrames == 0 {
|
|
213
|
+
let rate = sampleRate
|
|
214
|
+
logger.info("PCM_PLAYBACK first_played frames=\(frameCount) sampleRate=\(rate)")
|
|
215
|
+
}
|
|
204
216
|
queuedFrames = max(0, queuedFrames - Int64(frameCount))
|
|
205
217
|
playedFrames += Int64(frameCount)
|
|
206
218
|
|
|
@@ -206,6 +206,21 @@ public struct SettingsAckEvent: CustomStringConvertible {
|
|
|
206
206
|
stringValue(values, "errorMessage")
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/// Saved setting value, when included in the acknowledgement.
|
|
210
|
+
public var enabled: Bool? {
|
|
211
|
+
boolValue(values, "enabled")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Whether the persistent gallery server is currently available over station Wi-Fi.
|
|
215
|
+
public var listening: Bool? {
|
|
216
|
+
boolValue(values, "listening")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Current gallery base URL, available only with a running server and station Wi-Fi.
|
|
220
|
+
public var url: String? {
|
|
221
|
+
stringValue(values, "url")
|
|
222
|
+
}
|
|
223
|
+
|
|
209
224
|
public var description: String {
|
|
210
225
|
"SettingsAckEvent(setting: \(setting), status: \(status))"
|
|
211
226
|
}
|
|
@@ -359,9 +359,7 @@ class PhoneMic {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
Bridge.log("MIC: handleRouteChange: \(reason)")
|
|
362
|
-
DeviceManager.
|
|
363
|
-
reason: reason, availableInputs: audioSession?.availableInputs ?? []
|
|
364
|
-
)
|
|
362
|
+
// DeviceManager observes readiness independently of microphone capture.
|
|
365
363
|
|
|
366
364
|
// // If we're recording and the audio route changed (e.g., AirPods connected/disconnected)
|
|
367
365
|
// if isRecording {
|
|
@@ -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.
|
|
@@ -2646,7 +2665,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2646
2665
|
|
|
2647
2666
|
// MARK: - Data Processing
|
|
2648
2667
|
|
|
2649
|
-
|
|
2668
|
+
// Internal so native integration tests can replay the actual BLE receive path.
|
|
2669
|
+
func processReceivedData(_ data: Data) {
|
|
2650
2670
|
guard data.count > 0 else { return }
|
|
2651
2671
|
|
|
2652
2672
|
let bytes = [UInt8](data)
|
|
@@ -3427,6 +3447,35 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3427
3447
|
)
|
|
3428
3448
|
}
|
|
3429
3449
|
|
|
3450
|
+
case "sr_mictun", "sr_micst":
|
|
3451
|
+
if let body = k900ParseBody(json["B"]) {
|
|
3452
|
+
handleMicTuningState(body)
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
case "sr_weartun":
|
|
3456
|
+
if let body = k900ParseBody(json["B"]) {
|
|
3457
|
+
handleWearTuningState(json, body)
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
case "sr_wrst":
|
|
3461
|
+
if let body = k900ParseBody(json["B"]) {
|
|
3462
|
+
Bridge.sendWearState(worn: (k900JsonInt(body, "on") ?? 0) != 0)
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
case "sr_micrms":
|
|
3466
|
+
if let body = k900ParseBody(json["B"]) {
|
|
3467
|
+
let generation = k900JsonInt(body, "gen") ?? 0
|
|
3468
|
+
// Measured under a config we have already replaced.
|
|
3469
|
+
if generation >= micTuningGeneration {
|
|
3470
|
+
Bridge.sendMicRms(
|
|
3471
|
+
rms: k900JsonInt(body, "rms") ?? 0,
|
|
3472
|
+
gateOpen: (k900JsonInt(body, "gate") ?? 0) != 0,
|
|
3473
|
+
speakerElevated: (k900JsonInt(body, "sp") ?? 0) != 0,
|
|
3474
|
+
generation: generation
|
|
3475
|
+
)
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3430
3479
|
case "sr_shut":
|
|
3431
3480
|
Bridge.log("K900 shutdown command received - glasses shutting down")
|
|
3432
3481
|
// Mark as killed to prevent reconnection attempts
|
|
@@ -3781,6 +3830,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3781
3830
|
sendJson(json, wakeUp: true)
|
|
3782
3831
|
}
|
|
3783
3832
|
|
|
3833
|
+
func sendGalleryServerEnabled(requestId: String, enabled: Bool) {
|
|
3834
|
+
sendJson([
|
|
3835
|
+
"type": "set_gallery_server_enabled",
|
|
3836
|
+
"request_id": requestId,
|
|
3837
|
+
"enabled": enabled,
|
|
3838
|
+
], wakeUp: true)
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3784
3841
|
func sendGalleryMode() {
|
|
3785
3842
|
let active = DeviceStore.shared.get("bluetooth", "gallery_mode") as! Bool
|
|
3786
3843
|
sendGalleryMode(requestId: nil, active: active)
|
|
@@ -6049,6 +6106,9 @@ extension MentraLive {
|
|
|
6049
6106
|
peerK900Le = false
|
|
6050
6107
|
peerWireCapsBinary = false
|
|
6051
6108
|
peerFilePayloadV2 = false
|
|
6109
|
+
peerWearTuning = false
|
|
6110
|
+
peerMicTuning = false
|
|
6111
|
+
micTuningGeneration = 0
|
|
6052
6112
|
BleJsonCompact.resetSession()
|
|
6053
6113
|
wireHandshakeSentGeneration = -1
|
|
6054
6114
|
}
|
|
@@ -6118,6 +6178,33 @@ extension MentraLive {
|
|
|
6118
6178
|
if caps.keys.contains("file_payload_v2") {
|
|
6119
6179
|
peerFilePayloadV2 = (caps["file_payload_v2"] as? Bool) == true
|
|
6120
6180
|
}
|
|
6181
|
+
if caps.keys.contains("wear_tuning"), !peerWearTuning {
|
|
6182
|
+
let supported = (caps["wear_tuning"] as? Bool) == true
|
|
6183
|
+
|| ((caps["wear_tuning"] as? NSNumber)?.intValue ?? 0) != 0
|
|
6184
|
+
if supported {
|
|
6185
|
+
peerWearTuning = true
|
|
6186
|
+
Bridge.log("LIVE: wire_caps wear_tuning supported")
|
|
6187
|
+
// Nothing to push: wear reporting starts off on the glasses
|
|
6188
|
+
// and stays off until the tuning screen asks for it. One read
|
|
6189
|
+
// per link: the flag is cleared on every wire epoch, but the
|
|
6190
|
+
// glasses only forget tuning on BLE disconnect.
|
|
6191
|
+
if !wearTuningQueriedThisLink {
|
|
6192
|
+
wearTuningQueriedThisLink = true
|
|
6193
|
+
requestWearTuning()
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
if caps.keys.contains("mic_tuning"), !peerMicTuning {
|
|
6198
|
+
let supported = (caps["mic_tuning"] as? Bool) == true
|
|
6199
|
+
|| ((caps["mic_tuning"] as? NSNumber)?.intValue ?? 0) != 0
|
|
6200
|
+
if supported {
|
|
6201
|
+
peerMicTuning = true
|
|
6202
|
+
// Caps can land after the on-connect batch already ran, which
|
|
6203
|
+
// would have skipped the tuning send.
|
|
6204
|
+
Bridge.log("LIVE: wire_caps mic_tuning supported")
|
|
6205
|
+
sendMicTuningSetting()
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6121
6208
|
}
|
|
6122
6209
|
|
|
6123
6210
|
private func advertiseFilePayloadCapabilityToBes() {
|
|
@@ -6786,6 +6873,165 @@ extension MentraLive {
|
|
|
6786
6873
|
|
|
6787
6874
|
// Send glasses-side loudness / Barrier gate setting.
|
|
6788
6875
|
sendLoudnessGateSetting()
|
|
6876
|
+
|
|
6877
|
+
// Send mic tuning. With nothing authorized this sends a reset, which is
|
|
6878
|
+
// what returns a freshly connected pair of glasses to stock behaviour.
|
|
6879
|
+
sendMicTuningSetting()
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6882
|
+
/// Mic tuning field names, matching the BES cs_mictun body.
|
|
6883
|
+
private static let micTuningFields = [
|
|
6884
|
+
"gain", "open", "close", "attack", "hang", "sp_open", "sp_close", "sp_hold",
|
|
6885
|
+
]
|
|
6886
|
+
|
|
6887
|
+
/// Push the effective mic tuning to the glasses.
|
|
6888
|
+
///
|
|
6889
|
+
/// The store holds only what the engine has authorized for this process; a
|
|
6890
|
+
/// missing value means "no tuning", which is sent as an explicit reset
|
|
6891
|
+
/// rather than silently skipped. That is what keeps a persisted super-mode
|
|
6892
|
+
/// value from surviving into a session where super mode is off.
|
|
6893
|
+
func sendMicTuningSetting() {
|
|
6894
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
6895
|
+
Bridge.log("LIVE: Cannot send mic tuning - BLE write path not ready")
|
|
6896
|
+
return
|
|
6897
|
+
}
|
|
6898
|
+
if !peerMicTuning {
|
|
6899
|
+
Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_mictun anyway")
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
var body: [String: Any] = [:]
|
|
6903
|
+
if let fields = DeviceStore.shared.get("bluetooth", "mic_tuning") as? [String: Any] {
|
|
6904
|
+
for name in Self.micTuningFields {
|
|
6905
|
+
if let number = fields[name] as? NSNumber {
|
|
6906
|
+
body[name] = number.intValue
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
if body.isEmpty {
|
|
6911
|
+
body["reset"] = 1
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
// Key order is fixed so equal bodies serialize identically.
|
|
6915
|
+
let serialized = body.keys.sorted().map { "\($0)=\(body[$0]!)" }.joined(separator: ",")
|
|
6916
|
+
if serialized == lastSentMicTuningBody {
|
|
6917
|
+
Bridge.log("LIVE: 🎚️ Mic tuning unchanged this link, not resending: \(serialized)")
|
|
6918
|
+
return
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
Bridge.log("LIVE: 🎚️ Sending mic tuning to glasses: \(body)")
|
|
6922
|
+
sendMicTuningCommand("cs_mictun", body: body)
|
|
6923
|
+
lastSentMicTuningBody = serialized
|
|
6924
|
+
}
|
|
6925
|
+
|
|
6926
|
+
/// Ask the glasses what tuning they are actually running (sr_micst).
|
|
6927
|
+
func requestMicTuningState() {
|
|
6928
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
6929
|
+
Bridge.log("LIVE: Cannot send cs_micst - BLE write path not ready")
|
|
6930
|
+
return
|
|
6931
|
+
}
|
|
6932
|
+
if !peerMicTuning {
|
|
6933
|
+
Bridge.log("LIVE: mic_tuning cap not advertised; sending cs_micst anyway")
|
|
6934
|
+
}
|
|
6935
|
+
sendMicTuningCommand("cs_micst", body: [:])
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6938
|
+
/// Enable or disable the sr_micrms readout.
|
|
6939
|
+
func setMicRmsTelemetry(_ enabled: Bool) {
|
|
6940
|
+
guard connectedPeripheral != nil, txCharacteristic != nil, peerMicTuning else { return }
|
|
6941
|
+
sendMicTuningCommand("cs_micrms", body: ["on": enabled ? 1 : 0])
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6944
|
+
/// Read the current wear state (sr_wrst). Always available.
|
|
6945
|
+
@objc func queryWearState() {
|
|
6946
|
+
sendWearCommandIfReady("cs_wrst", body: [:])
|
|
6947
|
+
}
|
|
6948
|
+
|
|
6949
|
+
/// Turn wear reporting on or off for this session.
|
|
6950
|
+
///
|
|
6951
|
+
/// Deliberately not the NV-backed cs_swit type 1: the glasses must forget
|
|
6952
|
+
/// this on disconnect, and any later switch write would re-persist a wear
|
|
6953
|
+
/// bit that had been enabled once.
|
|
6954
|
+
@objc func setWearReporting(_ enabled: Bool) {
|
|
6955
|
+
sendWearCommandIfReady("cs_weartun", body: ["enabled": enabled ? 1 : 0])
|
|
6956
|
+
}
|
|
6957
|
+
|
|
6958
|
+
/// Move the debounce vote. Negative means "leave this one alone".
|
|
6959
|
+
@objc func setWearTuning(intervalMs: Int, count: Int, majority: Int) {
|
|
6960
|
+
var body: [String: Any] = [:]
|
|
6961
|
+
if intervalMs >= 0 { body["interval"] = intervalMs }
|
|
6962
|
+
if count >= 0 { body["count"] = count }
|
|
6963
|
+
if majority >= 0 { body["majority"] = majority }
|
|
6964
|
+
guard !body.isEmpty else { return }
|
|
6965
|
+
sendWearCommandIfReady("cs_weartun", body: body)
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6968
|
+
/// Ask what the poll loop is actually running (sr_weartun).
|
|
6969
|
+
@objc func requestWearTuning() {
|
|
6970
|
+
sendWearCommandIfReady("cs_wearst", body: [:])
|
|
6971
|
+
}
|
|
6972
|
+
|
|
6973
|
+
/// Restore firmware defaults and disable reporting. Distinct from sending
|
|
6974
|
+
/// the default vote values, which would leave reporting on.
|
|
6975
|
+
@objc func resetWearTuning() {
|
|
6976
|
+
sendWearCommandIfReady("cs_weartun", body: ["reset": 1])
|
|
6977
|
+
}
|
|
6978
|
+
|
|
6979
|
+
private func sendWearCommandIfReady(_ name: String, body: [String: Any]) {
|
|
6980
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
6981
|
+
Bridge.log("LIVE: Cannot send \(name) - BLE write path not ready")
|
|
6982
|
+
return
|
|
6983
|
+
}
|
|
6984
|
+
if !peerWearTuning && name != "cs_wrst" {
|
|
6985
|
+
Bridge.log("LIVE: wear_tuning cap not advertised; sending \(name) anyway")
|
|
6986
|
+
}
|
|
6987
|
+
sendMicTuningCommand(name, body: body)
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6990
|
+
private func sendMicTuningCommand(_ name: String, body: [String: Any]) {
|
|
6991
|
+
do {
|
|
6992
|
+
let bodyData = try JSONSerialization.data(withJSONObject: body)
|
|
6993
|
+
guard let bodyString = String(data: bodyData, encoding: .utf8) else {
|
|
6994
|
+
Bridge.log("LIVE: Failed to encode \(name) payload")
|
|
6995
|
+
return
|
|
6996
|
+
}
|
|
6997
|
+
let command: [String: Any] = ["C": name, "V": 1, "B": bodyString]
|
|
6998
|
+
if !sendRawK900Command(command, wakeUp: true) {
|
|
6999
|
+
Bridge.log("LIVE: Failed to send \(name)")
|
|
7000
|
+
}
|
|
7001
|
+
} catch {
|
|
7002
|
+
Bridge.log("LIVE: Error encoding \(name) payload: \(error)")
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
7005
|
+
|
|
7006
|
+
/// Post-clamp tuning the glasses report as in force. Forwarded verbatim so
|
|
7007
|
+
/// the screen can show the applied value next to the requested one.
|
|
7008
|
+
private func handleMicTuningState(_ body: [String: Any]) {
|
|
7009
|
+
var state: [String: Any] = [:]
|
|
7010
|
+
for name in Self.micTuningFields {
|
|
7011
|
+
if let number = body[name] as? NSNumber {
|
|
7012
|
+
state[name] = number.intValue
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
let generation = (body["gen"] as? NSNumber)?.intValue ?? 0
|
|
7016
|
+
state["generation"] = generation
|
|
7017
|
+
state["overridden"] = ((body["ovr"] as? NSNumber)?.intValue ?? 0) != 0
|
|
7018
|
+
micTuningGeneration = generation
|
|
7019
|
+
Bridge.sendMicTuningState(state)
|
|
7020
|
+
}
|
|
7021
|
+
|
|
7022
|
+
/// What the wear poll loop is actually running. A rejected patch comes back
|
|
7023
|
+
/// with the unchanged values and a non-zero result code, so the screen can
|
|
7024
|
+
/// show that the request did not take.
|
|
7025
|
+
private func handleWearTuningState(_ json: [String: Any], _ body: [String: Any]) {
|
|
7026
|
+
let state: [String: Any] = [
|
|
7027
|
+
"enabled": (k900JsonInt(body, "enabled") ?? 0) != 0,
|
|
7028
|
+
"interval": k900JsonInt(body, "interval") ?? 0,
|
|
7029
|
+
"count": k900JsonInt(body, "count") ?? 0,
|
|
7030
|
+
"majority": k900JsonInt(body, "majority") ?? 0,
|
|
7031
|
+
"generation": k900JsonInt(body, "gen") ?? 0,
|
|
7032
|
+
"accepted": (k900JsonInt(json, "S") ?? 0) == 0,
|
|
7033
|
+
]
|
|
7034
|
+
Bridge.sendWearTuningState(state)
|
|
6789
7035
|
}
|
|
6790
7036
|
|
|
6791
7037
|
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")
|