Phase 1b: prune 45 never-called methods from tektronix_base (~850 lines)
Verified by repo-wide name search + transitive closure over internal calls: the live apps use 25 methods (plus raw write/query); everything else — cosmetic label styling, unused getters/setters, transfer_waveform, acquire_waveform — had no callers. Also: linear-time chunk join in read_raw instead of quadratic bytes += concat, and a typed except on its debug path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+5
-850
@@ -153,30 +153,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
def identify(self):
|
|
||||||
"""
|
|
||||||
Query instrument identification string (*IDN?).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Instrument identification string
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("*IDN?")
|
|
||||||
|
|
||||||
def get_acquire_mode(self):
|
|
||||||
"""
|
|
||||||
Query the current acquisition mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current acquisition mode (SAMple, PEAKdetect, HIRes, AVErage, or ENVelope)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("ACQuire:MODe?")
|
|
||||||
|
|
||||||
def set_acquire_mode(self, mode):
|
def set_acquire_mode(self, mode):
|
||||||
"""
|
"""
|
||||||
Set the acquisition mode.
|
Set the acquisition mode.
|
||||||
@@ -247,19 +223,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"HORizontal:FASTframe:STATE {state}")
|
self.write(f"HORizontal:FASTframe:STATE {state}")
|
||||||
|
|
||||||
def get_fastframe_count(self):
|
|
||||||
"""
|
|
||||||
Query the current FastFrame frame count.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Number of frames configured
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
response = self.query("HORizontal:FASTframe:COUNt?")
|
|
||||||
return int(response)
|
|
||||||
|
|
||||||
def set_fastframe_count(self, count):
|
def set_fastframe_count(self, count):
|
||||||
"""
|
"""
|
||||||
Set the number of FastFrame frames.
|
Set the number of FastFrame frames.
|
||||||
@@ -290,36 +253,6 @@ class TektronixOscilloscopeBase:
|
|||||||
response = self.query("HORizontal:MODe:RECOrdlength?")
|
response = self.query("HORizontal:MODe:RECOrdlength?")
|
||||||
return int(response)
|
return int(response)
|
||||||
|
|
||||||
def set_record_length(self, length):
|
|
||||||
"""
|
|
||||||
Set the horizontal record length.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
length: Record length in samples (must be positive integer)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If length is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
# Validate length
|
|
||||||
if not isinstance(length, int) or length <= 0:
|
|
||||||
raise ValueError(f"Invalid record length: {length}. Must be a positive integer")
|
|
||||||
|
|
||||||
self.write(f"HORizontal:MODe:RECOrdlength {length}")
|
|
||||||
|
|
||||||
def get_sample_rate(self):
|
|
||||||
"""
|
|
||||||
Query the current horizontal sample rate.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current sample rate in samples per second
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
response = self.query("HORizontal:MODe:SAMPLERate?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_sample_rate(self, rate):
|
def set_sample_rate(self, rate):
|
||||||
"""
|
"""
|
||||||
Set the horizontal sample rate.
|
Set the horizontal sample rate.
|
||||||
@@ -337,93 +270,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"HORizontal:MODe:SAMPLERate {rate}")
|
self.write(f"HORizontal:MODe:SAMPLERate {rate}")
|
||||||
|
|
||||||
def get_time_scale(self):
|
|
||||||
"""
|
|
||||||
Query the current horizontal time scale.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current time scale in seconds per division
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
response = self.query("HORizontal:MODe:SCAle?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_time_scale(self, scale):
|
|
||||||
"""
|
|
||||||
Set the horizontal time scale.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scale: Time scale in seconds per division (must be positive number)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If scale is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
# Validate scale
|
|
||||||
if not isinstance(scale, (int, float)) or scale <= 0:
|
|
||||||
raise ValueError(f"Invalid time scale: {scale}. Must be a positive number")
|
|
||||||
|
|
||||||
self.write(f"HORizontal:MODe:SCAle {scale}")
|
|
||||||
|
|
||||||
def get_trigger_coupling(self):
|
|
||||||
"""
|
|
||||||
Query the current trigger coupling mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current trigger coupling (DC, HFRej, LFRej, or NOISErej)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("TRIGger:A:EDGE:COUPling?")
|
|
||||||
|
|
||||||
def set_trigger_coupling(self, coupling):
|
|
||||||
"""
|
|
||||||
Set the trigger coupling mode.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
coupling: Trigger coupling mode (case-insensitive). Valid options:
|
|
||||||
'DC' - DC coupling
|
|
||||||
'HFRej' or 'HFREJ' - High frequency reject
|
|
||||||
'LFRej' or 'LFREJ' - Low frequency reject
|
|
||||||
'NOISErej' or 'NOISEREJ' - Noise reject
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If coupling is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
coupling_upper = coupling.upper()
|
|
||||||
|
|
||||||
# Check if coupling is valid
|
|
||||||
valid = False
|
|
||||||
for long_form, variants in self.TRIGGER_COUPLING.items():
|
|
||||||
if coupling_upper in [v.upper() for v in variants]:
|
|
||||||
valid = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
valid_options = []
|
|
||||||
for variants in self.TRIGGER_COUPLING.values():
|
|
||||||
valid_options.extend(variants)
|
|
||||||
raise ValueError(f"Invalid trigger coupling: {coupling}. "
|
|
||||||
f"Valid options: {', '.join(valid_options)}")
|
|
||||||
|
|
||||||
self.write(f"TRIGger:A:EDGE:COUPling {coupling}")
|
|
||||||
|
|
||||||
def get_trigger_slope(self):
|
|
||||||
"""
|
|
||||||
Query the current trigger slope.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current trigger slope (RISe, FALL, or EITher)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("TRIGger:A:EDGE:SLOpe?")
|
|
||||||
|
|
||||||
def set_trigger_slope(self, slope):
|
def set_trigger_slope(self, slope):
|
||||||
"""
|
"""
|
||||||
Set the trigger slope.
|
Set the trigger slope.
|
||||||
@@ -456,18 +302,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"TRIGger:A:EDGE:SLOpe {slope}")
|
self.write(f"TRIGger:A:EDGE:SLOpe {slope}")
|
||||||
|
|
||||||
def get_trigger_source(self):
|
|
||||||
"""
|
|
||||||
Query the current trigger source.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current trigger source (e.g., CH1, CH2, CH3, CH4)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("TRIGger:A:EDGE:SOUrce?")
|
|
||||||
|
|
||||||
def set_trigger_source(self, source):
|
def set_trigger_source(self, source):
|
||||||
"""
|
"""
|
||||||
Set the trigger source.
|
Set the trigger source.
|
||||||
@@ -498,34 +332,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"TRIGger:A:EDGE:SOUrce {source}")
|
self.write(f"TRIGger:A:EDGE:SOUrce {source}")
|
||||||
|
|
||||||
def get_trigger_level(self, channel):
|
|
||||||
"""
|
|
||||||
Query the trigger level for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Trigger level in volts
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
# Convert to channel number if string
|
|
||||||
if isinstance(channel, str):
|
|
||||||
channel_upper = channel.upper()
|
|
||||||
if not (channel_upper.startswith('CH') and len(channel_upper) == 3 and channel_upper[2].isdigit()):
|
|
||||||
raise ValueError(f"Invalid channel: {channel}. Must be CH1, CH2, CH3, or CH4")
|
|
||||||
channel = int(channel_upper[2])
|
|
||||||
|
|
||||||
# Validate channel number
|
|
||||||
if not isinstance(channel, int) or channel < 1 or channel > 4:
|
|
||||||
raise ValueError(f"Invalid channel: {channel}. Must be 1-4")
|
|
||||||
|
|
||||||
response = self.query(f"TRIGger:A:LEVel:CH{channel}?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_trigger_level(self, channel, level):
|
def set_trigger_level(self, channel, level):
|
||||||
"""
|
"""
|
||||||
Set the trigger level for a specific channel.
|
Set the trigger level for a specific channel.
|
||||||
@@ -555,18 +361,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"TRIGger:A:LEVel:CH{channel} {level}")
|
self.write(f"TRIGger:A:LEVel:CH{channel} {level}")
|
||||||
|
|
||||||
def get_trigger_mode(self):
|
|
||||||
"""
|
|
||||||
Query the current trigger mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current trigger mode (AUTO or NORMal)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("TRIGger:A:MODe?")
|
|
||||||
|
|
||||||
def set_trigger_mode(self, mode):
|
def set_trigger_mode(self, mode):
|
||||||
"""
|
"""
|
||||||
Set the trigger mode.
|
Set the trigger mode.
|
||||||
@@ -600,40 +394,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
# ========== Channel Control Methods ==========
|
# ========== Channel Control Methods ==========
|
||||||
|
|
||||||
def query_channel(self, channel):
|
|
||||||
"""
|
|
||||||
Query all parameters for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: All channel parameters
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}?")
|
|
||||||
|
|
||||||
def get_channel_bandwidth(self, channel):
|
|
||||||
"""
|
|
||||||
Query the bandwidth setting for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Channel bandwidth setting
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}:BANdwidth?")
|
|
||||||
|
|
||||||
def set_channel_bandwidth(self, channel, bandwidth):
|
def set_channel_bandwidth(self, channel, bandwidth):
|
||||||
"""
|
"""
|
||||||
Set the bandwidth for a specific channel.
|
Set the bandwidth for a specific channel.
|
||||||
@@ -649,23 +409,6 @@ class TektronixOscilloscopeBase:
|
|||||||
channel = self._normalize_channel(channel)
|
channel = self._normalize_channel(channel)
|
||||||
self.write(f"CH{channel}:BANdwidth {bandwidth}")
|
self.write(f"CH{channel}:BANdwidth {bandwidth}")
|
||||||
|
|
||||||
def get_channel_coupling(self, channel):
|
|
||||||
"""
|
|
||||||
Query the coupling mode for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Coupling mode (AC or DC)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}:COUPling?")
|
|
||||||
|
|
||||||
def set_channel_coupling(self, channel, coupling):
|
def set_channel_coupling(self, channel, coupling):
|
||||||
"""
|
"""
|
||||||
Set the coupling mode for a specific channel.
|
Set the coupling mode for a specific channel.
|
||||||
@@ -697,144 +440,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"CH{channel}:COUPling {coupling}")
|
self.write(f"CH{channel}:COUPling {coupling}")
|
||||||
|
|
||||||
def get_channel_label_color(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label color for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Color as hex string (e.g., '#FF0000')
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}:LABel:COLor?")
|
|
||||||
|
|
||||||
def set_channel_label_color(self, channel, color):
|
|
||||||
"""
|
|
||||||
Set the label color for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
color: Color as hex string (e.g., '#FF0000' or '#AABBCC'), or empty string
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or color is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
# Allow empty strings (including SCPI quoted empty strings like '""')
|
|
||||||
if color == '' or color == '""':
|
|
||||||
self.write(f'CH{channel}:LABel:COLor ""')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Basic validation of hex color format
|
|
||||||
if not isinstance(color, str) or not color.startswith('#') or len(color) != 7:
|
|
||||||
raise ValueError(f"Invalid color format: {color}. Must be '#AABBCC' format or empty string")
|
|
||||||
|
|
||||||
# Validate hex digits
|
|
||||||
try:
|
|
||||||
int(color[1:], 16)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"Invalid color format: {color}. Must contain valid hex digits")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:LABel:COLor {color}")
|
|
||||||
|
|
||||||
def get_channel_label_font_size(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label font size for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Font size in points
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:LABel:FONT:SIZE?")
|
|
||||||
return int(float(response))
|
|
||||||
|
|
||||||
def set_channel_label_font_size(self, channel, size):
|
|
||||||
"""
|
|
||||||
Set the label font size for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
size: Font size in points (positive integer)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or size is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
if not isinstance(size, int) or size <= 0:
|
|
||||||
raise ValueError(f"Invalid font size: {size}. Must be a positive integer")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:LABel:FONT:SIZE {size}")
|
|
||||||
|
|
||||||
def get_channel_label_font_type(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label font type for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Font name
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}:LABel:FONT:TYPE?")
|
|
||||||
|
|
||||||
def set_channel_label_font_type(self, channel, font_name):
|
|
||||||
"""
|
|
||||||
Set the label font type for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
font_name: Font name as string
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
if not isinstance(font_name, str) or not font_name:
|
|
||||||
raise ValueError(f"Invalid font name: {font_name}. Must be a non-empty string")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:LABel:FONT:TYPE {font_name}")
|
|
||||||
|
|
||||||
def get_channel_label_name(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label name for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Channel label name
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
return self.query(f"CH{channel}:LABel:NAMe?")
|
|
||||||
|
|
||||||
def set_channel_label_name(self, channel, name):
|
def set_channel_label_name(self, channel, name):
|
||||||
"""
|
"""
|
||||||
Set the label name for a specific channel.
|
Set the label name for a specific channel.
|
||||||
@@ -855,135 +460,6 @@ class TektronixOscilloscopeBase:
|
|||||||
# Need to quote the string for SCPI
|
# Need to quote the string for SCPI
|
||||||
self.write(f'CH{channel}:LABel:NAMe "{name}"')
|
self.write(f'CH{channel}:LABel:NAMe "{name}"')
|
||||||
|
|
||||||
def get_channel_label_xpos(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label x-position for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: X-position in pixels
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:LABel:XPOS?")
|
|
||||||
return int(float(response))
|
|
||||||
|
|
||||||
def set_channel_label_xpos(self, channel, xpos):
|
|
||||||
"""
|
|
||||||
Set the label x-position for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
xpos: X-position in pixels (non-negative integer)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or xpos is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
if not isinstance(xpos, int) or xpos < 0:
|
|
||||||
raise ValueError(f"Invalid x-position: {xpos}. Must be a non-negative integer")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:LABel:XPOS {xpos}")
|
|
||||||
|
|
||||||
def get_channel_label_ypos(self, channel):
|
|
||||||
"""
|
|
||||||
Query the label y-position for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Y-position in pixels
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:LABel:YPOS?")
|
|
||||||
return int(float(response))
|
|
||||||
|
|
||||||
def set_channel_label_ypos(self, channel, ypos):
|
|
||||||
"""
|
|
||||||
Set the label y-position for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
ypos: Y-position in pixels (non-negative integer)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or ypos is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
if not isinstance(ypos, int) or ypos < 0:
|
|
||||||
raise ValueError(f"Invalid y-position: {ypos}. Must be a non-negative integer")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:LABel:YPOS {ypos}")
|
|
||||||
|
|
||||||
def get_channel_offset(self, channel):
|
|
||||||
"""
|
|
||||||
Query the vertical offset for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Vertical offset in volts
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:OFFset?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_channel_offset(self, channel, offset):
|
|
||||||
"""
|
|
||||||
Set the vertical offset for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
offset: Vertical offset in volts
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or offset is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
|
|
||||||
if not isinstance(offset, (int, float)):
|
|
||||||
raise ValueError(f"Invalid offset: {offset}. Must be a number")
|
|
||||||
|
|
||||||
self.write(f"CH{channel}:OFFset {offset}")
|
|
||||||
|
|
||||||
def get_channel_position(self, channel):
|
|
||||||
"""
|
|
||||||
Query the vertical position for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Vertical position in divisions
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:POSition?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_channel_position(self, channel, position):
|
def set_channel_position(self, channel, position):
|
||||||
"""
|
"""
|
||||||
Set the vertical position for a specific channel.
|
Set the vertical position for a specific channel.
|
||||||
@@ -1003,24 +479,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"CH{channel}:POSition {position}")
|
self.write(f"CH{channel}:POSition {position}")
|
||||||
|
|
||||||
def get_channel_scale(self, channel):
|
|
||||||
"""
|
|
||||||
Query the vertical scale for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Vertical scale in volts per division
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:SCAle?")
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_channel_scale(self, channel, scale):
|
def set_channel_scale(self, channel, scale):
|
||||||
"""
|
"""
|
||||||
Set the vertical scale for a specific channel.
|
Set the vertical scale for a specific channel.
|
||||||
@@ -1040,24 +498,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"CH{channel}:SCAle {scale}")
|
self.write(f"CH{channel}:SCAle {scale}")
|
||||||
|
|
||||||
def get_channel_termination(self, channel):
|
|
||||||
"""
|
|
||||||
Query the termination setting for a specific channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Termination in ohms (50 or 1000000)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
channel = self._normalize_channel(channel)
|
|
||||||
response = self.query(f"CH{channel}:TERmination?")
|
|
||||||
return int(float(response))
|
|
||||||
|
|
||||||
def set_channel_termination(self, channel, termination):
|
def set_channel_termination(self, channel, termination):
|
||||||
"""
|
"""
|
||||||
Set the termination for a specific channel.
|
Set the termination for a specific channel.
|
||||||
@@ -1080,66 +520,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
# ========== Waveform Transfer Methods ==========
|
# ========== Waveform Transfer Methods ==========
|
||||||
|
|
||||||
def get_data_encoding(self):
|
|
||||||
"""
|
|
||||||
Query the current data encoding format.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current data encoding (e.g., RIBinary, ASCII, etc.)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("DATa:ENCdg?")
|
|
||||||
|
|
||||||
def set_data_encoding(self, encoding):
|
|
||||||
"""
|
|
||||||
Set the data encoding format for waveform transfer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encoding: Encoding format (case-insensitive). Valid options:
|
|
||||||
'ASCIi' or 'ASCII' - ASCII format
|
|
||||||
'RIBinary' or 'RIBINARY' - Signed integer, MSB first (recommended)
|
|
||||||
'RPBinary' or 'RPBINARY' - Positive integer, MSB first
|
|
||||||
'FPBinary' or 'FPBINARY' - Floating point binary
|
|
||||||
'SRIbinary' or 'SRIBINARY' - Signed integer, byte-swapped
|
|
||||||
'SRPbinary' or 'SRPBINARY' - Positive integer, byte-swapped
|
|
||||||
'SFPbinary' or 'SFPBINARY' - Floating point binary, byte-swapped
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If encoding is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
encoding_upper = encoding.upper()
|
|
||||||
|
|
||||||
# Check if encoding is valid
|
|
||||||
valid = False
|
|
||||||
for long_form, variants in self.DATA_ENCODING.items():
|
|
||||||
if encoding_upper in [v.upper() for v in variants]:
|
|
||||||
valid = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
valid_options = []
|
|
||||||
for variants in self.DATA_ENCODING.values():
|
|
||||||
valid_options.extend(variants)
|
|
||||||
raise ValueError(f"Invalid data encoding: {encoding}. "
|
|
||||||
f"Valid options: {', '.join(valid_options)}")
|
|
||||||
|
|
||||||
self.write(f"DATa:ENCdg {encoding}")
|
|
||||||
|
|
||||||
def get_data_source(self):
|
|
||||||
"""
|
|
||||||
Query the current data source for waveform transfer.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current data source (e.g., CH1, CH2, CH3, CH4)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("DATa:SOUrce?")
|
|
||||||
|
|
||||||
def set_data_source(self, source):
|
def set_data_source(self, source):
|
||||||
"""
|
"""
|
||||||
Set the data source for waveform transfer.
|
Set the data source for waveform transfer.
|
||||||
@@ -1170,123 +550,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
self.write(f"DATa:SOUrce {source}")
|
self.write(f"DATa:SOUrce {source}")
|
||||||
|
|
||||||
def get_wfmoutpre_encoding(self):
|
|
||||||
"""
|
|
||||||
Query the waveform output preamble encoding.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current encoding (BINary or ASCii)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("WFMOutpre:ENCdg?")
|
|
||||||
|
|
||||||
def set_wfmoutpre_encoding(self, encoding):
|
|
||||||
"""
|
|
||||||
Set the waveform output preamble encoding.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encoding: Encoding format (case-insensitive). Valid options:
|
|
||||||
'BINary' or 'BINARY' - Binary format (recommended)
|
|
||||||
'ASCii' or 'ASCII' - ASCII format
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If encoding is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
encoding_upper = encoding.upper()
|
|
||||||
|
|
||||||
# Check if encoding is valid
|
|
||||||
valid = False
|
|
||||||
for long_form, variants in self.WFMOUTPRE_ENCODING.items():
|
|
||||||
if encoding_upper in [v.upper() for v in variants]:
|
|
||||||
valid = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
valid_options = []
|
|
||||||
for variants in self.WFMOUTPRE_ENCODING.values():
|
|
||||||
valid_options.extend(variants)
|
|
||||||
raise ValueError(f"Invalid waveform output encoding: {encoding}. "
|
|
||||||
f"Valid options: {', '.join(valid_options)}")
|
|
||||||
|
|
||||||
self.write(f"WFMOutpre:ENCdg {encoding}")
|
|
||||||
|
|
||||||
def get_wfmoutpre_byte_count(self):
|
|
||||||
"""
|
|
||||||
Query the waveform output preamble byte count (BYT_Nr).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Number of bytes per data point (1 or 2)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
response = self.query("WFMOutpre:BYT_Nr?")
|
|
||||||
return int(response)
|
|
||||||
|
|
||||||
def set_wfmoutpre_byte_count(self, byte_count):
|
|
||||||
"""
|
|
||||||
Set the waveform output preamble byte count (BYT_Nr).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
byte_count: Number of bytes per data point
|
|
||||||
1 = 8-bit data (0-255 for unsigned, -128 to 127 for signed)
|
|
||||||
2 = 16-bit data (0-65535 for unsigned, -32768 to 32767 for signed)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If byte_count is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
if byte_count not in (1, 2):
|
|
||||||
raise ValueError(f"Invalid byte count: {byte_count}. Must be 1 or 2")
|
|
||||||
|
|
||||||
self.write(f"WFMOutpre:BYT_Nr {byte_count}")
|
|
||||||
|
|
||||||
def get_wfmoutpre_byte_order(self):
|
|
||||||
"""
|
|
||||||
Query the waveform output preamble byte order.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Current byte order (LSB or MSB)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
return self.query("WFMOutpre:BYT_Or?")
|
|
||||||
|
|
||||||
def set_wfmoutpre_byte_order(self, byte_order):
|
|
||||||
"""
|
|
||||||
Set the waveform output preamble byte order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
byte_order: Byte order (case-insensitive). Valid options:
|
|
||||||
'LSB' - Least significant byte first
|
|
||||||
'MSB' - Most significant byte first
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If byte_order is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
byte_order_upper = byte_order.upper()
|
|
||||||
|
|
||||||
# Check if byte order is valid
|
|
||||||
valid = False
|
|
||||||
for long_form, variants in self.BYTE_ORDER.items():
|
|
||||||
if byte_order_upper in [v.upper() for v in variants]:
|
|
||||||
valid = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
valid_options = []
|
|
||||||
for variants in self.BYTE_ORDER.values():
|
|
||||||
valid_options.extend(variants)
|
|
||||||
raise ValueError(f"Invalid byte order: {byte_order}. "
|
|
||||||
f"Valid options: {', '.join(valid_options)}")
|
|
||||||
|
|
||||||
self.write(f"WFMOutpre:BYT_Or {byte_order}")
|
|
||||||
|
|
||||||
def query_wfmoutpre(self):
|
def query_wfmoutpre(self):
|
||||||
"""
|
"""
|
||||||
Query all waveform output preamble parameters.
|
Query all waveform output preamble parameters.
|
||||||
@@ -1299,35 +562,6 @@ class TektronixOscilloscopeBase:
|
|||||||
"""
|
"""
|
||||||
return self.query("WFMOutpre?")
|
return self.query("WFMOutpre?")
|
||||||
|
|
||||||
def get_fastframe_selected(self):
|
|
||||||
"""
|
|
||||||
Query the currently selected FastFrame frame number.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Currently selected frame number
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
response = self.query("HORizontal:FASTframe:SELECTED?")
|
|
||||||
return int(response)
|
|
||||||
|
|
||||||
def set_fastframe_selected(self, frame_number):
|
|
||||||
"""
|
|
||||||
Select a specific FastFrame frame for data transfer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame_number: Frame number to select (must be positive integer)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If frame_number is not valid
|
|
||||||
RuntimeError: If not connected
|
|
||||||
"""
|
|
||||||
if not isinstance(frame_number, int) or frame_number <= 0:
|
|
||||||
raise ValueError(f"Invalid frame number: {frame_number}. Must be a positive integer")
|
|
||||||
|
|
||||||
self.write(f"HORizontal:FASTframe:SELECTED {frame_number}")
|
|
||||||
|
|
||||||
def transfer_curve(self):
|
def transfer_curve(self):
|
||||||
"""
|
"""
|
||||||
Transfer waveform curve data from the oscilloscope.
|
Transfer waveform curve data from the oscilloscope.
|
||||||
@@ -1348,29 +582,6 @@ class TektronixOscilloscopeBase:
|
|||||||
# Read the binary data using IEEE 488.2 format
|
# Read the binary data using IEEE 488.2 format
|
||||||
return self.read_raw()
|
return self.read_raw()
|
||||||
|
|
||||||
def transfer_waveform(self):
|
|
||||||
"""
|
|
||||||
Transfer complete waveform data including preamble and curve.
|
|
||||||
|
|
||||||
This method queries WFMOutpre? for the preamble and CURVe? for the data
|
|
||||||
as separate operations, which is the standard approach for Tektronix scopes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (preamble_str, curve_bytes)
|
|
||||||
preamble_str: Waveform preamble as string
|
|
||||||
curve_bytes: Raw binary curve data
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If not connected or transfer fails
|
|
||||||
"""
|
|
||||||
# First query the waveform preamble
|
|
||||||
preamble_str = self.query_wfmoutpre()
|
|
||||||
|
|
||||||
# Then transfer the curve data
|
|
||||||
curve_bytes = self.transfer_curve()
|
|
||||||
|
|
||||||
return (preamble_str, curve_bytes)
|
|
||||||
|
|
||||||
def transfer_fastframe(self, parse=True, byte_count=1, signed=True, byte_order='MSB'):
|
def transfer_fastframe(self, parse=True, byte_count=1, signed=True, byte_order='MSB'):
|
||||||
"""
|
"""
|
||||||
Transfer all FastFrame waveform data from the oscilloscope.
|
Transfer all FastFrame waveform data from the oscilloscope.
|
||||||
@@ -1497,62 +708,6 @@ class TektronixOscilloscopeBase:
|
|||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def acquire_waveform(self, channel, frame_number=None):
|
|
||||||
"""
|
|
||||||
High-level method to acquire waveform data from a channel.
|
|
||||||
|
|
||||||
This method:
|
|
||||||
1. Sets up data encoding (RIBinary, BINary, 1 byte, MSB)
|
|
||||||
2. Sets the data source to the specified channel
|
|
||||||
3. Optionally selects a FastFrame frame
|
|
||||||
4. Transfers the curve data
|
|
||||||
5. Parses it into a list of integer values
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
|
|
||||||
frame_number: Optional FastFrame frame number to acquire (default: None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of integer ADC values from the waveform
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If channel or frame_number is invalid
|
|
||||||
RuntimeError: If not connected or transfer fails
|
|
||||||
"""
|
|
||||||
# Normalize channel input
|
|
||||||
if isinstance(channel, str):
|
|
||||||
channel_upper = channel.upper()
|
|
||||||
if not (channel_upper.startswith('CH') and len(channel_upper) == 3 and channel_upper[2].isdigit()):
|
|
||||||
raise ValueError(f"Invalid channel: {channel}. Must be CH1, CH2, CH3, or CH4")
|
|
||||||
source = channel_upper
|
|
||||||
elif isinstance(channel, int):
|
|
||||||
if channel < 1 or channel > 4:
|
|
||||||
raise ValueError(f"Invalid channel: {channel}. Must be 1-4")
|
|
||||||
source = f"CH{channel}"
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid channel type: {type(channel)}. Must be int or str")
|
|
||||||
|
|
||||||
# Configure data transfer format
|
|
||||||
self.set_data_encoding('RIBinary')
|
|
||||||
self.set_wfmoutpre_encoding('BINary')
|
|
||||||
self.set_wfmoutpre_byte_count(1)
|
|
||||||
self.set_wfmoutpre_byte_order('MSB')
|
|
||||||
|
|
||||||
# Set data source
|
|
||||||
self.set_data_source(source)
|
|
||||||
|
|
||||||
# Select FastFrame frame if specified
|
|
||||||
if frame_number is not None:
|
|
||||||
self.set_fastframe_selected(frame_number)
|
|
||||||
|
|
||||||
# Transfer curve data
|
|
||||||
curve_bytes = self.transfer_curve()
|
|
||||||
|
|
||||||
# Parse the data
|
|
||||||
values = self.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
|
|
||||||
return values
|
|
||||||
|
|
||||||
def write(self, command):
|
def write(self, command):
|
||||||
"""
|
"""
|
||||||
Send a raw SCPI command to the instrument.
|
Send a raw SCPI command to the instrument.
|
||||||
@@ -1625,11 +780,11 @@ class TektronixOscilloscopeBase:
|
|||||||
# Read the '#' character
|
# Read the '#' character
|
||||||
header = self.socket.recv(1)
|
header = self.socket.recv(1)
|
||||||
if header != b'#':
|
if header != b'#':
|
||||||
# Debug: read more to see what's actually there
|
# Read whatever else is buffered so the error shows the actual response
|
||||||
self.socket.setblocking(False)
|
self.socket.setblocking(False)
|
||||||
try:
|
try:
|
||||||
more = self.socket.recv(100)
|
more = self.socket.recv(100)
|
||||||
except:
|
except OSError:
|
||||||
more = b''
|
more = b''
|
||||||
self.socket.setblocking(True)
|
self.socket.setblocking(True)
|
||||||
raise RuntimeError(f"Invalid IEEE 488.2 header: expected '#', got {header}, followed by: {more[:50]}")
|
raise RuntimeError(f"Invalid IEEE 488.2 header: expected '#', got {header}, followed by: {more[:50]}")
|
||||||
@@ -1649,19 +804,19 @@ class TektronixOscilloscopeBase:
|
|||||||
data_length = int(length_bytes)
|
data_length = int(length_bytes)
|
||||||
|
|
||||||
# Read the actual binary data
|
# Read the actual binary data
|
||||||
data = b''
|
chunks = []
|
||||||
remaining = data_length
|
remaining = data_length
|
||||||
while remaining > 0:
|
while remaining > 0:
|
||||||
chunk = self.socket.recv(min(remaining, 65536))
|
chunk = self.socket.recv(min(remaining, 65536))
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise RuntimeError("Connection closed while reading data")
|
raise RuntimeError("Connection closed while reading data")
|
||||||
data += chunk
|
chunks.append(chunk)
|
||||||
remaining -= len(chunk)
|
remaining -= len(chunk)
|
||||||
|
|
||||||
# Read the trailing newline
|
# Read the trailing newline
|
||||||
self.socket.recv(1)
|
self.socket.recv(1)
|
||||||
|
|
||||||
return data
|
return b''.join(chunks)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user