Files
PilotVault/Fly App/lib/fly/action_rail_state.dart
T
tajniak81andClaude Opus 5.5 63acacced7 Fix what the first Fly App simulator runs on a Mavic Pro turned up
- 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>
2026-10-21 20:27:05 +02:00

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)';
}