@mentra/bluetooth-sdk 0.1.5 → 0.1.6

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 (42) hide show
  1. package/README.md +29 -13
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -7
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -46
  5. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +125 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +187 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +60 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +132 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +39 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +605 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +199 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +173 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +348 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +75 -0
  16. package/build/BluetoothSdk.types.d.ts +4 -0
  17. package/build/BluetoothSdk.types.d.ts.map +1 -1
  18. package/build/BluetoothSdk.types.js.map +1 -1
  19. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  20. package/build/_private/BluetoothSdkModule.js +3 -0
  21. package/build/_private/BluetoothSdkModule.js.map +1 -1
  22. package/ios/BluetoothSdkModule.swift +4 -4
  23. package/ios/Source/Audio/AudioModels.swift +159 -0
  24. package/ios/Source/Camera/CameraModels.swift +246 -0
  25. package/ios/Source/Connection/ScanSession.swift +27 -0
  26. package/ios/Source/DeviceStore.swift +1 -0
  27. package/ios/Source/Errors/BluetoothError.swift +19 -0
  28. package/ios/Source/Events/BluetoothEvents.swift +115 -0
  29. package/ios/Source/Internal/BluetoothAvailability.swift +58 -0
  30. package/ios/Source/Internal/ValueParsing.swift +90 -0
  31. package/ios/Source/MentraBluetoothSDK.swift +25 -2140
  32. package/ios/Source/Requests/DisplayRequests.swift +58 -0
  33. package/ios/Source/Status/DeviceStatus.swift +463 -0
  34. package/ios/Source/Status/RuntimeState.swift +350 -0
  35. package/ios/Source/Status/WifiHotspotStatus.swift +326 -0
  36. package/ios/Source/Streaming/StreamModels.swift +436 -0
  37. package/ios/Source/Types/DeviceModels.swift +134 -0
  38. package/ios/Source/sgcs/G2.swift +97 -36
  39. package/package.json +1 -1
  40. package/src/BluetoothSdk.types.ts +4 -0
  41. package/src/_private/BluetoothSdkModule.ts +3 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +0 -1810
@@ -1,2120 +1,6 @@
1
1
  import CoreBluetooth
2
2
  import Foundation
3
3
 
