refactorint app_controller step 1
This commit is contained in:
parent
c9d9bb36ec
commit
4d212b5f6e
@ -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()
|
||||
285
flightmonitor/controller/aircraft_db_importer.py
Normal file
285
flightmonitor/controller/aircraft_db_importer.py
Normal 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__}."
|
||||
)
|
||||
@ -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."
|
||||
)
|
||||
)
|
||||
308
flightmonitor/controller/live_data_processor.py
Normal file
308
flightmonitor/controller/live_data_processor.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user