From 3aea04723dabc5bf14a712cbc8c45b68096a20cb Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 9 Mar 2018 18:05:44 -0800 Subject: [PATCH 01/32] Update README.md --- Firmware/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/README.md b/Firmware/README.md index fac880aa..afca26ce 100644 --- a/Firmware/README.md +++ b/Firmware/README.md @@ -75,7 +75,7 @@ You must set: * `motor_type`: This is the type of motor being used. Currently two types of motors are supported -- High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). ### Motor Modes -The firwmare currently supports two different types of motors, high-current motors, and Gimbal motors. If you're using a regular hobby brushless motor like [this](https://hobbyking.com/en_us/turnigy-aerodrive-sk3-5065-236kv-brushless-outrunner-motor.html) one, you should set `motor_mode` to `MOTOR_TYPE_HIGH_CURRENT`. For high-torque gimbal motors like [this](https://hobbyking.com/en_us/turnigy-hd-5208-brushless-gimbal-motor-bldc.html) one, you should choose `MOTOR_TYPE_GIMBAL`. +If you're using a regular hobby brushless motor like [this](https://hobbyking.com/en_us/turnigy-aerodrive-sk3-5065-236kv-brushless-outrunner-motor.html) one, you should set `motor_mode` to `MOTOR_TYPE_HIGH_CURRENT`. For low-current gimbal motors like [this](https://hobbyking.com/en_us/turnigy-hd-5208-brushless-gimbal-motor-bldc.html) one, you should choose `MOTOR_TYPE_GIMBAL`. Do not use `MOTOR_TYPE_GIMBAL` on a motor that is not a gimbal motor, as it may overheat the motor or the ODrive. **Further detail:** From 2b2ed19ab26f7265fc28cc39db575073ff3c79ce Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 16 Mar 2018 17:56:19 -0700 Subject: [PATCH 02/32] move TIM_TIME_BASE to common region of main.h --- Firmware/Board/v3.3/Inc/main.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firmware/Board/v3.3/Inc/main.h b/Firmware/Board/v3.3/Inc/main.h index 75bd2260..b7dbdb53 100644 --- a/Firmware/Board/v3.3/Inc/main.h +++ b/Firmware/Board/v3.3/Inc/main.h @@ -69,7 +69,6 @@ #define TIM_APB1_CLOCK_HZ 84000000 #define TIM_APB1_PERIOD_CLOCKS 4096 #define TIM_APB1_DEADTIME_CLOCKS 40 -#define TIM_TIME_BASE TIM14 #define M0_nCS_Pin GPIO_PIN_13 @@ -157,6 +156,8 @@ /* USER CODE BEGIN Private defines */ #endif +#define TIM_TIME_BASE TIM14 + #define CURRENT_MEAS_PERIOD ((float)(2*TIM_1_8_PERIOD_CLOCKS)/(float)TIM_1_8_CLOCK_HZ) #define CURRENT_MEAS_HZ (TIM_1_8_CLOCK_HZ/(2*TIM_1_8_PERIOD_CLOCKS)) From 153b904732893b52692d19f4e58b16d7e6b2b99c Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 23 Mar 2018 17:09:04 -0700 Subject: [PATCH 03/32] robustify python USB discovery/communication --- tools/odrive/usbbulk_transport.py | 8 +++++++- tools/odrive/utils.py | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tools/odrive/usbbulk_transport.py b/tools/odrive/usbbulk_transport.py index f513789f..5eecbd24 100644 --- a/tools/odrive/usbbulk_transport.py +++ b/tools/odrive/usbbulk_transport.py @@ -35,6 +35,12 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink) return string def init(self): + # Under some conditions, the Linux USB/libusb stack ends up in a corrupt + # state where there are a few packets in a receive queue but a call + # to epr.read() does not return these packet until a new packet arrives. + # This undesirable queue can be cleared by resetting the device. + self.dev.reset() + try: if self.dev.is_kernel_driver_active(1): self.dev.detach_kernel_driver(1) @@ -106,7 +112,7 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink) else: # Try resetting halt/stall condition try: - self.epw.clear_halt() + self.epr.clear_halt() except usb.core.USBError: raise odrive.protocol.ChannelBrokenException() # Retry transfer diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index be19adcd..a8c0e680 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -73,11 +73,15 @@ class Event(): """ Alternative to threading.Event(), enhanced by the subscribe() function that the original fails to provide. + @param Trigger: if supplied, the newly created event will be triggered + as soon as the trigger event becomes set """ - def __init__(self): + def __init__(self, trigger=None): self._evt = threading.Event() self._subscribers = [] self._mutex = threading.Lock() + if not trigger is None: + trigger.subscribe(self.set()) def is_set(self): return self._evt.is_set() @@ -120,7 +124,18 @@ class Event(): self._mutex.release() def wait(self, timeout=None): - return self._evt.wait(timeout=timeout) + if not self._evt.wait(timeout=timeout): + raise TimeoutError() + + def trigger_after(self, timeout): + """ + Triggers the event after the specified timeout. + This function returns immediately. + """ + def delayed_trigger(): + if not self.wait(timeout=timeout): + self.set() + threading.Thread(target=delayed_trigger, daemon=True).start() def wait_any(*events, timeout=None): """ From 3373ce29a8a0d9f2cd3476c2d7370212634cdc5c Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 23 Mar 2018 19:50:35 -0700 Subject: [PATCH 04/32] more USB and discovery fixes --- tools/odrive/discovery.py | 24 ++++++++++++------------ tools/odrive/usbbulk_transport.py | 6 ++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tools/odrive/discovery.py b/tools/odrive/discovery.py index f31f0a2d..18b5107a 100644 --- a/tools/odrive/discovery.py +++ b/tools/odrive/discovery.py @@ -12,6 +12,7 @@ import odrive.utils import odrive.remote_object import odrive.usbbulk_transport import odrive.serial_transport +from odrive.utils import Event channel_types = { "usb": odrive.usbbulk_transport.discover_channels, @@ -59,7 +60,7 @@ def find_all(path, serial_number, 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]" + device_serial_number = format(obj.serial_number, 'x').upper() 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 @@ -78,19 +79,18 @@ def find_all(path, serial_number, raise Exception("Invalid path spec \"{}\"".format(search_spec)) -def find_any(path="usb", serial_number=None, printer=noprint): +def find_any(path="usb", serial_number=None, cancellation_token=None, timeout=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() + result = [ None ] + done_signal = Event(cancellation_token) def did_discover_object(obj): - global result - result = obj + result[0] = 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 + find_all(path, serial_number, did_discover_object, done_signal, printer) + try: + done_signal.wait(timeout=timeout) + finally: + done_signal.set() # terminate find_all + return result[0] diff --git a/tools/odrive/usbbulk_transport.py b/tools/odrive/usbbulk_transport.py index 5eecbd24..edf057e2 100644 --- a/tools/odrive/usbbulk_transport.py +++ b/tools/odrive/usbbulk_transport.py @@ -87,7 +87,10 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink) except usb.core.USBError as ex: if ex.errno == 19: # "no such device" raise odrive.protocol.ChannelBrokenException() + elif ex.errno == 110: # timeout + raise odrive.utils.TimeoutException() else: + self._printer("halt condition: {}".format(ex.errno)) # Try resetting halt/stall condition try: self.epw.clear_halt() @@ -109,7 +112,10 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink) except usb.core.USBError as ex: if ex.errno == 19: # "no such device" raise odrive.protocol.ChannelBrokenException() + elif ex.errno == 110: # timeout + raise odrive.utils.TimeoutException() else: + self._printer("halt condition: {}".format(ex.errno)) # Try resetting halt/stall condition try: self.epr.clear_halt() From ef52687a377a2d03c30d4fcc5d296eaab3252933 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sun, 25 Mar 2018 15:57:50 -0700 Subject: [PATCH 05/32] make fancy terminal features work on Windows Add Windows support for the following terminal features: - colored output - output on the second last line On Unix systems, VT100 escape codes are used to achieve this, functionality however Windows <10 doesn't interpret VT100 escape codes. For normal colored output, we use the colorama module to abstract this away. To print text on the second-last line we call the appropriate Win32 API functions directly (using the win32console module). --- tools/explore_odrive.py | 31 ++------- tools/odrive/serial_transport.py | 2 +- tools/odrive/usbbulk_transport.py | 8 ++- tools/odrive/utils.py | 112 ++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 28 deletions(-) diff --git a/tools/explore_odrive.py b/tools/explore_odrive.py index ccc252d4..66884d0f 100755 --- a/tools/explore_odrive.py +++ b/tools/explore_odrive.py @@ -8,7 +8,7 @@ import sys import platform import threading import odrive.discovery -from odrive.utils import start_liveplotter +from odrive.utils import start_liveplotter, Logger # Flush stdout by default import functools @@ -63,27 +63,7 @@ else: ## 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) +logger = Logger() def print_banner(): print('ODrive control utility v0.4') @@ -135,7 +115,7 @@ def did_discover_device(odrive): # 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) + 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)) @@ -146,7 +126,7 @@ def did_lose_device(interactive_name): a message. """ if not app_shutdown_token.is_set(): - print_on_second_last_line(COLOR_RED + "Oh no {} disappeared".format(interactive_name) + COLOR_RESET) + logger.warn("Oh no {} disappeared".format(interactive_name)) # Connect to device printer("Waiting for device...") @@ -183,7 +163,7 @@ else: # Enable tab complete if possible try: import rlcompleter - import readline + import readline # Works only on Unix readline.parse_and_bind("tab: complete") except: sudo_prefix = "" if platform.system() == "Windows" else "sudo " @@ -206,5 +186,6 @@ console.runcode('sys.excepthook=newexcepthook') # Launch shell print_banner() +logger._skip_bottom_line = True interact() app_shutdown_token.set() diff --git a/tools/odrive/serial_transport.py b/tools/odrive/serial_transport.py index 53ca6850..dd595c6d 100644 --- a/tools/odrive/serial_transport.py +++ b/tools/odrive/serial_transport.py @@ -89,7 +89,7 @@ def discover_channels(path, serial_number, callback, cancellation_token, printer input_stream, output_stream, printer) channel.serial_device = serial_device except serial.serialutil.SerialException: - printer("Serial device init failed. Ignoring this port") + printer("Serial device init failed. Ignoring this port. More info: " + traceback.format_exc()) known_devices.append(port_name) else: known_devices.append(port_name) diff --git a/tools/odrive/usbbulk_transport.py b/tools/odrive/usbbulk_transport.py index edf057e2..e1004ba8 100644 --- a/tools/odrive/usbbulk_transport.py +++ b/tools/odrive/usbbulk_transport.py @@ -6,6 +6,8 @@ import time import usb.core import usb.util import odrive.protocol +import traceback +import platform ODRIVE_VID_PID_PAIRS = [ (0x1209, 0x0D31), @@ -39,7 +41,9 @@ class USBBulkTransport(odrive.protocol.PacketSource, odrive.protocol.PacketSink) # state where there are a few packets in a receive queue but a call # to epr.read() does not return these packet until a new packet arrives. # This undesirable queue can be cleared by resetting the device. - self.dev.reset() + # On windows this would cause file-not-found errors in subsequent dev calls + if platform.system() != 'Windows': + self.dev.reset() try: if self.dev.is_kernel_driver_active(1): @@ -185,7 +189,7 @@ def discover_channels(path, serial_number, callback, cancellation_token, printer usb_device.reset() continue else: - printer("USB device init failed. Ignoring this device") + printer("USB device init failed. Ignoring this device. More info: " + traceback.format_exc()) known_devices.append((usb_device.bus, usb_device.address)) else: known_devices.append((usb_device.bus, usb_device.address)) diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index a8c0e680..19cde1a8 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -6,6 +6,16 @@ Liveplotter import sys import time import threading +import platform + +try: + if platform.system() == 'Windows': + import win32console + import colorama + colorama.init() +except ModuleNotFoundError: + print("Could not init terminal colors") + pass data_rate = 100 plot_rate = 10 @@ -154,3 +164,105 @@ def wait_any(*events, timeout=None): if events[i].is_set(): return i raise TimeoutException() + +class Logger(): + """ + Logs messages to stdout + """ + + COLOR_DEFAULT = 0 + COLOR_GREEN = 1 + COLOR_CYAN = 2 + COLOR_YELLOW = 3 + COLOR_RED = 4 + + _VT100Colors = { + COLOR_GREEN: '\x1b[92;1m', + COLOR_CYAN: '\x1b[96;1m', + COLOR_YELLOW: '\x1b[93;1m', + COLOR_RED: '\x1b[91;1m', + COLOR_DEFAULT: '\x1b[0m' + } + + _Win32Colors = { + COLOR_GREEN: 0x0A, + COLOR_CYAN: 0x0B, + COLOR_YELLOW: 0x0E, + COLOR_RED: 0x0C, + COLOR_DEFAULT: 0x07 + } + + def __init__(self): + self._prefix = '' + self._skip_bottom_line = False # If true, messages are printed one line above the cursor + if platform.system() == 'Windows': + self._stdout_buf = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE) + + def indent(self, prefix=' '): + indented_logger = Logger() + indented_logger._prefix = self._prefix + prefix + return indented_logger + + def print_on_second_last_line(self, text, color): + """ + Prints a text on the second last line. + This can be used to print a message above the command + prompt. If the command prompt spans multiple lines + there will be glitches. + If the printed text spans multiple lines there will also + be glitches (though this could be fixed). + """ + + if platform.system() == 'Windows': + # Windows <10 doesn't understand VT100 escape codes and the colorama + # also doesn't support the specific escape codes we need so we use the + # native Win32 API. + info = self._stdout_buf.GetConsoleScreenBufferInfo() + cursor_pos = info['CursorPosition'] + scroll_rect=win32console.PySMALL_RECTType( + Left=0, Top=1, + Right=info['Window'].Right, + Bottom=cursor_pos.Y-1) + scroll_dest = win32console.PyCOORDType(scroll_rect.Left, scroll_rect.Top-1) + self._stdout_buf.ScrollConsoleScreenBuffer( + scroll_rect, scroll_rect, scroll_dest, # clipping rect is same as scroll rect + u' ', Logger._Win32Colors[color]) # fill with empty cells with the desired color attributes + line_start = win32console.PyCOORDType(0, cursor_pos.Y-1) + self._stdout_buf.WriteConsoleOutputCharacter(text, line_start) + + else: + # Assume we're in a terminal that interprets VT100 escape codes. + # TODO: test on macOS + + # Escape character sequence: + # ESC 7: store cursor position + # ESC 1A: move cursor up by one + # ESC 1S: scroll entire viewport by one + # ESC 1L: insert 1 line at cursor position + # (print text) + # ESC 8: restore old cursor position + + sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L', end='', flush=True) + sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT]) + sys.stdout.write('\x1b8', end='', flush=True) + sys.stdout.flush() + + def print_colored(self, text, color): + if self._skip_bottom_line: + self.print_on_second_last_line(text, color) + else: + # On Windows, colorama does the job of interpreting the VT100 escape sequences + sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT] + '\n') + sys.stdout.flush() + + def debug(self, text): + self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT) + def success(self, text): + self.print_colored(self._prefix + text, Logger.COLOR_GREEN) + def info(self, text): + self.print_colored(self._prefix + text, Logger.COLOR_CYAN) + def warn(self, text): + self.print_colored(self._prefix + text, Logger.COLOR_YELLOW) + def error(self, text): + # TODO: write to stderr + self.print_colored(self._prefix + text, Logger.COLOR_RED) From b894ba37f2c1f59f3bb03523f2950a1658e56f62 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sun, 25 Mar 2018 17:35:41 -0700 Subject: [PATCH 06/32] fix Unix print-on-second-last-line --- tools/odrive/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index 19cde1a8..e65a5dbb 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -242,9 +242,9 @@ class Logger(): # (print text) # ESC 8: restore old cursor position - sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L', end='', flush=True) + sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L') sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT]) - sys.stdout.write('\x1b8', end='', flush=True) + sys.stdout.write('\x1b8') sys.stdout.flush() def print_colored(self, text, color): From c0d7d816fc8fc6d40224d4048951f29ece3883b1 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sun, 25 Mar 2018 18:25:27 -0700 Subject: [PATCH 07/32] add github API key for travis --- .travis.yml | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index b541bbd6..bc6468e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ # adapted from https://github.com/andysworkshop/stm32plus/blob/master/.travis.yml branches: - only: - - master - - devel + only: + - master + - devel language: c sudo: false @@ -15,39 +15,40 @@ addons: cache: directories: - - $HOME/dl + - "$HOME/dl" install: - - export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4 - - export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 - - export GCC_URL=https://launchpad.net/gcc-arm-embedded/5.0/5-2015-q4-major/+download/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 - - if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi - - export PATH=$PATH:$GCC_DIR/bin - - export TUP_DIR=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64 - - export TUP_ARCHIVE=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64.deb - - export TUP_URL=http://ppa.launchpad.net/jonathonf/tup/ubuntu/pool/main/t/tup/tup_0.7.5-0~16.04.york0_amd64.deb - - if [ ! -e $TUP_DIR/bin/tup ]; then wget $TUP_URL -O $TUP_ARCHIVE; dpkg-deb -R $TUP_ARCHIVE $TUP_DIR; fi - - export PATH=$PATH:$TUP_DIR/usr/bin +- export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4 +- export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 +- export GCC_URL=https://launchpad.net/gcc-arm-embedded/5.0/5-2015-q4-major/+download/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 +- if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi +- export PATH=$PATH:$GCC_DIR/bin +- export TUP_DIR=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64 +- export TUP_ARCHIVE=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64.deb +- export TUP_URL=http://ppa.launchpad.net/jonathonf/tup/ubuntu/pool/main/t/tup/tup_0.7.5-0~16.04.york0_amd64.deb +- if [ ! -e $TUP_DIR/bin/tup ]; then wget $TUP_URL -O $TUP_ARCHIVE; dpkg-deb -R $TUP_ARCHIVE $TUP_DIR; fi +- export PATH=$PATH:$TUP_DIR/usr/bin env: # Build default configuration for each board - - "CONFIG_BOARD_VERSION=v3.2 DEPLOY=v3.2" - - "CONFIG_BOARD_VERSION=v3.3 DEPLOY=v3.3" - - "CONFIG_BOARD_VERSION=v3.4-24V DEPLOY=v3.4-24V" - - "CONFIG_BOARD_VERSION=v3.4-48V DEPLOY=v3.4-48V" + - CONFIG_BOARD_VERSION=v3.2 DEPLOY=v3.2 + - CONFIG_BOARD_VERSION=v3.3 DEPLOY=v3.3 + - CONFIG_BOARD_VERSION=v3.4-24V DEPLOY=v3.4-24V + - CONFIG_BOARD_VERSION=v3.4-48V DEPLOY=v3.4-48V - # Various protocol combinations - - "CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native-stream CONFIG_UART_PROTOCOL=native" - - "CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none" + # Various protocol combinations + - CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native-stream CONFIG_UART_PROTOCOL=native + - CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none script: - - ./tools/build.sh +- "./tools/build.sh" deploy: provider: releases - api_key: "GITHUB OAUTH TOKEN" + api_key: + secure: RM66joGTn11Z5PmG7Nlj8cRcVY0w7ga5qUl2ahinbDm6jMV8OxroaXKivEa478alx9fygMHVeKdJZUlrAkRJ5OYJ1trfpW+43S3OYEnfy1nyXEXRwhgeIlb9LqdrumXVAp7TZ0Vppfom8A2ZWbxKxW3lG/EAmA4G9fnxHf0S9rF0y95YVfGrxdapTKcxvbP7Yojo53474ZI6+VYrqx8lq0JAnn4FwNT9ZJ1QASrmIw4w08f60XXv25BzndCTscvLb2qUu0AaGLbQUosde0Bb7P+aQsBVY6uSkg9MWV8gWPQjtO3u5IRR1bTshxf2kPqtzwK+SpcYrddoGN6BkKAB3lVorJIW5VguUkRmtPZ1K9+NhIztNevB2qr0ASutumNLF3aqMt19KL3A+SRx6froj5VhRHf4i/Xjm3SDLTaTcc8ZIh2PEE6scUMUMs5Mzu8LQWjInRe25MSb+pQB1mNOHmoFBtVb0J3u7Nvs8jdImN5gQWvvowWXfXRNE0ncT1YsLmevwi3q+YdEjpAIPnrD/rouY8WaqQZ/vE15JM9uwdQRqKAbzGtMaKHDk7EZ7ANTyaP+UrQ/M5cVDa0bWsWSvqSqDJMy4IVHRlirYA/5u74lXNhmA8DGDB/gFVlVCmoEzas/pnYiAE1hh4RpsYxts78Ix+wbeo1hmt7t65X8cyo= file: Firmware/deploy/* - skip_cleanup: true on: + repo: madcowswe/ODrive branch: master tags: true From 937953688c759c9ab36bf6047f6aef5e57cfed58 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sun, 25 Mar 2018 16:13:46 -0700 Subject: [PATCH 08/32] don't fail if build dir doesn't exist --- tools/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build.sh b/tools/build.sh index 7591b0e5..a953d480 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -9,8 +9,8 @@ THIS_DIR="$(dirname "$0")" cd "$THIS_DIR/../Firmware" # Write all environment variables that start with "CONFIG_" to tup.config +rm -rdf build mkdir -p build -rm build/* env | grep ^CONFIG > tup.config tup generate ./tup_build.sh bash -xe ./tup_build.sh From 21437eaf599b083452850455887bb2098df2b7e3 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sun, 25 Mar 2018 18:46:48 -0700 Subject: [PATCH 09/32] amend changelog --- Firmware/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 1d7dbd71..813cc4b8 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -5,6 +5,7 @@ Please add a note of your changes below this heading if you make a Pull Request. * **Storing of configuration parameters to Non Volatile Memory** * **USB Bootloader** * `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `my_odrive.erase_configuration()`) +* Travis-CI builds firmware for all board versions and deploys the binaries when a tag is pushed to master ### Changed * The build is now configured using the `tup.config` file instead of editing source files. Make sure you set your board version correctly. See [here](README.md#configuring-the-build) for details. From 603ef50ea46ef7e4131969168bc9222cabf01276 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 13:12:20 -0700 Subject: [PATCH 10/32] move build.sh to Firmware --- .travis.yml | 2 +- {tools => Firmware}/build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {tools => Firmware}/build.sh (95%) diff --git a/.travis.yml b/.travis.yml index bc6468e4..669c98c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ env: - CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none script: -- "./tools/build.sh" +- "./Firmware/build.sh" deploy: provider: releases diff --git a/tools/build.sh b/Firmware/build.sh similarity index 95% rename from tools/build.sh rename to Firmware/build.sh index a953d480..26718282 100755 --- a/tools/build.sh +++ b/Firmware/build.sh @@ -6,7 +6,7 @@ set -euo pipefail THIS_DIR="$(dirname "$0")" -cd "$THIS_DIR/../Firmware" +cd "$THIS_DIR" # Write all environment variables that start with "CONFIG_" to tup.config rm -rdf build From 2d251b3ca60fc029d74c4cd0fef9d18934ccd038 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 14:55:54 -0700 Subject: [PATCH 11/32] register FreeRTOS heap change in CubeMX When we changed the FreeRTOS heap to reside in the CCM, we didn't update the CubeMX settings. This means CubeMX would revert the change. This commit fixes this. --- Firmware/Board/v3/Inc/FreeRTOSConfig.h | 3 +-- Firmware/Board/v3/Inc/main.h | 1 + Firmware/Board/v3/Odrive.ioc | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Firmware/Board/v3/Inc/FreeRTOSConfig.h b/Firmware/Board/v3/Inc/FreeRTOSConfig.h index 93f9b39d..b31807fa 100644 --- a/Firmware/Board/v3/Inc/FreeRTOSConfig.h +++ b/Firmware/Board/v3/Inc/FreeRTOSConfig.h @@ -102,8 +102,7 @@ #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES ( 7 ) #define configMINIMAL_STACK_SIZE ((uint16_t)128) -#define configTOTAL_HEAP_SIZE ((size_t)65536) // FreeRTOS heap takes up the entire 64kB core-coupled memory -#define configAPPLICATION_ALLOCATED_HEAP 1 +#define configTOTAL_HEAP_SIZE ((size_t)65536) #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_16_BIT_TICKS 0 #define configUSE_MUTEXES 1 diff --git a/Firmware/Board/v3/Inc/main.h b/Firmware/Board/v3/Inc/main.h index 50256101..ac9bd857 100644 --- a/Firmware/Board/v3/Inc/main.h +++ b/Firmware/Board/v3/Inc/main.h @@ -68,6 +68,7 @@ #define TIM_APB1_CLOCK_HZ 84000000 #define TIM_APB1_PERIOD_CLOCKS 4096 #define TIM_APB1_DEADTIME_CLOCKS 40 +#define configAPPLICATION_ALLOCATED_HEAP 1 #define M0_nCS_Pin GPIO_PIN_13 #define M0_nCS_GPIO_Port GPIOC diff --git a/Firmware/Board/v3/Odrive.ioc b/Firmware/Board/v3/Odrive.ioc index b27ffb67..aea71bef 100644 --- a/Firmware/Board/v3/Odrive.ioc +++ b/Firmware/Board/v3/Odrive.ioc @@ -101,9 +101,11 @@ Dma.UART4_TX.1.PeriphDataAlignment=DMA_PDATAALIGN_BYTE Dma.UART4_TX.1.PeriphInc=DMA_PINC_DISABLE Dma.UART4_TX.1.Priority=DMA_PRIORITY_LOW Dma.UART4_TX.1.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode +FREERTOS.FootprintOK=true FREERTOS.INCLUDE_vTaskDelayUntil=1 -FREERTOS.IPParameters=Tasks01,INCLUDE_vTaskDelayUntil +FREERTOS.IPParameters=Tasks01,INCLUDE_vTaskDelayUntil,configTOTAL_HEAP_SIZE,FootprintOK FREERTOS.Tasks01=defaultTask,-3,256,StartDefaultTask,Default +FREERTOS.configTOTAL_HEAP_SIZE=65536 File.Version=6 KeepUserPlacement=true Mcu.Family=STM32F4 @@ -186,7 +188,7 @@ Mcu.Pin8=PC3 Mcu.Pin9=PA0-WKUP Mcu.PinsNb=56 Mcu.ThirdPartyNb=0 -Mcu.UserConstants=TIM_1_8_CLOCK_HZ,168000000;TIM_1_8_PERIOD_CLOCKS,10192;TIM_1_8_DEADTIME_CLOCKS,20;TIM_APB1_CLOCK_HZ,84000000;TIM_APB1_PERIOD_CLOCKS,4096;TIM_APB1_DEADTIME_CLOCKS,40 +Mcu.UserConstants=TIM_1_8_CLOCK_HZ,168000000;TIM_1_8_PERIOD_CLOCKS,10192;TIM_1_8_DEADTIME_CLOCKS,20;TIM_APB1_CLOCK_HZ,84000000;TIM_APB1_PERIOD_CLOCKS,4096;TIM_APB1_DEADTIME_CLOCKS,40;configAPPLICATION_ALLOCATED_HEAP,1 Mcu.UserName=STM32F405RGTx MxCube.Version=4.24.0 MxDb.Version=DB.4.0.240 From 43aa805fd349371f43c2c2add5b48c44686793bd Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 23:07:34 -0700 Subject: [PATCH 12/32] amend release notes --- Firmware/CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 813cc4b8..1dacb0c1 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -1,6 +1,14 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. +### Added +### Changed +### Fixed + +# Releases + +## [0.3.6] - 2018-03-26 + ### Added * **Storing of configuration parameters to Non Volatile Memory** * **USB Bootloader** @@ -17,8 +25,6 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Fixed * malloc now fails if we run out of memory (before it would always succeed even if we are out of ram...) -# Releases - ## [0.3.5] - 2018-03-04 ### Added From e4fb0016e5b61c0e48c9b8470e933f1b92c247a2 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 23:43:55 -0700 Subject: [PATCH 13/32] fix travis deploy --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 669c98c8..35932eb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ branches: only: - master - devel + - /^fw-v/ language: c sudo: false From 3593a6812859446ddc62431060350d2e7b60f3fc Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 23:53:22 -0700 Subject: [PATCH 14/32] add skip_cleanup and file_glob to .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 35932eb7..d530f0a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,6 +48,8 @@ deploy: provider: releases api_key: secure: RM66joGTn11Z5PmG7Nlj8cRcVY0w7ga5qUl2ahinbDm6jMV8OxroaXKivEa478alx9fygMHVeKdJZUlrAkRJ5OYJ1trfpW+43S3OYEnfy1nyXEXRwhgeIlb9LqdrumXVAp7TZ0Vppfom8A2ZWbxKxW3lG/EAmA4G9fnxHf0S9rF0y95YVfGrxdapTKcxvbP7Yojo53474ZI6+VYrqx8lq0JAnn4FwNT9ZJ1QASrmIw4w08f60XXv25BzndCTscvLb2qUu0AaGLbQUosde0Bb7P+aQsBVY6uSkg9MWV8gWPQjtO3u5IRR1bTshxf2kPqtzwK+SpcYrddoGN6BkKAB3lVorJIW5VguUkRmtPZ1K9+NhIztNevB2qr0ASutumNLF3aqMt19KL3A+SRx6froj5VhRHf4i/Xjm3SDLTaTcc8ZIh2PEE6scUMUMs5Mzu8LQWjInRe25MSb+pQB1mNOHmoFBtVb0J3u7Nvs8jdImN5gQWvvowWXfXRNE0ncT1YsLmevwi3q+YdEjpAIPnrD/rouY8WaqQZ/vE15JM9uwdQRqKAbzGtMaKHDk7EZ7ANTyaP+UrQ/M5cVDa0bWsWSvqSqDJMy4IVHRlirYA/5u74lXNhmA8DGDB/gFVlVCmoEzas/pnYiAE1hh4RpsYxts78Ix+wbeo1hmt7t65X8cyo= + skip_cleanup: true + file_glob: true file: Firmware/deploy/* on: repo: madcowswe/ODrive From a0dc56f2b0da8e9c1c6999ae0cf5ff0484bb2f06 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 27 Mar 2018 13:21:38 -0700 Subject: [PATCH 15/32] change release binary naming convention --- Firmware/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/build.sh b/Firmware/build.sh index 26718282..811a4ab7 100755 --- a/Firmware/build.sh +++ b/Firmware/build.sh @@ -18,6 +18,6 @@ bash -xe ./tup_build.sh # Deploy if ! [ -z ${DEPLOY+x} ]; then mkdir -p deploy - cp build/ODriveFirmware.elf deploy/ODriveFirmware-"$DEPLOY".elf - cp build/ODriveFirmware.hex deploy/ODriveFirmware-"$DEPLOY".hex + cp build/ODriveFirmware.elf deploy/ODriveFirmware_"$DEPLOY".elf + cp build/ODriveFirmware.hex deploy/ODriveFirmware_"$DEPLOY".hex fi From 3b157bfbacfce83cf6d04e2b94742078070bdf01 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 27 Mar 2018 17:37:37 -0700 Subject: [PATCH 16/32] implement verification in DFU script --- tools/dfu.py | 249 ++++++++++++++++++++++++++------------- tools/dfuse/DfuDevice.py | 3 + 2 files changed, 167 insertions(+), 85 deletions(-) diff --git a/tools/dfu.py b/tools/dfu.py index 7ad27bf8..c010aceb 100755 --- a/tools/dfu.py +++ b/tools/dfu.py @@ -9,9 +9,10 @@ import time import threading import platform import struct +import array +import fractions import dfuse import usb.core -import usb.util import odrive.core # We are interactively printing status messages, so flush by default @@ -27,21 +28,26 @@ except: SIZE_MULTIPLIERS = {' ': 1, 'K': 1024, 'M' : 1024*1024} -TRANSFER_SIZE = 2048 +MAX_TRANSFER_SIZE = 2048 -def load_sectors(dfudev, hexfile): +def get_device_sectors(dfudev): """ - Checks for which on-device sectors there is data in the hex file and - returns a sector object for each touched sector. Each sector object - is filled with the associated data from the hex file. + 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 dfudev.alternates(): # example for name: # '@Internal Flash /0x08000000/04*016Kg,01*064Kg,07*128Kg' - label, addr, layout = name.split('/') - addr = int(addr, 0) # convert hex to decimal + 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('*')) @@ -49,93 +55,122 @@ def load_sectors(dfudev, hexfile): mode = sector[-1] while repeat > 0: - # check if any segment from the hexfile overlaps with this sector - touched = False - for (start, end) in hexfile.segments(): - if start < addr and end > addr: - touched = True - break - elif start >= addr and start < addr + size: - touched = True - break - - if touched: - # TODO: verify if the section is writable - yield { - 'alt': alt, - 'addr': addr, - 'data': hexfile.tobinarray(addr, addr + size - 1) - } + # 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 populate_sectors(sectors, hexfile): + """ + Checks for which on-device sectors there is data in the hex file and + returns a (sector, data) tuple for each touched sector where data + is a byte array of the same size as the sector. + """ + for sector in sectors: + addr = sector['addr'] + size = sector['len'] + # check if any segment from the hexfile overlaps with this sector + touched = False + for (start, end) in hexfile.segments(): + if start < addr and end > addr: + touched = True + break + elif start >= addr and start < addr + size: + touched = True + break + + if touched: + # TODO: verify if the section is writable + yield (sector, hexfile.tobinarray(addr, addr + size - 1)) + def set_alternate_safe(dfudev, alt): dfudev.set_alternate(alt) if dfudev.get_state() == dfuse.DfuState.DFU_ERROR: dfudev.clear_status() dfudev.wait_while_state(dfuse.DfuState.DFU_ERROR) -def erase(dfudev, sectors): - for i, sector in enumerate(sectors): - print("Erasing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) - set_alternate_safe(dfudev, sector['alt']) - dfudev.erase(sector['addr']) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY, timeout=len(sector['data'])/32) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: %r" % status) - print('Erasing... done ') +#def clear_error(dfudev) +def set_address_safe(dfudef, addr): + dfudev.set_address(addr) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + # take device out of DFU_DOWNLOAD_SYNC and into DFU_IDLE + dfudev.abort() + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_SYNC) + if status[1] != dfuse.DfuState.DFU_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + -def flash(dfudev, sectors): - for i, sector in enumerate(sectors): - print("Flashing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) - set_alternate_safe(dfudev, sector['alt']) - dfudev.set_address(sector['addr']) +def erase(dfudev, sector): + set_alternate_safe(dfudev, sector['alt']) + dfudev.erase(sector['addr']) + status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY, timeout=sector['len']/32) + if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + +def flash(dfudev, sector, data): + set_alternate_safe(dfudev, sector['alt']) + set_address_safe(dfudev, 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))) + dfudev.write(blocknum, block) status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: raise RuntimeError("An error occured. Device Status: %r" % status) - - data = sector['data'] - 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))) - dfudev.write(blocknum, block) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: %r" % status) - print('Flashing... done ') +def read(dfudev, sector): + """ + Reads data from the specified sector + Returns: a byte array containing the data + """ + set_alternate_safe(dfudev, sector['alt']) + set_address_safe(dfudev, sector['addr']) -# Results in usb.core.USBError. Probably the device should go to dfuIDLE first, but how? -#def verify(dfudev, sectors): -# for i, sector in enumerate(sectors): -# print("Verifying... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) -# set_alternate_safe(dfudev, sector['alt']) -# dfudev.set_address(sector['addr']) -# status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) -# if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: -# raise RuntimeError("An error occured. Device Status: %r" % status) -# -# print("state: {}".format(dfudev.get_state())) -# #dfudev.clear_status() -# print("state: {}".format(dfudev.get_state())) -# data = sector['data'] -# blocks = [data[i:i + TRANSFER_SIZE] for i in range(0, len(data), TRANSFER_SIZE)] -# for blocknum, block in enumerate(blocks): -# print('read at {:08X}'.format(sector['addr'] + blocknum * TRANSFER_SIZE)) -# deviceBlock = dfudev.read(blocknum, TRANSFER_SIZE) -# print(dfudev.get_state()) -# if (deviceBlock != block): -# raise RuntimeError("verification failed at address {:08X}".format(sector['addr'] + blocknum * TRANSFER_SIZE)) -# print('Verifying... done ') + 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 = dfudev.read(blocknum, transfer_size) + data.extend(deviceBlock) + dfudev.abort() # take device into DFU_IDLE + return data + +def get_first_mismatch_index(array1, array2): + """ + Compares two arrays and returns the index of the + first unequal item or None if both arrays are equal + """ + if len(array1) != len(array2): + raise Exception("arrays must be same size") + for pos in range(len(array1)): + if (array1[pos] != array2[pos]): + return pos + return None def jump_to_application(dfudev, address): - dfudev.set_address(address) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + set_address_safe(dfudev, address) + #dfudev.set_address(address) + #status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) + #if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + # raise RuntimeError("An error occured. Device Status: {}".format(status[1])) dfudev.leave() status = dfudev.wait_while_state(dfuse.DfuState.DFU_MANIFEST_SYNC) @@ -214,6 +249,8 @@ def put_odrive_into_dfu_mode_thread(cancellation_token): parser = argparse.ArgumentParser(description="Program an STM32 in DFU mode. The device can be identified either by it's serial number or UUID." "You can list all connected devices by running" "(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial") +parser.add_argument("-v", "--verbose", action="store_true", + help="print debug information") parser.add_argument('file', metavar='HEX', help='the .hex file to be flashed') parser.add_argument("-u", "--uuid", help="The 12-byte UUID of the device. This is a hexadecimal number of the format" @@ -260,17 +297,59 @@ try: dfudev = dfuse.DfuDevice(stm_device) + sectors = list(get_device_sectors(dfudev)) + + if (args.verbose): + print("Sectors on device: ") + for sector in sectors: + print(" {:08X} to {:08X} ({})".format( + sector['addr'], + sector['addr'] + sector['len'] - 1, + sector['name'])) # fill sectors with data - sectors = list(load_sectors(dfudev, hexfile)) - print("Sectors to be flashed: ") - for sector in sectors: - print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + len(sector['data']) - 1)) + touched_sectors = list(populate_sectors(sectors, hexfile)) + + if (args.verbose): + print("The following sectors will be flashed: ") + for sector,_ in touched_sectors: + print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + sector['len'] - 1)) + + # Erase + try: + for i, (sector, data) in enumerate(touched_sectors): + print("Erasing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + erase(dfudev, sector) + print('Erasing... done \r', end='', flush=True) + finally: + print('', flush=True) + + # Flash + try: + for i, (sector, data) in enumerate(touched_sectors): + print("Flashing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + flash(dfudev, sector, data) + print('Flashing... done \r', end='', flush=True) + finally: + print('', flush=True) + + # Verify + try: + for i, (sector, expected_data) in enumerate(touched_sectors): + print("Verifying... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + observed_data = read(dfudev, sector) + mismatch_pos = get_first_mismatch_index(observed_data, expected_data) + if not mismatch_pos is None: + mismatch_pos -= mismatch_pos % 16 + observed_snippet = ' '.join('{:02X}'.format(x) for x in observed_data[mismatch_pos:mismatch_pos+16]) + expected_snippet = ' '.join('{:02X}'.format(x) for x in expected_data[mismatch_pos:mismatch_pos+16]) + raise RuntimeError("Verification failed around address 0x{:08X}:\n".format(sector['addr'] + mismatch_pos) + + " expected: " + expected_snippet + "\n" + " observed: " + observed_snippet) + print('Verifying... done \r', end='', flush=True) + finally: + print('', flush=True) - # flash! - erase(dfudev, sectors) - flash(dfudev, sectors) - #verify(dfudev, sectors) # If the flash operation failed for some reason, your device is bricked now. # You can unbrick it as long as the device remains powered on. diff --git a/tools/dfuse/DfuDevice.py b/tools/dfuse/DfuDevice.py index 9173dcc4..dc5ac152 100644 --- a/tools/dfuse/DfuDevice.py +++ b/tools/dfuse/DfuDevice.py @@ -59,6 +59,9 @@ class DfuDevice: def get_state(self): return self.control_msg(DFU_REQUEST_RECEIVE, DFU_GETSTATE, 0, 1)[0] + def abort(self): + self.control_msg(DFU_REQUEST_RECEIVE, DFU_ABORT, 0, 0) + def set_address(self, ap): return self.dnload(0x0, [0x21] + address_to_4bytes(ap)) From d78273a5aa37b2c3c3eafa2d0667ffa58bb91aed Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 27 Mar 2018 19:54:02 -0700 Subject: [PATCH 17/32] add write_otp to Makefile --- Firmware/Makefile | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Firmware/Makefile b/Firmware/Makefile index 84a28e30..dcfd74ae 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -28,6 +28,50 @@ bmp: all erase_config: openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c reset\ halt -c flash\ erase_address\ 0x80C0000\ 0x40000 -c reset\ run -c exit +# OTP format: +# - OTP format version (0xFE: version 1) +# - vendor ID (01: ODrive Robotics - do not use this on custom incompatible hardware!) +# - product ID (01: ODrive) +# - hardware major version +# - hardware minor version +# - hardware variant (00: 24V, 01: 48V) +# Bits in the OTP can only ever be set to 0 but never back to 1. +# Therefore do not try to run this on the same board twice with different data. +# +# This OpenOCD command is intended for a STM32F405 and does the following: +# FLASH_KEYR = 0x45670123; // unlock FLASH_CR +# FLASH_KEYR = 0xCDEF89AB; // unlock FLASH_CR +# FLASH_CR = (1 << FLASH_CR_PG); // unlock flash memory +# [write OTP] +write_otp: +ifeq ($(ODRV_FACTORY),TRUE) + # Data: + openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg \ + -c init \ + -c 'reset halt' \ + -c 'mww 0x40023C04 0x45670123' \ + -c 'mww 0x40023C04 0xCDEF89AB' \ + -c 'mww 0x40023C10 0x00000001' -c 'sleep 10' \ + -c 'mwb 0x1fff7800 0xFE' -c 'sleep 10' \ + -c 'mwb 0x1fff7801 0x01' -c 'sleep 10' \ + -c 'mwb 0x1fff7802 0x01' -c 'sleep 10' \ + -c 'mwb 0x1fff7803 0x03' -c 'sleep 10' \ + -c 'mwb 0x1fff7804 0x04' -c 'sleep 10' \ + -c 'mwb 0x1fff7805 0x01' -c 'sleep 10' \ + -c 'reset run' \ + -c exit + +else + @echo "The one-time programmable memory can only be" + @echo "written ONCE on every board (what a surprise)." + @echo "If you're on an ODrive v3.5 or later we already did this for you." + @echo "Otherwise, if you're mentally ready for this, do the following steps:" + @echo " 1. open the Makefile and look at the write_otp target" + @echo " 2. understand the structure of the OTP" + @echo " 3. edit the bytes that are written to match your board version" + @echo "Run this command again, this time with ODRV_FACTORY=TRUE" +endif + clean: -rm -fR .dep $(BUILD_DIR) From 29c153672d8c3c84b14aea6a87109c57d9f0817a Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 27 Mar 2018 20:17:14 -0700 Subject: [PATCH 18/32] expose board version on USB protocol --- Firmware/MotorControl/commands.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Firmware/MotorControl/commands.cpp b/Firmware/MotorControl/commands.cpp index d725afa0..fba72970 100644 --- a/Firmware/MotorControl/commands.cpp +++ b/Firmware/MotorControl/commands.cpp @@ -111,12 +111,30 @@ void enter_dfu_mode() { NVIC_SystemReset(); } +#if HW_VERSION_MAJOR == 3 +const uint8_t* otp_ptr = + *(uint8_t*)0x1fff7800 == 0xfe ? (uint8_t*)0x1fff7800 : + *(uint8_t*)0x1fff7800 != 0x00 ? NULL : + *(uint8_t*)0x1fff7810 == 0xfe ? (uint8_t*)0x1fff7810 : NULL; + +// Read hardware version from OTP if available, otherwise fall back +// to software defined version. +const uint8_t board_version_major = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; +const uint8_t board_version_minor = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; +const uint8_t board_version_variant = otp_ptr ? otp_ptr[5] : (HW_VERSION_VOLTAGE == 24 ? 0 : 1); +#else +#error "not implemented" +#endif + // This table specifies which fields and functions are exposed on the USB and UART ports. // TODO: Autogenerate this table. It will come up again very soon in the Arduino library. // clang-format off const Endpoint endpoints[] = { Endpoint::make_property("vbus_voltage", const_cast(&vbus_voltage)), Endpoint::make_property("serial_number", const_cast(&serial_number)), + Endpoint::make_property("board_version_major", &board_version_major), + Endpoint::make_property("board_version_minor", &board_version_minor), + Endpoint::make_property("board_version_variant", &board_version_variant), Endpoint::make_function("run_anticogging_calibration", &motors_run_anticogging_calibration_func), // No parameters, but still requires a close_tree() Endpoint::close_tree(), From 3703ae5558f7cb95d1be51a02754f7231029960e Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 27 Mar 2018 23:05:20 -0700 Subject: [PATCH 19/32] dfu.py: dump OTP when using verbose flag --- tools/dfu.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tools/dfu.py b/tools/dfu.py index c010aceb..786445a1 100755 --- a/tools/dfu.py +++ b/tools/dfu.py @@ -177,6 +177,23 @@ def jump_to_application(dfudev, address): if status[1] != dfuse.DfuState.DFU_MANIFEST: raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + +def dump_otp(): + """ + Dumps the contents of the one-time-programmable + memory. The OTP will be used in future versions of + this script to determine the board version. + """ + # 512 Byte OTP + otp_sector = [s for s in sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0] + data = read(dfudev, otp_sector) + print(' '.join('{:02X}'.format(x) for x in data)) + + # 16 lock bytes + otp_lock_sector = [s for s in sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7A00][0] + data = read(dfudev, otp_lock_sector) + print(' '.join('{:02X}'.format(x) for x in data)) + 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]) @@ -315,6 +332,10 @@ try: for sector,_ in touched_sectors: print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + sector['len'] - 1)) + if (args.verbose): + print("OTP:") + dump_otp() + # Erase try: for i, (sector, data) in enumerate(touched_sectors): From 13aacbfed28a8c609b6254d671acb5481fe98dda Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Wed, 28 Mar 2018 01:01:45 -0700 Subject: [PATCH 20/32] bake Git-derived version into firmware --- Firmware/MotorControl/commands.cpp | 11 +++++++ Firmware/Tupfile.lua | 5 ++++ Firmware/build.lua | 3 +- Firmware/dump_version.sh | 47 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100755 Firmware/dump_version.sh diff --git a/Firmware/MotorControl/commands.cpp b/Firmware/MotorControl/commands.cpp index fba72970..296d97e5 100644 --- a/Firmware/MotorControl/commands.cpp +++ b/Firmware/MotorControl/commands.cpp @@ -13,6 +13,7 @@ #include "freertos_vars.h" #include "utils.h" #include "config.h" +#include "../build/version.h" // autogenerated based on Git state #ifdef ENABLE_LEGACY_PROTOCOL #include "legacy_commands.h" @@ -126,6 +127,12 @@ const uint8_t board_version_variant = otp_ptr ? otp_ptr[5] : (HW_VERSION_VOLTAGE #error "not implemented" #endif +// the corresponding macros are defined in the autogenerated version.h +const uint8_t fw_version_major = FW_VERSION_MAJOR; +const uint8_t fw_version_minor = FW_VERSION_MINOR; +const uint8_t fw_version_revision = FW_VERSION_REVISION; +const uint8_t fw_version_unreleased = FW_VERSION_UNRELEASED; // 0 for official releases, 1 otherwise + // This table specifies which fields and functions are exposed on the USB and UART ports. // TODO: Autogenerate this table. It will come up again very soon in the Arduino library. // clang-format off @@ -135,6 +142,10 @@ const Endpoint endpoints[] = { Endpoint::make_property("board_version_major", &board_version_major), Endpoint::make_property("board_version_minor", &board_version_minor), Endpoint::make_property("board_version_variant", &board_version_variant), + Endpoint::make_property("fw_version_major", &fw_version_major), + Endpoint::make_property("fw_version_minor", &fw_version_minor), + Endpoint::make_property("fw_version_revision", &fw_version_revision), + Endpoint::make_property("fw_version_unreleased", &fw_version_unreleased), Endpoint::make_function("run_anticogging_calibration", &motors_run_anticogging_calibration_func), // No parameters, but still requires a close_tree() Endpoint::close_tree(), diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index c6440db0..26b4308e 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -119,6 +119,11 @@ build{ includes=stm_includes } +tup.frule{ + command='bash dump_version.sh %o', + outputs={'build/version.h'} +} + build{ name='ODriveFirmware', toolchains={toolchain}, diff --git a/Firmware/build.lua b/Firmware/build.lua index ba5ecc30..898b357f 100644 --- a/Firmware/build.lua +++ b/Firmware/build.lua @@ -67,8 +67,9 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) else extra_outputs = {} end + if src == 'MotorControl/commands.cpp' then extra_inputs = 'build/version.h' end -- TODO: fix hack tup.frule{ - inputs= { src }, + inputs= { src, extra_inputs=extra_inputs }, command=compiler..' -c %f '.. tostring(compiler_flags)..' '.. -- CFLAGS for this compiler tostring(inc_flags)..' '.. -- CFLAGS for this translation unit diff --git a/Firmware/dump_version.sh b/Firmware/dump_version.sh new file mode 100755 index 00000000..cdaaa372 --- /dev/null +++ b/Firmware/dump_version.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +if [ $# -eq 1 ]; then + OUTPUT="$1" +else + OUTPUT="/dev/stdout" +fi + +# The git root lies outside of the tup root +export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 + +# Get a description of the current Git state +# Examples of what this string may become: +# fw-v0.3.6 The current commit is exactly at tag "fw-v0.3.6" +# There may or may not be untracked files in the +# working directory. +# fw-v0.3.6* The current commit is at tag "fw-v0.3.6" and there +# are uncommitted changes in the working directory. +# fw-v0.3.6-4-g3703ae5 The working directory at a commit with hash 3703ae5, +# 4 commits ahead of tag fw-v0.3.6 and clean. +FW_VERSION="$(git describe --always --tags --dirty=* || echo "[unknown commit]")" + +# Extract version numbers +FW_VERSION_MAJOR="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\1/p' <<< "$FW_VERSION")" +FW_VERSION_MINOR="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\2/p' <<< "$FW_VERSION")" +FW_VERSION_REVISION="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\3/p' <<< "$FW_VERSION")" +FW_VERSION_SUFFIX="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\4/p' <<< "$FW_VERSION")" + +# Fall back to 0 if the verions does not match the expected pattern +[ "$FW_VERSION_MAJOR" == "" ] && FW_VERSION_MAJOR=0 +[ "$FW_VERSION_MINOR" == "" ] && FW_VERSION_MINOR=0 +[ "$FW_VERSION_REVISION" == "" ] && FW_VERSION_REVISION=0 + +if [ "$FW_VERSION_SUFFIX" == "" ]; then + FW_VERSION_UNRELEASED=0 +else + FW_VERSION_UNRELEASED=1 +fi + +cat > "$OUTPUT" < Date: Wed, 28 Mar 2018 01:02:11 -0700 Subject: [PATCH 21/32] amend changelog --- Firmware/CHANGELOG.md | 5 +++++ Firmware/Makefile | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 1dacb0c1..9b9e3486 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -2,7 +2,12 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Added + * `make write_otp` command to burn the board version onto the ODrive's one-time programmable memory. If you have an ODrive v3.4 or older, you should run this once for a better firmware update user experience in the future. Run the command without any options for more details. Once set, the board version is exposed through the `board_version_[...]` properties. + * bake Git-derived firmware version into firmware binary. The firmware version is exposed through the `fw_version_[...]` properties. + ### Changed + * The DFU script now verifies the flash after writing + ### Fixed # Releases diff --git a/Firmware/Makefile b/Firmware/Makefile index dcfd74ae..f4b505dc 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -28,7 +28,8 @@ bmp: all erase_config: openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c reset\ halt -c flash\ erase_address\ 0x80C0000\ 0x40000 -c reset\ run -c exit -# OTP format: +# The one-time programmable memory stores the board version +# has the following format: # - OTP format version (0xFE: version 1) # - vendor ID (01: ODrive Robotics - do not use this on custom incompatible hardware!) # - product ID (01: ODrive) @@ -36,7 +37,8 @@ erase_config: # - hardware minor version # - hardware variant (00: 24V, 01: 48V) # Bits in the OTP can only ever be set to 0 but never back to 1. -# Therefore do not try to run this on the same board twice with different data. +# Therefore do not try to run this command on the same board +# twice with different data. # # This OpenOCD command is intended for a STM32F405 and does the following: # FLASH_KEYR = 0x45670123; // unlock FLASH_CR @@ -65,7 +67,8 @@ else @echo "The one-time programmable memory can only be" @echo "written ONCE on every board (what a surprise)." @echo "If you're on an ODrive v3.5 or later we already did this for you." - @echo "Otherwise, if you're mentally ready for this, do the following steps:" + @echo "Otherwise, if you're mentally ready for this irreversible action," + @echo "take the following steps:" @echo " 1. open the Makefile and look at the write_otp target" @echo " 2. understand the structure of the OTP" @echo " 3. edit the bytes that are written to match your board version" From a4812f4ed869bb41c1fae254eb6a590dcf8a5cd4 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 29 Mar 2018 14:58:21 -0700 Subject: [PATCH 22/32] change USB PID from 0x0D33 to 0x0D32 The official ODrive PID is 0x0D32 however the firmware wrongly announced PID 0x0D33. This was already fixed earlier but the fix was undone by CubeMX. This commit also fixes the CubeMX setting. --- .../0001-display-correct-ODrive-version-in-USB-descriptor.patch | 2 +- Firmware/Board/v3/Odrive.ioc | 2 +- Firmware/Board/v3/Src/usbd_desc.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch b/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch index f57298b0..0b031242 100644 --- a/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch +++ b/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch @@ -14,7 +14,7 @@ index 94dc49b..37b4302 100644 @@ -96,7 +96,9 @@ #define USBD_LANGID_STRING 1033 #define USBD_MANUFACTURER_STRING "ODrive Robotics" - #define USBD_PID_FS 0x0D33 + #define USBD_PID_FS 0x0D32 -#define USBD_PRODUCT_STRING_FS "ODrive v3.3" +#define USBD_PRODUCT_XSTR(s) USBD_PRODUCT_STR(s) +#define USBD_PRODUCT_STR(s) #s diff --git a/Firmware/Board/v3/Odrive.ioc b/Firmware/Board/v3/Odrive.ioc index aea71bef..583cb7f7 100644 --- a/Firmware/Board/v3/Odrive.ioc +++ b/Firmware/Board/v3/Odrive.ioc @@ -599,7 +599,7 @@ USB_DEVICE.APP_TX_DATA_SIZE-CDC_FS=64 USB_DEVICE.CLASS_NAME_FS=CDC USB_DEVICE.IPParameters=VirtualMode-CDC_FS,VirtualModeFS,CLASS_NAME_FS,MANUFACTURER_STRING-CDC_FS,PRODUCT_STRING_CDC_FS,VID-CDC_FS,PID_CDC_FS,SERIALNUMBER_STRING_CDC_FS,APP_RX_DATA_SIZE-CDC_FS,APP_TX_DATA_SIZE-CDC_FS USB_DEVICE.MANUFACTURER_STRING-CDC_FS=ODrive Robotics -USB_DEVICE.PID_CDC_FS=0x0D33 +USB_DEVICE.PID_CDC_FS=0x0D32 USB_DEVICE.PRODUCT_STRING_CDC_FS=ODrive v3.3 USB_DEVICE.SERIALNUMBER_STRING_CDC_FS=000000000001 USB_DEVICE.VID-CDC_FS=0x1209 diff --git a/Firmware/Board/v3/Src/usbd_desc.c b/Firmware/Board/v3/Src/usbd_desc.c index 37b43026..e812a013 100644 --- a/Firmware/Board/v3/Src/usbd_desc.c +++ b/Firmware/Board/v3/Src/usbd_desc.c @@ -95,7 +95,7 @@ #define USBD_VID 0x1209 #define USBD_LANGID_STRING 1033 #define USBD_MANUFACTURER_STRING "ODrive Robotics" -#define USBD_PID_FS 0x0D33 +#define USBD_PID_FS 0x0D32 #define USBD_PRODUCT_XSTR(s) USBD_PRODUCT_STR(s) #define USBD_PRODUCT_STR(s) #s #define USBD_PRODUCT_STRING_FS ODrive version HW_VERSION_MAJOR.HW_VERSION_MINOR From b2df11a41699efbd1c80018a739fc1bd07f26d9c Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Thu, 29 Mar 2018 23:47:58 -0700 Subject: [PATCH 23/32] Update tup.config.default --- Firmware/tup.config.default | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/tup.config.default b/Firmware/tup.config.default index 740e1dde..be0515fc 100644 --- a/Firmware/tup.config.default +++ b/Firmware/tup.config.default @@ -1,6 +1,6 @@ # Copy this file to tup.config and adapt it to your needs # make sure this fits your board -#CONFIG_BOARD_VERSION=v3.3 +#CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native CONFIG_UART_PROTOCOL=ascii CONFIG_STEP_DIR=n From e5badaf724f309d26606b26e668576834e3e8797 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 12:49:53 -0700 Subject: [PATCH 24/32] update VSCode C++ settings --- Firmware/.vscode/c_cpp_properties.json | 145 +++++++++++++------------ 1 file changed, 74 insertions(+), 71 deletions(-) diff --git a/Firmware/.vscode/c_cpp_properties.json b/Firmware/.vscode/c_cpp_properties.json index b337bc20..88c6c273 100644 --- a/Firmware/.vscode/c_cpp_properties.json +++ b/Firmware/.vscode/c_cpp_properties.json @@ -5,17 +5,17 @@ "includePath": [ "${workspaceRoot}", "${workspaceRoot}/MotorControl", - "${workspaceRoot}/Board/v3.3/Inc", - "${workspaceRoot}/Board/v3.3/Drivers/CMSIS/Include", - "${workspaceRoot}/Board/v3.3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3.3/Drivers/DRV8301", - "${workspaceRoot}/Board/v3.3/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Board/v3.3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Board/v3.3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Board/v3.3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1/backward", @@ -39,79 +39,82 @@ "limitSymbolsToIncludedHeaders": true } }, - { - "name": "Linux", - "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Drivers/CMSIS/Include", - "${workspaceRoot}/Inc", - "${workspaceRoot}/MotorControl", - "/usr/lib/gcc/arm-none-eabi/4.9.3/include", - "/usr/lib/arm-none-eabi/include" - ], - "defines": [ + { + "name": "Linux", + "includePath": [ + "${workspaceRoot}", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/MotorControl", + "/usr/lib/gcc/arm-none-eabi/4.9.3/include", + "/usr/lib/arm-none-eabi/include" + ], + "defines": [ "STM32F405xx", "USE_HAL_DRIVER", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" - ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ + ], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ "${workspaceRoot}", - "/usr/lib/gcc/arm-none-eabi/4.9.3/include", - "/usr/lib/arm-none-eabi/include" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" - } - }, - { - "name": "Mac", - "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Drivers/CMSIS/Include", - "${workspaceRoot}/Inc", - "${workspaceRoot}/MotorControl", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/lib/gcc/arm-none-eabi/6.3.1/include" - ], - "defines": [ + "/usr/lib/gcc/arm-none-eabi/4.9.3/include", + "/usr/lib/arm-none-eabi/include" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + }, + "compilerPath": "arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float", + "cStandard": "c11", + "cppStandard": "c++14" + }, + { + "name": "Mac", + "includePath": [ + "${workspaceRoot}", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/MotorControl", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/lib/gcc/arm-none-eabi/6.3.1/include" + ], + "defines": [ "STM32F405xx", "USE_HAL_DRIVER", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" - ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ + ], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ "${workspaceRoot}", "/usr/local/Caskroom/gcc-arm-embedded/" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" } } ], From f6e52c0f14818282069871cba064b7aaa744a3dc Mon Sep 17 00:00:00 2001 From: Oskar Weigl Date: Fri, 30 Mar 2018 13:23:28 -0700 Subject: [PATCH 25/32] reflect move of drv driver --- Firmware/.vscode/c_cpp_properties.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Firmware/.vscode/c_cpp_properties.json b/Firmware/.vscode/c_cpp_properties.json index 88c6c273..de6eb463 100644 --- a/Firmware/.vscode/c_cpp_properties.json +++ b/Firmware/.vscode/c_cpp_properties.json @@ -5,10 +5,10 @@ "includePath": [ "${workspaceRoot}", "${workspaceRoot}/MotorControl", + "${workspaceRoot}/Drivers/DRV8301", "${workspaceRoot}/Board/v3/Inc", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3/Drivers/DRV8301", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", @@ -43,18 +43,18 @@ "name": "Linux", "includePath": [ "${workspaceRoot}", + "${workspaceRoot}/MotorControl", + "${workspaceRoot}/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Inc", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3/Drivers/DRV8301", "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", - "${workspaceRoot}/Board/v3/Inc", - "${workspaceRoot}/MotorControl", "/usr/lib/gcc/arm-none-eabi/4.9.3/include", "/usr/lib/arm-none-eabi/include" ], @@ -83,18 +83,18 @@ "name": "Mac", "includePath": [ "${workspaceRoot}", + "${workspaceRoot}/MotorControl", + "${workspaceRoot}/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Inc", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3/Drivers/DRV8301", "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", - "${workspaceRoot}/Board/v3/Inc", - "${workspaceRoot}/MotorControl", "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include", "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1", "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", From e9ec8c6276a9b9e5a06891d68fb3f02b74bb572f Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 29 Mar 2018 21:58:41 -0700 Subject: [PATCH 26/32] combine python tools into one single odrvtool.py --- Firmware/CHANGELOG.md | 15 +- Firmware/Makefile | 2 +- Firmware/README.md | 14 +- tools/dfuse/__init__.py | 4 - tools/drv_status.py | 30 ---- tools/explore_odrive.py | 191 -------------------------- tools/liveplotter.py | 21 --- tools/{ => odrive}/dfu.py | 89 +++++------- tools/{ => odrive}/dfuse/COPYING | 0 tools/{ => odrive}/dfuse/DfuDevice.py | 0 tools/{ => odrive}/dfuse/DfuFile.py | 0 tools/{ => odrive}/dfuse/DfuState.py | 0 tools/{ => odrive}/dfuse/DfuStatus.py | 0 tools/odrive/dfuse/__init__.py | 4 + tools/odrive/enums.py | 11 ++ tools/odrive/shell.py | 135 ++++++++++++++++++ tools/odrive/utils.py | 44 +++++- tools/{demo.py => odrive_demo.py} | 0 tools/odrvtool | 119 ++++++++++++++++ tools/rate_test.py | 25 ---- 20 files changed, 365 insertions(+), 339 deletions(-) delete mode 100644 tools/dfuse/__init__.py delete mode 100755 tools/drv_status.py delete mode 100755 tools/explore_odrive.py delete mode 100755 tools/liveplotter.py rename tools/{ => odrive}/dfu.py (81%) rename tools/{ => odrive}/dfuse/COPYING (100%) rename tools/{ => odrive}/dfuse/DfuDevice.py (100%) rename tools/{ => odrive}/dfuse/DfuFile.py (100%) rename tools/{ => odrive}/dfuse/DfuState.py (100%) rename tools/{ => odrive}/dfuse/DfuStatus.py (100%) create mode 100644 tools/odrive/dfuse/__init__.py create mode 100644 tools/odrive/enums.py create mode 100644 tools/odrive/shell.py rename tools/{demo.py => odrive_demo.py} (100%) create mode 100755 tools/odrvtool delete mode 100755 tools/rate_test.py diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 2c636c12..28b7dfa4 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -7,6 +7,14 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Changed * The DFU script now verifies the flash after writing + * Refactor python tools + * The scripts `explore_odrive.py`, `liveplotter.py`, `drv_status.py` and `rate_test.py` have been merged into one single `odrvtool` script. Running this script without any arguments provides the shell that `explore_odrive.py` used to provide. + * The command line options of `odrvtool` have changed compared to the original `explore_odrive.py`. See `odrvtool --help` for more details. + * `odrvtool` (previously `explore_odrive.py`) now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...) + * No need to restart the `odrvtool` shell when devices get disconnected and reconnected + * ODrive accesses from within python tools are now thread-safe. That means you can read from the same remote property from multiple threads concurrently. + * The liveplotter (`odrvtool liveplotter`, formerly `liveplotter.py`) does no longer steal focus and closes as expected + * (experimental: start liveplotter from `odrvtool` shell by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`) ### Fixed @@ -26,13 +34,6 @@ 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...) diff --git a/Firmware/Makefile b/Firmware/Makefile index f4b505dc..73e1d8db 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -16,7 +16,7 @@ gdb: all arm-none-eabi-gdb $(FIRMWARE) -x openocd.gdbinit dfu: all - ../tools/dfu.py $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) $(FIRMWARE_HEX) + ../tools/odrvtool $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) dfu $(FIRMWARE_HEX) bmp: all arm-none-eabi-gdb --ex 'target extended-remote /dev/stlink' \ diff --git a/Firmware/README.md b/Firmware/README.md index a16c0508..7ee1181d 100644 --- a/Firmware/README.md +++ b/Firmware/README.md @@ -139,7 +139,7 @@ For working with the ODrive code you don't need an IDE, but the open-source IDE ## Communicating over USB or UART Warning: If testing USB or UART communication for the first time it is recommend that your motors are free to spin continuously and are not connected to a drivetrain with limited travel. ### From Linux/Windows/macOS -There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/explore_odrive.py](../tools/explore_odrive.py)). The other is a demo application to show you how to control the ODrive programmatically ([tools/demo.py](../tools/demo.py)). Below follows a step-by-step guide on how to run these. +There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/odrvtool](../tools/odrvtool)). The other is a demo application to show you how to control the ODrive programmatically ([tools/odrive_demo.py](../tools/odrive_demo.py)). Below follows a step-by-step guide on how to run these. * __Windows__: It is recommended to use a Unix style command prompt, such as Git Bash that comes with [Git for windows](https://git-scm.com/download/win). @@ -165,9 +165,9 @@ pip install pyusb pyserial 5. __Windows__: Use the [Zadig](http://zadig.akeo.ie/) utility to set ODrive (not STLink!) driver to libusb-win32. * If 'Odrive version 3.x' is not in the list of devices upon opening Zadig, check 'List All Devices' from the options menu. With the Odrive selected in the device list choose 'libusb-win32' from the target driver list and select the large 'install driver' button. 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 --path serial`. +7. Run `python3 odrive_demo.py` or `python3 odrvtool`. +- `odrive_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. +- `odrvtool` drops you into an interactive python shell when started without any arguments. There 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/odrvtool --path serial`. Run `./tools/odrvtool --help` to see what else you can do with the script. ### From Arduino [See ODrive Arduino Library](https://github.com/madcowswe/ODriveArduino) @@ -180,7 +180,7 @@ See the [protocol specification](protocol.md) or the [legacy protocol specificat The majority of the important parameters you would want to set after flashing the ODrive with firmware are configurable over the USB communication interface. These include some mandatory parameters that you must set for correct operation, as well as tuning and optional parameters. To start the configuration session: -* Launch `./tools/explore_odrive.py`. This will give you a command prompt where you can modify using simple assignments. +* Launch `./tools/odrvtool`. This will give you a command prompt where you can modify using simple assignments. * Configure parameters of the `my_odrive.[...].config` objects. * For example to adjust the position gain: `my_odrive.motor0.config.pos_gain = 30` Enter. * The complete list of configurable parameters is: @@ -253,7 +253,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the * All the parameters we will be modifying are in the motor structs at the top of [MotorControl/low_level.c](MotorControl/low_level.c). * Set `.encoder.use_index = true` and `.encoder.calibrated = false`. * Flash this configuration, and let the motor scan for the index pulse and then complete the encoder calibration. -* Run `explore_odrive.py`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. +* Run `odrvtool`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. * Enter the following to print out the calibration parameters (substitute the motor number you are calibrating for ``): * `my_odrive.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. * `my_odrive.motor.encoder.motor_dir` - This should print 1 or -1. @@ -267,7 +267,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the

