# FlightMonitor/controller/live_data_processor.py """ Manages the processing of live flight data from the adapter's output queue. It pulls data, saves it to storage, updates GUI elements, and handles adapter status messages. """ import tkinter as tk from queue import Queue, Empty as QueueEmpty from datetime import datetime, timezone from typing import List, Optional, Dict, Any, TYPE_CHECKING import time from flightmonitor.utils.logger import get_logger from flightmonitor.data.common_models import CanonicalFlightState from flightmonitor.data.opensky_live_adapter import ( AdapterMessage, MSG_TYPE_FLIGHT_DATA, MSG_TYPE_ADAPTER_STATUS, STATUS_STARTING, STATUS_FETCHING, STATUS_RECOVERED, STATUS_RATE_LIMITED, STATUS_API_ERROR_TEMPORARY, STATUS_PERMANENT_FAILURE, STATUS_STOPPED, ) from flightmonitor.utils.gui_utils import ( GUI_STATUS_OK, GUI_STATUS_WARNING, GUI_STATUS_ERROR, GUI_STATUS_FETCHING, ) if TYPE_CHECKING: from flightmonitor.controller.app_controller import AppController from flightmonitor.gui.main_window import MainWindow module_logger = get_logger(__name__) GUI_QUEUE_CHECK_INTERVAL_MS = 150 class LiveDataProcessor: """ Processes live flight data from a queue, dispatching updates to various parts of the application (storage, GUI) and managing adapter status. """ def __init__(self, app_controller: "AppController", flight_data_queue: Queue[AdapterMessage]): self.app_controller = app_controller self.flight_data_queue = flight_data_queue self._gui_after_id: Optional[str] = None module_logger.debug("LiveDataProcessor initialized.") def start_processing_queue(self): main_window = self.app_controller.main_window if not (main_window and main_window.root and main_window.root.winfo_exists()): module_logger.error("LiveDataProcessor: Cannot start queue processing. MainWindow or root is missing.") return if self._gui_after_id: try: main_window.root.after_cancel(self._gui_after_id) except Exception: pass module_logger.info("LiveDataProcessor: Starting live data queue processing loop.") self._gui_after_id = main_window.root.after(GUI_QUEUE_CHECK_INTERVAL_MS, self._process_queue_cycle) def stop_processing_queue(self): main_window = self.app_controller.main_window if self._gui_after_id and main_window and main_window.root and main_window.root.winfo_exists(): try: main_window.root.after_cancel(self._gui_after_id) module_logger.info("LiveDataProcessor: Stopped live data queue processing loop.") except Exception: pass finally: self._gui_after_id = None self._process_queue_cycle(is_final_flush=True) def _process_queue_cycle(self, is_final_flush: bool = False): main_window: Optional["MainWindow"] = self.app_controller.main_window data_storage = self.app_controller.data_storage aircraft_db_manager = self.app_controller.aircraft_db_manager is_live_monitoring_active = self.app_controller.is_live_monitoring_active active_detail_window_ref = self.app_controller.active_detail_window_ref active_detail_window_icao = self.app_controller.active_detail_window_icao if not (main_window and main_window.root and main_window.root.winfo_exists()): self._gui_after_id = None return flight_payloads_this_cycle: List[CanonicalFlightState] = [] try: while not self.flight_data_queue.empty(): message = self.flight_data_queue.get(block=False) try: message_type = message.get("type") if message_type == MSG_TYPE_FLIGHT_DATA: # MODIFICATO: Estrae il payload strutturato (canonico + grezzo) payload_dict = message.get("payload", {}) flight_states_payload: Optional[List[CanonicalFlightState]] = payload_dict.get("canonical") raw_json_payload: str = payload_dict.get("raw_json", "{}") if flight_states_payload is not None: # MODIFICATO: Notifica il controller per il salvataggio dei dati grezzi if hasattr(self.app_controller, 'process_raw_data_logging'): self.app_controller.process_raw_data_logging(raw_json_payload, flight_states_payload) # MODIFICATO: Notifica il controller per aggiornare la tabella di riepilogo if hasattr(self.app_controller, 'update_live_summary_table'): self.app_controller.update_live_summary_table(time.time(), len(flight_states_payload)) flight_payloads_this_cycle.extend(flight_states_payload) if data_storage: for state in flight_states_payload: if not isinstance(state, CanonicalFlightState): continue try: flight_id = data_storage.add_or_update_flight_daily( icao24=state.icao24, callsign=state.callsign, origin_country=state.origin_country, detection_timestamp=state.timestamp ) if flight_id: data_storage.add_position_daily(flight_id, state) except Exception as e_db_add: module_logger.error(f"LiveDataProcessor: Error saving flight/position to DB for ICAO {state.icao24}: {e_db_add}", exc_info=True) if main_window.map_manager_instance and self.app_controller.is_live_monitoring_active: main_window.map_manager_instance.update_flights_on_map(flight_states_payload) gui_message = f"Live data: {len(flight_states_payload)} aircraft in area." if flight_states_payload else "Live data: No aircraft in area." main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message) else: main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload from adapter.") elif message_type == MSG_TYPE_ADAPTER_STATUS: status_code = message.get("status_code") gui_message_from_adapter = message.get("message", f"Adapter status: {status_code}") gui_status_level = GUI_STATUS_OK if status_code == STATUS_PERMANENT_FAILURE: gui_status_level = GUI_STATUS_ERROR self.app_controller.stop_live_monitoring(from_error=True) elif status_code == STATUS_API_ERROR_TEMPORARY: gui_status_level = GUI_STATUS_ERROR elif status_code == STATUS_RATE_LIMITED: gui_status_level = GUI_STATUS_WARNING elif status_code == STATUS_FETCHING: gui_status_level = GUI_STATUS_FETCHING main_window.update_semaphore_and_status(gui_status_level, gui_message_from_adapter) except Exception as e_msg_proc: module_logger.error(f"LiveDataProcessor: Error processing adapter message: {e_msg_proc}", exc_info=True) finally: self.flight_data_queue.task_done() if active_detail_window_ref and active_detail_window_icao and active_detail_window_ref.winfo_exists(): latest_live_data_for_detail_icao: Optional[Dict[str, Any]] = None for state_obj in flight_payloads_this_cycle: if state_obj.icao24 == active_detail_window_icao: latest_live_data_for_detail_icao = state_obj.to_dict() if latest_live_data_for_detail_icao: static_data_upd = aircraft_db_manager.get_aircraft_details(active_detail_window_icao) if aircraft_db_manager else None full_track_data_list_upd: List[Dict[str, Any]] = [] if data_storage: try: track_states_upd = data_storage.get_flight_track_for_icao_on_date(active_detail_window_icao, datetime.now(timezone.utc)) if track_states_upd: full_track_data_list_upd = [s.to_dict() for s in track_states_upd] except Exception as e_track_upd: module_logger.error(f"LiveDataProcessor: Error retrieving updated track for detail view {active_detail_window_icao}: {e_track_upd}") try: active_detail_window_ref.update_details(static_data_upd, latest_live_data_for_detail_icao, full_track_data_list_upd) except Exception as e_upd_detail_win: module_logger.error(f"LiveDataProcessor: Error updating detail window for {active_detail_window_icao}: {e_upd_detail_win}", exc_info=True) except QueueEmpty: pass # No messages to process, which is normal except Exception as e_outer: module_logger.error(f"LiveDataProcessor: Outer error in _process_queue_cycle: {e_outer}", exc_info=True) finally: if not is_final_flush and is_live_monitoring_active and main_window.root.winfo_exists(): self._gui_after_id = main_window.root.after(GUI_QUEUE_CHECK_INTERVAL_MS, self._process_queue_cycle) else: self._gui_after_id = None