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:
parent
e81affb5f1
commit
5c0731f0b4
34
README.md
34
README.md
@ -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
1
scenarios.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
393
settings.json
393
settings.json
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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("$"):
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user