@mentra/bluetooth-sdk 0.1.12 → 0.1.14

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 (69) hide show
  1. package/README.md +30 -11
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +47 -12
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +75 -29
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -5
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +208 -45
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +80 -14
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -16
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +4 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3831 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -28
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +514 -561
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8674 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +5 -17
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +9 -20
  20. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  21. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
  22. package/build/BluetoothSdk.types.d.ts +48 -17
  23. package/build/BluetoothSdk.types.d.ts.map +1 -1
  24. package/build/BluetoothSdk.types.js.map +1 -1
  25. package/build/_private/BluetoothSdkModule.d.ts +9 -9
  26. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  27. package/build/_private/BluetoothSdkModule.js +0 -6
  28. package/build/_private/BluetoothSdkModule.js.map +1 -1
  29. package/build/_private/photoRequestPayload.d.ts +3 -1
  30. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  31. package/build/_private/photoRequestPayload.js +46 -2
  32. package/build/_private/photoRequestPayload.js.map +1 -1
  33. package/build/debug.d.ts +3 -0
  34. package/build/debug.d.ts.map +1 -0
  35. package/build/debug.js +8 -0
  36. package/build/debug.js.map +1 -0
  37. package/build/index.d.ts +1 -1
  38. package/build/index.d.ts.map +1 -1
  39. package/build/index.js +8 -8
  40. package/build/index.js.map +1 -1
  41. package/ios/BluetoothSdkModule.swift +44 -68
  42. package/ios/Source/BluetoothSdkDefaults.swift +23 -3
  43. package/ios/Source/DeviceManager.swift +38 -33
  44. package/ios/Source/DeviceStore.swift +5 -5
  45. package/ios/Source/MentraBluetoothSDK.swift +217 -50
  46. package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
  47. package/ios/Source/ObservableStore.swift +11 -0
  48. package/ios/Source/OtaManifest.swift +170 -0
  49. package/ios/Source/camera/CameraModels.swift +219 -13
  50. package/ios/Source/controllers/ControllerManager.swift +3 -7
  51. package/ios/Source/controllers/R1.swift +3 -7
  52. package/ios/Source/sgcs/Frame.swift +0 -2
  53. package/ios/Source/sgcs/G1.swift +7 -8
  54. package/ios/Source/sgcs/G2.swift +21 -20
  55. package/ios/Source/sgcs/Mach1.swift +7 -8
  56. package/ios/Source/sgcs/MentraLive.swift +132 -63
  57. package/ios/Source/sgcs/MentraNex.swift +7 -8
  58. package/ios/Source/sgcs/SGCManager.swift +7 -10
  59. package/ios/Source/sgcs/Simulated.swift +8 -8
  60. package/ios/Source/status/DeviceStatus.swift +1 -9
  61. package/ios/Source/streaming/StreamModels.swift +7 -32
  62. package/package.json +6 -1
  63. package/src/BluetoothSdk.types.ts +49 -17
  64. package/src/_private/BluetoothSdkModule.ts +9 -17
  65. package/src/_private/photoRequestPayload.ts +48 -3
  66. package/src/debug.ts +9 -0
  67. package/src/index.ts +19 -10
  68. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
  69. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7434
@@ -1,17 +1,44 @@
1
1
  import Foundation
2
2
 
3
3
  public enum PhotoSize: String {
4
- case small
4
+ case low
5
5
  case medium
6
- case large
7
- case full
6
+ case high
7
+ case max
8
+
9
+ public static func normalizeLegacy(_ value: String?) -> String {
10
+ switch value {
11
+ case "small":
12
+ return PhotoSize.low.rawValue
13
+ case "large":
14
+ return PhotoSize.high.rawValue
15
+ case "full":
16
+ return PhotoSize.max.rawValue
17
+ default:
18
+ return value ?? PhotoSize.medium.rawValue
19
+ }
20
+ }
21
+
22
+ public init(normalizedRawValue value: String?) {
23
+ let normalized = PhotoSize.normalizeLegacy(value)
24
+ self = PhotoSize(rawValue: normalized) ?? .medium
25
+ }
8
26
  }
9
27
 
