@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
package/README.md CHANGED
@@ -315,7 +315,7 @@ console.log(ledAck.state)
315
315
  Settings commands that return `SettingsAckSuccessEvent` reject when the ASG reports an error ack. The SDK updates its local settings store only after that ASG ack resolves successfully, so observed SDK state reflects the acknowledged glasses state rather than a queued request. Raw `settings_ack` listener events still use `SettingsAckEvent` because they can include both success and failure statuses. `rgbLedControl(...)` resolves from a successful ASG `rgb_led_control_response` and rejects when the ASG reports `state: "error"`; raw `settings_ack` and `rgb_led_control_response` events remain available through listeners.
316
316
 
317
317
  WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
318
- `requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` resolves when that SSID is no longer connected, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` resolves from the ASG `version_info` response instead of local store changes.
318
+ `requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` resolves when that SSID is no longer connected, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` resolves from the ASG `version_info` response instead of local store changes.
319
319
 
320
320
  React Native narrows returned values to success shapes where the raw listener event can also report errors:
321
321
 
@@ -327,7 +327,7 @@ React Native narrows returned values to success shapes where the raw listener ev
327
327
  | `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
328
328
  | `checkForOtaUpdate()` | `boolean`, true when the configured OTA manifest has an ASG APK, MTK, or BES update for the connected glasses; false only when the manifest was checked successfully and no update is available. | Rejects when the glasses are disconnected, version info is unavailable, the manifest cannot be fetched, or the manifest response is invalid/missing required ASG app version fields. |
329
329
 
330
- Android and iOS async APIs use `BluetoothException` / `BluetoothError` for the same error paths. Their returned event structs are the successful response in normal `try`/`await` code, while raw listener/delegate events still include both success and error payloads.
330
+ Android and iOS async APIs use `BluetoothSdkException` / `BluetoothSdkError` for the same error paths. Their returned event structs are the successful response in normal `try`/`await` code, while raw listener/delegate events still include both success and error payloads.
331
331
 
332
332
  `setMicState(true)` defaults to continuous microphone PCM from the glasses. The SDK does not apply phone-side Voice Activity Detection gating to microphone audio events. Glasses-side Voice Activity Detection is disabled by default for public SDK consumers; use `setVoiceActivityDetectionEnabled(true)` when you want supported glasses to gate microphone audio and emit live speaking status. `voice_activity_detection_status` reports whether glasses-side Voice Activity Detection is enabled, and `speaking_status` reports speaking/not-speaking when supported. Microphone events include the latest `voiceActivityDetectionEnabled` value.
333
333
 
@@ -369,8 +369,6 @@ OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and
369
369
 
370
370
  ```ts