4
- private func intValue(_ value: Any?) -> Int? {
5
- if let int = value as? Int { return int }
6
- if let double = value as? Double { return Int(double) }
7
- if let number = value as? NSNumber { return number.intValue }
8
- return nil
9
- }
10
-
11
- private func stringValue(_ values: [String: Any], _ keys: String...) -> String? {
12
- stringValue(values, keys)
13
- }
14
-
15
- private func stringValue(_ values: [String: Any], _ keys: [String]) -> String? {
16
- for key in keys {
17
- if let value = values[key] as? String {
18
- return value
19
- }
20
- }
21
- return nil
22
- }
23
-
24
- private func boolValue(_ values: [String: Any], _ keys: String...) -> Bool? {
25
- boolValue(values, keys)
26
- }
27
-
28
- private func boolValue(_ values: [String: Any], _ keys: [String]) -> Bool? {
29
- for key in keys {
30
- if let value = values[key] as? Bool { return value }
31
- if let value = values[key] as? NSNumber { return value.boolValue }
32
- }
33
- return nil
34
- }
35
-
36
- private func hasAnyKey(_ values: [String: Any], _ keys: String...) -> Bool {
37
- hasAnyKey(values, keys)
38
- }
39
-
40
- private func hasAnyKey(_ values: [String: Any], _ keys: [String]) -> Bool {
41
- keys.contains { values.keys.contains($0) }
42
- }
43
-
44
- private func optionalStringValue(_ values: [String: Any], _ keys: String...) -> String? {
45
- hasAnyKey(values, keys) ? (stringValue(values, keys) ?? "") : nil
46
- }
47
-
48
- private func nonEmptyStringValue(_ values: [String: Any], _ keys: String...) -> String? {
49
- for key in keys {
50
- guard let value = values[key] as? String else { continue }
51
- let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
52
- if !trimmed.isEmpty {
53
- return value
54
- }
55
- }
56
- return nil
57
- }
58
-
59
- private func optionalIntValue(_ values: [String: Any], _ keys: String...) -> Int? {
60
- guard hasAnyKey(values, keys) else { return nil }
61
- for key in keys {
62
- if let value = intValue(values[key]) { return value }
63
- }
64
- return nil
65
- }
66
-
67
- private func optionalBoolValue(_ values: [String: Any], _ keys: String...) -> Bool? {
68
- hasAnyKey(values, keys) ? (boolValue(values, keys) ?? false) : nil
69
- }
70
-
71
- private func stringListValue(_ values: [String: Any], _ key: String) -> [String] {
72
- values[key] as? [String] ?? []
73
- }
74
-
75
- private func optionalStringListValue(_ values: [String: Any], _ key: String) -> [String]? {
76
- values.keys.contains(key) ? stringListValue(values, key) : nil
77
- }
78
-
79
- private func dictionaryListValue(_ values: [String: Any], _ key: String) -> [[String: Any]] {
80
- values[key] as? [[String: Any]] ?? []
81
- }
82
-
83
- private func optionalDictionaryListValue(_ values: [String: Any], _ key: String) -> [[String: Any]]? {
84
- values.keys.contains(key) ? dictionaryListValue(values, key) : nil
85
- }
86
-
87
- private func putIfNotNil(_ map: inout [String: Any], _ key: String, _ value: Any?) {
88
- if let value {
89
- map[key] = value
90
- }
91
- }
92
-
93
- public struct MentraBluetoothSDKConfiguration {
94
- public static let `default` = MentraBluetoothSDKConfiguration()
95
-
96
- public init() {}
97
- }
98
-
99
- public enum DeviceModel: String {
100
- case g1
101
- case g2
102
- case mentraLive
103
- case mentraNex
104
- case mach1
105
- case z100
106
- case frame
107
- case simulated
108
- case r1
109
-
110
- public var deviceType: String {
111
- switch self {
112
- case .g1:
113
- DeviceTypes.G1
114
- case .g2:
115
- DeviceTypes.G2
116
- case .mentraLive:
117
- DeviceTypes.LIVE
118
- case .mentraNex:
119
- DeviceTypes.NEX
120
- case .mach1:
121
- DeviceTypes.MACH1
122
- case .z100:
123
- DeviceTypes.Z100
124
- case .frame:
125
- DeviceTypes.FRAME
126
- case .simulated:
127
- DeviceTypes.SIMULATED
128
- case .r1:
129
- ControllerTypes.R1
130
- }
131
- }
132
-
133
- public static func fromDeviceType(_ deviceType: String?) -> DeviceModel {
134
- switch deviceType {
135
- case DeviceTypes.G1:
136
- .g1
137
- case DeviceTypes.G2:
138
- .g2
139
- case DeviceTypes.LIVE:
140
- .mentraLive
141
- case DeviceTypes.NEX:
142
- .mentraNex
143
- case DeviceTypes.MACH1:
144
- .mach1
145
- case DeviceTypes.Z100:
146
- .z100
147
- case DeviceTypes.FRAME:
148
- .frame
149
- case DeviceTypes.SIMULATED:
150
- .simulated
151
- case ControllerTypes.R1:
152
- .r1
153
- default:
154
- .mentraLive
155
- }
156
- }
157
- }
158
-
159
- public struct Device: Identifiable, Equatable, CustomStringConvertible {
160
- public let model: DeviceModel
161
- public let name: String
162
- /// CoreBluetooth identifier when available.
163
- public let identifier: String?
164
- public let rssi: Int?
165
- /// Stable app-facing scan-result key. Do not parse; use typed fields instead.
166
- public let id: String
167
-
168
- public init(
169
- model: DeviceModel,
170
- name: String,
171
- identifier: String? = nil,
172
- rssi: Int? = nil,
173
- id: String? = nil
174
- ) {
175
- self.model = model
176
- self.name = name
177
- self.identifier = identifier
178
- self.rssi = rssi
179
- self.id = id ?? identifier.flatMap { $0.isEmpty ? nil : $0 } ?? "\(model.deviceType):\(name)"
180
- }
181
-
182
- public var description: String {
183
- "Device(model: \(model), name: \(name))"
184
- }
185
-
186
- var dictionary: [String: Any] {
187
- var values: [String: Any] = [
188
- "id": id,
189
- "model": model.deviceType,
190
- "name": name,
191
- ]
192
- if let identifier, !identifier.isEmpty {
193
- values["address"] = identifier
194
- }
195
- if let rssi {
196
- values["rssi"] = rssi
197
- }
198
- return values
199
- }
200
-
201
- init?(values: [String: Any]) {
202
- guard let model = stringValue(values, "model") else { return nil }
203
- guard let name = stringValue(values, "name") else { return nil }
204
- let identifier = stringValue(values, "address").flatMap { $0.isEmpty ? nil : $0 }
205
- let rssi = intValue(values["rssi"])
206
- self.init(
207
- model: DeviceModel.fromDeviceType(model),
208
- name: name,
209
- identifier: identifier,
210
- rssi: rssi,
211
- id: stringValue(values, "id")
212
- )
213
- }
214
- }
215
-
216
- public struct ConnectOptions {
217
- public let saveAsDefault: Bool
218
- public let cancelExistingConnectionAttempt: Bool
219
-
220
- public init(saveAsDefault: Bool = true, cancelExistingConnectionAttempt: Bool = true) {
221
- self.saveAsDefault = saveAsDefault
222
- self.cancelExistingConnectionAttempt = cancelExistingConnectionAttempt
223
- }
224
- }
225
-
226
- public struct WifiScanResult: CustomStringConvertible {
227
- public let ssid: String
228
- public let requiresPassword: Bool
229
- public let signalStrength: Int
230
- public let frequency: Int?
231
-
232
- public init(ssid: String, requiresPassword: Bool, signalStrength: Int, frequency: Int? = nil) {
233
- self.ssid = ssid
234
- self.requiresPassword = requiresPassword
235
- self.signalStrength = signalStrength
236
- self.frequency = frequency
237
- }
238
-
239
- init(values: [String: Any]) {
240
- ssid = stringValue(values, "ssid") ?? ""
241
- requiresPassword = boolValue(values, "requiresPassword") ?? false
242
- signalStrength = intValue(values["signalStrength"]) ?? -1
243
- frequency = intValue(values["frequency"])
244
- }
245
-
246
- var dictionary: [String: Any] {
247
- var values: [String: Any] = [
248
- "ssid": ssid,
249
- "requiresPassword": requiresPassword,
250
- "signalStrength": signalStrength,
251
- ]
252
- if let frequency {
253
- values["frequency"] = frequency
254
- }
255
- return values
256
- }
257
-
258
- public var description: String {
259
- "WifiScanResult(ssid: \(ssid), signalStrength: \(signalStrength))"
260
- }
261
- }
262
-
263
- public enum GlassesConnectionState: String, CustomStringConvertible, Equatable {
264
- case disconnected = "DISCONNECTED"
265
- case scanning = "SCANNING"
266
- case connecting = "CONNECTING"
267
- case bonding = "BONDING"
268
- case connected = "CONNECTED"
269
-
270
- public init(_ value: String?) {
271
- self = Self.fromValue(value) ?? .disconnected
272
- }
273
-
274
- public static func fromValue(_ value: String?) -> GlassesConnectionState? {
275
- guard let normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines).uppercased(),
276
- !normalized.isEmpty
277
- else {
278
- return nil
279
- }
280
- return Self(rawValue: normalized)
281
- }
282
-
283
- public var isConnected: Bool {
284
- self == .connected
285
- }
286
-
287
- public var isBusy: Bool {
288
- self == .scanning || self == .connecting || self == .bonding
289
- }
290
-
291
- func statusValues(connected: Bool, fullyBooted: Bool) -> [String: Any] {
292
- if self == .connected || connected || fullyBooted {
293
- return ["state": "connected", "fullyBooted": fullyBooted]
294
- }
295
- switch self {
296
- case .scanning:
297
- return ["state": "scanning"]
298
- case .connecting:
299
- return ["state": "connecting"]
300
- case .bonding:
301
- return ["state": "bonding"]
302
- case .connected:
303
- return ["state": "connected", "fullyBooted": fullyBooted]
304
- case .disconnected:
305
- return ["state": "disconnected"]
306
- }
307
- }
308
-
309
- public var description: String {
310
- rawValue
311
- }
312
- }
313
-
314
- public struct GlassesStatus: CustomStringConvertible {
315
- let values: [String: Any]
316
-
317
- init(values: [String: Any]) {
318
- self.values = values
319
- }
320
-
321
- public func applying(_ update: GlassesStatusUpdate) -> GlassesStatus {
322
- GlassesStatus(values: values.merging(update.values) { _, new in new })
323
- }
324
-
325
- public func withBattery(level: Int, charging: Bool) -> GlassesStatus {
326
- applying(GlassesStatusUpdate(values: ["batteryLevel": level, "charging": charging]))
327
- }
328
-
329
- public func withWifi(_ wifi: WifiStatus) -> GlassesStatus {
330
- applying(GlassesStatusUpdate(values: wifi.storeValues))
331
- }
332
-
333
- public func withHotspot(_ hotspot: HotspotStatus) -> GlassesStatus {
334
- applying(GlassesStatusUpdate(values: hotspot.storeValues))
335
- }
336
-
337
- public func disconnected() -> GlassesStatus {
338
- applying(GlassesStatusUpdate(values: [
339
- "connected": false,
340
- "connectionState": "DISCONNECTED",
341
- "fullyBooted": false,
342
- "batteryLevel": -1,
343
- "charging": false,
344
- "hotspotEnabled": false,
345
- "hotspotGatewayIp": "",
346
- "hotspotPassword": "",
347
- "hotspotSsid": "",
348
- "wifiConnected": false,
349
- "wifiSsid": "",
350
- "wifiLocalIp": "",
351
- "signalStrength": -1,
352
- "signalStrengthUpdatedAt": 0,
353
- ]))
354
- }
355
-
356
- public var fullyBooted: Bool { boolValue(values, "fullyBooted") ?? false }
357
- public var connected: Bool { boolValue(values, "connected") ?? false }
358
- public var micEnabled: Bool { boolValue(values, "micEnabled") ?? false }
359
- public var connectionState: GlassesConnectionState { GlassesConnectionState(stringValue(values, "connectionState")) }
360
- public var bluetoothClassicConnected: Bool { boolValue(values, "bluetoothClassicConnected") ?? false }
361
- public var signalStrength: Int { intValue(values["signalStrength"]) ?? -1 }
362
- public var signalStrengthUpdatedAt: Int { intValue(values["signalStrengthUpdatedAt"]) ?? 0 }
363
- public var deviceModel: String { stringValue(values, "deviceModel") ?? "" }
364
- public var androidVersion: String { stringValue(values, "androidVersion") ?? "" }
365
- public var firmwareVersion: String { stringValue(values, "firmwareVersion") ?? "" }
366
- public var besFirmwareVersion: String { stringValue(values, "besFirmwareVersion") ?? "" }
367
- public var mtkFirmwareVersion: String { stringValue(values, "mtkFirmwareVersion") ?? "" }
368
- public var bluetoothMacAddress: String { stringValue(values, "bluetoothMacAddress") ?? "" }
369
- public var leftMacAddress: String { stringValue(values, "leftMacAddress") ?? "" }
370
- public var rightMacAddress: String { stringValue(values, "rightMacAddress") ?? "" }
371
- public var macAddress: String { stringValue(values, "macAddress") ?? "" }
372
- public var buildNumber: String { stringValue(values, "buildNumber") ?? "" }
373
- public var otaVersionUrl: String { stringValue(values, "otaVersionUrl") ?? "" }
374
- public var appVersion: String { stringValue(values, "appVersion") ?? "" }
375
- public var bluetoothName: String { stringValue(values, "bluetoothName") ?? "" }
376
- public var serialNumber: String { stringValue(values, "serialNumber") ?? "" }
377
- public var style: String { stringValue(values, "style") ?? "" }
378
- public var color: String { stringValue(values, "color") ?? "" }
379
- public var wifi: WifiStatus { WifiStatus.fromStoreValues(values) ?? .disconnected }
380
- public var hotspot: HotspotStatus { HotspotStatus.fromStoreValues(values) ?? .disabled }
381
- public var dictionary: [String: Any] { Self.dictionary(from: values) }
382
- public var batteryLevel: Int { intValue(values["batteryLevel"]) ?? -1 }
383
- public var charging: Bool { boolValue(values, "charging") ?? false }
384
- public var caseBatteryLevel: Int { intValue(values["caseBatteryLevel"]) ?? -1 }
385
- public var caseCharging: Bool { boolValue(values, "caseCharging") ?? false }
386
- public var caseOpen: Bool { boolValue(values, "caseOpen") ?? true }
387
- public var caseRemoved: Bool { boolValue(values, "caseRemoved") ?? true }
388
- public var headUp: Bool { boolValue(values, "headUp") ?? false }
389
- public var controllerConnected: Bool { boolValue(values, "controllerConnected") ?? false }
390
- public var controllerFullyBooted: Bool { boolValue(values, "controllerFullyBooted") ?? false }
391
- public var controllerMacAddress: String { stringValue(values, "controllerMacAddress") ?? "" }
392
- public var controllerBatteryLevel: Int { intValue(values["controllerBatteryLevel"]) ?? -1 }
393
- public var controllerSignalStrength: Int { intValue(values["controllerSignalStrength"]) ?? -1 }
394
- public var ringSignalStrength: Int { intValue(values["ringSignalStrength"]) ?? -1 }
395
-
396
- public var description: String {
397
- values.description
398
- }
399
-
400
- static func dictionary(from values: [String: Any]) -> [String: Any] {
401
- var dictionary = values
402
- dictionary["connection"] = GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
403
- connected: boolValue(values, "connected") ?? false,
404
- fullyBooted: boolValue(values, "fullyBooted") ?? false
405
- )
406
- dictionary.removeValue(forKey: "connected")
407
- dictionary.removeValue(forKey: "fullyBooted")
408
- dictionary.removeValue(forKey: "connectionState")
409
- dictionary["wifi"] = (WifiStatus.fromStoreValues(values) ?? .disconnected).values
410
- dictionary["hotspot"] = (HotspotStatus.fromStoreValues(values) ?? .disabled).values
411
- dictionary.removeValue(forKey: "wifiConnected")
412
- dictionary.removeValue(forKey: "wifiSsid")
413
- dictionary.removeValue(forKey: "wifiLocalIp")
414
- dictionary.removeValue(forKey: "hotspotEnabled")
415
- dictionary.removeValue(forKey: "hotspotSsid")
416
- dictionary.removeValue(forKey: "hotspotPassword")
417
- dictionary.removeValue(forKey: "hotspotGatewayIp")
418
- return dictionary
419
- }
420
-
421
- static func updateDictionary(from values: [String: Any]) -> [String: Any] {
422
- var dictionary = values
423
- if hasAnyKey(values, "connection", "connected", "fullyBooted", "connectionState") {
424
- dictionary["connection"] = (values["connection"] as? [String: Any])
425
- ?? GlassesConnectionState(stringValue(values, "connectionState")).statusValues(
426
- connected: boolValue(values, "connected") ?? false,
427
- fullyBooted: boolValue(values, "fullyBooted") ?? false
428
- )
429
- dictionary.removeValue(forKey: "connected")
430
- dictionary.removeValue(forKey: "fullyBooted")
431
- dictionary.removeValue(forKey: "connectionState")
432
- }
433
- if hasAnyKey(values, "wifi", "wifiConnected", "wifiSsid", "wifiLocalIp") {
434
- let wifi = (values["wifi"] as? [String: Any]).flatMap(WifiStatus.init(values:))
435
- ?? WifiStatus.fromStoreValues(values)
436
- if let wifi {
437
- dictionary["wifi"] = wifi.values
438
- }
439
- dictionary.removeValue(forKey: "wifiConnected")
440
- dictionary.removeValue(forKey: "wifiSsid")
441
- dictionary.removeValue(forKey: "wifiLocalIp")
442
- }
443
- if hasAnyKey(values, "hotspot") {
444
- if let hotspot = (values["hotspot"] as? [String: Any]).flatMap(HotspotStatus.init(values:)) {
445
- dictionary["hotspot"] = hotspot.values
446
- }
447
- } else if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp") {
448
- if let hotspot = HotspotStatus.fromStoreValues(values) {
449
- dictionary["hotspot"] = hotspot.values
450
- }
451
- dictionary.removeValue(forKey: "hotspotEnabled")
452
- dictionary.removeValue(forKey: "hotspotSsid")
453
- dictionary.removeValue(forKey: "hotspotPassword")
454
- dictionary.removeValue(forKey: "hotspotGatewayIp")
455
- }
456
- return dictionary
457
- }
458
- }
459
-
460
- public struct BluetoothStatus: CustomStringConvertible {
461
- let values: [String: Any]
462
-
463
- init(values: [String: Any]) {
464
- self.values = Self.normalized(values)
465
- }
466
-
467
- private static func normalized(_ values: [String: Any]) -> [String: Any] {
468
- var normalizedValues = values
469
- if let searchResults = values["searchResults"] as? [[String: Any]] {
470
- normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
471
- }
472
- return normalizedValues
473
- }
474
-
475
- public func applying(_ update: BluetoothStatusUpdate) -> BluetoothStatus {
476
- BluetoothStatus(values: values.merging(update.values) { _, new in new })
477
- }
478
-
479
- public func withDefaultDevice(_ device: Device?) -> BluetoothStatus {
480
- applying(BluetoothStatusUpdate(values: [
481
- "default_wearable": device?.model.deviceType ?? "",
482
- "device_name": device?.name ?? "",
483
- "device_address": device?.identifier ?? "",
484
- ]))
485
- }
486
-
487
- public var searching: Bool { boolValue(values, "searching") ?? false }
488
- public var searchingController: Bool { boolValue(values, "searchingController") ?? false }
489
- public var systemMicUnavailable: Bool { boolValue(values, "systemMicUnavailable") ?? false }
490
- public var micEnabled: Bool { boolValue(values, "micEnabled") ?? false }
491
- public var currentMic: String { stringValue(values, "currentMic") ?? "" }
492
- public var micRanking: [String] { stringListValue(values, "micRanking") }
493
- /// Nearby glasses in stable discovery order. Existing entries keep their array position as
494
- /// details refresh; new glasses append at the end, and removals should not reorder remaining entries.
495
- public var searchResults: [Device] {
496
- dictionaryListValue(values, "searchResults").compactMap(Device.init(values:))
497
- }
498
- public var wifiScanResults: [WifiScanResult] {
499
- dictionaryListValue(values, "wifiScanResults").map(WifiScanResult.init(values:))
500
- }
501
- public var lastLog: [String] { stringListValue(values, "lastLog") }
502
- public var otherBtConnected: Bool { boolValue(values, "otherBtConnected") ?? false }
503
- public var defaultWearable: String { stringValue(values, "default_wearable") ?? "" }
504
- public var pendingWearable: String { stringValue(values, "pending_wearable") ?? "" }
505
- public var deviceName: String { stringValue(values, "device_name") ?? "" }
506
- public var deviceAddress: String { stringValue(values, "device_address") ?? "" }
507
- public var defaultController: String { stringValue(values, "default_controller") ?? "" }
508
- public var pendingController: String { stringValue(values, "pending_controller") ?? "" }
509
- public var controllerDeviceName: String { stringValue(values, "controller_device_name") ?? "" }
510
- public var screenDisabled: Bool { boolValue(values, "screen_disabled") ?? false }
511
- public var preferredMic: String { stringValue(values, "preferred_mic") ?? "auto" }
512
- public var sensingEnabled: Bool { boolValue(values, "sensing_enabled") ?? true }
513
- public var powerSavingMode: Bool { boolValue(values, "power_saving_mode") ?? false }
514
- public var brightness: Int { intValue(values["brightness"]) ?? 50 }
515
- public var autoBrightness: Bool { boolValue(values, "auto_brightness") ?? true }
516
- public var dashboardHeight: Int { intValue(values["dashboard_height"]) ?? 4 }
517
- public var dashboardDepth: Int { intValue(values["dashboard_depth"]) ?? 2 }
518
- public var headUpAngle: Int { intValue(values["head_up_angle"]) ?? 30 }
519
- public var contextualDashboard: Bool { boolValue(values, "contextual_dashboard") ?? true }
520
- public var galleryModeAuto: Bool { boolValue(values, "galleryModeAuto") ?? true }
521
- public var buttonPhotoSize: ButtonPhotoSize {
522
- ButtonPhotoSize(rawValue: stringValue(values, "button_photo_size") ?? "") ?? .medium
523
- }
524
- public var buttonCameraLed: Bool { boolValue(values, "button_camera_led") ?? true }
525
- public var buttonMaxRecordingTime: Int { intValue(values["button_max_recording_time"]) ?? 10 }
526
- public var buttonVideoWidth: Int { intValue(values["button_video_width"]) ?? 1280 }
527
- public var buttonVideoHeight: Int { intValue(values["button_video_height"]) ?? 720 }
528
- public var buttonVideoFrameRate: Int { intValue(values["button_video_fps"]) ?? 30 }
529
- public var shouldSendPcm: Bool { boolValue(values, "should_send_pcm") ?? false }
530
- public var shouldSendLc3: Bool { boolValue(values, "should_send_lc3") ?? false }
531
- public var shouldSendTranscript: Bool { boolValue(values, "should_send_transcript") ?? false }
532
- public var bypassVad: Bool { boolValue(values, "bypass_vad") ?? true }
533
- public var offlineCaptionsRunning: Bool { boolValue(values, "offline_captions_running") ?? false }
534
- public var localSttFallbackActive: Bool { boolValue(values, "local_stt_fallback_active") ?? false }
535
- public var shouldSendBootingMessage: Bool { boolValue(values, "shouldSendBootingMessage") ?? true }
536
-
537
- public var defaultDevice: Device? {
538
- guard !defaultWearable.isEmpty else { return nil }
539
- return Device(
540
- model: DeviceModel.fromDeviceType(defaultWearable),
541
- name: deviceName,
542
- identifier: deviceAddress.isEmpty ? nil : deviceAddress
543
- )
544
- }
545
-
546
- public var description: String {
547
- values.description
548
- }
549
- }
550
-
551
- public struct GlassesStatusUpdate: CustomStringConvertible {
552
- let values: [String: Any]
553
-
554
- init(values: [String: Any]) {
555
- self.values = values
556
- }
557
-
558
- public var fullyBooted: Bool? { optionalBoolValue(values, "fullyBooted") }
559
- public var connected: Bool? { optionalBoolValue(values, "connected") }
560
- public var micEnabled: Bool? { optionalBoolValue(values, "micEnabled") }
561
- public var connectionState: GlassesConnectionState? {
562
- GlassesConnectionState.fromValue(optionalStringValue(values, "connectionState"))
563
- }
564
- public var bluetoothClassicConnected: Bool? { optionalBoolValue(values, "bluetoothClassicConnected") }
565
- public var signalStrength: Int? { optionalIntValue(values, "signalStrength") }
566
- public var signalStrengthUpdatedAt: Int? { optionalIntValue(values, "signalStrengthUpdatedAt") }
567
- public var deviceModel: String? { optionalStringValue(values, "deviceModel") }
568
- public var androidVersion: String? { optionalStringValue(values, "androidVersion") }
569
- public var firmwareVersion: String? { optionalStringValue(values, "firmwareVersion") }
570
- public var besFirmwareVersion: String? { optionalStringValue(values, "besFirmwareVersion") }
571
- public var mtkFirmwareVersion: String? { optionalStringValue(values, "mtkFirmwareVersion") }
572
- public var bluetoothMacAddress: String? { optionalStringValue(values, "bluetoothMacAddress") }
573
- public var leftMacAddress: String? { optionalStringValue(values, "leftMacAddress") }
574
- public var rightMacAddress: String? { optionalStringValue(values, "rightMacAddress") }
575
- public var macAddress: String? { optionalStringValue(values, "macAddress") }
576
- public var buildNumber: String? { optionalStringValue(values, "buildNumber") }
577
- public var otaVersionUrl: String? { optionalStringValue(values, "otaVersionUrl") }
578
- public var appVersion: String? { optionalStringValue(values, "appVersion") }
579
- public var bluetoothName: String? { optionalStringValue(values, "bluetoothName") }
580
- public var serialNumber: String? { optionalStringValue(values, "serialNumber") }
581
- public var style: String? { optionalStringValue(values, "style") }
582
- public var color: String? { optionalStringValue(values, "color") }
583
- public var wifi: WifiStatus? {
584
- if let wifi = values["wifi"] as? [String: Any] {
585
- return WifiStatus(values: wifi)
586
- }
587
- if hasAnyKey(values, "wifiConnected", "wifiSsid", "wifiLocalIp") {
588
- return WifiStatus.fromStoreValues(values)
589
- }
590
- return nil
591
- }
592
- public var hotspot: HotspotStatus? {
593
- if let hotspot = values["hotspot"] as? [String: Any] {
594
- return HotspotStatus(values: hotspot)
595
- }
596
- if hasAnyKey(values, "hotspotEnabled", "hotspotSsid", "hotspotPassword", "hotspotGatewayIp") {
597
- return HotspotStatus.fromStoreValues(values)
598
- }
599
- return nil
600
- }
601
- public var dictionary: [String: Any] { GlassesStatus.updateDictionary(from: values) }
602
- public var batteryLevel: Int? { optionalIntValue(values, "batteryLevel") }
603
- public var charging: Bool? { optionalBoolValue(values, "charging") }
604
- public var caseBatteryLevel: Int? { optionalIntValue(values, "caseBatteryLevel") }
605
- public var caseCharging: Bool? { optionalBoolValue(values, "caseCharging") }
606
- public var caseOpen: Bool? { optionalBoolValue(values, "caseOpen") }
607
- public var caseRemoved: Bool? { optionalBoolValue(values, "caseRemoved") }
608
- public var headUp: Bool? { optionalBoolValue(values, "headUp") }
609
- public var controllerConnected: Bool? { optionalBoolValue(values, "controllerConnected") }
610
- public var controllerFullyBooted: Bool? { optionalBoolValue(values, "controllerFullyBooted") }
611
- public var controllerMacAddress: String? { optionalStringValue(values, "controllerMacAddress") }
612
- public var controllerBatteryLevel: Int? { optionalIntValue(values, "controllerBatteryLevel") }
613
- public var controllerSignalStrength: Int? { optionalIntValue(values, "controllerSignalStrength") }
614
- public var ringSignalStrength: Int? { optionalIntValue(values, "ringSignalStrength") }
615
-
616
- public var description: String {
617
- values.description
618
- }
619
- }
620
-
621
- public struct BluetoothStatusUpdate: CustomStringConvertible {
622
- let values: [String: Any]
623
-
624
- init(values: [String: Any]) {
625
- var normalizedValues = values
626
- if let searchResults = values["searchResults"] as? [[String: Any]] {
627
- normalizedValues["searchResults"] = searchResults.compactMap { Device(values: $0)?.dictionary }
628
- }
629
- self.values = normalizedValues
630
- }
631
-
632
- public var searching: Bool? { optionalBoolValue(values, "searching") }
633
- public var searchingController: Bool? { optionalBoolValue(values, "searchingController") }
634
- public var systemMicUnavailable: Bool? { optionalBoolValue(values, "systemMicUnavailable") }
635
- public var micEnabled: Bool? { optionalBoolValue(values, "micEnabled") }
636
- public var currentMic: String? { optionalStringValue(values, "currentMic") }
637
- public var micRanking: [String]? { optionalStringListValue(values, "micRanking") }
638
- /// Nearby glasses in stable discovery order when included in an update. Existing entries keep their
639
- /// array position as details refresh; new glasses append at the end, and removals should not reorder
640
- /// remaining entries.
641
- public var searchResults: [Device]? {
642
- optionalDictionaryListValue(values, "searchResults")?.compactMap(Device.init(values:))
643
- }
644
- public var wifiScanResults: [WifiScanResult]? {
645
- optionalDictionaryListValue(values, "wifiScanResults")?.map(WifiScanResult.init(values:))
646
- }
647
- public var lastLog: [String]? { optionalStringListValue(values, "lastLog") }
648
- public var otherBtConnected: Bool? { optionalBoolValue(values, "otherBtConnected") }
649
- public var defaultWearable: String? { optionalStringValue(values, "default_wearable") }
650
- public var pendingWearable: String? { optionalStringValue(values, "pending_wearable") }
651
- public var deviceName: String? { optionalStringValue(values, "device_name") }
652
- public var deviceAddress: String? { optionalStringValue(values, "device_address") }
653
- public var defaultController: String? { optionalStringValue(values, "default_controller") }
654
- public var pendingController: String? { optionalStringValue(values, "pending_controller") }
655
- public var controllerDeviceName: String? { optionalStringValue(values, "controller_device_name") }
656
- public var screenDisabled: Bool? { optionalBoolValue(values, "screen_disabled") }
657
- public var preferredMic: String? { optionalStringValue(values, "preferred_mic") }
658
- public var sensingEnabled: Bool? { optionalBoolValue(values, "sensing_enabled") }
659
- public var powerSavingMode: Bool? { optionalBoolValue(values, "power_saving_mode") }
660
- public var brightness: Int? { optionalIntValue(values, "brightness") }
661
- public var autoBrightness: Bool? { optionalBoolValue(values, "auto_brightness") }
662
- public var dashboardHeight: Int? { optionalIntValue(values, "dashboard_height") }
663
- public var dashboardDepth: Int? { optionalIntValue(values, "dashboard_depth") }
664
- public var headUpAngle: Int? { optionalIntValue(values, "head_up_angle") }
665
- public var contextualDashboard: Bool? { optionalBoolValue(values, "contextual_dashboard") }
666
- public var galleryModeAuto: Bool? { optionalBoolValue(values, "galleryModeAuto") }
667
- public var buttonPhotoSize: ButtonPhotoSize? {
668
- optionalStringValue(values, "button_photo_size").flatMap(ButtonPhotoSize.init(rawValue:))
669
- }
670
- public var buttonCameraLed: Bool? { optionalBoolValue(values, "button_camera_led") }
671
- public var buttonMaxRecordingTime: Int? { optionalIntValue(values, "button_max_recording_time") }
672
- public var buttonVideoWidth: Int? { optionalIntValue(values, "button_video_width") }
673
- public var buttonVideoHeight: Int? { optionalIntValue(values, "button_video_height") }
674
- public var buttonVideoFrameRate: Int? { optionalIntValue(values, "button_video_fps") }
675
- public var shouldSendPcm: Bool? { optionalBoolValue(values, "should_send_pcm") }
676
- public var shouldSendLc3: Bool? { optionalBoolValue(values, "should_send_lc3") }
677
- public var shouldSendTranscript: Bool? { optionalBoolValue(values, "should_send_transcript") }
678
- public var bypassVad: Bool? { optionalBoolValue(values, "bypass_vad") }
679
- public var offlineCaptionsRunning: Bool? { optionalBoolValue(values, "offline_captions_running") }
680
- public var localSttFallbackActive: Bool? { optionalBoolValue(values, "local_stt_fallback_active") }
681
- public var shouldSendBootingMessage: Bool? { optionalBoolValue(values, "shouldSendBootingMessage") }
682
-
683
- public var description: String {
684
- values.description
685
- }
686
- }
687
-
688
- public struct DisplayTextRequest {
689
- public let text: String
690
- public let x: Int
691
- public let y: Int
692
- public let size: Int
693
-
694
- public init(text: String, x: Int = 0, y: Int = 0, size: Int = 24) {
695
- self.text = text
696
- self.x = x
697
- self.y = y
698
- self.size = size
699
- }
700
-
701
- var dictionary: [String: Any] {
702
- [
703
- "text": text,
704
- "x": x,
705
- "y": y,
706
- "size": size,
707
- ]
708
- }
709
- }
710
-
711
- struct DisplayEventRequest {
712
- let values: [String: Any]
713
-
714
- init(values: [String: Any]) {
715
- self.values = values
716
- }
717
- }
718
-
719
- public struct DashboardPositionRequest {
720
- public let height: Int
721
- public let depth: Int
722
-
723
- public init(height: Int, depth: Int) {
724
- self.height = height
725
- self.depth = depth
726
- }
727
- }
728
-
729
- struct DashboardMenuItem {
730
- let title: String
731
- let packageName: String
732
- let values: [String: Any]
733
-
734
- init(title: String, packageName: String, values: [String: Any] = [:]) {
735
- self.title = title
736
- self.packageName = packageName
737
- self.values = values
738
- }
739
-
740
- var dictionary: [String: Any] {
741
- values.merging(["title": title, "packageName": packageName]) { _, new in new }
742
- }
743
- }
744
-
745
- public enum GalleryMode {
746
- case auto
747
- case manual
748
- }
749
-
750
- public enum PhotoSize: String {
751
- case small
752
- case medium
753
- case large
754
- case full
755
- }
756
-
757
- public enum ButtonPhotoSize: String {
758
- case small
759
- case medium
760
- case large
761
- }
762
-
763
- public enum PhotoCompression: String {
764
- case none
765
- case medium
766
- case heavy
767
- }
768
-
769
- public struct ButtonPhotoSettings {
770
- public let size: ButtonPhotoSize
771
-
772
- public init(size: ButtonPhotoSize) {
773
- self.size = size
774
- }
775
- }
776
-
777
- public struct ButtonVideoRecordingSettings {
778
- public let width: Int
779
- public let height: Int
780
- public let frameRate: Int
781
-
782
- public init(width: Int, height: Int, frameRate: Int) {
783
- self.width = width
784
- self.height = height
785
- self.frameRate = frameRate
786
- }
787
- }
788
-
789
- public enum CameraFov {
790
- case standard
791
- case wide
792
-
793
- var value: [String: Int] {
794
- switch self {
795
- case .standard:
796
- ["fov": 118, "roiPosition": 0]
797
- case .wide:
798
- ["fov": 118, "roiPosition": 0]
799
- }
800
- }
801
- }
802
-
803
- public enum MicPreference: String {
804
- case auto
805
- case phone
806
- case glasses
807
- case bluetooth
808
- }
809
-
810
- public struct PhotoRequest {
811
- public let requestId: String
812
- public let appId: String
813
- public let size: PhotoSize
814
- public let webhookUrl: String?
815
- public let authToken: String?
816
- public let compress: PhotoCompression?
817
- public let sound: Bool
818
-
819
- public init(
820
- requestId: String,
821
- appId: String,
822
- size: PhotoSize,
823
- webhookUrl: String? = nil,
824
- authToken: String? = nil,
825
- compress: PhotoCompression? = nil,
826
- sound: Bool
827
- ) {
828
- self.requestId = requestId
829
- self.appId = appId
830
- self.size = size
831
- self.webhookUrl = webhookUrl
832
- self.authToken = authToken
833
- self.compress = compress
834
- self.sound = sound
835
- }
836
- }
837
-
838
- public struct StreamVideoConfig {
839
- public let width: Int?
840
- public let height: Int?
841
- public let bitrate: Int?
842
- public let frameRate: Int?
843
-
844
- public init(
845
- width: Int? = nil,
846
- height: Int? = nil,
847
- bitrate: Int? = nil,
848
- frameRate: Int? = nil
849
- ) {
850
- self.width = width
851
- self.height = height
852
- self.bitrate = bitrate
853
- self.frameRate = frameRate
854
- }
855
-
856
- var dictionary: [String: Any] {
857
- var values: [String: Any] = [:]
858
- if let width { values["width"] = width }
859
- if let height { values["height"] = height }
860
- if let bitrate { values["bitrate"] = bitrate }
861
- if let frameRate { values["frameRate"] = frameRate }
862
- return values
863
- }
864
-
865
- init?(values: [String: Any]?) {
866
- guard let values else { return nil }
867
- self.init(
868
- width: intValue(values["width"]),
869
- height: intValue(values["height"]),
870
- bitrate: intValue(values["bitrate"]),
871
- frameRate: intValue(values["frameRate"])
872
- )
873
- }
874
- }
875
-
876
- public struct StreamAudioConfig {
877
- public let bitrate: Int?
878
- public let sampleRate: Int?
879
- public let echoCancellation: Bool?
880
- public let noiseSuppression: Bool?
881
-
882
- public init(
883
- bitrate: Int? = nil,
884
- sampleRate: Int? = nil,
885
- echoCancellation: Bool? = nil,
886
- noiseSuppression: Bool? = nil
887
- ) {
888
- self.bitrate = bitrate
889
- self.sampleRate = sampleRate
890
- self.echoCancellation = echoCancellation
891
- self.noiseSuppression = noiseSuppression
892
- }
893
-
894
- var dictionary: [String: Any] {
895
- var values: [String: Any] = [:]
896
- if let bitrate { values["bitrate"] = bitrate }
897
- if let sampleRate { values["sampleRate"] = sampleRate }
898
- if let echoCancellation { values["echoCancellation"] = echoCancellation }
899
- if let noiseSuppression { values["noiseSuppression"] = noiseSuppression }
900
- return values
901
- }
902
-
903
- init?(values: [String: Any]?) {
904
- guard let values else { return nil }
905
- self.init(
906
- bitrate: intValue(values["bitrate"]),
907
- sampleRate: intValue(values["sampleRate"]),
908
- echoCancellation: values["echoCancellation"] as? Bool,
909
- noiseSuppression: values["noiseSuppression"] as? Bool
910
- )
911
- }
912
- }
913
-
914
- public struct StreamRequest {
915
- public let streamUrl: String
916
- public let streamId: String
917
- public let keepAlive: Bool
918
- public let keepAliveIntervalSeconds: Int
919
- public let sound: Bool
920
- public let video: StreamVideoConfig?
921
- public let audio: StreamAudioConfig?
922
- public let extraValues: [String: Any]
923
-
924
- public init(
925
- streamUrl: String,
926
- streamId: String = "",
927
- keepAlive: Bool = true,
928
- keepAliveIntervalSeconds: Int = 15,
929
- sound: Bool = true,
930
- video: StreamVideoConfig? = nil,
931
- audio: StreamAudioConfig? = nil,
932
- extraValues: [String: Any] = [:]
933
- ) {
934
- self.streamUrl = streamUrl
935
- self.streamId = streamId
936
- self.keepAlive = keepAlive
937
- self.keepAliveIntervalSeconds = keepAliveIntervalSeconds
938
- self.sound = sound
939
- self.video = video
940
- self.audio = audio
941
- self.extraValues = extraValues
942
- }
943
-
944
- init(values: [String: Any]) {
945
- self.init(
946
- streamUrl: values["streamUrl"] as? String
947
- ?? values["rtmpUrl"] as? String
948
- ?? values["srtUrl"] as? String
949
- ?? values["whipUrl"] as? String
950
- ?? "",
951
- streamId: values["streamId"] as? String ?? "",
952
- keepAlive: values["keepAlive"] as? Bool ?? true,
953
- keepAliveIntervalSeconds: intValue(values["keepAliveIntervalSeconds"]) ?? 15,
954
- sound: values["sound"] as? Bool ?? true,
955
- video: StreamVideoConfig(values: values["video"] as? [String: Any]),
956
- audio: StreamAudioConfig(values: values["audio"] as? [String: Any]),
957
- extraValues: values
958
- )
959
- }
960
-
961
- public var values: [String: Any] {
962
- var values = extraValues
963
- values["type"] = "start_stream"
964
- values["streamUrl"] = streamUrl
965
- values["streamId"] = streamId
966
- values["keepAlive"] = keepAlive
967
- values["keepAliveIntervalSeconds"] = keepAliveIntervalSeconds
968
- // The camera light is a privacy indicator and cannot be disabled by SDK callers.
969
- values["flash"] = true
970
- values["sound"] = sound
971
- if let videoValues = video?.dictionary, !videoValues.isEmpty {
972
- values["video"] = videoValues
973
- }
974
- if let audioValues = audio?.dictionary, !audioValues.isEmpty {
975
- values["audio"] = audioValues
976
- }
977
- return values
978
- }
979
- }
980
-
981
- public struct StreamKeepAliveRequest {
982
- public let streamId: String
983
- public let ackId: String
984
- public let extraValues: [String: Any]
985
-
986
- public init(streamId: String, ackId: String, extraValues: [String: Any] = [:]) {
987
- self.streamId = streamId
988
- self.ackId = ackId
989
- self.extraValues = extraValues
990
- }
991
-
992
- init(values: [String: Any]) {
993
- self.init(
994
- streamId: values["streamId"] as? String ?? "",
995
- ackId: values["ackId"] as? String ?? "",
996
- extraValues: values
997
- )
998
- }
999
-
1000
- public var values: [String: Any] {
1001
- var values = extraValues
1002
- values["type"] = "keep_stream_alive"
1003
- values["streamId"] = streamId
1004
- values["ackId"] = ackId
1005
- return values
1006
- }
1007
- }
1008
-
1009
- public enum RgbLedAction: String {
1010
- case on
1011
- case off
1012
- }
1013
-
1014
- public enum RgbLedColor: String {
1015
- case red
1016
- case green
1017
- case blue
1018
- case orange
1019
- case white
1020
- }
1021
-
1022
- public struct RgbLedRequest {
1023
- public let requestId: String
1024
- public let packageName: String?
1025
- public let action: RgbLedAction
1026
- public let color: RgbLedColor?
1027
- public let onDurationMs: Int
1028
- public let offDurationMs: Int
1029
- public let count: Int
1030
-
1031
- public init(
1032
- requestId: String,
1033
- packageName: String?,
1034
- action: RgbLedAction,
1035
- color: RgbLedColor?,
1036
- onDurationMs: Int,
1037
- offDurationMs: Int,
1038
- count: Int
1039
- ) {
1040
- self.requestId = requestId
1041
- self.packageName = packageName
1042
- self.action = action
1043
- self.color = color
1044
- self.onDurationMs = onDurationMs
1045
- self.offDurationMs = offDurationMs
1046
- self.count = count
1047
- }
1048
- }
1049
-
1050
- public struct VideoRecordingRequest {
1051
- public let requestId: String
1052
- public let save: Bool
1053
- public let sound: Bool
1054
-
1055
- public init(requestId: String, save: Bool, sound: Bool) {
1056
- self.requestId = requestId
1057
- self.save = save
1058
- self.sound = sound
1059
- }
1060
- }
1061
-
1062
- public struct ButtonPressEvent: CustomStringConvertible {
1063
- public let buttonId: String
1064
- public let pressType: String
1065
- public let timestamp: Int?
1066
-
1067
- public init(buttonId: String, pressType: String, timestamp: Int? = nil) {
1068
- self.buttonId = buttonId
1069
- self.pressType = pressType
1070
- self.timestamp = timestamp
1071
- }
1072
-
1073
- public var description: String {
1074
- "ButtonPressEvent(buttonId: \(buttonId), pressType: \(pressType))"
1075
- }
1076
- }
1077
-
1078
- public struct TouchEvent: CustomStringConvertible {
1079
- public let values: [String: Any]
1080
-
1081
- public init(values: [String: Any]) {
1082
- self.values = values
1083
- }
1084
-
1085
- public var deviceModel: String? {
1086
- stringValue(values, "deviceModel")
1087
- }
1088
-
1089
- public var gestureName: String? {
1090
- stringValue(values, "gestureName")
1091
- }
1092
-
1093
- public var timestamp: Int? {
1094
- intValue(values["timestamp"])
1095
- }
1096
-
1097
- public var isSwipe: Bool {
1098
- gestureName?.localizedCaseInsensitiveContains("swipe") == true
1099
- }
1100
-
1101
- public var description: String {
1102
- "TouchEvent(gestureName: \(gestureName ?? "unknown"))"
1103
- }
1104
- }
1105
-
1106
- public enum WifiStatus: CustomStringConvertible, Equatable {
1107
- public enum State: String {
1108
- case disconnected
1109
- case connected
1110
- }
1111
-
1112
- case disconnected
1113
- case connected(ssid: String, localIp: String?)
1114
-
1115
- private init?(connected: Bool, ssid: String?, localIp: String?) {
1116
- if connected {
1117
- guard
1118
- let ssid = ssid?.trimmingCharacters(in: .whitespacesAndNewlines),
1119
- !ssid.isEmpty
1120
- else {
1121
- return nil
1122
- }
1123
- let trimmedLocalIp = localIp?.trimmingCharacters(in: .whitespacesAndNewlines)
1124
- self = .connected(
1125
- ssid: ssid,
1126
- localIp: trimmedLocalIp?.isEmpty == false ? trimmedLocalIp : nil
1127
- )
1128
- } else {
1129
- self = .disconnected
1130
- }
1131
- }
1132
-
1133
- init?(values: [String: Any]) {
1134
- if let nested = values["wifi"] as? [String: Any] {
1135
- guard let wifi = WifiStatus(values: nested) else {
1136
- return nil
1137
- }
1138
- self = wifi
1139
- return
1140
- }
1141
-
1142
- if let state = stringValue(values, "state")?.lowercased() {
1143
- switch state {
1144
- case State.connected.rawValue:
1145
- guard let wifi = WifiStatus(
1146
- connected: true,
1147
- ssid: nonEmptyStringValue(values, "ssid"),
1148
- localIp: nonEmptyStringValue(values, "localIp")
1149
- ) else {
1150
- return nil
1151
- }
1152
- self = wifi
1153
- case State.disconnected.rawValue:
1154
- self = .disconnected
1155
- default:
1156
- return nil
1157
- }
1158
- return
1159
- }
1160
-
1161
- return nil
1162
- }
1163
-
1164
- static func fromStoreValues(_ values: [String: Any]) -> WifiStatus? {
1165
- guard let connected = boolValue(values, "wifiConnected") else { return nil }
1166
- return fromStoreFields(
1167
- connected: connected,
1168
- ssid: nonEmptyStringValue(values, "wifiSsid"),
1169
- localIp: nonEmptyStringValue(values, "wifiLocalIp")
1170
- )
1171
- }
1172
-
1173
- static func fromStoreFields(connected: Bool, ssid: String?, localIp: String?) -> WifiStatus? {
1174
- WifiStatus(connected: connected, ssid: ssid, localIp: localIp)
1175
- }
1176
-
1177
- public var state: State {
1178
- switch self {
1179
- case .disconnected:
1180
- .disconnected
1181
- case .connected:
1182
- .connected
1183
- }
1184
- }
1185
-
1186
- public var isConnected: Bool {
1187
- if case .connected = self {
1188
- return true
1189
- }
1190
- return false
1191
- }
1192
-
1193
- public var values: [String: Any] {
1194
- switch self {
1195
- case .disconnected:
1196
- return ["state": State.disconnected.rawValue]
1197
- case let .connected(ssid, localIp):
1198
- var values: [String: Any] = [
1199
- "state": State.connected.rawValue,
1200
- "ssid": ssid,
1201
- ]
1202
- if let localIp = localIp {
1203
- values["localIp"] = localIp
1204
- }
1205
- return values
1206
- }
1207
- }
1208
-
1209
- var storeValues: [String: Any] {
1210
- switch self {
1211
- case .disconnected:
1212
- [
1213
- "wifiConnected": false,
1214
- "wifiSsid": "",
1215
- "wifiLocalIp": "",
1216
- ]
1217
- case let .connected(ssid, localIp):
1218
- [
1219
- "wifiConnected": true,
1220
- "wifiSsid": ssid,
1221
- "wifiLocalIp": localIp ?? "",
1222
- ]
1223
- }
1224
- }
1225
-
1226
- public var description: String {
1227
- switch self {
1228
- case .disconnected:
1229
- "WifiStatus(disconnected)"
1230
- case let .connected(ssid, localIp):
1231
- "WifiStatus(connected: \(ssid), localIp: \(localIp ?? "unknown"))"
1232
- }
1233
- }
1234
- }
1235
-
1236
- public struct WifiStatusEvent: CustomStringConvertible {
1237
- public let status: WifiStatus
1238
-
1239
- public init(status: WifiStatus) {
1240
- self.status = status
1241
- }
1242
-
1243
- init(connected: Bool, ssid: String?, localIp: String?) {
1244
- self.status = WifiStatus.fromStoreFields(connected: connected, ssid: ssid, localIp: localIp) ?? .disconnected
1245
- }
1246
-
1247
- init(values: [String: Any]) {
1248
- self.status = WifiStatus(values: values) ?? .disconnected
1249
- }
1250
-
1251
- public var values: [String: Any] {
1252
- status.values.merging(["type": "wifi_status_change"]) { _, new in new }
1253
- }
1254
-
1255
- public var description: String {
1256
- "WifiStatusEvent(\(status))"
1257
- }
1258
- }
1259
-
1260
- public enum HotspotStatus: CustomStringConvertible, Equatable {
1261
- public enum State: String {
1262
- case disabled
1263
- case enabled
1264
- }
1265
-
1266
- case disabled
1267
- case enabled(ssid: String, password: String, localIp: String)
1268
-
1269
- public init?(enabled: Bool, ssid: String?, password: String?, localIp: String?) {
1270
- if enabled {
1271
- guard
1272
- let ssid = ssid?.trimmingCharacters(in: .whitespacesAndNewlines),
1273
- !ssid.isEmpty,
1274
- let password = password?.trimmingCharacters(in: .whitespacesAndNewlines),
1275
- !password.isEmpty,
1276
- let localIp = localIp?.trimmingCharacters(in: .whitespacesAndNewlines),
1277
- !localIp.isEmpty
1278
- else {
1279
- return nil
1280
- }
1281
- self = .enabled(ssid: ssid, password: password, localIp: localIp)
1282
- } else {
1283
- self = .disabled
1284
- }
1285
- }
1286
-
1287
- public init?(values: [String: Any]) {
1288
- if let nested = values["hotspot"] as? [String: Any] {
1289
- self.init(values: nested)
1290
- return
1291
- }
1292
-
1293
- guard let state = stringValue(values, "state")?.lowercased() else {
1294
- return nil
1295
- }
1296
-
1297
- switch state {
1298
- case State.enabled.rawValue:
1299
- self.init(
1300
- enabled: true,
1301
- ssid: nonEmptyStringValue(values, "ssid"),
1302
- password: nonEmptyStringValue(values, "password"),
1303
- localIp: nonEmptyStringValue(values, "localIp")
1304
- )
1305
- case State.disabled.rawValue:
1306
- self = .disabled
1307
- default:
1308
- return nil
1309
- }
1310
- }
1311
-
1312
- static func fromStoreValues(_ values: [String: Any]) -> HotspotStatus? {
1313
- guard let enabled = boolValue(values, "hotspotEnabled") else {
1314
- return nil
1315
- }
1316
- return fromStoreFields(
1317
- enabled: enabled,
1318
- ssid: nonEmptyStringValue(values, "hotspotSsid"),
1319
- password: nonEmptyStringValue(values, "hotspotPassword"),
1320
- localIp: nonEmptyStringValue(values, "hotspotGatewayIp")
1321
- )
1322
- }
1323
-
1324
- static func fromStoreFields(enabled: Bool, ssid: String?, password: String?, localIp: String?) -> HotspotStatus? {
1325
- HotspotStatus(enabled: enabled, ssid: ssid, password: password, localIp: localIp)
1326
- }
1327
-
1328
- var storeValues: [String: Any] {
1329
- switch self {
1330
- case .disabled:
1331
- [
1332
- "hotspotEnabled": false,
1333
- "hotspotSsid": "",
1334
- "hotspotPassword": "",
1335
- "hotspotGatewayIp": "",
1336
- ]
1337
- case let .enabled(ssid, password, localIp):
1338
- [
1339
- "hotspotEnabled": true,
1340
- "hotspotSsid": ssid,
1341
- "hotspotPassword": password,
1342
- "hotspotGatewayIp": localIp,
1343
- ]
1344
- }
1345
- }
1346
-
1347
- public var values: [String: Any] {
1348
- switch self {
1349
- case .disabled:
1350
- ["state": State.disabled.rawValue]
1351
- case let .enabled(ssid, password, localIp):
1352
- [
1353
- "state": State.enabled.rawValue,
1354
- "ssid": ssid,
1355
- "password": password,
1356
- "localIp": localIp,
1357
- ]
1358
- }
1359
- }
1360
-
1361
- public var state: State {
1362
- switch self {
1363
- case .disabled:
1364
- .disabled
1365
- case .enabled:
1366
- .enabled
1367
- }
1368
- }
1369
-
1370
- public var isEnabled: Bool {
1371
- if case .enabled = self {
1372
- return true
1373
- }
1374
- return false
1375
- }
1376
-
1377
- public var description: String {
1378
- switch self {
1379
- case .disabled:
1380
- "HotspotStatus(disabled)"
1381
- case let .enabled(ssid, _, localIp):
1382
- "HotspotStatus(enabled: \(ssid), localIp: \(localIp))"
1383
- }
1384
- }
1385
- }
1386
-
1387
- public struct HotspotStatusEvent: CustomStringConvertible {
1388
- public let status: HotspotStatus
1389
-
1390
- public init(status: HotspotStatus) {
1391
- self.status = status
1392
- }
1393
-
1394
- init(enabled: Bool, ssid: String?, password: String?, localIp: String?) {
1395
- self.status = HotspotStatus.fromStoreFields(enabled: enabled, ssid: ssid, password: password, localIp: localIp) ?? .disabled
1396
- }
1397
-
1398
- init(values: [String: Any]) {
1399
- self.status = HotspotStatus(values: values) ?? .disabled
1400
- }
1401
-
1402
- public var values: [String: Any] {
1403
- status.values.merging(["type": "hotspot_status_change"]) { _, new in new }
1404
- }
1405
-
1406
- public var description: String {
1407
- "HotspotStatusEvent(\(status))"
1408
- }
1409
- }
1410
-
1411
- public struct HotspotErrorEvent: CustomStringConvertible {
1412
- public let values: [String: Any]
1413
-
1414
- public init(values: [String: Any]) {
1415
- self.values = values
1416
- }
1417
-
1418
- public var message: String? {
1419
- stringValue(values, "errorMessage")
1420
- }
1421
-
1422
- public var timestamp: Int? {
1423
- intValue(values["timestamp"])
1424
- }
1425
-
1426
- public var description: String {
1427
- "HotspotErrorEvent(message: \(message ?? "unknown"))"
1428
- }
1429
- }
1430
-
1431
- public enum PhotoResponse: CustomStringConvertible, Equatable {
1432
- public enum State: String {
1433
- case success
1434
- case error
1435
- }
1436
-
1437
- case success(requestId: String, uploadUrl: String, timestamp: Int)
1438
- case error(requestId: String, errorCode: String?, errorMessage: String, timestamp: Int)
1439
-
1440
- public init(values: [String: Any]) {
1441
- let requestId = stringValue(values, "requestId") ?? ""
1442
- let timestamp = intValue(values["timestamp"]) ?? Int(Date().timeIntervalSince1970 * 1000)
1443
- let state = stringValue(values, "state")?.lowercased()
1444
- if state == State.success.rawValue {
1445
- self = .success(
1446
- requestId: requestId,
1447
- uploadUrl: stringValue(values, "uploadUrl") ?? "",
1448
- timestamp: timestamp
1449
- )
1450
- } else {
1451
- self = .error(
1452
- requestId: requestId,
1453
- errorCode: stringValue(values, "errorCode"),
1454
- errorMessage: stringValue(values, "errorMessage") ?? "Unknown photo error",
1455
- timestamp: timestamp
1456
- )
1457
- }
1458
- }
1459
-
1460
- public var state: State {
1461
- switch self {
1462
- case .success:
1463
- .success
1464
- case .error:
1465
- .error
1466
- }
1467
- }
1468
-
1469
- public var requestId: String {
1470
- switch self {
1471
- case let .success(requestId, _, _), let .error(requestId, _, _, _):
1472
- requestId
1473
- }
1474
- }
1475
-
1476
- public var timestamp: Int {
1477
- switch self {
1478
- case let .success(_, _, timestamp), let .error(_, _, _, timestamp):
1479
- timestamp
1480
- }
1481
- }
1482
-
1483
- public var values: [String: Any] {
1484
- switch self {
1485
- case let .success(requestId, uploadUrl, timestamp):
1486
- return [
1487
- "state": State.success.rawValue,
1488
- "requestId": requestId,
1489
- "uploadUrl": uploadUrl,
1490
- "timestamp": timestamp,
1491
- ]
1492
- case let .error(requestId, errorCode, errorMessage, timestamp):
1493
- var values: [String: Any] = [
1494
- "state": State.error.rawValue,
1495
- "requestId": requestId,
1496
- "errorMessage": errorMessage,
1497
- "timestamp": timestamp,
1498
- ]
1499
- if let errorCode, !errorCode.isEmpty {
1500
- values["errorCode"] = errorCode
1501
- }
1502
- return values
1503
- }
1504
- }
1505
-
1506
- public var description: String {
1507
- "PhotoResponse(requestId: \(requestId), state: \(state.rawValue))"
1508
- }
1509
- }
1510
-
1511
- public struct PhotoResponseEvent: CustomStringConvertible {
1512
- public let response: PhotoResponse
1513
-
1514
- public init(response: PhotoResponse) {
1515
- self.response = response
1516
- }
1517
-
1518
- public init(values: [String: Any]) {
1519
- self.response = PhotoResponse(values: values)
1520
- }
1521
-
1522
- public var requestId: String {
1523
- response.requestId
1524
- }
1525
-
1526
- public var values: [String: Any] {
1527
- var values = response.values
1528
- values["type"] = "photo_response"
1529
- return values
1530
- }
1531
-
1532
- public var description: String {
1533
- "PhotoResponseEvent(requestId: \(requestId), state: \(response.state.rawValue))"
1534
- }
1535
- }
1536
-
1537
- public enum StreamState: String, Equatable {
1538
- case initializing
1539
- case streaming
1540
- case stopping
1541
- case stopped
1542
- case reconnecting
1543
- case reconnected
1544
- case reconnectFailed = "reconnect_failed"
1545
- case error
1546
-
1547
- fileprivate static func from(_ value: String?) -> StreamState? {
1548
- switch value?.lowercased() {
1549
- case "initializing", "starting", "connecting":
1550
- return .initializing
1551
- case "streaming", "streaming_started", "active":
1552
- return .streaming
1553
- case "stopping":
1554
- return .stopping
1555
- case "stopped", "not_streaming", "disconnected", "timeout":
1556
- return .stopped
1557
- case "reconnecting":
1558
- return .reconnecting
1559
- case "reconnected":
1560
- return .reconnected
1561
- case "reconnect_failed":
1562
- return .reconnectFailed
1563
- case "error", "error_not_streaming":
1564
- return .error
1565
- default:
1566
- return nil
1567
- }
1568
- }
1569
- }
1570
-
1571
- public enum StreamStatusKind: String, Equatable {
1572
- case lifecycle
1573
- case reconnect
1574
- case error
1575
- case snapshot
1576
- }
1577
-
1578
- public enum StreamStatus: CustomStringConvertible, Equatable {
1579
- case lifecycle(state: StreamState, streamId: String?, timestamp: Int?)
1580
- case reconnecting(streamId: String?, attempt: Int, maxAttempts: Int, reason: String, timestamp: Int?)
1581
- case reconnected(streamId: String?, attempt: Int, timestamp: Int?)
1582
- case reconnectFailed(streamId: String?, maxAttempts: Int, timestamp: Int?)
1583
- case error(streamId: String?, errorDetails: String, timestamp: Int?)
1584
- case snapshot(state: StreamState, streaming: Bool, reconnecting: Bool, streamId: String?, attempt: Int?, timestamp: Int?)
1585
-
1586
- public init(values: [String: Any]) {
1587
- let rawState = stringValue(values, "status")
1588
- let streamId = stringValue(values, "streamId")
1589
- let timestamp = intValue(values["timestamp"])
1590
- let attempt = optionalIntValue(values, "attempt")
1591
- let maxAttempts = optionalIntValue(values, "maxAttempts") ?? 0
1592
-
1593
- if hasAnyKey(values, "streaming") || hasAnyKey(values, "reconnecting") {
1594
- let streaming = boolValue(values, "streaming") == true
1595
- let reconnecting = boolValue(values, "reconnecting") == true
1596
- let snapshotState: StreamState = reconnecting ? .reconnecting : (streaming ? .streaming : .stopped)
1597
- self = .snapshot(
1598
- state: snapshotState,
1599
- streaming: streaming,
1600
- reconnecting: reconnecting,
1601
- streamId: streamId,
1602
- attempt: attempt,
1603
- timestamp: timestamp
1604
- )
1605
- return
1606
- }
1607
-
1608
- guard let state = StreamState.from(rawState) else {
1609
- self = .error(
1610
- streamId: streamId,
1611
- errorDetails: rawState.map { "Unknown stream status: \($0)" } ?? "Missing stream status",
1612
- timestamp: timestamp
1613
- )
1614
- return
1615
- }
1616
-
1617
- switch state {
1618
- case .reconnecting:
1619
- self = .reconnecting(
1620
- streamId: streamId,
1621
- attempt: attempt ?? 0,
1622
- maxAttempts: maxAttempts,
1623
- reason: stringValue(values, "reason") ?? "",
1624
- timestamp: timestamp
1625
- )
1626
- case .reconnected:
1627
- self = .reconnected(streamId: streamId, attempt: attempt ?? 0, timestamp: timestamp)
1628
- case .reconnectFailed:
1629
- self = .reconnectFailed(streamId: streamId, maxAttempts: maxAttempts, timestamp: timestamp)
1630
- case .error:
1631
- self = .error(
1632
- streamId: streamId,
1633
- errorDetails: stringValue(values, "errorDetails")
1634
- ?? (rawState == "error_not_streaming" ? "not_streaming" : "Unknown stream error"),
1635
- timestamp: timestamp
1636
- )
1637
- default:
1638
- self = .lifecycle(state: state, streamId: streamId, timestamp: timestamp)
1639
- }
1640
- }
1641
-
1642
- public var kind: StreamStatusKind {
1643
- switch self {
1644
- case .lifecycle:
1645
- .lifecycle
1646
- case .reconnecting, .reconnected, .reconnectFailed:
1647
- .reconnect
1648
- case .error:
1649
- .error
1650
- case .snapshot:
1651
- .snapshot
1652
- }
1653
- }
1654
-
1655
- public var state: StreamState {
1656
- switch self {
1657
- case let .lifecycle(state, _, _):
1658
- state
1659
- case .reconnecting:
1660
- .reconnecting
1661
- case .reconnected:
1662
- .reconnected
1663
- case .reconnectFailed:
1664
- .reconnectFailed
1665
- case .error:
1666
- .error
1667
- case let .snapshot(state, _, _, _, _, _):
1668
- state
1669
- }
1670
- }
1671
-
1672
- public var streamId: String? {
1673
- switch self {
1674
- case let .lifecycle(_, streamId, _),
1675
- let .reconnecting(streamId, _, _, _, _),
1676
- let .reconnected(streamId, _, _),
1677
- let .reconnectFailed(streamId, _, _),
1678
- let .error(streamId, _, _),
1679
- let .snapshot(_, _, _, streamId, _, _):
1680
- streamId
1681
- }
1682
- }
1683
-
1684
- public var timestamp: Int? {
1685
- switch self {
1686
- case let .lifecycle(_, _, timestamp),
1687
- let .reconnecting(_, _, _, _, timestamp),
1688
- let .reconnected(_, _, timestamp),
1689
- let .reconnectFailed(_, _, timestamp),
1690
- let .error(_, _, timestamp),
1691
- let .snapshot(_, _, _, _, _, timestamp):
1692
- timestamp
1693
- }
1694
- }
1695
-
1696
- public var values: [String: Any] {
1697
- var values: [String: Any] = [
1698
- "kind": kind.rawValue,
1699
- "status": state.rawValue,
1700
- ]
1701
- if let streamId, !streamId.isEmpty {
1702
- values["streamId"] = streamId
1703
- }
1704
- if let timestamp {
1705
- values["timestamp"] = timestamp
1706
- }
1707
-
1708
- switch self {
1709
- case .lifecycle:
1710
- break
1711
- case let .reconnecting(_, attempt, maxAttempts, reason, _):
1712
- values["attempt"] = attempt
1713
- values["maxAttempts"] = maxAttempts
1714
- values["reason"] = reason
1715
- case let .reconnected(_, attempt, _):
1716
- values["attempt"] = attempt
1717
- case let .reconnectFailed(_, maxAttempts, _):
1718
- values["maxAttempts"] = maxAttempts
1719
- case let .error(_, errorDetails, _):
1720
- values["errorDetails"] = errorDetails
1721
- case let .snapshot(_, streaming, reconnecting, _, attempt, _):
1722
- values["streaming"] = streaming
1723
- values["reconnecting"] = reconnecting
1724
- if let attempt {
1725
- values["attempt"] = attempt
1726
- }
1727
- }
1728
-
1729
- return values
1730
- }
1731
-
1732
- public var description: String {
1733
- "StreamStatus(kind: \(kind.rawValue), status: \(state.rawValue), streamId: \(streamId ?? "none"))"
1734
- }
1735
- }
1736
-
1737
- public struct StreamStatusEvent: CustomStringConvertible {
1738
- public let status: StreamStatus
1739
-
1740
- public init(status: StreamStatus) {
1741
- self.status = status
1742
- }
1743
-
1744
- public init(values: [String: Any]) {
1745
- self.status = StreamStatus(values: values)
1746
- }
1747
-
1748
- public var state: StreamState {
1749
- status.state
1750
- }
1751
-
1752
- public var streamId: String? {
1753
- status.streamId
1754
- }
1755
-
1756
- public var values: [String: Any] {
1757
- var values = status.values
1758
- values["type"] = "stream_status"
1759
- return values
1760
- }
1761
-
1762
- public var description: String {
1763
- "StreamStatusEvent(kind: \(status.kind.rawValue), status: \(state.rawValue), streamId: \(streamId ?? "none"))"
1764
- }
1765
- }
1766
-
1767
- public struct KeepAliveAckEvent: CustomStringConvertible, Equatable {
1768
- public let streamId: String
1769
- public let ackId: String
1770
- public let timestamp: Int?
1771
-
1772
- public init(streamId: String, ackId: String, timestamp: Int? = nil) {
1773
- self.streamId = streamId
1774
- self.ackId = ackId
1775
- self.timestamp = timestamp
1776
- }
1777
-
1778
- public init(values: [String: Any]) {
1779
- self.streamId = stringValue(values, "streamId") ?? ""
1780
- self.ackId = stringValue(values, "ackId") ?? ""
1781
- self.timestamp = intValue(values["timestamp"])
1782
- }
1783
-
1784
- public var values: [String: Any] {
1785
- var values: [String: Any] = [
1786
- "type": "keep_alive_ack",
1787
- "streamId": streamId,
1788
- "ackId": ackId,
1789
- ]
1790
- if let timestamp {
1791
- values["timestamp"] = timestamp
1792
- }
1793
- return values
1794
- }
1795
-
1796
- public var description: String {
1797
- "KeepAliveAckEvent(streamId: \(streamId), ackId: \(ackId))"
1798
- }
1799
- }
1800
-
1801
- public struct BluetoothError: Error, LocalizedError, CustomStringConvertible {
1802
- public let code: String
1803
- public let message: String
1804
-
1805
- public init(code: String, message: String) {
1806
- self.code = code
1807
- self.message = message
1808
- }
1809
-
1810
- public var errorDescription: String? {
1811
- message
1812
- }
1813
-
1814
- public var description: String {
1815
- "\(code): \(message)"
1816
- }
1817
- }
1818
-
1819
- private final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
1820
- static let shared = BluetoothAvailability()
1821
-
1822
- private var centralManager: CBCentralManager?
1823
- private var state: CBManagerState = .unknown
1824
-
1825
- override private init() {
1826
- super.init()
1827
- centralManager = CBCentralManager(
1828
- delegate: self,
1829
- queue: .main,
1830
- options: [CBCentralManagerOptionShowPowerAlertKey: false]
1831
- )
1832
- state = centralManager?.state ?? .unknown
1833
- }
1834
-
1835
- func centralManagerDidUpdateState(_ central: CBCentralManager) {
1836
- state = central.state
1837
- }
1838
-
1839
- func requirePoweredOn(operation: String) throws {
1840
- if let current = centralManager?.state {
1841
- state = current
1842
- }
1843
- switch state {
1844
- case .poweredOn:
1845
- return
1846
- case .poweredOff:
1847
- throw BluetoothError(
1848
- code: "bluetooth_powered_off",
1849
- message: "Turn on phone Bluetooth to \(operation)."
1850
- )
1851
- case .unauthorized:
1852
- throw BluetoothError(
1853
- code: "bluetooth_unauthorized",
1854
- message: "Allow Bluetooth access to \(operation)."
1855
- )
1856
- case .unsupported:
1857
- throw BluetoothError(
1858
- code: "bluetooth_unsupported",
1859
- message: "This phone does not support Bluetooth."
1860
- )
1861
- case .resetting, .unknown:
1862
- throw BluetoothError(
1863
- code: "bluetooth_not_ready",
1864
- message: "Bluetooth is not ready yet. Try again."
1865
- )
1866
- @unknown default:
1867
- throw BluetoothError(
1868
- code: "bluetooth_unavailable",
1869
- message: "Bluetooth is unavailable. Try again."
1870
- )
1871
- }
1872
- }
1873
- }
1874
-
1875
- public enum ScanStopReason {
1876
- case completed
1877
- case cancelled
1878
- case error
1879
- }
1880
-
1881
- @MainActor
1882
- public final class ScanSession {
1883
- private let stopAction: () -> Void
1884
- private var stopped = false
1885
-
1886
- init(stopAction: @escaping () -> Void) {
1887
- self.stopAction = stopAction
1888
- }
1889
-
1890
- public func stop() {
1891
- guard !stopped else { return }
1892
- stopped = true
1893
- stopAction()
1894
- }
1895
-
1896
- fileprivate func markStopped() {
1897
- stopped = true
1898
- }
1899
- }
1900
-
1901
- public struct LocalTranscriptionEvent: CustomStringConvertible {
1902
- public let text: String
1903
- public let isFinal: Bool
1904
- public let values: [String: Any]
1905
-
1906
- public init(text: String, isFinal: Bool, values: [String: Any]) {
1907
- self.text = text
1908
- self.isFinal = isFinal
1909
- self.values = values
1910
- }
1911
-
1912
- public var description: String {
1913
- "LocalTranscriptionEvent(text: \(text), isFinal: \(isFinal))"
1914
- }
1915
- }
1916
-
1917
- public struct MicPcmEvent: CustomStringConvertible {
1918
- public static let sampleRate = 16_000
1919
- public static let bitsPerSample = 16
1920
- public static let channels = 1
1921
- public static let encoding = "pcm_s16le"
1922
-
1923
- public let pcm: Data
1924
- public let sampleRate: Int
1925
- public let bitsPerSample: Int
1926
- public let channels: Int
1927
- public let encoding: String
1928
- public let vadGated: Bool
1929
- public let values: [String: Any]
1930
-
1931
- public init(values: [String: Any]) {
1932
- let pcm = values["pcm"] as? Data ?? Data()
1933
- let sampleRate = intValue(values["sampleRate"]) ?? Self.sampleRate
1934
- let bitsPerSample = intValue(values["bitsPerSample"]) ?? Self.bitsPerSample
1935
- let channels = intValue(values["channels"]) ?? Self.channels
1936
- let encoding = values["encoding"] as? String ?? Self.encoding
1937
- let vadGated = boolValue(values, "vadGated") ?? false
1938
-
1939
- var normalized = values
1940
- normalized["type"] = "mic_pcm"
1941
- normalized["pcm"] = pcm
1942
- normalized["sampleRate"] = sampleRate
1943
- normalized["bitsPerSample"] = bitsPerSample
1944
- normalized["channels"] = channels
1945
- normalized["encoding"] = encoding
1946
- normalized["vadGated"] = vadGated
1947
-
1948
- self.pcm = pcm
1949
- self.sampleRate = sampleRate
1950
- self.bitsPerSample = bitsPerSample
1951
- self.channels = channels
1952
- self.encoding = encoding
1953
- self.vadGated = vadGated
1954
- self.values = normalized
1955
- }
1956
-
1957
- public var description: String {
1958
- "MicPcmEvent(bytes: \(pcm.count), sampleRate: \(sampleRate), bitsPerSample: \(bitsPerSample), channels: \(channels), encoding: \(encoding), vadGated: \(vadGated))"
1959
- }
1960
- }
1961
-
1962
- public struct MicLc3Event: CustomStringConvertible {
1963
- public static let sampleRate = 16_000
1964
- public static let channels = 1
1965
- public static let encoding = "lc3"
1966
- public static let frameDurationMs = 10
1967
- public static let defaultFrameSizeBytes = 60
1968
-
1969
- public let lc3: Data
1970
- public let sampleRate: Int
1971
- public let channels: Int
1972
- public let encoding: String
1973
- public let frameDurationMs: Int
1974
- public let frameSizeBytes: Int
1975
- public let bitrate: Int
1976
- public let packetizedFromGlasses: Bool
1977
- public let vadGated: Bool
1978
- public let values: [String: Any]
1979
-
1980
- public init(values: [String: Any]) {
1981
- let lc3 = values["lc3"] as? Data ?? Data()
1982
- let sampleRate = intValue(values["sampleRate"]) ?? Self.sampleRate
1983
- let channels = intValue(values["channels"]) ?? Self.channels
1984
- let encoding = values["encoding"] as? String ?? Self.encoding
1985
- let frameDurationMs = intValue(values["frameDurationMs"]) ?? Self.frameDurationMs
1986
- let frameSizeBytes = intValue(values["frameSizeBytes"]) ?? Self.defaultFrameSizeBytes
1987
- let bitrate = intValue(values["bitrate"]) ?? frameSizeBytes * 8 * (1000 / frameDurationMs)
1988
- let packetizedFromGlasses = boolValue(values, "packetizedFromGlasses") ?? false
1989
- let vadGated = boolValue(values, "vadGated") ?? false
1990
-
1991
- var normalized = values
1992
- normalized["type"] = "mic_lc3"
1993
- normalized["lc3"] = lc3
1994
- normalized["sampleRate"] = sampleRate
1995
- normalized["channels"] = channels
1996
- normalized["encoding"] = encoding
1997
- normalized["frameDurationMs"] = frameDurationMs
1998
- normalized["frameSizeBytes"] = frameSizeBytes
1999
- normalized["bitrate"] = bitrate
2000
- normalized["packetizedFromGlasses"] = packetizedFromGlasses
2001
- normalized["vadGated"] = vadGated
2002
-
2003
- self.lc3 = lc3
2004
- self.sampleRate = sampleRate
2005
- self.channels = channels
2006
- self.encoding = encoding
2007
- self.frameDurationMs = frameDurationMs
2008
- self.frameSizeBytes = frameSizeBytes
2009
- self.bitrate = bitrate
2010
- self.packetizedFromGlasses = packetizedFromGlasses
2011
- self.vadGated = vadGated
2012
- self.values = normalized
2013
- }
2014
-
2015
- public var description: String {
2016
- "MicLc3Event(bytes: \(lc3.count), sampleRate: \(sampleRate), channels: \(channels), frameDurationMs: \(frameDurationMs), frameSizeBytes: \(frameSizeBytes), bitrate: \(bitrate), packetizedFromGlasses: \(packetizedFromGlasses), vadGated: \(vadGated))"
2017
- }
2018
- }
2019
-
2020
- public struct GlassesMediaVolumeGetResult: CustomStringConvertible {
2021
- public let level: Int?
2022
- public let statusCode: Int?
2023
- public let values: [String: Any]
2024
-
2025
- public init(values: [String: Any]) {
2026
- self.level = intValue(values["level"])
2027
- self.statusCode = intValue(values["statusCode"])
2028
- self.values = values
2029
- }
2030
-
2031
- public var description: String {
2032
- let levelText = level.map(String.init) ?? "unknown"
2033
- let statusCodeText = statusCode.map(String.init) ?? "unknown"
2034
- return "GlassesMediaVolumeGetResult(level: \(levelText), statusCode: \(statusCodeText))"
2035
- }
2036
- }
2037
-
2038
- public struct GlassesMediaVolumeSetResult: CustomStringConvertible {
2039
- public let statusCode: Int?
2040
- public let values: [String: Any]
2041
-
2042
- public init(values: [String: Any]) {
2043
- self.statusCode = intValue(values["statusCode"])
2044
- self.values = values
2045
- }
2046
-
2047
- public var description: String {
2048
- "GlassesMediaVolumeSetResult(statusCode: \(statusCode.map(String.init) ?? "unknown"))"
2049
- }
2050
- }
2051
-
2052
- public enum BluetoothEvent: CustomStringConvertible {
2053
- case buttonPress(ButtonPressEvent)
2054
- case touch(TouchEvent)
2055
- case wifiStatus(WifiStatusEvent)
2056
- case hotspotStatus(HotspotStatusEvent)
2057
- case hotspotError(HotspotErrorEvent)
2058
- case photoResponse(PhotoResponseEvent)
2059
- case streamStatus(StreamStatusEvent)
2060
- case keepAliveAck(KeepAliveAckEvent)
2061
- case localTranscription(LocalTranscriptionEvent)
2062
- case raw(name: String, values: [String: Any])
2063
-
2064
- public var description: String {
2065
- switch self {
2066
- case let .buttonPress(event):
2067
- event.description
2068
- case let .touch(event):
2069
- event.description
2070
- case let .wifiStatus(event):
2071
- event.description
2072
- case let .hotspotStatus(event):
2073
- event.description
2074
- case let .hotspotError(event):
2075
- event.description
2076
- case let .photoResponse(event):
2077
- event.description
2078
- case let .streamStatus(event):
2079
- event.description
2080
- case let .keepAliveAck(event):
2081
- event.description
2082
- case let .localTranscription(event):
2083
- event.description
2084
- case let .raw(name, values):
2085
- "\(name): \(values)"
2086
- }
2087
- }
2088
- }
2089
-
2090
- @MainActor
2091
- public protocol MentraBluetoothSDKDelegate: AnyObject {
2092
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didUpdateGlassesStatus status: GlassesStatusUpdate)
2093
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didUpdateBluetoothStatus status: BluetoothStatusUpdate)
2094
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didDiscover device: Device)
2095
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didStopScan reason: ScanStopReason)
2096
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceive event: BluetoothEvent)
2097
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicPcm event: MicPcmEvent)
2098
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didReceiveMicLc3 event: MicLc3Event)
2099
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didChangeDefaultDevice device: Device?)
2100
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didLog message: String)
2101
- func mentraBluetoothSDK(_ sdk: MentraBluetoothSDK, didFail error: BluetoothError)
2102
- }
2103
-
2104
- @MainActor
2105
- public extension MentraBluetoothSDKDelegate {
2106
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didUpdateGlassesStatus _: GlassesStatusUpdate) {}
2107
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didUpdateBluetoothStatus _: BluetoothStatusUpdate) {}
2108
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didDiscover _: Device) {}
2109
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didStopScan _: ScanStopReason) {}
2110
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceive _: BluetoothEvent) {}
2111
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicPcm _: MicPcmEvent) {}
2112
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didReceiveMicLc3 _: MicLc3Event) {}
2113
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didChangeDefaultDevice _: Device?) {}
2114
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didLog _: String) {}
2115
- func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail _: BluetoothError) {}
2116
- }
2117
-
2118
4
  @MainActor
