-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Background
The current GUI module (host/gui.py) works—serial connection, ELF parsing, and variable read/write are all functional. But periodic read just dumps values into a table. Watching numbers scroll by isn't useful for tuning.
When tuning PID parameters, what you actually need is to see the step response curve of motor_speed—overshoot, settling time, oscillation. You can't extract any of that from a stream of numbers.
The protocol already supports periodic sampling (commands 0x11–0x18): send an address list once, and the device uploads data on its own schedule. The GUI doesn't use this capability—it's still polling with QTimer for single reads, wasting the protocol's design.
Goals
- Real-time waveform display with multi-variable overlay, zoom, pause, and export
- Leverage the protocol's periodic sampling instead of polling
- A better-looking interface with a dark theme and sensible layout
Tech Stack
| Component | Choice | Reason |
|---|---|---|
| GUI Framework | PySide6 | Already in use, no migration needed |
| Waveform Plotting | pyqtgraph | Qt OpenGL-based, handles 10ms refresh easily, integrates naturally with PySide6 |
| Theme | Catppuccin Mocha | Dark theme with high contrast, easy on the eyes during debugging sessions |
| Layout | Three-panel | Variable list on the left, toolbar on top, waveform in the center |
Tauri + React + ECharts would produce a prettier interface, but it requires a WebSocket bridge to the Python backend and pushes development time to 2–3 weeks. pyqtgraph can deliver a working prototype in one week—let's start there.
Interface Design
Layout
┌─────────────────────────────────────────────────────────────────┐
│ sparam [COM3 ▼] [115200 ▼] [Connect] ⚙ ⏹ │
├────────────┬────────────────────────────────────────────────────┤
│ │ │
│ 🔍 Search │ Waveform Panel │
│ ──────── │ │
│ ▸ motor_ │ ┌──────────────────────────────────────────┐ │
│ speed │ │ ╭─────────────────────────────────────╮ │ │
│ current │ │ │ 〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰〰 │ │ │
│ kp │ │ ╰─────────────────────────────────────╯ │ │
│ ki │ └──────────────────────────────────────────┘ │
│ ├────────────────────────────────────────────────────┤
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │speed │ │current │ │kp │ │
│ │ │1234.56 │ │56.78 │ │1.50 │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │
│ ├────────────────────────────────────────────────────┤
│ │ Log: 10:23:45 WRITE kp <= 1.5 │
│ │ 10:23:46 READ speed = 1234 │
└────────────┴────────────────────────────────────────────────────┘
Sidebar: Variable list with search and collapsible groups. Double-click to add to monitor.
Toolbar: Serial configuration and global actions.
Main area: Waveform takes visual priority. Value cards below show current readings and trends. Log panel records operation history.
Color Scheme
Catppuccin Mocha dark theme:
| Element | Hex | Usage |
|---|---|---|
| Background | #1e1e2e | Window base |
| Sidebar | #181825 | Left panel |
| Card | #313244 | Inputs, value cards |
| Border | #45475a | Dividers |
| Primary text | #cdd6f4 | Titles, values |
| Muted text | #a6adc8 | Secondary info |
| Accent | #89b4fa | Buttons, selection, first waveform |
| Success | #a6e3a1 | Connected, ACK |
| Warning | #f9e2af | NACK |
| Error | #f38ba8 | Disconnected, errors |
Waveform colors cycle through blue, green, orange, purple, cyan for multi-variable plots.
Waveform Features
- Auto-scaling Y-axis
- Scrolling X-axis window (configurable: 5s/10s/30s/∞)
- Mouse wheel zoom, drag to pan
- Click to show precise value at cursor
- Pause/resume for screenshots and analysis
- Export to PNG or CSV
- Multi-variable overlay with distinct colors
Value Cards
Each monitored variable gets a card:
┌──────────────────┐
│ motor_speed │ Variable name
│ 1234.56 │ Current value (large)
│ ▲ +2.3% │ Trend indicator
└──────────────────┘
A colored stripe on the left matches the waveform color.
Implementation
Directory Structure
host/
├── sparam/ # Backend logic (unchanged)
├── gui/ # Refactored GUI
│ ├── main.py # Entry point
│ ├── main_window.py # Main window layout
│ ├── widgets/ # Custom widgets
│ │ ├── sidebar.py # Variable list
│ │ ├── toolbar.py # Toolbar
│ │ ├── waveform_plot.py # Waveform display
│ │ ├── value_card.py # Value cards
│ │ └── log_panel.py # Log panel
│ └── styles/
│ └── catppuccin.py # Theme QSS
├── cli.py
└── pyproject.toml
Core Classes
WaveformPlot — Waveform widget wrapping pyqtgraph:
class WaveformPlot(QWidget):
def add_variable(self, name: str, color: str): ...
def remove_variable(self, name: str): ...
def update_data(self, name: str, timestamp: float, value: float): ...
def set_time_window(self, seconds: float): ...
def pause(self): ...
def resume(self): ...
def export_png(self, filepath: str): ...
def export_csv(self, filepath: str): ...DeviceManager — Handles periodic sampling and data distribution:
class DeviceManager:
def start_monitor(self, variables: List[Variable], rate: int): ...
def stop_monitor(self): ...
def add_callback(self, callback): ...Data Flow
flowchart LR
subgraph Device
A[sparam.c] -->|Periodic sampling| B[UART DMA]
end
subgraph Host
C[SerialConnection] -->|Raw bytes| D[DeviceManager]
D -->|Decode frame| E[Protocol.decode]
E -->|name, timestamp, value| F[Callback dispatch]
F --> G[WaveformPlot]
F --> H[ValueCard]
end
B -->|Serial| C
The device uploads data on its own after receiving a periodic read command. DeviceManager runs a receive thread, parses frames, and dispatches results to waveform widgets and value cards.
Key Changes
Replace polling with event-driven model
Current:
self.monitor_timer.timeout.connect(self.poll_monitored_var)
# Request every N ms, wait for responseNew:
device_manager.start_monitor(variables, rate=3) # Send once
device_manager.add_callback(self._on_data_received) # Register callback
# Device uploads continuously, callback fires passivelyRing buffer for waveform data
Each variable has a ring buffer (default 1000 points). When full, oldest data is discarded. Pause stops updates but preserves history.
Thread safety
Data reception runs in a worker thread; UI updates must happen on the main thread. Use Qt Signal for cross-thread communication:
class DeviceManager(QObject):
data_received = Signal(str, float, float)
device_manager.data_received.connect(waveform_plot.update_data)Potential Risks
- pyqtgraph may stutter at high refresh rates (>50Hz) or with many variables (>10). Mitigation: limit buffer size, reduce draw precision
- Serial packet loss can cause frame boundary misalignment. The existing frame header search recovers, but lost data cannot be retransmitted
- Large Y-axis range differences between variables (e.g., 0–100 vs 0–10000) compress waveforms. May need dual Y-axes or split views
- Windows high-DPI displays may appear blurry without explicit high-DPI scaling
- pyqtgraph depends on OpenGL; may not render correctly in VMs or remote desktop sessions
Future Extensions
- Variable grouping by module/function
- Waveform playback from CSV history
- FFT analysis for frequency-domain view
- Remote access via WebSocket + web frontend
- Multi-device simultaneous monitoring