Files
PilotVault/Fly App/lib/fly/state/airsense_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

121 lines
3.8 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';
/// 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();
}
}