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 [], }); factory VisionReading.fromMap(String position, Map 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: [ for (final Map 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 sectors; bool get dangerous => systemWarning == 'DANGEROUS'; } /// Omnidirectional perception (Mavic 2-class): one distance per [angleInterval] /// degrees clockwise from the nose, plus straight up and down. Null distances /// saw nothing in range. @immutable class Perception { const Perception({required this.angleInterval, this.distances = const [], this.up, this.down}); factory Perception.fromMap(Map m) => Perception( angleInterval: finite(m['angleInterval']) ?? 0, distances: [if (m['distances'] is List) for (final Object? d in m['distances'] as List) finite(d)], up: finite(m['up']), down: finite(m['down']), ); final double angleInterval; // deg final List distances; // m final double? up; final double? down; } /// One obstacle-sensing direction's health (`ObstacleAvoidanceSensorState`). @immutable class SensorHealth { const SensorHealth({required this.working, required this.enabled}); final bool working; final bool enabled; /// Switched on but not seeing: covered, dirty or too dark. bool get blocked => enabled && !working; } /// 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 sensors = const {}; /// Omni perception, on the aircraft that report it (`perception` group). Perception? perception; /// Per-direction sensor health, keyed `forward` / `backward` / `left` / /// `right` / `up` / `down` (`obstacleSensors` group). Map health = const {}; DateTime? updatedAt; void applyPerception(Map? m) { perception = m == null ? null : Perception.fromMap(m); notifyListeners(); } void applyHealth(Map? m) { health = { if (m != null) for (final MapEntry e in m.entries) if (e.value is Map) e.key: SensorHealth( working: asBool((e.value as Map)['working']) ?? true, enabled: asBool((e.value as Map)['enabled']) ?? true, ), }; notifyListeners(); } 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? vision) { sensors = { if (vision != null) for (final MapEntry e in vision.entries) if (e.value is Map) e.key: VisionReading.fromMap(e.key, Map.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 {'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 {}; perception = null; health = const {}; updatedAt = null; palmControl = null; notifyListeners(); } }