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>
104 lines
3.8 KiB
Dart
104 lines
3.8 KiB
Dart
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)),
|
||
);
|
||
}
|
||
}
|