@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.12
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 +56 -9
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +205 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1137 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +56 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +43 -4
- package/ios/Source/DeviceManager.swift +143 -13
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +410 -30
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +104 -0
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +67 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
|
@@ -857,20 +857,65 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
-
func handleDiscoveredPeripheral(
|
|
860
|
+
func handleDiscoveredPeripheral(
|
|
861
|
+
_ peripheral: CBPeripheral,
|
|
862
|
+
advertisementData: [String: Any] = [:],
|
|
863
|
+
rssi: NSNumber? = nil
|
|
864
|
+
) {
|
|
861
865
|
guard let name = peripheral.name else { return }
|
|
862
866
|
|
|
863
867
|
// Check for compatible device names
|
|
864
868
|
if name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
865
869
|
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
866
870
|
{
|
|
871
|
+
let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
|
|
872
|
+
let isReconnectTarget = savedDeviceName != nil && savedDeviceName == name
|
|
873
|
+
if !isReconnectTarget && advertisesPairingFlag(advertisementData)
|
|
874
|
+
&& !isPairingDiscoverable(advertisementData)
|
|
875
|
+
{
|
|
876
|
+
// Nearby but not pairable: RN uses this for the empty-state hint, not the list.
|
|
877
|
+
Bridge.log(
|
|
878
|
+
"LIVE: Nearby \(name) is secure firmware not in pairing mode — hiding from scan list"
|
|
879
|
+
)
|
|
880
|
+
discoveredPeripherals[name] = peripheral
|
|
881
|
+
cacheAdvPairing(
|
|
882
|
+
name,
|
|
883
|
+
pairingMode: false,
|
|
884
|
+
pairingCode: nil,
|
|
885
|
+
securePairingCapable: true
|
|
886
|
+
)
|
|
887
|
+
emitDiscoveredDeviceFromCache(
|
|
888
|
+
name,
|
|
889
|
+
identifier: peripheral.identifier.uuidString,
|
|
890
|
+
rssi: rssi?.intValue
|
|
891
|
+
)
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
867
895
|
let glassType = name == "Xy_A" ? "Standard" : "K900"
|
|
868
|
-
|
|
896
|
+
let trailer = parseSecurePairingTrailer(advertisementData)
|
|
897
|
+
Bridge.log(
|
|
898
|
+
trailer.secureCapable
|
|
899
|
+
? "Found compatible \(glassType) glasses device: \(name) (pairingMode=\(trailer.pairingMode))"
|
|
900
|
+
: "Found compatible \(glassType) glasses device: \(name) (legacy firmware, pairable)"
|
|
901
|
+
)
|
|
869
902
|
|
|
870
903
|
// Store the peripheral
|
|
871
904
|
discoveredPeripherals[name] = peripheral
|
|
872
905
|
|
|
873
|
-
|
|
906
|
+
cacheAdvPairing(
|
|
907
|
+
name,
|
|
908
|
+
// Field firmware has no pairing flag. Treat it as pairable so the
|
|
909
|
+
// new Mentra App can still find old glasses.
|
|
910
|
+
pairingMode: trailer.secureCapable ? trailer.pairingMode : true,
|
|
911
|
+
pairingCode: trailer.pairingCode,
|
|
912
|
+
securePairingCapable: trailer.secureCapable
|
|
913
|
+
)
|
|
914
|
+
emitDiscoveredDeviceFromCache(
|
|
915
|
+
name,
|
|
916
|
+
identifier: peripheral.identifier.uuidString,
|
|
917
|
+
rssi: rssi?.intValue
|
|
918
|
+
)
|
|
874
919
|
|
|
875
920
|
// Check if this is the device we want to connect to
|
|
876
921
|
if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
|
|
@@ -887,20 +932,40 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
887
932
|
|
|
888
933
|
nonisolated func centralManager(
|
|
889
934
|
_: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
890
|
-
advertisementData
|
|
935
|
+
advertisementData: [String: Any], rssi: NSNumber
|
|
891
936
|
) {
|
|
892
937
|
DispatchQueue.main.async { [weak self] in
|
|
893
|
-
self?.handleDiscoveredPeripheral(peripheral, rssi: rssi)
|
|
938
|
+
self?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: rssi)
|
|
894
939
|
}
|
|
895
940
|
}
|
|
896
941
|
|
|
897
942
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
898
943
|
DispatchQueue.main.async { [weak self] in
|
|
899
944
|
guard let self else { return }
|
|
945
|
+
let matchesActiveAttempt = self.connectingPeripheral === peripheral
|
|
946
|
+
guard MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
947
|
+
pairingYieldActive: self.pairingYieldActive,
|
|
948
|
+
matchesActiveAttempt: matchesActiveAttempt
|
|
949
|
+
) else {
|
|
950
|
+
Bridge.log(
|
|
951
|
+
"LIVE: Rejecting stale connection callback during pairing yield or after attempt replacement: \(peripheral.identifier)"
|
|
952
|
+
)
|
|
953
|
+
self.stopConnectionTimeout()
|
|
954
|
+
if self.connectingPeripheral === peripheral {
|
|
955
|
+
self.connectingPeripheral = nil
|
|
956
|
+
}
|
|
957
|
+
if self.connectedPeripheral === peripheral {
|
|
958
|
+
self.connectedPeripheral = nil
|
|
959
|
+
}
|
|
960
|
+
self.isConnecting = false
|
|
961
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
962
|
+
return
|
|
963
|
+
}
|
|
900
964
|
Bridge.log("Connected to GATT server, discovering services...")
|
|
901
965
|
|
|
902
966
|
self.stopConnectionTimeout()
|
|
903
967
|
self.isConnecting = false
|
|
968
|
+
self.connectingPeripheral = nil
|
|
904
969
|
self.connectedPeripheral = peripheral
|
|
905
970
|
|
|
906
971
|
// Save device name and address for future reconnection
|
|
@@ -939,10 +1004,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
939
1004
|
}
|
|
940
1005
|
|
|
941
1006
|
nonisolated func centralManager(
|
|
942
|
-
_: CBCentralManager, didDisconnectPeripheral
|
|
1007
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
|
|
943
1008
|
) {
|
|
944
1009
|
DispatchQueue.main.async { [weak self] in
|
|
945
1010
|
guard let self else { return }
|
|
1011
|
+
let isCurrent =
|
|
1012
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
1013
|
+
if !isCurrent {
|
|
1014
|
+
Bridge.log("LIVE: Ignoring stale disconnect for \(peripheral.identifier)")
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
self.connectingPeripheral = nil
|
|
946
1018
|
Bridge.log("LIVE: Disconnected from GATT server")
|
|
947
1019
|
|
|
948
1020
|
self.isConnecting = false
|
|
@@ -962,6 +1034,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
962
1034
|
self.txCharacteristic = nil
|
|
963
1035
|
self.rxCharacteristic = nil
|
|
964
1036
|
|
|
1037
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1038
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1039
|
+
return
|
|
1040
|
+
}
|
|
1041
|
+
|
|
965
1042
|
// Attempt reconnection if not killed
|
|
966
1043
|
if !self.isKilled {
|
|
967
1044
|
self.handleReconnection()
|
|
@@ -969,10 +1046,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
969
1046
|
}
|
|
970
1047
|
}
|
|
971
1048
|
|
|
972
|
-
nonisolated func centralManager(_: CBCentralManager, didFailToConnect
|
|
1049
|
+
nonisolated func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
973
1050
|
let errorDescription = error?.localizedDescription ?? "Unknown error"
|
|
974
1051
|
DispatchQueue.main.async { [weak self] in
|
|
975
1052
|
guard let self else { return }
|
|
1053
|
+
let isCurrent =
|
|
1054
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
1055
|
+
if !isCurrent {
|
|
1056
|
+
Bridge.log("LIVE: Ignoring stale connect failure for \(peripheral.identifier)")
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
self.connectingPeripheral = nil
|
|
976
1060
|
Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
|
|
977
1061
|
|
|
978
1062
|
self.stopConnectionTimeout()
|
|
@@ -981,6 +1065,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
981
1065
|
self.connectedPeripheral = nil
|
|
982
1066
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
983
1067
|
|
|
1068
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1069
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1070
|
+
return
|
|
1071
|
+
}
|
|
1072
|
+
|
|
984
1073
|
if !self.isKilled {
|
|
985
1074
|
self.handleReconnection()
|
|
986
1075
|
}
|
|
@@ -1276,6 +1365,39 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1276
1365
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1277
1366
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1278
1367
|
private static let loudnessGateSwitchType = 10
|
|
1368
|
+
private func pairingAdvertisement(
|
|
1369
|
+
_ advertisementData: [String: Any]
|
|
1370
|
+
) -> MentraLivePairingAdvertisement? {
|
|
1371
|
+
MentraLivePairingAdvertisement.parse(
|
|
1372
|
+
coreBluetoothManufacturerData:
|
|
1373
|
+
advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
|
1374
|
+
)
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
private func isPairingDiscoverable(_ advertisementData: [String: Any]) -> Bool {
|
|
1378
|
+
pairingAdvertisement(advertisementData)?.pairingMode == true
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private struct SecurePairingTrailer {
|
|
1382
|
+
let pairingMode: Bool
|
|
1383
|
+
let pairingCode: String?
|
|
1384
|
+
let secureCapable: Bool
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
private func parseSecurePairingTrailer(_ advertisementData: [String: Any]) -> SecurePairingTrailer {
|
|
1388
|
+
guard let advertisement = pairingAdvertisement(advertisementData) else {
|
|
1389
|
+
return SecurePairingTrailer(pairingMode: false, pairingCode: nil, secureCapable: false)
|
|
1390
|
+
}
|
|
1391
|
+
return SecurePairingTrailer(
|
|
1392
|
+
pairingMode: advertisement.pairingMode,
|
|
1393
|
+
pairingCode: advertisement.pairingCode,
|
|
1394
|
+
secureCapable: true
|
|
1395
|
+
)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
private func advertisesPairingFlag(_ advertisementData: [String: Any]) -> Bool {
|
|
1399
|
+
pairingAdvertisement(advertisementData) != nil
|
|
1400
|
+
}
|
|
1279
1401
|
|
|
1280
1402
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
1281
1403
|
|
|
@@ -1290,6 +1412,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1290
1412
|
// return, unlike Android).
|
|
1291
1413
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1292
1414
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1415
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
|
|
1416
|
+
DeviceStore.shared.apply("glasses", "hotspotOtaVersion", 0)
|
|
1293
1417
|
}
|
|
1294
1418
|
connectionState = state
|
|
1295
1419
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
@@ -1343,14 +1467,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1343
1467
|
func forget() {
|
|
1344
1468
|
Bridge.log("LIVE: Forgetting Mentra Live glasses")
|
|
1345
1469
|
|
|
1470
|
+
// Clear saved device name so the pairing filter is not bypassed on rescan
|
|
1471
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
|
|
1472
|
+
|
|
1346
1473
|
// Stop scanning first
|
|
1347
1474
|
if isScanning {
|
|
1348
1475
|
stopScan()
|
|
1349
1476
|
emitStopScanEvent()
|
|
1350
1477
|
}
|
|
1351
1478
|
|
|
1352
|
-
// Then do full cleanup
|
|
1353
|
-
|
|
1479
|
+
// Then do full cleanup after the unpair write can leave GATT.
|
|
1480
|
+
sendJson(["type": "unpair"], wakeUp: true, requireAck: false)
|
|
1481
|
+
unpairFlushWorkItem?.cancel()
|
|
1482
|
+
unpairFlushPending = true
|
|
1483
|
+
let work = DispatchWorkItem { [self] in
|
|
1484
|
+
self.unpairFlushPending = false
|
|
1485
|
+
self.unpairFlushWorkItem = nil
|
|
1486
|
+
self.destroy()
|
|
1487
|
+
}
|
|
1488
|
+
unpairFlushWorkItem = work
|
|
1489
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: work)
|
|
1354
1490
|
}
|
|
1355
1491
|
|
|
1356
1492
|
var type = "Mentra Live"
|
|
@@ -1428,7 +1564,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1428
1564
|
private var lc3ReadCharacteristic: CBCharacteristic?
|
|
1429
1565
|
private var lc3WriteCharacteristic: CBCharacteristic?
|
|
1430
1566
|
private var supportsLC3Audio = true
|
|
1431
|
-
private var lastReceivedLc3Sequence: Int8 = -1
|
|
1432
1567
|
private let LC3_FRAME_SIZE = 40 // bytes per LC3 frame
|
|
1433
1568
|
private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes in seconds
|
|
1434
1569
|
private var micBeatTimer: Timer?
|
|
@@ -1474,6 +1609,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1474
1609
|
private var centralManager: CBCentralManager?
|
|
1475
1610
|
|
|
1476
1611
|
private var connectedPeripheral: CBPeripheral?
|
|
1612
|
+
/// Peripheral for the in-flight connect; used to ignore stale disconnect/fail callbacks.
|
|
1613
|
+
private var connectingPeripheral: CBPeripheral?
|
|
1477
1614
|
private var txCharacteristic: CBCharacteristic?
|
|
1478
1615
|
private var rxCharacteristic: CBCharacteristic?
|
|
1479
1616
|
private let bes2700MtuLimit = 509
|
|
@@ -1483,6 +1620,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1483
1620
|
private var isScanning = false
|
|
1484
1621
|
private var isConnecting = false
|
|
1485
1622
|
private var isKilled = false
|
|
1623
|
+
/// Glasses opened pairing window — stand down without forgetting identity/bonds.
|
|
1624
|
+
private var pairingYieldActive = false
|
|
1625
|
+
private var pairingYieldAwaitingReclaim = false
|
|
1626
|
+
private var pairingYieldEndWorkItem: DispatchWorkItem?
|
|
1627
|
+
/// Hold GATT teardown until the unpair write can leave the phone.
|
|
1628
|
+
private var unpairFlushPending = false
|
|
1629
|
+
private var unpairFlushWorkItem: DispatchWorkItem?
|
|
1486
1630
|
private var reconnectAttempts = 0
|
|
1487
1631
|
private var isNewVersion = false
|
|
1488
1632
|
private var peerWireProtocolVersion = 0
|
|
@@ -1601,6 +1745,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1601
1745
|
// MARK: - React Native Interface
|
|
1602
1746
|
|
|
1603
1747
|
private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
|
|
1748
|
+
/// Last advertisement pairing trailer for a discovered name. Re-emits (already-
|
|
1749
|
+
/// connected / scan replay) must not drop these flags: omitting them makes a
|
|
1750
|
+
/// secure idle unit look like pairable legacy firmware.
|
|
1751
|
+
private var discoveredAdvPairing = [String: CachedAdvPairing]()
|
|
1752
|
+
|
|
1753
|
+
private struct CachedAdvPairing {
|
|
1754
|
+
let pairingMode: Bool
|
|
1755
|
+
let pairingCode: String?
|
|
1756
|
+
let securePairingCapable: Bool
|
|
1757
|
+
}
|
|
1604
1758
|
|
|
1605
1759
|
func findCompatibleDevices() {
|
|
1606
1760
|
Bridge.log("Finding compatible Mentra Live glasses")
|
|
@@ -1622,7 +1776,89 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1622
1776
|
}
|
|
1623
1777
|
}
|
|
1624
1778
|
|
|
1779
|
+
func isPairingYieldActive() -> Bool {
|
|
1780
|
+
pairingYieldActive
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
private func enterPairingYield(windowMs: Int) {
|
|
1784
|
+
pairingYieldActive = true
|
|
1785
|
+
pairingYieldAwaitingReclaim = false
|
|
1786
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1787
|
+
if isScanning {
|
|
1788
|
+
stopScan()
|
|
1789
|
+
}
|
|
1790
|
+
// Publish disconnect immediately for Phone A UI. didDisconnectPeripheral may lag
|
|
1791
|
+
// or race with cancelPeripheralConnection; do not wait on it for stand-down.
|
|
1792
|
+
isConnecting = false
|
|
1793
|
+
connected = false
|
|
1794
|
+
fullyBooted = false
|
|
1795
|
+
glassesSessionId = nil
|
|
1796
|
+
readinessCompletedThisBleSession = false
|
|
1797
|
+
rgbLedAuthorityClaimed = false
|
|
1798
|
+
stopAllTimers()
|
|
1799
|
+
closeL2capFileChannel()
|
|
1800
|
+
txCharacteristic = nil
|
|
1801
|
+
rxCharacteristic = nil
|
|
1802
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1803
|
+
if let peripheral = connectedPeripheral {
|
|
1804
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1805
|
+
}
|
|
1806
|
+
connectedPeripheral = nil
|
|
1807
|
+
connectingPeripheral = nil
|
|
1808
|
+
// Stand down for the whole window. Probing GATT during yield races the new
|
|
1809
|
+
// phone for the single BLE slot and is exactly what entering_pairing_mode forbids.
|
|
1810
|
+
|
|
1811
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1812
|
+
guard let self else { return }
|
|
1813
|
+
Bridge.log("LIVE: Pairing yield ended — resume reconnect if still owned")
|
|
1814
|
+
self.pairingYieldActive = false
|
|
1815
|
+
self.pairingYieldAwaitingReclaim = true
|
|
1816
|
+
self.pairingYieldEndWorkItem = nil
|
|
1817
|
+
if !self.isKilled, self.connectedPeripheral == nil {
|
|
1818
|
+
self.handleReconnection()
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
pairingYieldEndWorkItem = work
|
|
1822
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(windowMs), execute: work)
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/// Pairing/auth failures after yield — CoreBluetooth codes, not localized strings.
|
|
1826
|
+
/// Connection timeouts are not owner-loss.
|
|
1827
|
+
private static func isPairingAuthFailure(_ error: Error?) -> Bool {
|
|
1828
|
+
guard let error = error as NSError? else { return false }
|
|
1829
|
+
if error.domain == CBATTErrorDomain {
|
|
1830
|
+
return error.code == CBATTError.insufficientAuthentication.rawValue
|
|
1831
|
+
|| error.code == CBATTError.insufficientEncryption.rawValue
|
|
1832
|
+
}
|
|
1833
|
+
if error.domain == CBErrorDomain {
|
|
1834
|
+
return error.code == CBError.peerRemovedPairingInformation.rawValue
|
|
1835
|
+
}
|
|
1836
|
+
return false
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private func clearSavedGlassesAfterOwnerLoss(reason: String) {
|
|
1840
|
+
Bridge.log("LIVE: Owner loss (\(reason)) — clearing saved glasses")
|
|
1841
|
+
pairingYieldAwaitingReclaim = false
|
|
1842
|
+
pairingYieldActive = false
|
|
1843
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1844
|
+
pairingYieldEndWorkItem = nil
|
|
1845
|
+
forget()
|
|
1846
|
+
Task { @MainActor in
|
|
1847
|
+
DeviceStore.shared.apply("bluetooth", "default_wearable", "")
|
|
1848
|
+
DeviceStore.shared.apply("bluetooth", "device_name", "")
|
|
1849
|
+
DeviceStore.shared.apply("bluetooth", "device_address", "")
|
|
1850
|
+
}
|
|
1851
|
+
Bridge.saveSetting("default_wearable", "")
|
|
1852
|
+
Bridge.saveSetting("device_name", "")
|
|
1853
|
+
Bridge.saveSetting("device_address", "")
|
|
1854
|
+
Bridge.sendTypedMessage("owner_replaced", body: ["reason": reason])
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1625
1857
|
func connectById(_ deviceName: String) {
|
|
1858
|
+
if pairingYieldActive {
|
|
1859
|
+
Bridge.log("LIVE: connectById blocked — pairing yield active")
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1626
1862
|
Bridge.log("connectById: \(deviceName)")
|
|
1627
1863
|
// Save the device name for future reconnection
|
|
1628
1864
|
UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
|
|
@@ -1647,7 +1883,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1647
1883
|
{
|
|
1648
1884
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1649
1885
|
discoveredPeripherals[name] = peripheral
|
|
1650
|
-
|
|
1886
|
+
emitDiscoveredDeviceFromCache(name)
|
|
1651
1887
|
|
|
1652
1888
|
// Check if this is the device we want
|
|
1653
1889
|
if let savedDeviceName = UserDefaults.standard.string(
|
|
@@ -1673,6 +1909,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1673
1909
|
}
|
|
1674
1910
|
|
|
1675
1911
|
@objc func disconnect() {
|
|
1912
|
+
if unpairFlushPending {
|
|
1913
|
+
Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
|
|
1914
|
+
return
|
|
1915
|
+
}
|
|
1676
1916
|
Bridge.log("LIVE: disconnect() -Disconnecting from Mentra Live glasses")
|
|
1677
1917
|
|
|
1678
1918
|
// if rgbLedAuthorityClaimed {
|
|
@@ -1698,6 +1938,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1698
1938
|
|
|
1699
1939
|
/// Start the micbeat mechanism to keep LC3 audio streaming active
|
|
1700
1940
|
private func startMicBeat() {
|
|
1941
|
+
// A resumed custom-audio stream starts a new sequence domain.
|
|
1942
|
+
DeviceManager.shared.resetMicSequenceBaseline()
|
|
1701
1943
|
Bridge.log("LIVE: 🎤 Starting micbeat mechanism")
|
|
1702
1944
|
micBeatCount = 0
|
|
1703
1945
|
|
|
@@ -2094,7 +2336,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2094
2336
|
// MARK: - BLE Scanning
|
|
2095
2337
|
|
|
2096
2338
|
private func startScan() {
|
|
2097
|
-
|
|
2339
|
+
if pairingYieldActive {
|
|
2340
|
+
Bridge.log("LIVE: startScan blocked — pairing yield active")
|
|
2341
|
+
return
|
|
2342
|
+
}
|
|
2098
2343
|
|
|
2099
2344
|
guard centralManager!.state == .poweredOn else {
|
|
2100
2345
|
Bridge.log("Attempting to scan but bluetooth is not powered on.")
|
|
@@ -2102,6 +2347,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2102
2347
|
}
|
|
2103
2348
|
|
|
2104
2349
|
Bridge.log("Starting BLE scan for Mentra Live glasses")
|
|
2350
|
+
discoveredAdvPairing.removeAll()
|
|
2105
2351
|
isScanning = true
|
|
2106
2352
|
|
|
2107
2353
|
startReadinessCheckLoop()
|
|
@@ -2118,11 +2364,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2118
2364
|
|
|
2119
2365
|
centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
2120
2366
|
|
|
2121
|
-
//
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
emitDiscoveredDevice(peripheral.name!)
|
|
2125
|
-
}
|
|
2367
|
+
// Fresh advertisements refill pairing metadata. Re-emitting the last scan's
|
|
2368
|
+
// cache would keep a unit pairable after it left pairing mode.
|
|
2369
|
+
emitConnectedDeviceForPairingScan()
|
|
2126
2370
|
|
|
2127
2371
|
// var dName = DeviceManager.shared.deviceName
|
|
2128
2372
|
// if dName.isEmpty {
|
|
@@ -2152,10 +2396,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2152
2396
|
// MARK: - Connection Management
|
|
2153
2397
|
|
|
2154
2398
|
private func connectToDevice(_ peripheral: CBPeripheral) {
|
|
2399
|
+
if pairingYieldActive {
|
|
2400
|
+
Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
|
|
2401
|
+
isConnecting = false
|
|
2402
|
+
return
|
|
2403
|
+
}
|
|
2155
2404
|
Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
|
|
2156
2405
|
|
|
2157
2406
|
isConnecting = true
|
|
2158
2407
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
2408
|
+
connectingPeripheral = peripheral
|
|
2159
2409
|
connectedPeripheral = peripheral
|
|
2160
2410
|
peripheral.delegate = self
|
|
2161
2411
|
|
|
@@ -2200,6 +2450,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2200
2450
|
return
|
|
2201
2451
|
}
|
|
2202
2452
|
|
|
2453
|
+
if pairingYieldActive {
|
|
2454
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2455
|
+
return
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2203
2458
|
// Check if we've exceeded max attempts
|
|
2204
2459
|
if reconnectAttempts >= MAX_RECONNECT_ATTEMPTS {
|
|
2205
2460
|
Bridge.log("LIVE: Maximum reconnection attempts reached (\(MAX_RECONNECT_ATTEMPTS))")
|
|
@@ -2232,6 +2487,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2232
2487
|
guard let self = self else { return }
|
|
2233
2488
|
|
|
2234
2489
|
// Use peripheral presence, not connectionState: we stay CONNECTING during backoff until scan/connect.
|
|
2490
|
+
if self.pairingYieldActive {
|
|
2491
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2492
|
+
return
|
|
2493
|
+
}
|
|
2235
2494
|
if self.connectedPeripheral == nil, !self.isKilled {
|
|
2236
2495
|
// Check for last known device name to start scan
|
|
2237
2496
|
if let lastDeviceName = UserDefaults.standard.string(
|
|
@@ -2461,11 +2720,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2461
2720
|
|
|
2462
2721
|
switch type {
|
|
2463
2722
|
case "glasses_ready":
|
|
2723
|
+
// A queued glasses_ready from the connection we just cancelled for yield
|
|
2724
|
+
// must not close the pairing window. Only accept readiness on a live GATT.
|
|
2725
|
+
if pairingYieldActive && connectedPeripheral == nil {
|
|
2726
|
+
Bridge.log("LIVE: Ignoring stale glasses_ready during pairing yield")
|
|
2727
|
+
return
|
|
2728
|
+
}
|
|
2464
2729
|
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
2465
2730
|
// onTransportReset() before sending it, so their side is back on legacy
|
|
2466
2731
|
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
2467
2732
|
// wire epoch (clears v2-active state that would otherwise gate the
|
|
2468
2733
|
// handshake off), then negotiate from the caps this message advertises.
|
|
2734
|
+
pairingYieldAwaitingReclaim = false
|
|
2735
|
+
pairingYieldActive = false
|
|
2736
|
+
pairingYieldEndWorkItem?.cancel()
|
|
2737
|
+
pairingYieldEndWorkItem = nil
|
|
2469
2738
|
resetWireNegotiationState()
|
|
2470
2739
|
parsePeerWireCaps(json)
|
|
2471
2740
|
maybeSendWireHandshake()
|
|
@@ -2617,6 +2886,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2617
2886
|
case "pong":
|
|
2618
2887
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
2619
2888
|
|
|
2889
|
+
case "entering_pairing_mode":
|
|
2890
|
+
let windowMs = max(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2891
|
+
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2892
|
+
enterPairingYield(windowMs: windowMs)
|
|
2893
|
+
let body: [String: Any] = [
|
|
2894
|
+
"window_ms": windowMs,
|
|
2895
|
+
"reason": json["reason"] as? String ?? "user_gesture",
|
|
2896
|
+
]
|
|
2897
|
+
Bridge.sendTypedMessage("entering_pairing_mode", body: body)
|
|
2898
|
+
|
|
2899
|
+
case "pairing_info":
|
|
2900
|
+
Bridge.sendPairingInfo(
|
|
2901
|
+
hadPreviousBond: json["had_previous_bond"] as? Bool ?? false,
|
|
2902
|
+
pairingCode: json["pairing_code"] as? String,
|
|
2903
|
+
classicBondReady: json["classic_bond_ready"] as? Bool ?? false,
|
|
2904
|
+
// Legacy firmware that omits this field is not secure-capable.
|
|
2905
|
+
securePairingCapable: json["secure_pairing_capable"] as? Bool ?? false,
|
|
2906
|
+
protocolVersion: json["protocol_version"] as? Int ?? 1
|
|
2907
|
+
)
|
|
2908
|
+
|
|
2620
2909
|
case "imu_response", "imu_stream_response", "imu_gesture_response",
|
|
2621
2910
|
"imu_gesture_subscribed", "imu_ack", "imu_error":
|
|
2622
2911
|
// Handle IMU-related responses
|
|
@@ -2778,9 +3067,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2778
3067
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2779
3068
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2780
3069
|
}
|
|
3070
|
+
if let hotspotOtaVersion = fields["hotspot_ota_version"] as? NSNumber {
|
|
3071
|
+
DeviceStore.shared.apply(
|
|
3072
|
+
"glasses",
|
|
3073
|
+
"hotspotOtaVersion",
|
|
3074
|
+
hotspotOtaVersion.intValue
|
|
3075
|
+
)
|
|
3076
|
+
}
|
|
2781
3077
|
if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
|
|
2782
3078
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2783
3079
|
}
|
|
3080
|
+
if let wifiMacAddress = nonEmptyStringValue(fields, "wifi_mac_address") {
|
|
3081
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", wifiMacAddress)
|
|
3082
|
+
}
|
|
2784
3083
|
if let serialNumber = nonEmptyStringValue(fields, "serial_number") {
|
|
2785
3084
|
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2786
3085
|
}
|
|
@@ -3219,6 +3518,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3219
3518
|
sendJson(json, wakeUp: true)
|
|
3220
3519
|
}
|
|
3221
3520
|
|
|
3521
|
+
func sendWifiAdbState(_ enabled: Bool) {
|
|
3522
|
+
Bridge.log("LIVE: 🔧 Sending Wi-Fi ADB state: \(enabled)")
|
|
3523
|
+
|
|
3524
|
+
let json: [String: Any] = [
|
|
3525
|
+
"type": "set_wifi_adb_state",
|
|
3526
|
+
"enabled": enabled,
|
|
3527
|
+
]
|
|
3528
|
+
|
|
3529
|
+
sendJson(json, wakeUp: true)
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3222
3532
|
func sendSetSystemTime(_ timestampMs: Int64) {
|
|
3223
3533
|
Bridge.log("LIVE: ⏰ Sending set_system_time: \(timestampMs)")
|
|
3224
3534
|
|
|
@@ -3338,7 +3648,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3338
3648
|
{
|
|
3339
3649
|
json["ota_version_url"] = otaVersionUrl
|
|
3340
3650
|
}
|
|
3341
|
-
|
|
3342
3651
|
sendJson(json, wakeUp: true)
|
|
3343
3652
|
}
|
|
3344
3653
|
|
|
@@ -3509,17 +3818,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3509
3818
|
return
|
|
3510
3819
|
}
|
|
3511
3820
|
|
|
3512
|
-
let sequenceNumber =
|
|
3821
|
+
let sequenceNumber = Int(data[1])
|
|
3513
3822
|
let lc3Data = data.subdata(in: 2 ..< data.count)
|
|
3514
3823
|
|
|
3515
|
-
// Validate sequence number for packet loss detection
|
|
3516
|
-
if lastReceivedLc3Sequence != -1 && (lastReceivedLc3Sequence &+ 1) != sequenceNumber {
|
|
3517
|
-
Bridge.log(
|
|
3518
|
-
"LIVE: LC3 packet sequence mismatch. Expected: \(lastReceivedLc3Sequence &+ 1), Got: \(sequenceNumber)"
|
|
3519
|
-
)
|
|
3520
|
-
}
|
|
3521
|
-
lastReceivedLc3Sequence = sequenceNumber
|
|
3522
|
-
|
|
3523
3824
|
// // Decode LC3 to PCM using existing PcmConverter
|
|
3524
3825
|
// let pcmConverter = PcmConverter()
|
|
3525
3826
|
// guard let pcmData = pcmConverter.decode(lc3Data) as? Data, pcmData.count > 0 else {
|
|
@@ -3533,7 +3834,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3533
3834
|
// Bridge.log(
|
|
3534
3835
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count) bytes"
|
|
3535
3836
|
// )
|
|
3536
|
-
DeviceManager.shared.handleGlassesMicData(lc3Data, 40)
|
|
3837
|
+
DeviceManager.shared.handleGlassesMicData(lc3Data, 40, sequenceNumber: sequenceNumber)
|
|
3537
3838
|
|
|
3538
3839
|
// Bridge.log(
|
|
3539
3840
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count)→\(pcmData.count) bytes"
|
|
@@ -5201,8 +5502,71 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5201
5502
|
|
|
5202
5503
|
// MARK: - Event Emission
|
|
5203
5504
|
|
|
5204
|
-
|
|
5205
|
-
|
|
5505
|
+
/// Pairing scan listens for advertisements. A unit that is already GATT-connected
|
|
5506
|
+
/// has stopped ADV, so emit it as pairable or the scan list stays empty.
|
|
5507
|
+
private func emitConnectedDeviceForPairingScan() {
|
|
5508
|
+
guard connected, let peripheral = connectedPeripheral, let name = peripheral.name,
|
|
5509
|
+
name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
5510
|
+
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
5511
|
+
else {
|
|
5512
|
+
return
|
|
5513
|
+
}
|
|
5514
|
+
Bridge.log("LIVE: Pairing scan: already GATT-connected to \(name) — emitting as pairable (ADV off while connected)")
|
|
5515
|
+
emitDiscoveredDevice(
|
|
5516
|
+
name,
|
|
5517
|
+
identifier: peripheral.identifier.uuidString,
|
|
5518
|
+
pairingMode: true,
|
|
5519
|
+
pairingCode: nil,
|
|
5520
|
+
securePairingCapable: discoveredAdvPairing[name]?.securePairingCapable ?? false
|
|
5521
|
+
)
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
private func cacheAdvPairing(
|
|
5525
|
+
_ name: String,
|
|
5526
|
+
pairingMode: Bool,
|
|
5527
|
+
pairingCode: String?,
|
|
5528
|
+
securePairingCapable: Bool
|
|
5529
|
+
) {
|
|
5530
|
+
discoveredAdvPairing[name] = CachedAdvPairing(
|
|
5531
|
+
pairingMode: pairingMode,
|
|
5532
|
+
pairingCode: pairingCode,
|
|
5533
|
+
securePairingCapable: securePairingCapable
|
|
5534
|
+
)
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
private func emitDiscoveredDeviceFromCache(
|
|
5538
|
+
_ name: String,
|
|
5539
|
+
identifier: String = "",
|
|
5540
|
+
rssi: Int? = nil
|
|
5541
|
+
) {
|
|
5542
|
+
let cached = discoveredAdvPairing[name]
|
|
5543
|
+
emitDiscoveredDevice(
|
|
5544
|
+
name,
|
|
5545
|
+
identifier: identifier,
|
|
5546
|
+
rssi: rssi,
|
|
5547
|
+
pairingMode: cached?.pairingMode,
|
|
5548
|
+
pairingCode: cached?.pairingCode,
|
|
5549
|
+
securePairingCapable: cached?.securePairingCapable
|
|
5550
|
+
)
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
private func emitDiscoveredDevice(
|
|
5554
|
+
_ name: String,
|
|
5555
|
+
identifier: String = "",
|
|
5556
|
+
rssi: Int? = nil,
|
|
5557
|
+
pairingMode: Bool? = nil,
|
|
5558
|
+
pairingCode: String? = nil,
|
|
5559
|
+
securePairingCapable: Bool? = nil
|
|
5560
|
+
) {
|
|
5561
|
+
Bridge.sendDiscoveredDevice(
|
|
5562
|
+
"Mentra Live",
|
|
5563
|
+
name,
|
|
5564
|
+
deviceAddress: identifier,
|
|
5565
|
+
rssi: rssi,
|
|
5566
|
+
pairingMode: pairingMode,
|
|
5567
|
+
pairingCode: pairingCode,
|
|
5568
|
+
securePairingCapable: securePairingCapable
|
|
5569
|
+
)
|
|
5206
5570
|
}
|
|
5207
5571
|
|
|
5208
5572
|
private func emitStopScanEvent() {
|
|
@@ -5291,6 +5655,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5291
5655
|
Bridge.log("Destroying MentraLiveManager")
|
|
5292
5656
|
|
|
5293
5657
|
isKilled = true
|
|
5658
|
+
unpairFlushWorkItem?.cancel()
|
|
5659
|
+
unpairFlushWorkItem = nil
|
|
5660
|
+
unpairFlushPending = false
|
|
5294
5661
|
|
|
5295
5662
|
// Stop scanning
|
|
5296
5663
|
if isScanning {
|
|
@@ -5324,6 +5691,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5324
5691
|
DeviceStore.shared.apply("glasses", "hotspotGatewayIp", "")
|
|
5325
5692
|
|
|
5326
5693
|
connectedPeripheral = nil
|
|
5694
|
+
discoveredPeripherals.removeAll()
|
|
5695
|
+
discoveredAdvPairing.removeAll()
|
|
5327
5696
|
centralManager?.delegate = nil
|
|
5328
5697
|
centralManager = nil
|
|
5329
5698
|
|
|
@@ -6533,6 +6902,17 @@ extension MentraLive {
|
|
|
6533
6902
|
stopVideoRecording(requestId: requestId, webhookUrl: nil, authToken: nil)
|
|
6534
6903
|
}
|
|
6535
6904
|
|
|
6905
|
+
func queryVideoRecordingStatus(requestId: String) {
|
|
6906
|
+
guard connectionState == ConnTypes.CONNECTED else {
|
|
6907
|
+
Bridge.log("Cannot query video recording status - not connected")
|
|
6908
|
+
return
|
|
6909
|
+
}
|
|
6910
|
+
sendJson([
|
|
6911
|
+
"type": "get_video_recording_status",
|
|
6912
|
+
"requestId": requestId,
|
|
6913
|
+
])
|
|
6914
|
+
}
|
|
6915
|
+
|
|
6536
6916
|
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
6537
6917
|
Bridge.log(
|
|
6538
6918
|
"Stopping video recording on glasses: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|