@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.2.1
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 +4 -4
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +13 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- 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 +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +201 -156
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -318,9 +318,9 @@ struct ViewState {
|
|
|
318
318
|
var lc3Converter: PcmConverter?
|
|
319
319
|
/// Audio output format - defaults to LC3 for bandwidth savings
|
|
320
320
|
private var audioOutputFormat: AudioOutputFormat = .lc3
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
private var micWatchdog = GlassesMicWatchdog()
|
|
322
|
+
// Injectable monotonic clock so recovery deadlines can be tested without sleeping.
|
|
323
|
+
var micWatchdogNow: () -> TimeInterval = { ProcessInfo.processInfo.systemUptime }
|
|
324
324
|
private var sequenceGapEvents: Int64 = 0
|
|
325
325
|
private var decodeFailures: Int64 = 0
|
|
326
326
|
private var lastLc3ReceivedAt: Int64?
|
|
@@ -330,7 +330,7 @@ struct ViewState {
|
|
|
330
330
|
|
|
331
331
|
/// STT:
|
|
332
332
|
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
333
|
-
private
|
|
333
|
+
private let transcriber = SherpaOnnxTranscriber()
|
|
334
334
|
#endif
|
|
335
335
|
|
|
336
336
|
var viewStates: [ViewState] = [
|
|
@@ -383,24 +383,6 @@ struct ViewState {
|
|
|
383
383
|
// Start memory monitoring (logs every 30s to help detect leaks)
|
|
384
384
|
// MemoryMonitor.start()
|
|
385
385
|
|
|
386
|
-
// Initialize SherpaOnnx Transcriber
|
|
387
|
-
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
388
|
-
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
389
|
-
let window = windowScene.windows.first,
|
|
390
|
-
let rootViewController = window.rootViewController
|
|
391
|
-
{
|
|
392
|
-
transcriber = SherpaOnnxTranscriber(context: rootViewController)
|
|
393
|
-
} else {
|
|
394
|
-
Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Initialize the transcriber
|
|
398
|
-
if let transcriber = transcriber {
|
|
399
|
-
transcriber.initialize()
|
|
400
|
-
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
401
|
-
}
|
|
402
|
-
#endif
|
|
403
|
-
|
|
404
386
|
// Initialize persistent LC3 converter for unified audio encoding
|
|
405
387
|
lc3Converter = PcmConverter()
|
|
406
388
|
Bridge.log("LC3 converter initialized for unified audio encoding")
|
|
@@ -472,10 +454,17 @@ struct ViewState {
|
|
|
472
454
|
}
|
|
473
455
|
|
|
474
456
|
func reportGlassesAudioActivity() {
|
|
475
|
-
|
|
457
|
+
micWatchdog.receivedAudio(at: micWatchdogNow())
|
|
476
458
|
lastPcmProducedAt = nowMs()
|
|
477
459
|
}
|
|
478
460
|
|
|
461
|
+
/// Decoded glasses audio must update recovery health; phone PCM must not.
|
|
462
|
+
func handleGlassesPcm(_ pcmData: Data) {
|
|
463
|
+
guard !pcmData.isEmpty else { return }
|
|
464
|
+
reportGlassesAudioActivity()
|
|
465
|
+
handlePcm(pcmData)
|
|
466
|
+
}
|
|
467
|
+
|
|
479
468
|
private func micHealth() -> MicHealth {
|
|
480
469
|
MicHealth(
|
|
481
470
|
sequenceGapEvents: sequenceGapEvents,
|
|
@@ -499,13 +488,13 @@ struct ViewState {
|
|
|
499
488
|
lastLc3ReceivedAt = nil
|
|
500
489
|
lastPcmProducedAt = nil
|
|
501
490
|
lastLc3Sequence = nil
|
|
502
|
-
|
|
491
|
+
micWatchdog = GlassesMicWatchdog()
|
|
503
492
|
}
|
|
504
493
|
|
|
505
494
|
private func recordLc3Packet(sequenceNumber: Int?) {
|
|
506
495
|
let receivedAt = nowMs()
|
|
507
496
|
lastLc3ReceivedAt = receivedAt
|
|
508
|
-
|
|
497
|
+
micWatchdog.receivedAudio(at: micWatchdogNow())
|
|
509
498
|
var hasGap = false
|
|
510
499
|
if let sequenceNumber {
|
|
511
500
|
let normalized = sequenceNumber & 0xFF
|
|
@@ -535,7 +524,7 @@ struct ViewState {
|
|
|
535
524
|
// Send PCM to local transcriber.
|
|
536
525
|
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
537
526
|
if shouldSendTranscript || localSttFallbackActive {
|
|
538
|
-
transcriber
|
|
527
|
+
transcriber.acceptAudio(pcm16le: pcmData)
|
|
539
528
|
}
|
|
540
529
|
#endif
|
|
541
530
|
}
|
|
@@ -605,6 +594,7 @@ struct ViewState {
|
|
|
605
594
|
}
|
|
606
595
|
|
|
607
596
|
currentMic = micUsed
|
|
597
|
+
updateGlassesMicExpectation()
|
|
608
598
|
|
|
609
599
|
// log if no mic was found:
|
|
610
600
|
if micUsed == "" && micEnabled {
|
|
@@ -723,6 +713,7 @@ struct ViewState {
|
|
|
723
713
|
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
724
714
|
sgc?.cleanup()
|
|
725
715
|
sgc = nil
|
|
716
|
+
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
726
717
|
resetSystemTimeSync()
|
|
727
718
|
}
|
|
728
719
|
|
|
@@ -902,27 +893,20 @@ struct ViewState {
|
|
|
902
893
|
return result
|
|
903
894
|
}
|
|
904
895
|
|
|
905
|
-
private func
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
let
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
|
|
915
|
-
return
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying() {
|
|
919
|
-
Bridge.log("MAN: Mentra audio is playing; skipping glasses mic recovery")
|
|
920
|
-
return
|
|
921
|
-
}
|
|
896
|
+
private func updateGlassesMicExpectation() {
|
|
897
|
+
// Demand/route and device readiness decide whether audio is expected. The
|
|
898
|
+
// shared micEnabled flag alone is not evidence of a running hardware stream.
|
|
899
|
+
let expected = micEnabled && currentMic == MicTypes.GLASSES_CUSTOM
|
|
900
|
+
&& sgc?.fullyBooted == true && sgc?.hasMic == true
|
|
901
|
+
&& sgc?.isMicSuspendedForAudio != true
|
|
902
|
+
&& !PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying()
|
|
903
|
+
micWatchdog.expectAudio(expected, at: micWatchdogNow())
|
|
904
|
+
}
|
|
922
905
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
906
|
+
func checkAndReinitGlassesMic() {
|
|
907
|
+
updateGlassesMicExpectation()
|
|
908
|
+
if micWatchdog.shouldRetry(at: micWatchdogNow()) {
|
|
909
|
+
Bridge.log("MAN: Expected glasses audio missing for 5 seconds; retrying mic start")
|
|
926
910
|
sgc?.setMicEnabled(true)
|
|
927
911
|
}
|
|
928
912
|
}
|
|
@@ -982,7 +966,7 @@ struct ViewState {
|
|
|
982
966
|
func restartTranscriber() {
|
|
983
967
|
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
984
968
|
Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
|
|
985
|
-
transcriber
|
|
969
|
+
transcriber.restart()
|
|
986
970
|
#else
|
|
987
971
|
Bridge.log("MAN: Local STT is not included in this SwiftPM build")
|
|
988
972
|
#endif
|
|
@@ -998,12 +982,16 @@ struct ViewState {
|
|
|
998
982
|
Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
|
|
999
983
|
resetMicHealth()
|
|
1000
984
|
|
|
985
|
+
// A new identity must never inherit the previous glasses' address when
|
|
986
|
+
// pairing supplied only a name. Same-device reconnects retain their cache.
|
|
987
|
+
let identityChanged = !pendingDeviceName.isEmpty &&
|
|
988
|
+
(pendingDeviceName != deviceName || sgc.type != defaultWearable)
|
|
989
|
+
if identityChanged || !pendingDeviceAddress.isEmpty {
|
|
990
|
+
deviceAddress = pendingDeviceAddress
|
|
991
|
+
}
|
|
1001
992
|
if !pendingDeviceName.isEmpty {
|
|
1002
993
|
deviceName = pendingDeviceName
|
|
1003
994
|
}
|
|
1004
|
-
if !pendingDeviceAddress.isEmpty {
|
|
1005
|
-
deviceAddress = pendingDeviceAddress
|
|
1006
|
-
}
|
|
1007
995
|
clearPendingConnection()
|
|
1008
996
|
defaultWearable = sgc.type
|
|
1009
997
|
searching = false
|
|
@@ -1020,18 +1008,22 @@ struct ViewState {
|
|
|
1020
1008
|
let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
|
|
1021
1009
|
// Fall back to the canonical default (2), matching DeviceStore, not 1.
|
|
1022
1010
|
let rawDepth = DeviceStore.shared.get("bluetooth", "dashboard_depth") as? Int ?? 2
|
|
1023
|
-
let d = min(max(rawDepth, 1), 4)
|
|
1011
|
+
let d = sgc.type == DeviceTypes.NIMO ? min(max(rawDepth, 0), 10) : min(max(rawDepth, 1), 4)
|
|
1024
1012
|
sgc.setDashboardPosition(h, d)
|
|
1025
1013
|
}
|
|
1026
1014
|
|
|
1027
|
-
//
|
|
1015
|
+
// Preserve the connected-edge scene replay on full-frame adapters.
|
|
1028
1016
|
if shouldSendBootingMessage {
|
|
1029
|
-
Task {
|
|
1030
|
-
await sgc.sendTextWall("// MentraOS Connected")
|
|
1031
|
-
try? await Task.sleep(nanoseconds: 3_000_000_000) // 1 second
|
|
1032
|
-
sgc.clearDisplay()
|
|
1033
|
-
}
|
|
1034
1017
|
shouldSendBootingMessage = false
|
|
1018
|
+
if sgc.showConnectionConfirmation {
|
|
1019
|
+
Task {
|
|
1020
|
+
guard (self.sgc as AnyObject?) === (sgc as AnyObject) else { return }
|
|
1021
|
+
await sgc.sendTextWall("// MentraOS Connected")
|
|
1022
|
+
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
|
1023
|
+
guard (self.sgc as AnyObject?) === (sgc as AnyObject) else { return }
|
|
1024
|
+
sgc.clearDisplay()
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1035
1027
|
}
|
|
1036
1028
|
|
|
1037
1029
|
// Call device-specific setup handlers
|
|
@@ -1058,6 +1050,11 @@ struct ViewState {
|
|
|
1058
1050
|
#endif
|
|
1059
1051
|
checkCurrentAudioDevice()
|
|
1060
1052
|
|
|
1053
|
+
// Disconnect clears micEnabled but preserves the consumers' audio requests.
|
|
1054
|
+
// Recompute demand before selecting a microphone; unchanged requests are
|
|
1055
|
+
// deduplicated by DeviceStore.apply().
|
|
1056
|
+
setMicState()
|
|
1057
|
+
|
|
1061
1058
|
// save the default_wearable now that we're connected:
|
|
1062
1059
|
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
1063
1060
|
Bridge.saveSetting("device_name", deviceName)
|
|
@@ -1214,7 +1211,9 @@ struct ViewState {
|
|
|
1214
1211
|
self.dashboardSceneCleanupPending = false
|
|
1215
1212
|
let elementIds = Array(self.pendingDashboardSceneElementIds)
|
|
1216
1213
|
self.pendingDashboardSceneElementIds.removeAll()
|
|
1217
|
-
|
|
1214
|
+
if self.sgc?.sceneHandoffRequiresClear == true {
|
|
1215
|
+
await self.sgc?.clearSceneElements(elementIds)
|
|
1216
|
+
}
|
|
1218
1217
|
}
|
|
1219
1218
|
self.dashboardSceneCleanupTask = nil
|
|
1220
1219
|
}
|
|
@@ -1261,7 +1260,7 @@ struct ViewState {
|
|
|
1261
1260
|
if stateIndex == 1, dashboardSceneCleanupDeferred {
|
|
1262
1261
|
dashboardSceneCleanupPending = true
|
|
1263
1262
|
pendingDashboardSceneElementIds.formUnion(prevFrame.elements.map(\.id))
|
|
1264
|
-
} else if layoutType != "clear_view" {
|
|
1263
|
+
} else if layoutType != "clear_view", shouldClearSceneHandoff(stateIndex) {
|
|
1265
1264
|
let ids = prevFrame.elements.map(\.id)
|
|
1266
1265
|
Task { [weak self] in
|
|
1267
1266
|
await self?.sgc?.clearSceneElements(ids)
|
|
@@ -1355,7 +1354,7 @@ struct ViewState {
|
|
|
1355
1354
|
// on G2 - no page rebuild).
|
|
1356
1355
|
let prevLegacyType = viewStates[stateIndex].layoutType
|
|
1357
1356
|
let cleanupDeferred = stateIndex == 1 && dashboardSceneCleanupDeferred
|
|
1358
|
-
if !cleanupDeferred,
|
|
1357
|
+
if !cleanupDeferred, shouldClearSceneHandoff(stateIndex),
|
|
1359
1358
|
!prevLegacyType.isEmpty,
|
|
1360
1359
|
prevLegacyType != "clear_view",
|
|
1361
1360
|
prevLegacyType != "scene"
|
|
@@ -1371,7 +1370,7 @@ struct ViewState {
|
|
|
1371
1370
|
if stateIndex == 1, dashboardSceneCleanupDeferred {
|
|
1372
1371
|
dashboardSceneCleanupPending = true
|
|
1373
1372
|
pendingDashboardSceneElementIds.formUnion(prevFrame.elements.map(\.id))
|
|
1374
|
-
} else {
|
|
1373
|
+
} else if shouldClearSceneHandoff(stateIndex) {
|
|
1375
1374
|
let ids = prevFrame.elements.map(\.id)
|
|
1376
1375
|
Task { [weak self] in
|
|
1377
1376
|
await self?.sgc?.clearSceneElements(ids)
|
|
@@ -1395,6 +1394,13 @@ struct ViewState {
|
|
|
1395
1394
|
}
|
|
1396
1395
|
}
|
|
1397
1396
|
|
|
1397
|
+
private func shouldClearSceneHandoff(_ stateIndex: Int) -> Bool {
|
|
1398
|
+
let visibleIndex = headUp && contextualDashboard ? 1 : 0
|
|
1399
|
+
guard let sgc else { return false }
|
|
1400
|
+
return stateIndex == visibleIndex && !screenDisabled && sgc.fullyBooted
|
|
1401
|
+
&& !sgc.type.contains(DeviceTypes.SIMULATED) && sgc.sceneHandoffRequiresClear
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1398
1404
|
/// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
|
|
1399
1405
|
private func dispatchSceneFrame(_ frame: SceneFrame, stateIndex: Int) {
|
|
1400
1406
|
if screenDisabled { return }
|
|
@@ -1738,6 +1744,9 @@ struct ViewState {
|
|
|
1738
1744
|
func setMicState() {
|
|
1739
1745
|
let willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1740
1746
|
let willSendTranscript = shouldSendTranscript || localSttFallbackActive
|
|
1747
|
+
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
1748
|
+
transcriber.setActive(willSendTranscript)
|
|
1749
|
+
#endif
|
|
1741
1750
|
micEnabled = willSendPcm || willSendTranscript
|
|
1742
1751
|
updateMicState()
|
|
1743
1752
|
}
|
|
@@ -1943,6 +1952,9 @@ struct ViewState {
|
|
|
1943
1952
|
sgc?.clearDisplay() // clear the screen
|
|
1944
1953
|
sgc?.disconnect()
|
|
1945
1954
|
sgc = nil // Clear the SGC reference after disconnect
|
|
1955
|
+
// This cache belongs to the discarded connection. Keep consumer demand,
|
|
1956
|
+
// but require a new mic-enable command when replacement glasses are ready.
|
|
1957
|
+
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
1946
1958
|
resetSystemTimeSync()
|
|
1947
1959
|
resetMicHealth()
|
|
1948
1960
|
searching = false
|
|
@@ -1987,6 +1999,28 @@ struct ViewState {
|
|
|
1987
1999
|
controller = nil // Clear the controller reference after disconnect
|
|
1988
2000
|
}
|
|
1989
2001
|
|
|
2002
|
+
private var unpairInProgress = false
|
|
2003
|
+
|
|
2004
|
+
/// Explicit user Unpair, separate from passive pairing cleanup and logout.
|
|
2005
|
+
func unpair() async throws {
|
|
2006
|
+
guard !unpairInProgress else {
|
|
2007
|
+
throw NSError(domain: "NimoUnpair", code: 1,
|
|
2008
|
+
userInfo: [NSLocalizedDescriptionKey: "Unpair already in progress"])
|
|
2009
|
+
}
|
|
2010
|
+
unpairInProgress = true
|
|
2011
|
+
defer { unpairInProgress = false }
|
|
2012
|
+
if let nimo = sgc as? Nimo {
|
|
2013
|
+
_ = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
|
|
2014
|
+
nimo.resetForUnpair { continuation.resume(returning: $0) }
|
|
2015
|
+
}
|
|
2016
|
+
guard (sgc as? Nimo) === nimo else {
|
|
2017
|
+
throw NSError(domain: "NimoUnpair", code: 2,
|
|
2018
|
+
userInfo: [NSLocalizedDescriptionKey: "Glasses changed during Unpair"])
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
forget()
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1990
2024
|
func forget() {
|
|
1991
2025
|
Bridge.log("MAN: Forgetting smart glasses")
|
|
1992
2026
|
// Call forget first to stop timers/handlers/reconnect logic
|
|
@@ -2053,8 +2087,7 @@ struct ViewState {
|
|
|
2053
2087
|
#endif
|
|
2054
2088
|
// Clean up transcriber resources
|
|
2055
2089
|
#if !os(macOS) && (!SWIFT_PACKAGE || MENTRA_FEATURE_LOCAL_STT)
|
|
2056
|
-
transcriber
|
|
2057
|
-
transcriber = nil
|
|
2090
|
+
transcriber.shutdown()
|
|
2058
2091
|
#endif
|
|
2059
2092
|
|
|
2060
2093
|
// Clean up LC3 converter
|
|
@@ -94,6 +94,7 @@ class DeviceStore {
|
|
|
94
94
|
store.set("bluetooth", "gallery_mode", true)
|
|
95
95
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
96
96
|
store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.loudnessGateEnabled)
|
|
97
|
+
store.set("bluetooth", "auto_power_off_enabled", BluetoothSdkDefaults.autoPowerOffEnabled)
|
|
97
98
|
// Mentra Nex feature flag (off by default; toggled from Nex Developer Settings):
|
|
98
99
|
store.set("bluetooth", "nex_chinese_captions", false)
|
|
99
100
|
store.set("bluetooth", "screen_disabled", false)
|
|
@@ -191,14 +192,25 @@ class DeviceStore {
|
|
|
191
192
|
|
|
192
193
|
// BLUETOOTH:
|
|
193
194
|
|
|
195
|
+
case ("bluetooth", "contextual_dashboard"):
|
|
196
|
+
if value is Bool {
|
|
197
|
+
Task { @MainActor in
|
|
198
|
+
if self.store.get("glasses", "headUp") as? Bool == true {
|
|
199
|
+
DeviceManager.shared.sendCurrentState()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
case ("bluetooth", "brightness"):
|
|
195
205
|
let b = value as? Int ?? 50
|
|
196
206
|
let auto = store.get("bluetooth", "auto_brightness") as? Bool ?? true
|
|
197
207
|
Task {
|
|
198
|
-
DeviceManager.shared.sgc
|
|
199
|
-
|
|
208
|
+
guard let device = DeviceManager.shared.sgc else { return }
|
|
209
|
+
device.setBrightness(b, autoMode: auto)
|
|
210
|
+
guard device.showBrightnessConfirmation else { return }
|
|
211
|
+
await device.sendTextWall("Set brightness to \(b)%")
|
|
200
212
|
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
|
201
|
-
DeviceManager.shared.sgc
|
|
213
|
+
if (DeviceManager.shared.sgc as AnyObject?) === (device as AnyObject) { device.clearDisplay() }
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
case ("bluetooth", "auto_brightness"):
|
|
@@ -206,13 +218,14 @@ class DeviceStore {
|
|
|
206
218
|
let auto = value as? Bool ?? true
|
|
207
219
|
let autoBrightnessChanged = (oldValue as? Bool) != auto
|
|
208
220
|
Task {
|
|
209
|
-
DeviceManager.shared.sgc
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
guard let device = DeviceManager.shared.sgc else { return }
|
|
222
|
+
device.setBrightness(b, autoMode: auto)
|
|
223
|
+
if autoBrightnessChanged, device.showBrightnessConfirmation {
|
|
224
|
+
await device.sendTextWall(
|
|
212
225
|
auto ? "Enabled auto brightness" : "Disabled auto brightness"
|
|
213
226
|
)
|
|
214
227
|
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
|
215
|
-
DeviceManager.shared.sgc
|
|
228
|
+
if (DeviceManager.shared.sgc as AnyObject?) === (device as AnyObject) { device.clearDisplay() }
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
231
|
|
|
@@ -254,6 +267,12 @@ class DeviceStore {
|
|
|
254
267
|
case ("bluetooth", "loudness_gate_enabled"):
|
|
255
268
|
DeviceManager.shared.sgc?.sendLoudnessGateSetting()
|
|
256
269
|
|
|
270
|
+
case ("bluetooth", "auto_power_off_enabled"):
|
|
271
|
+
Bridge.log(
|
|
272
|
+
"DeviceStore: auto_power_off_enabled changed to \(String(describing: value)) — sending cs_swit type 11"
|
|
273
|
+
)
|
|
274
|
+
DeviceManager.shared.sgc?.sendAutoPowerOffSetting()
|
|
275
|
+
|
|
257
276
|
// Deliberately has no seeded default: the key starts absent so that a
|
|
258
277
|
// session where the engine has not authorized tuning can only ever
|
|
259
278
|
// send a reset.
|
|
@@ -2,8 +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: "
|
|
6
|
-
ReleaseChangelog(version: "3.2.0", markdown: "
|
|
5
|
+
ReleaseChangelog(version: "3.2.1", markdown: "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."),
|
|
6
|
+
ReleaseChangelog(version: "3.2.0", markdown: "- Improved glasses connectivity and Wi-Fi setup."),
|
|
7
7
|
ReleaseChangelog(version: "3.1.1", markdown: "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access."),
|
|
8
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."),
|
|
9
9
|
]
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.2.1"
|
|
6
|
-
static let releaseIdentity = "3.2.1
|
|
7
|
-
static let releaseSetId = "mentra-3.2.1
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.2.1"
|
|
7
|
+
static let releaseSetId = "mentra-3.2.1"
|
|
8
|
+
static let sourceCommit = "be56e1456c69f681985642f376ed3d397ec022cc"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.578.json"
|
|
10
|
+
static let otaManifestSha256 = "21af62269e29fd2ee6b9737745a895694bc221672b191d80795057b5a832f400"
|
|
11
11
|
}
|
|
@@ -299,8 +299,22 @@ public final class MentraBluetoothSDK {
|
|
|
299
299
|
private let wifiSessionCapabilities = WifiSessionCapabilities()
|
|
300
300
|
private var configuredOtaVersionUrl: String?
|
|
301
301
|
|
|
302
|
+
/// Whether the app declares the `bluetooth-central` background mode. When false on iOS, the
|
|
303
|
+
/// glasses stay disconnected after a link drop until the user opens the app, so a software
|
|
304
|
+
/// update started on a locked phone stalls. Always true on macOS.
|
|
305
|
+
public static var supportsBackgroundReconnect: Bool {
|
|
306
|
+
BluetoothBackgroundMode.isDeclared(in: Bundle.main.infoDictionary)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private static var didWarnMissingBackgroundMode = false
|
|
310
|
+
|
|
302
311
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
303
312
|
self.configuration = configuration
|
|
313
|
+
// Once per process: apps may create the SDK more than once.
|
|
314
|
+
if !Self.didWarnMissingBackgroundMode, !Self.supportsBackgroundReconnect {
|
|
315
|
+
Self.didWarnMissingBackgroundMode = true
|
|
316
|
+
Bridge.log(BluetoothBackgroundMode.missingWarning)
|
|
317
|
+
}
|
|
304
318
|
analytics = BluetoothSdkAnalytics(configuration: configuration.analytics)
|
|
305
319
|
bluetoothAvailabilityListenerId = BluetoothAvailability.shared.addStateListener { [weak self] state in
|
|
306
320
|
Task { @MainActor [weak self] in
|
|
@@ -688,6 +702,10 @@ public final class MentraBluetoothSDK {
|
|
|
688
702
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "loudness_gate_enabled", enabled)
|
|
689
703
|
}
|
|
690
704
|
|
|
705
|
+
public func setAutoPowerOffEnabled(_ enabled: Bool) async throws {
|
|
706
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_power_off_enabled", enabled)
|
|
707
|
+
}
|
|
708
|
+
|
|
691
709
|
@available(*, deprecated, message: "Sticky action-button photo presets are deprecated. Prefer per-request requestPhoto(...) options (e.g. mode .text for text sensor size/crop, or explicit per-shot fields). This method still works but will be removed in a future release.")
|
|
692
710
|
public func setPhotoCaptureDefaults(_ settings: PhotoCaptureDefaults) async throws -> SettingsAckEvent {
|
|
693
711
|
try await performSettingsCommand(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Tracks expected audio separately from observed audio. All times use a monotonic clock.
|
|
4
|
+
struct GlassesMicWatchdog {
|
|
5
|
+
private var expectedSince: TimeInterval?
|
|
6
|
+
private var lastActivity: TimeInterval?
|
|
7
|
+
private var lastRetry: TimeInterval?
|
|
8
|
+
private let timeout: TimeInterval = 5
|
|
9
|
+
|
|
10
|
+
mutating func expectAudio(_ expected: Bool, at now: TimeInterval) {
|
|
11
|
+
guard expected else {
|
|
12
|
+
self = GlassesMicWatchdog()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
if expectedSince == nil {
|
|
16
|
+
expectedSince = now
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
mutating func receivedAudio(at now: TimeInterval) {
|
|
21
|
+
guard expectedSince != nil else { return }
|
|
22
|
+
lastActivity = now
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
mutating func shouldRetry(at now: TimeInterval) -> Bool {
|
|
26
|
+
guard let expectedSince else { return false }
|
|
27
|
+
let lastProgress = max(expectedSince, lastActivity ?? expectedSince, lastRetry ?? expectedSince)
|
|
28
|
+
guard now - lastProgress >= timeout else { return false }
|
|
29
|
+
// Rate-limit retries even when the device never produces its first packet.
|
|
30
|
+
lastRetry = now
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
enum DashboardContentFormatter {
|
|
4
|
-
static let statusHeader = "$TIME12$ $DATE$ $GBATT$
|
|
4
|
+
static let statusHeader = "$TIME12$ $DATE$ $GBATT$"
|
|
5
5
|
|
|
6
6
|
static func template(for content: String) -> String {
|
|
7
7
|
content.isEmpty ? statusHeader : statusHeader + "\n\n" + content
|
|
@@ -7,7 +7,7 @@ enum VersionInfoAccumulatorOutcome {
|
|
|
7
7
|
case complete(VersionInfoResult)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/// One request, explicit modern completion, or the
|
|
10
|
+
/// One request, explicit modern completion, or the legacy firmware's terminal chunk.
|
|
11
11
|
final class VersionInfoResponseAccumulator {
|
|
12
12
|
static let responseChunkKey = "_responseChunk"
|
|
13
13
|
static let responseRequestIdKey = "_responseRequestId"
|
|
@@ -23,6 +23,7 @@ final class VersionInfoResponseAccumulator {
|
|
|
23
23
|
private var count: Int?
|
|
24
24
|
private var sid: String?
|
|
25
25
|
private var legacyStarted = false
|
|
26
|
+
private var legacyTwoChunkResponse = false
|
|
26
27
|
private var completed = false
|
|
27
28
|
|
|
28
29
|
init(expectedRequestId: String) {
|
|
@@ -63,10 +64,19 @@ final class VersionInfoResponseAccumulator {
|
|
|
63
64
|
case "version_info_1":
|
|
64
65
|
values.removeAll()
|
|
65
66
|
legacyStarted = true
|
|
67
|
+
legacyTwoChunkResponse = event["buildNumber"] as? String == "27"
|
|
66
68
|
merge(event)
|
|
67
69
|
case "version_info_2":
|
|
68
70
|
guard legacyStarted else { return .ignored }
|
|
71
|
+
// Factory ASG27 sends only chunks 1 and 2. Its second chunk carries the
|
|
72
|
+
// OTA URL and legacy firmware version; ASG31+ moves firmware to chunk 3.
|
|
69
73
|
merge(event)
|
|
74
|
+
if legacyTwoChunkResponse, values["buildNumber"] as? String == "27",
|
|
75
|
+
let otaUrl = event["otaVersionUrl"] as? String,
|
|
76
|
+
!otaUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
77
|
+
{
|
|
78
|
+
return finish()
|
|
79
|
+
}
|
|
70
80
|
case "version_info_3":
|
|
71
81
|
guard legacyStarted else { return .ignored }
|
|
72
82
|
merge(event)
|
|
@@ -1850,8 +1850,7 @@ final class Ar99: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SGCM
|
|
|
1850
1850
|
private func ensureOpusDecoder() {
|
|
1851
1851
|
guard opusDecoder == nil else { return }
|
|
1852
1852
|
opusDecoder = Ar99OpusPcmDecoder { pcm in
|
|
1853
|
-
DeviceManager.shared.
|
|
1854
|
-
DeviceManager.shared.handlePcm(pcm)
|
|
1853
|
+
DeviceManager.shared.handleGlassesPcm(pcm)
|
|
1855
1854
|
}
|
|
1856
1855
|
if opusDecoder == nil {
|
|
1857
1856
|
Bridge.log("AR99: system Opus decoder unavailable")
|