mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-09-25 02:47:27 +08:00
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:
@@ -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)
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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]))
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user