@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2
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 +22 -11
- 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 +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
- 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/MentraLive.kt +161 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
- 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/streaming/StreamModels.kt +41 -1
- 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/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 +30 -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 +58 -25
- 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 +1 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +1 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +6 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +6 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -1
- 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 +7 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +15 -3
- package/ios/Source/DeviceManager.swift +67 -17
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +37 -8
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/camera/CameraModels.swift +20 -5
- 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/MentraLive.swift +174 -38
- package/ios/Source/sgcs/SGCManager.swift +8 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/streaming/StreamModels.swift +71 -7
- 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 -1
- package/src/BluetoothSdk.types.ts +61 -28
- package/src/_internal.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +20 -6
- package/src/_private/cameraRequestPayload.ts +8 -2
- package/src/_private/photoRequestPayload.ts +12 -2
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +9 -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
|
|
@@ -950,6 +950,8 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
950
950
|
self.connectedPeripheral = nil
|
|
951
951
|
self.fullyBooted = false
|
|
952
952
|
self.connected = false
|
|
953
|
+
self.glassesSessionId = nil // Fresh BLE session starts with no sid known
|
|
954
|
+
self.readinessCompletedThisBleSession = false
|
|
953
955
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
954
956
|
self.rgbLedAuthorityClaimed = false
|
|
955
957
|
|
|
@@ -1273,11 +1275,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1273
1275
|
// to avoid overloading the MCU. Set to false to allow simultaneous A2DP + LC3 mic.
|
|
1274
1276
|
private let BLOCK_AUDIO_DUPLEX = false
|
|
1275
1277
|
private static let voiceActivityDetectionSwitchType = 8
|
|
1278
|
+
private static let loudnessGateSwitchType = 10
|
|
1276
1279
|
|
|
1277
1280
|
var connectionState: String = ConnTypes.DISCONNECTED
|
|
1278
1281
|
|
|
1279
1282
|
/// Mirrors Android `updateConnectionState` — RN home reads `glasses.connectionState` for reconnecting UI.
|
|
1280
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
|
+
}
|
|
1281
1293
|
connectionState = state
|
|
1282
1294
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
1283
1295
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state from
|
|
@@ -1313,6 +1325,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1313
1325
|
func sendText(_ text: String) async {
|
|
1314
1326
|
await sendTextWall(text)
|
|
1315
1327
|
}
|
|
1328
|
+
|
|
1316
1329
|
func sendTextWall(_: String) async {}
|
|
1317
1330
|
func ping() {
|
|
1318
1331
|
Bridge.log("LIVE: ping()")
|
|
@@ -1425,7 +1438,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1425
1438
|
// So we temporarily suspend the LC3 mic during phone audio playback
|
|
1426
1439
|
private var micIntentEnabled = false // User/system WANTS mic enabled
|
|
1427
1440
|
private var micSuspendedForAudio = false // Mic temporarily suspended due to phone audio
|
|
1428
|
-
var isMicSuspendedForAudio: Bool {
|
|
1441
|
+
var isMicSuspendedForAudio: Bool {
|
|
1442
|
+
micSuspendedForAudio
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1429
1445
|
private var phoneAudioMonitor: PhoneAudioMonitor?
|
|
1430
1446
|
|
|
1431
1447
|
// Timing Constants
|
|
@@ -1470,12 +1486,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1470
1486
|
private var useBinaryWireProtocol = false
|
|
1471
1487
|
private var wireHandshakeQueued = false
|
|
1472
1488
|
private var wireHandshakeAttempts = 0
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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).
|
|
1476
1492
|
private var wireHandshakeSentGeneration = -1
|
|
1477
|
-
|
|
1478
|
-
|
|
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.
|
|
1479
1495
|
private var wireSessionGeneration = 0
|
|
1480
1496
|
// Grace period before an unanswered BLE wire v2 handshake is re-armed for retry,
|
|
1481
1497
|
// and the per-session cap on automatic retries (see sendWireHandshake).
|
|
@@ -1489,6 +1505,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1489
1505
|
private var ancsRelayEnableRequested = false
|
|
1490
1506
|
private var peerWireCapsBinary = false
|
|
1491
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
|
|
1492
1518
|
private var globalMessageId = 0
|
|
1493
1519
|
private var lastReceivedMessageId = 0
|
|
1494
1520
|
private var bleWriteTraceSequence = 0
|
|
@@ -1789,7 +1815,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1789
1815
|
size: PhotoSize,
|
|
1790
1816
|
mode: PhotoMode = .photo,
|
|
1791
1817
|
exposureTimeNs: Double?,
|
|
1792
|
-
durationMs: Int
|
|
1818
|
+
durationMs: Int,
|
|
1819
|
+
zsl: Bool? = nil,
|
|
1820
|
+
mfnr: Bool? = nil
|
|
1793
1821
|
) {
|
|
1794
1822
|
Bridge.log(
|
|
1795
1823
|
"LIVE: warmUpCamera() entry requestId=\(requestId) size=\(size.rawValue) mode=\(mode.rawValue) durationMs=\(durationMs)"
|
|
@@ -1808,6 +1836,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1808
1836
|
if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
|
|
1809
1837
|
json["exposureTimeNs"] = Int64(e)
|
|
1810
1838
|
}
|
|
1839
|
+
if let mfnr {
|
|
1840
|
+
json["mfnr"] = mfnr
|
|
1841
|
+
}
|
|
1842
|
+
if let zsl {
|
|
1843
|
+
json["zsl"] = zsl
|
|
1844
|
+
}
|
|
1811
1845
|
|
|
1812
1846
|
sendJson(json, wakeUp: true)
|
|
1813
1847
|
}
|
|
@@ -1872,11 +1906,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1872
1906
|
let retries: Int
|
|
1873
1907
|
let id: String
|
|
1874
1908
|
let trace: BleWriteTrace?
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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.
|
|
1880
1914
|
let generation: Int
|
|
1881
1915
|
}
|
|
1882
1916
|
|
|
@@ -2157,6 +2191,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2157
2191
|
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
2158
2192
|
connected = false
|
|
2159
2193
|
fullyBooted = false
|
|
2194
|
+
glassesSessionId = nil
|
|
2195
|
+
readinessCompletedThisBleSession = false
|
|
2196
|
+
readinessCompletedThisBleSession = false // Fresh BLE session starts with no sid known
|
|
2197
|
+
readinessCompletedThisBleSession = false
|
|
2160
2198
|
return
|
|
2161
2199
|
}
|
|
2162
2200
|
|
|
@@ -2415,6 +2453,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2415
2453
|
resetWireNegotiationState()
|
|
2416
2454
|
parsePeerWireCaps(json)
|
|
2417
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
|
|
2418
2461
|
handleGlassesReady()
|
|
2419
2462
|
|
|
2420
2463
|
case "battery_status":
|
|
@@ -2676,7 +2719,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2676
2719
|
default:
|
|
2677
2720
|
// Flexible version_info parsing - handle any version_info* message
|
|
2678
2721
|
if type.hasPrefix("version_info") {
|
|
2679
|
-
Bridge.log("LIVE: Received \(type)
|
|
2722
|
+
Bridge.log("LIVE: Received \(type)")
|
|
2680
2723
|
|
|
2681
2724
|
// Extract all fields from JSON (except "type")
|
|
2682
2725
|
var fields: [String: Any] = [:]
|
|
@@ -2720,6 +2763,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2720
2763
|
if let bluetoothMacAddress = fields["bt_mac_address"] as? String {
|
|
2721
2764
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2722
2765
|
}
|
|
2766
|
+
if let serialNumber = fields["serial_number"] as? String, !serialNumber.isEmpty {
|
|
2767
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2768
|
+
}
|
|
2723
2769
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2724
2770
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2725
2771
|
}
|
|
@@ -2733,6 +2779,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2733
2779
|
// running them per chunk is safe.
|
|
2734
2780
|
parsePeerWireCaps(json)
|
|
2735
2781
|
maybeSendWireHandshake()
|
|
2782
|
+
handleGlassesSessionId(json)
|
|
2736
2783
|
|
|
2737
2784
|
Bridge.sendVersionInfo(fields)
|
|
2738
2785
|
} else {
|
|
@@ -3277,7 +3324,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3277
3324
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
3278
3325
|
]
|
|
3279
3326
|
if let otaVersionUrl = otaVersionUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3280
|
-
!otaVersionUrl.isEmpty
|
|
3327
|
+
!otaVersionUrl.isEmpty
|
|
3328
|
+
{
|
|
3281
3329
|
json["ota_version_url"] = otaVersionUrl
|
|
3282
3330
|
}
|
|
3283
3331
|
|
|
@@ -3322,6 +3370,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3322
3370
|
DeviceStore.shared.apply("glasses", "appVersion", "")
|
|
3323
3371
|
DeviceStore.shared.apply("glasses", "besFirmwareVersion", "")
|
|
3324
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", "")
|
|
3325
3377
|
Bridge.log("LIVE: Cleared cached version_info fields before refresh")
|
|
3326
3378
|
|
|
3327
3379
|
// Perform SOC-dependent initialization
|
|
@@ -3390,12 +3442,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3390
3442
|
let otaVersionUrl = json["ota_version_url"] as? String ?? ""
|
|
3391
3443
|
let firmwareVersion = json["firmware_version"] as? String ?? ""
|
|
3392
3444
|
let bluetoothMacAddress = json["bt_mac_address"] as? String ?? ""
|
|
3445
|
+
let serialNumber = json["serial_number"] as? String ?? ""
|
|
3393
3446
|
|
|
3394
3447
|
DeviceStore.shared.apply("glasses", "appVersion", appVersion)
|
|
3395
3448
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
3396
3449
|
DeviceStore.shared.apply("glasses", "otaVersionUrl", otaVersionUrl)
|
|
3397
3450
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3398
3451
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3452
|
+
if !serialNumber.isEmpty {
|
|
3453
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
3454
|
+
}
|
|
3399
3455
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
3400
3456
|
maybeSendWireHandshake()
|
|
3401
3457
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -3997,8 +4053,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3997
4053
|
responseBody: String
|
|
3998
4054
|
) {
|
|
3999
4055
|
guard !responseBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
|
4000
|
-
|
|
4001
|
-
|
|
4056
|
+
let data = responseBody.data(using: .utf8),
|
|
4057
|
+
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
4002
4058
|
else {
|
|
4003
4059
|
return
|
|
4004
4060
|
}
|
|
@@ -5041,6 +5097,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5041
5097
|
readinessCheckCounter = 0
|
|
5042
5098
|
fullyBooted = false
|
|
5043
5099
|
connected = false
|
|
5100
|
+
glassesSessionId = nil
|
|
5044
5101
|
|
|
5045
5102
|
Bridge.log("LIVE: 🔄 Starting glasses SOC readiness check loop")
|
|
5046
5103
|
|
|
@@ -5354,6 +5411,49 @@ extension MentraLive {
|
|
|
5354
5411
|
ancsRelayEnableRequested = false
|
|
5355
5412
|
}
|
|
5356
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
|
+
|
|
5357
5457
|
private func parsePeerWireCaps(_ json: [String: Any]) {
|
|
5358
5458
|
guard let caps = json["wire_caps"] as? [String: Any] else { return }
|
|
5359
5459
|
if (caps["k900_le"] as? Bool) == true {
|
|
@@ -6026,6 +6126,9 @@ extension MentraLive {
|
|
|
6026
6126
|
|
|
6027
6127
|
// Send glasses-side Voice Activity Detection setting.
|
|
6028
6128
|
sendVoiceActivityDetectionSetting()
|
|
6129
|
+
|
|
6130
|
+
// Send glasses-side loudness / Barrier gate setting.
|
|
6131
|
+
sendLoudnessGateSetting()
|
|
6029
6132
|
}
|
|
6030
6133
|
|
|
6031
6134
|
func sendVoiceActivityDetectionSetting() {
|
|
@@ -6062,6 +6165,38 @@ extension MentraLive {
|
|
|
6062
6165
|
}
|
|
6063
6166
|
}
|
|
6064
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
|
+
|
|
6065
6200
|
func sendButtonVideoRecordingSettings() {
|
|
6066
6201
|
let settings =
|
|
6067
6202
|
DeviceStore.shared.get("bluetooth", "button_video_settings") as? [String: Any] ?? [
|
|
@@ -6127,11 +6262,12 @@ extension MentraLive {
|
|
|
6127
6262
|
}
|
|
6128
6263
|
|
|
6129
6264
|
func sendButtonPhotoSettings() {
|
|
6265
|
+
let legacyZslMfnr = DeviceStore.shared.get("bluetooth", "button_photo_zsl_mfnr") as? Bool
|
|
6130
6266
|
let size = (DeviceStore.shared.get("bluetooth", "button_photo_size") as? String).flatMap { rawSize in
|
|
6131
6267
|
rawSize.isEmpty ? nil : PhotoSize(normalizedRawValue: rawSize)
|
|
6132
6268
|
}
|
|
6133
|
-
let mfnr = DeviceStore.shared.get("bluetooth", "button_photo_mfnr") as? Bool
|
|
6134
|
-
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
|
|
6135
6271
|
let noiseReduction = DeviceStore.shared.get("bluetooth", "button_photo_noise_reduction") as? Bool
|
|
6136
6272
|
let edgeEnhancement = DeviceStore.shared.get("bluetooth", "button_photo_edge_enhancement") as? Bool
|
|
6137
6273
|
let ispDigitalGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_digital_gain") as? Int
|
|
@@ -6248,8 +6384,8 @@ extension MentraLive {
|
|
|
6248
6384
|
}
|
|
6249
6385
|
|
|
6250
6386
|
func sendCameraFovSetting() {
|
|
6251
|
-
let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov":
|
|
6252
|
-
let fov = settings["fov"] as? Int ??
|
|
6387
|
+
let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov": 118, "roi_position": 0]
|
|
6388
|
+
let fov = settings["fov"] as? Int ?? CameraFov.defaultFov
|
|
6253
6389
|
let roiPosition = settings["roi_position"] as? Int ?? 0
|
|
6254
6390
|
sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
|
|
6255
6391
|
}
|
|
@@ -210,6 +210,10 @@ protocol SGCManager {
|
|
|
210
210
|
|
|
211
211
|
func sendVoiceActivityDetectionSetting()
|
|
212
212
|
|
|
213
|
+
// MARK: - Loudness / Barrier Gate
|
|
214
|
+
|
|
215
|
+
func sendLoudnessGateSetting()
|
|
216
|
+
|
|
213
217
|
// MARK: - Version Info
|
|
214
218
|
|
|
215
219
|
func requestVersionInfo()
|
|
@@ -365,6 +369,10 @@ extension SGCManager {
|
|
|
365
369
|
|
|
366
370
|
func sendVoiceActivityDetectionSetting() {}
|
|
367
371
|
|
|
372
|
+
// MARK: - Loudness / Barrier Gate (default no-op — Mentra Live supports this)
|
|
373
|
+
|
|
374
|
+
func sendLoudnessGateSetting() {}
|
|
375
|
+
|
|
368
376
|
/// Default no-op; Mentra Live and G2 override to handle phone-detected clock skew.
|
|
369
377
|
func sendSetSystemTime(_: Int64) {
|
|
370
378
|
Bridge.log("SGC: sendSetSystemTime not supported")
|