2119
5
  private final class ActiveScanSession {
2120
6
  let model: DeviceModel
@@ -2163,11 +49,27 @@ public final class MentraBluetoothSDK {
2163
49
  }
2164
50
  }
2165
51
 
2166
- public var glassesStatus: GlassesStatus {
52
+ public var state: MentraBluetoothState {
53
+ MentraBluetoothState(glassesStatus: glassesStatus, bluetoothStatus: bluetoothStatus)
54
+ }
55
+
56
+ public var glasses: GlassesRuntimeState {
57
+ state.glasses
58
+ }
59
+
60
+ public var sdkState: PhoneSdkRuntimeState {
61
+ state.sdk
62
+ }
63
+
64
+ public var scanState: BluetoothScanState {
65
+ state.scan
66
+ }
67
+
68
+ var glassesStatus: GlassesStatus {
2167
69
  GlassesStatus(values: DeviceStore.shared.store.getCategory("glasses"))
2168
70
  }
2169
71
 
2170
- public var bluetoothStatus: BluetoothStatus {
72
+ var bluetoothStatus: BluetoothStatus {
2171
73
  BluetoothStatus(values: DeviceStore.shared.store.getCategory(ObservableStore.bluetoothCategory))
2172
74
  }
2173
75
 
@@ -2564,9 +466,14 @@ public final class MentraBluetoothSDK {
2564
466
  private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
2565
467
  switch ObservableStore.normalizeCategory(category) {
2566
468
  case "glasses":
2567
- delegate?.mentraBluetoothSDK(self, didUpdateGlassesStatus: GlassesStatusUpdate(values: glassesStatusChanges(changes)))
469
+ let nextState = state
470
+ delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
471
+ delegate?.mentraBluetoothSDK(self, didUpdateGlasses: nextState.glasses)
2568
472
  case ObservableStore.bluetoothCategory:
2569
- delegate?.mentraBluetoothSDK(self, didUpdateBluetoothStatus: BluetoothStatusUpdate(values: changes))
473
+ let nextState = state
474
+ delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
475
+ delegate?.mentraBluetoothSDK(self, didUpdateSdkState: nextState.sdk)
476
+ delegate?.mentraBluetoothSDK(self, didUpdateScan: nextState.scan)
2570
477
  if !suppressDefaultDeviceEvents && changes.keys.contains(where: { defaultDeviceKeys.contains($0) }) {
2571
478
  dispatchDefaultDeviceChanged()
2572
479
  }
@@ -2577,28 +484,6 @@ public final class MentraBluetoothSDK {
2577
484
  }
2578
485
  }
2579
486
 
2580
- private func glassesStatusChanges(_ changes: [String: Any]) -> [String: Any] {
2581
- var merged = changes
2582
-
2583
- if changes.keys.contains(where: { ["wifiConnected", "wifiSsid", "wifiLocalIp"].contains($0) }) {
2584
- merged["wifiConnected"] = DeviceStore.shared.get("glasses", "wifiConnected") as? Bool ?? false
2585
- merged["wifiSsid"] = DeviceStore.shared.get("glasses", "wifiSsid") as? String ?? ""
2586
- merged["wifiLocalIp"] = DeviceStore.shared.get("glasses", "wifiLocalIp") as? String ?? ""
2587
- }
2588
-
2589
- if changes.keys.contains(where: { ["connected", "fullyBooted", "connectionState"].contains($0) }) {
2590
- merged["connected"] = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
2591
- merged["fullyBooted"] = DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
2592
- merged["connectionState"] = DeviceStore.shared.get("glasses", "connectionState") as? String ?? "DISCONNECTED"
2593
- }
2594
-
2595
- if changes["signalStrengthUpdatedAt"] != nil, changes["signalStrength"] == nil {
2596
- merged["signalStrength"] = DeviceStore.shared.get("glasses", "signalStrength") as? Int ?? -1
2597
- }
2598
-
2599
- return merged
2600
- }
2601
-
2602
487
  private func dispatchDefaultDeviceChanged() {
2603
488
  delegate?.mentraBluetoothSDK(self, didChangeDefaultDevice: currentDefaultDevice())
2604
489
  }