Skip to content

GUI Components API

PyQt6-based graphical user interface for oscilloscope control

Main Window

scpi_control.gui.main_window

Main window for Siglent oscilloscope control GUI.

MainWindow

MainWindow()

Bases: QMainWindow

Main application window for oscilloscope control.

Initialize main window.

Source code in scpi_control/gui/main_window.py
def __init__(self):
    """Initialize main window."""
    super().__init__()

    self.scope: Optional[Oscilloscope] = None
    self.psu: Optional[PowerSupply] = None
    self.daq: Optional[DataLogger] = None
    self.is_live_view = False
    self.live_view_worker: Optional[LiveViewWorker] = None
    self.capture_worker: Optional[WaveformCaptureWorker] = None
    self.progress_dialog: Optional[QProgressDialog] = None

    # Connection manager
    self.connection_manager = ConnectionManager()

    # Reference waveform manager
    self.reference_manager = ReferenceWaveform()

    # Protocol decoders
    self.i2c_decoder = I2CDecoder()
    self.spi_decoder = SPIDecoder()
    self.uart_decoder = UARTDecoder()

    # Control widgets (initialized in _init_ui)
    self.channel_control: Optional[ChannelControl] = None
    self.trigger_control: Optional[TriggerControl] = None
    self.measurement_panel: Optional[MeasurementPanel] = None
    self.timebase_control: Optional[TimebaseControl] = None
    self.math_panel: Optional[MathPanel] = None
    self.fft_display: Optional[FFTDisplay] = None
    self.reference_panel: Optional[ReferencePanel] = None
    self.protocol_decode_panel: Optional[ProtocolDecodePanel] = None
    self.psu_control: Optional[PSUControl] = None
    self.data_logger_control: Optional[DataLoggerControl] = None
    self.terminal_widget: Optional[TerminalWidget] = None

    # VNC window (separate window)
    self.vnc_window: Optional[VNCWindow] = None

    self._init_ui()
    self._create_menus()
    self._create_toolbar()
    self._create_status_bar()

    logger.info("Main window initialized")

closeEvent

closeEvent(event)

Handle window close event.

Source code in scpi_control/gui/main_window.py
def closeEvent(self, event):
    """Handle window close event."""
    # Stop live view if running
    if self.is_live_view and self.live_view_worker:
        logger.info("Stopping live view worker on close...")
        self.live_view_worker.stop()
        self.live_view_worker = None

    # Disconnect from scope
    if self.scope:
        self.scope.disconnect()

    # Disconnect from PSU
    if self.psu:
        self.psu.disconnect()

    # Disconnect from DAQ
    if self.daq:
        self.data_logger_control.set_daq(None)  # Stop any running acquisition
        self.daq.disconnect()

    event.accept()

options: show_root_heading: false show_source: true heading_level: 3 members_order: source group_by_category: true show_signature_annotations: true separate_signature: true merge_init_into_class: true filters: - "!^*" # Exclude private members

Connection Manager

scpi_control.gui.connection_manager

Connection manager for storing and retrieving recent oscilloscope connections.

ConnectionManager

ConnectionManager()

Manages recent oscilloscope connections using QSettings.

Stores connection history persistently across application sessions, including IP addresses, ports, model information, and timestamps.

Initialize connection manager with QSettings.

Source code in scpi_control/gui/connection_manager.py
def __init__(self):
    """Initialize connection manager with QSettings."""
    self.settings = QSettings("Siglent", "OscilloscopeControl")
    logger.info("Connection manager initialized")

add_connection

add_connection(host: str, port: int = 5024, model_name: Optional[str] = None) -> None

Add a connection to the recent connections list.

Parameters:

Name Type Description Default
host str

IP address or hostname

required
port int

TCP port (default: 5024)

5024
model_name Optional[str]

Optional model name (e.g., "SDS824X HD")

None
Source code in scpi_control/gui/connection_manager.py
def add_connection(self, host: str, port: int = 5024, model_name: Optional[str] = None) -> None:
    """Add a connection to the recent connections list.

    Args:
        host: IP address or hostname
        port: TCP port (default: 5024)
        model_name: Optional model name (e.g., "SDS824X HD")
    """
    # Get existing connections
    recent = self.get_recent_connections()

    # Create new connection entry
    new_connection = {
        "host": host,
        "port": port,
        "model_name": model_name or "Unknown",
        "timestamp": datetime.now().isoformat(),
    }

    # Remove duplicate if exists (update to latest)
    recent = [conn for conn in recent if not (conn["host"] == host and conn["port"] == port)]

    # Add new connection at the beginning
    recent.insert(0, new_connection)

    # Limit to max recent connections
    recent = recent[: self.MAX_RECENT_CONNECTIONS]

    # Save to settings
    self.settings.setValue("recent_connections", recent)
    logger.info(f"Added connection: {host}:{port} ({model_name})")

get_recent_connections

get_recent_connections() -> List[Dict[str, any]]

Get list of recent connections.

Returns:

Type Description
List[Dict[str, any]]

List of connection dictionaries with keys: host, port, model_name, timestamp

List[Dict[str, any]]

Sorted by most recent first

Source code in scpi_control/gui/connection_manager.py
def get_recent_connections(self) -> List[Dict[str, any]]:
    """Get list of recent connections.

    Returns:
        List of connection dictionaries with keys: host, port, model_name, timestamp
        Sorted by most recent first
    """
    recent = self.settings.value("recent_connections", [])

    # QSettings may return different types depending on platform/Qt version
    if recent is None:
        recent = []
    elif not isinstance(recent, list):
        recent = []

    return recent

clear_recent_connections

clear_recent_connections() -> None

Clear all recent connections.

Source code in scpi_control/gui/connection_manager.py
def clear_recent_connections(self) -> None:
    """Clear all recent connections."""
    self.settings.setValue("recent_connections", [])
    logger.info("Cleared all recent connections")

remove_connection

remove_connection(host: str, port: int = 5024) -> None

Remove a specific connection from recent list.

Parameters:

Name Type Description Default
host str

IP address or hostname

required
port int

TCP port

5024
Source code in scpi_control/gui/connection_manager.py
def remove_connection(self, host: str, port: int = 5024) -> None:
    """Remove a specific connection from recent list.

    Args:
        host: IP address or hostname
        port: TCP port
    """
    recent = self.get_recent_connections()
    recent = [conn for conn in recent if not (conn["host"] == host and conn["port"] == port)]
    self.settings.setValue("recent_connections", recent)
    logger.info(f"Removed connection: {host}:{port}")

get_last_connection

get_last_connection() -> Optional[Dict[str, any]]

Get the most recent connection.

Returns:

Type Description
Optional[Dict[str, any]]

Connection dictionary or None if no recent connections

Source code in scpi_control/gui/connection_manager.py
def get_last_connection(self) -> Optional[Dict[str, any]]:
    """Get the most recent connection.

    Returns:
        Connection dictionary or None if no recent connections
    """
    recent = self.get_recent_connections()
    return recent[0] if recent else None

save_connection_profile

save_connection_profile(name: str, host: str, port: int = 5024, model_name: Optional[str] = None, notes: Optional[str] = None) -> None

Save a named connection profile.

Parameters:

Name Type Description Default
name str

Profile name

required
host str

IP address or hostname

required
port int

TCP port

5024
model_name Optional[str]

Optional model name

None
notes Optional[str]

Optional notes about this connection

None
Source code in scpi_control/gui/connection_manager.py
def save_connection_profile(
    self,
    name: str,
    host: str,
    port: int = 5024,
    model_name: Optional[str] = None,
    notes: Optional[str] = None,
) -> None:
    """Save a named connection profile.

    Args:
        name: Profile name
        host: IP address or hostname
        port: TCP port
        model_name: Optional model name
        notes: Optional notes about this connection
    """
    profiles = self.get_connection_profiles()

    profile = {
        "name": name,
        "host": host,
        "port": port,
        "model_name": model_name or "Unknown",
        "notes": notes or "",
        "created": datetime.now().isoformat(),
    }

    # Update if exists, otherwise add
    profiles[name] = profile

    self.settings.setValue("connection_profiles", profiles)
    logger.info(f"Saved connection profile: {name}")

get_connection_profiles

get_connection_profiles() -> Dict[str, Dict[str, any]]

Get all saved connection profiles.

Returns:

Type Description
Dict[str, Dict[str, any]]

Dictionary of profile_name -> profile_data

Source code in scpi_control/gui/connection_manager.py
def get_connection_profiles(self) -> Dict[str, Dict[str, any]]:
    """Get all saved connection profiles.

    Returns:
        Dictionary of profile_name -> profile_data
    """
    profiles = self.settings.value("connection_profiles", {})

    if profiles is None:
        profiles = {}
    elif not isinstance(profiles, dict):
        profiles = {}

    return profiles

get_connection_profile

get_connection_profile(name: str) -> Optional[Dict[str, any]]

Get a specific connection profile by name.

Parameters:

Name Type Description Default
name str

Profile name

required

Returns:

Type Description
Optional[Dict[str, any]]

Profile dictionary or None if not found

Source code in scpi_control/gui/connection_manager.py
def get_connection_profile(self, name: str) -> Optional[Dict[str, any]]:
    """Get a specific connection profile by name.

    Args:
        name: Profile name

    Returns:
        Profile dictionary or None if not found
    """
    profiles = self.get_connection_profiles()
    return profiles.get(name)

delete_connection_profile

delete_connection_profile(name: str) -> None

Delete a connection profile.

Parameters:

Name Type Description Default
name str

Profile name to delete

required
Source code in scpi_control/gui/connection_manager.py
def delete_connection_profile(self, name: str) -> None:
    """Delete a connection profile.

    Args:
        name: Profile name to delete
    """
    profiles = self.get_connection_profiles()
    if name in profiles:
        del profiles[name]
        self.settings.setValue("connection_profiles", profiles)
        logger.info(f"Deleted connection profile: {name}")

format_connection_display

format_connection_display(connection: Dict[str, any]) -> str

Format a connection for display in UI.

Parameters:

Name Type Description Default
connection Dict[str, any]

Connection dictionary

required

Returns:

Type Description
str

Formatted string for display

