import 'package:flutter/foundation.dart'; import 'parse.dart'; enum AlertSeverity { info, warning, critical } /// One live diagnostic from the aircraft (`DJIDiagnostics`). @immutable class DiagnosticAlert { const DiagnosticAlert({ required this.component, required this.code, this.subCode, this.componentIndex, required this.reason, this.solution, required this.severity, required this.firstSeen, }); /// `FLIGHT_CONTROLLER`, `BATTERY`, `CAMERA`, `GIMBAL`, `VISION`, `REMOTE_CONTROLLER` … final String component; final int code; final int? subCode; final int? componentIndex; final String reason; final String? solution; final AlertSeverity severity; final DateTime firstSeen; /// What de-duplicates an alert: the same fault on the same component. String get key => '$component/$code/${componentIndex ?? 0}'; } /// The aircraft's active diagnostics, graded and ordered for the tip marquee, /// the error stack and the Aircraft Status sheet. /// /// The bridge sends the *complete* current list on every change, so [apply] /// replaces the set: what is no longer listed has cleared. An alert keeps its /// [DiagnosticAlert.firstSeen] for as long as it stays listed, and one the pilot /// [dismiss]ed stays hidden until it clears and comes back. class AlertState extends ChangeNotifier { List _all = const []; final Set _dismissed = {}; /// Keys that appeared in the last [apply] — what P3 buzzes and pops a card for. Set lastAdded = const {}; DateTime? updatedAt; /// Every listed alert, most severe first, then oldest first. List get all => _all; /// [all] minus what the pilot dismissed. List get active => [for (final DiagnosticAlert a in _all) if (!_dismissed.contains(a.key)) a]; /// The marquee's message: the most severe alert, dismissed or not. DiagnosticAlert? get top => _all.isEmpty ? null : _all.first; AlertSeverity? get worst => top?.severity; void apply(List> items, {DateTime? now}) { final DateTime t = now ?? DateTime.now(); final Map previous = {for (final DiagnosticAlert a in _all) a.key: a}; final Map next = {}; for (final Map m in items) { final String component = asString(m['component']) ?? 'OTHER'; final int code = asInt(m['code']) ?? 0; final String reason = (asString(m['reason']) ?? '').trim(); final String? solution = asString(m['solution'])?.trim(); final int? index = asInt(m['componentIndex']); final String key = '$component/$code/${index ?? 0}'; if (next.containsKey(key)) continue; next[key] = DiagnosticAlert( component: component, code: code, subCode: asInt(m['subCode']), componentIndex: index, reason: reason.isEmpty ? '$component error $code' : reason, solution: (solution == null || solution.isEmpty) ? null : solution, severity: grade(component, reason, solution), firstSeen: previous[key]?.firstSeen ?? t, ); } lastAdded = {for (final String k in next.keys) if (!previous.containsKey(k)) k}; // A cleared alert forgets its dismissal, so it pops up again if it returns. _dismissed.removeWhere((String k) => !next.containsKey(k)); _all = next.values.toList() ..sort((DiagnosticAlert a, DiagnosticAlert b) { final int s = b.severity.index.compareTo(a.severity.index); return s != 0 ? s : a.firstSeen.compareTo(b.firstSeen); }); updatedAt = t; notifyListeners(); } void dismiss(String key) { if (_dismissed.add(key)) notifyListeners(); } void clear() { _all = const []; _dismissed.clear(); lastAdded = const {}; updatedAt = null; notifyListeners(); } static final RegExp _critical = RegExp( r'critical|serious|forced? landing|landing immediately|auto[- ]?land|motor|propeller|\besc\b|overheat|' r'imu (error|abnormal)|compass (error|abnormal)|obstacle|collision', caseSensitive: false, ); static final RegExp _info = RegExp(r'calibrat(ing|ion in progress)|updating|initializ|preheat', caseSensitive: false); /// The SDK attaches no severity, so it is read off the message. A heuristic /// until runtime capture (plan R2) maps GO 4's own code → colour table. @visibleForTesting static AlertSeverity grade(String component, String reason, String? solution) { final String text = '$reason ${solution ?? ''}'; if (_critical.hasMatch(text)) return AlertSeverity.critical; if (_info.hasMatch(text)) return AlertSeverity.info; return AlertSeverity.warning; } }