@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.7
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 +38 -2
- 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 +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +198 -17
- 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 +1174 -191
- 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/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +61 -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 +47 -3
- package/ios/Source/DeviceManager.swift +135 -10
- 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 +439 -30
- 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/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 +72 -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,10 +932,10 @@ 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
|
|
|
@@ -939,10 +984,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
939
984
|
}
|
|
940
985
|
|
|
941
986
|
nonisolated func centralManager(
|
|
942
|
-
_: CBCentralManager, didDisconnectPeripheral
|
|
987
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
|
|
943
988
|
) {
|
|
944
989
|
DispatchQueue.main.async { [weak self] in
|
|
945
990
|
guard let self else { return }
|
|
991
|
+
let isCurrent =
|
|
992
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
993
|
+
if !isCurrent {
|
|
994
|
+
Bridge.log("LIVE: Ignoring stale disconnect for \(peripheral.identifier)")
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
self.connectingPeripheral = nil
|
|
946
998
|
Bridge.log("LIVE: Disconnected from GATT server")
|
|
947
999
|
|
|
948
1000
|
self.isConnecting = false
|
|
@@ -962,6 +1014,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
962
1014
|
self.txCharacteristic = nil
|
|
963
1015
|
self.rxCharacteristic = nil
|
|
964
1016
|
|
|
1017
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1018
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1019
|
+
return
|
|
1020
|
+
}
|
|
1021
|
+
|
|
965
1022
|
// Attempt reconnection if not killed
|
|
966
1023
|
if !self.isKilled {
|
|
967
1024
|
self.handleReconnection()
|
|
@@ -969,10 +1026,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
969
1026
|
}
|
|
970
1027
|
}
|
|
971
1028
|
|
|
972
|
-
nonisolated func centralManager(_: CBCentralManager, didFailToConnect
|
|
1029
|
+
nonisolated func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
973
1030
|
let errorDescription = error?.localizedDescription ?? "Unknown error"
|
|
974
1031
|
DispatchQueue.main.async { [weak self] in
|
|
975
1032
|
guard let self else { return }
|
|
1033
|
+
let isCurrent =
|
|
1034
|
+
self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
|
|
1035
|
+
if !isCurrent {
|
|
1036
|
+
Bridge.log("LIVE: Ignoring stale connect failure for \(peripheral.identifier)")
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1039
|
+
self.connectingPeripheral = nil
|
|
976
1040
|
Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
|
|
977
1041
|
|
|
978
1042
|
self.stopConnectionTimeout()
|
|
@@ -981,6 +1045,11 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
981
1045
|
self.connectedPeripheral = nil
|
|
982
1046
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
983
1047
|
|
|
1048
|
+
if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
|
|
1049
|
+
self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
|
|
1050
|
+
return
|
|
1051
|
+
}
|
|
1052
|
+
|
|
984
1053
|
if !self.isKilled {
|
|
985
1054
|
self.handleReconnection()
|
|
986
1055
|
}
|
|
@@ -1276,6 +1345,83 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1276
1345
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1277
1346
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1278
1347
|
private static let loudnessGateSwitchType = 10
|
|
1348
|
+
private let mentraManufacturerId: UInt16 = 0xB822
|
|
1349
|
+
// Payload-relative offset of the pairing flag, matching Android's index into the
|
|
1350
|
+
// company-id-stripped manufacturer data from getManufacturerSpecificData().
|
|
1351
|
+
private let advManufPairingFlagOffset = 5
|
|
1352
|
+
// CoreBluetooth returns manufacturer data with the 2-byte company id prefix still attached,
|
|
1353
|
+
// whereas Android strips it. Skip the prefix so both platforms read the same payload byte.
|
|
1354
|
+
private let advManufCompanyIdLength = 2
|
|
1355
|
+
private let advPairingDiscoverable: UInt8 = 0x01
|
|
1356
|
+
|
|
1357
|
+
// CoreBluetooth returns manufacturer data prefixed with the 2-byte company id
|
|
1358
|
+
// (little-endian) and does NOT filter by company id itself (unlike Android's
|
|
1359
|
+
// getManufacturerSpecificData(companyId)), so every reader of this data must
|
|
1360
|
+
// verify the company id before trusting any flag/trailer byte.
|
|
1361
|
+
private func mentraManufacturerData(_ advertisementData: [String: Any]) -> Data? {
|
|
1362
|
+
guard let manufData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
1363
|
+
manufData.count >= advManufCompanyIdLength
|
|
1364
|
+
else {
|
|
1365
|
+
return nil
|
|
1366
|
+
}
|
|
1367
|
+
let companyId = UInt16(manufData[0]) | (UInt16(manufData[1]) << 8)
|
|
1368
|
+
guard companyId == mentraManufacturerId else {
|
|
1369
|
+
return nil
|
|
1370
|
+
}
|
|
1371
|
+
return manufData
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/// OS-1615 ads append `flag | version | capability | code_lo | code_hi` after the
|
|
1375
|
+
/// connected byte. Field firmware uses the same 0xB822 company id but then writes
|
|
1376
|
+
/// the XOR'd Classic MAC at those offsets. Length alone is not a pairing flag.
|
|
1377
|
+
private func hasSecurePairingTrailer(_ advertisementData: [String: Any]) -> Bool {
|
|
1378
|
+
let flagIndex = advManufCompanyIdLength + advManufPairingFlagOffset
|
|
1379
|
+
let trailerBase = flagIndex + 1
|
|
1380
|
+
guard let manufData = mentraManufacturerData(advertisementData),
|
|
1381
|
+
manufData.count >= trailerBase + 4
|
|
1382
|
+
else {
|
|
1383
|
+
return false
|
|
1384
|
+
}
|
|
1385
|
+
let version = Int(manufData[trailerBase])
|
|
1386
|
+
let capability = Int(manufData[trailerBase + 1])
|
|
1387
|
+
return (1...15).contains(version) && (capability & 0x01) != 0
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private func isPairingDiscoverable(_ advertisementData: [String: Any]) -> Bool {
|
|
1391
|
+
guard hasSecurePairingTrailer(advertisementData),
|
|
1392
|
+
let manufData = mentraManufacturerData(advertisementData)
|
|
1393
|
+
else {
|
|
1394
|
+
return false
|
|
1395
|
+
}
|
|
1396
|
+
let flagIndex = advManufCompanyIdLength + advManufPairingFlagOffset
|
|
1397
|
+
return manufData[flagIndex] == advPairingDiscoverable
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
private struct SecurePairingTrailer {
|
|
1401
|
+
let pairingMode: Bool
|
|
1402
|
+
let pairingCode: String?
|
|
1403
|
+
let secureCapable: Bool
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/// Trailer immediately after pairing flag: version | capability | code_lo | code_hi
|
|
1407
|
+
private func parseSecurePairingTrailer(_ advertisementData: [String: Any]) -> SecurePairingTrailer {
|
|
1408
|
+
guard hasSecurePairingTrailer(advertisementData),
|
|
1409
|
+
let manufData = mentraManufacturerData(advertisementData)
|
|
1410
|
+
else {
|
|
1411
|
+
return SecurePairingTrailer(pairingMode: false, pairingCode: nil, secureCapable: false)
|
|
1412
|
+
}
|
|
1413
|
+
let flagIndex = advManufCompanyIdLength + advManufPairingFlagOffset
|
|
1414
|
+
let trailerBase = flagIndex + 1
|
|
1415
|
+
let pairingMode = manufData[flagIndex] == advPairingDiscoverable
|
|
1416
|
+
let codeLo = Int(manufData[trailerBase + 2])
|
|
1417
|
+
let codeHi = Int(manufData[trailerBase + 3])
|
|
1418
|
+
let code = String(format: "%02X%02X", codeHi, codeLo)
|
|
1419
|
+
return SecurePairingTrailer(pairingMode: pairingMode, pairingCode: code, secureCapable: true)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private func advertisesPairingFlag(_ advertisementData: [String: Any]) -> Bool {
|
|
1423
|
+
hasSecurePairingTrailer(advertisementData)
|
|
1424
|
+
}
|
|
1279
1425
|
|
|
1280
1426
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
1281
1427
|
|
|
@@ -1290,6 +1436,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1290
1436
|
// return, unlike Android).
|
|
1291
1437
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1292
1438
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1439
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
|
|
1440
|
+
DeviceStore.shared.apply("glasses", "hotspotOtaVersion", 0)
|
|
1293
1441
|
}
|
|
1294
1442
|
connectionState = state
|
|
1295
1443
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
@@ -1343,14 +1491,26 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1343
1491
|
func forget() {
|
|
1344
1492
|
Bridge.log("LIVE: Forgetting Mentra Live glasses")
|
|
1345
1493
|
|
|
1494
|
+
// Clear saved device name so the pairing filter is not bypassed on rescan
|
|
1495
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
|
|
1496
|
+
|
|
1346
1497
|
// Stop scanning first
|
|
1347
1498
|
if isScanning {
|
|
1348
1499
|
stopScan()
|
|
1349
1500
|
emitStopScanEvent()
|
|
1350
1501
|
}
|
|
1351
1502
|
|
|
1352
|
-
// Then do full cleanup
|
|
1353
|
-
|
|
1503
|
+
// Then do full cleanup after the unpair write can leave GATT.
|
|
1504
|
+
sendJson(["type": "unpair"], wakeUp: true, requireAck: false)
|
|
1505
|
+
unpairFlushWorkItem?.cancel()
|
|
1506
|
+
unpairFlushPending = true
|
|
1507
|
+
let work = DispatchWorkItem { [self] in
|
|
1508
|
+
self.unpairFlushPending = false
|
|
1509
|
+
self.unpairFlushWorkItem = nil
|
|
1510
|
+
self.destroy()
|
|
1511
|
+
}
|
|
1512
|
+
unpairFlushWorkItem = work
|
|
1513
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: work)
|
|
1354
1514
|
}
|
|
1355
1515
|
|
|
1356
1516
|
var type = "Mentra Live"
|
|
@@ -1428,7 +1588,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1428
1588
|
private var lc3ReadCharacteristic: CBCharacteristic?
|
|
1429
1589
|
private var lc3WriteCharacteristic: CBCharacteristic?
|
|
1430
1590
|
private var supportsLC3Audio = true
|
|
1431
|
-
private var lastReceivedLc3Sequence: Int8 = -1
|
|
1432
1591
|
private let LC3_FRAME_SIZE = 40 // bytes per LC3 frame
|
|
1433
1592
|
private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes in seconds
|
|
1434
1593
|
private var micBeatTimer: Timer?
|
|
@@ -1474,6 +1633,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1474
1633
|
private var centralManager: CBCentralManager?
|
|
1475
1634
|
|
|
1476
1635
|
private var connectedPeripheral: CBPeripheral?
|
|
1636
|
+
/// Peripheral for the in-flight connect; used to ignore stale disconnect/fail callbacks.
|
|
1637
|
+
private var connectingPeripheral: CBPeripheral?
|
|
1477
1638
|
private var txCharacteristic: CBCharacteristic?
|
|
1478
1639
|
private var rxCharacteristic: CBCharacteristic?
|
|
1479
1640
|
private let bes2700MtuLimit = 509
|
|
@@ -1483,6 +1644,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1483
1644
|
private var isScanning = false
|
|
1484
1645
|
private var isConnecting = false
|
|
1485
1646
|
private var isKilled = false
|
|
1647
|
+
/// Glasses opened pairing window — stand down without forgetting identity/bonds.
|
|
1648
|
+
private var pairingYieldActive = false
|
|
1649
|
+
private var pairingYieldAwaitingReclaim = false
|
|
1650
|
+
private var pairingYieldEndWorkItem: DispatchWorkItem?
|
|
1651
|
+
/// Hold GATT teardown until the unpair write can leave the phone.
|
|
1652
|
+
private var unpairFlushPending = false
|
|
1653
|
+
private var unpairFlushWorkItem: DispatchWorkItem?
|
|
1486
1654
|
private var reconnectAttempts = 0
|
|
1487
1655
|
private var isNewVersion = false
|
|
1488
1656
|
private var peerWireProtocolVersion = 0
|
|
@@ -1601,6 +1769,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1601
1769
|
// MARK: - React Native Interface
|
|
1602
1770
|
|
|
1603
1771
|
private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
|
|
1772
|
+
/// Last advertisement pairing trailer for a discovered name. Re-emits (already-
|
|
1773
|
+
/// connected / scan replay) must not drop these flags: omitting them makes a
|
|
1774
|
+
/// secure idle unit look like pairable legacy firmware.
|
|
1775
|
+
private var discoveredAdvPairing = [String: CachedAdvPairing]()
|
|
1776
|
+
|
|
1777
|
+
private struct CachedAdvPairing {
|
|
1778
|
+
let pairingMode: Bool
|
|
1779
|
+
let pairingCode: String?
|
|
1780
|
+
let securePairingCapable: Bool
|
|
1781
|
+
}
|
|
1604
1782
|
|
|
1605
1783
|
func findCompatibleDevices() {
|
|
1606
1784
|
Bridge.log("Finding compatible Mentra Live glasses")
|
|
@@ -1622,7 +1800,89 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1622
1800
|
}
|
|
1623
1801
|
}
|
|
1624
1802
|
|
|
1803
|
+
func isPairingYieldActive() -> Bool {
|
|
1804
|
+
pairingYieldActive
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
private func enterPairingYield(windowMs: Int) {
|
|
1808
|
+
pairingYieldActive = true
|
|
1809
|
+
pairingYieldAwaitingReclaim = false
|
|
1810
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1811
|
+
if isScanning {
|
|
1812
|
+
stopScan()
|
|
1813
|
+
}
|
|
1814
|
+
// Publish disconnect immediately for Phone A UI. didDisconnectPeripheral may lag
|
|
1815
|
+
// or race with cancelPeripheralConnection; do not wait on it for stand-down.
|
|
1816
|
+
isConnecting = false
|
|
1817
|
+
connected = false
|
|
1818
|
+
fullyBooted = false
|
|
1819
|
+
glassesSessionId = nil
|
|
1820
|
+
readinessCompletedThisBleSession = false
|
|
1821
|
+
rgbLedAuthorityClaimed = false
|
|
1822
|
+
stopAllTimers()
|
|
1823
|
+
closeL2capFileChannel()
|
|
1824
|
+
txCharacteristic = nil
|
|
1825
|
+
rxCharacteristic = nil
|
|
1826
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1827
|
+
if let peripheral = connectedPeripheral {
|
|
1828
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1829
|
+
}
|
|
1830
|
+
connectedPeripheral = nil
|
|
1831
|
+
connectingPeripheral = nil
|
|
1832
|
+
// Stand down for the whole window. Probing GATT during yield races the new
|
|
1833
|
+
// phone for the single BLE slot and is exactly what entering_pairing_mode forbids.
|
|
1834
|
+
|
|
1835
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1836
|
+
guard let self else { return }
|
|
1837
|
+
Bridge.log("LIVE: Pairing yield ended — resume reconnect if still owned")
|
|
1838
|
+
self.pairingYieldActive = false
|
|
1839
|
+
self.pairingYieldAwaitingReclaim = true
|
|
1840
|
+
self.pairingYieldEndWorkItem = nil
|
|
1841
|
+
if !self.isKilled, self.connectedPeripheral == nil {
|
|
1842
|
+
self.handleReconnection()
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
pairingYieldEndWorkItem = work
|
|
1846
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(windowMs), execute: work)
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/// Pairing/auth failures after yield — CoreBluetooth codes, not localized strings.
|
|
1850
|
+
/// Connection timeouts are not owner-loss.
|
|
1851
|
+
private static func isPairingAuthFailure(_ error: Error?) -> Bool {
|
|
1852
|
+
guard let error = error as NSError? else { return false }
|
|
1853
|
+
if error.domain == CBATTErrorDomain {
|
|
1854
|
+
return error.code == CBATTError.insufficientAuthentication.rawValue
|
|
1855
|
+
|| error.code == CBATTError.insufficientEncryption.rawValue
|
|
1856
|
+
}
|
|
1857
|
+
if error.domain == CBErrorDomain {
|
|
1858
|
+
return error.code == CBError.peerRemovedPairingInformation.rawValue
|
|
1859
|
+
}
|
|
1860
|
+
return false
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private func clearSavedGlassesAfterOwnerLoss(reason: String) {
|
|
1864
|
+
Bridge.log("LIVE: Owner loss (\(reason)) — clearing saved glasses")
|
|
1865
|
+
pairingYieldAwaitingReclaim = false
|
|
1866
|
+
pairingYieldActive = false
|
|
1867
|
+
pairingYieldEndWorkItem?.cancel()
|
|
1868
|
+
pairingYieldEndWorkItem = nil
|
|
1869
|
+
forget()
|
|
1870
|
+
Task { @MainActor in
|
|
1871
|
+
DeviceStore.shared.apply("bluetooth", "default_wearable", "")
|
|
1872
|
+
DeviceStore.shared.apply("bluetooth", "device_name", "")
|
|
1873
|
+
DeviceStore.shared.apply("bluetooth", "device_address", "")
|
|
1874
|
+
}
|
|
1875
|
+
Bridge.saveSetting("default_wearable", "")
|
|
1876
|
+
Bridge.saveSetting("device_name", "")
|
|
1877
|
+
Bridge.saveSetting("device_address", "")
|
|
1878
|
+
Bridge.sendTypedMessage("owner_replaced", body: ["reason": reason])
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1625
1881
|
func connectById(_ deviceName: String) {
|
|
1882
|
+
if pairingYieldActive {
|
|
1883
|
+
Bridge.log("LIVE: connectById blocked — pairing yield active")
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1626
1886
|
Bridge.log("connectById: \(deviceName)")
|
|
1627
1887
|
// Save the device name for future reconnection
|
|
1628
1888
|
UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
|
|
@@ -1647,7 +1907,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1647
1907
|
{
|
|
1648
1908
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1649
1909
|
discoveredPeripherals[name] = peripheral
|
|
1650
|
-
|
|
1910
|
+
emitDiscoveredDeviceFromCache(name)
|
|
1651
1911
|
|
|
1652
1912
|
// Check if this is the device we want
|
|
1653
1913
|
if let savedDeviceName = UserDefaults.standard.string(
|
|
@@ -1673,6 +1933,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1673
1933
|
}
|
|
1674
1934
|
|
|
1675
1935
|
@objc func disconnect() {
|
|
1936
|
+
if unpairFlushPending {
|
|
1937
|
+
Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
|
|
1938
|
+
return
|
|
1939
|
+
}
|
|
1676
1940
|
Bridge.log("LIVE: disconnect() -Disconnecting from Mentra Live glasses")
|
|
1677
1941
|
|
|
1678
1942
|
// if rgbLedAuthorityClaimed {
|
|
@@ -1698,6 +1962,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1698
1962
|
|
|
1699
1963
|
/// Start the micbeat mechanism to keep LC3 audio streaming active
|
|
1700
1964
|
private func startMicBeat() {
|
|
1965
|
+
// A resumed custom-audio stream starts a new sequence domain.
|
|
1966
|
+
DeviceManager.shared.resetMicSequenceBaseline()
|
|
1701
1967
|
Bridge.log("LIVE: 🎤 Starting micbeat mechanism")
|
|
1702
1968
|
micBeatCount = 0
|
|
1703
1969
|
|
|
@@ -2094,7 +2360,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2094
2360
|
// MARK: - BLE Scanning
|
|
2095
2361
|
|
|
2096
2362
|
private func startScan() {
|
|
2097
|
-
|
|
2363
|
+
if pairingYieldActive {
|
|
2364
|
+
Bridge.log("LIVE: startScan blocked — pairing yield active")
|
|
2365
|
+
return
|
|
2366
|
+
}
|
|
2098
2367
|
|
|
2099
2368
|
guard centralManager!.state == .poweredOn else {
|
|
2100
2369
|
Bridge.log("Attempting to scan but bluetooth is not powered on.")
|
|
@@ -2102,6 +2371,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2102
2371
|
}
|
|
2103
2372
|
|
|
2104
2373
|
Bridge.log("Starting BLE scan for Mentra Live glasses")
|
|
2374
|
+
discoveredAdvPairing.removeAll()
|
|
2105
2375
|
isScanning = true
|
|
2106
2376
|
|
|
2107
2377
|
startReadinessCheckLoop()
|
|
@@ -2118,11 +2388,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2118
2388
|
|
|
2119
2389
|
centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
2120
2390
|
|
|
2121
|
-
//
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
emitDiscoveredDevice(peripheral.name!)
|
|
2125
|
-
}
|
|
2391
|
+
// Fresh advertisements refill pairing metadata. Re-emitting the last scan's
|
|
2392
|
+
// cache would keep a unit pairable after it left pairing mode.
|
|
2393
|
+
emitConnectedDeviceForPairingScan()
|
|
2126
2394
|
|
|
2127
2395
|
// var dName = DeviceManager.shared.deviceName
|
|
2128
2396
|
// if dName.isEmpty {
|
|
@@ -2152,10 +2420,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2152
2420
|
// MARK: - Connection Management
|
|
2153
2421
|
|
|
2154
2422
|
private func connectToDevice(_ peripheral: CBPeripheral) {
|
|
2423
|
+
if pairingYieldActive {
|
|
2424
|
+
Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
|
|
2425
|
+
isConnecting = false
|
|
2426
|
+
return
|
|
2427
|
+
}
|
|
2155
2428
|
Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
|
|
2156
2429
|
|
|
2157
2430
|
isConnecting = true
|
|
2158
2431
|
updateConnectionState(ConnTypes.CONNECTING)
|
|
2432
|
+
connectingPeripheral = peripheral
|
|
2159
2433
|
connectedPeripheral = peripheral
|
|
2160
2434
|
peripheral.delegate = self
|
|
2161
2435
|
|
|
@@ -2200,6 +2474,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2200
2474
|
return
|
|
2201
2475
|
}
|
|
2202
2476
|
|
|
2477
|
+
if pairingYieldActive {
|
|
2478
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2479
|
+
return
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2203
2482
|
// Check if we've exceeded max attempts
|
|
2204
2483
|
if reconnectAttempts >= MAX_RECONNECT_ATTEMPTS {
|
|
2205
2484
|
Bridge.log("LIVE: Maximum reconnection attempts reached (\(MAX_RECONNECT_ATTEMPTS))")
|
|
@@ -2232,6 +2511,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2232
2511
|
guard let self = self else { return }
|
|
2233
2512
|
|
|
2234
2513
|
// Use peripheral presence, not connectionState: we stay CONNECTING during backoff until scan/connect.
|
|
2514
|
+
if self.pairingYieldActive {
|
|
2515
|
+
Bridge.log("LIVE: Reconnection aborted - pairing yield active")
|
|
2516
|
+
return
|
|
2517
|
+
}
|
|
2235
2518
|
if self.connectedPeripheral == nil, !self.isKilled {
|
|
2236
2519
|
// Check for last known device name to start scan
|
|
2237
2520
|
if let lastDeviceName = UserDefaults.standard.string(
|
|
@@ -2461,11 +2744,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2461
2744
|
|
|
2462
2745
|
switch type {
|
|
2463
2746
|
case "glasses_ready":
|
|
2747
|
+
// A queued glasses_ready from the connection we just cancelled for yield
|
|
2748
|
+
// must not close the pairing window. Only accept readiness on a live GATT.
|
|
2749
|
+
if pairingYieldActive && connectedPeripheral == nil {
|
|
2750
|
+
Bridge.log("LIVE: Ignoring stale glasses_ready during pairing yield")
|
|
2751
|
+
return
|
|
2752
|
+
}
|
|
2464
2753
|
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
2465
2754
|
// onTransportReset() before sending it, so their side is back on legacy
|
|
2466
2755
|
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
2467
2756
|
// wire epoch (clears v2-active state that would otherwise gate the
|
|
2468
2757
|
// handshake off), then negotiate from the caps this message advertises.
|
|
2758
|
+
pairingYieldAwaitingReclaim = false
|
|
2759
|
+
pairingYieldActive = false
|
|
2760
|
+
pairingYieldEndWorkItem?.cancel()
|
|
2761
|
+
pairingYieldEndWorkItem = nil
|
|
2469
2762
|
resetWireNegotiationState()
|
|
2470
2763
|
parsePeerWireCaps(json)
|
|
2471
2764
|
maybeSendWireHandshake()
|
|
@@ -2617,6 +2910,31 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2617
2910
|
case "pong":
|
|
2618
2911
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
2619
2912
|
|
|
2913
|
+
case "entering_pairing_mode":
|
|
2914
|
+
let windowMs = max(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2915
|
+
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2916
|
+
enterPairingYield(windowMs: windowMs)
|
|
2917
|
+
var body: [String: Any] = [
|
|
2918
|
+
"window_ms": windowMs,
|
|
2919
|
+
"reason": json["reason"] as? String ?? "user_gesture",
|
|
2920
|
+
]
|
|
2921
|
+
if let txn = json["txn"] {
|
|
2922
|
+
body["txn"] = txn
|
|
2923
|
+
}
|
|
2924
|
+
Bridge.sendTypedMessage("entering_pairing_mode", body: body)
|
|
2925
|
+
|
|
2926
|
+
case "pairing_info":
|
|
2927
|
+
Bridge.sendPairingInfo(
|
|
2928
|
+
hadPreviousBond: json["had_previous_bond"] as? Bool ?? false,
|
|
2929
|
+
transferId: json["transfer_id"] as? String,
|
|
2930
|
+
pairingCode: json["pairing_code"] as? String,
|
|
2931
|
+
classicBondReady: json["classic_bond_ready"] as? Bool ?? false,
|
|
2932
|
+
// Legacy firmware that omits this field is not secure-capable.
|
|
2933
|
+
securePairingCapable: json["secure_pairing_capable"] as? Bool ?? false,
|
|
2934
|
+
protocolVersion: json["protocol_version"] as? Int ?? 1,
|
|
2935
|
+
binding: json["binding"] as? String
|
|
2936
|
+
)
|
|
2937
|
+
|
|
2620
2938
|
case "imu_response", "imu_stream_response", "imu_gesture_response",
|
|
2621
2939
|
"imu_gesture_subscribed", "imu_ack", "imu_error":
|
|
2622
2940
|
// Handle IMU-related responses
|
|
@@ -2778,9 +3096,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2778
3096
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2779
3097
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2780
3098
|
}
|
|
3099
|
+
if let hotspotOtaVersion = fields["hotspot_ota_version"] as? NSNumber {
|
|
3100
|
+
DeviceStore.shared.apply(
|
|
3101
|
+
"glasses",
|
|
3102
|
+
"hotspotOtaVersion",
|
|
3103
|
+
hotspotOtaVersion.intValue
|
|
3104
|
+
)
|
|
3105
|
+
}
|
|
2781
3106
|
if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
|
|
2782
3107
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2783
3108
|
}
|
|
3109
|
+
if let wifiMacAddress = nonEmptyStringValue(fields, "wifi_mac_address") {
|
|
3110
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", wifiMacAddress)
|
|
3111
|
+
}
|
|
2784
3112
|
if let serialNumber = nonEmptyStringValue(fields, "serial_number") {
|
|
2785
3113
|
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2786
3114
|
}
|
|
@@ -3219,6 +3547,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3219
3547
|
sendJson(json, wakeUp: true)
|
|
3220
3548
|
}
|
|
3221
3549
|
|
|
3550
|
+
func sendWifiAdbState(_ enabled: Bool) {
|
|
3551
|
+
Bridge.log("LIVE: 🔧 Sending Wi-Fi ADB state: \(enabled)")
|
|
3552
|
+
|
|
3553
|
+
let json: [String: Any] = [
|
|
3554
|
+
"type": "set_wifi_adb_state",
|
|
3555
|
+
"enabled": enabled,
|
|
3556
|
+
]
|
|
3557
|
+
|
|
3558
|
+
sendJson(json, wakeUp: true)
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3222
3561
|
func sendSetSystemTime(_ timestampMs: Int64) {
|
|
3223
3562
|
Bridge.log("LIVE: ⏰ Sending set_system_time: \(timestampMs)")
|
|
3224
3563
|
|
|
@@ -3338,7 +3677,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3338
3677
|
{
|
|
3339
3678
|
json["ota_version_url"] = otaVersionUrl
|
|
3340
3679
|
}
|
|
3341
|
-
|
|
3342
3680
|
sendJson(json, wakeUp: true)
|
|
3343
3681
|
}
|
|
3344
3682
|
|
|
@@ -3509,17 +3847,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3509
3847
|
return
|
|
3510
3848
|
}
|
|
3511
3849
|
|
|
3512
|
-
let sequenceNumber =
|
|
3850
|
+
let sequenceNumber = Int(data[1])
|
|
3513
3851
|
let lc3Data = data.subdata(in: 2 ..< data.count)
|
|
3514
3852
|
|
|
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
3853
|
// // Decode LC3 to PCM using existing PcmConverter
|
|
3524
3854
|
// let pcmConverter = PcmConverter()
|
|
3525
3855
|
// guard let pcmData = pcmConverter.decode(lc3Data) as? Data, pcmData.count > 0 else {
|
|
@@ -3533,7 +3863,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3533
3863
|
// Bridge.log(
|
|
3534
3864
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count) bytes"
|
|
3535
3865
|
// )
|
|
3536
|
-
DeviceManager.shared.handleGlassesMicData(lc3Data, 40)
|
|
3866
|
+
DeviceManager.shared.handleGlassesMicData(lc3Data, 40, sequenceNumber: sequenceNumber)
|
|
3537
3867
|
|
|
3538
3868
|
// Bridge.log(
|
|
3539
3869
|
// "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count)→\(pcmData.count) bytes"
|
|
@@ -5201,8 +5531,71 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5201
5531
|
|
|
5202
5532
|
// MARK: - Event Emission
|
|
5203
5533
|
|
|
5204
|
-
|
|
5205
|
-
|
|
5534
|
+
/// Pairing scan listens for advertisements. A unit that is already GATT-connected
|
|
5535
|
+
/// has stopped ADV, so emit it as pairable or the scan list stays empty.
|
|
5536
|
+
private func emitConnectedDeviceForPairingScan() {
|
|
5537
|
+
guard connected, let peripheral = connectedPeripheral, let name = peripheral.name,
|
|
5538
|
+
name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
5539
|
+
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
5540
|
+
else {
|
|
5541
|
+
return
|
|
5542
|
+
}
|
|
5543
|
+
Bridge.log("LIVE: Pairing scan: already GATT-connected to \(name) — emitting as pairable (ADV off while connected)")
|
|
5544
|
+
emitDiscoveredDevice(
|
|
5545
|
+
name,
|
|
5546
|
+
identifier: peripheral.identifier.uuidString,
|
|
5547
|
+
pairingMode: true,
|
|
5548
|
+
pairingCode: nil,
|
|
5549
|
+
securePairingCapable: discoveredAdvPairing[name]?.securePairingCapable ?? false
|
|
5550
|
+
)
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
private func cacheAdvPairing(
|
|
5554
|
+
_ name: String,
|
|
5555
|
+
pairingMode: Bool,
|
|
5556
|
+
pairingCode: String?,
|
|
5557
|
+
securePairingCapable: Bool
|
|
5558
|
+
) {
|
|
5559
|
+
discoveredAdvPairing[name] = CachedAdvPairing(
|
|
5560
|
+
pairingMode: pairingMode,
|
|
5561
|
+
pairingCode: pairingCode,
|
|
5562
|
+
securePairingCapable: securePairingCapable
|
|
5563
|
+
)
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
private func emitDiscoveredDeviceFromCache(
|
|
5567
|
+
_ name: String,
|
|
5568
|
+
identifier: String = "",
|
|
5569
|
+
rssi: Int? = nil
|
|
5570
|
+
) {
|
|
5571
|
+
let cached = discoveredAdvPairing[name]
|
|
5572
|
+
emitDiscoveredDevice(
|
|
5573
|
+
name,
|
|
5574
|
+
identifier: identifier,
|
|
5575
|
+
rssi: rssi,
|
|
5576
|
+
pairingMode: cached?.pairingMode,
|
|
5577
|
+
pairingCode: cached?.pairingCode,
|
|
5578
|
+
securePairingCapable: cached?.securePairingCapable
|
|
5579
|
+
)
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
private func emitDiscoveredDevice(
|
|
5583
|
+
_ name: String,
|
|
5584
|
+
identifier: String = "",
|
|
5585
|
+
rssi: Int? = nil,
|
|
5586
|
+
pairingMode: Bool? = nil,
|
|
5587
|
+
pairingCode: String? = nil,
|
|
5588
|
+
securePairingCapable: Bool? = nil
|
|
5589
|
+
) {
|
|
5590
|
+
Bridge.sendDiscoveredDevice(
|
|
5591
|
+
"Mentra Live",
|
|
5592
|
+
name,
|
|
5593
|
+
deviceAddress: identifier,
|
|
5594
|
+
rssi: rssi,
|
|
5595
|
+
pairingMode: pairingMode,
|
|
5596
|
+
pairingCode: pairingCode,
|
|
5597
|
+
securePairingCapable: securePairingCapable
|
|
5598
|
+
)
|
|
5206
5599
|
}
|
|
5207
5600
|
|
|
5208
5601
|
private func emitStopScanEvent() {
|
|
@@ -5291,6 +5684,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5291
5684
|
Bridge.log("Destroying MentraLiveManager")
|
|
5292
5685
|
|
|
5293
5686
|
isKilled = true
|
|
5687
|
+
unpairFlushWorkItem?.cancel()
|
|
5688
|
+
unpairFlushWorkItem = nil
|
|
5689
|
+
unpairFlushPending = false
|
|
5294
5690
|
|
|
5295
5691
|
// Stop scanning
|
|
5296
5692
|
if isScanning {
|
|
@@ -5324,6 +5720,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5324
5720
|
DeviceStore.shared.apply("glasses", "hotspotGatewayIp", "")
|
|
5325
5721
|
|
|
5326
5722
|
connectedPeripheral = nil
|
|
5723
|
+
discoveredPeripherals.removeAll()
|
|
5724
|
+
discoveredAdvPairing.removeAll()
|
|
5327
5725
|
centralManager?.delegate = nil
|
|
5328
5726
|
centralManager = nil
|
|
5329
5727
|
|
|
@@ -6533,6 +6931,17 @@ extension MentraLive {
|
|
|
6533
6931
|
stopVideoRecording(requestId: requestId, webhookUrl: nil, authToken: nil)
|
|
6534
6932
|
}
|
|
6535
6933
|
|
|
6934
|
+
func queryVideoRecordingStatus(requestId: String) {
|
|
6935
|
+
guard connectionState == ConnTypes.CONNECTED else {
|
|
6936
|
+
Bridge.log("Cannot query video recording status - not connected")
|
|
6937
|
+
return
|
|
6938
|
+
}
|
|
6939
|
+
sendJson([
|
|
6940
|
+
"type": "get_video_recording_status",
|
|
6941
|
+
"requestId": requestId,
|
|
6942
|
+
])
|
|
6943
|
+
}
|
|
6944
|
+
|
|
6536
6945
|
func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
6537
6946
|
Bridge.log(
|
|
6538
6947
|
"Stopping video recording on glasses: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|