diff options
| author | Sam Scholten | 2025-10-21 17:08:55 +1000 |
|---|---|---|
| committer | Sam Scholten | 2025-10-21 17:08:55 +1000 |
| commit | 51ece17d4a8c4736b4e052cae54ae87886b727c5 (patch) | |
| tree | b2955733536a1c1e1d440550d947b25dfcb1926b | |
| parent | 8cbb351e7787ecbd84a09d7068804e0e9243f708 (diff) | |
| download | picostream-51ece17d4a8c4736b4e052cae54ae87886b727c5.tar.gz picostream-51ece17d4a8c4736b4e052cae54ae87886b727c5.zip | |
move ui feedback into QT sidebar
| -rw-r--r-- | picostream/cli.py | 18 | ||||
| -rw-r--r-- | picostream/dfplot.py | 45 | ||||
| -rw-r--r-- | picostream/main.py | 131 | ||||
| -rw-r--r-- | picostream/pico.py | 35 |
4 files changed, 209 insertions, 20 deletions
diff --git a/picostream/cli.py b/picostream/cli.py index 87c665b..d8e7ffb 100644 --- a/picostream/cli.py +++ b/picostream/cli.py @@ -49,6 +49,7 @@ class Streamer: is_gui_mode: bool = False, y_min: Optional[float] = None, y_max: Optional[float] = None, + bandwidth_limiter: str = "full", ) -> None: # --- Configuration --- self.output_file = output_file @@ -155,6 +156,9 @@ class Streamer: self.shutdown_event, downsample_mode=downsample_mode, ) + + # Store bandwidth limiter setting + self.bandwidth_limiter = bandwidth_limiter self.pico_device.set_channel( "PS5000A_CHANNEL_A", 1, "PS5000A_DC", self.pico_channel_range, offset_v @@ -162,6 +166,12 @@ class Streamer: self.pico_device.set_channel( "PS5000A_CHANNEL_B", 0, "PS5000A_DC", self.pico_channel_range, 0.0 ) + + # Set bandwidth filter for Channel A if specified + if self.bandwidth_limiter == "20MHz": + self.pico_device.set_bandwidth_filter("PS5000A_CHANNEL_A", "PS5000A_BW_20MHZ") + else: + self.pico_device.set_bandwidth_filter("PS5000A_CHANNEL_A", "PS5000A_BW_FULL") self.pico_device.set_data_buffer("PS5000A_CHANNEL_A", 0, pico_ratio_mode) self.pico_device.configure_streaming_var( self.pico_sample_interval_ns, @@ -527,6 +537,12 @@ def generate_unique_filename(base_path: str) -> str: help="Analog offset in Volts (only for ranges < 5V). [default: 0.0]", ) @click.option( + "--bandwidth", + type=click.Choice(["full", "20MHz"]), + default="full", + help="Bandwidth limiter to reduce noise. [default: full]", +) +@click.option( "--max-buff-sec", type=float, help="Maximum buffer duration in seconds for live-only mode (limits file size).", @@ -560,6 +576,7 @@ def main( hardware_downsample: int, downsample_mode: str, offset: float, + bandwidth: str, max_buff_sec: Optional[float], force: bool, y_min: Optional[float], @@ -633,6 +650,7 @@ def main( max_buffer_seconds=max_buff_sec, y_min=y_min, y_max=y_max, + bandwidth_limiter=bandwidth, ) # Update the acquisition command in metadata diff --git a/picostream/dfplot.py b/picostream/dfplot.py index 15196fa..c9cca03 100644 --- a/picostream/dfplot.py +++ b/picostream/dfplot.py @@ -231,13 +231,24 @@ class HDF5LivePlotter(QWidget): """Stops the plot update timer.""" self.timer.stop() + def hide_status_bar(self, hide: bool = True) -> None: + """Hide or show the status bar. + + Parameters + ---------- + hide : bool + If True, hide the status bar. If False, show it. + """ + if hasattr(self, 'status_container'): + self.status_container.setVisible(not hide) + def setup_ui(self) -> None: """Sets up the main window, widgets, and plot layout.""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Status bar - status_layout = QHBoxLayout() + self.status_layout = QHBoxLayout() self.heartbeat_label = QLabel("UI: -") self.samples_label = QLabel("Samples: 0") self.rate_label = QLabel("Rate: -") @@ -262,21 +273,23 @@ class HDF5LivePlotter(QWidget): label.setFont(font) # Add separators between status items - status_layout.addWidget(self.heartbeat_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.error_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.saturation_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.samples_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.plotter_latency_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.rate_label) - status_layout.addWidget(QLabel(" | ")) - status_layout.addWidget(self.acq_status_label) - status_layout.addStretch() - layout.addLayout(status_layout) + self.status_layout.addWidget(self.heartbeat_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.error_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.saturation_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.samples_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.plotter_latency_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.rate_label) + self.status_layout.addWidget(QLabel(" | ")) + self.status_layout.addWidget(self.acq_status_label) + self.status_layout.addStretch() + self.status_container = QWidget() + self.status_container.setLayout(self.status_layout) + layout.addWidget(self.status_container) # Plot widget self.plot_widget = pg.PlotWidget() diff --git a/picostream/main.py b/picostream/main.py index 1a0b4d8..fc1b914 100644 --- a/picostream/main.py +++ b/picostream/main.py @@ -1,12 +1,13 @@ import os +import re import sys from typing import Any, Dict, Optional from loguru import logger -from PyQt5.QtCore import QObject, QSettings, QThread, pyqtSignal, pyqtSlot, Qt -from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import QObject, QSettings, QThread, QTimer, pyqtSignal, pyqtSlot, Qt +from PyQt5.QtGui import QCloseEvent, QFont from PyQt5.QtWidgets import ( QApplication, QCheckBox, @@ -119,6 +120,10 @@ class PicoStreamMainWindow(QMainWindow): self.offset_v_input.setSuffix(" V") form_layout.addRow("Offset:", self.offset_v_input) + self.bandwidth_limiter_input = QComboBox() + self.bandwidth_limiter_input.addItems(["Full", "20 MHz"]) + form_layout.addRow("Bandwidth:", self.bandwidth_limiter_input) + self.live_only_checkbox = QCheckBox("Live-only (overwrite buffer)") self.max_buffer_input = QDoubleSpinBox() self.max_buffer_input.setRange(0.1, 60.0) @@ -172,18 +177,69 @@ class PicoStreamMainWindow(QMainWindow): self.stop_button = QPushButton("Stop Acquisition") self.stop_button.setEnabled(False) self.apply_plot_settings_button = QPushButton("Apply Plot Settings") - + settings_layout.addWidget(self.start_button) settings_layout.addWidget(self.stop_button) settings_layout.addWidget(self.apply_plot_settings_button) + + # Add status display section + status_group = QWidget() + status_group_layout = QVBoxLayout(status_group) + status_title = QLabel("Status") + status_title.setStyleSheet("font-weight: bold; font-size: 12px;") + status_group_layout.addWidget(status_title) + + # Create status labels with initial values + self.status_heartbeat = QLabel("UI: -") + self.status_samples = QLabel("Samples: 0") + self.status_rate = QLabel("Rate: -") + self.status_latency = QLabel("Latency: -") + self.status_errors = QLabel("Errors: 0") + self.status_saturation = QLabel("Saturation: -") + self.status_acquisition = QLabel("Waiting for file...") + + # Set monospace font for status + font = QFont() + font.setFamily("Monospace") + font.setFixedPitch(True) + font.setPointSize(9) + for label in [ + self.status_heartbeat, + self.status_samples, + self.status_rate, + self.status_latency, + self.status_errors, + self.status_saturation, + self.status_acquisition, + ]: + label.setFont(font) + label.setWordWrap(True) + + status_group_layout.addWidget(self.status_heartbeat) + status_group_layout.addWidget(self.status_errors) + status_group_layout.addWidget(self.status_saturation) + status_group_layout.addWidget(self.status_samples) + status_group_layout.addWidget(self.status_latency) + status_group_layout.addWidget(self.status_rate) + status_group_layout.addWidget(self.status_acquisition) + + settings_layout.addWidget(status_group) settings_layout.addStretch() # Right panel for the plot self.plotter = HDF5LivePlotter(hdf5_path=self.output_file_input.text()) + + # Hide the plotter's status bar since we're showing it in the sidebar + self.plotter.hide_status_bar(hide=True) main_layout.addWidget(settings_panel) main_layout.addWidget(self.plotter, 1) + # Create a timer to sync status from plotter to main window + self.status_sync_timer = QTimer() + self.status_sync_timer.timeout.connect(self.sync_status_from_plotter) + # Timer will be started when acquisition begins + # Connect signals self.start_button.clicked.connect(self.start_acquisition) self.stop_button.clicked.connect(self.stop_acquisition) @@ -224,6 +280,9 @@ class PicoStreamMainWindow(QMainWindow): # Clear the plotter's display and reset all internal state self.plotter.reset_for_new_file() + # Ensure status bar remains hidden after reset + self.plotter.hide_status_bar(hide=True) + # Remove old file if it exists if os.path.exists(output_file): try: @@ -253,6 +312,7 @@ class PicoStreamMainWindow(QMainWindow): else None, "enable_live_plot": False, "is_gui_mode": True, + "bandwidth_limiter": self.bandwidth_limiter_input.currentText().lower(), } self.thread = QThread() @@ -271,10 +331,16 @@ class PicoStreamMainWindow(QMainWindow): # Start plotter updates - it will wait for the file to exist with valid data self.plotter.start_updates() + + # Start status sync timer + self.status_sync_timer.start(100) # Update every 100ms def stop_acquisition(self) -> None: """Stop the background data acquisition.""" self.plotter.stop_updates() + # Stop the status sync timer to save CPU + if hasattr(self, 'status_sync_timer'): + self.status_sync_timer.stop() if self.worker: self.worker.stopRequested.emit() self.stop_button.setEnabled(False) @@ -292,9 +358,62 @@ class PicoStreamMainWindow(QMainWindow): """Handle acquisition error.""" logger.error(f"Acquisition error: {err_msg}") + def sync_status_from_plotter(self) -> None: + """Sync status from the plotter to the main window status labels.""" + try: + # Check if plotter exists and has been initialized + if not hasattr(self, 'plotter') or not self.plotter: + return + + # Check if plotter has the required attributes + required_labels = [ + 'heartbeat_label', 'samples_label', 'rate_label', + 'plotter_latency_label', 'error_label', + 'saturation_label', 'acq_status_label' + ] + + # Helper function to safely strip HTML tags + def strip_html(text: str) -> str: + if not text: + return text + # Remove all HTML tags + return re.sub(r'<[^>]+>', '', text) + + # Safely get text from label or return empty string + def safe_get_text(label_name: str) -> str: + if hasattr(self.plotter, label_name): + label = getattr(self.plotter, label_name) + if hasattr(label, 'text'): + return strip_html(label.text()) + return "" + + # Update all status labels + self.status_heartbeat.setText(safe_get_text('heartbeat_label')) + self.status_samples.setText(safe_get_text('samples_label')) + self.status_rate.setText(safe_get_text('rate_label')) + self.status_latency.setText(safe_get_text('plotter_latency_label')) + self.status_errors.setText(safe_get_text('error_label')) + self.status_saturation.setText(safe_get_text('saturation_label')) + self.status_acquisition.setText(safe_get_text('acq_status_label')) + + except Exception as e: + # Don't let errors in status sync crash the app + logger.debug(f"Error syncing status from plotter: {e}") + pass + def closeEvent(self, event: QCloseEvent) -> None: """Handle window close event.""" self.save_settings() + + # Stop the status sync timer first + if hasattr(self, 'status_sync_timer'): + self.status_sync_timer.stop() + self.status_sync_timer = None + + # Stop plotter updates + if hasattr(self, 'plotter') and self.plotter: + self.plotter.stop_updates() + if self.thread and self.thread.isRunning(): self.stop_acquisition() self.thread.wait() @@ -318,6 +437,8 @@ class PicoStreamMainWindow(QMainWindow): if not self.y_axis_auto_checkbox.isChecked(): self.settings.setValue("y_min", self.y_min_input.value()) self.settings.setValue("y_max", self.y_max_input.value()) + + self.settings.setValue("bandwidth_limiter", self.bandwidth_limiter_input.currentText()) def load_settings(self) -> None: """Load settings.""" @@ -355,6 +476,10 @@ class PicoStreamMainWindow(QMainWindow): self.y_min_input.setEnabled(not y_axis_auto) self.y_max_input.setEnabled(not y_axis_auto) + + self.bandwidth_limiter_input.setCurrentText( + self.settings.value("bandwidth_limiter", "Full") + ) def main() -> None: diff --git a/picostream/pico.py b/picostream/pico.py index cd1b13b..6b7eada 100644 --- a/picostream/pico.py +++ b/picostream/pico.py @@ -11,11 +11,21 @@ from loguru import logger from picosdk.functions import PICO_STATUS from picosdk.errors import CannotFindPicoSDKError + try: from picosdk.ps5000a import ps5000a as ps + from picosdk.PicoDeviceEnums import picoEnum as enums + + # Add PS5000A bandwidth limiter enum if not present + if not hasattr(ps, 'PS5000A_BANDWIDTH_LIMITER'): + ps.PS5000A_BANDWIDTH_LIMITER = { + 'PS5000A_BW_FULL': 0, + 'PS5000A_BW_20MHZ': 20000000, + } except CannotFindPicoSDKError: logger.critical("PICOSDK IMPORT FAILED: CANNOT FIND SDK LIB - download from pico website") - + ps = None + enums = None def check_status(status: int, function_name: str) -> None: @@ -115,6 +125,7 @@ class PicoDevice: self.channel_a_coupling: Optional[str] = None self.analog_offset_v: float = 0.0 self.channel_a_range_str: Optional[str] = None + self.bandwidth_limiter: Optional[str] = None # --- Threading and Queues --- self.shutdown_event: threading.Event = shutdown_event @@ -203,6 +214,27 @@ class PicoDevice: f"voltage_range_v: {self.voltage_range_v}V, max_adc: {self.max_adc.value}" ) + def set_bandwidth_filter(self, channel: str, bandwidth: str) -> None: + """Set the bandwidth filter for a channel to reduce noise. + + Args: + channel: The channel identifier string, e.g., "PS5000A_CHANNEL_A". + bandwidth: The bandwidth limiter string, e.g., "PS5000A_BW_FULL" or "PS5000A_BW_20MHZ". + """ + channel_enum = ps.PS5000A_CHANNEL[channel] + bandwidth_enum = ps.PS5000A_BANDWIDTH_LIMITER[bandwidth] + + if channel == "PS5000A_CHANNEL_A": + self.bandwidth_limiter = bandwidth + + status = ps.ps5000aSetBandwidthFilter( + self.handle, + channel_enum, + bandwidth_enum + ) + check_status(status, f"ps5000aSetBandwidthFilter ({channel}, {bandwidth})") + logger.info(f"Bandwidth filter set - Channel: {channel}, Bandwidth: {bandwidth}") + def set_data_buffer(self, chan: str, segment: int, rat: str) -> None: """Set up the data buffer for a specific channel for streaming. @@ -275,6 +307,7 @@ class PicoDevice: "picostream_version": picostream_version, "acquisition_command": acquisition_command, "was_live_mode": was_live_mode, + "bandwidth_limiter": self.bandwidth_limiter, } if self.downsample_mode == "aggregate": |
