Driver support for multi-row FastFrame bursts; fix read_raw block parsing
Groundwork for burst acquisition: the scope needs to report and transfer a whole multi-row FastFrame acquisition, and the BBD needs to gate its trigger output per row rather than staying armed for the scan. tektronix_base - get_fastframe_max_frames() exposes HORizontal:FASTframe:MAXFRames?, which is what sizes a burst once the horizontal settings are fixed. - transfer_fastframe_bulk() pulls a burst as one contiguous buffer. Unlike transfer_fastframe it does not assume how the scope frames the response: it accumulates until the expected byte count is reached, so one large IEEE block and one block per frame both work. - set_data_encoding() / set_data_width() make the transfer format settable instead of inherited from whatever the front panel was left on. - read_raw() had two real defects. The length-digit read used a bare recv() and only checked the length afterwards, so a short read raised "Failed to read data length" on a perfectly good transfer; it now goes through a _recv_exact() helper, as does the trailing separator. And a #0 indeterminate-length block was parsed as int("") -> ValueError. #0 is normally delimited by EOI, which a raw socket never sees, so read_raw now takes expected_bytes to size it. The bulk transfer relies on this. pybbd202 - arm_scan_gate(axis, armed) raises and drops the max-velocity trigger output the scope's AND-gate uses. A burst spans several rows with the scope running throughout, so the gate must be low for the flyback or the return move reaches max velocity and injects frames between rows. - set_trigger_verified() reads the mode back after setting it. set_trigger is fire-and-forget over the shared TX queue; burst mode toggles the gate between every row, where a dropped change silently corrupts the file rather than failing loudly. - set_trigger_gate_off() so the scan can leave the output idle on exit. - TRIGOUT_GATE_OFF is deliberately marked unverified. §7.6 of the BBD203 protocol doc describes `mode` as an enumeration capping at 0x11, which contradicts the bitmask this driver actually sends (TRIGOUT_MAXV = 0x90, known working), so the doc cannot settle which value idles the pin low. The engine's preflight check resolves it on the rig instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+88
-19
@@ -183,6 +183,10 @@ class TektronixOscilloscopeBase:
|
||||
|
||||
self.write(f"HORizontal:FASTframe:COUNt {count}")
|
||||
|
||||
def get_fastframe_max_frames(self):
|
||||
"""Query how many FastFrame frames the current horizontal settings allow."""
|
||||
return int(self.query("HORizontal:FASTframe:MAXFRames?"))
|
||||
|
||||
def get_record_length(self):
|
||||
"""Query the current horizontal record length."""
|
||||
response = self.query("HORizontal:MODe:RECOrdlength?")
|
||||
@@ -361,6 +365,21 @@ class TektronixOscilloscopeBase:
|
||||
|
||||
self.write(f"DATa:SOUrce {source}")
|
||||
|
||||
def set_data_encoding(self, encoding):
|
||||
"""Set the curve transfer encoding (e.g. RIBinary = signed int, MSB first)."""
|
||||
valid = ('ASCII', 'RIBinary', 'RPBinary', 'FPBinary',
|
||||
'SRIbinary', 'SRPbinary', 'SFPbinary')
|
||||
if encoding.upper() not in [v.upper() for v in valid]:
|
||||
raise ValueError(f"Invalid data encoding: {encoding}. "
|
||||
f"Valid options: {', '.join(valid)}")
|
||||
self.write(f"DATa:ENCdg {encoding}")
|
||||
|
||||
def set_data_width(self, width):
|
||||
"""Set bytes per sample for curve transfers."""
|
||||
if width not in (1, 2):
|
||||
raise ValueError(f"Invalid data width: {width}. Must be 1 or 2")
|
||||
self.write(f"DATa:WIDth {width}")
|
||||
|
||||
def query_wfmoutpre(self):
|
||||
"""Query all waveform output preamble parameters."""
|
||||
return self.query("WFMOutpre?")
|
||||
@@ -455,6 +474,43 @@ class TektronixOscilloscopeBase:
|
||||
|
||||
return waveforms
|
||||
|
||||
def transfer_fastframe_bulk(self, frame_count, samples_per_frame,
|
||||
bytes_per_sample=1):
|
||||
"""Transfer a whole FastFrame burst as one contiguous buffer.
|
||||
|
||||
Unlike transfer_fastframe this does not care how the scope frames the
|
||||
response — it accumulates blocks until it has the expected byte count,
|
||||
so one large IEEE block and one block per frame both work. Returns a
|
||||
bytearray of frame_count * samples_per_frame * bytes_per_sample bytes.
|
||||
"""
|
||||
if not self.get_fastframe_state():
|
||||
raise RuntimeError("FastFrame is not enabled. Enable it with set_fastframe_state(True)")
|
||||
|
||||
expected = frame_count * samples_per_frame * bytes_per_sample
|
||||
if expected <= 0:
|
||||
raise RuntimeError(
|
||||
f"Nothing to transfer: {frame_count} frames × "
|
||||
f"{samples_per_frame} samples × {bytes_per_sample} bytes"
|
||||
)
|
||||
|
||||
self.write("CURVe?")
|
||||
|
||||
buf = bytearray()
|
||||
while len(buf) < expected:
|
||||
block = self.read_raw(expected_bytes=expected - len(buf))
|
||||
if not block:
|
||||
raise RuntimeError(
|
||||
f"Scope returned an empty block {len(buf)}/{expected} bytes "
|
||||
"into the burst transfer"
|
||||
)
|
||||
buf += block
|
||||
|
||||
if len(buf) != expected:
|
||||
raise RuntimeError(
|
||||
f"Burst transfer overran: got {len(buf)} bytes, expected {expected}"
|
||||
)
|
||||
return buf
|
||||
|
||||
def parse_curve_data(self, curve_bytes, byte_count=1, signed=True, byte_order='MSB'):
|
||||
"""Parse raw curve data into integer array."""
|
||||
if byte_count not in (1, 2):
|
||||
@@ -526,7 +582,19 @@ class TektronixOscilloscopeBase:
|
||||
|
||||
return response.decode('ascii').strip()
|
||||
|
||||
def read_raw(self):
|
||||
def _recv_exact(self, count):
|
||||
"""Read exactly `count` bytes; recv() is free to return fewer."""
|
||||
chunks = []
|
||||
remaining = count
|
||||
while remaining > 0:
|
||||
chunk = self.socket.recv(min(remaining, 65536))
|
||||
if not chunk:
|
||||
raise RuntimeError("Connection closed while reading data")
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
return b''.join(chunks)
|
||||
|
||||
def read_raw(self, expected_bytes=None):
|
||||
"""
|
||||
Read raw binary data from the instrument.
|
||||
|
||||
@@ -535,6 +603,10 @@ class TektronixOscilloscopeBase:
|
||||
where N is a digit indicating how many digits follow,
|
||||
and those digits specify the length of the data block.
|
||||
|
||||
`#0` announces an indeterminate-length block, normally delimited by EOI
|
||||
— which a raw socket never sees. Pass expected_bytes to say how many
|
||||
bytes to take in that case.
|
||||
|
||||
Returns:
|
||||
bytes: Raw binary data (without IEEE 488.2 header)
|
||||
|
||||
@@ -564,27 +636,24 @@ class TektronixOscilloscopeBase:
|
||||
|
||||
num_digits = int(length_of_length)
|
||||
|
||||
# Read the data length
|
||||
length_bytes = self.socket.recv(num_digits)
|
||||
if len(length_bytes) != num_digits:
|
||||
raise RuntimeError("Failed to read data length")
|
||||
if num_digits == 0:
|
||||
# Indeterminate length: no byte count follows, and the EOI that
|
||||
# would delimit it does not exist on a raw socket.
|
||||
if expected_bytes is None:
|
||||
raise RuntimeError(
|
||||
"Scope returned an indeterminate-length block (#0); "
|
||||
"read_raw needs expected_bytes to size it over a socket"
|
||||
)
|
||||
data_length = expected_bytes
|
||||
else:
|
||||
data_length = int(self._recv_exact(num_digits))
|
||||
|
||||
data_length = int(length_bytes)
|
||||
data = self._recv_exact(data_length)
|
||||
|
||||
# Read the actual binary data
|
||||
chunks = []
|
||||
remaining = data_length
|
||||
while remaining > 0:
|
||||
chunk = self.socket.recv(min(remaining, 65536))
|
||||
if not chunk:
|
||||
raise RuntimeError("Connection closed while reading data")
|
||||
chunks.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
# Read the trailing newline / block separator
|
||||
self._recv_exact(1)
|
||||
|
||||
# Read the trailing newline
|
||||
self.socket.recv(1)
|
||||
|
||||
return b''.join(chunks)
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
|
||||
Reference in New Issue
Block a user