cambiate le dimensioni della finestra, settato porte di debug per essere le stesse del programma, cambiato reset target verso server

This commit is contained in:
VALLONGOL 2025-11-03 13:14:55 +01:00
parent ab22246f54
commit a0652998ab
4 changed files with 205 additions and 132 deletions

View File

@ -2,7 +2,7 @@
"general": { "general": {
"scan_limit": 60, "scan_limit": 60,
"max_range": 100, "max_range": 100,
"geometry": "1599x1089+587+179", "geometry": "1599x1075+587+179",
"last_selected_scenario": "scenario_dritto", "last_selected_scenario": "scenario_dritto",
"connection": { "connection": {
"target": { "target": {

View File

@ -6,10 +6,10 @@
import re import re
# --- Version Data (Generated) --- # --- Version Data (Generated) ---
__version__ = "v.0.0.0.62-0-g00ffcb5-dirty" __version__ = "v.0.0.0.67-0-gc8c1bb0-dirty"
GIT_COMMIT_HASH = "00ffcb54a320df4b682bb77920dbb758e10fbca0" GIT_COMMIT_HASH = "c8c1bb0f372c8217a86fbdb82e4922888594d2ba"
GIT_BRANCH = "master" GIT_BRANCH = "master"
BUILD_TIMESTAMP = "2025-10-31T11:56:55.656151+00:00" BUILD_TIMESTAMP = "2025-11-03T11:17:11.985027+00:00"
IS_GIT_REPO = True IS_GIT_REPO = True
# --- Default Values (for comparison or fallback) --- # --- Default Values (for comparison or fallback) ---
@ -17,7 +17,6 @@ DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown" DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown" DEFAULT_BRANCH = "Unknown"
# --- Helper Function --- # --- Helper Function ---
def get_version_string(format_string=None): def get_version_string(format_string=None):
""" """
@ -45,39 +44,29 @@ def get_version_string(format_string=None):
replacements = {} replacements = {}
try: try:
replacements["version"] = __version__ if __version__ else DEFAULT_VERSION replacements['version'] = __version__ if __version__ else DEFAULT_VERSION
replacements["commit"] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT
replacements["commit_short"] = ( replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT
GIT_COMMIT_HASH[:7] replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH
if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown"
else DEFAULT_COMMIT replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown"
) replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown"
replacements["branch"] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else ""
replacements["timestamp"] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown"
replacements["timestamp_short"] = (
BUILD_TIMESTAMP.split("T")[0]
if BUILD_TIMESTAMP and "T" in BUILD_TIMESTAMP
else "Unknown"
)
replacements["is_git"] = "Git" if IS_GIT_REPO else "Unknown"
replacements["dirty"] = (
"-dirty" if __version__ and __version__.endswith("-dirty") else ""
)
tag = DEFAULT_VERSION tag = DEFAULT_VERSION
if __version__ and IS_GIT_REPO: if __version__ and IS_GIT_REPO:
match = re.match(r"^(v?([0-9]+(?:\.[0-9]+)*))", __version__) match = re.match(r'^(v?([0-9]+(?:\.[0-9]+)*))', __version__)
if match: if match:
tag = match.group(1) tag = match.group(1)
replacements["tag"] = tag replacements['tag'] = tag
output_string = format_string output_string = format_string
for placeholder, value in replacements.items(): for placeholder, value in replacements.items():
pattern = re.compile(r"{{\s*" + re.escape(placeholder) + r"\s*}}") pattern = re.compile(r'{{\s*' + re.escape(placeholder) + r'\s*}}')
output_string = pattern.sub(str(value), output_string) output_string = pattern.sub(str(value), output_string)
if re.search(r"{\s*\w+\s*}", output_string): if re.search(r'{\s*\w+\s*}', output_string):
pass # Or log a warning: print(f"Warning: Unreplaced placeholders found: {output_string}") pass # Or log a warning: print(f"Warning: Unreplaced placeholders found: {output_string}")
return output_string return output_string

View File

@ -37,6 +37,26 @@ from target_simulator.gui.analysis_window import AnalysisWindow
from target_simulator.core import command_builder from target_simulator.core import command_builder
from target_simulator.analysis.simulation_archive import SimulationArchive from target_simulator.analysis.simulation_archive import SimulationArchive
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from target_simulator import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
# This might happen if you run the wrapper directly from source
# without generating its _version.py first (if you use that approach for the wrapper itself)
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- End Import Version Info ---
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
GUI_QUEUE_POLL_INTERVAL_MS = 100 GUI_QUEUE_POLL_INTERVAL_MS = 100
GUI_REFRESH_RATE_MS = 40 GUI_REFRESH_RATE_MS = 40
@ -85,8 +105,8 @@ class MainView(tk.Tk):
self._slider_is_dragging = False self._slider_is_dragging = False
# --- Window and UI Setup --- # --- Window and UI Setup ---
self.title("Radar Target Simulator") self.title(f"Radar Target Simulator")
self.geometry(settings.get("geometry", "1200x1024")) self.geometry(settings.get("geometry", "1200x900"))
self.minsize(1024, 768) self.minsize(1024, 768)
self._create_menubar() self._create_menubar()
@ -525,6 +545,10 @@ class MainView(tk.Tk):
ttk.Button( ttk.Button(
btn_frame, text="Refresh List", command=self._refresh_analysis_list btn_frame, text="Refresh List", command=self._refresh_analysis_list
).pack(side=tk.LEFT) ).pack(side=tk.LEFT)
# Button to open the archive folder in the system file explorer
ttk.Button(
btn_frame, text="Open Archive Folder", command=self._open_archive_folder
).pack(side=tk.LEFT, padx=(6, 0))
ttk.Button( ttk.Button(
btn_frame, text="Analyze Selected", command=self._on_analyze_run btn_frame, text="Analyze Selected", command=self._on_analyze_run
).pack(side=tk.RIGHT) ).pack(side=tk.RIGHT)
@ -617,7 +641,7 @@ class MainView(tk.Tk):
def _update_window_title(self): def _update_window_title(self):
"""Updates the window title based on the current scenario.""" """Updates the window title based on the current scenario."""
base_title = "Radar Target Simulator" base_title = (f"Radar Target Simulator- {WRAPPER_APP_VERSION_STRING}")
if self.current_scenario_name: if self.current_scenario_name:
self.title(f"{base_title} - {self.current_scenario_name}") self.title(f"{base_title} - {self.current_scenario_name}")
else: else:
@ -830,119 +854,116 @@ class MainView(tk.Tk):
True if the reset commands were sent successfully, False otherwise. True if the reset commands were sent successfully, False otherwise.
""" """
if not self.target_communicator or not self.target_communicator.is_open: if not self.target_communicator or not self.target_communicator.is_open:
self.logger.error( self.logger.error("Cannot reset radar state: communicator is not connected.")
"Cannot reset radar state: communicator is not connected." messagebox.showerror("Connection Error", "Cannot reset radar: Not Connected.")
)
messagebox.showerror(
"Connection Error", "Cannot reset radar: Not Connected."
)
return False return False
self.logger.info("Sending reset commands to deactivate all radar targets...") self.logger.info("Attempting to reset radar state (legacy vs JSON-aware logic)")
# Build the atomic reset command.
try: try:
reset_command = "tgtset /-s" use_json = bool(getattr(self.target_communicator, "_use_json_protocol", False))
except Exception:
self.logger.exception(
"Error while building atomic reset command; falling back to raw string."
)
reset_command = "tgtset /-s"
# Some radar servers require adjusting internal parameters for legacy mode.
# Use the non-$ textual commands and ensure newline termination per the
# user's request: send exactly "mex.t_rows=80\n" and "tgtset /-s\n".
prep_command = "mex.t_rows=80\n"
# If the communicator was configured to use the JSON protocol, send
# the JSON reset command expected by the server. Otherwise, fall back
# to the legacy prep + atomic reset command sequence.
try:
use_json = bool(
getattr(self.target_communicator, "_use_json_protocol", False)
)
except Exception: except Exception:
use_json = False use_json = False
if use_json: # Helper: wait until hub reports no active real targets (bounded)
# Send both the per-target zeroing payloads (if available) followed def _wait_for_clear(timeout_s: float = 3.0, poll_interval: float = 0.2) -> bool:
# by a minimal {'CMD':'reset'} line. The per-target payloads clear waited = 0.0
# active flags on individual IDs; the simple reset is a noop on while waited < timeout_s:
# servers that don't implement it but is harmless when supported.
try:
json_payloads = command_builder.build_json_reset_ids()
# Ensure all payloads are newline-terminated and then append
# the simple reset as a final step so servers that process
# the reset command can react accordingly.
final_reset = '{"CMD":"reset"}\n'
commands_to_send = [
p if p.endswith("\n") else p + "\n" for p in json_payloads
] + [final_reset]
self.logger.info(
"Using JSON Reset IDs payloads for radar reset (parts=%d + reset).",
len(json_payloads),
)
except Exception:
self.logger.exception(
"Failed to build Reset IDs JSON payloads; falling back to simple JSON reset."
)
commands_to_send = ['{"CMD":"reset"}\n']
else:
# Legacy textual reset sequence (no leading $; newline-terminated strings)
commands_to_send = [prep_command, reset_command + "\n"]
# When using JSON protocol, send each payload part individually.
if use_json:
all_ok = True
for payload in commands_to_send:
try: try:
ok = self.target_communicator.send_commands([payload]) if not self.simulation_hub.has_active_real_targets():
return True
except Exception: except Exception:
self.logger.exception("Exception while sending JSON reset payload") # If hub query fails, treat as not cleared and keep waiting
ok = False self.logger.debug("Error while querying simulation_hub during reset wait", exc_info=True)
if not ok: time.sleep(poll_interval)
all_ok = False waited += poll_interval
self.logger.error("Failed to send JSON reset payload part.") return False
break
if not all_ok: # 1) Legacy textual command path
messagebox.showerror( if not use_json:
"Reset Error", "Failed to send reset payload(s) to the radar." cmd = "tgtset /-s\n"
) self.logger.info("Sending legacy reset command: %s", cmd.strip())
return False try:
else: ok = self.target_communicator.send_commands([cmd])
if not self.target_communicator.send_commands(commands_to_send): except Exception:
self.logger.error( self.logger.exception("Failed to send legacy reset command")
"Failed to send preparatory/reset commands to the radar." ok = False
)
messagebox.showerror( if not ok:
"Reset Error", "Failed to send reset command to the radar." messagebox.showerror("Reset Error", "Failed to send reset command to the radar.")
)
return False return False
self.logger.info( # Wait briefly for the server to apply the reset and for hub to reflect it
"Successfully sent preparatory and atomic reset commands: %s", cleared = _wait_for_clear()
commands_to_send, if cleared:
) self.logger.info("Legacy reset acknowledged: no active real targets remain.")
# Poll the simulation hub to confirm the server processed the reset.
# The success condition is that there are no more active REAL targets being reported.
timeout_s = 3.0
poll_interval = 0.2
waited = 0.0
while waited < timeout_s:
# MODIFICATION: Use the new, correct check.
if not self.simulation_hub.has_active_real_targets():
self.logger.info("Radar reported zero active real targets after reset.")
return True return True
time.sleep(poll_interval) else:
waited += poll_interval 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
# If we reach here, the hub still reports active real targets — treat as failure # 2) JSON-capable communicator path
self.logger.error( # First attempt: send a single simple JSON reset command
"Radar did not clear real targets after reset within timeout." try:
) json_reset = '{"CMD":"reset"}\n'
messagebox.showerror("Reset Error", "Radar did not clear targets after reset.") self.logger.info("Sending JSON reset command: %s", json_reset.strip())
return False 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): def _on_start_simulation(self):
if self.is_simulation_running.get(): if self.is_simulation_running.get():
@ -1997,6 +2018,41 @@ class MainView(tk.Tk):
iid=run["filepath"], iid=run["filepath"],
) )
def _open_archive_folder(self):
"""Open the simulation archive folder in the system file explorer."""
archive_folder = SimulationArchive.ARCHIVE_FOLDER
try:
# Ensure the folder exists
os.makedirs(archive_folder, exist_ok=True)
# On Windows, os.startfile opens the folder in Explorer
try:
os.startfile(archive_folder)
return
except Exception:
# Fallback to using explorer.exe via subprocess
import subprocess
try:
subprocess.run(["explorer", os.path.abspath(archive_folder)])
return
except Exception:
pass
except Exception as e:
self.logger.exception("Failed to open archive folder: %s", e)
# If we get here, show an error to the user
try:
messagebox.showerror(
"Error",
f"Could not open archive folder: {archive_folder}\nSee logs for details.",
)
except Exception:
# If even showing a messagebox fails, log and continue
try:
self.logger.error("Could not open archive folder: %s", archive_folder)
except Exception:
pass
def _on_analyze_run(self): def _on_analyze_run(self):
selected_item = self.analysis_tree.focus() selected_item = self.analysis_tree.focus()
if not selected_item: if not selected_item:

View File

@ -124,6 +124,34 @@ class SfpDebugWindow(tk.Toplevel):
self._create_widgets() self._create_widgets()
# Initialize connection fields from the central connection settings
try:
cfg = None
# Prefer the master's runtime connection_config if available
if hasattr(self.master, "connection_config") and self.master.connection_config:
cfg = self.master.connection_config.get("target", {})
else:
# Fallback to ConfigManager if present on master
gm = getattr(self.master, "config_manager", None)
if gm:
cfg = gm.get_connection_settings().get("target", {})
if cfg is not None:
sfp_cfg = cfg.get("sfp", {})
if sfp_cfg:
ip = sfp_cfg.get("ip") or sfp_cfg.get("host")
if ip:
self.ip_var.set(str(ip))
port = sfp_cfg.get("port")
if port is not None:
self.server_port_var.set(str(port))
local = sfp_cfg.get("local_port")
if local is not None:
self.local_port_var.set(str(local))
except Exception:
# Do not prevent window from opening on any config read error
self.logger.debug("Could not initialise SFP debug connection fields from central config", exc_info=True)
if self.shared_communicator: if self.shared_communicator:
self._update_toggle_state(self.shared_communicator.is_open) self._update_toggle_state(self.shared_communicator.is_open)
# Schedule the legacy polling loop used by tests and older UI code # Schedule the legacy polling loop used by tests and older UI code