cleaning and updated readme
This commit is contained in:
+449
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user