Skip to content

Commit 21436ba

Browse files
committed
Add new real-time exercise
Set up MVVM architecture with placeholder components for a PyQt5 and VisPy-based real-time RMS signal plotting app. Includes basic files for main logic, signal processing, plot view, main view, and updated dependencies (PyQt5, VisPy). All components are scaffolded with TODOs for future feature development.
1 parent a4332af commit 21436ba

File tree

8 files changed

+671
-1
lines changed

8 files changed

+671
-1
lines changed

exercises/04/04_exercise/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Live RMS Signal Plotting Exercise
2+
3+
This exercise implements a real-time signal plotting application using the MVVM (Model-View-ViewModel) architecture pattern. The application displays a live plot of a signal's Root Mean Square (RMS) value over time, demonstrating the use of PyQt5 for the UI and VisPy for high-performance plotting.
4+
5+
## Application Overview
6+
7+
The application provides:
8+
- A live plot showing the RMS value of a signal over time
9+
- A control button to start/stop the plotting
10+
- A fixed view range for consistent visualization
11+
- Real-time updates at 30 Hz
12+
13+
## MVVM Architecture
14+
15+
The application follows the MVVM (Model-View-ViewModel) pattern, which separates the application into three main components:
16+
17+
### Model
18+
- Handles data generation and signal processing
19+
- Provides raw signal data and RMS calculations
20+
- Already implemented for you
21+
22+
### View
23+
- Manages the user interface components
24+
- Consists of two main classes:
25+
1. `MainView`: Main window with plot and controls
26+
2. `VisPyPlotWidget`: Custom widget for high-performance plotting
27+
- Contains TODOs for UI setup and signal connections
28+
29+
### ViewModel
30+
- Acts as a mediator between Model and View
31+
- Manages the application state and business logic
32+
- Handles data updates and plotting state
33+
- Contains TODOs for signal processing and state management
34+
35+
## Implementation Tasks
36+
37+
### 1. Main Application (`main.py`)
38+
TODOs:
39+
- Import necessary classes (MainView and MainViewModel)
40+
- Create ViewModel instance
41+
- Create View instance with ViewModel
42+
- Show the main window
43+
44+
### 2. View Layer (`view/mainView.py`)
45+
TODOs:
46+
- Connect ViewModel signals to appropriate slots
47+
- Implement plotting toggle functionality
48+
49+
### 3. Plot Widget (`view/plotView.py`)
50+
TODOs:
51+
- Set up the VisPy canvas and layout
52+
- Create and configure the plot view
53+
- Implement the line plot visualization
54+
- Set up fixed view ranges
55+
- Implement data update mechanism
56+
57+
### 4. ViewModel Layer (`viewmodel/mainViewModel.py`)
58+
TODOs:
59+
- Implement the update_data method to:
60+
- Get current window of data
61+
- Handle window padding
62+
- Emit new data
63+
- Update current index
64+
- Handle end of data reset
65+
66+
## VisPy Guide
67+
68+
VisPy is a high-performance visualization library that uses OpenGL for efficient plotting. Here's a basic example of how to create a simple line plot with VisPy:
69+
70+
```python
71+
from PyQt5.QtWidgets import QWidget, QVBoxLayout
72+
from vispy import scene
73+
import numpy as np
74+
75+
class SimplePlotWidget(QWidget):
76+
def __init__(self, parent=None):
77+
super().__init__(parent)
78+
79+
# Create layout
80+
layout = QVBoxLayout()
81+
self.setLayout(layout)
82+
83+
# Create canvas
84+
self.canvas = scene.SceneCanvas(keys='interactive', size=(800, 400))
85+
layout.addWidget(self.canvas.native)
86+
87+
# Create view
88+
self.view = self.canvas.central_widget.add_view()
89+
self.view.camera = 'panzoom'
90+
91+
# Create line plot
92+
x = np.linspace(0, 10, 100)
93+
y = np.sin(x)
94+
self.line = scene.Line(np.column_stack((x, y)), parent=self.view.scene)
95+
96+
# Set view range
97+
self.view.camera.set_range(x=(0, 10), y=(-1, 1))
98+
```
99+
100+
Key VisPy concepts:
101+
1. **Canvas**: The main drawing area (SceneCanvas)
102+
2. **View**: A region of the canvas where visuals are drawn
103+
3. **Camera**: Controls how the view is displayed (panzoom for interactive navigation)
104+
4. **Visuals**: Objects that can be drawn (Line, Markers, etc.)
105+
5. **Scene**: The container for all visuals
106+
107+
Common operations:
108+
- Creating a line: `scene.Line(data, parent=view.scene)`
109+
- Updating data: `line.set_data(new_data)`
110+
- Setting view range: `view.camera.set_range(x=(min, max), y=(min, max))`
111+
- Updating canvas: `canvas.update()`
112+
113+
## Technical Details
114+
115+
### Signal Processing
116+
- Window size: 10 seconds
117+
- Sampling rate: 2048 Hz
118+
- Update rate: 30 Hz
119+
- Signal type: Sine wave with added noise
120+
121+
### Dependencies
122+
- PyQt5: For the user interface
123+
- NumPy: For numerical computations
124+
- VisPy: For high-performance OpenGL-based plotting
125+
126+
## Getting Started
127+
128+
1. Review the code structure and TODOs in each file
129+
2. Implement the missing components in the following order:
130+
- Main application setup
131+
- Plot widget setup and visualization
132+
- View signal connections
133+
- ViewModel data update logic
134+
3. Test the application by running `main.py`
135+
136+
## Tips for Implementation
137+
138+
1. **Main Application**:
139+
- Import classes from the correct modules
140+
- Create ViewModel before View
141+
- Show the window after setup
142+
143+
2. **View Layer**:
144+
- Use PyQt5's signal-slot mechanism for UI updates
145+
- Connect all necessary signals to their slots
146+
147+
3. **Plot Widget**:
148+
- Use VisPy's SceneCanvas for efficient plotting
149+
- Maintain fixed view ranges for consistent visualization
150+
- Handle data updates efficiently
151+
152+
4. **ViewModel Layer**:
153+
- Handle window padding correctly
154+
- Update the current index properly
155+
- Reset when reaching the end of data
156+
157+
## Testing
158+
159+
After implementation, verify that:
160+
- The plot updates smoothly in real-time
161+
- The start/stop button works correctly
162+
- The view range remains fixed
163+
- The application handles data updates efficiently

