fix: 8 button-state bugs + wire radar position into replay for map display

State machine fixes:
1. Raw IQ replay EOF now calls _stop_radar() to fully restore UI
2. Worker thread finished signal triggers UI recovery on crash/exit
3. _stop_radar() stops demo simulator to prevent cross-mode interference
4. _stop_demo() correctly identifies Mock mode via combo text
5. Demo start no longer clobbers status bar when acquisition is running
6. _stop_radar() resets playback button text, frame counter, file label
7. _start_raw_iq_replay() error path cleans up stale controller/worker
8. _refresh_gui() preserves Raw IQ paused status instead of overwriting

Map/location:
- RawIQReplayWorker now receives _radar_position (GPSData ref) so
  targets get real lat/lon projected from the virtual radar position
- Added heading control to Map tab sidebar (0-360 deg, wrapping)
- Manual lat/lon/heading changes in Map tab apply to replay targets

Ruff clean, 120/120 tests pass.
This commit is contained in:
Jason
2026-04-14 01:49:34 +05:45
parent 2cb56e8b13
commit a12ea90cdf
2 changed files with 69 additions and 10 deletions
+58 -2
View File
@@ -176,6 +176,7 @@ class RadarDashboard(QMainWindow):
# State
self._running = False
self._demo_mode = False
self._replay_status_override: str | None = None # "playing"/"paused"
self._start_time = time.time()
self._current_frame: RadarFrame | None = None
self._last_status: StatusResponse | None = None
@@ -551,12 +552,22 @@ class RadarDashboard(QMainWindow):
self._alt_spin.setValue(0.0)
self._alt_spin.setSuffix(" m")
self._heading_spin = QDoubleSpinBox()
self._heading_spin.setRange(0, 360)
self._heading_spin.setDecimals(1)
self._heading_spin.setValue(0.0)
self._heading_spin.setSuffix("\u00b0")
self._heading_spin.setWrapping(True)
self._heading_spin.valueChanged.connect(self._on_position_changed)
pos_layout.addWidget(QLabel("Latitude:"), 0, 0)
pos_layout.addWidget(self._lat_spin, 0, 1)
pos_layout.addWidget(QLabel("Longitude:"), 1, 0)
pos_layout.addWidget(self._lon_spin, 1, 1)
pos_layout.addWidget(QLabel("Altitude:"), 2, 0)
pos_layout.addWidget(self._alt_spin, 2, 1)
pos_layout.addWidget(QLabel("Heading:"), 3, 0)
pos_layout.addWidget(self._heading_spin, 3, 1)
sb_layout.addWidget(pos_group)
@@ -1318,6 +1329,7 @@ class RadarDashboard(QMainWindow):
self._radar_worker.targetsUpdated.connect(self._on_radar_targets)
self._radar_worker.statsUpdated.connect(self._on_radar_stats)
self._radar_worker.errorOccurred.connect(self._on_worker_error)
self._radar_worker.finished.connect(self._on_worker_finished)
self._radar_worker.start()
# Optionally start GPS worker
@@ -1350,6 +1362,16 @@ class RadarDashboard(QMainWindow):
def _stop_radar(self):
self._running = False
self._replay_status_override = None
# Stop demo simulator if active (prevents cross-mode interference)
if self._simulator:
self._simulator.stop()
self._simulator = None
self._demo_mode = False
self._demo_btn_main.setText("Start Demo")
self._demo_btn_map.setText("Start Demo")
self._demo_btn_map.setChecked(False)
if self._radar_worker:
self._radar_worker.stop()
@@ -1381,6 +1403,9 @@ class RadarDashboard(QMainWindow):
self._stop_btn.setEnabled(False)
self._mode_combo.setEnabled(True)
self._playback_frame.setVisible(False)
self._pb_play_btn.setText("Play")
self._pb_frame_label.setText("Frame: 0 / 0")
self._pb_file_label.setText("")
self._status_label_main.setText("Status: Radar stopped")
self._sb_status.setText("Radar stopped")
self._sb_mode.setText("Idle")
@@ -1458,12 +1483,14 @@ class RadarDashboard(QMainWindow):
processor=self._iq_processor,
host_processor=self._processor,
settings=self._settings,
gps_data_ref=self._radar_position,
)
self._replay_worker.frameReady.connect(self._on_frame_ready)
self._replay_worker.statusReceived.connect(self._on_status_received)
self._replay_worker.targetsUpdated.connect(self._on_radar_targets)
self._replay_worker.statsUpdated.connect(self._on_radar_stats)
self._replay_worker.errorOccurred.connect(self._on_worker_error)
self._replay_worker.finished.connect(self._on_worker_finished)
self._replay_worker.playbackStateChanged.connect(
self._on_playback_state_changed)
self._replay_worker.frameIndexChanged.connect(
@@ -1474,6 +1501,7 @@ class RadarDashboard(QMainWindow):
# UI state
self._running = True
self._replay_status_override = "paused"
self._start_time = time.time()
self._frame_count = 0
self._start_btn.setEnabled(False)
@@ -1491,6 +1519,13 @@ class RadarDashboard(QMainWindow):
logger.info(f"Raw IQ Replay started: {npy_path}")
except (ValueError, OSError) as e:
# Clean up any partially-created objects
if self._replay_worker is not None:
self._replay_worker.stop()
self._replay_worker.wait(1000)
self._replay_worker = None
self._replay_controller = None
self._iq_processor = None
QMessageBox.critical(self, "Error",
f"Failed to load raw IQ file:\n{e}")
logger.error(f"Raw IQ load error: {e}")
@@ -1526,11 +1561,13 @@ class RadarDashboard(QMainWindow):
def _on_playback_state_changed(self, state_str: str):
if state_str == "playing":
self._pb_play_btn.setText("Pause")
self._replay_status_override = "playing"
elif state_str == "paused":
self._pb_play_btn.setText("Play")
self._replay_status_override = "paused"
elif state_str == "stopped":
self._pb_play_btn.setText("Play")
self._status_label_main.setText("Status: Replay finished")
self._replay_status_override = None
self._stop_radar()
@pyqtSlot(int, int)
def _on_frame_index_changed(self, current: int, total: int):
@@ -1547,6 +1584,7 @@ class RadarDashboard(QMainWindow):
self._simulator.targetsUpdated.connect(self._on_demo_targets)
self._simulator.start(500)
self._demo_mode = True
if not self._running:
self._sb_mode.setText("Demo Mode")
self._sb_status.setText("Demo mode active")
self._demo_btn_main.setText("Stop Demo")
@@ -1566,8 +1604,12 @@ class RadarDashboard(QMainWindow):
elif isinstance(self._connection, ReplayConnection):
mode = "Replay"
else:
# Use mode combo text for Mock vs Live distinction
mode = self._mode_combo.currentText()
if "Mock" not in mode and "Live" not in mode:
mode = "Live"
self._sb_mode.setText(mode)
if not self._running:
self._sb_status.setText("Demo stopped")
self._demo_btn_main.setText("Start Demo")
self._demo_btn_map.setText("Start Demo")
@@ -1620,6 +1662,12 @@ class RadarDashboard(QMainWindow):
def _on_worker_error(self, msg: str):
logger.error(f"Worker error: {msg}")
def _on_worker_finished(self):
"""Handle unexpected worker thread exit — recover UI to stopped state."""
if self._running:
logger.warning("Worker thread exited unexpectedly, resetting UI")
self._stop_radar()
@pyqtSlot(object)
def _on_gps_received(self, gps: GPSData):
self._gps_packet_count += 1
@@ -1800,6 +1848,7 @@ class RadarDashboard(QMainWindow):
self._radar_position.latitude = self._lat_spin.value()
self._radar_position.longitude = self._lon_spin.value()
self._radar_position.altitude = self._alt_spin.value()
self._radar_position.heading = self._heading_spin.value()
self._map_widget.set_radar_position(self._radar_position)
if self._simulator:
self._simulator.set_radar_position(self._radar_position)
@@ -1874,6 +1923,13 @@ class RadarDashboard(QMainWindow):
if self._running:
det = (self._current_frame.detection_count
if self._current_frame else 0)
# Preserve replay-specific status (paused/playing)
if self._replay_status_override == "paused":
self._status_label_main.setText(
f"Status: Raw IQ Replay (paused) \u2014 "
f"Frames: {self._frame_count} \u2014 Detections: {det}"
)
else:
self._status_label_main.setText(
f"Status: Running \u2014 Frames: {self._frame_count} "
f"\u2014 Detections: {det}"
+3
View File
@@ -272,6 +272,7 @@ class RawIQReplayWorker(QThread):
processor: RawIQFrameProcessor,
host_processor: RadarProcessor | None = None,
settings: RadarSettings | None = None,
gps_data_ref: GPSData | None = None,
parent=None,
):
super().__init__(parent)
@@ -279,6 +280,7 @@ class RawIQReplayWorker(QThread):
self._processor = processor
self._host_processor = host_processor
self._settings = settings or RadarSettings()
self._gps = gps_data_ref
self._running = False
self._frame_count = 0
self._error_count = 0
@@ -358,6 +360,7 @@ class RawIQReplayWorker(QThread):
frame,
self._settings.range_resolution,
self._settings.velocity_resolution,
gps=self._gps,
)
# Clustering + tracking