separati gli scenari dalla configurazione

aggiunta la sezione per lam creazione dei file temporanei
sistemato il problema dell'invio dellle flag anche in tgtset
This commit is contained in:
VALLONGOL 2025-10-22 10:03:43 +02:00
parent e81affb5f1
commit 5c0731f0b4
7 changed files with 168 additions and 409 deletions

View File

@ -14,3 +14,37 @@ A brief description of target_simulator.
## License ## License
... ...
## Note: debug GUI vs simulation command behaviour
The project provides two different code paths for sending target update commands:
- Debug GUI (SFP Packet Inspector / Simple Target Sender):
- When you press "Send Target" from the debug window, the GUI sends a
`tgtset` command that includes state qualifiers such as `/s` (active),
`/t` (traceable) and `/r` (restart) if the corresponding checkboxes are set.
- This behaviour is intentional: the debug UI is meant for manual testing and
for explicitly toggling target flags.
- Runtime Simulation:
- The simulation engine and continuous updates use `tgtset` commands that
contain only the positional/kinematic parameters (range, azimuth, velocity,
heading, altitude) and do not include the state qualifiers. This avoids
unintentionally changing target lifecycle flags during normal simulation.
Why this matters
- Historically the code used `tgtinit` in some debug flows. `tgtinit` sets
some different internal parameters (for example `heading_start`) and can
change target motion in unintended ways. To preserve correct simulation
behaviour, the debug window now sends `tgtset` + qualifiers while the
simulation keeps sending `tgtset` without qualifiers.
Where to look in the code
- Debug GUI sender: `target_simulator/gui/sfp_debug_window.py` (`_on_send_target`).
- Command builders: `target_simulator/core/command_builder.py` (`build_tgtset_from_target_state`, `build_tgtset_selective`, `build_tgtinit`).
- Settings: debug file names and options are in `settings.json` (section `debug`) and `target_simulator/config.py` contains defaults.
If you want the debug UI to exactly mimic runtime behaviour, uncheck the
Active/Traceable/Restart checkboxes before sending, or use the runtime APIs to
send `tgtset` without qualifiers.

1
scenarios.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -2,8 +2,8 @@
"general": { "general": {
"scan_limit": 60, "scan_limit": 60,
"max_range": 100, "max_range": 100,
"geometry": "1599x1024+453+144", "geometry": "1599x1024+626+57",
"last_selected_scenario": "scenario_9g", "last_selected_scenario": null,
"connection": { "connection": {
"target": { "target": {
"type": "sfp", "type": "sfp",
@ -39,389 +39,10 @@
} }
} }
}, },
"scenarios": { "debug": {
"scenario1": { "enable_io_trace": true,
"name": "scenario1", "temp_folder_name": "Temp",
"targets": [ "io_trace_sent_filename": "sent_positions.csv",
{ "io_trace_received_filename": "received_positions.csv"
"target_id": 0,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 10.0,
"target_altitude_ft": 10000.0,
"target_range_nm": 20.0,
"target_azimuth_deg": 0.0
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 100.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 1670.9318999999994,
"target_heading_deg": 10.0
},
{
"maneuver_type": "Fly to Point",
"duration_s": 400.0,
"target_altitude_ft": 10000.0,
"target_range_nm": 25.0,
"target_azimuth_deg": -20.0
}
],
"use_spline": false
}
]
},
"scenario2": {
"name": "scenario2",
"targets": [
{
"target_id": 0,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 10.0,
"target_range_nm": 10.0,
"target_azimuth_deg": -3.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 0.0,
"target_heading_deg": 0.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 200.0,
"target_range_nm": 20.0,
"target_azimuth_deg": 10.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 200.0,
"target_range_nm": 30.0,
"target_azimuth_deg": -10.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 200.0,
"target_range_nm": 35.0,
"target_azimuth_deg": 10.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 200.0,
"target_range_nm": 35.0,
"target_azimuth_deg": 30.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 200.0,
"target_range_nm": 20.0,
"target_azimuth_deg": 45.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": true
},
{
"target_id": 1,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 10.0,
"target_range_nm": 10.0,
"target_azimuth_deg": 10.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 10.0,
"target_range_nm": 20.0,
"target_azimuth_deg": 20.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 30.0,
"target_range_nm": 30.0,
"target_azimuth_deg": 30.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 30.0,
"target_range_nm": 35.0,
"target_azimuth_deg": -10.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": true
},
{
"target_id": 2,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 1.0,
"target_range_nm": 28.0,
"target_azimuth_deg": 0.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 300.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Dynamic Maneuver",
"duration_s": 9.0,
"maneuver_speed_fps": 1519.0289999999995,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 9.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 100.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 1012.686,
"target_heading_deg": -90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": false
}
]
},
"scenario3": {
"name": "scenario3",
"targets": [
{
"target_id": 0,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 10.0,
"target_range_nm": 5.0,
"target_azimuth_deg": 0.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 20.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 10.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 20.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 30.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": -180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": true
}
]
},
"scenario_9g": {
"name": "scenario2",
"targets": [
{
"target_id": 2,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 1.0,
"target_range_nm": 28.0,
"target_azimuth_deg": 0.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 300.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Dynamic Maneuver",
"duration_s": 9.0,
"maneuver_speed_fps": 1519.0289999999995,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 9.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 100.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 1012.686,
"target_heading_deg": -90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": false
}
]
},
"scenario_dritto": {
"name": "scenario_dritto",
"targets": [
{
"target_id": 0,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 1.0,
"target_range_nm": 20.0,
"target_azimuth_deg": -45.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly to Point",
"duration_s": 100.0,
"target_range_nm": 20.0,
"target_azimuth_deg": 45.0,
"target_altitude_ft": 10000.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": false
}
]
}
} }
} }

