Merge branch 'sam_python_fixes' into sam_testing

This commit is contained in:
Samuel Sadok
2018-03-17 22:42:28 -07:00
16 changed files with 908 additions and 609 deletions
+7
View File
@@ -15,6 +15,13 @@ Please add a note of your changes below this heading if you make a Pull Request.
* Update CubeMX generated STM platform code to version 1.19.0
* Remove `UUID_0`, `UUID_1` and `UUID_2` from USB protocol. Use `serial_number` instead.
* Freertos memory pool (task stacks, etc) now uses Core Coupled Memory.
* Refactor python tools
* `explore_odrive.py` now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...)
* No need to restart `explore_odrive.py` when devices get disconnected and reconnected
* The command line arguments of `explore_odrive.py` have changed. See `explore_odrive.py --help` for more details.
* ODrive accesses from within python tools are now thread-safe
* Liveplotter does no longer steal focus and closes as expected
* (experimental: start liveplotter from `explore_odrive.py` by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`)
### Fixed
* malloc now fails if we run out of memory (before it would always succeed even if we are out of ram...)
+1 -1
View File
@@ -165,7 +165,7 @@ pip install pyusb pyserial
6. Open the bash prompt in the `ODrive/tools/` folder.
7. Run `python3 demo.py` or `python3 explore_odrive.py`.
- `demo.py` is a very simple script which will make motor 0 turn back and forth. Use this as an example if you want to control the ODrive yourself programatically.
- `explore_odrive.py` drops you into an interactive python shell where you can explore and edit the parameters that are available on your device. For instance `my_odrive.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/explore_odrive.py --discover serial`.
- `explore_odrive.py` drops you into an interactive python shell where you can explore and edit the parameters that are available on your device. For instance `my_odrive.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/explore_odrive.py --path serial`.
### From Arduino
[See ODrive Arduino Library](https://github.com/madcowswe/ODriveArduino)
+5 -2
View File
@@ -5,12 +5,15 @@ Example usage of the ODrive python library to monitor and control ODrive devices
from __future__ import print_function
import odrive.core
import odrive.discovery
import time
import math
# Find a connected ODrive (this will block until you connect one)
my_drive = odrive.core.find_any(consider_usb=True, consider_serial=False, printer=print)
my_drive = odrive.discovery.find_any()
# Find an ODrive that is connected on the serial port /dev/ttyUSB0
#my_drive = odrive.discovery.find_any("serial:/dev/ttyUSB0")
# The above call returns a python object with a dynamically generated type. The
# type hierarchy will correspond to the endpoint list in `MotorControl/protocol.cpp`.
+21 -55
View File
@@ -12,7 +12,7 @@ import struct
import dfuse
import usb.core
import usb.util
import odrive.core
import odrive.discovery
# We are interactively printing status messages, so flush by default
import functools
@@ -142,18 +142,6 @@ def jump_to_application(dfudev, address):
if status[1] != dfuse.DfuState.DFU_MANIFEST:
raise RuntimeError("An error occured. Device Status: {}".format(status[1]))
def str_to_uuid(uuid):
uuid = bytearray.fromhex(uuid.replace('-', ''))
return struct.unpack('>I', uuid[0:4]), struct.unpack('>I', uuid[4:8]), struct.unpack('>I', uuid[8:12])
def uuid_to_str(uuid0, uuid1, uuid2):
return "{:08X}-{:08X}-{:08X}".format(struct.pack('>I', uuid0), struct.pack('>I', uuid1), struct.pack('>I', uuid2))
def uuid_to_serial(uuid0, uuid1, uuid2):
return (struct.pack('>I', uuid0 + uuid2) + struct.pack('>I', uuid1)[0:2]).hex().upper()
### THREADS ###
def show_deferred_message(message, cancellation_token):
"""
@@ -170,43 +158,25 @@ def show_deferred_message(message, cancellation_token):
t.daemon = True
t.start()
def put_odrive_into_dfu_mode_thread(cancellation_token):
def put_odrive_into_dfu_mode(my_drive):
"""
Waits for an ODrive with a matching serial number and puts
it into DFU mode once it's found. The thread continues to put
matching devices into DFU mode until cancellation_token
is set.
Puts the specified device into DFU mode
"""
global app_cancellation_token
while not cancellation_token.is_set():
constraints = {} if serial_number == None else {'serial_number': serial_number}
my_drive = odrive.core.find_any(consider_usb=True, consider_serial=False,
cancellation_token=cancellation_token,
**constraints)
if cancellation_token.is_set():
return
if not hasattr(my_drive, "enter_dfu_mode"):
print("The firmware on device {} does not support DFU. You need to \n"
"flash the firmware once using STLink (`make flash`), after that \n"
"DFU with this script should work fine."
.format(my_drive.__channel__.usb_device.serial_number))
# Terminate script, otherwise it would try to reconnect to the same
# incompatible device
app_cancellation_token.set() # TODO: implement a more sensible discorvery mechanism to fix this
return
print("Putting device {} into DFU mode...".format(my_drive.__channel__.usb_device.serial_number))
try:
my_drive.enter_dfu_mode()
except usb.core.USBError as ex:
pass # this is expected because the device reboots
if platform.system() == "Windows":
show_deferred_message("Still waiting for the device to reappear.\n"
"Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.",
cancellation_token)
# If we immediately continue we might still pick up the device that was
# just rebooted. This isn't an issue but will display a distracting
# error message.
time.sleep(1)
if not hasattr(my_drive, "enter_dfu_mode"):
print("The firmware on device {} does not support DFU. You need to \n"
"flash the firmware once using STLink (`make flash`), after that \n"
"DFU with this script should work fine."
.format(my_drive.__channel__.usb_device.serial_number))
return
print("Putting device {} into DFU mode...".format(my_drive.__channel__.usb_device.serial_number))
try:
my_drive.enter_dfu_mode()
except odrive.protocol.ChannelBrokenException as ex:
pass # this is expected because the device reboots
if platform.system() == "Windows":
show_deferred_message("Still waiting for the device to reappear.\n"
"Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.",
find_odrive_cancellation_token)
### BEGINNING OF APPLICATION ###
@@ -230,12 +200,7 @@ hexfile = IntelHex(args.file)
#for start, end in hexfile.segments():
# print(" {:08X} to {:08X}".format(start, end - 1))
if args.uuid != None:
serial_number = uuid_to_serial(*str_to_uuid(args.uuid))
elif args.serial_number != None:
serial_number = args.serial_number
else:
serial_number = None
serial_number = args.serial_number
app_cancellation_token = threading.Event()
@@ -244,7 +209,8 @@ try:
print("Waiting for ODrive...")
# Scan for ODrives not in DFU mode and put them into DFU mode once they appear
threading.Thread(target=put_odrive_into_dfu_mode_thread, args=(find_odrive_cancellation_token,)).start()
# We only scan on USB because DFU is only possible over USB
odrive.discovery.find_all("usb", serial_number, put_odrive_into_dfu_mode, find_odrive_cancellation_token)
# Poll libUSB until a device in DFU mode is found
while not app_cancellation_token.is_set():
Regular → Executable
+4 -2
View File
@@ -5,12 +5,14 @@ Example usage of the ODrive python library to monitor and control ODrive devices
from __future__ import print_function
import odrive.core
import odrive.discovery
import time
import math
# Find a connected ODrive (this will block until you connect one)
my_drive = odrive.core.find_any(consider_usb=True, consider_serial=False, printer=print)
print("Waiting for ODrive...")
my_drive = odrive.discovery.find_any()
print("connected")
# Print DRV device regs for Motor 0
fault = my_drive.motor0.gate_driver.drv_fault
+174 -78
View File
@@ -3,37 +3,16 @@
Load an odrive object to play with in the IPython interactive shell.
"""
import odrive.core
import argparse
import sys
import platform
import threading
import odrive.discovery
from odrive.utils import start_liveplotter
# Check if IPython is installed
try:
import IPython
embed_ipython = True
except:
embed_ipython = False
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")
# Ensure interactive mode
if not bool(getattr(sys, 'ps1', sys.flags.interactive)):
print("You're not running in interactive mode. Run python -i explore_odrive.py")
print('')
sys.exit(1)
# Enable tab complete if possible
try:
import readline
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))
# Flush stdout by default
import functools
print = functools.partial(print, flush=True)
# some enums described in the README
@@ -48,25 +27,32 @@ CTRL_MODE_VELOCITY_CONTROL = 2,
CTRL_MODE_POSITION_CONTROL = 3
# Parse arguments
parser = argparse.ArgumentParser(description='Load an odrive object to play with in the IPython interactive shell.')
## Parse arguments ##
parser = argparse.ArgumentParser(description='Load an odrive object to play with in the IPython interactive shell.',
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("-v", "--verbose", action="store_true",
help="print debug information")
group = parser.add_mutually_exclusive_group()
group.add_argument("-d", "--discover", metavar="CHANNELS", action="store",
help="Automatically discover ODrives. Takes a comma-separated list (without spaces) "
"to indicate which connection types should be considered. Possible values are "
"usb and serial. For example \"--discover usb,serial\" indicates "
"that USB and serial ports should be scanned for ODrives. "
"If none of the below options are specified, --discover usb is assumed.")
group.add_argument("-u", "--usb", metavar="BUS:DEVICE", action="store",
help="Specifies the USB port on which the device is connected. "
"For example \"001:014\" means bus 001, device 014. The numbers can be obtained "
"using `lsusb`.")
group.add_argument("-s", "--serial", metavar="PORT", action="store",
help="Specifies the serial port on which the device is connected. "
"For example \"/dev/ttyUSB0\". Use `ls /dev/tty*` to find your port name.")
parser.set_defaults(discover="usb")
parser.add_argument("-p", "--path", metavar="PATH", action="store",
help="The path(s) where ODrive(s) should be discovered.\n"
"By default the script will connect to any ODrive 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("--no-ipython", action="store_true",
help="Use the regular Python shell\n"
"instead of the IPython shell,\n"
"even if IPython is installed\n")
parser.add_argument("-s", "--serial-number", action="store",
help="The serial number of the device. If omitted, any device is accepted.\n")
parser.set_defaults(path="usb")
args = parser.parse_args()
if (args.verbose):
@@ -74,41 +60,151 @@ if (args.verbose):
else:
printer = lambda x: None
## Interactive console utils ##
COLOR_RED = '\x1b[91;1m'
COLOR_CYAN = '\x1b[96;1m'
COLOR_RESET = '\x1b[0m'
def print_on_second_last_line(text, **kwargs):
"""
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.
"""
# 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
kwargs['end'] = ''
kwargs['flush'] = True
print('\x1b7\x1b[1A\x1b[1S\x1b[1L' + text + '\x1b8', **kwargs)
def print_banner():
print('ODrive control utility v0.4')
print('Please connect your ODrive.')
print('Type help() for help.')
def print_help():
print('')
if len(discovered_devices) == 0:
print('Connect your ODrive to {} and power it up.'.format(args.path))
print('After that, the following message should appear:')
print(' "Connected to ODrive [serial number] as odrv0"')
print('')
print('Once the ODrive is connected, type "odrv0." and press <tab>')
else:
print('Type "odrv0." and press <tab>')
print('This will present you with all the properties that you can reference')
print('')
print('For example: "odrv0.motor0.encoder.pll_pos"')
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 = {}
interactive_variables["help"] = print_help
## Device discovery ##
discovered_devices = []
def did_discover_device(odrive):
"""
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
print_on_second_last_line(COLOR_CYAN + "{} to ODrive {:012X} as {}".format(verb, serial_number, interactive_name) + COLOR_RESET)
# Subscribe to disappearance of the device
odrive.__channel__._channel_broken.subscribe(lambda: did_lose_device(interactive_name))
def did_lose_device(interactive_name):
"""
Handles the disappearance of a device by displaying
a message.
"""
if not app_shutdown_token.is_set():
print_on_second_last_line(COLOR_RED + "Oh no {} disappeared".format(interactive_name) + COLOR_RESET)
# Connect to device
if not args.usb is None:
try:
bus = int(args.usb.split(":")[0])
address = int(args.usb.split(":")[1])
except (ValueError, IndexError):
print("the --usb argument must look something like this: \"001:014\"")
sys.exit(1)
try:
my_odrive = odrive.core.open_usb(bus, address, printer=printer)
except odrive.protocol.DeviceInitException as ex:
print(str(ex))
sys.exit(1)
elif not args.serial is None:
my_odrive = odrive.core.open_serial(args.serial, printer=printer)
printer("Waiting for device...")
app_shutdown_token = threading.Event()
odrive.discovery.find_all(args.path, args.serial_number,
did_discover_device, app_shutdown_token,
printer=printer)
## Launch interactive shell ##
# Check if IPython is installed
if args.no_ipython:
use_ipython = False
else:
print("Waiting for device...")
consider_usb = 'usb' in args.discover.split(',')
consider_serial = 'serial' in args.discover.split(',')
my_odrive = odrive.core.find_any(consider_usb, consider_serial, printer=printer)
print("Connected!")
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
# If IPython is installed, embed IPython shell, otherwise embed regular shell
if use_ipython:
#interactive_variables["lalala"] = print_help
help = print_help # Override help function
console = IPython.terminal.embed.InteractiveShellEmbed(local_ns=interactive_variables, 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 rlcompleter
import readline
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__ != "odrive.protocol.ChannelBrokenException":\n'
' superexcepthook(ex_class,ex,trace)')
console.runcode('sys.excepthook=newexcepthook')
print('')
print('ODRIVE EXPLORER')
print('')
print('You can now type "my_odrive." and press <tab>')
print('This will present you with all the properties that you can reference')
print('')
print('For example: "my_odrive.motor0.encoder.pll_pos"')
print('will print the current encoder position on motor 0')
print('and "my_odrive.motor0.pos_setpoint = 10000"')
print('will send motor0 to 10000')
print('')
# If IPython is installed, embed shell, otherwise drop into interactive stock python shell
if embed_ipython:
IPython.embed()
# Launch shell
print_banner()
interact()
app_shutdown_token.set()
+7 -38
View File
@@ -4,49 +4,18 @@ Liveplotter
"""
import time
import odrive.core
import matplotlib.pyplot as plt
import numpy as np
import threading
import odrive.discovery
from odrive.utils import start_liveplotter
data_rate = 100
plot_rate = 10
num_samples = 1000
my_odrive = odrive.core.find_any()
my_odrive = odrive.discovery.find_any()
plt.ion()
global vals
vals = []
# If you want to plot different values, change them here.
# You can plot any number of values concurrently.
start_liveplotter(lambda: [my_odrive.motor0.encoder.pll_pos,
my_odrive.motor1.encoder.pll_pos])
# Make sure the script terminates when the user closes the plotter
cancellation_token = threading.Event()
def handle_close(evt):
cancellation_token.set()
fig = plt.figure()
fig.canvas.mpl_connect('close_event', handle_close)
def fetch_data():
global vals
global cancellation_token
while not cancellation_token.is_set():
vals.append(my_odrive.motor0.timing_log.TIMING_LOG_FOC_CURRENT)
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
global cancellation_token
while not cancellation_token.is_set():
plt.clf()
plt.plot(vals)
#time.sleep(1/plot_rate)
fig.canvas.flush_events()
fetch_thread = threading.Thread(target=fetch_data, daemon=True)
fetch_thread.start()
plot_data()
-334
View File
@@ -1,334 +0,0 @@
"""
Provides functions for the discovery of ODrive devices
"""
import sys
import time
import json
import usb.core
import usb.util
import serial
import serial.tools.list_ports
import odrive.util
import odrive.usbbulk_transport
import odrive.serial_transport
import re
import time
import os
import odrive.protocol
import itertools
import struct
import functools
def noprint(x):
pass
class SimpleDeviceProperty(property):
"""
Used internally by dynamically created objects to translate
property assignments and fetches into endpoint operations on the
object's associated channel
"""
def __init__(self, channel, id, type, struct_format, can_read, can_write):
self._channel = channel
self._id = id
self._type = type
self._struct_format = struct_format
property.__init__(self,
self.fget if can_read else None,
self.fset if can_write else None)
def fget(self, obj):
size = struct.calcsize(self._struct_format)
buffer = self._channel.remote_endpoint_operation(self._id, None, True, size)
return struct.unpack(self._struct_format, buffer)[0]
def fset(self, obj, value):
value = self._type(value)
buffer = struct.pack(self._struct_format, value)
# TODO: Currenly we wait for an ack here. Settle on the default guarantee.
self._channel.remote_endpoint_operation(self._id, buffer, True, 0)
def call_remote_function(channel, trigger_id, arg_properties, *args):
"""
Used internally by the dynamically created objects to translate
function calls into endpoint operations on the associated channel
"""
if (len(arg_properties) != len(args)):
raise TypeError("expected {} arguments but have {}".format(len(arg_properties), len(args)))
for i in range(len(args)):
arg_properties[i].fset(None, args[i])
channel.remote_endpoint_operation(trigger_id, None, True, 0)
def setattr_or_raise_if_undefined(self, name, value):
"""
If employed as an object's __setattr__ function, this function
makes sure that an assignment to an undefined attribute doesn't
create a new attribute but instead raises an exception
"""
# We can't use hasattr here because internally it fetches the property
# value, creating unnecessary bus traffic
if name in dir(self):
object.__setattr__(self, name, value)
else:
raise TypeError('Cannot set name %r on object of type %s' % (
name, self.__class__.__name__))
def create_property(name, json_data, channel, printer):
"""
Dynamically creates a property based on a JSON definition
"""
name = name or "[anonymous]"
type_str = json_data.get("type", None)
if type_str is None:
printer("property {} has no specified type".format(name))
return None
if type_str == "float":
property_type = float
struct_format = "<f"
elif type_str == "bool":
property_type = bool
struct_format = "<?"
elif type_str == "int8":
property_type = int
struct_format = "<b"
elif type_str == "uint8":
property_type = int
struct_format = "<B"
elif type_str == "int16":
property_type = int
struct_format = "<h"
elif type_str == "uint16":
property_type = int
struct_format = "<H"
elif type_str == "int32":
property_type = int
struct_format = "<i"
elif type_str == "uint32":
property_type = int
struct_format = "<I"
elif type_str == "int64":
property_type = int
struct_format = "<q"
elif type_str == "uint64":
property_type = int
struct_format = "<Q"
else:
printer("property {} has unsupported type {}".format(name, type_str))
return None
id_str = json_data.get("id", None)
if id_str is None:
printer("property {} has no specified ID".format(name))
return None
access_mode = json_data.get("access", "r")
return SimpleDeviceProperty(channel, id_str, property_type,
struct_format,
'r' in access_mode,
'w' in access_mode)
def create_function(name, json_data, channel, printer):
"""
Dynamically creates a function based on a JSON definition
"""
id_str = json_data.get("id", None)
if id_str is None:
printer("function {} has no specified ID".format(name))
return None
inputs = []
for param in json_data.get("arguments", []):
param["mode"] = "r"
inputs.append(create_property(json_data["name"], param, channel, printer))
return functools.partial(call_remote_function, channel, id_str, inputs)
def create_object(name, json_data, namespace, channel, printer=noprint):
"""
Creates an object that implements the specified JSON type description by
communicating with the provided device object
"""
if not namespace is None:
namespace = namespace + "." + name
else:
namespace = name
# Build attribute list from JSON
attributes = {"__setattr__": setattr_or_raise_if_undefined,
"__channel__": channel}
for member in json_data.get("members", []):
member_name = member.get("name", None)
if member_name is None:
printer("ignoring unnamed attribute in {}".format(namespace))
continue
type_str = member.get("type", None)
if type_str is None:
printer("member {} has no specified type".format(member_name))
continue
if type_str == "object":
attribute = create_object(member_name, member, namespace, channel, printer=printer)
elif type_str == "function":
attribute = create_function(member_name, member, channel, printer)
else:
attribute = create_property(member_name, member, channel, printer)
if not attribute is None:
attributes[member_name] = attribute
# Create a type from the property list and instantiate it
jit_type = type(str(namespace), (object,), attributes)
new_object = jit_type()
return new_object
def channel_from_usb_device(usb_device, printer=noprint):
"""
Inits an ODrive Protocol channel from a PyUSB device object.
"""
bulk_device = odrive.usbbulk_transport.USBBulkTransport(usb_device, printer)
printer(bulk_device.info())
bulk_device.init()
channel = odrive.protocol.Channel(
"USB device bus {} device {}".format(usb_device.bus, usb_device.address),
bulk_device, bulk_device)
channel.usb_device = usb_device # for debugging only
return channel
def channel_from_serial_port(port, baud, packet_based, printer=noprint):
"""
Inits an ODrive Protocol channel from a serial port name and baudrate.
"""
if packet_based == True:
# TODO: implement packet based transport over serial
raise NotImplementedError("not supported yet")
serial_device = odrive.serial_transport.SerialStreamTransport(port, baud)
input_stream = odrive.protocol.PacketFromStreamConverter(serial_device)
output_stream = odrive.protocol.PacketToStreamConverter(serial_device)
return odrive.protocol.Channel(
"serial port {}@{}".format(port, baud),
input_stream, output_stream)
def object_from_channel(channel, printer=noprint):
"""
Inits an object from a given channel.
This queries the endpoint 0 on that channel to gain information
about the interface, which is then used to init the corresponding object.
"""
printer("Connecting to device on " + channel._name)
try:
json_bytes = channel.remote_endpoint_read_buffer(0)
except (odrive.protocol.TimeoutException, odrive.protocol.ChannelBrokenException):
raise odrive.protocol.DeviceInitException("no response - probably incompatible")
json_crc16 = odrive.protocol.calc_crc16(odrive.protocol.PROTOCOL_VERSION, json_bytes)
channel._interface_definition_crc = json_crc16
try:
json_string = json_bytes.decode("ascii")
except UnicodeDecodeError:
raise odrive.protocol.DeviceInitException("device responded on endpoint 0 with something that is not ASCII")
printer("JSON: " + json_string)
try:
json_data = json.loads(json_string)
except json.decoder.JSONDecodeError:
raise odrive.protocol.DeviceInitException("device responded on endpoint 0 with something that is not JSON")
json_data = {"name": "odrive", "members": json_data}
return create_object("odrive", json_data, None, channel, printer=printer)
def find_usb_channels(vid_pid_pairs=odrive.util.USB_VID_PID_PAIRS, printer=noprint, **kwargs):
"""
Scans for compatible USB devices.
Returns a generator of odrive.protocol.Channel objects.
"""
for vid_pid_pair in vid_pid_pairs:
for usb_device in usb.core.find(idVendor=vid_pid_pair[0], idProduct=vid_pid_pair[1], find_all=True):
if "serial_number" in kwargs:
if usb_device.serial_number != kwargs["serial_number"]:
continue
printer("Found ODrive via PyUSB")
try:
yield channel_from_usb_device(usb_device, printer)
except usb.core.USBError as ex:
if ex.errno == 13:
printer("USB device access denied. Did you set up your udev rules correctly?")
continue
raise
def find_dev_serial_ports(search_regex):
try:
return ['/dev/' + x for x in filter(re.compile(search_regex).search, os.listdir('/dev'))]
except FileNotFoundError:
return []
def find_pyserial_ports():
return [x.device for x in serial.tools.list_ports.comports()]
def find_serial_channels(printer=noprint):
"""
Scans for serial ports.
Returns a generator of odrive.protocol.Channel objects.
Not every returned object necessarily represents a compatible device.
"""
# Real serial ports or USB-Serial converters (tested on Linux and Windows)
real_serial_ports = find_pyserial_ports()
# Serial devices that are exposed by the platform
# for the device's USB connection
linux_usb_serial_ports = find_dev_serial_ports(r'^ttyACM')
macos_usb_serial_ports = find_dev_serial_ports(r'^tty\.usbmodem')
for port in real_serial_ports + linux_usb_serial_ports + macos_usb_serial_ports:
yield channel_from_serial_port(port, 115200, False, printer)
def find_all(consider_usb=True, consider_serial=False, printer=noprint, **kwargs):
"""
Returns a generator with all the connected devices that speak the ODrive protocol
"""
channels = iter(())
if (consider_usb):
channels = itertools.chain(channels, find_usb_channels(printer=printer, **kwargs))
if (consider_serial):
channels = itertools.chain(channels, find_serial_channels(printer=printer))
for channel in channels:
# TODO: blacklist known bad channels
try:
yield object_from_channel(channel, printer)
except odrive.protocol.DeviceInitException as ex:
printer(str(ex))
continue
def find_any(consider_usb=True, consider_serial=False,
cancellation_token=None, printer=noprint, **kwargs):
"""
Scans for ODrives on all supported interfaces and returns the first device
that is found. If no device is connected the function blocks.
"""
# TODO: do device discovery and instantiation in a separate thread and just wait on a semaphore here
# poll for device
printer("looking for ODrive...")
while cancellation_token == None or not cancellation_token.is_set():
dev = next(find_all(consider_usb, consider_serial, printer=printer, **kwargs), None)
if dev is not None:
return dev
printer("no device found")
time.sleep(1)
return None
def open_serial(port_name, printer=noprint):
channel = channel_from_serial_port(port_name, 115200, False, printer)
return object_from_channel(channel, printer)
def open_usb(bus, address, printer=noprint):
usb_device1 = usb.core.find(bus=1, address=16)
usb_device = usb.core.find(bus=bus, address=address)
if usb_device is None:
raise odrive.protocol.DeviceInitException("No USB device found on bus {} device {}".format(bus, address))
channel = channel_from_usb_device(usb_device, printer)
return object_from_channel(channel, printer)
+96
View File
@@ -0,0 +1,96 @@
"""
Provides functions for the discovery of ODrive devices
"""
import sys
import json
import time
import threading
import traceback
import odrive.protocol
import odrive.utils
import odrive.remote_object
import odrive.usbbulk_transport
import odrive.serial_transport
channel_types = {
"usb": odrive.usbbulk_transport.discover_channels,
"serial": odrive.serial_transport.discover_channels
}
def noprint(text):
pass
def find_all(path, serial_number,
did_discover_object_callback,
cancellation_token, printer=noprint):
"""
Starts scanning for ODrives that match the specified path spec and calls
the callback for each ODrive 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:
printer("Connecting to device on " + channel._name)
try:
json_bytes = channel.remote_endpoint_read_buffer(0)
except (odrive.utils.TimeoutException, odrive.protocol.ChannelBrokenException):
printer("no response - probably incompatible")
return
json_crc16 = odrive.protocol.calc_crc16(odrive.protocol.PROTOCOL_VERSION, json_bytes)
channel._interface_definition_crc = json_crc16
try:
json_string = json_bytes.decode("ascii")
except UnicodeDecodeError:
printer("device responded on endpoint 0 with something that is not ASCII")
return
printer("JSON: " + json_string)
try:
json_data = json.loads(json_string)
except json.decoder.JSONDecodeError as error:
printer("device responded on endpoint 0 with something that is not JSON: " + str(error))
return
json_data = {"name": "odrive", "members": json_data}
obj = odrive.remote_object.RemoteObject(json_data, None, channel, printer)
device_serial_number = serial_number if hasattr(obj, 'serial_number') else "[unknown serial number]"
if serial_number != None and device_serial_number != serial_number:
printer("Ignoring device with serial number {}".format(device_serial_number))
return
did_discover_object_callback(obj)
except Exception:
printer("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:
threading.Thread(target=channel_types[prefix],
args=(the_rest, serial_number, did_discover_channel, cancellation_token, printer)).start()
else:
raise Exception("Invalid path spec \"{}\"".format(search_spec))
def find_any(path="usb", serial_number=None, printer=noprint):
"""
Blocks until the first matching ODrive is connected and then returns that device
"""
cancellation_token = None # TODO: make this a parameter (see todo below)
if cancellation_token is None:
cancellation_token = threading.Event()
done_signal = threading.Event()
def did_discover_object(obj):
global result
result = obj
done_signal.set()
find_all(path, serial_number, did_discover_object, cancellation_token, printer)
done_signal.wait() # TODO: wait on done_signal OR cancellation_token
cancellation_token.set()
return result
+94 -46
View File
@@ -3,6 +3,11 @@
import time
import struct
import sys
import threading
import traceback
import odrive.utils
from odrive.utils import wait_any
from odrive.utils import Event
import abc
@@ -63,17 +68,21 @@ def calc_crc16(remainder, value):
#print(hex(calc_crc16(0xfeef, [1, 2, 3, 4, 5, 0x10, 0x13, 0x37])))
class TimeoutException(Exception):
pass
class ChannelBrokenException(Exception):
pass
class DeviceInitException(Exception):
pass
class USBHaltException(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):
@@ -98,11 +107,10 @@ class PacketSink(ABC):
class StreamToPacketConverter(StreamSink):
_header = []
_packet = []
_packet_length = 0
def __init__(self, output):
self._header = []
self._packet = []
self._packet_length = 0
self._output = output
def process_bytes(self, bytes):
@@ -194,15 +202,11 @@ class PacketFromStreamConverter(PacketSource):
class Channel(PacketSink):
_outbound_seq_no = 0
_interface_definition_crc = 0
_expected_acks = {}
# Choose these parameters to be sensible for a specific transport layer
_resend_timeout = 0.1 # [s]
_send_attempts = 5
def __init__(self, name, input, output):
def __init__(self, name, input, output, printer):
"""
Params:
input: A PacketSource where this channel will source packets from on
@@ -213,6 +217,41 @@ class Channel(PacketSink):
self._name = name
self._input = input
self._output = output
self._printer = printer
self._outbound_seq_no = 0
self._interface_definition_crc = 0
self._expected_acks = {}
self._responses = {}
self._my_lock = threading.Lock()
self._channel_broken = Event()
self.start_receiver_thread(Event()) # TODO: use app_shutdown_token
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():
try:
while (not cancellation_token.is_set()) and (not self._channel_broken.is_set()):
# 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 odrive.utils.TimeoutException:
continue # try again
except ChannelDamagedException:
continue # try again
# Process response
# This should not throw an exception, otherwise the channel breaks
self.process_packet(response)
print("receiver thread is exiting")
except Exception:
self._printer("receiver thread is exiting: " + traceback.format_exc())
finally:
self._channel_broken.set()
threading.Thread(target=receiver_thread, daemon=True).start()
def remote_endpoint_operation(self, endpoint_id, input, expect_ack, output_length):
if input is None:
@@ -223,9 +262,13 @@ class Channel(PacketSink):
if (expect_ack):
endpoint_id |= 0x8000
self._outbound_seq_no = ((self._outbound_seq_no + 1) & 0x7fff)
self._outbound_seq_no |= 0x80 # FIXME: we hardwire one bit of the seq-no to 1 to avoid conflicts with the legacy protocol
seq_no = self._outbound_seq_no
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 legacy protocol
packet = struct.pack('<HHH', seq_no, endpoint_id, output_length)
packet = packet + input
@@ -238,32 +281,32 @@ class Channel(PacketSink):
packet = packet + struct.pack('<H', trailer)
if (expect_ack):
self._expected_acks[seq_no] = None
attempt = 0
while (attempt < self._send_attempts):
try:
self._output.process_packet(packet)
except USBHaltException:
attempt += 1
continue # resend
deadline = time.monotonic() + self._resend_timeout
# Read and process packets until we get an ack or need to resend
# TODO: support I/O driven reception (wait on semaphore)
while True:
ack_event = Event()
self._expected_acks[seq_no] = ack_event
try:
attempt = 0
while (attempt < self._send_attempts):
self._my_lock.acquire()
try:
response = self._input.get_packet(deadline)
except TimeoutException:
break # resend
except USBHaltException:
break # resend
# process response, which is hopefully our ACK
self.process_packet(response)
if not self._expected_acks[seq_no] is None:
return self._expected_acks.pop(seq_no, None)
break
# TODO: record channel statistics
attempt += 1
raise ChannelBrokenException()
self._output.process_packet(packet)
except ChannelDamagedException:
attempt += 1
continue # resend
finally:
self._my_lock.release()
# Wait for ACK until the resend timeout is exceeded
try:
if wait_any(ack_event, self._channel_broken, timeout=self._resend_timeout) != 0:
raise ChannelBrokenException()
except odrive.utils.TimeoutException:
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)
@@ -293,7 +336,12 @@ class Channel(PacketSink):
if (seq_no & 0x8000):
seq_no &= 0x7fff
self._expected_acks[seq_no] = packet[2:]
ack_signal = self._expected_acks.get(seq_no, None)
if (ack_signal):
self._responses[seq_no] = packet[2:]
ack_signal.set()
else:
print("received unexpected ACK: " + str(seq_no))
else:
#if (calc_crc16(CRC16_INIT, struct.pack('<HBB', PROTOCOL_VERSION, packet[-2], packet[-1]))):
+193
View File
@@ -0,0 +1,193 @@
"""
Provides functions for the discovery of ODrive devices
"""
import sys
import json
import struct
import threading
import odrive.protocol
#class ObjectDisappearedError(Exception):
# def __init__(self, channel):
# self._obj = obj
# pass
class ObjectDefinitionError(Exception):
pass
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
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")
if type_str == "float":
self._property_type = float
self._struct_format = "<f"
elif type_str == "bool":
self._property_type = bool
self._struct_format = "<?"
elif type_str == "int8":
self._property_type = int
self._struct_format = "<b"
elif type_str == "uint8":
self._property_type = int
self._struct_format = "<B"
elif type_str == "int16":
self._property_type = int
self._struct_format = "<h"
elif type_str == "uint16":
self._property_type = int
self._struct_format = "<H"
elif type_str == "int32":
self._property_type = int
self._struct_format = "<i"
elif type_str == "uint32":
self._property_type = int
self._struct_format = "<I"
elif type_str == "int64":
self._property_type = int
self._struct_format = "<q"
elif type_str == "uint64":
self._property_type = int
self._struct_format = "<Q"
else:
raise ObjectDefinitionError("unsupported type {}".format(type_str))
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):
size = struct.calcsize(self._struct_format)
buffer = self._parent.__channel__.remote_endpoint_operation(self._id, None, True, size)
return struct.unpack(self._struct_format, buffer)[0]
def set_value(self, value):
value = self._property_type(value)
buffer = struct.pack(self._struct_format, 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)
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._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))
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)
class RemoteObject(object):
"""
Object with functions and properties that map to remote endpoints
"""
def __init__(self, json_data, parent, channel, printer):
"""
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:
printer("ignoring unnamed attribute")
continue
try:
type_str = member_json.get("type", None)
if type_str == "object":
attribute = RemoteObject(member_json, self, channel, printer)
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:
printer("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 __str__(self):
return str(dir(self)) # TODO: improve print output
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 = {}
+66 -4
View File
@@ -3,9 +3,15 @@ Provides classes that implement the StreamSource/StreamSink and
PacketSource/PacketSink interfaces for serial ports.
"""
import odrive
import serial
import os
import re
import time
import serial
import serial.tools.list_ports
import odrive.protocol
import odrive.utils
ODRIVE_BAUDRATE = 115200
class SerialStreamTransport(odrive.protocol.StreamSource, odrive.protocol.StreamSink):
def __init__(self, port, baud):
@@ -30,7 +36,63 @@ class SerialStreamTransport(odrive.protocol.StreamSource, odrive.protocol.Stream
def get_bytes_or_fail(self, n_bytes, deadline):
result = self.get_bytes(n_bytes, deadline)
if len(result) < n_bytes:
raise odrive.protocol.TimeoutException("expected {} bytes but got only {}", n_bytes, len(result))
raise odrive.utils.TimeoutException("expected {} bytes but got only {}", n_bytes, len(result))
return result
# TODO: provide SerialPacketTransport
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, printer):
"""
Scans for serial ports that match the path spec.
This function blocks until cancellation_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, ODRIVE_BAUDRATE)
input_stream = odrive.protocol.PacketFromStreamConverter(serial_device)
output_stream = odrive.protocol.PacketToStreamConverter(serial_device)
channel = odrive.protocol.Channel(
"serial port {}@{}".format(port_name, ODRIVE_BAUDRATE),
input_stream, output_stream, printer)
channel.serial_device = serial_device
except serial.serialutil.SerialException:
printer("Serial device init failed. Ignoring this port")
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)
+92 -24
View File
@@ -1,21 +1,24 @@
# requires pyusb
# pip install --pre pyusb
import sys
import time
import usb.core
import usb.util
import sys
import odrive.protocol
import time
def noprint(x):
pass
ODRIVE_VID_PID_PAIRS = [
(0x1209, 0x0D31),
(0x1209, 0x0D32), # <== TODO: this is the only official ODrive PID, remove the other ones
(0x1209, 0x0D33)
]
class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink):
def __init__(self, dev, printer=noprint):
def __init__(self, dev, printer):
self._printer = printer
self.dev = dev
self._name = "USB device {}:{}".format(dev.idVendor, dev.idProduct)
self._was_damaged = False
##
# information about the connected device
@@ -26,16 +29,12 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink)
for cfg in self.dev:
string += "ConfigurationValue {0}\n".format(cfg.bConfigurationValue)
for intf in cfg:
string += "\tInterfaceNumber {0},{0}\n".format(intf.bInterfaceNumber, intf.bAlternateSetting)
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):
# Resetting device to start init from a known state
# self.dev.reset()
# time.sleep(1)
# detach kernel driver
try:
if self.dev.is_kernel_driver_active(1):
self.dev.detach_kernel_driver(1)
@@ -75,39 +74,108 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink)
def process_packet(self, usbBuffer):
try:
ret = self.epw.write(usbBuffer, 0)
if self._was_damaged:
self._printer("Recovered from USB halt/stall condition")
self._was_damaged = False
return ret
except usb.core.USBError as ex:
if ex.errno == 19: # "no such device"
raise odrive.protocol.ChannelBrokenException()
else:
# Try resetting halt/stall condition
self.epw.clear_halt()
# Resend
ret = self.epw.write(usbBuffer, 0)
self._printer("Recovered from USB halt/stall condition on write")
return ret
# Signal to retry transfer
# raise odrive.protocol.USBHaltException()
try:
self.epw.clear_halt()
except usb.core.USBError:
raise odrive.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise odrive.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._printer("Recovered from USB halt/stall condition")
self._was_damaged = False
return bytearray(ret)
except usb.core.USBError as ex:
if ex.errno == 19: # "no such device"
raise odrive.protocol.ChannelBrokenException()
else:
# Try resetting halt/stall condition and flush buffer
self.epr.clear_halt()
ret = self.epr.read(bufferLen, timeout)
self._printer("Recovered from USB halt/stall condition on read")
# Signal to retry transfer
raise odrive.protocol.USBHaltException()
# Try resetting halt/stall condition
try:
self.epw.clear_halt()
except usb.core.USBError:
raise odrive.protocol.ChannelBrokenException()
# Retry transfer
self._was_damaged = True
raise odrive.protocol.ChannelDamagedException()
def send_max(self):
return 64
def receive_max(self):
return 64
def discover_channels(path, serial_number, callback, cancellation_token, printer):
"""
Scans for USB devices that match the path spec.
This function blocks until cancellation_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))
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 ODRIVE_VID_PID_PAIRS:
return False
return True
while not cancellation_token.is_set():
printer("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, printer)
printer(bulk_device.info())
bulk_device.init()
channel = odrive.protocol.Channel(
"USB device bus {} device {}".format(usb_device.bus, usb_device.address),
bulk_device, bulk_device, printer)
channel.usb_device = usb_device # for debugging only
except usb.core.USBError as ex:
if ex.errno == 13:
printer("USB device access denied. Did you set up your udev rules correctly?")
continue
elif ex.errno == 16:
printer("USB device busy. I'll reset it and try again.")
usb_device.reset()
continue
else:
printer("USB device init failed. Ignoring this device")
known_devices.append((usb_device.bus, usb_device.address))
else:
known_devices.append((usb_device.bus, usb_device.address))
callback(channel)
time.sleep(1)
-23
View File
@@ -1,23 +0,0 @@
# requires pyusb
# pip install --pre pyusb
# Exceptions
class ODriveError(Exception):
pass
class ODriveNotConnectedError(ODriveError):
pass
USB_DEV_ODRIVE_3_1 = (0x1209, 0x0D31)
USB_DEV_ODRIVE_3_2 = (0x1209, 0x0D32)
USB_DEV_ODRIVE_3_3 = (0x1209, 0x0D33)
# all devices
USB_VID_PID_PAIRS = [
USB_DEV_ODRIVE_3_1,
USB_DEV_ODRIVE_3_2,
USB_DEV_ODRIVE_3_3,
]
def noprint(x):
pass
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
Liveplotter
"""
import sys
import time
import threading
data_rate = 100
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 = threading.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)
#time.sleep(1/plot_rate)
fig.canvas.flush_events()
threading.Thread(target=fetch_data).start()
threading.Thread(target=plot_data).start()
#plot_data()
## Exceptions ##
class TimeoutException(Exception):
pass
## Threading utils ##
class Event():
"""
Alternative to threading.Event(), enhanced by the subscribe() function
that the original fails to provide.
"""
def __init__(self):
self._evt = threading.Event()
self._subscribers = []
self._mutex = threading.Lock()
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.
"""
self._mutex.acquire()
try:
self._subscribers.append(handler)
if self._evt.is_set():
handler()
finally:
self._mutex.release()
return lambda: self.unsubscribe(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):
return self._evt.wait(timeout=timeout)
def wait_any(*events, timeout=None):
"""
Blocks until any of the specified events are triggered.
Returns the number of the event that was triggerd or raises
a TimeoutException
"""
or_event = threading.Event()
unsubscribe_functions = []
for event in events:
unsubscribe_functions.append(event.subscribe(lambda: or_event.set()))
or_event.wait(timeout=timeout)
for unsubscribe_function in unsubscribe_functions:
unsubscribe_function()
for i in range(len(events)):
if events[i].is_set():
return i
raise TimeoutException()
Regular → Executable
+7 -2
View File
@@ -1,9 +1,14 @@
#!/usr/bin/env python3
import time
import odrive.core
import odrive.discovery
import matplotlib.pyplot as plt
import numpy as np
myOdrive = odrive.core.find_any()
# Find a connected ODrive (this will block until you connect one)
print("Waiting for ODrive...")
myOdrive = odrive.discovery.find_any()
print("connected")
plt.ion()