From 7c7bbe57ba82ba2aa93f163ad96a7be81e3c786b Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Fri, 7 Nov 2025 12:37:25 +0100 Subject: [PATCH] sistemata la rotazione della mappa ppi seguendo il platform azimuth fornito dallo scenario, selezionando heading-up --- target_simulator/gui/main_view.py | 13 +- target_simulator/gui/payload_router.py | 34 ++-- target_simulator/gui/ppi_display.py | 215 +++++++------------------ todo.md | 4 + 4 files changed, 92 insertions(+), 174 deletions(-) diff --git a/target_simulator/gui/main_view.py b/target_simulator/gui/main_view.py index 288fdf6..ae39bef 100644 --- a/target_simulator/gui/main_view.py +++ b/target_simulator/gui/main_view.py @@ -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) \ No newline at end of file diff --git a/target_simulator/gui/payload_router.py b/target_simulator/gui/payload_router.py index b3aa24b..f330012 100644 --- a/target_simulator/gui/payload_router.py +++ b/target_simulator/gui/payload_router.py @@ -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) \ No newline at end of file diff --git a/target_simulator/gui/ppi_display.py b/target_simulator/gui/ppi_display.py index a22b6b7..abe7f81 100644 --- a/target_simulator/gui/ppi_display.py +++ b/target_simulator/gui/ppi_display.py @@ -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 \ No newline at end of file diff --git a/todo.md b/todo.md index 2477422..b768a8a 100644 --- a/todo.md +++ b/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