SXXXXXXX_FlightMonitor/flightmonitor/controller/app_controller.py

359 lines
21 KiB
Python

# FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty
import threading
import tkinter as tk # Only for tk.TclError if absolutely needed by GUI interaction
import time # Potentially for timestamps if not from adapter
# Relative imports
from ..data.opensky_live_adapter import (
OpenSkyLiveAdapter,
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 # Make sure STATUS_OK is not from here if not defined
)
from ..data import config
from ..utils.logger import get_logger
from ..data.storage import DataStorage
from ..data.common_models import CanonicalFlightState # For type hinting List[CanonicalFlightState]
from typing import List, Optional, Dict, Any # For type hints
module_logger = get_logger(__name__) # flightmonitor.controller.app_controller
GUI_QUEUE_CHECK_INTERVAL_MS = 150 # Check queue a bit less frequently
ADAPTER_JOIN_TIMEOUT_SECONDS = 2.0 # MODIFICA: Timeout per l'attesa del thread adapter
# Define GUI status levels that MainWindow.update_semaphore_and_status expects
GUI_STATUS_OK = "OK"
GUI_STATUS_WARNING = "WARNING"
GUI_STATUS_ERROR = "ERROR"
GUI_STATUS_FETCHING = "FETCHING" # For the new semaphore color
GUI_STATUS_UNKNOWN = "UNKNOWN"
class AppController:
def __init__(self, main_window: Optional[tk.Tk]=None): # main_window type can be more specific if MainWindow class is imported
self.main_window = main_window # Will be set by set_main_window
self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None
self.is_live_monitoring_active: bool = False
self.flight_data_queue: Optional[Queue[AdapterMessage]] = None
self._gui_after_id: Optional[str] = None
self._active_bounding_box: Optional[Dict[str, float]] = None
self.data_storage: Optional[DataStorage] = None
try:
self.data_storage = DataStorage()
module_logger.info("DataStorage initialized successfully by AppController.")
except Exception as e:
module_logger.critical(f"CRITICAL: Failed to initialize DataStorage in AppController: {e}", exc_info=True)
self.data_storage = None
# GUI error message will be shown in set_main_window if main_window is available
module_logger.info("AppController initialized.")
def set_main_window(self, main_window_instance): # Parameter renamed for clarity
"""Sets the main window instance and shows initial status or errors."""
self.main_window = main_window_instance
module_logger.debug(f"Main window instance ({type(main_window_instance)}) set in AppController.")
if self.main_window and hasattr(self.main_window, 'update_semaphore_and_status'):
if not self.data_storage:
err_msg = "Data storage init failed. Data will not be saved. Check logs."
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg)
# Optionally, also show a messagebox here if it's critical for app function
# self.main_window.show_error_message("Critical Storage Error", err_msg)
else:
# If DataStorage is fine, MainWindow should have already set its initial "Ready" status.
# We don't need to override it from here unless there's a specific controller state.
pass
else:
module_logger.error("Main window not set or lacks update_semaphore_and_status during set_main_window.")
def _process_flight_data_queue(self):
"""Processes messages from the OpenSkyLiveAdapter's output queue."""
if not self.flight_data_queue:
module_logger.warning("_process_flight_data_queue: flight_data_queue is None.")
return
try:
while not self.flight_data_queue.empty(): # Process all available messages
message: AdapterMessage = self.flight_data_queue.get_nowait()
self.flight_data_queue.task_done() # Important for queue management
message_type = message.get("type")
gui_message: str = message.get("message", "Processing...") # Default message
if message_type == MSG_TYPE_FLIGHT_DATA:
flight_states_payload: Optional[List[CanonicalFlightState]] = message.get("payload")
if flight_states_payload is not None: # Payload can be an empty list
module_logger.debug(f"Received flight data with {len(flight_states_payload)} states.")
if self.data_storage:
saved_count = 0
for state in flight_states_payload:
if not isinstance(state, CanonicalFlightState): continue
flight_id = self.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:
pos_id = self.data_storage.add_position_daily(flight_id, state)
if pos_id: saved_count += 1
if saved_count > 0: module_logger.info(f"Saved {saved_count} position updates to DB.")
if self.main_window and self.is_live_monitoring_active and self._active_bounding_box:
self.main_window.display_flights_on_canvas(flight_states_payload, self._active_bounding_box)
gui_message = f"Live data: {len(flight_states_payload)} aircraft tracked." if flight_states_payload else "Live data: No aircraft in area."
# Solo aggiorna se ci sono dati o se è cambiata la situazione (es. da dati a nessun dato)
if self.main_window and hasattr(self.main_window, 'update_semaphore_and_status'):
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message)
else:
module_logger.warning("Received flight_data message with None payload.")
if self.main_window and hasattr(self.main_window, 'update_semaphore_and_status'):
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload.")
elif message_type == MSG_TYPE_ADAPTER_STATUS:
status_code = message.get("status_code")
gui_message = message.get("message", f"Adapter status: {status_code}") # Use adapter's message
module_logger.info(f"Adapter status: Code='{status_code}', Message='{gui_message}'")
gui_status_level = GUI_STATUS_UNKNOWN
if status_code == STATUS_STARTING:
gui_status_level = GUI_STATUS_WARNING # Or FETCHING
gui_message = message.get("message", "Adapter starting...")
elif status_code == STATUS_FETCHING:
gui_status_level = GUI_STATUS_FETCHING
gui_message = message.get("message", "Fetching new data...")
elif status_code == STATUS_RECOVERED:
gui_status_level = GUI_STATUS_OK
gui_message = message.get("message", "Connection recovered, fetching data.")
elif status_code == STATUS_RATE_LIMITED:
gui_status_level = GUI_STATUS_WARNING
delay = message.get("details", {}).get("delay", "N/A")
gui_message = f"API Rate Limit. Retrying in {delay:.0f}s."
elif status_code == STATUS_API_ERROR_TEMPORARY:
gui_status_level = GUI_STATUS_WARNING
orig_err_code = message.get("details", {}).get("status_code", "N/A")
delay = message.get("details", {}).get("delay", "N/A")
gui_message = f"Temporary API Error ({orig_err_code}). Retrying in {delay:.0f}s."
elif status_code == STATUS_PERMANENT_FAILURE:
gui_status_level = GUI_STATUS_ERROR
gui_message = message.get("message", "Too many API errors. Live updates stopped.")
elif status_code == STATUS_STOPPED: # Gestito da stop_live_monitoring o on_application_exit
gui_status_level = GUI_STATUS_OK
gui_message = message.get("message", "Live data adapter stopped.")
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(gui_status_level, gui_message)
if status_code == STATUS_PERMANENT_FAILURE:
module_logger.critical("Permanent failure from adapter. Stopping live monitoring via controller.")
self.stop_live_monitoring(from_error=True)
else:
module_logger.warning(f"Unknown message type from adapter: '{message_type}'")
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, f"Unknown adapter message: {message_type}")
except QueueEmpty:
pass # Normal if queue is empty
except Exception as e:
module_logger.error(f"CRITICAL Error processing adapter message queue: {e}", exc_info=True)
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Critical error processing data. See logs.")
finally:
# Riprogramma solo se il monitoraggio è attivo E il controller non è in fase di shutdown dell'adapter
if self.is_live_monitoring_active and \
self.main_window and self.main_window.root and self.main_window.root.winfo_exists():
try:
self._gui_after_id = self.main_window.root.after(
GUI_QUEUE_CHECK_INTERVAL_MS, self._process_flight_data_queue
)
except tk.TclError:
module_logger.warning("TclError scheduling next queue check, window might be gone.")
self._gui_after_id = None
def start_live_monitoring(self, bounding_box: Dict[str, float]):
if not self.main_window:
module_logger.error("Controller: Main window not set. Cannot start live monitoring.")
return
if not self.data_storage:
err_msg = "DataStorage not initialized. Live monitoring cannot start."
module_logger.error(f"Controller: {err_msg}")
if self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg + " Check logs.")
if hasattr(self.main_window, '_reset_gui_to_stopped_state'):
self.main_window._reset_gui_to_stopped_state(err_msg)
return
if self.is_live_monitoring_active:
module_logger.warning("Controller: Live monitoring already active. Start request ignored.")
return
module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}")
self._active_bounding_box = bounding_box
if self.flight_data_queue is None:
self.flight_data_queue = Queue()
while not self.flight_data_queue.empty():
try: self.flight_data_queue.get_nowait()
except QueueEmpty: break
else: self.flight_data_queue.task_done() # Assicurati che i task_done siano chiamati
# --- MODIFICA: Gestione più robusta del vecchio thread ---
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
module_logger.warning("Controller: Old LiveAdapter thread still alive. Attempting to stop and join it first.")
self.live_adapter_thread.stop()
self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
if self.live_adapter_thread.is_alive():
module_logger.error("Controller: Old LiveAdapter thread did not stop in time! May cause issues. Discarding reference.")
self.live_adapter_thread = None # Resetta comunque
self.live_adapter_thread = OpenSkyLiveAdapter(
output_queue=self.flight_data_queue,
bounding_box=self._active_bounding_box,
)
self.is_live_monitoring_active = True # Imposta PRIMA di avviare il thread e il polling
self.live_adapter_thread.start()
if self._gui_after_id:
if self.main_window.root.winfo_exists():
try: self.main_window.root.after_cancel(self._gui_after_id)
except tk.TclError: pass
self._gui_after_id = None
if self.main_window.root.winfo_exists():
self._gui_after_id = self.main_window.root.after(
10,
self._process_flight_data_queue
)
module_logger.info("Controller: Live monitoring adapter thread started and queue polling scheduled.")
def stop_live_monitoring(self, from_error: bool = False):
module_logger.info(f"Controller: Attempting to stop live monitoring. (Triggered by error: {from_error})")
# Salva il riferimento al thread corrente che stiamo per fermare
adapter_thread_to_stop = self.live_adapter_thread
# 1. Impedisci ulteriori scheduling e processamento come "live"
self.is_live_monitoring_active = False # Prima cosa!
if self._gui_after_id:
if self.main_window and self.main_window.root and self.main_window.root.winfo_exists():
try: self.main_window.root.after_cancel(self._gui_after_id)
except Exception: pass # Ignora errori qui, stiamo chiudendo
self._gui_after_id = None
module_logger.debug("Controller: Cancelled GUI queue check callback.")
# 2. Se il thread adapter esiste ed è vivo, segnalagli di fermarsi.
if adapter_thread_to_stop and adapter_thread_to_stop.is_alive():
module_logger.debug(f"Controller: Signaling LiveAdapter thread ({adapter_thread_to_stop.name}) to stop.")
adapter_thread_to_stop.stop() # Questo setta _stop_event nell'adapter
# 3. Attendi che il thread adapter termini (join).
# Il thread adapter dovrebbe uscire dal suo loop e terminare run().
module_logger.debug(f"Controller: Waiting for LiveAdapter thread ({adapter_thread_to_stop.name}) to join...")
adapter_thread_to_stop.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS + 1.0) # Aumenta un po' il timeout per sicurezza
if adapter_thread_to_stop.is_alive():
module_logger.error(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) did NOT join in time after stop signal! This is a problem.")
# Potrebbe essere necessario un cleanup più aggressivo o segnalare un errore critico.
else:
module_logger.info(f"Controller: LiveAdapter thread ({adapter_thread_to_stop.name}) joined successfully.")
else:
module_logger.debug("Controller: No active LiveAdapter thread to stop or already stopped.")
# 4. Rimuovi il riferimento al thread ora (dovrebbe essere terminato o timeout scaduto).
if self.live_adapter_thread == adapter_thread_to_stop: # Controlla se è ancora lo stesso thread
self.live_adapter_thread = None
# 5. Svuota la coda da messaggi residui (inclusi STATUS_STOPPED o STATUS_PERMANENT_FAILURE).
# Questo svuotamento avviene DOPO il join, quindi il thread adapter non sta più scrivendo.
# È importante processare qui eventuali messaggi di stato finali dall'adapter.
final_adapter_status_processed = False
if self.flight_data_queue:
module_logger.debug("Controller: Processing any final messages from adapter queue post-join...")
while not self.flight_data_queue.empty():
try:
message = self.flight_data_queue.get_nowait()
self.flight_data_queue.task_done()
# Processa specificamente i messaggi di stato finali
msg_type = message.get("type")
status_code = message.get("status_code")
if msg_type == MSG_TYPE_ADAPTER_STATUS:
module_logger.info(f"Controller: Processing final adapter status from queue: {status_code} - {message.get('message')}")
# Aggiorna la GUI con questo stato finale
if self.main_window and self.main_window.root.winfo_exists():
gui_status_level = GUI_STATUS_OK
if status_code == STATUS_PERMANENT_FAILURE: gui_status_level = GUI_STATUS_ERROR
self.main_window.update_semaphore_and_status(gui_status_level, message.get('message', 'Adapter stopped.'))
final_adapter_status_processed = True
# else:
# module_logger.debug(f"Controller: Discarding other message type '{msg_type}' from queue after stop.")
except QueueEmpty:
break
except Exception as e:
module_logger.error(f"Controller: Error processing/discarding message from queue: {e}")
break
module_logger.debug("Controller: Finished processing/discarding final adapter queue messages.")
# 6. Aggiorna la GUI allo stato fermato, se non già fatto da un messaggio finale dell'adapter.
if not from_error and not final_adapter_status_processed:
if self.main_window and self.main_window.root.winfo_exists():
if hasattr(self.main_window, '_reset_gui_to_stopped_state'):
self.main_window._reset_gui_to_stopped_state("Monitoring stopped.")
else:
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "Monitoring stopped.")
elif from_error and not final_adapter_status_processed: # Fermato per errore, ma nessun messaggio di errore processato dalla coda
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, "Monitoring stopped due to an error.")
module_logger.info("Controller: Live monitoring shutdown sequence fully completed.")
def on_application_exit(self):
module_logger.info("Controller: Application exit requested. Cleaning up resources.")
# Controlla specificamente se il thread esiste ED è vivo
# is_live_monitoring_active potrebbe essere già False, ma il thread potrebbe essere in fase di join
is_adapter_running = self.live_adapter_thread and self.live_adapter_thread.is_alive()
if self.is_live_monitoring_active or is_adapter_running:
module_logger.debug("Controller: Live monitoring/adapter active during app exit, stopping it.")
# Chiamare stop_live_monitoring qui si occuperà del join e della pulizia della coda
self.stop_live_monitoring(from_error=False)
else:
module_logger.debug("Controller: Live monitoring/adapter was not active or already stopped during app exit.")
if self.data_storage:
module_logger.debug("Controller: Closing DataStorage connection during app exit.")
self.data_storage.close_connection()
module_logger.info("Controller: Cleanup on application exit finished.")
# --- History Mode (Placeholders) ---
def start_history_monitoring(self):
if not self.main_window: module_logger.error("Main window not set for history."); return
if not self.data_storage:
err_msg = "DataStorage not initialized. Cannot use history features."
module_logger.error(f"Controller: {err_msg}")
if self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_ERROR, err_msg)
return
module_logger.info("Controller: History monitoring started (placeholder).")
if self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History mode active (placeholder).")
def stop_history_monitoring(self):
if not self.main_window: return
module_logger.info("Controller: History monitoring stopped (placeholder).")
if self.main_window.root.winfo_exists():
self.main_window.update_semaphore_and_status(GUI_STATUS_OK, "History monitoring stopped.")