Give the Fly App GO 4-style intelligent modes and a Waypoints editor
P7a of the GO 4-parity plan. The rail's GRID button becomes MODES, which opens a picker of the product's intelligent flight modes with first-use terms; Waypoints is the first one built. Waypoints opens on the big map: tap to add waypoints or POIs, long-press to drag, "+" to insert on a leg, and a point editor for altitude, speed, heading (route / custom / POI), gimbal (set or aim at POI), camera action and hover, and turn radius, with a swipe-to-adjust readout. Route settings, a local mission library and undo sit in a tool column. Start runs the SafetyGate's pre-start check in the slide-to-confirm dialog, then uploads and starts; a run panel shows upload and flight progress with Pause, Resume and Stop. MissionBridge now listens to the waypoint operator and builds the full V1 mission (per-waypoint headings, turn direction, gimbal pitch, actions). 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
a892bfb209
commit
43a66df8df
22 files changed
+4276
-83
No files matched your search
@@ -1,22 +1,49 @@
|
||||
package com.pilotvault.flyapp
|
||||
|
||||
import android.graphics.RectF
|
||||
import dji.common.error.DJIError
|
||||
import dji.common.mission.activetrack.ActiveTrackMission
|
||||
import dji.common.mission.activetrack.ActiveTrackMode
|
||||
import dji.common.mission.activetrack.QuickShotMode
|
||||
import dji.common.mission.waypoint.Waypoint
|
||||
import dji.common.mission.waypoint.WaypointAction
|
||||
import dji.common.mission.waypoint.WaypointActionType
|
||||
import dji.common.mission.waypoint.WaypointMission
|
||||
import dji.common.mission.waypoint.WaypointMissionDownloadEvent
|
||||
import dji.common.mission.waypoint.WaypointMissionExecutionEvent
|
||||
import dji.common.mission.waypoint.WaypointMissionFinishedAction
|
||||
import dji.common.mission.waypoint.WaypointMissionFlightPathMode
|
||||
import dji.common.mission.waypoint.WaypointMissionGotoWaypointMode
|
||||
import dji.common.mission.waypoint.WaypointMissionHeadingMode
|
||||
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.ActiveTrackOperator
|
||||
import dji.sdk.mission.waypoint.WaypointMissionOperator
|
||||
import dji.sdk.mission.waypoint.WaypointMissionOperatorListener
|
||||
import dji.sdk.sdkmanager.DJISDKManager
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* Intelligent-flight & waypoint missions.
|
||||
* Intelligent-flight & waypoint missions (plan P7).
|
||||
*
|
||||
* - Waypoint route (from the Map screen) → [WaypointMissionOperator].
|
||||
* - Waypoints → [WaypointMissionOperator]. `uploadWaypointMission` takes the
|
||||
* resolved mission the Dart editor builds (`WaypointMissionSpec.toBridge`):
|
||||
*
|
||||
* ```
|
||||
* {waypoints: [{lat, lon, altitude, heading?, turnMode, speed, cornerRadius,
|
||||
* gimbalPitch?, actions: [{type: WaypointActionType, param}]}],
|
||||
* autoFlightSpeed, maxFlightSpeed, finishedAction, headingMode,
|
||||
* flightPathMode, gotoFirstWaypointMode, exitMissionOnRCSignalLost,
|
||||
* gimbalPitchRotation, repeatTimes, pointOfInterest?: {lat, lon}}
|
||||
* ```
|
||||
*
|
||||
* The operator's listener is published as `mission` events with
|
||||
* `operator: 'waypoint'`: `state` (WaypointMissionState), `upload`
|
||||
* {index, total, summary}, `execution` {target, reached, executeState,
|
||||
* total}, `event` (`start` / `finish`) and `error`.
|
||||
* - ActiveTrack subject tracking → [ActiveTrackOperator].
|
||||
* - DJI-Fly QuickShots (Dronie/Rocket/Circle/Helix/Boomerang/Asteroid) → an
|
||||
* ActiveTrack mission in [ActiveTrackMode.QUICK_SHOT] with the matching
|
||||
@@ -30,13 +57,88 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
private fun trackOp(): ActiveTrackOperator? =
|
||||
DJISDKManager.getInstance().missionControl?.activeTrackOperator
|
||||
|
||||
private var listenedOp: WaypointMissionOperator? = null
|
||||
|
||||
private val waypointListener = object : WaypointMissionOperatorListener {
|
||||
override fun onDownloadUpdate(event: WaypointMissionDownloadEvent) {}
|
||||
|
||||
override fun onUploadUpdate(event: WaypointMissionUploadEvent) {
|
||||
val p = event.progress
|
||||
emitWaypoint(
|
||||
state = event.currentState?.name,
|
||||
upload = p?.let { mapOf("index" to it.uploadedWaypointIndex, "total" to it.totalWaypointCount, "summary" to it.isSummaryUploaded) },
|
||||
error = event.error,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onExecutionUpdate(event: WaypointMissionExecutionEvent) {
|
||||
val p = event.progress
|
||||
emitWaypoint(
|
||||
state = event.currentState?.name,
|
||||
execution = p?.let {
|
||||
mapOf(
|
||||
"target" to it.targetWaypointIndex,
|
||||
"reached" to it.isWaypointReached,
|
||||
"executeState" to it.executeState?.name,
|
||||
"total" to it.totalWaypointCount,
|
||||
)
|
||||
},
|
||||
error = event.error,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onExecutionStart() {
|
||||
emitWaypoint(state = waypointOp()?.currentState?.name, event = "start")
|
||||
}
|
||||
|
||||
override fun onExecutionFinish(error: DJIError?) {
|
||||
emitWaypoint(state = waypointOp()?.currentState?.name, event = "finish", error = error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitWaypoint(
|
||||
state: String?,
|
||||
upload: Map<String, Any?>? = null,
|
||||
execution: Map<String, Any?>? = null,
|
||||
event: String? = null,
|
||||
error: DJIError? = null,
|
||||
) {
|
||||
val m = linkedMapOf<String, Any?>("type" to "mission", "operator" to "waypoint")
|
||||
if (state != null) m["state"] = state
|
||||
if (upload != null) m["upload"] = upload
|
||||
if (execution != null) m["execution"] = execution
|
||||
if (event != null) m["event"] = event
|
||||
if (error != null) m["error"] = error.description
|
||||
ctx.emit(m)
|
||||
}
|
||||
|
||||
override fun bind(product: BaseProduct?) {
|
||||
val op = waypointOp() ?: return
|
||||
if (listenedOp !== op) {
|
||||
listenedOp?.removeListener(waypointListener)
|
||||
op.addListener(waypointListener)
|
||||
listenedOp = op
|
||||
}
|
||||
emitWaypoint(state = op.currentState?.name)
|
||||
}
|
||||
|
||||
override fun handle(call: MethodCall, result: MethodChannel.Result): Boolean {
|
||||
when (call.method) {
|
||||
"uploadWaypointMission" -> uploadWaypoint(call, result)
|
||||
"getWaypointMissionState" -> {
|
||||
val op = waypointOp()
|
||||
result.success(op?.currentState?.name)
|
||||
if (op != null) emitWaypoint(state = op.currentState?.name)
|
||||
}
|
||||
"startWaypointMission" -> waypoint(result) { it.startMission(ctx.completion(result)) }
|
||||
"stopWaypointMission" -> waypoint(result) { it.stopMission(ctx.completion(result)) }
|
||||
"pauseWaypointMission" -> waypoint(result) { it.pauseMission(ctx.completion(result)) }
|
||||
"resumeWaypointMission" -> waypoint(result) { it.resumeMission(ctx.completion(result)) }
|
||||
"cancelWaypointUpload" -> waypoint(result) { it.cancelUploadingMission(ctx.completion(result)) }
|
||||
"setWaypointAutoFlightSpeed" -> waypoint(result) {
|
||||
val speed = (call.argument<Number>("speed") ?: 8).toFloat()
|
||||
it.setAutoFlightSpeed(speed, ctx.completion(result))
|
||||
}
|
||||
"startActiveTrack" -> startActiveTrack(call, result)
|
||||
"stopActiveTrack" -> track(result) { it.stopTracking(ctx.completion(result)) }
|
||||
"startQuickShot" -> startQuickShot(call, result)
|
||||
@@ -55,41 +157,78 @@ class MissionBridge(private val ctx: BridgeCtx) : SubBridge {
|
||||
block(op)
|
||||
}
|
||||
|
||||
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 {
|
||||
java.lang.Enum.valueOf(T::class.java, raw)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadWaypoint(call: MethodCall, result: MethodChannel.Result) {
|
||||
val op = waypointOp() ?: run { ctx.fail(result, "Waypoint missions unavailable"); return }
|
||||
val raw = call.argument<List<Map<String, Any>>>("points") ?: emptyList()
|
||||
if (raw.size < 2) {
|
||||
val raw = call.argument<List<Map<String, Any?>>>("waypoints") ?: emptyList()
|
||||
if (raw.size < WaypointMission.MIN_WAYPOINT_COUNT) {
|
||||
ctx.fail(result, "A route needs at least 2 waypoints")
|
||||
return
|
||||
}
|
||||
val speed = (call.argument<Double>("speed") ?: 8.0).toFloat()
|
||||
val finish = try {
|
||||
WaypointMissionFinishedAction.valueOf(call.argument<String>("finishAction") ?: "NO_ACTION")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
WaypointMissionFinishedAction.NO_ACTION
|
||||
|
||||
val waypoints = ArrayList<Waypoint>(raw.size)
|
||||
for (p in raw) {
|
||||
val wp = Waypoint(
|
||||
(p["lat"] as Number).toDouble(),
|
||||
(p["lon"] as Number).toDouble(),
|
||||
(p["altitude"] as? Number)?.toFloat() ?: 50f,
|
||||
)
|
||||
(p["heading"] as? Number)?.let { wp.heading = it.toInt() }
|
||||
wp.turnMode = if (p["turnMode"] == "COUNTER_CLOCKWISE") WaypointTurnMode.COUNTER_CLOCKWISE else WaypointTurnMode.CLOCKWISE
|
||||
(p["speed"] as? Number)?.let { wp.speed = it.toFloat() }
|
||||
(p["cornerRadius"] as? Number)?.let { wp.cornerRadiusInMeters = it.toFloat() }
|
||||
(p["gimbalPitch"] as? Number)?.let { wp.gimbalPitch = it.toFloat() }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val actions = p["actions"] as? List<Map<String, Any?>> ?: emptyList()
|
||||
for (a in actions) {
|
||||
val type = try {
|
||||
WaypointActionType.valueOf(a["type"] as String)
|
||||
} catch (_: Exception) {
|
||||
continue
|
||||
}
|
||||
wp.addAction(WaypointAction(type, (a["param"] as? Number)?.toInt() ?: 0))
|
||||
}
|
||||
waypoints.add(wp)
|
||||
}
|
||||
|
||||
val waypoints = raw.map { p ->
|
||||
val lat = (p["lat"] as Number).toDouble()
|
||||
val lon = (p["lon"] as Number).toDouble()
|
||||
val alt = (p["altitude"] as? Number)?.toFloat() ?: 50f
|
||||
Waypoint(lat, lon, alt)
|
||||
}
|
||||
|
||||
val mission = WaypointMission.Builder()
|
||||
.autoFlightSpeed(speed)
|
||||
.maxFlightSpeed(maxOf(speed, 10f))
|
||||
.finishedAction(finish)
|
||||
.waypointList(ArrayList(waypoints))
|
||||
val headingMode = enumArg(call, "headingMode", WaypointMissionHeadingMode.AUTO)
|
||||
val builder = WaypointMission.Builder()
|
||||
.autoFlightSpeed((call.argument<Number>("autoFlightSpeed") ?: 8).toFloat())
|
||||
.maxFlightSpeed((call.argument<Number>("maxFlightSpeed") ?: 15).toFloat())
|
||||
.finishedAction(enumArg(call, "finishedAction", WaypointMissionFinishedAction.NO_ACTION))
|
||||
.headingMode(headingMode)
|
||||
.flightPathMode(enumArg(call, "flightPathMode", WaypointMissionFlightPathMode.NORMAL))
|
||||
.gotoFirstWaypointMode(enumArg(call, "gotoFirstWaypointMode", WaypointMissionGotoWaypointMode.SAFELY))
|
||||
.setExitMissionOnRCSignalLostEnabled(call.argument<Boolean>("exitMissionOnRCSignalLost") ?: true)
|
||||
.setGimbalPitchRotationEnabled(call.argument<Boolean>("gimbalPitchRotation") ?: false)
|
||||
.repeatTimes(call.argument<Number>("repeatTimes")?.toInt() ?: 1)
|
||||
.waypointList(waypoints)
|
||||
.waypointCount(waypoints.size)
|
||||
.build()
|
||||
call.argument<Map<String, Any?>>("pointOfInterest")?.let { poi ->
|
||||
builder.setPointOfInterest(
|
||||
LocationCoordinate2D((poi["lat"] as Number).toDouble(), (poi["lon"] as Number).toDouble()),
|
||||
)
|
||||
}
|
||||
|
||||
val loadErr = op.loadMission(mission)
|
||||
builder.checkParameters()?.let {
|
||||
ctx.fail(result, it.description)
|
||||
return
|
||||
}
|
||||
val loadErr = op.loadMission(builder.build())
|
||||
if (loadErr != null) {
|
||||
ctx.fail(result, loadErr.description)
|
||||
return
|
||||
}
|
||||
ctx.emit(mapOf("type" to "mission", "state" to "uploading"))
|
||||
// Completes once every waypoint is on the aircraft; progress arrives
|
||||
// through the listener meanwhile.
|
||||
op.uploadMission(ctx.completion(result))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user