@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.
- package/README.md +30 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +47 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +75 -29
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +208 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +80 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +4 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3831 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -28
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +514 -561
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8674 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +5 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +9 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
- package/build/BluetoothSdk.types.d.ts +48 -17
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -9
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -6
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +46 -2
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8 -8
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +44 -68
- package/ios/Source/BluetoothSdkDefaults.swift +23 -3
- package/ios/Source/DeviceManager.swift +38 -33
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +217 -50
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +219 -13
- package/ios/Source/controllers/ControllerManager.swift +3 -7
- package/ios/Source/controllers/R1.swift +3 -7
- package/ios/Source/sgcs/Frame.swift +0 -2
- package/ios/Source/sgcs/G1.swift +7 -8
- package/ios/Source/sgcs/G2.swift +21 -20
- package/ios/Source/sgcs/Mach1.swift +7 -8
- package/ios/Source/sgcs/MentraLive.swift +132 -63
- package/ios/Source/sgcs/MentraNex.swift +7 -8
- package/ios/Source/sgcs/SGCManager.swift +7 -10
- package/ios/Source/sgcs/Simulated.swift +8 -8
- package/ios/Source/status/DeviceStatus.swift +1 -9
- package/ios/Source/streaming/StreamModels.swift +7 -32
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +49 -17
- package/src/_private/BluetoothSdkModule.ts +9 -17
- package/src/_private/photoRequestPayload.ts +48 -3
- package/src/debug.ts +9 -0
- package/src/index.ts +19 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7434
package/README.md
CHANGED
|
@@ -325,7 +325,7 @@ React Native narrows returned values to success shapes where the raw listener ev
|
|
|
325
325
|
| `startVideoRecording(...)` | `VideoRecordingStartedStatusEvent` with `success: true` and `status: "recording_started"`. | Rejects on `success: false` statuses such as `already_recording`, send failure, or timeout. |
|
|
326
326
|
| `stopVideoRecording(...)` | `VideoRecordingStoppedStatusEvent` with `success: true` and `status: "recording_stopped"`. When a webhook URL is supplied, resolves after the video upload succeeds. | Rejects on `success: false` statuses such as `not_recording`, webhook upload failure, send failure, or timeout. |
|
|
327
327
|
| `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
|
|
328
|
-
| `checkForOtaUpdate()`
|
|
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
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.
|
|
331
331
|
|
|
@@ -335,9 +335,16 @@ Android and iOS async APIs use `BluetoothException` / `BluetoothError` for the s
|
|
|
335
335
|
|
|
336
336
|
Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app commands and events:
|
|
337
337
|
|
|
338
|
-
- `checkForOtaUpdate()`
|
|
339
|
-
- `startOtaUpdate()` sends `ota_start` and resolves with the ASG start ack after your app presents the update and the user accepts it.
|
|
340
|
-
|
|
338
|
+
- `checkForOtaUpdate()` fetches the configured manifest and resolves with `true` when an ASG APK, MTK, or BES update is available.
|
|
339
|
+
- `startOtaUpdate()` sends `ota_start` with the same configured manifest URL and resolves with the ASG start ack after your app presents the update and the user accepts it.
|
|
340
|
+
|
|
341
|
+
The default manifest is derived from the SDK version:
|
|
342
|
+
`https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota/bluetooth-sdk-<sdkVersion>-version.json`.
|
|
343
|
+
Each published SDK version points at a durable ASG client APK and firmware
|
|
344
|
+
manifest that were built for that SDK release. Pre-wall-clock ASG builds that
|
|
345
|
+
ignore `ota_start.ota_version_url` are checked against the URL they advertise,
|
|
346
|
+
or the production default if they do not advertise one, so the app does not
|
|
347
|
+
prompt for an update the glasses cannot install.
|
|
341
348
|
|
|
342
349
|
```ts
|
|
343
350
|
import BluetoothSdk from '@mentra/bluetooth-sdk'
|
|
@@ -346,9 +353,9 @@ BluetoothSdk.addListener('ota_status', (event) => {
|
|
|
346
353
|
console.log(`OTA ${event.status}: ${event.overall_percent}%`)
|
|
347
354
|
})
|
|
348
355
|
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
351
|
-
const userAccepted = await promptUserToInstallUpdate(
|
|
356
|
+
const hasUpdate = await BluetoothSdk.checkForOtaUpdate()
|
|
357
|
+
if (hasUpdate) {
|
|
358
|
+
const userAccepted = await promptUserToInstallUpdate() // your app's UI
|
|
352
359
|
if (userAccepted) {
|
|
353
360
|
const startAck = await BluetoothSdk.startOtaUpdate()
|
|
354
361
|
console.log('OTA start acknowledged', startAck.timestamp)
|
|
@@ -398,8 +405,8 @@ await BluetoothSdk.startStream({
|
|
|
398
405
|
await BluetoothSdk.stopStream()
|
|
399
406
|
```
|
|
400
407
|
|
|
401
|
-
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.
|
|
402
|
-
`stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight.
|
|
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. 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.
|
|
409
|
+
`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`.
|
|
403
410
|
|
|
404
411
|
## Events
|
|
405
412
|
|
|
@@ -425,7 +432,7 @@ export function HardwareEventLogger() {
|
|
|
425
432
|
|
|
426
433
|
For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscription API. Keep the returned subscription and call `remove()` when the listener is no longer needed.
|
|
427
434
|
|
|
428
|
-
Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `
|
|
435
|
+
Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `ota_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
|
|
429
436
|
|
|
430
437
|
React Native event payload fields usually use camelCase. OTA events intentionally mirror the glasses firmware field names, such as `overall_percent` and `version_name`. For example, `touch_event` includes `gestureName`, `wifi_scan_result` includes `networks` and `scanComplete`, `version_info` matches the `requestVersionInfo()` result shape, `photo_response` success includes `uploadUrl` and may include webhook-returned `photoUrl`, `statusUrl`, `contentType`, and `fileSizeBytes`, and `gallery_status` includes `hasContent`, `cameraBusy`, and optional `cameraBusyReason`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `ble_fallback_compression`, `uploading`, `ready_for_transfer`, `transferring`, and `failed`; the `configuring` event includes `resolvedConfig` with the effective JPEG dimensions, quality, requested size, transfer method, compression, and manual exposure fields when present. The `capturing` event may include `requestedCaptureConfig` and `meteredPreview`; the `captured` event may include `captureMetadata` with the HAL-applied exposure, ISO, frame duration, and AE state. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
|
|
431
438
|
|
|
@@ -452,13 +459,25 @@ MENTRA_BLUETOOTH_SDK_PACKAGE_PATH=/path/to/MentraOS/mobile/modules/bluetooth-sdk
|
|
|
452
459
|
|
|
453
460
|
Use `bunx expo run:android` for Android. Keep local paths in your shell or CI environment, not in committed app config.
|
|
454
461
|
|
|
462
|
+
For local Android source compile checks inside this monorepo, run from the
|
|
463
|
+
MentraOS repo root:
|
|
464
|
+
|
|
465
|
+
```sh
|
|
466
|
+
./scripts/check-android-compile.sh bluetooth-sdk
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The `android/` folder in this package is source for the generated Expo Android
|
|
470
|
+
project, not the local Gradle entrypoint. The check script prepares
|
|
471
|
+
`mobile/android` and uses its Gradle wrapper with `-PmentraPublicSdk=true`,
|
|
472
|
+
matching the CI release workflow's public SDK dependency mode.
|
|
473
|
+
|
|
455
474
|
For bare native iOS apps, use the public SwiftPM repository:
|
|
456
475
|
|
|
457
476
|
```text
|
|
458
477
|
https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
|
|
459
478
|
```
|
|
460
479
|
|
|
461
|
-
|
|
480
|
+
Select version `0.1.14`, then add the `MentraBluetoothSDK` product to your app target.
|
|
462
481
|
|
|
463
482
|
For local SDK development, add this package folder directly in Xcode:
|
|
464
483
|
|
|
@@ -441,20 +441,16 @@ class BluetoothSdkModule : Module() {
|
|
|
441
441
|
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
AsyncFunction("
|
|
445
|
-
requireSdk().
|
|
444
|
+
AsyncFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
445
|
+
requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
AsyncFunction("
|
|
449
|
-
requireSdk().
|
|
448
|
+
AsyncFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
|
|
449
|
+
requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
AsyncFunction("
|
|
453
|
-
requireSdk().
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
AsyncFunction("setButtonMaxRecordingTime") { minutes: Int ->
|
|
457
|
-
requireSdk().setButtonMaxRecordingTime(minutes).values
|
|
452
|
+
AsyncFunction("setMaxVideoRecordingDuration") { minutes: Int ->
|
|
453
|
+
requireSdk().setMaxVideoRecordingDuration(minutes).values
|
|
458
454
|
}
|
|
459
455
|
|
|
460
456
|
AsyncFunction("setCameraFov") { fov: Map<String, Any> ->
|
|
@@ -476,14 +472,33 @@ class BluetoothSdkModule : Module() {
|
|
|
476
472
|
}.toMap()
|
|
477
473
|
val req = PhotoRequest.fromMap(sanitized)
|
|
478
474
|
Bridge.log(
|
|
479
|
-
"NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress}
|
|
475
|
+
"NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
|
|
480
476
|
)
|
|
481
477
|
requireSdk().requestPhoto(req).values
|
|
482
478
|
}
|
|
483
479
|
|
|
484
480
|
// MARK: - OTA Commands
|
|
485
481
|
|
|
486
|
-
|
|
482
|
+
Function("setOtaVersionUrl") { otaVersionUrl: String ->
|
|
483
|
+
requireSdk().setOtaVersionUrl(otaVersionUrl)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
Function("getOtaVersionUrl") { requireSdk().getOtaVersionUrl() }
|
|
487
|
+
|
|
488
|
+
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
|
|
489
|
+
// manifest fetches and version waits can block for several seconds.
|
|
490
|
+
AsyncFunction("checkForOtaUpdate") Coroutine { ->
|
|
491
|
+
withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
AsyncFunction("startOtaUpdate") { otaVersionUrl: String? ->
|
|
495
|
+
val sdk = requireSdk()
|
|
496
|
+
if (otaVersionUrl.isNullOrBlank()) {
|
|
497
|
+
sdk.startOtaUpdate().values
|
|
498
|
+
} else {
|
|
499
|
+
sdk.startOtaUpdate(otaVersionUrl).values
|
|
500
|
+
}
|
|
501
|
+
}
|
|
487
502
|
|
|
488
503
|
AsyncFunction("sendOtaQueryStatus") { requireSdk().sendOtaQueryStatus().values }
|
|
489
504
|
|
|
@@ -538,6 +553,10 @@ class BluetoothSdkModule : Module() {
|
|
|
538
553
|
requireSdk().startStream(StreamRequest.fromMap(params)).values
|
|
539
554
|
}
|
|
540
555
|
|
|
556
|
+
AsyncFunction("startExternallyManagedStream") { params: Map<String, Any> ->
|
|
557
|
+
requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
|
|
558
|
+
}
|
|
559
|
+
|
|
541
560
|
AsyncFunction("stopStream") { requireSdk().stopStream().values }
|
|
542
561
|
|
|
543
562
|
AsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
|
|
@@ -727,6 +746,22 @@ private fun Map<String, Any>?.toMentraDevice(): Device? {
|
|
|
727
746
|
)
|
|
728
747
|
}
|
|
729
748
|
|
|
749
|
+
private fun Map<String, Any?>.toPhotoCaptureDefaults(): PhotoCaptureDefaults =
|
|
750
|
+
PhotoCaptureDefaults(
|
|
751
|
+
size = (this["size"] as? String)?.let { PhotoSize.fromValue(it) },
|
|
752
|
+
mfnr = this["mfnr"] as? Boolean,
|
|
753
|
+
zsl = this["zsl"] as? Boolean,
|
|
754
|
+
noiseReduction = this["noiseReduction"] as? Boolean,
|
|
755
|
+
edgeEnhancement = this["edgeEnhancement"] as? Boolean,
|
|
756
|
+
ispDigitalGain = (this["ispDigitalGain"] as? Number)?.toInt(),
|
|
757
|
+
ispAnalogGain = this["ispAnalogGain"] as? String,
|
|
758
|
+
aeExposureDivisor = (this["aeExposureDivisor"] as? Number)?.toInt(),
|
|
759
|
+
isoCap = (this["isoCap"] as? Number)?.toInt(),
|
|
760
|
+
compress = this["compress"] as? String,
|
|
761
|
+
sound = this["sound"] as? Boolean,
|
|
762
|
+
resetCaptureTuning = this["resetCaptureTuning"] as? Boolean == true,
|
|
763
|
+
)
|
|
764
|
+
|
|
730
765
|
private fun Map<String, Any>?.toMentraConnectOptions(): ConnectOptions {
|
|
731
766
|
val values = this ?: return ConnectOptions()
|
|
732
767
|
return ConnectOptions(
|
|
@@ -247,6 +247,7 @@ class DeviceManager {
|
|
|
247
247
|
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
248
248
|
private var lastLc3Event: Long? = null
|
|
249
249
|
private var micReinitRunnable: Runnable? = null
|
|
250
|
+
private var systemMicAvailabilityRecheckRunnable: Runnable? = null
|
|
250
251
|
|
|
251
252
|
// VAD
|
|
252
253
|
private val vadBuffer = mutableListOf<ByteArray>()
|
|
@@ -358,6 +359,37 @@ class DeviceManager {
|
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
|
|
362
|
+
private fun scheduleSystemMicAvailabilityRecheck(reason: String) {
|
|
363
|
+
if (systemMicAvailabilityRecheckRunnable != null) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
val recheck =
|
|
368
|
+
object : Runnable {
|
|
369
|
+
override fun run() {
|
|
370
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
371
|
+
|
|
372
|
+
if (!micEnabled || !systemMicUnavailable) {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
val stillBlocked = phoneMic?.hasBlockingMicInterruption() ?: true
|
|
377
|
+
if (stillBlocked) {
|
|
378
|
+
scheduleSystemMicAvailabilityRecheck(reason)
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
systemMicUnavailable = false
|
|
383
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
384
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
385
|
+
updateMicState()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
systemMicAvailabilityRecheckRunnable = recheck
|
|
390
|
+
mainHandler.postDelayed(recheck, 2_000)
|
|
391
|
+
}
|
|
392
|
+
|
|
361
393
|
// MARK: - Unique (Android)
|
|
362
394
|
private fun setupPermissionMonitoring() {
|
|
363
395
|
val context = Bridge.getContext()
|
|
@@ -947,6 +979,7 @@ class DeviceManager {
|
|
|
947
979
|
systemMicUnavailable = true
|
|
948
980
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
949
981
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
982
|
+
scheduleSystemMicAvailabilityRecheck("external_app_recording")
|
|
950
983
|
}
|
|
951
984
|
"audio_focus_available" -> {
|
|
952
985
|
// Audio focus is available again
|
|
@@ -965,6 +998,7 @@ class DeviceManager {
|
|
|
965
998
|
systemMicUnavailable = true
|
|
966
999
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
967
1000
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1001
|
+
scheduleSystemMicAvailabilityRecheck("phone_call_interruption")
|
|
968
1002
|
}
|
|
969
1003
|
"phone_call_ended" -> {
|
|
970
1004
|
// Phone call ended - mark mic as available again
|
|
@@ -977,12 +1011,14 @@ class DeviceManager {
|
|
|
977
1011
|
systemMicUnavailable = true
|
|
978
1012
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
979
1013
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1014
|
+
scheduleSystemMicAvailabilityRecheck("phone_call_active")
|
|
980
1015
|
}
|
|
981
1016
|
"audio_focus_denied" -> {
|
|
982
1017
|
// Another app has audio focus
|
|
983
1018
|
systemMicUnavailable = true
|
|
984
1019
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
985
1020
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1021
|
+
scheduleSystemMicAvailabilityRecheck("audio_focus_denied")
|
|
986
1022
|
}
|
|
987
1023
|
"permission_denied" -> {
|
|
988
1024
|
// Microphone permission not granted
|
|
@@ -1010,6 +1046,9 @@ class DeviceManager {
|
|
|
1010
1046
|
// systemMicUnavailable = false
|
|
1011
1047
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1012
1048
|
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1049
|
+
if (systemMicUnavailable) {
|
|
1050
|
+
scheduleSystemMicAvailabilityRecheck("recording_stopped")
|
|
1051
|
+
}
|
|
1013
1052
|
}
|
|
1014
1053
|
else -> {
|
|
1015
1054
|
// Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
|
|
@@ -1326,7 +1365,6 @@ class DeviceManager {
|
|
|
1326
1365
|
|
|
1327
1366
|
fun startStream(message: MutableMap<String, Any>) {
|
|
1328
1367
|
Bridge.log("MAN: startStream")
|
|
1329
|
-
message["flash"] = true
|
|
1330
1368
|
sgc?.startStream(message)
|
|
1331
1369
|
}
|
|
1332
1370
|
|
|
@@ -1380,9 +1418,9 @@ class DeviceManager {
|
|
|
1380
1418
|
* Send OTA start command to glasses. Called when user approves an update (onboarding or
|
|
1381
1419
|
* background mode). Triggers glasses to begin download and installation.
|
|
1382
1420
|
*/
|
|
1383
|
-
fun sendOtaStart() {
|
|
1421
|
+
fun sendOtaStart(otaVersionUrl: String? = null) {
|
|
1384
1422
|
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1385
|
-
(sgc as? MentraLive)?.sendOtaStart()
|
|
1423
|
+
(sgc as? MentraLive)?.sendOtaStart(otaVersionUrl)
|
|
1386
1424
|
}
|
|
1387
1425
|
|
|
1388
1426
|
fun sendOtaQueryStatus() {
|
|
@@ -1396,18 +1434,31 @@ class DeviceManager {
|
|
|
1396
1434
|
}
|
|
1397
1435
|
|
|
1398
1436
|
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1399
|
-
|
|
1400
|
-
live.sendButtonPhotoSettings(requestId, size)
|
|
1437
|
+
sendButtonPhotoSettings(requestId, PhotoCaptureDefaults(PhotoSize.fromValue(size)))
|
|
1401
1438
|
}
|
|
1402
1439
|
|
|
1403
|
-
fun
|
|
1440
|
+
fun sendButtonPhotoSettings(requestId: String, settings: PhotoCaptureDefaults) {
|
|
1404
1441
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1405
|
-
live.
|
|
1442
|
+
live.sendButtonPhotoSettings(
|
|
1443
|
+
requestId,
|
|
1444
|
+
settings.size?.value,
|
|
1445
|
+
settings.mfnr,
|
|
1446
|
+
settings.zsl,
|
|
1447
|
+
settings.noiseReduction,
|
|
1448
|
+
settings.edgeEnhancement,
|
|
1449
|
+
settings.ispDigitalGain,
|
|
1450
|
+
settings.ispAnalogGain,
|
|
1451
|
+
settings.aeExposureDivisor,
|
|
1452
|
+
settings.isoCap,
|
|
1453
|
+
settings.compress,
|
|
1454
|
+
settings.sound,
|
|
1455
|
+
settings.resetCaptureTuning == true,
|
|
1456
|
+
)
|
|
1406
1457
|
}
|
|
1407
1458
|
|
|
1408
|
-
fun
|
|
1459
|
+
fun sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) {
|
|
1409
1460
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1410
|
-
live.
|
|
1461
|
+
live.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
1411
1462
|
}
|
|
1412
1463
|
|
|
1413
1464
|
fun sendButtonMaxRecordingTime(requestId: String, minutes: Int) {
|
|
@@ -1508,11 +1559,11 @@ class DeviceManager {
|
|
|
1508
1559
|
maxRecordingTimeMinutes: Int = 0,
|
|
1509
1560
|
) {
|
|
1510
1561
|
Bridge.log(
|
|
1511
|
-
"MAN: onStartVideoRecording: requestId=$requestId, save=$save,
|
|
1562
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, sound=$sound, " +
|
|
1512
1563
|
"resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
|
|
1513
1564
|
)
|
|
1514
1565
|
sgc?.startVideoRecording(
|
|
1515
|
-
requestId, save,
|
|
1566
|
+
requestId, save, sound, width, height, fps, maxRecordingTimeMinutes)
|
|
1516
1567
|
}
|
|
1517
1568
|
|
|
1518
1569
|
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
@@ -1538,38 +1589,31 @@ class DeviceManager {
|
|
|
1538
1589
|
updateMicState()
|
|
1539
1590
|
}
|
|
1540
1591
|
|
|
1541
|
-
fun requestPhoto(
|
|
1542
|
-
requestId: String,
|
|
1543
|
-
appId: String,
|
|
1544
|
-
size: String,
|
|
1545
|
-
webhookUrl: String,
|
|
1546
|
-
authToken: String?,
|
|
1547
|
-
compress: String,
|
|
1548
|
-
flash: Boolean,
|
|
1549
|
-
save: Boolean,
|
|
1550
|
-
sound: Boolean,
|
|
1551
|
-
exposureTimeNs: Double? = null,
|
|
1552
|
-
iso: Int? = null,
|
|
1553
|
-
) {
|
|
1592
|
+
fun requestPhoto(request: PhotoRequest) {
|
|
1554
1593
|
val exposureNs: Long? =
|
|
1555
|
-
exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1594
|
+
request.exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1556
1595
|
when {
|
|
1557
1596
|
v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
1558
1597
|
else -> v.toLong()
|
|
1559
1598
|
}
|
|
1560
1599
|
}
|
|
1561
|
-
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1600
|
+
val manualIso = if (exposureNs != null) request.iso?.takeIf { it > 0 } else null
|
|
1601
|
+
val routed =
|
|
1602
|
+
request.copy(
|
|
1603
|
+
exposureTimeNs = exposureNs?.toDouble(),
|
|
1604
|
+
iso = manualIso,
|
|
1605
|
+
)
|
|
1562
1606
|
Bridge.log(
|
|
1563
|
-
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress
|
|
1607
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} appId=${routed.appId} size=${routed.size.value} compress=${routed.compress.value} save=${routed.save} sound=${routed.sound} exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} aeDivisor=${routed.aeExposureDivisor} isoCap=${routed.isoCap} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1564
1608
|
)
|
|
1565
1609
|
val activeSgc = sgc
|
|
1566
1610
|
if (activeSgc == null) {
|
|
1567
1611
|
Bridge.log(
|
|
1568
|
-
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=$requestId"
|
|
1612
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
|
|
1569
1613
|
)
|
|
1570
1614
|
return
|
|
1571
1615
|
}
|
|
1572
|
-
activeSgc.requestPhoto(
|
|
1616
|
+
activeSgc.requestPhoto(routed)
|
|
1573
1617
|
}
|
|
1574
1618
|
|
|
1575
1619
|
fun rgbLedControl(
|
|
@@ -1781,6 +1825,8 @@ class DeviceManager {
|
|
|
1781
1825
|
|
|
1782
1826
|
micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1783
1827
|
micReinitRunnable = null
|
|
1828
|
+
systemMicAvailabilityRecheckRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1829
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
1784
1830
|
|
|
1785
1831
|
// Clean up transcriber resources
|
|
1786
1832
|
transcriber?.shutdown()
|
|
@@ -126,8 +126,7 @@ object DeviceStore {
|
|
|
126
126
|
store.set("bluetooth", "gallery_mode", true)
|
|
127
127
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
128
128
|
store.set("bluetooth", "screen_disabled", false)
|
|
129
|
-
store.set("bluetooth", "button_photo_size", "
|
|
130
|
-
store.set("bluetooth", "button_camera_led", true)
|
|
129
|
+
store.set("bluetooth", "button_photo_size", "max")
|
|
131
130
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
132
131
|
store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
|
|
133
132
|
store.set("bluetooth", "button_video_width", 1280)
|
|
@@ -287,9 +286,6 @@ object DeviceStore {
|
|
|
287
286
|
"bluetooth" to "button_photo_size" -> {
|
|
288
287
|
DeviceManager.getInstance().sgc?.sendButtonPhotoSettings()
|
|
289
288
|
}
|
|
290
|
-
"bluetooth" to "button_camera_led" -> {
|
|
291
|
-
DeviceManager.getInstance().sgc?.sendButtonCameraLedSetting()
|
|
292
|
-
}
|
|
293
289
|
"bluetooth" to "button_max_recording_time" -> {
|
|
294
290
|
DeviceManager.getInstance().sgc?.sendButtonMaxRecordingTime()
|
|
295
291
|
}
|