View File

@ -37,7 +37,7 @@ def build_tgtinit(target: Target) -> str:
return full_command return full_command
def build_tgtset_from_target_state(target: Target) -> str: def build_tgtset_from_target_state(target: Target, include_flags: bool = False) -> str:
""" """
Builds the 'tgtset' command from a target's CURRENT dynamic state. Builds the 'tgtset' command from a target's CURRENT dynamic state.
This is used for continuous updates during simulation. This is used for continuous updates during simulation.
@ -50,10 +50,21 @@ def build_tgtset_from_target_state(target: Target) -> str:
f"{target.current_heading_deg:.2f}", f"{target.current_heading_deg:.2f}",
f"{target.current_altitude_ft:.2f}", f"{target.current_altitude_ft:.2f}",
] ]
# For live tgtset updates we only send the positional/kinematic parameters. # For live tgtset updates we normally only send the positional/kinematic
# Qualifiers such as /s or /t are part of initialization commands (tgtinit) # parameters. However, callers can request inclusion of qualifiers
# and are not included in continuous update packets. # (/s, /-s, /t, /-t, /r) by setting include_flags=True (used by debug UI).
command_parts = ["tgtset"] + [str(p) for p in params] command_parts = ["tgtset"] + [str(p) for p in params]
if include_flags:
if getattr(target, "active", False):
command_parts.append("/s")
else:
command_parts.append("/-s")
if getattr(target, "traceable", False):
command_parts.append("/t")
else:
command_parts.append("/-t")
if hasattr(target, "restart") and getattr(target, "restart", False):
command_parts.append("/r")
full_command = " ".join(command_parts) full_command = " ".join(command_parts)
logger.debug(f"Built command: {full_command!r}") logger.debug(f"Built command: {full_command!r}")
@ -79,6 +90,9 @@ def build_tgtset_selective(target_id: int, updates: Dict[str, Any]) -> str:
qualifiers.append("/s" if updates["active"] else "/-s") qualifiers.append("/s" if updates["active"] else "/-s")
if "traceable" in updates: if "traceable" in updates:
qualifiers.append("/t" if updates["traceable"] else "/-t") qualifiers.append("/t" if updates["traceable"] else "/-t")
# Support restart qualifier for selective commands when requested
if "restart" in updates and updates.get("restart"):
qualifiers.append("/r")
command_parts.extend(qualifiers) command_parts.extend(qualifiers)
full_command = " ".join(command_parts) full_command = " ".join(command_parts)

View File