371
371
  const photo = await BluetoothSdk.requestPhoto({
372
- requestId: `photo-${Date.now()}`,
373
- appId: 'com.example.app',
374
372
  size: 'medium',
375
373
  webhookUrl: 'https://api.example.com/mentra/photo',
376
374
  authToken: 'optional-token',
@@ -382,7 +380,7 @@ const photo = await BluetoothSdk.requestPhoto({
382
380
  console.log('photo delivered', photo.photoUrl ?? photo.uploadUrl, photo.fileSizeBytes)
383
381
  ```
384
382
 
385
- `requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives before the command timeout. Use `photo_status` for intermediate stages such as `accepted`, `configuring`, `capturing`, `captured`, `uploading`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring`; `photo_status` is progress, while `photo_response` is terminal success/error. The raw `photo_response` event stream still includes both success and error events for subscribers. The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
383
+ `requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. If you omit `requestId`, the SDK generates one and the terminal response includes it. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives before the command timeout. Use `photo_status` for intermediate stages such as `accepted`, `configuring`, `capturing`, `captured`, `uploading`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring`; `photo_status` is progress, while `photo_response` is terminal success/error. The raw `photo_response` event stream still includes both success and error events for subscribers. The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
386
384
 
387
385
  For one-shot manual capture tuning, pass `exposureTimeNs` and `iso` together. `exposureTimeNs` is sensor exposure time in nanoseconds; `iso` is sensor ISO. If `exposureTimeNs` is omitted, `null`, invalid, or unsupported by the connected glasses, the camera uses auto exposure and ignores `iso`.
388
386
 
@@ -405,8 +403,8 @@ await BluetoothSdk.startStream({
405
403
  await BluetoothSdk.stopStream()
406
404
  ```
407
405
 
408
- Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. `startStream()` resolves with the correlated `stream_status` event once the glasses report `status: "streaming"`; `stopStream()` resolves when the glasses report `status: "stopped"` or confirms the stream was already stopped / not streaming. `stopStream()` returns a normalized stopped event for that already-stopped case. Stream starts reject if the glasses report an error before streaming; stream stops reject for real stop errors, send failure, another stop in flight, or timeout. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
409
- `stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight.
406
+ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. `startStream()` resolves with the correlated `stream_status` event once the glasses report `status: "streaming"`; `stopStream()` resolves when the glasses report `status: "stopped"` or confirms the stream was already stopped / not streaming. `stopStream()` returns a normalized stopped event for that already-stopped case. Stream starts reject if the glasses report an error before streaming; stream stops reject for real stop errors, send failure, another stop in flight, or timeout. Stream video input fields are `width`, `height`, `bitrate`, and `fps`. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
407
+ `stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight. The resolved effective frame rate is reported as `resolvedConfig.video.fps`.
410
408
 
411
409
  ## Events
412
410
 
@@ -459,13 +457,25 @@ MENTRA_BLUETOOTH_SDK_PACKAGE_PATH=/path/to/MentraOS/mobile/modules/bluetooth-sdk
459
457
 
460
458
  Use `bunx expo run:android` for Android. Keep local paths in your shell or CI environment, not in committed app config.
461
459
 
460
+ For local Android source compile checks inside this monorepo, run from the
461
+ MentraOS repo root:
462
+
463
+ ```sh
464
+ ./scripts/check-android-compile.sh bluetooth-sdk
465
+ ```
466
+
467
+ The `android/` folder in this package is source for the generated Expo Android
468
+ project, not the local Gradle entrypoint. The check script prepares
469
+ `mobile/android` and uses its Gradle wrapper with `-PmentraPublicSdk=true`,
470
+ matching the CI release workflow's public SDK dependency mode.
471
+
462
472
  For bare native iOS apps, use the public SwiftPM repository:
463
473
 
464
474
  ```text
465
475
  https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
466
476
  ```
467
477
 
468
- Select version `0.1.13`, then add the `MentraBluetoothSDK` product to your app target.
478
+ Select version `0.1.16`, then add the `MentraBluetoothSDK` product to your app target.
469
479
 
470
480
  For local SDK development, add this package folder directly in Xcode:
471
481
 
@@ -116,6 +116,10 @@ class BluetoothSdkModule : Module() {
116
116
  sendEvent("photo_status", event.values)
117
117
  }
118
118
 
119
+ override fun onCameraStatus(event: CameraStatusEvent) {
120
+ sendEvent("camera_status", event.values)
121
+ }
122
+
119
123
  override fun onVideoRecordingStatus(event: VideoRecordingStatusEvent) {
120
124
  sendEvent("video_recording_status", event.values)
121
125
  }
@@ -136,10 +140,6 @@ class BluetoothSdkModule : Module() {
136
140
  sendEvent("keep_alive_ack", event.values)
137
141
  }
138
142
 
139
- override fun onOtaUpdateAvailable(event: OtaUpdateAvailableEvent) {
140
- sendEvent("ota_update_available", event.values)
141
- }
142
-
143
143
  override fun onOtaStartAck(event: OtaStartAckEvent) {
144
144
  sendEvent("ota_start_ack", event.values)
145
145
  }
@@ -183,7 +183,7 @@ class BluetoothSdkModule : Module() {
183
183
 
184
184
  private fun requireSdk(): MentraBluetoothSdk =
185
185
  sdk
186
- ?: throw BluetoothException(
186
+ ?: throw BluetoothSdkException(
187
187
  "sdk_not_initialized",
188
188
  "Bluetooth SDK is not initialized.",
189
189
  )
@@ -216,6 +216,7 @@ class BluetoothSdkModule : Module() {
216
216
  "hotspot_error",
217
217
  "photo_response",
218
218
  "photo_status",
219
+ "camera_status",
219
220
  "video_recording_status",
220
221
  "media_success",
221
222
  "media_error",
@@ -244,7 +245,6 @@ class BluetoothSdkModule : Module() {
244
245
  "stream_status",
245
246
  "keep_alive_ack",
246
247
  "mtk_update_complete",
247
- "ota_update_available",
248
248
  "ota_progress",
249
249
  "ota_start_ack",
250
250
  "ota_status",
@@ -441,49 +441,16 @@ class BluetoothSdkModule : Module() {
441
441
  sdk?.setVoiceActivityDetectionEnabled(enabled)
442
442
  }
443
443
 
444
- AsyncFunction("setButtonPhotoCaptureSettings") { params: Map<String, Any?> ->
445
- val size = (params["size"] as? String)?.let { ButtonPhotoSize.fromValue(it) }
446
- val mfnr = params["mfnr"] as? Boolean
447
- val zsl = params["zsl"] as? Boolean
448
- val noiseReduction = params["noiseReduction"] as? Boolean
449
- val edgeEnhancement = params["edgeEnhancement"] as? Boolean
450
- val ispDigitalGain = (params["ispDigitalGain"] as? Number)?.toInt()
451
- val ispAnalogGain = params["ispAnalogGain"] as? String
452
- val aeExposureDivisor = (params["aeExposureDivisor"] as? Number)?.toInt()
453
- val isoCap = (params["isoCap"] as? Number)?.toInt()
454
- val compress = params["compress"] as? String
455
- val sound = params["sound"] as? Boolean
456
- val resetCaptureTuning = params["resetCaptureTuning"] as? Boolean == true
457
- requireSdk()
458
- .setButtonPhotoSettings(
459
- ButtonPhotoSettings(
460
- size = size,
461
- mfnr = mfnr,
462
- zsl = zsl,
463
- noiseReduction = noiseReduction,
464
- edgeEnhancement = edgeEnhancement,
465
- ispDigitalGain = ispDigitalGain,
466
- ispAnalogGain = ispAnalogGain,
467
- aeExposureDivisor = aeExposureDivisor,
468
- isoCap = isoCap,
469
- compress = compress,
470
- sound = sound,
471
- resetCaptureTuning = resetCaptureTuning,
472
- ),
473
- )
474
- .values
475
- }
476
-
477
- AsyncFunction("setButtonVideoRecordingSettings") { width: Int, height: Int, fps: Int ->
478
- requireSdk().setButtonVideoRecordingSettings(width, height, fps).values
444
+ AsyncFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
445
+ requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
479
446
  }
480
447
 
481
- AsyncFunction("setButtonCameraLed") { enabled: Boolean ->
482
- requireSdk().setButtonCameraLed(enabled).values
448
+ AsyncFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
449
+ requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
483
450
  }
484
451
 
485
- AsyncFunction("setButtonMaxRecordingTime") { minutes: Int ->
486
- requireSdk().setButtonMaxRecordingTime(minutes).values
452
+ AsyncFunction("setMaxVideoRecordingDuration") { minutes: Int ->
453
+ requireSdk().setMaxVideoRecordingDuration(minutes).values
487
454
  }
488
455
 
489
456
  AsyncFunction("setCameraFov") { fov: Map<String, Any> ->
@@ -495,6 +462,10 @@ class BluetoothSdkModule : Module() {
495
462
  requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
496
463
  }
497
464
 
465
+ AsyncFunction("setCameraTuningConfig") { anrOn: Boolean, gainOn: Boolean ->
466
+ requireSdk().setCameraTuningConfig(anrOn, gainOn).values
467
+ }
468
+
498
469
  AsyncFunction("queryGalleryStatus") { requireSdk().queryGalleryStatus().values }
499
470
 
500
471
  AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
@@ -505,11 +476,22 @@ class BluetoothSdkModule : Module() {
505
476
  }.toMap()
506
477
  val req = PhotoRequest.fromMap(sanitized)
507
478
  Bridge.log(
508
- "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress} flash=${req.flash} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
479
+ "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} size=${req.size} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
509
480
  )
510
481
  requireSdk().requestPhoto(req).values
511
482
  }
512
483
 
484
+ AsyncFunction("warmUpCamera") { params: Map<String, Any?> ->
485
+ val requestId = params["requestId"] as? String
486
+ val size = PhotoSize.fromValue(params["size"] as? String)
487
+ val exposureRaw = (params["exposureTimeNs"] as? Number)?.toDouble()
488
+ val exposureTimeNs =
489
+ if (exposureRaw != null && exposureRaw.isFinite() && exposureRaw > 0) exposureRaw.toLong() else null
490
+ val durationRaw = (params["durationMs"] as? Number)?.toInt() ?: 0
491
+ val durationMs = if (durationRaw > 0) durationRaw else 15000
492
+ requireSdk().warmUpCamera(requestId, size, exposureTimeNs, durationMs).values
493
+ }
494
+
513
495
  // MARK: - OTA Commands
514
496
 
515
497
  Function("setOtaVersionUrl") { otaVersionUrl: String ->
@@ -535,8 +517,6 @@ class BluetoothSdkModule : Module() {
535
517
 
536
518
  AsyncFunction("sendOtaQueryStatus") { requireSdk().sendOtaQueryStatus().values }
537
519
 
538
- AsyncFunction("retryOtaVersionCheck") { requireSdk().retryOtaVersionCheck().values }
539
-
540
520
  // MARK: - Version Info Commands
541
521
 
542
522
  AsyncFunction("requestVersionInfo") { requireSdk().requestVersionInfo().toMap() }
@@ -586,6 +566,10 @@ class BluetoothSdkModule : Module() {
586
566
  requireSdk().startStream(StreamRequest.fromMap(params)).values
587
567
  }
588
568
 
569
+ AsyncFunction("startExternallyManagedStream") { params: Map<String, Any> ->
570
+ requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
571
+ }
572
+
589
573
  AsyncFunction("stopStream") { requireSdk().stopStream().values }
590
574
 
591
575
  AsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
@@ -775,6 +759,22 @@ private fun Map<String, Any>?.toMentraDevice(): Device? {
775
759
  )
776
760
  }
777
761
 
762
+ private fun Map<String, Any?>.toPhotoCaptureDefaults(): PhotoCaptureDefaults =
763
+ PhotoCaptureDefaults(
764
+ size = (this["size"] as? String)?.let { PhotoSize.fromValue(it) },
765
+ mfnr = this["mfnr"] as? Boolean,
766
+ zsl = this["zsl"] as? Boolean,
767
+ noiseReduction = this["noiseReduction"] as? Boolean,
768
+ edgeEnhancement = this["edgeEnhancement"] as? Boolean,
769
+ ispDigitalGain = (this["ispDigitalGain"] as? Number)?.toInt(),
770
+ ispAnalogGain = this["ispAnalogGain"] as? String,
771
+ aeExposureDivisor = (this["aeExposureDivisor"] as? Number)?.toInt(),
772
+ isoCap = (this["isoCap"] as? Number)?.toInt(),
773
+ compress = this["compress"] as? String,
774
+ sound = this["sound"] as? Boolean,
775
+ resetCaptureTuning = this["resetCaptureTuning"] as? Boolean == true,
776
+ )
777
+
778
778
  private fun Map<String, Any>?.toMentraConnectOptions(): ConnectOptions {
779
779
  val values = this ?: return ConnectOptions()
780
780
  return ConnectOptions(
@@ -374,6 +374,11 @@ public class Bridge private constructor() {
374
374
  sendTypedMessage("photo_status", statusJson)
375
375
  }
376
376
 
377
+ @JvmStatic
378
+ fun sendCameraStatus(statusJson: Map<String, Any>) {
379
+ sendTypedMessage("camera_status", statusJson)
380
+ }
381
+
377
382
  @JvmStatic
378
383
  fun sendPhotoResponse(responseJson: Map<String, Any>) {
379
384
  sendTypedMessage("photo_response", responseJson)
@@ -587,26 +592,6 @@ public class Bridge private constructor() {
587
592
  sendTypedMessage("mtk_update_complete", eventBody as Map<String, Any>)
588
593
  }
589
594
 
590
- /**
591
- * Send OTA update available notification - glasses have detected an available update
592
- * (background mode)
593
- */
594
- @JvmStatic
595
- fun sendOtaUpdateAvailable(
596
- versionCode: Long,
597
- versionName: String,
598
- updates: List<String>,
599
- totalSize: Long
600
- ) {
601
- val eventBody = HashMap<String, Any>()
602
- eventBody["version_code"] = versionCode
603
- eventBody["version_name"] = versionName
604
- eventBody["updates"] = updates
605
- eventBody["total_size"] = totalSize
606
-
607
- sendTypedMessage("ota_update_available", eventBody as Map<String, Any>)
608
- }
609
-
610
595
  /** Send ota_start_ack — glasses confirmed receipt of ota_start command */
611
596
  @JvmStatic
612
597
  fun sendOtaStartAck() {