add raw save for historical data

This commit is contained in:
VALLONGOL 2025-06-13 11:41:39 +02:00
parent c465ab48cb
commit dfd58a347c
3 changed files with 137 additions and 199 deletions

View File

@ -3,8 +3,8 @@ import threading
import json import json
import os import os
import time import time
import sys # MODIFICATO: Aggiunto per controllo OS import sys
import subprocess # MODIFICATO: Aggiunto per aprire cartelle import subprocess
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty
from typing import List, Optional, Dict, Any, TYPE_CHECKING from typing import List, Optional, Dict, Any, TYPE_CHECKING
from tkinter import messagebox, simpledialog from tkinter import messagebox, simpledialog
@ -44,19 +44,16 @@ class AppController:
def __init__(self): def __init__(self):
self.main_window: Optional["MainWindow"] = None self.main_window: Optional["MainWindow"] = None
# --- Live Monitoring Attributes ---
self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None self.live_adapter_thread: Optional[OpenSkyLiveAdapter] = None
self.is_live_monitoring_active: bool = False self.is_live_monitoring_active: bool = False
self.live_data_queue: Optional[Queue[AdapterMessage]] = None self.live_data_queue: Optional[Queue[AdapterMessage]] = None
self.live_data_processor: Optional[LiveDataProcessor] = None self.live_data_processor: Optional[LiveDataProcessor] = None
# --- Historical Download Attributes ---
self.historical_adapter_thread: Optional[OpenSkyHistoricalAdapter] = None self.historical_adapter_thread: Optional[OpenSkyHistoricalAdapter] = None
self.is_historical_download_active: bool = False self.is_historical_download_active: bool = False
self.historical_data_queue: Optional[Queue[AdapterMessage]] = None self.historical_data_queue: Optional[Queue[AdapterMessage]] = None
self.historical_data_processor: Optional[HistoricalDataProcessor] = None self.historical_data_processor: Optional[HistoricalDataProcessor] = None
# --- Shared/General Attributes ---
self._current_scan_params: Optional[Dict[str, Any]] = None self._current_scan_params: Optional[Dict[str, Any]] = None
self._active_bounding_box: Optional[Dict[str, float]] = None self._active_bounding_box: Optional[Dict[str, float]] = None
self.data_storage: Optional[DataStorage] = None self.data_storage: Optional[DataStorage] = None
@ -172,10 +169,7 @@ class AppController:
self.main_window.update_semaphore_and_status(initial_status_level, initial_status_msg) self.main_window.update_semaphore_and_status(initial_status_level, initial_status_msg)
def start_live_monitoring(self): def start_live_monitoring(self):
if not self.main_window: if not self.main_window: return
module_logger.error("Controller: Main window not set for live monitoring.")
return
bounding_box = self.main_window.get_bounding_box_from_gui() bounding_box = self.main_window.get_bounding_box_from_gui()
if not bounding_box: if not bounding_box:
err_msg = "Invalid or missing Bounding Box. Define monitoring area." err_msg = "Invalid or missing Bounding Box. Define monitoring area."
@ -190,38 +184,29 @@ class AppController:
if not log_dir: if not log_dir:
self.main_window.show_error_message("Logging Error", "Logging is enabled but no save directory is specified.") self.main_window.show_error_message("Logging Error", "Logging is enabled but no save directory is specified.")
return return
if self.raw_data_logger and not self.raw_data_logger.start_logging_session(log_dir, bounding_box): if self.raw_data_logger and not self.raw_data_logger.start_logging_session(log_dir, bounding_box):
self.main_window.show_error_message("Logging Error", "Failed to start raw data logging session. Check logs.") self.main_window.show_error_message("Logging Error", "Failed to start raw data logging session. Check logs.")
return return
if hasattr(self.main_window.function_notebook_panel, "clear_all_panel_data"): if hasattr(self.main_window.function_notebook_panel, "clear_all_panel_data"):
self.main_window.function_notebook_panel.clear_all_panel_data() self.main_window.function_notebook_panel.clear_all_panel_data()
if hasattr(self.main_window, "clear_all_views_data"): if hasattr(self.main_window, "clear_all_views_data"):
self.main_window.clear_all_views_data() self.main_window.clear_all_views_data()
if hasattr(self.main_window, "set_monitoring_button_states"): if hasattr(self.main_window, "set_monitoring_button_states"):
self.main_window.set_monitoring_button_states(True) self.main_window.set_monitoring_button_states(True)
if not self.data_storage and hasattr(self.main_window, "update_semaphore_and_status"):
self.main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "DataStorage N/A. History will not be saved.")
if self.is_live_monitoring_active: if self.is_live_monitoring_active:
module_logger.warning("Live monitoring requested but already active.") module_logger.warning("Live monitoring requested but already active.")
return return
module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}") module_logger.info(f"Controller: Starting live monitoring for bbox: {bounding_box}")
self._active_bounding_box = bounding_box self._active_bounding_box = bounding_box
if self.main_window.map_manager_instance: if self.main_window.map_manager_instance:
self.main_window.map_manager_instance.set_target_bbox(bounding_box) self.main_window.map_manager_instance.set_target_bbox(bounding_box)
if self.live_data_queue: if self.live_data_queue:
while not self.live_data_queue.empty(): while not self.live_data_queue.empty():
try: self.live_data_queue.get_nowait() try: self.live_data_queue.get_nowait()
except QueueEmpty: break except QueueEmpty: break
if not self.live_data_processor: if not self.live_data_processor:
module_logger.critical("Controller: LiveDataProcessor not initialized.") module_logger.critical("Controller: LiveDataProcessor not initialized.")
if hasattr(self.main_window, "_reset_gui_to_stopped_state"): if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
@ -229,9 +214,7 @@ class AppController:
return return
self.live_adapter_thread = OpenSkyLiveAdapter( self.live_adapter_thread = OpenSkyLiveAdapter(
output_queue=self.live_data_queue, self.live_data_queue, self._active_bounding_box, app_config.LIVE_POLLING_INTERVAL_SECONDS
bounding_box=self._active_bounding_box,
polling_interval=app_config.LIVE_POLLING_INTERVAL_SECONDS,
) )
self.is_live_monitoring_active = True self.is_live_monitoring_active = True
self.live_adapter_thread.start() self.live_adapter_thread.start()
@ -246,19 +229,17 @@ class AppController:
module_logger.info(f"Controller: Stopping live monitoring (from_error={from_error}).") module_logger.info(f"Controller: Stopping live monitoring (from_error={from_error}).")
self.is_live_monitoring_active = False self.is_live_monitoring_active = False
if self.live_data_processor: if self.live_data_processor: self.live_data_processor.stop_processing_queue()
self.live_data_processor.stop_processing_queue()
if self.live_adapter_thread and self.live_adapter_thread.is_alive(): if self.live_adapter_thread and self.live_adapter_thread.is_alive():
self.live_adapter_thread.stop() self.live_adapter_thread.stop()
self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS) self.live_adapter_thread.join(ADAPTER_JOIN_TIMEOUT_SECONDS)
self.live_adapter_thread = None self.live_adapter_thread = None
if self.raw_data_logger and self.raw_data_logger.is_active: if self.raw_data_logger and self.raw_data_logger.is_active:
self.raw_data_logger.stop_logging_session() self.raw_data_logger.stop_logging_session()
if self.main_window and hasattr(self.main_window, "_reset_gui_to_stopped_state"): if self.main_window and hasattr(self.main_window, "_reset_gui_to_stopped_state"):
msg = "Monitoring stopped due to an error." if from_error else "Monitoring stopped. Last view retained." msg = "Monitoring stopped due to an error." if from_error else "Monitoring stopped."
self.main_window._reset_gui_to_stopped_state(msg) self.main_window._reset_gui_to_stopped_state(msg)
def process_raw_data_logging(self, raw_json_string: str, canonical_data: List['CanonicalFlightState']): def process_raw_data_logging(self, raw_json_string: str, canonical_data: List['CanonicalFlightState']):
@ -275,26 +256,20 @@ class AppController:
panel.add_summary_entry(timestamp, aircraft_count) panel.add_summary_entry(timestamp, aircraft_count)
panel.update_last_query_result(timestamp, aircraft_count) panel.update_last_query_result(timestamp, aircraft_count)
# MODIFICATO: Aggiunto nuovo metodo per aprire la cartella
def open_log_directory(self, directory_path: str): def open_log_directory(self, directory_path: str):
"""Opens the specified directory in the system's file explorer."""
if not directory_path: if not directory_path:
self.main_window.show_error_message("Error", "No directory path specified.") self.main_window.show_error_message("Error", "No directory path specified.")
return return
abs_path = os.path.abspath(directory_path) abs_path = os.path.abspath(directory_path)
if not os.path.isdir(abs_path): if not os.path.isdir(abs_path):
self.main_window.show_error_message("Error", f"Directory does not exist:\n{abs_path}") self.main_window.show_error_message("Error", f"Directory does not exist:\n{abs_path}")
return return
try: try:
if sys.platform == "win32": if sys.platform == "win32": os.startfile(abs_path)
os.startfile(abs_path) elif sys.platform == "darwin": subprocess.Popen(["open", abs_path])
elif sys.platform == "darwin": # macOS else: subprocess.Popen(["xdg-open", abs_path])
subprocess.Popen(["open", abs_path])
else: # Linux and other UNIX-like
subprocess.Popen(["xdg-open", abs_path])
module_logger.info(f"Opening log directory: {abs_path}") module_logger.info(f"Opening log directory: {abs_path}")
except Exception as e: except Exception as e:
module_logger.error(f"Failed to open directory '{abs_path}': {e}") module_logger.error(f"Failed to open directory '{abs_path}': {e}")
@ -316,19 +291,30 @@ class AppController:
params = historical_panel.get_download_parameters() params = historical_panel.get_download_parameters()
if not params: if not params:
self.main_window.show_error_message("Input Error", "Invalid download parameters. Please check dates, times, and numeric values.") self.main_window.show_error_message("Input Error", "Invalid download parameters.")
return return
bbox = self.main_window.get_bounding_box_from_gui() bbox = self.main_window.get_bounding_box_from_gui()
if not bbox: if not bbox:
self.main_window.show_error_message("Input Error", "A valid Bounding Box must be defined in the main panel.") self.main_window.show_error_message("Input Error", "A valid Bounding Box must be defined.")
return
# MODIFICATO: Aggiunta logica di avvio del logging
logging_settings = self.main_window.function_notebook_panel.get_logging_panel_settings()
if logging_settings and logging_settings['enabled']:
log_dir = logging_settings.get('directory')
if not log_dir:
self.main_window.show_error_message("Logging Error", "Logging is enabled but no save directory is specified.")
return
if self.raw_data_logger and not self.raw_data_logger.start_logging_session(log_dir, bbox):
self.main_window.show_error_message("Logging Error", "Failed to start raw data logging session. Check logs.")
return return
if self.aircraft_db_manager: if self.aircraft_db_manager:
overlapping_scans = self.aircraft_db_manager.find_overlapping_scans(params['start_time'], params['end_time'], bbox) overlapping_scans = self.aircraft_db_manager.find_overlapping_scans(params['start_time'], params['end_time'], bbox)
if overlapping_scans: if overlapping_scans and not messagebox.askyesno("Confirm Download", "A similar scan exists. Download again?"):
if not messagebox.askyesno("Confirm Download", "A similar time/area range has been scanned before. Download again? (Existing data points will not be duplicated)."): module_logger.info("Historical download cancelled by user.")
module_logger.info("Historical download cancelled by user due to existing scan.") if self.raw_data_logger and self.raw_data_logger.is_active: self.raw_data_logger.stop_logging_session()
return return
module_logger.info(f"Controller: Starting historical download with params: {params} for bbox: {bbox}") module_logger.info(f"Controller: Starting historical download with params: {params} for bbox: {bbox}")
@ -337,18 +323,15 @@ class AppController:
self._active_bounding_box = bbox self._active_bounding_box = bbox
historical_panel.set_controls_state(is_downloading=True) historical_panel.set_controls_state(is_downloading=True)
if hasattr(self.main_window.function_notebook_panel, "clear_all_panel_data"):
self.main_window.function_notebook_panel.clear_all_panel_data()
self.main_window.clear_all_views_data() self.main_window.clear_all_views_data()
self.historical_adapter_thread = OpenSkyHistoricalAdapter( self.historical_adapter_thread = OpenSkyHistoricalAdapter(
output_queue=self.historical_data_queue, self.historical_data_queue, bbox, params['start_time'], params['end_time'],
bounding_box=bbox, params['sampling_interval_sec'], params['scan_rate_sec']
start_timestamp=params['start_time'],
end_timestamp=params['end_time'],
sampling_interval_sec=params['sampling_interval_sec'],
scan_rate_sec=params['scan_rate_sec']
) )
self.historical_adapter_thread.start() self.historical_adapter_thread.start()
self.historical_data_processor.start_processing() self.historical_data_processor.start_processing()
self.main_window.update_semaphore_and_status(GUI_STATUS_FETCHING, "Historical download started.") self.main_window.update_semaphore_and_status(GUI_STATUS_FETCHING, "Historical download started.")
@ -360,84 +343,58 @@ class AppController:
module_logger.info(f"Controller: Stopping historical download (from_error={from_error}, finished_normally={finished_normally}).") module_logger.info(f"Controller: Stopping historical download (from_error={from_error}, finished_normally={finished_normally}).")
if self.aircraft_db_manager and self._current_scan_params and self._active_bounding_box: if self.aircraft_db_manager and self._current_scan_params and self._active_bounding_box:
status = "unknown" status = "completed" if finished_normally else ("failed" if from_error else "aborted")
if finished_normally: status = "completed" self.aircraft_db_manager.add_scan_history(self._current_scan_params, self._active_bounding_box, status)
elif from_error: status = "failed"
else: status = "aborted"
self.aircraft_db_manager.add_scan_history(
self._current_scan_params, self._active_bounding_box, status
)
self.is_historical_download_active = False self.is_historical_download_active = False
self._current_scan_params = None self._current_scan_params = None
if self.historical_data_processor: if self.historical_data_processor: self.historical_data_processor.stop_processing()
self.historical_data_processor.stop_processing()
if self.historical_adapter_thread and self.historical_adapter_thread.is_alive(): if self.historical_adapter_thread and self.historical_adapter_thread.is_alive():
if hasattr(self.historical_adapter_thread, 'stop'): if hasattr(self.historical_adapter_thread, 'stop'):
self.historical_adapter_thread.stop() self.historical_adapter_thread.stop(); self.historical_adapter_thread.join(ADAPTER_JOIN_TIMEOUT_SECONDS)
self.historical_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
self.historical_adapter_thread = None self.historical_adapter_thread = None
if self.main_window and hasattr(self.main_window.function_notebook_panel, 'historical_panel'): # MODIFICATO: Aggiunta chiusura del logger
historical_panel = self.main_window.function_notebook_panel.historical_panel
if historical_panel:
historical_panel.set_controls_state(is_downloading=False)
status_msg = "Historical download finished." if finished_normally else "Historical download stopped."
if from_error: status_msg = "Historical download stopped due to an error."
self.main_window.update_semaphore_and_status(GUI_STATUS_OK if finished_normally else GUI_STATUS_WARNING, status_msg)
def on_application_exit(self):
self._save_app_settings()
if self.is_historical_download_active:
self.stop_historical_download()
if self.raw_data_logger and self.raw_data_logger.is_active: if self.raw_data_logger and self.raw_data_logger.is_active:
self.raw_data_logger.stop_logging_session() self.raw_data_logger.stop_logging_session()
if self.cleanup_manager: if self.main_window and hasattr(self.main_window.function_notebook_panel, 'historical_panel'):
self.cleanup_manager.on_application_exit() historical_panel = self.main_window.function_notebook_panel.historical_panel
else: if historical_panel: historical_panel.set_controls_state(is_downloading=False)
module_logger.critical("CleanupManager not initialized. Cannot perform proper cleanup.")
status_msg = "Historical download finished." if finished_normally else ("stopped due to an error." if from_error else "stopped.")
self.main_window.update_semaphore_and_status(GUI_STATUS_OK if finished_normally else GUI_STATUS_WARNING, f"Historical download {status_msg}")
def on_application_exit(self):
self._save_app_settings()
if self.is_live_monitoring_active: self.stop_live_monitoring()
if self.is_historical_download_active: self.stop_historical_download()
if self.raw_data_logger and self.raw_data_logger.is_active:
self.raw_data_logger.stop_logging_session()
if self.cleanup_manager: self.cleanup_manager.on_application_exit()
else: module_logger.critical("CleanupManager not initialized.")
def on_function_tab_changed(self, tab_text: str): def on_function_tab_changed(self, tab_text: str):
module_logger.info(f"Controller notified of function tab change to: {tab_text}") module_logger.info(f"Controller notified of function tab change to: {tab_text}")
if self.is_live_monitoring_active: self.stop_live_monitoring()
if self.is_live_monitoring_active: if self.is_historical_download_active: self.stop_historical_download()
self.stop_live_monitoring()
if self.is_historical_download_active:
self.stop_historical_download()
if self.main_window: if self.main_window:
self.main_window.clear_all_views_data() self.main_window.clear_all_views_data()
if hasattr(self.main_window.function_notebook_panel, "clear_all_panel_data"): if hasattr(self.main_window.function_notebook_panel, "clear_all_panel_data"):
self.main_window.function_notebook_panel.clear_all_panel_data() self.main_window.function_notebook_panel.clear_all_panel_data()
placeholder = "Select a mode to begin." placeholders = {"Live Monitor": "Define area and press Start Live Monitoring.", "Historical Download": "Set parameters and press Start Download.", "Playback": "Playback - Coming Soon"}
if "Live Monitor" in tab_text: placeholder = placeholders.get(tab_text, "Select a mode to begin.")
placeholder = "Define area and press Start Live Monitoring." if hasattr(self.main_window, "_update_map_placeholder"): self.main_window._update_map_placeholder(placeholder)
elif "Historical Download" in tab_text:
placeholder = "Set parameters and press Start Download."
elif "Playback" in tab_text:
placeholder = "Playback - Coming Soon"
if hasattr(self.main_window, "_update_map_placeholder"): def import_aircraft_database_from_file_with_progress(self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"):
self.main_window._update_map_placeholder(placeholder)
def import_aircraft_database_from_file_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
if not self.aircraft_db_importer: if not self.aircraft_db_importer:
module_logger.error("AircraftDBImporter not initialized. Cannot import.") module_logger.error("AircraftDBImporter not initialized.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists(): if progress_dialog_ref and progress_dialog_ref.winfo_exists():
if self.main_window and self.main_window.root.winfo_exists(): if self.main_window and self.main_window.root.winfo_exists():
self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished( self.main_window.root.after(0, lambda: progress_dialog_ref.import_finished(False, "Error: Import function not initialized."))
False, "Error: Import function not initialized correctly."
))
return return
self.aircraft_db_importer.import_aircraft_database_with_progress(csv_filepath, progress_dialog_ref) self.aircraft_db_importer.import_aircraft_database_with_progress(csv_filepath, progress_dialog_ref)
@ -447,11 +404,7 @@ class AppController:
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
max_age_hours = getattr(app_config, "TRACK_MAX_AGE_HOURS_FOR_CONTINUITY", 4) max_age_hours = getattr(app_config, "TRACK_MAX_AGE_HOURS_FOR_CONTINUITY", 4)
current_utc_dt = datetime.now(timezone.utc) current_utc_dt = datetime.now(timezone.utc)
since_dt = current_utc_dt - timedelta(hours=max_age_hours) track_states = self.data_storage.get_flight_track_for_icao_on_date(icao24, current_utc_dt)
since_timestamp = since_dt.timestamp()
track_states = self.data_storage.get_flight_track_for_icao_on_date(
icao24, current_utc_dt, since_timestamp=since_timestamp
)
if track_states: return [state.to_dict() for state in track_states] if track_states: return [state.to_dict() for state in track_states]
except Exception as e: except Exception as e:
module_logger.error(f"Controller: Error retrieving historical track for {icao24}: {e}", exc_info=True) module_logger.error(f"Controller: Error retrieving historical track for {icao24}: {e}", exc_info=True)
@ -468,9 +421,7 @@ class AppController:
map_mgr = self.main_window.map_manager_instance map_mgr = self.main_window.map_manager_instance
with map_mgr._map_data_lock: with map_mgr._map_data_lock:
for state in map_mgr._current_flights_to_display_gui: for state in map_mgr._current_flights_to_display_gui:
if state.icao24 == normalized_icao24: if state.icao24 == normalized_icao24: combined_details.update(state.to_dict()); break
combined_details.update(state.to_dict())
break
if self.aircraft_db_manager: if self.aircraft_db_manager:
static_data = self.aircraft_db_manager.get_aircraft_details(normalized_icao24) static_data = self.aircraft_db_manager.get_aircraft_details(normalized_icao24)
if static_data: if static_data:
@ -481,13 +432,10 @@ class AppController:
def request_and_show_full_flight_details(self, icao24: str): def request_and_show_full_flight_details(self, icao24: str):
normalized_icao24 = icao24.lower().strip() normalized_icao24 = icao24.lower().strip()
module_logger.info(f"Controller: Requesting to show full details for ICAO24: {normalized_icao24}") module_logger.info(f"Controller: Requesting full details for ICAO24: {normalized_icao24}")
if not (self.main_window and self.main_window.root and self.main_window.root.winfo_exists()): if not (self.main_window and self.main_window.root and self.main_window.root.winfo_exists()): return
module_logger.error("Controller: MainWindow not available to show full flight details.")
return
if not normalized_icao24: if not normalized_icao24:
module_logger.warning("Controller: Empty ICAO24 for full details request.") if hasattr(self.main_window, "show_info_message"): self.main_window.show_info_message("Flight Details", "No ICAO24 provided.")
if hasattr(self.main_window, "show_info_message"): self.main_window.show_info_message("Flight Details", "No ICAO24 provided for full details.")
return return
if self.active_detail_window_ref and self.active_detail_window_ref.winfo_exists(): if self.active_detail_window_ref and self.active_detail_window_ref.winfo_exists():
if self.active_detail_window_icao == normalized_icao24: if self.active_detail_window_icao == normalized_icao24:
@ -495,52 +443,42 @@ class AppController:
else: else:
try: self.active_detail_window_ref.destroy() try: self.active_detail_window_ref.destroy()
except tk.TclError: pass except tk.TclError: pass
static_data: Optional[Dict[str, Any]] = self.aircraft_db_manager.get_aircraft_details(normalized_icao24) if self.aircraft_db_manager else None static_data, live_data, full_track_data_list = None, None, []
live_data: Optional[Dict[str, Any]] = None if self.aircraft_db_manager: static_data = self.aircraft_db_manager.get_aircraft_details(normalized_icao24)
if self.main_window and self.main_window.map_manager_instance: if self.main_window and self.main_window.map_manager_instance:
map_mgr = self.main_window.map_manager_instance map_mgr = self.main_window.map_manager_instance
with map_mgr._map_data_lock: with map_mgr._map_data_lock:
for state in map_mgr._current_flights_to_display_gui: for state in map_mgr._current_flights_to_display_gui:
if state.icao24 == normalized_icao24: if state.icao24 == normalized_icao24: live_data = state.to_dict(); break
live_data = state.to_dict(); break
full_track_data_list: List[Dict[str, Any]] = []
if self.data_storage: if self.data_storage:
try: try:
from datetime import datetime, timezone from datetime import datetime, timezone
current_utc_date = datetime.now(timezone.utc) track_states = self.data_storage.get_flight_track_for_icao_on_date(normalized_icao24, datetime.now(timezone.utc))
track_states = self.data_storage.get_flight_track_for_icao_on_date(normalized_icao24, current_utc_date)
if track_states: full_track_data_list = [state.to_dict() for state in track_states] if track_states: full_track_data_list = [state.to_dict() for state in track_states]
except Exception as e_track: except Exception as e_track: module_logger.error(f"Error retrieving track for {normalized_icao24}: {e_track}", exc_info=True)
module_logger.error(f"FullDetails: Error retrieving historical track for {normalized_icao24}: {e_track}", exc_info=True)
try: try:
from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow
details_win = FullFlightDetailsWindow(self.main_window.root, normalized_icao24, self) details_win = FullFlightDetailsWindow(self.main_window.root, normalized_icao24, self)
self.active_detail_window_ref = details_win self.active_detail_window_ref, self.active_detail_window_icao = details_win, normalized_icao24
self.active_detail_window_icao = normalized_icao24
if self.main_window: self.main_window.full_flight_details_window = details_win if self.main_window: self.main_window.full_flight_details_window = details_win
details_win.update_details(static_data, live_data, full_track_data_list) details_win.update_details(static_data, live_data, full_track_data_list)
except ImportError: except Exception as e_show:
module_logger.error("FullFlightDetailsWindow class not found.") module_logger.error(f"Error showing full details for {normalized_icao24}: {e_show}", exc_info=True)
if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message("UI Error", "Could not open full details window (import error).") if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message("Error", f"Could not display full details: {e_show}")
except Exception as e_show_details: self.active_detail_window_ref, self.active_detail_window_icao = None, None
module_logger.error(f"Error showing full flight details window for {normalized_icao24}: {e_show_details}", exc_info=True)
if hasattr(self.main_window, "show_error_message"): self.main_window.show_error_message("Error", f"Could not display full details: {e_show_details}")
self.active_detail_window_ref = None
self.active_detail_window_icao = None
if self.main_window: self.main_window.full_flight_details_window = None if self.main_window: self.main_window.full_flight_details_window = None
def details_window_closed(self, closed_icao24: str): def details_window_closed(self, closed_icao24: str):
if self.cleanup_manager: self.cleanup_manager.details_window_closed(closed_icao24) if self.cleanup_manager: self.cleanup_manager.details_window_closed(closed_icao24)
else: module_logger.warning("CleanupManager not initialized for details_window_closed.") def on_map_left_click(self, lat, lon, cx, cy, sx, sy):
def on_map_left_click(self, latitude: Optional[float], longitude: Optional[float], canvas_x: int, canvas_y: int, screen_x: int, screen_y: int): if self.map_command_handler: self.map_command_handler.on_map_left_click(lat, lon, cx, cy, sx, sy)
if self.map_command_handler: self.map_command_handler.on_map_left_click(latitude, longitude, canvas_x, canvas_y, screen_x, screen_y) def on_map_right_click(self, lat, lon, sx, sy):
def on_map_right_click(self, latitude: float, longitude: float, screen_x: int, screen_y: int): if self.map_command_handler: self.map_command_handler.on_map_right_click(lat, lon, sx, sy)
if self.map_command_handler: self.map_command_handler.on_map_right_click(latitude, longitude, screen_x, screen_y) def recenter_map_at_coords(self, lat, lon):
def recenter_map_at_coords(self, lat: float, lon: float):
if self.map_command_handler: self.map_command_handler.recenter_map_at_coords(lat, lon) if self.map_command_handler: self.map_command_handler.recenter_map_at_coords(lat, lon)
def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float): def set_bbox_around_coords(self, lat, lon, size_km):
if self.map_command_handler: self.map_command_handler.set_bbox_around_coords(center_lat, center_lon, area_size_km) if self.map_command_handler: self.map_command_handler.set_bbox_around_coords(lat, lon, size_km)
def update_bbox_gui_fields(self, bbox_dict: Dict[str, float]): def update_bbox_gui_fields(self, bbox_dict):
if self.main_window: self.main_window.update_bbox_gui_fields(bbox_dict) if self.main_window: self.main_window.update_bbox_gui_fields(bbox_dict)
def update_general_map_info(self): def update_general_map_info(self):
if self.map_command_handler: self.map_command_handler.update_general_map_info() if self.map_command_handler: self.map_command_handler.update_general_map_info()
@ -548,13 +486,12 @@ class AppController:
if self.map_command_handler: self.map_command_handler.map_zoom_in() if self.map_command_handler: self.map_command_handler.map_zoom_in()
def map_zoom_out(self): def map_zoom_out(self):
if self.map_command_handler: self.map_command_handler.map_zoom_out() if self.map_command_handler: self.map_command_handler.map_zoom_out()
def map_pan_direction(self, direction: str): def map_pan_direction(self, direction):
if self.map_command_handler: self.map_command_handler.map_pan_direction(direction) if self.map_command_handler: self.map_command_handler.map_pan_direction(direction)
def map_center_on_coords_and_fit_patch(self, lat: float, lon: float, patch_size_km: float): def map_center_on_coords_and_fit_patch(self, lat, lon, patch_km):
if self.map_command_handler: self.map_command_handler.map_center_on_coords_and_fit_patch(lat, lon, patch_size_km) if self.map_command_handler: self.map_command_handler.map_center_on_coords_and_fit_patch(lat, lon, patch_km)
def set_map_track_length(self, length: int): def set_map_track_length(self, length):
if self.main_window and self.main_window.map_manager_instance: self.main_window.map_manager_instance.set_max_track_points(length) if self.main_window and self.main_window.map_manager_instance: self.main_window.map_manager_instance.set_max_track_points(length)
else: module_logger.warning("Controller: Cannot set track length, map manager not available.")
def get_profile_names(self) -> List[str]: def get_profile_names(self) -> List[str]:
if self.profile_manager: return self.profile_manager.get_profile_names() if self.profile_manager: return self.profile_manager.get_profile_names()
return [] return []