10
28
  public enum ButtonPhotoSize: String {
11
- case small
29
+ case low
12
30
  case medium
13
- case large
31
+ case high
14
32
  case max
33
+
34
+ public static func normalizeLegacy(_ value: String?) -> String {
35
+ PhotoSize.normalizeLegacy(value)
36
+ }
37
+
38
+ public init(normalizedRawValue value: String?) {
39
+ let normalized = ButtonPhotoSize.normalizeLegacy(value)
40
+ self = ButtonPhotoSize(rawValue: normalized) ?? .medium
41
+ }
15
42
  }
16
43
 
17
44
  public enum PhotoCompression: String {
@@ -20,15 +47,71 @@ public enum PhotoCompression: String {
20
47
  case heavy
21
48
  }
22
49
 
23
- public struct ButtonPhotoSettings {
24
- public let size: ButtonPhotoSize
50
+ public struct PhotoCaptureDefaults {
51
+ public let size: PhotoSize?
52
+ public let mfnr: Bool?
53
+ public let zsl: Bool?
54
+ public let noiseReduction: Bool?
55
+ public let edgeEnhancement: Bool?
56
+ public let ispDigitalGain: Int?
57
+ public let ispAnalogGain: String?
58
+ public let aeExposureDivisor: Int?
59
+ public let isoCap: Int?
60
+ public let compress: String?
61
+ public let sound: Bool?
62
+ public let resetCaptureTuning: Bool?
25
63
 
26
- public init(size: ButtonPhotoSize) {
64
+ public init(
65
+ size: PhotoSize? = nil,
66
+ mfnr: Bool? = nil,
67
+ zsl: Bool? = nil,
68
+ noiseReduction: Bool? = nil,
69
+ edgeEnhancement: Bool? = nil,
70
+ ispDigitalGain: Int? = nil,
71
+ ispAnalogGain: String? = nil,
72
+ aeExposureDivisor: Int? = nil,
73
+ isoCap: Int? = nil,
74
+ compress: String? = nil,
75
+ sound: Bool? = nil,
76
+ resetCaptureTuning: Bool? = nil
77
+ ) {
27
78
  self.size = size
79
+ self.mfnr = mfnr
80
+ self.zsl = zsl
81
+ self.noiseReduction = noiseReduction
82
+ self.edgeEnhancement = edgeEnhancement
83
+ self.ispDigitalGain = ispDigitalGain
84
+ self.ispAnalogGain = ispAnalogGain
85
+ self.aeExposureDivisor = aeExposureDivisor
86
+ self.isoCap = isoCap
87
+ self.compress = compress
88
+ self.sound = sound
89
+ self.resetCaptureTuning = resetCaptureTuning
90
+ }
91
+
92
+ static func from(params: [String: Any]) -> PhotoCaptureDefaults {
93
+ let size = (params["size"] as? String).map { PhotoSize(normalizedRawValue: $0) }
94
+ let aeExposureDivisor =
95
+ optionalIntValue(params, "aeExposureDivisor").flatMap { $0 > 1 ? $0 : nil }
96
+ let isoCap = optionalIntValue(params, "isoCap").flatMap { $0 > 0 ? $0 : nil }
97
+ return PhotoCaptureDefaults(
98
+ size: size,
99
+ mfnr: optionalBoolValue(params, "mfnr"),
100
+ zsl: optionalBoolValue(params, "zsl"),
101
+ noiseReduction: optionalBoolValue(params, "noiseReduction"),
102
+ edgeEnhancement: optionalBoolValue(params, "edgeEnhancement"),
103
+ ispDigitalGain: optionalIntValue(params, "ispDigitalGain"),
104
+ ispAnalogGain: optionalStringValue(params, "ispAnalogGain"),
105
+ aeExposureDivisor: aeExposureDivisor,
106
+ isoCap: isoCap,
107
+ compress: optionalStringValue(params, "compress"),
108
+ sound: optionalBoolValue(params, "sound"),
109
+ resetCaptureTuning: optionalBoolValue(params, "resetCaptureTuning")
110
+ )
28
111
  }
29
112
  }
30
113
 
31
- public struct ButtonVideoRecordingSettings {
114
+ public struct VideoRecordingDefaults {
32
115
  public let width: Int
33
116
  public let height: Int
34
117
  public let fps: Int
@@ -145,13 +228,20 @@ public struct PhotoRequest {
145
228
  public let webhookUrl: String?
146
229
  public let authToken: String?
147
230
  public let compress: PhotoCompression?
148
- public let flash: Bool
149
231
  public let save: Bool
150
232
  public let sound: Bool
151
233
  /// Sensor exposure time for this capture only (ns), or nil for auto exposure
152
234
  public let exposureTimeNs: Double?
153
235
  /// Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure.
154
236
  public let iso: Int?
237
+ public let aeExposureDivisor: Int?
238
+ public let isoCap: Int?
239
+ public let noiseReduction: Bool?
240
+ public let edgeEnhancement: Bool?
241
+ public let mfnr: Bool?
242
+ public let zsl: Bool?
243
+ public let ispDigitalGain: Int?
244
+ public let ispAnalogGain: String?
155
245
 
156
246
  public init(
157
247
  requestId: String,
@@ -160,11 +250,18 @@ public struct PhotoRequest {
160
250
  webhookUrl: String? = nil,
161
251
  authToken: String? = nil,
162
252
  compress: PhotoCompression? = nil,
163
- flash: Bool = true,
164
253
  save: Bool = false,
165
254
  sound: Bool,
166
255
  exposureTimeNs: Double? = nil,
167
- iso: Int? = nil
256
+ iso: Int? = nil,
257
+ aeExposureDivisor: Int? = nil,
258
+ isoCap: Int? = nil,
259
+ noiseReduction: Bool? = nil,
260
+ edgeEnhancement: Bool? = nil,
261
+ mfnr: Bool? = nil,
262
+ zsl: Bool? = nil,
263
+ ispDigitalGain: Int? = nil,
264
+ ispAnalogGain: String? = nil
168
265
  ) {
169
266
  self.requestId = requestId
170
267
  self.appId = appId
@@ -172,11 +269,120 @@ public struct PhotoRequest {
172
269
  self.webhookUrl = webhookUrl
173
270
  self.authToken = authToken
174
271
  self.compress = compress
175
- self.flash = flash
176
272
  self.save = save
177
273
  self.sound = sound
178
274
  self.exposureTimeNs = exposureTimeNs
179
275
  self.iso = iso
276
+ self.aeExposureDivisor = aeExposureDivisor
277
+ self.isoCap = isoCap
278
+ self.noiseReduction = noiseReduction
279
+ self.edgeEnhancement = edgeEnhancement
280
+ self.mfnr = mfnr
281
+ self.zsl = zsl
282
+ self.ispDigitalGain = ispDigitalGain
283
+ self.ispAnalogGain = ispAnalogGain
284
+ }
285
+
286
+ public static func from(params: [String: Any]) -> PhotoRequest {
287
+ let sizeRaw = params["size"] as? String ?? "medium"
288
+ let compressRaw = params["compress"] as? String ?? "none"
289
+ let exposureTimeNs: Double?
290
+ switch params["exposureTimeNs"] {
291
+ case let value as Double:
292
+ exposureTimeNs = value.isFinite && value > 0 ? value : nil
293
+ case let value as Int:
294
+ exposureTimeNs = value > 0 ? Double(value) : nil
295
+ case let value as NSNumber:
296
+ let d = value.doubleValue
297
+ exposureTimeNs = d.isFinite && d > 0 ? d : nil
298
+ default:
299
+ exposureTimeNs = nil
300
+ }
301
+ let iso: Int?
302
+ switch params["iso"] {
303
+ case let value as Int:
304
+ iso = value > 0 ? value : nil
305
+ case let value as Double:
306
+ iso = value.isFinite && value > 0 && value < Double(Int.max) ? Int(value) : nil
307
+ case let value as NSNumber:
308
+ let intValue = value.intValue
309
+ iso = intValue > 0 ? intValue : nil
310
+ default:
311
+ iso = nil
312
+ }
313
+ func optionalInt(_ key: String, min: Int = Int.min, filter: (Int) -> Bool = { _ in true }) -> Int? {
314
+ guard params.keys.contains(key) else { return nil }
315
+ switch params[key] {
316
+ case let value as Int:
317
+ return filter(value) ? value : nil
318
+ case let value as Double:
319
+ guard value.isFinite, value >= Double(min) else { return nil }
320
+ return filter(Int(value)) ? Int(value) : nil
321
+ case let value as NSNumber:
322
+ let intValue = value.intValue
323
+ return filter(intValue) ? intValue : nil
324
+ default:
325
+ return nil
326
+ }
327
+ }
328
+ func optionalBool(_ key: String) -> Bool? {
329
+ guard params.keys.contains(key) else { return nil }
330
+ return params[key] as? Bool
331
+ }
332
+
333
+ return PhotoRequest(
334
+ requestId: params["requestId"] as? String ?? "",
335
+ appId: params["appId"] as? String ?? "",
336
+ size: PhotoSize(normalizedRawValue: sizeRaw),
337
+ webhookUrl: params["webhookUrl"] as? String,
338
+ authToken: (params["authToken"] as? String)?.nilIfBlank,
339
+ compress: PhotoCompression(rawValue: compressRaw),
340
+ save: (params["save"] as? Bool) ?? (params["saveToGallery"] as? Bool) ?? false,
341
+ sound: params["sound"] as? Bool ?? true,
342
+ exposureTimeNs: exposureTimeNs,
343
+ iso: iso,
344
+ aeExposureDivisor: optionalInt("aeExposureDivisor", min: 2) { $0 > 1 },
345
+ isoCap: optionalInt("isoCap", min: 1) { $0 > 0 },
346
+ noiseReduction: optionalBool("noiseReduction"),
347
+ edgeEnhancement: optionalBool("edgeEnhancement"),
348
+ mfnr: optionalBool("mfnr"),
349
+ zsl: optionalBool("zsl"),
350
+ ispDigitalGain: optionalInt("ispDigitalGain"),
351
+ ispAnalogGain: params["ispAnalogGain"] as? String
352
+ )
353
+ }
354
+
355
+ func appendScanFields(to json: inout [String: Any]) {
356
+ if let aeExposureDivisor {
357
+ json["aeExposureDivisor"] = aeExposureDivisor
358
+ }
359
+ if let isoCap {
360
+ json["isoCap"] = isoCap
361
+ }
362
+ if let noiseReduction {
363
+ json["noiseReduction"] = noiseReduction
364
+ }
365
+ if let edgeEnhancement {
366
+ json["edgeEnhancement"] = edgeEnhancement
367
+ }
368
+ if let mfnr {
369
+ json["mfnr"] = mfnr
370
+ }
371
+ if let zsl {
372
+ json["zsl"] = zsl
373
+ }
374
+ if let ispDigitalGain {
375
+ json["ispDigitalGain"] = ispDigitalGain
376
+ }
377
+ if let ispAnalogGain {
378
+ json["ispAnalogGain"] = ispAnalogGain
379
+ }
380
+ }
381
+ }
382
+
383
+ private extension String {
384
+ var nilIfBlank: String? {
385
+ isEmpty ? nil : self
180
386
  }
181
387
  }
182
388
 
@@ -18,14 +18,11 @@ protocol ControllerManager {
18
18
 
19
19
  // MARK: - Camera & Media
20
20
 
21
- func requestPhoto(
22
- _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
23
- compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
24
- )
21
+ func requestPhoto(_ request: PhotoRequest)
25
22
  func startStream(_ message: [String: Any])
26
23
  func stopStream()
27
24
  func sendStreamKeepAlive(_ message: [String: Any])
28
- func startVideoRecording(requestId: String, save: Bool, flash: Bool, sound: Bool)
25
+ func startVideoRecording(requestId: String, save: Bool, sound: Bool)
29
26
  func stopVideoRecording(requestId: String)
30
27
 
31
28
  // MARK: - Button Settings
@@ -33,7 +30,6 @@ protocol ControllerManager {
33
30
  func sendButtonPhotoSettings()
34
31
  func sendButtonVideoRecordingSettings()
35
32
  func sendButtonMaxRecordingTime()
36
- func sendButtonCameraLedSetting()
37
33
 
38
34
  // MARK: - Display Control
39
35
 
@@ -75,7 +71,7 @@ protocol ControllerManager {
75
71
  func sendWifiCredentials(_ ssid: String, _ password: String)
76
72
  func forgetWifiNetwork(_ ssid: String)
77
73
  func sendHotspotState(_ enabled: Bool)
78
- func sendOtaStart()
74
+ func sendOtaStart(otaVersionUrl: String?)
79
75
  func sendOtaQueryStatus()
80
76
 
81
77
  // MARK: - User Context (for crash reporting)
@@ -504,11 +504,8 @@ class R1: NSObject, ControllerManager {
504
504
  }
505
505
 
506
506
  func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
507
- func requestPhoto(
508
- _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
509
- compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
510
- ) {}
511
- func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
507
+ func requestPhoto(_: PhotoRequest) {}
508
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
512
509
  func stopVideoRecording(requestId _: String) {}
513
510
  func startStream(_: [String: Any]) {}
514
511
  func stopStream() {}
@@ -516,7 +513,6 @@ class R1: NSObject, ControllerManager {
516
513
  func sendButtonPhotoSettings() {}
517
514
  func sendButtonVideoRecordingSettings() {}
518
515
  func sendButtonMaxRecordingTime() {}
519
- func sendButtonCameraLedSetting() {}
520
516
  func setBrightness(_: Int, autoMode _: Bool) {}
521
517
  func clearDisplay() {}
522
518
  func sendTextWall(_: String) {}
@@ -543,7 +539,7 @@ class R1: NSObject, ControllerManager {
543
539
  func sendWifiCredentials(_: String, _: String) {}
544
540
  func forgetWifiNetwork(_: String) {}
545
541
  func sendHotspotState(_: Bool) {}
546
- func sendOtaStart() {}
542
+ func sendOtaStart(otaVersionUrl: String?) {}
547
543
  func sendOtaQueryStatus() {}
548
544
  func sendUserEmailToGlasses(_: String) {}
549
545
  func queryGalleryStatus() {}
@@ -75,8 +75,6 @@
75
75
 
76
76
  // func sendButtonMaxRecordingTime(_: Int) {}
77
77
 
78
- // func sendButtonCameraLedSetting() {}
79
-
80
78
  func exit() {}
81
79
 
82
80
  func sendRgbLedControl(requestId: String, packageName _: String?, action _: String, color _: String?, onDurationMs _: Int, offDurationMs _: Int, count _: Int) {
@@ -276,10 +276,7 @@ class G1: NSObject, SGCManager {
276
276
 
277
277
  func sendButtonMaxRecordingTime(_: Int) {}
278
278
 
279
- func requestPhoto(
280
- _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
281
- compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
282
- ) {}
279
+ func requestPhoto(_: PhotoRequest) {}
283
280
 
284
281
  func startStream(_: [String: Any]) {}
285
282
 
@@ -287,7 +284,7 @@ class G1: NSObject, SGCManager {
287
284
 
288
285
  func sendStreamKeepAlive(_: [String: Any]) {}
289
286
 
290
- func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
287
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
291
288
 
292
289
  func stopVideoRecording(requestId _: String) {}
293
290
 
@@ -297,8 +294,6 @@ class G1: NSObject, SGCManager {
297
294
 
298
295
  func sendButtonMaxRecordingTime() {}
299
296
 
300
- func sendButtonCameraLedSetting() {}
301
-
302
297
  func sendCameraFovSetting() {}
303
298
 
304
299
  func showDashboard() {}
@@ -317,7 +312,7 @@ class G1: NSObject, SGCManager {
317
312
 
318
313
  func queryGalleryStatus() {}
319
314
 
320
- func sendOtaStart() {}
315
+ func sendOtaStart(otaVersionUrl: String?) {}
321
316
  func sendOtaQueryStatus() {}
322
317
 
323
318
  func ping() {}
@@ -786,6 +781,10 @@ class G1: NSObject, SGCManager {
786
781
  // }
787
782
  }
788
783
 
784
+ func sendText(_ text: String) async {
785
+ await sendTextWall(text)
786
+ }
787
+
789
788
  func sendTextWall(_ text: String) async {
790
789
  let chunks = textHelper.createTextWallChunks(text)
791
790
  queueChunks(chunks, sleepAfterMs: 10)
@@ -1534,7 +1534,6 @@ class G2: NSObject, SGCManager {
1534
1534
  /// Fixed pool of container IDs the page protocol expects.
1535
1535
  private let imageContainerIDPool: [Int32] = [10, 11, 12, 13]
1536
1536
  private let textContainerIDPool: [Int32] = [1, 2, 3, 4, 5, 6]
1537
- /// Default container seeded into every fresh page: 100x100 in the top-left.
1538
1537
  private static let defaultImgContainer = (
1539
1538
  x: Int32(188), y: Int32(44), width: Int32(200), height: Int32(100)
1540
1539
  )
@@ -1927,7 +1926,7 @@ class G2: NSObject, SGCManager {
1927
1926
  // MARK: - SGCManager: Display Control
1928
1927
 
1929
1928
  func sendTextWall(_ text: String) async {
1930
- await sendText(
1929
+ await sendTextAt(
1931
1930
  text,
1932
1931
  x: G2.defaultTextContainer.x,
1933
1932
  y: G2.defaultTextContainer.y,
@@ -1940,7 +1939,13 @@ class G2: NSObject, SGCManager {
1940
1939
  )
1941
1940
  }
1942
1941
 
1943
- func sendText(
1942
+ // Protocol witness for SGCManager.sendText — G2 renders a simple string as a
1943
+ // default-positioned text wall. The positioned variant is `sendTextAt`.
1944
+ func sendText(_ text: String) async {
1945
+ await sendTextWall(text)
1946
+ }
1947
+
1948
+ func sendTextAt(
1944
1949
  _ text: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil, height: Int32? = nil,
1945
1950
  borderWidth: Int32? = nil, borderColor: Int32? = nil, borderRadius: Int32? = nil,
1946
1951
  paddingLength: Int32? = nil
@@ -1983,7 +1988,7 @@ class G2: NSObject, SGCManager {
1983
1988
  let msg = EvenHubProto.updateTextMessage(
1984
1989
  containerID: container.id,
1985
1990
  contentOffset: 0,
1986
- contentLength: Int32(text.utf8.count),
1991
+ contentLength: Int32(container.content.utf8.count),
1987
1992
  content: container.content
1988
1993
  )
1989
1994
  queueEvenHubCommand(msg)
@@ -2011,10 +2016,10 @@ class G2: NSObject, SGCManager {
2011
2016
  // Don't shutdown the EvenHub page — that kills audio streaming too.
2012
2017
  // Instead, just clear the text content by sending a space.
2013
2018
 
2014
- if !pageCreated {
2015
- Bridge.log("G2: clearDisplay() - page not created")
2016
- createPageWithContainers()
2017
- }
2019
+ // if !pageCreated {
2020
+ // Bridge.log("G2: clearDisplay() - page not created")
2021
+ // createPageWithContainers()
2022
+ // }
2018
2023
 
2019
2024
  // reset the content of all text containers to empty:
2020
2025
  for i in textContainers.indices {
@@ -2024,10 +2029,7 @@ class G2: NSObject, SGCManager {
2024
2029
  imageContainers[i].bmpData = Data()
2025
2030
  }
2026
2031
  // shutdown the page and then recreate the containers without the content:
2027
- let msg = EvenHubProto.shutdownMessage()
2028
- sendEvenHubCommand(msg)
2029
- createPageWithContainers()
2030
- restartMicIfAlreadyEnabled()
2032
+ Task { await rebuildPage() }
2031
2033
  }
2032
2034
 
2033
2035
  /// Send BMP data to an image container via fragmented updateImageRawData
@@ -2207,6 +2209,9 @@ class G2: NSObject, SGCManager {
2207
2209
  // )
2208
2210
  // sendEvenHubCommand(msg)
2209
2211
  // }
2212
+
2213
+ try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
2214
+ restartMicIfAlreadyEnabled()
2210
2215
  }
2211
2216
 
2212
2217
  /// Upscale BMP pixel data by 2x (200x100 → 400x200) using nearest-neighbor
@@ -2766,6 +2771,7 @@ class G2: NSObject, SGCManager {
2766
2771
  func disconnect() {
2767
2772
  Bridge.log("G2: disconnect()")
2768
2773
  isDisconnecting = true
2774
+ clearDisplay()
2769
2775
  cancelPairingTimeout()
2770
2776
  stopHeartbeats()
2771
2777
  Task { await reconnectionManager.stop() }
@@ -3140,12 +3146,8 @@ class G2: NSObject, SGCManager {
3140
3146
 
3141
3147
  // MARK: - SGCManager: Camera & Media (not supported on G2)
3142
3148
 
3143
- func requestPhoto(
3144
- _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
3145
- compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?,
3146
- iso _: Int?
3147
- ) {}
3148
- func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
3149
+ func requestPhoto(_: PhotoRequest) {}
3150
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
3149
3151
  func startStream(_: [String: Any]) {}
3150
3152
  func stopStream() {}
3151
3153
  func sendStreamKeepAlive(_: [String: Any]) {}
@@ -3153,7 +3155,6 @@ class G2: NSObject, SGCManager {
3153
3155
  func sendButtonPhotoSettings() {}
3154
3156
  func sendButtonVideoRecordingSettings() {}
3155
3157
  func sendButtonMaxRecordingTime() {}
3156
- func sendButtonCameraLedSetting() {}
3157
3158
 
3158
3159
  func sendCameraFovSetting() {}
3159
3160
 
@@ -3163,7 +3164,7 @@ class G2: NSObject, SGCManager {
3163
3164
  func sendWifiCredentials(_: String, _: String) {}
3164
3165
  func forgetWifiNetwork(_: String) {}
3165
3166
  func sendHotspotState(_: Bool) {}
3166
- func sendOtaStart() {}
3167
+ func sendOtaStart(otaVersionUrl: String?) {}
3167
3168
  func sendOtaQueryStatus() {}
3168
3169
 
3169
3170
  // MARK: - SGCManager: User Context
@@ -15,10 +15,7 @@ import UltraliteSDK
15
15
  class Mach1: UltraliteBaseViewController, SGCManager {
16
16
  func sendIncidentId(_: String, apiBaseUrl _: String?) {}
17
17
 
18
- func requestPhoto(
19
- _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
20
- compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
21
- ) {}
18
+ func requestPhoto(_: PhotoRequest) {}
22
19
 
23
20
  func sendGalleryMode() {}
24
21
 
@@ -26,7 +23,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
26
23
 
27
24
  var connectionState: String = ConnTypes.DISCONNECTED
28
25
 
29
- func sendOtaStart() {}
26
+ func sendOtaStart(otaVersionUrl: String?) {}
30
27
  func sendOtaQueryStatus() {}
31
28
 
32
29
  func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
@@ -37,8 +34,6 @@ class Mach1: UltraliteBaseViewController, SGCManager {
37
34
 
38
35
  func sendButtonMaxRecordingTime(_: Int) {}
39
36
 
40
- func sendButtonCameraLedSetting() {}
41
-
42
37
  func sendCameraFovSetting() {}
43
38
 
44
39
  func exit() {}
@@ -90,7 +85,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
90
85
 
91
86
  func sendStreamKeepAlive(_: [String: Any]) {}
92
87
 
93
- func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
88
+ func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
94
89
 
95
90
  func stopVideoRecording(requestId _: String) {}
96
91
 
@@ -325,6 +320,10 @@ class Mach1: UltraliteBaseViewController, SGCManager {
325
320
  UltraliteManager.shared.stopScan()
326
321
  }
327
322
 
323
+ func sendText(_ text: String) async {
324
+ await sendTextWall(text)
325
+ }
326
+
328
327
  func sendTextWall(_ text: String) async {
329
328
  // displayTextWall(text)
330
329
  guard let device = UltraliteManager.shared.currentDevice else {