Intelligent modes: Point of Interest (orbit on the map, live radius and speed), Follow Me (follows the phone), QuickShots on a boxed subject, Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and Circle. Every mode has a first-use tutorial and stops from the rail. Waypoints can resume an interrupted mission, change cruise speed in flight and check terrain clearance (Open-Meteo, on tap). Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI output, a link spectrum chart, Wi-Fi name and password, aircraft name, video cache, battery warning history and pairing, non-smart cell voltages. Calibration: compass interference meter, per-IMU calibration, lens infinity focus and a first-manual-focus prompt. Also an RTK chip, gimbal movement presets and motor tuning, Precision Takeoff gated on the flight controller, and the precise GEO database's server version. Built and unit/widget-tested only; nothing has flown yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
475 lines
18 KiB
Dart
475 lines
18 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import '../hud_geometry.dart';
|
|
import 'parse.dart';
|
|
|
|
/// How a fly zone (or the aircraft's standing towards one) should look.
|
|
enum ZoneLevel { none, info, caution, danger }
|
|
|
|
/// One part of a GEO zone: a circle, or a polygon with its own height limit.
|
|
@immutable
|
|
class SubFlyZone {
|
|
const SubFlyZone({required this.shape, this.latitude, this.longitude, this.radius, this.maxHeight, this.vertices = const <(double, double)>[]});
|
|
|
|
factory SubFlyZone.fromMap(Map<String, dynamic> m) => SubFlyZone(
|
|
shape: asString(m['shape']) ?? 'UNKNOWN',
|
|
latitude: finite(m['latitude']),
|
|
longitude: finite(m['longitude']),
|
|
radius: finite(m['radius']),
|
|
maxHeight: asInt(m['maxHeight']),
|
|
vertices: <(double, double)>[
|
|
if (m['vertices'] is List)
|
|
for (final Object? v in m['vertices'] as List)
|
|
if (v is List && v.length == 2 && finite(v[0]) != null && finite(v[1]) != null) (finite(v[0])!, finite(v[1])!),
|
|
],
|
|
);
|
|
|
|
/// `CYLINDER` or `POLYGON`.
|
|
final String shape;
|
|
final double? latitude;
|
|
final double? longitude;
|
|
final double? radius; // m
|
|
/// Height limit inside this part, m (0 = no flight).
|
|
final int? maxHeight;
|
|
final List<(double, double)> vertices;
|
|
|
|
bool get isPolygon => shape == 'POLYGON' && vertices.length >= 3;
|
|
|
|
/// Where the height label goes: the circle's centre, or the polygon's vertex average.
|
|
(double, double)? get labelPoint {
|
|
if (isPolygon) {
|
|
double la = 0, lo = 0;
|
|
for (final (double, double) v in vertices) {
|
|
la += v.$1;
|
|
lo += v.$2;
|
|
}
|
|
return (la / vertices.length, lo / vertices.length);
|
|
}
|
|
return latitude == null || longitude == null ? null : (latitude!, longitude!);
|
|
}
|
|
|
|
/// `NO FLY` at 0 m, `120 m` for a ceiling, null when the part sets none.
|
|
String? get heightLabel {
|
|
final int? h = maxHeight;
|
|
if (h == null || h < 0) return null;
|
|
return h == 0 ? 'NO FLY' : '$h m';
|
|
}
|
|
}
|
|
|
|
/// One DJI GEO zone (`FlyZoneInformation`).
|
|
@immutable
|
|
class FlyZone {
|
|
const FlyZone({
|
|
required this.id,
|
|
this.name = '',
|
|
this.category = 'UNKNOWN',
|
|
this.reason,
|
|
this.latitude,
|
|
this.longitude,
|
|
this.radius,
|
|
this.startTime,
|
|
this.endTime,
|
|
this.unlockStartTime,
|
|
this.unlockEndTime,
|
|
this.subZones = const <SubFlyZone>[],
|
|
});
|
|
|
|
factory FlyZone.fromMap(Map<String, dynamic> m) => FlyZone(
|
|
id: asInt(m['id']) ?? 0,
|
|
name: (asString(m['name']) ?? '').trim(),
|
|
category: asString(m['category']) ?? 'UNKNOWN',
|
|
reason: asString(m['reason']),
|
|
latitude: finite(m['latitude']),
|
|
longitude: finite(m['longitude']),
|
|
radius: finite(m['radius']),
|
|
startTime: _blank(asString(m['startTime'])),
|
|
endTime: _blank(asString(m['endTime'])),
|
|
unlockStartTime: _blank(asString(m['unlockStartTime'])),
|
|
unlockEndTime: _blank(asString(m['unlockEndTime'])),
|
|
subZones: <SubFlyZone>[for (final Map<String, dynamic> s in asMapList(m['subZones'])) SubFlyZone.fromMap(s)],
|
|
);
|
|
|
|
static String? _blank(String? s) => s == null || s.trim().isEmpty ? null : s.trim();
|
|
|
|
final int id;
|
|
final String name;
|
|
|
|
/// `WARNING`, `ENHANCED_WARNING`, `AUTHORIZATION`, `RESTRICTED`, `UNKNOWN`.
|
|
final String category;
|
|
|
|
/// `FlyZoneReason` name: `AIRPORT`, `MILITARY`, `PRISON`, …
|
|
final String? reason;
|
|
final double? latitude;
|
|
final double? longitude;
|
|
final double? radius; // m
|
|
final String? startTime;
|
|
final String? endTime;
|
|
final String? unlockStartTime;
|
|
final String? unlockEndTime;
|
|
final List<SubFlyZone> subZones;
|
|
|
|
bool get unlockable => category == 'AUTHORIZATION';
|
|
|
|
ZoneLevel get level => switch (category) {
|
|
'RESTRICTED' => ZoneLevel.danger,
|
|
'AUTHORIZATION' || 'ENHANCED_WARNING' => ZoneLevel.caution,
|
|
'WARNING' => ZoneLevel.info,
|
|
_ => ZoneLevel.none,
|
|
};
|
|
|
|
String get categoryLabel => switch (category) {
|
|
'RESTRICTED' => 'Restricted zone',
|
|
'AUTHORIZATION' => 'Authorization zone',
|
|
'ENHANCED_WARNING' => 'Enhanced warning zone',
|
|
'WARNING' => 'Warning zone',
|
|
_ => 'Fly zone',
|
|
};
|
|
|
|
/// `COMMERCIAL_AIRPORTS` → `Commercial airports`.
|
|
String? get reasonLabel {
|
|
final String? r = reason;
|
|
if (r == null || r == 'UNKNOWN') return null;
|
|
final String s = r.replaceAll('_', ' ').toLowerCase();
|
|
return s[0].toUpperCase() + s.substring(1);
|
|
}
|
|
|
|
/// Metres from ([lat], [lon]) to the zone's centre, minus its radius (0 inside).
|
|
double? distanceFrom(double lat, double lon) {
|
|
if (latitude == null || longitude == null) return null;
|
|
final double d = HudGeometry.distance(lat, lon, latitude!, longitude!) - (radius ?? 0);
|
|
return d < 0 ? 0 : d;
|
|
}
|
|
}
|
|
|
|
/// One FlySafe notification: transient advice from DJI's GEO system.
|
|
@immutable
|
|
class FlySafeNotice {
|
|
const FlySafeNotice({required this.kind, this.event, this.description, this.heightLimit, this.countdown, this.zoneIds = const <int>[], required this.at});
|
|
|
|
factory FlySafeNotice.fromMap(Map<String, dynamic> m, DateTime at) => FlySafeNotice(
|
|
kind: asString(m['kind']) ?? 'UNKNOWN',
|
|
event: asString(m['event']),
|
|
description: (asString(m['description']) ?? '').trim(),
|
|
heightLimit: asInt(m['heightLimit']),
|
|
countdown: asInt(m['countdown']),
|
|
zoneIds: <int>[if (m['zones'] is List) for (final Object? z in m['zones'] as List) if (z is int) z],
|
|
at: at,
|
|
);
|
|
|
|
/// `FLYING_SAFETY_TIP`, `WARNING`, `SERIOUS_WARNING`, `RETURN_TO_HOME`.
|
|
final String kind;
|
|
final String? event;
|
|
final String? description;
|
|
final int? heightLimit;
|
|
final int? countdown;
|
|
final List<int> zoneIds;
|
|
final DateTime at;
|
|
|
|
ZoneLevel get level => switch (kind) {
|
|
'SERIOUS_WARNING' => ZoneLevel.danger,
|
|
'WARNING' || 'RETURN_TO_HOME' => ZoneLevel.caution,
|
|
_ => ZoneLevel.info,
|
|
};
|
|
|
|
/// A short title when the SDK's description is empty.
|
|
String get title => switch (event) {
|
|
'IN_NO_FLY_ZONE' => 'In a no-fly zone: the aircraft is landing',
|
|
'IN_LIMIT_HEIGHT_AREA' => 'In a height-limited zone',
|
|
'IN_AUTHORIZATION_ZONE_WITHOUT_UNLOCKING' => 'In an authorization zone that is not unlocked',
|
|
'TAKE_OFF_FAILED_IN_NO_FLY_ZONE' || 'TAKE_OFF_FAILED_IN_NO_FLY_ZONE_WITHOUT_GPS' => 'Take-off refused: no-fly zone',
|
|
'TAKE_OFF_FAILED_IN_AUTHORIZED_ZONE_WITHOUT_UNLOCKING' ||
|
|
'TAKE_OFF_FAILED_IN_AUTHORIZED_AREA_WITHOUT_GPS_AND_UNLOCKING' =>
|
|
'Take-off refused: unlock the authorization zone first',
|
|
'NEAR_NO_FLY_ZONE' => 'Return to Home passes near a no-fly zone',
|
|
'CROSS_NO_FLY_ZONE' => 'Return to Home would cross a no-fly zone',
|
|
'TOUCH_NO_FLY_ZONE' => 'At the edge of a no-fly zone',
|
|
'TOUCH_AUTHORIZATION_ZONE' => 'At the edge of an authorization zone',
|
|
'REACH_MAXIMUM_FLIGHT_HEIGHT' => 'At the zone\'s height limit',
|
|
'TAKE_OFF_NEAR_NO_FLY_ZONE' => 'Near a no-fly zone',
|
|
'AUTHORIZED_ZONE_WITHOUT_UNLOCKING_NEARBY' || 'AUTHORIZED_ZONE_AND_RESTRICTIONS_NEARBY' => 'Authorization zone nearby',
|
|
'IN_AUTHORIZATION_ZONE' || 'TAKE_OFF_FAILED_IN_AUTHORIZED_ZONE' => 'In an authorization zone',
|
|
'LIMITED_RESTRICTIONS_NEARBY' || 'RESTRICTIONS_IN_LIMITED_FLY_ZONE_NEARBY' => 'Height-limited zone nearby',
|
|
'TAKE_OFF_IN_ENHANCED_WARNING_ZONE' => 'In an enhanced warning zone',
|
|
'TAKE_OFF_IN_WARNING_AREA' => 'In a warning zone',
|
|
'TAKE_OFF_IN_HEIGHT_LIMIT_ZONE' || 'TAKE_OFF_IN_HEIGHT_LIMIT_ZONE_WITHOUT_GPS' => 'Taking off in a height-limited zone',
|
|
'RTH_AFFECTED_AT_BOUNDARY_OF_GEO_ZONE' => 'Return to Home is limited by a fly zone',
|
|
_ => kind == 'SERIOUS_WARNING' ? 'Fly zone warning' : 'Fly zone notice',
|
|
};
|
|
}
|
|
|
|
/// DJI GEO fly zones (plan P8): the aircraft's standing (`flyZone.state`), the
|
|
/// latest FlySafe notification, the zones around, and which are unlocked.
|
|
class FlyZoneState extends ChangeNotifier {
|
|
/// `FlyZoneState` name, null until reported.
|
|
String? state;
|
|
|
|
FlySafeNotice? notice;
|
|
|
|
List<FlyZone> zones = const <FlyZone>[];
|
|
|
|
/// Where [zones] were fetched around, and when.
|
|
(double, double)? zonesAround;
|
|
DateTime? zonesAt;
|
|
|
|
Set<int> unlocked = const <int>{};
|
|
|
|
/// A notice stays on screen this long (a serious one with a countdown, until replaced).
|
|
static const Duration noticeLife = Duration(seconds: 15);
|
|
|
|
ZoneLevel get stateLevel => switch (state) {
|
|
'IN_RESTRICTED_ZONE' || 'SUSPECTED_IN_RESTRICTED_ZONE' || 'PHONE_IN_RESTRICTED_ZONE' => ZoneLevel.danger,
|
|
'IN_AUTHORIZED_ZONE' || 'SUSPECTED_IN_AUTHORIZED_ZONE' || 'IN_ENHANCE_WARNING_ZONE' || 'NEAR_RESTRICTED_ZONE' => ZoneLevel.caution,
|
|
'IN_WARNING_ZONE' ||
|
|
'IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' ||
|
|
'SUSPECTED_IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' ||
|
|
'PHONE_IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' ||
|
|
'NEAR_HEIGHT_LIMITED_ZONE' ||
|
|
'NEAR_AUTHORIZED_ZONE' ||
|
|
'NEAR_AUTHORIZED_ZONE_WITH_LICENSE' ||
|
|
'NEAR_MULTIPLE_TYPE_FLY_ZONE' ||
|
|
'IN_AUTHORIZED_ZONE_WITH_LICENSE' =>
|
|
ZoneLevel.info,
|
|
_ => ZoneLevel.none,
|
|
};
|
|
|
|
String get stateLabel => switch (state) {
|
|
'IN_RESTRICTED_ZONE' || 'SUSPECTED_IN_RESTRICTED_ZONE' => 'In a restricted zone',
|
|
'PHONE_IN_RESTRICTED_ZONE' => 'You are in a restricted zone',
|
|
'NEAR_RESTRICTED_ZONE' => 'Near a restricted zone',
|
|
'IN_AUTHORIZED_ZONE' || 'SUSPECTED_IN_AUTHORIZED_ZONE' => 'In an authorization zone',
|
|
'IN_AUTHORIZED_ZONE_WITH_LICENSE' => 'In an unlocked authorization zone',
|
|
'NEAR_AUTHORIZED_ZONE' || 'NEAR_AUTHORIZED_ZONE_WITH_LICENSE' => 'Near an authorization zone',
|
|
'IN_ENHANCE_WARNING_ZONE' => 'In an enhanced warning zone',
|
|
'IN_WARNING_ZONE' => 'In a warning zone',
|
|
'IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' ||
|
|
'SUSPECTED_IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' ||
|
|
'PHONE_IN_WARNING_ZONE_WITH_HEIGHT_LIMITATION' =>
|
|
'In a height-limited zone',
|
|
'NEAR_HEIGHT_LIMITED_ZONE' => 'Near a height-limited zone',
|
|
'NEAR_MULTIPLE_TYPE_FLY_ZONE' => 'Near several fly zones',
|
|
_ => 'No fly zone around',
|
|
};
|
|
|
|
/// The notice to show at [now]: fresh, or a serious one still counting down.
|
|
FlySafeNotice? noticeAt(DateTime now) {
|
|
final FlySafeNotice? n = notice;
|
|
if (n == null) return null;
|
|
final Duration age = now.difference(n.at);
|
|
if (age <= noticeLife) return n;
|
|
if (n.kind == 'SERIOUS_WARNING' && (n.countdown ?? 0) > age.inSeconds) return n;
|
|
return null;
|
|
}
|
|
|
|
/// The zone(s) a notice names, as loaded.
|
|
List<FlyZone> zonesFor(FlySafeNotice n) => <FlyZone>[for (final FlyZone z in zones) if (n.zoneIds.contains(z.id)) z];
|
|
|
|
/// Zones nearest first from ([lat], [lon]).
|
|
List<FlyZone> nearest(double? lat, double? lon) {
|
|
if (lat == null || lon == null) return zones;
|
|
return <FlyZone>[...zones]..sort((FlyZone a, FlyZone b) =>
|
|
(a.distanceFrom(lat, lon) ?? double.infinity).compareTo(b.distanceFrom(lat, lon) ?? double.infinity));
|
|
}
|
|
|
|
void applyState(Map<String, dynamic>? m) {
|
|
state = asString(m?['state']);
|
|
notifyListeners();
|
|
}
|
|
|
|
void applyNotice(Map<String, dynamic> m, {DateTime? now}) {
|
|
notice = FlySafeNotice.fromMap(m, now ?? DateTime.now());
|
|
notifyListeners();
|
|
}
|
|
|
|
void applyZones(List<Map<String, dynamic>> list, {required double latitude, required double longitude, DateTime? now}) {
|
|
zones = <FlyZone>[for (final Map<String, dynamic> m in list) FlyZone.fromMap(m)];
|
|
zonesAround = (latitude, longitude);
|
|
zonesAt = now ?? DateTime.now();
|
|
notifyListeners();
|
|
}
|
|
|
|
void applyUnlocked(Iterable<int> ids) {
|
|
unlocked = ids.toSet();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Whether a fresh fetch is due for a fix at ([lat], [lon]).
|
|
bool needsZonesFor(double lat, double lon, {DateTime? now, double moveM = 2000, Duration maxAge = const Duration(minutes: 10)}) {
|
|
final (double, double)? at = zonesAround;
|
|
final DateTime? when = zonesAt;
|
|
if (at == null || when == null) return true;
|
|
if ((now ?? DateTime.now()).difference(when) > maxAge) return true;
|
|
return HudGeometry.distance(at.$1, at.$2, lat, lon) > moveM;
|
|
}
|
|
|
|
// ── Licences and the GEO database ─────────────────────────────────────────
|
|
|
|
/// Null until listed; false on aircraft without custom unlock zones.
|
|
bool? licencesSupported;
|
|
List<FlyZoneLicence> licences = const <FlyZoneLicence>[];
|
|
|
|
FlyZoneDatabase? database;
|
|
|
|
/// Basic database download, 0..1, while an upgrade runs; null otherwise.
|
|
double? upgradeProgress;
|
|
|
|
FlyZoneLicence? get enabledLicence => licences.where((FlyZoneLicence l) => l.enabled).firstOrNull;
|
|
|
|
void applyLicences(Map<String, dynamic> m) {
|
|
licencesSupported = asBool(m['supported']) ?? false;
|
|
licences = <FlyZoneLicence>[for (final Map<String, dynamic> z in asMapList(m['zones'])) FlyZoneLicence.fromMap(z)]
|
|
..sort((FlyZoneLicence a, FlyZoneLicence b) => a.index.compareTo(b.index));
|
|
notifyListeners();
|
|
}
|
|
|
|
void applyDatabase(Map<String, dynamic> m) {
|
|
final FlyZoneDatabase next = FlyZoneDatabase.fromMap(m);
|
|
// A read that came before the remote check keeps the version heard since.
|
|
database = next.preciseRemoteVersion == null ? next.withRemote(database?.preciseRemoteVersion) : next;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// The SDK heard which precise database DJI's server has (`flyZoneDbRemote`).
|
|
void applyRemoteVersion(String? v) {
|
|
database = (database ?? const FlyZoneDatabase()).withRemote(v);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// The SDK's progress float; read as a percentage when above 1. A complete
|
|
/// download ends the upgrade (MSDK does not say whether the start call's
|
|
/// completion waits for it).
|
|
void applyUpgradeProgress(double? p) {
|
|
final double? v = p == null ? null : (p > 1 ? p / 100 : p).clamp(0.0, 1.0);
|
|
upgradeProgress = v != null && v >= 1 ? null : v;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Aircraft-bound parts only: the zones database is app-level and stays.
|
|
void clearAircraft() {
|
|
state = null;
|
|
notice = null;
|
|
unlocked = const <int>{};
|
|
licencesSupported = null;
|
|
licences = const <FlyZoneLicence>[];
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// One licence on the aircraft (`CustomUnlockZone`): a DJI-issued unlock for a
|
|
/// circle, polygon, height or whole country, valid between two dates. One at a
|
|
/// time is enabled.
|
|
@immutable
|
|
class FlyZoneLicence {
|
|
const FlyZoneLicence({
|
|
required this.id,
|
|
this.index = 0,
|
|
this.name = '',
|
|
this.type = 'UNKNOWN',
|
|
this.latitude,
|
|
this.longitude,
|
|
this.radius,
|
|
this.height,
|
|
this.country,
|
|
this.points = const <(double, double)>[],
|
|
this.startTime,
|
|
this.endTime,
|
|
this.expired = false,
|
|
this.enabled = false,
|
|
});
|
|
|
|
factory FlyZoneLicence.fromMap(Map<String, dynamic> m) => FlyZoneLicence(
|
|
id: asInt(m['id']) ?? 0,
|
|
index: asInt(m['index']) ?? 0,
|
|
name: (asString(m['name']) ?? '').trim(),
|
|
type: asString(m['type']) ?? 'UNKNOWN',
|
|
latitude: finite(m['latitude']),
|
|
longitude: finite(m['longitude']),
|
|
radius: finite(m['radius']),
|
|
height: asInt(m['height']),
|
|
country: asString(m['country']),
|
|
points: <(double, double)>[
|
|
if (m['points'] is List)
|
|
for (final Object? v in m['points'] as List)
|
|
if (v is List && v.length == 2 && finite(v[0]) != null && finite(v[1]) != null) (finite(v[0])!, finite(v[1])!),
|
|
],
|
|
startTime: FlyZone._blank(asString(m['startTime'])),
|
|
endTime: FlyZone._blank(asString(m['endTime'])),
|
|
expired: asBool(m['expired']) ?? false,
|
|
enabled: asBool(m['enabled']) ?? false,
|
|
);
|
|
|
|
final int id;
|
|
final int index;
|
|
final String name;
|
|
|
|
/// `UnlockZoneType`: `GEO_UNLOCK`, `CIRCLE_UNLOCK_AREA`, `COUNTRY_UNLOCK`, `HEIGHT_UNLOCK`, `PENTAGON_UNLOCK`.
|
|
final String type;
|
|
final double? latitude;
|
|
final double? longitude;
|
|
final double? radius; // m
|
|
final int? height; // m, for height unlocks
|
|
final String? country;
|
|
final List<(double, double)> points;
|
|
final String? startTime;
|
|
final String? endTime;
|
|
final bool expired;
|
|
final bool enabled;
|
|
|
|
String get typeLabel => switch (type) {
|
|
'GEO_UNLOCK' => 'GEO zone unlock',
|
|
'CIRCLE_UNLOCK_AREA' => 'Circle unlock',
|
|
'PENTAGON_UNLOCK' => 'Area unlock',
|
|
'COUNTRY_UNLOCK' => 'Country unlock',
|
|
'HEIGHT_UNLOCK' => 'Height unlock',
|
|
_ => 'Licence',
|
|
};
|
|
|
|
/// Drawable on the map: a circle with a centre and radius, or a polygon.
|
|
bool get hasArea => points.length >= 3 || (latitude != null && longitude != null && (radius ?? 0) > 0);
|
|
|
|
String get validity => <String>[if (startTime != null) startTime!, if (endTime != null) endTime!].join(' → ');
|
|
}
|
|
|
|
/// The GEO database's versions and state (`FlyZoneDatabaseState` names).
|
|
@immutable
|
|
class FlyZoneDatabase {
|
|
const FlyZoneDatabase({this.basicVersion, this.basicState, this.preciseVersion, this.preciseState, this.preciseRemoteVersion});
|
|
|
|
factory FlyZoneDatabase.fromMap(Map<String, dynamic> m) => FlyZoneDatabase(
|
|
basicVersion: asString(m['basicVersion']),
|
|
basicState: asString(m['basicState']),
|
|
preciseVersion: asString(m['preciseVersion']),
|
|
preciseState: asString(m['preciseState']),
|
|
preciseRemoteVersion: asString(m['preciseRemoteVersion']),
|
|
);
|
|
|
|
final String? basicVersion;
|
|
final String? basicState; // INITIALIZING / NO_INTERNET_CONNECTION / OUT_OF_DATE / UP_TO_DATE / UNKNOWN
|
|
final String? preciseVersion;
|
|
final String? preciseState;
|
|
|
|
/// The newest precise database on DJI's server, once the SDK has asked.
|
|
final String? preciseRemoteVersion;
|
|
|
|
FlyZoneDatabase withRemote(String? v) => FlyZoneDatabase(
|
|
basicVersion: basicVersion,
|
|
basicState: basicState,
|
|
preciseVersion: preciseVersion,
|
|
preciseState: preciseState,
|
|
preciseRemoteVersion: v,
|
|
);
|
|
|
|
/// DJI has a newer precise database than the aircraft's (it updates with the firmware).
|
|
bool get preciseBehind =>
|
|
preciseRemoteVersion != null && preciseVersion != null && preciseRemoteVersion!.isNotEmpty && preciseRemoteVersion != preciseVersion;
|
|
|
|
bool get outOfDate => basicState == 'OUT_OF_DATE' || preciseState == 'OUT_OF_DATE' || preciseBehind;
|
|
|
|
static String stateLabel(String? s) => switch (s) {
|
|
'UP_TO_DATE' => 'Up to date',
|
|
'OUT_OF_DATE' => 'Out of date',
|
|
'NO_INTERNET_CONNECTION' => 'No internet connection',
|
|
'INITIALIZING' => 'Initializing',
|
|
_ => 'Unknown',
|
|
};
|
|
}
|