View File

@ -5,9 +5,11 @@ It updates the GUI's virtual clock and dispatches flight states to the map.
""" """
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
import time # MODIFICATO: Aggiunto per ottenere il timestamp corrente
from flightmonitor.utils.logger import get_logger from flightmonitor.utils.logger import get_logger
from flightmonitor.data.opensky_live_adapter import AdapterMessage, MSG_TYPE_FLIGHT_DATA, MSG_TYPE_ADAPTER_STATUS, STATUS_STOPPED from flightmonitor.data.opensky_live_adapter import AdapterMessage, MSG_TYPE_FLIGHT_DATA, MSG_TYPE_ADAPTER_STATUS, STATUS_STOPPED
from flightmonitor.data.common_models import CanonicalFlightState # MODIFICATO: Importato per il type hinting
if TYPE_CHECKING: if TYPE_CHECKING:
from flightmonitor.controller.app_controller import AppController from flightmonitor.controller.app_controller import AppController
@ -90,16 +92,24 @@ class HistoricalDataProcessor:
if message_type == MSG_TYPE_FLIGHT_DATA: if message_type == MSG_TYPE_FLIGHT_DATA:
snapshot_timestamp = message.get("timestamp") snapshot_timestamp = message.get("timestamp")
flight_states = message.get("payload", [])
# MODIFICATO: Estrae il payload strutturato
payload_dict = message.get("payload", {})
flight_states: Optional[list[CanonicalFlightState]] = payload_dict.get("canonical")
raw_json_payload: str = payload_dict.get("raw_json", "{}")
# Aggiorna sempre l'orologio virtuale per mostrare il progresso # Aggiorna sempre l'orologio virtuale per mostrare il progresso
if snapshot_timestamp and hasattr(main_window, "function_notebook_panel") and main_window.function_notebook_panel.historical_panel: if snapshot_timestamp and hasattr(main_window, "function_notebook_panel") and main_window.function_notebook_panel.historical_panel:
main_window.function_notebook_panel.historical_panel.update_virtual_clock(snapshot_timestamp) main_window.function_notebook_panel.historical_panel.update_virtual_clock(snapshot_timestamp)
# --- INIZIO MODIFICA --- if flight_states is not None:
# Controlla se sono stati trovati aerei in questo snapshot # MODIFICATO: Notifica il controller per il logging dei dati grezzi e l'aggiornamento della UI
if flight_states: if hasattr(self.app_controller, 'process_raw_data_logging'):
# Se ci sono dati, salvali e aggiorna la mappa self.app_controller.process_raw_data_logging(raw_json_payload, flight_states)
if hasattr(self.app_controller, 'update_live_summary_table'):
self.app_controller.update_live_summary_table(snapshot_timestamp, len(flight_states))
if flight_states: # Se ci sono dati, salvali e aggiorna la mappa
if data_storage: if data_storage:
for state in flight_states: for state in flight_states:
flight_id = data_storage.add_or_update_flight_daily( flight_id = data_storage.add_or_update_flight_daily(
@ -111,8 +121,7 @@ class HistoricalDataProcessor:
if main_window.map_manager_instance: if main_window.map_manager_instance:
main_window.map_manager_instance.update_flights_on_map(flight_states) main_window.map_manager_instance.update_flights_on_map(flight_states)
else: else: # Se non ci sono dati, aggiorna solo il messaggio di stato per dare feedback
# Se non ci sono dati, aggiorna solo il messaggio di stato per dare feedback
if main_window and hasattr(main_window, 'update_semaphore_and_status'): if main_window and hasattr(main_window, 'update_semaphore_and_status'):
from ..utils.gui_utils import GUI_STATUS_FETCHING from ..utils.gui_utils import GUI_STATUS_FETCHING
from datetime import datetime from datetime import datetime
@ -124,7 +133,6 @@ class HistoricalDataProcessor:
GUI_STATUS_FETCHING, GUI_STATUS_FETCHING,
f"No flights found at {time_str}. Continuing scan..." f"No flights found at {time_str}. Continuing scan..."
) )
# --- FINE MODIFICA ---
elif message_type == MSG_TYPE_ADAPTER_STATUS: elif message_type == MSG_TYPE_ADAPTER_STATUS:
if message.get("status_code") == STATUS_STOPPED: if message.get("status_code") == STATUS_STOPPED:

