diff --git a/scenario_simulator/gui/gui.py b/scenario_simulator/gui/gui.py index dc8876c..ae6a3ec 100644 --- a/scenario_simulator/gui/gui.py +++ b/scenario_simulator/gui/gui.py @@ -7,7 +7,9 @@ widgets, and coordinating the simulation and plotting managers. import tkinter as tk from tkinter import ttk, messagebox, simpledialog +from tkinter.scrolledtext import ScrolledText import queue +import logging import numpy as np from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg @@ -17,9 +19,10 @@ from matplotlib.figure import Figure from ..core.simulation_engine import RadarConfig, AntennaConfig, ScanConfig, Target from ..utils import radar_math, config_manager -# Import new manager classes +# Import new manager classes and logger from .plot_manager import PlotManager from .simulation_manager import SimulationManager +from ..utils import logger # --- Helper Dialog for Adding/Editing Targets --- class AddTargetDialog(tk.Toplevel): @@ -79,16 +82,32 @@ class App(tk.Tk): # --- Main Layout --- main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL) main_paned_window.pack(fill=tk.BOTH, expand=True) + + # --- Left Column (Notebook and Logs) --- left_column_frame = ttk.Frame(main_paned_window, width=600) main_paned_window.add(left_column_frame, weight=1) - right_frame = ttk.Frame(main_paned_window) - main_paned_window.add(right_frame, weight=2) + left_paned_window = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL) left_paned_window.pack(fill=tk.BOTH, expand=True) - notebook_frame = ttk.Frame(left_paned_window, height=450) - left_paned_window.add(notebook_frame, weight=1) - bottom_left_frame = ttk.Frame(left_paned_window) - left_paned_window.add(bottom_left_frame, weight=1) + + notebook_frame = ttk.Frame(left_paned_window, height=500) + left_paned_window.add(notebook_frame, weight=2) + + log_frame = ttk.LabelFrame(left_paned_window, text="Log", height=200) + left_paned_window.add(log_frame, weight=1) + + # --- Right Column (RD and PPI plots) --- + right_column_frame = ttk.Frame(main_paned_window) + main_paned_window.add(right_column_frame, weight=3) + + right_paned_window = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL) + right_paned_window.pack(fill=tk.BOTH, expand=True) + + rd_frame = ttk.LabelFrame(right_paned_window, text="Range-Doppler Map") + right_paned_window.add(rd_frame, weight=1) + + ppi_frame = ttk.Frame(right_paned_window) + right_paned_window.add(ppi_frame, weight=1) # --- Initialize UI Variables --- self._init_vars() @@ -107,15 +126,20 @@ class App(tk.Tk): self._populate_target_tab(self.target_tab) self._populate_scenario_tab(self.scenario_tab) + # --- Setup Logging Area --- + self.log_widget = ScrolledText(log_frame, state=tk.DISABLED, wrap=tk.WORD, font=("TkDefaultFont", 9)) + self.log_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self._init_logging() + # --- Setup Plotting Area --- - ppi_plot_frame = ttk.LabelFrame(bottom_left_frame, text="PPI Display") + ppi_plot_frame = ttk.LabelFrame(ppi_frame, text="PPI Display") ppi_plot_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0), pady=5) - scan_controls_frame = ttk.LabelFrame(bottom_left_frame, text="Scan & PPI Control") - scan_controls_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5), pady=5) + scan_controls_frame = ttk.LabelFrame(ppi_frame, text="Scan & PPI Control") + scan_controls_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 5), pady=5) self._populate_scan_ppi_controls(scan_controls_frame) self.rd_figure = Figure(figsize=(8, 6), dpi=100, facecolor='#3a3a3a') - self.rd_canvas = FigureCanvasTkAgg(self.rd_figure, master=right_frame) + self.rd_canvas = FigureCanvasTkAgg(self.rd_figure, master=rd_frame) self.rd_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.ppi_figure = Figure(figsize=(4, 4), dpi=100, facecolor='#3a3a3a') @@ -130,10 +154,31 @@ class App(tk.Tk): self.toggle_amplitude_controls() self.update_derived_parameters() self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.log.info("Application initialized successfully.") + + def _init_logging(self): + logging_config = { + "default_root_level": logging.INFO, + "format": "%(asctime)s [%(levelname)-8s] %(name)-20s: %(message)s", + "date_format": "%H:%M:%S", + "enable_console": True, + "colors": { + logging.DEBUG: "gray", + logging.INFO: "black", + logging.WARNING: "orange", + logging.ERROR: "red", + logging.CRITICAL: "red", + } + } + logger.setup_basic_logging(self, logging_config) + logger.add_tkinter_handler(self.log_widget, logging_config) + self.log = logger.get_logger(__name__) def on_closing(self): + self.log.info("Shutdown sequence initiated.") if self.simulation_manager.is_running(): self.stop_simulation() + logger.shutdown_logging_system() self.destroy() def _init_vars(self): @@ -288,11 +333,13 @@ class App(tk.Tk): def start_simulation(self): if self.simulation_manager.is_running(): return + self.log.info("Starting simulation...") self.generate_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) radar_cfg = self.get_radar_config_from_gui() targets = self.get_targets_from_gui() if not targets: + self.log.warning("Simulation start requested but no targets are defined.") messagebox.showwarning("No Targets", "Please add at least one target to simulate.", parent=self) self.stop_simulation() return @@ -305,6 +352,7 @@ class App(tk.Tk): self.after(100, self._check_simulation_queue) def stop_simulation(self): + self.log.info("Stopping simulation...") self.simulation_manager.stop() self.generate_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) @@ -318,6 +366,7 @@ class App(tk.Tk): try: frame_data = self.simulation_manager.data_queue.get_nowait() if frame_data is None: + self.log.info("Simulation thread finished.") self.stop_simulation() return radar_cfg = self.get_radar_config_from_gui() @@ -350,24 +399,9 @@ class App(tk.Tk): self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) def get_radar_config_from_gui(self) -> RadarConfig: - antenna_cfg = AntennaConfig( - beamwidth_az_deg=self.vars["beamwidth_az_deg"].get(), - beamwidth_el_deg=self.vars["beamwidth_el_deg"].get() - ) - scan_cfg = ScanConfig( - mode=self.vars["scan_mode"].get(), - min_az_deg=self.vars["min_az_deg"].get(), - max_az_deg=self.vars["max_az_deg"].get(), - scan_speed_deg_s=self.vars["scan_speed_deg_s"].get() - ) - return RadarConfig( - carrier_frequency=self.vars["carrier_frequency"].get(), - prf=self.vars["prf"].get(), - duty_cycle=self.vars["duty_cycle"].get(), - sample_rate=self.vars["sample_rate"].get(), - antenna_config=antenna_cfg, - scan_config=scan_cfg - ) + antenna_cfg = AntennaConfig(beamwidth_az_deg=self.vars["beamwidth_az_deg"].get(), beamwidth_el_deg=self.vars["beamwidth_el_deg"].get()) + scan_cfg = ScanConfig(mode=self.vars["scan_mode"].get(), min_az_deg=self.vars["min_az_deg"].get(), max_az_deg=self.vars["max_az_deg"].get(), scan_speed_deg_s=self.vars["scan_speed_deg_s"].get()) + return RadarConfig(carrier_frequency=self.vars["carrier_frequency"].get(), prf=self.vars["prf"].get(), duty_cycle=self.vars["duty_cycle"].get(), sample_rate=self.vars["sample_rate"].get(), antenna_config=antenna_cfg, scan_config=scan_cfg) def get_targets_from_gui(self) -> list[Target]: targets = [] @@ -380,6 +414,7 @@ class App(tk.Tk): rcs = float_values[6] targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs)) except (ValueError, IndexError) as e: + self.log.error(f"Skipping invalid target data: {values}. Error: {e}") messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}", parent=self) return targets @@ -389,7 +424,7 @@ class App(tk.Tk): max_vel = radar_math.calculate_max_unambiguous_velocity(radar_cfg.carrier_frequency, radar_cfg.prf) report.append(f"--- Radar Configuration Analysis ---") report.append(f"Max Unambiguous Range: {max_range:,.0f} m") - report.append(f"Max Unambiguous Velocity: \u00b1{max_vel:,.1f} m/s") + report.append(f"Max Unambiguous Velocity: b1{max_vel:,.1f} m/s") report.append(f"Current Antenna Azimuth: {current_az_deg:,.1f} deg") if radar_cfg.scan_config.mode != 'staring' and radar_cfg.scan_config.scan_speed_deg_s > 0: dwell_time = radar_math.calculate_dwell_time(radar_cfg.antenna_config.beamwidth_az_deg, radar_cfg.scan_config.scan_speed_deg_s) @@ -420,87 +455,74 @@ class App(tk.Tk): scan_speed = self.vars["scan_speed_deg_s"].get() pri = 1.0 / prf pulse_width = pri * (duty_cycle / 100.0) - listening_time = pri - self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:,.2f} µs") - self.vars["listening_time_text"].set(f"Listening Window: {listening_time * 1e6:,.2f} µs (Max Range Time)") + self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:, .2f} µs") + self.vars["listening_time_text"].set(f"Listening Window: {pri * 1e6:, .2f} µs (Max Range Time)") max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) - self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:,.0f} m") - self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: \u00b1{max_vel:,.1f} m/s") + self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:, .0f} m") + self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: b1{max_vel:, .1f} m/s") if scan_mode == 'staring': self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)") self.vars["pulses_on_target_text"].set("Pulses on Target: N/A (Staring)") else: dwell_time = radar_math.calculate_dwell_time(beamwidth, scan_speed) pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, prf) - self.vars["dwell_time_text"].set(f"Dwell Time: {dwell_time * 1e3:,.2f} ms") + self.vars["dwell_time_text"].set(f"Dwell Time: {dwell_time * 1e3:, .2f} ms") self.vars["pulses_on_target_text"].set(f"Pulses on Target: {pulses_on_target:,}") self.check_target_warnings() self._update_scan_info_text() - except (tk.TclError, ValueError, ZeroDivisionError): - pass + except (tk.TclError, ValueError, ZeroDivisionError): pass def _update_scan_info_text(self): mode = self.vars['scan_mode'].get() - if mode == 'staring': - self.vars['scan_info_text'].set("Mode: Staring") + if mode == 'staring': self.vars['scan_info_text'].set("Mode: Staring") else: - min_az = self.vars['min_az_deg'].get() - max_az = self.vars['max_az_deg'].get() - speed = self.vars['scan_speed_deg_s'].get() + min_az, max_az, speed = self.vars['min_az_deg'].get(), self.vars['max_az_deg'].get(), self.vars['scan_speed_deg_s'].get() self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}°, {max_az}°] @ {speed}°/s") def check_target_warnings(self): - if not hasattr(self, 'target_table') or not hasattr(self, 'max_range_label'): - return + if not hasattr(self, 'target_table'): return try: - prf = self.vars["prf"].get() - carrier_freq = self.vars["carrier_frequency"].get() + prf, carrier_freq = self.vars["prf"].get(), self.vars["carrier_frequency"].get() max_range = radar_math.calculate_max_unambiguous_range(prf) max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf) range_warning, vel_warning = False, False for item in self.target_table.get_children(): values = [float(v) for v in self.target_table.item(item)['values']] target_initial_range = np.linalg.norm(values[0:3]) - target_radial_vel = np.dot(values[3:6], values[0:3] / target_initial_range) if target_initial_range > 0 else 0 + if target_initial_range > 0: target_radial_vel = np.dot(values[3:6], values[0:3] / target_initial_range) + else: target_radial_vel = 0 if target_initial_range > max_range: range_warning = True if abs(target_radial_vel) > max_vel: vel_warning = True self.max_range_label.config(foreground='orange' if range_warning else 'black') self.max_velocity_label.config(foreground='orange' if vel_warning else 'black') - except (tk.TclError, ValueError, ZeroDivisionError): + except (tk.TclError, ValueError, ZeroDivisionError): if hasattr(self, 'max_range_label'): self.max_range_label.config(foreground='black') if hasattr(self, 'max_velocity_label'): self.max_velocity_label.config(foreground='black') def open_add_target_dialog(self): dialog = AddTargetDialog(self) result = dialog.show() - if result: - self.add_target_to_table(result) + if result: self.add_target_to_table(result) def on_target_double_click(self, event): selected_item = self.target_table.selection() if not selected_item: return item_data = self.target_table.item(selected_item[0])['values'] - target_data = { - "pos_x": float(item_data[0]), "pos_y": float(item_data[1]), "pos_z": float(item_data[2]), - "vel_x": float(item_data[3]), "vel_y": float(item_data[4]), "vel_z": float(item_data[5]), - "rcs": float(item_data[6]) - } + target_data = { "pos_x": float(item_data[0]), "pos_y": float(item_data[1]), "pos_z": float(item_data[2]), "vel_x": float(item_data[3]), "vel_y": float(item_data[4]), "vel_z": float(item_data[5]), "rcs": float(item_data[6]) } dialog = AddTargetDialog(self, target_data=target_data) result = dialog.show() if result: self.target_table.item(selected_item[0], values=[f"{v:.2f}" for v in result.values()]) self.check_target_warnings() - if self.plot_manager: - self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) + if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui()) def on_profile_select(self, event=None): profile_name = self.selected_profile.get() if profile_name in self.profiles: profile_data = self.profiles[profile_name] for key, value in profile_data.items(): - if key in self.vars: - self.vars[key].set(value) + if key in self.vars: self.vars[key].set(value) self.update_scan_mode_controls() messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' has been loaded.", parent=self) @@ -520,8 +542,7 @@ class App(tk.Tk): def delete_profile(self): profile_name = self.selected_profile.get() - if not profile_name: - messagebox.showwarning("No Profile Selected", "Please select a profile to delete.", parent=self); return + if not profile_name: messagebox.showwarning("No Profile Selected", "Please select a profile to delete.", parent=self); return if messagebox.askyesno("Delete Profile", f"Are you sure you want to delete the profile '{profile_name}'?", parent=self): if profile_name in self.profiles: del self.profiles[profile_name] @@ -536,19 +557,17 @@ class App(tk.Tk): def toggle_amplitude_controls(self): state = tk.DISABLED if self.vars["auto_scale"].get() else tk.NORMAL - self.min_db_spinbox.config(state=state) - self.max_db_spinbox.config(state=state) + if hasattr(self, 'min_db_spinbox'): self.min_db_spinbox.config(state=state) + if hasattr(self, 'max_db_spinbox'): self.max_db_spinbox.config(state=state) def update_scan_mode_controls(self, *args): mode = self.vars["scan_mode"].get() state = tk.NORMAL if mode != 'staring' else tk.DISABLED - if mode == 'staring': - self.vars["scan_speed_deg_s"].set(0.0001) - elif self.vars["scan_speed_deg_s"].get() <= 0.0001: - self.vars["scan_speed_deg_s"].set(20.0) - self.min_az_spinbox.config(state=state) - self.max_az_spinbox.config(state=state) - self.scan_speed_spinbox.config(state=state) + if mode == 'staring': self.vars["scan_speed_deg_s"].set(0.0001) + elif self.vars["scan_speed_deg_s"].get() <= 0.0001: self.vars["scan_speed_deg_s"].set(20.0) + if hasattr(self, 'min_az_spinbox'): self.min_az_spinbox.config(state=state) + if hasattr(self, 'max_az_spinbox'): self.max_az_spinbox.config(state=state) + if hasattr(self, 'scan_speed_spinbox'): self.scan_speed_spinbox.config(state=state) self.update_derived_parameters() def start_gui(): diff --git a/scenario_simulator/gui/plot_manager.py b/scenario_simulator/gui/plot_manager.py index 864bc8d..07d7837 100644 --- a/scenario_simulator/gui/plot_manager.py +++ b/scenario_simulator/gui/plot_manager.py @@ -119,6 +119,13 @@ class PlotManager: self.im_rd.set_data(range_doppler_map_db) self.im_rd.set_clim(vmin=vmin, vmax=vmax) + # Re-create colorbar to prevent state issues + if self.cbar_rd: + self.cbar_rd.remove() + self.cbar_rd = self.rd_figure.colorbar(self.im_rd, ax=self.ax_rd) + self.cbar_rd.ax.tick_params(colors='white', labelsize=8) + self.cbar_rd.set_label('Amplitude (dB)', color='white', fontsize=8) + doppler_freq_axis = np.fft.fftshift(np.fft.fftfreq(iq_data_cpi.shape[0], d=1.0/radar_cfg.prf)) velocity_axis = doppler_freq_axis * (c / radar_cfg.carrier_frequency) / 2 range_axis_samples = iq_data_cpi.shape[1] diff --git a/scenario_simulator/utils/logger.py b/scenario_simulator/utils/logger.py new file mode 100644 index 0000000..a105011 --- /dev/null +++ b/scenario_simulator/utils/logger.py @@ -0,0 +1,159 @@ +# FlightMonitor/utils/logger.py +import logging +import logging.handlers # For RotatingFileHandler +import tkinter as tk +from tkinter.scrolledtext import ScrolledText +from queue import Queue, Empty as QueueEmpty +from typing import Optional, Dict, Any + +# --- Module-level globals for the centralized logging queue system --- +_global_log_queue: Optional[Queue[logging.LogRecord]] = None +_actual_console_handler: Optional[logging.StreamHandler] = None +_actual_file_handler: Optional[logging.handlers.RotatingFileHandler] = None +_actual_tkinter_handler: Optional["TkinterTextHandler"] = None + +_log_processor_after_id: Optional[str] = None +_logging_system_active: bool = False +_tk_root_instance_for_processing: Optional[tk.Tk] = None +_base_formatter: Optional[logging.Formatter] = None + +GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS = 100 + + +class TkinterTextHandler(logging.Handler): + """ + A logging handler that directs log messages to a Tkinter Text widget. + This handler is called directly from the GUI thread's processing loop. + """ + def __init__(self, text_widget: tk.Text, level_colors: Dict[int, str]): + super().__init__() + self.text_widget = text_widget + self.level_colors = level_colors + self._configure_tags() + + def _configure_tags(self): + for level, color_value in self.level_colors.items(): + level_name = logging.getLevelName(level) + if color_value: + try: + self.text_widget.tag_config(level_name, foreground=color_value) + except tk.TclError: + pass # Widget might not be ready + + def emit(self, record: logging.LogRecord): + try: + if not self.text_widget.winfo_exists(): + return + + msg = self.format(record) + level_name = record.levelname + + self.text_widget.configure(state=tk.NORMAL) + self.text_widget.insert(tk.END, msg + "\n", (level_name,)) + self.text_widget.configure(state=tk.DISABLED) + self.text_widget.see(tk.END) + except Exception as e: + print(f"Error in TkinterTextHandler.emit: {e}", flush=True) + + +class QueuePuttingHandler(logging.Handler): + """ + A simple handler that puts any received LogRecord into a global queue. + """ + def __init__(self, handler_queue: Queue[logging.LogRecord]): + super().__init__() + self.handler_queue = handler_queue + + def emit(self, record: logging.LogRecord): + self.handler_queue.put_nowait(record) + + +def _process_global_log_queue(): + """ + GUI Thread: Periodically processes LogRecords from the _global_log_queue + and dispatches them to the actual configured handlers. + """ + global _logging_system_active, _log_processor_after_id + + if not _logging_system_active or not _tk_root_instance_for_processing or not _tk_root_instance_for_processing.winfo_exists(): + return + + try: + while _global_log_queue and not _global_log_queue.empty(): + record = _global_log_queue.get_nowait() + if _actual_console_handler: + _actual_console_handler.handle(record) + if _actual_file_handler: + _actual_file_handler.handle(record) + if _actual_tkinter_handler: + _actual_tkinter_handler.handle(record) + _global_log_queue.task_done() + except QueueEmpty: + pass + except Exception as e: + print(f"Error in log processing queue: {e}", flush=True) + finally: + if _logging_system_active: + _log_processor_after_id = _tk_root_instance_for_processing.after( + GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue + ) + +def setup_basic_logging(root_tk_instance_for_processor: tk.Tk, logging_config_dict: Optional[Dict[str, Any]] = None): + global _global_log_queue, _actual_console_handler, _actual_file_handler, _logging_system_active + global _tk_root_instance_for_processing, _log_processor_after_id, _base_formatter + + if _logging_system_active: return + + if logging_config_dict is None: logging_config_dict = {} + + log_format_str = logging_config_dict.get("format", "% (asctime)s [%(levelname)-8s] %(name)-25s : %(message)s") + log_date_format_str = logging_config_dict.get("date_format", "%Y-%m-%d %H:%M:%S") + _base_formatter = logging.Formatter(log_format_str, datefmt=log_date_format_str) + + _global_log_queue = Queue() + _tk_root_instance_for_processing = root_tk_instance_for_processor + + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) + root_logger.setLevel(logging_config_dict.get("default_root_level", logging.INFO)) + + if logging_config_dict.get("enable_console", True): + _actual_console_handler = logging.StreamHandler() + _actual_console_handler.setFormatter(_base_formatter) + _actual_console_handler.setLevel(logging.DEBUG) + + queue_putter = QueuePuttingHandler(handler_queue=_global_log_queue) + queue_putter.setLevel(logging.DEBUG) + root_logger.addHandler(queue_putter) + + _logging_system_active = True + _log_processor_after_id = _tk_root_instance_for_processing.after(GLOBAL_LOG_QUEUE_POLL_INTERVAL_MS, _process_global_log_queue) + +def add_tkinter_handler(gui_log_widget: tk.Text, logging_config_dict: Dict[str, Any]): + global _actual_tkinter_handler, _base_formatter + + if not _logging_system_active or not _base_formatter: return + + if _actual_tkinter_handler: _actual_tkinter_handler.close() + + if isinstance(gui_log_widget, (tk.Text, ScrolledText)) and gui_log_widget.winfo_exists(): + level_colors = logging_config_dict.get("colors", {}) + _actual_tkinter_handler = TkinterTextHandler(text_widget=gui_log_widget, level_colors=level_colors) + _actual_tkinter_handler.setFormatter(_base_formatter) + _actual_tkinter_handler.setLevel(logging.DEBUG) + logging.getLogger(__name__).info("Tkinter log handler added successfully.") + else: + print("ERROR: GUI log widget invalid, cannot add TkinterTextHandler.", flush=True) + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) + +def shutdown_logging_system(): + global _logging_system_active, _log_processor_after_id + if not _logging_system_active: return + _logging_system_active = False + if _log_processor_after_id and _tk_root_instance_for_processing and _tk_root_instance_for_processing.winfo_exists(): + _tk_root_instance_for_processing.after_cancel(_log_processor_after_id) + # Final flush of the queue + _process_global_log_queue() + logging.shutdown() \ No newline at end of file