Source code in scpi_control/gui/connection_manager.py
def format_connection_display(self, connection: Dict[str, any]) -> str:
    """Format a connection for display in UI.

    Args:
        connection: Connection dictionary

    Returns:
        Formatted string for display
    """
    host = connection.get("host", "Unknown")
    port = connection.get("port", 5024)
    model = connection.get("model_name", "Unknown")

    # Parse timestamp if available
    timestamp_str = connection.get("timestamp", "")
    if timestamp_str:
        try:
            timestamp = datetime.fromisoformat(timestamp_str)
            time_display = timestamp.strftime("%Y-%m-%d %H:%M")
        except:
            time_display = "Unknown time"
    else:
        time_display = "Unknown time"

    return f"{host}:{port} - {model} ({time_display})"

options: show_root_heading: false show_source: true heading_level: 3 members_order: source group_by_category: true show_signature_annotations: true separate_signature: true merge_init_into_class: true filters: - "!^*"

Widgets

Waveform Display

scpi_control.gui.widgets.waveform_display

Waveform display widget using matplotlib.

WaveformDisplay

WaveformDisplay(parent=None)

Bases: QWidget

Widget for displaying oscilloscope waveforms using matplotlib.

Features: - Multiple channel display with different colors - Grid toggle - Autoscale - Zoom and pan (via matplotlib toolbar) - Export to image

Initialize waveform display widget.

Parameters:

Name Type Description Default
parent

Parent widget

