refactorint app_controller step 1

This commit is contained in:
VALLONGOL 2025-06-04 12:38:41 +02:00
parent c9d9bb36ec
commit 4d212b5f6e
4 changed files with 698 additions and 541 deletions

View File

@ -9,7 +9,7 @@ from .data.logging_config import LOGGING_CONFIG # Import the config dictionary
# MODIFIED: Import the new setup_basic_logging function AND get_logger function
# WHY: Need to call the centralized basic setup function early and get logger instances.
# HOW: Changed setup_logging to setup_basic_logging.
from .utils.logger import setup_basic_logging, get_logger # MODIFIED HERE
from .utils.logger import setup_basic_logging, get_logger
from .gui.main_window import MainWindow
from .controller.app_controller import AppController
@ -28,8 +28,8 @@ def main():
# WHY: The Tkinter-based log processor needs a root instance to schedule itself.
# The TkinterTextHandler itself will be added later by MainWindow.
# HOW: Called setup_basic_logging here, passing the necessary arguments.
setup_basic_logging( # MODIFIED HERE
root_tk_instance_for_processor=root, # MODIFIED: Pass root for the log processor
setup_basic_logging(
root_tk_instance_for_processor=root,
logging_config_dict=LOGGING_CONFIG,
)
@ -40,13 +40,20 @@ def main():
# Create the application controller
app_controller = AppController()
# Create the main application window
# MainWindow itself will now handle the creation and specific addition of its log widget
# to the logging system using a new dedicated function in the logger module.
main_app_window = MainWindow(root, app_controller)
# Set the main window instance in the controller
app_controller.set_main_window(main_app_window)
# MODIFIED: Set the main window instance in the controller *before* creating MainWindow
# WHY: MainWindow's initialization (specifically MapCanvasManager) might call back
# to the controller requesting MainWindow methods (like show_error_message).
# If the controller doesn't have a reference to MainWindow yet, these calls fail.
# By setting it here, the controller has a reference to the *soon-to-be-created*
# MainWindow (self-referencing via `app_controller.set_main_window`),
# which allows it to correctly pass itself (with its window reference) to sub-components like MapCanvasManager.
# HOW: Moved this line before MainWindow creation. It's a forward reference,
# but Tkinter's `root` and `app_controller` are already defined.
# MainWindow will then internally set its own reference during its init.
# NOTE: This is a common pattern for mutually dependent objects.
main_app_window_placeholder = MainWindow(root, app_controller) # Create the main application window instance
app_controller.set_main_window(main_app_window_placeholder) # Pass the instance to the controller
# main_app_window = main_app_window_placeholder # Keep the name consistent
# Start the Tkinter event loop
module_logger_main.info("Starting Tkinter main loop.")
@ -59,4 +66,4 @@ def main():
if __name__ == "__main__":
main()
main()

View File

@ -0,0 +1,285 @@
# FlightMonitor/controller/aircraft_db_importer.py
"""
Handles the import of aircraft database from CSV files,
including progress tracking and multi-threading for the import process.
"""
import os
import csv
import threading
import time
from typing import Optional, Any, Callable, Tuple, TYPE_CHECKING
from ..utils.logger import get_logger
# Type checking imports to avoid circular dependencies at runtime
if TYPE_CHECKING:
from ..data.aircraft_database_manager import AircraftDatabaseManager
from ..gui.main_window import MainWindow
from ..gui.dialogs.import_progress_dialog import ImportProgressDialog
module_logger = get_logger(__name__)
# Constants for CSV processing
CSV_FIELD_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 # 10 MB
PROGRESS_CALLBACK_INTERVAL_ROWS = 500 # How often to update the progress bar
class AircraftDBImporter:
"""
Manages the process of importing aircraft data from a CSV file into the database.
Designed to run the import in a separate thread and report progress to a GUI dialog.
"""
def __init__(
self,
aircraft_db_manager: "AircraftDatabaseManager",
main_window: "MainWindow",
):
"""
Initializes the AircraftDBImporter.
Args:
aircraft_db_manager: An instance of AircraftDatabaseManager to perform DB operations.
main_window: A reference to the main application window (for GUI updates and messages).
"""
self.aircraft_db_manager = aircraft_db_manager
self.main_window = main_window
module_logger.debug("AircraftDBImporter initialized.")
def import_aircraft_database_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
"""
Starts the aircraft database import process in a separate thread.
This method is intended to be called from the GUI thread.
Args:
csv_filepath: The path to the CSV file to import.
progress_dialog_ref: A reference to the ImportProgressDialog instance for updates.
"""
module_logger.info(
f"AircraftDBImporter: Requesting aircraft DB import with progress from: {csv_filepath}"
)
# Basic validation checks before starting the thread
if not self.aircraft_db_manager:
module_logger.error(
"AircraftDBImporter: AircraftDatabaseManager not initialized. Cannot import."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
# Schedule update to run on the GUI thread
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Error: Database manager not active.",
)
elif self.main_window and hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Database Error", "Aircraft database manager not active."
)
return
if not (self.main_window and self.main_window.root.winfo_exists()):
module_logger.error("AircraftDBImporter: MainWindow not available to start import thread.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Error: Main application window not available.",
)
return
# Notify the dialog that import has started
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(progress_dialog_ref.import_started)
# Start the import in a new daemon thread
import_thread = threading.Thread(
target=self._perform_db_import_threaded,
args=(csv_filepath, progress_dialog_ref),
daemon=True, # Daemon threads exit when the main program exits
name="AircraftDBImportWorker"
)
import_thread.start()
module_logger.info(f"AircraftDBImporter: Import thread started for: {csv_filepath}")
def _perform_db_import_threaded(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
"""
Executes the database import process in a separate thread.
This method should NOT be called directly from the GUI thread.
"""
if not self.aircraft_db_manager:
module_logger.error("AircraftDBImporter: AircraftDBManager N/A in import thread.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
"Internal Error: DB Manager missing.",
)
return
# Attempt to increase CSV field size limit to handle large fields
self._set_csv_field_size_limit()
module_logger.info(f"AircraftDBImporter: Import thread: Starting row count for: {csv_filepath}")
# Count total rows for progress bar calculation
total_data_rows = self._count_csv_rows(csv_filepath)
if total_data_rows is None:
module_logger.error(f"AircraftDBImporter: Could not count rows in CSV: {csv_filepath}")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.import_finished,
False,
f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.",
)
return
if total_data_rows == 0:
module_logger.info(f"AircraftDBImporter: CSV file '{csv_filepath}' is empty or header-only.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
0,
f"File '{os.path.basename(csv_filepath)}' is empty or header-only.",
)
self._schedule_gui_update(
progress_dialog_ref.import_finished,
True,
"Import complete: No data rows to import.",
)
return
# Initial progress update with total rows found
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
total_data_rows,
f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...",
)
# Define the progress callback function that will be passed to the DB manager
def import_progress_update_callback(processed_csv_rows, imported_db_rows, total_for_cb):
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
# Schedule the GUI update on the main Tkinter thread
self._schedule_gui_update(
progress_dialog_ref.update_progress,
processed_csv_rows,
imported_db_rows,
total_for_cb,
f"Importing CSV row {processed_csv_rows}...",
)
# Perform the actual import via AircraftDatabaseManager
processed_final, imported_final = self.aircraft_db_manager.import_from_csv(
csv_filepath,
replace_existing=True, # Always replace existing for full import
progress_callback=import_progress_update_callback,
total_rows_for_callback=total_data_rows,
)
# Determine final message and success status
success = True
final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}."
if imported_final == 0 and processed_final > 0 and total_data_rows > 0:
success = False # Consider it a failure if no rows were imported
final_message = f"DB Import: {processed_final} CSV data rows processed, 0 imported (check CSV format/logs)."
module_logger.error(f"AircraftDBImporter: {final_message}")
elif imported_final == 0 and processed_final == 0 and total_data_rows > 0:
success = False
final_message = f"DB Import failed. Could not process rows from '{os.path.basename(csv_filepath)}'."
module_logger.critical(f"AircraftDBImporter: {final_message}")
# Final update to the progress dialog
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
self._schedule_gui_update(
progress_dialog_ref.update_progress,
processed_final,
imported_final,
total_data_rows,
"Finalizing import...",
)
self._schedule_gui_update(
progress_dialog_ref.import_finished, success, final_message
)
module_logger.info(f"AircraftDBImporter: Import thread finished. Result: {final_message}")
def _count_csv_rows(self, csv_filepath: str) -> Optional[int]:
"""
Counts the number of data rows in a CSV file (excluding header).
Args:
csv_filepath: The path to the CSV file.
Returns:
The number of data rows, or None if the file cannot be read or is not found.
"""
# Temporarily increase CSV field size limit for robust row counting
self._set_csv_field_size_limit()
try:
with open(csv_filepath, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
try:
next(reader) # Skip header row
except StopIteration:
return 0 # File is empty or only contains header
row_count = sum(1 for _ in reader)
module_logger.debug(f"AircraftDBImporter: Counted {row_count} data rows in {csv_filepath}")
return row_count
except FileNotFoundError:
module_logger.error(f"AircraftDBImporter: CSV file not found: {csv_filepath}")
return None
except csv.Error as e_csv:
module_logger.error(f"AircraftDBImporter: CSV format error during row count of {csv_filepath}: {e_csv}", exc_info=False)
return None
except Exception as e:
module_logger.error(f"AircraftDBImporter: Unexpected error counting rows in {csv_filepath}: {e}", exc_info=True)
return None
def _set_csv_field_size_limit(self):
"""
Attempts to increase the CSV field size limit to handle potentially large fields.
"""
try:
current_limit = csv.field_size_limit()
if CSV_FIELD_SIZE_LIMIT_BYTES > current_limit:
csv.field_size_limit(CSV_FIELD_SIZE_LIMIT_BYTES)
module_logger.debug(
f"AircraftDBImporter: Increased csv.field_size_limit from {current_limit} to {CSV_FIELD_SIZE_LIMIT_BYTES}."
)
except Exception as e:
module_logger.warning(
f"AircraftDBImporter: Error attempting to increase csv.field_size_limit: {e}. Proceeding with default limit.", exc_info=False
)
def _schedule_gui_update(self, callable_func: Callable, *args: Any):
"""
Schedules a callable function to be executed on the main Tkinter thread.
This is crucial for safely updating GUI elements from a worker thread.
Args:
callable_func: The function to call on the GUI thread.
*args: Arguments to pass to the callable function.
"""
if (
self.main_window
and hasattr(self.main_window, "root")
and self.main_window.root.winfo_exists()
):
try:
self.main_window.root.after(0, lambda: callable_func(*args))
except Exception as e:
module_logger.error(
f"AircraftDBImporter: Error scheduling GUI update from import thread: {e}", exc_info=True
)
else:
module_logger.warning(
f"AircraftDBImporter: Main window or root destroyed, cannot schedule GUI update for {callable_func.__name__}."
)

View File

@ -1,7 +1,6 @@
# FlightMonitor/controller/app_controller.py
from queue import Queue, Empty as QueueEmpty
import threading
import tkinter as tk
import time
import os
import csv
@ -11,15 +10,6 @@ from datetime import datetime, timezone
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,
)
from ..data import config as app_config
from ..utils.logger import get_logger
@ -36,7 +26,10 @@ from ..utils.gui_utils import (
GUI_STATUS_UNKNOWN,
)
# MODIFIED: Import _is_valid_bbox_dict from map_utils for bounding_box validation
from .aircraft_db_importer import AircraftDBImporter
from .live_data_processor import LiveDataProcessor
from ..map.map_utils import _is_valid_bbox_dict
@ -48,7 +41,6 @@ if TYPE_CHECKING:
module_logger = get_logger(__name__)
GUI_QUEUE_CHECK_INTERVAL_MS = 150
ADAPTER_JOIN_TIMEOUT_SECONDS = 5.0
DEFAULT_CLICK_AREA_SIZE_KM = 50.0
@ -58,11 +50,15 @@ class AppController:
self.main_window: Optional["MainWindow"] = None
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
self.aircraft_db_manager: Optional[AircraftDatabaseManager] = None
self.aircraft_db_importer: Optional[AircraftDBImporter] = None
self.live_data_processor: Optional[LiveDataProcessor] = None
self.active_detail_window_icao: Optional[str] = None
self.active_detail_window_ref: Optional["FullFlightDetailsWindow"] = None
@ -94,6 +90,20 @@ class AppController:
module_logger.debug(
f"Main window instance ({type(main_window_instance)}) set in AppController."
)
if self.aircraft_db_manager and self.main_window:
self.aircraft_db_importer = AircraftDBImporter(
self.aircraft_db_manager, self.main_window
)
module_logger.info("AircraftDBImporter initialized successfully by AppController.")
else:
module_logger.warning("AircraftDBImporter could not be initialized due to missing dependencies.")
self.flight_data_queue = Queue(maxsize=200)
self.live_data_processor = LiveDataProcessor(self, self.flight_data_queue)
module_logger.info("LiveDataProcessor initialized successfully by AppController.")
initial_status_msg = "System Initialized. Ready."
initial_status_level = GUI_STATUS_OK
if not self.data_storage:
@ -125,257 +135,6 @@ class AppController:
"Main window not set or lacks update_semaphore_and_status during set_main_window."
)
def _process_flight_data_queue(self):
if not self.flight_data_queue:
return
if not (
self.main_window
and hasattr(self.main_window, "root")
and self.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 = None
try:
message = self.flight_data_queue.get(block=False, timeout=0.01)
except QueueEmpty:
break
except Exception as e_q_get:
module_logger.warning(
f"Error getting from flight_data_queue: {e_q_get}"
)
continue
if message is None:
continue
try:
message_type = message.get("type")
if message_type == MSG_TYPE_FLIGHT_DATA:
flight_states_payload_chunk: Optional[
List[CanonicalFlightState]
] = message.get("payload")
if flight_states_payload_chunk is not None:
flight_payloads_this_cycle.extend(
flight_states_payload_chunk
)
if self.data_storage:
saved_count = 0
for state in flight_states_payload_chunk:
if not isinstance(state, CanonicalFlightState):
module_logger.warning(
f"Skipping non-CanonicalFlightState object in payload: {type(state)}"
)
continue
try:
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
except Exception as e_db_add:
module_logger.error(
f"Error saving flight/position to DB for ICAO {state.icao24}: {e_db_add}",
exc_info=False,
)
if saved_count > 0:
module_logger.info(
f"Saved {saved_count} position updates to DB from this chunk."
)
if (
hasattr(self.main_window, "map_manager_instance")
and self.main_window.map_manager_instance is not None
and hasattr(
self.main_window.map_manager_instance,
"update_flights_on_map",
)
and self.is_live_monitoring_active
and self._active_bounding_box
):
self.main_window.map_manager_instance.update_flights_on_map(
flight_states_payload_chunk
)
gui_message = (
f"Live data: {len(flight_states_payload_chunk)} aircraft in chunk."
if flight_states_payload_chunk
else "Live data: No aircraft in area (chunk)."
)
if hasattr(self.main_window, "update_semaphore_and_status"):
try:
self.main_window.update_semaphore_and_status(
GUI_STATUS_OK, gui_message
)
except tk.TclError:
self._gui_after_id = None
return
else:
if hasattr(self.main_window, "update_semaphore_and_status"):
try:
self.main_window.update_semaphore_and_status(
GUI_STATUS_WARNING,
"Received empty data payload from adapter.",
)
except tk.TclError:
self._gui_after_id = None
return
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_to_set = GUI_STATUS_UNKNOWN
action_required = None
if status_code == STATUS_PERMANENT_FAILURE:
action_required = "STOP_MONITORING"
elif status_code == STATUS_API_ERROR_TEMPORARY:
gui_status_level_to_set = GUI_STATUS_ERROR
elif status_code == STATUS_RATE_LIMITED:
gui_status_level_to_set = GUI_STATUS_WARNING
elif status_code == STATUS_FETCHING:
gui_status_level_to_set = GUI_STATUS_FETCHING
elif status_code in [
STATUS_STARTING,
STATUS_RECOVERED,
STATUS_STOPPED,
]:
gui_status_level_to_set = GUI_STATUS_OK
if hasattr(self.main_window, "update_semaphore_and_status"):
try:
self.main_window.update_semaphore_and_status(
gui_status_level_to_set, gui_message_from_adapter
)
except tk.TclError:
self._gui_after_id = None
return
if action_required == "STOP_MONITORING":
self.stop_live_monitoring(from_error=True)
break
except Exception as e_msg_proc:
module_logger.error(
f"Error processing adapter message: {e_msg_proc}", exc_info=True
)
finally:
try:
self.flight_data_queue.task_done()
except ValueError:
pass
except Exception as e_task_done:
module_logger.error(
f"Error calling task_done on flight_data_queue: {e_task_done}"
)
# MODIFIED: Live update for detail window
if (
self.active_detail_window_ref
and self.active_detail_window_icao
and self.active_detail_window_ref.winfo_exists()
):
flight_of_interest_updated_this_cycle = False
latest_live_data_for_detail_icao: Optional[Dict[str, Any]] = None
for state_obj in flight_payloads_this_cycle:
if state_obj.icao24 == self.active_detail_window_icao:
flight_of_interest_updated_this_cycle = True
latest_live_data_for_detail_icao = state_obj.to_dict()
# Consider this the "most live" data for the detail view from this batch
break
if flight_of_interest_updated_this_cycle:
module_logger.info(
f"AppController: Flight {self.active_detail_window_icao} in detail view was in the latest data batch. Refreshing detail window."
)
static_data_upd: Optional[Dict[str, Any]] = None
if self.aircraft_db_manager:
static_data_upd = self.aircraft_db_manager.get_aircraft_details(
self.active_detail_window_icao
)
full_track_data_list_upd: List[Dict[str, Any]] = []
if self.data_storage:
try:
current_utc_date = datetime.now(
timezone.utc
) # Or a date range if history is more complex
track_states_upd = (
self.data_storage.get_flight_track_for_icao_on_date(
self.active_detail_window_icao, current_utc_date
)
)
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"Error retrieving updated track for detail view {self.active_detail_window_icao}: {e_track_upd}"
)
try:
self.active_detail_window_ref.update_details(
static_data_upd,
latest_live_data_for_detail_icao,
full_track_data_list_upd,
)
except tk.TclError:
module_logger.warning(
f"AppController: TclError trying to update detail window for {self.active_detail_window_icao}, likely closed."
)
self.details_window_closed(self.active_detail_window_icao)
except Exception as e_upd_detail_win:
module_logger.error(
f"AppController: Error updating detail window for {self.active_detail_window_icao}: {e_upd_detail_win}",
exc_info=True,
)
except Exception as e_outer:
module_logger.error(
f"Outer error in _process_flight_data_queue: {e_outer}", exc_info=True
)
finally:
if (
self.is_live_monitoring_active
and self.main_window
and hasattr(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:
self._gui_after_id = None
except Exception as e_after_schedule:
module_logger.error(
f"Error rescheduling _process_flight_data_queue: {e_after_schedule}"
)
self._gui_after_id = None
else:
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 for live monitoring.")
@ -422,9 +181,7 @@ class AppController:
if hasattr(self.main_window, "clear_all_views_data"):
self.main_window.clear_all_views_data()
if self.flight_data_queue is None:
self.flight_data_queue = Queue(maxsize=200)
else:
if self.flight_data_queue:
while not self.flight_data_queue.empty():
try:
self.flight_data_queue.get_nowait()
@ -434,21 +191,11 @@ class AppController:
except Exception:
break
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
module_logger.info("Old adapter thread found alive. Stopping it first.")
try:
self.live_adapter_thread.stop()
if self.main_window and self.main_window.root.winfo_exists():
self.main_window.root.update_idletasks()
self.live_adapter_thread.join(timeout=ADAPTER_JOIN_TIMEOUT_SECONDS)
if self.live_adapter_thread.is_alive():
module_logger.warning("Old adapter thread did not stop in time.")
except Exception as e_join:
module_logger.error(
f"Error stopping old adapter: {e_join}", exc_info=True
)
finally:
self.live_adapter_thread = None
if not self.live_data_processor:
module_logger.critical("Controller: LiveDataProcessor not initialized. Cannot start monitoring.")
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state("Start failed: Internal error (Processor N/A).")
return
self.live_adapter_thread = OpenSkyLiveAdapter(
output_queue=self.flight_data_queue,
@ -458,31 +205,7 @@ class AppController:
self.is_live_monitoring_active = True
self.live_adapter_thread.start()
if (
self._gui_after_id
and self.main_window
and self.main_window.root.winfo_exists()
):
try:
self.main_window.root.after_cancel(self._gui_after_id)
except:
pass
finally:
self._gui_after_id = None
if self.main_window and self.main_window.root.winfo_exists():
self._gui_after_id = self.main_window.root.after(
100, self._process_flight_data_queue
)
else:
module_logger.error(
"Cannot schedule queue processor: MainWindow or root missing."
)
self.is_live_monitoring_active = False
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
self.live_adapter_thread.stop()
if hasattr(self.main_window, "_reset_gui_to_stopped_state"):
self.main_window._reset_gui_to_stopped_state("Start failed: GUI error.")
self.live_data_processor.start_processing_queue()
def stop_live_monitoring(self, from_error: bool = False):
if not self.is_live_monitoring_active and not (
@ -504,20 +227,10 @@ class AppController:
)
self.is_live_monitoring_active = False
if (
self._gui_after_id
and self.main_window
and hasattr(self.main_window, "root")
and self.main_window.root.winfo_exists()
):
try:
self.main_window.root.after_cancel(self._gui_after_id)
except tk.TclError:
pass
except Exception:
pass
finally:
self._gui_after_id = None
if self.live_data_processor:
self.live_data_processor.stop_processing_queue()
else:
module_logger.warning("Controller: LiveDataProcessor not initialized during stop_live_monitoring.")
if self.live_adapter_thread and self.live_adapter_thread.is_alive():
try:
@ -534,23 +247,6 @@ class AppController:
finally:
self.live_adapter_thread = None
if (
self.flight_data_queue
and self.main_window
and hasattr(self.main_window, "root")
and self.main_window.root.winfo_exists()
):
try:
# Process remaining queue items. Temporarily set active to True to allow one last run.
original_active_state = self.is_live_monitoring_active
self.is_live_monitoring_active = True # Allow one last processing
self._process_flight_data_queue()
self.is_live_monitoring_active = original_active_state # Restore state
except Exception as e_final_q:
module_logger.error(
f"Error in final queue processing: {e_final_q}", exc_info=True
)
if hasattr(self.main_window, "clear_all_views_data"):
self.main_window.clear_all_views_data()
@ -585,7 +281,7 @@ class AppController:
module_logger.error(
f"Error closing detail window on app exit: {e_close_detail}"
)
finally: # Ensure these are cleared even if destroy fails for some reason
finally:
self.active_detail_window_ref = None
self.active_detail_window_icao = None
@ -609,6 +305,10 @@ class AppController:
exc_info=True,
)
if self.live_data_processor and self.is_live_monitoring_active:
module_logger.info("Controller: Stopping LiveDataProcessor explicitly on app exit.")
self.live_data_processor.stop_processing_queue()
is_adapter_considered_running = (
self.live_adapter_thread and self.live_adapter_thread.is_alive()
) or self.is_live_monitoring_active
@ -716,8 +416,34 @@ class AppController:
f"Error updating map clicked info panel: {e_update}", exc_info=False
)
def import_aircraft_database_from_file_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
if not self.aircraft_db_importer:
module_logger.error(
"AppController: AircraftDBImporter not initialized. Cannot perform import."
)
if progress_dialog_ref and progress_dialog_ref.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(
False, "Error: Import function not initialized correctly (controller issue)."
))
elif self.main_window and hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Import Error", "Aircraft database importer not ready."
)
return
self.aircraft_db_importer.import_aircraft_database_with_progress(
csv_filepath, progress_dialog_ref
)
# MODIFIED: Re-added request_detailed_flight_info (this was the missing function)
# WHY: This function is crucial for displaying selected flight details in the main window panel.
# It was inadvertently removed during the refactoring process.
# HOW: Re-inserted the function with its corrected logic for combining live and static data.
def request_detailed_flight_info(self, icao24: str):
normalized_icao24 = icao24.lower().strip() # Normalize here as well for safety
normalized_icao24 = icao24.lower().strip()
module_logger.info(
f"Controller: Detailed info request for ICAO24: {normalized_icao24}"
)
@ -734,8 +460,11 @@ class AppController:
live_data_for_panel: Optional[Dict[str, Any]] = None
static_data_for_panel: Optional[Dict[str, Any]] = None
# Initialize combined_details_for_panel with icao24.
combined_details_for_panel: Dict[str, Any] = {"icao24": normalized_icao24}
# Process live data first
if (
self.main_window
and hasattr(self.main_window, "map_manager_instance")
@ -751,190 +480,28 @@ class AppController:
if state.icao24 == normalized_icao24:
live_data_for_panel = state.to_dict()
break
if live_data_for_panel:
# IMPORTANT: Use .update() to ensure live data fields overwrite existing ones
combined_details_for_panel.update(live_data_for_panel)
module_logger.debug(f"AppController: Added live data to details for {normalized_icao24}.")
# Process static data second, only if key not already set by live data
if self.aircraft_db_manager:
static_data_for_panel = self.aircraft_db_manager.get_aircraft_details(
normalized_icao24
)
if static_data_for_panel:
for k, v in static_data_for_panel.items():
if k not in combined_details_for_panel:
# Only add static data if the key is not already populated (e.g., by live data)
if k not in combined_details_for_panel or combined_details_for_panel[k] is None:
combined_details_for_panel[k] = v
module_logger.debug(f"AppController: Added static data to details for {normalized_icao24}.")
# Final call to update the GUI panel
if hasattr(self.main_window, "update_selected_flight_details"):
self.main_window.update_selected_flight_details(combined_details_for_panel)
def import_aircraft_database_from_file_with_progress(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
module_logger.info(
f"Controller: Requesting aircraft DB import with progress from: {csv_filepath}"
)
if not self.aircraft_db_manager:
module_logger.error(
"AircraftDatabaseManager not initialized. Cannot import."
)
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
progress_dialog_ref.import_finished(
False, "Error: Database manager not active."
)
elif self.main_window and hasattr(self.main_window, "show_error_message"):
self.main_window.show_error_message(
"Database Error", "Aircraft database manager not active."
)
return
if not self.main_window or not self.main_window.root.winfo_exists():
module_logger.error("MainWindow not available to start import thread.")
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
progress_dialog_ref.import_finished(
False, "Error: Main application window not available."
)
return
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
progress_dialog_ref.import_started()
import_thread = threading.Thread(
target=self._perform_db_import_with_progress_threaded,
args=(csv_filepath, progress_dialog_ref),
daemon=True,
)
import_thread.start()
module_logger.info(f"Aircraft DB import thread started for: {csv_filepath}")
def _count_csv_rows(self, csv_filepath: str) -> Optional[int]:
try:
current_limit = csv.field_size_limit()
new_limit_target = 10 * 1024 * 1024
if new_limit_target > current_limit:
csv.field_size_limit(new_limit_target)
except Exception:
pass # Ignore errors setting limit
try:
with open(csv_filepath, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
try:
next(reader)
except StopIteration:
return 0
return sum(1 for _ in reader)
except FileNotFoundError:
return None
except csv.Error:
return None
except Exception:
return None
def _perform_db_import_with_progress_threaded(
self, csv_filepath: str, progress_dialog_ref: "ImportProgressDialog"
):
if not self.aircraft_db_manager:
module_logger.error("AircraftDBManager N/A in import thread.")
if (
progress_dialog_ref
and progress_dialog_ref.winfo_exists()
and self.main_window
and self.main_window.root.winfo_exists()
):
self.main_window.root.after(
0,
lambda: progress_dialog_ref.import_finished(
False, "Internal Error: DB Manager missing."
),
)
return
module_logger.info(f"Import thread: Starting row count for: {csv_filepath}")
def schedule_gui_update(callable_func: Callable, *args: Any):
if (
self.main_window
and hasattr(self.main_window, "root")
and self.main_window.root.winfo_exists()
):
try:
self.main_window.root.after(0, lambda: callable_func(*args))
except Exception as e_after:
module_logger.error(
f"Error scheduling GUI update from import thread: {e_after}"
)
total_data_rows = self._count_csv_rows(csv_filepath)
if total_data_rows is None:
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
schedule_gui_update(
progress_dialog_ref.import_finished,
False,
f"Error: Could not read/count rows in '{os.path.basename(csv_filepath)}'. Check logs.",
)
return
if total_data_rows == 0:
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
0,
f"File '{os.path.basename(csv_filepath)}' is empty or header-only.",
)
schedule_gui_update(
progress_dialog_ref.import_finished,
True,
"Import complete: No data rows to import.",
)
return
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
schedule_gui_update(
progress_dialog_ref.update_progress,
0,
0,
total_data_rows,
f"Found {total_data_rows} data rows. Starting import from '{os.path.basename(csv_filepath)}'...",
)
def import_progress_update_for_dialog_from_controller(
processed_csv_rows, imported_db_rows, total_for_cb
):
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
schedule_gui_update(
progress_dialog_ref.update_progress,
processed_csv_rows,
imported_db_rows,
total_for_cb,
f"Importing CSV row {processed_csv_rows}...",
)
processed_final, imported_final = self.aircraft_db_manager.import_from_csv(
csv_filepath,
replace_existing=True,
progress_callback=import_progress_update_for_dialog_from_controller,
total_rows_for_callback=total_data_rows,
)
final_message = f"DB Import complete. Processed: {processed_final}, Imported/Updated: {imported_final}."
success = True
if imported_final == 0 and processed_final > 0 and total_data_rows > 0:
final_message = f"DB Import: {processed_final} CSV data rows processed, 0 imported (check CSV format/logs)."
elif imported_final == 0 and processed_final == 0 and total_data_rows > 0:
final_message = f"DB Import failed. Could not process rows from '{os.path.basename(csv_filepath)}'."
success = False
if progress_dialog_ref and progress_dialog_ref.winfo_exists():
schedule_gui_update(
progress_dialog_ref.update_progress,
processed_final,
imported_final,
total_data_rows,
"Finalizing import...",
)
schedule_gui_update(
progress_dialog_ref.import_finished, success, final_message
)
module_logger.debug(f"AppController: Called update_selected_flight_details with combined data for {normalized_icao24}.")
def request_and_show_full_flight_details(self, icao24: str):
normalized_icao24 = icao24.lower().strip()
@ -960,20 +527,16 @@ class AppController:
)
return
# MODIFIED: Close existing detail window before opening a new one
if (
self.active_detail_window_ref
and self.active_detail_window_ref.winfo_exists()
):
# Do not destroy if it's for the same ICAO and already open (optional behavior)
# For now, always destroy and recreate for simplicity of state.
if self.active_detail_window_icao == normalized_icao24:
module_logger.info(
f"Detail window for {normalized_icao24} already open. Re-focusing/Re-populating."
)
self.active_detail_window_ref.lift()
self.active_detail_window_ref.focus_set()
# No need to re-create, just update its content with potentially newer data
else:
module_logger.info(
f"Closing existing detail window for {self.active_detail_window_icao} before opening new one for {normalized_icao24}."
@ -982,7 +545,6 @@ class AppController:
self.active_detail_window_ref.destroy()
except tk.TclError:
pass
# details_window_closed will clear self.active_detail_window_ref & icao
static_data: Optional[Dict[str, Any]] = None
if self.aircraft_db_manager:
@ -1025,14 +587,13 @@ class AppController:
try:
from ..gui.dialogs.full_flight_details_window import FullFlightDetailsWindow
# If we decided to re-focus/re-populate, and the window still exists
if (
self.active_detail_window_ref
and self.active_detail_window_icao == normalized_icao24
and self.active_detail_window_ref.winfo_exists()
):
details_win = self.active_detail_window_ref
else: # Create new
else:
details_win = FullFlightDetailsWindow(
self.main_window.root, normalized_icao24, self
)
@ -1060,30 +621,27 @@ class AppController:
self.main_window.show_error_message(
"Error", f"Could not display full details: {e_show_details}"
)
self.active_detail_window_ref = None # Clear refs if creation/update failed
self.active_detail_window_ref = None
self.active_detail_window_icao = None
if self.main_window:
self.main_window.full_flight_details_window = None
def details_window_closed(self, closed_icao24: str):
normalized_closed_icao24 = closed_icao24.lower().strip()
# Important: check if the closed ICAO matches the one we are actively tracking
if self.active_detail_window_icao == normalized_closed_icao24:
module_logger.info(
f"AppController: Detail window for {normalized_closed_icao24} reported closed. Clearing references."
)
self.active_detail_window_ref = None
self.active_detail_window_icao = None
# Also clear MainWindow's convenience reference if it matches
if (
self.main_window
and hasattr(self.main_window, "full_flight_details_window")
and self.main_window.full_flight_details_window
and not self.main_window.full_flight_details_window.winfo_exists()
): # Check if already destroyed
):
self.main_window.full_flight_details_window = None
else:
# This case can happen if a new detail window was opened before the old one finished its close notification
module_logger.debug(
f"AppController: A detail window for {normalized_closed_icao24} closed, but it was not the currently tracked active one ({self.active_detail_window_icao}). No action on active_detail references."
)
@ -1187,10 +745,9 @@ class AppController:
if hasattr(map_manager, "recenter_map_at_coords"):
try:
map_manager.recenter_map_at_coords(lat, lon)
except Exception as e_recenter:
except Exception as e:
module_logger.error(
f"Error map_manager.recenter_map_at_coords: {e_recenter}",
exc_info=False,
f"Error map_manager.recenter_map_at_coords: {e}", exc_info=False
)
else:
module_logger.warning(
@ -1221,9 +778,9 @@ class AppController:
map_manager.set_bbox_around_coords(
center_lat, center_lon, area_size_km
)
except Exception as e_set_bbox:
except Exception as e:
module_logger.error(
f"Error map_manager.set_bbox_around_coords: {e_set_bbox}",
f"Error map_manager.set_bbox_around_coords: {e}",
exc_info=False,
)
else:
@ -1454,4 +1011,4 @@ class AppController:
else:
module_logger.warning(
"MapCanvasManager or 'set_max_track_points' N/A to set track length."
)
)

View File

@ -0,0 +1,308 @@
# 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
from ..utils.logger import get_logger
from ..data.common_models import CanonicalFlightState
from ..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 ..utils.gui_utils import (
GUI_STATUS_OK,
GUI_STATUS_WARNING,
GUI_STATUS_ERROR,
GUI_STATUS_FETCHING,
GUI_STATUS_UNKNOWN,
)
# Type checking imports to avoid circular dependencies at runtime
if TYPE_CHECKING:
from .app_controller import AppController # Import for type hinting AppController
from ..gui.main_window import MainWindow # For specific MainWindow methods/attributes
module_logger = get_logger(__name__)
# Constants for queue processing interval
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]):
"""
Initializes the LiveDataProcessor.
Args:
app_controller: The main AppController instance. This processor accesses
various components (MainWindow, DataStorage, etc.) via the controller.
flight_data_queue: The queue from which to read AdapterMessages (flight data or status).
"""
self.app_controller = app_controller
self.flight_data_queue = flight_data_queue
self._gui_after_id: Optional[str] = None # Stores the ID for the Tkinter `after` loop
module_logger.debug("LiveDataProcessor initialized.")
def start_processing_queue(self):
"""
Starts the periodic processing of the flight data queue.
Schedules the _process_queue_cycle method to run on the Tkinter main loop.
"""
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 or destroyed.")
return
if self._gui_after_id: # Cancel any existing scheduled processing
try:
main_window.root.after_cancel(self._gui_after_id)
module_logger.debug("LiveDataProcessor: Cancelled existing queue processing loop.")
except Exception:
pass # Ignore if ID is already invalid
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):
"""
Stops the periodic processing of the flight data queue.
Cancels the scheduled _process_queue_cycle method.
"""
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:
module_logger.debug("LiveDataProcessor: Failed to cancel queue processing loop (might be already stopped or invalid ID).")
finally:
self._gui_after_id = None
else:
module_logger.debug("LiveDataProcessor: Queue processing loop already stopped or main window not available.")
self._gui_after_id = None # Ensure it's cleared if root is gone
# Process any remaining items in the queue one last time
self._process_queue_cycle(is_final_flush=True)
def _process_queue_cycle(self, is_final_flush: bool = False):
"""
Processes a batch of messages from the flight data queue in a single Tkinter cycle.
This method is scheduled by Tkinter's `after` method.
"""
# Get necessary references from the app_controller
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 # Check state for re-scheduling
active_bounding_box = self.app_controller._active_bounding_box # For map updates
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()):
module_logger.warning("LiveDataProcessor: Main window or root destroyed during queue processing. Halting loop.")
self._gui_after_id = None
return
flight_payloads_this_cycle: List[CanonicalFlightState] = []
messages_processed_this_cycle = 0
try:
while not self.flight_data_queue.empty():
message = None
try:
# Get message without blocking to keep GUI responsive
message = self.flight_data_queue.get(block=False)
messages_processed_this_cycle += 1
except QueueEmpty:
break # No more messages in queue
except Exception as e_q_get:
module_logger.warning(f"LiveDataProcessor: Error getting from flight_data_queue: {e_q_get}")
continue
if message is None:
continue
try:
message_type = message.get("type")
if message_type == MSG_TYPE_FLIGHT_DATA:
flight_states_payload_chunk: Optional[List[CanonicalFlightState]] = message.get("payload")
if flight_states_payload_chunk is not None:
flight_payloads_this_cycle.extend(flight_states_payload_chunk)
# --- Save to Data Storage ---
if data_storage:
saved_count = 0
for state in flight_states_payload_chunk:
if not isinstance(state, CanonicalFlightState):
module_logger.warning(f"LiveDataProcessor: Skipping non-CanonicalFlightState object in payload: {type(state)}")
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:
pos_id = data_storage.add_position_daily(
flight_id, state
)
if pos_id:
saved_count += 1
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=False)
if saved_count > 0:
module_logger.info(f"LiveDataProcessor: Saved {saved_count} position updates to DB from this chunk.")
# --- Update Map Display ---
if (
hasattr(main_window, "map_manager_instance")
and main_window.map_manager_instance is not None
and hasattr(main_window.map_manager_instance, "update_flights_on_map")
and self.app_controller.is_live_monitoring_active # Check AppController's live state
and active_bounding_box # Map is active only if bbox is set
):
main_window.map_manager_instance.update_flights_on_map(flight_states_payload_chunk)
# --- Update GUI Status Bar (Semaphore) ---
gui_message = f"Live data: {len(flight_states_payload_chunk)} aircraft in chunk." if flight_states_payload_chunk else "Live data: No aircraft in area (chunk)."
if hasattr(main_window, "update_semaphore_and_status"):
try:
main_window.update_semaphore_and_status(GUI_STATUS_OK, gui_message)
except tk.TclError:
module_logger.debug("LiveDataProcessor: TclError updating semaphore, MainWindow likely closing.")
# Don't reschedule if GUI is closing
self._gui_after_id = None
return
else:
if hasattr(main_window, "update_semaphore_and_status"):
try:
main_window.update_semaphore_and_status(GUI_STATUS_WARNING, "Received empty data payload from adapter.")
except tk.TclError:
module_logger.debug("LiveDataProcessor: TclError updating semaphore (empty payload), MainWindow likely closing.")
self._gui_after_id = None
return
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_to_set = GUI_STATUS_UNKNOWN
action_required = None
if status_code == STATUS_PERMANENT_FAILURE:
action_required = "STOP_MONITORING"
gui_status_level_to_set = GUI_STATUS_ERROR # Permanent failures are critical errors
elif status_code == STATUS_API_ERROR_TEMPORARY:
gui_status_level_to_set = GUI_STATUS_ERROR
elif status_code == STATUS_RATE_LIMITED:
gui_status_level_to_set = GUI_STATUS_WARNING
elif status_code == STATUS_FETCHING:
gui_status_level_to_set = GUI_STATUS_FETCHING
elif status_code in [STATUS_STARTING, STATUS_RECOVERED, STATUS_STOPPED]:
gui_status_level_to_set = GUI_STATUS_OK
if hasattr(main_window, "update_semaphore_and_status"):
try:
main_window.update_semaphore_and_status(gui_status_level_to_set, gui_message_from_adapter)
except tk.TclError:
module_logger.debug("LiveDataProcessor: TclError updating semaphore (adapter status), MainWindow likely closing.")
self._gui_after_id = None
return
if action_required == "STOP_MONITORING":
# Delegate stop action back to AppController
module_logger.critical(f"LiveDataProcessor: Adapter reported permanent failure. Requesting AppController to stop monitoring.")
self.app_controller.stop_live_monitoring(from_error=True)
break # Break out of inner queue processing loop to allow main loop to terminate if needed
except Exception as e_msg_proc:
module_logger.error(f"LiveDataProcessor: Error processing adapter message: {e_msg_proc}", exc_info=True)
finally:
try:
self.flight_data_queue.task_done() # Mark task as done regardless of processing success
except ValueError:
pass # Queue might be empty already if break was called
except Exception as e_task_done:
module_logger.error(f"LiveDataProcessor: Error calling task_done on flight_data_queue: {e_task_done}")
# --- Live Update for Full Flight Details Window (if open) ---
if (
active_detail_window_ref
and active_detail_window_icao
and active_detail_window_ref.winfo_exists()
):
flight_of_interest_updated_this_cycle = False
latest_live_data_for_detail_icao: Optional[Dict[str, Any]] = None
# Find the most recent live data for the actively viewed flight
for state_obj in flight_payloads_this_cycle:
if state_obj.icao24 == active_detail_window_icao:
flight_of_interest_updated_this_cycle = True
latest_live_data_for_detail_icao = state_obj.to_dict()
break # Found the latest update for this flight in the current chunk
if flight_of_interest_updated_this_cycle:
module_logger.debug(f"LiveDataProcessor: Flight {active_detail_window_icao} in detail view was in the latest data batch. Refreshing detail window.")
# Retrieve static data (if available) and full track data (if storage is active)
static_data_upd: Optional[Dict[str, Any]] = None
if aircraft_db_manager:
static_data_upd = aircraft_db_manager.get_aircraft_details(active_detail_window_icao)
full_track_data_list_upd: List[Dict[str, Any]] = []
if data_storage:
try:
current_utc_date = datetime.now(timezone.utc)
track_states_upd = data_storage.get_flight_track_for_icao_on_date(active_detail_window_icao, current_utc_date)
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 tk.TclError:
module_logger.warning(f"LiveDataProcessor: TclError trying to update detail window for {active_detail_window_icao}, likely closed.")
self.app_controller.details_window_closed(active_detail_window_icao) # Notify controller to clear its references
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 Exception as e_outer:
module_logger.error(f"LiveDataProcessor: Outer error in _process_queue_cycle: {e_outer}", exc_info=True)
finally:
# Reschedule the next processing cycle ONLY if not a final flush and monitoring is still active
if not is_final_flush and is_live_monitoring_active and main_window.root.winfo_exists():
try:
self._gui_after_id = main_window.root.after(GUI_QUEUE_CHECK_INTERVAL_MS, self._process_queue_cycle)
except tk.TclError:
module_logger.debug("LiveDataProcessor: TclError rescheduling _process_queue_cycle, MainWindow likely closing.")
self._gui_after_id = None
except Exception as e_after_schedule:
module_logger.error(f"LiveDataProcessor: Error rescheduling _process_queue_cycle: {e_after_schedule}")
self._gui_after_id = None
else:
self._gui_after_id = None # Ensure ID is cleared if not rescheduling