Give the Fly App GO 4-style ActiveTrack and TapFly
P7b of the GO 4-parity plan. Both modes open from the MODES picker on the live video, where they take over the touches the camera and gimbal used. ActiveTrack: drag a box round the subject (or tap it), then GO. The operator's own box follows on the video, coloured by confidence, with confirm / reject when it asks and the reason when it cannot follow. Trace, Profile and Spotlight switch live from the mode panel. TapFly: tap where to fly, then GO; the aircraft's heading point and any obstacle bypass show while it flies, and the speed changes live. Forward only on aircraft without rear sensors; Backward and Free otherwise. Both go through new SafetyGate checks (flying, P mode, above 2 m, nothing else in control, battery), start on GO when the check is clean and ask for the slide when it warns. Stop on the rail and Exit stop them. MissionBridge now listens to the ActiveTrack and TapFly operators. Not yet run on a simulator or aircraft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43a66df8df
commit
2c908cf17e
13 files changed
+1519
-12
No files matched your search
@@ -1,10 +1,14 @@
|
||||
package com.pilotvault.flyapp
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.graphics.RectF
|
||||
import dji.common.error.DJIError
|
||||
import dji.common.mission.activetrack.ActiveTrackMission
|
||||
import dji.common.mission.activetrack.ActiveTrackMissionEvent
|
||||
import dji.common.mission.activetrack.ActiveTrackMode
|
||||
import dji.common.mission.activetrack.QuickShotMode
|
||||
import dji.common.mission.tapfly.TapFlyMission
|
||||
import dji.common.mission.tapfly.TapFlyMode
|
||||
import dji.common.mission.waypoint.Waypoint
|
||||
import dji.common.mission.waypoint.WaypointAction
|
||||
import dji.common.mission.waypoint.WaypointActionType
|
||||
@@ -19,7 +23,11 @@ import dji.common.mission.waypoint.WaypointMissionUploadEvent
|
||||
import dji.common.mission.waypoint.WaypointTurnMode
|
||||
import dji.common.model.LocationCoordinate2D
|
||||
import dji.sdk.base.BaseProduct
|
||||
import dji.sdk.mission.activetrack.ActiveTrackMissionOperatorListener
|
||||
import dji.sdk.mission.activetrack.ActiveTrackOperator
|
||||
import dji.sdk.mission.tapfly.TapFlyMissionEvent
|
||||
import dji.sdk.mission.tapfly.TapFlyMissionOperator
|
||||
import dji.sdk.mission.tapfly.TapFlyMissionOperatorListener
|
||||
import dji.sdk.mission.waypoint.WaypointMissionOperator
|
||||
import dji.sdk.mission.waypoint.WaypointMissionOperatorListener
|
||||
import dji.sdk.sdkmanager.DJISDKManager
|
||||
@@ -44,7 +52,18 @@ import io.flutter.plugin.common.MethodChannel
|
||||
* `operator: 'waypoint'`: `state` (WaypointMissionState), `upload`
|
||||
* {index, total, summary}, `execution` {target, reached, executeState,
|
||||
* total}, `event` (`start` / `finish`) and `error`.
|
||||
* - ActiveTrack subject tracking → [ActiveTrackOperator].
|
||||
* - ActiveTrack subject tracking → [ActiveTrackOperator]. Its listener goes
|
||||
* out as `mission` events with `operator: 'activeTrack'`: `state`
|
||||
* (ActiveTrackState), `tracking` {targetState, targetType, rect: {l, t, r, b}
|
||||
* normalised to the live view, index, percent, reason}, `error`. Methods:
|
||||
* `startActiveTrack {x, y, w, h, mode}` (checked with `canStartTracking`
|
||||
* first), `acceptActiveTrackConfirmation`, `rejectActiveTrackConfirmation`,
|
||||
* `switchActiveTrackMode {mode}`, `setActiveTrackRetreat {on}`, `stopActiveTrack`.
|
||||
* - TapFly → [TapFlyMissionOperator]: `mission` events with `operator:
|
||||
* 'tapFly'`: `state` (TapFlyMissionState), `execution` {imageX, imageY,
|
||||
* bypass, relativeHeading, speed}, `error`. Methods: `startTapFly {x, y,
|
||||
* mode, speed, avoidance}` (x / y normalised), `stopTapFly`,
|
||||
* `setTapFlySpeed {speed}`, `resetTapFlyHeading`.
|
||||
* - DJI-Fly QuickShots (Dronie/Rocket/Circle/Helix/Boomerang/Asteroid) → an
|
||||
* ActiveTrack mission in [ActiveTrackMode.QUICK_SHOT] with the matching
|
||||
* [QuickShotMode]. Unsupported modes on a given aircraft fail cleanly.
|
||||
@@ -57,7 +76,49 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
private fun trackOp(): ActiveTrackOperator? =
|
||||
DJISDKManager.getInstance().missionControl?.activeTrackOperator
|
||||
|
||||
private fun tapFlyOp(): TapFlyMissionOperator? =
|
||||
DJISDKManager.getInstance().missionControl?.tapFlyMissionOperator
|
||||
|
||||
private var listenedOp: WaypointMissionOperator? = null
|
||||
private var listenedTrack: ActiveTrackOperator? = null
|
||||
private var listenedTapFly: TapFlyMissionOperator? = null
|
||||
|
||||
private fun rect(r: RectF?): Map<String, Any?>? =
|
||||
r?.let { mapOf("l" to it.left.toDouble(), "t" to it.top.toDouble(), "r" to it.right.toDouble(), "b" to it.bottom.toDouble()) }
|
||||
|
||||
private val trackListener = ActiveTrackMissionOperatorListener { event: ActiveTrackMissionEvent ->
|
||||
val m = linkedMapOf<String, Any?>("type" to "mission", "operator" to "activeTrack")
|
||||
event.currentState?.let { m["state"] = it.name }
|
||||
event.trackingState?.let { t ->
|
||||
m["tracking"] = mapOf(
|
||||
"targetState" to t.state?.name,
|
||||
"targetType" to t.type?.name,
|
||||
"rect" to rect(t.targetRect),
|
||||
"index" to t.targetIndex,
|
||||
"percent" to t.completionPercentage,
|
||||
"reason" to t.reason?.name,
|
||||
)
|
||||
}
|
||||
event.error?.let { m["error"] = it.description }
|
||||
ctx.emit(m)
|
||||
}
|
||||
|
||||
private val tapFlyListener = TapFlyMissionOperatorListener { event: TapFlyMissionEvent? ->
|
||||
if (event == null) return@TapFlyMissionOperatorListener
|
||||
val m = linkedMapOf<String, Any?>("type" to "mission", "operator" to "tapFly")
|
||||
event.currentState?.let { m["state"] = it.name }
|
||||
event.executionState?.let { x ->
|
||||
m["execution"] = mapOf(
|
||||
"imageX" to x.imageLocation?.x?.toDouble(),
|
||||
"imageY" to x.imageLocation?.y?.toDouble(),
|
||||
"bypass" to x.bypassDirection?.name,
|
||||
"relativeHeading" to x.relativeHeading.toDouble(),
|
||||
"speed" to x.speed.toDouble(),
|
||||
)
|
||||
}
|
||||
event.error?.let { m["error"] = it.description }
|
||||
ctx.emit(m)
|
||||
}
|
||||
|
||||
private val waypointListener = object : WaypointMissionOperatorListener {
|
||||
override fun onDownloadUpdate(event: WaypointMissionDownloadEvent) {}
|
||||
@@ -113,6 +174,21 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
}
|
||||
|
||||
override fun bind(product: BaseProduct?) {
|
||||
trackOp()?.let { op ->
|
||||
if (listenedTrack !== op) {
|
||||
listenedTrack?.removeListener(trackListener)
|
||||
op.addListener(trackListener)
|
||||
listenedTrack = op
|
||||
}
|
||||
ctx.emit(mapOf("type" to "mission", "operator" to "activeTrack", "state" to op.currentState?.name))
|
||||
}
|
||||
tapFlyOp()?.let { op ->
|
||||
if (listenedTapFly !== op) {
|
||||
listenedTapFly?.removeListener(tapFlyListener)
|
||||
op.addListener(tapFlyListener)
|
||||
listenedTapFly = op
|
||||
}
|
||||
}
|
||||
val op = waypointOp() ?: return
|
||||
if (listenedOp !== op) {
|
||||
listenedOp?.removeListener(waypointListener)
|
||||
@@ -141,7 +217,21 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
}
|
||||
"startActiveTrack" -> startActiveTrack(call, result)
|
||||
"stopActiveTrack" -> track(result) { it.stopTracking(ctx.completion(result)) }
|
||||
"acceptActiveTrackConfirmation" -> track(result) { it.acceptConfirmation(ctx.completion(result)) }
|
||||
"rejectActiveTrackConfirmation" -> track(result) { it.rejectConfirmation(ctx.completion(result)) }
|
||||
"switchActiveTrackMode" -> track(result) {
|
||||
it.switchMode(enumArg(call, "mode", ActiveTrackMode.TRACE), ctx.completion(result))
|
||||
}
|
||||
"setActiveTrackRetreat" -> track(result) {
|
||||
it.setRetreatEnabled(call.argument<Boolean>("on") ?: false, ctx.completion(result))
|
||||
}
|
||||
"startQuickShot" -> startQuickShot(call, result)
|
||||
"startTapFly" -> startTapFly(call, result)
|
||||
"stopTapFly" -> tapFly(result) { it.stopMission(ctx.completion(result)) }
|
||||
"setTapFlySpeed" -> tapFly(result) {
|
||||
it.setAutoFlightSpeed((call.argument<Number>("speed") ?: 5).toFloat(), ctx.completion(result))
|
||||
}
|
||||
"resetTapFlyHeading" -> tapFly(result) { it.resetHeading(ctx.completion(result)) }
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
@@ -157,6 +247,24 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
block(op)
|
||||
}
|
||||
|
||||
private inline fun tapFly(result: MethodChannel.Result, block: (TapFlyMissionOperator) -> Unit) {
|
||||
val op = tapFlyOp() ?: run { ctx.fail(result, "TapFly unavailable"); return }
|
||||
block(op)
|
||||
}
|
||||
|
||||
private fun startTapFly(call: MethodCall, result: MethodChannel.Result) {
|
||||
val op = tapFlyOp() ?: run { ctx.fail(result, "TapFly unavailable"); return }
|
||||
val mission = TapFlyMission()
|
||||
mission.tapFlyMode = enumArg(call, "mode", TapFlyMode.FORWARD)
|
||||
mission.speed = (call.argument<Number>("speed") ?: 5).toFloat()
|
||||
mission.isHorizontalObstacleAvoidanceEnabled = call.argument<Boolean>("avoidance") ?: true
|
||||
mission.target = PointF(
|
||||
(call.argument<Number>("x") ?: 0.5).toFloat(),
|
||||
(call.argument<Number>("y") ?: 0.5).toFloat(),
|
||||
)
|
||||
op.startMission(mission, ctx.completion(result))
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> enumArg(call: MethodCall, name: String, fallback: T): T {
|
||||
val raw = call.argument<String>(name) ?: return fallback
|
||||
return try {
|
||||
@@ -244,7 +352,11 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
ActiveTrackMode.TRACE
|
||||
}
|
||||
val mission = ActiveTrackMission(RectF(x, y, x + w, y + h), mode)
|
||||
ctx.emit(mapOf("type" to "mission", "state" to "tracking"))
|
||||
op.canStartTracking(mission)?.let {
|
||||
ctx.fail(result, it.description)
|
||||
return
|
||||
}
|
||||
// The operator's listener reports the tracking from here on.
|
||||
op.startTracking(mission, ctx.completion(result))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user