@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-beta.103
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 +67 -25
- 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/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -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/ReleaseChangelog.kt +61 -0
- 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 +1186 -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/ReleaseChangelogTest.kt +13 -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 +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +82 -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/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- 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 +24 -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/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/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/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +91 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/ReleaseChangelog.swift +65 -0
- 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 +453 -31
- 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 +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +10 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/public-ota-api.test.mjs +37 -0
- 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 +95 -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/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +43 -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
- package/src/ota-transport/index.ts +116 -0
|
@@ -25,6 +25,35 @@ struct MentraLiveDevice {
|
|
|
25
25
|
let address: String
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
enum MentraLivePendingPairingTarget {
|
|
29
|
+
static func matches(
|
|
30
|
+
connectedName: String,
|
|
31
|
+
connectedIdentifier: String,
|
|
32
|
+
pendingName: String,
|
|
33
|
+
pendingIdentifier: String
|
|
34
|
+
) -> Bool {
|
|
35
|
+
if !pendingIdentifier.isEmpty {
|
|
36
|
+
return connectedIdentifier.caseInsensitiveCompare(pendingIdentifier) == .orderedSame
|
|
37
|
+
}
|
|
38
|
+
return !pendingName.isEmpty && connectedName == pendingName
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func shouldRecover(
|
|
42
|
+
isConnected: Bool,
|
|
43
|
+
connectedName: String,
|
|
44
|
+
connectedIdentifier: String,
|
|
45
|
+
pendingName: String,
|
|
46
|
+
pendingIdentifier: String
|
|
47
|
+
) -> Bool {
|
|
48
|
+
isConnected && matches(
|
|
49
|
+
connectedName: connectedName,
|
|
50
|
+
connectedIdentifier: connectedIdentifier,
|
|
51
|
+
pendingName: pendingName,
|
|
52
|
+
pendingIdentifier: pendingIdentifier
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
28
57
|
// MARK: - BlePhotoUploadService
|
|
29
58
|
|
|
30
59
|
class BlePhotoUploadService {
|
|
@@ -857,20 +886,66 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
857
886
|
}
|
|
858
887
|
}
|
|
859
888
|
|
|
860
|
-
func handleDiscoveredPeripheral(
|
|
889
|
+
func handleDiscoveredPeripheral(
|
|
890
|
+
_ peripheral: CBPeripheral,
|
|
891
|
+
advertisementData: [String: Any] = [:],
|
|
892
|
+
rssi: NSNumber? = nil
|
|
893
|
+
) {
|
|
861
894
|
guard let name = peripheral.name else { return }
|
|
862
895
|
|
|
863
896
|
// Check for compatible device names
|
|
864
897
|
if name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
865
898
|
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
866
899
|
{
|
|
900
|
+
let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
|
|
901
|
+
let isReconnectTarget = savedDeviceName != nil && savedDeviceName == name
|
|
902
|
+
if !isReconnectTarget && advertisesPairingFlag(advertisementData)
|
|
903
|
+
&& !isPairingDiscoverable(advertisementData)
|
|
904
|
+
{
|
|
905
|
+
// Keep nearby secure units visible with pairing-mode guidance, while RN blocks
|
|
906
|
+
// the connection until a pairable advertisement arrives.
|
|
907
|
+
Bridge.log(
|
|
908
|
+
"LIVE: Nearby \(name) is secure firmware not in pairing mode — exposing as non-pairable"
|
|
909
|
+
)
|
|
910
|
+
discoveredPeripherals[name] = peripheral
|
|
911
|
+
cacheAdvPairing(
|
|
912
|
+
name,
|
|
913
|
+
pairingMode: false,
|
|
914
|
+
pairingCode: nil,
|
|
915
|
+
securePairingCapable: true
|
|
916
|
+
)
|
|
917
|
+
emitDiscoveredDeviceFromCache(
|
|
918
|
+
name,
|
|
919
|
+
identifier: peripheral.identifier.uuidString,
|
|
920
|
+
rssi: rssi?.intValue
|
|
921
|
+
)
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
|
|
867
925
|
let glassType = name == "Xy_A" ? "Standard" : "K900"
|
|
868
|
-
|
|
926
|
+
let trailer = parseSecurePairingTrailer(advertisementData)
|
|
927
|
+
Bridge.log(
|
|
928
|
+
trailer.secureCapable
|
|
929
|
+
? "Found compatible \(glassType) glasses device: \(name) (pairingMode=\(trailer.pairingMode))"
|
|
930
|
+
: "Found compatible \(glassType) glasses device: \(name) (legacy firmware, pairable)"
|
|
931
|
+
)
|
|
869
932
|
|
|
870
933
|
// Store the peripheral
|
|
871
934
|
discoveredPeripherals[name] = peripheral
|
|
872
935
|
|
|
873
|
-
|
|
936
|
+
cacheAdvPairing(
|
|
937
|
+
name,
|
|
938
|
+
// Field firmware has no pairing flag. Treat it as pairable so the
|
|
939
|
+
// new Mentra App can still find old glasses.
|
|
940
|
+
pairingMode: trailer.secureCapable ? trailer.pairingMode : true,
|
|
941
|
+
pairingCode: trailer.pairingCode,
|
|
942
|
+
securePairingCapable: trailer.secureCapable
|
|
943
|
+
)
|
|
944
|
+
emitDiscoveredDeviceFromCache(
|
|
945
|
+
name,
|
|
946
|
+
identifier: peripheral.identifier.uuidString,
|
|
947
|
+
rssi: rssi?.intValue
|
|
948
|
+
)
|
|
874
949
|
|
|
875
950
|
// Check if this is the device we want to connect to
|
|
876
951
|
if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
|
|
@@ -887,21 +962,42 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
887
962
|
|
|
888
963
|
nonisolated func centralManager(
|
|
889
964
|
_: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
890
|
-
advertisementData
|
|
965
|
+
advertisementData: [String: Any], rssi: NSNumber
|
|
891
966
|
) {
|
|
892
967
|
DispatchQueue.main.async { [weak self] in
|
|
893
|
-
self?.handleDiscoveredPeripheral(peripheral, rssi: rssi)
|
|
968
|
+
self?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: rssi)
|
|
894
969
|
}
|
|
895
970
|
}
|
|
896
971
|
|
|
897
972
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
898
973
|
DispatchQueue.main.async { [weak self] in
|
|
899
974
|
guard let self else { return }
|
|
975
|
+
let matchesActiveAttempt = self.connectingPeripheral === peripheral
|
|
976
|
+
guard MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
|
|
977
|
+
pairingYieldActive: self.pairingYieldActive,
|
|
978
|
+
matchesActiveAttempt: matchesActiveAttempt
|
|
979
|
+
) else {
|
|
980
|
+
Bridge.log(
|
|
981
|
+
"LIVE: Rejecting stale connection callback during pairing yield or after attempt replacement: \(peripheral.identifier)"
|
|
982
|
+
)
|
|
983
|
+
self.stopConnectionTimeout()
|
|
984
|
+
if self.connectingPeripheral === peripheral {
|
|
985
|
+
self.connectingPeripheral = nil
|
|
986
|
+
}
|
|
987
|
+
if self.connectedPeripheral === peripheral {
|
|
988
|
+
self.connectedPeripheral = nil
|
|
989
|
+
}
|
|
990
|
+
self.isConnecting = false
|
|
991
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
992
|
+
return
|
|
993
|
+
}
|
|
900
994
|
Bridge.log("Connected to GATT server, discovering services...")
|
|
901
995
|
|
|
902
996
|
self.stopConnectionTimeout()
|
|
903
997
|
self.isConnecting = false
|
|
998
|
+
self.connectingPeripheral = nil
|
|
904
999
|
self.connectedPeripheral = peripheral
|
|
1000
|
+
self.emitConnectedPendingDeviceForPairingScan()
|
|
905
1001
|
|
|
906
1002
|
// Save device name and address for future reconnection
|
|
907
1003
|
if let name = peripheral.name {
|
|
@@ -939,10 +1035,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
939
1035
|
}
|
|
940
1036
|
|
|
941
1037
|
nonisolated func centralManager(
|
|
942
|
-
_: CBCentralManager, didDisconnectPeripheral
|
|
1038
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
|
|
943
1039
|
) {
|
|
944
1040
|
DispatchQueue.main.async { [weak self] in
|
|
945
1041
|
guard let self else { return }
|
|
1042
|
+
let isCurrent =
|
|
1043
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
1044
|
+
if !isCurrent {
|
|
1045
|
+
Bridge.log("LIVE: Ignoring stale disconnect for \(peripheral.identifier)")
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
self.connectingPeripheral = nil
|
|
946
1049
|
Bridge.log("LIVE: Disconnected from GATT server")
|
|
947
1050
|
|
|
948
1051
|
self.isConnecting = false
|
|
@@ -962,6 +1065,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
962
1065
|
self.txCharacteristic = nil
|
|
963
1066
|
self.rxCharacteristic = nil
|
|
964
1067
|
|
|
1068
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1069
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1070
|
+
return
|
|
1071
|
+
}
|
|
1072
|
+
|
|
965
1073
|
// Attempt reconnection if not killed
|
|
966
1074
|
if !self.isKilled {
|
|
967
1075
|
self.handleReconnection()
|
|
@@ -969,10 +1077,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
969
1077
|
}
|
|
970
1078
|
}
|
|
971
1079
|
|
|
972
|
-
nonisolated func centralManager(_: CBCentralManager, didFailToConnect
|
|
1080
|
+
nonisolated func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
973
1081
|
let errorDescription = error?.localizedDescription ?? "Unknown error"
|
|
974
1082
|
DispatchQueue.main.async { [weak self] in
|
|
975
1083
|
guard let self else { return }
|
|
1084
|
+
let isCurrent =
|
|
1085
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
1086
|
+
if !isCurrent {
|
|
1087
|
+
Bridge.log("LIVE: Ignoring stale connect failure for \(peripheral.identifier)")
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
self.connectingPeripheral = nil
|
|
976
1091
|
Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
|
|
977
1092
|
|
|
978
1093
|
self.stopConnectionTimeout()
|
|
@@ -981,6 +1096,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
981
1096
|
self.connectedPeripheral = nil
|
|
982
1097
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
983
1098
|
|
|
1099
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1100
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
|
|
984
1104
|
if !self.isKilled {
|
|
985
1105
|
self.handleReconnection()
|
|
986
1106
|
}
|
|
@@ -1276,6 +1396,39 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1276
1396
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1277
1397
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1278
1398
|
private static let loudnessGateSwitchType = 10
|
|
1399
|
+
private func pairingAdvertisement(
|
|
1400
|
+
_ advertisementData: [String: Any]
|
|
1401
|
+
) -> MentraLivePairingAdvertisement? {
|
|
1402
|
+
MentraLivePairingAdvertisement.parse(
|
|
1403
|
+
coreBluetoothManufacturerData:
|
|
1404
|
+
advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
|
1405
|
+
)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private func isPairingDiscoverable(_ advertisementData: [String: Any]) -> Bool {
|
|
1409
|
+
pairingAdvertisement(advertisementData)?.pairingMode == true
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private struct SecurePairingTrailer {
|
|
1413
|
+
let pairingMode: Bool
|
|
1414
|
+
let pairingCode: String?
|
|
1415
|
+
let secureCapable: Bool
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private func parseSecurePairingTrailer(_ advertisementData: [String: Any]) -> SecurePairingTrailer {
|
|
1419
|
+
guard let advertisement = pairingAdvertisement(advertisementData) else {
|
|
1420
|
+
return SecurePairingTrailer(pairingMode: false, pairingCode: nil, secureCapable: false)
|
|
1421
|
+
}
|
|
1422
|
+
return SecurePairingTrailer(
|
|
1423
|
+
pairingMode: advertisement.pairingMode,
|
|
1424
|
+
pairingCode: advertisement.pairingCode,
|
|
1425
|
+
secureCapable: true
|
|
1426
|
+
)
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
private func advertisesPairingFlag(_ advertisementData: [String: Any]) -> Bool {
|
|
1430
|
+
pairingAdvertisement(advertisementData) != nil
|
|
1431
|
+
}
|
|
1279
1432
|
|
|
1280
1433
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
1281
1434
|
|
|
@@ -1290,6 +1443,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1290
1443
|
// return, unlike Android).
|
|
1291
1444
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1292
1445
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1446
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
|
|
1447
|
+
DeviceStore.shared.apply("glasses", "hotspotOtaVersion", 0)
|
|
1293
1448
|
}
|
|
1294
1449
|
connectionState = state
|
|
1295
1450
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
@@ -1343,14 +1498,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1343
1498
|
func forget() {
|
|
1344
1499
|
Bridge.log("LIVE: Forgetting Mentra Live glasses")
|
|
1345
1500
|
|
|
1501
|
+
// Clear saved device name so the pairing filter is not bypassed on rescan
|
|
1502
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
|
|
1503
|
+
|
|
1346
1504
|
// Stop scanning first
|
|
1347
1505
|
if isScanning {
|
|
1348
1506
|
stopScan()
|
|
1349
1507
|
emitStopScanEvent()
|
|
1350
1508
|
}
|
|
1351
1509
|
|
|
1352
|
-
// Then do full cleanup
|
|
1353
|
-
|
|
1510
|
+
// Then do full cleanup after the unpair write can leave GATT.
|
|
1511
|
+
sendJson(["type": "unpair"], wakeUp: true, requireAck: false)
|
|
1512
|
+
unpairFlushWorkItem?.cancel()
|
|
1513
|
+
unpairFlushPending = true
|
|
1514
|
+
let work = DispatchWorkItem { [self] in
|
|
1515
|
+
self.unpairFlushPending = false
|
|
1516
|
+
self.unpairFlushWorkItem = nil
|
|
1517
|
+
self.destroy()
|
|
1518
|
+
}
|
|
1519
|
+
unpairFlushWorkItem = work
|
|
1520
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: work)
|
|
1354
1521
|
}
|
|
1355
1522
|
|
|
1356
1523
|
var type = "Mentra Live"
|
|
@@ -1428,7 +1595,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1428
1595
|
private var lc3ReadCharacteristic: CBCharacteristic?
|
|
1429
1596
|
private var lc3WriteCharacteristic: CBCharacteristic?
|
|
1430
1597
|
private var supportsLC3Audio = true
|
|
1431
|
-
private var lastReceivedLc3Sequence: Int8 = -1
|
|
1432
1598
|
private let LC3_FRAME_SIZE = 40 // bytes per LC3 frame
|
|
1433
1599
|
private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes in seconds
|
|
1434
1600
|
private var micBeatTimer: Timer?
|
|
@@ -1474,6 +1640,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1474
1640
|
private var centralManager: CBCentralManager?
|
|
1475
1641
|
|
|
1476
1642
|
private var connectedPeripheral: CBPeripheral?
|
|
1643
|
+
/// Peripheral for the in-flight connect; used to ignore stale disconnect/fail callbacks.
|
|
1644
|
+
private var connectingPeripheral: CBPeripheral?
|
|
1477
1645
|
private var txCharacteristic: CBCharacteristic?
|
|
1478
1646
|
private var rxCharacteristic: CBCharacteristic?
|
|
1479
1647
|
private let bes2700MtuLimit = 509
|
|
@@ -1481,8 +1649,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1481
1649
|
|
|
1482
1650
|
// State Tracking
|
|
1483
1651
|
private var isScanning = false
|
|
1652
|
+
private var manualDiscoveryActive = false
|
|
1484
1653
|
private var isConnecting = false
|
|
1485
1654
|
private var isKilled = false
|
|
1655
|
+
/// Glasses opened pairing window — stand down without forgetting identity/bonds.
|
|
1656
|
+
private var pairingYieldActive = false
|
|
1657
|
+
private var pairingYieldAwaitingReclaim = false
|
|
1658
|
+
private var pairingYieldEndWorkItem: DispatchWorkItem?
|
|
1659
|
+
/// Hold GATT teardown until the unpair write can leave the phone.
|
|
1660
|
+
private var unpairFlushPending = false
|
|
1661
|
+
private var unpairFlushWorkItem: DispatchWorkItem?
|
|
1486
1662
|
private var reconnectAttempts = 0
|
|
1487
1663
|
private var isNewVersion = false
|
|
1488
1664
|
private var peerWireProtocolVersion = 0
|
|
@@ -1601,6 +1777,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1601
1777
|
// MARK: - React Native Interface
|
|
1602
1778
|
|
|
1603
1779
|
private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
|
|
1780
|
+
/// Last advertisement pairing trailer for a discovered name. Re-emits (already-
|
|
1781
|
+
/// connected / scan replay) must not drop these flags: omitting them makes a
|
|
1782
|
+
/// secure idle unit look like pairable legacy firmware.
|
|
1783
|
+
private var discoveredAdvPairing = [String: CachedAdvPairing]()
|
|
1784
|
+
|
|
1785
|
+
private struct CachedAdvPairing {
|
|
1786
|
+
let pairingMode: Bool
|
|
1787
|
+
let pairingCode: String?
|
|
1788
|
+
let securePairingCapable: Bool
|
|
1789
|
+
}
|
|
1604
1790
|
|
|
1605
1791
|
func findCompatibleDevices() {
|
|
1606
1792
|
Bridge.log("Finding compatible Mentra Live glasses")
|
|
@@ -1618,11 +1804,95 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1618
1804
|
// clear the saved device name:
|
|
1619
1805
|
UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
|
|
1620
1806
|
|
|
1807
|
+
manualDiscoveryActive = true
|
|
1621
1808
|
startScan()
|
|
1809
|
+
emitConnectedPendingDeviceForPairingScan()
|
|
1622
1810
|
}
|
|
1623
1811
|
}
|
|
1624
1812
|
|
|
1813
|
+
func isPairingYieldActive() -> Bool {
|
|
1814
|
+
pairingYieldActive
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private func enterPairingYield(windowMs: Int) {
|
|
1818
|
+
pairingYieldActive = true
|
|
1819
|
+
pairingYieldAwaitingReclaim = false
|
|
1820
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1821
|
+
if isScanning {
|
|
1822
|
+
stopScan()
|
|
1823
|
+
}
|
|
1824
|
+
// Publish disconnect immediately for Phone A UI. didDisconnectPeripheral may lag
|
|
1825
|
+
// or race with cancelPeripheralConnection; do not wait on it for stand-down.
|
|
1826
|
+
isConnecting = false
|
|
1827
|
+
connected = false
|
|
1828
|
+
fullyBooted = false
|
|
1829
|
+
glassesSessionId = nil
|
|
1830
|
+
readinessCompletedThisBleSession = false
|
|
1831
|
+
rgbLedAuthorityClaimed = false
|
|
1832
|
+
stopAllTimers()
|
|
1833
|
+
closeL2capFileChannel()
|
|
1834
|
+
txCharacteristic = nil
|
|
1835
|
+
rxCharacteristic = nil
|
|
1836
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1837
|
+
if let peripheral = connectedPeripheral {
|
|
1838
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1839
|
+
}
|
|
1840
|
+
connectedPeripheral = nil
|
|
1841
|
+
connectingPeripheral = nil
|
|
1842
|
+
// Stand down for the whole window. Probing GATT during yield races the new
|
|
1843
|
+
// phone for the single BLE slot and is exactly what entering_pairing_mode forbids.
|
|
1844
|
+
|
|
1845
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1846
|
+
guard let self else { return }
|
|
1847
|
+
Bridge.log("LIVE: Pairing yield ended — resume reconnect if still owned")
|
|
1848
|
+
self.pairingYieldActive = false
|
|
1849
|
+
self.pairingYieldAwaitingReclaim = true
|
|
1850
|
+
self.pairingYieldEndWorkItem = nil
|
|
1851
|
+
if !self.isKilled, self.connectedPeripheral == nil {
|
|
1852
|
+
self.handleReconnection()
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
pairingYieldEndWorkItem = work
|
|
1856
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(windowMs), execute: work)
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/// Pairing/auth failures after yield — CoreBluetooth codes, not localized strings.
|
|
1860
|
+
/// Connection timeouts are not owner-loss.
|
|
1861
|
+
private static func isPairingAuthFailure(_ error: Error?) -> Bool {
|
|
1862
|
+
guard let error = error as NSError? else { return false }
|
|
1863
|
+
if error.domain == CBATTErrorDomain {
|
|
1864
|
+
return error.code == CBATTError.insufficientAuthentication.rawValue
|
|
1865
|
+
|| error.code == CBATTError.insufficientEncryption.rawValue
|
|
1866
|
+
}
|
|
1867
|
+
if error.domain == CBErrorDomain {
|
|
1868
|
+
return error.code == CBError.peerRemovedPairingInformation.rawValue
|
|
1869
|
+
}
|
|
1870
|
+
return false
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private func clearSavedGlassesAfterOwnerLoss(reason: String) {
|
|
1874
|
+
Bridge.log("LIVE: Owner loss (\(reason)) — clearing saved glasses")
|
|
1875
|
+
pairingYieldAwaitingReclaim = false
|
|
1876
|
+
pairingYieldActive = false
|
|
1877
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1878
|
+
pairingYieldEndWorkItem = nil
|
|
1879
|
+
forget()
|
|
1880
|
+
Task { @MainActor in
|
|
1881
|
+
DeviceStore.shared.apply("bluetooth", "default_wearable", "")
|
|
1882
|
+
DeviceStore.shared.apply("bluetooth", "device_name", "")
|
|
1883
|
+
DeviceStore.shared.apply("bluetooth", "device_address", "")
|
|
1884
|
+
}
|
|
1885
|
+
Bridge.saveSetting("default_wearable", "")
|
|
1886
|
+
Bridge.saveSetting("device_name", "")
|
|
1887
|
+
Bridge.saveSetting("device_address", "")
|
|
1888
|
+
Bridge.sendTypedMessage("owner_replaced", body: ["reason": reason])
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1625
1891
|
func connectById(_ deviceName: String) {
|
|
1892
|
+
if pairingYieldActive {
|
|
1893
|
+
Bridge.log("LIVE: connectById blocked — pairing yield active")
|
|
1894
|
+
return
|
|
1895
|
+
}
|
|
1626
1896
|
Bridge.log("connectById: \(deviceName)")
|
|
1627
1897
|
// Save the device name for future reconnection
|
|
1628
1898
|
UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
|
|
@@ -1647,7 +1917,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1647
1917
|
{
|
|
1648
1918
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1649
1919
|
discoveredPeripherals[name] = peripheral
|
|
1650
|
-
|
|
1920
|
+
emitDiscoveredDeviceFromCache(name)
|
|
1651
1921
|
|
|
1652
1922
|
// Check if this is the device we want
|
|
1653
1923
|
if let savedDeviceName = UserDefaults.standard.string(
|
|
@@ -1673,6 +1943,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1673
1943
|
}
|
|
1674
1944
|
|
|
1675
1945
|
@objc func disconnect() {
|
|
1946
|
+
if unpairFlushPending {
|
|
1947
|
+
Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
|
|
1948
|
+
return
|
|
1949
|
+
}
|
|
1676
1950
|
Bridge.log("LIVE: disconnect() -Disconnecting from Mentra Live glasses")
|
|
1677
1951
|
|
|
1678
1952
|
// if rgbLedAuthorityClaimed {
|
|
@@ -1698,6 +1972,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1698
1972
|
|
|
1699
1973
|
/// Start the micbeat mechanism to keep LC3 audio streaming active
|
|
1700
1974
|
private func startMicBeat() {
|
|
1975
|
+
// A resumed custom-audio stream starts a new sequence domain.
|
|
1976
|
+
DeviceManager.shared.resetMicSequenceBaseline()
|
|
1701
1977
|
Bridge.log("LIVE: 🎤 Starting micbeat mechanism")
|
|
1702
1978
|
micBeatCount = 0
|
|
1703
1979
|
|
|
@@ -2094,7 +2370,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2094
2370
|
// MARK: - BLE Scanning
|
|
2095
2371
|
|
|
2096
2372
|
private func startScan() {
|
|
2097
|
-
|
|
2373
|
+
if pairingYieldActive {
|
|
2374
|
+
Bridge.log("LIVE: startScan blocked — pairing yield active")
|
|
2375
|
+
return
|
|
2376
|
+
}
|
|
2098
2377
|
|
|
2099
2378
|
guard centralManager!.state == .poweredOn else {
|
|
2100
2379
|
Bridge.log("Attempting to scan but bluetooth is not powered on.")
|
|
@@ -2102,6 +2381,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2102
2381
|
}
|
|
2103
2382
|
|
|
2104
2383
|
Bridge.log("Starting BLE scan for Mentra Live glasses")
|
|
2384
|
+
discoveredAdvPairing.removeAll()
|
|
2105
2385
|
isScanning = true
|
|
2106
2386
|
|
|
2107
2387
|
startReadinessCheckLoop()
|
|
@@ -2118,12 +2398,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2118
2398
|
|
|
2119
2399
|
centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
2120
2400
|
|
|
2121
|
-
// emit already discovered peripherals:
|
|
2122
|
-
for (_, peripheral) in discoveredPeripherals {
|
|
2123
|
-
Bridge.log("LIVE: (Already discovered) peripheral: \(peripheral.name ?? "Unknown")")
|
|
2124
|
-
emitDiscoveredDevice(peripheral.name!)
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
2401
|
// var dName = DeviceManager.shared.deviceName
|
|
2128
2402
|
// if dName.isEmpty {
|
|
2129
2403
|
// dName = "MENTRA_LIVE"
|
|
@@ -2141,6 +2415,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2141
2415
|
}
|
|
2142
2416
|
|
|
2143
2417
|
func stopScan() {
|
|
2418
|
+
manualDiscoveryActive = false
|
|
2144
2419
|
guard isScanning else { return }
|
|
2145
2420
|
|
|
2146
2421
|
centralManager?.stopScan()
|
|
@@ -2152,10 +2427,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2152
2427
|
// MARK: - Connection Management
|
|
2153
2428
|
|
|
2154
2429
|
private func connectToDevice(_ peripheral: CBPeripheral) {
|
|
2430
|
+
if pairingYieldActive {
|
|
2431
|
+
Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
|
|
2432
|
+
isConnecting = false
|
|
2433
|
+
return
|
|
2434
|
+
}
|
|
2155
2435
|
Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
|
|
2156
2436
|
|
|
2157
2437
|
isConnecting = true
|
|
2158
2438
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
2439
|
+
connectingPeripheral = peripheral
|
|
2159
2440
|
connectedPeripheral = peripheral
|
|
2160
2441
|
peripheral.delegate = self
|
|
2161
2442
|
|
|
@@ -2200,6 +2481,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2200
2481
|
return
|
|
2201
2482
|
}
|
|
2202
2483
|
|
|
2484
|
+
if pairingYieldActive {
|
|
2485
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2486
|
+
return
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2203
2489
|
// Check if we've exceeded max attempts
|
|
2204
2490
|
if reconnectAttempts >= MAX_RECONNECT_ATTEMPTS {
|
|
2205
2491
|
Bridge.log("LIVE: Maximum reconnection attempts reached (\(MAX_RECONNECT_ATTEMPTS))")
|
|
@@ -2232,6 +2518,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2232
2518
|
guard let self = self else { return }
|
|
2233
2519
|
|
|
2234
2520
|
// Use peripheral presence, not connectionState: we stay CONNECTING during backoff until scan/connect.
|
|
2521
|
+
if self.pairingYieldActive {
|
|
2522
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2523
|
+
return
|
|
2524
|
+
}
|
|
2235
2525
|
if self.connectedPeripheral == nil, !self.isKilled {
|
|
2236
2526
|
// Check for last known device name to start scan
|
|
2237
2527
|
if let lastDeviceName = UserDefaults.standard.string(
|
|
@@ -2461,11 +2751,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2461
2751
|
|
|
2462
2752
|
switch type {
|
|
2463
2753
|
case "glasses_ready":
|
|
2754
|
+
// A queued glasses_ready from the connection we just cancelled for yield
|
|
2755
|
+
// must not close the pairing window. Only accept readiness on a live GATT.
|
|
2756
|
+
if pairingYieldActive && connectedPeripheral == nil {
|
|
2757
|
+
Bridge.log("LIVE: Ignoring stale glasses_ready during pairing yield")
|
|
2758
|
+
return
|
|
2759
|
+
}
|
|
2464
2760
|
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
2465
2761
|
// onTransportReset() before sending it, so their side is back on legacy
|
|
2466
2762
|
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
2467
2763
|
// wire epoch (clears v2-active state that would otherwise gate the
|
|
2468
2764
|
// handshake off), then negotiate from the caps this message advertises.
|
|
2765
|
+
pairingYieldAwaitingReclaim = false
|
|
2766
|
+
pairingYieldActive = false
|
|
2767
|
+
pairingYieldEndWorkItem?.cancel()
|
|
2768
|
+
pairingYieldEndWorkItem = nil
|
|
2469
2769
|
resetWireNegotiationState()
|
|
2470
2770
|
parsePeerWireCaps(json)
|
|
2471
2771
|
maybeSendWireHandshake()
|
|
@@ -2617,6 +2917,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2617
2917
|
case "pong":
|
|
2618
2918
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
2619
2919
|
|
|
2920
|
+
case "entering_pairing_mode":
|
|
2921
|
+
let windowMs = max(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2922
|
+
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2923
|
+
enterPairingYield(windowMs: windowMs)
|
|
2924
|
+
let body: [String: Any] = [
|
|
2925
|
+
"window_ms": windowMs,
|
|
2926
|
+
"reason": json["reason"] as? String ?? "user_gesture",
|
|
2927
|
+
]
|
|
2928
|
+
Bridge.sendTypedMessage("entering_pairing_mode", body: body)
|
|
2929
|
+
|
|
2930
|
+
case "pairing_info":
|
|
2931
|
+
Bridge.sendPairingInfo(
|
|
2932
|
+
hadPreviousBond: json["had_previous_bond"] as? Bool ?? false,
|
|
2933
|
+
pairingCode: json["pairing_code"] as? String,
|
|
2934
|
+
classicBondReady: json["classic_bond_ready"] as? Bool ?? false,
|
|
2935
|
+
// Legacy firmware that omits this field is not secure-capable.
|
|
2936
|
+
securePairingCapable: json["secure_pairing_capable"] as? Bool ?? false,
|
|
2937
|
+
protocolVersion: json["protocol_version"] as? Int ?? 1
|
|
2938
|
+
)
|
|
2939
|
+
|
|
2620
2940
|
case "imu_response", "imu_stream_response", "imu_gesture_response",
|
|
2621
2941
|
"imu_gesture_subscribed", "imu_ack", "imu_error":
|
|
2622
2942
|
// Handle IMU-related responses
|
|
@@ -2778,9 +3098,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2778
3098
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2779
3099
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2780
3100
|
}
|
|
3101
|
+
if let hotspotOtaVersion = fields["hotspot_ota_version"] as? NSNumber {
|
|
3102
|
+
DeviceStore.shared.apply(
|
|
3103
|
+
"glasses",
|
|
3104
|
+
"hotspotOtaVersion",
|
|
3105
|
+
hotspotOtaVersion.intValue
|
|
3106
|
+
)
|
|
3107
|
+
}
|
|
2781
3108
|
if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
|
|
2782
3109
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2783
3110
|
}
|
|
3111
|
+
if let wifiMacAddress = nonEmptyStringValue(fields, "wifi_mac_address") {
|
|
3112
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", wifiMacAddress)
|
|
3113
|
+
}
|
|
2784
3114
|
if let serialNumber = nonEmptyStringValue(fields, "serial_number") {
|
|
2785
3115
|
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2786
3116
|
}
|
|
@@ -3219,6 +3549,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3219
3549
|
sendJson(json, wakeUp: true)
|
|
3220
3550
|
}
|
|
3221
3551
|
|
|
3552
|
+
func sendWifiAdbState(_ enabled: Bool) {
|
|
3553
|
+
Bridge.log("LIVE: 🔧 Sending Wi-Fi ADB state: \(enabled)")
|
|
3554
|
+
|
|
3555
|
+
let json: [String: Any] = [
|
|
3556
|
+
"type": "set_wifi_adb_state",
|
|
3557
|
+
"enabled": enabled,
|
|
3558
|
+
]
|
|
3559
|
+
|
|
3560
|
+
sendJson(json, wakeUp: true)
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3222
3563
|
func sendSetSystemTime(_ timestampMs: Int64) {
|
|
3223
3564
|
Bridge.log("LIVE: ⏰ Sending set_system_time: \(timestampMs)")
|
|
3224
3565
|
|
|
@@ -3338,7 +3679,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3338
3679
|
{
|
|
3339
3680
|
json["ota_version_url"] = otaVersionUrl
|
|
3340
3681
|
}
|
|
3341
|
-
|
|
3342
3682
|
sendJson(json, wakeUp: true)
|
|
3343
3683
|
}
|
|
3344
3684
|
|
|
@@ -3509,17 +3849,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3509
3849
|
return
|
|
3510
3850
|
}
|
|
3511
3851
|
|
|
3512
|
-
let sequenceNumber =
|
|
3852
|
+
let sequenceNumber = Int(data[1])
|
|
3513
3853
|
let lc3Data = data.subdata(in: 2 ..< data.count)
|
|
3514
3854
|
|
|
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
3855
|
// // Decode LC3 to PCM using existing PcmConverter
|
|
3524
3856
|
// let pcmConverter = PcmConverter()
|
|
3525
3857
|
// guard let pcmData = pcmConverter.decode(lc3Data) as? Data, pcmData.count > 0 else {
|
|
@@ -3533,7 +3865,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3533
3865
|
// Bridge.log(
|
|
3534
3866
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count) bytes"
|
|
3535
3867
|
// )
|
|
3536
|
-
DeviceManager.shared.handleGlassesMicData(lc3Data, 40)
|
|
3868
|
+
DeviceManager.shared.handleGlassesMicData(lc3Data, 40, sequenceNumber: sequenceNumber)
|
|
3537
3869
|
|
|
3538
3870
|
// Bridge.log(
|
|
3539
3871
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count)→\(pcmData.count) bytes"
|
|
@@ -5201,8 +5533,82 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5201
5533
|
|
|
5202
5534
|
// MARK: - Event Emission
|
|
5203
5535
|
|
|
5204
|
-
|
|
5205
|
-
|
|
5536
|
+
/// A selected pairing target can stop advertising once GATT connects, before readiness
|
|
5537
|
+
/// promotes it to the default device. Re-emit only that explicit pending target; an
|
|
5538
|
+
/// established owner's connected glasses have no pending identity and remain hidden.
|
|
5539
|
+
private func emitConnectedPendingDeviceForPairingScan() {
|
|
5540
|
+
guard manualDiscoveryActive, !pairingYieldActive, let peripheral = connectedPeripheral,
|
|
5541
|
+
let name = peripheral.name,
|
|
5542
|
+
MentraLivePendingPairingTarget.shouldRecover(
|
|
5543
|
+
isConnected: peripheral.state == .connected,
|
|
5544
|
+
connectedName: name,
|
|
5545
|
+
connectedIdentifier: peripheral.identifier.uuidString,
|
|
5546
|
+
pendingName: DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "",
|
|
5547
|
+
pendingIdentifier: DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? ""
|
|
5548
|
+
)
|
|
5549
|
+
else {
|
|
5550
|
+
return
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
Bridge.log("LIVE: Pairing scan: recovering connected pending target \(name) (\(peripheral.identifier))")
|
|
5554
|
+
emitDiscoveredDevice(
|
|
5555
|
+
name,
|
|
5556
|
+
identifier: peripheral.identifier.uuidString,
|
|
5557
|
+
pairingMode: true,
|
|
5558
|
+
pairingCode: nil,
|
|
5559
|
+
securePairingCapable: DeviceStore.shared.get(
|
|
5560
|
+
"bluetooth",
|
|
5561
|
+
"pending_device_secure_pairing_capable"
|
|
5562
|
+
) as? Bool
|
|
5563
|
+
)
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
private func cacheAdvPairing(
|
|
5567
|
+
_ name: String,
|
|
5568
|
+
pairingMode: Bool,
|
|
5569
|
+
pairingCode: String?,
|
|
5570
|
+
securePairingCapable: Bool
|
|
5571
|
+
) {
|
|
5572
|
+
discoveredAdvPairing[name] = CachedAdvPairing(
|
|
5573
|
+
pairingMode: pairingMode,
|
|
5574
|
+
pairingCode: pairingCode,
|
|
5575
|
+
securePairingCapable: securePairingCapable
|
|
5576
|
+
)
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
private func emitDiscoveredDeviceFromCache(
|
|
5580
|
+
_ name: String,
|
|
5581
|
+
identifier: String = "",
|
|
5582
|
+
rssi: Int? = nil
|
|
5583
|
+
) {
|
|
5584
|
+
let cached = discoveredAdvPairing[name]
|
|
5585
|
+
emitDiscoveredDevice(
|
|
5586
|
+
name,
|
|
5587
|
+
identifier: identifier,
|
|
5588
|
+
rssi: rssi,
|
|
5589
|
+
pairingMode: cached?.pairingMode,
|
|
5590
|
+
pairingCode: cached?.pairingCode,
|
|
5591
|
+
securePairingCapable: cached?.securePairingCapable
|
|
5592
|
+
)
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5595
|
+
private func emitDiscoveredDevice(
|
|
5596
|
+
_ name: String,
|
|
5597
|
+
identifier: String = "",
|
|
5598
|
+
rssi: Int? = nil,
|
|
5599
|
+
pairingMode: Bool? = nil,
|
|
5600
|
+
pairingCode: String? = nil,
|
|
5601
|
+
securePairingCapable: Bool? = nil
|
|
5602
|
+
) {
|
|
5603
|
+
Bridge.sendDiscoveredDevice(
|
|
5604
|
+
"Mentra Live",
|
|
5605
|
+
name,
|
|
5606
|
+
deviceAddress: identifier,
|
|
5607
|
+
rssi: rssi,
|
|
5608
|
+
pairingMode: pairingMode,
|
|
5609
|
+
pairingCode: pairingCode,
|
|
5610
|
+
securePairingCapable: securePairingCapable
|
|
5611
|
+
)
|
|
5206
5612
|
}
|
|
5207
5613
|
|
|
5208
5614
|
private func emitStopScanEvent() {
|
|
@@ -5291,6 +5697,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5291
5697
|
Bridge.log("Destroying MentraLiveManager")
|
|
5292
5698
|
|
|
5293
5699
|
isKilled = true
|
|
5700
|
+
unpairFlushWorkItem?.cancel()
|
|
5701
|
+
unpairFlushWorkItem = nil
|
|
5702
|
+
unpairFlushPending = false
|
|
5294
5703
|
|
|
5295
5704
|
// Stop scanning
|
|
5296
5705
|
if isScanning {
|
|
@@ -5324,6 +5733,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5324
5733
|
DeviceStore.shared.apply("glasses", "hotspotGatewayIp", "")
|
|
5325
5734
|
|
|
5326
5735
|
connectedPeripheral = nil
|
|
5736
|
+
discoveredPeripherals.removeAll()
|
|
5737
|
+
discoveredAdvPairing.removeAll()
|
|
5327
5738
|
centralManager?.delegate = nil
|
|
5328
5739
|
centralManager = nil
|
|
5329
5740
|
|
|
@@ -6533,6 +6944,17 @@ extension MentraLive {
|
|
|
6533
6944
|
stopVideoRecording(requestId: requestId, webhookUrl: nil, authToken: nil)
|
|
6534
6945
|
}
|
|
6535
6946
|
|
|
6947
|
+
func queryVideoRecordingStatus(requestId: String) {
|
|
6948
|
+
guard connectionState == ConnTypes.CONNECTED else {
|
|
6949
|
+
Bridge.log("Cannot query video recording status - not connected")
|
|
6950
|
+
return
|
|
6951
|
+
}
|
|
6952
|
+
sendJson([
|
|
6953
|
+
"type": "get_video_recording_status",
|
|
6954
|
+
"requestId": requestId,
|
|
6955
|
+
])
|
|
6956
|
+
}
|
|
6957
|
+
|
|
6536
6958
|
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
6537
6959
|
Bridge.log(
|
|
6538
6960
|
"Stopping video recording on glasses: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|