Phase 6: strip signature-restating docstrings; correct README/SETUP

- Collapsed Args:/Returns:/Raises: blocks that only restated the
  signature (364 lines): tektronix_base 48% -> ~20% doc density,
  helios_laser and uc480_camera likewise. Only docstrings whose entire
  body was those sections were touched.
- Preserved verbatim the comments that carry hardware knowledge the code
  can't express: uc480's USB split-transaction contention note (with its
  measured fps), the IS_ALLOW_STARTER_FW_UPLOAD segfault explanation, the
  QImage-copy rationale, and tektronix's NUMFRAMESACQuired warning.
- README: project structure, quick start, and every usage example now
  describe code that exists (they referenced hardware/bbd202.py,
  CoherentHOPSLaser, get_curve_binary, and 'python -m scanengine.app',
  none of which do). Added a headless-scan example and a read-a-scan-file
  example, since reuse without the GUI is the point of the refactor.
- SETUP: structure section defers to README instead of keeping a second
  stale copy; documents the vendored uEye SDK and the Genesis quarantine.
- ruff is now clean repo-wide: fixed the remaining raise-from, unused
  loop variables, placeholder f-strings, and a non-strict zip; the
  widget-layout semicolon idiom is an explicit config ignore rather than
  22 standing warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-28 11:27:36 -05:00