exercises/04/04_exercise/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from PyQt5.QtWidgets import QApplication
2+
import sys
3+
4+
# TODO: Import the MainView class from view.mainView
5+
# TODO: Import the MainViewModel class from viewmodel.mainViewModel
6+
7+
def main():
8+
app = QApplication(sys.argv)
9+
10+
# TODO: Create an instance of MainViewModel
11+
# TODO: Create an instance of MainView and pass the viewmodel
12+
# TODO: Show the main window
13+
14+
sys.exit(app.exec_())
15+
16+
if __name__ == '__main__':
17+
main()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import numpy as np
2+
3+
class SignalProcessor:
4+
"""
5+
Service class for signal processing and generation.
6+
7+
This class is part of the Model layer in the MVVM architecture.
8+
"""
9+
10+
def __init__(self, window_size=10, sampling_rate=2048):
11+
"""
12+
Initialize the signal processor.
13+
14+
Args:
15+
window_size: Size of the time window in seconds
16+
sampling_rate: Sampling rate in Hz
17+
"""
18+
self.window_size = window_size
19+
self.sampling_rate = sampling_rate
20+
self.points_per_window = window_size * sampling_rate
21+
22+
def generate_test_signal(self, duration=60):
23+
"""
24+
Generate a test signal (sine wave with noise).
25+
26+
Args:
27+
duration: Duration of the signal in seconds
28+
29+
Returns:
30+
tuple: (time_points, signal_data)
31+
"""
32+
# Generate time points
33+
time_points = np.linspace(0, duration, int(duration * self.sampling_rate))
34+
35+
# Generate sine wave (1 Hz) with noise
36+
signal = np.sin(2 * np.pi * time_points) + 0.1 * np.random.randn(len(time_points))
37+
38+
return time_points, signal
39+
40+
def get_window(self, data, start_idx, window_size):
41+
"""
42+
Get a window of data.
43+
44+
Args:
45+
data: Full signal data array
46+
start_idx: Starting index
47+
window_size: Size of the window in samples
48+
49+
Returns:
50+
numpy.ndarray: Window of data
51+
"""
52+
# TODO: Implement window extraction
53+
# - Extract window of data
54+
# - Handle end of data case
55+
pass
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QPushButton
2+
from PyQt5.QtCore import Qt
3+
from .plotView import VisPyPlotWidget
4+
5+
# TODO: Import the PlotView class from plotView
6+
7+
class MainView(QMainWindow):
8+
"""
9+
Main application window that combines the plot widget and controls.
10+
11+
This class is part of the View layer in the MVVM architecture. It:
12+
- Creates and manages the main window layout
13+
- Contains the plot widget and control buttons
14+
- Connects the ViewModel signals to the View
15+
- Handles user interactions
16+
17+
The window provides a simple interface with:
18+
- A plot widget showing the live signal
19+
- A button to start/stop the plotting
20+
"""
21+
22+
def __init__(self, view_model):
23+
"""
24+
Initialize the main window with plot widget and controls.
25+
26+
Args:
27+
view_model: The ViewModel that manages the data and plotting state
28+
"""
29+
super().__init__()
30+
self.view_model = view_model
31+
32+
# Set up the main window
33+
self.setWindowTitle("Live RMS Plot")
34+
self.setGeometry(100, 100, 800, 500)
35+
36+
# Create central widget and layout
37+
central_widget = QWidget()
38+
self.setCentralWidget(central_widget)
39+
layout = QVBoxLayout(central_widget)
40+
41+
# Create plot widget
42+
self.plot_widget = VisPyPlotWidget()
43+
layout.addWidget(self.plot_widget)
44+
45+
# Create control button
46+
self.control_button = QPushButton("Start Plotting")
47+
self.control_button.clicked.connect(self.toggle_plotting)
48+
layout.addWidget(self.control_button)
49+
50+
# TODO: Connect view model signals
51+
# In MVVM, the View needs to connect to the ViewModel's signals to receive updates
52+
# The ViewModel has a signal called 'data_updated' that emits new data
53+
# You need to connect this signal to the plot widget's update_data method
54+
# This is how the ViewModel communicates new data to the View
55+
# Hint: Use the connect() method on the signal, similar to how the button's clicked signal is connected
56+
57+
def toggle_plotting(self):
58+
"""
59+
Toggle the plotting state and update button text.
60+
"""
61+
# TODO: Implement plotting toggle
62+
# This method is called when the control button is clicked
63+
# You need to:
64+
# 1. Check the current plotting state using view_model.is_plotting
65+
# 2. If plotting is active:
66+
# - Update button text to "Start Plotting"
67+
# - Call view_model.stop_plotting() to stop the data flow
68+
# 3. If plotting is inactive:
69+
# - Update button text to "Stop Plotting"
70+
# - Call view_model.start_plotting() to start the data flow
71+
# This method demonstrates the View's role in handling user input and updating the UI
72+
pass
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from PyQt5.QtWidgets import QWidget, QVBoxLayout
2+
from vispy import app, scene
3+
import numpy as np
4+
5+
class VisPyPlotWidget(QWidget):
6+
"""
7+
A widget that displays live plotting using VisPy.
8+
9+
This class is part of the View layer in the MVVM architecture. It:
10+
- Creates and manages the VisPy canvas
11+
- Handles the visualization of the signal
12+
- Maintains a fixed view range
13+
- Updates the plot with new data
14+
15+
The widget uses VisPy for high-performance OpenGL-based plotting,
16+
which is essential for smooth real-time updates.
17+
"""
18+
19+
def __init__(self, parent=None):
20+
"""
21+
Initialize the plot widget with VisPy canvas and view.
22+
23+
Args:
24+
parent: Parent widget (optional)
25+
"""
26+
super().__init__(parent)
27+
28+
# TODO: Set up the widget
29+
# First, create a QVBoxLayout to organize the widget's contents
30+
# Then, set this layout as the widget's layout using setLayout()
31+
# This layout will hold the VisPy canvas
32+
33+
# TODO: Create VisPy canvas
34+
# Create a SceneCanvas with size (800, 400) for the plot
35+
# The canvas is the main drawing area for VisPy
36+
# Add the canvas to the layout using addWidget(canvas.native)
37+
# The .native property is needed because VisPy's canvas needs to be converted to a Qt widget
38+
39+
# TODO: Create view
40+
# Add a view to the canvas's central widget
41+
# Set the camera to 'panzoom' to allow interactive navigation
42+
# The view is where we'll add our plot elements
43+
44+
# TODO: Create line plot
45+
# Create a Line visual with initial data [[0, 0]]
46+
# Add this line to the view's scene
47+
# This line will be updated with new data points
48+
49+
# TODO: Set up view range
50+
# Set a fixed range for the x-axis (0 to 10 seconds)
51+
# Set a fixed range for the y-axis (-10 to 10 for RMS values)
52+
# This ensures the plot maintains a consistent view
53+
54+
def update_data(self, time_points, data):
55+
"""
56+
Update the plot with new data.
57+
58+
This method is called whenever new data is available. It:
59+
- Creates a new line data array from time and signal data
60+
- Updates the plot with the new data
61+
- Maintains the fixed view range
62+
63+
Args:
64+
time_points (np.ndarray): Array of time values
65+
data (np.ndarray): Array of signal values
66+
"""
67+
# TODO: Update plot data
68+
# 1. Create the line data by stacking time_points and data arrays
69+
# Use np.column_stack() to combine them into a 2D array
70+
# Each row will be [time, value] pair
71+
# 2. Update the line visual with the new data using set_data()
72+
# 3. Keep the view range fixed by setting the camera range again
73+
# 4. Update the canvas to show the changes
74+
# This method is called by the ViewModel when new data is available
75+
pass

0 commit comments

Comments
 (0)