@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.
Files changed (147) hide show
  1. package/README.md +116 -18
  2. package/android/build.gradle +289 -16
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/build.gradle +95 -6
  5. package/android/settings.gradle +3 -0
  6. package/android/silero/LICENSE.md +17 -0
  7. package/android/silero/build.gradle +42 -0
  8. package/android/silero/proguard-rules.pro +24 -0
  9. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  10. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  11. package/android/silero/src/main/assets/LICENSE +21 -0
  12. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  13. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  14. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  15. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  16. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  17. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  18. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  19. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  20. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
  23. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
  24. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
  25. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
  26. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
  27. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
  29. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
  30. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
  31. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
  32. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
  33. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
  34. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
  36. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
  37. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
  42. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
  43. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
  44. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
  45. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
  46. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
  47. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
  48. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
  49. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
  50. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  51. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
  52. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  53. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
  54. package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  55. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
  56. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
  57. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
  58. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
  59. package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
  62. package/android/src/test/resources/avif_with_exif.avif +0 -0
  63. package/build/BluetoothSdk.types.d.ts +241 -43
  64. package/build/BluetoothSdk.types.d.ts.map +1 -1
  65. package/build/BluetoothSdk.types.js +11 -10
  66. package/build/BluetoothSdk.types.js.map +1 -1
  67. package/build/_private/BluetoothSdkModule.d.ts +36 -23
  68. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  69. package/build/_private/BluetoothSdkModule.js +30 -52
  70. package/build/_private/BluetoothSdkModule.js.map +1 -1
  71. package/build/_private/photoRequestPayload.d.ts +4 -0
  72. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  73. package/build/_private/photoRequestPayload.js +25 -0
  74. package/build/_private/photoRequestPayload.js.map +1 -0
  75. package/build/index.d.ts +1 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +23 -0
  78. package/build/index.js.map +1 -1
  79. package/build/react/useMentraBluetooth.d.ts +2 -2
  80. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  81. package/build/react/useMentraBluetooth.js +3 -3
  82. package/build/react/useMentraBluetooth.js.map +1 -1
  83. package/ios/BluetoothSdkModule.swift +174 -70
  84. package/ios/LocalPhotoUploadServer.swift +370 -105
  85. package/ios/MentraBluetoothSDK.podspec +3 -1
  86. package/ios/MentraPhotoReceiverModule.swift +62 -10
  87. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  88. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
  89. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
  90. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  91. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  92. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  93. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  94. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  95. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  96. package/ios/Source/BluetoothSdkDefaults.swift +6 -0
  97. package/ios/Source/Bridge.swift +58 -19
  98. package/ios/Source/DeviceManager.swift +149 -17
  99. package/ios/Source/DeviceStore.swift +16 -6
  100. package/ios/Source/MentraBluetoothSDK.swift +803 -47
  101. package/ios/Source/ObservableStore.swift +8 -2
  102. package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
  103. package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
  104. package/ios/Source/controllers/ControllerManager.swift +4 -3
  105. package/ios/Source/controllers/R1.swift +3 -3
  106. package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
  107. package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
  108. package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
  109. package/ios/Source/services/PhoneMic.swift +1 -0
  110. package/ios/Source/sgcs/G1.swift +2 -2
  111. package/ios/Source/sgcs/G2.swift +645 -403
  112. package/ios/Source/sgcs/Mach1.swift +2 -2
  113. package/ios/Source/sgcs/MentraLive.swift +812 -319
  114. package/ios/Source/sgcs/MentraNex.swift +412 -182
  115. package/ios/Source/sgcs/SGCManager.swift +57 -3
  116. package/ios/Source/sgcs/Simulated.swift +7 -3
  117. package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
  118. package/ios/Source/status/DeviceStatus.swift +974 -0
  119. package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
  120. package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
  121. package/ios/Source/stt/STTTools.swift +27 -12
  122. package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
  123. package/ios/Source/tts/TTSTools.swift +164 -0
  124. package/ios/Source/utils/JSCExperiment.swift +7 -7
  125. package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
  126. package/ios/Source/utils/MessageChunker.swift +78 -21
  127. package/ios/Source/utils/TarBz2Extractor.swift +53 -2
  128. package/package.json +1 -1
  129. package/plugin/build/withAndroid.js +68 -4
  130. package/src/BluetoothSdk.types.ts +331 -59
  131. package/src/_private/BluetoothSdkModule.ts +113 -102
  132. package/src/_private/photoRequestPayload.ts +28 -0
  133. package/src/index.ts +58 -15
  134. package/src/react/useMentraBluetooth.ts +6 -4
  135. package/android/.project +0 -28
  136. package/android/lc3Lib/.project +0 -28
  137. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
  138. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
  139. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
  140. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  141. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  142. package/ios/Source/Status/DeviceStatus.swift +0 -471
  143. /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
  144. /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
  145. /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
  146. /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
  147. /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
