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>
102 lines
4.2 KiB
Dart
102 lines
4.2 KiB
Dart
import 'dart:ui' show Size;
|
|
|
|
import '../flight_model.dart';
|
|
import 'state/parse.dart';
|
|
|
|
/// Routes the bridge's telemetry-bearing events into [FlightModel]'s sub-models
|
|
/// (plan P1). Kept out of the widget tree so it can be unit-tested against
|
|
/// recorded events.
|
|
///
|
|
/// A `telemetry` event is the native [TelemetryHub]'s full snapshot: the flight
|
|
/// controller's flat keys plus one nested group per subsystem (`battery`,
|
|
/// `links`, `rc`, `storage`, `ssd`, `thermal`, `cameraSettings`, `gimbal`,
|
|
/// `gimbalSettings`, `vision`, `calibration`). It arrives at
|
|
/// up to 10 Hz with the slow groups repeated unchanged between their 2 Hz
|
|
/// refreshes, so each group is compared with the last one seen and its
|
|
/// sub-model only notified when it actually moved. A group missing from the
|
|
/// snapshot was never reported by this aircraft and leaves its sub-model empty.
|
|
class TelemetryReducer {
|
|
TelemetryReducer(this.model);
|
|
|
|
final FlightModel model;
|
|
|
|
final Map<String, Object?> _lastGroups = <String, Object?>{};
|
|
|
|
static const List<String> _groups = <String>['battery', 'links', 'rc', 'storage', 'ssd', 'thermal', 'cameraSettings', 'gimbal', 'gimbalSettings', 'vision', 'calibration', 'palmControl'];
|
|
|
|
/// Applies [event] if it is one of the telemetry types; returns whether it was.
|
|
bool apply(Map<String, dynamic> event) {
|
|
switch (event['type'] as String?) {
|
|
case 'telemetry':
|
|
// Stray packets trailing a disconnect would repopulate what was cleared.
|
|
if (model.connected) model.batch(() => _telemetry(event));
|
|
return true;
|
|
case 'camera':
|
|
if (model.connected) model.camera.applySystem(event);
|
|
return true;
|
|
case 'exposure':
|
|
if (model.connected) model.camera.applyExposure(event);
|
|
return true;
|
|
case 'histogram':
|
|
if (model.connected) model.camera.applyHistogram(event);
|
|
return true;
|
|
case 'diagnostics':
|
|
if (model.connected) model.alerts.apply(asMapList(event['items']));
|
|
return true;
|
|
case 'key':
|
|
final String? id = asString(event['id']);
|
|
if (model.connected && id != null) model.keys.apply(id, event['value']);
|
|
return true;
|
|
case 'mission':
|
|
model.mission.apply(event);
|
|
return true;
|
|
case 'videoSize':
|
|
final int? w = asInt(event['width']), h = asInt(event['height']);
|
|
if (w != null && h != null && w > 0 && h > 0) model.videoSize.value = Size(w.toDouble(), h.toDouble());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Forgets the last-seen groups, so the next aircraft's first tick applies in full.
|
|
void reset() => _lastGroups.clear();
|
|
|
|
void _telemetry(Map<String, dynamic> t) {
|
|
// Flight-controller keys change every tick; absent until the FC first reports.
|
|
if (t.containsKey('satelliteCount')) model.aircraft.apply(t);
|
|
|
|
final Set<String> changed = <String>{};
|
|
for (final String g in _groups) {
|
|
final Object? v = t[g];
|
|
if (_lastGroups.containsKey(g) && deepEquals(_lastGroups[g], v)) continue;
|
|
_lastGroups[g] = v;
|
|
changed.add(g);
|
|
}
|
|
if (changed.isEmpty) return;
|
|
|
|
if (changed.contains('battery')) {
|
|
final Map<String, dynamic>? b = asMap(t['battery']);
|
|
b == null ? model.battery.clear() : model.battery.apply(b);
|
|
}
|
|
if (changed.contains('links') || changed.contains('rc')) {
|
|
model.links.apply(links: asMap(t['links']), rc: asMap(t['rc']));
|
|
}
|
|
if (const <String>['storage', 'ssd', 'thermal', 'cameraSettings'].any(changed.contains)) {
|
|
model.camera.applyTelemetry(
|
|
storage: asMap(t['storage']),
|
|
ssd: asMap(t['ssd']),
|
|
thermal: asMap(t['thermal']),
|
|
settings: asMap(t['cameraSettings']),
|
|
);
|
|
}
|
|
if (changed.contains('gimbal')) {
|
|
final Map<String, dynamic>? g = asMap(t['gimbal']);
|
|
g == null ? model.gimbal.clear() : model.gimbal.apply(g);
|
|
}
|
|
if (changed.contains('gimbalSettings')) model.gimbal.applySettings(asMap(t['gimbalSettings']));
|
|
if (changed.contains('vision')) model.vision.apply(asMap(t['vision']));
|
|
if (changed.contains('calibration')) model.calibration.apply(asMap(t['calibration']));
|
|
if (changed.contains('palmControl')) model.vision.applyPalm(asString(t['palmControl']));
|
|
}
|
|
}
|