None
Source code in scpi_control/gui/widgets/waveform_display.py
def __init__(self, parent=None):
    """Initialize waveform display widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.waveforms: Dict[int, WaveformData] = {}  # Store waveforms by channel
    self.current_waveforms: List[WaveformData] = []  # Store most recently displayed waveforms
    self.show_grid = True

    # Cursor state
    self.cursor_mode = "off"  # 'off', 'vertical', 'horizontal', 'both'
    self.cursor_lines = {"v1": None, "v2": None, "h1": None, "h2": None}
    self.cursor_positions = {"v1": None, "v2": None, "h1": None, "h2": None}
    self.dragging_cursor = None

    # Reference waveform state
    self.reference_data = None  # Reference waveform data
    self.reference_line = None  # Matplotlib line for reference
    self.show_reference = False  # Whether to show reference overlay
    self.show_difference = False  # Whether to show difference instead of overlay

    # Measurement marker state
    self.measurement_markers = []  # List of MeasurementMarker objects
    self.marker_mode = "off"  # 'off', 'add', 'edit'
    self.selected_marker = None  # Currently selected marker
    self.pending_marker_type = None  # Marker type to add in 'add' mode
    self.pending_marker_channel = None  # Channel for pending marker
    self.dragging_marker_handle = None  # (marker, handle_id) during drag

    self._init_ui()
    logger.info("Waveform display widget initialized")

plot_waveform

plot_waveform(waveform: WaveformData, clear_others: bool = False)

Plot a waveform on the display.

Parameters:

Name Type Description Default
waveform WaveformData

WaveformData object to plot

required
clear_others bool

If True, clear other channels before plotting

False
Source code in scpi_control/gui/widgets/waveform_display.py
def plot_waveform(self, waveform: WaveformData, clear_others: bool = False):
    """Plot a waveform on the display.

    Args:
        waveform: WaveformData object to plot
        clear_others: If True, clear other channels before plotting
    """
    if clear_others:
        self.waveforms.clear()

    # Store waveform
    self.waveforms[waveform.channel] = waveform

    # Replot all waveforms
    self._replot()

    logger.info(f"Plotted waveform from channel {waveform.channel}")

plot_multiple_waveforms

plot_multiple_waveforms(waveforms: List[WaveformData], fast_update: bool = False)

Plot multiple waveforms.

Parameters:

Name Type Description Default
waveforms List[WaveformData]

List of WaveformData objects to plot

required
fast_update bool

If True, use fast update for live view (doesn't clear axes)

False
Source code in scpi_control/gui/widgets/waveform_display.py
def plot_multiple_waveforms(self, waveforms: List[WaveformData], fast_update: bool = False):
    """Plot multiple waveforms.

    Args:
        waveforms: List of WaveformData objects to plot
        fast_update: If True, use fast update for live view (doesn't clear axes)
    """
    logger.info(f"plot_multiple_waveforms called with {len(waveforms)} waveform(s), fast_update={fast_update}")

    self.waveforms.clear()

    for waveform in waveforms:
        logger.debug(f"Adding waveform from channel {waveform.channel}, {len(waveform.voltage)} samples")
        self.waveforms[waveform.channel] = waveform

    # Store current waveforms for saving
    self.current_waveforms = waveforms

    logger.debug("Calling _replot()...")
    if fast_update:
        self._fast_replot()
    else:
        self._replot()

    logger.info(f"Plotted {len(waveforms)} waveforms successfully")

update_waveform

update_waveform(waveform: WaveformData)

Update existing waveform or add new one.

Parameters:

Name Type Description Default
waveform WaveformData

WaveformData object to update/add

required
Source code in scpi_control/gui/widgets/waveform_display.py
def update_waveform(self, waveform: WaveformData):
    """Update existing waveform or add new one.

    Args:
        waveform: WaveformData object to update/add
    """
    self.waveforms[waveform.channel] = waveform
    self._replot()

clear_channel

clear_channel(channel: int)

Clear waveform for a specific channel.

Parameters:

Name Type Description Default
channel int

Channel number to clear (1-4)

required
Source code in scpi_control/gui/widgets/waveform_display.py
def clear_channel(self, channel: int):
    """Clear waveform for a specific channel.

    Args:
        channel: Channel number to clear (1-4)
    """
    if channel in self.waveforms:
        del self.waveforms[channel]
        self._replot()
        logger.info(f"Cleared channel {channel}")

clear_all

clear_all()

Clear all waveforms.

Source code in scpi_control/gui/widgets/waveform_display.py
def clear_all(self):
    """Clear all waveforms."""
    self.waveforms.clear()
    self._replot()
    logger.info("Cleared all waveforms")

set_theme

set_theme(dark: bool = True)

Set display theme.

Parameters:

Name Type Description Default
dark bool

True for dark theme, False for light theme

True
Source code in scpi_control/gui/widgets/waveform_display.py
def set_theme(self, dark: bool = True):
    """Set display theme.

    Args:
        dark: True for dark theme, False for light theme
    """
    if dark:
        # Dark theme (default)
        self.figure.set_facecolor("#1a1a1a")
        self.ax.set_facecolor("#0a0a0a")
        text_color = "#cccccc"
        grid_color = "#444444"
        spine_color = "#444444"
    else:
        # Light theme
        self.figure.set_facecolor("#ffffff")
        self.ax.set_facecolor("#f8f8f8")
        text_color = "#000000"
        grid_color = "#cccccc"
        spine_color = "#000000"

    # Update colors
    self.ax.tick_params(colors=text_color, which="both")
    self.ax.spines["bottom"].set_color(spine_color)
    self.ax.spines["top"].set_color(spine_color)
    self.ax.spines["left"].set_color(spine_color)
    self.ax.spines["right"].set_color(spine_color)
    self.ax.set_xlabel("Time", color=text_color)
    self.ax.set_ylabel("Voltage (V)", color=text_color)
    self.ax.set_title("Waveform Display", color=text_color)
    self.ax.grid(self.show_grid, alpha=0.3, color=grid_color)

    self.canvas.draw_idle()
    logger.info(f"Theme set to {'dark' if dark else 'light'}")

toggle_grid

toggle_grid()

Toggle grid display (callable from external sources like keyboard shortcuts).

Source code in scpi_control/gui/widgets/waveform_display.py
def toggle_grid(self):
    """Toggle grid display (callable from external sources like keyboard shortcuts)."""
    self.show_grid = not self.show_grid
    self.ax.grid(self.show_grid, alpha=0.3, color="#444444", linestyle="--", linewidth=0.5)
    self.canvas.draw_idle()
    logger.info(f"Grid {'enabled' if self.show_grid else 'disabled'}")

reset_zoom

reset_zoom()

Reset zoom to default view (callable from external sources).

Source code in scpi_control/gui/widgets/waveform_display.py
def reset_zoom(self):
    """Reset zoom to default view (callable from external sources)."""
    self.ax.autoscale(enable=True, axis="both", tight=False)
    self.ax.relim()
    self.ax.autoscale_view()
    self.canvas.draw_idle()
    logger.info("Zoom reset")

set_cursor_mode

set_cursor_mode(mode: str)

Set cursor mode.

Parameters:

Name Type Description Default
mode str

Cursor mode ('off', 'vertical', 'horizontal', 'both')

required
Source code in scpi_control/gui/widgets/waveform_display.py
def set_cursor_mode(self, mode: str):
    """Set cursor mode.

    Args:
        mode: Cursor mode ('off', 'vertical', 'horizontal', 'both')
    """
    self.cursor_mode = mode.lower()
    logger.info(f"Cursor mode set to: {self.cursor_mode}")

    # Clear existing cursors when changing modes
    if self.cursor_mode == "off":
        self._clear_all_cursors()

get_cursor_values

get_cursor_values() -> dict

Get current cursor position values.

Returns:

Type Description
dict

Dictionary with cursor positions

Source code in scpi_control/gui/widgets/waveform_display.py
def get_cursor_values(self) -> dict:
    """Get current cursor position values.

    Returns:
        Dictionary with cursor positions
    """
    return {
        "x1": self.cursor_positions.get("v1"),
        "y1": self.cursor_positions.get("h1"),
        "x2": self.cursor_positions.get("v2"),
        "y2": self.cursor_positions.get("h2"),
    }

set_reference

set_reference(reference_data: dict)

Set reference waveform for overlay.

Parameters:

Name Type Description Default
reference_data dict

Reference data dictionary with 'time' and 'voltage' keys

required
Source code in scpi_control/gui/widgets/waveform_display.py
def set_reference(self, reference_data: dict):
    """Set reference waveform for overlay.

    Args:
        reference_data: Reference data dictionary with 'time' and 'voltage' keys
    """
    self.reference_data = reference_data
    self.show_reference = True
    self._replot()
    logger.info("Reference waveform set")

clear_reference

clear_reference()

Clear reference waveform.

Source code in scpi_control/gui/widgets/waveform_display.py
def clear_reference(self):
    """Clear reference waveform."""
    self.reference_data = None
    self.show_reference = False
    self.show_difference = False
    self._replot()
    logger.info("Reference waveform cleared")

toggle_reference_visibility

toggle_reference_visibility(visible: bool)

Toggle reference waveform visibility.

Parameters:

Name Type Description Default
visible bool

Whether reference should be visible

required
Source code in scpi_control/gui/widgets/waveform_display.py
def toggle_reference_visibility(self, visible: bool):
    """Toggle reference waveform visibility.

    Args:
        visible: Whether reference should be visible
    """
    self.show_reference = visible
    self._replot()

toggle_difference_mode

toggle_difference_mode(enabled: bool)

Toggle difference mode (show waveform - reference).

Parameters:

Name Type Description Default
enabled bool

Whether to show difference

required
Source code in scpi_control/gui/widgets/waveform_display.py
def toggle_difference_mode(self, enabled: bool):
    """Toggle difference mode (show waveform - reference).

    Args:
        enabled: Whether to show difference
    """
    self.show_difference = enabled
    self._replot()

get_reference_data

get_reference_data()

Get current reference data.

Returns:

Type Description

Reference data dictionary or None

Source code in scpi_control/gui/widgets/waveform_display.py
def get_reference_data(self):
    """Get current reference data.

    Returns:
        Reference data dictionary or None
    """
    return self.reference_data

add_measurement_marker

add_measurement_marker(marker) -> None

Add a measurement marker to display.

Parameters:

Name Type Description Default
marker

MeasurementMarker object to add

required
Source code in scpi_control/gui/widgets/waveform_display.py
def add_measurement_marker(self, marker) -> None:
    """Add a measurement marker to display.

    Args:
        marker: MeasurementMarker object to add
    """
    self.measurement_markers.append(marker)
    marker.render()
    self.canvas.draw_idle()
    logger.info(f"Added measurement marker {marker.marker_id}")

remove_measurement_marker

remove_measurement_marker(marker) -> None

Remove a measurement marker from display.

Parameters:

Name Type Description Default
marker

MeasurementMarker object to remove

required
Source code in scpi_control/gui/widgets/waveform_display.py
def remove_measurement_marker(self, marker) -> None:
    """Remove a measurement marker from display.

    Args:
        marker: MeasurementMarker object to remove
    """
    if marker in self.measurement_markers:
        marker.remove()
        self.measurement_markers.remove(marker)

        if self.selected_marker == marker:
            self.selected_marker = None

        self.canvas.draw_idle()
        logger.info(f"Removed measurement marker {marker.marker_id}")

clear_all_markers

clear_all_markers() -> None

Clear all measurement markers.

Source code in scpi_control/gui/widgets/waveform_display.py
def clear_all_markers(self) -> None:
    """Clear all measurement markers."""
    for marker in self.measurement_markers[:]:
        marker.remove()

    self.measurement_markers.clear()
    self.selected_marker = None
    self.canvas.draw_idle()
    logger.info("Cleared all measurement markers")

set_marker_mode

set_marker_mode(mode: str, marker_type: str = None, channel: int = None) -> None

Set interaction mode for markers.

Parameters:

Name Type Description Default
mode str

Marker mode ('off', 'add', 'edit')

required
marker_type str

Type of marker to add (required if mode='add')

None
channel int

Channel for marker (required if mode='add')

None
Source code in scpi_control/gui/widgets/waveform_display.py
def set_marker_mode(self, mode: str, marker_type: str = None, channel: int = None) -> None:
    """Set interaction mode for markers.

    Args:
        mode: Marker mode ('off', 'add', 'edit')
        marker_type: Type of marker to add (required if mode='add')
        channel: Channel for marker (required if mode='add')
    """
    self.marker_mode = mode.lower()
    self.pending_marker_type = marker_type
    self.pending_marker_channel = channel

    logger.info(f"Marker mode set to: {self.marker_mode}")

    if mode == "off":
        # Deselect all markers
        for marker in self.measurement_markers:
            marker.set_selected(False)
        self.selected_marker = None
        self.canvas.draw_idle()

get_marker_measurements

get_marker_measurements() -> Dict

Get all measurement results from markers.

Returns:

Type Description
Dict

Dictionary mapping marker IDs to results

Source code in scpi_control/gui/widgets/waveform_display.py
def get_marker_measurements(self) -> Dict:
    """Get all measurement results from markers.

    Returns:
        Dictionary mapping marker IDs to results
    """
    return {
        marker.marker_id: {
            "type": marker.measurement_type,
            "channel": marker.channel,
            "result": marker.result,
            "unit": marker.unit,
            "enabled": marker.enabled,
        }
        for marker in self.measurement_markers
    }

update_all_markers

update_all_markers() -> None

Update all enabled markers with current waveform data.

Source code in scpi_control/gui/widgets/waveform_display.py
def update_all_markers(self) -> None:
    """Update all enabled markers with current waveform data."""
    if not self.current_waveforms:
        return

    for marker in self.measurement_markers:
        if marker.enabled:
            # Find waveform for marker's channel
            waveform = next((w for w in self.current_waveforms if w.channel == marker.channel), None)

            if waveform:
                marker.update_measurement(waveform)

    # Redraw markers with updated values
    self._render_all_markers()

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

scpi_control.gui.widgets.waveform_display_pg

PyQtGraph-based waveform display widget for high-performance real-time plotting.

This module provides a drop-in replacement for the matplotlib-based waveform display, offering 100x performance improvement for real-time live view updates.

Performance Characteristics
  • 1000+ fps capability (vs 5-10 fps with matplotlib)
  • Updates existing plot items instead of clearing/redrawing
  • Typical update time: <1ms per frame
  • Non-blocking updates for smooth GUI interaction
Features
  • Real-time multi-channel waveform display (up to 4 channels)
  • Interactive zoom, pan, and autoscaling
  • Oscilloscope-style color scheme and grid
  • Channel enable/disable controls
  • Cursor support for measurements
  • Measurement marker overlay support
  • Export to PNG/JPEG
Architecture

Uses PyQtGraph's PlotWidget for hardware-accelerated rendering. Plot items are created once and updated with setData() for minimal overhead. Designed for threaded live view with LiveViewWorker.

Example

display = WaveformDisplayPG() display.plot_multiple_waveforms([waveform1, waveform2]) display.toggle_grid() display.autoscale()

WaveformDisplayPG

WaveformDisplayPG(parent=None)

Bases: QWidget

High-performance waveform display using PyQtGraph.

Features: - Real-time plotting at 1000+ fps - Multiple channel display with different colors - Interactive cursors - Measurement markers - Grid toggle - Autoscale - Export to image

Initialize PyQtGraph waveform display widget.

Parameters:

Name Type Description Default
parent

Parent widget

None
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def __init__(self, parent=None):
    """Initialize PyQtGraph waveform display widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.waveforms: Dict[int, WaveformData] = {}
    self.current_waveforms: List[WaveformData] = []
    self.show_grid = True

    # Plot items (curves) for each channel
    self.plot_items: Dict[int, pg.PlotDataItem] = {}

    # Cursor state
    self.cursor_mode = "off"  # 'off', 'vertical', 'horizontal', 'both'
    self.cursor_lines = {"v1": None, "v2": None, "h1": None, "h2": None}
    self.cursor_positions = {"v1": None, "v2": None, "h1": None, "h2": None}
    self.dragging_cursor = None

    # Measurement marker state
    self.measurement_markers = []
    self.marker_mode = "off"
    self.selected_marker = None
    self.pending_marker_type = None
    self.pending_marker_channel = None
    self.dragging_marker_handle = None

    # Reference waveform state
    self.reference_data = None
    self.reference_item = None
    self.show_reference = False
    self.show_difference = False

    self._init_ui()
    logger.info("PyQtGraph waveform display widget initialized")

plot_waveform

plot_waveform(waveform: WaveformData, clear_others: bool = False)

Plot a waveform on the display.

Parameters:

Name Type Description Default
waveform WaveformData

WaveformData object to plot

required
clear_others bool

If True, clear other channels before plotting

False
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def plot_waveform(self, waveform: WaveformData, clear_others: bool = False):
    """Plot a waveform on the display.

    Args:
        waveform: WaveformData object to plot
        clear_others: If True, clear other channels before plotting
    """
    if clear_others:
        self.waveforms.clear()
        for item in self.plot_items.values():
            self.plot_item.removeItem(item)
        self.plot_items.clear()

    # Store waveform
    self.waveforms[waveform.channel] = waveform

    # Update plot
    self._update_plot()

    logger.info(f"Plotted waveform from channel {waveform.channel}")

plot_multiple_waveforms

plot_multiple_waveforms(waveforms: List[WaveformData], fast_update: bool = False)

Plot multiple waveforms.

Parameters:

Name Type Description Default
waveforms List[WaveformData]

List of WaveformData objects to plot

required
fast_update bool

If True, use fast update for live view (PyQtGraph is always fast)

False
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def plot_multiple_waveforms(self, waveforms: List[WaveformData], fast_update: bool = False):
    """Plot multiple waveforms.

    Args:
        waveforms: List of WaveformData objects to plot
        fast_update: If True, use fast update for live view (PyQtGraph is always fast)
    """
    logger.info(f"plot_multiple_waveforms called with {len(waveforms)} waveform(s)")

    self.waveforms.clear()

    # Validate all waveforms before plotting
    valid_waveforms, invalid_info = WaveformValidator.validate_multiple(waveforms)

    # Log any invalid waveforms at WARNING level (visible to users)
    if invalid_info:
        for channel, issues in invalid_info:
            logger.warning(f"Invalid waveform CH{channel}: {'; '.join(issues)}")

    # Add valid waveforms to display
    for waveform in valid_waveforms:
        summary = WaveformValidator.get_summary(waveform)
        logger.info(f"Adding valid waveform: {summary}")
        self.waveforms[waveform.channel] = waveform

    # Store current waveforms for saving (only valid ones)
    self.current_waveforms = valid_waveforms

    # Update info label if all waveforms were invalid
    if not valid_waveforms and waveforms:
        error_msg = f"All {len(waveforms)} waveform(s) invalid - see console for details"
        logger.error(error_msg)
        self.info_label.setText("Invalid data - check logs")
        return

    self._update_plot()

    logger.info(f"Plotted {len(valid_waveforms)} valid waveform(s) successfully")

update_waveform

update_waveform(waveform: WaveformData)

Update existing waveform or add new one.

Parameters:

Name Type Description Default
waveform WaveformData

WaveformData object to update/add

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def update_waveform(self, waveform: WaveformData):
    """Update existing waveform or add new one.

    Args:
        waveform: WaveformData object to update/add
    """
    self.waveforms[waveform.channel] = waveform
    self._update_plot()

clear_channel

clear_channel(channel: int)

Clear waveform for a specific channel.

Parameters:

Name Type Description Default
channel int

Channel number to clear (1-4)

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def clear_channel(self, channel: int):
    """Clear waveform for a specific channel.

    Args:
        channel: Channel number to clear (1-4)
    """
    if channel in self.waveforms:
        del self.waveforms[channel]

        if channel in self.plot_items:
            self.plot_item.removeItem(self.plot_items[channel])
            del self.plot_items[channel]

        self._update_plot()
        logger.info(f"Cleared channel {channel}")

clear_all

clear_all()

Clear all waveforms.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def clear_all(self):
    """Clear all waveforms."""
    self.waveforms.clear()

    for item in self.plot_items.values():
        self.plot_item.removeItem(item)
    self.plot_items.clear()

    self.info_label.setText("No data")
    logger.info("Cleared all waveforms")

toggle_grid

toggle_grid()

Toggle grid display.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def toggle_grid(self):
    """Toggle grid display."""
    self.show_grid = not self.show_grid
    self.plot_item.showGrid(x=self.show_grid, y=self.show_grid, alpha=0.3)
    logger.info(f"Grid {'enabled' if self.show_grid else 'disabled'}")

reset_zoom

reset_zoom()

Reset zoom to default view.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def reset_zoom(self):
    """Reset zoom to default view."""
    self.plot_item.enableAutoRange()
    self.plot_item.autoRange()
    logger.info("Zoom reset")

set_cursor_mode

set_cursor_mode(mode: str)

Set cursor mode.

Parameters:

Name Type Description Default
mode str

Cursor mode ('off', 'vertical', 'horizontal', 'both')

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def set_cursor_mode(self, mode: str):
    """Set cursor mode.

    Args:
        mode: Cursor mode ('off', 'vertical', 'horizontal', 'both')
    """
    self.cursor_mode = mode.lower()
    logger.info(f"Cursor mode set to: {self.cursor_mode}")

    if self.cursor_mode == "off":
        self._clear_all_cursors()

get_cursor_values

get_cursor_values() -> dict

Get current cursor position values.

Returns:

Type Description
dict

Dictionary with cursor positions

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def get_cursor_values(self) -> dict:
    """Get current cursor position values.

    Returns:
        Dictionary with cursor positions
    """
    return {
        "x1": self.cursor_positions.get("v1"),
        "y1": self.cursor_positions.get("h1"),
        "x2": self.cursor_positions.get("v2"),
        "y2": self.cursor_positions.get("h2"),
    }

add_measurement_marker

add_measurement_marker(marker) -> None

Add a measurement marker to display.

Parameters:

Name Type Description Default
marker

MeasurementMarker object to add

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def add_measurement_marker(self, marker) -> None:
    """Add a measurement marker to display.

    Args:
        marker: MeasurementMarker object to add
    """
    self.measurement_markers.append(marker)
    # Marker rendering will be handled by the marker class
    logger.info(f"Added measurement marker {marker.marker_id}")

remove_measurement_marker

remove_measurement_marker(marker) -> None

Remove a measurement marker from display.

Parameters:

Name Type Description Default
marker

MeasurementMarker object to remove

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def remove_measurement_marker(self, marker) -> None:
    """Remove a measurement marker from display.

    Args:
        marker: MeasurementMarker object to remove
    """
    if marker in self.measurement_markers:
        marker.remove()
        self.measurement_markers.remove(marker)

        if self.selected_marker == marker:
            self.selected_marker = None

        logger.info(f"Removed measurement marker {marker.marker_id}")

clear_all_markers

clear_all_markers() -> None

Clear all measurement markers.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def clear_all_markers(self) -> None:
    """Clear all measurement markers."""
    for marker in self.measurement_markers[:]:
        marker.remove()

    self.measurement_markers.clear()
    self.selected_marker = None
    logger.info("Cleared all measurement markers")

set_marker_mode

set_marker_mode(mode: str, marker_type: str = None, channel: int = None) -> None

Set interaction mode for markers.

Parameters:

Name Type Description Default
mode str

Marker mode ('off', 'add', 'edit')

required
marker_type str

Type of marker to add (required if mode='add')

None
channel int

Channel for marker (required if mode='add')

None
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def set_marker_mode(self, mode: str, marker_type: str = None, channel: int = None) -> None:
    """Set interaction mode for markers.

    Args:
        mode: Marker mode ('off', 'add', 'edit')
        marker_type: Type of marker to add (required if mode='add')
        channel: Channel for marker (required if mode='add')
    """
    self.marker_mode = mode.lower()
    self.pending_marker_type = marker_type
    self.pending_marker_channel = channel

    logger.info(f"Marker mode set to: {self.marker_mode}")

    if mode == "off":
        for marker in self.measurement_markers:
            marker.set_selected(False)
        self.selected_marker = None

get_marker_measurements

get_marker_measurements() -> Dict

Get all measurement results from markers.

Returns:

Type Description
Dict

Dictionary mapping marker IDs to results

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def get_marker_measurements(self) -> Dict:
    """Get all measurement results from markers.

    Returns:
        Dictionary mapping marker IDs to results
    """
    return {
        marker.marker_id: {
            "type": marker.measurement_type,
            "channel": marker.channel,
            "result": marker.result,
            "unit": marker.unit,
            "enabled": marker.enabled,
        }
        for marker in self.measurement_markers
    }

update_all_markers

update_all_markers() -> None

Update all enabled markers with current waveform data.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def update_all_markers(self) -> None:
    """Update all enabled markers with current waveform data."""
    if not self.current_waveforms:
        return

    for marker in self.measurement_markers:
        if marker.enabled:
            waveform = next((w for w in self.current_waveforms if w.channel == marker.channel), None)

            if waveform:
                marker.update_measurement(waveform)

set_reference

set_reference(reference_data: dict)

Set reference waveform for overlay.

Parameters:

Name Type Description Default
reference_data dict

Reference data dictionary with 'time' and 'voltage' keys

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def set_reference(self, reference_data: dict):
    """Set reference waveform for overlay.

    Args:
        reference_data: Reference data dictionary with 'time' and 'voltage' keys
    """
    self.reference_data = reference_data
    self.show_reference = True
    self._plot_reference_overlay()
    logger.info("Reference waveform set")

clear_reference

clear_reference()

Clear reference waveform.

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def clear_reference(self):
    """Clear reference waveform."""
    self.reference_data = None
    self.show_reference = False
    self.show_difference = False

    if self.reference_item:
        self.plot_item.removeItem(self.reference_item)
        self.reference_item = None

    logger.info("Reference waveform cleared")

toggle_reference_visibility

toggle_reference_visibility(visible: bool)

Toggle reference waveform visibility.

Parameters:

Name Type Description Default
visible bool

Whether reference should be visible

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def toggle_reference_visibility(self, visible: bool):
    """Toggle reference waveform visibility.

    Args:
        visible: Whether reference should be visible
    """
    self.show_reference = visible

    if self.reference_item:
        self.reference_item.setVisible(visible)

toggle_difference_mode

toggle_difference_mode(enabled: bool)

Toggle difference mode (show waveform - reference).

Parameters:

Name Type Description Default
enabled bool

Whether to show difference

required
Source code in scpi_control/gui/widgets/waveform_display_pg.py
def toggle_difference_mode(self, enabled: bool):
    """Toggle difference mode (show waveform - reference).

    Args:
        enabled: Whether to show difference
    """
    self.show_difference = enabled
    self._plot_reference_overlay()

get_reference_data

get_reference_data()

Get current reference data.

Returns:

Type Description

Reference data dictionary or None

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def get_reference_data(self):
    """Get current reference data.

    Returns:
        Reference data dictionary or None
    """
    return self.reference_data

get_plot_item

get_plot_item()

Get the PyQtGraph PlotItem for advanced customization.

Returns:

Type Description

PyQtGraph PlotItem

Source code in scpi_control/gui/widgets/waveform_display_pg.py
def get_plot_item(self):
    """Get the PyQtGraph PlotItem for advanced customization.

    Returns:
        PyQtGraph PlotItem
    """
    return self.plot_item

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Channel Control

scpi_control.gui.widgets.channel_control

Channel control widget for oscilloscope GUI.

ChannelControl

ChannelControl(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for controlling oscilloscope channels.

Initialize channel control widget.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/channel_control.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize channel control widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self.scope: Optional[Oscilloscope] = None
    self.channel_widgets = {}
    self.channel_groups = {}  # Store group boxes for show/hide
    self._init_ui()

set_scope

set_scope(scope: Optional[Oscilloscope])

Set the oscilloscope instance.

Updates channel visibility based on model capability and refreshes controls.

Parameters:

Name Type Description Default
scope Optional[Oscilloscope]

Oscilloscope instance

required
Source code in scpi_control/gui/widgets/channel_control.py
def set_scope(self, scope: Optional[Oscilloscope]):
    """Set the oscilloscope instance.

    Updates channel visibility based on model capability and refreshes controls.

    Args:
        scope: Oscilloscope instance
    """
    self.scope = scope
    if scope and scope.model_capability:
        # Show/hide channels based on model capability
        num_channels = scope.model_capability.num_channels
        logger.info(f"Configuring GUI for {num_channels} channels")

        for ch_num in range(1, 5):
            group = self.channel_groups.get(ch_num)
            if group:
                if ch_num <= num_channels:
                    group.setVisible(True)
                    group.setEnabled(True)
                else:
                    group.setVisible(False)
                    group.setEnabled(False)

        self._refresh_all_channels()
    elif scope:
        # Scope connected but no capability info - show all channels
        logger.warning("Scope connected but model capability not available, showing all channels")
        for ch_num in range(1, 5):
            group = self.channel_groups.get(ch_num)
            if group:
                group.setVisible(True)
                group.setEnabled(True)
        self._refresh_all_channels()
    else:
        # No scope - hide all channels
        for ch_num in range(1, 5):
            group = self.channel_groups.get(ch_num)
            if group:
                group.setVisible(False)
                group.setEnabled(False)

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Trigger Control

scpi_control.gui.widgets.trigger_control

Trigger control widget for oscilloscope GUI.

TriggerControl

TriggerControl(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for controlling oscilloscope trigger settings.

Initialize trigger control widget.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/trigger_control.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize trigger control widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self.scope: Optional[Oscilloscope] = None
    self.widgets = {}
    self._init_ui()

set_scope

set_scope(scope: Optional[Oscilloscope])

Set the oscilloscope instance.

Parameters:

Name Type Description Default
scope Optional[Oscilloscope]

Oscilloscope instance

required
Source code in scpi_control/gui/widgets/trigger_control.py
def set_scope(self, scope: Optional[Oscilloscope]):
    """Set the oscilloscope instance.

    Args:
        scope: Oscilloscope instance
    """
    self.scope = scope
    if scope:
        self._refresh_trigger_settings()

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Timebase Control

scpi_control.gui.widgets.timebase_control

Timebase control widget for oscilloscope GUI.

TimebaseControl

TimebaseControl(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for controlling oscilloscope timebase (horizontal) settings.

Initialize timebase control widget.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/timebase_control.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize timebase control widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self.scope: Optional[Oscilloscope] = None
    self.widgets = {}
    self._init_ui()

set_scope

set_scope(scope: Optional[Oscilloscope])

Set the oscilloscope instance.

Parameters:

Name Type Description Default
scope Optional[Oscilloscope]

Oscilloscope instance

required
Source code in scpi_control/gui/widgets/timebase_control.py
def set_scope(self, scope: Optional[Oscilloscope]):
    """Set the oscilloscope instance.

    Args:
        scope: Oscilloscope instance
    """
    self.scope = scope
    if scope:
        self._refresh_settings()

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Measurement Panel

scpi_control.gui.widgets.measurement_panel

Measurement panel widget for oscilloscope GUI.

MeasurementPanel

MeasurementPanel(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for displaying and controlling measurements.

Initialize measurement panel widget.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/measurement_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize measurement panel widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self.scope: Optional[Oscilloscope] = None
    self.active_measurements: List[Dict] = []
    self.auto_update = False
    self.update_timer = QTimer()
    self.update_timer.timeout.connect(self._update_measurements)
    self._init_ui()

set_scope

set_scope(scope: Optional[Oscilloscope])

Set the oscilloscope instance.

Parameters:

Name Type Description Default
scope Optional[Oscilloscope]

Oscilloscope instance

required
Source code in scpi_control/gui/widgets/measurement_panel.py
def set_scope(self, scope: Optional[Oscilloscope]):
    """Set the oscilloscope instance.

    Args:
        scope: Oscilloscope instance
    """
    self.scope = scope

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Cursor Panel

scpi_control.gui.widgets.cursor_panel

Cursor control panel for waveform measurements.

CursorPanel

CursorPanel(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for controlling measurement cursors and displaying cursor values.

Provides controls for cursor mode selection and displays cursor measurements including time, voltage, and delta values.

Signals

cursor_mode_changed: Emitted when cursor mode changes (mode: str) clear_cursors: Emitted when clear cursors button is clicked

Initialize cursor panel.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/cursor_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize cursor panel.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.current_mode = "off"

    self._init_ui()
    logger.info("Cursor panel initialized")

set_mode

set_mode(mode: str)

Set cursor mode programmatically.

Parameters:

Name Type Description Default
mode str

Cursor mode ('off', 'vertical', 'horizontal', 'both')

required
Source code in scpi_control/gui/widgets/cursor_panel.py
def set_mode(self, mode: str):
    """Set cursor mode programmatically.

    Args:
        mode: Cursor mode ('off', 'vertical', 'horizontal', 'both')
    """
    mode = mode.lower()
    if mode == "off":
        self.off_radio.setChecked(True)
    elif mode == "vertical":
        self.vertical_radio.setChecked(True)
    elif mode == "horizontal":
        self.horizontal_radio.setChecked(True)
    elif mode == "both":
        self.both_radio.setChecked(True)

update_cursor_values

update_cursor_values(x1: Optional[float] = None, y1: Optional[float] = None, x2: Optional[float] = None, y2: Optional[float] = None)

Update cursor value displays.

Parameters:

Name Type Description Default
x1 Optional[float]

Cursor 1 X position (time)

None
y1 Optional[float]

Cursor 1 Y position (voltage)

None
x2 Optional[float]

Cursor 2 X position (time)

None
y2 Optional[float]

Cursor 2 Y position (voltage)

None
Source code in scpi_control/gui/widgets/cursor_panel.py
def update_cursor_values(
    self,
    x1: Optional[float] = None,
    y1: Optional[float] = None,
    x2: Optional[float] = None,
    y2: Optional[float] = None,
):
    """Update cursor value displays.

    Args:
        x1: Cursor 1 X position (time)
        y1: Cursor 1 Y position (voltage)
        x2: Cursor 2 X position (time)
        y2: Cursor 2 Y position (voltage)
    """
    # Update cursor 1
    if x1 is not None:
        self.x1_label.setText(self._format_time(x1))
    else:
        self.x1_label.setText("--")

    if y1 is not None:
        self.y1_label.setText(self._format_voltage(y1))
    else:
        self.y1_label.setText("--")

    # Update cursor 2
    if x2 is not None:
        self.x2_label.setText(self._format_time(x2))
    else:
        self.x2_label.setText("--")

    if y2 is not None:
        self.y2_label.setText(self._format_voltage(y2))
    else:
        self.y2_label.setText("--")

    # Calculate and update deltas
    if x1 is not None and x2 is not None:
        delta_time = x2 - x1
        self.delta_time_label.setText(self._format_time(delta_time))

        # Calculate frequency (1/ΔT) if ΔT is not zero
        if abs(delta_time) > 1e-15:
            frequency = 1.0 / abs(delta_time)
            self.frequency_label.setText(self._format_frequency(frequency))
        else:
            self.frequency_label.setText("∞")
    else:
        self.delta_time_label.setText("--")
        self.frequency_label.setText("--")

    if y1 is not None and y2 is not None:
        delta_voltage = y2 - y1
        self.delta_voltage_label.setText(self._format_voltage(delta_voltage))
    else:
        self.delta_voltage_label.setText("--")

clear_values

clear_values()

Clear all cursor value displays.

Source code in scpi_control/gui/widgets/cursor_panel.py
def clear_values(self):
    """Clear all cursor value displays."""
    self.x1_label.setText("--")
    self.y1_label.setText("--")
    self.x2_label.setText("--")
    self.y2_label.setText("--")
    self.delta_time_label.setText("--")
    self.delta_voltage_label.setText("--")
    self.frequency_label.setText("--")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Math Panel

scpi_control.gui.widgets.math_panel

Math channel control panel for waveform operations.

MathPanel

MathPanel(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for controlling math channels.

Provides expression builder, enable/disable toggles, and quick access buttons for common operations.

Signals

math1_expression_changed: Emitted when Math1 expression changes (expression: str) math2_expression_changed: Emitted when Math2 expression changes (expression: str) math1_enabled_changed: Emitted when Math1 enable state changes (enabled: bool) math2_enabled_changed: Emitted when Math2 enable state changes (enabled: bool)

Initialize math panel.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/math_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize math panel.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self._init_ui()
    logger.info("Math panel initialized")

set_math1_enabled

set_math1_enabled(enabled: bool)

Set Math1 enable state programmatically.

Parameters:

Name Type Description Default
enabled bool

Enable state

required
Source code in scpi_control/gui/widgets/math_panel.py
def set_math1_enabled(self, enabled: bool):
    """Set Math1 enable state programmatically.

    Args:
        enabled: Enable state
    """
    self.math1_enable.setChecked(enabled)

set_math2_enabled

set_math2_enabled(enabled: bool)

Set Math2 enable state programmatically.

Parameters:

Name Type Description Default
enabled bool

Enable state

required
Source code in scpi_control/gui/widgets/math_panel.py
def set_math2_enabled(self, enabled: bool):
    """Set Math2 enable state programmatically.

    Args:
        enabled: Enable state
    """
    self.math2_enable.setChecked(enabled)

set_math1_expression

set_math1_expression(expression: str)

Set Math1 expression programmatically.

Parameters:

Name Type Description Default
expression str

Expression string

required
Source code in scpi_control/gui/widgets/math_panel.py
def set_math1_expression(self, expression: str):
    """Set Math1 expression programmatically.

    Args:
        expression: Expression string
    """
    self.math1_expression.setText(expression)

set_math2_expression

set_math2_expression(expression: str)

Set Math2 expression programmatically.

Parameters:

Name Type Description Default
expression str

Expression string

required
Source code in scpi_control/gui/widgets/math_panel.py
def set_math2_expression(self, expression: str):
    """Set Math2 expression programmatically.

    Args:
        expression: Expression string
    """
    self.math2_expression.setText(expression)

get_math1_expression

get_math1_expression() -> str

Get Math1 expression.

Returns:

Type Description
str

Expression string

Source code in scpi_control/gui/widgets/math_panel.py
def get_math1_expression(self) -> str:
    """Get Math1 expression.

    Returns:
        Expression string
    """
    return self.math1_expression.text().strip()

get_math2_expression

get_math2_expression() -> str

Get Math2 expression.

Returns:

Type Description
str

Expression string

Source code in scpi_control/gui/widgets/math_panel.py
def get_math2_expression(self) -> str:
    """Get Math2 expression.

    Returns:
        Expression string
    """
    return self.math2_expression.text().strip()

is_math1_enabled

is_math1_enabled() -> bool

Check if Math1 is enabled.

Returns:

Type Description
bool

True if enabled

Source code in scpi_control/gui/widgets/math_panel.py
def is_math1_enabled(self) -> bool:
    """Check if Math1 is enabled.

    Returns:
        True if enabled
    """
    return self.math1_enable.isChecked()

is_math2_enabled

is_math2_enabled() -> bool

Check if Math2 is enabled.

Returns:

Type Description
bool

True if enabled

Source code in scpi_control/gui/widgets/math_panel.py
def is_math2_enabled(self) -> bool:
    """Check if Math2 is enabled.

    Returns:
        True if enabled
    """
    return self.math2_enable.isChecked()

update_available_channels

update_available_channels(num_channels: int)

Update available channels in channel selector.

Parameters:

Name Type Description Default
num_channels int

Number of available channels (2 or 4)

required
Source code in scpi_control/gui/widgets/math_panel.py
def update_available_channels(self, num_channels: int):
    """Update available channels in channel selector.

    Args:
        num_channels: Number of available channels (2 or 4)
    """
    channels = [f"C{i+1}" for i in range(num_channels)]

    self.math1_channel_combo.clear()
    self.math1_channel_combo.addItems(channels)

    self.math2_channel_combo.clear()
    self.math2_channel_combo.addItems(channels)

    logger.info(f"Math panel updated for {num_channels} channels")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Reference Panel

scpi_control.gui.widgets.reference_panel

Reference waveform panel for managing and comparing reference waveforms.

ReferencePanel

ReferencePanel(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for managing reference waveforms.

Provides a browser for saved reference waveforms with options to load, delete, and view information about each reference.

Signals

load_reference: Emitted when user requests to load a reference (filepath: str) save_reference: Emitted when user requests to save current waveform as reference delete_reference: Emitted when user deletes a reference (filepath: str) show_difference: Emitted when user wants to show difference with reference

Initialize reference panel.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/reference_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize reference panel.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.current_references = []
    self.loaded_reference = None

    self._init_ui()
    logger.info("Reference panel initialized")

update_reference_list

update_reference_list(references: list)

Update the reference list display.

Parameters:

Name Type Description Default
references list

List of reference info dictionaries

required
Source code in scpi_control/gui/widgets/reference_panel.py
def update_reference_list(self, references: list):
    """Update the reference list display.

    Args:
        references: List of reference info dictionaries
    """
    self.reference_list.clear()
    self.current_references = references

    for ref in references:
        # Create display text
        name = ref.get("name", "Unknown")
        channel = ref.get("channel", "Unknown")
        timestamp = ref.get("timestamp", "")

        # Format timestamp
        if timestamp:
            try:
                from datetime import datetime

                dt = datetime.fromisoformat(timestamp)
                time_str = dt.strftime("%Y-%m-%d %H:%M")
            except:
                time_str = "Unknown time"
        else:
            time_str = "Unknown time"

        # Format file size
        file_size = ref.get("file_size", 0)
        if file_size < 1024:
            size_str = f"{file_size} B"
        elif file_size < 1024 * 1024:
            size_str = f"{file_size / 1024:.1f} KB"
        else:
            size_str = f"{file_size / (1024 * 1024):.1f} MB"

        # Create item text
        item_text = f"{name}\n{channel} | {time_str} | {size_str}"

        # Create list item
        item = QListWidgetItem(item_text)
        item.setData(Qt.ItemDataRole.UserRole, ref.get("filepath"))

        self.reference_list.addItem(item)

    logger.info(f"Reference list updated with {len(references)} items")

set_loaded_reference

set_loaded_reference(reference_data: Dict[str, Any])

Update display when a reference is loaded.

Parameters:

Name Type Description Default
reference_data Dict[str, Any]

Loaded reference data

required
Source code in scpi_control/gui/widgets/reference_panel.py
def set_loaded_reference(self, reference_data: Dict[str, Any]):
    """Update display when a reference is loaded.

    Args:
        reference_data: Loaded reference data
    """
    self.loaded_reference = reference_data

    # Update info label
    metadata = reference_data.get("metadata", {})
    name = metadata.get("name", "Unknown")
    channel = metadata.get("channel", "Unknown")
    num_samples = metadata.get("num_samples", 0)
    time_span = metadata.get("time_span", 0.0)

    # Format time span
    if time_span < 1e-6:
        time_str = f"{time_span * 1e9:.2f} ns"
    elif time_span < 1e-3:
        time_str = f"{time_span * 1e6:.2f} µs"
    elif time_span < 1:
        time_str = f"{time_span * 1e3:.2f} ms"
    else:
        time_str = f"{time_span:.2f} s"

    info_text = f"<b>Loaded:</b> {name}<br>" f"<b>Channel:</b> {channel}<br>" f"<b>Samples:</b> {num_samples}<br>" f"<b>Time Span:</b> {time_str}"

    self.info_label.setText(info_text)

    # Enable comparison tools
    self.show_diff_btn.setEnabled(True)

    logger.info(f"Reference loaded: {name}")

update_comparison_stats

update_comparison_stats(correlation: Optional[float], rms_diff: Optional[float])

Update comparison statistics display.

Parameters:

Name Type Description Default
correlation Optional[float]

Correlation coefficient (0.0 to 1.0)

required
rms_diff Optional[float]

RMS difference value

required
Source code in scpi_control/gui/widgets/reference_panel.py
def update_comparison_stats(self, correlation: Optional[float], rms_diff: Optional[float]):
    """Update comparison statistics display.

    Args:
        correlation: Correlation coefficient (0.0 to 1.0)
        rms_diff: RMS difference value
    """
    if correlation is not None:
        # Color code correlation
        if correlation > 0.95:
            color = "green"
        elif correlation > 0.8:
            color = "orange"
        else:
            color = "red"

        self.correlation_label.setText(f"Correlation: <span style='color: {color}; font-weight: bold;'>{correlation:.4f}</span>")
    else:
        self.correlation_label.setText("Correlation: --")

    if rms_diff is not None:
        # Format RMS difference with appropriate units
        if abs(rms_diff) < 1e-3:
            diff_str = f"{rms_diff * 1e6:.3f} µV"
        elif abs(rms_diff) < 1:
            diff_str = f"{rms_diff * 1e3:.3f} mV"
        else:
            diff_str = f"{rms_diff:.3f} V"

        self.stats_label.setText(f"RMS Difference: {diff_str}")
    else:
        self.stats_label.setText("RMS Difference: --")

clear_comparison_stats

clear_comparison_stats()

Clear comparison statistics display.

Source code in scpi_control/gui/widgets/reference_panel.py
def clear_comparison_stats(self):
    """Clear comparison statistics display."""
    self.correlation_label.setText("Correlation: --")
    self.stats_label.setText("RMS Difference: --")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Protocol Decode Panel

scpi_control.gui.widgets.protocol_decode_panel

Protocol decode panel for analyzing digital communication protocols.

ProtocolDecodePanel

ProtocolDecodePanel(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for protocol decode configuration and display.

Provides protocol selection, parameter configuration, and decoded event display in a table format.

Signals

decode_requested: Emitted when user requests decode (protocol: str, params: dict, channel_map: dict) export_requested: Emitted when user wants to export events

Initialize protocol decode panel.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/protocol_decode_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize protocol decode panel.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.current_protocol = None
    self.decoded_events = []

    self._init_ui()
    logger.info("Protocol decode panel initialized")

display_events

display_events(events: list)

Display decoded events in table.

Parameters:

Name Type Description Default
events list

List of DecodedEvent objects

required
Source code in scpi_control/gui/widgets/protocol_decode_panel.py
def display_events(self, events: list):
    """Display decoded events in table.

    Args:
        events: List of DecodedEvent objects
    """
    self.decoded_events = events
    self.events_table.setRowCount(len(events))

    for i, event in enumerate(events):
        # Timestamp
        time_item = QTableWidgetItem(f"{event.timestamp:.6f}")
        self.events_table.setItem(i, 0, time_item)

        # Event type
        type_item = QTableWidgetItem(event.event_type.value)
        self.events_table.setItem(i, 1, type_item)

        # Data
        data_str = str(event.data) if event.data is not None else ""
        data_item = QTableWidgetItem(data_str)
        self.events_table.setItem(i, 2, data_item)

        # Description
        desc_item = QTableWidgetItem(event.description)
        self.events_table.setItem(i, 3, desc_item)

        # Status
        status_item = QTableWidgetItem("✓" if event.valid else "✗")
        if not event.valid:
            status_item.setBackground(QColor(255, 200, 200))
        self.events_table.setItem(i, 4, status_item)

    # Resize columns to content
    self.events_table.resizeColumnsToContents()

    # Update count
    self.event_count_label.setText(f"{len(events)} events")

    logger.info(f"Displayed {len(events)} events")

get_events

get_events() -> list

Get current decoded events.

Returns:

Type Description
list

List of decoded events

Source code in scpi_control/gui/widgets/protocol_decode_panel.py
def get_events(self) -> list:
    """Get current decoded events.

    Returns:
        List of decoded events
    """
    return self.decoded_events

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

FFT Display

scpi_control.gui.widgets.fft_display

FFT display widget for frequency domain analysis.

FFTDisplay

FFTDisplay(parent: Optional[QWidget] = None)

Bases: QWidget

Widget for displaying FFT analysis results.

Provides frequency domain plot with controls for window function, scale selection, and peak detection.

Signals

fft_compute_requested: Emitted when FFT computation is requested (channel: str, window: str)

Initialize FFT display.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/fft_display.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize FFT display.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)

    self.fft_result = None
    self.peak_markers = []

    self._init_ui()
    logger.info("FFT display initialized")

set_fft_result

set_fft_result(fft_result)

Set and display FFT result.

Parameters:

Name Type Description Default
fft_result

FFTResult object

required
Source code in scpi_control/gui/widgets/fft_display.py
def set_fft_result(self, fft_result):
    """Set and display FFT result.

    Args:
        fft_result: FFTResult object
    """
    self.fft_result = fft_result
    self._plot_fft(fft_result)
    self._update_peaks()

clear_display

clear_display()

Clear the FFT display.

Source code in scpi_control/gui/widgets/fft_display.py
def clear_display(self):
    """Clear the FFT display."""
    self.fft_result = None
    self.ax.clear()
    self.ax.set_xlabel("Frequency (Hz)")
    self.ax.set_ylabel("Magnitude (dB)")
    self.ax.set_title("FFT Spectrum")
    self.ax.grid(True, alpha=0.3)
    self.canvas.draw()

    for label in self.peak_labels:
        label.setText("--")

update_available_channels

update_available_channels(num_channels: int)

Update available channels in channel selector.

Parameters:

Name Type Description Default
num_channels int

Number of available channels (2 or 4)

required
Source code in scpi_control/gui/widgets/fft_display.py
def update_available_channels(self, num_channels: int):
    """Update available channels in channel selector.

    Args:
        num_channels: Number of available channels (2 or 4)
    """
    channels = [f"C{i+1}" for i in range(num_channels)]
    channels.extend(["M1", "M2"])  # Always include math channels

    current_channel = self.channel_combo.currentText()
    self.channel_combo.clear()
    self.channel_combo.addItems(channels)

    # Restore selection if still valid
    if current_channel in channels:
        self.channel_combo.setCurrentText(current_channel)

    logger.info(f"FFT display updated for {num_channels} channels")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Visual Measurement Panel

scpi_control.gui.widgets.visual_measurement_panel

Visual measurement panel for interactive waveform measurements.

This module provides the UI panel for managing visual measurement markers on waveforms. Users can add measurement markers of different types, adjust their positions, and see real-time measurement results.

Features
  • 15+ measurement types (frequency, voltage, timing)
  • Interactive marker placement and adjustment
  • Save/load measurement configurations
  • Export results to CSV/JSON
  • Auto-update mode with configurable refresh rate
  • Batch measurement support
Supported Measurement Types
  • Frequency/Period: FREQ, PER
  • Voltage: PKPK, AMPL, MAX, MIN, RMS, MEAN, TOP, BASE
  • Timing: RISE, FALL, WID, NWID, DUTY
Architecture

The panel acts as a controller for visual markers on the waveform display. It creates marker objects and adds them to the display widget, managing their lifecycle and updating their measurements.

Example

panel = VisualMeasurementPanel(waveform_display) panel.marker_added.connect(on_marker_added)

User clicks "Add Marker" button

Marker appears on waveform with live measurements

VisualMeasurementPanel

VisualMeasurementPanel(waveform_display: WaveformDisplay, parent: Optional[QWidget] = None)

Bases: QWidget

Panel for controlling visual measurement markers.

Provides UI for: - Adding/removing markers - Enabling/disabling markers - Updating measurements - Saving/loading configurations - Exporting results

Signals

marker_added: Emitted when a marker is added marker_removed: Emitted when a marker is removed measurements_updated: Emitted when measurements are updated

Initialize visual measurement panel.

Parameters:

Name Type Description Default
waveform_display WaveformDisplay

WaveformDisplay widget to control

required
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/visual_measurement_panel.py
def __init__(self, waveform_display: "WaveformDisplay", parent: Optional[QWidget] = None):
    """Initialize visual measurement panel.

    Args:
        waveform_display: WaveformDisplay widget to control
        parent: Parent widget
    """
    super().__init__(parent)

    self.waveform_display = waveform_display
    self.marker_counter = 0  # For generating unique marker IDs
    self.auto_update_enabled = False

    # Auto-update timer
    self.auto_update_timer = QTimer()
    self.auto_update_timer.timeout.connect(self._on_auto_update)
    self.auto_update_timer.setInterval(1000)  # 1 second

    self._init_ui()
    logger.info("Visual measurement panel initialized")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Vector Graphics Panel

scpi_control.gui.widgets.vector_graphics_panel

Vector graphics panel for XY mode drawing on oscilloscope.

This panel provides UI controls for generating vector graphics waveforms that can be displayed in XY mode on the oscilloscope.

Features
  • Multiple shape types (circle, rectangle, star, etc.)
  • Parameter controls for each shape
  • Live preview of generated paths
  • Waveform generation and export
  • Animation frame generation

Requires 'fun' extras installation: pip install "Siglent-Oscilloscope[fun]"

VectorGraphicsPanel

VectorGraphicsPanel(parent: Optional[QWidget] = None)

Bases: QWidget

Panel for vector graphics generation and XY mode control.

This panel allows users to generate waveforms for drawing shapes on the oscilloscope in XY mode.

Initialize vector graphics panel.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/widgets/vector_graphics_panel.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize vector graphics panel.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self._scope = None
    self._vector_display = None
    self._current_path: Optional[VectorPath] = None

    self._init_ui()

set_scope

set_scope(scope)

Set the oscilloscope instance.

Parameters:

Name Type Description Default
scope

Oscilloscope instance or None

required
Source code in scpi_control/gui/widgets/vector_graphics_panel.py
def set_scope(self, scope):
    """Set the oscilloscope instance.

    Args:
        scope: Oscilloscope instance or None
    """
    self._scope = scope
    self._vector_display = None

    if VECTOR_GRAPHICS_AVAILABLE:
        if scope:
            self.enable_xy_btn.setEnabled(True)
            self.info_text.setPlainText(f"Connected to: {scope.model_capability.model_name}\n\n" "Ready to configure XY mode!")
        else:
            self.enable_xy_btn.setEnabled(False)
            self.disable_xy_btn.setEnabled(False)
            self.info_text.setPlainText("Not connected.\n\n" "You can still generate and export waveforms!")

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Terminal Widget

scpi_control.gui.widgets.terminal_widget

Terminal widget for sending custom SCPI commands to the oscilloscope.

TerminalWidget

TerminalWidget(parent=None)

Bases: QWidget

Terminal widget for sending custom SCPI commands.

Provides a console-like interface for sending raw SCPI commands to the oscilloscope and viewing responses.

Initialize terminal widget.

Parameters:

Name Type Description Default
parent

Parent widget

None
Source code in scpi_control/gui/widgets/terminal_widget.py
def __init__(self, parent=None):
    """Initialize terminal widget.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self._scope = None
    self._command_history = []
    self._history_index = -1

    self._init_ui()

set_oscilloscope

set_oscilloscope(scope)

Set the oscilloscope instance.

Parameters:

Name Type Description Default
scope

Oscilloscope instance

required
Source code in scpi_control/gui/widgets/terminal_widget.py
def set_oscilloscope(self, scope):
    """Set the oscilloscope instance.

    Args:
        scope: Oscilloscope instance
    """
    self._scope = scope
    if scope:
        self._append_output("=== Oscilloscope Connected ===", "#4CAF50")
        self._append_output("")
    else:
        self._append_output("=== Oscilloscope Disconnected ===", "#f44336")
        self._append_output("")

send_command

send_command(command: str)

Programmatically send a command.

Parameters:

Name Type Description Default
command str

SCPI command to send

required
Source code in scpi_control/gui/widgets/terminal_widget.py
def send_command(self, command: str):
    """Programmatically send a command.

    Args:
        command: SCPI command to send
    """
    self.command_input.setText(command)
    self._on_send_command()

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Error Dialog

scpi_control.gui.widgets.error_dialog

Detailed error dialog widget for displaying user-friendly error messages.

This module provides an error dialog that shows both user-friendly summaries and expandable technical details for debugging.

DetailedErrorDialog

DetailedErrorDialog(error_info: dict, parent=None)

Bases: QDialog

Shows user-friendly error messages with expandable technical details.

This dialog provides two levels of information: 1. User-friendly summary for non-technical users 2. Technical details (stack trace, context) for debugging

Example

error_info = { ... 'type': 'TimeoutError', ... 'message': 'Timeout acquiring waveform from CH1', ... 'details': 'Socket timeout after 5.0 seconds', ... 'context': {'channel': 1, 'operation': 'get_waveform'}, ... 'traceback': '...' ... } dialog = DetailedErrorDialog(error_info, parent=main_window) dialog.exec()

Initialize the error dialog.

Parameters:

Name Type Description Default
error_info dict

Dictionary containing error details: - type: Error type name (e.g., 'TimeoutError') - message: User-friendly error message - details: Additional error details (optional) - context: Dictionary of context info (optional) - traceback: Stack trace string (optional) - timestamp: Error timestamp (optional, defaults to now)

required
parent

Parent widget

None
Source code in scpi_control/gui/widgets/error_dialog.py
def __init__(self, error_info: dict, parent=None):
    """Initialize the error dialog.

    Args:
        error_info: Dictionary containing error details:
            - type: Error type name (e.g., 'TimeoutError')
            - message: User-friendly error message
            - details: Additional error details (optional)
            - context: Dictionary of context info (optional)
            - traceback: Stack trace string (optional)
            - timestamp: Error timestamp (optional, defaults to now)
        parent: Parent widget
    """
    super().__init__(parent)
    self.error_info = error_info
    self.setWindowTitle("Error Occurred")
    self.resize(500, 300)

    self._setup_ui()

show_error_dialog

show_error_dialog(error_info: dict, parent=None) -> int

Convenience function to show an error dialog.

Parameters:

Name Type Description Default
error_info dict

Dictionary containing error details (see DetailedErrorDialog)

required
parent

Parent widget

None

Returns:

Type Description
int

Dialog result code (typically QDialog.DialogCode.Accepted)

Example

show_error_dialog({ ... 'type': 'ConnectionError', ... 'message': 'Failed to connect to oscilloscope', ... 'details': 'Connection refused on port 5024' ... })

Source code in scpi_control/gui/widgets/error_dialog.py
def show_error_dialog(error_info: dict, parent=None) -> int:
    """Convenience function to show an error dialog.

    Args:
        error_info: Dictionary containing error details (see DetailedErrorDialog)
        parent: Parent widget

    Returns:
        Dialog result code (typically QDialog.DialogCode.Accepted)

    Example:
        >>> show_error_dialog({
        ...     'type': 'ConnectionError',
        ...     'message': 'Failed to connect to oscilloscope',
        ...     'details': 'Connection refused on port 5024'
        ... })
    """
    dialog = DetailedErrorDialog(error_info, parent)
    return dialog.exec()

options: show_root_heading: false show_source: true heading_level: 4 members_order: source show_signature_annotations: true filters: - "!^*"

Workers

Live View Worker

scpi_control.gui.live_view_worker

Background worker for live view waveform acquisition.

This module provides a QThread-based worker that continuously acquires waveforms from the oscilloscope without blocking the GUI thread.

The worker solves a critical performance issue: SCPI queries take 100-500ms each, and querying multiple channels every 200ms would freeze the GUI. By running acquisition in a background thread, the GUI remains responsive while waveforms are continuously updated.

Thread Safety
  • Uses Qt signals/slots for thread-safe communication
  • Worker runs in separate thread via QThread
  • GUI thread only handles display updates (<1ms)
  • No shared mutable state between threads
Signals

waveforms_ready(list): Emitted when new waveforms are acquired error_occurred(dict): Emitted on acquisition errors with structured error info status_update(str): Emitted for status updates during acquisition

Example

worker = LiveViewWorker(scope) worker.waveforms_ready.connect(display.plot_multiple_waveforms) worker.error_occurred.connect(handle_error) worker.start()

... later ...

worker.stop()

LiveViewWorker

LiveViewWorker(scope, parent=None)

Bases: QThread

Background thread worker for acquiring waveforms without blocking GUI.

Signals

waveforms_ready: Emitted when waveforms are acquired (List[WaveformData]) error_occurred: Emitted when an error occurs (dict with error details) status_update: Emitted for status messages (str)

Initialize live view worker.

Parameters:

Name Type Description Default
scope

Oscilloscope instance

required
parent

Parent QObject

None
Source code in scpi_control/gui/live_view_worker.py
def __init__(self, scope, parent=None):
    """Initialize live view worker.

    Args:
        scope: Oscilloscope instance
        parent: Parent QObject
    """
    super().__init__(parent)
    self.scope = scope
    self.running = False
    self.update_interval = 200  # ms

run

run()

Thread run method - continuously acquires waveforms.

Source code in scpi_control/gui/live_view_worker.py
def run(self):
    """Thread run method - continuously acquires waveforms."""
    self.running = True
    logger.info("Live view worker thread started")

    while self.running:
        try:
            # Acquire waveforms from enabled channels
            waveforms = self._acquire_waveforms()

            if waveforms:
                # Emit signal with waveforms
                self.waveforms_ready.emit(waveforms)
            else:
                logger.debug("No waveforms acquired in this cycle")

        except Exception as e:
            logger.error(f"Error in live view worker: {e}", exc_info=True)

            # Create structured error info for detailed error dialog
            error_info = {
                "type": type(e).__name__,
                "message": f"Live view error: {str(e)}",
                "details": str(e),
                "context": {
                    "operation": "live_view_acquisition",
                    "update_interval": f"{self.update_interval}ms",
                },
                "traceback": traceback.format_exc(),
                "timestamp": datetime.now(),
            }
            self.error_occurred.emit(error_info)

        # Sleep for update interval
        self.msleep(self.update_interval)

    logger.info("Live view worker thread stopped")

stop

stop()

Stop the worker thread.

Source code in scpi_control/gui/live_view_worker.py
def stop(self):
    """Stop the worker thread."""
    logger.info("Stopping live view worker...")
    self.running = False
    self.wait(2000)  # Wait up to 2 seconds for thread to finish

options: show_root_heading: false show_source: true heading_level: 3 members_order: source show_signature_annotations: true filters: - "!^*"

Waveform Capture Worker

scpi_control.gui.waveform_capture_worker

Background worker for single waveform capture.

This module provides a QThread-based worker that acquires waveforms from the oscilloscope without blocking the GUI thread.

The worker solves the GUI freeze issue: SCPI queries take 100-500ms per channel, and with network timeouts up to 5 seconds, the GUI would completely freeze during capture. By running acquisition in a background thread, the GUI remains responsive and can show progress updates and allow cancellation.

Thread Safety
  • Uses Qt signals/slots for thread-safe communication
  • Worker runs in separate thread via QThread
  • GUI thread only handles display updates
  • No shared mutable state between threads
Signals

progress_update(str, int): Emitted for progress updates (message, percentage) waveforms_ready(list): Emitted when waveforms are acquired capture_complete(int): Emitted when capture completes (number of waveforms) error_occurred(str): Emitted on acquisition errors

WaveformCaptureWorker

WaveformCaptureWorker(scope, enabled_channels: List[int], parent=None)

Bases: QThread

Background thread worker for capturing waveforms without blocking GUI.

Signals

progress_update: Emitted for progress (message: str, percentage: int) waveforms_ready: Emitted when waveforms are acquired (List[WaveformData]) capture_complete: Emitted when capture completes (waveform_count: int) error_occurred: Emitted when an error occurs (str)

Initialize waveform capture worker.

Parameters:

Name Type Description Default
scope

Oscilloscope instance

required
enabled_channels List[int]

List of channel numbers to capture from

required
parent

Parent QObject

None
Source code in scpi_control/gui/waveform_capture_worker.py
def __init__(self, scope, enabled_channels: List[int], parent=None):
    """Initialize waveform capture worker.

    Args:
        scope: Oscilloscope instance
        enabled_channels: List of channel numbers to capture from
        parent: Parent QObject
    """
    super().__init__(parent)
    self.scope = scope
    self.enabled_channels = enabled_channels
    self._cancelled = False

run

run()

Thread run method - captures waveforms from enabled channels.

Source code in scpi_control/gui/waveform_capture_worker.py
def run(self):
    """Thread run method - captures waveforms from enabled channels."""
    logger.info(f"Capture worker started for channels: {self.enabled_channels}")

    waveforms = []
    errors = []
    total_channels = len(self.enabled_channels)

    for idx, ch_num in enumerate(self.enabled_channels):
        if self._cancelled:
            logger.info("Capture cancelled by user")
            self.error_occurred.emit("Capture cancelled")
            return

        # Update progress
        percentage = int((idx / total_channels) * 100)
        self.progress_update.emit(f"Downloading CH{ch_num} data from scope...", percentage)
        logger.info(f"Capturing waveform from channel {ch_num}...")

        try:
            # This may take several seconds for large waveforms (e.g., 5M samples)
            waveform = self.scope.get_waveform(ch_num)
            if waveform:
                logger.info(f"Got waveform from CH{ch_num}: {len(waveform.voltage)} samples, " f"voltage range: {waveform.voltage.min():.3f} to {waveform.voltage.max():.3f} V")
                waveforms.append(waveform)
            else:
                error_msg = f"CH{ch_num}: No data returned"
                logger.warning(error_msg)
                errors.append(error_msg)

        except Exception as e:
            error_msg = f"CH{ch_num}: {str(e)}"
            logger.error(f"Failed to capture from channel {ch_num}: {e}", exc_info=True)
            errors.append(error_msg)

    # Final progress update
    self.progress_update.emit("Validating waveforms...", 100)

    # Validate all captured waveforms before emitting
    if waveforms:
        valid_waveforms, invalid_info = WaveformValidator.validate_multiple(waveforms)

        # Log validation results
        for channel, issues in invalid_info:
            logger.warning(f"Capture worker: Invalid waveform CH{channel}: {'; '.join(issues)}")
            errors.append(f"CH{channel} validation failed: {'; '.join(issues)}")

        if valid_waveforms:
            logger.info(f"Capture complete: {len(valid_waveforms)} valid waveform(s) captured")
            self.waveforms_ready.emit(valid_waveforms)
            self.capture_complete.emit(len(valid_waveforms))
        else:
            error_msg = f"All {len(waveforms)} captured waveform(s) failed validation."
            if errors:
                error_msg += "\n\nErrors:\n" + "\n".join(errors[:3])  # Show first 3 errors
            logger.error(f"Capture failed: {error_msg}")
            self.error_occurred.emit(error_msg)
    else:
        error_msg = "Could not capture waveforms."
        if errors:
            error_msg += "\n\nErrors:\n" + "\n".join(errors[:3])  # Show first 3 errors
        logger.error(f"Capture failed: {error_msg}")
        self.error_occurred.emit(error_msg)

    logger.info("Capture worker thread finished")

cancel

cancel()

Cancel the capture operation.

Source code in scpi_control/gui/waveform_capture_worker.py
def cancel(self):
    """Cancel the capture operation."""
    logger.info("Cancelling capture...")
    self._cancelled = True

options: show_root_heading: false show_source: true heading_level: 3 members_order: source show_signature_annotations: true filters: - "!^*"

VNC Window

scpi_control.gui.vnc_window

Separate window for VNC oscilloscope display.

VNCWindow

VNCWindow(parent: Optional[QWidget] = None)

Bases: QMainWindow

Separate window for displaying VNC oscilloscope interface.

Initialize VNC window.

Parameters:

Name Type Description Default
parent Optional[QWidget]

Parent widget

None
Source code in scpi_control/gui/vnc_window.py
def __init__(self, parent: Optional[QWidget] = None):
    """Initialize VNC window.

    Args:
        parent: Parent widget
    """
    super().__init__(parent)
    self.scope_ip: Optional[str] = None
    self._init_ui()

set_scope_ip

set_scope_ip(ip: str)

Set the oscilloscope IP address and load interface.

Parameters:

Name Type Description Default
ip str

Oscilloscope IP address

required
Source code in scpi_control/gui/vnc_window.py
def set_scope_ip(self, ip: str):
    """Set the oscilloscope IP address and load interface.

    Args:
        ip: Oscilloscope IP address
    """
    self.scope_ip = ip
    self.url_input.setText(ip)
    self._load_scope_interface()

keyPressEvent

keyPressEvent(event)

Handle key press events.

Parameters:

Name Type Description Default
event

Key event

required
Source code in scpi_control/gui/vnc_window.py
def keyPressEvent(self, event):
    """Handle key press events.

    Args:
        event: Key event
    """
    # ESC key exits fullscreen
    if event.key() == Qt.Key.Key_Escape and self.isFullScreen():
        self.showNormal()
    else:
        super().keyPressEvent(event)

options: show_root_heading: false show_source: true heading_level: 3 members_order: source show_signature_annotations: true filters: - "!^*"

See Also