@ -312,6 +312,14 @@ class SfpDebugWindow(tk.Toplevel):
controls_frame, text="Send Target", command=self._on_send_target controls_frame, text="Send Target", command=self._on_send_target
) )
send_button.pack(side=tk.LEFT, padx=(10, 0)) send_button.pack(side=tk.LEFT, padx=(10, 0))
# Small helper label to clarify that this debug button will include
# state qualifiers (/s /t /r) in the sent tgtset command.
ttk.Label(
controls_frame,
text="(debug: sends /s /t /r if set)",
foreground="#555555",
font=("Segoe UI", 8),
).pack(side=tk.LEFT, padx=(6, 0))
# --- Quick command buttons (moved here from connection frame) --- # --- Quick command buttons (moved here from connection frame) ---
quick_cmd_frame = ttk.Frame(parent) quick_cmd_frame = ttk.Frame(parent)
@ -524,7 +532,14 @@ class SfpDebugWindow(tk.Toplevel):
self.notebook.add(self.json_tab, text="JSON") self.notebook.add(self.json_tab, text="JSON")
def _on_send_target(self): def _on_send_target(self):
"""Callback to build and send a tgtinit command for a simple target.""" """Callback to build and send a debug 'tgtset' command for a simple target.
Notes:
- This debug sender uses `tgtset` with qualifiers (/s, /t, /r) when the
corresponding checkboxes are set in the UI. The runtime simulation
should continue to use `tgtset` without qualifiers for continuous
updates; qualifiers are only included here for manual debugging.
"""
# 1. Collect data from UI # 1. Collect data from UI
ip = self.ip_var.get() ip = self.ip_var.get()
@ -595,8 +610,12 @@ class SfpDebugWindow(tk.Toplevel):
temp_target.current_heading_deg = hdg_deg temp_target.current_heading_deg = hdg_deg
temp_target.current_altitude_ft = alt_ft temp_target.current_altitude_ft = alt_ft
# 3. Build the command string # 3. Build the command string: use tgtset but INCLUDE qualifiers
command_str = command_builder.build_tgtset_from_target_state(temp_target) # so that the debug window sends the flags (/s, /t, /r) while the
# simulation runtime continues to use tgtset without flags.
command_str = command_builder.build_tgtset_from_target_state(
temp_target, include_flags=True
)
# Ensure the command is trimmed, prefixed with '$' and terminated with a newline # Ensure the command is trimmed, prefixed with '$' and terminated with a newline
command_str = command_str.strip() command_str = command_str.strip()
# if not command_str.startswith("$"): # if not command_str.startswith("$"):

View File

