Files
PilotVault/Fly App/lib/ui/fly/panels/settings/sensing_rows.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

107 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../dji_service.dart';
import '../../../../fly/sensing_distances.dart';
import '../../hud_kit.dart';
import '../camera_menu/camera_rows.dart';
/// Settings → Vision → obstacle distances (plan P8): where the aircraft brakes
/// and how far it looks, per direction the aircraft reports. Read once when the
/// tab opens; a step writes at once and rolls back if the aircraft refuses.
class SensingDistanceRows extends StatefulWidget {
const SensingDistanceRows({super.key, required this.dji, required this.connected, required this.onError});
final DjiService dji;
final bool connected;
final void Function(String label, Object error) onError;
@override
State<SensingDistanceRows> createState() => _SensingDistanceRowsState();
}
class _SensingDistanceRowsState extends State<SensingDistanceRows> {
SensingDistances? _d;
bool _loading = false;
final Set<(SensingKind, SensingDirection)> _pending = <(SensingKind, SensingDirection)>{};
@override
void initState() {
super.initState();
_load();
}
@override
void didUpdateWidget(SensingDistanceRows old) {
super.didUpdateWidget(old);
if (widget.connected && !old.connected) _load();
}
Future<void> _load() async {
if (!widget.connected || _loading) return;
_loading = true;
try {
final SensingDistances d = SensingDistances.fromBridge(await widget.dji.getSensingDistances());
if (mounted) setState(() => _d = d);
} catch (_) {
if (mounted) setState(() => _d = const SensingDistances());
} finally {
_loading = false;
}
}
Future<void> _step(SensingKind k, SensingDirection dir, int steps) async {
final SensingDistances? before = _d;
final double? v = before?.value(k, dir);
if (before == null || v == null) return;
final double next = SensingDistances.stepped(k, dir, v, steps);
if (next == v) return;
setState(() {
_d = before.withValue(k, dir, next);
_pending.add((k, dir));
});
try {
await (k == SensingKind.avoidance
? widget.dji.setAvoidanceDistance(dir.sdk, next)
: widget.dji.setMaxPerceptionDistance(dir.sdk, next));
} catch (e) {
if (mounted) setState(() => _d = before);
widget.onError(k == SensingKind.avoidance ? 'Braking distance' : 'Sensing range', e);
} finally {
if (mounted) setState(() => _pending.remove((k, dir)));
}
}
@override
Widget build(BuildContext context) {
final SensingDistances? d = _d;
// Nothing reported (or not connected): the rows stay out of the way.
if (d == null || d.isEmpty) return const SizedBox.shrink();
Widget row(SensingKind k, SensingDirection dir) {
final double? v = d.value(k, dir);
if (v == null) return const SizedBox.shrink();
final ({double min, double max, double step}) r = SensingDistances.range(k, dir);
final String what = k == SensingKind.avoidance ? 'braking distance' : 'sensing range';
return StepperTile(
key: ValueKey<String>('row-sensing-${k.name}-${dir.name}'),
id: 'sensing-${k.name}-${dir.name}',
label: '${dir.label} $what',
hint: k == SensingKind.avoidance ? 'The aircraft brakes this far from an obstacle' : 'Obstacles closer than this are reported',
shown: '${v.toStringAsFixed(r.step < 1 ? 1 : 0)} m',
canDown: v > r.min,
canUp: v < r.max,
pending: _pending.contains((k, dir)),
onStep: (int s) => _step(k, dir, s),
);
}
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 2),
child: Text('OBSTACLE DISTANCES', style: HudText.label),
),
for (final SensingKind k in SensingKind.values)
for (final SensingDirection dir in SensingDirection.values) row(k, dir),
]);
}
}