@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +169 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +72 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +127 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +400 -99
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +18 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +342 -42
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +52 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +45 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/build/BluetoothSdk.types.d.ts +100 -22
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +2 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +3 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +3 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +31 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +26 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +7 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +20 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +28 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +113 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +87 -21
- package/ios/Source/DeviceManager.swift +118 -20
- package/ios/Source/DeviceStore.swift +4 -0
- package/ios/Source/MentraBluetoothSDK.swift +326 -33
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +26 -6
- package/ios/Source/internal/BleTraceLogger.swift +1 -1
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +478 -61
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +13 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +68 -2
- package/ios/Source/tts/TTSTools.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +13 -0
- package/ios/Source/utils/BleJsonCompact.swift +8 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +106 -24
- package/src/_internal.ts +3 -0
- package/src/_private/BluetoothSdkModule.ts +83 -18
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +9 -2
- package/src/_private/photoRequestPayload.ts +13 -4
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +13 -1
|
@@ -163,7 +163,7 @@ class BlePhotoUploadService {
|
|
|
163
163
|
let bytes = [UInt8](data.prefix(12))
|
|
164
164
|
if bytes.count >= 2, bytes[0] == 0xFF, bytes[1] == 0xD8 { return "jpeg" }
|
|
165
165
|
if bytes.count >= 12, bytes[4] == 0x66, bytes[5] == 0x74, bytes[6] == 0x79, bytes[7] == 0x70 {
|
|
166
|
-
let brand = String(bytes: bytes[8..<12], encoding: .ascii) ?? "?"
|
|
166
|
+
let brand = String(bytes: bytes[8 ..< 12], encoding: .ascii) ?? "?"
|
|
167
167
|
return "iso_bmff/ftyp=\(brand)"
|
|
168
168
|
}
|
|
169
169
|
return "unknown"
|
|
@@ -173,8 +173,8 @@ class BlePhotoUploadService {
|
|
|
173
173
|
let marker: [UInt8] = [0x45, 0x78, 0x69, 0x66, 0, 0]
|
|
174
174
|
let bytes = [UInt8](data)
|
|
175
175
|
guard bytes.count >= marker.count else { return false }
|
|
176
|
-
for i in 0...(bytes.count - marker.count) {
|
|
177
|
-
if Array(bytes[i..<(i + marker.count)]) == marker { return true }
|
|
176
|
+
for i in 0 ... (bytes.count - marker.count) {
|
|
177
|
+
if Array(bytes[i ..< (i + marker.count)]) == marker { return true }
|
|
178
178
|
}
|
|
179
179
|
return false
|
|
180
180
|
}
|
|
@@ -182,7 +182,7 @@ class BlePhotoUploadService {
|
|
|
182
182
|
private static func readImuJsonFromImageData(_ imageData: Data) -> String? {
|
|
183
183
|
// Primary: ImageIO EXIF (works for JPEG and well-formed AVIF)
|
|
184
184
|
if let source = CGImageSourceCreateWithData(imageData as CFData, nil),
|
|
185
|
-
|
|
185
|
+
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
|
|
186
186
|
{
|
|
187
187
|
let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any]
|
|
188
188
|
let userComment = exif?[kCGImagePropertyExifUserComment as String] as? String
|
|
@@ -223,8 +223,8 @@ class BlePhotoUploadService {
|
|
|
223
223
|
guard magic == 0x002A else { searchFrom = exifOff + 1; continue }
|
|
224
224
|
let ifd0Off = Int(readU32(bytes, at: tiff + 4, le: littleEndian))
|
|
225
225
|
if let result = readTagFromIfd(
|
|
226
|
-
bytes, tiff: tiff, ifdOff: tiff + ifd0Off, tag: 0x9286, le: littleEndian
|
|
227
|
-
{
|
|
226
|
+
bytes, tiff: tiff, ifdOff: tiff + ifd0Off, tag: 0x9286, le: littleEndian
|
|
227
|
+
) {
|
|
228
228
|
return result
|
|
229
229
|
}
|
|
230
230
|
searchFrom = exifOff + 1
|
|
@@ -238,7 +238,7 @@ class BlePhotoUploadService {
|
|
|
238
238
|
guard ifdOff + 2 <= bytes.count else { return nil }
|
|
239
239
|
let count = Int(readU16(bytes, at: ifdOff, le: le))
|
|
240
240
|
var off = ifdOff + 2
|
|
241
|
-
for _ in 0..<count {
|
|
241
|
+
for _ in 0 ..< count {
|
|
242
242
|
guard off + 12 <= bytes.count else { break }
|
|
243
243
|
let entryTag = readU16(bytes, at: off, le: le)
|
|
244
244
|
let type = readU16(bytes, at: off + 2, le: le)
|
|
@@ -255,7 +255,7 @@ class BlePhotoUploadService {
|
|
|
255
255
|
let startOff = (tag == 0x9286 && byteLen > 8) ? valueOff + 8 : valueOff
|
|
256
256
|
let len = (tag == 0x9286 && byteLen > 8) ? byteLen - 8 : byteLen
|
|
257
257
|
guard startOff + len <= bytes.count else { return nil }
|
|
258
|
-
return String(bytes: Array(bytes[startOff..<startOff + len]), encoding: .utf8)?
|
|
258
|
+
return String(bytes: Array(bytes[startOff ..< startOff + len]), encoding: .utf8)?
|
|
259
259
|
.trimmingCharacters(in: .init(charactersIn: "\0"))
|
|
260
260
|
}
|
|
261
261
|
// Follow Exif IFD pointer
|
|
@@ -272,8 +272,8 @@ class BlePhotoUploadService {
|
|
|
272
272
|
|
|
273
273
|
private static func findBytes(_ needle: [UInt8], in haystack: [UInt8], from: Int) -> Int? {
|
|
274
274
|
guard haystack.count >= needle.count else { return nil }
|
|
275
|
-
for i in from...(haystack.count - needle.count) {
|
|
276
|
-
if Array(haystack[i..<i + needle.count]) == needle { return i }
|
|
275
|
+
for i in from ... (haystack.count - needle.count) {
|
|
276
|
+
if Array(haystack[i ..< i + needle.count]) == needle { return i }
|
|
277
277
|
}
|
|
278
278
|
return nil
|
|
279
279
|
}
|
|
@@ -298,8 +298,8 @@ class BlePhotoUploadService {
|
|
|
298
298
|
|
|
299
299
|
private static func writeImuJsonToJpegData(_ jpegData: Data, imuJson: String) throws -> Data {
|
|
300
300
|
guard let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
let imageType = CGImageSourceGetType(source),
|
|
302
|
+
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)
|
|
303
303
|
else {
|
|
304
304
|
throw PhotoUploadError.decodingFailed
|
|
305
305
|
}
|
|
@@ -373,8 +373,8 @@ class BlePhotoUploadService {
|
|
|
373
373
|
|
|
374
374
|
// Find last Exif marker
|
|
375
375
|
var lastExif = -1
|
|
376
|
-
for i in 0...(bytes.count - marker.count) {
|
|
377
|
-
if Array(bytes[i..<i + marker.count]) == marker { lastExif = i }
|
|
376
|
+
for i in 0 ... (bytes.count - marker.count) {
|
|
377
|
+
if Array(bytes[i ..< i + marker.count]) == marker { lastExif = i }
|
|
378
378
|
}
|
|
379
379
|
guard lastExif >= 0 else { return data }
|
|
380
380
|
|
|
@@ -383,7 +383,7 @@ class BlePhotoUploadService {
|
|
|
383
383
|
while off + 8 <= bytes.count {
|
|
384
384
|
let boxSize = Int(readU32BE(bytes, at: off))
|
|
385
385
|
guard boxSize >= 8, off + boxSize <= bytes.count else { break }
|
|
386
|
-
let boxType = String(bytes: Array(bytes[off + 4..<off + 8]), encoding: .ascii) ?? ""
|
|
386
|
+
let boxType = String(bytes: Array(bytes[off + 4 ..< off + 8]), encoding: .ascii) ?? ""
|
|
387
387
|
if boxType == "mdat" {
|
|
388
388
|
let payloadStart = off + 8
|
|
389
389
|
let payloadEnd = off + boxSize
|
|
@@ -393,7 +393,7 @@ class BlePhotoUploadService {
|
|
|
393
393
|
|
|
394
394
|
var result = Data()
|
|
395
395
|
// Everything up to mdat header
|
|
396
|
-
result.append(contentsOf: bytes[0..<off])
|
|
396
|
+
result.append(contentsOf: bytes[0 ..< off])
|
|
397
397
|
// New mdat box header with updated size
|
|
398
398
|
let newMdatSize = UInt32(8 + newPayloadLen)
|
|
399
399
|
result.append(UInt8((newMdatSize >> 24) & 0xFF))
|
|
@@ -401,7 +401,7 @@ class BlePhotoUploadService {
|
|
|
401
401
|
result.append(UInt8((newMdatSize >> 8) & 0xFF))
|
|
402
402
|
result.append(UInt8(newMdatSize & 0xFF))
|
|
403
403
|
result.append(contentsOf: [0x6D, 0x64, 0x61, 0x74]) // "mdat"
|
|
404
|
-
result.append(contentsOf: bytes[payloadStart..<payloadStart + newPayloadLen])
|
|
404
|
+
result.append(contentsOf: bytes[payloadStart ..< payloadStart + newPayloadLen])
|
|
405
405
|
return result
|
|
406
406
|
}
|
|
407
407
|
off += boxSize
|
|
@@ -824,7 +824,7 @@ private final class BleIncidentLogRelayEntry {
|
|
|
824
824
|
// MARK: - CBCentralManagerDelegate
|
|
825
825
|
|
|
826
826
|
extension MentraLive: CBCentralManagerDelegate {
|
|
827
|
-
|
|
827
|
+
/// CoreBluetooth delivers these callbacks on `bluetoothQueue`; hop back before touching MainActor state.
|
|
828
828
|
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
829
829
|
let state = central.state
|
|
830
830
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -926,6 +926,18 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
926
926
|
}
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
+
nonisolated func centralManager(
|
|
930
|
+
_: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral
|
|
931
|
+
) {
|
|
932
|
+
DispatchQueue.main.async { [weak self] in
|
|
933
|
+
guard let self, peripheral === self.connectedPeripheral else { return }
|
|
934
|
+
Bridge.log(
|
|
935
|
+
"LIVE: ANCS authorization updated: \(peripheral.ancsAuthorized ? "authorized" : "not authorized")"
|
|
936
|
+
)
|
|
937
|
+
self.enableAncsRelayIfAuthorized()
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
929
941
|
nonisolated func centralManager(
|
|
930
942
|
_: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error _: Error?
|
|
931
943
|
) {
|
|
@@ -938,6 +950,8 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
938
950
|
self.connectedPeripheral = nil
|
|
939
951
|
self.fullyBooted = false
|
|
940
952
|
self.connected = false
|
|
953
|
+
self.glassesSessionId = nil // Fresh BLE session starts with no sid known
|
|
954
|
+
self.readinessCompletedThisBleSession = false
|
|
941
955
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
942
956
|
self.rgbLedAuthorityClaimed = false
|
|
943
957
|
|
|
@@ -1095,6 +1109,10 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
1095
1109
|
Bridge.log("LIVE: 🎤 Enabled LC3 audio notifications")
|
|
1096
1110
|
}
|
|
1097
1111
|
|
|
1112
|
+
// Authorization may complete before or after service discovery.
|
|
1113
|
+
// This covers the former; the central-manager callback covers the latter.
|
|
1114
|
+
self.enableAncsRelayIfAuthorized()
|
|
1115
|
+
|
|
1098
1116
|
// Start readiness check loop
|
|
1099
1117
|
self.startSignalStrengthPolling()
|
|
1100
1118
|
self.startReadinessCheckLoop()
|
|
@@ -1213,6 +1231,7 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
1213
1231
|
|
|
1214
1232
|
if uuid == self.RX_CHAR_UUID, isNotifying {
|
|
1215
1233
|
Bridge.log("LIVE: 🔔 Ready to receive data via notifications")
|
|
1234
|
+
self.enableAncsRelayIfAuthorized()
|
|
1216
1235
|
}
|
|
1217
1236
|
}
|
|
1218
1237
|
}
|
|
@@ -1256,25 +1275,33 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1256
1275
|
// to avoid overloading the MCU. Set to false to allow simultaneous A2DP + LC3 mic.
|
|
1257
1276
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1258
1277
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1278
|
+
private static let loudnessGateSwitchType = 10
|
|
1259
1279
|
|
|
1260
1280
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
1261
1281
|
|
|
1262
1282
|
/// Mirrors Android `updateConnectionState` — RN home reads `glasses.connectionState` for reconnecting UI.
|
|
1263
1283
|
private func updateConnectionState(_ state: String) {
|
|
1284
|
+
if state == ConnTypes.DISCONNECTED {
|
|
1285
|
+
// A manufacturing serial is session-bound. Clear it on disconnect so a previous
|
|
1286
|
+
// pair's serial cannot be associated with the next connection. Connect must NOT
|
|
1287
|
+
// clear it: DeviceManager.disconnect already wipes it before any new connection, and
|
|
1288
|
+
// clearing on CONNECTED would wipe a still-valid serial mid-session when a same-link
|
|
1289
|
+
// glasses_ready (e.g. ASG restart) re-publishes CONNECTED (iOS has no equal-state
|
|
1290
|
+
// early return, unlike Android).
|
|
1291
|
+
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1292
|
+
}
|
|
1264
1293
|
connectionState = state
|
|
1265
1294
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
1266
1295
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state from
|
|
1267
1296
|
// a previous pairing into the next one (would otherwise surface as wrong overall_percent
|
|
1268
1297
|
// or stale lastBesOtaProgress on the next OTA).
|
|
1269
1298
|
if state == ConnTypes.DISCONNECTED {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
peerFilePayloadV2 = false
|
|
1277
|
-
BleJsonCompact.resetSession()
|
|
1299
|
+
resetWireNegotiationState()
|
|
1300
|
+
resetAncsRelayState()
|
|
1301
|
+
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
1302
|
+
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
1303
|
+
// negotiated) is the bug class this clear removes.
|
|
1304
|
+
Task { await commandQueue.removeAll() }
|
|
1278
1305
|
stopSignalStrengthPolling()
|
|
1279
1306
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1280
1307
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -1298,6 +1325,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1298
1325
|
func sendText(_ text: String) async {
|
|
1299
1326
|
await sendTextWall(text)
|
|
1300
1327
|
}
|
|
1328
|
+
|
|
1301
1329
|
func sendTextWall(_: String) async {}
|
|
1302
1330
|
func ping() {
|
|
1303
1331
|
Bridge.log("LIVE: ping()")
|
|
@@ -1410,6 +1438,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1410
1438
|
// So we temporarily suspend the LC3 mic during phone audio playback
|
|
1411
1439
|
private var micIntentEnabled = false // User/system WANTS mic enabled
|
|
1412
1440
|
private var micSuspendedForAudio = false // Mic temporarily suspended due to phone audio
|
|
1441
|
+
var isMicSuspendedForAudio: Bool {
|
|
1442
|
+
micSuspendedForAudio
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1413
1445
|
private var phoneAudioMonitor: PhoneAudioMonitor?
|
|
1414
1446
|
|
|
1415
1447
|
// Timing Constants
|
|
@@ -1453,12 +1485,36 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1453
1485
|
private var peerWireProtocolVersion = 0
|
|
1454
1486
|
private var useBinaryWireProtocol = false
|
|
1455
1487
|
private var wireHandshakeQueued = false
|
|
1488
|
+
private var wireHandshakeAttempts = 0
|
|
1489
|
+
/// Session generation in which the LAST outgoing v2 handshake was sent; a reply only
|
|
1490
|
+
/// activates v2 when it answers a handshake from the CURRENT epoch (see PendingMessage
|
|
1491
|
+
/// .generation for the queue-side guard).
|
|
1492
|
+
private var wireHandshakeSentGeneration = -1
|
|
1493
|
+
/// Bumped on every BLE session reset; scheduled handshake-retry callbacks capture it
|
|
1494
|
+
/// and no-op if the session changed, so a timer from a dead session can't poke a new one.
|
|
1495
|
+
private var wireSessionGeneration = 0
|
|
1496
|
+
// Grace period before an unanswered BLE wire v2 handshake is re-armed for retry,
|
|
1497
|
+
// and the per-session cap on automatic retries (see sendWireHandshake).
|
|
1498
|
+
private static let wireHandshakeRetryGraceSeconds: TimeInterval = 5
|
|
1499
|
+
private static let wireHandshakeMaxAttempts = 3
|
|
1456
1500
|
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to legacy
|
|
1457
1501
|
// big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le (or a
|
|
1458
1502
|
// v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
1459
1503
|
private var peerK900Le = false
|
|
1504
|
+
private var ancsAppIdentifiers: [UInt32: String] = [:]
|
|
1505
|
+
private var ancsRelayEnableRequested = false
|
|
1460
1506
|
private var peerWireCapsBinary = false
|
|
1461
1507
|
private var peerFilePayloadV2 = false
|
|
1508
|
+
/// Last observed glasses process session id (`sid` in glasses_ready / version_info_1).
|
|
1509
|
+
/// The BES keeps the BLE link alive across asg_client restarts, so transport state
|
|
1510
|
+
/// cannot signal a restart - a CHANGED (or newly appearing) sid is the restart signal.
|
|
1511
|
+
/// Nil = no sid observed this BLE session (legacy glasses, or none seen yet).
|
|
1512
|
+
private var glassesSessionId: String?
|
|
1513
|
+
// True once a glasses_ready completed on THIS physical BLE session. Unlike
|
|
1514
|
+
// fullyBooted, this never flaps on sr_hrt ready=0 heartbeats — it only resets with
|
|
1515
|
+
// the physical connection — so a first-seen sid after an upgrade OTA cannot be
|
|
1516
|
+
// mistaken for a fresh pairing just because a not-ready heartbeat slipped in first.
|
|
1517
|
+
private var readinessCompletedThisBleSession = false
|
|
1462
1518
|
private var globalMessageId = 0
|
|
1463
1519
|
private var lastReceivedMessageId = 0
|
|
1464
1520
|
private var bleWriteTraceSequence = 0
|
|
@@ -1705,7 +1761,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1705
1761
|
let bleImgId =
|
|
1706
1762
|
"I" + String(format: "%09d", Int(Date().timeIntervalSince1970 * 1000) % 100_000_000)
|
|
1707
1763
|
json["bleImgId"] = bleImgId
|
|
1708
|
-
json["transferMethod"] =
|
|
1764
|
+
json["transferMethod"] = request.transferMethod
|
|
1709
1765
|
|
|
1710
1766
|
if let webhookUrl = request.webhookUrl, !webhookUrl.isEmpty {
|
|
1711
1767
|
json["webhookUrl"] = webhookUrl
|
|
@@ -1746,7 +1802,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1746
1802
|
}
|
|
1747
1803
|
request.appendScanFields(to: &json)
|
|
1748
1804
|
|
|
1749
|
-
Bridge.log(
|
|
1805
|
+
Bridge.log(
|
|
1806
|
+
"LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=\(request.transferMethod)"
|
|
1807
|
+
)
|
|
1750
1808
|
Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
|
|
1751
1809
|
|
|
1752
1810
|
sendJson(json, wakeUp: true)
|
|
@@ -1755,11 +1813,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1755
1813
|
func warmUpCamera(
|
|
1756
1814
|
requestId: String,
|
|
1757
1815
|
size: PhotoSize,
|
|
1816
|
+
mode: PhotoMode = .photo,
|
|
1758
1817
|
exposureTimeNs: Double?,
|
|
1759
|
-
durationMs: Int
|
|
1818
|
+
durationMs: Int,
|
|
1819
|
+
zsl: Bool? = nil,
|
|
1820
|
+
mfnr: Bool? = nil
|
|
1760
1821
|
) {
|
|
1761
1822
|
Bridge.log(
|
|
1762
|
-
"LIVE: warmUpCamera() entry requestId=\(requestId) size=\(size.rawValue) durationMs=\(durationMs)"
|
|
1823
|
+
"LIVE: warmUpCamera() entry requestId=\(requestId) size=\(size.rawValue) mode=\(mode.rawValue) durationMs=\(durationMs)"
|
|
1763
1824
|
)
|
|
1764
1825
|
|
|
1765
1826
|
let allowedSizes = ["low", "medium", "high", "max"]
|
|
@@ -1768,16 +1829,30 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1768
1829
|
"type": "camera_warm_up",
|
|
1769
1830
|
"requestId": requestId,
|
|
1770
1831
|
"size": allowedSizes.contains(sizeRaw) ? sizeRaw : "medium",
|
|
1832
|
+
"mode": mode.rawValue,
|
|
1771
1833
|
"durationMs": durationMs > 0 ? durationMs : 15000,
|
|
1772
1834
|
]
|
|
1773
1835
|
|
|
1774
1836
|
if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
|
|
1775
1837
|
json["exposureTimeNs"] = Int64(e)
|
|
1776
1838
|
}
|
|
1839
|
+
if let mfnr {
|
|
1840
|
+
json["mfnr"] = mfnr
|
|
1841
|
+
}
|
|
1842
|
+
if let zsl {
|
|
1843
|
+
json["zsl"] = zsl
|
|
1844
|
+
}
|
|
1777
1845
|
|
|
1778
1846
|
sendJson(json, wakeUp: true)
|
|
1779
1847
|
}
|
|
1780
1848
|
|
|
1849
|
+
func stopCameraWarmUp(requestId: String) {
|
|
1850
|
+
sendJson(
|
|
1851
|
+
["type": "camera_warm_up_stop", "requestId": requestId],
|
|
1852
|
+
wakeUp: true
|
|
1853
|
+
)
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1781
1856
|
func startStream(_ message: [String: Any]) {
|
|
1782
1857
|
Bridge.log("Starting stream")
|
|
1783
1858
|
var json = message
|
|
@@ -1819,17 +1894,24 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1819
1894
|
// MARK: - Command Queue
|
|
1820
1895
|
|
|
1821
1896
|
class PendingMessage {
|
|
1822
|
-
init(data: Data, id: String, retries: Int, trace: BleWriteTrace? = nil) {
|
|
1897
|
+
init(data: Data, id: String, retries: Int, trace: BleWriteTrace? = nil, generation: Int = 0) {
|
|
1823
1898
|
self.data = data
|
|
1824
1899
|
self.id = id
|
|
1825
1900
|
self.retries = retries
|
|
1826
1901
|
self.trace = trace
|
|
1902
|
+
self.generation = generation
|
|
1827
1903
|
}
|
|
1828
1904
|
|
|
1829
1905
|
let data: Data
|
|
1830
1906
|
let retries: Int
|
|
1831
1907
|
let id: String
|
|
1832
1908
|
let trace: BleWriteTrace?
|
|
1909
|
+
/// Wire-session generation at enqueue time; the drain loop drops entries from a
|
|
1910
|
+
/// previous session so a stale write (worst case: an old handshake whose reply
|
|
1911
|
+
/// would activate v2 pre-negotiation) can never cross a session boundary. This is
|
|
1912
|
+
/// the deterministic guard - the async removeAll() on disconnect is best-effort
|
|
1913
|
+
/// cleanup that may land after a reconnect has already resumed the drain loop.
|
|
1914
|
+
let generation: Int
|
|
1833
1915
|
}
|
|
1834
1916
|
|
|
1835
1917
|
struct OutgoingBleCommandTraceInfo {
|
|
@@ -1875,6 +1957,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1875
1957
|
guard !commands.isEmpty else { return nil }
|
|
1876
1958
|
return commands.removeFirst()
|
|
1877
1959
|
}
|
|
1960
|
+
|
|
1961
|
+
func removeAll() {
|
|
1962
|
+
commands.removeAll()
|
|
1963
|
+
}
|
|
1878
1964
|
}
|
|
1879
1965
|
|
|
1880
1966
|
private func setupCommandQueue() {
|
|
@@ -1884,7 +1970,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1884
1970
|
let pendingIsNil = await MainActor.run { self.pending == nil }
|
|
1885
1971
|
if pendingIsNil {
|
|
1886
1972
|
if let command = await self.commandQueue.dequeue() {
|
|
1887
|
-
await self.
|
|
1973
|
+
let currentGeneration = await MainActor.run { self.wireSessionGeneration }
|
|
1974
|
+
if command.generation != currentGeneration {
|
|
1975
|
+
Bridge.log("LIVE: Dropping stale queued write from a previous session epoch (id: \(command.id))")
|
|
1976
|
+
} else {
|
|
1977
|
+
await self.processSendQueue(command)
|
|
1978
|
+
}
|
|
1888
1979
|
}
|
|
1889
1980
|
}
|
|
1890
1981
|
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
|
@@ -1960,7 +2051,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1960
2051
|
data: pendingMessage.data,
|
|
1961
2052
|
id: pendingMessage.id,
|
|
1962
2053
|
retries: pendingMessage.retries + 1,
|
|
1963
|
-
trace: pendingMessage.trace
|
|
2054
|
+
trace: pendingMessage.trace,
|
|
2055
|
+
generation: pendingMessage.generation
|
|
1964
2056
|
)
|
|
1965
2057
|
|
|
1966
2058
|
// Push to front of queue for immediate retry
|
|
@@ -2054,7 +2146,36 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2054
2146
|
// Set connection timeout
|
|
2055
2147
|
startConnectionTimeout()
|
|
2056
2148
|
|
|
2057
|
-
|
|
2149
|
+
// ANCS is hosted by iOS and is only exposed to authorized accessories.
|
|
2150
|
+
// Requiring it at connect time lets the system complete that authorization
|
|
2151
|
+
// flow before the glasses subscribe to the ANCS characteristics.
|
|
2152
|
+
centralManager?.connect(
|
|
2153
|
+
peripheral,
|
|
2154
|
+
options: [CBConnectPeripheralOptionRequiresANCS: true]
|
|
2155
|
+
)
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/// Opt the firmware into ANCS only after iOS has authorized this accessory.
|
|
2159
|
+
/// Old firmware safely ignores the command, while new firmware stays inert
|
|
2160
|
+
/// for old mobile clients that never send it.
|
|
2161
|
+
private func enableAncsRelayIfAuthorized() {
|
|
2162
|
+
guard !ancsRelayEnableRequested,
|
|
2163
|
+
let peripheral = connectedPeripheral,
|
|
2164
|
+
txCharacteristic != nil,
|
|
2165
|
+
rxCharacteristic?.isNotifying == true,
|
|
2166
|
+
peripheral.ancsAuthorized
|
|
2167
|
+
else {
|
|
2168
|
+
return
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
let command: [String: Any] = [
|
|
2172
|
+
"C": "cs_ancs",
|
|
2173
|
+
"B": ["enabled": 1],
|
|
2174
|
+
]
|
|
2175
|
+
if sendRawK900Command(command) {
|
|
2176
|
+
ancsRelayEnableRequested = true
|
|
2177
|
+
Bridge.log("LIVE: Requested ANCS relay from compatible firmware")
|
|
2178
|
+
}
|
|
2058
2179
|
}
|
|
2059
2180
|
|
|
2060
2181
|
private func handleReconnection() {
|
|
@@ -2070,6 +2191,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2070
2191
|
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
2071
2192
|
connected = false
|
|
2072
2193
|
fullyBooted = false
|
|
2194
|
+
glassesSessionId = nil
|
|
2195
|
+
readinessCompletedThisBleSession = false
|
|
2196
|
+
readinessCompletedThisBleSession = false // Fresh BLE session starts with no sid known
|
|
2197
|
+
readinessCompletedThisBleSession = false
|
|
2073
2198
|
return
|
|
2074
2199
|
}
|
|
2075
2200
|
|
|
@@ -2320,7 +2445,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2320
2445
|
|
|
2321
2446
|
switch type {
|
|
2322
2447
|
case "glasses_ready":
|
|
2448
|
+
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
2449
|
+
// onTransportReset() before sending it, so their side is back on legacy
|
|
2450
|
+
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
2451
|
+
// wire epoch (clears v2-active state that would otherwise gate the
|
|
2452
|
+
// handshake off), then negotiate from the caps this message advertises.
|
|
2453
|
+
resetWireNegotiationState()
|
|
2323
2454
|
parsePeerWireCaps(json)
|
|
2455
|
+
maybeSendWireHandshake()
|
|
2456
|
+
// Record the session id BEFORE marking ready: an unsolicited glasses_ready
|
|
2457
|
+
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
2458
|
+
// is correct here; version_info detection covers the restart case.
|
|
2459
|
+
if let sid = json["sid"] as? String, !sid.isEmpty { glassesSessionId = sid }
|
|
2460
|
+
readinessCompletedThisBleSession = true
|
|
2324
2461
|
handleGlassesReady()
|
|
2325
2462
|
|
|
2326
2463
|
case "battery_status":
|
|
@@ -2355,7 +2492,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2355
2492
|
} else if connected {
|
|
2356
2493
|
DeviceStore.shared.apply("glasses", "wifiError", "")
|
|
2357
2494
|
}
|
|
2358
|
-
updateWifiStatus(connected: connected, ssid: ssid, ip: ip)
|
|
2495
|
+
updateWifiStatus(connected: connected, ssid: ssid, ip: ip, error: wifiError.isEmpty ? nil : wifiError)
|
|
2359
2496
|
|
|
2360
2497
|
case "hotspot_status_update":
|
|
2361
2498
|
let enabled = json["hotspot_enabled"] as? Bool ?? false
|
|
@@ -2582,7 +2719,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2582
2719
|
default:
|
|
2583
2720
|
// Flexible version_info parsing - handle any version_info* message
|
|
2584
2721
|
if type.hasPrefix("version_info") {
|
|
2585
|
-
Bridge.log("LIVE: Received \(type)
|
|
2722
|
+
Bridge.log("LIVE: Received \(type)")
|
|
2586
2723
|
|
|
2587
2724
|
// Extract all fields from JSON (except "type")
|
|
2588
2725
|
var fields: [String: Any] = [:]
|
|
@@ -2599,8 +2736,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2599
2736
|
if let buildNumber = fields["build_number"] as? String {
|
|
2600
2737
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
2601
2738
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
2602
|
-
parsePeerWireCaps(json)
|
|
2603
|
-
maybeSendWireHandshake()
|
|
2604
2739
|
}
|
|
2605
2740
|
if let deviceModel = fields["device_model"] as? String {
|
|
2606
2741
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -2628,10 +2763,24 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2628
2763
|
if let bluetoothMacAddress = fields["bt_mac_address"] as? String {
|
|
2629
2764
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2630
2765
|
}
|
|
2766
|
+
if let serialNumber = fields["serial_number"] as? String, !serialNumber.isEmpty {
|
|
2767
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2768
|
+
}
|
|
2631
2769
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2632
2770
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2633
2771
|
}
|
|
2634
2772
|
|
|
2773
|
+
// Negotiate wire caps from ANY version_info chunk, not only the one
|
|
2774
|
+
// carrying build_number: the glasses put wire_caps in version_info_3
|
|
2775
|
+
// (firmware chunk) while build_number travels in version_info_1, so
|
|
2776
|
+
// gating negotiation on build_number silently discards the caps.
|
|
2777
|
+
// parsePeerWireCaps no-ops without a wire_caps key and
|
|
2778
|
+
// maybeSendWireHandshake gates on caps + build + not-yet-queued, so
|
|
2779
|
+
// running them per chunk is safe.
|
|
2780
|
+
parsePeerWireCaps(json)
|
|
2781
|
+
maybeSendWireHandshake()
|
|
2782
|
+
handleGlassesSessionId(json)
|
|
2783
|
+
|
|
2635
2784
|
Bridge.sendVersionInfo(fields)
|
|
2636
2785
|
} else {
|
|
2637
2786
|
Bridge.log("Unhandled message type: \(type)")
|
|
@@ -2729,6 +2878,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2729
2878
|
}
|
|
2730
2879
|
|
|
2731
2880
|
switch command {
|
|
2881
|
+
case "sr_ancs":
|
|
2882
|
+
if let body = k900ParseBody(json["B"]) {
|
|
2883
|
+
processAncsRelay(body)
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2732
2886
|
case "sr_hrt":
|
|
2733
2887
|
if let bodyObj = json["B"] as? [String: Any] {
|
|
2734
2888
|
let readyResponse = bodyObj["ready"] as? Int ?? 0
|
|
@@ -2885,7 +3039,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2885
3039
|
|
|
2886
3040
|
let syntheticStatus: String
|
|
2887
3041
|
if besOtaStatus == "FINISHED" {
|
|
2888
|
-
|
|
3042
|
+
// The glasses power-cycle right after the final BES tick, so a session
|
|
3043
|
+
// whose BES step is the LAST step never gets a follow-up ota_status from
|
|
3044
|
+
// the glasses — consumers mapping on this synthetic status would otherwise
|
|
3045
|
+
// never see a terminal state. Emit "complete" for the final step;
|
|
3046
|
+
// mid-session BES steps keep "step_complete" so session-level trackers
|
|
3047
|
+
// advance normally. Unknown sessions (cachedOtaTotalSteps == 0, e.g.
|
|
3048
|
+
// legacy glasses that never sent an ota_status) conservatively keep
|
|
3049
|
+
// "step_complete".
|
|
3050
|
+
syntheticStatus = (cachedOtaTotalSteps > 0 && cachedOtaCurrentStep >= cachedOtaTotalSteps)
|
|
3051
|
+
? "complete"
|
|
3052
|
+
: "step_complete"
|
|
2889
3053
|
} else if besOtaStatus == "FAILED" {
|
|
2890
3054
|
syntheticStatus = "failed"
|
|
2891
3055
|
} else {
|
|
@@ -2933,11 +3097,87 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2933
3097
|
}
|
|
2934
3098
|
}
|
|
2935
3099
|
|
|
3100
|
+
/// Convert a Mentra Live firmware ANCS relay packet into the same native
|
|
3101
|
+
/// events used by Android's NotificationListenerService.
|
|
3102
|
+
private func processAncsRelay(_ body: [String: Any]) {
|
|
3103
|
+
guard let event = body["event"] as? String,
|
|
3104
|
+
let uid = (body["uid"] as? NSNumber)?.uint32Value
|
|
3105
|
+
else {
|
|
3106
|
+
Bridge.log("LIVE: Ignoring malformed ANCS relay packet")
|
|
3107
|
+
return
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
let notificationId = "ancs-\(uid)"
|
|
3111
|
+
let appIdentifier = body["appIdentifier"] as? String ?? ""
|
|
3112
|
+
|
|
3113
|
+
if event == "removed" {
|
|
3114
|
+
let removedAppIdentifier = ancsAppIdentifiers.removeValue(forKey: uid) ?? appIdentifier
|
|
3115
|
+
Bridge.sendTypedMessage(
|
|
3116
|
+
"phone_notification_dismissed",
|
|
3117
|
+
body: [
|
|
3118
|
+
"notificationId": notificationId,
|
|
3119
|
+
"notificationKey": notificationId,
|
|
3120
|
+
"packageName": removedAppIdentifier,
|
|
3121
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
3122
|
+
]
|
|
3123
|
+
)
|
|
3124
|
+
return
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
guard event == "added" || event == "modified" else {
|
|
3128
|
+
Bridge.log("LIVE: Ignoring unknown ANCS event: \(event)")
|
|
3129
|
+
return
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
let title = body["title"] as? String ?? ""
|
|
3133
|
+
let subtitle = body["subtitle"] as? String ?? ""
|
|
3134
|
+
let message = body["message"] as? String ?? ""
|
|
3135
|
+
let content = [subtitle, message]
|
|
3136
|
+
.filter { !$0.isEmpty }
|
|
3137
|
+
.joined(separator: "\n")
|
|
3138
|
+
let flags = (body["flags"] as? NSNumber)?.uint8Value ?? 0
|
|
3139
|
+
if !appIdentifier.isEmpty {
|
|
3140
|
+
ancsAppIdentifiers[uid] = appIdentifier
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
Bridge.sendTypedMessage(
|
|
3144
|
+
"phone_notification",
|
|
3145
|
+
body: [
|
|
3146
|
+
"notificationId": notificationId,
|
|
3147
|
+
"app": appIdentifier,
|
|
3148
|
+
"title": title,
|
|
3149
|
+
"content": content,
|
|
3150
|
+
"priority": (flags & 0x02) != 0 ? "1" : "0",
|
|
3151
|
+
"timestamp": ancsTimestamp(body["date"] as? String),
|
|
3152
|
+
"packageName": appIdentifier,
|
|
3153
|
+
]
|
|
3154
|
+
)
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
private func ancsTimestamp(_ value: String?) -> Int64 {
|
|
3158
|
+
guard let value, !value.isEmpty else {
|
|
3159
|
+
return Int64(Date().timeIntervalSince1970 * 1000)
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
let formatter = DateFormatter()
|
|
3163
|
+
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
3164
|
+
formatter.calendar = Calendar(identifier: .gregorian)
|
|
3165
|
+
formatter.timeZone = .current
|
|
3166
|
+
formatter.dateFormat = "yyyyMMdd'T'HHmmss"
|
|
3167
|
+
guard let date = formatter.date(from: value) else {
|
|
3168
|
+
return Int64(Date().timeIntervalSince1970 * 1000)
|
|
3169
|
+
}
|
|
3170
|
+
return Int64(date.timeIntervalSince1970 * 1000)
|
|
3171
|
+
}
|
|
3172
|
+
|
|
2936
3173
|
// commands to send to the glasses:
|
|
2937
3174
|
|
|
2938
|
-
func requestWifiScan() {
|
|
3175
|
+
func requestWifiScan(scanId: String?) {
|
|
2939
3176
|
Bridge.log("LIVE: Requesting WiFi scan from glasses")
|
|
2940
|
-
|
|
3177
|
+
var json: [String: Any] = ["type": "request_wifi_scan"]
|
|
3178
|
+
if let scanId, !scanId.isEmpty {
|
|
3179
|
+
json["scanId"] = scanId
|
|
3180
|
+
}
|
|
2941
3181
|
sendJson(json, wakeUp: true)
|
|
2942
3182
|
}
|
|
2943
3183
|
|
|
@@ -3084,7 +3324,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3084
3324
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
3085
3325
|
]
|
|
3086
3326
|
if let otaVersionUrl = otaVersionUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3087
|
-
!otaVersionUrl.isEmpty
|
|
3327
|
+
!otaVersionUrl.isEmpty
|
|
3328
|
+
{
|
|
3088
3329
|
json["ota_version_url"] = otaVersionUrl
|
|
3089
3330
|
}
|
|
3090
3331
|
|
|
@@ -3129,6 +3370,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3129
3370
|
DeviceStore.shared.apply("glasses", "appVersion", "")
|
|
3130
3371
|
DeviceStore.shared.apply("glasses", "besFirmwareVersion", "")
|
|
3131
3372
|
DeviceStore.shared.apply("glasses", "mtkFirmwareVersion", "")
|
|
3373
|
+
// Modern ASG builds omit ota_version_url entirely (the phone owns manifest selection),
|
|
3374
|
+
// and the chunked parser only writes present fields — clear it here so a URL reported
|
|
3375
|
+
// by a previous build cannot leak into this session.
|
|
3376
|
+
DeviceStore.shared.apply("glasses", "otaVersionUrl", "")
|
|
3132
3377
|
Bridge.log("LIVE: Cleared cached version_info fields before refresh")
|
|
3133
3378
|
|
|
3134
3379
|
// Perform SOC-dependent initialization
|
|
@@ -3177,7 +3422,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3177
3422
|
}
|
|
3178
3423
|
|
|
3179
3424
|
let scanComplete = json["scan_complete"] as? Bool ?? json["scanComplete"] as? Bool ?? false
|
|
3180
|
-
|
|
3425
|
+
let scanId = (json["scanId"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
3426
|
+
Bridge.updateWifiScanResults(networks, scanComplete: scanComplete, scanId: scanId)
|
|
3181
3427
|
}
|
|
3182
3428
|
|
|
3183
3429
|
private func handleButtonPress(_ json: [String: Any]) {
|
|
@@ -3196,12 +3442,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3196
3442
|
let otaVersionUrl = json["ota_version_url"] as? String ?? ""
|
|
3197
3443
|
let firmwareVersion = json["firmware_version"] as? String ?? ""
|
|
3198
3444
|
let bluetoothMacAddress = json["bt_mac_address"] as? String ?? ""
|
|
3445
|
+
let serialNumber = json["serial_number"] as? String ?? ""
|
|
3199
3446
|
|
|
3200
3447
|
DeviceStore.shared.apply("glasses", "appVersion", appVersion)
|
|
3201
3448
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
3202
3449
|
DeviceStore.shared.apply("glasses", "otaVersionUrl", otaVersionUrl)
|
|
3203
3450
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3204
3451
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3452
|
+
if !serialNumber.isEmpty {
|
|
3453
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
3454
|
+
}
|
|
3205
3455
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
3206
3456
|
maybeSendWireHandshake()
|
|
3207
3457
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -3803,8 +4053,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3803
4053
|
responseBody: String
|
|
3804
4054
|
) {
|
|
3805
4055
|
guard !responseBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
|
3806
|
-
|
|
3807
|
-
|
|
4056
|
+
let data = responseBody.data(using: .utf8),
|
|
4057
|
+
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
3808
4058
|
else {
|
|
3809
4059
|
return
|
|
3810
4060
|
}
|
|
@@ -3861,8 +4111,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3861
4111
|
|
|
3862
4112
|
func queueSend(_ data: Data, id: String, trace: BleWriteTrace?) {
|
|
3863
4113
|
let significantQueueSize = SIGNIFICANT_BLE_TRACE_QUEUE_SIZE
|
|
4114
|
+
// Capture the epoch at enqueue-request time: the Task body may run after a
|
|
4115
|
+
// session reset, and a pre-reset payload stamped with the new generation would
|
|
4116
|
+
// defeat the stale-write guard in the drain loop.
|
|
4117
|
+
let generation = wireSessionGeneration
|
|
3864
4118
|
Task {
|
|
3865
|
-
let queueSize = await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace))
|
|
4119
|
+
let queueSize = await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace, generation: generation))
|
|
3866
4120
|
guard trace != nil, queueSize >= significantQueueSize else {
|
|
3867
4121
|
return
|
|
3868
4122
|
}
|
|
@@ -4092,19 +4346,47 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4092
4346
|
}
|
|
4093
4347
|
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
4094
4348
|
wireHandshakeQueued = true
|
|
4349
|
+
wireHandshakeSentGeneration = wireSessionGeneration
|
|
4095
4350
|
queueSend(packed, id: "-1")
|
|
4351
|
+
// The handshake and its reply are fire-and-forget binary frames with no ACK
|
|
4352
|
+
// tracking; if either is lost, wireHandshakeQueued would block every future
|
|
4353
|
+
// attempt and the session would silently stay on the legacy string wire.
|
|
4354
|
+
// Re-arm after a grace period so negotiation can retry, bounded per session
|
|
4355
|
+
// so a peer that advertises binary but never answers doesn't get pinged
|
|
4356
|
+
// forever (later caps/version triggers may still retry explicitly).
|
|
4357
|
+
wireHandshakeAttempts += 1
|
|
4358
|
+
let scheduledGeneration = wireSessionGeneration
|
|
4359
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.wireHandshakeRetryGraceSeconds) { [weak self] in
|
|
4360
|
+
guard let self else { return }
|
|
4361
|
+
guard scheduledGeneration == self.wireSessionGeneration else { return }
|
|
4362
|
+
if self.wireHandshakeQueued, self.peerWireProtocolVersion < BleWireProtocol.protocolV2 {
|
|
4363
|
+
Bridge.log(
|
|
4364
|
+
"LIVE: BLE wire v2 handshake unanswered after \(Self.wireHandshakeRetryGraceSeconds)s " +
|
|
4365
|
+
"(attempt \(self.wireHandshakeAttempts)/\(Self.wireHandshakeMaxAttempts)) - re-arming"
|
|
4366
|
+
)
|
|
4367
|
+
self.wireHandshakeQueued = false
|
|
4368
|
+
if self.wireHandshakeAttempts < Self.wireHandshakeMaxAttempts {
|
|
4369
|
+
self.maybeSendWireHandshake()
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4096
4373
|
}
|
|
4097
4374
|
|
|
4098
4375
|
private func activateBinaryWireV2Session(logMessage: String) {
|
|
4099
4376
|
peerWireProtocolVersion = BleWireProtocol.protocolV2
|
|
4100
4377
|
useBinaryWireProtocol = true
|
|
4101
4378
|
wireHandshakeQueued = false
|
|
4379
|
+
wireHandshakeAttempts = 0
|
|
4102
4380
|
peerK900Le = true
|
|
4103
4381
|
BleJsonCompact.markSessionConnected(epochMs: Int64(Date().timeIntervalSince1970 * 1000))
|
|
4104
4382
|
Bridge.log(logMessage)
|
|
4105
4383
|
}
|
|
4106
4384
|
|
|
4107
4385
|
private func handlePeerWireHandshake() {
|
|
4386
|
+
if wireHandshakeSentGeneration != wireSessionGeneration {
|
|
4387
|
+
Bridge.log("LIVE: Ignoring wire v2 handshake reply from a previous session epoch")
|
|
4388
|
+
return
|
|
4389
|
+
}
|
|
4108
4390
|
activateBinaryWireV2Session(logMessage: "LIVE: Peer confirmed BLE wire protocol v2")
|
|
4109
4391
|
}
|
|
4110
4392
|
|
|
@@ -4624,12 +4906,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4624
4906
|
}
|
|
4625
4907
|
}
|
|
4626
4908
|
|
|
4627
|
-
private func updateWifiStatus(connected: Bool, ssid: String, ip: String) {
|
|
4909
|
+
private func updateWifiStatus(connected: Bool, ssid: String, ip: String, error: String? = nil) {
|
|
4628
4910
|
Bridge.log("LIVE: 🌐 Updating WiFi status - connected: \(connected), ssid: \(ssid)")
|
|
4629
4911
|
DeviceStore.shared.apply("glasses", "wifiConnected", connected)
|
|
4630
4912
|
DeviceStore.shared.apply("glasses", "wifiSsid", ssid)
|
|
4631
4913
|
DeviceStore.shared.apply("glasses", "wifiLocalIp", ip)
|
|
4632
|
-
emitWifiStatusChange()
|
|
4914
|
+
emitWifiStatusChange(error: error)
|
|
4633
4915
|
}
|
|
4634
4916
|
|
|
4635
4917
|
private func updateHotspotStatus(enabled: Bool, ssid: String, password: String, ip: String) {
|
|
@@ -4815,6 +5097,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4815
5097
|
readinessCheckCounter = 0
|
|
4816
5098
|
fullyBooted = false
|
|
4817
5099
|
connected = false
|
|
5100
|
+
glassesSessionId = nil
|
|
4818
5101
|
|
|
4819
5102
|
Bridge.log("LIVE: 🔄 Starting glasses SOC readiness check loop")
|
|
4820
5103
|
|
|
@@ -4923,8 +5206,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4923
5206
|
// emitEvent("BatteryLevelEvent", body: eventBody)
|
|
4924
5207
|
// }
|
|
4925
5208
|
|
|
4926
|
-
private func emitWifiStatusChange() {
|
|
4927
|
-
Bridge.sendWifiStatusChange(connected: wifiConnected, ssid: wifiSsid, localIp: wifiLocalIp)
|
|
5209
|
+
private func emitWifiStatusChange(error: String? = nil) {
|
|
5210
|
+
Bridge.sendWifiStatusChange(connected: wifiConnected, ssid: wifiSsid, localIp: wifiLocalIp, error: error)
|
|
4928
5211
|
}
|
|
4929
5212
|
|
|
4930
5213
|
private func emitHotspotStatusChange() {
|
|
@@ -5006,14 +5289,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5006
5289
|
|
|
5007
5290
|
// Stop all timers
|
|
5008
5291
|
stopAllTimers()
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
useBinaryWireProtocol = false
|
|
5012
|
-
wireHandshakeQueued = false
|
|
5013
|
-
peerK900Le = false
|
|
5014
|
-
peerWireCapsBinary = false
|
|
5015
|
-
peerFilePayloadV2 = false
|
|
5016
|
-
BleJsonCompact.resetSession()
|
|
5292
|
+
resetWireNegotiationState()
|
|
5293
|
+
Task { await commandQueue.removeAll() } // stale writes die with the session
|
|
5017
5294
|
closeL2capFileChannel()
|
|
5018
5295
|
|
|
5019
5296
|
// Disconnect BLE
|
|
@@ -5106,6 +5383,77 @@ extension MentraLive {
|
|
|
5106
5383
|
* per-link endianness and binary support. Missing wire_caps leaves the legacy defaults (BE, no
|
|
5107
5384
|
* binary) untouched so older glasses keep working.
|
|
5108
5385
|
*/
|
|
5386
|
+
/// Drop every negotiated wire-session artifact and bump the session generation so
|
|
5387
|
+
/// scheduled callbacks from the old session stand down. Called on BLE disconnect and on
|
|
5388
|
+
/// glasses_ready: the glasses run onTransportReset() before sending glasses_ready, so a
|
|
5389
|
+
/// mid-link ASG restart (no BLE disconnect) resets THEIR side to legacy framing - the
|
|
5390
|
+
/// phone must treat every glasses_ready as a new wire epoch and renegotiate (bounded by
|
|
5391
|
+
/// the per-session handshake attempt cap) instead of trusting stale v2-active state.
|
|
5392
|
+
private func resetWireNegotiationState() {
|
|
5393
|
+
incomingChunkReassembler.clear()
|
|
5394
|
+
peerWireProtocolVersion = 0
|
|
5395
|
+
useBinaryWireProtocol = false
|
|
5396
|
+
wireHandshakeQueued = false
|
|
5397
|
+
wireHandshakeAttempts = 0
|
|
5398
|
+
wireSessionGeneration += 1
|
|
5399
|
+
peerK900Le = false
|
|
5400
|
+
peerWireCapsBinary = false
|
|
5401
|
+
peerFilePayloadV2 = false
|
|
5402
|
+
BleJsonCompact.resetSession()
|
|
5403
|
+
wireHandshakeSentGeneration = -1
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
/// ANCS notification IDs live for the BLE link, not the wire epoch. A
|
|
5407
|
+
/// glasses_ready message may reset wire negotiation without disconnecting,
|
|
5408
|
+
/// so clear this state only when the BLE session itself ends.
|
|
5409
|
+
private func resetAncsRelayState() {
|
|
5410
|
+
ancsAppIdentifiers.removeAll()
|
|
5411
|
+
ancsRelayEnableRequested = false
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
/// Sends phone_ready to (re)start the glasses-driven readiness flow.
|
|
5415
|
+
private func sendPhoneReady(reason: String) {
|
|
5416
|
+
Bridge.log("LIVE: 📱 Sending phone_ready to glasses (\(reason)) - waiting for glasses_ready response")
|
|
5417
|
+
let readyMsg: [String: Any] = [
|
|
5418
|
+
"type": "phone_ready",
|
|
5419
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
5420
|
+
]
|
|
5421
|
+
sendJson(readyMsg, wakeUp: true)
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
/// Tracks the glasses process session id (`sid`) and re-runs the phone-driven readiness
|
|
5425
|
+
/// flow when it changes under a live BLE link. The BES holds the link across asg_client
|
|
5426
|
+
/// restarts (APK OTA, crash recovery), so a restarted glasses process is invisible to
|
|
5427
|
+
/// transport state; the sid is the explicit protocol signal.
|
|
5428
|
+
///
|
|
5429
|
+
/// Tri-state by design (retro-compat with pre-sid glasses builds): no `sid` key = legacy
|
|
5430
|
+
/// glasses, do nothing; sid appearing on an ESTABLISHED session (upgrade OTA from a
|
|
5431
|
+
/// pre-sid build) or differing from the recorded one = restart; same sid = no action.
|
|
5432
|
+
/// A restart is a LOGICAL session reset: send phone_ready immediately (bypassing the
|
|
5433
|
+
/// sr_hrt heartbeat's stale-readiness suppression); the returning glasses_ready runs
|
|
5434
|
+
/// the full existing remote-wire-reset flow. fullyBooted is deliberately untouched -
|
|
5435
|
+
/// the physical link never dropped, so the connection UI must not flap.
|
|
5436
|
+
private func handleGlassesSessionId(_ json: [String: Any]) {
|
|
5437
|
+
guard let sid = json["sid"] as? String, !sid.isEmpty else { return }
|
|
5438
|
+
let previous = glassesSessionId
|
|
5439
|
+
if sid == previous { return }
|
|
5440
|
+
glassesSessionId = sid
|
|
5441
|
+
if previous == nil, !readinessCompletedThisBleSession {
|
|
5442
|
+
// First sid of a fresh BLE session before readiness completes: the normal
|
|
5443
|
+
// pairing/readiness flow is already running - just record it.
|
|
5444
|
+
return
|
|
5445
|
+
}
|
|
5446
|
+
Bridge.log(
|
|
5447
|
+
"LIVE: 🔁 Glasses session changed (\(previous ?? "<pre-sid build>") -> \(sid)) - " +
|
|
5448
|
+
"asg restarted under a live link, re-running readiness"
|
|
5449
|
+
)
|
|
5450
|
+
sendPhoneReady(reason: "glasses session changed")
|
|
5451
|
+
Bridge.sendTypedMessage(
|
|
5452
|
+
"glasses_session_changed",
|
|
5453
|
+
body: ["previous_sid": previous ?? "", "sid": sid]
|
|
5454
|
+
)
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5109
5457
|
private func parsePeerWireCaps(_ json: [String: Any]) {
|
|
5110
5458
|
guard let caps = json["wire_caps"] as? [String: Any] else { return }
|
|
5111
5459
|
if (caps["k900_le"] as? Bool) == true {
|
|
@@ -5778,6 +6126,9 @@ extension MentraLive {
|
|
|
5778
6126
|
|
|
5779
6127
|
// Send glasses-side Voice Activity Detection setting.
|
|
5780
6128
|
sendVoiceActivityDetectionSetting()
|
|
6129
|
+
|
|
6130
|
+
// Send glasses-side loudness / Barrier gate setting.
|
|
6131
|
+
sendLoudnessGateSetting()
|
|
5781
6132
|
}
|
|
5782
6133
|
|
|
5783
6134
|
func sendVoiceActivityDetectionSetting() {
|
|
@@ -5814,6 +6165,38 @@ extension MentraLive {
|
|
|
5814
6165
|
}
|
|
5815
6166
|
}
|
|
5816
6167
|
|
|
6168
|
+
func sendLoudnessGateSetting() {
|
|
6169
|
+
let enabled = DeviceStore.shared.get("bluetooth", "loudness_gate_enabled") as? Bool
|
|
6170
|
+
?? BluetoothSdkDefaults.loudnessGateEnabled
|
|
6171
|
+
Bridge.log("LIVE: 🎚️ Sending loudness/Barrier gate setting to glasses: \(enabled)")
|
|
6172
|
+
|
|
6173
|
+
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
6174
|
+
Bridge.log("Cannot send loudness gate setting - BLE write path not ready")
|
|
6175
|
+
return
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
do {
|
|
6179
|
+
let bodyData = try JSONSerialization.data(withJSONObject: [
|
|
6180
|
+
"type": Self.loudnessGateSwitchType,
|
|
6181
|
+
"switch": enabled ? 1 : 0,
|
|
6182
|
+
])
|
|
6183
|
+
guard let bodyString = String(data: bodyData, encoding: .utf8) else {
|
|
6184
|
+
Bridge.log("LIVE: Failed to encode loudness gate payload")
|
|
6185
|
+
return
|
|
6186
|
+
}
|
|
6187
|
+
let command: [String: Any] = [
|
|
6188
|
+
"C": "cs_swit",
|
|
6189
|
+
"V": 1,
|
|
6190
|
+
"B": bodyString,
|
|
6191
|
+
]
|
|
6192
|
+
if !sendRawK900Command(command, wakeUp: true) {
|
|
6193
|
+
Bridge.log("LIVE: Failed to send loudness gate setting command")
|
|
6194
|
+
}
|
|
6195
|
+
} catch {
|
|
6196
|
+
Bridge.log("LIVE: Error encoding loudness gate payload: \(error)")
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
|
|
5817
6200
|
func sendButtonVideoRecordingSettings() {
|
|
5818
6201
|
let settings =
|
|
5819
6202
|
DeviceStore.shared.get("bluetooth", "button_video_settings") as? [String: Any] ?? [
|
|
@@ -5879,11 +6262,12 @@ extension MentraLive {
|
|
|
5879
6262
|
}
|
|
5880
6263
|
|
|
5881
6264
|
func sendButtonPhotoSettings() {
|
|
6265
|
+
let legacyZslMfnr = DeviceStore.shared.get("bluetooth", "button_photo_zsl_mfnr") as? Bool
|
|
5882
6266
|
let size = (DeviceStore.shared.get("bluetooth", "button_photo_size") as? String).flatMap { rawSize in
|
|
5883
6267
|
rawSize.isEmpty ? nil : PhotoSize(normalizedRawValue: rawSize)
|
|
5884
6268
|
}
|
|
5885
|
-
let mfnr = DeviceStore.shared.get("bluetooth", "button_photo_mfnr") as? Bool
|
|
5886
|
-
let zsl = DeviceStore.shared.get("bluetooth", "button_photo_zsl") as? Bool
|
|
6269
|
+
let mfnr = DeviceStore.shared.get("bluetooth", "button_photo_mfnr") as? Bool ?? legacyZslMfnr
|
|
6270
|
+
let zsl = DeviceStore.shared.get("bluetooth", "button_photo_zsl") as? Bool ?? legacyZslMfnr
|
|
5887
6271
|
let noiseReduction = DeviceStore.shared.get("bluetooth", "button_photo_noise_reduction") as? Bool
|
|
5888
6272
|
let edgeEnhancement = DeviceStore.shared.get("bluetooth", "button_photo_edge_enhancement") as? Bool
|
|
5889
6273
|
let ispDigitalGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_digital_gain") as? Int
|
|
@@ -6001,7 +6385,7 @@ extension MentraLive {
|
|
|
6001
6385
|
|
|
6002
6386
|
func sendCameraFovSetting() {
|
|
6003
6387
|
let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov": 118, "roi_position": 0]
|
|
6004
|
-
let fov = settings["fov"] as? Int ??
|
|
6388
|
+
let fov = settings["fov"] as? Int ?? CameraFov.defaultFov
|
|
6005
6389
|
let roiPosition = settings["roi_position"] as? Int ?? 0
|
|
6006
6390
|
sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
|
|
6007
6391
|
}
|
|
@@ -6027,6 +6411,39 @@ extension MentraLive {
|
|
|
6027
6411
|
sendJson(json, wakeUp: true)
|
|
6028
6412
|
}
|
|
6029
6413
|
|
|
6414
|
+
func sendCameraFovOverride(
|
|
6415
|
+
requestId: String,
|
|
6416
|
+
leaseId: String,
|
|
6417
|
+
fov: Int,
|
|
6418
|
+
roiPosition: Int,
|
|
6419
|
+
ttlMs: Int
|
|
6420
|
+
) {
|
|
6421
|
+
sendJson(
|
|
6422
|
+
[
|
|
6423
|
+
"type": "camera_fov_override",
|
|
6424
|
+
"request_id": requestId,
|
|
6425
|
+
"params": [
|
|
6426
|
+
"lease_id": leaseId,
|
|
6427
|
+
"fov": fov,
|
|
6428
|
+
"roi_position": roiPosition,
|
|
6429
|
+
"ttl_ms": ttlMs,
|
|
6430
|
+
],
|
|
6431
|
+
],
|
|
6432
|
+
wakeUp: true
|
|
6433
|
+
)
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
func releaseCameraFovOverride(requestId: String, leaseId: String) {
|
|
6437
|
+
sendJson(
|
|
6438
|
+
[
|
|
6439
|
+
"type": "camera_fov_override_release",
|
|
6440
|
+
"request_id": requestId,
|
|
6441
|
+
"params": ["lease_id": leaseId],
|
|
6442
|
+
],
|
|
6443
|
+
wakeUp: true
|
|
6444
|
+
)
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6030
6447
|
func sendCameraTuningConfig(requestId: String?, anrOn: Bool, gainOn: Bool) {
|
|
6031
6448
|
Bridge.log("Sending camera tuning config: anr=\(anrOn), gain=\(gainOn)")
|
|
6032
6449
|
|