@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -2
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +198 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1174 -191
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +61 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +47 -3
- package/ios/Source/DeviceManager.swift +135 -10
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +74 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +439 -30
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/package.json +5 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +72 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +19 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
|
@@ -32,7 +32,7 @@ public class MentraPhotoReceiverModule: Module {
|
|
|
32
32
|
receiverLock.lock()
|
|
33
33
|
defer { receiverLock.unlock() }
|
|
34
34
|
|
|
35
|
-
guard let host = bestLocalIPv4Address() else {
|
|
35
|
+
guard let host = LocalIPv4.bestLocalIPv4Address() else {
|
|
36
36
|
throw PhotoReceiverError("No Wi-Fi/LAN IPv4 address found for this phone.")
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -104,66 +104,6 @@ public class MentraPhotoReceiverModule: Module {
|
|
|
104
104
|
])
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
private func bestLocalIPv4Address() -> String? {
|
|
108
|
-
var interfaces: UnsafeMutablePointer<ifaddrs>?
|
|
109
|
-
guard getifaddrs(&interfaces) == 0, let first = interfaces else {
|
|
110
|
-
return nil
|
|
111
|
-
}
|
|
112
|
-
defer { freeifaddrs(interfaces) }
|
|
113
|
-
|
|
114
|
-
var preferredPrivate: String?
|
|
115
|
-
var preferred: String?
|
|
116
|
-
var privateFallback: String?
|
|
117
|
-
var fallback: String?
|
|
118
|
-
var cursor: UnsafeMutablePointer<ifaddrs>? = first
|
|
119
|
-
while let current = cursor {
|
|
120
|
-
defer { cursor = current.pointee.ifa_next }
|
|
121
|
-
let interface = current.pointee
|
|
122
|
-
guard let addressPointer = interface.ifa_addr,
|
|
123
|
-
addressPointer.pointee.sa_family == UInt8(AF_INET) else {
|
|
124
|
-
continue
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let name = String(cString: interface.ifa_name)
|
|
128
|
-
let flags = interface.ifa_flags
|
|
129
|
-
guard (flags & UInt32(IFF_UP)) != 0,
|
|
130
|
-
(flags & UInt32(IFF_LOOPBACK)) == 0 else {
|
|
131
|
-
continue
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
var address = addressPointer.pointee
|
|
135
|
-
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
136
|
-
let result = getnameinfo(
|
|
137
|
-
&address,
|
|
138
|
-
socklen_t(address.sa_len),
|
|
139
|
-
&hostname,
|
|
140
|
-
socklen_t(hostname.count),
|
|
141
|
-
nil,
|
|
142
|
-
0,
|
|
143
|
-
NI_NUMERICHOST
|
|
144
|
-
)
|
|
145
|
-
guard result == 0 else {
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let ip = String(cString: hostname)
|
|
150
|
-
guard ip != "127.0.0.1", !isLinkLocalIPv4(ip) else {
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
153
|
-
if isPreferredLocalInterface(name), isPrivateIPv4(ip) {
|
|
154
|
-
preferredPrivate = preferredPrivate ?? ip
|
|
155
|
-
} else if isPreferredLocalInterface(name) {
|
|
156
|
-
preferred = preferred ?? ip
|
|
157
|
-
} else if isPrivateIPv4(ip) {
|
|
158
|
-
privateFallback = privateFallback ?? ip
|
|
159
|
-
} else {
|
|
160
|
-
fallback = fallback ?? ip
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return preferredPrivate ?? preferred ?? privateFallback ?? fallback
|
|
165
|
-
}
|
|
166
|
-
|
|
167
107
|
private func receiverResult(uploadUrl: String, host: String, port: UInt16) -> [String: Any] {
|
|
168
108
|
[
|
|
169
109
|
"uploadUrl": uploadUrl,
|
|
@@ -172,26 +112,6 @@ public class MentraPhotoReceiverModule: Module {
|
|
|
172
112
|
]
|
|
173
113
|
}
|
|
174
114
|
|
|
175
|
-
private func isPreferredLocalInterface(_ name: String) -> Bool {
|
|
176
|
-
name == "en0" ||
|
|
177
|
-
name.hasPrefix("bridge") ||
|
|
178
|
-
name.hasPrefix("ap") ||
|
|
179
|
-
name.hasPrefix("awdl") ||
|
|
180
|
-
name.hasPrefix("llw")
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private func isPrivateIPv4(_ ip: String) -> Bool {
|
|
184
|
-
if ip.hasPrefix("192.168.") || ip.hasPrefix("10.") {
|
|
185
|
-
return true
|
|
186
|
-
}
|
|
187
|
-
let parts = ip.split(separator: ".").compactMap { Int($0) }
|
|
188
|
-
return parts.count == 4 && parts[0] == 172 && (16...31).contains(parts[1])
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private func isLinkLocalIPv4(_ ip: String) -> Bool {
|
|
192
|
-
ip.hasPrefix("169.254.")
|
|
193
|
-
}
|
|
194
|
-
|
|
195
115
|
private let photoPorts = [8787, 8788, 8789, 8790]
|
|
196
116
|
}
|
|
197
117
|
|
|
@@ -3,12 +3,15 @@ import Foundation
|
|
|
3
3
|
/// Defaults for the public Bluetooth SDK surface.
|
|
4
4
|
enum BluetoothSdkDefaults {
|
|
5
5
|
static var sdkVersion: String? {
|
|
6
|
+
// Release exports replace the source placeholder; Expo builds stamp the host Info.plist.
|
|
6
7
|
normalizedSdkVersion(swiftPackageSdkVersion)
|
|
8
|
+
?? normalizedSdkVersion(Bundle.main.object(forInfoDictionaryKey: infoSdkVersionKey) as? String)
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
static let voiceActivityDetectionEnabled = false
|
|
10
12
|
static let loudnessGateEnabled = true
|
|
11
|
-
private static let
|
|
13
|
+
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
14
|
+
private static let swiftPackageSdkVersion = "3.1.0-dev.7"
|
|
12
15
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
13
16
|
|
|
14
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
package/ios/Source/Bridge.swift
CHANGED
|
@@ -98,6 +98,27 @@ class Bridge {
|
|
|
98
98
|
Bridge.sendTypedMessage("pair_failure", body: data)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
static func sendPairingInfo(
|
|
102
|
+
hadPreviousBond: Bool,
|
|
103
|
+
transferId: String? = nil,
|
|
104
|
+
pairingCode: String? = nil,
|
|
105
|
+
classicBondReady: Bool = false,
|
|
106
|
+
securePairingCapable: Bool = true,
|
|
107
|
+
protocolVersion: Int = 1,
|
|
108
|
+
binding: String? = nil
|
|
109
|
+
) {
|
|
110
|
+
var body: [String: Any] = [
|
|
111
|
+
"had_previous_bond": hadPreviousBond,
|
|
112
|
+
"classic_bond_ready": classicBondReady,
|
|
113
|
+
"secure_pairing_capable": securePairingCapable,
|
|
114
|
+
"protocol_version": protocolVersion,
|
|
115
|
+
]
|
|
116
|
+
if let transferId { body["transfer_id"] = transferId }
|
|
117
|
+
if let pairingCode { body["pairing_code"] = pairingCode }
|
|
118
|
+
if let binding { body["binding"] = binding }
|
|
119
|
+
Bridge.sendTypedMessage("pairing_info", body: body)
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
@MainActor
|
|
102
123
|
static func sendMicPcm(_ data: Data) {
|
|
103
124
|
Bridge.sendTypedMessage("mic_pcm", body: micPcmEventBody(data))
|
|
@@ -108,6 +129,14 @@ class Bridge {
|
|
|
108
129
|
Bridge.sendTypedMessage("mic_lc3", body: micLc3EventBody(data))
|
|
109
130
|
}
|
|
110
131
|
|
|
132
|
+
@MainActor
|
|
133
|
+
static func sendMicHealth(_ health: MicHealth, reason: String) {
|
|
134
|
+
var body = health.dictionary
|
|
135
|
+
body["reason"] = reason
|
|
136
|
+
body["timestamp"] = Int64(Date().timeIntervalSince1970 * 1000)
|
|
137
|
+
Bridge.sendTypedMessage("mic_health", body: body)
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
@MainActor
|
|
112
141
|
private static func micPcmEventBody(_ data: Data) -> [String: Any] {
|
|
113
142
|
let voiceActivityDetectionEnabled =
|
|
@@ -178,7 +207,10 @@ class Bridge {
|
|
|
178
207
|
_ deviceName: String,
|
|
179
208
|
deviceAddress: String = "",
|
|
180
209
|
rssi: Int? = nil,
|
|
181
|
-
projectName: String? = nil
|
|
210
|
+
projectName: String? = nil,
|
|
211
|
+
pairingMode: Bool? = nil,
|
|
212
|
+
pairingCode: String? = nil,
|
|
213
|
+
securePairingCapable: Bool? = nil
|
|
182
214
|
) {
|
|
183
215
|
Task {
|
|
184
216
|
await MainActor.run {
|
|
@@ -200,6 +232,15 @@ class Bridge {
|
|
|
200
232
|
if let rssi {
|
|
201
233
|
newResult["rssi"] = rssi
|
|
202
234
|
}
|
|
235
|
+
if let pairingMode {
|
|
236
|
+
newResult["pairingMode"] = pairingMode
|
|
237
|
+
}
|
|
238
|
+
if let pairingCode, !pairingCode.isEmpty {
|
|
239
|
+
newResult["pairingCode"] = pairingCode
|
|
240
|
+
}
|
|
241
|
+
if let securePairingCapable {
|
|
242
|
+
newResult["securePairingCapable"] = securePairingCapable
|
|
243
|
+
}
|
|
203
244
|
// Keep the public searchResults array stable as glasses are added or removed.
|
|
204
245
|
// Duplicate discoveries refresh their existing row; only new glasses append.
|
|
205
246
|
let uniqueResults = mergeStableSearchResults(
|
|
@@ -340,6 +381,11 @@ class Bridge {
|
|
|
340
381
|
if let systemTimeMs = intValue(values["systemTimeMs"]) ?? intValue(values["system_time_ms"]) {
|
|
341
382
|
body["systemTimeMs"] = systemTimeMs
|
|
342
383
|
}
|
|
384
|
+
if let hotspotOtaVersion = intValue(values["hotspotOtaVersion"])
|
|
385
|
+
?? intValue(values["hotspot_ota_version"])
|
|
386
|
+
{
|
|
387
|
+
body["hotspotOtaVersion"] = hotspotOtaVersion
|
|
388
|
+
}
|
|
343
389
|
Bridge.sendTypedMessage("version_info", body: body)
|
|
344
390
|
}
|
|
345
391
|
|
|
@@ -617,5 +663,3 @@ class Bridge {
|
|
|
617
663
|
|
|
618
664
|
|
|
619
665
|
|
|
620
|
-
|
|
621
|
-
|
|
@@ -123,6 +123,16 @@ struct ViewState {
|
|
|
123
123
|
set { DeviceStore.shared.apply("bluetooth", "pending_wearable", newValue) }
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
private var pendingDeviceName: String {
|
|
127
|
+
get { DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "" }
|
|
128
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_device_name", newValue) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private var pendingDeviceAddress: String {
|
|
132
|
+
get { DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? "" }
|
|
133
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_device_address", newValue) }
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
private var deviceName: String {
|
|
127
137
|
get { DeviceStore.shared.get("bluetooth", "device_name") as? String ?? "" }
|
|
128
138
|
set { DeviceStore.shared.apply("bluetooth", "device_name", newValue) }
|
|
@@ -241,6 +251,8 @@ struct ViewState {
|
|
|
241
251
|
}
|
|
242
252
|
|
|
243
253
|
private var lastSystemTimeSyncConnectionKey = ""
|
|
254
|
+
private var pendingSystemTimeSync: DispatchWorkItem?
|
|
255
|
+
private var systemTimeSyncGeneration = 0
|
|
244
256
|
|
|
245
257
|
private var systemMicUnavailable: Bool {
|
|
246
258
|
get { DeviceStore.shared.get("bluetooth", "systemMicUnavailable") as? Bool ?? false }
|
|
@@ -294,6 +306,11 @@ struct ViewState {
|
|
|
294
306
|
/// Last time we received an LC3 frame from the glasses (used by the mic
|
|
295
307
|
/// inactivity watchdog).
|
|
296
308
|
private var lastLc3Event: Date?
|
|
309
|
+
private var sequenceGapEvents: Int64 = 0
|
|
310
|
+
private var decodeFailures: Int64 = 0
|
|
311
|
+
private var lastLc3ReceivedAt: Int64?
|
|
312
|
+
private var lastPcmProducedAt: Int64?
|
|
313
|
+
private var lastLc3Sequence: Int?
|
|
297
314
|
private var micReinitTimer: Timer?
|
|
298
315
|
|
|
299
316
|
/// STT:
|
|
@@ -396,29 +413,87 @@ struct ViewState {
|
|
|
396
413
|
* Decodes the glasses LC3 to PCM, then forwards to handlePcm for processing.
|
|
397
414
|
* This matches Android behavior - glasses forward raw LC3, DeviceManager handles encoding.
|
|
398
415
|
*/
|
|
399
|
-
func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20) {
|
|
400
|
-
|
|
416
|
+
func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20, sequenceNumber: Int? = nil) {
|
|
417
|
+
recordLc3Packet(sequenceNumber: sequenceNumber)
|
|
401
418
|
guard let lc3Converter = lc3Converter else {
|
|
402
419
|
Bridge.log("MAN: LC3 converter not initialized")
|
|
420
|
+
recordMicDecodeFailure()
|
|
403
421
|
return
|
|
404
422
|
}
|
|
405
423
|
|
|
406
424
|
guard lc3Data.count > 2 else {
|
|
407
425
|
Bridge.log("MAN: Received invalid LC3 data size: \(lc3Data.count)")
|
|
426
|
+
recordMicDecodeFailure()
|
|
408
427
|
return
|
|
409
428
|
}
|
|
410
429
|
|
|
411
430
|
let pcmData = lc3Converter.decode(lc3Data, frameSize: frameSize) as Data
|
|
412
431
|
guard pcmData.count > 0 else {
|
|
413
432
|
Bridge.log("MAN: Failed to decode glasses LC3 audio")
|
|
433
|
+
recordMicDecodeFailure()
|
|
414
434
|
return
|
|
415
435
|
}
|
|
416
436
|
// Forward to handlePcm which handles SDK audio events and encoding.
|
|
437
|
+
lastPcmProducedAt = nowMs()
|
|
417
438
|
handlePcm(pcmData)
|
|
418
439
|
}
|
|
419
440
|
|
|
420
441
|
func reportGlassesAudioActivity() {
|
|
421
442
|
lastLc3Event = Date()
|
|
443
|
+
lastPcmProducedAt = nowMs()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private func micHealth() -> MicHealth {
|
|
447
|
+
MicHealth(
|
|
448
|
+
sequenceGapEvents: sequenceGapEvents,
|
|
449
|
+
decodeFailures: decodeFailures,
|
|
450
|
+
lastLc3ReceivedAt: lastLc3ReceivedAt,
|
|
451
|
+
lastPcmProducedAt: lastPcmProducedAt
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
func resetMicSequenceBaseline() {
|
|
456
|
+
lastLc3Sequence = nil
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private func nowMs() -> Int64 {
|
|
460
|
+
Int64(Date().timeIntervalSince1970 * 1000)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private func resetMicHealth() {
|
|
464
|
+
sequenceGapEvents = 0
|
|
465
|
+
decodeFailures = 0
|
|
466
|
+
lastLc3ReceivedAt = nil
|
|
467
|
+
lastPcmProducedAt = nil
|
|
468
|
+
lastLc3Sequence = nil
|
|
469
|
+
lastLc3Event = nil
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private func recordLc3Packet(sequenceNumber: Int?) {
|
|
473
|
+
let receivedAt = nowMs()
|
|
474
|
+
lastLc3ReceivedAt = receivedAt
|
|
475
|
+
lastLc3Event = Date()
|
|
476
|
+
var hasGap = false
|
|
477
|
+
if let sequenceNumber {
|
|
478
|
+
let normalized = sequenceNumber & 0xFF
|
|
479
|
+
if let previous = lastLc3Sequence {
|
|
480
|
+
let expected = (previous + 1) & 0xFF
|
|
481
|
+
if normalized != expected {
|
|
482
|
+
hasGap = true
|
|
483
|
+
sequenceGapEvents += 1
|
|
484
|
+
Bridge.log("MAN: LC3 packet sequence mismatch. Expected: \(expected), Got: \(normalized)")
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
lastLc3Sequence = normalized
|
|
488
|
+
}
|
|
489
|
+
if hasGap {
|
|
490
|
+
Bridge.sendMicHealth(micHealth(), reason: "sequence_gap")
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private func recordMicDecodeFailure() {
|
|
495
|
+
decodeFailures += 1
|
|
496
|
+
Bridge.sendMicHealth(micHealth(), reason: "decode_failure")
|
|
422
497
|
}
|
|
423
498
|
|
|
424
499
|
func handlePcm(_ pcmData: Data) {
|
|
@@ -613,7 +688,7 @@ struct ViewState {
|
|
|
613
688
|
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
614
689
|
sgc?.cleanup()
|
|
615
690
|
sgc = nil
|
|
616
|
-
|
|
691
|
+
resetSystemTimeSync()
|
|
617
692
|
}
|
|
618
693
|
|
|
619
694
|
if sgc != nil {
|
|
@@ -929,7 +1004,16 @@ struct ViewState {
|
|
|
929
1004
|
return
|
|
930
1005
|
}
|
|
931
1006
|
Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
|
|
1007
|
+
resetMicHealth()
|
|
932
1008
|
|
|
1009
|
+
if !pendingDeviceName.isEmpty {
|
|
1010
|
+
deviceName = pendingDeviceName
|
|
1011
|
+
}
|
|
1012
|
+
if !pendingDeviceAddress.isEmpty {
|
|
1013
|
+
deviceAddress = pendingDeviceAddress
|
|
1014
|
+
}
|
|
1015
|
+
pendingDeviceName = ""
|
|
1016
|
+
pendingDeviceAddress = ""
|
|
933
1017
|
pendingWearable = ""
|
|
934
1018
|
defaultWearable = sgc.type
|
|
935
1019
|
searching = false
|
|
@@ -994,9 +1078,30 @@ struct ViewState {
|
|
|
994
1078
|
}
|
|
995
1079
|
|
|
996
1080
|
lastSystemTimeSyncConnectionKey = connectionKey
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1081
|
+
pendingSystemTimeSync?.cancel()
|
|
1082
|
+
systemTimeSyncGeneration += 1
|
|
1083
|
+
let generation = systemTimeSyncGeneration
|
|
1084
|
+
let sync = DispatchWorkItem { [weak self] in
|
|
1085
|
+
guard let self,
|
|
1086
|
+
self.lastSystemTimeSyncConnectionKey == connectionKey,
|
|
1087
|
+
self.systemTimeSyncGeneration == generation
|
|
1088
|
+
else {
|
|
1089
|
+
return
|
|
1090
|
+
}
|
|
1091
|
+
self.pendingSystemTimeSync = nil
|
|
1092
|
+
let timestampMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1093
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: \(timestampMs)")
|
|
1094
|
+
sgc.sendSetSystemTime(timestampMs)
|
|
1095
|
+
}
|
|
1096
|
+
pendingSystemTimeSync = sync
|
|
1097
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: sync)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private func resetSystemTimeSync() {
|
|
1101
|
+
pendingSystemTimeSync?.cancel()
|
|
1102
|
+
pendingSystemTimeSync = nil
|
|
1103
|
+
systemTimeSyncGeneration += 1
|
|
1104
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1000
1105
|
}
|
|
1001
1106
|
|
|
1002
1107
|
func handleControllerReady() {
|
|
@@ -1043,7 +1148,8 @@ struct ViewState {
|
|
|
1043
1148
|
|
|
1044
1149
|
func handleDeviceDisconnected() {
|
|
1045
1150
|
Bridge.log("MAN: Device disconnected")
|
|
1046
|
-
|
|
1151
|
+
resetSystemTimeSync()
|
|
1152
|
+
resetMicHealth()
|
|
1047
1153
|
DeviceStore.shared.apply("glasses", "headUp", false)
|
|
1048
1154
|
DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
1049
1155
|
// shouldSendBootingMessage = true // Reset for next first connect
|
|
@@ -1327,6 +1433,11 @@ struct ViewState {
|
|
|
1327
1433
|
sgc?.sendHotspotState(enabled)
|
|
1328
1434
|
}
|
|
1329
1435
|
|
|
1436
|
+
func setWifiAdbState(_ enabled: Bool) {
|
|
1437
|
+
Bridge.log("MAN: 🔧 Setting glasses Wi-Fi ADB state: \(enabled)")
|
|
1438
|
+
sgc?.sendWifiAdbState(enabled)
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1330
1441
|
func setSystemTime(_ timestampMs: Int64) {
|
|
1331
1442
|
Bridge.log("MAN: Setting glasses system time: \(timestampMs)")
|
|
1332
1443
|
sgc?.sendSetSystemTime(timestampMs)
|
|
@@ -1506,6 +1617,11 @@ struct ViewState {
|
|
|
1506
1617
|
)
|
|
1507
1618
|
}
|
|
1508
1619
|
|
|
1620
|
+
func queryVideoRecordingStatus(_ requestId: String) {
|
|
1621
|
+
Bridge.log("MAN: Querying video recording status: requestId=\(requestId)")
|
|
1622
|
+
sgc?.queryVideoRecordingStatus(requestId: requestId)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1509
1625
|
func stopVideoRecording(_ requestId: String, _ webhookUrl: String?, _ authToken: String?) {
|
|
1510
1626
|
Bridge.log(
|
|
1511
1627
|
"MAN: onStopVideoRecording: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|
|
@@ -1626,6 +1742,10 @@ struct ViewState {
|
|
|
1626
1742
|
return
|
|
1627
1743
|
}
|
|
1628
1744
|
initSGC(defaultWearable)
|
|
1745
|
+
if let live = sgc as? MentraLive, live.isPairingYieldActive() {
|
|
1746
|
+
Bridge.log("MAN: connectDefault skipped — Mentra Live pairing yield active")
|
|
1747
|
+
return
|
|
1748
|
+
}
|
|
1629
1749
|
searching = true
|
|
1630
1750
|
sgc?.connectById(reconnectTarget)
|
|
1631
1751
|
connectDefaultController()
|
|
@@ -1675,10 +1795,10 @@ struct ViewState {
|
|
|
1675
1795
|
disconnect()
|
|
1676
1796
|
try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
|
|
1677
1797
|
self.searching = true
|
|
1678
|
-
self.
|
|
1798
|
+
self.pendingDeviceName = name
|
|
1679
1799
|
|
|
1680
1800
|
initSGC(self.pendingWearable)
|
|
1681
|
-
sgc?.connectById(
|
|
1801
|
+
sgc?.connectById(name)
|
|
1682
1802
|
}
|
|
1683
1803
|
}
|
|
1684
1804
|
|
|
@@ -1710,7 +1830,8 @@ struct ViewState {
|
|
|
1710
1830
|
sgc?.clearDisplay() // clear the screen
|
|
1711
1831
|
sgc?.disconnect()
|
|
1712
1832
|
sgc = nil // Clear the SGC reference after disconnect
|
|
1713
|
-
|
|
1833
|
+
resetSystemTimeSync()
|
|
1834
|
+
resetMicHealth()
|
|
1714
1835
|
searching = false
|
|
1715
1836
|
micEnabled = false
|
|
1716
1837
|
updateMicState()
|
|
@@ -1721,6 +1842,7 @@ struct ViewState {
|
|
|
1721
1842
|
// previously connected pair can never be reported for the next connection.
|
|
1722
1843
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1723
1844
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1845
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
|
|
1724
1846
|
DeviceStore.shared.apply("glasses", "leftMacAddress", "")
|
|
1725
1847
|
DeviceStore.shared.apply("glasses", "rightMacAddress", "")
|
|
1726
1848
|
DeviceStore.shared.apply("glasses", "macAddress", "")
|
|
@@ -1752,6 +1874,9 @@ struct ViewState {
|
|
|
1752
1874
|
defaultWearable = ""
|
|
1753
1875
|
deviceName = ""
|
|
1754
1876
|
deviceAddress = ""
|
|
1877
|
+
pendingDeviceName = ""
|
|
1878
|
+
pendingDeviceAddress = ""
|
|
1879
|
+
pendingWearable = ""
|
|
1755
1880
|
Bridge.saveSetting("default_wearable", "")
|
|
1756
1881
|
Bridge.saveSetting("device_name", "")
|
|
1757
1882
|
Bridge.saveSetting("device_address", "")
|
|
@@ -27,6 +27,7 @@ class DeviceStore {
|
|
|
27
27
|
store.set("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
28
28
|
store.set("glasses", "deviceModel", "")
|
|
29
29
|
store.set("glasses", "firmwareVersion", "")
|
|
30
|
+
store.set("glasses", "hotspotOtaVersion", 0)
|
|
30
31
|
store.set("glasses", "micEnabled", false)
|
|
31
32
|
store.set("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
32
33
|
store.set("glasses", "bluetoothClassicConnected", false)
|
|
@@ -36,6 +37,7 @@ class DeviceStore {
|
|
|
36
37
|
store.set("glasses", "caseBatteryLevel", -1)
|
|
37
38
|
store.set("glasses", "headUp", false)
|
|
38
39
|
store.set("glasses", "bluetoothMacAddress", "")
|
|
40
|
+
store.set("glasses", "wifiMacAddress", "")
|
|
39
41
|
store.set("glasses", "leftMacAddress", "")
|
|
40
42
|
store.set("glasses", "rightMacAddress", "")
|
|
41
43
|
store.set("glasses", "serialNumber", "")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
|
+
enum GeneratedReleaseMetadata {
|
|
5
|
+
static let familyBaseVersion = "3.1.0"
|
|
6
|
+
static let releaseIdentity = "3.1.0-dev.7"
|
|
7
|
+
static let releaseSetId = "mentra-3.1.0-dev.7"
|
|
8
|
+
static let sourceCommit = "872697a6395c31d30ec75d64f039eaa763dcbb51"
|
|
9
|
+
static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.7/mentra-live-ota-3.1.0-dev.7.json"
|
|
10
|
+
static let otaManifestSha256 = "8fbea7589345658dc5dffcb394229ffc5c52fbe75352acb8f6827d83290ce750"
|
|
11
|
+
}
|
|
@@ -309,6 +309,8 @@ public final class MentraBluetoothSDK {
|
|
|
309
309
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", device.name)
|
|
310
310
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", device.identifier ?? "")
|
|
311
311
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", device.projectName ?? "")
|
|
312
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
313
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
312
314
|
finishDefaultDeviceApply(generation: generation)
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -320,6 +322,8 @@ public final class MentraBluetoothSDK {
|
|
|
320
322
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
|
|
321
323
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
|
|
322
324
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
|
|
325
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
326
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
323
327
|
finishDefaultDeviceApply(generation: generation)
|
|
324
328
|
}
|
|
325
329
|
|
|
@@ -394,6 +398,9 @@ public final class MentraBluetoothSDK {
|
|
|
394
398
|
}
|
|
395
399
|
if options.saveAsDefault && !isController {
|
|
396
400
|
setDefaultDevice(device)
|
|
401
|
+
} else if !isController {
|
|
402
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", device.name)
|
|
403
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", device.identifier ?? "")
|
|
397
404
|
}
|
|
398
405
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
|
|
399
406
|
DeviceManager.shared.connectByName(device.name)
|
|
@@ -418,6 +425,8 @@ public final class MentraBluetoothSDK {
|
|
|
418
425
|
|
|
419
426
|
public func cancelConnectionAttempt() {
|
|
420
427
|
clearBluetoothRestoreIntent()
|
|
428
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
429
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
421
430
|
DeviceManager.shared.disconnect()
|
|
422
431
|
}
|
|
423
432
|
|
|
@@ -428,6 +437,8 @@ public final class MentraBluetoothSDK {
|
|
|
428
437
|
|
|
429
438
|
public func disconnect() {
|
|
430
439
|
clearBluetoothRestoreIntent()
|
|
440
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
441
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
431
442
|
DeviceManager.shared.disconnect()
|
|
432
443
|
}
|
|
433
444
|
|
|
@@ -890,6 +901,13 @@ public final class MentraBluetoothSDK {
|
|
|
890
901
|
}
|
|
891
902
|
}
|
|
892
903
|
|
|
904
|
+
/// Fire-and-forget Mentra Live Wi-Fi ADB toggle. Glasses do not ack this
|
|
905
|
+
/// command, so there is no pending-response wait (unlike hotspot / Wi-Fi forget).
|
|
906
|
+
public func setWifiAdbState(enabled: Bool) async throws {
|
|
907
|
+
try requireGlassesConnected(operation: "toggle Wi-Fi ADB")
|
|
908
|
+
DeviceManager.shared.setWifiAdbState(enabled)
|
|
909
|
+
}
|
|
910
|
+
|
|
893
911
|
func setSystemTime(timestampMs: Int64) {
|
|
894
912
|
DeviceManager.shared.setSystemTime(timestampMs)
|
|
895
913
|
}
|
|
@@ -1141,6 +1159,36 @@ public final class MentraBluetoothSDK {
|
|
|
1141
1159
|
}
|
|
1142
1160
|
}
|
|
1143
1161
|
|
|
1162
|
+
public func queryVideoRecordingStatus(requestId: String) async throws -> VideoRecordingStatusEvent {
|
|
1163
|
+
guard !requestId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
1164
|
+
throw BluetoothSdkError(
|
|
1165
|
+
code: "missing_request_id",
|
|
1166
|
+
message: "requestId is required to query video recording status."
|
|
1167
|
+
)
|
|
1168
|
+
}
|
|
1169
|
+
try requireGlassesConnected(operation: "query video recording status")
|
|
1170
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)")
|
|
1171
|
+
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1172
|
+
throw BluetoothSdkError(
|
|
1173
|
+
code: "request_in_flight",
|
|
1174
|
+
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
|
|
1178
|
+
expectedStatus: "recording_status",
|
|
1179
|
+
pending: pending
|
|
1180
|
+
)
|
|
1181
|
+
DeviceManager.shared.queryVideoRecordingStatus(requestId)
|
|
1182
|
+
do {
|
|
1183
|
+
let event = try await pending.wait()
|
|
1184
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1185
|
+
return event
|
|
1186
|
+
} catch {
|
|
1187
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1188
|
+
throw error
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1144
1192
|
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
1145
1193
|
guard pendingVersionInfo == nil else {
|
|
1146
1194
|
throw BluetoothSdkError(
|
|
@@ -1165,11 +1213,13 @@ public final class MentraBluetoothSDK {
|
|
|
1165
1213
|
}
|
|
1166
1214
|
}
|
|
1167
1215
|
|
|
1168
|
-
|
|
1216
|
+
/// Select the HTTP(S) manifest used by subsequent OTA checks and installs.
|
|
1217
|
+
public func setOtaVersionUrl(_ otaVersionUrl: String) throws {
|
|
1169
1218
|
configuredOtaVersionUrl = try OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
1170
1219
|
}
|
|
1171
1220
|
|
|
1172
|
-
|
|
1221
|
+
/// Return the configured OTA manifest, or this SDK release's default manifest.
|
|
1222
|
+
public func getOtaVersionUrl() throws -> String {
|
|
1173
1223
|
try configuredOtaVersionUrl ?? OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1174
1224
|
}
|
|
1175
1225
|
|
|
@@ -2057,6 +2107,8 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2057
2107
|
if !event.lc3.isEmpty {
|
|
2058
2108
|
delegate?.mentraBluetoothSDK(self, didReceiveMicLc3: event)
|
|
2059
2109
|
}
|
|
2110
|
+
case "mic_health":
|
|
2111
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .micHealth(MicHealthEvent(values: data)))
|
|
2060
2112
|
case "local_transcription":
|
|
2061
2113
|
let event = LocalTranscriptionEvent(
|
|
2062
2114
|
text: data["text"] as? String ?? "",
|
|
@@ -2105,6 +2157,10 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2105
2157
|
let event = GalleryStatusEvent(values: data)
|
|
2106
2158
|
pendingGalleryStatus?.resolve(event)
|
|
2107
2159
|
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "gallery_status", values: event.values))
|
|
2160
|
+
case "pairing_info":
|
|
2161
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "pairing_info", values: data))
|
|
2162
|
+
case "entering_pairing_mode", "owner_replaced":
|
|
2163
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2108
2164
|
case "photo_response":
|
|
2109
2165
|
let event = PhotoResponseEvent(values: data)
|
|
2110
2166
|
handlePhotoResponseForRequests(event)
|
|
@@ -2147,7 +2203,13 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2147
2203
|
var resultValues = data
|
|
2148
2204
|
resultValues["type"] = "ota_status"
|
|
2149
2205
|
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
2150
|
-
|
|
2206
|
+
let event = OtaStatusEvent(values: resultValues)
|
|
2207
|
+
if let errorCode = otaStartRejectionErrorCode(event) {
|
|
2208
|
+
pendingOtaStart?.reject(
|
|
2209
|
+
BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
|
|
2210
|
+
)
|
|
2211
|
+
}
|
|
2212
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
|
|
2151
2213
|
case "settings_ack":
|
|
2152
2214
|
let event = SettingsAckEvent(values: data)
|
|
2153
2215
|
handleSettingsAckForRequests(event)
|
|
@@ -2172,6 +2234,12 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2172
2234
|
}
|
|
2173
2235
|
}
|
|
2174
2236
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2237
|
+
/// OTA status messages are not request-correlated, so only known pre-ack failures settle a start.
|
|
2238
|
+
private func otaStartRejectionErrorCode(_ event: OtaStatusEvent) -> String? {
|
|
2239
|
+
guard event.status.lowercased() == "failed",
|
|
2240
|
+
event.errorMessage?.lowercased() == "battery_low"
|
|
2241
|
+
else {
|
|
2242
|
+
return nil
|
|
2243
|
+
}
|
|
2244
|
+
return "battery_low"
|
|
2245
|
+
}
|