@mentra/bluetooth-sdk 0.1.13 → 0.1.16

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 (80) hide show
  1. package/README.md +18 -8
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +48 -48
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +360 -269
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -7
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +152 -72
  7. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +25 -11
  9. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +51 -8
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -800
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -5
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +423 -117
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +4 -6
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +17 -5
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -7
  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/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  24. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
  25. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  26. package/build/BluetoothSdk.types.d.ts +50 -24
  27. package/build/BluetoothSdk.types.d.ts.map +1 -1
  28. package/build/BluetoothSdk.types.js +1 -0
  29. package/build/BluetoothSdk.types.js.map +1 -1
  30. package/build/_internal.d.ts +4 -4
  31. package/build/_internal.js +4 -4
  32. package/build/_internal.js.map +1 -1
  33. package/build/_private/BluetoothSdkModule.d.ts +14 -8
  34. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  35. package/build/_private/BluetoothSdkModule.js +4 -3
  36. package/build/_private/BluetoothSdkModule.js.map +1 -1
  37. package/build/_private/cameraRequestPayload.d.ts +4 -0
  38. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  39. package/build/_private/cameraRequestPayload.js +25 -0
  40. package/build/_private/cameraRequestPayload.js.map +1 -0
  41. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  42. package/build/_private/photoRequestPayload.js +11 -3
  43. package/build/_private/photoRequestPayload.js.map +1 -1
  44. package/build/index.d.ts +1 -1
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +70 -61
  47. package/build/index.js.map +1 -1
  48. package/ios/BluetoothSdkModule.swift +51 -28
  49. package/ios/Source/Bridge.swift +4 -16
  50. package/ios/Source/DeviceManager.swift +76 -22
  51. package/ios/Source/DeviceStore.swift +0 -4
  52. package/ios/Source/MentraBluetoothSDK.swift +144 -78
  53. package/ios/Source/OtaManifest.swift +13 -13
  54. package/ios/Source/camera/CameraModels.swift +82 -20
  55. package/ios/Source/controllers/ControllerManager.swift +1 -2
  56. package/ios/Source/controllers/R1.swift +1 -2
  57. package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
  58. package/ios/Source/events/BluetoothEvents.swift +5 -27
  59. package/ios/Source/internal/BluetoothAvailability.swift +5 -5
  60. package/ios/Source/sgcs/Frame.swift +0 -2
  61. package/ios/Source/sgcs/G1.swift +52 -4
  62. package/ios/Source/sgcs/G2.swift +643 -204
  63. package/ios/Source/sgcs/Mach1.swift +1 -3
  64. package/ios/Source/sgcs/MentraLive.swift +64 -77
  65. package/ios/Source/sgcs/MentraNex.swift +1 -3
  66. package/ios/Source/sgcs/Nimo.swift +1928 -0
  67. package/ios/Source/sgcs/SGCManager.swift +16 -10
  68. package/ios/Source/sgcs/Simulated.swift +2 -6
  69. package/ios/Source/status/DeviceStatus.swift +0 -8
  70. package/ios/Source/streaming/StreamModels.swift +7 -32
  71. package/ios/Source/stt/STTTools.swift +1 -1
  72. package/ios/Source/types/DeviceModels.swift +5 -0
  73. package/ios/Source/utils/Constants.swift +2 -0
  74. package/package.json +6 -1
  75. package/src/BluetoothSdk.types.ts +54 -28
  76. package/src/_internal.ts +4 -4
  77. package/src/_private/BluetoothSdkModule.ts +20 -11
  78. package/src/_private/cameraRequestPayload.ts +29 -0
  79. package/src/_private/photoRequestPayload.ts +12 -3
  80. package/src/index.ts +77 -67
@@ -31,6 +31,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
31
31
  "hotspot_error",
32
32
  "photo_response",
33
33
  "photo_status",
34
+ "camera_status",
34
35
  "video_recording_status",
35
36
  "media_success",
36
37
  "media_error",
@@ -58,7 +59,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
58
59
  "stream_status",
59
60
  "keep_alive_ack",
60
61
  "mtk_update_complete",
61
- "ota_update_available",
62
62
  "ota_progress",
63
63
  "ota_start_ack",
64
64
  "ota_status",
