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>
203 lines
8.0 KiB
Dart
203 lines
8.0 KiB
Dart
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();
|
||
}
|
||
}
|