@ -39,7 +39,7 @@ class ConfigManager:
"""Handles reading and writing application settings and scenarios from a JSON file.""" """Handles reading and writing application settings and scenarios from a JSON file."""
def __init__(self, filename: str = "settings.json"): def __init__(self, filename: str = "settings.json", scenarios_filename: str = "scenarios.json"):
""" """
Initializes the ConfigManager. Initializes the ConfigManager.
@ -55,21 +55,30 @@ class ConfigManager:
) )
self.filepath = os.path.join(application_path, filename) self.filepath = os.path.join(application_path, filename)
self.scenarios_filepath = os.path.join(application_path, scenarios_filename)
self._settings = self._load_or_initialize_settings() self._settings = self._load_or_initialize_settings()
# Load scenarios from separate file if present, otherwise keep any scenarios
# found inside settings.json (fallback).
self._scenarios = self._load_or_initialize_scenarios()
def _load_or_initialize_settings(self) -> Dict[str, Any]: def _load_or_initialize_settings(self) -> Dict[str, Any]:
"""Loads settings from the JSON file or initializes with a default structure.""" """Loads settings from the JSON file or initializes with a default structure."""
# If someone calls this at runtime and scenarios were previously loaded
# from another location, clear them so the caller can decide how to
# reload scenarios (this supports tests that swap filepath at runtime).
if hasattr(self, "_scenarios"):
self._scenarios = {}
if not os.path.exists(self.filepath): if not os.path.exists(self.filepath):
return {"general": {}, "scenarios": {}} return {"general": {}, "scenarios": {}}
try: try:
with open(self.filepath, "r", encoding="utf-8") as f: with open(self.filepath, "r", encoding="utf-8") as f:
settings = json.load(f) settings = json.load(f)
if ( if not isinstance(settings, dict) or "general" not in settings:
not isinstance(settings, dict) # If file contained only general settings (old style), wrap it
or "general" not in settings
or "scenarios" not in settings
):
return {"general": settings, "scenarios": {}} return {"general": settings, "scenarios": {}}
# Keep scenarios key if present; actual source of truth for scenarios
# will be the separate scenarios file when available.
return settings return settings
except (json.JSONDecodeError, IOError): except (json.JSONDecodeError, IOError):
return {"general": {}, "scenarios": {}} return {"general": {}, "scenarios": {}}
@ -78,10 +87,50 @@ class ConfigManager:
"""Saves the current settings to the JSON file.""" """Saves the current settings to the JSON file."""
try: try:
with open(self.filepath, "w", encoding="utf-8") as f: with open(self.filepath, "w", encoding="utf-8") as f:
json.dump(self._settings, f, indent=4, cls=EnumEncoder) # Write all settings except 'scenarios' (which is persisted
# separately). This preserves custom top-level keys while
# ensuring scenarios are stored in their dedicated file.
to_write = dict(self._settings)
if "scenarios" in to_write:
# don't duplicate scenarios in settings.json
del to_write["scenarios"]
json.dump(to_write, f, indent=4, cls=EnumEncoder)
except IOError as e: except IOError as e:
print(f"Error saving settings to {self.filepath}: {e}") print(f"Error saving settings to {self.filepath}: {e}")
def _load_or_initialize_scenarios(self) -> Dict[str, Any]:
"""Loads scenarios from the separate scenarios file if present.
Falls back to scenarios inside settings.json when scenarios.json is absent.
"""
# If scenarios file exists, load from it.
if os.path.exists(self.scenarios_filepath):
try:
with open(self.scenarios_filepath, "r", encoding="utf-8") as f:
scenarios = json.load(f)
if isinstance(scenarios, dict):
return scenarios
except (json.JSONDecodeError, IOError):
return {}
# Fallback: try to read scenarios stored inside settings.json
try:
with open(self.filepath, "r", encoding="utf-8") as f:
settings = json.load(f)
if isinstance(settings, dict) and "scenarios" in settings:
return settings.get("scenarios", {})
except Exception:
pass
return {}
def _save_scenarios(self):
"""Saves scenarios to the separate scenarios file."""
try:
with open(self.scenarios_filepath, "w", encoding="utf-8") as f:
json.dump(self._scenarios, f, indent=4, cls=EnumEncoder)
except IOError as e:
print(f"Error saving scenarios to {self.scenarios_filepath}: {e}")
def get_general_settings(self) -> Dict[str, Any]: def get_general_settings(self) -> Dict[str, Any]:
"""Returns the general settings.""" """Returns the general settings."""
return self._settings.get("general", {}) return self._settings.get("general", {})
@ -93,7 +142,7 @@ class ConfigManager:
def get_scenario_names(self) -> List[str]: def get_scenario_names(self) -> List[str]:
"""Returns a list of all scenario names.""" """Returns a list of all scenario names."""
return list(self._settings.get("scenarios", {}).keys()) return list(self._scenarios.keys())
def get_scenario(self, name: str) -> Optional[Dict[str, Any]]: def get_scenario(self, name: str) -> Optional[Dict[str, Any]]:
""" """
@ -105,7 +154,7 @@ class ConfigManager:
Returns: Returns:
A dictionary with the scenario data, or None if not found. A dictionary with the scenario data, or None if not found.
""" """
return self._settings.get("scenarios", {}).get(name) return self._scenarios.get(name)
def save_scenario(self, name: str, data: Dict[str, Any]): def save_scenario(self, name: str, data: Dict[str, Any]):
""" """
@ -115,10 +164,8 @@ class ConfigManager:
name: The name of the scenario to save. name: The name of the scenario to save.
data: The dictionary of scenario data to save. data: The dictionary of scenario data to save.
""" """
if "scenarios" not in self._settings: self._scenarios[name] = data
self._settings["scenarios"] = {} self._save_scenarios()
self._settings["scenarios"][name] = data
self._save_settings()
def delete_scenario(self, name: str): def delete_scenario(self, name: str):
""" """
@ -127,6 +174,6 @@ class ConfigManager:
Args: Args:
name: The name of the scenario to delete. name: The name of the scenario to delete.
""" """
if "scenarios" in self._settings and name in self._settings["scenarios"]: if name in self._scenarios:
del self._settings["scenarios"][name] del self._scenarios[name]
self._save_settings() self._save_scenarios()

View File

@ -68,3 +68,26 @@ def test_build_simple_commands():
assert command_builder.build_acunlatch() == "acunlatch" assert command_builder.build_acunlatch() == "acunlatch"
assert command_builder.build_tgtreset() == "tgtreset -1" assert command_builder.build_tgtreset() == "tgtreset -1"
assert command_builder.build_tgtreset(target_id=8) == "tgtreset 8" assert command_builder.build_tgtreset(target_id=8) == "tgtreset 8"
def test_build_tgtset_with_flags(sample_target):
"""Verifica che build_tgtset_from_target_state includa i qualificatori quando richiesto."""
# Prepare a sample target with restart flag
sample_target.current_range_nm = 10.0
sample_target.current_azimuth_deg = 0.0
sample_target.current_velocity_fps = 300.0
sample_target.current_heading_deg = 0.0
sample_target.current_altitude_ft = 10000.0
sample_target.active = True
sample_target.traceable = True
sample_target.restart = True
cmd_with_flags = command_builder.build_tgtset_from_target_state(
sample_target, include_flags=True
)
# Order of qualifiers should be /s /t /r
assert cmd_with_flags.endswith("/s /t /r")
# Without flags it should not contain qualifiers
cmd_no_flags = command_builder.build_tgtset_from_target_state(sample_target)
assert not cmd_no_flags.endswith("/s /t /r")