@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- 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/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- 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 +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- 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/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- 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 +247 -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/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -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/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- 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/MentraLiveClassicDisconnectTest.kt +133 -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/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -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 +233 -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 +16 -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 +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.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/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -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 +272 -197
- 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 +272 -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 +25 -1
- 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/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -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/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -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 +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -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 +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -677,145 +677,13 @@ enum K900ProtocolUtils {
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
private struct FileTransferSession {
|
|
681
|
-
let fileName: String
|
|
682
|
-
let fileSize: Int // NOTE: May be "fake" (inflated) due to BES firmware workaround
|
|
683
|
-
var actualPackSize: Int = 0 // Actual pack size from first received packet
|
|
684
|
-
var totalPackets: Int
|
|
685
|
-
var expectedNextPacket: Int = 0
|
|
686
|
-
var receivedPackets: [Int: Data] = [:]
|
|
687
|
-
let startTime: Date
|
|
688
|
-
var isComplete: Bool = false
|
|
689
|
-
var isAnnounced: Bool = false
|
|
690
|
-
|
|
691
|
-
/// BES2700 firmware hardcodes FILE_PACK_SIZE=400 when calculating totalPack.
|
|
692
|
-
/// Android glasses "lie" about fileSize to make BES expect correct packet count.
|
|
693
|
-
private static let BES_HARDCODED_PACK_SIZE = 400
|
|
694
|
-
|
|
695
|
-
init(fileName: String, fileSize: Int, announcedPackets: Int? = nil) {
|
|
696
|
-
self.fileName = fileName
|
|
697
|
-
self.fileSize = fileSize
|
|
698
|
-
let computedPackets =
|
|
699
|
-
(fileSize + K900ProtocolUtils.FILE_PACK_SIZE - 1) / K900ProtocolUtils.FILE_PACK_SIZE
|
|
700
|
-
if let announced = announcedPackets, announced > 0 {
|
|
701
|
-
totalPackets = announced
|
|
702
|
-
isAnnounced = true
|
|
703
|
-
} else {
|
|
704
|
-
totalPackets = computedPackets
|
|
705
|
-
isAnnounced = false
|
|
706
|
-
}
|
|
707
|
-
startTime = Date()
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
mutating func updateAnnouncedPackets(_ announced: Int) {
|
|
711
|
-
guard announced > 0 else { return }
|
|
712
|
-
totalPackets = announced
|
|
713
|
-
isAnnounced = true
|
|
714
|
-
if expectedNextPacket >= totalPackets {
|
|
715
|
-
expectedNextPacket = min(expectedNextPacket, max(totalPackets - 1, 0))
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/// Recalculate total packets based on actual pack size from received packet.
|
|
720
|
-
/// Detects BES lie: if fileSize is multiple of 400 but actual pack size differs.
|
|
721
|
-
mutating func recalculateTotalPackets(actualPackSize: Int) {
|
|
722
|
-
guard actualPackSize > 0, actualPackSize <= K900ProtocolUtils.FILE_PACK_SIZE else { return }
|
|
723
|
-
|
|
724
|
-
self.actualPackSize = actualPackSize
|
|
725
|
-
|
|
726
|
-
// Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
|
|
727
|
-
let isBesLie =
|
|
728
|
-
(fileSize % Self.BES_HARDCODED_PACK_SIZE == 0)
|
|
729
|
-
&& (actualPackSize != Self.BES_HARDCODED_PACK_SIZE)
|
|
730
|
-
|
|
731
|
-
let newTotalPackets: Int
|
|
732
|
-
if isBesLie {
|
|
733
|
-
// BES lie detected: totalPackets = fileSize / 400
|
|
734
|
-
newTotalPackets = fileSize / Self.BES_HARDCODED_PACK_SIZE
|
|
735
|
-
Bridge.log(
|
|
736
|
-
"📦 BES Lie detected! fakeFileSize=\(fileSize), totalPackets=\(newTotalPackets), actualPackSize=\(actualPackSize)"
|
|
737
|
-
)
|
|
738
|
-
} else {
|
|
739
|
-
// Normal case: calculate based on actual pack size
|
|
740
|
-
newTotalPackets = (fileSize + actualPackSize - 1) / actualPackSize
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if newTotalPackets != totalPackets {
|
|
744
|
-
Bridge.log(
|
|
745
|
-
"📦 Recalculating totalPackets: \(totalPackets) -> \(newTotalPackets) (packSize=\(actualPackSize), fileSize=\(fileSize))"
|
|
746
|
-
)
|
|
747
|
-
totalPackets = newTotalPackets
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
mutating func addPacket(_ index: Int, data: Data) -> Bool {
|
|
752
|
-
guard index >= 0 else { return false }
|
|
753
|
-
|
|
754
|
-
// On first packet, recalculate total packets only when we do not already
|
|
755
|
-
// have an authoritative pack size from protocol metadata.
|
|
756
|
-
if receivedPackets.isEmpty && actualPackSize == 0 && !data.isEmpty {
|
|
757
|
-
recalculateTotalPackets(actualPackSize: data.count)
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if index >= totalPackets {
|
|
761
|
-
totalPackets = index + 1
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
guard receivedPackets[index] == nil else {
|
|
765
|
-
return false
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
receivedPackets[index] = data
|
|
769
|
-
|
|
770
|
-
while receivedPackets[expectedNextPacket] != nil, expectedNextPacket < totalPackets {
|
|
771
|
-
expectedNextPacket += 1
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
isComplete = (receivedPackets.count == totalPackets)
|
|
775
|
-
return true
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
func isFinalPacket(_ index: Int) -> Bool {
|
|
779
|
-
index == totalPackets - 1
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
func missingPacketIndices() -> [Int] {
|
|
783
|
-
guard totalPackets > receivedPackets.count else { return [] }
|
|
784
|
-
return (0 ..< totalPackets).compactMap { receivedPackets[$0] == nil ? $0 : nil }
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
/// Assemble file from received packets.
|
|
788
|
-
/// NOTE: Calculates actual file size from received data, NOT from header fileSize,
|
|
789
|
-
/// because fileSize may be "fake" (inflated) due to BES firmware workaround.
|
|
790
|
-
func assembleFile() -> Data? {
|
|
791
|
-
guard isComplete else { return nil }
|
|
792
|
-
|
|
793
|
-
// Calculate actual file size by summing all received packet sizes
|
|
794
|
-
let actualFileSize = receivedPackets.values.reduce(0) { $0 + $1.count }
|
|
795
|
-
|
|
796
|
-
Bridge.log(
|
|
797
|
-
"📦 Assembling file: headerFileSize=\(fileSize), actualFileSize=\(actualFileSize), totalPackets=\(totalPackets)"
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
var fileData = Data(capacity: actualFileSize)
|
|
801
|
-
|
|
802
|
-
for i in 0 ..< totalPackets {
|
|
803
|
-
if let packet = receivedPackets[i] {
|
|
804
|
-
fileData.append(packet)
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
return fileData
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
680
|
private struct BlePhotoTransfer {
|
|
813
681
|
var isThumbnail = false
|
|
814
682
|
let bleImgId: String
|
|
815
683
|
let requestId: String
|
|
816
684
|
let webhookUrl: String
|
|
817
685
|
var authToken: String?
|
|
818
|
-
var session:
|
|
686
|
+
var session: MentraLiveFileTransferSession?
|
|
819
687
|
let phoneStartTime: Date
|
|
820
688
|
var bleTransferStartTime: Date?
|
|
821
689
|
var glassesCompressionDurationMs: Int64 = 0
|
|
@@ -838,7 +706,7 @@ private final class BleIncidentLogRelayEntry {
|
|
|
838
706
|
let incidentId: String
|
|
839
707
|
let apiBaseUrl: String
|
|
840
708
|
let kind: BleIncidentLogRelayKind
|
|
841
|
-
var session:
|
|
709
|
+
var session: MentraLiveFileTransferSession?
|
|
842
710
|
|
|
843
711
|
init(
|
|
844
712
|
fileBaseKey: String, incidentId: String, apiBaseUrl: String, kind: BleIncidentLogRelayKind
|
|
@@ -994,6 +862,7 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
994
862
|
Bridge.log("Connected to GATT server, discovering services...")
|
|
995
863
|
|
|
996
864
|
self.stopConnectionTimeout()
|
|
865
|
+
self.clearPendingReconnectTracking()
|
|
997
866
|
self.isConnecting = false
|
|
998
867
|
self.connectingPeripheral = nil
|
|
999
868
|
self.connectedPeripheral = peripheral
|
|
@@ -1048,12 +917,13 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
1048
917
|
return
|
|
1049
918
|
}
|
|
1050
919
|
self.connectingPeripheral = nil
|
|
920
|
+
self.clearPendingReconnectTracking()
|
|
1051
921
|
Bridge.log("LIVE: Disconnected from GATT server")
|
|
1052
922
|
|
|
1053
923
|
self.isConnecting = false
|
|
1054
924
|
|
|
1055
925
|
self.connectedPeripheral = nil
|
|
1056
|
-
self.
|
|
926
|
+
self.clearPhysicalReadiness()
|
|
1057
927
|
self.connected = false
|
|
1058
928
|
self.glassesSessionId = nil // Fresh BLE session starts with no sid known
|
|
1059
929
|
self.streamControlVersion = 0
|
|
@@ -1077,8 +947,9 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
1077
947
|
return
|
|
1078
948
|
}
|
|
1079
949
|
|
|
1080
|
-
//
|
|
1081
|
-
|
|
950
|
+
// Prefer a pending direct reconnect: it is the only path iOS completes while the
|
|
951
|
+
// app is suspended. Fall back to the scan backoff when it does not apply.
|
|
952
|
+
if !self.isKilled, !self.reconnectDirectly(to: peripheral) {
|
|
1082
953
|
self.handleReconnection()
|
|
1083
954
|
}
|
|
1084
955
|
}
|
|
@@ -1098,6 +969,7 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
1098
969
|
Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
|
|
1099
970
|
|
|
1100
971
|
self.stopConnectionTimeout()
|
|
972
|
+
self.clearPendingReconnectTracking()
|
|
1101
973
|
self.isConnecting = false
|
|
1102
974
|
self.closeL2capFileChannel()
|
|
1103
975
|
self.connectedPeripheral = nil
|
|
@@ -1414,6 +1286,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1414
1286
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1415
1287
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1416
1288
|
private static let loudnessGateSwitchType = 10
|
|
1289
|
+
private static let autoPowerOffSwitchType = 11
|
|
1417
1290
|
private func pairingAdvertisement(
|
|
1418
1291
|
_ advertisementData: [String: Any]
|
|
1419
1292
|
) -> MentraLivePairingAdvertisement? {
|
|
@@ -1476,7 +1349,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1476
1349
|
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
1477
1350
|
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
1478
1351
|
// negotiated) is the bug class this clear removes.
|
|
1479
|
-
|
|
1352
|
+
commandQueue.removeAll()
|
|
1480
1353
|
stopSignalStrengthPolling()
|
|
1481
1354
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1482
1355
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -1601,7 +1474,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1601
1474
|
// NEW: File transfer properties
|
|
1602
1475
|
private var fileReadCharacteristic: CBCharacteristic?
|
|
1603
1476
|
private var fileWriteCharacteristic: CBCharacteristic?
|
|
1604
|
-
private var activeFileTransfers = [String:
|
|
1477
|
+
private var activeFileTransfers = [String: MentraLiveFileTransferSession]()
|
|
1605
1478
|
private var blePhotoTransfers = [String: BlePhotoTransfer]()
|
|
1606
1479
|
private var bleIncidentLogRelays = [String: BleIncidentLogRelayEntry]()
|
|
1607
1480
|
private var l2capFileChannel: MentraLiveL2capChannel?
|
|
@@ -1744,6 +1617,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1744
1617
|
set { DeviceStore.shared.apply("glasses", "connected", newValue) }
|
|
1745
1618
|
}
|
|
1746
1619
|
|
|
1620
|
+
private func clearPhysicalReadiness() {
|
|
1621
|
+
let readinessWasPending = connected && !fullyBooted
|
|
1622
|
+
fullyBooted = false
|
|
1623
|
+
if readinessWasPending {
|
|
1624
|
+
// Logical ASG readiness already emitted false without disconnecting.
|
|
1625
|
+
// The real disconnect must still perform the cleanup apply deduplicates.
|
|
1626
|
+
DeviceManager.shared.handleDeviceDisconnected()
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1747
1630
|
// Queue Management
|
|
1748
1631
|
private let commandQueue = CommandQueue()
|
|
1749
1632
|
private let bluetoothQueue = DispatchQueue(label: "MentraLiveBluetooth", qos: .userInitiated)
|
|
@@ -1777,6 +1660,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1777
1660
|
private static let glassesMediaVolumeTimeoutSec: TimeInterval = 2.0
|
|
1778
1661
|
|
|
1779
1662
|
private var connectionTimeoutTimer: Timer?
|
|
1663
|
+
/// Peripheral of an armed pending reconnect (see `reconnectDirectly`).
|
|
1664
|
+
private var pendingReconnectPeripheral: CBPeripheral?
|
|
1665
|
+
private var pendingReconnect = MentraLivePendingReconnect()
|
|
1666
|
+
private var pendingReconnectSettleWorkItem: DispatchWorkItem?
|
|
1780
1667
|
private var reconnectionWorkItem: DispatchWorkItem?
|
|
1781
1668
|
private var requiresAncs = true
|
|
1782
1669
|
|
|
@@ -1844,6 +1731,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1844
1731
|
|
|
1845
1732
|
// clear the saved device name:
|
|
1846
1733
|
UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
|
|
1734
|
+
cancelPendingReconnect(reason: "discovery")
|
|
1847
1735
|
|
|
1848
1736
|
manualDiscoveryActive = true
|
|
1849
1737
|
startScan()
|
|
@@ -1856,6 +1744,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1856
1744
|
}
|
|
1857
1745
|
|
|
1858
1746
|
private func enterPairingYield(windowMs: Int) {
|
|
1747
|
+
cancelPendingReconnect(reason: "pairing_yield")
|
|
1859
1748
|
pairingYieldActive = true
|
|
1860
1749
|
pairingYieldAwaitingReclaim = false
|
|
1861
1750
|
pairingYieldEndWorkItem?.cancel()
|
|
@@ -1865,8 +1754,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1865
1754
|
// Publish disconnect immediately for Phone A UI. didDisconnectPeripheral may lag
|
|
1866
1755
|
// or race with cancelPeripheralConnection; do not wait on it for stand-down.
|
|
1867
1756
|
isConnecting = false
|
|
1757
|
+
clearPhysicalReadiness()
|
|
1868
1758
|
connected = false
|
|
1869
|
-
fullyBooted = false
|
|
1870
1759
|
glassesSessionId = nil
|
|
1871
1760
|
streamControlVersion = 0
|
|
1872
1761
|
readinessCompletedThisBleSession = false
|
|
@@ -1936,6 +1825,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1936
1825
|
return
|
|
1937
1826
|
}
|
|
1938
1827
|
Bridge.log("connectById: \(deviceName)")
|
|
1828
|
+
cancelPendingReconnect(reason: "connect_by_id")
|
|
1939
1829
|
// Save the device name for future reconnection
|
|
1940
1830
|
UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
|
|
1941
1831
|
|
|
@@ -2243,8 +2133,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2243
2133
|
/// Wire-session generation at enqueue time; the drain loop drops entries from a
|
|
2244
2134
|
/// previous session so a stale write (worst case: an old handshake whose reply
|
|
2245
2135
|
/// would activate v2 pre-negotiation) can never cross a session boundary. This is
|
|
2246
|
-
/// the deterministic guard
|
|
2247
|
-
/// cleanup that may land after a reconnect has already resumed the drain loop.
|
|
2136
|
+
/// the deterministic guard if a consumer already dequeued a previous-session write.
|
|
2248
2137
|
let generation: Int
|
|
2249
2138
|
}
|
|
2250
2139
|
|
|
@@ -2286,7 +2175,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2286
2175
|
Bridge.log("LIVE: Cleared pending ACK mId \(pendingMessage.id) because \(reason)")
|
|
2287
2176
|
}
|
|
2288
2177
|
|
|
2289
|
-
|
|
2178
|
+
@MainActor final class CommandQueue {
|
|
2290
2179
|
private var commands: [PendingMessage] = []
|
|
2291
2180
|
|
|
2292
2181
|
func enqueue(_ command: PendingMessage) -> Int {
|
|
@@ -2402,15 +2291,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2402
2291
|
)
|
|
2403
2292
|
|
|
2404
2293
|
// Push to front of queue for immediate retry
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
"queueSizeAfterAdd": queueSize,
|
|
2411
|
-
])
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2294
|
+
let queueSize = commandQueue.pushToFront(retryMessage)
|
|
2295
|
+
logBleWriteTrace("retry_queued", retryMessage.trace, extra: [
|
|
2296
|
+
"retryDelayMs": 0,
|
|
2297
|
+
"queueSizeAfterAdd": queueSize,
|
|
2298
|
+
])
|
|
2414
2299
|
|
|
2415
2300
|
Bridge.log(
|
|
2416
2301
|
"🔄 Retrying message mId: \(pendingMessage.id) (attempt \(retryMessage.retries)/3)"
|
|
@@ -2488,15 +2373,59 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2488
2373
|
}
|
|
2489
2374
|
Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
|
|
2490
2375
|
|
|
2376
|
+
// A timed attempt supersedes any pending reconnect: withdraw one aimed at other
|
|
2377
|
+
// glasses, and drop tracking for this one so its settle timer cannot touch this attempt.
|
|
2378
|
+
if let pending = pendingReconnectPeripheral, pending !== peripheral {
|
|
2379
|
+
cancelPendingReconnect(reason: "superseded")
|
|
2380
|
+
} else {
|
|
2381
|
+
clearPendingReconnectTracking()
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2491
2384
|
isConnecting = true
|
|
2492
2385
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
2493
2386
|
connectingPeripheral = peripheral
|
|
2494
2387
|
connectedPeripheral = peripheral
|
|
2495
|
-
peripheral.delegate = self
|
|
2496
2388
|
|
|
2497
2389
|
// Set connection timeout
|
|
2498
2390
|
startConnectionTimeout()
|
|
2499
2391
|
|
|
2392
|
+
issueConnect(peripheral)
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/// Re-arms a pending connection to the glasses that just dropped. Unlike `connectToDevice`,
|
|
2396
|
+
/// it sets no timeout: CoreBluetooth keeps the request open until the glasses advertise
|
|
2397
|
+
/// again (for example after a BES firmware reset), completes it in the background and wakes
|
|
2398
|
+
/// the app. Discovery, connect-by-id, pairing yield and `destroy()` cancel it through
|
|
2399
|
+
/// `cancelPendingReconnect`. Returns false when the policy does not apply, so the caller can
|
|
2400
|
+
/// fall back to scanning.
|
|
2401
|
+
private func reconnectDirectly(to peripheral: CBPeripheral) -> Bool {
|
|
2402
|
+
guard let centralManager,
|
|
2403
|
+
MentraLiveConnectionAttemptPolicy.shouldReconnectDirectly(
|
|
2404
|
+
isKilled: isKilled,
|
|
2405
|
+
pairingYieldActive: pairingYieldActive,
|
|
2406
|
+
bluetoothPoweredOn: centralManager.state == .poweredOn,
|
|
2407
|
+
peripheralName: peripheral.name,
|
|
2408
|
+
savedDeviceName: UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
|
|
2409
|
+
)
|
|
2410
|
+
else {
|
|
2411
|
+
return false
|
|
2412
|
+
}
|
|
2413
|
+
Bridge.log(
|
|
2414
|
+
"LIVE: Pending direct reconnect to \(peripheral.identifier.uuidString) (no timeout, completes in background)"
|
|
2415
|
+
)
|
|
2416
|
+
isConnecting = true
|
|
2417
|
+
updateConnectionState(ConnTypes.CONNECTING)
|
|
2418
|
+
connectingPeripheral = peripheral
|
|
2419
|
+
connectedPeripheral = peripheral
|
|
2420
|
+
pendingReconnectPeripheral = peripheral
|
|
2421
|
+
issueConnect(peripheral)
|
|
2422
|
+
schedulePendingReconnectSettle(attempt: pendingReconnect.arm())
|
|
2423
|
+
return true
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/// The single CoreBluetooth connect call, shared by user-initiated and pending reconnects.
|
|
2427
|
+
private func issueConnect(_ peripheral: CBPeripheral) {
|
|
2428
|
+
peripheral.delegate = self
|
|
2500
2429
|
#if os(macOS)
|
|
2501
2430
|
centralManager?.connect(peripheral, options: nil)
|
|
2502
2431
|
#else
|
|
@@ -2511,6 +2440,58 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2511
2440
|
#endif
|
|
2512
2441
|
}
|
|
2513
2442
|
|
|
2443
|
+
/// A pending connect never times out, so the glasses may simply never return (battery
|
|
2444
|
+
/// dead, left behind). After the old scan backoff's span, report DISCONNECTED like that
|
|
2445
|
+
/// backoff did, but keep the request armed: iOS still completes it whenever the glasses
|
|
2446
|
+
/// advertise, and `didConnect` accepts it through `connectingPeripheral`.
|
|
2447
|
+
private func schedulePendingReconnectSettle(attempt: Int) {
|
|
2448
|
+
pendingReconnectSettleWorkItem?.cancel()
|
|
2449
|
+
let work = DispatchWorkItem { [weak self] in
|
|
2450
|
+
// Only the attempt that armed this timer may settle state; a failure, fallback
|
|
2451
|
+
// connect or re-arm clears or replaces it.
|
|
2452
|
+
guard let self, self.pendingReconnect.owns(attempt),
|
|
2453
|
+
self.connectionState != ConnTypes.CONNECTED
|
|
2454
|
+
else { return }
|
|
2455
|
+
Bridge.log("LIVE: Glasses still unreachable; reporting disconnected, pending reconnect stays armed")
|
|
2456
|
+
self.pendingReconnectSettleWorkItem = nil
|
|
2457
|
+
self.isConnecting = false
|
|
2458
|
+
self.connectedPeripheral = nil
|
|
2459
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
2460
|
+
}
|
|
2461
|
+
pendingReconnectSettleWorkItem = work
|
|
2462
|
+
DispatchQueue.main.asyncAfter(
|
|
2463
|
+
deadline: .now() + .milliseconds(MentraLiveConnectionAttemptPolicy.pendingReconnectSettleMs),
|
|
2464
|
+
execute: work
|
|
2465
|
+
)
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/// Stops tracking a pending reconnect after it connected; no CoreBluetooth call needed.
|
|
2469
|
+
private func clearPendingReconnectTracking() {
|
|
2470
|
+
pendingReconnectSettleWorkItem?.cancel()
|
|
2471
|
+
pendingReconnectSettleWorkItem = nil
|
|
2472
|
+
pendingReconnect.clear()
|
|
2473
|
+
pendingReconnectPeripheral = nil
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/// Withdraws a pending reconnect before the user retargets (discovery, connect-by-id,
|
|
2477
|
+
/// pairing yield) or tears down, so iOS cannot later complete it to the previous glasses.
|
|
2478
|
+
private func cancelPendingReconnect(reason: String) {
|
|
2479
|
+
guard let peripheral = pendingReconnectPeripheral else {
|
|
2480
|
+
clearPendingReconnectTracking()
|
|
2481
|
+
return
|
|
2482
|
+
}
|
|
2483
|
+
clearPendingReconnectTracking()
|
|
2484
|
+
Bridge.log("LIVE: Cancelling pending reconnect to \(peripheral.identifier.uuidString) (\(reason))")
|
|
2485
|
+
if connectingPeripheral === peripheral {
|
|
2486
|
+
connectingPeripheral = nil
|
|
2487
|
+
}
|
|
2488
|
+
if connectedPeripheral === peripheral, connectionState != ConnTypes.CONNECTED {
|
|
2489
|
+
connectedPeripheral = nil
|
|
2490
|
+
}
|
|
2491
|
+
isConnecting = false
|
|
2492
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2514
2495
|
/// Opt the firmware into ANCS only after iOS has authorized this accessory.
|
|
2515
2496
|
/// Old firmware safely ignores the command, while new firmware stays inert
|
|
2516
2497
|
/// for old mobile clients that never send it.
|
|
@@ -2552,8 +2533,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2552
2533
|
Bridge.log("LIVE: Maximum reconnection attempts reached (\(MAX_RECONNECT_ATTEMPTS))")
|
|
2553
2534
|
reconnectAttempts = 0
|
|
2554
2535
|
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
2536
|
+
clearPhysicalReadiness()
|
|
2555
2537
|
connected = false
|
|
2556
|
-
fullyBooted = false
|
|
2557
2538
|
glassesSessionId = nil
|
|
2558
2539
|
streamControlVersion = 0
|
|
2559
2540
|
readinessCompletedThisBleSession = false
|
|
@@ -2638,7 +2619,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2638
2619
|
// The reader thread keeps draining the stream (and returning CoC credits)
|
|
2639
2620
|
// while the existing transfer state remains serialized on the main actor.
|
|
2640
2621
|
DispatchQueue.main.async {
|
|
2641
|
-
self
|
|
2622
|
+
guard let self, self.l2capFileChannelId == channelId else { return }
|
|
2623
|
+
self.processReceivedData(frame)
|
|
2642
2624
|
}
|
|
2643
2625
|
},
|
|
2644
2626
|
onClose: { [weak self] in
|
|
@@ -2665,7 +2647,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2665
2647
|
|
|
2666
2648
|
// MARK: - Data Processing
|
|
2667
2649
|
|
|
2668
|
-
|
|
2650
|
+
/// Internal so native integration tests can replay the actual BLE receive path.
|
|
2669
2651
|
func processReceivedData(_ data: Data) {
|
|
2670
2652
|
guard data.count > 0 else { return }
|
|
2671
2653
|
|
|
@@ -3015,7 +2997,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3015
2997
|
|
|
3016
2998
|
case "stream_controller_probe":
|
|
3017
2999
|
if let response = StreamControllerProbe.response(json) {
|
|
3018
|
-
|
|
3000
|
+
// BES buffers non-waking commands when MTK enters standby, even while
|
|
3001
|
+
// its streaming CPU lease is held. The current probe needs a live reply.
|
|
3002
|
+
sendJson(response, wakeUp: true)
|
|
3019
3003
|
}
|
|
3020
3004
|
|
|
3021
3005
|
case "pong":
|
|
@@ -3164,6 +3148,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3164
3148
|
if type.hasPrefix("version_info") {
|
|
3165
3149
|
Bridge.log("LIVE: Received \(type)")
|
|
3166
3150
|
|
|
3151
|
+
// A new process can report its build before glasses_ready resets the
|
|
3152
|
+
// wire epoch. Publish its pending readiness before those version fields
|
|
3153
|
+
// can complete OTA and trigger the next request.
|
|
3154
|
+
handleGlassesSessionId(json)
|
|
3155
|
+
|
|
3167
3156
|
// Extract all fields from JSON (except "type")
|
|
3168
3157
|
var fields: [String: Any] = [:]
|
|
3169
3158
|
fields["version_info_type"] = type
|
|
@@ -3236,7 +3225,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3236
3225
|
// running them per chunk is safe.
|
|
3237
3226
|
parsePeerWireCaps(json)
|
|
3238
3227
|
maybeSendWireHandshake()
|
|
3239
|
-
handleGlassesSessionId(json)
|
|
3240
3228
|
|
|
3241
3229
|
Bridge.sendVersionInfo(fields, responseChunk: type)
|
|
3242
3230
|
} else {
|
|
@@ -3440,10 +3428,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3440
3428
|
if let body = k900ParseBody(json["B"]) {
|
|
3441
3429
|
let switchType = k900JsonInt(body, "type") ?? -1
|
|
3442
3430
|
let switchValue = k900JsonInt(body, "switch") ?? -1
|
|
3431
|
+
// K900 replies carry result in "S"; 0 is RC_SUCCESS.
|
|
3432
|
+
let resultCode = k900JsonInt(json, "S") ?? -1
|
|
3443
3433
|
handleSwitchStatus(
|
|
3444
3434
|
switchType: switchType,
|
|
3445
3435
|
value: switchValue,
|
|
3446
|
-
timestamp: Int64(Date().timeIntervalSince1970 * 1000)
|
|
3436
|
+
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
|
|
3437
|
+
resultCode: resultCode
|
|
3447
3438
|
)
|
|
3448
3439
|
}
|
|
3449
3440
|
|
|
@@ -3459,7 +3450,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3459
3450
|
|
|
3460
3451
|
case "sr_wrst":
|
|
3461
3452
|
if let body = k900ParseBody(json["B"]) {
|
|
3462
|
-
Bridge.sendWearState(
|
|
3453
|
+
Bridge.sendWearState(
|
|
3454
|
+
worn: (k900JsonInt(body, "on") ?? 0) != 0,
|
|
3455
|
+
elapsedMs: k900JsonInt(body, "elapsed_ms"),
|
|
3456
|
+
timeoutMs: k900JsonInt(body, "timeout_ms"),
|
|
3457
|
+
enabled: body["enabled"] == nil ? nil : (k900JsonInt(body, "enabled") ?? 0) != 0,
|
|
3458
|
+
armed: body["armed"] == nil ? nil : (k900JsonInt(body, "armed") ?? 0) != 0,
|
|
3459
|
+
inhibited: body["inhibited"] == nil ? nil : (k900JsonInt(body, "inhibited") ?? 0) != 0
|
|
3460
|
+
)
|
|
3463
3461
|
}
|
|
3464
3462
|
|
|
3465
3463
|
case "sr_micrms":
|
|
@@ -3725,7 +3723,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3725
3723
|
sendJson(json, wakeUp: true)
|
|
3726
3724
|
}
|
|
3727
3725
|
|
|
3728
|
-
|
|
3726
|
+
var incidentLogTransportReady: Bool {
|
|
3727
|
+
connectedPeripheral?.state == .connected && txCharacteristic != nil
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String?) throws {
|
|
3731
|
+
guard incidentLogTransportReady else {
|
|
3732
|
+
throw BluetoothSdkError(code: "glasses_not_connected", message: "Cannot request incident logs because the glasses BLE link is not ready.")
|
|
3733
|
+
}
|
|
3734
|
+
// Token sync may happen after glasses_ready; never rely on the last pairing's token.
|
|
3735
|
+
let coreToken = DeviceStore.shared.get("bluetooth", "core_token") as? String ?? ""
|
|
3736
|
+
guard !coreToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
3737
|
+
throw BluetoothSdkError(code: "core_token_unavailable", message: "Cannot request incident logs without the current Core access token.")
|
|
3738
|
+
}
|
|
3729
3739
|
var base = (apiBaseUrl ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3730
3740
|
if base.isEmpty {
|
|
3731
3741
|
base = "https://api.mentra.glass"
|
|
@@ -3742,10 +3752,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3742
3752
|
fileBaseKey: lKey, incidentId: incidentId, apiBaseUrl: base, kind: .logcat
|
|
3743
3753
|
)
|
|
3744
3754
|
|
|
3745
|
-
|
|
3746
|
-
"
|
|
3747
|
-
|
|
3748
|
-
|
|
3755
|
+
let auth: [String: Any] = [
|
|
3756
|
+
"type": "auth_token", "coreToken": coreToken,
|
|
3757
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
3758
|
+
]
|
|
3759
|
+
// MainActor queue insertion preserves auth-before-upload order, including all fragments.
|
|
3760
|
+
guard sendJson(auth, wakeUp: true, requireAck: true, bridgeLogging: true),
|
|
3761
|
+
sendJson(["type": "upload_incident_logs", "incidentId": incidentId, "apiBaseUrl": base],
|
|
3762
|
+
wakeUp: true, requireAck: true, bridgeLogging: true)
|
|
3763
|
+
else {
|
|
3764
|
+
bleIncidentLogRelays.removeValue(forKey: bKey)
|
|
3765
|
+
bleIncidentLogRelays.removeValue(forKey: lKey)
|
|
3766
|
+
throw BluetoothSdkError(code: "incident_dispatch_failed", message: "Could not queue the glasses incident-log request.")
|
|
3767
|
+
}
|
|
3768
|
+
Bridge.log("LIVE: Queued incidentId to glasses for log upload: \(incidentId) (BLE relay \(bKey), \(lKey))")
|
|
3749
3769
|
}
|
|
3750
3770
|
|
|
3751
3771
|
private static func incidentBleFileBase(incidentId: String, prefix: Character) -> String {
|
|
@@ -4148,7 +4168,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4148
4168
|
activeFileTransfers.removeValue(forKey: fileName)
|
|
4149
4169
|
}
|
|
4150
4170
|
|
|
4151
|
-
var session =
|
|
4171
|
+
var session = MentraLiveFileTransferSession(
|
|
4152
4172
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4153
4173
|
)
|
|
4154
4174
|
session.isAnnounced = true
|
|
@@ -4158,7 +4178,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4158
4178
|
if var bleTransfer = blePhotoTransfers[bleImgId] {
|
|
4159
4179
|
var bleSession =
|
|
4160
4180
|
bleTransfer.session
|
|
4161
|
-
??
|
|
4181
|
+
?? MentraLiveFileTransferSession(
|
|
4162
4182
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4163
4183
|
)
|
|
4164
4184
|
bleSession.updateAnnouncedPackets(totalPackets)
|
|
@@ -4251,8 +4271,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4251
4271
|
|
|
4252
4272
|
if incidentRelay.session == nil {
|
|
4253
4273
|
activeFileTransfers.removeValue(forKey: packetInfo.fileName)
|
|
4254
|
-
var session =
|
|
4255
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4274
|
+
var session = MentraLiveFileTransferSession(
|
|
4275
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4256
4276
|
)
|
|
4257
4277
|
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4258
4278
|
incidentRelay.session = session
|
|
@@ -4303,10 +4323,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4303
4323
|
|
|
4304
4324
|
// Get or create session for this transfer
|
|
4305
4325
|
if photoTransfer.session == nil {
|
|
4306
|
-
var session =
|
|
4326
|
+
var session = MentraLiveFileTransferSession(
|
|
4307
4327
|
fileName: packetInfo.fileName,
|
|
4308
|
-
fileSize: Int(packetInfo.fileSize)
|
|
4328
|
+
fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4309
4329
|
)
|
|
4330
|
+
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4310
4331
|
photoTransfer.session = session
|
|
4311
4332
|
blePhotoTransfers[bleImgId] = photoTransfer
|
|
4312
4333
|
Bridge.log(
|
|
@@ -4379,9 +4400,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4379
4400
|
var session = activeFileTransfers[packetInfo.fileName]
|
|
4380
4401
|
if session == nil {
|
|
4381
4402
|
// New file transfer
|
|
4382
|
-
session =
|
|
4383
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4403
|
+
session = MentraLiveFileTransferSession(
|
|
4404
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4384
4405
|
)
|
|
4406
|
+
session?.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4385
4407
|
activeFileTransfers[packetInfo.fileName] = session
|
|
4386
4408
|
|
|
4387
4409
|
Bridge.log(
|
|
@@ -4680,20 +4702,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4680
4702
|
|
|
4681
4703
|
func queueSend(_ data: Data, id: String, trace: BleWriteTrace?) {
|
|
4682
4704
|
let significantQueueSize = SIGNIFICANT_BLE_TRACE_QUEUE_SIZE
|
|
4683
|
-
//
|
|
4684
|
-
// session reset, and a pre-reset payload stamped with the new generation would
|
|
4685
|
-
// defeat the stale-write guard in the drain loop.
|
|
4705
|
+
// Insert synchronously on MainActor so fragments and dependent commands retain call order.
|
|
4686
4706
|
let generation = wireSessionGeneration
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
await MainActor.run {
|
|
4693
|
-
self.logBleChunkTrace("queued", trace, extra: [
|
|
4694
|
-
"queueSizeAfterAdd": queueSize,
|
|
4695
|
-
])
|
|
4696
|
-
}
|
|
4707
|
+
let queueSize = commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace, generation: generation))
|
|
4708
|
+
if trace != nil, queueSize >= significantQueueSize {
|
|
4709
|
+
logBleChunkTrace("queued", trace, extra: [
|
|
4710
|
+
"queueSizeAfterAdd": queueSize,
|
|
4711
|
+
])
|
|
4697
4712
|
}
|
|
4698
4713
|
}
|
|
4699
4714
|
|
|
@@ -5210,7 +5225,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5210
5225
|
bridgeLogging: bridgeLogging
|
|
5211
5226
|
)
|
|
5212
5227
|
}
|
|
5213
|
-
|
|
5228
|
+
// Credentials reach the wire unchanged; diagnostic logs omit their values.
|
|
5229
|
+
let loggedPayload = json["password"] == nil && json["coreToken"] == nil
|
|
5230
|
+
? jsonString : "<\(commandInfo.commandType) with credentials omitted>"
|
|
5231
|
+
transportLog("LIVE: Sending data to glasses: \(loggedPayload)", bridgeLogging: bridgeLogging)
|
|
5214
5232
|
let packedData =
|
|
5215
5233
|
packJson(
|
|
5216
5234
|
jsonString,
|
|
@@ -5273,7 +5291,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5273
5291
|
if let requestId {
|
|
5274
5292
|
json["request_id"] = requestId
|
|
5275
5293
|
}
|
|
5276
|
-
|
|
5294
|
+
// Wake ASG so the version request and its response can finish after idle.
|
|
5295
|
+
sendJson(json, wakeUp: true)
|
|
5277
5296
|
}
|
|
5278
5297
|
|
|
5279
5298
|
private func sendCoreTokenToAsgClient() {
|
|
@@ -5523,8 +5542,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5523
5542
|
Bridge.sendSpeakingStatus(speaking)
|
|
5524
5543
|
}
|
|
5525
5544
|
|
|
5526
|
-
private func handleSwitchStatus(
|
|
5545
|
+
private func handleSwitchStatus(
|
|
5546
|
+
switchType: Int, value: Int, timestamp: Int64, resultCode: Int = -1
|
|
5547
|
+
) {
|
|
5527
5548
|
Bridge.sendSwitchStatus(switchType: switchType, value: value, timestamp: timestamp)
|
|
5549
|
+
if switchType == Self.autoPowerOffSwitchType {
|
|
5550
|
+
let ok = resultCode == 0
|
|
5551
|
+
Bridge.log(
|
|
5552
|
+
"LIVE: 🔋 auto power-off sr_swit reply type=\(switchType) switch=\(value)"
|
|
5553
|
+
+ " result=\(resultCode) ok=\(ok)"
|
|
5554
|
+
)
|
|
5555
|
+
}
|
|
5528
5556
|
if switchType == Self.voiceActivityDetectionSwitchType, value == 0 || value == 1 {
|
|
5529
5557
|
handleVoiceActivityDetectionStatus(enabled: value == 1)
|
|
5530
5558
|
}
|
|
@@ -5723,7 +5751,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5723
5751
|
stopReadinessCheckLoop()
|
|
5724
5752
|
|
|
5725
5753
|
readinessCheckCounter = 0
|
|
5726
|
-
|
|
5754
|
+
clearPhysicalReadiness()
|
|
5727
5755
|
connected = false
|
|
5728
5756
|
glassesSessionId = nil
|
|
5729
5757
|
streamControlVersion = 0
|
|
@@ -5995,16 +6023,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5995
6023
|
// Stop all timers
|
|
5996
6024
|
stopAllTimers()
|
|
5997
6025
|
resetWireNegotiationState()
|
|
5998
|
-
|
|
6026
|
+
commandQueue.removeAll() // stale writes die with the session
|
|
5999
6027
|
closeL2capFileChannel()
|
|
6000
6028
|
|
|
6001
6029
|
// Disconnect BLE
|
|
6030
|
+
cancelPendingReconnect(reason: "destroy")
|
|
6002
6031
|
if let peripheral = connectedPeripheral {
|
|
6003
6032
|
centralManager?.cancelPeripheralConnection(peripheral)
|
|
6004
6033
|
}
|
|
6005
6034
|
|
|
6035
|
+
clearPhysicalReadiness()
|
|
6006
6036
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
6007
|
-
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
6008
6037
|
DeviceStore.shared.apply("glasses", "wifiConnected", false)
|
|
6009
6038
|
DeviceStore.shared.apply("glasses", "wifiSsid", "")
|
|
6010
6039
|
DeviceStore.shared.apply("glasses", "wifiLocalIp", "")
|
|
@@ -6141,8 +6170,8 @@ extension MentraLive {
|
|
|
6141
6170
|
/// pre-sid build) or differing from the recorded one = restart; same sid = no action.
|
|
6142
6171
|
/// A restart is a LOGICAL session reset: send phone_ready immediately (bypassing the
|
|
6143
6172
|
/// sr_hrt heartbeat's stale-readiness suppression); the returning glasses_ready runs
|
|
6144
|
-
/// the full existing remote-wire-reset flow.
|
|
6145
|
-
///
|
|
6173
|
+
/// the full existing remote-wire-reset flow. Keep the physical connection intact,
|
|
6174
|
+
/// but mark readiness pending until that new wire epoch has been established.
|
|
6146
6175
|
private func handleGlassesSessionId(_ json: [String: Any]) {
|
|
6147
6176
|
guard let sid = json["sid"] as? String, !sid.isEmpty else { return }
|
|
6148
6177
|
let previous = glassesSessionId
|
|
@@ -6157,6 +6186,9 @@ extension MentraLive {
|
|
|
6157
6186
|
"LIVE: 🔁 Glasses session changed (\(previous ?? "<pre-sid build>") -> \(sid)) - " +
|
|
6158
6187
|
"asg restarted under a live link, re-running readiness"
|
|
6159
6188
|
)
|
|
6189
|
+
// Emit the existing connected-but-not-ready status without invoking the
|
|
6190
|
+
// physical-disconnect side effects of DeviceStore.apply(fullyBooted: false).
|
|
6191
|
+
DeviceStore.shared.set("glasses", "fullyBooted", false)
|
|
6160
6192
|
sendPhoneReady(reason: "glasses session changed")
|
|
6161
6193
|
Bridge.sendTypedMessage(
|
|
6162
6194
|
"glasses_session_changed",
|
|
@@ -6874,6 +6906,9 @@ extension MentraLive {
|
|
|
6874
6906
|
// Send glasses-side loudness / Barrier gate setting.
|
|
6875
6907
|
sendLoudnessGateSetting()
|
|
6876
6908
|
|
|
6909
|
+
// Send glasses-side auto power-off setting.
|
|
6910
|
+
sendAutoPowerOffSetting()
|
|
6911
|
+
|
|
6877
6912
|
// Send mic tuning. With nothing authorized this sends a reset, which is
|
|
6878
6913
|
// what returns a freshly connected pair of glasses to stock behaviour.
|
|
6879
6914
|
sendMicTuningSetting()
|
|
@@ -7100,6 +7135,46 @@ extension MentraLive {
|
|
|
7100
7135
|
}
|
|
7101
7136
|
}
|
|
7102
7137
|
|
|
7138
|
+
func sendAutoPowerOffSetting() {
|
|
7139
|
+
let enabled = DeviceStore.shared.get("bluetooth", "auto_power_off_enabled") as? Bool
|
|
7140
|
+
?? BluetoothSdkDefaults.autoPowerOffEnabled
|
|
7141
|
+
Bridge.log(
|
|
7142
|
+
"LIVE: 🔋 Sending auto power-off setting to glasses: enabled=\(enabled)"
|
|
7143
|
+
+ " (cs_swit type=\(Self.autoPowerOffSwitchType))"
|
|
7144
|
+
)
|
|
7145
|
+
|
|
7146
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
7147
|
+
Bridge.log("LIVE: Cannot send auto power-off setting - BLE write path not ready")
|
|
7148
|
+
return
|
|
7149
|
+
}
|
|
7150
|
+
|
|
7151
|
+
do {
|
|
7152
|
+
let bodyData = try JSONSerialization.data(withJSONObject: [
|
|
7153
|
+
"type": Self.autoPowerOffSwitchType,
|
|
7154
|
+
"switch": enabled ? 1 : 0,
|
|
7155
|
+
])
|
|
7156
|
+
guard let bodyString = String(data: bodyData, encoding: .utf8) else {
|
|
7157
|
+
Bridge.log("LIVE: Failed to encode auto power-off payload")
|
|
7158
|
+
return
|
|
7159
|
+
}
|
|
7160
|
+
let command: [String: Any] = [
|
|
7161
|
+
"C": "cs_swit",
|
|
7162
|
+
"V": 1,
|
|
7163
|
+
"B": bodyString,
|
|
7164
|
+
]
|
|
7165
|
+
if !sendRawK900Command(command, wakeUp: true) {
|
|
7166
|
+
Bridge.log("LIVE: Failed to send auto power-off setting command")
|
|
7167
|
+
} else {
|
|
7168
|
+
Bridge.log(
|
|
7169
|
+
"LIVE: 🔋 Queued auto power-off cs_swit type=\(Self.autoPowerOffSwitchType)"
|
|
7170
|
+
+ " switch=\(enabled ? 1 : 0)"
|
|
7171
|
+
)
|
|
7172
|
+
}
|
|
7173
|
+
} catch {
|
|
7174
|
+
Bridge.log("LIVE: Error encoding auto power-off payload: \(error)")
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
|
|
7103
7178
|
func sendButtonVideoRecordingSettings() {
|
|
7104
7179
|
let settings =
|
|
7105
7180
|
DeviceStore.shared.get("bluetooth", "button_video_settings") as? [String: Any] ?? [
|
|
@@ -7418,7 +7493,7 @@ extension MentraLive {
|
|
|
7418
7493
|
if fps > 0 { settings["fps"] = fps }
|
|
7419
7494
|
json["settings"] = settings
|
|
7420
7495
|
}
|
|
7421
|
-
sendJson(json)
|
|
7496
|
+
sendJson(json, wakeUp: true)
|
|
7422
7497
|
}
|
|
7423
7498
|
|
|
7424
7499
|
func stopVideoRecording(requestId: String) {
|
|
@@ -7433,7 +7508,7 @@ extension MentraLive {
|
|
|
7433
7508
|
sendJson([
|
|
7434
7509
|
"type": "get_video_recording_status",
|
|
7435
7510
|
"requestId": requestId,
|
|
7436
|
-
])
|
|
7511
|
+
], wakeUp: true)
|
|
7437
7512
|
}
|
|
7438
7513
|
|
|
7439
7514
|
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
@@ -7458,7 +7533,7 @@ extension MentraLive {
|
|
|
7458
7533
|
if let authToken, !authToken.isEmpty {
|
|
7459
7534
|
json["authToken"] = authToken
|
|
7460
7535
|
}
|
|
7461
|
-
sendJson(json)
|
|
7536
|
+
sendJson(json, wakeUp: true)
|
|
7462
7537
|
}
|
|
7463
7538
|
}
|
|
7464
7539
|
|