@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
|
@@ -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,6 +917,7 @@ 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
|
|
@@ -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? {
|
|
@@ -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?
|
|
@@ -1777,6 +1650,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1777
1650
|
private static let glassesMediaVolumeTimeoutSec: TimeInterval = 2.0
|
|
1778
1651
|
|
|
1779
1652
|
private var connectionTimeoutTimer: Timer?
|
|
1653
|
+
/// Peripheral of an armed pending reconnect (see `reconnectDirectly`).
|
|
1654
|
+
private var pendingReconnectPeripheral: CBPeripheral?
|
|
1655
|
+
private var pendingReconnect = MentraLivePendingReconnect()
|
|
1656
|
+
private var pendingReconnectSettleWorkItem: DispatchWorkItem?
|
|
1780
1657
|
private var reconnectionWorkItem: DispatchWorkItem?
|
|
1781
1658
|
private var requiresAncs = true
|
|
1782
1659
|
|
|
@@ -1844,6 +1721,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1844
1721
|
|
|
1845
1722
|
// clear the saved device name:
|
|
1846
1723
|
UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
|
|
1724
|
+
cancelPendingReconnect(reason: "discovery")
|
|
1847
1725
|
|
|
1848
1726
|
manualDiscoveryActive = true
|
|
1849
1727
|
startScan()
|
|
@@ -1856,6 +1734,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1856
1734
|
}
|
|
1857
1735
|
|
|
1858
1736
|
private func enterPairingYield(windowMs: Int) {
|
|
1737
|
+
cancelPendingReconnect(reason: "pairing_yield")
|
|
1859
1738
|
pairingYieldActive = true
|
|
1860
1739
|
pairingYieldAwaitingReclaim = false
|
|
1861
1740
|
pairingYieldEndWorkItem?.cancel()
|
|
@@ -1936,6 +1815,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1936
1815
|
return
|
|
1937
1816
|
}
|
|
1938
1817
|
Bridge.log("connectById: \(deviceName)")
|
|
1818
|
+
cancelPendingReconnect(reason: "connect_by_id")
|
|
1939
1819
|
// Save the device name for future reconnection
|
|
1940
1820
|
UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
|
|
1941
1821
|
|
|
@@ -2488,15 +2368,59 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2488
2368
|
}
|
|
2489
2369
|
Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
|
|
2490
2370
|
|
|
2371
|
+
// A timed attempt supersedes any pending reconnect: withdraw one aimed at other
|
|
2372
|
+
// glasses, and drop tracking for this one so its settle timer cannot touch this attempt.
|
|
2373
|
+
if let pending = pendingReconnectPeripheral, pending !== peripheral {
|
|
2374
|
+
cancelPendingReconnect(reason: "superseded")
|
|
2375
|
+
} else {
|
|
2376
|
+
clearPendingReconnectTracking()
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2491
2379
|
isConnecting = true
|
|
2492
2380
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
2493
2381
|
connectingPeripheral = peripheral
|
|
2494
2382
|
connectedPeripheral = peripheral
|
|
2495
|
-
peripheral.delegate = self
|
|
2496
2383
|
|
|
2497
2384
|
// Set connection timeout
|
|
2498
2385
|
startConnectionTimeout()
|
|
2499
2386
|
|
|
2387
|
+
issueConnect(peripheral)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
/// Re-arms a pending connection to the glasses that just dropped. Unlike `connectToDevice`,
|
|
2391
|
+
/// it sets no timeout: CoreBluetooth keeps the request open until the glasses advertise
|
|
2392
|
+
/// again (for example after a BES firmware reset), completes it in the background and wakes
|
|
2393
|
+
/// the app. Discovery, connect-by-id, pairing yield and `destroy()` cancel it through
|
|
2394
|
+
/// `cancelPendingReconnect`. Returns false when the policy does not apply, so the caller can
|
|
2395
|
+
/// fall back to scanning.
|
|
2396
|
+
private func reconnectDirectly(to peripheral: CBPeripheral) -> Bool {
|
|
2397
|
+
guard let centralManager,
|
|
2398
|
+
MentraLiveConnectionAttemptPolicy.shouldReconnectDirectly(
|
|
2399
|
+
isKilled: isKilled,
|
|
2400
|
+
pairingYieldActive: pairingYieldActive,
|
|
2401
|
+
bluetoothPoweredOn: centralManager.state == .poweredOn,
|
|
2402
|
+
peripheralName: peripheral.name,
|
|
2403
|
+
savedDeviceName: UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
|
|
2404
|
+
)
|
|
2405
|
+
else {
|
|
2406
|
+
return false
|
|
2407
|
+
}
|
|
2408
|
+
Bridge.log(
|
|
2409
|
+
"LIVE: Pending direct reconnect to \(peripheral.identifier.uuidString) (no timeout, completes in background)"
|
|
2410
|
+
)
|
|
2411
|
+
isConnecting = true
|
|
2412
|
+
updateConnectionState(ConnTypes.CONNECTING)
|
|
2413
|
+
connectingPeripheral = peripheral
|
|
2414
|
+
connectedPeripheral = peripheral
|
|
2415
|
+
pendingReconnectPeripheral = peripheral
|
|
2416
|
+
issueConnect(peripheral)
|
|
2417
|
+
schedulePendingReconnectSettle(attempt: pendingReconnect.arm())
|
|
2418
|
+
return true
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
/// The single CoreBluetooth connect call, shared by user-initiated and pending reconnects.
|
|
2422
|
+
private func issueConnect(_ peripheral: CBPeripheral) {
|
|
2423
|
+
peripheral.delegate = self
|
|
2500
2424
|
#if os(macOS)
|
|
2501
2425
|
centralManager?.connect(peripheral, options: nil)
|
|
2502
2426
|
#else
|
|
@@ -2511,6 +2435,58 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2511
2435
|
#endif
|
|
2512
2436
|
}
|
|
2513
2437
|
|
|
2438
|
+
/// A pending connect never times out, so the glasses may simply never return (battery
|
|
2439
|
+
/// dead, left behind). After the old scan backoff's span, report DISCONNECTED like that
|
|
2440
|
+
/// backoff did, but keep the request armed: iOS still completes it whenever the glasses
|
|
2441
|
+
/// advertise, and `didConnect` accepts it through `connectingPeripheral`.
|
|
2442
|
+
private func schedulePendingReconnectSettle(attempt: Int) {
|
|
2443
|
+
pendingReconnectSettleWorkItem?.cancel()
|
|
2444
|
+
let work = DispatchWorkItem { [weak self] in
|
|
2445
|
+
// Only the attempt that armed this timer may settle state; a failure, fallback
|
|
2446
|
+
// connect or re-arm clears or replaces it.
|
|
2447
|
+
guard let self, self.pendingReconnect.owns(attempt),
|
|
2448
|
+
self.connectionState != ConnTypes.CONNECTED
|
|
2449
|
+
else { return }
|
|
2450
|
+
Bridge.log("LIVE: Glasses still unreachable; reporting disconnected, pending reconnect stays armed")
|
|
2451
|
+
self.pendingReconnectSettleWorkItem = nil
|
|
2452
|
+
self.isConnecting = false
|
|
2453
|
+
self.connectedPeripheral = nil
|
|
2454
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
2455
|
+
}
|
|
2456
|
+
pendingReconnectSettleWorkItem = work
|
|
2457
|
+
DispatchQueue.main.asyncAfter(
|
|
2458
|
+
deadline: .now() + .milliseconds(MentraLiveConnectionAttemptPolicy.pendingReconnectSettleMs),
|
|
2459
|
+
execute: work
|
|
2460
|
+
)
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/// Stops tracking a pending reconnect after it connected; no CoreBluetooth call needed.
|
|
2464
|
+
private func clearPendingReconnectTracking() {
|
|
2465
|
+
pendingReconnectSettleWorkItem?.cancel()
|
|
2466
|
+
pendingReconnectSettleWorkItem = nil
|
|
2467
|
+
pendingReconnect.clear()
|
|
2468
|
+
pendingReconnectPeripheral = nil
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/// Withdraws a pending reconnect before the user retargets (discovery, connect-by-id,
|
|
2472
|
+
/// pairing yield) or tears down, so iOS cannot later complete it to the previous glasses.
|
|
2473
|
+
private func cancelPendingReconnect(reason: String) {
|
|
2474
|
+
guard let peripheral = pendingReconnectPeripheral else {
|
|
2475
|
+
clearPendingReconnectTracking()
|
|
2476
|
+
return
|
|
2477
|
+
}
|
|
2478
|
+
clearPendingReconnectTracking()
|
|
2479
|
+
Bridge.log("LIVE: Cancelling pending reconnect to \(peripheral.identifier.uuidString) (\(reason))")
|
|
2480
|
+
if connectingPeripheral === peripheral {
|
|
2481
|
+
connectingPeripheral = nil
|
|
2482
|
+
}
|
|
2483
|
+
if connectedPeripheral === peripheral, connectionState != ConnTypes.CONNECTED {
|
|
2484
|
+
connectedPeripheral = nil
|
|
2485
|
+
}
|
|
2486
|
+
isConnecting = false
|
|
2487
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2514
2490
|
/// Opt the firmware into ANCS only after iOS has authorized this accessory.
|
|
2515
2491
|
/// Old firmware safely ignores the command, while new firmware stays inert
|
|
2516
2492
|
/// for old mobile clients that never send it.
|
|
@@ -2638,7 +2614,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2638
2614
|
// The reader thread keeps draining the stream (and returning CoC credits)
|
|
2639
2615
|
// while the existing transfer state remains serialized on the main actor.
|
|
2640
2616
|
DispatchQueue.main.async {
|
|
2641
|
-
self
|
|
2617
|
+
guard let self, self.l2capFileChannelId == channelId else { return }
|
|
2618
|
+
self.processReceivedData(frame)
|
|
2642
2619
|
}
|
|
2643
2620
|
},
|
|
2644
2621
|
onClose: { [weak self] in
|
|
@@ -2665,7 +2642,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2665
2642
|
|
|
2666
2643
|
// MARK: - Data Processing
|
|
2667
2644
|
|
|
2668
|
-
|
|
2645
|
+
/// Internal so native integration tests can replay the actual BLE receive path.
|
|
2669
2646
|
func processReceivedData(_ data: Data) {
|
|
2670
2647
|
guard data.count > 0 else { return }
|
|
2671
2648
|
|
|
@@ -3015,7 +2992,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3015
2992
|
|
|
3016
2993
|
case "stream_controller_probe":
|
|
3017
2994
|
if let response = StreamControllerProbe.response(json) {
|
|
3018
|
-
|
|
2995
|
+
// BES buffers non-waking commands when MTK enters standby, even while
|
|
2996
|
+
// its streaming CPU lease is held. The current probe needs a live reply.
|
|
2997
|
+
sendJson(response, wakeUp: true)
|
|
3019
2998
|
}
|
|
3020
2999
|
|
|
3021
3000
|
case "pong":
|
|
@@ -3440,10 +3419,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3440
3419
|
if let body = k900ParseBody(json["B"]) {
|
|
3441
3420
|
let switchType = k900JsonInt(body, "type") ?? -1
|
|
3442
3421
|
let switchValue = k900JsonInt(body, "switch") ?? -1
|
|
3422
|
+
// K900 replies carry result in "S"; 0 is RC_SUCCESS.
|
|
3423
|
+
let resultCode = k900JsonInt(json, "S") ?? -1
|
|
3443
3424
|
handleSwitchStatus(
|
|
3444
3425
|
switchType: switchType,
|
|
3445
3426
|
value: switchValue,
|
|
3446
|
-
timestamp: Int64(Date().timeIntervalSince1970 * 1000)
|
|
3427
|
+
timestamp: Int64(Date().timeIntervalSince1970 * 1000),
|
|
3428
|
+
resultCode: resultCode
|
|
3447
3429
|
)
|
|
3448
3430
|
}
|
|
3449
3431
|
|
|
@@ -3459,7 +3441,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3459
3441
|
|
|
3460
3442
|
case "sr_wrst":
|
|
3461
3443
|
if let body = k900ParseBody(json["B"]) {
|
|
3462
|
-
Bridge.sendWearState(
|
|
3444
|
+
Bridge.sendWearState(
|
|
3445
|
+
worn: (k900JsonInt(body, "on") ?? 0) != 0,
|
|
3446
|
+
elapsedMs: k900JsonInt(body, "elapsed_ms"),
|
|
3447
|
+
timeoutMs: k900JsonInt(body, "timeout_ms"),
|
|
3448
|
+
enabled: body["enabled"] == nil ? nil : (k900JsonInt(body, "enabled") ?? 0) != 0,
|
|
3449
|
+
armed: body["armed"] == nil ? nil : (k900JsonInt(body, "armed") ?? 0) != 0,
|
|
3450
|
+
inhibited: body["inhibited"] == nil ? nil : (k900JsonInt(body, "inhibited") ?? 0) != 0
|
|
3451
|
+
)
|
|
3463
3452
|
}
|
|
3464
3453
|
|
|
3465
3454
|
case "sr_micrms":
|
|
@@ -4148,7 +4137,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4148
4137
|
activeFileTransfers.removeValue(forKey: fileName)
|
|
4149
4138
|
}
|
|
4150
4139
|
|
|
4151
|
-
var session =
|
|
4140
|
+
var session = MentraLiveFileTransferSession(
|
|
4152
4141
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4153
4142
|
)
|
|
4154
4143
|
session.isAnnounced = true
|
|
@@ -4158,7 +4147,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4158
4147
|
if var bleTransfer = blePhotoTransfers[bleImgId] {
|
|
4159
4148
|
var bleSession =
|
|
4160
4149
|
bleTransfer.session
|
|
4161
|
-
??
|
|
4150
|
+
?? MentraLiveFileTransferSession(
|
|
4162
4151
|
fileName: fileName, fileSize: fileSize, announcedPackets: totalPackets
|
|
4163
4152
|
)
|
|
4164
4153
|
bleSession.updateAnnouncedPackets(totalPackets)
|
|
@@ -4251,8 +4240,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4251
4240
|
|
|
4252
4241
|
if incidentRelay.session == nil {
|
|
4253
4242
|
activeFileTransfers.removeValue(forKey: packetInfo.fileName)
|
|
4254
|
-
var session =
|
|
4255
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4243
|
+
var session = MentraLiveFileTransferSession(
|
|
4244
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4256
4245
|
)
|
|
4257
4246
|
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4258
4247
|
incidentRelay.session = session
|
|
@@ -4303,10 +4292,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4303
4292
|
|
|
4304
4293
|
// Get or create session for this transfer
|
|
4305
4294
|
if photoTransfer.session == nil {
|
|
4306
|
-
var session =
|
|
4295
|
+
var session = MentraLiveFileTransferSession(
|
|
4307
4296
|
fileName: packetInfo.fileName,
|
|
4308
|
-
fileSize: Int(packetInfo.fileSize)
|
|
4297
|
+
fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4309
4298
|
)
|
|
4299
|
+
session.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4310
4300
|
photoTransfer.session = session
|
|
4311
4301
|
blePhotoTransfers[bleImgId] = photoTransfer
|
|
4312
4302
|
Bridge.log(
|
|
@@ -4379,9 +4369,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4379
4369
|
var session = activeFileTransfers[packetInfo.fileName]
|
|
4380
4370
|
if session == nil {
|
|
4381
4371
|
// New file transfer
|
|
4382
|
-
session =
|
|
4383
|
-
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize)
|
|
4372
|
+
session = MentraLiveFileTransferSession(
|
|
4373
|
+
fileName: packetInfo.fileName, fileSize: Int(packetInfo.fileSize), flags: packetInfo.flags
|
|
4384
4374
|
)
|
|
4375
|
+
session?.recalculateTotalPackets(actualPackSize: Int(packetInfo.packSize))
|
|
4385
4376
|
activeFileTransfers[packetInfo.fileName] = session
|
|
4386
4377
|
|
|
4387
4378
|
Bridge.log(
|
|
@@ -5273,7 +5264,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5273
5264
|
if let requestId {
|
|
5274
5265
|
json["request_id"] = requestId
|
|
5275
5266
|
}
|
|
5276
|
-
|
|
5267
|
+
// Wake ASG so the version request and its response can finish after idle.
|
|
5268
|
+
sendJson(json, wakeUp: true)
|
|
5277
5269
|
}
|
|
5278
5270
|
|
|
5279
5271
|
private func sendCoreTokenToAsgClient() {
|
|
@@ -5523,8 +5515,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5523
5515
|
Bridge.sendSpeakingStatus(speaking)
|
|
5524
5516
|
}
|
|
5525
5517
|
|
|
5526
|
-
private func handleSwitchStatus(
|
|
5518
|
+
private func handleSwitchStatus(
|
|
5519
|
+
switchType: Int, value: Int, timestamp: Int64, resultCode: Int = -1
|
|
5520
|
+
) {
|
|
5527
5521
|
Bridge.sendSwitchStatus(switchType: switchType, value: value, timestamp: timestamp)
|
|
5522
|
+
if switchType == Self.autoPowerOffSwitchType {
|
|
5523
|
+
let ok = resultCode == 0
|
|
5524
|
+
Bridge.log(
|
|
5525
|
+
"LIVE: 🔋 auto power-off sr_swit reply type=\(switchType) switch=\(value)"
|
|
5526
|
+
+ " result=\(resultCode) ok=\(ok)"
|
|
5527
|
+
)
|
|
5528
|
+
}
|
|
5528
5529
|
if switchType == Self.voiceActivityDetectionSwitchType, value == 0 || value == 1 {
|
|
5529
5530
|
handleVoiceActivityDetectionStatus(enabled: value == 1)
|
|
5530
5531
|
}
|
|
@@ -5999,6 +6000,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5999
6000
|
closeL2capFileChannel()
|
|
6000
6001
|
|
|
6001
6002
|
// Disconnect BLE
|
|
6003
|
+
cancelPendingReconnect(reason: "destroy")
|
|
6002
6004
|
if let peripheral = connectedPeripheral {
|
|
6003
6005
|
centralManager?.cancelPeripheralConnection(peripheral)
|
|
6004
6006
|
}
|
|
@@ -6874,6 +6876,9 @@ extension MentraLive {
|
|
|
6874
6876
|
// Send glasses-side loudness / Barrier gate setting.
|
|
6875
6877
|
sendLoudnessGateSetting()
|
|
6876
6878
|
|
|
6879
|
+
// Send glasses-side auto power-off setting.
|
|
6880
|
+
sendAutoPowerOffSetting()
|
|
6881
|
+
|
|
6877
6882
|
// Send mic tuning. With nothing authorized this sends a reset, which is
|
|
6878
6883
|
// what returns a freshly connected pair of glasses to stock behaviour.
|
|
6879
6884
|
sendMicTuningSetting()
|
|
@@ -7100,6 +7105,46 @@ extension MentraLive {
|
|
|
7100
7105
|
}
|
|
7101
7106
|
}
|
|
7102
7107
|
|
|
7108
|
+
func sendAutoPowerOffSetting() {
|
|
7109
|
+
let enabled = DeviceStore.shared.get("bluetooth", "auto_power_off_enabled") as? Bool
|
|
7110
|
+
?? BluetoothSdkDefaults.autoPowerOffEnabled
|
|
7111
|
+
Bridge.log(
|
|
7112
|
+
"LIVE: 🔋 Sending auto power-off setting to glasses: enabled=\(enabled)"
|
|
7113
|
+
+ " (cs_swit type=\(Self.autoPowerOffSwitchType))"
|
|
7114
|
+
)
|
|
7115
|
+
|
|
7116
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
7117
|
+
Bridge.log("LIVE: Cannot send auto power-off setting - BLE write path not ready")
|
|
7118
|
+
return
|
|
7119
|
+
}
|
|
7120
|
+
|
|
7121
|
+
do {
|
|
7122
|
+
let bodyData = try JSONSerialization.data(withJSONObject: [
|
|
7123
|
+
"type": Self.autoPowerOffSwitchType,
|
|
7124
|
+
"switch": enabled ? 1 : 0,
|
|
7125
|
+
])
|
|
7126
|
+
guard let bodyString = String(data: bodyData, encoding: .utf8) else {
|
|
7127
|
+
Bridge.log("LIVE: Failed to encode auto power-off payload")
|
|
7128
|
+
return
|
|
7129
|
+
}
|
|
7130
|
+
let command: [String: Any] = [
|
|
7131
|
+
"C": "cs_swit",
|
|
7132
|
+
"V": 1,
|
|
7133
|
+
"B": bodyString,
|
|
7134
|
+
]
|
|
7135
|
+
if !sendRawK900Command(command, wakeUp: true) {
|
|
7136
|
+
Bridge.log("LIVE: Failed to send auto power-off setting command")
|
|
7137
|
+
} else {
|
|
7138
|
+
Bridge.log(
|
|
7139
|
+
"LIVE: 🔋 Queued auto power-off cs_swit type=\(Self.autoPowerOffSwitchType)"
|
|
7140
|
+
+ " switch=\(enabled ? 1 : 0)"
|
|
7141
|
+
)
|
|
7142
|
+
}
|
|
7143
|
+
} catch {
|
|
7144
|
+
Bridge.log("LIVE: Error encoding auto power-off payload: \(error)")
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7103
7148
|
func sendButtonVideoRecordingSettings() {
|
|
7104
7149
|
let settings =
|
|
7105
7150
|
DeviceStore.shared.get("bluetooth", "button_video_settings") as? [String: Any] ?? [
|
|
@@ -7418,7 +7463,7 @@ extension MentraLive {
|
|
|
7418
7463
|
if fps > 0 { settings["fps"] = fps }
|
|
7419
7464
|
json["settings"] = settings
|
|
7420
7465
|
}
|
|
7421
|
-
sendJson(json)
|
|
7466
|
+
sendJson(json, wakeUp: true)
|
|
7422
7467
|
}
|
|
7423
7468
|
|
|
7424
7469
|
func stopVideoRecording(requestId: String) {
|
|
@@ -7433,7 +7478,7 @@ extension MentraLive {
|
|
|
7433
7478
|
sendJson([
|
|
7434
7479
|
"type": "get_video_recording_status",
|
|
7435
7480
|
"requestId": requestId,
|
|
7436
|
-
])
|
|
7481
|
+
], wakeUp: true)
|
|
7437
7482
|
}
|
|
7438
7483
|
|
|
7439
7484
|
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
@@ -7458,7 +7503,7 @@ extension MentraLive {
|
|
|
7458
7503
|
if let authToken, !authToken.isEmpty {
|
|
7459
7504
|
json["authToken"] = authToken
|
|
7460
7505
|
}
|
|
7461
|
-
sendJson(json)
|
|
7506
|
+
sendJson(json, wakeUp: true)
|
|
7462
7507
|
}
|
|
7463
7508
|
}
|
|
7464
7509
|
|