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>
This commit is contained in:
1 parent
93f4a580e9
commit
e02bb0e5cd
34 files changed
+4376
-15
No files matched your search
@@ -1,10 +1,12 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart' show listEquals;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../flight_model.dart';
|
||||
import '../../../fly/hud_geometry.dart';
|
||||
import '../../../fly/state/aircraft_state.dart';
|
||||
import '../../../fly/vision_radar.dart';
|
||||
import '../../../theme.dart';
|
||||
import '../hud_kit.dart';
|
||||
|
||||
@@ -12,6 +14,10 @@ import '../hud_kit.dart';
|
||||
/// roll, with a north-up radar on top — the aircraft arrow in the middle
|
||||
/// pointing along its heading, the home point and the pilot (phone GPS)
|
||||
/// plotted by bearing and distance, and a tick on the rim for the gimbal's yaw.
|
||||
///
|
||||
/// Plan P8 adds GO 4's `DJIVisualObstacleRadarView` ring: the vision radar's
|
||||
/// graded sectors drawn on the rim, turned with the heading (the ball is
|
||||
/// north-up, the sensors are not).
|
||||
class AttitudeBall extends StatelessWidget {
|
||||
const AttitudeBall({super.key, required this.model, this.size = 96});
|
||||
|
||||
@@ -21,13 +27,14 @@ class AttitudeBall extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: Listenable.merge(<Listenable>[model.aircraft, model.gimbal]),
|
||||
listenable: Listenable.merge(<Listenable>[model.aircraft, model.gimbal, model.vision]),
|
||||
builder: (BuildContext context, _) {
|
||||
final AttitudeBallData d = AttitudeBallData.from(
|
||||
model.aircraft,
|
||||
gimbalYaw: model.gimbal.yaw,
|
||||
phoneLat: model.phoneLatitude,
|
||||
phoneLon: model.phoneLongitude,
|
||||
radar: VisionRadar.of(model.vision),
|
||||
);
|
||||
return SizedBox(
|
||||
width: size,
|
||||
@@ -43,6 +50,9 @@ class AttitudeBall extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// One graded radar sector on the ball's rim.
|
||||
typedef BallObstacle = ({double start, double sweep, RadarBand band});
|
||||
|
||||
/// Everything the painter needs, resolved from the models so it can be compared
|
||||
/// for [CustomPainter.shouldRepaint] and built in tests.
|
||||
@immutable
|
||||
@@ -57,10 +67,11 @@ class AttitudeBallData {
|
||||
this.pilotBearing,
|
||||
this.pilotDistance,
|
||||
this.rangeM = 100,
|
||||
this.obstacles = const <BallObstacle>[],
|
||||
});
|
||||
|
||||
/// Bearings and distances are taken from the aircraft to each point.
|
||||
factory AttitudeBallData.from(AircraftState a, {double? gimbalYaw, double? phoneLat, double? phoneLon}) {
|
||||
factory AttitudeBallData.from(AircraftState a, {double? gimbalYaw, double? phoneLat, double? phoneLon, VisionRadar? radar}) {
|
||||
final double? lat = a.latitude, lon = a.longitude;
|
||||
final bool fix = lat != null && lon != null && !(lat == 0 && lon == 0);
|
||||
double? hb, hd, pb, pd;
|
||||
@@ -82,9 +93,18 @@ class AttitudeBallData {
|
||||
pilotBearing: pb,
|
||||
pilotDistance: pd,
|
||||
rangeM: HudGeometry.ballRange(math.max(hd ?? 0, pd ?? 0)),
|
||||
obstacles: <BallObstacle>[
|
||||
for (final RadarSideView side in radar?.sides.values ?? const <RadarSideView>[])
|
||||
for (final RadarSector s in side.sectors)
|
||||
if (s.band != RadarBand.none) (start: s.start, sweep: s.sweep, band: s.band),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Graded radar sectors, degrees clockwise from the *nose*; the painter
|
||||
/// turns them by [heading].
|
||||
final List<BallObstacle> obstacles;
|
||||
|
||||
final double? pitch;
|
||||
final double? roll;
|
||||
final double? heading;
|
||||
@@ -108,11 +128,12 @@ class AttitudeBallData {
|
||||
other.homeDistance == homeDistance &&
|
||||
other.pilotBearing == pilotBearing &&
|
||||
other.pilotDistance == pilotDistance &&
|
||||
other.rangeM == rangeM;
|
||||
other.rangeM == rangeM &&
|
||||
listEquals(other.obstacles, obstacles);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(pitch, roll, heading, gimbalYaw, homeBearing, homeDistance, pilotBearing, pilotDistance, rangeM);
|
||||
Object.hash(pitch, roll, heading, gimbalYaw, homeBearing, homeDistance, pilotBearing, pilotDistance, rangeM, Object.hashAll(obstacles));
|
||||
}
|
||||
|
||||
class AttitudeBallPainter extends CustomPainter {
|
||||
@@ -154,6 +175,21 @@ class AttitudeBallPainter extends CustomPainter {
|
||||
..color = const Color(0x33EAF0FA));
|
||||
_label(canvas, 'N', c + Offset(0, -r + 7), Glass.ink, 8);
|
||||
|
||||
// ── Obstacle ring (plan P8): sensor sectors turned by the heading ──────
|
||||
final double heading = d.heading ?? 0;
|
||||
for (final BallObstacle o in d.obstacles) {
|
||||
canvas.drawArc(
|
||||
Rect.fromCircle(center: c, radius: r - 2.5),
|
||||
(heading + o.start + 0.5 - 90) * math.pi / 180,
|
||||
(o.sweep - 1) * math.pi / 180,
|
||||
false,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = o.band == RadarBand.danger ? 4 : 3
|
||||
..color = RadarColors.of(o.band),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Gimbal yaw tick (on the rim) ───────────────────────────────────────
|
||||
final double? gy = d.gimbalYaw;
|
||||
if (gy != null) {
|
||||
|
||||
Reference in new issue
Block a user