Give the Fly App GO 4-style camera controls
One settings table in CameraBridge watches every camera value into the telemetry tick and backs a generic setCameraValue, so the tabbed camera menu (Exposure / Photo / Video / Other) is built from row specs and the camera's own capability ranges. Writes show at once and fall back to what the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider, shutter and aperture wheels, EV strip, metered EV in M), a new camera column with record timer and interval/pano progress, a zoom column for zoom cameras, tap-to-focus / tap-to-meter, and a live histogram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ebc567e97
commit
c1770fff73
26 files changed
+3546
-171
No files matched your search
@@ -1,5 +1,7 @@
|
||||
package com.pilotvault.flyapp
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.PointF
|
||||
import dji.common.camera.ExposureSettings
|
||||
import dji.common.camera.ResolutionAndFrameRate
|
||||
import dji.common.camera.SSDState
|
||||
@@ -7,7 +9,10 @@ import dji.common.camera.SettingsDefinitions
|
||||
import dji.common.camera.StorageState
|
||||
import dji.common.camera.SystemState
|
||||
import dji.common.camera.WhiteBalance
|
||||
import dji.common.error.DJIError
|
||||
import dji.keysdk.CameraKey
|
||||
import dji.keysdk.KeyManager
|
||||
import dji.keysdk.callback.SetCallback
|
||||
import dji.sdk.base.BaseProduct
|
||||
import dji.sdk.camera.Camera
|
||||
import dji.sdk.products.Aircraft
|
||||
@@ -15,18 +20,22 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* Camera control: mode, photo/record, shoot-photo modes, and Pro exposure
|
||||
* (ISO / shutter / aperture / EV / WB). Emits `camera` (system state) and
|
||||
* `exposure` events. String↔enum mapping mirrors the v2 kit's scrubber labels;
|
||||
* unsupported values fail cleanly rather than crashing.
|
||||
* Camera control: mode, photo/record, shoot-photo modes, exposure, the camera
|
||||
* menu's settings, tap-to-focus / spot metering, zoom and the histogram. Emits
|
||||
* `camera` (system state), `exposure` and `histogram` events. String↔enum
|
||||
* mapping mirrors the v2 kit's scrubber labels; unsupported values fail cleanly
|
||||
* rather than crashing.
|
||||
*
|
||||
* Also feeds two slow groups of the [TelemetryHub]:
|
||||
* Also feeds four slow groups of the [TelemetryHub]:
|
||||
* ```
|
||||
* storage: {location, inserted, initializing, formatting, formatted, full, error,
|
||||
* readOnly, invalidFormat, verified, totalMB, remainingMB,
|
||||
* availableCaptureCount, availableRecordingSeconds}
|
||||
* cameraSettings: {aeLock, focusMode, photoAspectRatio, photoFileFormat,
|
||||
* videoResolution, videoFrameRate, videoFileFormat, colorProfile}
|
||||
* cameraSettings: {<every SETTINGS id>, isoSetting, videoResolution, videoFrameRate,
|
||||
* whiteBalancePreset, whiteBalanceKelvin, intervalCount, intervalSeconds,
|
||||
* styleSharpness, styleContrast, styleSaturation, spotCol, spotRow,
|
||||
* focusX, focusY, zoomFocalLength, zoomMin, zoomMax, zoomStep,
|
||||
* zoomOpticalMax, digitalZoomFactor}
|
||||
* ssd: {connected, state, capacity, remainingMB, availableRecordingSeconds}
|
||||
* thermal: {spotTemperature}
|
||||
* ```
|
||||
@@ -34,14 +43,53 @@ import io.flutter.plugin.common.MethodChannel
|
||||
* on thermal cameras (XT / XT2 / Mavic 2 Enterprise Dual); the HUD's SSD and
|
||||
* thermal readouts (plan P2) are hidden on everything else.
|
||||
* The settings are watched keys, so a change made from the RC's dial or the
|
||||
* settings sheet shows up without a re-read.
|
||||
* camera menu shows up without a re-read — the menu (plan P4) shows what the
|
||||
* camera reports back, never what it asked for.
|
||||
*/
|
||||
class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
|
||||
/**
|
||||
* One simple camera setting: its id in the `cameraSettings` group (and in
|
||||
* `setCameraValue`), its CameraKey, and its value type (an enum class, or
|
||||
* Boolean). The table drives both the watch and the setter, so a camera
|
||||
* menu row is one line here plus one row spec in Dart.
|
||||
*/
|
||||
private class Setting(val key: String, val type: Class<*>)
|
||||
|
||||
private val settings: Map<String, Setting> = linkedMapOf(
|
||||
"aeLock" to Setting(CameraKey.AE_LOCK, java.lang.Boolean::class.java),
|
||||
"focusMode" to Setting(CameraKey.FOCUS_MODE, SettingsDefinitions.FocusMode::class.java),
|
||||
"exposureMode" to Setting(CameraKey.EXPOSURE_MODE, SettingsDefinitions.ExposureMode::class.java),
|
||||
"meteringMode" to Setting(CameraKey.METERING_MODE, SettingsDefinitions.MeteringMode::class.java),
|
||||
"shootPhotoMode" to Setting(CameraKey.SHOOT_PHOTO_MODE, SettingsDefinitions.ShootPhotoMode::class.java),
|
||||
"aebCount" to Setting(CameraKey.PHOTO_AEB_COUNT, SettingsDefinitions.PhotoAEBCount::class.java),
|
||||
"burstCount" to Setting(CameraKey.PHOTO_BURST_COUNT, SettingsDefinitions.PhotoBurstCount::class.java),
|
||||
"panoramaMode" to Setting(CameraKey.PHOTO_PANORAMA_MODE, SettingsDefinitions.PhotoPanoramaMode::class.java),
|
||||
"photoAspectRatio" to Setting(CameraKey.PHOTO_ASPECT_RATIO, SettingsDefinitions.PhotoAspectRatio::class.java),
|
||||
"photoFileFormat" to Setting(CameraKey.PHOTO_FILE_FORMAT, SettingsDefinitions.PhotoFileFormat::class.java),
|
||||
"videoFileFormat" to Setting(CameraKey.VIDEO_FILE_FORMAT, SettingsDefinitions.VideoFileFormat::class.java),
|
||||
"videoStandard" to Setting(CameraKey.VIDEO_STANDARD, SettingsDefinitions.VideoStandard::class.java),
|
||||
"colorProfile" to Setting(CameraKey.CAMERA_COLOR, SettingsDefinitions.CameraColor::class.java),
|
||||
"antiFlicker" to Setting(CameraKey.ANTI_FLICKER_FREQUENCY, SettingsDefinitions.AntiFlickerFrequency::class.java),
|
||||
"fileIndexMode" to Setting(CameraKey.FILE_INDEX_MODE, SettingsDefinitions.FileIndexMode::class.java),
|
||||
"videoCaption" to Setting(CameraKey.VIDEO_CAPTION_ENABLED, java.lang.Boolean::class.java),
|
||||
"autoLockGimbal" to Setting(CameraKey.AUTO_LOCK_GIMBAL_ENABLED, java.lang.Boolean::class.java),
|
||||
"histogramEnabled" to Setting(CameraKey.HISTOGRAM_ENABLED, java.lang.Boolean::class.java),
|
||||
)
|
||||
|
||||
private val keys = KeyWatch(ctx)
|
||||
|
||||
/** Histogram frames arrive at the video rate; the HUD needs a few per second. */
|
||||
private var lastHistogramAt = 0L
|
||||
|
||||
private fun cam(): Camera? = ctx.aircraft()?.camera
|
||||
|
||||
private fun index(camera: Camera): Int = try {
|
||||
camera.index
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
|
||||
override fun handle(call: MethodCall, result: MethodChannel.Result): Boolean {
|
||||
val camera = cam()
|
||||
when (call.method) {
|
||||
@@ -64,6 +112,8 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
if (mode == null) ctx.fail(result, "Unsupported photo mode")
|
||||
else it.setShootPhotoMode(mode, ctx.completion(result))
|
||||
}
|
||||
// The classic flight screen's Auto / Pro switch. The HUD sets the four
|
||||
// GO 4 modes through `setCameraValue` / `exposureMode` instead.
|
||||
"setExposureProgram" -> guard(camera, result) {
|
||||
val mode = if (call.argument<String>("program") == "pro")
|
||||
SettingsDefinitions.ExposureMode.MANUAL
|
||||
@@ -91,6 +141,71 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
if (wb == null) ctx.fail(result, "Unsupported white balance")
|
||||
else it.setWhiteBalance(wb, ctx.completion(result))
|
||||
}
|
||||
"setCameraValue" -> guard(camera, result) { setValue(it, call, result) }
|
||||
"setVideoResolutionAndFrameRate" -> guard(camera, result) {
|
||||
val res = enumOrNull<SettingsDefinitions.VideoResolution>(call.argument<String>("resolution"))
|
||||
val fps = enumOrNull<SettingsDefinitions.VideoFrameRate>(call.argument<String>("frameRate"))
|
||||
if (res == null || fps == null) ctx.fail(result, "Unsupported video size")
|
||||
else it.setVideoResolutionAndFrameRate(ResolutionAndFrameRate(res, fps), ctx.completion(result))
|
||||
}
|
||||
"setPhotoTimeInterval" -> guard(camera, result) {
|
||||
val count = call.argument<Int>("count") ?: 255
|
||||
val seconds = call.argument<Int>("seconds") ?: 5
|
||||
it.setPhotoTimeIntervalSettings(
|
||||
SettingsDefinitions.PhotoTimeIntervalSettings(count, seconds),
|
||||
ctx.completion(result),
|
||||
)
|
||||
}
|
||||
"setPictureStyle" -> guard(camera, result) {
|
||||
val style = SettingsDefinitions.PictureStylePreset.Builder()
|
||||
.sharpness(call.argument<Int>("sharpness") ?: 0)
|
||||
.contrast(call.argument<Int>("contrast") ?: 0)
|
||||
.saturation(call.argument<Int>("saturation") ?: 0)
|
||||
.build()
|
||||
setKey(CameraKey.create(CameraKey.PICTURE_STYLE_PRESET, index(it)), style, result)
|
||||
}
|
||||
// Tap-to-focus: x/y are 0..1 across the live view.
|
||||
"setFocusTarget" -> guard(camera, result) {
|
||||
val p = PointF(unit(call.argument<Double>("x")), unit(call.argument<Double>("y")))
|
||||
it.setFocusTarget(p, ctx.completion(result))
|
||||
}
|
||||
// Spot metering on the camera's own grid (12 × 8 on most cameras): switch
|
||||
// to SPOT, then move the spot to the cell under x/y.
|
||||
"setSpotMeteringTarget" -> guard(camera, result) { c ->
|
||||
val (cols, rows) = meteringGrid(c)
|
||||
val col = (unit(call.argument<Double>("x")) * cols).toInt().coerceIn(0, cols - 1)
|
||||
val row = (unit(call.argument<Double>("y")) * rows).toInt().coerceIn(0, rows - 1)
|
||||
c.setMeteringMode(SettingsDefinitions.MeteringMode.SPOT) { error ->
|
||||
if (error != null) ctx.mainHandler.post { result.error("DJI_ERROR", error.description, null) }
|
||||
else c.setSpotMeteringTarget(Point(col, row), ctx.completion(result))
|
||||
}
|
||||
}
|
||||
"startContinuousZoom" -> guard(camera, result) {
|
||||
if (safe { it.isOpticalZoomSupported } != true) {
|
||||
ctx.fail(result, "This camera zooms in steps only")
|
||||
} else {
|
||||
val dir = if (call.argument<String>("direction") == "out")
|
||||
SettingsDefinitions.ZoomDirection.ZOOM_OUT
|
||||
else SettingsDefinitions.ZoomDirection.ZOOM_IN
|
||||
it.startContinuousOpticalZoom(dir, SettingsDefinitions.ZoomSpeed.NORMAL, ctx.completion(result))
|
||||
}
|
||||
}
|
||||
"stopContinuousZoom" -> guard(camera, result) { it.stopContinuousOpticalZoom(ctx.completion(result)) }
|
||||
// A focal length in the camera's own units (mm × 10 on the Mavic 2 Zoom).
|
||||
"setZoomFocalLength" -> guard(camera, result) {
|
||||
val mm = call.argument<Int>("value")
|
||||
when {
|
||||
mm == null -> ctx.fail(result, "No focal length")
|
||||
safe { it.isHybridZoomSupported } == true -> it.setHybridZoomFocalLength(mm, ctx.completion(result))
|
||||
safe { it.isOpticalZoomSupported } == true -> it.setOpticalZoomFocalLength(mm, ctx.completion(result))
|
||||
else -> ctx.fail(result, "No optical zoom")
|
||||
}
|
||||
}
|
||||
"setDigitalZoomFactor" -> guard(camera, result) {
|
||||
val f = call.argument<Double>("value")?.toFloat() ?: 1f
|
||||
it.setDigitalZoomFactor(f, ctx.completion(result))
|
||||
}
|
||||
"restoreCameraDefaults" -> guard(camera, result) { it.restoreFactorySettings(ctx.completion(result)) }
|
||||
// Erases the card (Aircraft Status sheet, plan P3). `location` picks the SD
|
||||
// card or internal storage; the UI slide-confirms and refuses while recording.
|
||||
"formatStorage" -> guard(camera, result) {
|
||||
@@ -100,10 +215,8 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
}
|
||||
it.formatStorage(location, ctx.completion(result))
|
||||
}
|
||||
"setHistogramEnabled" -> {
|
||||
// Histogram data streaming is model-dependent; accept the toggle so
|
||||
// the UI stays responsive. Real histogram bins are a later add.
|
||||
result.success(null)
|
||||
"setHistogramEnabled" -> guard(camera, result) {
|
||||
it.setHistogramEnabled(call.argument<Boolean>("value") == true, ctx.completion(result))
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
@@ -114,6 +227,54 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
if (camera == null) ctx.fail(result, "No camera connected") else block(camera)
|
||||
}
|
||||
|
||||
/** `setCameraValue {id, value}`: one row of [settings], by enum name or bool. */
|
||||
private fun setValue(camera: Camera, call: MethodCall, result: MethodChannel.Result) {
|
||||
val id = call.argument<String>("id")
|
||||
val s = settings[id]
|
||||
if (s == null) {
|
||||
ctx.fail(result, "Unknown camera setting $id")
|
||||
return
|
||||
}
|
||||
val raw = call.argument<Any>("value")
|
||||
val value: Any? = if (s.type == java.lang.Boolean::class.java) {
|
||||
raw as? Boolean
|
||||
} else {
|
||||
s.type.enumConstants?.firstOrNull { (it as Enum<*>).name == raw?.toString() }
|
||||
}
|
||||
if (value == null) {
|
||||
ctx.fail(result, "Unsupported value $raw for $id")
|
||||
return
|
||||
}
|
||||
setKey(CameraKey.create(s.key, index(camera)), value, result)
|
||||
}
|
||||
|
||||
private fun setKey(key: CameraKey, value: Any, result: MethodChannel.Result) {
|
||||
val km = KeyManager.getInstance()
|
||||
if (km == null) {
|
||||
ctx.fail(result, "SDK not ready")
|
||||
return
|
||||
}
|
||||
km.setValue(key, value, object : SetCallback {
|
||||
override fun onSuccess() {
|
||||
ctx.mainHandler.post { result.success(null) }
|
||||
}
|
||||
|
||||
override fun onFailure(error: DJIError) {
|
||||
ctx.mainHandler.post { result.error("DJI_ERROR", error.description, null) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun meteringGrid(camera: Camera): Pair<Int, Int> {
|
||||
val km = KeyManager.getInstance()
|
||||
val i = index(camera)
|
||||
val cols = (safe { km?.getValue(CameraKey.create(CameraKey.SPOT_METERING_COL_NUMS, i)) } as? Number)?.toInt()
|
||||
val rows = (safe { km?.getValue(CameraKey.create(CameraKey.SPOT_METERING_ROW_NUMS, i)) } as? Number)?.toInt()
|
||||
return Pair(cols?.takeIf { it > 0 } ?: 12, rows?.takeIf { it > 0 } ?: 8)
|
||||
}
|
||||
|
||||
private fun unit(v: Double?): Float = (v ?: 0.5).toFloat().coerceIn(0f, 1f)
|
||||
|
||||
override fun bind(product: BaseProduct?) {
|
||||
if (product !is Aircraft) return
|
||||
val camera = product.camera ?: return
|
||||
@@ -143,30 +304,89 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val index = try {
|
||||
camera.index
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
safe {
|
||||
camera.setHistogramCallback { bins: ShortArray ->
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastHistogramAt >= 250) {
|
||||
lastHistogramAt = now
|
||||
val values = bins.map { it.toInt() }
|
||||
ctx.mainHandler.post { ctx.emit(mapOf("type" to "histogram", "bins" to values)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val index = index(camera)
|
||||
val optical = safe { camera.isOpticalZoomSupported } == true
|
||||
val hybrid = safe { camera.isHybridZoomSupported } == true
|
||||
val digital = safe { camera.isDigitalZoomSupported } == true
|
||||
ctx.mainHandler.post {
|
||||
keys.clear()
|
||||
fun setting(name: String, field: String) = keys.watch(CameraKey.create(name, index)) { v ->
|
||||
ctx.hub.patchSlow("cameraSettings", mapOf(field to KeyValues.toDart(v)))
|
||||
}
|
||||
setting(CameraKey.AE_LOCK, "aeLock")
|
||||
setting(CameraKey.FOCUS_MODE, "focusMode")
|
||||
setting(CameraKey.PHOTO_ASPECT_RATIO, "photoAspectRatio")
|
||||
setting(CameraKey.PHOTO_FILE_FORMAT, "photoFileFormat")
|
||||
setting(CameraKey.VIDEO_FILE_FORMAT, "videoFileFormat")
|
||||
setting(CameraKey.CAMERA_COLOR, "colorProfile")
|
||||
keys.watch(CameraKey.create(CameraKey.RESOLUTION_FRAME_RATE, index)) { v ->
|
||||
fun patch(values: Map<String, Any?>) = ctx.hub.patchSlow("cameraSettings", values)
|
||||
fun watch(key: String, onValue: (Any?) -> Unit) = keys.watch(CameraKey.create(key, index), onValue)
|
||||
|
||||
for ((id, s) in settings) watch(s.key) { v -> patch(mapOf(id to KeyValues.toDart(v))) }
|
||||
// The ISO *setting* (AUTO included); `exposure.iso` is what the camera metered.
|
||||
watch(CameraKey.ISO) { v -> patch(mapOf("isoSetting" to isoLabel((v as? Enum<*>)?.name))) }
|
||||
watch(CameraKey.RESOLUTION_FRAME_RATE) { v ->
|
||||
val r = v as? ResolutionAndFrameRate
|
||||
ctx.hub.patchSlow(
|
||||
"cameraSettings",
|
||||
mapOf("videoResolution" to r?.resolution?.name, "videoFrameRate" to r?.frameRate?.name),
|
||||
patch(mapOf("videoResolution" to r?.resolution?.name, "videoFrameRate" to r?.frameRate?.name))
|
||||
}
|
||||
watch(CameraKey.WHITE_BALANCE) { v ->
|
||||
val wb = v as? WhiteBalance
|
||||
patch(
|
||||
mapOf(
|
||||
"whiteBalancePreset" to wb?.whiteBalancePreset?.name,
|
||||
// The SDK counts colour temperature in units of 100 K.
|
||||
"whiteBalanceKelvin" to wb?.colorTemperature?.takeIf { it > 0 }?.times(100),
|
||||
)
|
||||
)
|
||||
}
|
||||
watch(CameraKey.PHOTO_TIME_INTERVAL_SETTINGS) { v ->
|
||||
val t = v as? SettingsDefinitions.PhotoTimeIntervalSettings
|
||||
patch(mapOf("intervalCount" to t?.captureCount, "intervalSeconds" to t?.timeIntervalInSeconds))
|
||||
}
|
||||
watch(CameraKey.PICTURE_STYLE_PRESET) { v ->
|
||||
val p = v as? SettingsDefinitions.PictureStylePreset
|
||||
patch(mapOf("styleSharpness" to p?.sharpness, "styleContrast" to p?.contrast, "styleSaturation" to p?.saturation))
|
||||
}
|
||||
watch(CameraKey.SPOT_METERING_TARGET) { v ->
|
||||
val p = v as? Point
|
||||
patch(mapOf("spotCol" to p?.x, "spotRow" to p?.y))
|
||||
}
|
||||
watch(CameraKey.FOCUS_TARGET) { v ->
|
||||
val p = v as? PointF
|
||||
patch(mapOf("focusX" to p?.x?.toDouble(), "focusY" to p?.y?.toDouble()))
|
||||
}
|
||||
if (hybrid) {
|
||||
watch(CameraKey.HYBRID_ZOOM_SPEC) { v ->
|
||||
val z = v as? SettingsDefinitions.HybridZoomSpec
|
||||
patch(
|
||||
mapOf(
|
||||
"zoomMin" to z?.minHybridFocalLength,
|
||||
"zoomMax" to z?.maxHybridFocalLength,
|
||||
"zoomOpticalMax" to z?.maxOpticalFocalLength,
|
||||
"zoomStep" to z?.focalLengthStep,
|
||||
)
|
||||
)
|
||||
}
|
||||
watch(CameraKey.HYBRID_ZOOM_FOCAL_LENGTH) { v -> patch(mapOf("zoomFocalLength" to (v as? Number)?.toInt())) }
|
||||
} else if (optical) {
|
||||
watch(CameraKey.OPTICAL_ZOOM_SPEC) { v ->
|
||||
val z = v as? SettingsDefinitions.OpticalZoomSpec
|
||||
patch(
|
||||
mapOf(
|
||||
"zoomMin" to z?.minFocalLength,
|
||||
"zoomMax" to z?.maxFocalLength,
|
||||
"zoomOpticalMax" to z?.maxFocalLength,
|
||||
"zoomStep" to z?.focalLengthStep,
|
||||
)
|
||||
)
|
||||
}
|
||||
watch(CameraKey.OPTICAL_ZOOM_FOCAL_LENGTH) { v -> patch(mapOf("zoomFocalLength" to (v as? Number)?.toInt())) }
|
||||
}
|
||||
if (digital) {
|
||||
watch(CameraKey.DIGITAL_ZOOM_FACTOR) { v -> patch(mapOf("digitalZoomFactor" to (v as? Number)?.toDouble())) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +422,14 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
"isRecording" to state.isRecording,
|
||||
"recordingTimeSeconds" to state.currentVideoRecordingTimeInSeconds,
|
||||
"mode" to state.mode?.name,
|
||||
"shootingSingle" to state.isShootingSinglePhoto,
|
||||
"shootingInterval" to state.isShootingIntervalPhoto,
|
||||
"shootingBurst" to (state.isShootingBurstPhoto || state.isShootingRAWBurstPhoto),
|
||||
"shootingPanorama" to state.isShootingPanoramaPhoto,
|
||||
"panoramaProgress" to state.panoramaProgress,
|
||||
"storingPhoto" to state.isStoringPhoto,
|
||||
"overheating" to state.isOverheating,
|
||||
"hasError" to state.hasError(),
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -213,6 +441,7 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
"iso" to s.iso.toString(),
|
||||
"shutter" to shutterLabel(s.shutterSpeed?.name),
|
||||
"aperture" to apertureLabel(s.aperture?.name),
|
||||
// In manual exposure this is the meter's offset from a correct exposure.
|
||||
"ev" to evLabel(s.exposureCompensation?.name),
|
||||
)
|
||||
)
|
||||
@@ -240,10 +469,12 @@ class CameraBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
return (if (neg) "N_" else "P_") + digits
|
||||
}
|
||||
|
||||
/** `AUTO`, a preset name (`SUNNY`, `CLOUDY`, …) or a custom temperature (`5600K`). */
|
||||
private fun whiteBalance(v: String?): WhiteBalance? {
|
||||
if (v == null) return null
|
||||
if (v.equals("AUTO", true)) {
|
||||
return WhiteBalance(SettingsDefinitions.WhiteBalancePreset.AUTO)
|
||||
val preset = enumOrNull<SettingsDefinitions.WhiteBalancePreset>(v.uppercase())
|
||||
if (preset != null && preset != SettingsDefinitions.WhiteBalancePreset.CUSTOM) {
|
||||
return WhiteBalance(preset)
|
||||
}
|
||||
val kelvin = v.replace("K", "", true).trim().toIntOrNull() ?: return null
|
||||
// DJI custom colour temperature is expressed in units of 100 K.
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.pilotvault.flyapp
|
||||
|
||||
import dji.common.gimbal.CapabilityKey
|
||||
import dji.common.util.DJIParamMinMaxCapability
|
||||
import dji.keysdk.CameraKey
|
||||
import dji.keysdk.KeyManager
|
||||
import dji.sdk.base.BaseProduct
|
||||
import dji.sdk.camera.Camera
|
||||
import dji.sdk.gimbal.Gimbal
|
||||
@@ -163,9 +165,29 @@ class ProductProfileBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
"$res@$fps"
|
||||
}
|
||||
.distinct(),
|
||||
// Ranges `Capabilities` has no getter for, from the key cache (empty
|
||||
// until the handshake, like the ones above). The camera menu (plan P4)
|
||||
// builds its option lists from these.
|
||||
"photoModes" to cachedNames(cam, CameraKey.SHOOT_PHOTO_MODE_RANGE),
|
||||
"whiteBalancePresets" to cachedNames(cam, CameraKey.WHITE_BALANCE_PRESENT_RANGE),
|
||||
"antiFlickerModes" to cachedNames(cam, CameraKey.ANTI_FLICKER_RANGE),
|
||||
"videoStandards" to cachedNames(cam, CameraKey.VIDEO_STANDARD_RANGE),
|
||||
"colors" to cachedNames(cam, CameraKey.CAMERA_COLOR_RANGE),
|
||||
)
|
||||
}
|
||||
|
||||
/** Enum names of a cached range key (an array or list of enums), UNKNOWN dropped. */
|
||||
private fun cachedNames(cam: Camera, key: String): List<String> {
|
||||
val index = probe { cam.index } ?: 0
|
||||
val v = probe { KeyManager.getInstance()?.getValue(CameraKey.create(key, index)) }
|
||||
val items: List<Any?> = when (v) {
|
||||
is Array<*> -> v.toList()
|
||||
is Collection<*> -> v.toList()
|
||||
else -> emptyList()
|
||||
}
|
||||
return items.mapNotNull { (it as? Enum<*>)?.name }.filter { it != "UNKNOWN" }.distinct()
|
||||
}
|
||||
|
||||
/** Enum names (or their setter labels) with the SDK's UNKNOWN placeholders dropped. */
|
||||
private fun names(values: Array<out Enum<*>?>?, label: (String?) -> String? = { it }): List<String> =
|
||||
values.orEmpty()
|
||||
|
||||
Reference in new issue
Block a user