Give the Fly App GO 4-style calibration wizards
P6b: a new CalibrationBridge owns every calibration call and publishes a `calibration` telemetry group (IMU per-unit and per-side progress, gravity centre, lens, sensor cleaning, RC calibration state and live sticks). Guided full-screen wizards cover compass, IMU, remote controller, gimbal auto calibration, lens, sensor cleaning, centre of gravity, propellers and a vision-sensor guide; gimbal adjust is a horizon-line overlay on the live video. Every start goes through SafetyGate.runMaintenance, and closing mid-run stops what the SDK can stop. Reachable from Settings > Safety > Calibration & maintenance, the Aircraft Status calibrate buttons and the gimbal sheet. Not yet run on an aircraft; the RC key sequence still needs checking per remote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
284de5442a
commit
a892bfb209
20 files changed
+3119
-77
No files matched your search
@@ -0,0 +1,294 @@
|
||||
package com.pilotvault.flyapp
|
||||
|
||||
import dji.common.camera.DJICameraCalibrateResult
|
||||
import dji.common.camera.DJICameraCalibrateState
|
||||
import dji.common.error.DJIError
|
||||
import dji.common.flightcontroller.GravityCenterState
|
||||
import dji.common.flightcontroller.imu.IMUState
|
||||
import dji.common.remotecontroller.CalibrationState
|
||||
import dji.keysdk.CameraKey
|
||||
import dji.keysdk.DJIKey
|
||||
import dji.keysdk.FlightControllerKey
|
||||
import dji.keysdk.KeyManager
|
||||
import dji.keysdk.RemoteControllerKey
|
||||
import dji.keysdk.callback.SetCallback
|
||||
import dji.sdk.base.BaseProduct
|
||||
import dji.sdk.camera.Camera
|
||||
import dji.sdk.flightcontroller.FlightController
|
||||
import dji.sdk.products.Aircraft
|
||||
import dji.sdk.remotecontroller.RemoteController
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* The calibration and maintenance wizards' native half (plan P6b).
|
||||
*
|
||||
* State goes out as the `calibration` fast group of the [TelemetryHub]:
|
||||
*
|
||||
* ```
|
||||
* calibration: {
|
||||
* support: {compassCount, imuCount, gravityCenter, propeller, lens, sensorCleaning, rc},
|
||||
* imu: {units: [{index, state, progress, gyroscope, accelerometer, connected}],
|
||||
* multiSide, orientationState, toCalibrate: [NOSE_DOWN …], calibrated: [...]},
|
||||
* gravityCenter: {state, error},
|
||||
* lens: {state, result},
|
||||
* sensorCleaning: SensorCleaningState name,
|
||||
* rc: {state, segments, axes: {A: …, …}},
|
||||
* rcHardware: {lx, ly, rx, ry, leftDial, rightDial} // only while watched
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The compass walks its own `compassCalibrationState` in the flight-controller
|
||||
* keys ([FlightControllerBridge]); the gimbal its `gimbal` group ([GimbalBridge]).
|
||||
*
|
||||
* Methods: `startCompassCalibration`, `stopCompassCalibration`,
|
||||
* `startIMUCalibration {index?}`, `startGravityCenterCalibration`,
|
||||
* `stopGravityCenterCalibration`, `startPropellerCalibration`,
|
||||
* `stopPropellerCalibration`, `startLensCalibration`, `stopLensCalibration`,
|
||||
* `initSensorCleaning`, `startSensorCleaning`, `exitSensorCleaning`,
|
||||
* `setRcCalibrationStep {step: MIDDLE | LIMITS | QUIT}` and
|
||||
* `watchRcHardware {on}` (the sticks and dials, for the RC wizard's live view).
|
||||
*
|
||||
* The RC calibration key sequence (MIDDLE → LIMITS → QUIT) is what DJI's UX SDK
|
||||
* drives; which remotes accept it is still to be confirmed on hardware (R2).
|
||||
*/
|
||||
class CalibrationBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
|
||||
/** The `calibration` group as last built; re-published whole on every change. */
|
||||
private val group = LinkedHashMap<String, Any?>()
|
||||
|
||||
/** IMU units by index — the callback reports one unit at a time. */
|
||||
private val imuUnits = sortedMapOf<Int, Map<String, Any?>>()
|
||||
|
||||
private val watch = KeyWatch(ctx)
|
||||
private val rcWatch = KeyWatch(ctx)
|
||||
private var rcHardwareOn = false
|
||||
|
||||
private fun fc(): FlightController? = ctx.aircraft()?.flightController
|
||||
private fun camera(): Camera? = ctx.product()?.camera
|
||||
private fun rc(): RemoteController? = ctx.aircraft()?.remoteController
|
||||
|
||||
override fun handle(call: MethodCall, result: MethodChannel.Result): Boolean {
|
||||
when (call.method) {
|
||||
"startCompassCalibration" -> withFc(result) {
|
||||
val compass = it.compass
|
||||
if (compass == null) ctx.fail(result, "No compass on this aircraft")
|
||||
else compass.startCalibration(ctx.completion(result))
|
||||
}
|
||||
"stopCompassCalibration" -> withFc(result) {
|
||||
val compass = it.compass
|
||||
if (compass == null) ctx.fail(result, "No compass on this aircraft")
|
||||
else compass.stopCalibration(ctx.completion(result))
|
||||
}
|
||||
"startIMUCalibration" -> withFc(result) {
|
||||
val index = call.argument<Int>("index")
|
||||
if (index == null) it.startIMUCalibration(ctx.completion(result))
|
||||
else it.startIMUCalibration(index, ctx.completion(result))
|
||||
}
|
||||
"startGravityCenterCalibration" -> withFc(result) { it.startGravityCenterCalibration(ctx.completion(result)) }
|
||||
"stopGravityCenterCalibration" -> withFc(result) { it.stopGravityCenterCalibration(ctx.completion(result)) }
|
||||
"startPropellerCalibration" -> withFc(result) {
|
||||
if (!it.isPropellerCalibrationSupported) ctx.fail(result, "Propeller calibration is not supported on this aircraft")
|
||||
else it.startPropellerCalibration(ctx.completion(result))
|
||||
}
|
||||
"stopPropellerCalibration" -> withFc(result) { it.stopPropellerCalibration(ctx.completion(result)) }
|
||||
"startLensCalibration" -> withCamera(result) { it.startCalibration(ctx.completion(result)) }
|
||||
"stopLensCalibration" -> withCamera(result) { it.stopCalibration(ctx.completion(result)) }
|
||||
"initSensorCleaning" -> withCamera(result) { it.initSensorCleaningMode(ctx.completion(result)) }
|
||||
"startSensorCleaning" -> withCamera(result) { it.startSensorCleaning(ctx.completion(result)) }
|
||||
"exitSensorCleaning" -> withCamera(result) { it.exitSensorCleaningMode(ctx.completion(result)) }
|
||||
"setRcCalibrationStep" -> {
|
||||
val step = CalibrationState.values().firstOrNull { it.name == call.argument<String>("step") }
|
||||
val km = KeyManager.getInstance()
|
||||
if (step == null) {
|
||||
ctx.fail(result, "Unknown RC calibration step")
|
||||
} else if (km == null || rc() == null) {
|
||||
ctx.fail(result, "No remote controller connected")
|
||||
} else {
|
||||
km.setValue(RemoteControllerKey.create(RemoteControllerKey.CALIBRATION_STATE), step, 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) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
"watchRcHardware" -> {
|
||||
val on = call.argument<Boolean>("on") ?: false
|
||||
ctx.mainHandler.post {
|
||||
setRcHardware(on)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private inline fun withFc(result: MethodChannel.Result, block: (FlightController) -> Unit) {
|
||||
val f = fc()
|
||||
if (f == null) ctx.fail(result, "No aircraft connected") else block(f)
|
||||
}
|
||||
|
||||
private inline fun withCamera(result: MethodChannel.Result, block: (Camera) -> Unit) {
|
||||
val c = camera()
|
||||
if (c == null) ctx.fail(result, "No camera connected") else block(c)
|
||||
}
|
||||
|
||||
override fun bind(product: BaseProduct?) {
|
||||
if (product !is Aircraft) return
|
||||
watch.clear()
|
||||
val fc = product.flightController
|
||||
safe {
|
||||
fc?.setIMUStateCallback { s -> ctx.mainHandler.post { onImu(s) } }
|
||||
}
|
||||
safe {
|
||||
fc?.setGravityCenterStateCallback { s: GravityCenterState ->
|
||||
val v = mapOf("state" to s.calibrationState?.name, "error" to s.calibrationError?.description)
|
||||
ctx.mainHandler.post { put("gravityCenter", v) }
|
||||
}
|
||||
}
|
||||
safe {
|
||||
product.camera?.setCalibrationCallback(object : Camera.CalibrationCallback {
|
||||
override fun OnStateUpdate(state: DJICameraCalibrateState?) {
|
||||
ctx.mainHandler.post { patch("lens", mapOf("state" to state?.name)) }
|
||||
}
|
||||
|
||||
override fun OnResultUpdate(r: DJICameraCalibrateResult?) {
|
||||
ctx.mainHandler.post { patch("lens", mapOf("result" to r?.name)) }
|
||||
}
|
||||
})
|
||||
}
|
||||
watch.watch(key { CameraKey.create(CameraKey.SENSOR_CLEANING_STATE) }) { v -> put("sensorCleaning", KeyValues.toDart(v)) }
|
||||
watch.watch(key { CameraKey.create(CameraKey.IS_SENSOR_CLEANING_SUPPORTED) }) { v -> patch("support", mapOf("sensorCleaning" to (v as? Boolean))) }
|
||||
watch.watch(key { RemoteControllerKey.create(RemoteControllerKey.CALIBRATION_STATE) }) { v -> patch("rc", mapOf("state" to KeyValues.toDart(v))) }
|
||||
|
||||
readSupport()
|
||||
// Counts and key support are often unknown until the handshake finishes.
|
||||
ctx.mainHandler.postDelayed({ if (ctx.aircraft() === product) readSupport() }, 3000)
|
||||
}
|
||||
|
||||
/** What the wizard hub offers; a missing fact is unknown, not unsupported. */
|
||||
private fun readSupport() {
|
||||
val a = ctx.aircraft() ?: return
|
||||
val km = KeyManager.getInstance()
|
||||
val fc = a.flightController
|
||||
fun supported(k: DJIKey?): Boolean? = if (km == null || k == null) null else safe { km.isKeySupported(k) }
|
||||
patch(
|
||||
"support",
|
||||
mapOf(
|
||||
"compassCount" to safe { fc?.compassCount },
|
||||
"imuCount" to safe { fc?.imuCount },
|
||||
"gravityCenter" to supported(key { FlightControllerKey.create(FlightControllerKey.START_GRAVITY_CENTER_CALIBRATION) }),
|
||||
"propeller" to safe { fc?.isPropellerCalibrationSupported },
|
||||
"lens" to safe { a.camera?.isInterchangeableLensSupported },
|
||||
"rc" to (if (a.remoteController == null) false else supported(key { RemoteControllerKey.create(RemoteControllerKey.CALIBRATION_STATE) })),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onImu(s: IMUState) {
|
||||
val units = mutableListOf(s)
|
||||
s.subIMUState?.filterNotNull()?.let { units += it }
|
||||
for (u in units) {
|
||||
imuUnits[u.index] = mapOf(
|
||||
"index" to u.index,
|
||||
"state" to u.calibrationState?.name,
|
||||
"progress" to u.calibrationProgress,
|
||||
"gyroscope" to u.gyroscopeState?.name,
|
||||
"accelerometer" to u.accelerometerState?.name,
|
||||
"connected" to u.isConnected,
|
||||
)
|
||||
}
|
||||
val hint = s.multipleOrientationCalibrationHint
|
||||
put(
|
||||
"imu",
|
||||
mapOf(
|
||||
"units" to imuUnits.values.toList(),
|
||||
"multiSide" to s.isMultiSideCalibrationType,
|
||||
"orientationState" to hint?.state?.name,
|
||||
"toCalibrate" to hint?.orientationsToCalibrate?.map { it.name }?.sorted(),
|
||||
"calibrated" to hint?.orientationsCalibrated?.map { it.name }?.sorted(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** The RC wizard's live sticks and dials, plus the per-axis calibration keys. */
|
||||
private fun setRcHardware(on: Boolean) {
|
||||
if (on == rcHardwareOn) return
|
||||
rcHardwareOn = on
|
||||
val remote = rc()
|
||||
if (!on) {
|
||||
safe { remote?.setHardwareStateCallback(null) }
|
||||
rcWatch.clear()
|
||||
group.remove("rcHardware")
|
||||
publish()
|
||||
return
|
||||
}
|
||||
safe {
|
||||
remote?.setHardwareStateCallback { h ->
|
||||
val v = mapOf(
|
||||
"lx" to h.leftStick?.horizontalPosition,
|
||||
"ly" to h.leftStick?.verticalPosition,
|
||||
"rx" to h.rightStick?.horizontalPosition,
|
||||
"ry" to h.rightStick?.verticalPosition,
|
||||
"leftDial" to h.leftDial,
|
||||
"rightDial" to h.rightDial?.value,
|
||||
)
|
||||
ctx.mainHandler.post { if (rcHardwareOn) put("rcHardware", v) }
|
||||
}
|
||||
}
|
||||
rcWatch.watch(key { RemoteControllerKey.create(RemoteControllerKey.REMOTE_CONTROLLER_CALIBRATION_NUMBER_OF_SEGMENT) }) { v ->
|
||||
patch("rc", mapOf("segments" to KeyValues.toDart(v)))
|
||||
}
|
||||
for (axis in "ABCDEFGH") {
|
||||
val name = "REMOTE_CONTROLLER_CALIBRATION_${axis}_AXIS_STATUS"
|
||||
val param = safe { RemoteControllerKey::class.java.getField(name).get(null) as? String } ?: continue
|
||||
rcWatch.watch(key { RemoteControllerKey.create(param) }) { v ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val rcGroup = group["rc"] as? Map<String, Any?>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val axes = (rcGroup?.get("axes") as? Map<String, Any?>).orEmpty()
|
||||
patch("rc", mapOf("axes" to axes + (axis.toString() to KeyValues.toDart(v))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main thread only.
|
||||
private fun put(k: String, v: Any?) {
|
||||
if (group[k] == v && group.containsKey(k)) return
|
||||
group[k] = v
|
||||
publish()
|
||||
}
|
||||
|
||||
private fun patch(k: String, values: Map<String, Any?>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val current = (group[k] as? Map<String, Any?>).orEmpty()
|
||||
put(k, current + values)
|
||||
}
|
||||
|
||||
private fun publish() = ctx.hub.putFast(mapOf("calibration" to LinkedHashMap(group)))
|
||||
|
||||
override fun reset() {
|
||||
watch.clear()
|
||||
rcWatch.clear()
|
||||
rcHardwareOn = false
|
||||
imuUnits.clear()
|
||||
group.clear()
|
||||
}
|
||||
|
||||
private inline fun key(make: () -> DJIKey?): DJIKey? = try {
|
||||
make()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
private inline fun <T> safe(block: () -> T): T? = try {
|
||||
block()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import io.flutter.plugin.common.MethodChannel
|
||||
* helpers ([FlightControllerBridge], [BatteryBridge], [AirLinkBridge],
|
||||
* [FlightAssistantBridge], [DiagnosticsBridge], [CameraBridge], [GimbalBridge],
|
||||
* [MissionBridge], [MediaBridge], [AccountBridge], [ProductProfileBridge],
|
||||
* [KeyBridge]).
|
||||
* [KeyBridge], [CalibrationBridge]).
|
||||
* - [EVENT_CHANNEL] streams every subsystem's async updates to Dart. Each event
|
||||
* is a map carrying a `type` key. Telemetry from every subsystem is batched by
|
||||
* the shared [TelemetryHub] into one `telemetry` event per tick.
|
||||
@@ -110,6 +110,7 @@ class DjiSdkBridge(
|
||||
AccountBridge(ctx),
|
||||
ProductProfileBridge(ctx),
|
||||
KeyBridge(ctx),
|
||||
CalibrationBridge(ctx),
|
||||
)
|
||||
|
||||
init {
|
||||
|
||||
@@ -76,18 +76,6 @@ class FlightControllerBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
it.setHomeLocation(LocationCoordinate2D(lat, lon), ctx.completion(result))
|
||||
}
|
||||
}
|
||||
// Calibration starts (Aircraft Status sheet); the guided wizards are P6b.
|
||||
"startCompassCalibration" -> guard(controller, result) {
|
||||
val compass = it.compass
|
||||
if (compass == null) ctx.fail(result, "No compass on this aircraft")
|
||||
else compass.startCalibration(ctx.completion(result))
|
||||
}
|
||||
"stopCompassCalibration" -> guard(controller, result) {
|
||||
val compass = it.compass
|
||||
if (compass == null) ctx.fail(result, "No compass on this aircraft")
|
||||
else compass.stopCalibration(ctx.completion(result))
|
||||
}
|
||||
"startIMUCalibration" -> guard(controller, result) { it.startIMUCalibration(ctx.completion(result)) }
|
||||
"setMaxFlightHeight" -> guard(controller, result) {
|
||||
it.setMaxFlightHeight((call.argument<Int>("value") ?: 120), ctx.completion(result))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user