Auto-detect: on aircraft that can sense subjects themselves, the ActiveTrack panel offers Auto-detect; the subjects it finds show as dashed boxes on the video, and tapping one then GO follows it by its index. Trace gets a Backward switch (fly ahead of a subject walking towards the aircraft, obstacle-sensing aircraft only) and TapFly Free a Reset heading button. Settings > Vision gains Gesture mode and Palm control where the flight assistant supports them, and the HUD shows when palm control is flying the aircraft. The live view used to stretch the feed over the screen. It now fits the picture to the decoder's own size with black bars, and reports that size, so the ActiveTrack / TapFly overlays, tap-to-focus / meter and the grid all map into the picture and ignore taps on the bars. Not yet run on a simulator or aircraft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
380 lines
14 KiB
Dart
380 lines
14 KiB
Dart
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,
|
|
});
|
|
|
|
/// 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,
|
|
);
|
|
}
|
|
|
|
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;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(state, targetState, targetType, rect, reason, Object.hashAll(subjects), autoSensingSupported, autoSensing);
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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();
|
|
|
|
/// 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.
|
|
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 (_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;
|
|
}
|
|
_legacy = asString(e['state']) ?? 'idle';
|
|
error = asString(e['error']);
|
|
notifyListeners();
|
|
}
|
|
|
|
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();
|
|
notifyListeners();
|
|
}
|
|
}
|