@@ -154,7 +154,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
154
154
  AsyncFunction("connectWithOptions") { (device: [String: Any], options: [String: Any]) in
155
155
  try await MainActor.run {
156
156
  guard let target = Device(dictionary: device) else {
157
- throw BluetoothError(
157
+ throw BluetoothSdkError(
158
158
  code: "invalid_device",
159
159
  message: "connect requires a Device with model and name."
160
160
  )
@@ -303,7 +303,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
303
303
  timestampMs >= Double(Int64.min),
304
304
  timestampMs <= maxTimestamp
305
305
  else {
306
- throw BluetoothError(
306
+ throw BluetoothSdkError(
307
307
  code: "invalid_timestamp",
308
308
  message: "setSystemTime timestampMs must be a finite Int64 millisecond timestamp."
309
309
  )
@@ -326,29 +326,19 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
326
326
  try await sdk.setVoiceActivityDetectionEnabled(enabled)
327
327
  }
328
328
 
329
- AsyncFunction("setButtonPhotoSettings") { (size: String) in
329
+ AsyncFunction("setPhotoCaptureDefaults") { (params: [String: Any]) in
330
330
  let sdk = await MainActor.run { self.bluetoothSdk() }
331
- return try await sdk.setButtonPhotoSettings(size: ButtonPhotoSize(rawValue: size) ?? .medium).values
331
+ return try await sdk.setPhotoCaptureDefaults(PhotoCaptureDefaults.from(params: params)).values
332
332
  }
333
333
 
334
- AsyncFunction("setButtonPhotoCaptureSettings") { (params: [String: Any]) in
334
+ AsyncFunction("setVideoRecordingDefaults") { (width: Int, height: Int, fps: Int) in
335
335
  let sdk = await MainActor.run { self.bluetoothSdk() }
336
- return try await sdk.setButtonPhotoSettings(ButtonPhotoSettings.from(params: params)).values
336
+ return try await sdk.setVideoRecordingDefaults(VideoRecordingDefaults(width: width, height: height, fps: fps)).values
337
337
  }
338
338
 
339
- AsyncFunction("setButtonVideoRecordingSettings") { (width: Int, height: Int, fps: Int) in
339
+ AsyncFunction("setMaxVideoRecordingDuration") { (minutes: Int) in
340
340
  let sdk = await MainActor.run { self.bluetoothSdk() }
341
- return try await sdk.setButtonVideoRecordingSettings(width: width, height: height, fps: fps).values
342
- }
343
-
344
- AsyncFunction("setButtonCameraLed") { (enabled: Bool) in
345
- let sdk = await MainActor.run { self.bluetoothSdk() }
346
- return try await sdk.setButtonCameraLed(enabled: enabled).values
347
- }
348
-
349
- AsyncFunction("setButtonMaxRecordingTime") { (minutes: Int) in
350
- let sdk = await MainActor.run { self.bluetoothSdk() }
351
- return try await sdk.setButtonMaxRecordingTime(minutes: minutes).values
341
+ return try await sdk.setMaxVideoRecordingDuration(minutes: minutes).values
352
342
  }
353
343
 
354
344
  AsyncFunction("setCameraFov") { (fov: [String: Any]) in
@@ -360,6 +350,11 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
360
350
  return try await sdk.setCameraFov(CameraFov(fov: value, roiPosition: roiPosition)).values
361
351
  }
362
352
 
353
+ AsyncFunction("setCameraTuningConfig") { (anrOn: Bool, gainOn: Bool) in
354
+ let sdk = await MainActor.run { self.bluetoothSdk() }
355
+ return try await sdk.setCameraTuningConfig(anrOn: anrOn, gainOn: gainOn).values
356
+ }
357
+
363
358
  AsyncFunction("queryGalleryStatus") {
364
359
  let sdk = await MainActor.run { self.bluetoothSdk() }
365
360
  return try await sdk.queryGalleryStatus().values
@@ -368,13 +363,41 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
368
363
  AsyncFunction("requestPhoto") { (params: [String: Any]) in
369
364
  let req = PhotoRequest.from(params: params)
370
365
  Bridge.log(
371
- "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=\(req.requestId) appId=\(req.appId) size=\(req.size.rawValue) compress=\(req.compress?.rawValue ?? "none") aeDivisor=\(req.aeExposureDivisor.map { String($0) } ?? "nil")"
366
+ "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=\(req.requestId) size=\(req.size.rawValue) compress=\(req.compress?.rawValue ?? "none") aeDivisor=\(req.aeExposureDivisor.map { String($0) } ?? "nil")"
372
367
  )
373
368
 
374
369
  let sdk = await MainActor.run { self.bluetoothSdk() }
375
370
  return try await sdk.requestPhoto(req).values
376
371
  }
377
372
 
373
+ AsyncFunction("warmUpCamera") { (params: [String: Any]) in
374
+ let requestId = params["requestId"] as? String
375
+ let sizeRaw = params["size"] as? String ?? "medium"
376
+ let size = PhotoSize(normalizedRawValue: sizeRaw)
377
+ let exposureTimeNs: Double?
378
+ switch params["exposureTimeNs"] {
379
+ case let value as Double:
380
+ exposureTimeNs = value.isFinite && value > 0 ? value : nil
381
+ case let value as Int:
382
+ exposureTimeNs = value > 0 ? Double(value) : nil
383
+ case let value as NSNumber:
384
+ let d = value.doubleValue
385
+ exposureTimeNs = d.isFinite && d > 0 ? d : nil
386
+ default:
387
+ exposureTimeNs = nil
388
+ }
389
+ let durationRaw = intValue(params["durationMs"]) ?? 0
390
+ let durationMs = durationRaw > 0 ? durationRaw : 15000
391
+
392
+ let sdk = await MainActor.run { self.bluetoothSdk() }
393
+ return try await sdk.warmUpCamera(
394
+ requestId: requestId,
395
+ size: size,
396
+ exposureTimeNs: exposureTimeNs,
397
+ durationMs: durationMs
398
+ ).values
399
+ }
400
+
378
401
  // MARK: - OTA Commands
379
402
 
380
403
  Function("setOtaVersionUrl") { (otaVersionUrl: String) in
@@ -409,11 +432,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
409
432
  return try await sdk.sendOtaQueryStatus().values
410
433
  }
411
434
 
412
- AsyncFunction("retryOtaVersionCheck") {
413
- let sdk = await MainActor.run { self.bluetoothSdk() }
414
- return try await sdk.retryOtaVersionCheck().values
415
- }
416
-
417
435
  // MARK: - Version Info Commands
418
436
 
419
437
  AsyncFunction("requestVersionInfo") {
@@ -472,6 +490,11 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
472
490
  return try await sdk.startStream(StreamRequest(values: params)).values
473
491
  }
474
492
 
493
+ AsyncFunction("startExternallyManagedStream") { (params: [String: Any]) in
494
+ let sdk = await MainActor.run { self.bluetoothSdk() }
495
+ return try await sdk.startExternallyManagedStream(StreamRequest(values: params)).values
496
+ }
497
+
475
498
  AsyncFunction("stopStream") {
476
499
  let sdk = await MainActor.run { self.bluetoothSdk() }
477
500
  return try await sdk.stopStream().values
@@ -694,6 +717,8 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
694
717
  sendEvent("photo_response", response.values)
695
718
  case let .photoStatus(status):
696
719
  sendEvent("photo_status", status.values)
720
+ case let .cameraStatus(status):
721
+ sendEvent("camera_status", status.values)
697
722
  case let .videoRecordingStatus(status):
698
723
  sendEvent("video_recording_status", status.values)
699
724
  case let .mediaUpload(event):
@@ -704,8 +729,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
704
729
  sendEvent("stream_status", status.values)
705
730
  case let .keepAliveAck(ack):
706
731
  sendEvent("keep_alive_ack", ack.values)
707
- case let .otaUpdateAvailable(event):
708
- sendEvent("ota_update_available", event.values)
709
732
  case let .otaStartAck(event):
710
733
  sendEvent("ota_start_ack", event.values)
711
734
  case let .otaStatus(event):
@@ -748,7 +771,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
748
771
  }
749
772
 
750
773
  @MainActor
751
- public func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail error: BluetoothError) {
774
+ public func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail error: BluetoothSdkError) {
752
775
  sendEvent("pair_failure", ["error": error.message])
753
776
  }
754
777
  }
@@ -347,6 +347,10 @@ class Bridge {
347
347
  Bridge.sendTypedMessage("photo_status", body: status)
348
348
  }
349
349
 
350
+ static func sendCameraStatus(_ status: [String: Any]) {
351
+ Bridge.sendTypedMessage("camera_status", body: status)
352
+ }
353
+
350
354
  static func sendPhotoResponse(_ response: [String: Any]) {
351
355
  Bridge.sendTypedMessage("photo_response", body: response)
352
356
  }
@@ -434,22 +438,6 @@ class Bridge {
434
438
  Bridge.sendTypedMessage("ota_start_ack", body: eventBody)
435
439
  }
436
440
 
437
- /// Send OTA update available notification - glasses have detected an available update (background mode)
438
- static func sendOtaUpdateAvailable(
439
- versionCode: Int64,
440
- versionName: String,
441
- updates: [String],
442
- totalSize: Int64
443
- ) {
444
- let eventBody: [String: Any] = [
445
- "version_code": versionCode,
446
- "version_name": versionName,
447
- "updates": updates,
448
- "total_size": totalSize,
449
- ]
450
- Bridge.sendTypedMessage("ota_update_available", body: eventBody)
451
- }
452
-
453
441
  static func sendOtaStatus(
454
442
  sessionId: String,
455
443
  totalSteps: Int,
@@ -22,11 +22,14 @@ struct ViewState {
22
22
  var text: String
23
23
  var data: String?
24
24
  var animationData: [String: Any]?
25
- // Optional bitmap_view container position/size (used by G2; ignored by others)
26
- var bmpX: Int32?
27
- var bmpY: Int32?
28
- var bmpWidth: Int32?
29
- var bmpHeight: Int32?
25
+ // Optional container position/size used by bitmap_view and positioned_text (G2; ignored by others)
26
+ var bmpX: Int32? = nil
27
+ var bmpY: Int32? = nil
28
+ var bmpWidth: Int32? = nil
29
+ var bmpHeight: Int32? = nil
30
+ // Optional positioned_text border (used by G2; ignored by others)
31
+ var borderWidth: Int32? = nil
32
+ var borderRadius: Int32? = nil
30
33
  }
31
34
 
32
35
  @MainActor
@@ -620,6 +623,8 @@ struct ViewState {
620
623
  sgc = G2()
621
624
  } else if wearable.contains(DeviceTypes.LIVE) {
622
625
  sgc = MentraLive()
626
+ } else if wearable.contains(DeviceTypes.NIMO) {
627
+ sgc = Nimo()
623
628
  } else if wearable.contains(DeviceTypes.FRAME) {
624
629
  // sgc = FrameManager()
625
630
  }
@@ -714,6 +719,19 @@ struct ViewState {
714
719
  width: currentViewState.bmpWidth,
715
720
  height: currentViewState.bmpHeight
716
721
  )
722
+ case "positioned_text":
723
+ Bridge.log(
724
+ "MAN: positioned_text → text='\(currentViewState.text)' rect=\(currentViewState.bmpX ?? 0),\(currentViewState.bmpY ?? 0) \(currentViewState.bmpWidth ?? 576)x\(currentViewState.bmpHeight ?? 288)"
725
+ )
726
+ await sgc?.sendPositionedText(
727
+ currentViewState.text,
728
+ x: currentViewState.bmpX ?? 0,
729
+ y: currentViewState.bmpY ?? 0,
730
+ width: currentViewState.bmpWidth ?? 576,
731
+ height: currentViewState.bmpHeight ?? 288,
732
+ borderWidth: currentViewState.borderWidth ?? 0,
733
+ borderRadius: currentViewState.borderRadius ?? 0
734
+ )
717
735
  case "clear_view":
718
736
  sgc?.clearDisplay()
719
737
  default:
@@ -1039,6 +1057,8 @@ struct ViewState {
1039
1057
  let bmpY = (layout["y"] as? NSNumber).map { $0.int32Value }
1040
1058
  let bmpWidth = (layout["width"] as? NSNumber).map { $0.int32Value }
1041
1059
  let bmpHeight = (layout["height"] as? NSNumber).map { $0.int32Value }
1060
+ let borderWidth = (layout["borderWidth"] as? NSNumber).map { $0.int32Value }
1061
+ let borderRadius = (layout["borderRadius"] as? NSNumber).map { $0.int32Value }
1042
1062
 
1043
1063
  text = parsePlaceholders(text)
1044
1064
  topText = parsePlaceholders(topText)
@@ -1048,7 +1068,8 @@ struct ViewState {
1048
1068
  var newViewState = ViewState(
1049
1069
  topText: topText, bottomText: bottomText, title: title, layoutType: layoutType,
1050
1070
  text: text, data: data, animationData: nil,
1051
- bmpX: bmpX, bmpY: bmpY, bmpWidth: bmpWidth, bmpHeight: bmpHeight
1071
+ bmpX: bmpX, bmpY: bmpY, bmpWidth: bmpWidth, bmpHeight: bmpHeight,
1072
+ borderWidth: borderWidth, borderRadius: borderRadius
1052
1073
  )
1053
1074
 
1054
1075
  if layoutType == "bitmap_animation" {
@@ -1069,6 +1090,27 @@ struct ViewState {
1069
1090
  }
1070
1091
  }
1071
1092
 
1093
+ // positioned_text is a sticky overlay container (e.g. the nav trip-stats
1094
+ // label) that lives ALONGSIDE the main view. It must NOT flow through the
1095
+ // single replaceable viewState[stateIndex] — otherwise the constantly-
1096
+ // refreshing main text_wall (maneuver text) and the minimap bitmap_view
1097
+ // overwrite it every frame and it never renders. Route it straight to the
1098
+ // SGC, which keeps its own persistent text container for that rect.
1099
+ if layoutType == "positioned_text" {
1100
+ Bridge.log(
1101
+ "MAN: positioned_text (sticky) → text='\(text)' rect=\(bmpX ?? 0),\(bmpY ?? 0) \(bmpWidth ?? 576)x\(bmpHeight ?? 288)"
1102
+ )
1103
+ Task { [weak self] in
1104
+ await self?.sgc?.sendPositionedText(
1105
+ text,
1106
+ x: bmpX ?? 0, y: bmpY ?? 0,
1107
+ width: bmpWidth ?? 576, height: bmpHeight ?? 288,
1108
+ borderWidth: borderWidth ?? 0, borderRadius: borderRadius ?? 0
1109
+ )
1110
+ }
1111
+ return
1112
+ }
1113
+
1072
1114
  let cS = viewStates[stateIndex]
1073
1115
  let nS = newViewState
1074
1116
  let currentState =
@@ -1115,7 +1157,6 @@ struct ViewState {
1115
1157
 
1116
1158
  func startStream(_ message: [String: Any]) {
1117
1159
  var message = message
1118
- message["flash"] = true
1119
1160
  Bridge.log("MAN: startStream: \(message)")
1120
1161
  sgc?.startStream(message)
1121
1162
  }
@@ -1181,7 +1222,7 @@ struct ViewState {
1181
1222
 
1182
1223
  private func liveSgc() throws -> MentraLive {
1183
1224
  guard let live = sgc as? MentraLive else {
1184
- throw BluetoothError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1225
+ throw BluetoothSdkError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1185
1226
  }
1186
1227
  return live
1187
1228
  }
@@ -1193,11 +1234,11 @@ struct ViewState {
1193
1234
  func sendButtonPhotoSettings(requestId: String, size: String) throws {
1194
1235
  try sendButtonPhotoSettings(
1195
1236
  requestId: requestId,
1196
- settings: ButtonPhotoSettings(size: ButtonPhotoSize(normalizedRawValue: size))
1237
+ settings: PhotoCaptureDefaults(size: PhotoSize(normalizedRawValue: size))
1197
1238
  )
1198
1239
  }
1199
1240
 
1200
- func sendButtonPhotoSettings(requestId: String, settings: ButtonPhotoSettings) throws {
1241
+ func sendButtonPhotoSettings(requestId: String, settings: PhotoCaptureDefaults) throws {
1201
1242
  try liveSgc().sendButtonPhotoSettings(requestId: requestId, settings: settings)
1202
1243
  }
1203
1244
 
@@ -1205,10 +1246,6 @@ struct ViewState {
1205
1246
  try liveSgc().sendButtonVideoRecordingSettings(requestId: requestId, width: width, height: height, fps: fps)
1206
1247
  }
1207
1248
 
1208
- func sendButtonCameraLedSetting(requestId: String, enabled: Bool) throws {
1209
- try liveSgc().sendButtonCameraLedSetting(requestId: requestId, enabled: enabled)
1210
- }
1211
-
1212
1249
  func sendButtonMaxRecordingTime(requestId: String, minutes: Int) throws {
1213
1250
  try liveSgc().sendButtonMaxRecordingTime(requestId: requestId, minutes: minutes)
1214
1251
  }
@@ -1217,9 +1254,28 @@ struct ViewState {
1217
1254
  try liveSgc().sendCameraFovSetting(requestId: requestId, fov: fov, roiPosition: roiPosition)
1218
1255
  }
1219
1256
 
1220
- func retryOtaVersionCheck() {
1221
- Bridge.log("MAN: Retrying glasses OTA version check after clock sync")
1222
- (sgc as? MentraLive)?.sendOtaRetryVersionCheck()
1257
+ func sendCameraTuningConfig(requestId: String, anrOn: Bool, gainOn: Bool) throws {
1258
+ try liveSgc().sendCameraTuningConfig(requestId: requestId, anrOn: anrOn, gainOn: gainOn)
1259
+ }
1260
+
1261
+ func warmUpCamera(
1262
+ requestId: String,
1263
+ size: PhotoSize,
1264
+ exposureTimeNs: Double?,
1265
+ durationMs: Int
1266
+ ) throws {
1267
+ guard let live = sgc as? MentraLive else {
1268
+ // Fail fast like other camera commands so the SDK promise rejects immediately instead
1269
+ // of hanging until the request timeout with no camera_status.
1270
+ throw BluetoothSdkError(
1271
+ code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1272
+ }
1273
+ live.warmUpCamera(
1274
+ requestId: requestId,
1275
+ size: size,
1276
+ exposureTimeNs: exposureTimeNs,
1277
+ durationMs: durationMs
1278
+ )
1223
1279
  }
1224
1280
 
1225
1281
  /// Request version info from glasses.
@@ -1248,10 +1304,10 @@ struct ViewState {
1248
1304
  _ fps: Int = 0, _ maxRecordingTimeMinutes: Int = 0
1249
1305
  ) {
1250
1306
  Bridge.log(
1251
- "MAN: onStartVideoRecording: requestId=\(requestId), save=\(save), flash=true, sound=\(sound), resolution=\(width)x\(height)@\(fps)fps, maxRecordingTimeMinutes=\(maxRecordingTimeMinutes)"
1307
+ "MAN: onStartVideoRecording: requestId=\(requestId), save=\(save), sound=\(sound), resolution=\(width)x\(height)@\(fps)fps, maxRecordingTimeMinutes=\(maxRecordingTimeMinutes)"
1252
1308
  )
1253
1309
  sgc?.startVideoRecording(
1254
- requestId: requestId, save: save, flash: true, sound: sound, width: width, height: height,
1310
+ requestId: requestId, save: save, sound: sound, width: width, height: height,
1255
1311
  fps: fps, maxRecordingTimeMinutes: maxRecordingTimeMinutes
1256
1312
  )
1257
1313
  }
@@ -1329,12 +1385,10 @@ struct ViewState {
1329
1385
  let manualIso = manualExposureNs != nil ? request.iso.flatMap { $0 > 0 ? $0 : nil } : nil
1330
1386
  let routed = PhotoRequest(
1331
1387
  requestId: request.requestId,
1332
- appId: request.appId,
1333
1388
  size: request.size,
1334
1389
  webhookUrl: request.webhookUrl,
1335
1390
  authToken: request.authToken,
1336
1391
  compress: request.compress,
1337
- flash: request.flash,
1338
1392
  save: request.save,
1339
1393
  sound: request.sound,
1340
1394
  exposureTimeNs: manualExposureNs,
@@ -1349,7 +1403,7 @@ struct ViewState {
1349
1403
  ispAnalogGain: request.ispAnalogGain
1350
1404
  )
1351
1405
  Bridge.log(
1352
- "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) appId=\(routed.appId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") flash=\(routed.flash) save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1406
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1353
1407
  )
1354
1408
  guard let sgc else {
1355
1409
  Bridge.log(
@@ -92,7 +92,6 @@ class DeviceStore {
92
92
  store.set("bluetooth", "nex_chinese_captions", false)
93
93
  store.set("bluetooth", "screen_disabled", false)
94
94
  store.set("bluetooth", "button_photo_size", "max")
95
- store.set("bluetooth", "button_camera_led", true)
96
95
  store.set("bluetooth", "button_max_recording_time", 10)
97
96
  store.set("bluetooth", "camera_fov", ["fov": 118, "roi_position": 0])
98
97
  store.set("bluetooth", "button_video_width", 1280)
@@ -258,9 +257,6 @@ class DeviceStore {
258
257
  case ("bluetooth", "button_photo_size"):
259
258
  DeviceManager.shared.sgc?.sendButtonPhotoSettings()
260
259
 
261
- case ("bluetooth", "button_camera_led"):
262
- DeviceManager.shared.sgc?.sendButtonCameraLedSetting()
263
-
264
260
  case ("bluetooth", "button_max_recording_time"):
265
261
  DeviceManager.shared.sgc?.sendButtonMaxRecordingTime()
266
262