Files
PilotVault/Fly App/lib/fly/state/vision_state.dart
T
tajniak81andClaude Opus 5.5 f6bbb1cfee Batch the Fly App's telemetry and split it per subsystem
The GO 4-style HUD needs far more live data than the bridge sent, and
pushing every SDK callback straight to Dart would rebuild the screen
dozens of times a second. This is the telemetry foundation (plan P1).

On the Kotlin side, every source now writes into a TelemetryHub that
emits one telemetry event per tick: flight-controller state, gimbal and
vision at up to 10 Hz, battery, links, RC, storage and camera settings
folded in at up to 2 Hz. Each event carries the full latest snapshot, so
a consumer that drops packets never loses a slow group. New bridges
cover batteries (every pack, cell voltages, warning thresholds), link
quality and RC battery, diagnostics, and obstacle sensing per sensor.
KeyBridge adds generic getKey/setKey/listenKey over KeyManager for the
settings rows to come. The flight controller also gains smart-RTH
confirmation and simulator start/stop.

On the Dart side, FlightModel now owns one ChangeNotifier per subsystem
(aircraft, battery, links, camera, gimbal, vision, alerts, mission), so
a HUD widget can listen to just the one it draws. TelemetryReducer
routes events into them, only notifies groups that changed, and gives
FlightModel one notification per tick. Flat getters keep the existing
screens working. Diagnostics become graded, de-duplicated alerts.
Settings > About > Telemetry inspector shows every field live and can
start the simulator.

Battery no longer arrives as its own event, so the API Server reads the
percentage from the telemetry snapshot (the old event is still accepted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:34:56 +02:00

95 lines
2.9 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 '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();
}
void clear() {
sensors = const <String, VisionReading>{};
updatedAt = null;
notifyListeners();
}
}