parent
ba93523fb1
commit
a15c5b6ebe
@ -0,0 +1,201 @@ |
|||||||
|
import time |
||||||
|
|
||||||
|
import serial |
||||||
|
import usb.core |
||||||
|
import threading |
||||||
|
|
||||||
|
class BaseGexTransport: |
||||||
|
""" Base class for GEX transports """ |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
self._listener = None |
||||||
|
|
||||||
|
def close(self): |
||||||
|
# Tell the thread to shut down |
||||||
|
raise Exception("Not implemented") |
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb): |
||||||
|
""" End of a with block, close the thread """ |
||||||
|
self.close() |
||||||
|
|
||||||
|
def __enter__(self): |
||||||
|
""" This is needed for with blocks to work """ |
||||||
|
return self |
||||||
|
|
||||||
|
def write(self, buffer): |
||||||
|
""" Send a buffer of bytes """ |
||||||
|
raise Exception("Not implemented") |
||||||
|
|
||||||
|
def listen(self, listener): |
||||||
|
""" Attach a listener for incoming bytes """ |
||||||
|
self._listener = listener |
||||||
|
|
||||||
|
def poll(self, timeout, testfunc=None): |
||||||
|
""" |
||||||
|
Receive bytes until a timeout, testfunc returns True, |
||||||
|
or first data if no testfunc is given |
||||||
|
""" |
||||||
|
raise Exception("Not implemented") |
||||||
|
|
||||||
|
|
||||||
|
class SerialSync (BaseGexTransport): |
||||||
|
""" |
||||||
|
Transport based on pySerial, no async support. |
||||||
|
Call poll() to receive spontaneous events or responses. |
||||||
|
|
||||||
|
This can be used only if EXPOSE_ACM is enabled |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self, port='/dev/ttyACM0'): |
||||||
|
""" port - device to open """ |
||||||
|
super().__init__() |
||||||
|
self._serial = serial.Serial(port=port, timeout=0.3) |
||||||
|
|
||||||
|
def close(self): |
||||||
|
# Tell the thread to shut down |
||||||
|
self._serial.close() |
||||||
|
|
||||||
|
def write(self, buffer): |
||||||
|
""" Send a buffer of bytes """ |
||||||
|
self._serial.write(buffer) |
||||||
|
|
||||||
|
def poll(self, timeout, testfunc=None): |
||||||
|
""" |
||||||
|
Receive bytes until a timeout, testfunc returns True, |
||||||
|
or first data if no testfunc is given |
||||||
|
""" |
||||||
|
first = True |
||||||
|
attempts = 10 |
||||||
|
while attempts > 0: |
||||||
|
rv = bytearray() |
||||||
|
|
||||||
|
# Blocking read with a timeout |
||||||
|
if first: |
||||||
|
rv.extend(self._serial.read(1)) |
||||||
|
first = False |
||||||
|
|
||||||
|
# Non-blocking read of the rest |
||||||
|
rv.extend(self._serial.read(self._serial.in_waiting)) |
||||||
|
|
||||||
|
if 0 == len(rv): |
||||||
|
# nothing was read |
||||||
|
if testfunc is None or testfunc(): |
||||||
|
# TF is in base state, we're done |
||||||
|
return |
||||||
|
else: |
||||||
|
# Wait for TF to finish the frame |
||||||
|
attempts -= 1 |
||||||
|
first = True |
||||||
|
else: |
||||||
|
if self._listener: |
||||||
|
self._listener(rv) |
||||||
|
|
||||||
|
|
||||||
|
class RawUSB (BaseGexTransport): |
||||||
|
""" |
||||||
|
pyUSB-based transport with minimal overhead and async IO |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self, sn=None): |
||||||
|
""" sn - GEX serial number """ |
||||||
|
super().__init__() |
||||||
|
|
||||||
|
self.dataSem = threading.Semaphore() |
||||||
|
self.dataSem.acquire() |
||||||
|
|
||||||
|
GEX_ID = (0x0483, 0x572a) |
||||||
|
|
||||||
|
# -------------------- FIND THE DEVICE ------------------------ |
||||||
|
|
||||||
|
def dev_match(d): |
||||||
|
if (d.idVendor, d.idProduct) != GEX_ID: |
||||||
|
return False |
||||||
|
|
||||||
|
# Match only by ID if serial not given |
||||||
|
if sn is None: |
||||||
|
return True |
||||||
|
|
||||||
|
# Reading the S/N can fail with insufficient permissions (wrong udev rules) |
||||||
|
# Note that this error will happen later when configuring the device, too |
||||||
|
try: |
||||||
|
if d.serial_number == sn: |
||||||
|
return True |
||||||
|
except Exception as e: |
||||||
|
print(e) |
||||||
|
pass |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
dev = usb.core.find(custom_match=dev_match) |
||||||
|
if dev is None: |
||||||
|
raise Exception("Found no matching and accessible device.") |
||||||
|
self._dev = dev |
||||||
|
|
||||||
|
# -------------------- PREPARE TO CONNECT --------------------- |
||||||
|
|
||||||
|
# If the ACM interface is visible (not 255), the system driver may be attached. |
||||||
|
# Here we tear that down and expose the raw endpoints |
||||||
|
|
||||||
|
def detach_kernel_driver(dev, iface): |
||||||
|
if dev.is_kernel_driver_active(1): |
||||||
|
try: |
||||||
|
dev.detach_kernel_driver(1) |
||||||
|
except usb.core.USBError as e: |
||||||
|
raise Exception("Could not detach kernel driver from iface %d: %s" % (iface, str(e))) |
||||||
|
|
||||||
|
# EP0 - control |
||||||
|
# EP1 - VFS in/out |
||||||
|
# EP2 - CDC data in/out |
||||||
|
# EP3 - CDC control |
||||||
|
|
||||||
|
detach_kernel_driver(dev, 2) # CDC data |
||||||
|
detach_kernel_driver(dev, 3) # CDC control |
||||||
|
|
||||||
|
# Set default configuration |
||||||
|
# (this will fail if we don't have the right permissions) |
||||||
|
dev.set_configuration() |
||||||
|
|
||||||
|
# We could now print the configuration |
||||||
|
#cfg = dev.get_active_configuration() |
||||||
|
|
||||||
|
# ----------------------- RX THREAD --------------------------- |
||||||
|
|
||||||
|
# The reception is done using a thread. |
||||||
|
# It ends when _ending is set True |
||||||
|
self._ending = False |
||||||
|
|
||||||
|
def worker(): |
||||||
|
while not self._ending: |
||||||
|
try: |
||||||
|
resp = self._dev.read(0x82, 64, 100) |
||||||
|
if self._listener is not None: |
||||||
|
self._listener(bytearray(resp)) |
||||||
|
self.dataSem.release() # notify we have data |
||||||
|
except usb.USBError: |
||||||
|
pass # timeout |
||||||
|
|
||||||
|
t = threading.Thread(target=worker) |
||||||
|
t.start() |
||||||
|
|
||||||
|
# Save a reference for calling join() later |
||||||
|
self._thread = t |
||||||
|
|
||||||
|
def close(self): |
||||||
|
# Tell the thread to shut down |
||||||
|
self._ending = True |
||||||
|
self._thread.join() |
||||||
|
|
||||||
|
def write(self, buffer): |
||||||
|
""" Send a buffer of bytes """ |
||||||
|
self._dev.write(0x02, buffer, 100) |
||||||
|
|
||||||
|
def poll(self, timeout, testfunc=None): |
||||||
|
# Using time.sleep() would block for too long. Instead we release the semaphore on each Rx chunk of data |
||||||
|
# and then check if it's what we wanted (let TF handle it and call the listener) |
||||||
|
start = time.time() |
||||||
|
while time.time() - start < timeout: |
||||||
|
self.dataSem.acquire() |
||||||
|
if testfunc is None or testfunc(): |
||||||
|
break |
||||||
|
pass |
||||||
|
|
Loading…
Reference in new issue