aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--picostream/cli.py18
-rw-r--r--picostream/dfplot.py45
-rw-r--r--picostream/main.py131
-rw-r--r--picostream/pico.py35
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":