cleaning and updated readme

This commit is contained in:
2018-06-18 07:45:58 +02:00
parent 666e18c57e
commit 3a5ac05d9d
56 changed files with 72 additions and 3209 deletions
+449
View File
@@ -0,0 +1,449 @@
import array
import time
import gex
from gex import TF, TF_Msg
from gex.Client import EventReport
import numpy as np
CMD_READ_RAW = 0
CMD_READ_SMOOTHED = 1
CMD_READ_CAL_CONSTANTS = 2
CMD_GET_ENABLED_CHANNELS = 10
CMD_GET_SAMPLE_RATE = 11
CMD_SETUP_TRIGGER = 20
CMD_ARM = 21
CMD_DISARM = 22
CMD_ABORT = 23
CMD_FORCE_TRIGGER = 24
CMD_BLOCK_CAPTURE = 25
CMD_STREAM_START = 26
CMD_STREAM_STOP = 27
CMD_SET_SMOOTHING_FACTOR = 28
CMD_SET_SAMPLE_RATE = 29
CMD_ENABLE_CHANNELS = 30
CMD_SET_SAMPLE_TIME = 31
EVT_CAPT_START = 50
EVT_CAPT_MORE = 51
EVT_CAPT_DONE = 52
class TriggerReport:
def __init__(self, data, edge, pretrig, timestamp):
self.data = data
self.edge = edge
self.pretrig = pretrig
self.timestamp = timestamp
def __str__(self):
return "EventReport(edge %d, pretrig len %d, ts %d, data %s)" % (self.edge, self.pretrig, self.timestamp, self.data)
class ADC_CalData:
def __init__(self, pp:gex.PayloadParser):
self.VREFINT_CAL = pp.u16() # ADC raw value for VREFINT, 30C ambient
self.VREFINT_CAL_VADCREF = pp.u16() # Analog reference voltage during VREFINT calibration (mV) +-10mV
self.TSENSE_CAL1 = pp.u16() # ADC raw value in point 1
self.TSENSE_CAL2 = pp.u16() # ADC raw value in point 2
self.TSENSE_CAL1_TEMP = pp.u8() # Temperature for point 1 (Celsius) +-5C
self.TSENSE_CAL2_TEMP = pp.u8() # Temperature for point 2 (Celsius) +-5C
self.TSENSE_CAL_VADCREF = pp.u16() # Analog reference voltage during TSENSE calibration (mV) +-10mV
def __str__(self):
return "ADC_CalData(VREFINT=%d at Vref=%d mV, TSENSE_%dC=%d, TSENSE_%dC=%d at Vref=%d mV)" % \
(self.VREFINT_CAL, self.VREFINT_CAL_VADCREF,
self.TSENSE_CAL1_TEMP, self.TSENSE_CAL1, self.TSENSE_CAL2_TEMP, self.TSENSE_CAL2, self.TSENSE_CAL_VADCREF)
# TODO utility for converting raw values to real voltage / temperature
class ADC(gex.Unit):
"""
ADC device
"""
def _type(self):
return 'ADC'
def _init(self):
self._trig_buf = None
self._trig_edge = 0 # 1, 2, 3
self._trig_pretrig_len = 0
self._trig_next_id = 0
self._trig_listener = None
self._trig_ts = 0
self._stream_next_id = 0
self._stream_running = False
self._stream_listener = None
self.channels = self.get_channels()
(_, self.sample_rate) = self.get_sample_rate()
def _on_trig_capt(self, msg:TF_Msg):
pp = gex.PayloadParser(msg.data)
if self._trig_buf is None:
raise Exception("Unexpected capture data frame")
# All but the first trig capture frame are prefixed by a sequence number
idx = pp.u8()
if idx != self._trig_next_id:
raise Exception("Lost capture data frame! Expected %d, got %d" % (self._trig_next_id, idx))
self._trig_next_id = (self._trig_next_id + 1) % 256
self._trig_buf.extend(pp.tail())
if msg.type == EVT_CAPT_DONE:
if self._trig_listener is not None:
self._trig_listener(TriggerReport(data=self._parse_buffer(self._trig_buf),
edge=self._trig_edge,
pretrig=self._trig_pretrig_len,
timestamp=self._trig_ts))
self._trig_buf = None
# We keep the trig listener
return TF.CLOSE
else:
return TF.STAY
def _on_stream_capt(self, msg:TF_Msg):
pp = gex.PayloadParser(msg.data)
if not self._stream_running:
raise Exception("Unexpected stream data frame")
if msg.type == EVT_CAPT_DONE:
if self._stream_listener is not None:
self._stream_listener(None) # Indicate it's closed
# We keep the stream listener, so user doesnt have to set it before each stream
self._stream_running = False
return TF.CLOSE
else:
# All stream data frames are prefixed by a sequence number
idx = pp.u8()
if idx != self._stream_next_id:
self._stream_running = False
raise Exception("Lost stream data frame! Expected %d, got %d" % (self._stream_next_id, idx))
self._stream_next_id = (self._stream_next_id + 1) % 256
tail = pp.tail()
if self._stream_listener is not None:
self._stream_listener(self._parse_buffer(tail))
return TF.STAY
def _on_event(self, evt:EventReport):
"""
Handle a trigger or stream start event.
- EVT_CAPT_START
First frame payload: edge:u8, pretrig_len:u32, payload:tail
Following are plain TF frames with the same ID, each prefixed with a sequence number in 1 byte.
Type EVT_CAPT_MORE or EVT_CAPT_DONE indicate whether this is the last frame of the sequence,
after which the ID listener should be removed.
"""
pp = gex.PayloadParser(evt.payload)
msg = evt.msg
if evt.code == EVT_CAPT_START:
if self._trig_buf is not None:
raise Exception("Unexpected start of capture")
self._trig_ts = evt.timestamp
self._trig_buf = bytearray()
self._trig_pretrig_len = pp.u32()
self._trig_edge = pp.u8()
self._trig_next_id = 0
msg.data = pp.tail()
# the rest is a regular capture frame with seq
self._on_trig_capt(msg)
self.client.tf.add_id_listener(msg.id, lambda tf,msg: self._on_trig_capt(msg))
def get_channels(self):
"""
Find enabled channel numbers.
Returns a list.
"""
msg = self._query(CMD_GET_ENABLED_CHANNELS)
return list(msg.data)
def get_calibration_data(self):
"""
Read ADC calibration data
"""
msg = self._query(CMD_READ_CAL_CONSTANTS)
return ADC_CalData(gex.PayloadParser(msg.data))
def set_sample_rate(self, freq:int):
""" Set sample rate in Hz. Returns the real achieved frequency as float. """
pb = gex.PayloadBuilder()
pb.u32(freq)
msg = self._query(CMD_SET_SAMPLE_RATE, pld=pb.close())
pp = gex.PayloadParser(msg.data)
req = pp.u32()
real = pp.float()
self.sample_rate = real
return real
def set_sample_time(self, sample_time:int, confirm=True):
""" Set sample time. Values 0-7 """
pb = gex.PayloadBuilder()
pb.u8(sample_time)
self._send(CMD_SET_SAMPLE_TIME, pld=pb.close(), confirm=confirm)
def get_sample_rate(self):
"""
Get the current real sample rate as float.
Returns tuple (requested:int, real:float)
"""
msg = self._query(CMD_GET_SAMPLE_RATE)
pp = gex.PayloadParser(msg.data)
req = pp.u32()
real = pp.float()
return (req, real)
def set_smoothing_factor(self, fac, confirm=True):
""" Set smoothing factor for read_smooth(), range 0-1.0 """
pb = gex.PayloadBuilder()
pb.u16(round(fac*1000))
self._send(CMD_SET_SMOOTHING_FACTOR, pld=pb.close(), confirm=confirm)
def read_raw(self):
""" Read raw values. Returns a dict. """
msg = self._query(CMD_READ_RAW)
pp = gex.PayloadParser(msg)
chs = dict()
i = 0
while pp.length() > 0:
chs[self.channels[i]] = pp.u16()
i += 1
return chs
def read_smooth(self):
""" Read smoothed values (floats). Returns a dict. """
msg = self._query(CMD_READ_SMOOTHED)
pp = gex.PayloadParser(msg)
chs = dict()
i = 0
while pp.length() > 0:
chs[self.channels[i]] = pp.float()
i += 1
return chs
def on_trigger(self, lst):
""" Set the trigger handler """
self._trig_listener = lst
def off_trigger(self):
""" Remove the trigger handler """
self.on_trigger(None)
def setup_trigger(self, channel, level, count,
edge='rising', pretrigger=0, holdoff=100,
auto=False, confirm=True, handler=None):
"""
Configure a trigger.
channel - 0-17 (16-tsense, 17-vrefint)
level - triggering threshold, raw (0-4095)
count - nbr of samples to capture after trigger
edge - "rising", "falling" or "both"
pretrigger - nbr of samples to capture before the trigger occurred. Limited by the internal buffer.
holdoff - hold-off time (trigger also can't fire while the capture is ongoing, and if it's not armed)
auto - auto re-arm after completing the capture. Normally the state switches to IDLE.
handler - attaches a callback handler for the received data
"""
nedge = 0
if edge == 'rising' or edge == 'up':
nedge = 1
elif edge == 'falling' or edge == 'down':
nedge = 2
elif edge == 'both':
nedge = 3
else:
raise Exception("Bad edge arg")
pb = gex.PayloadBuilder()
pb.u8(channel)
pb.u16(level)
pb.u8(nedge)
pb.u32(pretrigger)
pb.u32(count)
pb.u16(holdoff)
pb.bool(auto)
self._send(cmd=CMD_SETUP_TRIGGER, pld=pb.close(), confirm=confirm)
if handler is not None:
self._trig_listener = handler
def arm(self, auto=None, confirm=True):
"""
ARM for trigger.
The trigger must be configured first.
if auto is True or False, it sets the auto-rearm flag.
"""
pb = gex.PayloadBuilder()
if auto is None:
pb.u8(255)
else:
pb.u8(1 if auto else 0)
self._send(cmd=CMD_ARM, pld=pb.close(), confirm=confirm)
def disarm(self, confirm=True):
"""
DISARM.
No effect if not armed.
Always clears the auto-arm flag.
"""
self._send(cmd=CMD_DISARM, confirm=confirm)
def abort(self, confirm=True):
"""
Abort any ongoing capture and dis-arm.
Also clears the auto-arm flag.
"""
self._send(cmd=CMD_ABORT, confirm=confirm)
def force(self, handler=None, confirm=True):
"""
Force a trigger, including pre-trigger capture.
The device behavior is identical as if the trigger condition occurred naturally.
The captured data is received asynchronously via an event.
"""
if handler is not None:
self.on_trigger(handler)
self._send(cmd=CMD_FORCE_TRIGGER, confirm=confirm)
def set_active_channels(self, channels, confirm=True):
"""
Set which channels should be active.
"""
pb = gex.PayloadBuilder()
pb.u32(self.pins2int(channels))
self._send(cmd=CMD_ENABLE_CHANNELS, pld=pb.close(), confirm=confirm)
self.channels = self.pins2list(channels)
def _parse_buffer(self, buf):
"""
Convert a raw buffer to a more useful format
"""
arr = np.array(array.array('h', buf))
return np.reshape(arr, (-1,len(self.channels)))
def capture_in_progress(self):
return self._stream_running or self._trig_buf is not None
def capture(self, count, timeout=None):
"""
Start a block capture.
This is similar to a forced trigger, but has custom size and doesn't include any pre-trigger.
The captured data is received synchronously and returned as a dict of channel arrays
"""
if self.capture_in_progress():
raise Exception("Another capture already in progress")
if timeout is None:
timeout = 1 + float(count)/self.sample_rate * 2
#print("Timeout = %f" % timeout)
pb = gex.PayloadBuilder()
pb.u32(count)
buffer = bytearray()
self._bcap_next_id = 0
self._bcap_done = False
self._stream_running = True # we use this flag to block any concurrent access
def lst(frame):
pp = gex.PayloadParser(frame.data)
if frame.type == EVT_CAPT_MORE or len(frame.data) != 0:
index = pp.u8()
if index != self._bcap_next_id:
self._bcap_done = True
raise Exception("Lost capture data frame! Expected %d, got %d" % (self._bcap_next_id, index))
#return TF.CLOSE XXX
self._bcap_next_id = (self._bcap_next_id + 1) % 256
buffer.extend(pp.tail())
if frame.type == EVT_CAPT_DONE:
self._bcap_done = True
return TF.CLOSE
return TF.STAY
self._query_async(cmd=CMD_BLOCK_CAPTURE, pld=pb.close(), callback=lst)
# wait with a timeout
self.client.transport.poll(timeout, lambda: self._bcap_done == True)
self._stream_running = False
if not self._bcap_done:
self.abort()
raise Exception("Capture not completed within timeout")
return self._parse_buffer(buffer)
def on_stream(self, lst):
self._stream_listener = lst
def off_stream(self, lst):
self.on_stream(None)
def stream_start(self, lst=None):
""" Start a capture stream """
if self.capture_in_progress():
raise Exception("Another capture already in progress")
self._stream_next_id = 0
self._stream_running = True
if lst is not None:
self._stream_listener = lst
self._query_async(cmd=CMD_STREAM_START, callback=self._on_stream_capt)
def stream_stop(self, delay=0.1, confirm=True):
"""
Stop a stream. Delay is an extra time before removing the listener
to let the queued frames to finish being received.
"""
if not self._stream_running:
raise Exception("Not streaming")
self._send(cmd=CMD_STREAM_STOP, confirm=confirm)
time.sleep(delay)
self._stream_listener = None
+228
View File
@@ -0,0 +1,228 @@
import gex
CMD_WAVE_DC = 0
CMD_WAVE_SINE = 1
CMD_WAVE_TRIANGLE = 2
CMD_WAVE_SAWTOOTH_UP = 3
CMD_WAVE_SAWTOOTH_DOWN = 4
CMD_WAVE_RECTANGLE = 5
CMD_SYNC = 10
CMD_SET_FREQUENCY = 20
CMD_SET_PHASE = 21
CMD_SET_DITHER = 22
LUT_LEN = 8192
class DAC(gex.Unit):
"""
Analog output (2 channels)
"""
def _type(self):
return 'DAC'
def dc(self, channel, level, confirm=True):
"""
Set DC levels, 0-4095. None to leave the level unchanged
channel: 1,2 (3 = both)
level: 0-4095
"""
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
pb = gex.PayloadBuilder()
pb.u8(channel)
pb.u16(level)
if channel==3:
pb.u16(level)
self._send(CMD_WAVE_DC, pld=pb.close(), confirm=confirm)
def dc_dual(self, ch1, ch2, confirm=True):
"""
Set DC levels, 0-4095
"""
pb = gex.PayloadBuilder()
pb.u8(0b11)
pb.u16(ch1)
pb.u16(ch2)
self._send(CMD_WAVE_DC, pld=pb.close(), confirm=confirm)
def rectangle(self, channel, duty=None, high=None, low=None, confirm=True):
""" Enter rectangle gen mode (duty 0..1000) """
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
pb = gex.PayloadBuilder()
pb.u8(channel) # 0b01 or 0b10
for i in range(0,1 if channel != 3 else 2): # repeat if dual
pb.u16(round(duty * LUT_LEN) if duty is not None # todo ??
else 0xFFFF)
pb.u16(high if high is not None else 0xFFFF)
pb.u16(low if low is not None else 0xFFFF)
self._send(CMD_WAVE_RECTANGLE, pld=pb.close(), confirm=confirm)
def rectangle_dual(self,
duty1=None, duty2=None,
high1=None, high2=None,
low1=None, low2=None,
confirm=True):
""" Set rectangle dual (both at once in sync) """
pb = gex.PayloadBuilder()
pb.u8(0b11) # 0b01 or 0b10
pb.u16(round(duty1*LUT_LEN))
pb.u16(high1 if high1 is not None else 0xFFFF)
pb.u16(low1 if low1 is not None else 0xFFFF)
pb.u16(round(duty2*LUT_LEN))
pb.u16(high2 if high2 is not None else 0xFFFF)
pb.u16(low2 if low2 is not None else 0xFFFF)
self._send(CMD_WAVE_RECTANGLE, pld=pb.close(), confirm=confirm)
def sync(self, confirm=True):
self._send(CMD_SYNC, confirm=confirm)
def waveform(self, channel, waveform, confirm=True):
"""
Set a waveform. For DC or rectangle,
use the dedicated functions with extra parameters
channel: 1,2 (3 = both)
waveform:
- None - leave unchanged
- SINE
- TRIANGLE
- SAW_UP
- SAW_DOWN
"""
lookup = {'SINE': CMD_WAVE_SINE,
'SIN': CMD_WAVE_SINE,
'TRI': CMD_WAVE_TRIANGLE,
'TRIANGLE': CMD_WAVE_TRIANGLE,
'SAW': CMD_WAVE_SAWTOOTH_UP,
'RAMP': CMD_WAVE_SAWTOOTH_UP,
'RAMP_UP': CMD_WAVE_SAWTOOTH_UP,
'SAW_UP': CMD_WAVE_SAWTOOTH_UP,
'SAW_DOWN': CMD_WAVE_SAWTOOTH_DOWN,
'RAMP_DOWN': CMD_WAVE_SAWTOOTH_DOWN,
}
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
pb = gex.PayloadBuilder()
pb.u8(channel) # 0b01 or 0b10
self._send(lookup[waveform], pld=pb.close(), confirm=confirm)
def set_frequency(self, channel, freq, confirm=True):
"""
Set frequency using float in Hz
"""
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
pb = gex.PayloadBuilder()
pb.u8(channel)
pb.float(freq)
if channel == 3:
pb.float(freq)
self._send(CMD_SET_FREQUENCY, pld=pb.close(), confirm=confirm)
def set_frequency_dual(self, freq1, freq2, confirm=True):
"""
Set frequency of both channels using float in Hz
"""
pb = gex.PayloadBuilder()
pb.u8(0b11)
pb.float(freq1)
pb.float(freq2)
self._send(CMD_SET_FREQUENCY, pld=pb.close(), confirm=confirm)
def set_phase(self, channel, phase360, confirm=True):
"""
Set channel phase relative to it's "base phase".
If both channels use the same frequency, this could be used for drawing XY figures.
"""
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
pb = gex.PayloadBuilder()
pb.u8(channel)
pb.u16(round((phase360/360) * LUT_LEN))
if channel == 3:
pb.u16(round((phase360/360) * LUT_LEN))
self._send(CMD_SET_PHASE, pld=pb.close(), confirm=confirm)
def set_phase_dual(self, phase1, phase2, confirm=True):
"""
Set phase for both channels at once
"""
pb = gex.PayloadBuilder()
pb.u8(0b11)
pb.u16((phase1/360) * LUT_LEN)
pb.u16((phase2/360) * LUT_LEN)
self._send(CMD_SET_PHASE, pld=pb.close(), confirm=confirm)
def set_dither(self, channel, type=None, bits=None, confirm=True):
"""
Set dithering (superimposed noise waveform)
type: NONE, TRIANGLE, WHITE
bits: 1-12
"""
if channel != 1 and channel != 2 and channel != 3:
raise Exception("Bad channel arg: %s" % channel)
lookup = {'NONE': 0,
'WHITE': 1,
'NOISE': 1,
'TRIANGLE': 2,
'TRI': 2}
pb = gex.PayloadBuilder()
pb.u8(channel)
for i in range(0,1 if channel != 3 else 2): # repeat if dual
pb.u8(lookup[type] if type is not None else 0xFF)
pb.u8(bits if bits is not None else 0xFF)
self._send(CMD_SET_DITHER, pld=pb.close(), confirm=confirm)
+66
View File
@@ -0,0 +1,66 @@
import gex
from gex.Client import EventReport
class DIn(gex.Unit):
"""
Digital input port.
Pins are represented by bits of a control word, right-aligned.
For example, if pins C6, C5 and C0 are selected for the unit,
the read word has the format (bits) |<C6><C5><C0>|
"""
def _init(self):
self.handlers = {}
def _type(self):
return 'DI'
def read(self):
""" Read pins """
msg = self._query(0x00)
pp = gex.PayloadParser(msg)
return pp.u16()
def arm(self, pins, auto:bool=False, confirm:bool=False):
"""
Arm pins for single shot event generation
pins - array of pin indices to arm
auto - use auto trigger (auto re-arm after hold-off)
"""
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
self._send(0x02 if auto else 0x01, pb.close())
def disarm(self, pins, confirm:bool=False):
"""
DisArm pins
pins - array of pin indices to arm
"""
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
self._send(0x03, pb.close())
def on_trigger(self, sensitive_pins, callback):
"""
Assign a trigger callback.
Pins are passed as a list of indices (packed), or a bitmap
Arguments are: pins snapshot, timestamp
"""
for i in self.pins2list(sensitive_pins):
self.handlers[i] = callback
def _on_event(self, evt:EventReport):
if evt.code == 0x00:
# trigger interrupt
pp = gex.PayloadParser(evt.payload)
triggersources = pp.u16() # multiple can happen at once
snapshot = pp.u16()
for i in range(0,16):
if triggersources & (1<<i):
if i in self.handlers:
self.handlers[i](snapshot, evt.timestamp)
+67
View File
@@ -0,0 +1,67 @@
import gex
CMD_WRITE = 0
CMD_SET = 1
CMD_CLEAR = 2
CMD_TOGGLE = 3
CMD_PULSE = 4
class DOut(gex.Unit):
"""
Digital output port.
Pins are represented by bits of a control word, right-aligned.
For example, if pins C6, C5 and C0 are selected for the unit,
calling the "set" function with a word 0b111 will set all three to 1,
0b100 will set only C6.
"""
def _type(self):
return 'DO'
def write(self, pins:int, confirm=True):
""" Set pins to a value - packed, as int """
pb = gex.PayloadBuilder()
pb.u16(pins)
self._send(CMD_WRITE, pb.close(), confirm=confirm)
def set(self, pins=1, confirm=True):
""" Set pins high - packed, int or list """
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
self._send(CMD_SET, pb.close(), confirm=confirm)
def clear(self, pins=1, confirm=True):
""" Set pins low - packed, int or list """
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
self._send(CMD_CLEAR, pb.close(), confirm=confirm)
def toggle(self, pins=1, confirm=True):
""" Toggle pins - packed, int or list """
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
self._send(CMD_TOGGLE, pb.close(), confirm=confirm)
def pulse_ms(self, ms, pins=0b01, active=True, confirm=True):
""" Send a pulse with length 1-65535 ms on selected pins """
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
pb.bool(active)
pb.bool(False)
pb.u16(ms)
self._send(CMD_PULSE, pb.close(), confirm=confirm)
def pulse_us(self, us, pins=1, active=True, confirm=True):
""" Send a pulse of 1-999 us on selected pins """
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(pins))
pb.bool(active)
pb.bool(True)
pb.u16(us)
self._send(CMD_PULSE, pb.close(), confirm=confirm)
+274
View File
@@ -0,0 +1,274 @@
import gex
CMD_STOP = 0
# Measuring a waveform
CMD_INDIRECT_CONT_START = 1 # keep measuring, read on demand
CMD_INDIRECT_BURST_START = 2 # wait and reply
# Counting pulses
CMD_DIRECT_CONT_START = 3 # keep measuring, read on demand
CMD_DIRECT_BURST_START = 4 # wait and reply
CMD_FREECOUNT_START = 5 # keep counting pulses until stopped, read on reply
CMD_MEASURE_SINGLE_PULSE = 6
CMD_FREECOUNT_CLEAR = 7
# Results readout for continuous modes
CMD_INDIRECT_CONT_READ = 10
CMD_DIRECT_CONT_READ = 11
CMD_FREECOUNT_READ = 12
CMD_SET_POLARITY = 20
CMD_SET_DIR_PRESC = 21
CMD_SET_INPUT_FILTER = 22
CMD_SET_DIR_MSEC = 23
CMD_RESTORE_DEFAULTS = 30
class FCAP_Report:
def __init__(self):
self.period = None # s
self.ontime = None # s
self.frequency = None # Hz
self.duty = None # [-]
# Raw data (can be used to avoid distortion by float arithmetics)
self.period_raw = None
self.ontime_raw = None
self.sample_count = None
self.clock_freq = None # Hz
self.meas_time_ms = None
def __str__(self):
s = "{\n"
if self.frequency is not None:
s += " f = %f Hz\n" % self.frequency
if self.period is not None:
s += " T = %f s\n" % self.period
if self.ontime is not None:
s += " Ton = %f s\n" % self.ontime
if self.duty is not None:
s += " duty = %f\n" % self.duty
s += "}"
return s
class FCAP(gex.Unit):
"""
Frequency and pulse measurement
"""
def _type(self):
return 'FCAP'
def stop(self, confirm=True):
""" Stop any ongoing capture """
self._send(CMD_STOP, confirm=confirm)
def configure(self,
polarity=None,
presc=None,
filter=None,
msec=None,
confirm=True):
"""
Re-configure some capture parameters. None = unchanged
polarity: 0,1 active level
presc: 1,2,4,8 pulse counter prescaller
filter: 0-15 digital input filter
msec: <65535 milliseconds for direct capture
"""
if polarity is not None:
pb = gex.PayloadBuilder()
pb.u8(polarity) # 0,1
self._send(CMD_SET_POLARITY, pld=pb.close(), confirm=confirm)
if presc is not None:
pb = gex.PayloadBuilder()
pb.u8(presc)
self._send(CMD_SET_DIR_PRESC, pld=pb.close(), confirm=confirm)
if filter is not None:
pb = gex.PayloadBuilder()
pb.u8(filter)
self._send(CMD_SET_INPUT_FILTER, pld=pb.close(), confirm=confirm)
if msec is not None:
pb = gex.PayloadBuilder()
pb.u16(msec)
self._send(CMD_SET_DIR_MSEC, pld=pb.close(), confirm=confirm)
def config_reset(self, confirm=True):
""" Reset all config to persistent defaults and switch to IDLE mode. """
self._send(CMD_RESTORE_DEFAULTS, confirm=confirm)
def indirect_start(self, confirm=True):
""" Start continuous PWM measurement """
self._send(CMD_INDIRECT_CONT_START, confirm=confirm)
def counter_start(self, presc=None, confirm=True):
""" Start the free-running counter """
pb = gex.PayloadBuilder()
pb.u8(presc or 0)
self._send(CMD_FREECOUNT_START, pld=pb.close(), confirm=confirm)
def counter_read(self):
""" Read the free counter value """
resp = self._query(CMD_FREECOUNT_READ)
pp = gex.PayloadParser(resp.data)
return pp.u32()
def counter_clear(self):
"""
Restart the free-running counter, returns current value before the clear.
This should lose at most 1 tick for signals where f < core clock speed
"""
resp = self._query(CMD_FREECOUNT_CLEAR)
pp = gex.PayloadParser(resp.data)
return pp.u32()
def direct_start(self, msec=None, presc=None, confirm=True):
"""
Start continuous PWM measurement
msec - measurement time (ms), <65535
presc - pre-divider, 1,2,4,8.
arg None = unchanged
"""
pb = gex.PayloadBuilder()
pb.u16(msec or 0)
pb.u8(presc or 0)
self._send(CMD_DIRECT_CONT_START, pld=pb.close(), confirm=confirm)
def indirect_read(self):
"""
Read the current indirect continuous measurement values
Returns value of the last measurement in continuous indirect mode
"""
resp = self._query(CMD_INDIRECT_CONT_READ)
pp = gex.PayloadParser(resp.data)
mhz = pp.u16()
period = pp.u32()
ontime = pp.u32()
rp = FCAP_Report()
rp.period = period / (mhz*1e6) # to seconds
rp.frequency = 1 / rp.period
rp.ontime = ontime / (mhz*1e6) # in seconds
rp.duty = rp.ontime / rp.period
rp.clock_freq = mhz*1e6
rp.sample_count = 1
rp.period_raw = period
rp.ontime_raw = ontime
# returned in microseconds
return rp
def _process_direct_resp(self, resp):
pp = gex.PayloadParser(resp.data)
presc = pp.u8()
msec = pp.u16()
count = pp.u32() * presc
rp = FCAP_Report()
if count > 0:
sec = msec / 1000
freq = count / sec
period = 1 / freq
rp.period = period
rp.frequency = freq
rp.sample_count = count * presc
rp.meas_time_ms = msec
return rp
def direct_read(self):
"""
Read the current direct continuous measurement values
Returns value of the last measurement in continuous direct mode
"""
resp = self._query(CMD_DIRECT_CONT_READ)
return self._process_direct_resp(resp)
def measure_pulse(self, polarity=None, timeout=5):
"""
Measure a pulse. Optionally set polarity
"""
if polarity is not None:
self.configure(polarity=polarity)
resp = self._query(CMD_MEASURE_SINGLE_PULSE, timeout=timeout)
pp = gex.PayloadParser(resp.data)
mhz = pp.u16()
ontime = pp.u32()
rp = FCAP_Report()
rp.ontime = ontime / (mhz * 1e6) # in seconds
rp.clock_freq = mhz * 1e6
rp.sample_count = 1
rp.ontime_raw = ontime
return rp
def indirect_burst(self, count, timeout=5):
"""
Perform a burst measure with averaging (sum/count)
"""
pb = gex.PayloadBuilder()
pb.u16(count)
resp = self._query(CMD_INDIRECT_BURST_START, pld=pb.close(), timeout=timeout)
pp = gex.PayloadParser(resp.data)
mhz = pp.u16()
nsamp = pp.u16()
period = pp.u64()
ontime = pp.u64()
rp = FCAP_Report()
rp.period = period / (nsamp*mhz*1e6) # to seconds
rp.frequency = 1 / rp.period
rp.ontime = ontime / (nsamp*mhz*1e6) # in seconds
rp.duty = rp.ontime / rp.period
rp.clock_freq = mhz*1e6
rp.sample_count = 1
rp.period_raw = period
rp.ontime_raw = ontime
return rp
def direct_burst(self, msec=1000, presc=None):
"""
Perform direct burst measurement
"""
pb = gex.PayloadBuilder()
pb.u16(msec)
pb.u8(presc or 0)
resp = self._query(CMD_DIRECT_BURST_START,
pld=pb.close(),
timeout=(msec/1000)+1)
return self._process_direct_resp(resp)
+102
View File
@@ -0,0 +1,102 @@
import gex
class I2C(gex.Unit):
"""
I2C master direct access
"""
def _type(self):
return 'I2C'
def _begin_i2c_pld(self, address:int, a10bit:bool=False):
pb = gex.PayloadBuilder()
if a10bit: address |= 0x8000 # indication for the Unit driver that it's a 10b address
pb.u16(address)
return pb
def write(self, address:int, payload, a10bit:bool=False, confirm=True):
"""
Write to an address
"""
pb = self._begin_i2c_pld(address, a10bit)
pb.blob(payload) # payload to write
self._send(0x00, pb.close(), confirm=confirm)
def read(self, address:int, count, a10bit:bool=False):
"""
Read from an address
"""
pb = self._begin_i2c_pld(address, a10bit)
pb.u16(count) # number of bytes to read
self._query(0x01, pb.close())
def read_reg(self, address:int, reg, width:int=1, a10bit:bool=False, endian='little'):
"""
Read a single register
"""
return self.read_regs(address, reg, count=1, width=width, a10bit=a10bit, endian=endian)[0]
def read_regs(self, address:int, reg, count:int, width:int=1, a10bit:bool=False, endian='little'):
"""
Read multiple registers from an address
"""
pb = self._begin_i2c_pld(address, a10bit)
pb.u8(reg)
pb.u16(width*count) # we assume the device will auto-increment (most do)
resp = self._query(0x03, pb.close())
fields = []
pp = gex.PayloadParser(resp.data, endian=endian)
if width==1:
for i in range(0, count):
fields.append(pp.u8())
elif width==2:
for i in range(0, count):
fields.append(pp.u16())
elif width==3:
for i in range(0, count):
fields.append(pp.u24())
elif width==4:
for i in range(0, count):
fields.append(pp.u32())
else:
raise Exception("Bad width")
return fields
def write_reg(self, address:int, reg, value, width:int=1, a10bit:bool=False, endian='little', confirm=True):
"""
Write a to a single register.
value can be int or array (in which case `width` applies to each item)
"""
pb = self._begin_i2c_pld(address, a10bit)
pb.u8(reg)
pb.endian = endian
arr = value
if type(arr) is int:
arr = [value]
if width == 1:
pb.blob(arr)
elif width == 2:
for v in arr:
pb.u16(v)
elif width == 3:
for v in arr:
pb.u24(v)
elif width == 4:
for v in arr:
pb.u32(v)
else:
raise Exception("Bad width")
self._send(0x02, pb.close(), confirm=confirm)
def write_byte_data(self, address, reg, value):
""" Compatibility alias for python3-smbus """
return self.write_reg(address, reg, value)
def write_i2c_block_data(self, address, reg, block):
""" Compatibility alias for python3-smbus """
return self.write_reg(address, reg, block)
+33
View File
@@ -0,0 +1,33 @@
import gex
class Neopixel(gex.Unit):
"""
Raw access to a neopixel strip.
"""
def _type(self):
return 'NPX'
def get_len(self):
""" Get the neopixel strip length """
resp = self._query(10)
pp = gex.PayloadParser(resp)
return pp.u16()
def load(self, colors, reverse=True, confirm=True):
"""
Load colors to the strip.
The numbers are normally 0xRRGGBB
If 'reverse' is false, they're treated as little-endian: 0xBBGGRR.
"""
pb = gex.PayloadBuilder(endian='big' if reverse else 'little')
for c in colors:
pb.u24(c)
self._send(1, pb.close(), confirm=confirm)
def clear(self, confirm=True):
"""
Reset the strip (set all to black)
"""
self._send(0, confirm=confirm)
+64
View File
@@ -0,0 +1,64 @@
import gex
class OneWire(gex.Unit):
"""
Dallas 1-Wire master
"""
def _type(self):
return '1WIRE'
def test_presence(self):
""" Test presence fo any 1wire devices on the bus """
resp = self._query(0)
pp = gex.PayloadParser(resp)
return pp.bool()
def read_address(self, as_array=False):
""" Read the address of a lone device on the bus """
resp = self._query(4)
pp = gex.PayloadParser(resp)
if as_array:
return list(pp.tail())
else:
return pp.u64()
def search(self, alarm=False):
""" Find all devices, or devices with alarm """
devices = []
resp = self._query(2 if alarm else 1)
hasmore = True
while hasmore:
pp = gex.PayloadParser(resp)
hasmore = pp.bool()
while pp.length() > 0:
devices.append(pp.u64())
if hasmore:
resp = self._query(3)
return devices
def query(self, request, rcount, addr=0, verify=True, as_array=False):
""" Query a device """
pb = gex.PayloadBuilder()
pb.u64(addr)
pb.u16(rcount)
pb.bool(verify)
pb.blob(request)
resp = self._query(11, pb.close())
return resp.data if not as_array else list(resp.data)
def write(self, payload, addr=0, confirm=True):
""" Write to a device """
pb = gex.PayloadBuilder()
pb.u64(addr)
pb.blob(payload)
self._send(10, pb.close(), confirm=confirm)
def wait_ready(self):
""" Wait for DS18x20 to complete measurement (or other chip using the same polling mechanism) """
self._query(20)
+43
View File
@@ -0,0 +1,43 @@
import gex
CMD_SET_FREQUENCY = 0
CMD_SET_DUTY = 1
CMD_STOP = 2
CMD_START = 3
class PWMDim(gex.Unit):
"""
Simple 4-channel PWM output with a common frequency
"""
def _type(self):
return 'PWMDIM'
def set_frequency(self, hertz:int, confirm=True):
""" Set freq """
pb = gex.PayloadBuilder()
pb.u32(hertz)
self._send(CMD_SET_FREQUENCY, pb.close(), confirm=confirm)
def set_duty(self, duty_dict, confirm=True):
""" Set duty (dict - number1234 -> duty 0-1000) """
pb = gex.PayloadBuilder()
for (k,v) in enumerate(duty_dict):
pb.u8(k-1)
pb.u16(v)
self._send(CMD_SET_DUTY, pb.close(), confirm=confirm)
def set_duty_single(self, ch1234, duty1000, confirm=True):
""" Set duty of a single channel """
pb = gex.PayloadBuilder()
pb.u8(ch1234-1)
pb.u16(duty1000)
self._send(CMD_SET_DUTY, pb.close(), confirm=confirm)
def stop(self, confirm=True):
self._send(CMD_STOP, confirm=confirm)
def start(self, confirm=True):
self._send(CMD_START, confirm=confirm)
+48
View File
@@ -0,0 +1,48 @@
import gex
CMD_WRITE = 0
CMD_DIRECT_DATA = 1
CMD_DIRECT_SHIFT = 2
CMD_DIRECT_CLEAR = 3
CMD_DIRECT_STORE = 4
class SIPO(gex.Unit):
"""
Multi-channel SIPO driver
Designed for loading up to 16 74xx595 or 74xx4094 serial-input-parallel-output shift registers
The number of drivers can be significantly expanded via daisy-chaining.
"""
def _type(self):
return 'SIPO'
def load(self, buffers, end=0x0000, confirm=True):
""" Load data - buffers is a list of lists or byte arrays """
if type(buffers[0]) == int:
buffers = [buffers]
pb = gex.PayloadBuilder()
pb.u16(end)
for b in buffers:
pb.blob(b)
self._send(CMD_WRITE, pb.close(), confirm=confirm)
def set_data(self, packed:int, confirm=True):
""" Manually set the data pins """
pb = gex.PayloadBuilder()
pb.u16(packed)
self._send(CMD_DIRECT_DATA, pb.close(), confirm=confirm)
def shift(self, confirm=True):
""" Manually send a shift pulse (shift data one step further into the registers) """
self._send(CMD_DIRECT_SHIFT, confirm=confirm)
def store(self, confirm=True):
""" Manually send a store pulse (copy the shift register data to the outputs) """
self._send(CMD_DIRECT_STORE, confirm=confirm)
def clear(self, confirm=True):
""" Manually send a clear pulse (if connected correctly, this immediately resets the shift register outputs) """
self._send(CMD_DIRECT_CLEAR, confirm=confirm)
+52
View File
@@ -0,0 +1,52 @@
import gex
class SPI(gex.Unit):
"""
SPI master direct access
"""
def _type(self):
return 'SPI'
def query(self, slave:int, tbytes, rlen:int, rskip:int=-1, confirm=True):
"""
Query a slave.
If rskip is -1 (default), the tbytes length will be used.
Set it to 0 to skip nothing.
slave is 0-based index
"""
if rskip == -1:
rskip = len(tbytes)
pb = gex.PayloadBuilder()
pb.u8(slave)
pb.u16(rskip)
pb.u16(rlen)
pb.blob(tbytes)
# SPI does not respond if rlen is 0, but can be enforced using 'confirm'
if rlen > 0:
resp = self._query(0x00, pb.close())
return resp.data
else:
# write only
self._send(0x00, pb.close(), confirm=confirm)
return []
def write(self, slave:int, tbytes, confirm=True):
"""
Write with no response received
"""
self.query(slave, tbytes, rlen=0, rskip=0, confirm=confirm)
def multicast(self, slaves, tbytes, confirm=True):
"""
Write with multiple slaves at once.
Slaves is a right-aligned bitmap (eg. pins 0,2,3 would be 0b1101), or a list of active positions
"""
pb = gex.PayloadBuilder()
pb.u16(self.pins2int(slaves))
pb.blob(tbytes)
self._send(0x01, pb.close(), confirm=confirm)
+64
View File
@@ -0,0 +1,64 @@
import gex
from gex.Client import EventReport
CMD_READ = 0
CMD_SET_BIN_THR = 1
CMD_DISABLE_ALL_REPORTS = 2
CMD_GET_CH_COUNT = 10
class TOUCH(gex.Unit):
"""
Touch sensing
"""
def _init(self):
self._handlers = {}
def _type(self):
return 'TOUCH'
def read(self):
""" Read raw values """
msg = self._query(CMD_READ)
pp = gex.PayloadParser(msg)
items = []
while pp.length() > 0:
items.append(pp.u16())
return items
def set_button_thresholds(self, thresholds, confirm=True):
""" Set binary report thresholds """
pb = gex.PayloadBuilder()
for t in thresholds:
pb.u16(t)
self._send(CMD_SET_BIN_THR, pb.close(), confirm=confirm)
def disable_button_mode(self, confirm=True):
""" Disable all button reports by clearing the thresholds """
self._send(CMD_DISABLE_ALL_REPORTS, confirm=confirm)
def get_channel_count(self, confirm=True):
""" Read nbr of channels """
resp = self._query(CMD_GET_CH_COUNT)
pp = gex.PayloadParser(resp)
return pp.u8()
def listen(self, nb, handler):
self._handlers[nb] = handler
def _on_event(self, evt:EventReport):
l = []
pp = gex.PayloadParser(evt.payload)
snap = pp.u32()
changed = pp.u32()
for i in range(0, 32):
if changed & (1 << i):
if i in self._handlers:
self._handlers[i]((snap & (1 << i)) != 0, evt.timestamp)
+87
View File
@@ -0,0 +1,87 @@
import threading
import gex
from gex.Client import EventReport
class USART(gex.Unit):
"""
USART
"""
def _init(self):
self.handler_decode = None
self.handler = None
self.buffer = bytearray()
self.rxwaitnum = 0
self.rxdoneSem = threading.Semaphore()
def _type(self):
return 'USART'
def listen(self, handler, decode='utf-8'):
"""
Attach a Rx listener callback.
decode can be: None, 'utf-8', 'ascii' (any valid encoding for bytearray.decode())
None decoding returns bytearray
handler receives args: (bytes, timestamp)
"""
self.handler_decode = decode
self.handler = handler
def write(self, payload, sync=False, confirm=True):
"""
Write bytes. If 'sync' is True, wait for completion. sync implies confirm
"""
if type(payload) is str:
payload = payload.encode('utf-8')
pb = gex.PayloadBuilder()
pb.blob(payload) # payload to write
self._send(0x01 if sync else 0x00, pb.close(), confirm=confirm or sync)
def _on_event(self, evt:EventReport):
if evt.code == 0:
# Data received
if self.handler:
data = evt.payload if self.handler_decode is None \
else evt.payload.decode(self.handler_decode)
self.handler(data, evt.timestamp)
else:
self.buffer.extend(evt.payload)
if len(self.buffer) >= self.rxwaitnum:
self.rxdoneSem.release()
def clear_buffer(self):
self.buffer = bytearray()
def receive(self, nbytes, decode='utf-8', timeout=0.1):
if self.handler is not None:
raise Exception("Can't call .receive() with an async handler registered!")
if len(self.buffer) >= nbytes:
chunk = self.buffer[0:nbytes]
self.buffer = self.buffer[nbytes:] # put the rest back for later...
if decode is not None:
return chunk.decode(decode)
else:
return chunk
self.rxwaitnum = nbytes
self.rxdoneSem.acquire() # claim
# now the event handler releases the sem and we can take it again
suc = self.rxdoneSem.acquire(timeout=timeout)
# and release it back, to get into a defined state
self.rxdoneSem.release()
if not suc:
if len(self.buffer) < nbytes:
raise Exception("Data not Rx in timeout!")
# use the handling code above via recursion
return self.receive(nbytes, decode, timeout)