[Sw] Add CLI-Tool.

This commit is contained in:
peng-zhihui 2022-02-09 11:59:08 +08:00
parent fe1007c668
commit be2804ac0e
32 changed files with 2438 additions and 0 deletions

30
3.Software/CLI-Tool/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Python Distribution / packaging
.Python
#env/
#build/
#develop-eggs/
/dist/
#downloads/
#eggs/
#.eggs/
#lib/
#lib64/
#parts/
#sdist/
#var/
/*.egg-info/
#.installed.cfg
#*.egg
/MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
*.pyc
# Installer logs
pip-log.txt
pip-delete-this-directory.txt

8
3.Software/CLI-Tool/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/../../../../../../../:\onWorking\_Private\REF-STM32F4\3.Software\cmd_tool\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

17
3.Software/CLI-Tool/.idea/cmd_tool.iml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="workspace" level="project" />
<orderEntry type="library" name="ROS" level="project" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="pytest" />
</component>
</module>

14
3.Software/CLI-Tool/.idea/deployment.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="root@192.168.31.101:8022">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

4
3.Software/CLI-Tool/.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

View File

@ -0,0 +1,21 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="8">
<item index="0" class="java.lang.String" itemvalue="tqdm" />
<item index="1" class="java.lang.String" itemvalue="scipy" />
<item index="2" class="java.lang.String" itemvalue="tensorboard" />
<item index="3" class="java.lang.String" itemvalue="seaborn" />
<item index="4" class="java.lang.String" itemvalue="opencv-python" />
<item index="5" class="java.lang.String" itemvalue="PyYAML" />
<item index="6" class="java.lang.String" itemvalue="matplotlib" />
<item index="7" class="java.lang.String" itemvalue="pycocotools" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@ -0,0 +1,7 @@
<component name="libraryTable">
<library name="ROS">
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,7 @@
<component name="libraryTable">
<library name="workspace">
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

4
3.Software/CLI-Tool/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
</project>

8
3.Software/CLI-Tool/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cmd_tool.iml" filepath="$PROJECT_DIR$/.idea/cmd_tool.iml" />
</modules>
</component>
</project>

12
3.Software/CLI-Tool/.idea/ros.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ROSSettings">
<option name="rosPath" value="" />
<option name="workspacePath" value="" />
<option name="additionalSources" value="" />
<option name="excludedXmls" value="" />
<option name="licenseLinkType" value="Summary" />
<option name="knownKeys" value="" />
<option name="depSources" value="https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml&quot;https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml&quot;https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/ruby.yaml" />
</component>
</project>

6
3.Software/CLI-Tool/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python3
from __future__ import print_function
import ref_tool
# Find a connected REF-Unit (this will block until you connect one)
print("finding an ref_tool...")
my_drive = ref_tool.find_any()
# Find an REF-Unit that is connected on the serial port /dev/ttyUSB0
# my_drive = ref_tool.find_any("serial:/dev/ttyUSB0")

View File

@ -0,0 +1,5 @@
from .discovery import find_any, find_all
from .utils import Event, Logger, TimeoutError
from .protocol import ChannelBrokenException, ChannelDamagedException
from .shell import launch_shell

View File

@ -0,0 +1,129 @@
"""
Provides functions for the discovery of Fibre nodes
"""
import sys
import json
import time
import threading
import traceback
import fibre.protocol
import fibre.utils
import fibre.remote_object
from fibre.utils import Event, Logger
from fibre.protocol import ChannelBrokenException, TimeoutError
# Load all installed transport layers
channel_types = {}
try:
import fibre.usbbulk_transport
channel_types['usb'] = fibre.usbbulk_transport.discover_channels
except ImportError:
pass
try:
import fibre.serial_transport
channel_types['serial'] = fibre.serial_transport.discover_channels
except ImportError:
pass
try:
import fibre.tcp_transport
channel_types['tcp'] = fibre.tcp_transport.discover_channels
except ImportError:
pass
try:
import fibre.udp_transport
channel_types['udp'] = fibre.udp_transport.discover_channels
except ImportError:
pass
def noprint(text):
pass
def find_all(path, serial_number,
did_discover_object_callback,
search_cancellation_token,
channel_termination_token,
logger):
"""
Starts scanning for Fibre nodes that match the specified path spec and calls
the callback for each Fibre node that is found.
This function is non-blocking.
"""
def did_discover_channel(channel):
"""
Inits an object from a given channel and then calls did_discover_object_callback
with the created object
This queries the endpoint 0 on that channel to gain information
about the interface, which is then used to init the corresponding object.
"""
try:
logger.debug("Connecting to device on " + channel._name)
try:
json_bytes = channel.remote_endpoint_read_buffer(0)
except (TimeoutError, ChannelBrokenException):
logger.debug("no response - probably incompatible")
return
json_crc16 = fibre.protocol.calc_crc16(fibre.protocol.PROTOCOL_VERSION, json_bytes)
channel._interface_definition_crc = json_crc16
try:
json_string = json_bytes.decode("ascii")
except UnicodeDecodeError:
logger.debug("device responded on endpoint 0 with something that is not ASCII")
return
logger.debug("JSON: " + json_string.replace('{"name"', '\n{"name"'))
logger.debug("JSON checksum: 0x{:02X} 0x{:02X}".format(json_crc16 & 0xff, (json_crc16 >> 8) & 0xff))
try:
json_data = json.loads(json_string)
except json.decoder.JSONDecodeError as error:
logger.debug("device responded on endpoint 0 with something that is not JSON: " + str(error))
return
json_data = {"name": "fibre_node", "members": json_data}
obj = fibre.remote_object.RemoteObject(json_data, None, channel, logger)
obj.__dict__['_json_data'] = json_data['members']
obj.__dict__['_json_crc'] = json_crc16
device_serial_number = fibre.utils.get_serial_number_str(obj)
if serial_number != None and device_serial_number != serial_number:
logger.debug("Ignoring device with serial number {}".format(device_serial_number))
return
did_discover_object_callback(obj)
except Exception:
logger.debug("Unexpected exception after discovering channel: " + traceback.format_exc())
# For each connection type, kick off an appropriate discovery loop
for search_spec in path.split(','):
prefix = search_spec.split(':')[0]
the_rest = ':'.join(search_spec.split(':')[1:])
if prefix in channel_types:
t = threading.Thread(target=channel_types[prefix],
args=(the_rest, serial_number, did_discover_channel, search_cancellation_token, channel_termination_token, logger))
t.daemon = True
t.start()
else:
raise Exception("Invalid path spec \"{}\"".format(search_spec))
def find_any(path="usb", serial_number=None,
search_cancellation_token=None, channel_termination_token=None,
timeout=None, logger=Logger(verbose=False)):
"""
Blocks until the first matching Fibre node is connected and then returns that node
"""
result = [ None ]
done_signal = Event(search_cancellation_token)
def did_discover_object(obj):
result[0] = obj
done_signal.set()
find_all(path, serial_number, did_discover_object, done_signal, channel_termination_token, logger)
try:
done_signal.wait(timeout=timeout)
finally:
done_signal.set() # terminate find_all
return result[0]

View File

@ -0,0 +1,365 @@
# See protocol.hpp for an overview of the protocol
import time
import struct
import sys
import threading
import traceback
#import fibre.utils
from fibre.utils import Event, wait_any, TimeoutError
import abc
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta('ABC', (), {})
if sys.version_info < (3, 3):
from monotonic import monotonic
time.monotonic = monotonic
SYNC_BYTE = 0xAA
CRC8_INIT = 0x42
CRC16_INIT = 0x1337
PROTOCOL_VERSION = 1
CRC8_DEFAULT = 0x37 # this must match the polynomial in the C++ implementation
CRC16_DEFAULT = 0x3d65 # this must match the polynomial in the C++ implementation
MAX_PACKET_SIZE = 128
def calc_crc(remainder, value, polynomial, bitwidth):
topbit = (1 << (bitwidth - 1))
# Bring the next byte into the remainder.
remainder ^= (value << (bitwidth - 8))
for bitnumber in range(0,8):
if (remainder & topbit):
remainder = (remainder << 1) ^ polynomial
else:
remainder = (remainder << 1)
return remainder & ((1 << bitwidth) - 1)
def calc_crc8(remainder, value):
if isinstance(value, bytearray) or isinstance(value, bytes) or isinstance(value, list):
for byte in value:
if not isinstance(byte,int):
byte = ord(byte)
remainder = calc_crc(remainder, byte, CRC8_DEFAULT, 8)
else:
remainder = calc_crc(remainder, byte, CRC8_DEFAULT, 8)
return remainder
def calc_crc16(remainder, value):
if isinstance(value, bytearray) or isinstance(value, bytes) or isinstance(value, list):
for byte in value:
if not isinstance(byte, int):
byte = ord(byte)
remainder = calc_crc(remainder, byte, CRC16_DEFAULT, 16)
else:
remainder = calc_crc(remainder, value, CRC16_DEFAULT, 16)
return remainder
# Can be verified with http://www.sunshine2k.de/coding/javascript/crc/crc_js.html:
#print(hex(calc_crc8(0x12, [1, 2, 3, 4, 5, 0x10, 0x13, 0x37])))
#print(hex(calc_crc16(0xfeef, [1, 2, 3, 4, 5, 0x10, 0x13, 0x37])))
class DeviceInitException(Exception):
pass
class ChannelDamagedException(Exception):
"""
Raised when the channel is temporarily broken and a
resend of the message might be successful
"""
pass
class ChannelBrokenException(Exception):
"""
Raised when the channel is permanently broken
"""
pass
class StreamSource(ABC):
@abc.abstractmethod
def get_bytes(self, n_bytes, deadline):
pass
class StreamSink(ABC):
@abc.abstractmethod
def process_bytes(self, bytes):
pass
class PacketSource(ABC):
@abc.abstractmethod
def get_packet(self, deadline):
pass
class PacketSink(ABC):
@abc.abstractmethod
def process_packet(self, packet):
pass
class StreamToPacketSegmenter(StreamSink):
def __init__(self, output):
self._header = []
self._packet = []
self._packet_length = 0
self._output = output
def process_bytes(self, bytes):
"""
Processes an arbitrary number of bytes. If one or more full packets are
are received, they are sent to this instance's output PacketSink.
Incomplete packets are buffered between subsequent calls to this function.
"""
for byte in bytes:
if (len(self._header) < 3):
# Process header byte
self._header.append(byte)
if (len(self._header) == 1) and (self._header[0] != SYNC_BYTE):
self._header = []
elif (len(self._header) == 2) and (self._header[1] & 0x80):
self._header = [] # TODO: support packets larger than 128 bytes
elif (len(self._header) == 3) and calc_crc8(CRC8_INIT, self._header):
self._header = []
elif (len(self._header) == 3):
self._packet_length = self._header[1] + 2
else:
# Process payload byte
self._packet.append(byte)
# If both header and packet are fully received, hand it on to the packet processor
if (len(self._header) == 3) and (len(self._packet) == self._packet_length):
if calc_crc16(CRC16_INIT, self._packet) == 0:
self._output.process_packet(self._packet[:-2])
self._header = []
self._packet = []
self._packet_length = 0
class StreamBasedPacketSink(PacketSink):
def __init__(self, output):
self._output = output
def process_packet(self, packet):
if (len(packet) >= MAX_PACKET_SIZE):
raise NotImplementedError("packet larger than 127 currently not supported")
header = bytearray()
header.append(SYNC_BYTE)
header.append(len(packet))
header.append(calc_crc8(CRC8_INIT, header))
self._output.process_bytes(header)
self._output.process_bytes(packet)
# append CRC in big endian
crc16 = calc_crc16(CRC16_INIT, packet)
self._output.process_bytes(struct.pack('>H', crc16))
class PacketFromStreamConverter(PacketSource):
def __init__(self, input):
self._input = input
def get_packet(self, deadline):
"""
Requests bytes from the underlying input stream until a full packet is
received or the deadline is reached, in which case None is returned. A
deadline before the current time corresponds to non-blocking mode.
"""
while True:
header = bytes()
'''
# TODO: sometimes this call hangs, even though the device apparently sent something
header = header + self._input.get_bytes_or_fail(1, deadline)
if (header[0] != SYNC_BYTE):
#print("sync byte mismatch")
continue
header = header + self._input.get_bytes_or_fail(1, deadline)
if (header[1] & 0x80):
#print("packet too large")
continue # TODO: support packets larger than 128 bytes
header = header + self._input.get_bytes_or_fail(1, deadline)
if calc_crc8(CRC8_INIT, header) != 0:
#print("crc8 mismatch")
continue
packet_length = header[1] + 2
#print("wait for {} bytes".format(packet_length))
packet = self._input.get_bytes_or_fail(packet_length, deadline)
'''
header = self._input.get_bytes_or_fail(3, deadline)
#print("wait for {} bytes".format(packet_length))
packet = self._input.get_bytes_or_fail(header[1], deadline)
packet = packet + self._input.get_bytes_or_fail(2, deadline)
if calc_crc16(CRC16_INIT, packet) != 0:
#print("crc16 mismatch")
continue
return packet[:-2]
class Channel(PacketSink):
# Choose these parameters to be sensible for a specific transport layer
_resend_timeout = 5.0 # [s]
_send_attempts = 5
def __init__(self, name, input, output, cancellation_token, logger):
"""
Params:
input: A PacketSource where this channel will source packets from on
demand. Alternatively packets can be provided to this channel
directly by calling process_packet on this instance.
output: A PacketSink where this channel will put outgoing packets.
"""
self._name = name
self._input = input
self._output = output
self._logger = logger
self._outbound_seq_no = 0
self._interface_definition_crc = 0
self._expected_acks = {}
self._responses = {}
self._my_lock = threading.Lock()
self._channel_broken = Event(cancellation_token)
self.start_receiver_thread(Event(self._channel_broken))
def start_receiver_thread(self, cancellation_token):
"""
Starts the receiver thread that processes incoming messages.
The thread quits as soon as the channel enters a broken state.
"""
def receiver_thread():
error_ctr = 0
try:
while (not cancellation_token.is_set() and not self._channel_broken.is_set()
and error_ctr < 10):
# Set an arbitrary deadline because the get_packet function
# currently doesn't support a cancellation_token
deadline = time.monotonic() + 1.0
try:
response = self._input.get_packet(deadline)
except TimeoutError:
continue # try again
except ChannelDamagedException:
error_ctr += 1
continue # try again
if (error_ctr > 0):
error_ctr -= 1
# Process response
# This should not throw an exception, otherwise the channel breaks
self.process_packet(response)
#print("receiver thread is exiting")
except Exception:
self._logger.debug("receiver thread is exiting: " + traceback.format_exc())
finally:
self._channel_broken.set()
t = threading.Thread(target=receiver_thread)
t.daemon = True
t.start()
def remote_endpoint_operation(self, endpoint_id, input, expect_ack, output_length):
if input is None:
input = bytearray(0)
if (len(input) >= 128):
raise Exception("packet larger than 127 currently not supported")
if (expect_ack):
endpoint_id |= 0x8000
self._my_lock.acquire()
try:
self._outbound_seq_no = ((self._outbound_seq_no + 1) & 0x7fff)
seq_no = self._outbound_seq_no
finally:
self._my_lock.release()
seq_no |= 0x80 # FIXME: we hardwire one bit of the seq-no to 1 to avoid conflicts with the ascii protocol
packet = struct.pack('<HHH', seq_no, endpoint_id, output_length)
packet = packet + input
crc16 = calc_crc16(CRC16_INIT, packet)
if (endpoint_id & 0x7fff == 0):
trailer = PROTOCOL_VERSION
else:
trailer = self._interface_definition_crc
#print("append trailer " + trailer)
packet = packet + struct.pack('<H', trailer)
if (expect_ack):
ack_event = Event()
self._expected_acks[seq_no] = ack_event
try:
attempt = 0
while (attempt < self._send_attempts):
self._my_lock.acquire()
try:
self._output.process_packet(packet)
except ChannelDamagedException:
attempt += 1
continue # resend
except TimeoutError:
attempt += 1
continue # resend
finally:
self._my_lock.release()
# Wait for ACK until the resend timeout is exceeded
try:
if wait_any(self._resend_timeout, ack_event, self._channel_broken) != 0:
raise ChannelBrokenException()
except TimeoutError:
attempt += 1
continue # resend
return self._responses.pop(seq_no)
# TODO: record channel statistics
raise ChannelBrokenException() # Too many resend attempts
finally:
self._expected_acks.pop(seq_no)
self._responses.pop(seq_no, None)
else:
# fire and forget
self._output.process_packet(packet)
return None
def remote_endpoint_read_buffer(self, endpoint_id):
"""
Handles reads from long endpoints
"""
# TODO: handle device that could (maliciously) send infinite stream
buffer = bytes()
while True:
chunk_length = 512
chunk = self.remote_endpoint_operation(endpoint_id, struct.pack("<I", len(buffer)), True, chunk_length)
if (len(chunk) == 0):
break
buffer += chunk
return buffer
def process_packet(self, packet):
#print("process packet")
packet = bytes(packet)
if (len(packet) < 2):
raise Exception("packet too short")
seq_no = struct.unpack('<H', packet[0:2])[0]
if (seq_no & 0x8000):
seq_no &= 0x7fff
ack_signal = self._expected_acks.get(seq_no, None)
if (ack_signal):
self._responses[seq_no] = packet[2:]
ack_signal.set()
#print("received ack for packet " + str(seq_no))
else:
print("received unexpected ACK: " + str(seq_no))
else:
#if (calc_crc16(CRC16_INIT, struct.pack('<HBB', PROTOCOL_VERSION, packet[-2], packet[-1]))):
# raise Exception("CRC16 mismatch")
print("endpoint requested")
# TODO: handle local endpoint operation

View File

@ -0,0 +1,264 @@
"""
Provides functions for the discovery of Fibre nodes
"""
import sys
import json
import struct
import threading
import fibre.protocol
class ObjectDefinitionError(Exception):
pass
codecs = {}
class StructCodec():
"""
Generic serializer/deserializer based on struct pack
"""
def __init__(self, struct_format, target_type):
self._struct_format = struct_format
self._target_type = target_type
def get_length(self):
return struct.calcsize(self._struct_format)
def serialize(self, value):
value = self._target_type(value)
return struct.pack(self._struct_format, value)
def deserialize(self, buffer):
value = struct.unpack(self._struct_format, buffer)
value = value[0] if len(value) == 1 else value
return self._target_type(value)
class RemoteProperty():
"""
Used internally by dynamically created objects to translate
property assignments and fetches into endpoint operations on the
object's associated channel
"""
def __init__(self, json_data, parent):
self._parent = parent
self.__channel__ = parent.__channel__
id_str = json_data.get("id", None)
if id_str is None:
raise ObjectDefinitionError("unspecified endpoint ID")
self._id = int(id_str)
self._name = json_data.get("name", None)
if self._name is None:
self._name = "[anonymous]"
type_str = json_data.get("type", None)
if type_str is None:
raise ObjectDefinitionError("unspecified type")
# Find all codecs that match the type_str and build a dictionary
# of the form {type1: codec1, type2: codec2}
eligible_types = {k: v[type_str] for (k,v) in codecs.items() if type_str in v}
if not eligible_types:
raise ObjectDefinitionError("unsupported codec {}".format(type_str))
# TODO: better heuristics to select a matching type (i.e. prefer non lossless)
eligible_types = list(eligible_types.items())
self._property_type = eligible_types[0][0]
self._codec = eligible_types[0][1]
access_mode = json_data.get("access", "r")
self._can_read = 'r' in access_mode
self._can_write = 'w' in access_mode
def get_value(self):
buffer = self._parent.__channel__.remote_endpoint_operation(self._id, None, True, self._codec.get_length())
return self._codec.deserialize(buffer)
def set_value(self, value):
buffer = self._codec.serialize(value)
# TODO: Currenly we wait for an ack here. Settle on the default guarantee.
self._parent.__channel__.remote_endpoint_operation(self._id, buffer, True, 0)
def _dump(self):
if self._name == "serial_number":
# special case: serial number should be displayed in hex (TODO: generalize)
val_str = "{:012X}".format(self.get_value())
elif self._name == "error":
# special case: errors should be displayed in hex (TODO: generalize)
val_str = "0x{:04X}".format(self.get_value())
else:
val_str = str(self.get_value())
return "{} = {} ({})".format(self._name, val_str, self._property_type.__name__)
class EndpointRefCodec():
"""
Serializer/deserializer for an endpoint reference
"""
def get_length(self):
return struct.calcsize("<HH")
def serialize(self, value):
if value is None:
(ep_id, ep_crc) = (0, 0)
elif isinstance(value, RemoteProperty):
(ep_id, ep_crc) = (value._id, value.__channel__._interface_definition_crc)
else:
raise TypeError("Expected value of type RemoteProperty or None but got '{}'. En example for a RemoteProperty is this expression: odrv0.axis0.controller._remote_attributes['pos_setpoint']".format(type(value).__name__))
return struct.pack("<HH", ep_id, ep_crc)
def deserialize(self, buffer):
return struct.unpack("<HH", buffer)
codecs[int] = {
'int8': StructCodec("<b", int),
'uint8': StructCodec("<B", int),
'int16': StructCodec("<h", int),
'uint16': StructCodec("<H", int),
'int32': StructCodec("<i", int),
'uint32': StructCodec("<I", int),
'int64': StructCodec("<q", int),
'uint64': StructCodec("<Q", int)
}
codecs[bool] = {
'bool': StructCodec("<?", bool)
}
codecs[float] = {
'float': StructCodec("<f", float)
}
codecs[RemoteProperty] = {
'endpoint_ref': EndpointRefCodec()
}
class RemoteFunction(object):
"""
Represents a callable function that maps to a function call on a remote object
"""
def __init__(self, json_data, parent):
self._parent = parent
id_str = json_data.get("id", None)
if id_str is None:
raise ObjectDefinitionError("unspecified endpoint ID")
self._trigger_id = int(id_str)
self._name = json_data.get("name", None)
if self._name is None:
self._name = "[anonymous]"
self._inputs = []
for param_json in json_data.get("arguments", []) + json_data.get("inputs", []): # TODO: deprecate "arguments" keyword
param_json["mode"] = "r"
self._inputs.append(RemoteProperty(param_json, parent))
self._outputs = []
for param_json in json_data.get("outputs", []): # TODO: deprecate "arguments" keyword
param_json["mode"] = "r"
self._outputs.append(RemoteProperty(param_json, parent))
def __call__(self, *args):
if (len(self._inputs) != len(args)):
raise TypeError("expected {} arguments but have {}".format(len(self._inputs), len(args)))
for i in range(len(args)):
self._inputs[i].set_value(args[i])
self._parent.__channel__.remote_endpoint_operation(self._trigger_id, None, True, 0)
if len(self._outputs) > 0:
return self._outputs[0].get_value()
def _dump(self):
return "{}({})".format(self._name, ", ".join("{}: {}".format(x._name, x._property_type.__name__) for x in self._inputs))
class RemoteObject(object):
"""
Object with functions and properties that map to remote endpoints
"""
def __init__(self, json_data, parent, channel, logger):
"""
Creates an object that implements the specified JSON type description by
communicating over the provided channel
"""
# Directly write to __dict__ to avoid calling __setattr__ too early
object.__getattribute__(self, "__dict__")["_remote_attributes"] = {}
object.__getattribute__(self, "__dict__")["__sealed__"] = False
# Assign once more to make linter happy
self._remote_attributes = {}
self.__sealed__ = False
self.__channel__ = channel
self.__parent__ = parent
# Build attribute list from JSON
for member_json in json_data.get("members", []):
member_name = member_json.get("name", None)
if member_name is None:
logger.debug("ignoring unnamed attribute")
continue
try:
type_str = member_json.get("type", None)
if type_str == "object":
attribute = RemoteObject(member_json, self, channel, logger)
elif type_str == "function":
attribute = RemoteFunction(member_json, self)
elif type_str != None:
attribute = RemoteProperty(member_json, self)
else:
raise ObjectDefinitionError("no type information")
except ObjectDefinitionError as ex:
logger.debug("malformed member {}: {}".format(member_name, str(ex)))
continue
self._remote_attributes[member_name] = attribute
self.__dict__[member_name] = attribute
# Ensure that from here on out assignments to undefined attributes
# raise an exception
self.__sealed__ = True
channel._channel_broken.subscribe(self._tear_down)
def _dump(self, indent, depth):
if depth <= 0:
return "..."
lines = []
for key, val in self._remote_attributes.items():
if isinstance(val, RemoteObject):
val_str = indent + key + (": " if depth == 1 else ":\n") + val._dump(indent + " ", depth - 1)
else:
val_str = indent + val._dump()
lines.append(val_str)
return "\n".join(lines)
def __str__(self):
return self._dump("", depth=2)
def __repr__(self):
return self.__str__()
def __getattribute__(self, name):
attr = object.__getattribute__(self, "_remote_attributes").get(name, None)
if isinstance(attr, RemoteProperty):
if attr._can_read:
return attr.get_value()
else:
raise Exception("Cannot read from property {}".format(name))
elif attr != None:
return attr
else:
return object.__getattribute__(self, name)
#raise AttributeError("Attribute {} not found".format(name))
def __setattr__(self, name, value):
attr = object.__getattribute__(self, "_remote_attributes").get(name, None)
if isinstance(attr, RemoteProperty):
if attr._can_write:
attr.set_value(value)
else:
raise Exception("Cannot write to property {}".format(name))
elif not object.__getattribute__(self, "__sealed__") or name in object.__getattribute__(self, "__dict__"):
object.__getattribute__(self, "__dict__")[name] = value
else:
raise AttributeError("Attribute {} not found".format(name))
def _tear_down(self):
# Clear all remote members
for k in self._remote_attributes.keys():
self.__dict__.pop(k)
self._remote_attributes = {}

View File

@ -0,0 +1,101 @@
"""
Provides classes that implement the StreamSource/StreamSink and
PacketSource/PacketSink interfaces for serial ports.
"""
import os
import re
import time
import traceback
import serial
import serial.tools.list_ports
import fibre
from fibre.utils import TimeoutError
# TODO: make this customizable
DEFAULT_BAUDRATE = 115200
class SerialStreamTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSink):
def __init__(self, port, baud):
self._dev = serial.Serial(port, baud, timeout=1)
def process_bytes(self, bytes):
self._dev.write(bytes)
def get_bytes(self, n_bytes, deadline):
"""
Returns n bytes unless the deadline is reached, in which case the bytes
that were read up to that point are returned. If deadline is None the
function blocks forever. A deadline before the current time corresponds
to non-blocking mode.
"""
if deadline is None:
self._dev.timeout = None
else:
self._dev.timeout = max(deadline - time.monotonic(), 0)
return self._dev.read(n_bytes)
def get_bytes_or_fail(self, n_bytes, deadline):
result = self.get_bytes(n_bytes, deadline)
if len(result) < n_bytes:
raise TimeoutError("expected {} bytes but got only {}", n_bytes, len(result))
return result
def close(self):
self._dev.close()
def find_dev_serial_ports():
try:
return ['/dev/' + x for x in os.listdir('/dev')]
except FileNotFoundError:
return []
def find_pyserial_ports():
return [x.device for x in serial.tools.list_ports.comports()]
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Scans for serial ports that match the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
if path == None:
# This regex should match all desired port names on macOS,
# Linux and Windows but might match some incorrect port names.
regex = r'^(/dev/tty\.usbmodem.*|/dev/ttyACM.*|COM[0-9]+)$'
else:
regex = "^" + path + "$"
known_devices = []
def device_matcher(port_name):
if port_name in known_devices:
return False
return bool(re.match(regex, port_name))
def did_disconnect(port_name, device):
device.close()
# TODO: yes there is a race condition here in case you wonder.
known_devices.pop(known_devices.index(port_name))
while not cancellation_token.is_set():
all_ports = find_pyserial_ports() + find_dev_serial_ports()
new_ports = filter(device_matcher, all_ports)
for port_name in new_ports:
try:
serial_device = SerialStreamTransport(port_name, DEFAULT_BAUDRATE)
input_stream = fibre.protocol.PacketFromStreamConverter(serial_device)
output_stream = fibre.protocol.StreamBasedPacketSink(serial_device)
channel = fibre.protocol.Channel(
"serial port {}@{}".format(port_name, DEFAULT_BAUDRATE),
input_stream, output_stream, channel_termination_token, logger)
channel.serial_device = serial_device
except serial.serialutil.SerialException:
logger.debug("Serial device init failed. Ignoring this port. More info: " + traceback.format_exc())
known_devices.append(port_name)
else:
known_devices.append(port_name)
channel._channel_broken.subscribe(lambda: did_disconnect(port_name, serial_device))
callback(channel)
time.sleep(1)

View File

@ -0,0 +1,116 @@
import sys
import platform
import threading
import fibre
def did_discover_device(device,
interactive_variables, discovered_devices,
branding_short, branding_long,
logger, app_shutdown_token):
"""
Handles the discovery of new devices by displaying a
message and making the device available to the interactive
console
"""
serial_number = '{:012X}'.format(device.serial_number) if hasattr(device, 'serial_number') else "[unknown serial number]"
if serial_number in discovered_devices:
verb = "Reconnected"
index = discovered_devices.index(serial_number)
else:
verb = "Connected"
discovered_devices.append(serial_number)
index = len(discovered_devices) - 1
interactive_name = branding_short + str(index)
# Publish new device to interactive console
interactive_variables[interactive_name] = device
globals()[interactive_name] = device # Add to globals so tab complete works
logger.notify("{} to {} {} as {}".format(verb, branding_long, serial_number, interactive_name))
# Subscribe to disappearance of the device
device.__channel__._channel_broken.subscribe(lambda: did_lose_device(interactive_name, logger, app_shutdown_token))
def did_lose_device(interactive_name, logger, app_shutdown_token):
"""
Handles the disappearance of a device by displaying
a message.
"""
if not app_shutdown_token.is_set():
logger.warn("Oh no {} disappeared".format(interactive_name))
def launch_shell(args,
interactive_variables,
print_banner, print_help,
logger, app_shutdown_token,
branding_short="dev", branding_long="device"):
"""
Launches an interactive python or IPython command line
interface.
As devices are connected they are made available as
"dev0", "dev1", ...
The names of the variables can be customized by setting branding_short.
"""
discovered_devices = []
globals().update(interactive_variables)
# Connect to device
logger.debug("Waiting for {}...".format(branding_long))
fibre.find_all(args.path, args.serial_number,
lambda dev: did_discover_device(dev, interactive_variables, discovered_devices, branding_short, branding_long, logger, app_shutdown_token),
app_shutdown_token,
app_shutdown_token,
logger=logger)
# Check if IPython is installed
if args.no_ipython:
use_ipython = False
else:
try:
import IPython
use_ipython = True
except:
print("Warning: you don't have IPython installed.")
print("If you want to have an improved interactive console with pretty colors,")
print("you should install IPython\n")
use_ipython = False
interactive_variables["help"] = lambda: print_help(args, len(discovered_devices) > 0)
# If IPython is installed, embed IPython shell, otherwise embed regular shell
if use_ipython:
help = lambda: print_help(args, len(discovered_devices) > 0) # Override help function # pylint: disable=W0612
locals()['__name__'] = globals()['__name__'] # to fix broken "%run -i script.py"
console = IPython.terminal.embed.InteractiveShellEmbed(banner1='')
console.runcode = console.run_code # hack to make IPython look like the regular console
interact = console
else:
# Enable tab complete if possible
try:
import readline # Works only on Unix
readline.parse_and_bind("tab: complete")
except:
sudo_prefix = "" if platform.system() == "Windows" else "sudo "
print("Warning: could not enable tab-complete. User experience will suffer.\n"
"Run `{}pip install readline` and then restart this script to fix this."
.format(sudo_prefix))
import code
console = code.InteractiveConsole(locals=interactive_variables)
interact = lambda: console.interact(banner='')
# install hook to hide ChannelBrokenException
console.runcode('import sys')
console.runcode('superexcepthook = sys.excepthook')
console.runcode('def newexcepthook(ex_class,ex,trace):\n'
' if ex_class.__module__ + "." + ex_class.__name__ != "fibre.ChannelBrokenException":\n'
' superexcepthook(ex_class,ex,trace)')
console.runcode('sys.excepthook=newexcepthook')
# Launch shell
print_banner()
logger._skip_bottom_line = True
interact()
app_shutdown_token.set()

View File

@ -0,0 +1,85 @@
import sys
import socket
import time
import traceback
import fibre.protocol
from fibre.utils import wait_any, TimeoutError
def noprint(x):
pass
class TCPTransport(fibre.protocol.StreamSource, fibre.protocol.StreamSink):
def __init__(self, dest_addr, dest_port, logger):
# TODO: FIXME: use IPv6
# Problem: getaddrinfo fails if the resolver returns an
# IPv4 address, but we are using AF_INET6
#family = socket.AF_INET6 if socket.has_ipv6 else socket.AF_INET
family = socket.AF_INET
self.sock = socket.socket(family, socket.SOCK_STREAM)
# TODO: Determine the right address to use from the list
self.target = socket.getaddrinfo(dest_addr, dest_port, family)[0][4]
# TODO: this blocks until a connection is established, or the system cancels it
self.sock.connect(self.target)
def process_bytes(self, buffer):
self.sock.send(buffer)
def get_bytes(self, n_bytes, deadline):
"""
Returns n bytes unless the deadline is reached, in which case the bytes
that were read up to that point are returned. If deadline is None the
function blocks forever. A deadline before the current time corresponds
to non-blocking mode.
"""
# convert deadline to seconds (floating point)
deadline = None if deadline is None else max(deadline - time.monotonic(), 0)
self.sock.settimeout(deadline)
try:
data = self.sock.recv(n_bytes) # receive n_bytes
return data
except socket.timeout:
# if we got a timeout data will still be none, so we call recv again
# this time in non blocking state and see if we can get some data
try:
return self.sock.recv(n_bytes)
except socket.timeout:
raise TimeoutError
def get_bytes_or_fail(self, n_bytes, deadline):
result = self.get_bytes(n_bytes, deadline)
if len(result) < n_bytes:
raise TimeoutError("expected {} bytes but got only {}".format(n_bytes, len(result)))
return result
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Tries to connect to a TCP server based on the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
try:
dest_addr = ':'.join(path.split(":")[:-1])
dest_port = int(path.split(":")[-1])
except (ValueError, IndexError):
raise Exception('"{}" is not a valid TCP destination. The format should be something like "localhost:1234".'
.format(path))
while not cancellation_token.is_set():
try:
tcp_transport = fibre.tcp_transport.TCPTransport(dest_addr, dest_port, logger)
stream2packet_input = fibre.protocol.PacketFromStreamConverter(tcp_transport)
packet2stream_output = fibre.protocol.StreamBasedPacketSink(tcp_transport)
channel = fibre.protocol.Channel(
"TCP device {}:{}".format(dest_addr, dest_port),
stream2packet_input, packet2stream_output,
channel_termination_token, logger)
except:
#logger.debug("TCP channel init failed. More info: " + traceback.format_exc())
pass
else:
callback(channel)
wait_any(None, cancellation_token, channel._channel_broken)
time.sleep(1)

View File

@ -0,0 +1,67 @@
import sys
import socket
import time
import traceback
import fibre.protocol
from fibre.utils import wait_any
def noprint(x):
pass
class UDPTransport(fibre.protocol.PacketSource, fibre.protocol.PacketSink):
def __init__(self, dest_addr, dest_port, logger):
# TODO: FIXME: use IPv6
# Problem: getaddrinfo fails if the resolver returns an
# IPv4 address, but we are using AF_INET6
#family = socket.AF_INET6 if socket.has_ipv6 else socket.AF_INET
family = socket.AF_INET
self.sock = socket.socket(family, socket.SOCK_DGRAM)
# TODO: Determine the right address to use from the list
self.target = socket.getaddrinfo(dest_addr,dest_port, family)[0][4]
def process_packet(self, buffer):
self.sock.sendto(buffer, self.target)
def get_packet(self, deadline):
# TODO: implement deadline
deadline = None if deadline is None else max(deadline - time.monotonic(), 0)
self.sock.settimeout(deadline)
try:
data, _ = self.sock.recvfrom(1024)
return data
except socket.timeout:
# if we got a timeout data will still be none, so we call recv again
# this time in non blocking state and see if we can get some data
try:
return self.sock.recvfrom(1024)
except socket.timeout:
raise TimeoutError
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Tries to connect to a UDP server based on the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
try:
dest_addr = ':'.join(path.split(":")[:-1])
dest_port = int(path.split(":")[-1])
except (ValueError, IndexError):
raise Exception('"{}" is not a valid UDP destination. The format should be something like "localhost:1234".'
.format(path))
while not cancellation_token.is_set():
try:
udp_transport = fibre.udp_transport.UDPTransport(dest_addr, dest_port, logger)
channel = fibre.protocol.Channel(
"UDP device {}:{}".format(dest_addr, dest_port),
udp_transport, udp_transport,
channel_termination_token, logger)
except:
logger.debug("UDP channel init failed. More info: " + traceback.format_exc())
pass
else:
callback(channel)
wait_any(None, cancellation_token, channel._channel_broken)
time.sleep(1)

View File

@ -0,0 +1,215 @@
# requires pyusb
# pip install --pre pyusb
import usb.core
import usb.util
import sys
import time
import fibre.protocol
import traceback
import platform
from fibre.utils import TimeoutError
# Currently we identify fibre-enabled devices by VID,PID
# TODO: identify by USB descriptors
WELL_KNOWN_VID_PID_PAIRS = [
(0x1209, 0x0D31),
(0x1209, 0x0D32),
(0x1209, 0x0D33)
]
class USBBulkTransport(fibre.protocol.PacketSource, fibre.protocol.PacketSink):
def __init__(self, dev, logger):
self._logger = logger
self.dev = dev
self.intf = None
self._name = "USB device {}:{}".format(dev.idVendor, dev.idProduct)
self._was_damaged = False
##
# information about the connected device
##
def info(self):
# loop through configurations
string = ""
for cfg in self.dev:
string += "ConfigurationValue {0}\n".format(cfg.bConfigurationValue)
for intf in cfg:
string += "\tInterfaceNumber {0},{1}\n".format(intf.bInterfaceNumber, intf.bAlternateSetting)
for ep in intf:
string += "\t\tEndpointAddress {0}\n".format(ep.bEndpointAddress)
return string
def init(self):
# Under some conditions, the Linux USB/libusb stack ends up in a corrupt
# state where there are a few packets in a receive queue but a call
# to epr.read() does not return these packet until a new packet arrives.
# This undesirable queue can be cleared by resetting the device.
# On windows this would cause file-not-found errors in subsequent dev calls
if platform.system() != 'Windows':
self.dev.reset()
#self.dev.set_configuration() # no args: set first configuration
# Find the best interface
self.cfg = self.dev.get_active_configuration()
custom_interfaces = [i for i in self.cfg.interfaces() if i.bInterfaceClass == 0x00 and i.bInterfaceSubClass == 0x01]
cdc_interfaces = [i for i in self.cfg.interfaces() if i.bInterfaceClass == 0x0a and i.bInterfaceSubClass == 0x00]
all_compatible_interfaces = custom_interfaces + cdc_interfaces
if len(all_compatible_interfaces) == 0:
raise Exception("the device has no compatible interfaces")
self.intf = all_compatible_interfaces[0]
# Try to detach kernel driver from interface
try:
if self.dev.is_kernel_driver_active(self.intf.bInterfaceNumber):
self.dev.detach_kernel_driver(self.intf.bInterfaceNumber)
self._logger.debug("Detached Kernel Driver")
else:
self._logger.debug("Kernel Driver was not attached")
except NotImplementedError:
pass #is_kernel_driver_active not implemented on Windows
# find write endpoint (first OUT endpoint)
self.epw = usb.util.find_descriptor(self.intf,
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_OUT
)
assert self.epw is not None
self._logger.debug("EndpointAddress for writing {}".format(self.epw.bEndpointAddress))
# find read endpoint (first IN endpoint)
self.epr = usb.util.find_descriptor(self.intf,
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_IN
)
assert self.epr is not None
self._logger.debug("EndpointAddress for reading {}".format(self.epr.bEndpointAddress))
def deinit(self):
if not self.intf is None:
usb.util.release_interface(self.dev, self.intf)
def process_packet(self, usbBuffer):
try:
ret = self.epw.write(usbBuffer, 0)
if self._was_damaged:
self._logger.debug("Recovered from USB halt/stall condition")
self._was_damaged = False
return ret
except usb.core.USBError as ex:
if ex.errno == 19 or ex.errno == 32: # "no such device", "pipe error"
raise fibre.protocol.ChannelBrokenException()
elif ex.errno is None or ex.errno == 60 or ex.errno == 110: # timeout
raise TimeoutError()
else:
self._logger.debug("error in usbbulk_transport.py, process_packet")
self._logger.debug(traceback.format_exc())
self._logger.debug("halt condition: {}".format(ex.errno))
self._logger.debug(str(ex))
# Try resetting halt/stall condition
try:
self.deinit()
self.init()
except usb.core.USBError:
raise fibre.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise fibre.protocol.ChannelDamagedException()
def get_packet(self, deadline):
try:
bufferLen = self.epr.wMaxPacketSize
timeout = max(int((deadline - time.monotonic()) * 1000), 0)
ret = self.epr.read(bufferLen, timeout)
if self._was_damaged:
self._logger.debug("Recovered from USB halt/stall condition")
self._was_damaged = False
return bytearray(ret)
except usb.core.USBError as ex:
if ex.errno == 19 or ex.errno == 32: # "no such device", "pipe error"
raise fibre.protocol.ChannelBrokenException()
elif ex.errno is None or ex.errno == 60 or ex.errno == 110: # timeout
raise TimeoutError()
else:
self._logger.debug("error in usbbulk_transport.py, process_packet")
self._logger.debug(traceback.format_exc())
self._logger.debug("halt condition: {}".format(ex.errno))
self._logger.debug(str(ex))
# Try resetting halt/stall condition
try:
self.deinit()
self.init()
except usb.core.USBError:
raise fibre.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise fibre.protocol.ChannelDamagedException()
def discover_channels(path, serial_number, callback, cancellation_token, channel_termination_token, logger):
"""
Scans for USB devices that match the path spec.
This function blocks until cancellation_token is set.
Channels spawned by this function run until channel_termination_token is set.
"""
if path == None or path == "":
bus = None
address = None
else:
try:
bus = int(path.split(":")[0])
address = int(path.split(":")[1])
except (ValueError, IndexError):
raise Exception("{} is not a valid USB path specification. "
"Expected a string of the format BUS:DEVICE where BUS "
"and DEVICE are integers.".format(path))
known_devices = []
def device_matcher(device):
#print(" test {:04X}:{:04X}".format(device.idVendor, device.idProduct))
try:
if (device.bus, device.address) in known_devices:
return False
if bus != None and device.bus != bus:
return False
if address != None and device.address != address:
return False
if serial_number != None and device.serial_number != serial_number:
return False
if (device.idVendor, device.idProduct) not in WELL_KNOWN_VID_PID_PAIRS:
return False
except:
return False
return True
while not cancellation_token.is_set():
logger.debug("USB discover loop")
devices = usb.core.find(find_all=True, custom_match=device_matcher)
for usb_device in devices:
try:
bulk_device = USBBulkTransport(usb_device, logger)
logger.debug(bulk_device.info())
bulk_device.init()
channel = fibre.protocol.Channel(
"USB device bus {} device {}".format(usb_device.bus, usb_device.address),
bulk_device, bulk_device, channel_termination_token, logger)
channel.usb_device = usb_device # for debugging only
except usb.core.USBError as ex:
if ex.errno == 13:
logger.debug("USB device access denied. Did you set up your udev rules correctly?")
continue
elif ex.errno == 16:
logger.debug("USB device busy. I'll reset it and try again.")
usb_device.reset()
continue
else:
logger.debug("USB device init failed. Ignoring this device. More info: " + traceback.format_exc())
known_devices.append((usb_device.bus, usb_device.address))
else:
known_devices.append((usb_device.bus, usb_device.address))
callback(channel)
time.sleep(1)

View File

@ -0,0 +1,237 @@
import sys
import time
import threading
import platform
import subprocess
import os
try:
if platform.system() == 'Windows':
import win32console
# TODO: we should win32console anyway so we could just omit colorama
import colorama
colorama.init()
except ImportError:
print("Could not init terminal features.")
sys.stdout.flush()
pass
if sys.version_info < (3, 3):
class TimeoutError(Exception):
pass
else:
TimeoutError = TimeoutError
def get_serial_number_str(device):
if hasattr(device, 'serial_number'):
return format(device.serial_number, 'x').upper()
else:
return "[unknown serial number]"
## Threading utils ##
class Event():
"""
Alternative to threading.Event(), enhanced by the subscribe() function
that the original fails to provide.
@param Trigger: if supplied, the newly created event will be triggered
as soon as the trigger event becomes set
"""
def __init__(self, trigger=None):
self._evt = threading.Event()
self._subscribers = []
self._mutex = threading.Lock()
if not trigger is None:
trigger.subscribe(lambda: self.set())
def is_set(self):
return self._evt.is_set()
def set(self):
"""
Sets the event and invokes all subscribers if the event was
not already set
"""
self._mutex.acquire()
try:
if not self._evt.is_set():
self._evt.set()
for s in self._subscribers:
s()
finally:
self._mutex.release()
def subscribe(self, handler):
"""
Invokes the specified handler exactly once as soon as the
specified event is set. If the event is already set, the
handler is invoked immediately.
Returns a function that can be invoked to unsubscribe.
"""
if handler is None:
raise TypeError
self._mutex.acquire()
try:
self._subscribers.append(handler)
if self._evt.is_set():
handler()
finally:
self._mutex.release()
return handler
def unsubscribe(self, handler):
self._mutex.acquire()
try:
self._subscribers.pop(self._subscribers.index(handler))
finally:
self._mutex.release()
def wait(self, timeout=None):
if not self._evt.wait(timeout=timeout):
raise TimeoutError()
def trigger_after(self, timeout):
"""
Triggers the event after the specified timeout.
This function returns immediately.
"""
def delayed_trigger():
if not self.wait(timeout=timeout):
self.set()
threading.Thread(target=delayed_trigger)
t.daemon = True
t.start()
def wait_any(timeout=None, *events):
"""
Blocks until any of the specified events are triggered.
Returns the index of the event that was triggerd or raises
a TimeoutError
Param timeout: A timeout in seconds
"""
or_event = threading.Event()
subscriptions = []
for event in events:
subscriptions.append((event, event.subscribe(lambda: or_event.set())))
or_event.wait(timeout=timeout)
for event, sub in subscriptions:
event.unsubscribe(sub)
for i in range(len(events)):
if events[i].is_set():
return i
raise TimeoutError()
## Log utils ##
class Logger():
"""
Logs messages to stdout
"""
COLOR_DEFAULT = 0
COLOR_GREEN = 1
COLOR_CYAN = 2
COLOR_YELLOW = 3
COLOR_RED = 4
_VT100Colors = {
COLOR_GREEN: '\x1b[92;1m',
COLOR_CYAN: '\x1b[96;1m',
COLOR_YELLOW: '\x1b[93;1m',
COLOR_RED: '\x1b[91;1m',
COLOR_DEFAULT: '\x1b[0m'
}
_Win32Colors = {
COLOR_GREEN: 0x0A,
COLOR_CYAN: 0x0B,
COLOR_YELLOW: 0x0E,
COLOR_RED: 0x0C,
COLOR_DEFAULT: 0x07
}
def __init__(self, verbose=True):
self._prefix = ''
self._skip_bottom_line = False # If true, messages are printed one line above the cursor
self._verbose = verbose
self._print_lock = threading.Lock()
if platform.system() == 'Windows':
self._stdout_buf = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE)
def indent(self, prefix=' '):
indented_logger = Logger()
indented_logger._prefix = self._prefix + prefix
return indented_logger
def print_on_second_last_line(self, text, color):
"""
Prints a text on the second last line.
This can be used to print a message above the command
prompt. If the command prompt spans multiple lines
there will be glitches.
If the printed text spans multiple lines there will also
be glitches (though this could be fixed).
"""
if platform.system() == 'Windows':
# Windows <10 doesn't understand VT100 escape codes and the colorama
# also doesn't support the specific escape codes we need so we use the
# native Win32 API.
info = self._stdout_buf.GetConsoleScreenBufferInfo()
cursor_pos = info['CursorPosition']
scroll_rect=win32console.PySMALL_RECTType(
Left=0, Top=1,
Right=info['Window'].Right,
Bottom=cursor_pos.Y-1)
scroll_dest = win32console.PyCOORDType(scroll_rect.Left, scroll_rect.Top-1)
self._stdout_buf.ScrollConsoleScreenBuffer(
scroll_rect, scroll_rect, scroll_dest, # clipping rect is same as scroll rect
u' ', Logger._Win32Colors[color]) # fill with empty cells with the desired color attributes
line_start = win32console.PyCOORDType(0, cursor_pos.Y-1)
self._stdout_buf.WriteConsoleOutputCharacter(text, line_start)
else:
# Assume we're in a terminal that interprets VT100 escape codes.
# TODO: test on macOS
# Escape character sequence:
# ESC 7: store cursor position
# ESC 1A: move cursor up by one
# ESC 1S: scroll entire viewport by one
# ESC 1L: insert 1 line at cursor position
# (print text)
# ESC 8: restore old cursor position
self._print_lock.acquire()
sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L')
sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT])
sys.stdout.write('\x1b8')
sys.stdout.flush()
self._print_lock.release()
def print_colored(self, text, color):
if self._skip_bottom_line:
self.print_on_second_last_line(text, color)
else:
# On Windows, colorama does the job of interpreting the VT100 escape sequences
self._print_lock.acquire()
sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT] + '\n')
sys.stdout.flush()
self._print_lock.release()
def debug(self, text):
if self._verbose:
self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT)
def success(self, text):
self.print_colored(self._prefix + text, Logger.COLOR_GREEN)
def info(self, text):
self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT)
def notify(self, text):
self.print_colored(self._prefix + text, Logger.COLOR_CYAN)
def warn(self, text):
self.print_colored(self._prefix + text, Logger.COLOR_YELLOW)
def error(self, text):
# TODO: write to stderr
self.print_colored(self._prefix + text, Logger.COLOR_RED)

View File

@ -0,0 +1,21 @@
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.realpath(__file__)))),
"Firmware", "fibre", "python"))
# Syntactic sugar to make usage more intuative.
# Try/pass used to break install-time dep issues
try:
import fibre
find_any = fibre.find_any
find_all = fibre.find_all
except:
pass
# Standard convention is to add a __version__ attribute to the package
from .version import get_version_str
del get_version_str

View File

@ -0,0 +1,69 @@
import jinja2
import os
import json
def get_flat_endpoint_list(json, prefix, id_offset):
flat_list = []
for item in json:
item = item.copy()
if 'id' in item:
item['id'] -= id_offset
if 'type' in item:
if item['type'] in {'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'}:
item['type'] += '_t'
is_property = True
elif item['type'] in {'bool', 'float'}:
is_property = True
elif item['type'] in {'function'}:
if len(item.get('arguments', [])) == 0 and len(item.get('inputs', [])) == 0 and len(item.get('outputs', [])) == 0:
item['type'] = 'void'
is_property = True
else:
is_property = False
else:
is_property = False
if is_property:
item['name'] = prefix + item['name']
flat_list.append(item)
if 'members' in item:
flat_list = flat_list + get_flat_endpoint_list(item['members'], prefix + item['name'] + '.', id_offset)
return flat_list
def generate_code(odrv, template_file, output_file):
json_data = odrv._json_data
json_crc = odrv._json_crc
axis0_json = [item for item in json_data if item['name'].startswith("axis0")][0]
axis1_json = [item for item in json_data if item['name'].startswith("axis1")][0]
json_data = [item for item in json_data if not item['name'].startswith("axis")]
endpoints = get_flat_endpoint_list(json_data, '', 0)
per_axis_offset = axis1_json['members'][0]['id'] - axis0_json['members'][0]['id']
axis_endpoints = get_flat_endpoint_list(axis0_json['members'], 'axis.', 0)
axis_endpoints_copy = get_flat_endpoint_list(axis1_json['members'], 'axis.', per_axis_offset)
if axis_endpoints != axis_endpoints_copy:
raise Exception("axis0 and axis1 don't look exactly equal")
env = jinja2.Environment(
#loader = jinja2.FileSystemLoader("/Data/Projects/")
#trim_blocks=True,
#lstrip_blocks=True
)
# Expose helper functions to jinja template code
#env.filters["delimit"] = camel_case_to_words
#import ipdb; ipdb.set_trace()
# Load and render template
template = env.from_string(template_file.read())
output = template.render(
json_crc=json_crc,
endpoints=endpoints,
per_axis_offset=per_axis_offset,
axis_endpoints=axis_endpoints,
output_name=os.path.basename(output_file.name)
)
# Output
output_file.write(output)

View File

@ -0,0 +1,82 @@
import json
import os
import tempfile
import fibre.remote_object
from ref_tool.utils import OperationAbortedException, yes_no_prompt
def get_dict(obj, is_config_object):
result = {}
for (k,v) in obj._remote_attributes.items():
if isinstance(v, fibre.remote_object.RemoteProperty) and is_config_object:
result[k] = v.get_value()
elif isinstance(v, fibre.remote_object.RemoteObject):
sub_dict = get_dict(v, k == 'config')
if sub_dict != {}:
result[k] = sub_dict
return result
def set_dict(obj, path, config_dict):
errors = []
for (k,v) in config_dict.items():
name = path + ("." if path != "" else "") + k
if not k in obj._remote_attributes:
errors.append("Could not restore {}: property not found on device".format(name))
continue
remote_attribute = obj._remote_attributes[k]
if isinstance(remote_attribute, fibre.remote_object.RemoteObject):
errors += set_dict(remote_attribute, name, v)
else:
try:
remote_attribute.set_value(v)
except Exception as ex:
errors.append("Could not restore {}: {}".format(name, str(ex)))
return errors
def get_temp_config_filename(device):
serial_number = fibre.utils.get_serial_number_str(device)
safe_serial_number = ''.join(filter(str.isalnum, serial_number))
return os.path.join(tempfile.gettempdir(), 'ref_tool-config-{}.json'.format(safe_serial_number))
def backup_config(device, filename, logger):
"""
Exports the configuration of an ODrive to a JSON file.
If no file name is provided, the file is placed into a
temporary directory.
"""
if filename is None:
filename = get_temp_config_filename(device)
logger.info("Saving configuration to {}...".format(filename))
if os.path.exists(filename):
if not yes_no_prompt("The file {} already exists. Do you want to override it?".format(filename), True):
raise OperationAbortedException()
data = get_dict(device, False)
with open(filename, 'w') as file:
json.dump(data, file)
logger.info("Configuration saved.")
def restore_config(device, filename, logger):
"""
Restores the configuration stored in a file
"""
if filename is None:
filename = get_temp_config_filename(device)
with open(filename) as file:
data = json.load(file)
logger.info("Restoring configuration from {}...".format(filename))
errors = set_dict(device, "", data)
for error in errors:
logger.info(error)
if errors:
logger.warn("Some of the configuration could not be restored.")
device.save_configuration()
logger.info("Configuration restored.")

View File

@ -0,0 +1,80 @@
import fibre
import ref_tool
from ref_tool.utils import start_liveplotter
def print_banner():
print('Please connect your Dummy-Robot.')
print('You can also type help() or quit().')
def print_help(args, have_devices):
print('')
if have_devices:
print('Connect your REF-Unit to {} and power it up.'.format(args.path))
print('After that, the following message should appear:')
print(' "Connected to REF-Unit [serial number] as odrv0"')
print('')
print('Once the REF-Unit is connected, type "ref0." and press <tab>')
else:
print('Type "ref0." and press <tab>')
print('This will present you with all the properties that you can reference')
print('')
print('For example: "odrv0.motor0.encoder.pos_estimate"')
print('will print the current encoder position on motor 0')
print('and "odrv0.motor0.pos_setpoint = 10000"')
print('will send motor0 to 10000')
print('')
interactive_variables = {}
discovered_devices = []
def did_discover_device(odrive, logger, app_shutdown_token):
"""
Handles the discovery of new devices by displaying a
message and making the device available to the interactive
console
"""
serial_number = odrive.serial_number if hasattr(odrive, 'serial_number') else "[unknown serial number]"
if serial_number in discovered_devices:
verb = "Reconnected"
index = discovered_devices.index(serial_number)
else:
verb = "Connected"
discovered_devices.append(serial_number)
index = len(discovered_devices) - 1
interactive_name = "odrv" + str(index)
# Publish new ODrive to interactive console
interactive_variables[interactive_name] = odrive
globals()[interactive_name] = odrive # Add to globals so tab complete works
logger.notify("{} to ODrive {:012X} as {}".format(verb, serial_number, interactive_name))
# Subscribe to disappearance of the device
odrive.__channel__._channel_broken.subscribe(lambda: did_lose_device(interactive_name, logger, app_shutdown_token))
def did_lose_device(interactive_name, logger, app_shutdown_token):
"""
Handles the disappearance of a device by displaying
a message.
"""
if not app_shutdown_token.is_set():
logger.warn("Oh no {} disappeared".format(interactive_name))
def launch_shell(args, logger, app_shutdown_token):
"""
Launches an interactive python or IPython command line
interface.
As ODrives are connected they are made available as
"odrv0", "odrv1", ...
"""
interactive_variables = {
'start_liveplotter': start_liveplotter,
}
fibre.launch_shell(args,
interactive_variables,
print_banner, print_help,
logger, app_shutdown_token,
branding_short="dummy", branding_long="Dummy-Robot")

View File

@ -0,0 +1,181 @@
from __future__ import print_function
import platform
import sys
import threading
import time
from fibre.utils import Event
try:
if platform.system() == 'Windows':
import win32console
import colorama
colorama.init()
except ImportError:
print("Could not init terminal features.")
print("Refer to install instructions at http://docs.odriverobotics.com/#downloading-and-installing-tools")
sys.stdout.flush()
pass
_VT100Colors = {
'green': '\x1b[92;1m',
'cyan': '\x1b[96;1m',
'yellow': '\x1b[93;1m',
'red': '\x1b[91;1m',
'default': '\x1b[0m'
}
class OperationAbortedException(Exception):
pass
data_rate = 10
plot_rate = 10
num_samples = 1000
def start_liveplotter(get_var_callback):
"""
Starts a liveplotter.
The variable that is plotted is retrieved from get_var_callback.
This function returns immediately and the liveplotter quits when
the user closes it.
"""
import matplotlib.pyplot as plt
cancellation_token = Event()
global vals
vals = []
def fetch_data():
global vals
while not cancellation_token.is_set():
try:
data = get_var_callback()
except Exception as ex:
print(str(ex))
time.sleep(1)
continue
vals.append(data)
if len(vals) > num_samples:
vals = vals[-num_samples:]
time.sleep(1/data_rate)
# TODO: use animation for better UI performance, see:
# https://matplotlib.org/examples/animation/simple_anim.html
def plot_data():
global vals
plt.ion()
# Make sure the script terminates when the user closes the plotter
def did_close(evt):
cancellation_token.set()
fig = plt.figure()
fig.canvas.mpl_connect('close_event', did_close)
while not cancellation_token.is_set():
plt.clf()
plt.plot(vals)
plt.legend(list(range(len(vals))))
fig.canvas.draw()
fig.canvas.start_event_loop(1/plot_rate)
fetch_t = threading.Thread(target=fetch_data)
fetch_t.daemon = True
fetch_t.start()
plot_t = threading.Thread(target=plot_data)
plot_t.daemon = True
plot_t.start()
return cancellation_token;
#plot_data()
def print_drv_regs(name, motor):
"""
Dumps the current gate driver regisers for the specified motor
"""
fault = motor.gate_driver.drv_fault
status_reg_1 = motor.gate_driver.status_reg_1
status_reg_2 = motor.gate_driver.status_reg_2
ctrl_reg_1 = motor.gate_driver.ctrl_reg_1
ctrl_reg_2 = motor.gate_driver.ctrl_reg_2
print(name + ": " + str(fault))
print("DRV Fault Code: " + str(fault))
print("Status Reg 1: " + str(status_reg_1) + " (" + format(status_reg_1, '#010b') + ")")
print("Status Reg 2: " + str(status_reg_2) + " (" + format(status_reg_2, '#010b') + ")")
print("Control Reg 1: " + str(ctrl_reg_1) + " (" + format(ctrl_reg_1, '#013b') + ")")
print("Control Reg 2: " + str(ctrl_reg_2) + " (" + format(ctrl_reg_2, '#09b') + ")")
def show_oscilloscope(odrv):
size = 18000
values = []
for i in range(size):
values.append(odrv.get_oscilloscope_val(i))
import matplotlib.pyplot as plt
plt.plot(values)
plt.show()
def rate_test(device):
"""
Tests how many integers per second can be transmitted
"""
# import matplotlib.pyplot as plt
# plt.ion()
print("reading 10000 values...")
numFrames = 10000
vals = []
for _ in range(numFrames):
vals.append(device.axis0.loop_counter)
loopsPerFrame = (vals[-1] - vals[0])/numFrames
loopsPerSec = (168000000/(2*10192))
FramePerSec = loopsPerSec/loopsPerFrame
print("Frames per second: " + str(FramePerSec))
# plt.plot(vals)
# plt.show(block=True)
def usb_burn_in_test(get_var_callback, cancellation_token):
"""
Starts background threads that read a values form the USB device in a spin-loop
"""
def fetch_data():
global vals
i = 0
while not cancellation_token.is_set():
try:
get_var_callback()
i += 1
except Exception as ex:
print(str(ex))
time.sleep(1)
i = 0
continue
if i % 1000 == 0:
print("read {} values".format(i))
threading.Thread(target=fetch_data, daemon=True).start()
def yes_no_prompt(question, default=None):
if default is None:
question += " [y/n] "
elif default == True:
question += " [Y/n] "
elif default == False:
question += " [y/N] "
while True:
print(question, end='')
choice = input().lower()
if choice in {'yes', 'y'}:
return True
elif choice in {'no', 'n'}:
return False
elif choice == '' and default is not None:
return default

View File

@ -0,0 +1,94 @@
import os
import platform
import re
import subprocess
import sys
def version_str_to_tuple(version_string):
"""
Converts a version string to a tuple of the form
(major, minor, revision, prerelease)
Example: "fw-v0.3.6-23" => (0, 3, 6, True)
"""
regex=r'.*v([0-9a-zA-Z]+).([0-9a-zA-Z]+).([0-9a-zA-Z]+)(.*)'
return (int(re.sub(regex, r"\1", version_string)),
int(re.sub(regex, r"\2", version_string)),
int(re.sub(regex, r"\3", version_string)),
(re.sub(regex, r"\4", version_string) != ""))
def get_version_from_git():
script_dir = os.path.dirname(os.path.realpath(__file__))
try:
# Determine the current git commit version
git_tag = subprocess.check_output(["git", "describe", "--always", "--tags", "--dirty=*"],
cwd=script_dir)
git_tag = git_tag.decode(sys.stdout.encoding).rstrip('\n')
(major, minor, revision, is_prerelease) = version_str_to_tuple(git_tag)
# if is_prerelease:
# revision += 1
return git_tag, major, minor, revision, is_prerelease
except Exception as ex:
print(ex)
return "[unknown version]", 0, 0, 0, 1
def get_version_str(git_only=False, is_post_release=False, bump_rev=False, release_override=False):
"""
Returns the versions of the tools
If git_only is true, the version.txt file is ignored even
if it is present.
"""
script_dir = os.path.dirname(os.path.realpath(__file__))
# Try to read the version.txt file that is generated during
# the packaging step
version_file_path = os.path.join(script_dir, 'version.txt')
if os.path.exists(version_file_path) and git_only == False:
with open(version_file_path) as version_file:
return version_file.readline().rstrip('\n')
_, major, minor, revision, unreleased = get_version_from_git()
if bump_rev:
revision += 1
version = '{}.{}.{}'.format(major, minor, revision)
if is_post_release:
version += ".post"
elif not release_override and unreleased:
version += ".dev"
return version
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Version Dump\n')
parser.add_argument("--output", type=argparse.FileType('w'), default='-',
help="C header output file")
args = parser.parse_args()
git_name, major, minor, revision, unreleased = get_version_from_git()
print('Firmware version {}.{}.{}{} ({})'.format(
major, minor, revision, '-dev' if unreleased else '',
git_name))
args.output.write('#define FW_VERSION "{}"\n'.format(git_name))
args.output.write('#define FW_VERSION_MAJOR {}\n'.format(major))
args.output.write('#define FW_VERSION_MINOR {}\n'.format(minor))
args.output.write('#define FW_VERSION_REVISION {}\n'.format(revision))
args.output.write('#define FW_VERSION_UNRELEASED {}\n'.format(1 if unreleased else 0))
def setup_udev_rules(logger):
if platform.system() != 'Linux':
logger.error("This command only makes sense on Linux")
return
if os.getuid() != 0:
logger.warn("you should run this as root, otherwise it will probably not work")
with open('/etc/udev/rules.d/91-ref_tool.rules', 'w') as file:
file.write('SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d3[0-9]", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1"\n')
subprocess.check_call(["udevadm", "control", "--reload-rules"])
subprocess.check_call(["udevadm", "trigger"])
logger.info('udev rules configured successfully')

View File

@ -0,0 +1,9 @@
@echo off
if not defined TAG (
set TAG=1
start wt -p "cmd" %0
exit
)
ipython "%~dp0\run_shell.py" -- %*

View File

@ -0,0 +1,163 @@
#!/usr/bin/env python3
from __future__ import print_function
import argparse
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
os.path.realpath(__file__))),
"Firmware", "fibre", "python"))
from fibre import Logger, Event
import ref_tool
from ref_tool.configuration import *
old_print = print
def print(*args, **kwargs):
kwargs.pop('flush', False)
old_print(*args, **kwargs)
file = kwargs.get('file', sys.stdout)
file.flush() if file is not None else sys.stdout.flush()
script_path = os.path.dirname(os.path.realpath(__file__))
## Parse arguments ##
parser = argparse.ArgumentParser(description='Robot-Embedded-Framework command line utility\n',
formatter_class=argparse.RawTextHelpFormatter)
subparsers = parser.add_subparsers(help='sub-command help', dest='command')
shell_parser = subparsers.add_parser('shell',
help='Drop into an interactive python shell that lets you interact with the ODrive(s)')
shell_parser.add_argument("--no-ipython", action="store_true",
help="Use the regular Python shell "
"instead of the IPython shell, "
"even if IPython is installed.")
subparsers.add_parser('liveplotter', help="For plotting of REF parameters (i.e. position) in real time")
# General arguments
parser.add_argument("-p", "--path", metavar="PATH", action="store",
help="The path(s) where REF-board(s) should be discovered.\n"
"By default the script will connect to any REF-board on USB.\n\n"
"To select a specific USB device:\n"
" --path usb:BUS:DEVICE\n"
"usbwhere BUS and DEVICE are the bus and device numbers as shown in `lsusb`.\n\n"
"To select a specific serial port:\n"
" --path serial:PATH\n"
"where PATH is the path of the serial port. For example \"/dev/ttyUSB0\".\n"
"You can use `ls /dev/tty*` to find the correct port.\n\n"
"You can combine USB and serial specs by separating them with a comma (no space!)\n"
"Example:\n"
" --path usb,serial:/dev/ttyUSB0\n"
"means \"discover any USB device or a serial device on /dev/ttyUSB0\"")
parser.add_argument("-s", "--serial-number", action="store",
help="The 12-digit serial number of the device. "
"This is a string consisting of 12 upper case hexadecimal "
"digits as displayed in lsusb. \n"
" example: 385F324D3037\n"
"You can list all devices connected to USB by running\n"
"(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial\n"
"If omitted, any device is accepted.")
parser.add_argument("-v", "--verbose", action="store_true",
help="print debug information")
parser.add_argument("--version", action="store_true",
help="print version information and exit")
parser.set_defaults(path="usb")
args = parser.parse_args()
# Default command
if args.command is None:
args.command = 'shell'
args.no_ipython = False
logger = Logger(verbose=args.verbose)
app_shutdown_token = Event()
try:
if args.command == 'shell':
# if ".dev" in ref_tool.__version__:
# print("")
# logger.warn("Developer Preview")
# print("")
import ref_tool.shell
ref_tool.shell.launch_shell(args, logger, app_shutdown_token)
elif args.command == 'liveplotter':
from ref_tool.utils import start_liveplotter
print("Waiting for ODrive...")
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
# If you want to plot different values, change them here.
# You can plot any number of values concurrently.
cancellation_token = start_liveplotter(lambda: [
ref_unit.axis0.encoder.pos_estimate,
ref_unit.axis1.encoder.pos_estimate,
])
print("Showing plot. Press Ctrl+C to exit.")
while not cancellation_token.is_set():
time.sleep(1)
elif args.command == 'drv-status':
from ref_tool.utils import print_drv_regs
print("Waiting for ODrive...")
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
print_drv_regs("Motor 0", ref_unit.axis0.motor)
print_drv_regs("Motor 1", ref_unit.axis1.motor)
elif args.command == 'rate-test':
from ref_tool.utils import rate_test
print("Waiting for ODrive...")
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
rate_test(ref_unit)
elif args.command == 'udev-setup':
from ref_tool.version import setup_udev_rules
setup_udev_rules(logger)
elif args.command == 'generate-code':
from ref_tool.code_generator import generate_code
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
channel_termination_token=app_shutdown_token)
generate_code(ref_unit, args.template, args.output)
elif args.command == 'backup-config':
from ref_tool.configuration import backup_config
print("Waiting for ODrive...")
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
backup_config(ref_unit, args.file, logger)
elif args.command == 'restore-config':
from ref_tool.configuration import restore_config
print("Waiting for ODrive...")
ref_unit = ref_tool.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
restore_config(ref_unit, args.file, logger)
else:
raise Exception("unknown command: " + args.command)
except OperationAbortedException:
logger.info("Operation aborted.")
finally:
app_shutdown_token.set()