## Checking for error codes -`explore_odrive.py`can also be used to check error codes when your odrive is not working as expected. For example `my_odrive.motor0.error` will list the error code associated with motor 0. +`odrvtool` can also be used to check error codes when your odrive is not working as expected. For example `my_odrive.motor0.error` will list the error code associated with motor 0.

The error nummber corresponds to the following: diff --git a/tools/dfuse/__init__.py b/tools/dfuse/__init__.py deleted file mode 100644 index ff023bf7..00000000 --- a/tools/dfuse/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from dfuse.DfuDevice import DfuDevice -from dfuse.DfuStatus import DfuStatus -from dfuse.DfuState import DfuState -from dfuse.DfuFile import DfuFile diff --git a/tools/drv_status.py b/tools/drv_status.py deleted file mode 100755 index bc62c3ed..00000000 --- a/tools/drv_status.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -""" -Example usage of the ODrive python library to monitor and control ODrive devices -""" - -from __future__ import print_function - -import odrive.discovery -import time -import math - -# Find a connected ODrive (this will block until you connect one) -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 -status_reg_1 = my_drive.motor0.gate_driver.status_reg_1 -status_reg_2 = my_drive.motor0.gate_driver.status_reg_2 -ctrl_reg_1 = my_drive.motor0.gate_driver.ctrl_reg_1 -ctrl_reg_2 = my_drive.motor0.gate_driver.ctrl_reg_2 - -print("DRV Fault Code: " + str(fault)) -print("Status Reg 1: " + str(status_reg_1) + " (" + format(status_reg_1, '#010b') + ")") -print("Status Reg 2: " + str(status_reg_2) + " (" + format(status_reg_2, '#010b') + ")") -print("Control Reg 1: " + str(ctrl_reg_1) + " (" + format(ctrl_reg_1, '#010b') + ")") -print("Control Reg 2: " + str(ctrl_reg_2) + " (" + format(ctrl_reg_2, '#010b') + ")") - - diff --git a/tools/explore_odrive.py b/tools/explore_odrive.py deleted file mode 100755 index 66884d0f..00000000 --- a/tools/explore_odrive.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -""" -Load an odrive object to play with in the IPython interactive shell. -""" - -import argparse -import sys -import platform -import threading -import odrive.discovery -from odrive.utils import start_liveplotter, Logger - -# Flush stdout by default -import functools -print = functools.partial(print, flush=True) - - -# some enums described in the README -# TODO: transmit as part of the JSON -MOTOR_TYPE_HIGH_CURRENT = 0 -#MOTOR_TYPE_LOW_CURRENT = 1 -MOTOR_TYPE_GIMBAL = 2 - -CTRL_MODE_VOLTAGE_CONTROL = 0, -CTRL_MODE_CURRENT_CONTROL = 1, -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.', - formatter_class=argparse.RawTextHelpFormatter) -parser.add_argument("-v", "--verbose", action="store_true", - help="print debug information") -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): - printer = print -else: - printer = lambda x: None - - -## Interactive console utils ## - -logger = Logger() - -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 ') - else: - print('Type "odrv0." and press ') - 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 - 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)) - -def did_lose_device(interactive_name): - """ - 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)) - -# Connect to device -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: - 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 # Works only on Unix - readline.parse_and_bind("tab: complete") - except: - sudo_prefix = "" if platform.system() == "Windows" else "sudo " - print("Warning: could not enable tab-complete. User experience will suffer.\n" - "Run `{}pip install readline` and then restart this script to fix this." - .format(sudo_prefix)) - - import code - console = code.InteractiveConsole(locals=interactive_variables) - interact = lambda: console.interact(banner='') - -# install hook to hide ChannelBrokenException -console.runcode('import sys') -console.runcode('superexcepthook = sys.excepthook') -console.runcode('def newexcepthook(ex_class,ex,trace):\n' - ' if ex_class.__module__ + "." + ex_class.__name__ != "odrive.protocol.ChannelBrokenException":\n' - ' superexcepthook(ex_class,ex,trace)') -console.runcode('sys.excepthook=newexcepthook') - - -# Launch shell -print_banner() -logger._skip_bottom_line = True -interact() -app_shutdown_token.set() diff --git a/tools/liveplotter.py b/tools/liveplotter.py deleted file mode 100755 index 95c50c24..00000000 --- a/tools/liveplotter.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -""" -Liveplotter -""" - -import time -import threading -import odrive.discovery -from odrive.utils import start_liveplotter - -data_rate = 100 -plot_rate = 10 -num_samples = 1000 - -my_odrive = odrive.discovery.find_any() - -# 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]) - diff --git a/tools/dfu.py b/tools/odrive/dfu.py similarity index 81% rename from tools/dfu.py rename to tools/odrive/dfu.py index 6dc2aad7..aad9a3d8 100755 --- a/tools/dfu.py +++ b/tools/odrive/dfu.py @@ -11,13 +11,9 @@ import platform import struct import array import fractions -import dfuse import usb.core import odrive.discovery - -# We are interactively printing status messages, so flush by default -import functools -print = functools.partial(print, flush=True) +from odrive.dfuse import * try: from intelhex import IntelHex @@ -93,28 +89,28 @@ def populate_sectors(sectors, hexfile): def set_alternate_safe(dfudev, alt): dfudev.set_alternate(alt) - if dfudev.get_state() == dfuse.DfuState.DFU_ERROR: + if dfudev.get_state() == DfuState.DFU_ERROR: dfudev.clear_status() - dfudev.wait_while_state(dfuse.DfuState.DFU_ERROR) + dfudev.wait_while_state(DfuState.DFU_ERROR) #def clear_error(dfudev) -def set_address_safe(dfudef, addr): +def set_address_safe(dfudev, addr): dfudev.set_address(addr) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + status = dfudev.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 dfudev.abort() - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_SYNC) - if status[1] != dfuse.DfuState.DFU_IDLE: + status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_SYNC) + if status[1] != DfuState.DFU_IDLE: raise RuntimeError("An error occured. Device Status: %r" % status) def erase(dfudev, sector): set_alternate_safe(dfudev, sector['alt']) dfudev.erase(sector['addr']) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY, timeout=sector['len']/32) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + status = dfudev.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 flash(dfudev, sector, data): @@ -128,8 +124,8 @@ def flash(dfudev, sector, data): #print('write to {:08X} ({} bytes)'.format( # sector['addr'] + blocknum * TRANSFER_SIZE, len(block))) dfudev.write(blocknum, block) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + status = dfudev.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(dfudev, sector): @@ -168,13 +164,13 @@ def get_first_mismatch_index(array1, array2): def jump_to_application(dfudev, address): set_address_safe(dfudev, address) #dfudev.set_address(address) - #status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - #if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: + #status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) + #if status[1] != DfuState.DFU_DOWNLOAD_IDLE: # raise RuntimeError("An error occured. Device Status: {}".format(status[1])) dfudev.leave() - status = dfudev.wait_while_state(dfuse.DfuState.DFU_MANIFEST_SYNC) - if status[1] != dfuse.DfuState.DFU_MANIFEST: + status = dfudev.wait_while_state(DfuState.DFU_MANIFEST_SYNC) + if status[1] != DfuState.DFU_MANIFEST: raise RuntimeError("An error occured. Device Status: {}".format(status[1])) @@ -229,55 +225,47 @@ def put_odrive_into_dfu_mode(my_drive): "Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.", find_odrive_cancellation_token) -### BEGINNING OF APPLICATION ### +def launch_dfu(args, app_shutdown_token): + """ + Waits for a device that matches args.path and args.serial_number + and then upgrades the device's firmware. + """ -# parse arguments -parser = argparse.ArgumentParser(description="Program an STM32 in DFU mode. The device can be identified either by it's serial number or UUID." - "You can list all connected devices by running" - "(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial") -parser.add_argument("-v", "--verbose", action="store_true", - help="print debug information") -parser.add_argument('file', metavar='HEX', help='the .hex file to be flashed') -parser.add_argument("-u", "--uuid", - help="The 12-byte UUID of the device. This is a hexadecimal number of the format" - "00000000-00000000-00000000") -parser.add_argument("-s", "--serial-number", - help="The 12-digit serial number of the device. This is a string consisting of 12 upper case hexadecimal digits as displayed in lsusb" - "example: 385F324D3037") -args = parser.parse_args() + # load hex file + # TODO: Either use the elf format or pack a custom format with a manifest. + # This way we can for instance verify the target board version and only + # have to publish one file for every board. + hexfile = IntelHex(args.file) -# load hex file -hexfile = IntelHex(args.file) + if (args.verbose): + print("Contiguous segments in hex file:") + for start, end in hexfile.segments(): + print(" {:08X} to {:08X}".format(start, end - 1)) -#print("Contiguous segments in hex file:") -#for start, end in hexfile.segments(): -# print(" {:08X} to {:08X}".format(start, end - 1)) + serial_number = args.serial_number -serial_number = args.serial_number + find_odrive_cancellation_token = threading.Event() + app_shutdown_token.subscribe(lambda: find_odrive_cancellation_token.set()) - -app_cancellation_token = threading.Event() -find_odrive_cancellation_token = threading.Event() -try: print("Waiting for ODrive...") # Scan for ODrives not in DFU mode and put them into DFU mode once they appear # 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) + odrive.discovery.find_all(args.path, 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(): + while not app_shutdown_token.is_set(): params = {} if serial_number == None else {'serial_number': serial_number} stm_device = usb.core.find(idVendor=0x0483, idProduct=0xdf11, **params) if stm_device != None: break time.sleep(1) find_odrive_cancellation_token.set() # we don't need this thread anymore - if app_cancellation_token.is_set(): + if app_shutdown_token.is_set(): sys.exit(1) print("Found device {} in DFU mode".format(stm_device.serial_number)) - dfudev = dfuse.DfuDevice(stm_device) + dfudev = DfuDevice(stm_device) sectors = list(get_device_sectors(dfudev)) @@ -344,8 +332,7 @@ try: # Jump to application jump_to_application(dfudev, 0x08000000) -finally: - find_odrive_cancellation_token.set() + # Note: the flashed image can be verified using: (0x12000 is the number of bytes to read) diff --git a/tools/dfuse/COPYING b/tools/odrive/dfuse/COPYING similarity index 100% rename from tools/dfuse/COPYING rename to tools/odrive/dfuse/COPYING diff --git a/tools/dfuse/DfuDevice.py b/tools/odrive/dfuse/DfuDevice.py similarity index 100% rename from tools/dfuse/DfuDevice.py rename to tools/odrive/dfuse/DfuDevice.py diff --git a/tools/dfuse/DfuFile.py b/tools/odrive/dfuse/DfuFile.py similarity index 100% rename from tools/dfuse/DfuFile.py rename to tools/odrive/dfuse/DfuFile.py diff --git a/tools/dfuse/DfuState.py b/tools/odrive/dfuse/DfuState.py similarity index 100% rename from tools/dfuse/DfuState.py rename to tools/odrive/dfuse/DfuState.py diff --git a/tools/dfuse/DfuStatus.py b/tools/odrive/dfuse/DfuStatus.py similarity index 100% rename from tools/dfuse/DfuStatus.py rename to tools/odrive/dfuse/DfuStatus.py diff --git a/tools/odrive/dfuse/__init__.py b/tools/odrive/dfuse/__init__.py new file mode 100644 index 00000000..68500f04 --- /dev/null +++ b/tools/odrive/dfuse/__init__.py @@ -0,0 +1,4 @@ +from .DfuDevice import DfuDevice +from .DfuStatus import DfuStatus +from .DfuState import DfuState +from .DfuFile import DfuFile diff --git a/tools/odrive/enums.py b/tools/odrive/enums.py new file mode 100644 index 00000000..a58404b1 --- /dev/null +++ b/tools/odrive/enums.py @@ -0,0 +1,11 @@ + +# some enums described in the README +# TODO: This is dangerous. Transmit as part of the JSON +MOTOR_TYPE_HIGH_CURRENT = 0 +#MOTOR_TYPE_LOW_CURRENT = 1 +MOTOR_TYPE_GIMBAL = 2 + +CTRL_MODE_VOLTAGE_CONTROL = 0, +CTRL_MODE_CURRENT_CONTROL = 1, +CTRL_MODE_VELOCITY_CONTROL = 2, +CTRL_MODE_POSITION_CONTROL = 3 diff --git a/tools/odrive/shell.py b/tools/odrive/shell.py new file mode 100644 index 00000000..44026797 --- /dev/null +++ b/tools/odrive/shell.py @@ -0,0 +1,135 @@ + +import sys +import platform +import threading +import odrive.discovery +from odrive.utils import start_liveplotter +from odrive.enums import * + +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 ') + else: + print('Type "odrv0." and press ') + 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 + +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)) + +def launch_shell(args, logger, printer, app_shutdown_token): + """ + Launches an interactive python or IPython command line + interface. + As ODrives are connected they are made available as + "odrv0", "odrv1", ... + """ + + # Connect to device + logger.debug("Waiting for device...") + odrive.discovery.find_all(args.path, args.serial_number, + lambda dev: did_discover_device(dev, logger, app_shutdown_token), + app_shutdown_token, + printer=printer) + + # Check if IPython is installed + if args.no_ipython: + use_ipython = False + else: + try: + import IPython + use_ipython = True + except: + print("Warning: you don't have IPython installed.") + print("If you want to have an improved interactive console with pretty colors,") + print("you should install IPython\n") + use_ipython = False + + # 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 # Works only on Unix + readline.parse_and_bind("tab: complete") + except: + sudo_prefix = "" if platform.system() == "Windows" else "sudo " + print("Warning: could not enable tab-complete. User experience will suffer.\n" + "Run `{}pip install readline` and then restart this script to fix this." + .format(sudo_prefix)) + + import code + console = code.InteractiveConsole(locals=interactive_variables) + interact = lambda: console.interact(banner='') + + # install hook to hide ChannelBrokenException + console.runcode('import sys') + console.runcode('superexcepthook = sys.excepthook') + console.runcode('def newexcepthook(ex_class,ex,trace):\n' + ' if ex_class.__module__ + "." + ex_class.__name__ != "odrive.protocol.ChannelBrokenException":\n' + ' superexcepthook(ex_class,ex,trace)') + console.runcode('sys.excepthook=newexcepthook') + + + # Launch shell + print_banner() + logger._skip_bottom_line = True + interact() + app_shutdown_token.set() diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index e65a5dbb..28e2c830 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -72,6 +72,44 @@ def start_liveplotter(get_var_callback): threading.Thread(target=plot_data).start() #plot_data() +def print_drv_regs(device): + """ + Dumps the current gate driver regisers for Motor 0 + """ + fault = device.motor0.gate_driver.drv_fault + status_reg_1 = device.motor0.gate_driver.status_reg_1 + status_reg_2 = device.motor0.gate_driver.status_reg_2 + ctrl_reg_1 = device.motor0.gate_driver.ctrl_reg_1 + ctrl_reg_2 = device.motor0.gate_driver.ctrl_reg_2 + + print("DRV Fault Code: " + str(fault)) + print("Status Reg 1: " + str(status_reg_1) + " (" + format(status_reg_1, '#010b') + ")") + print("Status Reg 2: " + str(status_reg_2) + " (" + format(status_reg_2, '#010b') + ")") + print("Control Reg 1: " + str(ctrl_reg_1) + " (" + format(ctrl_reg_1, '#010b') + ")") + print("Control Reg 2: " + str(ctrl_reg_2) + " (" + format(ctrl_reg_2, '#010b') + ")") + +def rate_test(device): + """ + Tests how many integers per second can be transmitted + """ + + import matplotlib.pyplot as plt + plt.ion() + + print("reading 10000 values...") + numFrames = 10000 + vals = [] + for _ in range(numFrames): + vals.append(device.motor0.loop_counter) + + plt.plot(vals) + + loopsPerFrame = (vals[-1] - vals[0])/numFrames + loopsPerSec = (168000000/(2*10192)) + FramePerSec = loopsPerSec/loopsPerFrame + print("Frames per second: " + str(FramePerSec)) + + ## Exceptions ## class TimeoutException(Exception): @@ -192,9 +230,10 @@ class Logger(): COLOR_DEFAULT: 0x07 } - def __init__(self): + def __init__(self, verbose=True): self._prefix = '' self._skip_bottom_line = False # If true, messages are printed one line above the cursor + self._verbose = verbose if platform.system() == 'Windows': self._stdout_buf = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE) @@ -256,7 +295,8 @@ class Logger(): sys.stdout.flush() def debug(self, text): - self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT) + if self._verbose: + self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT) def success(self, text): self.print_colored(self._prefix + text, Logger.COLOR_GREEN) def info(self, text): diff --git a/tools/demo.py b/tools/odrive_demo.py similarity index 100% rename from tools/demo.py rename to tools/odrive_demo.py diff --git a/tools/odrvtool b/tools/odrvtool new file mode 100755 index 00000000..d8d31bda --- /dev/null +++ b/tools/odrvtool @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +ODrive command line utility +""" + +import argparse +import odrive.discovery +from odrive.utils import Logger, Event + +# Flush stdout by default +import functools +print = functools.partial(print, flush=True) + + +## Parse arguments ## +parser = argparse.ArgumentParser(description='ODrive command line utility\n' + 'Running this tool without any arguments is equivalent to running `odrvtool shell`\n', + formatter_class=argparse.RawTextHelpFormatter) + +# Subcommands +subparsers = parser.add_subparsers(help='sub-command help', dest='command') +shell_parser = subparsers.add_parser('shell', help='Drop into an interactive python shell that lets you interact with the ODrive(s)') +shell_parser.add_argument("--no-ipython", action="store_true", + help="Use the regular Python shell " + "instead of the IPython shell, " + "even if IPython is installed.") + +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.') + +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)") +subparsers.add_parser('rate-test', help="Estimate the average transmission bandwidth over USB") + +# General arguments +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("-s", "--serial-number", action="store", + help="The 12-digit serial number of the device. " + "This is a string consisting of 12 upper case hexadecimal " + "digits as displayed in lsusb. \n" + " example: 385F324D3037\n" + "You can list all devices connected to USB by running\n" + "(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial\n" + "If omitted, any device is accepted.") +parser.add_argument("-v", "--verbose", action="store_true", + help="print debug information") + +parser.set_defaults(path="usb") +args = parser.parse_args() + +# Default command +if args.command is None: + args.command = 'shell' + args.no_ipython = False + +# We are interactively printing status messages, so flush by default +import functools +print = functools.partial(print, flush=True) + +# TODO: deprecate printer - use logger instead +if (args.verbose): + printer = print +else: + printer = lambda x: None + +logger = Logger(verbose=args.verbose) +logger.debug(str(args)) + +app_shutdown_token = Event() + +try: + if args.command == 'shell': + import odrive.shell + odrive.shell.launch_shell(args, logger, printer, app_shutdown_token) + + elif args.command == 'dfu': + import odrive.dfu + odrive.dfu.launch_dfu(args, app_shutdown_token) + + elif args.command == 'liveplotter': + from odrive.utils import start_liveplotter + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + + # 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]) + + elif args.command == 'drv-status': + from odrive.utils import print_drv_regs + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + print_drv_regs(my_odrive) + + elif args.command == 'rate-test': + from odrive.utils import rate_test + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + rate_test(my_odrive) + + else: + raise Exception("unknown command: " + args.command) + +finally: + app_shutdown_token.set() diff --git a/tools/rate_test.py b/tools/rate_test.py deleted file mode 100755 index 76f359a6..00000000 --- a/tools/rate_test.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 - -import time -import odrive.discovery -import matplotlib.pyplot as plt -import numpy as np - -# Find a connected ODrive (this will block until you connect one) -print("Waiting for ODrive...") -myOdrive = odrive.discovery.find_any() -print("connected") - -plt.ion() - -numFrames = 10000 -vals = [] -for _ in range(numFrames): - vals.append(myOdrive.motor0.loop_counter) - -plt.plot(vals) - -loopsPerFrame = (vals[-1] - vals[0])/numFrames -loopsPerSec = (168000000/(2*10192)) -FramePerSec = loopsPerSec/loopsPerFrame -print(FramePerSec) \ No newline at end of file From 9fb5fceea71c61cb62cd42c08ca5032bf71d3855 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Thu, 29 Mar 2018 23:18:40 -0700 Subject: [PATCH 27/32] change all occurrences of my_odrive in the README to odrv0 --- Firmware/CHANGELOG.md | 2 +- Firmware/README.md | 46 +++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 28b7dfa4..c046e00f 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -25,7 +25,7 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Added * **Storing of configuration parameters to Non Volatile Memory** * **USB Bootloader** -* `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `my_odrive.erase_configuration()`) +* `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `odrv0.erase_configuration()`) * Travis-CI builds firmware for all board versions and deploys the binaries when a tag is pushed to master ### Changed diff --git a/Firmware/README.md b/Firmware/README.md index 7ee1181d..cd4a677d 100644 --- a/Firmware/README.md +++ b/Firmware/README.md @@ -167,7 +167,7 @@ pip install pyusb pyserial 6. Open the bash prompt in the `ODrive/tools/` folder. 7. Run `python3 odrive_demo.py` or `python3 odrvtool`. - `odrive_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. -- `odrvtool` drops you into an interactive python shell when started without any arguments. There 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/odrvtool --path serial`. Run `./tools/odrvtool --help` to see what else you can do with the script. +- `odrvtool` drops you into an interactive python shell when started without any arguments. There you can explore and edit the parameters that are available on your device. For instance `odrv0.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/odrvtool --path serial`. Run `./tools/odrvtool --help` to see what else you can do with the script. ### From Arduino [See ODrive Arduino Library](https://github.com/madcowswe/ODriveArduino) @@ -181,26 +181,26 @@ The majority of the important parameters you would want to set after flashing th To start the configuration session: * Launch `./tools/odrvtool`. This will give you a command prompt where you can modify using simple assignments. -* Configure parameters of the `my_odrive.[...].config` objects. - * For example to adjust the position gain: `my_odrive.motor0.config.pos_gain = 30` Enter. +* Configure parameters of the `odrv0.[...].config` objects. + * For example to adjust the position gain: `odrv0.motor0.config.pos_gain = 30` Enter. * The complete list of configurable parameters is: - * `my_odrive.motorN.config.*` - * `my_odrive.axisN.config.*` + * `odrv0.motorN.config.*` + * `odrv0.axisN.config.*` * where N is a valid motor number (0 or 1). -* Save the configuration into non-volatile memory: `my_odrive.save_configuration()` Enter +* Save the configuration into non-volatile memory: `odrv0.save_configuration()` Enter * This will save the properties of all the `[...].config` objects and no other parameters. -* Reboot the drive: `my_odrive.reboot()` Enter +* Reboot the drive: `odrv0.reboot()` Enter -Note that a firmware upgrade at this point will preserve the configuration if and only if the parameters of both firmware versions are identical. Should you need to reset the configuration, you can run `my_odrive.erase_configuration()`. +Note that a firmware upgrade at this point will preserve the configuration if and only if the parameters of both firmware versions are identical. Should you need to reset the configuration, you can run `odrv0.erase_configuration()`. __Developers__: Be aware that you can also modify the compile-time defaults for all of these parameters. Most of them you will find at the top of [MotorControl/low_level.c](MotorControl/low_level.c#L50). Note that the configuration parameters there are somewhat intertwined with runtime variables and hardware specific configuration that should not be changed. Also note that all parameters occur twice. ### Mandatory parameters You must set for every motor: -* `my_odrive.motorN.encoder.config.cpr`: Encoder Count Per Revolution (CPR). This is 4x the Pulse Per Revolution (PPR) value. -* `my_odrive.motorN.config.pole_pairs`: This is the number of magnet poles in the rotor, **divided by two**. You can simply count the number of permanent magnets in the rotor, if you can see them. Note: this is not the same as the number of coils in the stator. -* `my_odrive.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to 0.0f. -* `my_odrive.motorN.config.motor_type`: This is the type of motor being used. Currently two types of motors are supported -- High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). +* `odrv0.motorN.encoder.config.cpr`: Encoder Count Per Revolution (CPR). This is 4x the Pulse Per Revolution (PPR) value. +* `odrv0.motorN.config.pole_pairs`: This is the number of magnet poles in the rotor, **divided by two**. You can simply count the number of permanent magnets in the rotor, if you can see them. Note: this is not the same as the number of coils in the stator. +* `odrv0.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to 0.0f. +* `odrv0.motorN.config.motor_type`: This is the type of motor being used. Currently two types of motors are supported -- High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). #### Motor Modes If you're using a regular hobby brushless motor like [this](https://hobbyking.com/en_us/turnigy-aerodrive-sk3-5065-236kv-brushless-outrunner-motor.html) one, you should set `motor_mode` to `MOTOR_TYPE_HIGH_CURRENT`. For low-current gimbal motors like [this](https://hobbyking.com/en_us/turnigy-hd-5208-brushless-gimbal-motor-bldc.html) one, you should choose `MOTOR_TYPE_GIMBAL`. Do not use `MOTOR_TYPE_GIMBAL` on a motor that is not a gimbal motor, as it may overheat the motor or the ODrive. @@ -212,15 +212,15 @@ If 100's of mA current noise is "large" for you, and you intend to spin the moto ### Tuning parameters The most important parameters are the limits: -* The current limit: `my_odrive.motorN.current_control.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. +* The current limit: `odrv0.motorN.current_control.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. * Note: The motor current and the current drawn from the power supply is not the same in general. You should not look at the power supply current to see what is going on with the motor current. -* The velocity limit: `my_odrive.motorN.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. -* You can change `my_odrive.motorN.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. +* The velocity limit: `odrv0.motorN.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. +* You can change `odrv0.motorN.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. The motion control gains are currently manually tuned: -* `my_odrive.motorN.config.pos_gain = 20.0f` [(counts/s) / counts] -* `my_odrive.motorN.config.vel_gain = 15.0f / 10000.0f` [A/(counts/s)] -* `my_odrive.motorN.config.vel_integrator_gain = 10.0f / 10000.0f` [A/(counts/s * s)] +* `odrv0.motorN.config.pos_gain = 20.0f` [(counts/s) / counts] +* `odrv0.motorN.config.vel_gain = 15.0f / 10000.0f` [A/(counts/s)] +* `odrv0.motorN.config.vel_integrator_gain = 10.0f / 10000.0f` [A/(counts/s * s)] An upcoming feature will enable automatic tuning. Until then, here is a rough tuning procedure: * Set the integrator gain to 0 @@ -233,14 +233,14 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu ### Optional parameters By default both motors are enabled, and the default control mode is position control. -If you want a different mode, you can change `my_odrive.motorN.config.control_mode`. +If you want a different mode, you can change `odrv0.motorN.config.control_mode`. Possible values are: * `CTRL_MODE_POSITION_CONTROL` * `CTRL_MODE_VELOCITY_CONTROL` * `CTRL_MODE_CURRENT_CONTROL` * `CTRL_MODE_VOLTAGE_CONTROL` - this one is not normally used. -To disable a motor at startup, set `my_odrive.axisN.config.enable_control` and `my_odrive.axisN.config.do_calibration` to `False`. +To disable a motor at startup, set `odrv0.axisN.config.enable_control` and `odrv0.axisN.config.do_calibration` to `False`.

