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>
116 lines
3.7 KiB
Dart
116 lines
3.7 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
|
||
import 'parse.dart';
|
||
|
||
/// One obstacle-detection sector of a sensor, left to right.
|
||
@immutable
|
||
class VisionSector {
|
||
const VisionSector({this.distance, this.warningLevel});
|
||
|
||
final double? distance; // m
|
||
/// `LEVEL_1` (far) … `LEVEL_6` (very close), or `INVALID`.
|
||
final String? warningLevel;
|
||
|
||
/// 0 when invalid or unknown, else 1–6.
|
||
int get level {
|
||
final String? w = warningLevel;
|
||
if (w == null || !w.startsWith('LEVEL_')) return 0;
|
||
return int.tryParse(w.substring(6)) ?? 0;
|
||
}
|
||
}
|
||
|
||
/// What one vision sensor position (nose, tail, left, right) sees.
|
||
@immutable
|
||
class VisionReading {
|
||
const VisionReading({
|
||
required this.position,
|
||
this.isSensorBeingUsed = false,
|
||
this.isDisabled = false,
|
||
this.systemWarning,
|
||
this.distance,
|
||
this.avoidAlertLevel,
|
||
this.sectors = const <VisionSector>[],
|
||
});
|
||
|
||
factory VisionReading.fromMap(String position, Map<String, dynamic> m) => VisionReading(
|
||
position: position,
|
||
isSensorBeingUsed: asBool(m['isSensorBeingUsed']) ?? false,
|
||
isDisabled: asBool(m['isDisabled']) ?? false,
|
||
systemWarning: asString(m['systemWarning']),
|
||
distance: finite(m['distance']),
|
||
avoidAlertLevel: asInt(m['avoidAlertLevel']),
|
||
sectors: <VisionSector>[
|
||
for (final Map<String, dynamic> s in asMapList(m['sectors']))
|
||
VisionSector(distance: finite(s['distance']), warningLevel: asString(s['warningLevel'])),
|
||
],
|
||
);
|
||
|
||
final String position; // NOSE / TAIL / LEFT / RIGHT
|
||
final bool isSensorBeingUsed;
|
||
final bool isDisabled;
|
||
final String? systemWarning; // SAFE / DANGEROUS / INVALID
|
||
final double? distance; // m, closest obstacle
|
||
final int? avoidAlertLevel;
|
||
final List<VisionSector> sectors;
|
||
|
||
bool get dangerous => systemWarning == 'DANGEROUS';
|
||
}
|
||
|
||
/// Obstacle sensing per sensor position. Fed from the `vision` group of the
|
||
/// telemetry tick; empty on aircraft without obstacle sensing.
|
||
class VisionState extends ChangeNotifier {
|
||
Map<String, VisionReading> sensors = const <String, VisionReading>{};
|
||
|
||
DateTime? updatedAt;
|
||
|
||
VisionReading? get nose => sensors['NOSE'];
|
||
VisionReading? get tail => sensors['TAIL'];
|
||
|
||
/// The nearest obstacle any sensor reports, in metres.
|
||
double? get closest {
|
||
double? best;
|
||
for (final VisionReading r in sensors.values) {
|
||
final double? d = r.distance;
|
||
if (d != null && (best == null || d < best)) best = d;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
void apply(Map<String, dynamic>? vision) {
|
||
sensors = <String, VisionReading>{
|
||
if (vision != null)
|
||
for (final MapEntry<String, dynamic> e in vision.entries)
|
||
if (e.value is Map) e.key: VisionReading.fromMap(e.key, Map<String, dynamic>.from(e.value as Map)),
|
||
};
|
||
if (vision != null) updatedAt = DateTime.now();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// MSDK `PalmControlState` (Spark's palm control), from the tick's `palmControl`.
|
||
String? palmControl;
|
||
|
||
/// The aircraft is reading or following the pilot's palm.
|
||
bool get palmActive => const <String>{'INITIALIZING', 'RECOGNIZING_PALM', 'STARTING_FOLLOW', 'FOLLOWING', 'ADJUSTING_POSITION'}.contains(palmControl);
|
||
|
||
String get palmLabel => switch (palmControl) {
|
||
'INITIALIZING' || 'RECOGNIZING_PALM' => 'Looking for your palm',
|
||
'STARTING_FOLLOW' => 'Palm found',
|
||
'FOLLOWING' => 'Following your palm',
|
||
'ADJUSTING_POSITION' => 'Adjusting position',
|
||
_ => 'Off',
|
||
};
|
||
|
||
void applyPalm(String? state) {
|
||
if (state == palmControl) return;
|
||
palmControl = state;
|
||
notifyListeners();
|
||
}
|
||
|
||
void clear() {
|
||
sensors = const <String, VisionReading>{};
|
||
updatedAt = null;
|
||
palmControl = null;
|
||
notifyListeners();
|
||
}
|
||
}
|