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`, `perception`, `obstacleSensors`, /// `airSense`, `airSenseAvoidance`, `flyZone`). 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 _lastGroups = {}; static const List _groups = ['battery', 'links', 'rc', 'storage', 'ssd', 'thermal', 'cameraSettings', 'gimbal', 'gimbalSettings', 'vision', 'calibration', 'palmControl', 'perception', 'obstacleSensors', 'airSense', 'airSenseAvoidance', 'flyZone']; /// Applies [event] if it is one of the telemetry types; returns whether it was. bool apply(Map 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 'flySafe': // The GEO system is app-level: its notices count with or without an aircraft. model.flyZones.applyNotice(event); return true; case 'flyZoneDbProgress': model.flyZones.applyUpgradeProgress(finite(event['progress'])); 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 t) { // Flight-controller keys change every tick; absent until the FC first reports. if (t.containsKey('satelliteCount')) model.aircraft.apply(t); final Set changed = {}; 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? 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 ['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? 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'])); if (changed.contains('perception')) model.vision.applyPerception(asMap(t['perception'])); if (changed.contains('obstacleSensors')) model.vision.applyHealth(asMap(t['obstacleSensors'])); if (changed.contains('airSense')) model.airSense.apply(asMap(t['airSense'])); if (changed.contains('airSenseAvoidance')) model.airSense.applyAvoidance(asMap(t['airSenseAvoidance'])); if (changed.contains('flyZone')) model.flyZones.applyState(asMap(t['flyZone'])); } }