@@ -0,0 +1,974 @@
1
+ import Foundation
2
+
3
+ public struct WifiScanResult: CustomStringConvertible {
4
+ public let ssid: String
5
+ public let requiresPassword: Bool
6
+ public let signalStrength: Int
7
+ public let frequency: Int?
8
+
9
+ public init(ssid: String, requiresPassword: Bool, signalStrength: Int, frequency: Int? = nil) {
10
+ self.ssid = ssid
11
+ self.requiresPassword = requiresPassword
12
+ self.signalStrength = signalStrength
13
+ self.frequency = frequency
14
+ }
15
+
16
+ init(values: [String: Any]) {
17
+ ssid = stringValue(values, "ssid") ?? ""
18
+ requiresPassword = boolValue(values, "requiresPassword") ?? false
19
+ signalStrength = intValue(values["signalStrength"]) ?? -1
20
+ frequency = intValue(values["frequency"])
21
+ }
22
+
23
+ var dictionary: [String: Any] {
24
+ var values: [String: Any] = [
25
+ "ssid": ssid,
26
+ "requiresPassword": requiresPassword,
27
+ "signalStrength": signalStrength,
28
+ ]
29
+ if let frequency {
30
+ values["frequency"] = frequency
31
+ }
32
+ return values
33
+ }
34
+
35
+ public var description: String {
36
+ "WifiScanResult(ssid: \(ssid), signalStrength: \(signalStrength))"
37
+ }
38
+ }
39
+
40
+ public enum GlassesConnectionState: String, CustomStringConvertible, Equatable {
41
+ case disconnected = "DISCONNECTED"
42
+ case scanning = "SCANNING"
43
+ case connecting = "CONNECTING"
44
+ case bonding = "BONDING"
45
+ case connected = "CONNECTED"
46
+
47
+ public init(_ value: String?) {
48
+ self = Self.fromValue(value) ?? .disconnected
49
+ }
50
+
51
+ public static func fromValue(_ value: String?) -> GlassesConnectionState? {
52
+ guard let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines).uppercased(),
53
+ !normalized.isEmpty
54
+ else {
55
+ return nil
56
+ }
57
+ return Self(rawValue: normalized)
58
+ }
59
+
60
+ public var isConnected: Bool {
61
+ self == .connected
62
+ }
63
+
64
+ public var isBusy: Bool {
65
+ self == .scanning || self == .connecting || self == .bonding
66
+ }
67
+
68
+ func statusValues(connected: Bool, fullyBooted: Bool) -> [String: Any] {
69
+ if self == .connected || connected || fullyBooted {
70
+ return ["state": "connected", "fullyBooted": fullyBooted]
71
+ }
72
+ switch self {
73
+ case .scanning:
74
+ return ["state": "scanning"]
75
+ case .connecting:
76
+ return ["state": "connecting"]
77
+ case .bonding:
78
+ return ["state": "bonding"]
79
+ case .connected:
80
+ return ["state": "connected", "fullyBooted": fullyBooted]
81
+ case .disconnected:
82
+ return ["state": "disconnected"]
83
+ }
84
+ }
85
+
86
+ public var description: String {
87
+ rawValue
88
+ }
89
+ }
90
+
91
+ struct GlassesStatus: CustomStringConvertible {
92
+ let values: [String: Any]
93
+
94
+ func applying(_ update: GlassesStatusUpdate) -> GlassesStatus {
95
+ GlassesStatus(values: values.merging(update.values) { _, new in new })
96
+ }
97
+
98
+ func withBattery(level: Int, charging: Bool) -> GlassesStatus {
99
+ applying(GlassesStatusUpdate(values: ["batteryLevel": level, "charging": charging]))
100
+ }
101
+
102
+ func withWifi(_ wifi: WifiStatus) -> GlassesStatus {
103
+ applying(GlassesStatusUpdate(values: wifi.storeValues))
104
+ }
105
+
106
+ func withHotspot(_ hotspot: HotspotStatus) -> GlassesStatus {
107
+ applying(GlassesStatusUpdate(values: hotspot.storeValues))
108
+ }
109
+
110
+ func disconnected() -> GlassesStatus {
111
+ applying(GlassesStatusUpdate(values: [
112
+ "connected": false,
113
+ "connectionState": "DISCONNECTED",
114
+ "fullyBooted": false,
115
+ "batteryLevel": -1,
116
+ "charging": false,
117
+ "hotspotEnabled": false,
118
+ "hotspotGatewayIp": "",
119
+ "hotspotPassword": "",
120
+ "hotspotSsid": "",
121
+ "wifiConnected": false,
122
+ "wifiSsid": "",
123
+ "wifiLocalIp": "",
124
+ "signalStrength": -1,
125
+ "signalStrengthUpdatedAt": 0,
126
+ ]))
127
+ }
128
+
129
+ var fullyBooted: Bool {
130
+ boolValue(values, "fullyBooted") ?? false
131
+ }
132
+
133
+ var connected: Bool {
134
+ boolValue(values, "connected") ?? false
135
+ }
136
+
137
+ var micEnabled: Bool {
138
+ boolValue(values, "micEnabled") ?? false
139
+ }
140
+
141
+ var voiceActivityDetectionEnabled: Bool {
142
+ boolValue(values, "voiceActivityDetectionEnabled") ?? BluetoothSdkDefaults.voiceActivityDetectionEnabled
143
+ }
144
+
145
+ var connectionState: GlassesConnectionState {
146
+ GlassesConnectionState(stringValue(values, "connectionState"))
147
+ }
148
+
149
+ var bluetoothClassicConnected: Bool {
150
+ boolValue(values, "bluetoothClassicConnected") ?? false
151
+ }
152
+
153
+ var signalStrength: Int {
154
+ intValue(values["signalStrength"]) ?? -1
155
+ }
156
+
157
+ var signalStrengthUpdatedAt: Int {
158
+ intValue(values["signalStrengthUpdatedAt"]) ?? 0
159
+ }
160
+
161
+ var deviceModel: String {
162
+ stringValue(values, "deviceModel") ?? ""
163
+ }
164
+
165
+ var androidVersion: String {
166
+ stringValue(values, "androidVersion") ?? ""
167
+ }
168
+
169
+ var firmwareVersion: String {
170
+ stringValue(values, "firmwareVersion") ?? ""
171
+ }
172
+
173
+ var besFirmwareVersion: String {
174
+ stringValue(values, "besFirmwareVersion") ?? ""
175
+ }
176
+
177
+ var mtkFirmwareVersion: String {
178
+ stringValue(values, "mtkFirmwareVersion") ?? ""
179
+ }
180
+
181
+ var bluetoothMacAddress: String {
182
+ stringValue(values, "bluetoothMacAddress") ?? ""
183
+ }
184
+
185
+ var leftMacAddress: String {
186
+ stringValue(values, "leftMacAddress") ?? ""
187
+ }
188
+
189
+ var rightMacAddress: String {
190
+ stringValue(values, "rightMacAddress") ?? ""
191
+ }
192
+
193
+ var macAddress: String {
194
+ stringValue(values, "macAddress") ?? ""
195
+ }
196
+
197
+ var buildNumber: String {
198
+ stringValue(values, "buildNumber") ?? ""
199
+ }
200
+
201
+ var otaVersionUrl: String {
202
+ stringValue(values, "otaVersionUrl") ?? ""
203
+ }
204
+
205
+ var appVersion: String {
206
+ stringValue(values, "appVersion") ?? ""
207
+ }
208
+
209
+ var bluetoothName: String {
210
+ stringValue(values, "bluetoothName") ?? ""
211
+ }
212
+
213
+ var serialNumber: String {
214
+ stringValue(values, "serialNumber") ?? ""
215
+ }
216
+
217
+ var style: String {
218
+ stringValue(values, "style") ?? ""
219
+ }
220
+
221
+ var color: String {
222
+ stringValue(values, "color") ?? ""
223
+ }
224
+
225
+ var wifi: WifiStatus {
226
+ WifiStatus.fromStoreValues(values) ?? .disconnected
227
+ }
228
+
229
+ var hotspot: HotspotStatus {
230
+ HotspotStatus.fromStoreValues(values) ?? .disabled
231
+ }
232
+
233
+ var dictionary: [String: Any] {
234
+ Self.dictionary(from: values)
235
+ }
236
+
237
+ var batteryLevel: Int {
238
+ intValue(values["batteryLevel"]) ?? -1
239
+ }
240
+
241
+ var charging: Bool {
242
+ boolValue(values, "charging") ?? false
243
+ }
244
+
245
+ var caseBatteryLevel: Int {
246
+ intValue(values["caseBatteryLevel"]) ?? -1
247
+ }
248
+
249
+ var caseCharging: Bool {
250
+ boolValue(values, "caseCharging") ?? false
251
+ }
252
+
253
+ var caseOpen: Bool {
254
+ boolValue(values, "caseOpen") ?? true
255
+ }
256
+
257
+ var caseRemoved: Bool {
258
+ boolValue(values, "caseRemoved") ?? true
259
+ }
260
+
261
+ var headUp: Bool {
262
+ boolValue(values, "headUp") ?? false
263
+ }
264
+
265
+ var controllerConnected: Bool {
266
+ boolValue(values, "controllerConnected") ?? false
267
+ }
268
+
269
+ var controllerFullyBooted: Bool {
270
+ boolValue(values, "controllerFullyBooted") ?? false
271
+ }
272
+
273
+ var controllerMacAddress: String {
274
+ stringValue(values, "controllerMacAddress") ?? ""
275
+ }
276
+
277
+ var controllerBatteryLevel: Int {
278
+ intValue(values["controllerBatteryLevel"]) ?? -1
279
+ }
280
+
281
+ var controllerSignalStrength: Int {
282
+ intValue(values["controllerSignalStrength"]) ?? -1
283
+ }
284
+
285
+ var ringSignalStrength: Int {
286
+ intValue(values["ringSignalStrength"]) ?? -1
287
+ }
288
+
289
+ var description: String {
290
+ values.description
291
+ }
292
+
293
+ static func dictionary(from values: [String: Any]) -> [String: Any] {
294
+ var dictionary = values
295
+ dictionary["connection"] = GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
296
+ connected: boolValue(values, "connected") ?? false,
297
+ fullyBooted: boolValue(values, "fullyBooted") ?? false
298
+ )
299
+ dictionary.removeValue(forKey: "connected")
300
+ dictionary.removeValue(forKey: "fullyBooted")
301
+ dictionary.removeValue(forKey: "connectionState")
302
+ dictionary["wifi"] = (WifiStatus.fromStoreValues(values) ?? .disconnected).values
303
+ dictionary["hotspot"] = (HotspotStatus.fromStoreValues(values) ?? .disabled).values
304
+ dictionary.removeValue(forKey: "wifiConnected")
305
+ dictionary.removeValue(forKey: "wifiSsid")
306
+ dictionary.removeValue(forKey: "wifiLocalIp")
307
+ dictionary.removeValue(forKey: "hotspotEnabled")
308
+ dictionary.removeValue(forKey: "hotspotSsid")
309
+ dictionary.removeValue(forKey: "hotspotPassword")
310
+ dictionary.removeValue(forKey: "hotspotGatewayIp")
311
+ return dictionary
312
+ }
313
+
314
+ static func updateDictionary(from values: [String: Any]) -> [String: Any] {
315
+ var dictionary = values
316
+ if hasAnyKey(values, "connection", "connected", "fullyBooted", "connectionState") {
317
+ dictionary["connection"] = (values["connection"] as? [String: Any])
318
+ ?? GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
319
+ connected: boolValue(values, "connected") ?? false,
320
+ fullyBooted: boolValue(values, "fullyBooted") ?? false
321
+ )
322
+ dictionary.removeValue(forKey: "connected")
323
+ dictionary.removeValue(forKey: "fullyBooted")
324
+ dictionary.removeValue(forKey: "connectionState")
325
+ }
326
+ if hasAnyKey(values, "wifi", "wifiConnected", "wifiSsid", "wifiLocalIp") {
327
+ let wifi = (values["wifi"] as? [String: Any]).flatMap(WifiStatus.init(values:))
328
+ ?? WifiStatus.fromStoreValues(values)
329
+ if let wifi {
330
+ dictionary["wifi"] = wifi.values
331
+ }
332
+ dictionary.removeValue(forKey: "wifiConnected")
333
+ dictionary.removeValue(forKey: "wifiSsid")
334
+ dictionary.removeValue(forKey: "wifiLocalIp")
335
+ }
336
+ if hasAnyKey(values, "hotspot") {
337
+ if let hotspot = (values["hotspot"] as? [String: Any]).flatMap(HotspotStatus.init(values:)) {
338
+ dictionary["hotspot"] = hotspot.values
339
+ }
340
+ } else if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp") {
341
+ if let hotspot = HotspotStatus.fromStoreValues(values) {
342
+ dictionary["hotspot"] = hotspot.values
343
+ }
344
+ dictionary.removeValue(forKey: "hotspotEnabled")
345
+ dictionary.removeValue(forKey: "hotspotSsid")
346
+ dictionary.removeValue(forKey: "hotspotPassword")
347
+ dictionary.removeValue(forKey: "hotspotGatewayIp")
348
+ }
349
+ return dictionary
350
+ }
351
+ }
352
+
353
+ public struct VersionInfoResult: CustomStringConvertible {
354
+ public let androidVersion: String
355
+ public let firmwareVersion: String
356
+ public let besFirmwareVersion: String
357
+ public let mtkFirmwareVersion: String
358
+ public let buildNumber: String
359
+ public let systemTimeMs: Int?
360
+ public let otaVersionUrl: String
361
+ public let appVersion: String
362
+
363
+ init(status: GlassesStatus) {
364
+ androidVersion = status.androidVersion
365
+ firmwareVersion = status.firmwareVersion
366
+ besFirmwareVersion = status.besFirmwareVersion
367
+ mtkFirmwareVersion = status.mtkFirmwareVersion
368
+ buildNumber = status.buildNumber
369
+ systemTimeMs = intValue(status.values["systemTimeMs"])
370
+ otaVersionUrl = status.otaVersionUrl
371
+ appVersion = status.appVersion
372
+ }
373
+
374
+ init(values: [String: Any]) {
375
+ androidVersion = stringValue(values, "androidVersion", "android_version") ?? ""
376
+ firmwareVersion = stringValue(values, "firmwareVersion", "firmware_version") ?? ""
377
+ besFirmwareVersion = stringValue(values, "besFirmwareVersion", "bes_fw_version") ?? ""
378
+ mtkFirmwareVersion = stringValue(values, "mtkFirmwareVersion", "mtk_fw_version") ?? ""
379
+ buildNumber = stringValue(values, "buildNumber", "build_number") ?? ""
380
+ systemTimeMs = intValue(values["systemTimeMs"]) ?? intValue(values["system_time_ms"])
381
+ otaVersionUrl = stringValue(values, "otaVersionUrl", "ota_version_url") ?? ""
382
+ appVersion = stringValue(values, "appVersion", "app_version") ?? ""
383
+ }
384
+
385
+ public var dictionary: [String: Any] {
386
+ var values: [String: Any] = [
387
+ "androidVersion": androidVersion,
388
+ "firmwareVersion": firmwareVersion,
389
+ "besFirmwareVersion": besFirmwareVersion,
390
+ "mtkFirmwareVersion": mtkFirmwareVersion,
391
+ "buildNumber": buildNumber,
392
+ "otaVersionUrl": otaVersionUrl,
393
+ "appVersion": appVersion,
394
+ ]
395
+ if let systemTimeMs {
396
+ values["systemTimeMs"] = systemTimeMs
397
+ }
398
+ return values
399
+ }
400
+
401
+ public var description: String {
402
+ "VersionInfoResult(buildNumber: \(buildNumber), appVersion: \(appVersion))"
403
+ }
404
+ }
405
+
406
+ struct BluetoothStatus: CustomStringConvertible {
407
+ let values: [String: Any]
408
+
409
+ init(values: [String: Any]) {
410
+ self.values = Self.normalized(values)
411
+ }
412
+
413
+ private static func normalized(_ values: [String: Any]) -> [String: Any] {
414
+ var normalizedValues = values
415
+ if let searchResults = values["searchResults"] as? [[String: Any]] {
416
+ normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
417
+ }
418
+ if let galleryMode = boolValue(values, "gallery_mode") {
419
+ normalizedValues["galleryModeEnabled"] = galleryMode
420
+ normalizedValues.removeValue(forKey: "gallery_mode")
421
+ }
422
+ return normalizedValues
423
+ }
424
+
425
+ func applying(_ update: BluetoothStatusUpdate) -> BluetoothStatus {
426
+ BluetoothStatus(values: values.merging(update.values) { _, new in new })
427
+ }
428
+
429
+ func withDefaultDevice(_ device: Device?) -> BluetoothStatus {
430
+ applying(BluetoothStatusUpdate(values: [
431
+ "default_wearable": device?.model.deviceType ?? "",
432
+ "device_name": device?.name ?? "",
433
+ "device_address": device?.identifier ?? "",
434
+ ]))
435
+ }
436
+
437
+ var searching: Bool {
438
+ boolValue(values, "searching") ?? false
439
+ }
440
+
441
+ var searchingController: Bool {
442
+ boolValue(values, "searchingController") ?? false
443
+ }
444
+
445
+ var systemMicUnavailable: Bool {
446
+ boolValue(values, "systemMicUnavailable") ?? false
447
+ }
448
+
449
+ var micEnabled: Bool {
450
+ boolValue(values, "micEnabled") ?? false
451
+ }
452
+
453
+ var currentMic: String {
454
+ stringValue(values, "currentMic") ?? ""
455
+ }
456
+
457
+ var micRanking: [String] {
458
+ stringListValue(values, "micRanking")
459
+ }
460
+
461
+ /// Nearby glasses in stable discovery order. Existing entries keep their array position as
462
+ /// details refresh; new glasses append at the end, and removals should not reorder remaining entries.
463
+ var searchResults: [Device] {
464
+ dictionaryListValue(values, "searchResults").compactMap(Device.init(values:))
465
+ }
466
+
467
+ var wifiScanResults: [WifiScanResult] {
468
+ dictionaryListValue(values, "wifiScanResults").map(WifiScanResult.init(values:))
469
+ }
470
+
471
+ var lastLog: [String] {
472
+ stringListValue(values, "lastLog")
473
+ }
474
+
475
+ var otherBtConnected: Bool {
476
+ boolValue(values, "otherBtConnected") ?? false
477
+ }
478
+
479
+ var defaultWearable: String {
480
+ stringValue(values, "default_wearable") ?? ""
481
+ }
482
+
483
+ var pendingWearable: String {
484
+ stringValue(values, "pending_wearable") ?? ""
485
+ }
486
+
487
+ var deviceName: String {
488
+ stringValue(values, "device_name") ?? ""
489
+ }
490
+
491
+ var deviceAddress: String {
492
+ stringValue(values, "device_address") ?? ""
493
+ }
494
+
495
+ var defaultController: String {
496
+ stringValue(values, "default_controller") ?? ""
497
+ }
498
+
499
+ var pendingController: String {
500
+ stringValue(values, "pending_controller") ?? ""
501
+ }
502
+
503
+ var controllerDeviceName: String {
504
+ stringValue(values, "controller_device_name") ?? ""
505
+ }
506
+
507
+ var screenDisabled: Bool {
508
+ boolValue(values, "screen_disabled") ?? false
509
+ }
510
+
511
+ var preferredMic: String {
512
+ stringValue(values, "preferred_mic") ?? "auto"
513
+ }
514
+
515
+ var sensingEnabled: Bool {
516
+ boolValue(values, "sensing_enabled") ?? true
517
+ }
518
+
519
+ var powerSavingMode: Bool {
520
+ boolValue(values, "power_saving_mode") ?? false
521
+ }
522
+
523
+ var brightness: Int {
524
+ intValue(values["brightness"]) ?? 50
525
+ }
526
+
527
+ var autoBrightness: Bool {
528
+ boolValue(values, "auto_brightness") ?? true
529
+ }
530
+
531
+ var dashboardHeight: Int {
532
+ intValue(values["dashboard_height"]) ?? 4
533
+ }
534
+
535
+ var dashboardDepth: Int {
536
+ intValue(values["dashboard_depth"]) ?? 2
537
+ }
538
+
539
+ var headUpAngle: Int {
540
+ intValue(values["head_up_angle"]) ?? 30
541
+ }
542
+
543
+ var contextualDashboard: Bool {
544
+ boolValue(values, "contextual_dashboard") ?? true
545
+ }
546
+
547
+ var galleryModeEnabled: Bool {
548
+ boolValue(values, "gallery_mode") ?? boolValue(values, "galleryModeEnabled") ?? true
549
+ }
550
+
551
+ var buttonPhotoSize: ButtonPhotoSize {
552
+ ButtonPhotoSize(rawValue: stringValue(values, "button_photo_size") ?? "") ?? .medium
553
+ }
554
+
555
+ var buttonCameraLed: Bool {
556
+ boolValue(values, "button_camera_led") ?? true
557
+ }
558
+
559
+ var buttonMaxRecordingTime: Int {
560
+ intValue(values["button_max_recording_time"]) ?? 10
561
+ }
562
+
563
+ var buttonVideoWidth: Int {
564
+ intValue(values["button_video_width"]) ?? 1280
565
+ }
566
+
567
+ var buttonVideoHeight: Int {
568
+ intValue(values["button_video_height"]) ?? 720
569
+ }
570
+
571
+ var buttonVideoFrameRate: Int {
572
+ intValue(values["button_video_fps"]) ?? 30
573
+ }
574
+
575
+ var shouldSendPcm: Bool {
576
+ boolValue(values, "should_send_pcm") ?? false
577
+ }
578
+
579
+ var shouldSendLc3: Bool {
580
+ boolValue(values, "should_send_lc3") ?? false
581
+ }
582
+
583
+ var shouldSendTranscript: Bool {
584
+ boolValue(values, "should_send_transcript") ?? false
585
+ }
586
+
587
+ var offlineCaptionsRunning: Bool {
588
+ boolValue(values, "offline_captions_running") ?? false
589
+ }
590
+
591
+ var localSttFallbackActive: Bool {
592
+ boolValue(values, "local_stt_fallback_active") ?? false
593
+ }
594
+
595
+ var shouldSendBootingMessage: Bool {
596
+ boolValue(values, "shouldSendBootingMessage") ?? true
597
+ }
598
+
599
+ var defaultDevice: Device? {
600
+ guard !defaultWearable.isEmpty else { return nil }
601
+ return Device(
602
+ model: DeviceModel.fromDeviceType(defaultWearable),
603
+ name: deviceName,
604
+ identifier: deviceAddress.isEmpty ? nil : deviceAddress
605
+ )
606
+ }
607
+
608
+ var description: String {
609
+ values.description
610
+ }
611
+ }
612
+
613
+ struct GlassesStatusUpdate: CustomStringConvertible {
614
+ let values: [String: Any]
615
+
616
+ var fullyBooted: Bool? {
617
+ optionalBoolValue(values, "fullyBooted")
618
+ }
619
+
620
+ var connected: Bool? {
621
+ optionalBoolValue(values, "connected")
622
+ }
623
+
624
+ var micEnabled: Bool? {
625
+ optionalBoolValue(values, "micEnabled")
626
+ }
627
+
628
+ var voiceActivityDetectionEnabled: Bool? {
629
+ optionalBoolValue(values, "voiceActivityDetectionEnabled")
630
+ }
631
+
632
+ var connectionState: GlassesConnectionState? {
633
+ GlassesConnectionState.fromValue(optionalStringValue(values, "connectionState"))
634
+ }
635
+
636
+ var bluetoothClassicConnected: Bool? {
637
+ optionalBoolValue(values, "bluetoothClassicConnected")
638
+ }
639
+
640
+ var signalStrength: Int? {
641
+ optionalIntValue(values, "signalStrength")
642
+ }
643
+
644
+ var signalStrengthUpdatedAt: Int? {
645
+ optionalIntValue(values, "signalStrengthUpdatedAt")
646
+ }
647
+
648
+ var deviceModel: String? {
649
+ optionalStringValue(values, "deviceModel")
650
+ }
651
+
652
+ var androidVersion: String? {
653
+ optionalStringValue(values, "androidVersion")
654
+ }
655
+
656
+ var firmwareVersion: String? {
657
+ optionalStringValue(values, "firmwareVersion")
658
+ }
659
+
660
+ var besFirmwareVersion: String? {
661
+ optionalStringValue(values, "besFirmwareVersion")
662
+ }
663
+
664
+ var mtkFirmwareVersion: String? {
665
+ optionalStringValue(values, "mtkFirmwareVersion")
666
+ }
667
+
668
+ var bluetoothMacAddress: String? {
669
+ optionalStringValue(values, "bluetoothMacAddress")
670
+ }
671
+
672
+ var leftMacAddress: String? {
673
+ optionalStringValue(values, "leftMacAddress")
674
+ }
675
+
676
+ var rightMacAddress: String? {
677
+ optionalStringValue(values, "rightMacAddress")
678
+ }
679
+
680
+ var macAddress: String? {
681
+ optionalStringValue(values, "macAddress")
682
+ }
683
+
684
+ var buildNumber: String? {
685
+ optionalStringValue(values, "buildNumber")
686
+ }
687
+
688
+ var otaVersionUrl: String? {
689
+ optionalStringValue(values, "otaVersionUrl")
690
+ }
691
+
692
+ var appVersion: String? {
693
+ optionalStringValue(values, "appVersion")
694
+ }
695
+
696
+ var bluetoothName: String? {
697
+ optionalStringValue(values, "bluetoothName")
698
+ }
699
+
700
+ var serialNumber: String? {
701
+ optionalStringValue(values, "serialNumber")
702
+ }
703
+
704
+ var style: String? {
705
+ optionalStringValue(values, "style")
706
+ }
707
+
708
+ var color: String? {
709
+ optionalStringValue(values, "color")
710
+ }
711
+
712
+ var wifi: WifiStatus? {
713
+ if let wifi = values["wifi"] as? [String: Any] {
714
+ return WifiStatus(values: wifi)
715
+ }
716
+ if hasAnyKey(values, "wifiConnected", "wifiSsid", "wifiLocalIp") {
717
+ return WifiStatus.fromStoreValues(values)
718
+ }
719
+ return nil
720
+ }
721
+
722
+ var hotspot: HotspotStatus? {
723
+ if let hotspot = values["hotspot"] as? [String: Any] {
724
+ return HotspotStatus(values: hotspot)
725
+ }
726
+ if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp") {
727
+ return HotspotStatus.fromStoreValues(values)
728
+ }
729
+ return nil
730
+ }
731
+
732
+ var dictionary: [String: Any] {
733
+ GlassesStatus.updateDictionary(from: values)
734
+ }
735
+
736
+ var batteryLevel: Int? {
737
+ optionalIntValue(values, "batteryLevel")
738
+ }
739
+
740
+ var charging: Bool? {
741
+ optionalBoolValue(values, "charging")
742
+ }
743
+
744
+ var caseBatteryLevel: Int? {
745
+ optionalIntValue(values, "caseBatteryLevel")
746
+ }
747
+
748
+ var caseCharging: Bool? {
749
+ optionalBoolValue(values, "caseCharging")
750
+ }
751
+
752
+ var caseOpen: Bool? {
753
+ optionalBoolValue(values, "caseOpen")
754
+ }
755
+
756
+ var caseRemoved: Bool? {
757
+ optionalBoolValue(values, "caseRemoved")
758
+ }
759
+
760
+ var headUp: Bool? {
761
+ optionalBoolValue(values, "headUp")
762
+ }
763
+
764
+ var controllerConnected: Bool? {
765
+ optionalBoolValue(values, "controllerConnected")
766
+ }
767
+
768
+ var controllerFullyBooted: Bool? {
769
+ optionalBoolValue(values, "controllerFullyBooted")
770
+ }
771
+
772
+ var controllerMacAddress: String? {
773
+ optionalStringValue(values, "controllerMacAddress")
774
+ }
775
+
776
+ var controllerBatteryLevel: Int? {
777
+ optionalIntValue(values, "controllerBatteryLevel")
778
+ }
779
+
780
+ var controllerSignalStrength: Int? {
781
+ optionalIntValue(values, "controllerSignalStrength")
782
+ }
783
+
784
+ var ringSignalStrength: Int? {
785
+ optionalIntValue(values, "ringSignalStrength")
786
+ }
787
+
788
+ var description: String {
789
+ values.description
790
+ }
791
+ }
792
+
793
+ struct BluetoothStatusUpdate: CustomStringConvertible {
794
+ let values: [String: Any]
795
+
796
+ init(values: [String: Any]) {
797
+ var normalizedValues = values
798
+ if let searchResults = values["searchResults"] as? [[String: Any]] {
799
+ normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
800
+ }
801
+ if let galleryMode = optionalBoolValue(values, "gallery_mode") {
802
+ normalizedValues["galleryModeEnabled"] = galleryMode
803
+ normalizedValues.removeValue(forKey: "gallery_mode")
804
+ }
805
+ self.values = normalizedValues
806
+ }
807
+
808
+ var searching: Bool? {
809
+ optionalBoolValue(values, "searching")
810
+ }
811
+
812
+ var searchingController: Bool? {
813
+ optionalBoolValue(values, "searchingController")
814
+ }
815
+
816
+ var systemMicUnavailable: Bool? {
817
+ optionalBoolValue(values, "systemMicUnavailable")
818
+ }
819
+
820
+ var micEnabled: Bool? {
821
+ optionalBoolValue(values, "micEnabled")
822
+ }
823
+
824
+ var currentMic: String? {
825
+ optionalStringValue(values, "currentMic")
826
+ }
827
+
828
+ var micRanking: [String]? {
829
+ optionalStringListValue(values, "micRanking")
830
+ }
831
+
832
+ /// Nearby glasses in stable discovery order when included in an update. Existing entries keep their
833
+ /// array position as details refresh; new glasses append at the end, and removals should not reorder
834
+ /// remaining entries.
835
+ var searchResults: [Device]? {
836
+ optionalDictionaryListValue(values, "searchResults")?.compactMap(Device.init(values:))
837
+ }
838
+
839
+ var wifiScanResults: [WifiScanResult]? {
840
+ optionalDictionaryListValue(values, "wifiScanResults")?.map(WifiScanResult.init(values:))
841
+ }
842
+
843
+ var lastLog: [String]? {
844
+ optionalStringListValue(values, "lastLog")
845
+ }
846
+
847
+ var otherBtConnected: Bool? {
848
+ optionalBoolValue(values, "otherBtConnected")
849
+ }
850
+
851
+ var defaultWearable: String? {
852
+ optionalStringValue(values, "default_wearable")
853
+ }
854
+
855
+ var pendingWearable: String? {
856
+ optionalStringValue(values, "pending_wearable")
857
+ }
858
+
859
+ var deviceName: String? {
860
+ optionalStringValue(values, "device_name")
861
+ }
862
+
863
+ var deviceAddress: String? {
864
+ optionalStringValue(values, "device_address")
865
+ }
866
+
867
+ var defaultController: String? {
868
+ optionalStringValue(values, "default_controller")
869
+ }
870
+
871
+ var pendingController: String? {
872
+ optionalStringValue(values, "pending_controller")
873
+ }
874
+
875
+ var controllerDeviceName: String? {
876
+ optionalStringValue(values, "controller_device_name")
877
+ }
878
+
879
+ var screenDisabled: Bool? {
880
+ optionalBoolValue(values, "screen_disabled")
881
+ }
882
+
883
+ var preferredMic: String? {
884
+ optionalStringValue(values, "preferred_mic")
885
+ }
886
+
887
+ var sensingEnabled: Bool? {
888
+ optionalBoolValue(values, "sensing_enabled")
889
+ }
890
+
891
+ var powerSavingMode: Bool? {
892
+ optionalBoolValue(values, "power_saving_mode")
893
+ }
894
+
895
+ var brightness: Int? {
896
+ optionalIntValue(values, "brightness")
897
+ }
898
+
899
+ var autoBrightness: Bool? {
900
+ optionalBoolValue(values, "auto_brightness")
901
+ }
902
+
903
+ var dashboardHeight: Int? {
904
+ optionalIntValue(values, "dashboard_height")
905
+ }
906
+
907
+ var dashboardDepth: Int? {
908
+ optionalIntValue(values, "dashboard_depth")
909
+ }
910
+
911
+ var headUpAngle: Int? {
912
+ optionalIntValue(values, "head_up_angle")
913
+ }
914
+
915
+ var contextualDashboard: Bool? {
916
+ optionalBoolValue(values, "contextual_dashboard")
917
+ }
918
+
919
+ var galleryModeEnabled: Bool? {
920
+ optionalBoolValue(values, "gallery_mode") ?? optionalBoolValue(values, "galleryModeEnabled")
921
+ }
922
+
923
+ var buttonPhotoSize: ButtonPhotoSize? {
924
+ optionalStringValue(values, "button_photo_size").flatMap(ButtonPhotoSize.init(rawValue:))
925
+ }
926
+
927
+ var buttonCameraLed: Bool? {
928
+ optionalBoolValue(values, "button_camera_led")
929
+ }
930
+
931
+ var buttonMaxRecordingTime: Int? {
932
+ optionalIntValue(values, "button_max_recording_time")
933
+ }
934
+
935
+ var buttonVideoWidth: Int? {
936
+ optionalIntValue(values, "button_video_width")
937
+ }
938
+
939
+ var buttonVideoHeight: Int? {
940
+ optionalIntValue(values, "button_video_height")
941
+ }
942
+
943
+ var buttonVideoFrameRate: Int? {
944
+ optionalIntValue(values, "button_video_fps")
945
+ }
946
+
947
+ var shouldSendPcm: Bool? {
948
+ optionalBoolValue(values, "should_send_pcm")
949
+ }
950
+
951
+ var shouldSendLc3: Bool? {
952
+ optionalBoolValue(values, "should_send_lc3")
953
+ }
954
+
955
+ var shouldSendTranscript: Bool? {
956
+ optionalBoolValue(values, "should_send_transcript")
957
+ }
958
+
959
+ var offlineCaptionsRunning: Bool? {
960
+ optionalBoolValue(values, "offline_captions_running")
961
+ }
962
+
963
+ var localSttFallbackActive: Bool? {
964
+ optionalBoolValue(values, "local_stt_fallback_active")
965
+ }
966
+
967
+ var shouldSendBootingMessage: Bool? {
968
+ optionalBoolValue(values, "shouldSendBootingMessage")
969
+ }
970
+
971
+ var description: String {
972
+ values.description
973
+ }
974
+ }