fix: playback state race condition, C-locale spinboxes, and Leaflet CDN loading

- workers.py: Only emit playbackStateChanged on state transitions to
  prevent stale 'playing' signal from overwriting pause button text
- dashboard.py: Force C locale on all QDoubleSpinBox instances so
  comma-decimal locales don't break numeric input; add missing
  'Saturation' legend label to AGC chart
- map_widget.py: Enable LocalContentCanAccessRemoteUrls and set HTTP
  base URL so Leaflet CDN tiles/scripts load correctly in QtWebEngine
This commit is contained in:
Jason
2026-04-14 03:09:39 +05:45
parent a12ea90cdf
commit a16472480a
3 changed files with 43 additions and 14 deletions
+20 -9
View File
@@ -37,7 +37,7 @@ from PyQt6.QtWidgets import (
QTableWidget, QTableWidgetItem, QHeaderView, QTableWidget, QTableWidgetItem, QHeaderView,
QPlainTextEdit, QStatusBar, QMessageBox, QPlainTextEdit, QStatusBar, QMessageBox,
) )
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject from PyQt6.QtCore import Qt, QLocale, QTimer, pyqtSignal, pyqtSlot, QObject
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure from matplotlib.figure import Figure
@@ -72,6 +72,17 @@ logger = logging.getLogger(__name__)
NUM_RANGE_BINS = 64 NUM_RANGE_BINS = 64
NUM_DOPPLER_BINS = 32 NUM_DOPPLER_BINS = 32
# Force C locale (period as decimal separator) for all QDoubleSpinBox instances.
_C_LOCALE = QLocale(QLocale.Language.C)
_C_LOCALE.setNumberOptions(QLocale.NumberOption.RejectGroupSeparator)
def _make_dspin() -> QDoubleSpinBox:
"""Create a QDoubleSpinBox with C locale (no comma decimals)."""
sb = QDoubleSpinBox()
sb.setLocale(_C_LOCALE)
return sb
# ============================================================================= # =============================================================================
# Range-Doppler Canvas (matplotlib) # Range-Doppler Canvas (matplotlib)
@@ -447,7 +458,7 @@ class RadarDashboard(QMainWindow):
pb_layout.addWidget(self._pb_stop_btn) pb_layout.addWidget(self._pb_stop_btn)
pb_layout.addWidget(QLabel("FPS:")) pb_layout.addWidget(QLabel("FPS:"))
self._pb_fps_spin = QDoubleSpinBox() self._pb_fps_spin = _make_dspin()
self._pb_fps_spin.setRange(0.1, 60.0) self._pb_fps_spin.setRange(0.1, 60.0)
self._pb_fps_spin.setValue(10.0) self._pb_fps_spin.setValue(10.0)
self._pb_fps_spin.setSingleStep(1.0) self._pb_fps_spin.setSingleStep(1.0)
@@ -534,25 +545,25 @@ class RadarDashboard(QMainWindow):
pos_group = QGroupBox("Radar Position") pos_group = QGroupBox("Radar Position")
pos_layout = QGridLayout(pos_group) pos_layout = QGridLayout(pos_group)
self._lat_spin = QDoubleSpinBox() self._lat_spin = _make_dspin()
self._lat_spin.setRange(-90, 90) self._lat_spin.setRange(-90, 90)
self._lat_spin.setDecimals(6) self._lat_spin.setDecimals(6)
self._lat_spin.setValue(self._radar_position.latitude) self._lat_spin.setValue(self._radar_position.latitude)
self._lat_spin.valueChanged.connect(self._on_position_changed) self._lat_spin.valueChanged.connect(self._on_position_changed)
self._lon_spin = QDoubleSpinBox() self._lon_spin = _make_dspin()
self._lon_spin.setRange(-180, 180) self._lon_spin.setRange(-180, 180)
self._lon_spin.setDecimals(6) self._lon_spin.setDecimals(6)
self._lon_spin.setValue(self._radar_position.longitude) self._lon_spin.setValue(self._radar_position.longitude)
self._lon_spin.valueChanged.connect(self._on_position_changed) self._lon_spin.valueChanged.connect(self._on_position_changed)
self._alt_spin = QDoubleSpinBox() self._alt_spin = _make_dspin()
self._alt_spin.setRange(0, 50000) self._alt_spin.setRange(0, 50000)
self._alt_spin.setDecimals(1) self._alt_spin.setDecimals(1)
self._alt_spin.setValue(0.0) self._alt_spin.setValue(0.0)
self._alt_spin.setSuffix(" m") self._alt_spin.setSuffix(" m")
self._heading_spin = QDoubleSpinBox() self._heading_spin = _make_dspin()
self._heading_spin.setRange(0, 360) self._heading_spin.setRange(0, 360)
self._heading_spin.setDecimals(1) self._heading_spin.setDecimals(1)
self._heading_spin.setValue(0.0) self._heading_spin.setValue(0.0)
@@ -575,7 +586,7 @@ class RadarDashboard(QMainWindow):
cov_group = QGroupBox("Coverage") cov_group = QGroupBox("Coverage")
cov_layout = QGridLayout(cov_group) cov_layout = QGridLayout(cov_group)
self._coverage_spin = QDoubleSpinBox() self._coverage_spin = _make_dspin()
self._coverage_spin.setRange(1, 200) self._coverage_spin.setRange(1, 200)
self._coverage_spin.setDecimals(1) self._coverage_spin.setDecimals(1)
self._coverage_spin.setValue(self._settings.coverage_radius / 1000) self._coverage_spin.setValue(self._settings.coverage_radius / 1000)
@@ -991,7 +1002,7 @@ class RadarDashboard(QMainWindow):
for spine in self._agc_ax_sat.spines.values(): for spine in self._agc_ax_sat.spines.values():
spine.set_color(DARK_BORDER) spine.set_color(DARK_BORDER)
self._agc_sat_line, = self._agc_ax_sat.plot( self._agc_sat_line, = self._agc_ax_sat.plot(
[], [], color=DARK_ERROR, linewidth=1.0) [], [], color=DARK_ERROR, linewidth=1.0, label="Saturation")
self._agc_sat_fill_artist = None self._agc_sat_fill_artist = None
self._agc_ax_sat.legend( self._agc_ax_sat.legend(
loc="upper right", fontsize=8, loc="upper right", fontsize=8,
@@ -1139,7 +1150,7 @@ class RadarDashboard(QMainWindow):
row += 1 row += 1
p_layout.addWidget(QLabel("DBSCAN eps:"), row, 0) p_layout.addWidget(QLabel("DBSCAN eps:"), row, 0)
self._cluster_eps_spin = QDoubleSpinBox() self._cluster_eps_spin = _make_dspin()
self._cluster_eps_spin.setRange(1.0, 5000.0) self._cluster_eps_spin.setRange(1.0, 5000.0)
self._cluster_eps_spin.setDecimals(1) self._cluster_eps_spin.setDecimals(1)
self._cluster_eps_spin.setValue(self._processing_config.clustering_eps) self._cluster_eps_spin.setValue(self._processing_config.clustering_eps)
+16 -3
View File
@@ -17,7 +17,8 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QFrame, QWidget, QVBoxLayout, QHBoxLayout, QFrame,
QComboBox, QCheckBox, QPushButton, QLabel, QComboBox, QCheckBox, QPushButton, QLabel,
) )
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QObject from PyQt6.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject
from PyQt6.QtWebEngineCore import QWebEngineSettings
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
@@ -517,8 +518,20 @@ document.addEventListener('DOMContentLoaded', function() {{
# ---- load / helpers ---------------------------------------------------- # ---- load / helpers ----------------------------------------------------
def _load_map(self): def _load_map(self):
self._web_view.setHtml(self._get_map_html()) # Enable remote resource access so Leaflet CDN scripts/tiles can load.
logger.info("Leaflet map HTML loaded") settings = self._web_view.page().settings()
settings.setAttribute(
QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls,
True,
)
# Provide an HTTP base URL so the page has a proper origin;
# without this, setHtml() defaults to about:blank which blocks
# external resource loading in modern Chromium.
self._web_view.setHtml(
self._get_map_html(),
QUrl("http://localhost/radar_map"),
)
logger.info("Leaflet map HTML loaded (with HTTP base URL)")
def _on_map_ready(self): def _on_map_ready(self):
self._status_label.setText(f"Map ready - {len(self._targets)} targets") self._status_label.setText(f"Map ready - {len(self._targets)} targets")
+7 -2
View File
@@ -292,6 +292,7 @@ class RawIQReplayWorker(QThread):
def run(self): def run(self):
self._running = True self._running = True
self._frame_count = 0 self._frame_count = 0
self._last_emitted_state: str | None = None
logger.info("RawIQReplayWorker started") logger.info("RawIQReplayWorker started")
info = self._controller.info info = self._controller.info
@@ -322,9 +323,13 @@ class RawIQReplayWorker(QThread):
idx = self._controller.frame_index idx = self._controller.frame_index
self.frameIndexChanged.emit(idx, total_frames) self.frameIndexChanged.emit(idx, total_frames)
# Emit playback state # Emit playback state only on transitions (avoid race
# where a stale "playing" signal overwrites a pause)
state = self._controller.state state = self._controller.state
self.playbackStateChanged.emit(state.name.lower()) state_str = state.name.lower()
if state_str != self._last_emitted_state:
self._last_emitted_state = state_str
self.playbackStateChanged.emit(state_str)
# Run host-side DSP if configured # Run host-side DSP if configured
if self._host_processor is not None: if self._host_processor is not None: