Files
PilotVault/Fly App/lib/fly/state/aircraft_state.dart
T
tajniak81andClaude Opus 5.5 636da1ac2c Give the Fly App every remaining GO 4 flight mode and the missing settings
Intelligent modes: Point of Interest (orbit on the map, live radius and
speed), Follow Me (follows the phone), QuickShots on a boxed subject,
Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and
Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and
Circle. Every mode has a first-use tutorial and stops from the rail.
Waypoints can resume an interrupted mission, change cruise speed in
flight and check terrain clearance (Open-Meteo, on tap).

Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI
output, a link spectrum chart, Wi-Fi name and password, aircraft name,
video cache, battery warning history and pairing, non-smart cell
voltages. Calibration: compass interference meter, per-IMU calibration,
lens infinity focus and a first-manual-focus prompt. Also an RTK chip,
gimbal movement presets and motor tuning, Precision Takeoff gated on the
flight controller, and the precise GEO database's server version.

Built and unit/widget-tested only; nothing has flown yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:06:16 +02:00

280 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/foundation.dart';
import 'parse.dart';
/// Return-to-home budget from the flight controller (`GoHomeAssessment`):
/// what the smart battery bar draws its markers and remaining time from.
@immutable
class GoHomeAssessment {
const GoHomeAssessment({
this.remainingFlightTime,
this.timeNeededToGoHome,
this.timeNeededToLand,
this.batteryNeededToGoHome,
this.batteryNeededToLand,
this.maxRadiusCanFlyAndGoHome,
this.smartRTHState,
this.smartRTHCountdown,
});
factory GoHomeAssessment.fromMap(Map<String, dynamic> m) => GoHomeAssessment(
remainingFlightTime: asInt(m['remainingFlightTime']),
timeNeededToGoHome: asInt(m['timeNeededToGoHome']),
timeNeededToLand: asInt(m['timeNeededToLand']),
batteryNeededToGoHome: asInt(m['batteryNeededToGoHome']),
batteryNeededToLand: asInt(m['batteryNeededToLand']),
maxRadiusCanFlyAndGoHome: finite(m['maxRadiusCanFlyAndGoHome']),
smartRTHState: asString(m['smartRTHState']),
smartRTHCountdown: asInt(m['smartRTHCountdown']),
);
/// Seconds of flight left on the current battery.
final int? remainingFlightTime;
final int? timeNeededToGoHome; // s
final int? timeNeededToLand; // s, from the current height
final int? batteryNeededToGoHome; // %
final int? batteryNeededToLand; // %
final double? maxRadiusCanFlyAndGoHome; // m
/// `IDLE` / `COUNTING_DOWN` / `EXECUTED` / `CANCELLED`.
final String? smartRTHState;
final int? smartRTHCountdown; // s
bool get smartRTHCountingDown => smartRTHState == 'COUNTING_DOWN';
@override
bool operator ==(Object other) =>
other is GoHomeAssessment &&
other.remainingFlightTime == remainingFlightTime &&
other.timeNeededToGoHome == timeNeededToGoHome &&
other.timeNeededToLand == timeNeededToLand &&
other.batteryNeededToGoHome == batteryNeededToGoHome &&
other.batteryNeededToLand == batteryNeededToLand &&
other.maxRadiusCanFlyAndGoHome == maxRadiusCanFlyAndGoHome &&
other.smartRTHState == smartRTHState &&
other.smartRTHCountdown == smartRTHCountdown;
@override
int get hashCode => Object.hash(remainingFlightTime, timeNeededToGoHome, timeNeededToLand, batteryNeededToGoHome,
batteryNeededToLand, maxRadiusCanFlyAndGoHome, smartRTHState, smartRTHCountdown);
}
/// The RTK module's fix (`rtk` group), on aircraft that have one.
@immutable
class RtkStatus {
const RtkStatus({this.solution, this.inUse, this.satellites, this.error});
factory RtkStatus.fromMap(Map<String, dynamic> m) => RtkStatus(
solution: asString(m['solution']),
inUse: asBool(m['inUse']),
satellites: asInt(m['satellites']),
error: asString(m['error']),
);
/// `NONE` / `SINGLE_POINT` / `FLOAT` / `FIXED_POINT`.
final String? solution;
final bool? inUse;
final int? satellites;
final String? error;
/// GO 4's chip text: FIX, FLOAT, SINGLE or a dash.
String get label => switch (solution) {
'FIXED_POINT' => 'FIX',
'FLOAT' => 'FLOAT',
'SINGLE_POINT' => 'SINGLE',
_ => '—',
};
bool get fixed => solution == 'FIXED_POINT';
@override
bool operator ==(Object other) =>
other is RtkStatus && other.solution == solution && other.inUse == inUse && other.satellites == satellites && other.error == error;
@override
int get hashCode => Object.hash(solution, inUse, satellites, error);
}
/// One compass's magnetic reading (`compassSensors`), for the interference meter.
@immutable
class CompassSensor {
const CompassSensor(this.index, this.state, this.value);
final int index;
/// `NORMAL_MODULUS` / `WEAK_MODULUS` / `SERIOUS_MODULUS` / `UNCALIBRATED` / …
final String? state;
/// The field's modulus as the flight controller reports it (raw units).
final int? value;
/// 0 = clean, 1 = weak interference, 2 = serious, null = not graded.
int? get level => switch (state) {
'NORMAL_MODULUS' => 0,
'WEAK_MODULUS' => 1,
'SERIOUS_MODULUS' || 'DATA_EXCEPTION' || 'DIRECTION_EXCEPTION' => 2,
_ => null,
};
@override
bool operator ==(Object other) => other is CompassSensor && other.index == index && other.state == state && other.value == value;
@override
int get hashCode => Object.hash(index, state, value);
}
/// The flight controller's state: position, motion, attitude, flight mode and
/// the safety flags. Fed from the flat keys of every `telemetry` tick (~10 Hz).
class AircraftState extends ChangeNotifier {
int? satellites;
int? gpsSignalLevel; // 0..5
bool? isFlying;
bool? motorsOn;
/// Human-readable mode from the SDK ("P-GPS", "Sport", "Atti", "GoHome" …).
String? flightMode;
/// The `FlightMode` enum name behind [flightMode] (`GPS_ATTI`, `GO_HOME` …),
/// which is what logic should branch on.
String? flightModeEnum;
double? altitude; // m, relative to take-off
double? latitude;
double? longitude;
double? homeLatitude;
double? homeLongitude;
bool? homeLocationSet;
double? homeDistance; // m, aircraft ↔ home
double? horizontalSpeed; // m/s
double? verticalSpeed; // m/s (up positive)
double? heading; // deg, −180..180 (= yaw)
double? pitch; // deg
double? roll; // deg
double? velocityX; // m/s, north
double? velocityY; // m/s, east
double? velocityZ; // m/s, down
int? flightTimeSeconds;
double? goHomeHeight; // m
/// VPS: downward ultrasonic height, valid only while [ultrasonicBeingUsed].
double? ultrasonicHeight; // m
bool? ultrasonicBeingUsed;
bool? ultrasonicError;
bool? visionPositioningBeingUsed;
bool? landingConfirmationNeeded;
bool? goingHome;
/// `GoHomeExecutionState` name: `NOT_EXECUTING`, `GO_UP_TO_HEIGHT`, `AUTO_FLY_TO_HOME_POINT` …
String? goHomeExecutionState;
GoHomeAssessment? goHome;
bool? lowBattery;
bool? seriousLowBattery;
String? batteryThresholdBehavior; // FLY_NORMALLY / GO_HOME / LAND_IMMEDIATELY
String? windWarning; // LEVEL_0..LEVEL_2
bool? reachedMaxHeight;
bool? reachedMaxRadius;
bool? imuPreheating;
bool? failsafe;
String? orientationMode; // AIRCRAFT_HEADING / COURSE_LOCK / HOME_LOCK
/// The flight controller's built-in simulator is running (developer only).
bool? simulatorActive;
/// The flight controller is in virtual-stick mode (plan P8), from the native
/// stick loop's `virtualStick` group; null until sticks were first enabled.
bool? virtualStickEnabled;
bool? compassHasError;
bool? compassCalibrating;
/// `NOT_CALIBRATING` / `HORIZONTAL` / `VERTICAL` / `SUCCESSFUL` / `FAILED`.
String? compassCalibrationState;
/// Per-compass interference readings; empty until the compass reports.
List<CompassSensor> compassSensors = const <CompassSensor>[];
/// The RTK fix, on aircraft with the module; null otherwise.
RtkStatus? rtk;
DateTime? updatedAt;
/// VPS height when the sensor is actually in use, else null.
double? get vpsHeight => ultrasonicBeingUsed == true ? ultrasonicHeight : null;
void apply(Map<String, dynamic> t) {
satellites = asInt(t['satelliteCount']);
gpsSignalLevel = asInt(t['gpsSignalLevel']);
isFlying = asBool(t['isFlying']);
motorsOn = asBool(t['areMotorsOn']);
flightMode = asString(t['flightMode']);
flightModeEnum = asString(t['flightModeEnum']);
altitude = finite(t['altitude']);
latitude = finite(t['latitude']);
longitude = finite(t['longitude']);
homeLatitude = finite(t['homeLatitude']);
homeLongitude = finite(t['homeLongitude']);
homeLocationSet = asBool(t['isHomeLocationSet']);
homeDistance = finite(t['homeDistance']);
horizontalSpeed = finite(t['horizontalSpeed']);
verticalSpeed = finite(t['verticalSpeed']);
heading = finite(t['heading']);
pitch = finite(t['pitch']);
roll = finite(t['roll']);
velocityX = finite(t['velocityX']);
velocityY = finite(t['velocityY']);
velocityZ = finite(t['velocityZ']);
flightTimeSeconds = asInt(t['flightTimeSeconds']);
goHomeHeight = finite(t['goHomeHeight']);
ultrasonicHeight = finite(t['ultrasonicHeight']);
ultrasonicBeingUsed = asBool(t['isUltrasonicBeingUsed']);
ultrasonicError = asBool(t['ultrasonicError']);
visionPositioningBeingUsed = asBool(t['isVisionPositioningBeingUsed']);
landingConfirmationNeeded = asBool(t['isLandingConfirmationNeeded']);
goingHome = asBool(t['isGoingHome']);
goHomeExecutionState = asString(t['goHomeExecutionState']);
final Map<String, dynamic>? gh = asMap(t['goHome']);
goHome = gh == null ? null : GoHomeAssessment.fromMap(gh);
lowBattery = asBool(t['isLowBattery']);
seriousLowBattery = asBool(t['isSeriousLowBattery']);
batteryThresholdBehavior = asString(t['batteryThresholdBehavior']);
windWarning = asString(t['windWarning']);
reachedMaxHeight = asBool(t['reachedMaxHeight']);
reachedMaxRadius = asBool(t['reachedMaxRadius']);
imuPreheating = asBool(t['isIMUPreheating']);
failsafe = asBool(t['isFailsafe']);
orientationMode = asString(t['orientationMode']);
simulatorActive = asBool(t['simulatorActive']);
virtualStickEnabled = asBool(asMap(t['virtualStick'])?['enabled']);
compassHasError = asBool(t['compassHasError']);
compassCalibrating = asBool(t['compassCalibrating']);
compassCalibrationState = asString(t['compassCalibrationState']);
compassSensors = <CompassSensor>[
for (final Map<String, dynamic> c in asMapList(t['compassSensors'])) CompassSensor(asInt(c['index']) ?? 0, asString(c['state']), asInt(c['value'])),
];
final Map<String, dynamic>? r = asMap(t['rtk']);
rtk = r == null ? null : RtkStatus.fromMap(r);
updatedAt = DateTime.now();
notifyListeners();
}
void clear() {
satellites = gpsSignalLevel = flightTimeSeconds = null;
isFlying = motorsOn = homeLocationSet = ultrasonicBeingUsed = ultrasonicError = null;
visionPositioningBeingUsed = landingConfirmationNeeded = goingHome = null;
lowBattery = seriousLowBattery = reachedMaxHeight = reachedMaxRadius = imuPreheating = failsafe = simulatorActive = virtualStickEnabled = null;
compassHasError = compassCalibrating = null;
compassCalibrationState = null;
flightMode = flightModeEnum = goHomeExecutionState = batteryThresholdBehavior = windWarning = orientationMode = null;
altitude = latitude = longitude = homeLatitude = homeLongitude = homeDistance = null;
horizontalSpeed = verticalSpeed = heading = pitch = roll = velocityX = velocityY = velocityZ = null;
goHomeHeight = ultrasonicHeight = null;
goHome = null;
compassSensors = const <CompassSensor>[];
rtk = null;
updatedAt = null;
notifyListeners();
}
}