@mentra/bluetooth-sdk 0.1.8 → 0.1.10
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +8 -2
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import Combine
|
|
16
16
|
import CoreBluetooth
|
|
17
17
|
import Foundation
|
|
18
|
+
import ImageIO
|
|
18
19
|
import UIKit
|
|
19
20
|
|
|
20
21
|
// MARK: - Supporting Types
|
|
@@ -68,7 +69,9 @@ class BlePhotoUploadService {
|
|
|
68
69
|
imageData: Data,
|
|
69
70
|
requestId: String,
|
|
70
71
|
webhookUrl: String,
|
|
71
|
-
authToken: String
|
|
72
|
+
authToken: String?,
|
|
73
|
+
onSuccess: ((String, String) -> Void)? = nil,
|
|
74
|
+
onError: ((String, String) -> Void)? = nil
|
|
72
75
|
) {
|
|
73
76
|
Task {
|
|
74
77
|
do {
|
|
@@ -76,32 +79,11 @@ class BlePhotoUploadService {
|
|
|
76
79
|
"\(TAG): Processing BLE photo for upload. Image size: \(imageData.count) bytes"
|
|
77
80
|
)
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
guard let image = decodeImage(imageData: imageData) else {
|
|
81
|
-
throw NSError(
|
|
82
|
-
domain: "BlePhotoUpload",
|
|
83
|
-
code: -1,
|
|
84
|
-
userInfo: [NSLocalizedDescriptionKey: "Failed to decode image data"]
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
Bridge.log(
|
|
89
|
-
"\(TAG): Decoded image to bitmap: \(Int(image.size.width))x\(Int(image.size.height))"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
// 2. Convert to JPEG for upload (in case it was AVIF)
|
|
93
|
-
guard let jpegData = image.jpegData(compressionQuality: 0.9) else {
|
|
94
|
-
throw NSError(
|
|
95
|
-
domain: "BlePhotoUpload",
|
|
96
|
-
code: -2,
|
|
97
|
-
userInfo: [NSLocalizedDescriptionKey: "Failed to convert image to JPEG"]
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
82
|
+
let jpegData = try convertToJpegPreservingExif(imageData: imageData)
|
|
101
83
|
Bridge.log("\(TAG): Converted to JPEG for upload. Size: \(jpegData.count) bytes")
|
|
102
84
|
|
|
103
|
-
//
|
|
104
|
-
try await uploadToWebhook(
|
|
85
|
+
// Upload to webhook
|
|
86
|
+
let responseBody = try await uploadToWebhook(
|
|
105
87
|
jpegData: jpegData,
|
|
106
88
|
requestId: requestId,
|
|
107
89
|
webhookUrl: webhookUrl,
|
|
@@ -109,43 +91,313 @@ class BlePhotoUploadService {
|
|
|
109
91
|
)
|
|
110
92
|
|
|
111
93
|
Bridge.log("\(TAG): Photo uploaded successfully for requestId: \(requestId)")
|
|
112
|
-
|
|
113
|
-
// DispatchQueue.main.async {
|
|
114
|
-
// callback.onSuccess(requestId: requestId)
|
|
115
|
-
// }
|
|
94
|
+
onSuccess?(requestId, responseBody)
|
|
116
95
|
|
|
117
96
|
} catch {
|
|
118
97
|
Bridge.log(
|
|
119
98
|
"\(TAG): Error processing BLE photo for requestId: \(requestId), error: \(error)"
|
|
120
99
|
)
|
|
100
|
+
onError?(requestId, error.localizedDescription)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static func convertToJpegPreservingExif(imageData: Data) throws -> Data {
|
|
106
|
+
logIncomingImageDiagnostics(imageData: imageData)
|
|
107
|
+
let imuJson = readImuJsonFromImageData(imageData)
|
|
108
|
+
|
|
109
|
+
guard let image = decodeImage(imageData: imageData) else {
|
|
110
|
+
throw NSError(
|
|
111
|
+
domain: "BlePhotoUpload",
|
|
112
|
+
code: -1,
|
|
113
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to decode image data"]
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Bridge.log(
|
|
118
|
+
"\(TAG): Decoded image to bitmap: \(Int(image.size.width))x\(Int(image.size.height))"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
guard var jpegData = image.jpegData(compressionQuality: 0.9) else {
|
|
122
|
+
throw NSError(
|
|
123
|
+
domain: "BlePhotoUpload",
|
|
124
|
+
code: -2,
|
|
125
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to convert image to JPEG"]
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if let imuJson, !imuJson.isEmpty {
|
|
130
|
+
jpegData = try writeImuJsonToJpegData(jpegData, imuJson: imuJson)
|
|
131
|
+
Bridge.log("\(TAG): Re-attached IMU EXIF UserComment on output JPEG (\(imuJson.count) chars)")
|
|
132
|
+
} else {
|
|
133
|
+
let rawHasExif = containsExifMarker(in: imageData)
|
|
134
|
+
Bridge.log(
|
|
135
|
+
"\(TAG): No IMU from ImageIO (container=\(describeContainer(imageData)), rawHasExifMarker=\(rawHasExif))"
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return jpegData
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static func logIncomingImageDiagnostics(imageData: Data) {
|
|
143
|
+
Bridge.log(
|
|
144
|
+
"\(TAG): BLE image diagnostics: size=\(imageData.count) bytes, container=\(describeContainer(imageData)), rawHasExifMarker=\(containsExifMarker(in: imageData))"
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static func describeContainer(_ data: Data) -> String {
|
|
149
|
+
let bytes = [UInt8](data.prefix(12))
|
|
150
|
+
if bytes.count >= 2, bytes[0] == 0xFF, bytes[1] == 0xD8 { return "jpeg" }
|
|
151
|
+
if bytes.count >= 12, bytes[4] == 0x66, bytes[5] == 0x74, bytes[6] == 0x79, bytes[7] == 0x70 {
|
|
152
|
+
let brand = String(bytes: bytes[8..<12], encoding: .ascii) ?? "?"
|
|
153
|
+
return "iso_bmff/ftyp=\(brand)"
|
|
154
|
+
}
|
|
155
|
+
return "unknown"
|
|
156
|
+
}
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
private static func containsExifMarker(in data: Data) -> Bool {
|
|
159
|
+
let marker: [UInt8] = [0x45, 0x78, 0x69, 0x66, 0, 0]
|
|
160
|
+
let bytes = [UInt8](data)
|
|
161
|
+
guard bytes.count >= marker.count else { return false }
|
|
162
|
+
for i in 0...(bytes.count - marker.count) {
|
|
163
|
+
if Array(bytes[i..<(i + marker.count)]) == marker { return true }
|
|
164
|
+
}
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private static func readImuJsonFromImageData(_ imageData: Data) -> String? {
|
|
169
|
+
// Primary: ImageIO EXIF (works for JPEG and well-formed AVIF)
|
|
170
|
+
if let source = CGImageSourceCreateWithData(imageData as CFData, nil),
|
|
171
|
+
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
|
|
172
|
+
{
|
|
173
|
+
let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any]
|
|
174
|
+
let userComment = exif?[kCGImagePropertyExifUserComment as String] as? String
|
|
175
|
+
let tiff = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any]
|
|
176
|
+
let description = tiff?[kCGImagePropertyTIFFImageDescription as String] as? String
|
|
177
|
+
Bridge.log(
|
|
178
|
+
"\(TAG): ImageIO EXIF UserComment=\(describeExifAttribute(userComment)), ImageDescription=\(describeExifAttribute(description))"
|
|
179
|
+
)
|
|
180
|
+
if let userComment, !userComment.isEmpty { return userComment }
|
|
181
|
+
if let description, !description.isEmpty { return description }
|
|
182
|
+
} else {
|
|
183
|
+
Bridge.log("\(TAG): ImageIO: could not read properties")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fallback: raw TIFF scan for AVIFs where ImageIO doesn't expose the embedded Exif block
|
|
187
|
+
if containsExifMarker(in: imageData) {
|
|
188
|
+
if let tiffResult = readImuJsonFromTiff(imageData) {
|
|
189
|
+
Bridge.log("\(TAG): Read IMU UserComment via TIFF scan (\(tiffResult.count) chars)")
|
|
190
|
+
return tiffResult
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return nil
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Scans raw bytes for the {@code Exif\0\0} TIFF header and reads UserComment (0x9286).
|
|
197
|
+
private static func readImuJsonFromTiff(_ data: Data) -> String? {
|
|
198
|
+
let bytes = [UInt8](data)
|
|
199
|
+
let marker: [UInt8] = [0x45, 0x78, 0x69, 0x66, 0, 0]
|
|
200
|
+
var searchFrom = 0
|
|
201
|
+
while searchFrom <= bytes.count - marker.count {
|
|
202
|
+
guard let exifOff = findBytes(marker, in: bytes, from: searchFrom) else { break }
|
|
203
|
+
let tiff = exifOff + 6
|
|
204
|
+
guard tiff + 8 <= bytes.count else { break }
|
|
205
|
+
let littleEndian = bytes[tiff] == 0x49 && bytes[tiff + 1] == 0x49
|
|
206
|
+
let bigEndian = bytes[tiff] == 0x4D && bytes[tiff + 1] == 0x4D
|
|
207
|
+
guard littleEndian || bigEndian else { searchFrom = exifOff + 1; continue }
|
|
208
|
+
let magic = readU16(bytes, at: tiff + 2, le: littleEndian)
|
|
209
|
+
guard magic == 0x002A else { searchFrom = exifOff + 1; continue }
|
|
210
|
+
let ifd0Off = Int(readU32(bytes, at: tiff + 4, le: littleEndian))
|
|
211
|
+
if let result = readTagFromIfd(
|
|
212
|
+
bytes, tiff: tiff, ifdOff: tiff + ifd0Off, tag: 0x9286, le: littleEndian)
|
|
213
|
+
{
|
|
214
|
+
return result
|
|
125
215
|
}
|
|
216
|
+
searchFrom = exifOff + 1
|
|
126
217
|
}
|
|
218
|
+
return nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static func readTagFromIfd(
|
|
222
|
+
_ bytes: [UInt8], tiff: Int, ifdOff: Int, tag: UInt16, le: Bool
|
|
223
|
+
) -> String? {
|
|
224
|
+
guard ifdOff + 2 <= bytes.count else { return nil }
|
|
225
|
+
let count = Int(readU16(bytes, at: ifdOff, le: le))
|
|
226
|
+
var off = ifdOff + 2
|
|
227
|
+
for _ in 0..<count {
|
|
228
|
+
guard off + 12 <= bytes.count else { break }
|
|
229
|
+
let entryTag = readU16(bytes, at: off, le: le)
|
|
230
|
+
let type = readU16(bytes, at: off + 2, le: le)
|
|
231
|
+
let valueCount = Int(readU32(bytes, at: off + 4, le: le))
|
|
232
|
+
if entryTag == tag {
|
|
233
|
+
let byteLen = valueCount
|
|
234
|
+
let valueOff: Int
|
|
235
|
+
if byteLen > 4 {
|
|
236
|
+
valueOff = tiff + Int(readU32(bytes, at: off + 8, le: le))
|
|
237
|
+
} else {
|
|
238
|
+
valueOff = off + 8
|
|
239
|
+
}
|
|
240
|
+
// UserComment starts with 8-byte charset prefix
|
|
241
|
+
let startOff = (tag == 0x9286 && byteLen > 8) ? valueOff + 8 : valueOff
|
|
242
|
+
let len = (tag == 0x9286 && byteLen > 8) ? byteLen - 8 : byteLen
|
|
243
|
+
guard startOff + len <= bytes.count else { return nil }
|
|
244
|
+
return String(bytes: Array(bytes[startOff..<startOff + len]), encoding: .utf8)?
|
|
245
|
+
.trimmingCharacters(in: .init(charactersIn: "\0"))
|
|
246
|
+
}
|
|
247
|
+
// Follow Exif IFD pointer
|
|
248
|
+
if entryTag == 0x8769, type == 4 {
|
|
249
|
+
let subOff = tiff + Int(readU32(bytes, at: off + 8, le: le))
|
|
250
|
+
if let r = readTagFromIfd(bytes, tiff: tiff, ifdOff: subOff, tag: tag, le: le) {
|
|
251
|
+
return r
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
off += 12
|
|
255
|
+
}
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private static func findBytes(_ needle: [UInt8], in haystack: [UInt8], from: Int) -> Int? {
|
|
260
|
+
guard haystack.count >= needle.count else { return nil }
|
|
261
|
+
for i in from...(haystack.count - needle.count) {
|
|
262
|
+
if Array(haystack[i..<i + needle.count]) == needle { return i }
|
|
263
|
+
}
|
|
264
|
+
return nil
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private static func readU16(_ bytes: [UInt8], at off: Int, le: Bool) -> UInt16 {
|
|
268
|
+
let a = UInt16(bytes[off]), b = UInt16(bytes[off + 1])
|
|
269
|
+
return le ? a | (b << 8) : (a << 8) | b
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private static func readU32(_ bytes: [UInt8], at off: Int, le: Bool) -> UInt32 {
|
|
273
|
+
let a = UInt32(bytes[off]), b = UInt32(bytes[off + 1]),
|
|
274
|
+
c = UInt32(bytes[off + 2]), d = UInt32(bytes[off + 3])
|
|
275
|
+
return le ? a | (b << 8) | (c << 16) | (d << 24) : (a << 24) | (b << 16) | (c << 8) | d
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static func describeExifAttribute(_ value: String?) -> String {
|
|
279
|
+
guard let value else { return "null" }
|
|
280
|
+
if value.isEmpty { return "empty" }
|
|
281
|
+
let preview = value.count > 80 ? String(value.prefix(80)) + "…" : value
|
|
282
|
+
return "len=\(value.count) preview=\"\(preview)\""
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static func writeImuJsonToJpegData(_ jpegData: Data, imuJson: String) throws -> Data {
|
|
286
|
+
guard let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
|
|
287
|
+
let imageType = CGImageSourceGetType(source),
|
|
288
|
+
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)
|
|
289
|
+
else {
|
|
290
|
+
throw PhotoUploadError.decodingFailed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let output = NSMutableData()
|
|
294
|
+
guard let destination = CGImageDestinationCreateWithData(
|
|
295
|
+
output, imageType, 1, nil
|
|
296
|
+
) else {
|
|
297
|
+
throw PhotoUploadError.decodingFailed
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
var properties =
|
|
301
|
+
(CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]) ?? [:]
|
|
302
|
+
var exif =
|
|
303
|
+
(properties[kCGImagePropertyExifDictionary as String] as? [String: Any]) ?? [:]
|
|
304
|
+
exif[kCGImagePropertyExifUserComment as String] = imuJson
|
|
305
|
+
properties[kCGImagePropertyExifDictionary as String] = exif
|
|
306
|
+
|
|
307
|
+
CGImageDestinationAddImage(destination, cgImage, properties as CFDictionary)
|
|
308
|
+
guard CGImageDestinationFinalize(destination) else {
|
|
309
|
+
throw PhotoUploadError.decodingFailed
|
|
310
|
+
}
|
|
311
|
+
return output as Data
|
|
127
312
|
}
|
|
128
313
|
|
|
129
314
|
/**
|
|
130
|
-
* Decode image data (AVIF or JPEG) to UIImage
|
|
315
|
+
* Decode image data (AVIF or JPEG) to UIImage.
|
|
316
|
+
* AVIF arriving from glasses has a TIFF EXIF block appended to {@code mdat}; iOS ImageIO
|
|
317
|
+
* rejects those bytes the same way Android does. Strip the Exif tail before decoding.
|
|
131
318
|
*/
|
|
132
319
|
private static func decodeImage(imageData: Data) -> UIImage? {
|
|
133
|
-
|
|
134
|
-
|
|
320
|
+
let isAvif = isAvifData(imageData)
|
|
321
|
+
var decodeData = imageData
|
|
322
|
+
if isAvif && containsExifMarker(in: imageData) {
|
|
323
|
+
let stripped = stripAvifExifTail(imageData)
|
|
324
|
+
if stripped.count < imageData.count {
|
|
325
|
+
Bridge.log(
|
|
326
|
+
"\(TAG): Stripped Exif metadata item for decode: \(imageData.count) -> \(stripped.count) bytes"
|
|
327
|
+
)
|
|
328
|
+
decodeData = stripped
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if let image = UIImage(data: decodeData) {
|
|
135
333
|
return image
|
|
136
334
|
}
|
|
137
335
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
336
|
+
if isAvif {
|
|
337
|
+
if #available(iOS 16.0, *) {
|
|
338
|
+
return UIImage(data: decodeData)
|
|
339
|
+
} else {
|
|
340
|
+
Bridge.log("\(TAG): AVIF decoding not supported on iOS < 16")
|
|
341
|
+
return nil
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return nil
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private static func isAvifData(_ data: Data) -> Bool {
|
|
348
|
+
let bytes = [UInt8](data.prefix(12))
|
|
349
|
+
return bytes.count >= 12
|
|
350
|
+
&& bytes[4] == 0x66 && bytes[5] == 0x74 && bytes[6] == 0x79 && bytes[7] == 0x70
|
|
351
|
+
&& bytes[8] == 0x61 && bytes[9] == 0x76 && bytes[10] == 0x69 && bytes[11] == 0x66
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Truncates the {@code mdat} box at the {@code Exif\0\0} marker, removing the TIFF EXIF
|
|
355
|
+
/// block that the glasses encoder appends. Returns original data unchanged on any parse error.
|
|
356
|
+
private static func stripAvifExifTail(_ data: Data) -> Data {
|
|
357
|
+
let bytes = [UInt8](data)
|
|
358
|
+
let marker: [UInt8] = [0x45, 0x78, 0x69, 0x66, 0, 0]
|
|
359
|
+
|
|
360
|
+
// Find last Exif marker
|
|
361
|
+
var lastExif = -1
|
|
362
|
+
for i in 0...(bytes.count - marker.count) {
|
|
363
|
+
if Array(bytes[i..<i + marker.count]) == marker { lastExif = i }
|
|
364
|
+
}
|
|
365
|
+
guard lastExif >= 0 else { return data }
|
|
366
|
+
|
|
367
|
+
// Walk top-level boxes to find mdat
|
|
368
|
+
var off = 0
|
|
369
|
+
while off + 8 <= bytes.count {
|
|
370
|
+
let boxSize = Int(readU32BE(bytes, at: off))
|
|
371
|
+
guard boxSize >= 8, off + boxSize <= bytes.count else { break }
|
|
372
|
+
let boxType = String(bytes: Array(bytes[off + 4..<off + 8]), encoding: .ascii) ?? ""
|
|
373
|
+
if boxType == "mdat" {
|
|
374
|
+
let payloadStart = off + 8
|
|
375
|
+
let payloadEnd = off + boxSize
|
|
376
|
+
guard lastExif >= payloadStart, lastExif < payloadEnd else { break }
|
|
377
|
+
let newPayloadLen = lastExif - payloadStart
|
|
378
|
+
guard newPayloadLen > 0 else { break }
|
|
379
|
+
|
|
380
|
+
var result = Data()
|
|
381
|
+
// Everything up to mdat header
|
|
382
|
+
result.append(contentsOf: bytes[0..<off])
|
|
383
|
+
// New mdat box header with updated size
|
|
384
|
+
let newMdatSize = UInt32(8 + newPayloadLen)
|
|
385
|
+
result.append(UInt8((newMdatSize >> 24) & 0xFF))
|
|
386
|
+
result.append(UInt8((newMdatSize >> 16) & 0xFF))
|
|
387
|
+
result.append(UInt8((newMdatSize >> 8) & 0xFF))
|
|
388
|
+
result.append(UInt8(newMdatSize & 0xFF))
|
|
389
|
+
result.append(contentsOf: [0x6D, 0x64, 0x61, 0x74]) // "mdat"
|
|
390
|
+
result.append(contentsOf: bytes[payloadStart..<payloadStart + newPayloadLen])
|
|
391
|
+
return result
|
|
392
|
+
}
|
|
393
|
+
off += boxSize
|
|
148
394
|
}
|
|
395
|
+
return data
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private static func readU32BE(_ bytes: [UInt8], at off: Int) -> UInt32 {
|
|
399
|
+
UInt32(bytes[off]) << 24 | UInt32(bytes[off + 1]) << 16
|
|
400
|
+
| UInt32(bytes[off + 2]) << 8 | UInt32(bytes[off + 3])
|
|
149
401
|
}
|
|
150
402
|
|
|
151
403
|
private static func uploadToWebhook(
|
|
@@ -153,10 +405,10 @@ class BlePhotoUploadService {
|
|
|
153
405
|
requestId: String,
|
|
154
406
|
webhookUrl: String,
|
|
155
407
|
authToken: String?
|
|
156
|
-
) async throws {
|
|
408
|
+
) async throws -> String {
|
|
157
409
|
guard let url = URL(string: webhookUrl) else {
|
|
158
410
|
Bridge.log("LIVE: Invalid webhook URL: \(webhookUrl)")
|
|
159
|
-
|
|
411
|
+
throw PhotoUploadError.uploadFailed("Invalid webhook URL")
|
|
160
412
|
}
|
|
161
413
|
|
|
162
414
|
var request = URLRequest(url: url)
|
|
@@ -220,6 +472,7 @@ class BlePhotoUploadService {
|
|
|
220
472
|
}
|
|
221
473
|
|
|
222
474
|
print("LIVE: Upload successful. Response code: \(httpResponse.statusCode)")
|
|
475
|
+
return String(data: data, encoding: .utf8) ?? ""
|
|
223
476
|
|
|
224
477
|
} catch {
|
|
225
478
|
if error is PhotoUploadError {
|
|
@@ -546,31 +799,36 @@ private final class BleIncidentLogRelayEntry {
|
|
|
546
799
|
// MARK: - CBCentralManagerDelegate
|
|
547
800
|
|
|
548
801
|
extension MentraLive: CBCentralManagerDelegate {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
802
|
+
// CoreBluetooth delivers these callbacks on `bluetoothQueue`; hop back before touching MainActor state.
|
|
803
|
+
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
804
|
+
let state = central.state
|
|
805
|
+
DispatchQueue.main.async { [weak self] in
|
|
806
|
+
guard let self else { return }
|
|
807
|
+
switch state {
|
|
808
|
+
case .poweredOn:
|
|
809
|
+
Bridge.log("LIVE: Bluetooth powered on")
|
|
810
|
+
// If we have a saved device, try to reconnect
|
|
811
|
+
if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
|
|
812
|
+
!savedDeviceName.isEmpty
|
|
813
|
+
{
|
|
814
|
+
self.startScan()
|
|
815
|
+
}
|
|
559
816
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
817
|
+
case .poweredOff:
|
|
818
|
+
Bridge.log("LIVE: Bluetooth is powered off")
|
|
819
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
563
820
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
821
|
+
case .unauthorized:
|
|
822
|
+
Bridge.log("LIVE: Bluetooth is unauthorized")
|
|
823
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
567
824
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
825
|
+
case .unsupported:
|
|
826
|
+
Bridge.log("LIVE: Bluetooth is unsupported")
|
|
827
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
571
828
|
|
|
572
|
-
|
|
573
|
-
|
|
829
|
+
default:
|
|
830
|
+
Bridge.log("LIVE: Bluetooth state: \(state.rawValue)")
|
|
831
|
+
}
|
|
574
832
|
}
|
|
575
833
|
}
|
|
576
834
|
|
|
@@ -602,79 +860,89 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
602
860
|
}
|
|
603
861
|
}
|
|
604
862
|
|
|
605
|
-
func centralManager(
|
|
863
|
+
nonisolated func centralManager(
|
|
606
864
|
_: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
607
865
|
advertisementData _: [String: Any], rssi: NSNumber
|
|
608
866
|
) {
|
|
609
|
-
|
|
867
|
+
DispatchQueue.main.async { [weak self] in
|
|
868
|
+
self?.handleDiscoveredPeripheral(peripheral, rssi: rssi)
|
|
869
|
+
}
|
|
610
870
|
}
|
|
611
871
|
|
|
612
|
-
func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
613
|
-
|
|
872
|
+
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
873
|
+
DispatchQueue.main.async { [weak self] in
|
|
874
|
+
guard let self else { return }
|
|
875
|
+
Bridge.log("Connected to GATT server, discovering services...")
|
|
614
876
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
877
|
+
self.stopConnectionTimeout()
|
|
878
|
+
self.isConnecting = false
|
|
879
|
+
self.connectedPeripheral = peripheral
|
|
618
880
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
881
|
+
// Save device name and address for future reconnection
|
|
882
|
+
if let name = peripheral.name {
|
|
883
|
+
UserDefaults.standard.set(name, forKey: PREFS_DEVICE_NAME)
|
|
884
|
+
Bridge.log("Saved device name for future reconnection: \(name)")
|
|
885
|
+
DeviceStore.shared.apply("glasses", "bluetoothName", name)
|
|
886
|
+
}
|
|
887
|
+
// Persist peripheral UUID so DeviceManager can sync it to RN settings
|
|
888
|
+
DeviceStore.shared.apply("bluetooth", "device_address", peripheral.identifier.uuidString)
|
|
627
889
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
890
|
+
// Audio Pairing: Setup Bluetooth audio after BLE connection
|
|
891
|
+
if let deviceName = peripheral.name {
|
|
892
|
+
Bridge.log("BLE connection established, setting up audio...")
|
|
893
|
+
// setupAudioPairing(deviceName: deviceName)
|
|
894
|
+
}
|
|
633
895
|
|
|
634
|
-
|
|
635
|
-
|
|
896
|
+
// Discover services
|
|
897
|
+
peripheral.discoverServices([SERVICE_UUID])
|
|
636
898
|
|
|
637
|
-
|
|
638
|
-
|
|
899
|
+
// Reset reconnect attempts
|
|
900
|
+
self.reconnectAttempts = 0
|
|
901
|
+
}
|
|
639
902
|
}
|
|
640
903
|
|
|
641
|
-
func centralManager(
|
|
904
|
+
nonisolated func centralManager(
|
|
642
905
|
_: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error _: Error?
|
|
643
906
|
) {
|
|
644
|
-
|
|
907
|
+
DispatchQueue.main.async { [weak self] in
|
|
908
|
+
guard let self else { return }
|
|
909
|
+
Bridge.log("LIVE: Disconnected from GATT server")
|
|
645
910
|
|
|
646
|
-
|
|
911
|
+
self.isConnecting = false
|
|
647
912
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
913
|
+
self.connectedPeripheral = nil
|
|
914
|
+
self.fullyBooted = false
|
|
915
|
+
self.connected = false
|
|
916
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
917
|
+
self.rgbLedAuthorityClaimed = false
|
|
653
918
|
|
|
654
|
-
|
|
919
|
+
self.stopAllTimers()
|
|
655
920
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
921
|
+
// Clean up characteristics
|
|
922
|
+
self.txCharacteristic = nil
|
|
923
|
+
self.rxCharacteristic = nil
|
|
659
924
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
925
|
+
// Attempt reconnection if not killed
|
|
926
|
+
if !self.isKilled {
|
|
927
|
+
self.handleReconnection()
|
|
928
|
+
}
|
|
663
929
|
}
|
|
664
930
|
}
|
|
665
931
|
|
|
666
|
-
func centralManager(_: CBCentralManager, didFailToConnect _: CBPeripheral, error: Error?) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
932
|
+
nonisolated func centralManager(_: CBCentralManager, didFailToConnect _: CBPeripheral, error: Error?) {
|
|
933
|
+
let errorDescription = error?.localizedDescription ?? "Unknown error"
|
|
934
|
+
DispatchQueue.main.async { [weak self] in
|
|
935
|
+
guard let self else { return }
|
|
936
|
+
Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
|
|
670
937
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
938
|
+
self.stopConnectionTimeout()
|
|
939
|
+
self.isConnecting = false
|
|
940
|
+
self.connectedPeripheral = nil
|
|
941
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
675
942
|
|
|
676
|
-
|
|
677
|
-
|
|
943
|
+
if !self.isKilled {
|
|
944
|
+
self.handleReconnection()
|
|
945
|
+
}
|
|
678
946
|
}
|
|
679
947
|
}
|
|
680
948
|
}
|
|
@@ -682,201 +950,228 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
682
950
|
// MARK: - CBPeripheralDelegate
|
|
683
951
|
|
|
684
952
|
extension MentraLive: CBPeripheralDelegate {
|
|
685
|
-
func peripheral(_: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
953
|
+
nonisolated func peripheral(_: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
954
|
+
let errorDescription = error?.localizedDescription
|
|
955
|
+
DispatchQueue.main.async { [weak self] in
|
|
956
|
+
guard let self else { return }
|
|
957
|
+
self.signalStrengthReadInFlight = false
|
|
958
|
+
if let errorDescription {
|
|
959
|
+
Bridge.log("LIVE: Error reading RSSI: \(errorDescription)")
|
|
960
|
+
} else {
|
|
961
|
+
self.updateSignalStrength(Int(truncating: RSSI))
|
|
962
|
+
}
|
|
691
963
|
}
|
|
692
964
|
}
|
|
693
965
|
|
|
694
|
-
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
966
|
+
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
967
|
+
let errorDescription = error?.localizedDescription
|
|
968
|
+
DispatchQueue.main.async { [weak self] in
|
|
969
|
+
guard let self else { return }
|
|
970
|
+
if let errorDescription {
|
|
971
|
+
Bridge.log("LIVE: Error discovering services: \(errorDescription)")
|
|
972
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
973
|
+
return
|
|
974
|
+
}
|
|
700
975
|
|
|
701
|
-
|
|
976
|
+
guard let services = peripheral.services else { return }
|
|
702
977
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
978
|
+
for service in services where service.uuid == SERVICE_UUID {
|
|
979
|
+
Bridge.log("LIVE: Found UART service, discovering characteristics...")
|
|
980
|
+
peripheral.discoverCharacteristics(
|
|
981
|
+
[
|
|
982
|
+
TX_CHAR_UUID, RX_CHAR_UUID, FILE_READ_UUID, FILE_WRITE_UUID, LC3_READ_UUID,
|
|
983
|
+
LC3_WRITE_UUID,
|
|
984
|
+
], for: service
|
|
985
|
+
)
|
|
986
|
+
}
|
|
711
987
|
}
|
|
712
988
|
}
|
|
713
989
|
|
|
714
|
-
func peripheral(
|
|
990
|
+
nonisolated func peripheral(
|
|
715
991
|
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?
|
|
716
992
|
) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
993
|
+
let errorDescription = error?.localizedDescription
|
|
994
|
+
DispatchQueue.main.async { [weak self] in
|
|
995
|
+
guard let self else { return }
|
|
996
|
+
if let errorDescription {
|
|
997
|
+
Bridge.log("LIVE: Error discovering characteristics: \(errorDescription)")
|
|
998
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
722
1001
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1002
|
+
guard let characteristics = service.characteristics else { return }
|
|
1003
|
+
|
|
1004
|
+
for characteristic in characteristics {
|
|
1005
|
+
// Log characteristic properties for debugging
|
|
1006
|
+
let props = characteristic.properties
|
|
1007
|
+
let propsStr = [
|
|
1008
|
+
props.contains(.notify) ? "NOTIFY" : nil,
|
|
1009
|
+
props.contains(.indicate) ? "INDICATE" : nil,
|
|
1010
|
+
props.contains(.read) ? "READ" : nil,
|
|
1011
|
+
props.contains(.write) ? "WRITE" : nil,
|
|
1012
|
+
props.contains(.writeWithoutResponse) ? "WRITE_NO_RESPONSE" : nil,
|
|
1013
|
+
].compactMap { $0 }.joined(separator: " ")
|
|
1014
|
+
Bridge.log("📋 Characteristic \(characteristic.uuid): properties=[\(propsStr)]")
|
|
1015
|
+
|
|
1016
|
+
if characteristic.uuid == TX_CHAR_UUID {
|
|
1017
|
+
self.txCharacteristic = characteristic
|
|
1018
|
+
Bridge.log("LIVE: ✅ Found TX characteristic")
|
|
1019
|
+
} else if characteristic.uuid == RX_CHAR_UUID {
|
|
1020
|
+
self.rxCharacteristic = characteristic
|
|
1021
|
+
Bridge.log(
|
|
1022
|
+
"LIVE: ✅ Found RX characteristic - hasNotify=\(props.contains(.notify)), hasIndicate=\(props.contains(.indicate))"
|
|
1023
|
+
)
|
|
1024
|
+
} else if characteristic.uuid == FILE_READ_UUID {
|
|
1025
|
+
self.fileReadCharacteristic = characteristic
|
|
1026
|
+
Bridge.log("LIVE: 📁 Found FILE_READ characteristic (72FF)!")
|
|
1027
|
+
} else if characteristic.uuid == FILE_WRITE_UUID {
|
|
1028
|
+
self.fileWriteCharacteristic = characteristic
|
|
1029
|
+
Bridge.log("LIVE: 📁 Found FILE_WRITE characteristic (73FF)!")
|
|
1030
|
+
} else if characteristic.uuid == LC3_READ_UUID {
|
|
1031
|
+
self.lc3ReadCharacteristic = characteristic
|
|
1032
|
+
Bridge.log("LIVE: 🎤 Found LC3_READ characteristic (audio input)!")
|
|
1033
|
+
} else if characteristic.uuid == LC3_WRITE_UUID {
|
|
1034
|
+
self.lc3WriteCharacteristic = characteristic
|
|
1035
|
+
Bridge.log("LIVE: 🎤 Found LC3_WRITE characteristic (audio output)!")
|
|
1036
|
+
}
|
|
757
1037
|
}
|
|
758
|
-
}
|
|
759
1038
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1039
|
+
// Check if we have both characteristics
|
|
1040
|
+
if self.txCharacteristic != nil, let rx = self.rxCharacteristic {
|
|
1041
|
+
Bridge.log("LIVE: ✅ Both TX and RX characteristics found - BLE connection ready")
|
|
1042
|
+
Bridge.log("LIVE: 🔄 Waiting for glasses SOC to become ready...")
|
|
764
1043
|
|
|
765
|
-
|
|
766
|
-
|
|
1044
|
+
// Don't set connected=true here - wait for SOC to be ready (fullyBooted=true)
|
|
1045
|
+
// DeviceStore handles connected state based on fullyBooted
|
|
767
1046
|
|
|
768
|
-
|
|
769
|
-
|
|
1047
|
+
// Keep state as connecting until glasses are ready
|
|
1048
|
+
self.updateConnectionState(ConnTypes.CONNECTING)
|
|
770
1049
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1050
|
+
let withResponseMtu = peripheral.maximumWriteValueLength(for: .withResponse) + 3
|
|
1051
|
+
let withoutResponseMtu = peripheral.maximumWriteValueLength(for: .withoutResponse) + 3
|
|
1052
|
+
self.currentMtu = max(23, min(withResponseMtu, withoutResponseMtu))
|
|
1053
|
+
Bridge.log(
|
|
1054
|
+
"LIVE: Current MTU estimate: withResponse=\(withResponseMtu), withoutResponse=\(withoutResponseMtu), selected=\(self.currentMtu)"
|
|
1055
|
+
)
|
|
777
1056
|
|
|
778
|
-
|
|
779
|
-
|
|
1057
|
+
// Enable notifications on RX characteristic
|
|
1058
|
+
peripheral.setNotifyValue(true, for: rx)
|
|
780
1059
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1060
|
+
// Enable notifications on file characteristics if available
|
|
1061
|
+
if let fileRead = self.fileReadCharacteristic {
|
|
1062
|
+
peripheral.setNotifyValue(true, for: fileRead)
|
|
1063
|
+
}
|
|
785
1064
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1065
|
+
// Enable notifications on LC3 audio characteristic if device supports it
|
|
1066
|
+
if self.supportsLC3Audio, let lc3Read = self.lc3ReadCharacteristic {
|
|
1067
|
+
peripheral.setNotifyValue(true, for: lc3Read)
|
|
1068
|
+
Bridge.log("LIVE: 🎤 Enabled LC3 audio notifications")
|
|
1069
|
+
}
|
|
791
1070
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1071
|
+
// Start readiness check loop
|
|
1072
|
+
self.startSignalStrengthPolling()
|
|
1073
|
+
self.startReadinessCheckLoop()
|
|
1074
|
+
} else {
|
|
1075
|
+
Bridge.log("LIVE: Required BLE characteristics not found")
|
|
1076
|
+
if self.txCharacteristic == nil {
|
|
1077
|
+
Bridge.log("LIVE: TX characteristic not found")
|
|
1078
|
+
}
|
|
1079
|
+
if self.rxCharacteristic == nil {
|
|
1080
|
+
Bridge.log("LIVE: RX characteristic not found")
|
|
1081
|
+
}
|
|
1082
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
802
1083
|
}
|
|
803
|
-
centralManager?.cancelPeripheralConnection(peripheral)
|
|
804
1084
|
}
|
|
805
1085
|
}
|
|
806
1086
|
|
|
807
|
-
func peripheral(
|
|
1087
|
+
nonisolated func peripheral(
|
|
808
1088
|
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
809
1089
|
) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1090
|
+
let uuid = characteristic.uuid
|
|
1091
|
+
let data = characteristic.value
|
|
1092
|
+
let errorDescription = error?.localizedDescription
|
|
1093
|
+
DispatchQueue.main.async { [weak self] in
|
|
1094
|
+
guard let self else { return }
|
|
1095
|
+
// Bridge.log("LIVE: DEBUG: didUpdateValueFor CALLED - characteristic: \(uuid), dataSize: \(data?.count ?? 0)")
|
|
1096
|
+
// Log raw hex for debugging glasses_ready issue
|
|
1097
|
+
if let data {
|
|
1098
|
+
let hexString = data.prefix(50).map { String(format: "%02X ", $0) }.joined()
|
|
1099
|
+
_ = hexString
|
|
1100
|
+
// Bridge.log("LIVE: DEBUG: RAW HEX (first 50): \(hexString)")
|
|
1101
|
+
}
|
|
1102
|
+
if let errorDescription {
|
|
1103
|
+
Bridge.log("LIVE: Error updating value for characteristic: \(errorDescription)")
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
822
1106
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1107
|
+
guard let data else {
|
|
1108
|
+
Bridge.log("LIVE: Characteristic value is nil")
|
|
1109
|
+
return
|
|
1110
|
+
}
|
|
827
1111
|
|
|
828
|
-
|
|
829
|
-
|
|
1112
|
+
let threadId = Thread.current.hash
|
|
1113
|
+
_ = threadId
|
|
830
1114
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1115
|
+
// Bridge.log("Thread-\(threadId): 🎉 didUpdateValueFor CALLBACK TRIGGERED! Characteristic: \(uuid)")
|
|
1116
|
+
// if uuid == RX_CHAR_UUID {
|
|
1117
|
+
// Bridge.log("Thread-\(threadId): 🎯 RECEIVED DATA ON RX CHARACTERISTIC (Peripheral's TX)")
|
|
1118
|
+
// } else if uuid == TX_CHAR_UUID {
|
|
1119
|
+
// Bridge.log("Thread-\(threadId): 🎯 RECEIVED DATA ON TX CHARACTERISTIC (Peripheral's RX)")
|
|
1120
|
+
// }
|
|
1121
|
+
// Bridge.log("Thread-\(threadId): 🔍 Processing received data - \(data.count) bytes")
|
|
838
1122
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1123
|
+
// Handle LC3 audio data separately (dedicated characteristic for LC3-capable devices)
|
|
1124
|
+
if uuid == LC3_READ_UUID && self.supportsLC3Audio {
|
|
1125
|
+
// Bridge.log("LIVE: Received data on LC3_READ characteristic (audio input)")
|
|
1126
|
+
self.processLc3AudioPacket(data)
|
|
1127
|
+
return
|
|
1128
|
+
}
|
|
845
1129
|
|
|
846
|
-
|
|
847
|
-
|
|
1130
|
+
// Handle regular data (JSON messages, file transfers, etc.)
|
|
1131
|
+
self.processReceivedData(data)
|
|
1132
|
+
}
|
|
848
1133
|
}
|
|
849
1134
|
|
|
850
|
-
func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1135
|
+
nonisolated func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
|
|
1136
|
+
let errorDescription = error?.localizedDescription
|
|
1137
|
+
DispatchQueue.main.async {
|
|
1138
|
+
if let errorDescription {
|
|
1139
|
+
Bridge.log("LIVE: Error writing characteristic: \(errorDescription)")
|
|
1140
|
+
} else {
|
|
1141
|
+
Bridge.log("LIVE: Characteristic write successful")
|
|
1142
|
+
}
|
|
855
1143
|
}
|
|
856
1144
|
}
|
|
857
1145
|
|
|
858
|
-
func peripheral(
|
|
1146
|
+
nonisolated func peripheral(
|
|
859
1147
|
_: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
860
1148
|
error: Error?
|
|
861
1149
|
) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1150
|
+
let uuid = characteristic.uuid
|
|
1151
|
+
let isNotifying = characteristic.isNotifying
|
|
1152
|
+
let errorDescription = error?.localizedDescription
|
|
1153
|
+
DispatchQueue.main.async { [weak self] in
|
|
1154
|
+
guard let self else { return }
|
|
1155
|
+
if let errorDescription {
|
|
1156
|
+
Bridge.log("LIVE: Error updating notification state: \(errorDescription)")
|
|
1157
|
+
} else {
|
|
1158
|
+
Bridge.log("Notification state updated for \(uuid): \(isNotifying ? "ON" : "OFF")")
|
|
868
1159
|
|
|
869
|
-
|
|
870
|
-
|
|
1160
|
+
if uuid == self.RX_CHAR_UUID, isNotifying {
|
|
1161
|
+
Bridge.log("LIVE: 🔔 Ready to receive data via notifications")
|
|
1162
|
+
}
|
|
871
1163
|
}
|
|
872
1164
|
}
|
|
873
1165
|
}
|
|
874
1166
|
|
|
875
|
-
func peripheralDidUpdateRSSI(_ peripheral: CBPeripheral, error: Error?) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1167
|
+
nonisolated func peripheralDidUpdateRSSI(_ peripheral: CBPeripheral, error: Error?) {
|
|
1168
|
+
let errorDescription = error?.localizedDescription
|
|
1169
|
+
DispatchQueue.main.async {
|
|
1170
|
+
if let errorDescription {
|
|
1171
|
+
Bridge.log("LIVE: Error reading RSSI: \(errorDescription)")
|
|
1172
|
+
} else {
|
|
1173
|
+
Bridge.log("LIVE: RSSI: \(peripheral.readRSSI())")
|
|
1174
|
+
}
|
|
880
1175
|
}
|
|
881
1176
|
}
|
|
882
1177
|
}
|
|
@@ -918,6 +1213,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
918
1213
|
// a previous pairing into the next one (would otherwise surface as wrong overall_percent
|
|
919
1214
|
// or stale lastBesOtaProgress on the next OTA).
|
|
920
1215
|
if state == ConnTypes.DISCONNECTED {
|
|
1216
|
+
incomingChunkReassembler.clear()
|
|
921
1217
|
stopSignalStrengthPolling()
|
|
922
1218
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
923
1219
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -929,7 +1225,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
929
1225
|
func setSilentMode(_: Bool) {}
|
|
930
1226
|
func exit() {}
|
|
931
1227
|
func showDashboard() {}
|
|
932
|
-
func displayBitmap(base64ImageData _: String) async -> Bool {
|
|
1228
|
+
func displayBitmap(base64ImageData _: String, x _: Int32? = nil, y _: Int32? = nil, width _: Int32? = nil, height _: Int32? = nil) async -> Bool {
|
|
933
1229
|
return true
|
|
934
1230
|
}
|
|
935
1231
|
|
|
@@ -1317,10 +1613,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1317
1613
|
|
|
1318
1614
|
func requestPhoto(
|
|
1319
1615
|
_ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
|
|
1320
|
-
compress: String?, flash: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
1616
|
+
compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
|
|
1321
1617
|
) {
|
|
1322
1618
|
Bridge.log(
|
|
1323
|
-
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
|
|
1619
|
+
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) save=\(save) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
|
|
1324
1620
|
)
|
|
1325
1621
|
|
|
1326
1622
|
var json: [String: Any] = [
|
|
@@ -1366,6 +1662,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1366
1662
|
json["compress"] = compress ?? "none"
|
|
1367
1663
|
|
|
1368
1664
|
json["flash"] = flash
|
|
1665
|
+
json["save"] = save
|
|
1369
1666
|
json["sound"] = sound
|
|
1370
1667
|
|
|
1371
1668
|
if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
|
|
@@ -1838,7 +2135,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1838
2135
|
updateBatteryStatus(level: level, isCharging: isCharging)
|
|
1839
2136
|
|
|
1840
2137
|
case "voice_activity_detection_status":
|
|
1841
|
-
let enabled = json["voiceActivityDetectionEnabled"] as? Bool
|
|
2138
|
+
let enabled = json["voiceActivityDetectionEnabled"] as? Bool
|
|
2139
|
+
?? BluetoothSdkDefaults.voiceActivityDetectionEnabled
|
|
1842
2140
|
handleVoiceActivityDetectionStatus(enabled: enabled)
|
|
1843
2141
|
|
|
1844
2142
|
case "speaking_status":
|
|
@@ -1870,21 +2168,34 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1870
2168
|
case "stream_status":
|
|
1871
2169
|
emitRtmpStreamStatus(json)
|
|
1872
2170
|
|
|
2171
|
+
case "video_recording_status":
|
|
2172
|
+
emitVideoRecordingStatus(json)
|
|
2173
|
+
|
|
1873
2174
|
case "photo_status":
|
|
1874
2175
|
emitPhotoStatus(json)
|
|
1875
2176
|
|
|
2177
|
+
case "photo_response":
|
|
2178
|
+
emitPhotoResponse(json)
|
|
2179
|
+
|
|
1876
2180
|
case "gallery_status":
|
|
1877
2181
|
let photoCount = json["photos"] as? Int ?? 0
|
|
1878
2182
|
let videoCount = json["videos"] as? Int ?? 0
|
|
1879
2183
|
let totalCount = json["total"] as? Int ?? 0
|
|
1880
2184
|
let totalSize = json["total_size"] as? Int64 ?? 0
|
|
1881
2185
|
let hasContent = json["has_content"] as? Bool ?? false
|
|
2186
|
+
let cameraBusy = Self.galleryCameraBusy(json)
|
|
2187
|
+
let cameraBusyReason = Self.galleryCameraBusyReason(json)
|
|
1882
2188
|
handleGalleryStatus(
|
|
1883
2189
|
photoCount: photoCount, videoCount: videoCount,
|
|
1884
2190
|
totalCount: totalCount, totalSize: totalSize,
|
|
1885
|
-
hasContent: hasContent
|
|
2191
|
+
hasContent: hasContent,
|
|
2192
|
+
cameraBusy: cameraBusy,
|
|
2193
|
+
cameraBusyReason: cameraBusyReason
|
|
1886
2194
|
)
|
|
1887
2195
|
|
|
2196
|
+
case "settings_ack":
|
|
2197
|
+
emitSettingsAck(json)
|
|
2198
|
+
|
|
1888
2199
|
case "button_press":
|
|
1889
2200
|
handleButtonPress(json)
|
|
1890
2201
|
|
|
@@ -2035,6 +2346,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2035
2346
|
|
|
2036
2347
|
Bridge.log("LIVE: 📱 OTA status - step \(osCurrentStep)/\(osTotalSteps) \(osPhase) \(osStatus) \(osOverallPercent)%")
|
|
2037
2348
|
|
|
2349
|
+
let glassesTimeMs = (json["glasses_time_ms"] as? NSNumber)?.int64Value ?? 0
|
|
2038
2350
|
Bridge.sendOtaStatus(
|
|
2039
2351
|
sessionId: osSessionId,
|
|
2040
2352
|
totalSteps: osTotalSteps,
|
|
@@ -2044,7 +2356,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2044
2356
|
stepPercent: osStepPercent,
|
|
2045
2357
|
overallPercent: osOverallPercent,
|
|
2046
2358
|
status: osStatus,
|
|
2047
|
-
errorMessage: osErrorMessage
|
|
2359
|
+
errorMessage: osErrorMessage,
|
|
2360
|
+
glassesTimeMs: glassesTimeMs > 0 ? glassesTimeMs : nil
|
|
2048
2361
|
)
|
|
2049
2362
|
|
|
2050
2363
|
case "ota_progress":
|
|
@@ -2125,12 +2438,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2125
2438
|
if let bluetoothMacAddress = fields["bt_mac_address"] as? String {
|
|
2126
2439
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2127
2440
|
}
|
|
2441
|
+
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2442
|
+
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2443
|
+
}
|
|
2128
2444
|
|
|
2129
|
-
|
|
2130
|
-
// All fields including mtk_fw_version are forwarded to RN
|
|
2131
|
-
// Bridge.sendTypedMessage("version_info", body: fields)
|
|
2132
|
-
|
|
2133
|
-
// Bridge.log("LIVE: Processed version_info fields and sent to RN")
|
|
2445
|
+
Bridge.sendVersionInfo(fields)
|
|
2134
2446
|
} else {
|
|
2135
2447
|
Bridge.log("Unhandled message type: \(type)")
|
|
2136
2448
|
}
|
|
@@ -2461,6 +2773,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2461
2773
|
sendJson(json, wakeUp: true)
|
|
2462
2774
|
}
|
|
2463
2775
|
|
|
2776
|
+
func sendSetSystemTime(_ timestampMs: Int64) {
|
|
2777
|
+
Bridge.log("LIVE: ⏰ Sending set_system_time: \(timestampMs)")
|
|
2778
|
+
|
|
2779
|
+
let json: [String: Any] = [
|
|
2780
|
+
"type": "set_system_time",
|
|
2781
|
+
"timestamp_ms": timestampMs,
|
|
2782
|
+
]
|
|
2783
|
+
|
|
2784
|
+
sendJson(json, wakeUp: true)
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2464
2787
|
func sendUserEmailToGlasses(_ email: String) {
|
|
2465
2788
|
Bridge.log("LIVE: Sending user email to glasses for crash reporting")
|
|
2466
2789
|
|
|
@@ -2536,13 +2859,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2536
2859
|
|
|
2537
2860
|
func sendGalleryMode() {
|
|
2538
2861
|
let active = DeviceStore.shared.get("bluetooth", "gallery_mode") as! Bool
|
|
2862
|
+
sendGalleryMode(requestId: nil, active: active)
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
func sendGalleryMode(requestId: String?, active: Bool) {
|
|
2539
2866
|
Bridge.log("LIVE: 📸 Sending gallery mode active to glasses: \(active)")
|
|
2540
2867
|
|
|
2541
|
-
|
|
2868
|
+
var json: [String: Any] = [
|
|
2542
2869
|
"type": "save_in_gallery_mode",
|
|
2543
2870
|
"active": active,
|
|
2544
2871
|
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
2545
2872
|
]
|
|
2873
|
+
if let requestId, !requestId.isEmpty {
|
|
2874
|
+
json["request_id"] = requestId
|
|
2875
|
+
}
|
|
2546
2876
|
|
|
2547
2877
|
sendJson(json, wakeUp: true)
|
|
2548
2878
|
}
|
|
@@ -2572,6 +2902,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2572
2902
|
sendJson(json, wakeUp: true)
|
|
2573
2903
|
}
|
|
2574
2904
|
|
|
2905
|
+
func sendOtaRetryVersionCheck() {
|
|
2906
|
+
Bridge.log("LIVE: ⏰ Sending ota_retry_version_check command to glasses")
|
|
2907
|
+
|
|
2908
|
+
let json: [String: Any] = [
|
|
2909
|
+
"type": "ota_retry_version_check",
|
|
2910
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
2911
|
+
]
|
|
2912
|
+
|
|
2913
|
+
sendJson(json, wakeUp: true)
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2575
2916
|
func keepAwake() {
|
|
2576
2917
|
Bridge.log("LIVE: 📱 Sending keep_awake command to glasses")
|
|
2577
2918
|
|
|
@@ -2644,7 +2985,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2644
2985
|
networks = networksNeoArray
|
|
2645
2986
|
}
|
|
2646
2987
|
|
|
2647
|
-
|
|
2988
|
+
let scanComplete = json["scan_complete"] as? Bool ?? json["scanComplete"] as? Bool ?? false
|
|
2989
|
+
Bridge.updateWifiScanResults(networks, scanComplete: scanComplete)
|
|
2648
2990
|
}
|
|
2649
2991
|
|
|
2650
2992
|
private func handleButtonPress(_ json: [String: Any]) {
|
|
@@ -3226,7 +3568,53 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3226
3568
|
BlePhotoUploadService.processAndUploadPhoto(
|
|
3227
3569
|
imageData: imageData, requestId: transfer.requestId, webhookUrl: transfer.webhookUrl,
|
|
3228
3570
|
authToken: transfer.authToken
|
|
3229
|
-
)
|
|
3571
|
+
) { [weak self] requestId, responseBody in
|
|
3572
|
+
self?.sendPhotoTerminalSuccessResponse(
|
|
3573
|
+
requestId: requestId,
|
|
3574
|
+
uploadUrl: transfer.webhookUrl,
|
|
3575
|
+
responseBody: responseBody
|
|
3576
|
+
)
|
|
3577
|
+
} onError: { requestId, error in
|
|
3578
|
+
Bridge.sendPhotoError(
|
|
3579
|
+
requestId: requestId,
|
|
3580
|
+
errorCode: "PHONE_UPLOAD_FAILED",
|
|
3581
|
+
errorMessage: "BLE photo upload failed: \(error)"
|
|
3582
|
+
)
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
private func sendPhotoTerminalSuccessResponse(
|
|
3587
|
+
requestId: String,
|
|
3588
|
+
uploadUrl: String,
|
|
3589
|
+
responseBody: String
|
|
3590
|
+
) {
|
|
3591
|
+
var event: [String: Any] = [
|
|
3592
|
+
"type": "photo_response",
|
|
3593
|
+
"state": "success",
|
|
3594
|
+
"success": true,
|
|
3595
|
+
"requestId": requestId,
|
|
3596
|
+
"uploadUrl": uploadUrl,
|
|
3597
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
3598
|
+
]
|
|
3599
|
+
copyPhotoUploadResponseMetadata(into: &event, responseBody: responseBody)
|
|
3600
|
+
Bridge.sendPhotoResponse(event)
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
private func copyPhotoUploadResponseMetadata(
|
|
3604
|
+
into event: inout [String: Any],
|
|
3605
|
+
responseBody: String
|
|
3606
|
+
) {
|
|
3607
|
+
guard !responseBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
|
3608
|
+
let data = responseBody.data(using: .utf8),
|
|
3609
|
+
let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
3610
|
+
else {
|
|
3611
|
+
return
|
|
3612
|
+
}
|
|
3613
|
+
for key in ["photoUrl", "statusUrl", "mimeType", "contentType", "bytes", "size"] {
|
|
3614
|
+
if let value = response[key], !(value is NSNull) {
|
|
3615
|
+
event[key] = value
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3230
3618
|
}
|
|
3231
3619
|
|
|
3232
3620
|
private func sendAckToGlasses(messageId: Int) {
|
|
@@ -3305,8 +3693,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3305
3693
|
|
|
3306
3694
|
// Create chunks
|
|
3307
3695
|
let chunks = MessageChunker.createChunks(
|
|
3308
|
-
originalJson: jsonString, messageId: messageId
|
|
3696
|
+
originalJson: jsonString, messageId: messageId, wakeUp: wakeUp
|
|
3309
3697
|
)
|
|
3698
|
+
guard !chunks.isEmpty else {
|
|
3699
|
+
Bridge.log("LIVE: Failed to create BLE chunks within K900 packet limit")
|
|
3700
|
+
return
|
|
3701
|
+
}
|
|
3310
3702
|
Bridge.log("LIVE: Sending \(chunks.count) chunks")
|
|
3311
3703
|
|
|
3312
3704
|
// Send each chunk
|
|
@@ -3661,24 +4053,55 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3661
4053
|
|
|
3662
4054
|
private func handleGalleryStatus(
|
|
3663
4055
|
photoCount: Int, videoCount: Int, totalCount: Int,
|
|
3664
|
-
totalSize: Int64, hasContent: Bool
|
|
4056
|
+
totalSize: Int64, hasContent: Bool, cameraBusy: Bool,
|
|
4057
|
+
cameraBusyReason: String?
|
|
3665
4058
|
) {
|
|
3666
4059
|
Bridge.log(
|
|
3667
4060
|
"LIVE: 📸 Received gallery status - photos: \(photoCount), videos: \(videoCount), total size: \(totalSize) bytes"
|
|
3668
4061
|
)
|
|
3669
4062
|
|
|
3670
4063
|
// Emit gallery status event like other status events
|
|
3671
|
-
|
|
4064
|
+
var eventBody =
|
|
3672
4065
|
[
|
|
4066
|
+
"type": "gallery_status",
|
|
3673
4067
|
"photos": photoCount,
|
|
3674
4068
|
"videos": videoCount,
|
|
3675
4069
|
"total": totalCount,
|
|
3676
4070
|
"totalSize": totalSize,
|
|
3677
4071
|
"hasContent": hasContent,
|
|
4072
|
+
"cameraBusy": cameraBusy,
|
|
3678
4073
|
] as [String: Any]
|
|
4074
|
+
if let cameraBusyReason, !cameraBusyReason.isEmpty {
|
|
4075
|
+
eventBody["cameraBusyReason"] = cameraBusyReason
|
|
4076
|
+
}
|
|
3679
4077
|
Bridge.sendTypedMessage("gallery_status", body: eventBody)
|
|
3680
4078
|
}
|
|
3681
4079
|
|
|
4080
|
+
private static func galleryCameraBusy(_ json: [String: Any]) -> Bool {
|
|
4081
|
+
if let busy = json["cameraBusy"] as? Bool {
|
|
4082
|
+
return busy
|
|
4083
|
+
}
|
|
4084
|
+
if let busy = json["camera_busy"] as? Bool {
|
|
4085
|
+
return busy
|
|
4086
|
+
}
|
|
4087
|
+
if let reason = json["camera_busy"] as? String {
|
|
4088
|
+
let normalized = reason.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
4089
|
+
return !normalized.isEmpty && normalized != "false"
|
|
4090
|
+
}
|
|
4091
|
+
return false
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
private static func galleryCameraBusyReason(_ json: [String: Any]) -> String? {
|
|
4095
|
+
if let reason = json["cameraBusyReason"] as? String, !reason.isEmpty {
|
|
4096
|
+
return reason
|
|
4097
|
+
}
|
|
4098
|
+
if let reason = json["camera_busy"] as? String {
|
|
4099
|
+
let normalized = reason.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
4100
|
+
return normalized.isEmpty || normalized.lowercased() == "false" ? nil : normalized
|
|
4101
|
+
}
|
|
4102
|
+
return nil
|
|
4103
|
+
}
|
|
4104
|
+
|
|
3682
4105
|
// MARK: - Timers
|
|
3683
4106
|
|
|
3684
4107
|
private func startHeartbeat() {
|
|
@@ -3921,6 +4344,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3921
4344
|
Bridge.sendPhotoStatus(json)
|
|
3922
4345
|
}
|
|
3923
4346
|
|
|
4347
|
+
private func emitPhotoResponse(_ json: [String: Any]) {
|
|
4348
|
+
Bridge.sendPhotoResponse(json)
|
|
4349
|
+
}
|
|
4350
|
+
|
|
3924
4351
|
private func emitButtonPress(buttonId: String, pressType: String, timestamp: Int64) {
|
|
3925
4352
|
let eventBody: [String: Any] = [
|
|
3926
4353
|
"device_model": "Mentra Live",
|
|
@@ -3946,7 +4373,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3946
4373
|
"bt_mac_address": bluetoothMacAddress,
|
|
3947
4374
|
]
|
|
3948
4375
|
|
|
3949
|
-
Bridge.
|
|
4376
|
+
Bridge.sendVersionInfo(eventBody)
|
|
3950
4377
|
}
|
|
3951
4378
|
|
|
3952
4379
|
private func emitKeepAliveAck(_ json: [String: Any]) {
|
|
@@ -3972,6 +4399,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3972
4399
|
|
|
3973
4400
|
// Stop all timers
|
|
3974
4401
|
stopAllTimers()
|
|
4402
|
+
incomingChunkReassembler.clear()
|
|
3975
4403
|
|
|
3976
4404
|
// Disconnect BLE
|
|
3977
4405
|
if let peripheral = connectedPeripheral {
|
|
@@ -4544,6 +4972,50 @@ extension MentraLive {
|
|
|
4544
4972
|
return Int64(Date().timeIntervalSince1970 * 1000)
|
|
4545
4973
|
}
|
|
4546
4974
|
|
|
4975
|
+
private func emitSettingsAck(_ json: [String: Any]) {
|
|
4976
|
+
var body = json
|
|
4977
|
+
if let requestId = body["request_id"], body["requestId"] == nil {
|
|
4978
|
+
body["requestId"] = requestId
|
|
4979
|
+
}
|
|
4980
|
+
if let roiPosition = body["roi_position"], body["roiPosition"] == nil {
|
|
4981
|
+
body["roiPosition"] = roiPosition
|
|
4982
|
+
}
|
|
4983
|
+
if let errorCode = body["error_code"], body["errorCode"] == nil {
|
|
4984
|
+
body["errorCode"] = errorCode
|
|
4985
|
+
}
|
|
4986
|
+
if let errorMessage = body["error_message"], body["errorMessage"] == nil {
|
|
4987
|
+
body["errorMessage"] = errorMessage
|
|
4988
|
+
}
|
|
4989
|
+
if let hardwareApplied = body["hardware_applied"], body["hardwareApplied"] == nil {
|
|
4990
|
+
body["hardwareApplied"] = hardwareApplied
|
|
4991
|
+
}
|
|
4992
|
+
body.removeValue(forKey: "request_id")
|
|
4993
|
+
body.removeValue(forKey: "roi_position")
|
|
4994
|
+
body.removeValue(forKey: "error_code")
|
|
4995
|
+
body.removeValue(forKey: "error_message")
|
|
4996
|
+
body.removeValue(forKey: "hardware_applied")
|
|
4997
|
+
if body["timestamp"] == nil {
|
|
4998
|
+
body["timestamp"] = Int64(Date().timeIntervalSince1970 * 1000)
|
|
4999
|
+
}
|
|
5000
|
+
Bridge.sendSettingsAck(body)
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
private func emitVideoRecordingStatus(_ json: [String: Any]) {
|
|
5004
|
+
var body = json
|
|
5005
|
+
if let requestId = body["request_id"], body["requestId"] == nil {
|
|
5006
|
+
body["requestId"] = requestId
|
|
5007
|
+
}
|
|
5008
|
+
if let details = body["error_details"], body["details"] == nil {
|
|
5009
|
+
body["details"] = details
|
|
5010
|
+
}
|
|
5011
|
+
body.removeValue(forKey: "request_id")
|
|
5012
|
+
body.removeValue(forKey: "error_details")
|
|
5013
|
+
if body["timestamp"] == nil {
|
|
5014
|
+
body["timestamp"] = Int64(Date().timeIntervalSince1970 * 1000)
|
|
5015
|
+
}
|
|
5016
|
+
Bridge.sendVideoRecordingStatus(body)
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4547
5019
|
/**
|
|
4548
5020
|
* Check if a JSON string is already properly formatted for K900 protocol
|
|
4549
5021
|
*/
|
|
@@ -4624,7 +5096,8 @@ extension MentraLive {
|
|
|
4624
5096
|
}
|
|
4625
5097
|
|
|
4626
5098
|
func sendVoiceActivityDetectionSetting() {
|
|
4627
|
-
let enabled = DeviceStore.shared.get("bluetooth", "voice_activity_detection_enabled") as? Bool
|
|
5099
|
+
let enabled = DeviceStore.shared.get("bluetooth", "voice_activity_detection_enabled") as? Bool
|
|
5100
|
+
?? BluetoothSdkDefaults.voiceActivityDetectionEnabled
|
|
4628
5101
|
Bridge.log("LIVE: 🎤 Sending Voice Activity Detection setting to glasses: \(enabled)")
|
|
4629
5102
|
|
|
4630
5103
|
guard connectedPeripheral != nil, txCharacteristic != nil else {
|
|
@@ -4672,28 +5145,37 @@ extension MentraLive {
|
|
|
4672
5145
|
let finalHeight = height > 0 ? height : 720
|
|
4673
5146
|
let finalFps = fps > 0 ? fps : 30
|
|
4674
5147
|
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
5148
|
+
sendButtonVideoRecordingSettings(requestId: nil, width: finalWidth, height: finalHeight, fps: finalFps)
|
|
5149
|
+
}
|
|
5150
|
+
|
|
5151
|
+
func sendButtonVideoRecordingSettings(requestId: String?, width: Int, height: Int, fps: Int) {
|
|
5152
|
+
Bridge.log("Sending button video recording settings: \(width)x\(height)@\(fps)fps")
|
|
4678
5153
|
|
|
4679
5154
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
4680
5155
|
Bridge.log("Cannot send button video recording settings - not connected")
|
|
4681
5156
|
return
|
|
4682
5157
|
}
|
|
4683
5158
|
|
|
4684
|
-
|
|
5159
|
+
var json: [String: Any] = [
|
|
4685
5160
|
"type": "button_video_recording_setting",
|
|
4686
5161
|
"params": [
|
|
4687
|
-
"width":
|
|
4688
|
-
"height":
|
|
4689
|
-
"fps":
|
|
5162
|
+
"width": width,
|
|
5163
|
+
"height": height,
|
|
5164
|
+
"fps": fps,
|
|
4690
5165
|
],
|
|
4691
5166
|
]
|
|
5167
|
+
if let requestId, !requestId.isEmpty {
|
|
5168
|
+
json["request_id"] = requestId
|
|
5169
|
+
}
|
|
4692
5170
|
sendJson(json, wakeUp: true)
|
|
4693
5171
|
}
|
|
4694
5172
|
|
|
4695
5173
|
func sendButtonMaxRecordingTime() {
|
|
4696
5174
|
let maxTime = DeviceStore.shared.get("bluetooth", "button_max_recording_time") as? Int ?? 10
|
|
5175
|
+
sendButtonMaxRecordingTime(requestId: nil, minutes: maxTime)
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
func sendButtonMaxRecordingTime(requestId: String?, minutes maxTime: Int) {
|
|
4697
5179
|
Bridge.log("Sending button max recording time: \(maxTime) minutes")
|
|
4698
5180
|
|
|
4699
5181
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
@@ -4701,16 +5183,22 @@ extension MentraLive {
|
|
|
4701
5183
|
return
|
|
4702
5184
|
}
|
|
4703
5185
|
|
|
4704
|
-
|
|
5186
|
+
var json: [String: Any] = [
|
|
4705
5187
|
"type": "button_max_recording_time",
|
|
4706
5188
|
"minutes": maxTime,
|
|
4707
5189
|
]
|
|
5190
|
+
if let requestId, !requestId.isEmpty {
|
|
5191
|
+
json["request_id"] = requestId
|
|
5192
|
+
}
|
|
4708
5193
|
sendJson(json, wakeUp: true)
|
|
4709
5194
|
}
|
|
4710
5195
|
|
|
4711
5196
|
func sendButtonPhotoSettings() {
|
|
4712
5197
|
let size = DeviceStore.shared.get("bluetooth", "button_photo_size") as! String
|
|
5198
|
+
sendButtonPhotoSettings(requestId: nil, size: size)
|
|
5199
|
+
}
|
|
4713
5200
|
|
|
5201
|
+
func sendButtonPhotoSettings(requestId: String?, size: String) {
|
|
4714
5202
|
Bridge.log("Sending button photo setting: \(size)")
|
|
4715
5203
|
|
|
4716
5204
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
@@ -4718,16 +5206,22 @@ extension MentraLive {
|
|
|
4718
5206
|
return
|
|
4719
5207
|
}
|
|
4720
5208
|
|
|
4721
|
-
|
|
5209
|
+
var json: [String: Any] = [
|
|
4722
5210
|
"type": "button_photo_setting",
|
|
4723
5211
|
"size": size,
|
|
4724
5212
|
]
|
|
5213
|
+
if let requestId, !requestId.isEmpty {
|
|
5214
|
+
json["request_id"] = requestId
|
|
5215
|
+
}
|
|
4725
5216
|
sendJson(json, wakeUp: true)
|
|
4726
5217
|
}
|
|
4727
5218
|
|
|
4728
5219
|
func sendButtonCameraLedSetting() {
|
|
4729
5220
|
let enabled = DeviceStore.shared.get("bluetooth", "button_camera_led") as! Bool
|
|
5221
|
+
sendButtonCameraLedSetting(requestId: nil, enabled: enabled)
|
|
5222
|
+
}
|
|
4730
5223
|
|
|
5224
|
+
func sendButtonCameraLedSetting(requestId: String?, enabled: Bool) {
|
|
4731
5225
|
Bridge.log("Sending button camera LED setting: \(enabled)")
|
|
4732
5226
|
|
|
4733
5227
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
@@ -4735,10 +5229,13 @@ extension MentraLive {
|
|
|
4735
5229
|
return
|
|
4736
5230
|
}
|
|
4737
5231
|
|
|
4738
|
-
|
|
5232
|
+
var json: [String: Any] = [
|
|
4739
5233
|
"type": "button_camera_led",
|
|
4740
5234
|
"enabled": enabled,
|
|
4741
5235
|
]
|
|
5236
|
+
if let requestId, !requestId.isEmpty {
|
|
5237
|
+
json["request_id"] = requestId
|
|
5238
|
+
}
|
|
4742
5239
|
sendJson(json, wakeUp: true)
|
|
4743
5240
|
}
|
|
4744
5241
|
|
|
@@ -4746,7 +5243,10 @@ extension MentraLive {
|
|
|
4746
5243
|
let settings = DeviceStore.shared.get("bluetooth", "camera_fov") as? [String: Any] ?? ["fov": 118, "roi_position": 0]
|
|
4747
5244
|
let fov = settings["fov"] as? Int ?? 118
|
|
4748
5245
|
let roiPosition = settings["roi_position"] as? Int ?? 0
|
|
5246
|
+
sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
|
|
5247
|
+
}
|
|
4749
5248
|
|
|
5249
|
+
func sendCameraFovSetting(requestId: String?, fov: Int, roiPosition: Int) {
|
|
4750
5250
|
Bridge.log("Sending camera FOV setting: fov=\(fov), roiPosition=\(roiPosition)")
|
|
4751
5251
|
|
|
4752
5252
|
guard connectionState == ConnTypes.CONNECTED else {
|
|
@@ -4754,13 +5254,16 @@ extension MentraLive {
|
|
|
4754
5254
|
return
|
|
4755
5255
|
}
|
|
4756
5256
|
|
|
4757
|
-
|
|
5257
|
+
var json: [String: Any] = [
|
|
4758
5258
|
"type": "camera_fov_setting",
|
|
4759
5259
|
"params": [
|
|
4760
5260
|
"fov": fov,
|
|
4761
5261
|
"roi_position": roiPosition,
|
|
4762
5262
|
],
|
|
4763
5263
|
]
|
|
5264
|
+
if let requestId, !requestId.isEmpty {
|
|
5265
|
+
json["request_id"] = requestId
|
|
5266
|
+
}
|
|
4764
5267
|
sendJson(json, wakeUp: true)
|
|
4765
5268
|
}
|
|
4766
5269
|
|
|
@@ -4773,20 +5276,7 @@ extension MentraLive {
|
|
|
4773
5276
|
// MARK: - SGCManager Protocol Compliance
|
|
4774
5277
|
|
|
4775
5278
|
func sendButtonMaxRecordingTime(_ minutes: Int) {
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
Bridge.log("Sending button max recording time: \(maxTime) minutes")
|
|
4779
|
-
|
|
4780
|
-
guard connectionState == ConnTypes.CONNECTED else {
|
|
4781
|
-
Bridge.log("Cannot send button max recording time - not connected")
|
|
4782
|
-
return
|
|
4783
|
-
}
|
|
4784
|
-
|
|
4785
|
-
let json: [String: Any] = [
|
|
4786
|
-
"type": "button_max_recording_time",
|
|
4787
|
-
"minutes": maxTime,
|
|
4788
|
-
]
|
|
4789
|
-
sendJson(json, wakeUp: true)
|
|
5279
|
+
sendButtonMaxRecordingTime(requestId: nil, minutes: minutes)
|
|
4790
5280
|
}
|
|
4791
5281
|
|
|
4792
5282
|
func startVideoRecording(
|
|
@@ -4809,13 +5299,16 @@ extension MentraLive {
|
|
|
4809
5299
|
"sound": sound,
|
|
4810
5300
|
]
|
|
4811
5301
|
|
|
4812
|
-
// Add video settings
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
]
|
|
5302
|
+
// Add video settings when any field is overridden. Each field is sent
|
|
5303
|
+
// only when > 0; the glasses merge the missing fields onto their saved
|
|
5304
|
+
// button-video defaults, so a partial override (e.g. fps-only) still
|
|
5305
|
+
// takes effect instead of being dropped here.
|
|
5306
|
+
if width > 0 || height > 0 || fps > 0 {
|
|
5307
|
+
var settings: [String: Any] = [:]
|
|
5308
|
+
if width > 0 { settings["width"] = width }
|
|
5309
|
+
if height > 0 { settings["height"] = height }
|
|
5310
|
+
if fps > 0 { settings["fps"] = fps }
|
|
5311
|
+
json["settings"] = settings
|
|
4819
5312
|
}
|
|
4820
5313
|
sendJson(json)
|
|
4821
5314
|
}
|