## Encoder Calibration @@ -255,8 +255,8 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the * Flash this configuration, and let the motor scan for the index pulse and then complete the encoder calibration. * Run `odrvtool`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. * Enter the following to print out the calibration parameters (substitute the motor number you are calibrating for ``): - * `my_odrive.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. - * `my_odrive.motor.encoder.motor_dir` - This should print 1 or -1. + * `odrv0.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. + * `odrv0.motor.encoder.motor_dir` - This should print 1 or -1. * Copy these numbers to the corresponding entries in low_level.c: `.encoder.encoder_offset` and `.encoder.motor_dir`. * _Warning_: Please be careful to enter the correct numbers, and not to confuse the motor channels. Incorrect values may cause the motor to spin out of control. * Set `.encoder.calibrated = true`. @@ -267,7 +267,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the

## Checking for error codes -`odrvtool` can also be used to check error codes when your odrive is not working as expected. For example `my_odrive.motor0.error` will list the error code associated with motor 0. +`odrvtool` can also be used to check error codes when your odrive is not working as expected. For example `odrv0.motor0.error` will list the error code associated with motor 0.

The error nummber corresponds to the following: From 34a95f971bd46bd3c4d965866f6ca2db2d0aa73e Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 15:05:50 -0700 Subject: [PATCH 28/32] add get_version to python library --- tools/odrive/__init__.py | 5 +++++ tools/odrive/version.py | 46 ++++++++++++++++++++++++++++++++++++++++ tools/odrvtool | 7 +++++- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tools/odrive/version.py diff --git a/tools/odrive/__init__.py b/tools/odrive/__init__.py index e69de29b..6eca987b 100644 --- a/tools/odrive/__init__.py +++ b/tools/odrive/__init__.py @@ -0,0 +1,5 @@ + +# Standard convention is to add a __version__ attribute to the package +from .version import get_version +__version__ = get_version() +del get_version diff --git a/tools/odrive/version.py b/tools/odrive/version.py new file mode 100644 index 00000000..327c915c --- /dev/null +++ b/tools/odrive/version.py @@ -0,0 +1,46 @@ + +import re +import subprocess +import os +import sys + +def get_version(git_only=False): + """ + Returns the versions of the tools + If git_only is true, the version.txt file is ignored even + if it is present. + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + + # Try to read the version.txt file that is generated during + # the packaging step + version_file_path = os.path.join(script_dir, 'version.txt') + if os.path.exists(version_file_path) and git_only == False: + with open(version_file_path) as version_file: + return version_file.readline().rstrip('\n') + + try: + # Determine the current git commit version + git_result = subprocess.run(["git", "describe", "--always", "--tags", "--dirty=*"], + cwd=script_dir, + stdout=subprocess.PIPE, timeout=10) + git_tag = git_result.stdout.decode(sys.stdout.encoding) + + 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) != "") + + if package_version_unreleased: + package_version_revision += 1 + + # TODO: fetch from Git describe + version = '{}.{}.{}'.format(package_version_major, package_version_minor, package_version_revision) + + if package_version_unreleased: + version += ".dev" + except Exception as ex: + print(ex) + version = "whatever version in " + script_dir + return version diff --git a/tools/odrvtool b/tools/odrvtool index d8d31bda..8d67203e 100755 --- a/tools/odrvtool +++ b/tools/odrvtool @@ -57,6 +57,8 @@ parser.add_argument("-s", "--serial-number", action="store", "If omitted, any device is accepted.") parser.add_argument("-v", "--verbose", action="store_true", help="print debug information") +parser.add_argument("--version", action="store_true", + help="print version information and exit") parser.set_defaults(path="usb") args = parser.parse_args() @@ -82,7 +84,10 @@ logger.debug(str(args)) app_shutdown_token = Event() try: - if args.command == 'shell': + if args.version == True: + print("ODrive control utility v" + odrive.__version__) + + elif args.command == 'shell': import odrive.shell odrive.shell.launch_shell(args, logger, printer, app_shutdown_token) From eeb57863977eba54498fd6234aa60c7178341513 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 15:19:41 -0700 Subject: [PATCH 29/32] add setup.py to publish python tools to PyPi --- .gitignore | 28 -------------- Firmware/CHANGELOG.md | 1 + tools/.gitignore | 29 ++++++++++++++ tools/requirements.txt | 3 ++ tools/setup.py | 88 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 tools/.gitignore create mode 100644 tools/requirements.txt create mode 100644 tools/setup.py diff --git a/.gitignore b/.gitignore index 4a8546b5..db493d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,34 +6,6 @@ __pycache__/ # C extensions *.so -# Distribution / packaging -.Python -#env/ -#build/ -#develop-eggs/ -#dist/ -#downloads/ -#eggs/ -#.eggs/ -#lib/ -#lib64/ -#parts/ -#sdist/ -#var/ -#*.egg-info/ -#.installed.cfg -#*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - # Unit test / coverage reports htmlcov/ .tox/ diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index c046e00f..fb823525 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -4,6 +4,7 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Added * `make write_otp` command to burn the board version onto the ODrive's one-time programmable memory. If you have an ODrive v3.4 or older, you should run this once for a better firmware update user experience in the future. Run the command without any options for more details. Once set, the board version is exposed through the `board_version_[...]` properties. * bake Git-derived firmware version into firmware binary. The firmware version is exposed through the `fw_version_[...]` properties. + * infrastructure to publish the python tools to PyPi. See `tools/setup.py` for details. ### Changed * The DFU script now verifies the flash after writing diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 00000000..8effff5e --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,29 @@ + +# Python Distribution / packaging +.Python +#env/ +#build/ +#develop-eggs/ +/dist/ +#downloads/ +#eggs/ +#.eggs/ +#lib/ +#lib64/ +#parts/ +#sdist/ +#var/ +/*.egg-info/ +#.installed.cfg +#*.egg +/MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 00000000..7d1e0773 --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1,3 @@ +--index-url https://pypi.python.org/simple/ + +-e . \ No newline at end of file diff --git a/tools/setup.py b/tools/setup.py new file mode 100644 index 00000000..27b4fdcc --- /dev/null +++ b/tools/setup.py @@ -0,0 +1,88 @@ +""" +This script is used to deploy the ODrive python tools to PyPi +so that users can install them easily with +"pip install odrive" + +To install the package and its dependencies locally, run: + sudo pip install -r requirements.txt + +To build and package the python tools into a tar archive: + python setup.py sdist + +Warning: Before you proceed, be aware that you can upload a +specific version only ever once. After that you need to increment +the hotfix number. Deleting the release manually on the PyPi +website does not help. + +Use TestPyPi while developing. + +To build, package and upload the python tools to TestPyPi, run: + python setup.py sdist upload -r pypitest +To make a real release ensure you're at the release commit +and then run the above command without the "test". + +To install a prerelease version from test index: + sudo pip install --index-url https://test.pypi.org/simple/ --no-cache-dir odrive + + +PyPi access requires that you have set up ~/.pypirc with your +PyPi credentials and that your account has the rights +to publish packages with the name odrive. +""" + +# TODO: add additional y/n prompt to prevent from erroneous upload + +from distutils.core import setup +import os +import sys + +creating_package = "sdist" in sys.argv + +# Load version from Git tag +import odrive.version +version = odrive.version.get_version(git_only=creating_package) + +# Change this if you already uploaded the current +# version but need to release a hotfix +hotfix = 0 + +if creating_package and (hotfix > 0 or not version[-1].isdigit()): + # Add this for hotfixes + version += "-" + str(hotfix) + + +# If we're currently creating the package we need to autogenerate +# a file that contains the version string +if creating_package: + version_file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'odrive', 'version.txt') + with open(version_file_path, mode='w') as version_file: + version_file.write(version) + + +setup( + name = 'odrive', + packages = ['odrive'], # this must be the same as the name above + scripts = ['odrvtool', 'odrive_demo.py'], + version = version, + description = 'Control utilities for the ODrive high performance motor controller', + author = 'Oskar Weigl', + author_email = 'oskar.weigl@odriverobotics.com', + license='MIT', + url = 'https://github.com/madcowswe/ODrive', + keywords = ['odrive', 'motor', 'motor control'], + install_requires = [ + 'PyUSB', # Required to access USB devices from Python through libusb + 'PySerial', # Required to access serial devices from Python + 'IntelHex', # Used to by DFU to load firmware files + 'matplotlib' # Required to run the liveplotter + ], + package_data={'': ['version.txt']}, + include_package_data=True, + classifiers = [], +) + +# TODO: include README + +# clean up +if creating_package: + os.remove(version_file_path) From febdb248563b05f8116ba7773ddee4a84dd3468b Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 16:24:33 -0700 Subject: [PATCH 30/32] display correct python tools version --- tools/odrive/shell.py | 10 ++++------ tools/odrvtool | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/odrive/shell.py b/tools/odrive/shell.py index 44026797..9bfd39b6 100644 --- a/tools/odrive/shell.py +++ b/tools/odrive/shell.py @@ -7,11 +7,10 @@ from odrive.utils import start_liveplotter from odrive.enums import * def print_banner(): - print('ODrive control utility v0.4') print('Please connect your ODrive.') print('Type help() for help.') -def print_help(): +def print_help(args): print('') if len(discovered_devices) == 0: print('Connect your ODrive to {} and power it up.'.format(args.path)) @@ -31,7 +30,6 @@ def print_help(): interactive_variables = {} -interactive_variables["help"] = print_help discovered_devices = [] @@ -95,15 +93,15 @@ def launch_shell(args, logger, printer, app_shutdown_token): print("you should install IPython\n") use_ipython = False + interactive_variables["help"] = lambda: print_help(args) + # 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 + help = lambda: print_help(args) # 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 diff --git a/tools/odrvtool b/tools/odrvtool index 8d67203e..ae6f2662 100755 --- a/tools/odrvtool +++ b/tools/odrvtool @@ -81,11 +81,13 @@ else: logger = Logger(verbose=args.verbose) logger.debug(str(args)) +print("ODrive control utility v" + odrive.__version__) + app_shutdown_token = Event() try: if args.version == True: - print("ODrive control utility v" + odrive.__version__) + pass elif args.command == 'shell': import odrive.shell From 583ab2b76dc7d595b8f97c4895ac753f7630e7d5 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 16:32:33 -0700 Subject: [PATCH 31/32] silence python warnings --- tools/odrive/shell.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/odrive/shell.py b/tools/odrive/shell.py index 9bfd39b6..21670e3d 100644 --- a/tools/odrive/shell.py +++ b/tools/odrive/shell.py @@ -4,7 +4,7 @@ import platform import threading import odrive.discovery from odrive.utils import start_liveplotter -from odrive.enums import * +from odrive.enums import * # pylint: disable=W0614 def print_banner(): print('Please connect your ODrive.') @@ -97,14 +97,13 @@ def launch_shell(args, logger, printer, app_shutdown_token): # If IPython is installed, embed IPython shell, otherwise embed regular shell if use_ipython: - help = lambda: print_help(args) # Override help function + help = lambda: print_help(args) # Override help function # pylint: disable=W0612 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 # Works only on Unix readline.parse_and_bind("tab: complete") except: From fbaac861d73904626cb6bf77f60e4fca89cb9a0e Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 30 Mar 2018 16:46:22 -0700 Subject: [PATCH 32/32] rename odrvtool to odrivetool --- Firmware/CHANGELOG.md | 12 ++++++------ Firmware/Makefile | 2 +- Firmware/README.md | 12 ++++++------ tools/odrvtool | 2 +- tools/setup.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index fb823525..c8c4662b 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -9,13 +9,13 @@ Please add a note of your changes below this heading if you make a Pull Request. ### Changed * The DFU script now verifies the flash after writing * Refactor python tools - * The scripts `explore_odrive.py`, `liveplotter.py`, `drv_status.py` and `rate_test.py` have been merged into one single `odrvtool` script. Running this script without any arguments provides the shell that `explore_odrive.py` used to provide. - * The command line options of `odrvtool` have changed compared to the original `explore_odrive.py`. See `odrvtool --help` for more details. - * `odrvtool` (previously `explore_odrive.py`) now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...) - * No need to restart the `odrvtool` shell when devices get disconnected and reconnected + * The scripts `explore_odrive.py`, `liveplotter.py`, `drv_status.py` and `rate_test.py` have been merged into one single `odrivetool` script. Running this script without any arguments provides the shell that `explore_odrive.py` used to provide. + * The command line options of `odrivetool` have changed compared to the original `explore_odrive.py`. See `odrivetool --help` for more details. + * `odrivetool` (previously `explore_odrive.py`) now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...) + * No need to restart the `odrivetool` shell when devices get disconnected and reconnected * ODrive accesses from within python tools are now thread-safe. That means you can read from the same remote property from multiple threads concurrently. - * The liveplotter (`odrvtool liveplotter`, formerly `liveplotter.py`) does no longer steal focus and closes as expected - * (experimental: start liveplotter from `odrvtool` shell by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`) + * The liveplotter (`odrivetool liveplotter`, formerly `liveplotter.py`) does no longer steal focus and closes as expected + * (experimental: start liveplotter from `odrivetool` shell by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`) ### Fixed diff --git a/Firmware/Makefile b/Firmware/Makefile index 73e1d8db..d587105a 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -16,7 +16,7 @@ gdb: all arm-none-eabi-gdb $(FIRMWARE) -x openocd.gdbinit dfu: all - ../tools/odrvtool $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) dfu $(FIRMWARE_HEX) + ../tools/odrivetool $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) dfu $(FIRMWARE_HEX) bmp: all arm-none-eabi-gdb --ex 'target extended-remote /dev/stlink' \ diff --git a/Firmware/README.md b/Firmware/README.md index cd4a677d..5ce7736e 100644 --- a/Firmware/README.md +++ b/Firmware/README.md @@ -139,7 +139,7 @@ For working with the ODrive code you don't need an IDE, but the open-source IDE ## Communicating over USB or UART Warning: If testing USB or UART communication for the first time it is recommend that your motors are free to spin continuously and are not connected to a drivetrain with limited travel. ### From Linux/Windows/macOS -There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/odrvtool](../tools/odrvtool)). The other is a demo application to show you how to control the ODrive programmatically ([tools/odrive_demo.py](../tools/odrive_demo.py)). Below follows a step-by-step guide on how to run these. +There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/odrivetool](../tools/odrivetool)). The other is a demo application to show you how to control the ODrive programmatically ([tools/odrive_demo.py](../tools/odrive_demo.py)). Below follows a step-by-step guide on how to run these. * __Windows__: It is recommended to use a Unix style command prompt, such as Git Bash that comes with [Git for windows](https://git-scm.com/download/win). @@ -165,9 +165,9 @@ pip install pyusb pyserial 5. __Windows__: Use the [Zadig](http://zadig.akeo.ie/) utility to set ODrive (not STLink!) driver to libusb-win32. * If 'Odrive version 3.x' is not in the list of devices upon opening Zadig, check 'List All Devices' from the options menu. With the Odrive selected in the device list choose 'libusb-win32' from the target driver list and select the large 'install driver' button. 6. Open the bash prompt in the `ODrive/tools/` folder. -7. Run `python3 odrive_demo.py` or `python3 odrvtool`. +7. Run `python3 odrive_demo.py` or `python3 odrivetool`. - `odrive_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. -- `odrvtool` drops you into an interactive python shell when started without any arguments. There you can explore and edit the parameters that are available on your device. For instance `odrv0.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/odrvtool --path serial`. Run `./tools/odrvtool --help` to see what else you can do with the script. +- `odrivetool` drops you into an interactive python shell when started without any arguments. There you can explore and edit the parameters that are available on your device. For instance `odrv0.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/odrivetool --path serial`. Run `./tools/odrivetool --help` to see what else you can do with the script. ### From Arduino [See ODrive Arduino Library](https://github.com/madcowswe/ODriveArduino) @@ -180,7 +180,7 @@ See the [protocol specification](protocol.md) or the [legacy protocol specificat The majority of the important parameters you would want to set after flashing the ODrive with firmware are configurable over the USB communication interface. These include some mandatory parameters that you must set for correct operation, as well as tuning and optional parameters. To start the configuration session: -* Launch `./tools/odrvtool`. This will give you a command prompt where you can modify using simple assignments. +* Launch `./tools/odrivetool`. This will give you a command prompt where you can modify using simple assignments. * Configure parameters of the `odrv0.[...].config` objects. * For example to adjust the position gain: `odrv0.motor0.config.pos_gain = 30` Enter. * The complete list of configurable parameters is: @@ -253,7 +253,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the * All the parameters we will be modifying are in the motor structs at the top of [MotorControl/low_level.c](MotorControl/low_level.c). * Set `.encoder.use_index = true` and `.encoder.calibrated = false`. * Flash this configuration, and let the motor scan for the index pulse and then complete the encoder calibration. -* Run `odrvtool`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. +* Run `odrivetool`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. * Enter the following to print out the calibration parameters (substitute the motor number you are calibrating for ``): * `odrv0.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. * `odrv0.motor.encoder.motor_dir` - This should print 1 or -1. @@ -267,7 +267,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the

