diff --git a/target_simulator/gui/analysis_window.py b/target_simulator/gui/analysis_window.py index 2ea194e..6c93c0f 100644 --- a/target_simulator/gui/analysis_window.py +++ b/target_simulator/gui/analysis_window.py @@ -93,7 +93,10 @@ class AnalysisWindow(tk.Toplevel): self._update_stats_table(analysis_results[sel_id]) self._update_plot(sel_id) else: - self._clear_views() + # Provide diagnostic information when analysis cannot be + # produced for the selected target (common cause: no + # overlapping timestamps between simulated and real samples). + self._show_insufficient_data_info(sel_id) def _create_widgets(self): main_pane = ttk.PanedWindow(self, orient=tk.VERTICAL) @@ -305,6 +308,74 @@ class AnalysisWindow(tk.Toplevel): self.ax.autoscale_view() self.canvas.draw_idle() + def _show_insufficient_data_info(self, target_id: int): + """Display helpful information in the stats table when a target + cannot be analyzed (for example because simulated and real time + ranges do not overlap). This avoids an empty UI and gives the + user actionable context. + """ + try: + # Clear previous contents + self.stats_tree.delete(*self.stats_tree.get_children()) + + history = self._hub.get_target_history(target_id) + if history is None: + self.stats_tree.insert("", "end", values=("Info", "Target not found", "", "")) + self._clear_views() + return + + sim_times = [s[0] for s in history.get("simulated", [])] + real_times = [r[0] for r in history.get("real", [])] + + sim_count = len(sim_times) + real_count = len(real_times) + + sim_range = ( + (min(sim_times), max(sim_times)) if sim_times else (None, None) + ) + real_range = ( + (min(real_times), max(real_times)) if real_times else (None, None) + ) + + # populate the small table with human-readable diagnostic rows + self.stats_tree.insert( + "", + "end", + values=("Info", f"Target {target_id}", "", ""), + ) + self.stats_tree.insert( + "", + "end", + values=("Sim samples", str(sim_count), "", ""), + ) + self.stats_tree.insert( + "", + "end", + values=("Sim time range", f"{sim_range[0]} -> {sim_range[1]}", "", ""), + ) + self.stats_tree.insert( + "", + "end", + values=("Real samples", str(real_count), "", ""), + ) + self.stats_tree.insert( + "", + "end", + values=("Real time range", f"{real_range[0]} -> {real_range[1]}", "", ""), + ) + + # keep plot cleared + self.line_x.set_data([], []) + self.line_y.set_data([], []) + self.line_z.set_data([], []) + self.ax.relim() + self.ax.autoscale_view() + self.canvas.draw_idle() + except Exception: + # Fail silently to avoid breaking the analysis window; show + # the cleared view as a fallback. + self._clear_views() + def _on_close(self): self._active = False self.destroy() diff --git a/target_simulator/gui/ppi_display.py b/target_simulator/gui/ppi_display.py index cccafc2..a5f3f08 100644 --- a/target_simulator/gui/ppi_display.py +++ b/target_simulator/gui/ppi_display.py @@ -85,6 +85,10 @@ class PPIDisplay(ttk.Frame): "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 @@ -533,8 +537,25 @@ class PPIDisplay(ttk.Frame): return if next_ts <= last_ts: - cur = next_az - st["animating"] = False + # 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 diff --git a/tools/scan_archives.py b/tools/scan_archives.py new file mode 100644 index 0000000..6822dc0 --- /dev/null +++ b/tools/scan_archives.py @@ -0,0 +1,14 @@ +import json, os +folder = r'c:\src\____GitProjects\target_simulator\archive_simulations' +for fn in os.listdir(folder): + if not fn.endswith('.json'): + continue + p = os.path.join(folder, fn) + try: + with open(p,'r',encoding='utf-8') as f: + j = json.load(f) + except Exception: + continue + n = len(j.get('simulation_results', {})) + if n >= 3: + print(fn, n)