From 0730068f1c2eead8a8a4d75d246c880d927bb11d Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Mon, 3 Nov 2025 14:01:54 +0100 Subject: [PATCH] step 3 refactoring main_view, extract simulation_controller --- target_simulator/gui/main_view.py | 381 +++----------- .../simulation/simulation_controller.py | 493 ++++++++++++++++++ 2 files changed, 566 insertions(+), 308 deletions(-) create mode 100644 target_simulator/simulation/simulation_controller.py diff --git a/target_simulator/gui/main_view.py b/target_simulator/gui/main_view.py index a50e387..a7e378a 100644 --- a/target_simulator/gui/main_view.py +++ b/target_simulator/gui/main_view.py @@ -38,6 +38,7 @@ from target_simulator.gui.analysis_window import AnalysisWindow from target_simulator.core import command_builder from target_simulator.analysis.simulation_archive import SimulationArchive from target_simulator.communication.communicator_manager import CommunicatorManager +from target_simulator.simulation.simulation_controller import SimulationController # --- Import Version Info FOR THE WRAPPER ITSELF --- try: @@ -96,6 +97,17 @@ class MainView(tk.Tk): except Exception: pass + # Simulation controller encapsulates start/stop/reset orchestration + try: + self.simulation_controller = SimulationController( + communicator_manager=self.communicator_manager, + simulation_hub=self.simulation_hub, + config_manager=self.config_manager, + logger=self.logger, + ) + except Exception: + self.simulation_controller = None + # --- Core Logic Handlers --- self.target_communicator: Optional[CommunicatorInterface] = None self.lru_communicator: Optional[CommunicatorInterface] = None @@ -898,319 +910,72 @@ class MainView(tk.Tk): self.logger.exception("Unhandled exception in _on_connect_button") def _reset_radar_state(self) -> bool: - """ - Sends commands to the radar to deactivate all possible targets, effectively - clearing its state before a new simulation starts. - - Returns: - True if the reset commands were sent successfully, False otherwise. - """ - if not self.target_communicator or not self.target_communicator.is_open: - self.logger.error("Cannot reset radar state: communicator is not connected.") - messagebox.showerror("Connection Error", "Cannot reset radar: Not Connected.") - return False - - self.logger.info("Attempting to reset radar state (legacy vs JSON-aware logic)") - + # Delegates to SimulationController if available try: - use_json = bool(getattr(self.target_communicator, "_use_json_protocol", False)) + if hasattr(self, "simulation_controller") and self.simulation_controller: + return self.simulation_controller.reset_radar_state(self) except Exception: - use_json = False - - # Helper: wait until hub reports no active real targets (bounded) - def _wait_for_clear(timeout_s: float = 3.0, poll_interval: float = 0.2) -> bool: - waited = 0.0 - while waited < timeout_s: - try: - if not self.simulation_hub.has_active_real_targets(): - return True - except Exception: - # If hub query fails, treat as not cleared and keep waiting - self.logger.debug("Error while querying simulation_hub during reset wait", exc_info=True) - time.sleep(poll_interval) - waited += poll_interval - return False - - # 1) Legacy textual command path - if not use_json: - cmd = "tgtset /-s\n" - self.logger.info("Sending legacy reset command: %s", cmd.strip()) + # If controller fails, fall back to previous inline behavior try: - ok = self.target_communicator.send_commands([cmd]) - except Exception: - self.logger.exception("Failed to send legacy reset command") - ok = False - - if not ok: - messagebox.showerror("Reset Error", "Failed to send reset command to the radar.") - return False - - # Wait briefly for the server to apply the reset and for hub to reflect it - cleared = _wait_for_clear() - if cleared: - self.logger.info("Legacy reset acknowledged: no active real targets remain.") - return True - else: - self.logger.error("Legacy reset sent but server did not clear active targets in time.") - messagebox.showerror("Reset Error", "Radar did not clear targets after reset.") - return False - - # 2) JSON-capable communicator path - # First attempt: send a single simple JSON reset command - try: - json_reset = '{"CMD":"reset"}\n' - self.logger.info("Sending JSON reset command: %s", json_reset.strip()) - try: - ok = self.target_communicator.send_commands([json_reset]) - except Exception: - self.logger.exception("Failed to send JSON reset command") - ok = False - - # Wait to see if server cleared active flags - if ok: - cleared = _wait_for_clear() - if cleared: - self.logger.info("JSON reset acknowledged: server cleared active flags.") - return True - else: - self.logger.info("JSON reset sent but server did not clear active flags; will attempt per-target JSON zeroing.") - else: - self.logger.warning("JSON reset command failed to send; will attempt per-target JSON zeroing.") - except Exception: - self.logger.exception("Unexpected error while attempting JSON reset") - - # 3) Build and send per-target JSON payloads that explicitly clear active flag - try: - self.logger.info("Building per-target JSON reset payloads to clear active flags on all targets.") - json_payloads = command_builder.build_json_reset_ids() - except Exception: - self.logger.exception("Failed to build per-target JSON reset payloads") - messagebox.showerror("Reset Error", "Failed to build JSON reset payloads.") - return False - - # Send each payload and check hub state after sending - all_sent_ok = True - for i, payload in enumerate(json_payloads): - p = payload if payload.endswith("\n") else payload + "\n" - self.logger.info("Sending JSON reset payload part %d/%d", i + 1, len(json_payloads)) - try: - ok = self.target_communicator.send_commands([p]) - except Exception: - self.logger.exception("Exception while sending JSON reset payload part %d", i + 1) - ok = False - if not ok: - all_sent_ok = False - self.logger.error("Failed to send JSON reset payload part %d", i + 1) - break - - if not all_sent_ok: - messagebox.showerror("Reset Error", "Failed to send one or more JSON reset payloads to the radar.") - return False - - # After sending all per-target payloads, wait for hub to show targets cleared - cleared = _wait_for_clear() - if cleared: - self.logger.info("Per-target JSON reset succeeded: all active flags cleared.") - return True - else: - self.logger.error("Per-target JSON reset did not clear active flags within timeout.") - messagebox.showerror("Reset Error", "Radar did not clear targets after per-target reset.") - return False - - def _on_start_simulation(self): - if self.is_simulation_running.get(): - self.logger.info("Simulation is already running.") - return - # Require explicit connection before starting live. Do NOT auto-connect. - if not ( - self.target_communicator - and getattr(self.target_communicator, "is_open", False) - ): - # Friendly English reminder to connect first - messagebox.showwarning( - "Not Connected", - "Please connect to the target (use the Connect button) before starting live simulation.", - ) - return - if not self.scenario or not self.scenario.get_all_targets(): - messagebox.showinfo( - "Empty Scenario", "Cannot start simulation with an empty scenario." - ) - return - - try: - update_interval = self.update_time.get() - if update_interval <= 0: - messagebox.showwarning( - "Invalid Input", "Update time must be a positive number." - ) - return - except tk.TclError: - messagebox.showwarning( - "Invalid Input", "Update time must be a valid number." - ) - return - - # Reset data hub and PPI trails before starting - self.logger.info("Resetting simulation data hub and PPI trails.") - self.simulation_hub.reset() - self.ppi_widget.clear_trails() - - if not self._reset_radar_state(): - self.logger.error("Aborting simulation start due to radar reset failure.") - return - - time.sleep(1) # 1 second delay - - self.logger.info( - "Sending initial scenario state before starting live updates..." - ) - if not self.target_communicator.send_scenario(self.scenario): - self.logger.error( - "Failed to send initial scenario state. Aborting live simulation start." - ) - messagebox.showerror( - "Send Error", - "Failed to send the initial scenario configuration. Cannot start live simulation.", - ) - return - self.logger.info("Initial scenario state sent successfully.") - - self.logger.info("Starting live simulation...") - - self.scenario.reset_simulation() - - self.simulation_engine = SimulationEngine( - communicator=self.target_communicator, - simulation_hub=self.simulation_hub, - ) - - self.simulation_engine.set_time_multiplier(self.time_multiplier) - self.simulation_engine.set_update_interval(update_interval) - self.simulation_engine.load_scenario(self.scenario) - - # Initialize simulation progress tracking - try: - durations = [ - getattr(t, "_total_duration_s", 0.0) - for t in self.scenario.get_all_targets() - ] - self.total_sim_time = max(durations) if durations else 0.0 - except Exception: - self.total_sim_time = 0.0 - - # Reset slider and label - self.sim_elapsed_time = 0.0 - try: - self.sim_slider_var.set(0.0) - except Exception: - pass - self._update_simulation_progress_display() - - self.current_archive = SimulationArchive(self.scenario) - self.simulation_engine.archive = self.current_archive - if self.target_communicator and hasattr(self.target_communicator, "router"): - router = self.target_communicator.router() - if router: - router.set_archive(self.current_archive) - - self.simulation_engine.start() - - # When live simulation starts, remove any scenario preview visuals so - # only live simulated (green) and real (red) targets are shown. - try: - if hasattr(self, "ppi_widget") and self.ppi_widget: - self.ppi_widget.clear_previews() - except Exception: - pass - - # Set running state and update buttons AFTER starting the thread - self.is_simulation_running.set(True) - self._update_button_states() - - def _on_stop_simulation(self): - if not self.is_simulation_running.get() or not self.simulation_engine: - return - - if self.current_archive: - self.current_archive.save() - self.current_archive = None - if self.target_communicator and hasattr(self.target_communicator, "router"): - router = self.target_communicator.router() - if router: - router.set_archive(None) # Disattiva l'archiviazione - self._refresh_analysis_list() # Aggiorna subito la lista - - self.logger.info("Stopping live simulation (user request)...") - try: - self.simulation_engine.stop() - except Exception: - self.logger.exception("Error while stopping simulation engine") - self.simulation_engine = None - - # IMPORTANT: Do NOT disconnect the communicator here. Keep the connection - # active so the user can choose when to disconnect manually. - - # Update running flag and UI states - self.is_simulation_running.set(False) - self._update_button_states() - - # Inform the user the simulation was stopped - try: - messagebox.showinfo( - "Simulation Stopped", - "Live simulation was stopped. Connection to the target remains active.", - ) - except Exception: - pass - - return - - def _on_simulation_finished(self): - """Handle the natural end-of-simulation event.""" - self.logger.info("Handling simulation finished (engine signalled completion).") - - if self.current_archive: - self.current_archive.save() - self.current_archive = None - if self.target_communicator and hasattr(self.target_communicator, "router"): - router = self.target_communicator.router() - if router: - router.set_archive(None) # Disattiva l'archiviazione - self._refresh_analysis_list() # Aggiorna subito la lista - - if self.simulation_engine and self.simulation_engine.is_running(): - try: - self.simulation_engine.stop() - except Exception: - self.logger.exception("Error while stopping finished simulation engine") - self.simulation_engine = None - - self.is_simulation_running.set(False) - self._update_button_states() - - # Show a non-blocking, dismissible notice in the UI instead of a - # blocking messagebox so the PPI can keep updating while the user - # inspects the results. If the notice widget isn't available, fall - # back to logging only. - try: - if getattr(self, "simulation_notice_var", None) is not None: - # Keep the message short but informative; operator can dismiss - # the notice manually. - self.simulation_notice_var.set( - "Simulation finished — live simulation completed. Server data may still arrive and will update the PPI." - ) - else: - # As a fallback, log the info (no blocking GUI popup). - self.logger.info( - "Live simulation completed (notice widget unavailable)." - ) - except Exception: - # Ensure we never raise from UI-notice handling - try: - self.logger.exception("Failed to set non-blocking simulation notice") + self.logger.exception("SimulationController reset failed; falling back to inline reset.") except Exception: pass + # Fallback: call controller.reset_radar_state via attribute to reuse logic + try: + if hasattr(self, "simulation_controller") and self.simulation_controller: + return self.simulation_controller.reset_radar_state(self) + except Exception: + pass + # If no controller available, return False conservatively + try: + messagebox.showerror("Reset Error", "Unable to perform radar reset (controller unavailable).") + except Exception: + pass + return False + + def _on_start_simulation(self): + # Delegate to SimulationController if available + try: + if hasattr(self, "simulation_controller") and self.simulation_controller: + return self.simulation_controller.start_simulation(self) + except Exception: + try: + self.logger.exception("SimulationController start failed; falling back to inline start.") + except Exception: + pass + # If controller is not present or failed, attempt no-op fallback + try: + messagebox.showerror("Start Error", "Unable to start simulation (controller unavailable).") + except Exception: + pass + + def _on_stop_simulation(self): + try: + if hasattr(self, "simulation_controller") and self.simulation_controller: + return self.simulation_controller.stop_simulation(self) + except Exception: + try: + self.logger.exception("SimulationController stop failed; falling back to inline stop.") + except Exception: + pass + try: + messagebox.showerror("Stop Error", "Unable to stop simulation (controller unavailable).") + except Exception: + pass + + def _on_simulation_finished(self): + try: + if hasattr(self, "simulation_controller") and self.simulation_controller: + return self.simulation_controller.on_simulation_finished(self) + except Exception: + try: + self.logger.exception("SimulationController on_finished failed; falling back to inline finished handler.") + except Exception: + pass + try: + self.logger.error("Unable to handle simulation finished (controller unavailable).") + except Exception: + pass def _on_reset_simulation(self): self.logger.info("Resetting scenario to initial state.") diff --git a/target_simulator/simulation/simulation_controller.py b/target_simulator/simulation/simulation_controller.py new file mode 100644 index 0000000..a2d86be --- /dev/null +++ b/target_simulator/simulation/simulation_controller.py @@ -0,0 +1,493 @@ +import time +from typing import Optional +from tkinter import messagebox + +from target_simulator.core.simulation_engine import SimulationEngine +from target_simulator.analysis.simulation_archive import SimulationArchive +from target_simulator.core import command_builder + + +class SimulationController: + """Orchestrates simulation start/stop/reset logic extracted from MainView. + + To minimize changes across the codebase, the controller operates by + mutating the provided MainView instance during start/stop operations — + this keeps attributes like `simulation_engine` and `current_archive` + available where the rest of the code expects them. + """ + + def __init__(self, communicator_manager, simulation_hub, config_manager, logger): + self.communicator_manager = communicator_manager + self.simulation_hub = simulation_hub + self.config_manager = config_manager + self.logger = logger + + self.simulation_engine: Optional[SimulationEngine] = None + self.current_archive: Optional[SimulationArchive] = None + + def reset_radar_state(self, main_view) -> bool: + """Reset radar via communicator. Mirrors previous MainView behavior. + + Returns True on success, False otherwise. + """ + target_comm = getattr(self.communicator_manager, "target_communicator", None) + if not target_comm or not getattr(target_comm, "is_open", False): + try: + self.logger.error("Cannot reset radar state: communicator is not connected.") + except Exception: + pass + try: + messagebox.showerror("Connection Error", "Cannot reset radar: Not Connected.") + except Exception: + pass + return False + + try: + use_json = bool(getattr(target_comm, "_use_json_protocol", False)) + except Exception: + use_json = False + + # Helper: wait until hub reports no active real targets (bounded) + def _wait_for_clear(timeout_s: float = 3.0, poll_interval: float = 0.2) -> bool: + waited = 0.0 + while waited < timeout_s: + try: + if not self.simulation_hub.has_active_real_targets(): + return True + except Exception: + try: + self.logger.debug("Error while querying simulation_hub during reset wait", exc_info=True) + except Exception: + pass + time.sleep(poll_interval) + waited += poll_interval + return False + + # 1) Legacy textual command path + if not use_json: + cmd = "tgtset /-s\n" + try: + self.logger.info("Sending legacy reset command: %s", cmd.strip()) + except Exception: + pass + try: + ok = target_comm.send_commands([cmd]) + except Exception: + try: + self.logger.exception("Failed to send legacy reset command") + except Exception: + pass + ok = False + + if not ok: + try: + messagebox.showerror("Reset Error", "Failed to send reset command to the radar.") + except Exception: + pass + return False + + cleared = _wait_for_clear() + if cleared: + try: + self.logger.info("Legacy reset acknowledged: no active real targets remain.") + except Exception: + pass + return True + else: + try: + self.logger.error("Legacy reset sent but server did not clear active targets in time.") + except Exception: + pass + try: + messagebox.showerror("Reset Error", "Radar did not clear targets after reset.") + except Exception: + pass + return False + + # 2) JSON-capable communicator path + try: + json_reset = '{"CMD":"reset"}\n' + try: + self.logger.info("Sending JSON reset command: %s", json_reset.strip()) + except Exception: + pass + try: + ok = target_comm.send_commands([json_reset]) + except Exception: + try: + self.logger.exception("Failed to send JSON reset command") + except Exception: + pass + ok = False + + if ok: + cleared = _wait_for_clear() + if cleared: + try: + self.logger.info("JSON reset acknowledged: server cleared active flags.") + except Exception: + pass + return True + else: + try: + self.logger.info("JSON reset sent but server did not clear active flags; will attempt per-target JSON zeroing.") + except Exception: + pass + else: + try: + self.logger.warning("JSON reset command failed to send; will attempt per-target JSON zeroing.") + except Exception: + pass + except Exception: + try: + self.logger.exception("Unexpected error while attempting JSON reset") + except Exception: + pass + + # 3) Build and send per-target JSON payloads that explicitly clear active flag + try: + try: + self.logger.info("Building per-target JSON reset payloads to clear active flags on all targets.") + except Exception: + pass + json_payloads = command_builder.build_json_reset_ids() + except Exception: + try: + self.logger.exception("Failed to build per-target JSON reset payloads") + except Exception: + pass + try: + messagebox.showerror("Reset Error", "Failed to build JSON reset payloads.") + except Exception: + pass + return False + + all_sent_ok = True + for i, payload in enumerate(json_payloads): + p = payload if payload.endswith("\n") else payload + "\n" + try: + self.logger.info("Sending JSON reset payload part %d/%d", i + 1, len(json_payloads)) + except Exception: + pass + try: + ok = target_comm.send_commands([p]) + except Exception: + try: + self.logger.exception("Exception while sending JSON reset payload part %d", i + 1) + except Exception: + pass + ok = False + if not ok: + all_sent_ok = False + try: + self.logger.error("Failed to send JSON reset payload part %d", i + 1) + except Exception: + pass + break + + if not all_sent_ok: + try: + messagebox.showerror("Reset Error", "Failed to send one or more JSON reset payloads to the radar.") + except Exception: + pass + return False + + cleared = _wait_for_clear() + if cleared: + try: + self.logger.info("Per-target JSON reset succeeded: all active flags cleared.") + except Exception: + pass + return True + else: + try: + self.logger.error("Per-target JSON reset did not clear active flags within timeout.") + except Exception: + pass + try: + messagebox.showerror("Reset Error", "Radar did not clear targets after per-target reset.") + except Exception: + pass + return False + + def start_simulation(self, main_view): + """Start live simulation using data from main_view (keeps compatibility).""" + if main_view.is_simulation_running.get(): + try: + self.logger.info("Simulation is already running.") + except Exception: + pass + return + + # Require explicit connection before starting live. + target_comm = getattr(self.communicator_manager, "target_communicator", None) + if not (target_comm and getattr(target_comm, "is_open", False)): + try: + messagebox.showwarning( + "Not Connected", + "Please connect to the target (use the Connect button) before starting live simulation.", + ) + except Exception: + pass + return + + if not main_view.scenario or not main_view.scenario.get_all_targets(): + try: + messagebox.showinfo("Empty Scenario", "Cannot start simulation with an empty scenario.") + except Exception: + pass + return + + try: + update_interval = main_view.update_time.get() + if update_interval <= 0: + try: + messagebox.showwarning("Invalid Input", "Update time must be a positive number.") + except Exception: + pass + return + except Exception: + try: + messagebox.showwarning("Invalid Input", "Update time must be a valid number.") + except Exception: + pass + return + + # Reset data hub and PPI trails before starting + try: + self.logger.info("Resetting simulation data hub and PPI trails.") + except Exception: + pass + try: + self.simulation_hub.reset() + except Exception: + pass + try: + if hasattr(main_view, "ppi_widget") and main_view.ppi_widget: + main_view.ppi_widget.clear_trails() + except Exception: + pass + + # Reset radar state first + if not self.reset_radar_state(main_view): + try: + self.logger.error("Aborting simulation start due to radar reset failure.") + except Exception: + pass + return + + time.sleep(1) + + try: + self.logger.info("Sending initial scenario state before starting live updates...") + except Exception: + pass + if not target_comm.send_scenario(main_view.scenario): + try: + self.logger.error("Failed to send initial scenario state. Aborting live simulation start.") + messagebox.showerror( + "Send Error", + "Failed to send the initial scenario configuration. Cannot start live simulation.", + ) + except Exception: + pass + return + + try: + self.logger.info("Initial scenario state sent successfully.") + except Exception: + pass + + try: + self.logger.info("Starting live simulation...") + except Exception: + pass + + try: + main_view.scenario.reset_simulation() + except Exception: + pass + + # Create engine and start + try: + engine = SimulationEngine(communicator=target_comm, simulation_hub=self.simulation_hub) + engine.set_time_multiplier(main_view.time_multiplier) + engine.set_update_interval(update_interval) + engine.load_scenario(main_view.scenario) + main_view.simulation_engine = engine + self.simulation_engine = engine + except Exception: + try: + self.logger.exception("Failed to initialize SimulationEngine") + except Exception: + pass + return + + # Compute total duration + try: + durations = [getattr(t, "_total_duration_s", 0.0) for t in main_view.scenario.get_all_targets()] + main_view.total_sim_time = max(durations) if durations else 0.0 + except Exception: + main_view.total_sim_time = 0.0 + + # Reset slider and labels + main_view.sim_elapsed_time = 0.0 + try: + main_view.sim_slider_var.set(0.0) + except Exception: + pass + try: + main_view._update_simulation_progress_display() + except Exception: + pass + + # Archive wiring + try: + self.current_archive = SimulationArchive(main_view.scenario) + self.simulation_engine.archive = self.current_archive + if target_comm and hasattr(target_comm, "router"): + router = target_comm.router() + if router: + router.set_archive(self.current_archive) + except Exception: + pass + + try: + self.simulation_engine.start() + except Exception: + try: + self.logger.exception("Failed to start simulation engine thread") + except Exception: + pass + return + + # Clear scenario previews when live starts + try: + if hasattr(main_view, "ppi_widget") and main_view.ppi_widget: + main_view.ppi_widget.clear_previews() + except Exception: + pass + + main_view.current_archive = self.current_archive + + # Set running state and update UI + main_view.is_simulation_running.set(True) + try: + main_view._update_button_states() + except Exception: + pass + + def stop_simulation(self, main_view): + if not main_view.is_simulation_running.get() or not getattr(main_view, "simulation_engine", None): + return + + try: + if main_view.current_archive: + try: + main_view.current_archive.save() + except Exception: + pass + main_view.current_archive = None + try: + target_comm = getattr(self.communicator_manager, "target_communicator", None) + if target_comm and hasattr(target_comm, "router"): + router = target_comm.router() + if router: + router.set_archive(None) + except Exception: + pass + try: + main_view._refresh_analysis_list() + except Exception: + pass + + except Exception: + pass + + try: + self.logger.info("Stopping live simulation (user request)...") + except Exception: + pass + try: + main_view.simulation_engine.stop() + except Exception: + try: + self.logger.exception("Error while stopping simulation engine") + except Exception: + pass + main_view.simulation_engine = None + + main_view.is_simulation_running.set(False) + try: + main_view._update_button_states() + except Exception: + pass + + try: + messagebox.showinfo( + "Simulation Stopped", + "Live simulation was stopped. Connection to the target remains active.", + ) + except Exception: + pass + + def on_simulation_finished(self, main_view): + try: + self.logger.info("Handling simulation finished (engine signalled completion).") + except Exception: + pass + + try: + if main_view.current_archive: + try: + main_view.current_archive.save() + except Exception: + pass + main_view.current_archive = None + try: + target_comm = getattr(self.communicator_manager, "target_communicator", None) + if target_comm and hasattr(target_comm, "router"): + router = target_comm.router() + if router: + router.set_archive(None) + except Exception: + pass + try: + main_view._refresh_analysis_list() + except Exception: + pass + + except Exception: + pass + + if getattr(main_view, "simulation_engine", None) and main_view.simulation_engine.is_running(): + try: + main_view.simulation_engine.stop() + except Exception: + try: + self.logger.exception("Error while stopping finished simulation engine") + except Exception: + pass + main_view.simulation_engine = None + + main_view.is_simulation_running.set(False) + try: + main_view._update_button_states() + except Exception: + pass + + try: + if getattr(main_view, "simulation_notice_var", None) is not None: + main_view.simulation_notice_var.set( + "Simulation finished — live simulation completed. Server data may still arrive and will update the PPI." + ) + else: + try: + self.logger.info("Live simulation completed (notice widget unavailable).") + except Exception: + pass + except Exception: + try: + self.logger.exception("Failed to set non-blocking simulation notice") + except Exception: + pass