Merge branch 'devel' into sam_fibre

Conflicts:
	Firmware/Tupfile.lua
	Firmware/fibre/cpp/include/fibre/protocol.hpp
	Firmware/fibre/python/fibre/discovery.py
	Firmware/fibre/python/fibre/usbbulk_transport.py
	tools/odrive/dfu.py
	tools/odrive/shell.py
	tools/odrive/utils.py
	tools/odrivetool
This commit is contained in:
Oskar Weigl
2018-06-08 19:30:21 -07:00
87 changed files with 17349 additions and 773 deletions
+69
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)
+82
View File
@@ -0,0 +1,82 @@
import json
import os
import tempfile
import odrive.remote_object
from odrive.utils import OperationAbortedException
def get_dict(obj, is_config_object):
result = {}
for (k,v) in obj._remote_attributes.items():
if isinstance(v, odrive.remote_object.RemoteProperty) and is_config_object:
result[k] = v.get_value()
elif isinstance(v, odrive.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, odrive.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 = odrive.utils.get_serial_number_str(device)
safe_serial_number = ''.join(filter(str.isalnum, serial_number))
return os.path.join(tempfile.gettempdir(), 'odrive-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 odrive.utils.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 = odrive.configuration.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.")
+294 -197
View File
File diff suppressed because it is too large Load Diff
+119
View File
@@ -1,5 +1,8 @@
import usb.util
import time
import fractions
import array
from odrive.dfuse.DfuState import DfuState
DFU_REQUEST_SEND = 0x21
DFU_REQUEST_RECEIVE = 0xa1
@@ -12,6 +15,9 @@ DFU_CLRSTATUS = 0x04
DFU_GETSTATE = 0x05
DFU_ABORT = 0x06
SIZE_MULTIPLIERS = {' ': 1, 'K': 1024, 'M' : 1024*1024}
MAX_TRANSFER_SIZE = 2048
# Order is LSB first
def address_to_4bytes(a):
return [ a % 256, (a >> 8)%256, (a >> 16)%256, (a >> 24)%256 ]
@@ -24,6 +30,7 @@ class DfuDevice:
self.intf = None
#self.dev.reset()
self.cfg.set()
self.sectors = list(self.get_device_sectors())
def alternates(self):
return [(usb.util.get_string(self.dev, intf.iInterface), intf) for intf in self.cfg]
@@ -98,3 +105,115 @@ class DfuDevice:
return status
## High level functions ##
# by ODrive Robotics
def get_device_sectors(self):
"""
Returns a list of all sectors on the device.
Each sector is represented as a dictionary with the following keys:
- name: name of the associated memory region (e.g. "Internal Flash")
- alt: USB alternate setting associated with this memory region
- addr: Start address of the sector (e.g. 0x08004000 for the second flash sectors)
- baseaddr: Start address of the memory region associated with the sector
(e.g. 0x08000000 for all flash sectors)
- len: Number of bytes in the sector
"""
for name, alt in self.alternates():
# example for name:
# '@Internal Flash /0x08000000/04*016Kg,01*064Kg,07*128Kg'
label, baseaddr, layout = name.split('/')
baseaddr = int(baseaddr, 0) # convert hex to decimal
addr = baseaddr
for sector in layout.split(','):
repeat, size = map(int, sector[:-2].split('*'))
size *= SIZE_MULTIPLIERS[sector[-2].upper()]
mode = sector[-1]
while repeat > 0:
# TODO: verify if the section is writable
yield {
'name': label.strip().strip('@'),
'alt': alt,
'baseaddr': baseaddr,
'addr': addr,
'len': size,
'mode': mode
}
addr += size
repeat -= 1
def set_alternate_safe(self, alt):
self.set_alternate(alt)
if self.get_state() == DfuState.DFU_ERROR:
self.clear_status()
self.wait_while_state(DfuState.DFU_ERROR)
#def clear_error(self)
def set_address_safe(self, addr):
self.set_address(addr)
status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY)
if status[1] != DfuState.DFU_DOWNLOAD_IDLE:
raise RuntimeError("An error occured. Device Status: %r" % status)
# take device out of DFU_DOWNLOAD_SYNC and into DFU_IDLE
self.abort()
status = self.wait_while_state(DfuState.DFU_DOWNLOAD_SYNC)
if status[1] != DfuState.DFU_IDLE:
raise RuntimeError("An error occured. Device Status: %r" % status)
def erase_sector(self, sector):
self.set_alternate_safe(sector['alt'])
self.erase(sector['addr'])
status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY, timeout=sector['len']/32)
if status[1] != DfuState.DFU_DOWNLOAD_IDLE:
raise RuntimeError("An error occured. Device Status: %r" % status)
def write_sector(self, sector, data):
self.set_alternate_safe(sector['alt'])
self.set_address_safe(sector['addr'])
transfer_size = fractions.gcd(sector['len'], MAX_TRANSFER_SIZE)
blocks = [data[i:i + transfer_size] for i in range(0, len(data), transfer_size)]
for blocknum, block in enumerate(blocks):
#print('write to {:08X} ({} bytes)'.format(
# sector['addr'] + blocknum * TRANSFER_SIZE, len(block)))
self.write(blocknum, block)
status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY)
if status[1] != DfuState.DFU_DOWNLOAD_IDLE:
raise RuntimeError("An error occured. Device Status: %r" % status)
def read_sector(self, sector):
"""
Reads data from the specified sector
Returns: a byte array containing the data
"""
self.set_alternate_safe(sector['alt'])
self.set_address_safe(sector['addr'])
transfer_size = fractions.gcd(sector['len'], MAX_TRANSFER_SIZE)
#blocknum_offset = int((sector['addr'] - sector['baseaddr']) / transfer_size)
data = array.array(u'B')
for blocknum in range(int(sector['len'] / transfer_size)):
#print('read at {:08X}'.format(sector['addr'] + blocknum * TRANSFER_SIZE))
deviceBlock = self.read(blocknum, transfer_size)
data.extend(deviceBlock)
self.abort() # take device into DFU_IDLE
return data
def jump_to_application(self, address):
self.set_address_safe(address)
#self.set_address(address)
#status = self.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY)
#if status[1] != DfuState.DFU_DOWNLOAD_IDLE:
# raise RuntimeError("An error occured. Device Status: {}".format(status[1]))
self.leave()
status = self.wait_while_state(DfuState.DFU_MANIFEST_SYNC)
if status[1] != DfuState.DFU_MANIFEST:
raise RuntimeError("An error occured. Device Status: {}".format(status[1]))
+1 -1
View File
@@ -11,7 +11,7 @@ AXIS_STATE_ENCODER_INDEX_SEARCH = 6
AXIS_STATE_ENCODER_OFFSET_CALIBRATION = 7
AXIS_STATE_CLOSED_LOOP_CONTROL = 8
AXIS_ERROR_NO_ERROR = 0
AXIS_ERROR_NONE = 0
AXIS_ERROR_INVALID_STATE = 1
#AXIS_ERROR_DC_BUS_UNDER_VOLTAGE = 2
#AXIS_ERROR_DC_BUS_OVER_VOLTAGE = 3
+35 -35
View File
@@ -31,41 +31,41 @@ def print_help(args, have_devices):
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.info("{} 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))
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, printer, app_shutdown_token):
"""
+115 -7
View File
@@ -52,12 +52,22 @@ class AxisTestContext():
self.odrv_ctx = odrv_ctx
def test_assert_eq(observed, expected, range=None, accuracy=None):
if range is None and accuracy is None and observed != expected:
raise TestFailed("value mismatch: expected {} but observed {}".format(expected, observed))
if not range is None and ((observed < expected - range) or (observed > expected + range)):
raise TestFailed("value out of range: expected {}+-{} but observed {}".format(expected, range, observed))
elif not accuracy is None and ((observed < expected * (1 - accuracy)) or (observed > expected * (1 + accuracy))):
raise TestFailed("value out of range: expected {}+-{}% but observed {}".format(expected, accuracy*100.0, observed))
sign = lambda x: 1 if x >= 0 else -1
# Comparision with absolute range
if not range is None:
if (observed < expected - range) or (observed > expected + range):
raise TestFailed("value out of range: expected {}+-{} but observed {}".format(expected, range, observed))
# Comparision with relative range
elif not accuracy is None:
if sign(observed) != sign(expected) or (abs(observed) < abs(expected) * (1 - accuracy)) or (abs(observed) > abs(expected) * (1 + accuracy)):
raise TestFailed("value out of range: expected {}+-{}% but observed {}".format(expected, accuracy*100.0, observed))
# Exact comparision
else:
if observed != expected:
raise TestFailed("value mismatch: expected {} but observed {}".format(expected, observed))
def get_errors(axis_ctx: AxisTestContext):
errors = []
@@ -112,7 +122,7 @@ def request_state(axis_ctx: AxisTestContext, state, expect_success=True):
else:
test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE)
test_assert_eq(axis_ctx.handle.error, AXIS_ERROR_INVALID_STATE)
axis_ctx.handle.error = AXIS_ERROR_NO_ERROR # reset error
axis_ctx.handle.error = AXIS_ERROR_NONE # reset error
def set_limits(axis_ctx: AxisTestContext, logger, vel_limit=20000, current_limit=10):
"""
@@ -155,6 +165,9 @@ def get_max_rpm(axis_ctx: AxisTestContext):
rated_rpm = min(base_speed_rpm, axis_ctx.yaml['encoder-max-rpm'])
return rated_rpm
def get_sensorless_vel(axis_ctx: AxisTestContext, vel):
return vel * 2 * math.pi / axis_ctx.yaml['encoder-cpr'] * axis_ctx.yaml['motor-pole-pairs']
class ODriveTest(ABC):
"""
Tests inheriting from this class get full ownership of the ODrive
@@ -670,3 +683,98 @@ class TestVelCtrlVsPosCtrl(DualAxisTest):
#init_pos = axis1_ctx.handle.encoder.pos_estimate
#axis1_ctx.handle.controller.set_pos_setpoint(init_pos + 100000, 0, 0)
#request_state(axis1_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL)
# ASCII protocol helper functions
def gcode_calc_checksum(data):
from functools import reduce
return reduce(lambda a, b: a ^ b, data)
def gcode_append_checksum(data):
return data + b'*' + str(gcode_calc_checksum(data)).encode('ascii')
def get_lines(port):
buf = port.get_bytes(512, time.monotonic() + 0.2)
return [line.rstrip(b'\r') for line in buf.split(b'\n') if line.rstrip(b'\r')]
class TestAsciiProtocol(ODriveTest):
def run_test(self, odrv_ctx: ODriveTestContext, logger):
import odrive.serial_transport
port = odrive.serial_transport.SerialStreamTransport(odrv_ctx.yaml['uart'], 115200)
# send garbage to throw the device off track
port.process_bytes(b"garbage\r\n\r\0trash\n")
port.process_bytes(b"\n") # start a new clean line
get_lines(port) # flush RX buffer
# info command without checksum
port.process_bytes(b"i\n")
# check if it reports the serial number (among other things)
lines = get_lines(port)
expected_line = ('Serial number: ' + odrv_ctx.yaml['serial-number']).encode('ascii')
if not expected_line in lines:
raise Exception("expected {} in ASCII protocol response but got {}".format(expected_line, str(lines)))
# info command with checksum
port.process_bytes(gcode_append_checksum(b"i") + b" ; a useless comment\n")
# check if it reports the serial number with checksum (among other things)
lines = get_lines(port)
expected_line = gcode_append_checksum(('Serial number: ' + odrv_ctx.yaml['serial-number']).encode('ascii'))
if not expected_line in lines:
raise Exception("expected {} in ASCII protocol response but got {}".format(expected_line, str(lines)))
port.process_bytes(b"p 0 2000 -10 0.002\n")
time.sleep(0.01) # 1ms is too short, 2ms usually works, 10ms for good measure
test_assert_eq(odrv_ctx.handle.axis0.controller.pos_setpoint, 2000, accuracy=0.001)
test_assert_eq(odrv_ctx.handle.axis0.controller.vel_setpoint, -10, accuracy=0.001)
test_assert_eq(odrv_ctx.handle.axis0.controller.current_setpoint, 0.002, accuracy=0.001)
port.process_bytes(b"v 1 -21.1 0.32\n")
time.sleep(0.01)
test_assert_eq(odrv_ctx.handle.axis1.controller.vel_setpoint, -21.1, accuracy=0.001)
test_assert_eq(odrv_ctx.handle.axis1.controller.current_setpoint, 0.32, accuracy=0.001)
port.process_bytes(b"c 0 0.1\n")
time.sleep(0.01)
test_assert_eq(odrv_ctx.handle.axis0.controller.current_setpoint, 0.1, accuracy=0.001)
# write arbitrary parameter
port.process_bytes(b"w axis0.controller.pos_setpoint -123.456 ; comment\n")
time.sleep(0.01)
test_assert_eq(odrv_ctx.handle.axis0.controller.pos_setpoint, -123.456, accuracy=0.001)
port.process_bytes(b"r axis0.controller.pos_setpoint\n")
lines = get_lines(port)
expected_line = b'-123.4560'
if lines != [expected_line]:
raise Exception("expected {} in ASCII protocol response but got {}".format(expected_line, str(lines)))
# read/write enums
port.process_bytes(b"r axis0.error\n")
lines = get_lines(port)
expected_line = b'0'
if lines != [expected_line]:
raise Exception("expected {} in ASCII protocol response but got {}".format(expected_line, str(lines)))
test_assert_eq(odrv_ctx.axes[0].handle.current_state, AXIS_STATE_CLOSED_LOOP_CONTROL)
port.process_bytes(b"w axis0.requested_state {}\n".format(AXIS_STATE_IDLE))
time.sleep(0.01)
test_assert_eq(odrv_ctx.axes[0].handle.current_state, AXIS_STATE_IDLE)
# disable axes
odrv_ctx.handle.axis0.controller.set_pos_setpoint(0, 0, 0)
odrv_ctx.handle.axis1.controller.set_pos_setpoint(0, 0, 0)
request_state(odrv_ctx.axes[0], AXIS_STATE_IDLE)
request_state(odrv_ctx.axes[1], AXIS_STATE_IDLE)
class TestSensorlessControl(AxisTest):
def run_test(self, axis_ctx: AxisTestContext, logger):
odrv0.axis0.controller.config.vel_gain = 5 / get_sensorless_vel(axis_ctx, 10000)
odrv0.axis0.controller.config.vel_integrator_gain = 10 / get_sensorless_vel(axis_ctx, 10000)
target_vel = get_sensorless_vel(axis_ctx, 20000)
axis_ctx.handle.controller.set_vel_setpoint(target_vel, 0)
request_state(axis_ctx, AXIS_STATE_SENSORLESS_CONTROL)
# wait for spinup
time.sleep(2)
test_assert_eq(odrv0.axis0.encoder.pll_vel, target_vel, range=2000)
request_state(axis_ctx, AXIS_STATE_IDLE)
+28
View File
@@ -22,6 +22,9 @@ data_rate = 100
plot_rate = 10
num_samples = 1000
class OperationAbortedException(Exception):
pass
def start_liveplotter(get_var_callback):
"""
Starts a liveplotter.
@@ -153,3 +156,28 @@ def setup_udev_rules(logger):
subprocess.run(["udevadm", "control", "--reload-rules"], check=True)
subprocess.run(["udevadm", "trigger"], check=True)
logger.info('udev rules configured successfully')
def get_serial_number_str(device):
if hasattr(device, 'serial_number'):
return format(device.serial_number, 'x').upper()
else:
return "[unknown serial number]"
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
+18 -8
View File
@@ -4,6 +4,20 @@ import subprocess
import os
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:
@@ -12,19 +26,15 @@ def get_version_from_git():
cwd=script_dir)
git_tag = git_tag.decode(sys.stdout.encoding).rstrip('\n')
regex=r'.*v([0-9a-zA-Z]).([0-9a-zA-Z]).([0-9a-zA-Z])(.*)'
package_version_major = int(re.sub(regex, r"\1", git_tag))
package_version_minor = int(re.sub(regex, r"\2", git_tag))
package_version_revision = int(re.sub(regex, r"\3", git_tag))
package_version_unreleased = (re.sub(regex, r"\4", git_tag) != "")
(major, minor, revision, is_prerelease) = version_str_to_tuple(git_tag)
if package_version_unreleased:
package_version_revision += 1
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
return git_tag, package_version_major, package_version_minor, package_version_revision, package_version_unreleased
def get_version_str(git_only=False):
"""
+46
View File
@@ -0,0 +1,46 @@
/*
* This file was autogenerated using the "odrivetool generate-code" feature.
*
* The file matches a specific firmware version. If you add/remove/rename any
* properties exposed by the ODrive, this file needs to be regenerated, otherwise
* the ODrive will ignore all commands.
*/
#ifndef __ODRIVE_ENDPOINTS_HPP
#define __ODRIVE_ENDPOINTS_HPP
{% macro enum_name(endpoint) %}{{ endpoint.name | replace('.', '__') | upper }}{% endmacro %}
namespace odrive {
static constexpr const uint16_t json_crc = 0x{{ "%0x" | format(json_crc) }};
static constexpr const uint16_t per_axis_offset = {{ per_axis_offset }};
enum { {% for endpoint in endpoints %}
{{enum_name(endpoint)}} = {{endpoint.id}},
{%- endfor %}
// Per-Axis endpoints (to be used with read_axis_property and write_axis_property)
{%- for endpoint in axis_endpoints %}
{{enum_name(endpoint)}} = {{endpoint.id}},
{%- endfor %}
};
template<int I>
struct endpoint_type;
{% for endpoint in endpoints -%}
template<> struct endpoint_type<{{enum_name(endpoint)}}> { typedef {{endpoint.type}} type; };
{% endfor %}
// Per-axis endpoints
{% for endpoint in axis_endpoints -%}
template<> struct endpoint_type<{{enum_name(endpoint)}}> { typedef {{endpoint.type}} type; };
{% endfor %}
template<int I>
using endpoint_type_t = typename endpoint_type<I>::type;
}
#endif // __ODRIVE_ENDPOINTS_HPP
+65 -8
View File
@@ -14,8 +14,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(
import fibre.discovery
from fibre import Logger, Event
import odrive
#print("Refer to install instructions at http://docs.odriverobotics.com/#downloading-and-installing-tools")
import odrive.discovery
from odrive.utils import OperationAbortedException
from odrive.configuration import *
# Flush stdout by default
# Source:
@@ -27,6 +28,7 @@ def 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='ODrive command line utility\n'
@@ -41,8 +43,33 @@ shell_parser.add_argument("--no-ipython", action="store_true",
"instead of the IPython shell, "
"even if IPython is installed.")
dfu_parser = subparsers.add_parser('dfu', help="Upgrade the ODrive device firmware")
dfu_parser.add_argument('file', metavar='HEX', help='The .hex file to be flashed. Make sure your firmware board version matches the actual board version.')
dfu_parser = subparsers.add_parser('dfu', help="Upgrade the ODrive device firmware."
"If no serial number is specified, the first ODrive that is found is updated")
dfu_parser.add_argument('file', metavar='HEX', nargs='?',
help='The .hex file to be flashed. Make sure target board version '
'of the firmware file matches the actual board version. '
'You can download the latest release manually from '
'https://github.com/madcowswe/ODrive/releases. '
'If no file is provided, the script automatically downloads '
'the latest firmware.')
dfu_parser = subparsers.add_parser('backup-config', help="Saves the configuration of the ODrive to a JSON file")
dfu_parser.add_argument('file', nargs='?',
help="Path to the file where to store the data. "
"If no path is provided, the configuration is stored in {}.".format(tempfile.gettempdir()))
dfu_parser = subparsers.add_parser('restore-config', help="Restores the configuration of the ODrive from a JSON file")
dfu_parser.add_argument('file', nargs='?',
help="Path to the file that contains the configuration data. "
"If no path is provided, the configuration is loaded from {}.".format(tempfile.gettempdir()))
code_generator_parser = subparsers.add_parser('generate-code', help="Process a jinja2 template, passing the ODrive's JSON data as data input")
code_generator_parser.add_argument("-t", "--template", type=argparse.FileType('r'),
help="the code template")
code_generator_parser.add_argument("-o", "--output", type=argparse.FileType('w'), default='-',
help="path of the generated output")
code_generator_parser.set_defaults(template = os.path.join(script_path, 'odrive_header_template.h.in'))
subparsers.add_parser('liveplotter', help="Upgrade the ODrive's Firmware")
subparsers.add_parser('drv-status', help="Show status of the on-board DRV8301 chips (for debugging only)")
@@ -118,12 +145,14 @@ try:
elif args.command == 'dfu':
print_version()
import odrive.dfu
odrive.dfu.launch_dfu(args, app_shutdown_token)
odrive.dfu.launch_dfu(args, logger, app_shutdown_token)
elif args.command == 'liveplotter':
from odrive.utils import start_liveplotter
print("Waiting for ODrive...")
my_odrive = odrive.find_any(path=args.path, serial_number=args.serial_number)
my_odrive = odrive.discovery.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.
@@ -133,22 +162,50 @@ try:
elif args.command == 'drv-status':
from odrive.utils import print_drv_regs
print("Waiting for ODrive...")
my_odrive = odrive.find_any(path=args.path, serial_number=args.serial_number)
my_odrive = odrive.discovery.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", my_odrive.axis0.motor)
print_drv_regs("Motor 1", my_odrive.axis1.motor)
elif args.command == 'rate-test':
from odrive.utils import rate_test
print("Waiting for ODrive...")
my_odrive = odrive.find_any(path=args.path, serial_number=args.serial_number)
my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
rate_test(my_odrive)
elif args.command == 'udev-setup':
from odrive.utils import setup_udev_rules
setup_udev_rules(logger)
elif args.command == 'generate-code':
from odrive.code_generator import generate_code
my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number,
channel_termination_token=app_shutdown_token)
generate_code(my_odrive, args.template, args.output)
elif args.command == 'backup-config':
from odrive.configuration import backup_config
print("Waiting for ODrive...")
my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
backup_config(my_odrive, args.file, logger)
elif args.command == 'restore-config':
from odrive.configuration import restore_config
print("Waiting for ODrive...")
my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number,
search_cancellation_token=app_shutdown_token,
channel_termination_token=app_shutdown_token)
restore_config(my_odrive, args.file, logger)
else:
raise Exception("unknown command: " + args.command)
except OperationAbortedException:
logger.info("Operation aborted.")
finally:
app_shutdown_token.set()
+9 -3
View File
@@ -87,6 +87,12 @@ else:
all_tests.append(TestDiscoverAndGotoIdle())
all_tests.append(TestEncoderOffsetCalibration(pass_if_ready=True))
all_tests.append(TestAsciiProtocol())
all_tests.append(TestSensorlessControl())
#all_tests.append(TestStepDirInput())
#all_tests.append(TestPWMInput())
if test_rig_yaml['type'] == 'parallel':
#all_tests.append(TestHighVelocity())
all_tests.append(TestHighVelocityInViscousFluid(load_current=35, driver_current=45))
@@ -136,7 +142,7 @@ try:
if isinstance(test, ODriveTest):
def odrv_test_thread(odrv_name):
odrv_ctx = odrives_by_name[odrv_name]
logger.info('* running {} on {}...'.format(type(test).__name__, odrv_name))
logger.notify('* running {} on {}...'.format(type(test).__name__, odrv_name))
try:
test.check_preconditions(odrv_ctx,
logger.indent(' {}: '.format(odrv_name)))
@@ -165,7 +171,7 @@ try:
try:
if not app_shutdown_token.is_set():
# Run test on this axis
logger.info('* running {} on {}...'.format(type(test).__name__, axis_name))
logger.notify('* running {} on {}...'.format(type(test).__name__, axis_name))
try:
test.check_preconditions(axis_ctx,
logger.indent(' {}: '.format(axis_name)))
@@ -197,7 +203,7 @@ try:
try:
if not app_shutdown_token.is_set():
# Run test on this axis
logger.info('* running {} on {}...'.format(type(test).__name__, coupling_name))
logger.notify('* running {} on {}...'.format(type(test).__name__, coupling_name))
try:
test.check_preconditions(coupled_axes[0], coupled_axes[1],
logger.indent(' {}: '.format(coupling_name)))