mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-09-25 11:06:48 +08:00
combine python tools into one single odrvtool.py
This commit is contained in:
@@ -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...)
|
||||
|
||||
+1
-1
@@ -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' \
|
||||
|
||||
+7
-7
@@ -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` <kbd>Enter</kbd>.
|
||||
* 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 `<NUM>`):
|
||||
* `my_odrive.motor<NUM>.encoder.encoder_offset` - This should print a number, like -326 or 1364.
|
||||
* `my_odrive.motor<NUM>.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
|
||||
|
||||
<br><br>
|
||||
## 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.
|
||||
<br><br>
|
||||
The error nummber corresponds to the following:
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from dfuse.DfuDevice import DfuDevice
|
||||
from dfuse.DfuStatus import DfuStatus
|
||||
from dfuse.DfuState import DfuState
|
||||
from dfuse.DfuFile import DfuFile
|
||||
@@ -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') + ")")
|
||||
|
||||
|
||||
@@ -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 <tab>')
|
||||
else:
|
||||
print('Type "odrv0." and press <tab>')
|
||||
print('This will present you with all the properties that you can reference')
|
||||
print('')
|
||||
print('For example: "odrv0.motor0.encoder.pll_pos"')
|
||||
print('will print the current encoder position on motor 0')
|
||||
print('and "odrv0.motor0.pos_setpoint = 10000"')
|
||||
print('will send motor0 to 10000')
|
||||
print('')
|
||||
|
||||
interactive_variables = {}
|
||||
interactive_variables["help"] = print_help
|
||||
|
||||
|
||||
## Device discovery ##
|
||||
|
||||
discovered_devices = []
|
||||
|
||||
def did_discover_device(odrive):
|
||||
"""
|
||||
Handles the discovery of new devices by displaying a
|
||||
message and making the device available to the interactive
|
||||
console
|
||||
"""
|
||||
serial_number = odrive.serial_number if hasattr(odrive, 'serial_number') else "[unknown serial number]"
|
||||
if serial_number in discovered_devices:
|
||||
verb = "Reconnected"
|
||||
index = discovered_devices.index(serial_number)
|
||||
else:
|
||||
verb = "Connected"
|
||||
discovered_devices.append(serial_number)
|
||||
index = len(discovered_devices) - 1
|
||||
interactive_name = "odrv" + str(index)
|
||||
|
||||
# Publish new ODrive to interactive console
|
||||
interactive_variables[interactive_name] = odrive
|
||||
globals()[interactive_name] = odrive # Add to globals so tab complete works
|
||||
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()
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .DfuDevice import DfuDevice
|
||||
from .DfuStatus import DfuStatus
|
||||
from .DfuState import DfuState
|
||||
from .DfuFile import DfuFile
|
||||
@@ -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
|
||||
@@ -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 <tab>')
|
||||
else:
|
||||
print('Type "odrv0." and press <tab>')
|
||||
print('This will present you with all the properties that you can reference')
|
||||
print('')
|
||||
print('For example: "odrv0.motor0.encoder.pll_pos"')
|
||||
print('will print the current encoder position on motor 0')
|
||||
print('and "odrv0.motor0.pos_setpoint = 10000"')
|
||||
print('will send motor0 to 10000')
|
||||
print('')
|
||||
|
||||
|
||||
interactive_variables = {}
|
||||
interactive_variables["help"] = print_help
|
||||
|
||||
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()
|
||||
+42
-2
@@ -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):
|
||||
|
||||
Executable
+119
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user