Files
PilotVault/Fly App/lib/fly/state/aircraft_state.dart
T
tajniak81andClaude Opus 5.5 e02bb0e5cd Give the Fly App GO 4-style obstacle radar, on-screen sticks, AirSense and fly zones
Vision radar: graded obstacle sectors around the centre of the video,
the nearest distance per side, the obstacle overhead, and BLOCKED on a
side whose sensors cannot see once the motors run. On aircraft with 360°
sensing, sides without their own readings are filled from the
perception ring. The same sectors sit on the attitude ball's rim, turned
by the heading. Settings > Vision gains Display radar chart and the
braking / sensing distances per direction.

On-screen sticks: the rail's STICKS button hands the aircraft to two
stick pads after a slide-to-confirm. The 25 Hz stream runs natively and
sends zeros once Dart's last command is 300 ms old, so a stalled UI
leaves the aircraft hovering. The sticks close when the remote takes
over, on RTH or on landing. They are simulator-only on aircraft with a
remote unless Settings > General > Developer allows real flights; Spark
and Mavic Air on Wi-Fi may fly them for real. Which MSDK axis is forward
in velocity + body mode is coded from DJI's documentation and must be
checked in the simulator first.

AirSense: a banner for manned aircraft at warning level 2 and up, with
distance, direction and ICAO code; the planes on the map; the full list
in a sheet and Settings > Safety; and the aircraft's own avoidance
action with its countdown.

Fly zones: DJI GEO zones around the aircraft (or the phone) on the map
with their height limits, a banner for FlySafe notices and in/near-zone
states, take-off refused in restricted and locked authorization zones,
self-unlock of authorization zones, the aircraft's licences (enable,
sync from the DJI account, remove all; ground only), and the GEO
database version with an update button.

Tested with unit and widget tests and screenshot renders only; nothing
has run on an aircraft or the simulator yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:14:12 +02:00

203 lines
8.0 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 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;
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']);
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;
updatedAt = null;
notifyListeners();
}
}