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>
95 lines
2.7 KiB
Dart
95 lines
2.7 KiB
Dart
import 'dart:async';
|
|
|
|
import '../dji_service.dart';
|
|
import '../flight_model.dart';
|
|
|
|
/// Keeps [FlightModel.flyZones] stocked with the GEO zones around (plan P8):
|
|
/// fetched around the aircraft's fix, or the phone's before it has one, again
|
|
/// after moving 2 km or every 10 minutes; plus the unlocked list once per
|
|
/// connection with the aircraft's licences, and the GEO database's state at
|
|
/// start. One fetch at a time; failures wait for the next position.
|
|
class FlyZoneLoader {
|
|
FlyZoneLoader({required this.model, required this.dji});
|
|
|
|
final FlightModel model;
|
|
final DjiService dji;
|
|
|
|
bool _busy = false;
|
|
bool _unlockedLoaded = false;
|
|
|
|
void start() {
|
|
model.aircraft.addListener(_check);
|
|
model.addListener(_onModel);
|
|
unawaited(refreshDatabase());
|
|
_check();
|
|
}
|
|
|
|
void dispose() {
|
|
model.aircraft.removeListener(_check);
|
|
model.removeListener(_onModel);
|
|
}
|
|
|
|
void _onModel() {
|
|
if (!model.connected) _unlockedLoaded = false;
|
|
_check();
|
|
}
|
|
|
|
/// The aircraft's fix, else the phone's.
|
|
(double, double)? get _position {
|
|
final double? lat = model.aircraft.latitude, lon = model.aircraft.longitude;
|
|
if (lat != null && lon != null && !(lat == 0 && lon == 0)) return (lat, lon);
|
|
final double? pLat = model.phoneLatitude, pLon = model.phoneLongitude;
|
|
if (pLat != null && pLon != null) return (pLat, pLon);
|
|
return null;
|
|
}
|
|
|
|
void _check() {
|
|
if (model.connected && !_unlockedLoaded) {
|
|
_unlockedLoaded = true;
|
|
unawaited(refreshUnlocked());
|
|
unawaited(refreshLicences());
|
|
}
|
|
final (double, double)? p = _position;
|
|
if (p == null || _busy || !model.flyZones.needsZonesFor(p.$1, p.$2)) return;
|
|
unawaited(_fetch(p));
|
|
}
|
|
|
|
Future<void> _fetch((double, double) p) async {
|
|
_busy = true;
|
|
try {
|
|
final List<Map<String, dynamic>> zones = await dji.getFlyZonesNearby(latitude: p.$1, longitude: p.$2);
|
|
model.flyZones.applyZones(zones, latitude: p.$1, longitude: p.$2);
|
|
} catch (_) {
|
|
// Database not ready or offline: retry when the position moves on.
|
|
} finally {
|
|
_busy = false;
|
|
}
|
|
}
|
|
|
|
/// The aircraft's licences (custom unlock zones).
|
|
Future<void> refreshLicences() async {
|
|
try {
|
|
model.flyZones.applyLicences(await dji.getFlyZoneLicences());
|
|
} catch (_) {
|
|
// Not reported by this aircraft.
|
|
}
|
|
}
|
|
|
|
/// The GEO database's versions and state (app-level, no aircraft needed).
|
|
Future<void> refreshDatabase() async {
|
|
try {
|
|
model.flyZones.applyDatabase(await dji.getFlyZoneDatabase());
|
|
} catch (_) {
|
|
// SDK not registered yet.
|
|
}
|
|
}
|
|
|
|
Future<void> refreshUnlocked() async {
|
|
try {
|
|
model.flyZones.applyUnlocked(await dji.getUnlockedFlyZones());
|
|
} catch (_) {
|
|
// Not reported by this aircraft.
|
|
}
|
|
}
|
|
}
|