From 6a895d03b6ae040c095593defcf5396c825dee91 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Sat, 14 Apr 2018 01:09:36 -0700 Subject: [PATCH] Add documentation fixes here and there --- docs/_data/index.yaml | 4 +- docs/ascii-protocol.md | 2 + docs/commands.md | 59 ++++++++------ docs/developer-guide.md | 2 +- docs/encoders.md | 57 +++++++++----- docs/interfaces.md | 12 +-- docs/odrivetool.md | 2 +- {Firmware => docs}/protocol.md | 0 docs/quick-start.md | 4 +- docs/troubleshooting.md | 139 ++++++++++----------------------- 10 files changed, 131 insertions(+), 150 deletions(-) rename {Firmware => docs}/protocol.md (100%) diff --git a/docs/_data/index.yaml b/docs/_data/index.yaml index 978dc054..80d314aa 100644 --- a/docs/_data/index.yaml +++ b/docs/_data/index.yaml @@ -12,8 +12,8 @@ sections: - title: Parameters & Commands url: commands - title: Interfaces - url: commands - - title: Motors & Encoders + url: interfaces + - title: Encoders url: encoders - title: Troubleshooting url: troubleshooting diff --git a/docs/ascii-protocol.md b/docs/ascii-protocol.md index f7d89235..6bd5103a 100644 --- a/docs/ascii-protocol.md +++ b/docs/ascii-protocol.md @@ -1,4 +1,6 @@ +# ASCII Protocol + ## How to send commands * **Via USB:** diff --git a/docs/commands.md b/docs/commands.md index f57d0765..2ed9c505 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,39 +1,57 @@ # Parameters & Commands -Each axis can be controlled independently. -Most parameters can be set per axis. They are referred to as `` +We will use the `` as a placeholder for any ODrive object. In `odrivetool` this is usually `odrv0`. Furthermore we use `` as a placeholder for any axis (for example `odrv0.axis0`). + +## General system commands + +### Saving the configuration + +All variables that are part of a `[...].config` object can be saved to non-volatile memory on the ODrive so they persist after you remove power. The relevant commands are: + + * `.save_configuration()`: Stores the configuration to persistent memory on the ODrive. + * `.erase_configuration()`: Resets the configuration variables to their factory defaults. This only has an effect after a reboot. A side effect of this command is that motor control stops (in case it was running) and the USB communication breaks out temporarily. This is because erasing flash pages hangs the microcontroller for several seconds. + +### Diagnostics + + * `.serial_number`: A number that uniquely identifies your device. When printed in upper case hexadecimal (`hex(.serial_number).upper()`), this is identical to the serial number indicated by the USB descriptor. + * `.fw_version_major`, `.fw_version_minor`, `.fw_version_revision`: The firmware version that is currently running. + * `.hw_version_major`, `.hw_version_minor`, `.hw_version_revision`: The hardware version of your ODrive. + +## Per-Axis commands + +For the most part, both axes on the ODrive can be controlled independently. ### State Machine -Each axis has an independent state machine. The default state after startup is `AXIS_STATE_IDLE`. The current state is indicated by `.current_state`. The user can request a new state by assigning a new value to `.requested_state`. +The current state of an axis is indicated by `.current_state`. The user can request a new state by assigning a new value to `.requested_state`. The default state after startup is `AXIS_STATE_IDLE`. 1. `AXIS_STATE_IDLE` Disable motor PWM and do nothing. 1. `AXIS_STATE_STARTUP_SEQUENCE` Run the [startup procedure](#startup-procedure). - 1. `AXIS_STATE_FULL_CALIBRATION_SEQUENCE` Run motor calibration and then encoder offset calibration (or encoder index search if `.encoder.use_index` is `True`). + 1. `AXIS_STATE_FULL_CALIBRATION_SEQUENCE` Run motor calibration and then encoder offset calibration (or encoder index search if `.encoder.use_index` is `True`). 1. `AXIS_STATE_MOTOR_CALIBRATION` Measure phase resistance and phase inductance. - * To store the results set `.motor.config.pre_calibrated` to `True` and [save the configuration](#saving-the-configuration). After that you don't have to run the motor calibration on the next start up. - * This modifies the variables `.motor.config.phase_resistance` and `.motor.config.phase_inductance`. + * To store the results set `.motor.config.pre_calibrated` to `True` and [save the configuration](#saving-the-configuration). After that you don't have to run the motor calibration on the next start up. + * This modifies the variables `.motor.config.phase_resistance` and `.motor.config.phase_inductance`. 1. `AXIS_STATE_SENSORLESS_CONTROL` Run sensorless control. - * The motor must be calibrated (`.motor.is_calibrated`) - * [`.controller.control_mode`](#control-mode) must be `True`. - 1. `AXIS_STATE_ENCODER_INDEX_SEARCH` Turn the motor in one direction until the encoder index is traversed. This state can only be entered if `.encoder.config.use_index` is `True`. + * The motor must be calibrated (`.motor.is_calibrated`) + * [`.controller.control_mode`](#control-mode) must be `True`. + 1. `AXIS_STATE_ENCODER_INDEX_SEARCH` Turn the motor in one direction until the encoder index is traversed. This state can only be entered if `.encoder.config.use_index` is `True`. 1. `AXIS_STATE_ENCODER_OFFSET_CALIBRATION` Turn the motor in one direction for a few seconds and then back to measure the offset between the encoder position and the electrical phase. - * Can only be entered if the motor is calibrated (`.motor.is_calibrated`). - * A successful encoder calibration will make the `.encoder.is_ready` go to true. + * Can only be entered if the motor is calibrated (`.motor.is_calibrated`). + * A successful encoder calibration will make the `.encoder.is_ready` go to true. 1. `AXIS_STATE_CLOSED_LOOP_CONTROL` Run closed loop control. * The action depends on the [control mode](#control-mode). - * Can only be entered if the motor is calibrated (`.motor.is_calibrated`) and the encoder is ready (`.encoder.is_ready`). + * Can only be entered if the motor is calibrated (`.motor.is_calibrated`) and the encoder is ready (`.encoder.is_ready`). ### Startup Procedure By default the ODrive takes no action at startup and goes to idle immediately. This behavior can be changed by modifying the following parameters: -* `.config.startup_motor_calibration` -* `.config.startup_encoder_index_search` -* `.config.startup_encoder_offset_calibration` -* `.config.startup_closed_loop_control` -* `.config.startup_sensorless_control` +* `.config.startup_motor_calibration` +* `.config.startup_encoder_index_search` +* `.config.startup_encoder_offset_calibration` +* `.config.startup_closed_loop_control` +* `.config.startup_sensorless_control` See [state machine](#state-machine) for a description of each state. @@ -62,10 +80,3 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu * Increase `pos_gain` by around 30% per iteration until you see some overshoot. * Back down `pos_gain` until you do not have overshoot anymore. * The integrator is not easily tuned, nor is it strictly required. Tune at your own discression. - -### Saving the configuration - -All variables that are part of a `[...].config` object can be saved to non-volatile memory on the ODrive so they persist after you remove power. The relevant commands are: - - * `odrv.save_configuration()`: Stores the configuration to persistent memory on the ODrive. - * `odrv.erase_configuration()`: Resets the configuration variables to their factory defaults. This only has an effect after a reboot. A side effect of this command is that motor control stops (in case it was running) and the USB communication breaks out temporarily. This is because erasing flash pages hangs the microcontroller for several seconds. diff --git a/docs/developer-guide.md b/docs/developer-guide.md index af7fe90a..fd509b9e 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -56,7 +56,7 @@ sudo add-apt-repository ppa:jonathonf/tup && sudo apt-get update && sudo apt-get ``` #### Arch Linux -``` +```bash sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils sudo pacman -S arm-none-eabi-gdb sudo pacman -S tup diff --git a/docs/encoders.md b/docs/encoders.md index 5e88b9e5..da0e748f 100644 --- a/docs/encoders.md +++ b/docs/encoders.md @@ -1,26 +1,47 @@ -# Motors & Encoders +# Encoders -[TODO: UPDATE] +## Known and Supported Encoders + +Encoder|Manufacturer|Output|Index|Max CPR|Max RPM|Voltage|Supported|Price|Link|Datasheet +:--|:--|:--|:-:|:-:|:-:|:-:|:--|:--|:--|:-- +AMT102|CUI|Quadrature|Yes|8192|-|5V|Yes|-|-|- +AMT203|CUI|Quadrature + Absolute|Yes|4096|-|5V|Quadrature Only|-|-|- +AS5047P|AMS|Quadrature + Absolute|Yes|4096|28k|3.3V or 5V|Quadrature Only|-|-|- +E6B2-CWZ6C|Omron|Quadrature|Yes|8000|-|5-24V|Yes|-|-|[Datasheet](http://www.ia.omron.com/products/family/487/) +J733|-|Quadrature|No|2400|-|5-24V|Yes|-|-|- ## Encoder Calibration -By default the encoder-to-motor calibration will run on every startup. During encoder calibration the rotor must be allowed to rotate without any biased load during startup. That means mass and weak friction loads are fine, but gravity or spring loads are not okay. -### Encoder with Index signal -If you have an encoder with an index (Z) signal, you may avoid having to do the calibration on every startup, and instead use the index signal to re-sync the encoder to a stored calibration. Below are the steps to do the one-time calibration and configuration. Note that you can follow these steps with one motor at a time, or all motors together, as you wish. +All encoder types that are currently supported require the ODrive to do some sort of encoder calibration at every startup before you can run the motor control. Take this into account when designing your application. + + +### Encoder without index signal + +During encoder offset calibration the rotor must be allowed to rotate without any biased load during startup. That means mass and weak friction loads are fine, but gravity or spring loads are not okay. + +In the `odrivetool`, type `.encoder.requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION` Enter. + +To verify everything went well, check the following variables: + + * `.error` should be 0. + * `.encoder.config.offset` - This should print a number, like -326 or 1364. + * `.motor.config.direction` - This should print 1 or -1. + +### Encoder with index signal +If you have an encoder with an index (Z) signal, you may avoid having to do the offset calibration on every startup, and instead use the index signal to re-sync the encoder to a stored calibration. + +Below are the steps to do the one-time calibration and configuration. Note that you can follow these steps with one motor at a time, or all motors together, as you wish. * Since you will only do this once, it is recommended that you mechanically disengage the motor from anything other than the encoder, so it can spin freely. -* 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. -* 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. -* 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`. -* Flash this configuration and check that the motor scans for the index pulse but skips the encoder calibration. -* Congratulations, you are now done. You may now attach the motor to your mechanical load. +* Set `.encoder.config.use_index` to `True`. +* Run `.requested_state = AXIS_STATE_ENCODER_INDEX_SEARCH`. This will make the motor turn in one direction until it finds the encoder index. +* Follow the calibration instructions for an [encoder without index signal](#encoder-without-index-signal). +* Set `.encoder.config.pre_calibrated` to `True` to confirm that the offset is valid with respect to the index pulse. +* In case you didn't already, also set `.motor.config.pre_calibrated` to `True`. +* Set `.config.startup_encoder_index_search` to `True`. +* Save the configuration by typing `.save_configuration()` Enter. +That's it, now on every reboot the motor will turn in one direction until it finds the encoder index. -* If you wish to scan for the index pulse in the other direction (if for example your axis usually starts close to a hard-stop), you can set a negative value in `.encoder.config.idx_search_speed`. -* If your motor has problems reaching the index location due to the mechanical load, you can increase `.encoder.calibration_current`. +* If you wish to scan for the index pulse in the other direction (if for example your axis usually starts close to a hard-stop), you can set a negative value in `.encoder.config.idx_search_speed`. +* If your motor has problems reaching the index location due to the mechanical load, you can increase `.motor.config.calibration_current`. diff --git a/docs/interfaces.md b/docs/interfaces.md index 0f495f7d..33503cc1 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -1,6 +1,6 @@ # Interfaces -**Warning:** While developing custom ODrive control code it is recommend that your motors are free to spin continuously and are not connected to a drivetrain with limited travel. +
While developing custom ODrive control code it is recommend that your motors are free to spin continuously and are not connected to a drivetrain with limited travel.
The ODrive can be controlled over various interfaces and protocols. @@ -23,17 +23,17 @@ The ODrive tools you installed earlier come with a library that you can use to e Assuming you already installed the odrive library (`pip install odrive`), the simplest program to control the ODrive is this: -``` +```python import odrive.discovery odrv0 = odrive.discovery.find_any() print(str(odrv0.vbus_voltage)) ``` -For a more detailed example, see [odrive_demo.py](tools/odrive_demo.py). +For a more detailed example, see [odrive_demo.py](../tools/odrive_demo.py). ### Other languages -We don't have an official library for you just yet. Check the community, there might be someone working on it. If you want to write a library yourself, refer to the [native protocol specification](protocol.md). You are of course welcome to contribute it back. +We don't have an official library for you just yet. Check the community, there might be someone working on it. If you want to write a library yourself, refer to the [native protocol specification](protocol). You are of course welcome to contribute it back. ## ASCII protocol @@ -60,9 +60,9 @@ Pinout: Please note that GPIO_3 and GPIO_4 are NOT 5v tolerant on ODrive v3.2 and earlier, so 3.3V signals only! ODrive v3.3 and onward have 5V tolerant GPIO pins. -To enable step/dir mode for the GPIO, please see [Setting the GPIO mode](Firmware/README.md#communication-configuration). +To enable step/dir mode for the GPIO, set `.config.enable_step_dir` to true and reboot the ODrive. -There is also a config variable called `counts_per_step`, which specifies how many encoder counts a "step" corresponds to. It can be any floating point value. +There is also a config variable called `.config.counts_per_step`, which specifies how many encoder counts a "step" corresponds to. It can be any floating point value. The maximum step rate is pending tests, but it should handle at least 16kHz. If you want to test it, please be aware that the failure mode on too high step rates is expected to be that the motors shuts down and coasts. Please be aware that there is no enable line right now, and the step/direction interface is enabled by default, and remains active as long as the ODrive is in position control mode. By default the ODrive starts in position control mode, so you don't need to send any commands over USB to get going. You can still send USB commands if you want to. diff --git a/docs/odrivetool.md b/docs/odrivetool.md index f1bb3056..3786e8a2 100644 --- a/docs/odrivetool.md +++ b/docs/odrivetool.md @@ -18,6 +18,6 @@ If you have multiple ODrives connected, you should specify which one to connect Note: ODrive v3.4 and earlier require you to flash with the external programmer first (see below), before you can reflash in standalone mode. * Run `make dfu` in the `Firmware` directory. -* __Windows__: During the update, a new device called "STM32 BOOTLOADER" will appear. Open the [Zadig](http://zadig.akeo.ie/) utility and set the driver for "STM32 BOOTLOADER" to libusb-win32. After that the firmware update will continue. +* __Windows__: During the update, a new device called "STM32 BOOTLOADER" will appear. Open the [Zadig utility](http://zadig.akeo.ie/) and set the driver for "STM32 BOOTLOADER" to libusb-win32. After that the firmware update will continue. * On some machines you will need to unplug and plug back in the USB cable to make the PC understand that we switched from regular mode to bootloader mode. * Currently a firmware update will preserve the configuration if and only if the parameters of both firmware versions are identical. This will change in the future. diff --git a/Firmware/protocol.md b/docs/protocol.md similarity index 100% rename from Firmware/protocol.md rename to docs/protocol.md diff --git a/docs/quick-start.md b/docs/quick-start.md index 15934295..22356f59 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -73,7 +73,7 @@ Most instructions in this guide refer to a utility called `odrivetool`, so you s 1. [Install Python 3](https://www.python.org/downloads/). 2. Install the ODrive tools by opening a terminal and typing `pip install odrive` Enter 3. __Linux__: set up USB permissions -``` +```bash echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="0d[0-9][0-9]", MODE="0666"' | sudo tee /etc/udev/rules.d/50-odrive.rules sudo udevadm control --reload-rules sudo udevadm trigger # until you reboot you may need to do this everytime you reset the ODrive @@ -83,7 +83,7 @@ Most instructions in this guide refer to a utility called `odrivetool`, so you s To launch the main interactive ODrive tool, type `odrivetool` Enter. Connect your ODrive and wait for the tool to find it. Now you can for instance type `odrv0.vbus_voltage` Enter to inpect the boards main supply voltage. It should look something like this: -``` +```python ODrive control utility v0.4.0 Please connect your ODrive. Type help() for help. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 956480fb..ad536ab2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,11 +1,17 @@ # Troubleshooting ## Error codes -If your ODrive is not working as expected, run `odrivetool` and type `hex(odrv0.axis0.error)` Enter. This will display a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) representation of the error code. Each bit represents one error flag. +If your ODrive is not working as expected, run `odrivetool` and type `hex(.error)` Enter where `` is the axis that isn't working. This will display a [hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) representation of the error code. Each bit represents one error flag.
Example
-Say you typed `hex(odrv0.axis0.error)` and got `0x6` as a result. Written in binary this number corresponds to `110`, so bits 1 and 2 are set. +Say you got this error output: +```python +In [1]: hex(odrv0.axis0.error) +Out[1]: '0x6' +``` + +Written in binary, the number `0x6` corresponds to `110`, that means bits 1 and 2 are set (counting starts at 0). Looking at the reference below, this means that both `ERROR_DC_BUS_UNDER_VOLTAGE` and `ERROR_DC_BUS_OVER_VOLTAGE` occurred. @@ -31,114 +37,55 @@ Defined [here](../Firmware/MotorControl/axis.hpp) ### Motor error flags -Defined [here](MotorControl/motor.hpp) +Defined [here](../Firmware/MotorControl/motor.hpp) -0. ERROR_PHASE_RESISTANCE_OUT_OF_RANGE -1. ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE -2. ERROR_ADC_FAILED -3. ERROR_DRV_FAULT -4. ERROR_CONTROL_DEADLINE_MISSED -5. ERROR_NOT_IMPLEMENTED_MOTOR_TYPE -6. ERROR_BRAKE_CURRENT_OUT_OF_RANGE -7. ERROR_NUMERICAL +0. `ERROR_PHASE_RESISTANCE_OUT_OF_RANGE` +1. `ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE` +2. `ERROR_ADC_FAILED` +3. [`ERROR_DRV_FAULT`](#drv-fault) +4. `ERROR_CONTROL_DEADLINE_MISSED` +5. `ERROR_NOT_IMPLEMENTED_MOTOR_TYPE` +6. `ERROR_BRAKE_CURRENT_OUT_OF_RANGE` +7. `ERROR_NUMERICAL` ### Encoder error flags -Defined [here](MotorControl/encoder.hpp) +Defined [here](../Firmware/MotorControl/encoder.hpp) -0. ERROR_NUMERICAL -1. ERROR_CPR_OUT_OF_RANGE -2. ERROR_RESPONSE +0. `ERROR_NUMERICAL` +1. `ERROR_CPR_OUT_OF_RANGE` +2. `ERROR_RESPONSE` ### Sensorless estimator error flags -Defined [here](MotorControl/sensorless_estimator.hpp) +Defined [here](../Firmware/MotorControl/sensorless_estimator.hpp) -0. ERROR_NUMERICAL +0. `ERROR_NUMERICAL` -### Cannot connect to the ODrive +## DRV fault -ensure no other ODrive program is running -prepend `PYUSB_DEBUG=debug` to the script +The ODrive v3.4 is known to have a hardware issue whereby the motors would stop operating +when applying high currents to M0. The reported error of both motors in this case +is `ERROR_DRV_FAULT`. -## USB issues ## +The conjecture is that the high switching current creates large ripples in the +power supply of the DRV8301 gate driver chips, thus tripping its undervoltage +fault detection. -* Firmware: - 1. ODrive Firmware - 2. STM HAL code -* Electrical: - 1. ODrive hardware - 2. connection (cables, hub) -* PC side - 1. PC-side USB host controller - 2. kernel - 3. libusb driver - 4. libusb library - 5. PyUSB - 6. python code +* Limit the M0 current to 40A. The lowest current at which the DRV fault was observed is 45A on one test motor and 50A on another test motor. +* Refer to [this post](https://discourse.odriverobotics.com/t/drv-fault-on-odrive-v3-4/558) for instructions for a hardware fix +## USB Connectivity Issues -## DRV fault ## - -Hardware: 5330 (190kv), v3.4-48V, V_bus=12V -Settings: default gains, current_lim=50A, position control, stationary -Action: Applying a high torque manually -Result: Trips at I_q=45A (+- 2A) on the big motor, I_q=50A (+-1A) on the black motor, I_bus=8.3A (+-0.4), - -phase=-2.3 --2.7 (did not trip from -0.5 to -2.7 @ 50A) --2.8 --2.7 --2.4689412117004395 -0.7 -0.5 - -other odrive: --0.8651647567749023 - -Repeatability: about 5/5 - - -``` -ODrive control utility v0.3.7.dev -Please connect your ODrive. -Type help() for help. - -Connected to ODrive 385F324D3037 as odrv1 -In [1]: odrv1.axis0.requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION -In [2]: start_liveplotter(lambda: [odrv1.axis0.motor.current_control.Ibus, odrv1.axis0.motor.current_control.Iq_setpoint]) -In [4]: odrv1.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL -In [9]: odrv1.axis0.motor.config.current_lim=50 -In [11]: odrv1.axis0.controller.config.vel_limit -Out[11]: 20000.0 -In [12]: odrv1.axis0.controller.config.vel_gain -Out[12]: 0.0005000000237487257 -``` - - - -``` -In [9]: odrive.utils.print_drv_regs("M0",odrv1.axis0.motor) -M0: 0 -DRV Fault Code: 0 -Status Reg 1: 0 (0b00000000) -Status Reg 2: 1 (0b00000001) -Control Reg 1: 1360 (0b10101010000) -Control Reg 2: 8 (0b0001000) - -In [10]: odrive.utils.print_drv_regs("M1",odrv1.axis0.motor) -M1: 0 -DRV Fault Code: 0 -Status Reg 1: 0 (0b00000000) -Status Reg 2: 1 (0b00000001) -Control Reg 1: 1360 (0b10101010000) -Control Reg 2: 8 (0b0001000) - -In [11]: (hex(odrv1.axis0.error), hex(odrv1.axis0.motor.error)) -Out[11]: ('0x41', '0x18') - -In [12]: (hex(odrv1.axis1.error), hex(odrv1.axis1.motor.error)) -Out[12]: ('0x61', '0x10') -``` + * Try turning it on and off again (the ODrive, the script, the PC) + * Make sure you're using the latest firmware and python tools release + * **Linux**: Type `lsusb` to list all USB devices. Verify that your ODrive is listed. + * **Linux**: Make sure you [set up your udev rules](quick-start#downloading-and-installing-tools) correctly. + * **Windows**: Right-click on the start menu and open "Device Manager". Verify that your ODrive is listed. + * **Windows**: Use the [Zadig utility](http://zadig.akeo.ie/) to verify the driver is + * Ensure that no other ODrive program is running + * Run `odrivetools` with the `--verbose` command. + * Run `PYUSB_DEBUG=debug odrivetools` to get even more log output. + * If you're a developer you can use Wireshark to capture USB traffic.