Files
PilotVault/Fly App/lib/fly/state/mission_state.dart
T
tajniak81andClaude Opus 5.5 636da1ac2c Give the Fly App every remaining GO 4 flight mode and the missing settings
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>
2026-10-20 23:06:16 +02:00

485 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:ui' show Offset, Rect;
import 'package:flutter/foundation.dart';
import 'parse.dart';
/// The waypoint operator as `MissionBridge.kt` last reported it (plan P7).
@immutable
class WaypointProgress {
const WaypointProgress({
this.state = 'UNKNOWN',
this.uploaded,
this.total,
this.target,
this.reached = false,
this.executeState,
});
factory WaypointProgress.fromMap(Map<String, dynamic> e, WaypointProgress previous) {
final Map<String, dynamic>? up = asMap(e['upload']);
final Map<String, dynamic>? ex = asMap(e['execution']);
return WaypointProgress(
state: asString(e['state']) ?? previous.state,
uploaded: up == null ? previous.uploaded : asInt(up['index']),
total: asInt(ex?['total']) ?? asInt(up?['total']) ?? previous.total,
target: ex == null ? previous.target : asInt(ex['target']),
reached: ex == null ? previous.reached : asBool(ex['reached']) ?? false,
executeState: ex == null ? previous.executeState : asString(ex['executeState']),
);
}
/// MSDK `WaypointMissionState`: UNKNOWN, DISCONNECTED, NOT_SUPPORTED,
/// RECOVERING, READY_TO_UPLOAD, UPLOADING, READY_TO_EXECUTE, EXECUTING,
/// EXECUTION_PAUSED.
final String state;
/// Index of the last waypoint uploaded, and the mission's waypoint count.
final int? uploaded;
final int? total;
/// The waypoint being flown to, 0-based.
final int? target;
final bool reached;
/// MSDK `WaypointMissionExecuteState` (MOVING, DOING_ACTION, …).
final String? executeState;
bool get uploading => state == 'UPLOADING';
bool get readyToExecute => state == 'READY_TO_EXECUTE';
bool get executing => state == 'EXECUTING';
bool get paused => state == 'EXECUTION_PAUSED';
/// Flying the mission, paused or not.
bool get active => executing || paused;
/// The operator can take a new mission.
bool get available => state == 'READY_TO_UPLOAD' || state == 'READY_TO_EXECUTE';
bool get unsupported => state == 'NOT_SUPPORTED';
/// 0…1 through the upload, or null outside it.
double? get uploadFraction {
if (!uploading || total == null || total == 0) return null;
return (((uploaded ?? -1) + 1) / total!).clamp(0.0, 1.0);
}
String get executeLabel => switch (executeState) {
'INITIALIZING' => 'Starting',
'MOVING' || 'CURVE_MODE_MOVING' => 'Flying',
'CURVE_MODE_TURNING' => 'Turning',
'BEGIN_ACTION' || 'DOING_ACTION' => 'Action',
'FINISHED_ACTION' => 'Action done',
'RETURN_TO_FIRST_WAYPOINT' => 'Back to start',
'PAUSED' => 'Paused',
_ => paused ? 'Paused' : 'Flying',
};
@override
bool operator ==(Object other) =>
other is WaypointProgress &&
other.state == state &&
other.uploaded == uploaded &&
other.total == total &&
other.target == target &&
other.reached == reached &&
other.executeState == executeState;
@override
int get hashCode => Object.hash(state, uploaded, total, target, reached, executeState);
}
/// The ActiveTrack operator as last reported (plan P7b).
@immutable
class ActiveTrackProgress {
const ActiveTrackProgress({
this.state = 'UNKNOWN',
this.targetState,
this.targetType,
this.rect,
this.reason,
this.subjects = const <SensedSubject>[],
this.autoSensingSupported = false,
this.autoSensing = false,
this.percent,
});
/// A QuickShot's progress, 0–100, while it flies.
final int? percent;
/// Subjects the aircraft found itself (auto-sensing), to tap instead of drawing.
final List<SensedSubject> subjects;
final bool autoSensingSupported;
final bool autoSensing;
factory ActiveTrackProgress.fromMap(Map<String, dynamic> e, ActiveTrackProgress previous) {
final Map<String, dynamic>? t = asMap(e['tracking']);
final String state = asString(e['state']) ?? previous.state;
// A tracking-state-less event keeps the last target only while still tracking.
final bool keep = t == null && _tracking.contains(state);
final Map<String, dynamic>? r = asMap(t?['rect']);
return ActiveTrackProgress(
state: state,
targetState: t == null ? (keep ? previous.targetState : null) : asString(t['targetState']),
targetType: t == null ? (keep ? previous.targetType : null) : asString(t['targetType']),
rect: t == null ? (keep ? previous.rect : null) : _rect(r),
reason: t == null ? (keep ? previous.reason : null) : asString(t['reason']),
// Sensed subjects only mean something while auto-sensing looks for them.
subjects: t == null
? (state == 'AUTO_SENSING' ? previous.subjects : const <SensedSubject>[])
: <SensedSubject>[
for (final Map<String, dynamic> s in asMapList(t['subjects']))
if (_rect(asMap(s['rect'])) case final Rect box) SensedSubject(asInt(s['index']) ?? 0, box, asString(s['type'])),
],
autoSensingSupported: asBool(e['autoSensingSupported']) ?? previous.autoSensingSupported,
autoSensing: asBool(e['autoSensing']) ?? previous.autoSensing,
percent: t == null ? (keep ? previous.percent : null) : asInt(t['percent']),
);
}
static Rect? _rect(Map<String, dynamic>? r) {
if (r == null) return null;
final double? l = finite(r['l']), t = finite(r['t']), rr = finite(r['r']), b = finite(r['b']);
if (l == null || t == null || rr == null || b == null || rr <= l || b <= t) return null;
return Rect.fromLTRB(l, t, rr, b);
}
static const Set<String> _tracking = <String>{
'AIRCRAFT_FOLLOWING',
'ONLY_CAMERA_FOLLOWING',
'WAITING_FOR_CONFIRMATION',
'CANNOT_CONFIRM',
'FINDING_TRACKED_TARGET',
'PERFORMING_QUICK_SHOT',
};
/// MSDK `ActiveTrackState`.
final String state;
/// `ActiveTrackTargetState`: TRACKING_WITH_HIGH_CONFIDENCE, …_LOW_…, WAITING_FOR_CONFIRMATION, CANNOT_CONFIRM.
final String? targetState;
/// HUMAN, CAR, VAN, BIKE, BOAT, …
final String? targetType;
/// The subject's box, 0..1 across the live view.
final Rect? rect;
/// `ActiveTrackCannotConfirmReason`, when the aircraft cannot follow.
final String? reason;
/// A subject is selected and the operator is working on it.
bool get active => _tracking.contains(state);
bool get following => state == 'AIRCRAFT_FOLLOWING';
bool get waitingForConfirmation => state == 'WAITING_FOR_CONFIRMATION';
bool get cannotConfirm => state == 'CANNOT_CONFIRM' || targetState == 'CANNOT_CONFIRM';
bool get unsupported => state == 'NOT_SUPPORT';
bool get lowConfidence => targetState == 'TRACKING_WITH_LOW_CONFIDENCE';
String get stateLabel => switch (state) {
'AIRCRAFT_FOLLOWING' => 'Following',
'ONLY_CAMERA_FOLLOWING' => 'Camera only',
'WAITING_FOR_CONFIRMATION' => 'Confirm subject',
'CANNOT_CONFIRM' => 'Cannot follow',
'FINDING_TRACKED_TARGET' => 'Subject lost',
'PERFORMING_QUICK_SHOT' => 'QuickShot',
'CANNOT_START' => 'Cannot start',
'NOT_SUPPORT' => 'Not supported',
'AUTO_SENSING' => subjects.isEmpty ? 'Looking for subjects' : 'Tap a subject',
_ => 'Draw a box',
};
String? get reasonLabel => switch (reason) {
'UNSTABLE_TARGET' => 'Subject moving erratically',
'TARGET_TOO_HIGH' => 'Subject too high',
'BLOCKED_BY_OBSTACLE' => 'Obstacle in the way',
'GIMBAL_ATTITUDE_ERROR' => 'Gimbal angle',
'TARGET_TOO_FAR' => 'Subject too far',
'TARGET_TOO_CLOSE' => 'Subject too close',
'AIRCRAFT_TOO_HIGH' => 'Aircraft too high',
'AIRCRAFT_TOO_LOW' => 'Aircraft too low',
'OBSTACLE_SENSOR_ERROR' => 'Obstacle sensor error',
_ => null,
};
@override
bool operator ==(Object other) =>
other is ActiveTrackProgress &&
other.state == state &&
other.targetState == targetState &&
other.targetType == targetType &&
other.rect == rect &&
other.reason == reason &&
listEquals(other.subjects, subjects) &&
other.autoSensingSupported == autoSensingSupported &&
other.autoSensing == autoSensing &&
other.percent == percent;
@override
int get hashCode => Object.hash(state, targetState, targetType, rect, reason, Object.hashAll(subjects), autoSensingSupported, autoSensing, percent);
}
/// One subject auto-sensing found: its operator index and box (0..1 of the picture).
@immutable
class SensedSubject {
const SensedSubject(this.index, this.rect, this.type);
final int index;
final Rect rect;
final String? type;
@override
bool operator ==(Object other) => other is SensedSubject && other.index == index && other.rect == rect && other.type == type;
@override
int get hashCode => Object.hash(index, rect, type);
}
/// The TapFly operator as last reported (plan P7b).
@immutable
class TapFlyProgress {
const TapFlyProgress({this.state = 'UNKNOWN', this.image, this.bypass, this.speed, this.relativeHeading});
factory TapFlyProgress.fromMap(Map<String, dynamic> e, TapFlyProgress previous) {
final Map<String, dynamic>? x = asMap(e['execution']);
final String state = asString(e['state']) ?? previous.state;
final double? ix = finite(x?['imageX']), iy = finite(x?['imageY']);
final bool keep = x == null && _flying.contains(state);
return TapFlyProgress(
state: state,
image: x == null ? (keep ? previous.image : null) : (ix == null || iy == null ? null : Offset(ix, iy)),
bypass: x == null ? (keep ? previous.bypass : null) : asString(x['bypass']),
speed: x == null ? (keep ? previous.speed : null) : finite(x['speed']),
relativeHeading: x == null ? (keep ? previous.relativeHeading : null) : finite(x['relativeHeading']),
);
}
static const Set<String> _flying = <String>{'EXECUTION_STARTING', 'EXECUTING', 'EXECUTION_PAUSED', 'EXECUTION_RESETTING'};
/// MSDK `TapFlyMissionState`.
final String state;
/// Where the aircraft is heading on the live view, 0..1.
final Offset? image;
/// NONE / LEFT / RIGHT / OVER while going round an obstacle.
final String? bypass;
final double? speed;
final double? relativeHeading;
bool get active => _flying.contains(state);
bool get paused => state == 'EXECUTION_PAUSED';
bool get unsupported => state == 'NOT_SUPPORT';
String? get bypassLabel => switch (bypass) {
'LEFT' => 'Going round an obstacle: left',
'RIGHT' => 'Going round an obstacle: right',
'OVER' => 'Climbing over an obstacle',
_ => null,
};
@override
bool operator ==(Object other) =>
other is TapFlyProgress &&
other.state == state &&
other.image == image &&
other.bypass == bypass &&
other.speed == speed &&
other.relativeHeading == relativeHeading;
@override
int get hashCode => Object.hash(state, image, bypass, speed, relativeHeading);
}
/// The Point of Interest operator as last reported (plan P7).
@immutable
class HotpointProgress {
const HotpointProgress({this.state = 'UNKNOWN', this.radius, this.maxAngularVelocity});
factory HotpointProgress.fromMap(Map<String, dynamic> e, HotpointProgress previous) => HotpointProgress(
state: asString(e['state']) ?? previous.state,
radius: finite(e['radius']) ?? previous.radius,
maxAngularVelocity: finite(e['maxAngularVelocity']) ?? previous.maxAngularVelocity,
);
/// `HotpointMissionState`: UNKNOWN, DISCONNECTED, NOT_SUPPORTED, RECOVERING,
/// READY_TO_EXECUTE, INITIAL_PHASE (flying to the circle), EXECUTING, EXECUTION_PAUSED.
final String state;
final double? radius;
final double? maxAngularVelocity;
bool get active => state == 'INITIAL_PHASE' || state == 'EXECUTING' || state == 'EXECUTION_PAUSED';
bool get paused => state == 'EXECUTION_PAUSED';
bool get unsupported => state == 'NOT_SUPPORTED';
String get label => switch (state) {
'INITIAL_PHASE' => 'Flying to the circle',
'EXECUTING' => 'Circling',
'EXECUTION_PAUSED' => 'Paused',
'READY_TO_EXECUTE' => 'Ready',
'NOT_SUPPORTED' => 'Not supported',
_ => 'Set up',
};
@override
bool operator ==(Object other) =>
other is HotpointProgress && other.state == state && other.radius == radius && other.maxAngularVelocity == maxAngularVelocity;
@override
int get hashCode => Object.hash(state, radius, maxAngularVelocity);
}
/// The Follow Me operator as last reported (plan P7).
@immutable
class FollowMeProgress {
const FollowMeProgress({this.state = 'UNKNOWN', this.distance});
factory FollowMeProgress.fromMap(Map<String, dynamic> e, FollowMeProgress previous) => FollowMeProgress(
state: asString(e['state']) ?? previous.state,
distance: finite(e['distance']) ?? (asString(e['state']) == 'EXECUTING' ? previous.distance : null),
);
/// `FollowMeMissionState`: … READY_TO_EXECUTE, EXECUTING, GOT_STUCK, NOT_READY.
final String state;
/// Metres from the aircraft to the phone, while following.
final double? distance;
bool get active => state == 'EXECUTING' || state == 'GOT_STUCK';
bool get stuck => state == 'GOT_STUCK';
bool get unsupported => state == 'NOT_SUPPORTED';
@override
bool operator ==(Object other) => other is FollowMeProgress && other.state == state && other.distance == distance;
@override
int get hashCode => Object.hash(state, distance);
}
/// Waypoint / ActiveTrack / QuickShot execution, from the bridge's `mission` events.
///
/// Waypoint events carry `operator: 'waypoint'` and the operator's own state
/// ([waypoint]); the older ActiveTrack / QuickShot events carry a bare `state`.
/// [state] is the summary the rail and the gate read.
class MissionState extends ChangeNotifier {
WaypointProgress waypoint = const WaypointProgress();
ActiveTrackProgress activeTrack = const ActiveTrackProgress();
TapFlyProgress tapFly = const TapFlyProgress();
HotpointProgress hotpoint = const HotpointProgress();
FollowMeProgress followMe = const FollowMeProgress();
/// How the last POI / Follow Me ended, like [waypointResult]; bumped in [modeFinishes].
String? modeResult;
int modeFinishes = 0;
/// How the last waypoint mission ended: null while none has, `''` when it
/// finished cleanly, else the SDK's error.
String? waypointResult;
/// Bumped on every finish, so listeners can tell a new result from an old one.
int waypointFinishes = 0;
String _legacy = 'idle';
String? error;
/// idle / ready / uploading / executing / tracking / quickshot / tapfly / hotpoint / followme.
String get state {
if (waypoint.active) return 'executing';
if (activeTrack.active) return activeTrack.state == 'PERFORMING_QUICK_SHOT' ? 'quickshot' : 'tracking';
if (tapFly.active) return 'tapfly';
if (hotpoint.active) return 'hotpoint';
if (followMe.active) return 'followme';
if (_legacy != 'idle') return _legacy;
if (waypoint.uploading) return 'uploading';
if (waypoint.readyToExecute) return 'ready';
return 'idle';
}
set state(String v) {
_legacy = v;
notifyListeners();
}
bool get running => state != 'idle';
bool get tracking => state == 'tracking' || state == 'quickshot';
void apply(Map<String, dynamic> e) {
switch (asString(e['operator'])) {
case 'waypoint':
_applyWaypoint(e);
return;
case 'activeTrack':
final ActiveTrackProgress next = ActiveTrackProgress.fromMap(e, activeTrack);
final String? err = asString(e['error']);
if (next == activeTrack && err == null) return;
activeTrack = next;
if (err != null) error = err;
// The operator now speaks for itself; drop the bridge's old guess.
if (_legacy == 'tracking' || _legacy == 'quickshot') _legacy = 'idle';
notifyListeners();
return;
case 'tapFly':
final TapFlyProgress next = TapFlyProgress.fromMap(e, tapFly);
final String? err = asString(e['error']);
if (next == tapFly && err == null) return;
tapFly = next;
if (err != null) error = err;
notifyListeners();
return;
case 'hotpoint':
final HotpointProgress next = HotpointProgress.fromMap(e, hotpoint);
if (!_finish(e) && next == hotpoint) return;
hotpoint = next;
notifyListeners();
return;
case 'followMe':
final FollowMeProgress next = FollowMeProgress.fromMap(e, followMe);
if (!_finish(e) && next == followMe) return;
followMe = next;
notifyListeners();
return;
}
_legacy = asString(e['state']) ?? 'idle';
error = asString(e['error']);
notifyListeners();
}
/// Records a POI / Follow Me `finish` (and its error); true when the event said anything.
bool _finish(Map<String, dynamic> e) {
final String? err = asString(e['error']);
if (err != null) error = err;
if (asString(e['event']) == 'finish') {
modeResult = err ?? '';
modeFinishes++;
return true;
}
return err != null;
}
void _applyWaypoint(Map<String, dynamic> e) {
final WaypointProgress next = WaypointProgress.fromMap(e, waypoint);
final String? event = asString(e['event']);
final String? err = asString(e['error']);
if (event == 'finish') {
waypointResult = err ?? '';
waypointFinishes++;
} else if (event == 'start') {
waypointResult = null;
}
if (err != null) error = err;
if (next == waypoint && event == null && err == null) return;
waypoint = next;
notifyListeners();
}
void clear() {
_legacy = 'idle';
error = null;
waypoint = const WaypointProgress();
activeTrack = const ActiveTrackProgress();
tapFly = const TapFlyProgress();
hotpoint = const HotpointProgress();
followMe = const FollowMeProgress();
notifyListeners();
}
}