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>
121 lines
3.8 KiB
Dart
121 lines
3.8 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
|
||
import 'parse.dart';
|
||
|
||
/// One manned aircraft the AirSense (ADS-B In) receiver hears.
|
||
@immutable
|
||
class AirSensePlane {
|
||
const AirSensePlane({
|
||
required this.code,
|
||
this.warningLevel = 0,
|
||
this.direction,
|
||
this.heading,
|
||
this.distance,
|
||
this.latitude,
|
||
this.longitude,
|
||
});
|
||
|
||
factory AirSensePlane.fromMap(Map<String, dynamic> m) => AirSensePlane(
|
||
code: (asString(m['code']) ?? '').trim(),
|
||
warningLevel: asInt(m['warningLevel']) ?? 0,
|
||
direction: asString(m['direction']),
|
||
heading: finite(m['heading']),
|
||
distance: finite(m['distance']),
|
||
latitude: finite(m['latitude']),
|
||
longitude: finite(m['longitude']),
|
||
);
|
||
|
||
/// ICAO 24-bit address, as the receiver formats it.
|
||
final String code;
|
||
|
||
/// 0 (no threat) … 4 (collision risk).
|
||
final int warningLevel;
|
||
|
||
/// `AirSenseDirection` octant relative to the drone: `NORTH`, `NORTH_EAST`, … `UNKNOWN`.
|
||
final String? direction;
|
||
|
||
final double? heading; // deg
|
||
final double? distance; // m
|
||
final double? latitude;
|
||
final double? longitude;
|
||
|
||
bool get hasPosition => latitude != null && longitude != null && !(latitude == 0 && longitude == 0);
|
||
|
||
/// `NE`, `S`, … or null when unknown.
|
||
String? get octant => switch (direction) {
|
||
'NORTH' => 'N',
|
||
'NORTH_EAST' => 'NE',
|
||
'EAST' => 'E',
|
||
'SOUTH_EAST' => 'SE',
|
||
'SOUTH' => 'S',
|
||
'SOUTH_WEST' => 'SW',
|
||
'WEST' => 'W',
|
||
'NORTH_WEST' => 'NW',
|
||
_ => null,
|
||
};
|
||
}
|
||
|
||
/// AirSense (plan P8): the receiver's overall warning and every plane it hears,
|
||
/// from the telemetry tick's `airSense` group. [reported] stays false on
|
||
/// aircraft without a receiver.
|
||
class AirSenseState extends ChangeNotifier {
|
||
bool reported = false;
|
||
|
||
/// The receiver's overall level, 0–4.
|
||
int warningLevel = 0;
|
||
|
||
/// Nearest first.
|
||
List<AirSensePlane> planes = const <AirSensePlane>[];
|
||
|
||
/// The level to act on: the receiver's own, or the worst single plane's.
|
||
int get level => planes.fold(warningLevel, (int w, AirSensePlane p) => p.warningLevel > w ? p.warningLevel : w);
|
||
|
||
/// Level 2 asks for caution; 3 and 4 ask the pilot to descend.
|
||
static const int cautionLevel = 2;
|
||
static const int dangerLevel = 3;
|
||
|
||
/// Planes at caution level or worse, nearest first.
|
||
List<AirSensePlane> get threats => <AirSensePlane>[for (final AirSensePlane p in planes) if (p.warningLevel >= cautionLevel) p];
|
||
|
||
/// The plane a warning should name: the worst, then the nearest.
|
||
AirSensePlane? get worst {
|
||
AirSensePlane? best;
|
||
for (final AirSensePlane p in planes) {
|
||
if (best == null || p.warningLevel > best.warningLevel) best = p;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
void apply(Map<String, dynamic>? m) {
|
||
reported = m != null;
|
||
warningLevel = asInt(m?['warningLevel']) ?? 0;
|
||
planes = <AirSensePlane>[for (final Map<String, dynamic> p in asMapList(m?['airplanes'])) AirSensePlane.fromMap(p)]
|
||
..sort((AirSensePlane a, AirSensePlane b) => (a.distance ?? double.infinity).compareTo(b.distance ?? double.infinity));
|
||
notifyListeners();
|
||
}
|
||
|
||
/// What the flight controller does about a threat by itself, on the aircraft
|
||
/// that report it (`airSenseAvoidance`): `NONE`, `AVOIDING` or `LANDING`.
|
||
String? avoidanceMode;
|
||
|
||
/// Seconds until the avoidance action starts.
|
||
double? avoidanceCountdown;
|
||
|
||
bool get avoiding => avoidanceMode == 'AVOIDING' || avoidanceMode == 'LANDING';
|
||
|
||
void applyAvoidance(Map<String, dynamic>? m) {
|
||
avoidanceMode = asString(m?['mode']);
|
||
avoidanceCountdown = finite(m?['countdown']);
|
||
notifyListeners();
|
||
}
|
||
|
||
void clear() {
|
||
reported = false;
|
||
warningLevel = 0;
|
||
planes = const <AirSensePlane>[];
|
||
avoidanceMode = null;
|
||
avoidanceCountdown = null;
|
||
notifyListeners();
|
||
}
|
||
}
|