View File

@ -6,6 +6,7 @@ It requires authentication to access historical data.
""" """
import time import time
import threading import threading
import json # MODIFICATO: Aggiunto import per json
from queue import Queue, Full as QueueFull from queue import Queue, Full as QueueFull
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
@ -71,7 +72,6 @@ class OpenSkyHistoricalAdapter(BaseLiveDataAdapter):
if not (self.client_id and self.client_secret and self.token_url): if not (self.client_id and self.client_secret and self.token_url):
module_logger.critical(f"{self.name}: OAuth2 credentials/token URL missing. Historical adapter cannot function.") module_logger.critical(f"{self.name}: OAuth2 credentials/token URL missing. Historical adapter cannot function.")
# This will be handled in the run method to send a failure status.
def _get_oauth_token(self) -> bool: def _get_oauth_token(self) -> bool:
"""Requests an OAuth2 access token from OpenSky Network.""" """Requests an OAuth2 access token from OpenSky Network."""
@ -87,13 +87,10 @@ class OpenSkyHistoricalAdapter(BaseLiveDataAdapter):
} }
encoded_payload = urlencode(payload_dict) encoded_payload = urlencode(payload_dict)
# --- MODIFICA CHIAVE ---
# Aggiunto l'header User-Agent per replicare esattamente l'adapter live.
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0" "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0"
} }
# --- FINE MODIFICA ---
module_logger.info(f"{self.name}: Requesting new OAuth2 access token from {self.token_url}...") module_logger.info(f"{self.name}: Requesting new OAuth2 access token from {self.token_url}...")
try: try:
@ -109,7 +106,7 @@ class OpenSkyHistoricalAdapter(BaseLiveDataAdapter):
token_data = response.json() token_data = response.json()
self.access_token = token_data.get("access_token") self.access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in", 300) expires_in = token_data.get("expires_in", 300)
self.token_expires_at = time.time() + expires_in - 60 # 60-second buffer self.token_expires_at = time.time() + expires_in - 60
if self.access_token: if self.access_token:
module_logger.info(f"{self.name}: Successfully obtained OAuth2 access token.") module_logger.info(f"{self.name}: Successfully obtained OAuth2 access token.")
@ -205,23 +202,15 @@ class OpenSkyHistoricalAdapter(BaseLiveDataAdapter):
} }
headers = {"Authorization": f"Bearer {self.access_token}"} headers = {"Authorization": f"Bearer {self.access_token}"}
# --- INIZIO LOGGING FORENSE ---
req = requests.Request('GET', app_config.OPENSKY_API_URL, params=params, headers=headers)
prepared_req = req.prepare()
module_logger.info("-----------------------------------------------------")
module_logger.info(f"--- Making API Call for Historical Data ---")
module_logger.info(f"URL: {prepared_req.url}")
#module_logger.info(f"Headers: {prepared_req.headers}")
module_logger.info("-----------------------------------------------------")
# --- FINE LOGGING FORENSE ---
with requests.Session() as session: with requests.Session() as session:
req = requests.Request('GET', app_config.OPENSKY_API_URL, params=params, headers=headers)
prepared_req = session.prepare_request(req)
response = session.send(prepared_req, timeout=self.api_timeout, verify=False) response = session.send(prepared_req, timeout=self.api_timeout, verify=False)
response.raise_for_status() response.raise_for_status()
json_response = response.json() raw_json_string = response.text # Cattura la stringa JSON grezza
json_response = json.loads(raw_json_string) # Ora parsa il JSON
raw_states_list = json_response.get("states", []) raw_states_list = json_response.get("states", [])
canonical_states: List[CanonicalFlightState] = [] canonical_states: List[CanonicalFlightState] = []
@ -231,21 +220,25 @@ class OpenSkyHistoricalAdapter(BaseLiveDataAdapter):
if cs: if cs:
canonical_states.append(cs) canonical_states.append(cs)
data_message: AdapterMessage = {"type": MSG_TYPE_FLIGHT_DATA, "timestamp": current_time, "payload": canonical_states} # MODIFICATO: Crea un payload strutturato per la coda
payload = {
"canonical": canonical_states,
"raw_json": raw_json_string
}
data_message: AdapterMessage = {
"type": MSG_TYPE_FLIGHT_DATA,
"timestamp": current_time,
"payload": payload
}
self.output_queue.put(data_message) self.output_queue.put(data_message)
module_logger.info(f"{self.name}: Success for timestamp {current_time}. Found {len(canonical_states)} states.") module_logger.info(f"{self.name}: Success for timestamp {current_time}. Found {len(canonical_states)} states.")
except requests.exceptions.HTTPError as http_err: except requests.exceptions.HTTPError as http_err:
# --- LOGGING FORENSE DELL'ERRORE --- module_logger.error(f"API Call FAILED for timestamp {current_time}. Status: {http_err.response.status_code}")
module_logger.error("-----------------------------------------------------")
module_logger.error(f"--- API Call FAILED ---")
module_logger.error(f"Status Code: {http_err.response.status_code}")
try: try:
error_details = http_err.response.json() module_logger.error(f"Error JSON Response: {http_err.response.json()}")
module_logger.error(f"Error JSON Response: {error_details}") except json.JSONDecodeError:
except requests.exceptions.JSONDecodeError:
module_logger.error(f"Error Raw Text: {http_err.response.text}") module_logger.error(f"Error Raw Text: {http_err.response.text}")
module_logger.error("-----------------------------------------------------")
if http_err.response.status_code in [401, 403]: if http_err.response.status_code in [401, 403]:
self.access_token = None self.access_token = None