Pass/fail mask reverse-engineering, presented in Python. Boy it would have been easier if my Rigol rep had just been willing to tell me.
class PassFailData(bytearray):
"""Represents Rigol pass/fail data.
Treated as a bytearray, the layout of the mask data is:
* An 8 byte constant header
* Little-endian uint16 WINDOW_ON, where 0 is no window (full-screen) and
1 is a window
* Little-endian uint16 WINDOW_LEFT (0-699), ignored if not WINDOW_ON
* Little-endian uint16 WINDOW_RIGHT (0-699), ignored if not WINDOW_ON
* 700 pairs of (LOWER, UPPER), where each is a byte from 0 (bottom of the
screen) to 255 (top of the screen), working from the left of the screen
on to the right. Pairs that index left of WINDOW_LEFT or right of
WINDOW_RIGHT should be (0, 255) so as to enforce no mask.
Parameters:
arg (str, bytes): If arg is string, it's interpreted as the name of
a .pf file to open. If it's bytes (or a bytearray) then it's the
raw data that such a file would contain. The default, None,
creates an empty mask.
.. note::
This is EXTREMELY experimental. All information about the file
format has been derived through reverse-engineering and emperical
guessing. Test thoroughly before any kind of production use.
"""
DATALEN = 1400
FILELEN = DATALEN + 14
# First 8 bytes of the file are constant.
HEADER = b'\x03\x00\x00\x00x\x05\x0e\x00'
def __init__(self, arg=None):
# Set up the alternate views onto this array. There is an 8 byte
# constant header, then 3 little endian uint16s representing
# window on? 0:false, 1: true
# window left: 0-699
# window right: 0-699
self[:] = bytes(self.FILELEN)
self._params = np.frombuffer(self, dtype="<u2", count=3, offset=8)
self._maskdata = np.frombuffer(self, dtype=np.uint8, offset=14)
if arg is None:
self[:len(self.HEADER)] = self.HEADER
self.upper[:] = 0xFF
elif isinstance(arg, str):
self.read(arg)
elif isinstance(arg, (bytearray, bytes)):
self.setdata(arg)
else:
raise TypeError('invalid arg ' + type(arg).__name__)
def setdata(self, data):
if len(data) != self.FILELEN:
raise ValueError("Incorrect mask data length")
if not data.startswith(self.HEADER):
log.warning("Bad mask header")
self[:] = data
def __repr__(self):
wnd = self.window
if wnd:
return '{}({}-{})'.format(type(self).__name__, wnd[0], wnd[1])
else:
return '{}(all)'.format(type(self).__name__)
def read(self, filename):
"""Read a .pf file."""
with open(filename, 'rb') as f:
self.setdata(f.read(self.FILELEN + 1))
@property
def window(self):
"""Pass/fail window extents.
Either None or a 2-tuple of (left, right), where both left and right
are in the range 0-699.
"""
if self._params[0]:
return tuple(self._params[1:])
else:
return None
@window.setter
def window(self, value):
if value is None:
self._params[0] = 0
elif len(value) != 2:
raise ValueError('window must be None or 2-tuple')
elif all(0 <= v < 700 for v in value):
self._params[0] = 1
self._params[1:] = value
else:
raise ValueError('window must have 0 <= left, right < 700')
@property
def lower(self):
"""Upper mask limit.
700 data points in the range 0-255.
"""
return self._maskdata[0::2]
@property
def upper(self):
"""Upper mask limit.
700 data points in the range 0-255.
"""
return self._maskdata[1::2]