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>
107 lines
3.9 KiB
Dart
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),
|
|
]);
|
|
}
|
|
}
|