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
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../fly/virtual_sticks.dart';
|
||||
import '../../../theme.dart';
|
||||
import '../hud_kit.dart';
|
||||
|
||||
/// What each pad's axes do in [mode], for the captions under the pads.
|
||||
({String left, String right}) stickCaptions(StickMode mode) => switch (mode) {
|
||||
StickMode.mode1 => (left: 'FWD · YAW', right: 'UP · SIDE'),
|
||||
StickMode.mode2 => (left: 'UP · YAW', right: 'FWD · SIDE'),
|
||||
StickMode.mode3 => (left: 'FWD · SIDE', right: 'UP · YAW'),
|
||||
};
|
||||
|
||||
/// One on-screen stick (plan P8): a ring the thumb drags a knob inside,
|
||||
/// springing back to centre on release like a remote's stick. Reports −1…1
|
||||
/// with y up; the drag starts where the finger lands, not after the slop.
|
||||
class StickPad extends StatefulWidget {
|
||||
const StickPad({super.key, required this.value, required this.onChanged, required this.caption, this.size = 110});
|
||||
|
||||
final Offset value;
|
||||
final ValueChanged<Offset> onChanged;
|
||||
final String caption;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
State<StickPad> createState() => _StickPadState();
|
||||
}
|
||||
|
||||
class _StickPadState extends State<StickPad> {
|
||||
double get _radius => widget.size / 2 - 14;
|
||||
|
||||
void _to(Offset local) {
|
||||
final Offset centre = Offset(widget.size / 2, widget.size / 2);
|
||||
widget.onChanged(StickMapper.normalise(local - centre, _radius));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Offset v = widget.value;
|
||||
final Offset knob = Offset(v.dx * _radius, -v.dy * _radius);
|
||||
final bool active = v != Offset.zero;
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
onPanStart: (DragStartDetails d) => _to(d.localPosition),
|
||||
onPanUpdate: (DragUpdateDetails d) => _to(d.localPosition),
|
||||
onPanEnd: (_) => widget.onChanged(Offset.zero),
|
||||
onPanCancel: () => widget.onChanged(Offset.zero),
|
||||
child: SizedBox.square(
|
||||
dimension: widget.size,
|
||||
child: Stack(alignment: Alignment.center, children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Glass.pill,
|
||||
border: Border.all(color: active ? Glass.accent : const Color(0x4DEAF0FA), width: 1.5),
|
||||
),
|
||||
),
|
||||
// Cross-hair: the stick's two axes.
|
||||
Container(width: 1, height: widget.size - 28, color: Glass.hairline),
|
||||
Container(width: widget.size - 28, height: 1, color: Glass.hairline),
|
||||
Transform.translate(
|
||||
offset: knob,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: active ? Glass.accent : const Color(0xCC253552),
|
||||
border: Border.all(color: const Color(0x66FFFFFF)),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(widget.caption, style: HudText.label.copyWith(fontSize: 8)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The rail's STICKS button: lit while the on-screen sticks fly the aircraft.
|
||||
class SticksButton extends StatelessWidget {
|
||||
const SticksButton({super.key, required this.open, required this.onTap});
|
||||
|
||||
final bool open;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GlassPill(
|
||||
key: const ValueKey<String>('rail-sticks'),
|
||||
height: 24,
|
||||
color: open ? Glass.accent : Glass.pillStrong,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
onTap: onTap,
|
||||
child: Text('STICKS', style: HudText.label.copyWith(fontSize: 8, color: Glass.ink, fontWeight: FontWeight.w700)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user