## Checking for error codes -`odrvtool` can also be used to check error codes when your odrive is not working as expected. For example `odrv0.motor0.error` will list the error code associated with motor 0. +`odrivetool` can also be used to check error codes when your odrive is not working as expected. For example `odrv0.motor0.error` will list the error code associated with motor 0.

The error nummber corresponds to the following: diff --git a/tools/odrvtool b/tools/odrvtool index ae6f2662..3e8211d7 100755 --- a/tools/odrvtool +++ b/tools/odrvtool @@ -14,7 +14,7 @@ print = functools.partial(print, flush=True) ## Parse arguments ## parser = argparse.ArgumentParser(description='ODrive command line utility\n' - 'Running this tool without any arguments is equivalent to running `odrvtool shell`\n', + 'Running this tool without any arguments is equivalent to running `odrivetool shell`\n', formatter_class=argparse.RawTextHelpFormatter) # Subcommands diff --git a/tools/setup.py b/tools/setup.py index 27b4fdcc..b503d710 100644 --- a/tools/setup.py +++ b/tools/setup.py @@ -62,7 +62,7 @@ if creating_package: setup( name = 'odrive', packages = ['odrive'], # this must be the same as the name above - scripts = ['odrvtool', 'odrive_demo.py'], + scripts = ['odrivetool', 'odrive_demo.py'], version = version, description = 'Control utilities for the ODrive high performance motor controller', author = 'Oskar Weigl',