- Virtual sticks come from the product matrix: the Mavic Pro answers isVirtualStickControlModeAvailable = false even in flight, which hid the STICKS button. - While landing, the rail's second slot belongs to the landing (the Mavic Pro keeps isGoingHome set through a plain Land, so it showed Cancel RTH beside Confirm landing); Cancel landing is allowed at the confirmation. - Rail buttons have a fixed width so long labels wrap instead of pushing the rail onto the gimbal slider. - Outlined HUD buttons get a transparent background (the app theme filled them white behind white text). Verified in the simulator: take-off, RTH, cancel RTH, land, cancel and confirm landing, and the virtual-stick axes (roll = forward, pitch = right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.6 KiB
Dart
118 lines
4.6 KiB
Dart
import 'state/aircraft_state.dart';
|
|
import 'state/mission_state.dart';
|
|
|
|
/// Every command the flight screen can send that moves the aircraft (or stops
|
|
/// it moving). All of them go through `SafetyGate`.
|
|
enum FlightCommand { takeOff, land, cancelLanding, confirmLanding, goHome, cancelGoHome, stopMode }
|
|
|
|
/// What is running when the rail offers [FlightCommand.stopMode], i.e. which
|
|
/// operator "Stop" has to address.
|
|
enum ActiveMode { none, waypoint, activeTrack, tapFly, hotpoint, followMe, other }
|
|
|
|
/// The left rail's slots for the current flight state (plan P3, GO 4's
|
|
/// `fpv_leftmenu`): one primary button that walks Take-off → Land → Cancel
|
|
/// landing / Confirm landing, an RTH slot that turns into Cancel RTH, and a
|
|
/// Stop button while an intelligent mode flies the aircraft.
|
|
///
|
|
/// Pure: read off the flight controller and mission state, so the whole state
|
|
/// machine is unit-tested without a widget.
|
|
class RailState {
|
|
const RailState({required this.primary, this.rth, this.mode = ActiveMode.none, this.enabled = true});
|
|
|
|
/// Take-off, Land, Cancel landing or Confirm landing.
|
|
final FlightCommand primary;
|
|
|
|
/// Go home or Cancel RTH; Cancel landing while waiting for a landing
|
|
/// confirmation; null on the ground and during an auto-landing.
|
|
final FlightCommand? rth;
|
|
|
|
/// The intelligent mode a Stop button would end; [ActiveMode.none] hides it.
|
|
final ActiveMode mode;
|
|
|
|
/// False until the flight controller has reported: the buttons show but do nothing.
|
|
final bool enabled;
|
|
|
|
bool get showStop => mode != ActiveMode.none;
|
|
|
|
/// Flight-controller modes in which an intelligent mode is flying the aircraft.
|
|
static const Set<String> _waypointModes = <String>{'GPS_WAYPOINT'};
|
|
static const Set<String> _trackModes = <String>{'ACTIVE_TRACK', 'ACTIVE_TRACK_SPOTLIGHT'};
|
|
static const Set<String> _otherModes = <String>{'DRAW', 'TERRAIN_FOLLOW'};
|
|
|
|
static RailState of({required bool connected, required AircraftState a, MissionState? mission}) {
|
|
final bool reported = connected && a.updatedAt != null;
|
|
final bool airborne = a.isFlying == true;
|
|
final String mode = a.flightModeEnum ?? '';
|
|
final bool goingHome = a.goingHome == true || mode == 'GO_HOME';
|
|
|
|
final bool confirming = a.landingConfirmationNeeded == true || mode == 'CONFIRM_LANDING';
|
|
final bool landing = confirming || mode == 'AUTO_LANDING';
|
|
|
|
final FlightCommand primary;
|
|
if (confirming) {
|
|
primary = FlightCommand.confirmLanding;
|
|
} else if (mode == 'AUTO_LANDING') {
|
|
primary = FlightCommand.cancelLanding;
|
|
} else if (airborne) {
|
|
primary = FlightCommand.land;
|
|
} else {
|
|
primary = FlightCommand.takeOff;
|
|
}
|
|
|
|
// While landing, the second slot belongs to the landing: the Mavic Pro keeps
|
|
// `isGoingHome` set through a plain Land's descent (seen in the simulator,
|
|
// 2026-10-21), which used to put "Cancel RTH" beside Confirm landing.
|
|
final FlightCommand? rth;
|
|
if (!airborne) {
|
|
rth = null;
|
|
} else if (landing) {
|
|
rth = confirming ? FlightCommand.cancelLanding : null;
|
|
} else {
|
|
rth = goingHome ? FlightCommand.cancelGoHome : FlightCommand.goHome;
|
|
}
|
|
|
|
return RailState(
|
|
primary: primary,
|
|
rth: rth,
|
|
mode: airborne ? activeMode(a, mission) : ActiveMode.none,
|
|
enabled: reported,
|
|
);
|
|
}
|
|
|
|
/// The intelligent mode in control, from the bridge's mission state first
|
|
/// (it knows which operator it started) and the flight mode second.
|
|
static ActiveMode activeMode(AircraftState a, MissionState? mission) {
|
|
switch (mission?.state) {
|
|
case 'executing':
|
|
return ActiveMode.waypoint;
|
|
case 'tracking':
|
|
case 'quickshot':
|
|
return ActiveMode.activeTrack;
|
|
case 'tapfly':
|
|
return ActiveMode.tapFly;
|
|
case 'hotpoint':
|
|
return ActiveMode.hotpoint;
|
|
case 'followme':
|
|
return ActiveMode.followMe;
|
|
}
|
|
final String mode = a.flightModeEnum ?? '';
|
|
if (_waypointModes.contains(mode)) return ActiveMode.waypoint;
|
|
if (_trackModes.contains(mode)) return ActiveMode.activeTrack;
|
|
if (mode == 'TAP_FLY') return ActiveMode.tapFly;
|
|
if (mode == 'GPS_HOT_POINT') return ActiveMode.hotpoint;
|
|
if (mode == 'GPS_FOLLOW_ME') return ActiveMode.followMe;
|
|
if (_otherModes.contains(mode)) return ActiveMode.other;
|
|
return ActiveMode.none;
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is RailState && other.primary == primary && other.rth == rth && other.mode == mode && other.enabled == enabled;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(primary, rth, mode, enabled);
|
|
|
|
@override
|
|
String toString() => 'RailState($primary, rth: $rth, mode: $mode, enabled: $enabled)';
|
|
}
|