parent 44febe34b8
commit 709dc529df
10 changed files with 264 additions and 584 deletions
+30 -262
View File
@@ -93,13 +93,7 @@ class TektronixOscilloscopeBase:
self._connected = False
def connect(self):
"""
Establish TCP socket connection to the oscilloscope.
Raises:
ValueError: If resource_name is not provided
ConnectionError: If connection fails
"""
"""Establish TCP socket connection to the oscilloscope."""
if not self.resource_name:
raise ValueError("resource_name (IP address/hostname) must be provided")
@@ -114,7 +108,8 @@ class TektronixOscilloscopeBase:
except socket.error as e:
self.socket = None
self._connected = False
raise ConnectionError(f"Failed to connect to {self.resource_name}:{self.port} - {e}")
raise ConnectionError(
f"Failed to connect to {self.resource_name}:{self.port} - {e}") from e
def disconnect(self):
"""
@@ -130,18 +125,7 @@ class TektronixOscilloscopeBase:
self._connected = False
def _normalize_channel(self, channel):
"""
Normalize channel input to integer (1-4).
Args:
channel: Channel as int (1-4) or string ('CH1'-'CH4')
Returns:
int: Channel number (1-4)
Raises:
ValueError: If channel is invalid
"""
"""Normalize channel input to integer (1-4)."""
if isinstance(channel, str):
channel_upper = channel.upper()
if not (channel_upper.startswith('CH') and len(channel_upper) == 3 and channel_upper[2].isdigit()):
@@ -154,27 +138,13 @@ class TektronixOscilloscopeBase:
return channel
def set_acquire_mode(self, mode):
"""
Set the acquisition mode.
Args:
mode: Acquisition mode to set (case-insensitive). Valid options:
'SAMple' or 'SAM' or 'SAMPLE' - Sample mode (default)
'PEAKdetect' or 'PEAK' or 'PEAKDETECT' - Peak detect mode
'HIRes' or 'HIR' or 'HIRES' - High resolution mode
'AVErage' or 'AVE' or 'AVERAGE' - Average mode
'ENVelope' or 'ENV' or 'ENVELOPE' - Envelope mode
Raises:
ValueError: If mode is not valid
RuntimeError: If not connected
"""
"""Set the acquisition mode."""
# Normalize mode to uppercase for comparison
mode_upper = mode.upper()
# Check if mode is valid (any short or long form)
valid = False
for long_form, variants in self.ACQUIRE_MODES.items():
for variants in self.ACQUIRE_MODES.values():
if mode_upper in [v.upper() for v in variants]:
valid = True
break
@@ -189,30 +159,12 @@ class TektronixOscilloscopeBase:
self.write(f"ACQuire:MODe {mode}")
def get_fastframe_state(self):
"""
Query the current FastFrame state.
Returns:
int: 0 if FastFrame is off, 1 if active
Raises:
RuntimeError: If not connected
"""
"""Query the current FastFrame state."""
response = self.query("HORizontal:FASTframe:STATE?")
return int(response)
def set_fastframe_state(self, state):
"""
Enable or disable FastFrame mode.
Args:
state: FastFrame state (0 = off, 1 = active)
Can be int (0/1) or bool (False/True)
Raises:
ValueError: If state is not valid
RuntimeError: If not connected
"""
"""Enable or disable FastFrame mode."""
# Convert boolean to int if needed
if isinstance(state, bool):
state = 1 if state else 0
@@ -224,16 +176,7 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:FASTframe:STATE {state}")
def set_fastframe_count(self, count):
"""
Set the number of FastFrame frames.
Args:
count: Number of frames to capture (must be positive integer)
Raises:
ValueError: If count is not valid
RuntimeError: If not connected
"""
"""Set the number of FastFrame frames."""
# Validate count
if not isinstance(count, int) or count <= 0:
raise ValueError(f"Invalid frame count: {count}. Must be a positive integer")
@@ -241,29 +184,12 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:FASTframe:COUNt {count}")
def get_record_length(self):
"""
Query the current horizontal record length.
Returns:
int: Current record length in samples
Raises:
RuntimeError: If not connected
"""
"""Query the current horizontal record length."""
response = self.query("HORizontal:MODe:RECOrdlength?")
return int(response)
def set_sample_rate(self, rate):
"""
Set the horizontal sample rate.
Args:
rate: Sample rate in samples per second (must be positive number)
Raises:
ValueError: If rate is not valid
RuntimeError: If not connected
"""
"""Set the horizontal sample rate."""
# Validate rate
if not isinstance(rate, (int, float)) or rate <= 0:
raise ValueError(f"Invalid sample rate: {rate}. Must be a positive number")
@@ -271,24 +197,12 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:MODe:SAMPLERate {rate}")
def set_trigger_slope(self, slope):
"""
Set the trigger slope.
Args:
slope: Trigger slope (case-insensitive). Valid options:
'RISe' or 'RISE' - Rising edge
'FALL' - Falling edge
'EITher' or 'EITHER' - Either edge
Raises:
ValueError: If slope is not valid
RuntimeError: If not connected
"""
"""Set the trigger slope."""
slope_upper = slope.upper()
# Check if slope is valid
valid = False
for long_form, variants in self.TRIGGER_SLOPE.items():
for variants in self.TRIGGER_SLOPE.values():
if slope_upper in [v.upper() for v in variants]:
valid = True
break
@@ -303,18 +217,7 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:EDGE:SLOpe {slope}")
def set_trigger_source(self, source):
"""
Set the trigger source.
Args:
source: Trigger source channel. Valid options:
'CH1', 'CH2', 'CH3', 'CH4' (case-insensitive)
Can also pass as integer 1-4
Raises:
ValueError: If source is not valid
RuntimeError: If not connected
"""
"""Set the trigger source."""
# Convert integer to channel string if needed
if isinstance(source, int):
if source < 1 or source > 4:
@@ -333,17 +236,7 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:EDGE:SOUrce {source}")
def set_trigger_level(self, channel, level):
"""
Set the trigger level for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
level: Trigger level in volts
Raises:
ValueError: If channel or level is not valid
RuntimeError: If not connected
"""
"""Set the trigger level for a specific channel."""
# Convert to channel number if string
if isinstance(channel, str):
channel_upper = channel.upper()
@@ -362,23 +255,12 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:LEVel:CH{channel} {level}")
def set_trigger_mode(self, mode):
"""
Set the trigger mode.
Args:
mode: Trigger mode (case-insensitive). Valid options:
'AUTO' - Auto trigger mode
'NORMal' or 'NORMAL' - Normal trigger mode
Raises:
ValueError: If mode is not valid
RuntimeError: If not connected
"""
"""Set the trigger mode."""
mode_upper = mode.upper()
# Check if mode is valid
valid = False
for long_form, variants in self.TRIGGER_MODE.items():
for variants in self.TRIGGER_MODE.values():
if mode_upper in [v.upper() for v in variants]:
valid = True
break
@@ -395,38 +277,18 @@ class TektronixOscilloscopeBase:
# ========== Channel Control Methods ==========
def set_channel_bandwidth(self, channel, bandwidth):
"""
Set the bandwidth for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
bandwidth: Bandwidth setting (depends on scope model)
Raises:
ValueError: If channel is not valid
RuntimeError: If not connected
"""
"""Set the bandwidth for a specific channel."""
channel = self._normalize_channel(channel)
self.write(f"CH{channel}:BANdwidth {bandwidth}")
def set_channel_coupling(self, channel, coupling):
"""
Set the coupling mode for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
coupling: Coupling mode (AC or DC, case-insensitive)
Raises:
ValueError: If channel or coupling is not valid
RuntimeError: If not connected
"""
"""Set the coupling mode for a specific channel."""
channel = self._normalize_channel(channel)
coupling_upper = coupling.upper()
# Validate coupling
valid = False
for long_form, variants in self.CHANNEL_COUPLING.items():
for variants in self.CHANNEL_COUPLING.items():
if coupling_upper in [v.upper() for v in variants]:
valid = True
break
@@ -441,17 +303,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:COUPling {coupling}")
def set_channel_label_name(self, channel, name):
"""
Set the label name for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
name: Label name as string
Raises:
ValueError: If channel is not valid
RuntimeError: If not connected
"""
"""Set the label name for a specific channel."""
channel = self._normalize_channel(channel)
if not isinstance(name, str):
@@ -461,17 +313,7 @@ class TektronixOscilloscopeBase:
self.write(f'CH{channel}:LABel:NAMe "{name}"')
def set_channel_position(self, channel, position):
"""
Set the vertical position for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
position: Vertical position in divisions (can be negative or positive)
Raises:
ValueError: If channel or position is not valid
RuntimeError: If not connected
"""
"""Set the vertical position for a specific channel."""
channel = self._normalize_channel(channel)
if not isinstance(position, (int, float)):
@@ -480,17 +322,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:POSition {position}")
def set_channel_scale(self, channel, scale):
"""
Set the vertical scale for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
scale: Vertical scale in volts per division (must be positive)
Raises:
ValueError: If channel or scale is not valid
RuntimeError: If not connected
"""
"""Set the vertical scale for a specific channel."""
channel = self._normalize_channel(channel)
if not isinstance(scale, (int, float)) or scale <= 0:
@@ -499,17 +331,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:SCAle {scale}")
def set_channel_termination(self, channel, termination):
"""
Set the termination for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
termination: Termination in ohms (50 or 1000000)
Raises:
ValueError: If channel or termination is not valid
RuntimeError: If not connected
"""
"""Set the termination for a specific channel."""
channel = self._normalize_channel(channel)
if termination not in self.CHANNEL_TERMINATION:
@@ -521,18 +343,7 @@ class TektronixOscilloscopeBase:
# ========== Waveform Transfer Methods ==========
def set_data_source(self, source):
"""
Set the data source for waveform transfer.
Args:
source: Data source channel. Valid options:
'CH1', 'CH2', 'CH3', 'CH4' (case-insensitive)
Can also pass as integer 1-4
Raises:
ValueError: If source is not valid
RuntimeError: If not connected
"""
"""Set the data source for waveform transfer."""
# Convert integer to channel string if needed
if isinstance(source, int):
if source < 1 or source > 4:
@@ -551,15 +362,7 @@ class TektronixOscilloscopeBase:
self.write(f"DATa:SOUrce {source}")
def query_wfmoutpre(self):
"""
Query all waveform output preamble parameters.
Returns:
str: Complete waveform preamble string with all parameters
Raises:
RuntimeError: If not connected
"""
"""Query all waveform output preamble parameters."""
return self.query("WFMOutpre?")
def transfer_curve(self):
@@ -627,7 +430,7 @@ class TektronixOscilloscopeBase:
# fewer frames than configured — reading the configured count would block.
acquired = int(self.query("ACQuire:NUMFRAMESACQuired?"))
if acquired <= 0:
raise RuntimeError(f"Scope acquired 0 FastFrame frames — no data to read")
raise RuntimeError("Scope acquired 0 FastFrame frames — no data to read")
frame_count = acquired
# Send a single CURVe? query - scope will return all frames
@@ -653,21 +456,7 @@ class TektronixOscilloscopeBase:
return waveforms
def parse_curve_data(self, curve_bytes, byte_count=1, signed=True, byte_order='MSB'):
"""
Parse raw curve data into integer array.
Args:
curve_bytes: Raw binary curve data
byte_count: Number of bytes per sample (1 or 2)
signed: True for signed integer, False for unsigned
byte_order: 'MSB' or 'LSB' for byte order
Returns:
list: List of integer values from the curve data
Raises:
ValueError: If parameters are invalid
"""
"""Parse raw curve data into integer array."""
if byte_count not in (1, 2):
raise ValueError(f"Invalid byte count: {byte_count}. Must be 1 or 2")
@@ -709,16 +498,7 @@ class TektronixOscilloscopeBase:
return values
def write(self, command):
"""
Send a raw SCPI command to the instrument.
Args:
command: SCPI command string
Raises:
RuntimeError: If not connected
socket.error: If communication fails
"""
"""Send a raw SCPI command to the instrument."""
if not self._connected or not self.socket:
raise RuntimeError("Not connected to instrument")
@@ -728,19 +508,7 @@ class TektronixOscilloscopeBase:
self.socket.sendall(command.encode('ascii'))
def query(self, command):
"""
Send a SCPI query and return the response.
Args:
command: SCPI query command string
Returns:
str: Response from the instrument (stripped of trailing newline)
Raises:
RuntimeError: If not connected
socket.error: If communication fails
"""
"""Send a SCPI query and return the response."""
if not self._connected or not self.socket:
raise RuntimeError("Not connected to instrument")