sistemata la rotazione della mappa ppi seguendo il platform azimuth fornito dallo scenario, selezionando heading-up
This commit is contained in:
parent
e72aa8314e
commit
7c7bbe57ba
@ -640,9 +640,14 @@ class MainView(tk.Tk):
|
||||
|
||||
if self.simulation_hub:
|
||||
# Update antenna sweep line
|
||||
az_deg, az_ts = self.simulation_hub.get_antenna_azimuth()
|
||||
if az_deg is not None:
|
||||
self.ppi_widget.update_antenna_azimuth(az_deg, timestamp=az_ts)
|
||||
if self.ppi_widget.animate_antenna_var.get():
|
||||
az_deg, _ = self.simulation_hub.get_antenna_azimuth()
|
||||
if az_deg is not None:
|
||||
self.ppi_widget.render_antenna_line(az_deg)
|
||||
else:
|
||||
self.ppi_widget.render_antenna_line(None) # Hide if no data
|
||||
else:
|
||||
self.ppi_widget.render_antenna_line(None) # Hide if animation is off
|
||||
|
||||
# Update ownship state for both PPI orientation and status display
|
||||
ownship_state = self.simulation_hub.get_ownship_state()
|
||||
@ -771,4 +776,4 @@ class MainView(tk.Tk):
|
||||
self.logger.debug(f"Error updating latency status: {e}")
|
||||
finally:
|
||||
# Schedule the next update
|
||||
self.after(1000, self._update_latency_status)
|
||||
self.after(1000, self._update_latency_status)
|
||||
@ -335,11 +335,18 @@ class DebugPayloadRouter:
|
||||
|
||||
# --- Propagate antenna azimuth to hub ---
|
||||
if self._hub:
|
||||
plat_az_rad = scenario_dict.get(
|
||||
"ant_nav_az", scenario_dict.get("platform_azimuth")
|
||||
)
|
||||
if plat_az_rad is not None:
|
||||
az_deg = math.degrees(float(plat_az_rad))
|
||||
# Get platform heading and relative antenna sweep
|
||||
heading_rad = scenario_dict.get("true_heading", scenario_dict.get("platform_azimuth"))
|
||||
sweep_rad = scenario_dict.get("ant_nav_az")
|
||||
|
||||
total_az_rad = None
|
||||
if heading_rad is not None:
|
||||
total_az_rad = float(heading_rad)
|
||||
if sweep_rad is not None:
|
||||
total_az_rad += float(sweep_rad)
|
||||
|
||||
if total_az_rad is not None:
|
||||
az_deg = math.degrees(total_az_rad)
|
||||
self._hub.set_antenna_azimuth(az_deg, timestamp=time.monotonic())
|
||||
|
||||
except Exception:
|
||||
@ -383,14 +390,23 @@ class DebugPayloadRouter:
|
||||
return v.get(key)
|
||||
return None
|
||||
|
||||
plat_az = _find_scenario_field(obj, "ant_nav_az") or _find_scenario_field(
|
||||
# Find platform heading and relative antenna sweep
|
||||
heading_val = _find_scenario_field(obj, "true_heading") or _find_scenario_field(
|
||||
obj, "platform_azimuth"
|
||||
)
|
||||
if plat_az is not None:
|
||||
sweep_val = _find_scenario_field(obj, "ant_nav_az")
|
||||
|
||||
total_az_val = None
|
||||
if heading_val is not None:
|
||||
total_az_val = float(heading_val)
|
||||
if sweep_val is not None:
|
||||
total_az_val += float(sweep_val)
|
||||
|
||||
if total_az_val is not None:
|
||||
try:
|
||||
# Values may be in radians or degrees; if small absolute
|
||||
# value (< 2*pi) assume radians and convert.
|
||||
val = float(plat_az)
|
||||
val = float(total_az_val)
|
||||
if abs(val) <= (2 * math.pi + 0.01):
|
||||
az_deg = math.degrees(val)
|
||||
else:
|
||||
@ -529,4 +545,4 @@ class DebugPayloadRouter:
|
||||
|
||||
def set_persist(self, enabled: bool):
|
||||
with self._lock:
|
||||
self._persist = bool(enabled)
|
||||
self._persist = bool(enabled)
|
||||
@ -16,7 +16,7 @@ import collections
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import matplotlib as mpl
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from target_simulator.core.models import Target, Waypoint, ManeuverType, NM_TO_FT
|
||||
|
||||
@ -68,7 +68,7 @@ class PPIDisplay(ttk.Frame):
|
||||
self.display_mode_var = tk.StringVar(value="North-Up")
|
||||
|
||||
self.canvas = None
|
||||
self._ownship_artist: mpl.lines.Line2D | None = None
|
||||
self._ownship_artist: Optional[mpl.lines.Line2D] = None
|
||||
self.ownship_heading_deg = 0.0
|
||||
|
||||
self._create_controls()
|
||||
@ -78,19 +78,7 @@ class PPIDisplay(ttk.Frame):
|
||||
self._last_update_summary_time = time.monotonic()
|
||||
self._update_summary_interval_s = 1.0
|
||||
|
||||
self._antenna_state = {
|
||||
"last_az_deg": None,
|
||||
"last_ts": None,
|
||||
"next_az_deg": None,
|
||||
"next_ts": None,
|
||||
"animating": False,
|
||||
"tick_ms": 33,
|
||||
# When no new antenna timestamps arrive, perform a small
|
||||
# continuous idle sweep so the UI remains visually active.
|
||||
# Degrees per second to rotate while idle (fallback).
|
||||
"idle_rotation_deg_per_s": 30.0,
|
||||
}
|
||||
self._antenna_line_artist: mpl.lines.Line2D | None = None
|
||||
self._antenna_line_artist: Optional[mpl.lines.Line2D] = None
|
||||
|
||||
def _on_display_options_changed(self, *args):
|
||||
self.clear_all_targets()
|
||||
@ -134,7 +122,7 @@ class PPIDisplay(ttk.Frame):
|
||||
radar_frame,
|
||||
text="Animate Antenna",
|
||||
variable=self.animate_antenna_var,
|
||||
command=self._on_antenna_animate_changed,
|
||||
command=self._force_redraw,
|
||||
).pack(anchor="w", pady=(4, 0))
|
||||
|
||||
# Section 2: Display Mode
|
||||
@ -209,17 +197,25 @@ class PPIDisplay(ttk.Frame):
|
||||
real_sw.grid(row=2, column=0, padx=(0, 4))
|
||||
ttk.Label(legend_frame, text="Real").grid(row=2, column=1, sticky="w")
|
||||
|
||||
def _force_redraw(self):
|
||||
if self.canvas:
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def _create_plot(self):
|
||||
fig = Figure(figsize=(5, 5), dpi=100, facecolor="#3E3E3E")
|
||||
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)
|
||||
self.ax = fig.add_subplot(111, projection="polar", facecolor="#2E2E2E")
|
||||
self.ax.set_theta_zero_location("N")
|
||||
# self.ax.set_theta_direction(-1) # Clockwise <- RIGA RIMOSSA
|
||||
# Default direction is CCW (antiorario), which is correct for our model
|
||||
# self.ax.set_theta_direction(-1) # This was the old incorrect way
|
||||
self.ax.set_rlabel_position(90)
|
||||
self.ax.set_ylim(0, self.range_var.get())
|
||||
|
||||
# Correct labels for CCW positive convention
|
||||
angles_deg = np.arange(0, 360, 30)
|
||||
labels = [f"{a}°" for a in angles_deg]
|
||||
labels = [f"{(a - 360) if a > 180 else a}°" for a in angles_deg]
|
||||
self.ax.set_thetagrids(angles_deg, labels)
|
||||
|
||||
self.ax.tick_params(axis="x", colors="white", labelsize=8)
|
||||
self.ax.tick_params(axis="y", colors="white", labelsize=8)
|
||||
self.ax.grid(color="white", linestyle="--", linewidth=0.5, alpha=0.5)
|
||||
@ -238,20 +234,26 @@ class PPIDisplay(ttk.Frame):
|
||||
)
|
||||
self._ownship_artist.set_visible(True)
|
||||
|
||||
(self._path_plot,) = self.ax.plot([], [], "g--", linewidth=1.5)
|
||||
(self._start_plot,) = self.ax.plot([], [], "go", markersize=8)
|
||||
(self._waypoints_plot,) = self.ax.plot([], [], "y+", markersize=10, mew=2)
|
||||
(self._path_plot,) = self.ax.plot([], [], "g--", linewidth=1.5, zorder=3)
|
||||
(self._start_plot,) = self.ax.plot([], [], "go", markersize=8, zorder=3)
|
||||
(self._waypoints_plot,) = self.ax.plot([], [], "y+", markersize=10, mew=2, zorder=3)
|
||||
self.preview_artists = [self._path_plot, self._start_plot, self._waypoints_plot]
|
||||
|
||||
limit_rad = np.deg2rad(self.scan_limit_deg)
|
||||
(self._scan_line_1,) = self.ax.plot(
|
||||
[limit_rad, limit_rad], [0, self.max_range], "y--", linewidth=1
|
||||
[limit_rad, limit_rad], [0, self.max_range], "y--", linewidth=1, zorder=1
|
||||
)
|
||||
(self._scan_line_2,) = self.ax.plot(
|
||||
[-limit_rad, -limit_rad], [0, self.max_range], "y--", linewidth=1
|
||||
[-limit_rad, -limit_rad], [0, self.max_range], "y--", linewidth=1, zorder=1
|
||||
)
|
||||
(self._antenna_line_artist,) = self.ax.plot(
|
||||
[], [], color="lightgray", linestyle="--", linewidth=1.2, alpha=0.85
|
||||
[],
|
||||
[],
|
||||
color="lightgray",
|
||||
linestyle="--",
|
||||
linewidth=1.2,
|
||||
alpha=0.85,
|
||||
zorder=2,
|
||||
)
|
||||
|
||||
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
||||
@ -379,6 +381,7 @@ class PPIDisplay(ttk.Frame):
|
||||
if show_points or show_trail:
|
||||
for t in new_data:
|
||||
if t.active:
|
||||
# No negation needed anymore
|
||||
pos = (np.deg2rad(t.current_azimuth_deg), t.current_range_nm)
|
||||
trail_data[t.target_id].append(pos)
|
||||
|
||||
@ -410,7 +413,7 @@ class PPIDisplay(ttk.Frame):
|
||||
r_nm = target.current_range_nm
|
||||
theta_rad_plot = np.deg2rad(target.current_azimuth_deg)
|
||||
(dot,) = self.ax.plot(
|
||||
theta_rad_plot, r_nm, "o", markersize=6, color=color, alpha=0.6
|
||||
theta_rad_plot, r_nm, "o", markersize=6, color=color, alpha=0.6, zorder=5
|
||||
)
|
||||
artist_list.append(dot)
|
||||
(x_mark,) = self.ax.plot(
|
||||
@ -421,6 +424,7 @@ class PPIDisplay(ttk.Frame):
|
||||
markersize=8,
|
||||
markeredgewidth=0.9,
|
||||
linestyle="",
|
||||
zorder=6
|
||||
)
|
||||
label_artist_list.append(x_mark)
|
||||
except Exception:
|
||||
@ -441,7 +445,7 @@ class PPIDisplay(ttk.Frame):
|
||||
theta_rad_plot = np.deg2rad(target.current_azimuth_deg)
|
||||
|
||||
(dot,) = self.ax.plot(
|
||||
theta_rad_plot, r_nm, "o", markersize=marker_size, color=color
|
||||
theta_rad_plot, r_nm, "o", markersize=marker_size, color=color, zorder=5
|
||||
)
|
||||
artist_list.append(dot)
|
||||
|
||||
@ -464,6 +468,7 @@ class PPIDisplay(ttk.Frame):
|
||||
[r_nm, r_end_nm],
|
||||
color=color,
|
||||
linewidth=1.2,
|
||||
zorder=4,
|
||||
)
|
||||
artist_list.append(line)
|
||||
|
||||
@ -475,6 +480,7 @@ class PPIDisplay(ttk.Frame):
|
||||
fontsize=8,
|
||||
ha="center",
|
||||
va="bottom",
|
||||
zorder=7,
|
||||
)
|
||||
label_artist_list.append(txt)
|
||||
|
||||
@ -483,7 +489,7 @@ class PPIDisplay(ttk.Frame):
|
||||
if len(trail) > 1:
|
||||
thetas, rs = zip(*trail)
|
||||
(line,) = self.ax.plot(
|
||||
thetas, rs, color=color, linestyle="-", linewidth=0.8, alpha=0.7
|
||||
thetas, rs, color=color, linestyle="-", linewidth=0.8, alpha=0.7, zorder=3
|
||||
)
|
||||
artist_list.append(line)
|
||||
|
||||
@ -511,29 +517,6 @@ class PPIDisplay(ttk.Frame):
|
||||
if self.canvas:
|
||||
self.canvas.draw()
|
||||
|
||||
def _on_antenna_animate_changed(self):
|
||||
st = self._antenna_state
|
||||
enabled = self.animate_antenna_var.get()
|
||||
if not enabled:
|
||||
st["animating"] = False
|
||||
if self._antenna_line_artist:
|
||||
self._antenna_line_artist.set_visible(False)
|
||||
else:
|
||||
if self._antenna_line_artist:
|
||||
self._antenna_line_artist.set_visible(True)
|
||||
last_ts, next_ts = st.get("last_ts"), st.get("next_ts")
|
||||
if (
|
||||
last_ts is not None
|
||||
and next_ts is not None
|
||||
and next_ts > last_ts
|
||||
and not st.get("animating")
|
||||
):
|
||||
st["animating"] = True
|
||||
self.after(st.get("tick_ms", 33), self._antenna_animation_step)
|
||||
|
||||
if self.canvas:
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def clear_previews(self):
|
||||
for artist in self.preview_artists:
|
||||
artist.set_data([], [])
|
||||
@ -586,117 +569,6 @@ class PPIDisplay(ttk.Frame):
|
||||
if self.canvas:
|
||||
self.canvas.draw()
|
||||
|
||||
def update_antenna_azimuth(self, az_deg: float, timestamp: float = None):
|
||||
try:
|
||||
now = time.monotonic()
|
||||
ts = float(timestamp) if timestamp is not None else now
|
||||
az = float(az_deg) % 360
|
||||
except Exception:
|
||||
return
|
||||
|
||||
st = self._antenna_state
|
||||
if not self.animate_antenna_var.get():
|
||||
st.update(
|
||||
{
|
||||
"last_az_deg": az,
|
||||
"last_ts": ts,
|
||||
"next_az_deg": az,
|
||||
"next_ts": ts,
|
||||
"animating": False,
|
||||
}
|
||||
)
|
||||
if self._antenna_line_artist:
|
||||
self._antenna_line_artist.set_visible(False)
|
||||
if self.canvas:
|
||||
self.canvas.draw_idle()
|
||||
return
|
||||
|
||||
if st["last_az_deg"] is None or st["last_ts"] is None:
|
||||
st.update(
|
||||
{"last_az_deg": az, "last_ts": ts, "next_az_deg": az, "next_ts": ts}
|
||||
)
|
||||
self._render_antenna_line(az)
|
||||
return
|
||||
|
||||
cur_az, cur_ts = st["last_az_deg"], st["last_ts"]
|
||||
next_az, next_ts = st.get("next_az_deg"), st.get("next_ts")
|
||||
|
||||
if next_az is not None and next_ts is not None and next_ts > cur_ts:
|
||||
frac = max(0.0, min(1.0, (now - cur_ts) / (next_ts - cur_ts)))
|
||||
diff = ((next_az - cur_az + 180) % 360) - 180
|
||||
cur_az = (cur_az + diff * frac) % 360
|
||||
cur_ts = now
|
||||
|
||||
st.update(
|
||||
{"last_az_deg": cur_az, "last_ts": cur_ts, "next_az_deg": az, "next_ts": ts}
|
||||
)
|
||||
|
||||
if not st["animating"]:
|
||||
st["animating"] = True
|
||||
self.after(st["tick_ms"], self._antenna_animation_step)
|
||||
|
||||
def _antenna_animation_step(self):
|
||||
st = self._antenna_state
|
||||
if not self.animate_antenna_var.get():
|
||||
st["animating"] = False
|
||||
return
|
||||
|
||||
try:
|
||||
last_ts, next_ts = st.get("last_ts"), st.get("next_ts")
|
||||
last_az, next_az = st.get("last_az_deg"), st.get("next_az_deg")
|
||||
now = time.monotonic()
|
||||
|
||||
if any(v is None for v in [last_az, next_az, last_ts, next_ts]):
|
||||
st["animating"] = False
|
||||
return
|
||||
|
||||
if next_ts <= last_ts:
|
||||
# No upcoming timestamp to interpolate towards. Instead
|
||||
# perform a small idle rotation so the antenna appears to
|
||||
# sweep even when updates are sparse or timestamps do not
|
||||
# advance. This keeps the UI lively and avoids a static
|
||||
# antenna when the data source doesn't provide frequent
|
||||
# azimuth samples.
|
||||
try:
|
||||
idle_speed = float(st.get("idle_rotation_deg_per_s", 30.0))
|
||||
# rotate since last_ts using idle speed
|
||||
dt = max(0.0, now - (last_ts or now))
|
||||
cur = (last_az + idle_speed * dt) % 360
|
||||
# update last_ts so subsequent steps continue smoothly
|
||||
st["last_ts"] = now
|
||||
st["last_az_deg"] = cur
|
||||
# keep animating until explicit stop
|
||||
st["animating"] = True
|
||||
except Exception:
|
||||
cur = next_az
|
||||
st["animating"] = False
|
||||
else:
|
||||
frac = max(0.0, min(1.0, (now - last_ts) / (next_ts - last_ts)))
|
||||
diff = ((next_az - last_az + 180) % 360) - 180
|
||||
cur = (last_az + diff * frac) % 360
|
||||
if frac >= 1.0:
|
||||
st["last_az_deg"], st["last_ts"] = next_az, next_ts
|
||||
st["animating"] = False
|
||||
|
||||
self._render_antenna_line(cur)
|
||||
except Exception:
|
||||
st["animating"] = False
|
||||
|
||||
if st["animating"]:
|
||||
self.after(st["tick_ms"], self._antenna_animation_step)
|
||||
|
||||
def _render_antenna_line(self, az_deg: float):
|
||||
try:
|
||||
theta = np.deg2rad(float(az_deg))
|
||||
max_r = self.ax.get_ylim()[1]
|
||||
if self._antenna_line_artist:
|
||||
self._antenna_line_artist.set_visible(self.animate_antenna_var.get())
|
||||
self._antenna_line_artist.set_data([theta, theta], [0, max_r])
|
||||
if self.canvas:
|
||||
self.canvas.draw_idle()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def draw_trajectory_preview(self, waypoints: List[Waypoint], use_spline: bool):
|
||||
self.clear_previews()
|
||||
self.clear_trails()
|
||||
@ -745,3 +617,24 @@ class PPIDisplay(ttk.Frame):
|
||||
if self.range_var.get() not in valid_steps:
|
||||
self.range_var.set(max_range_nm)
|
||||
self._on_range_selected()
|
||||
|
||||
def render_antenna_line(self, az_deg: Optional[float]):
|
||||
"""Directly renders the antenna line at a given absolute azimuth."""
|
||||
try:
|
||||
if self._antenna_line_artist is None:
|
||||
return
|
||||
|
||||
if az_deg is None or not self.animate_antenna_var.get():
|
||||
self._antenna_line_artist.set_visible(False)
|
||||
else:
|
||||
theta = np.deg2rad(float(az_deg))
|
||||
max_r = self.ax.get_ylim()[1]
|
||||
self._antenna_line_artist.set_data([theta, theta], [0, max_r])
|
||||
self._antenna_line_artist.set_visible(True)
|
||||
|
||||
# This method is called frequently, so we rely on the main loop's
|
||||
# final draw call to update the canvas, avoiding redundant draws.
|
||||
# self.canvas.draw_idle()
|
||||
except Exception:
|
||||
# Silently fail to prevent logging floods
|
||||
pass
|
||||
4
todo.md
4
todo.md
@ -17,6 +17,10 @@
|
||||
|
||||
- [ ] funzione di sincronizzazione: è stato aggiunto al server la possibilità di gestire dei messaggi che sono di tipo SY (tag) che sono fatti per gestire il sincronismo tra client e server. In questa nuova tipologia di messaggi io invio un mio timetag che poi il server mi restituirà subito appena lo riceve, facendo così sappiamo in quanto tempo il messaggio che spedisco è arrivato al server, viene letto, e viene risposto il mio numero con anche il timetag del server. Facendo così misurando i delta posso scroprire esattamente il tempo che intercorre tra inviare un messaggio al server e ricevere una risposta. Per come è fatto il server il tempo di applicazione dei nuovi valori per i target sarà al massimo di 1 batch, che può essere variabile, ma a quel punto lo potremmo calibrare in altro modo. Con l'analisi sui sync possiamo sapere come allineare gli orologi.
|
||||
|
||||
- [ ] Aggiungere un tasto per duplicare uno scenario da uno già presente e dargli un nome diverso
|
||||
- [ ] aggiungere una funzione automatica durante il salvataggio dello scenario che cancelli quelli più vecchi di 10 salvataggi fa, per evitare che aumentino in numero senza controllo
|
||||
|
||||
|
||||
# FIXME List
|
||||
|
||||
- [ ] sistemare la visualizzazione nella tabe simulator, per poter vedere quale scenario è stato selezionato
|
||||
|
||||
Loading…
Reference in New Issue
Block a user