odrive 0.6.9.dev0__py37-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odrive/__init__.py +142 -0
- odrive/api_client.py +54 -0
- odrive/config.py +1296 -0
- odrive/crypto.py +369 -0
- odrive/data/brakeRs.json +18 -0
- odrive/data/drvs.json +82 -0
- odrive/data/encoders.json +86 -0
- odrive/data/motors.json +128 -0
- odrive/data/odrive-micro-x1.json +48 -0
- odrive/data/odrive-micro-x3.json +40 -0
- odrive/data/odrive-micro-x4.json +40 -0
- odrive/data/odrive-s1-x4.json +52 -0
- odrive/data/odrive-v4.4.json +58 -0
- odrive/data/schema-brakeR.json +6 -0
- odrive/data/schema-drv.json +6 -0
- odrive/data/schema-encoders.json +6 -0
- odrive/data/schema-motors.json +6 -0
- odrive/data/schema-odrive.json +5 -0
- odrive/data/schema.json +244 -0
- odrive/database.py +184 -0
- odrive/dfu.py +195 -0
- odrive/dfuse/DfuDevice.py +297 -0
- odrive/dfuse/__init__.py +1 -0
- odrive/enums.py +415 -0
- odrive/firmware.py +102 -0
- odrive/hw_version.py +67 -0
- odrive/legacy.py +73 -0
- odrive/legacy_config.py +123 -0
- odrive/legacy_dfu.py +442 -0
- odrive/lib/libodrive-windows-x64.dll +0 -0
- odrive/libodrive.py +325 -0
- odrive/pyfibre/fibre/__init__.py +4 -0
- odrive/pyfibre/fibre/libfibre-windows-amd64.dll +0 -0
- odrive/pyfibre/fibre/libfibre.py +1078 -0
- odrive/pyfibre/fibre/libwinpthread-1.dll +0 -0
- odrive/pyfibre/fibre/shell.py +162 -0
- odrive/pyfibre/fibre/utils.py +133 -0
- odrive/release_api.py +184 -0
- odrive/rich_text.py +134 -0
- odrive/shell.py +144 -0
- odrive/utils.py +881 -0
- odrive/version.py +1 -0
- odrive-0.6.9.dev0.data/scripts/odrive_demo.py +50 -0
- odrive-0.6.9.dev0.data/scripts/odrivetool +263 -0
- odrive-0.6.9.dev0.data/scripts/odrivetool.bat +2 -0
- odrive-0.6.9.dev0.dist-info/METADATA +22 -0
- odrive-0.6.9.dev0.dist-info/RECORD +49 -0
- odrive-0.6.9.dev0.dist-info/WHEEL +5 -0
- odrive-0.6.9.dev0.dist-info/top_level.txt +1 -0
odrive/legacy_config.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import List
|
|
6
|
+
import fibre.libfibre
|
|
7
|
+
import odrive
|
|
8
|
+
from odrive.utils import OperationAbortedException, yes_no_prompt
|
|
9
|
+
|
|
10
|
+
def _property_dict(device):
|
|
11
|
+
def _impl(prefix, obj):
|
|
12
|
+
for k in dir(obj):
|
|
13
|
+
v = getattr(obj, k)
|
|
14
|
+
if k.startswith('_') and k.endswith('_property'):
|
|
15
|
+
yield '.'.join(prefix + [k[1:-9]]), v
|
|
16
|
+
elif not k.startswith('_') and isinstance(v, fibre.libfibre.RemoteObject):
|
|
17
|
+
yield from _impl(prefix + [k], v)
|
|
18
|
+
return {k: v for k, v in list(_impl([], device))}
|
|
19
|
+
|
|
20
|
+
def _flatten(prefix: List[str], config: dict):
|
|
21
|
+
for k, v in config.items():
|
|
22
|
+
if isinstance(v, dict):
|
|
23
|
+
yield from _flatten(prefix + [k], v)
|
|
24
|
+
else:
|
|
25
|
+
yield '.'.join(prefix + [k]), v
|
|
26
|
+
|
|
27
|
+
def restore_config(device, config: dict):
|
|
28
|
+
"""
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
device: The ODrive to write the config to.
|
|
32
|
+
config: A dictionary of the form {path: value}
|
|
33
|
+
"""
|
|
34
|
+
errors = []
|
|
35
|
+
prop_dict = _property_dict(device)
|
|
36
|
+
|
|
37
|
+
# flatten config dict for legacy compatibility
|
|
38
|
+
config = {k: v for k, v in _flatten([], config)}
|
|
39
|
+
|
|
40
|
+
for name, v in config.items():
|
|
41
|
+
try:
|
|
42
|
+
remote_attribute = prop_dict[name]
|
|
43
|
+
if isinstance(v, str) and hasattr(type(remote_attribute), 'exchange') and type(remote_attribute).exchange._inputs[1][1] == 'object_ref':
|
|
44
|
+
v = prop_dict[v]
|
|
45
|
+
remote_attribute.exchange(v)
|
|
46
|
+
except Exception as ex:
|
|
47
|
+
errors.append("Could not restore {}: {}".format(name, str(ex)))
|
|
48
|
+
|
|
49
|
+
return errors
|
|
50
|
+
|
|
51
|
+
def backup_config(device) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
Returns a dict of the form {path: value} containing all properties on the
|
|
54
|
+
ODrive that have "config" in their path.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
device: The device to read from
|
|
59
|
+
"""
|
|
60
|
+
prop_dict = _property_dict(device)
|
|
61
|
+
result = {}
|
|
62
|
+
|
|
63
|
+
for k, prop in prop_dict.items():
|
|
64
|
+
if ".config." in f".{k}.":
|
|
65
|
+
val = prop.read()
|
|
66
|
+
if isinstance(val, fibre.libfibre.RemoteObject):
|
|
67
|
+
val = prop_dict[val]
|
|
68
|
+
print("path:", val)
|
|
69
|
+
result[k] = val
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_temp_config_filename(device):
|
|
75
|
+
serial_number = odrive.get_serial_number_str_sync(device)
|
|
76
|
+
safe_serial_number = ''.join(filter(str.isalnum, serial_number))
|
|
77
|
+
return os.path.join(tempfile.gettempdir(), 'odrive-config-{}.json'.format(safe_serial_number))
|
|
78
|
+
|
|
79
|
+
def backup_config_ui(device, filename, logger):
|
|
80
|
+
"""
|
|
81
|
+
Exports the configuration of an ODrive to a JSON file.
|
|
82
|
+
If no file name is provided, the file is placed into a
|
|
83
|
+
temporary directory.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if filename is None:
|
|
87
|
+
filename = get_temp_config_filename(device)
|
|
88
|
+
|
|
89
|
+
logger.info("Saving configuration to {}...".format(filename))
|
|
90
|
+
|
|
91
|
+
if os.path.exists(filename):
|
|
92
|
+
if not yes_no_prompt("The file {} already exists. Do you want to override it?".format(filename), True):
|
|
93
|
+
raise OperationAbortedException()
|
|
94
|
+
|
|
95
|
+
data = backup_config(device)
|
|
96
|
+
with open(filename, 'w') as file:
|
|
97
|
+
json.dump(data, file, indent=2)
|
|
98
|
+
logger.info("Configuration saved.")
|
|
99
|
+
|
|
100
|
+
def restore_config_ui(device, filename, logger):
|
|
101
|
+
"""
|
|
102
|
+
Restores the configuration stored in a file
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
if filename is None:
|
|
106
|
+
filename = get_temp_config_filename(device)
|
|
107
|
+
|
|
108
|
+
with open(filename) as file:
|
|
109
|
+
data = json.load(file)
|
|
110
|
+
|
|
111
|
+
logger.info("Restoring configuration from {}...".format(filename))
|
|
112
|
+
errors = restore_config(device, data)
|
|
113
|
+
|
|
114
|
+
for error in errors:
|
|
115
|
+
logger.info(error)
|
|
116
|
+
if errors:
|
|
117
|
+
logger.warn("Some of the configuration could not be restored.")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
device.save_configuration()
|
|
121
|
+
except fibre.libfibre.ObjectLostError:
|
|
122
|
+
pass # Saving configuration makes the device reboot
|
|
123
|
+
logger.info("Configuration restored.")
|
odrive/legacy_dfu.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Tool for flashing .hex files to the ODrive via the STM built-in USB DFU mode.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import asyncio
|
|
8
|
+
import concurrent
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Iterator, List, Tuple, Optional
|
|
11
|
+
import platform
|
|
12
|
+
import os
|
|
13
|
+
import usb.core
|
|
14
|
+
import fibre
|
|
15
|
+
import odrive
|
|
16
|
+
import odrive.legacy_config
|
|
17
|
+
import odrive.firmware
|
|
18
|
+
from odrive.utils import OperationAbortedException
|
|
19
|
+
from odrive.dfuse import *
|
|
20
|
+
from odrive.hw_version import HwVersion
|
|
21
|
+
import odrive.release_api as release_api
|
|
22
|
+
from odrive.release_api import VersionRelationship, format_version
|
|
23
|
+
from .dfu import DfuError, get_firmware
|
|
24
|
+
|
|
25
|
+
class ODriveInDfuMode(DfuDevice):
|
|
26
|
+
def __init__(self, usbdev):
|
|
27
|
+
DfuDevice.__init__(self, usbdev)
|
|
28
|
+
self.board = None
|
|
29
|
+
self._initialized = False
|
|
30
|
+
|
|
31
|
+
def init(self, logger, ask):
|
|
32
|
+
if self._initialized:
|
|
33
|
+
return
|
|
34
|
+
self._initialized = True
|
|
35
|
+
DfuDevice.init(self)
|
|
36
|
+
|
|
37
|
+
is_odrive3 = ('OTP Memory' in self.memories) and (self.memories['OTP Memory']['sectors'][0]['addr'] == 0x1fff7800)
|
|
38
|
+
if is_odrive3:
|
|
39
|
+
otp_sectors = self.memories['OTP Memory']['sectors']
|
|
40
|
+
else:
|
|
41
|
+
otp_sectors = None
|
|
42
|
+
|
|
43
|
+
if logger._verbose and is_odrive3:
|
|
44
|
+
logger.debug("OTP:")
|
|
45
|
+
|
|
46
|
+
self.select_memory('OTP Memory')
|
|
47
|
+
self.clear_status()
|
|
48
|
+
|
|
49
|
+
# 512 Byte OTP
|
|
50
|
+
otp_sector = [s for s in otp_sectors if s['addr'] == 0x1fff7800][0]
|
|
51
|
+
data = self.read_sector(otp_sector)
|
|
52
|
+
logger.debug(' '.join('{:02X}'.format(x) for x in data))
|
|
53
|
+
|
|
54
|
+
# 16 lock bytes
|
|
55
|
+
otp_lock_sector = [s for s in otp_sectors if s['addr'] == 0x1fff7A00][0]
|
|
56
|
+
data = self.read_sector(otp_lock_sector)
|
|
57
|
+
logger.debug(' '.join('{:02X}'.format(x) for x in data))
|
|
58
|
+
|
|
59
|
+
if is_odrive3:
|
|
60
|
+
# Reads the hardware version from one-time-programmable memory.
|
|
61
|
+
# This is written on all ODrives sold since Summer 2018.
|
|
62
|
+
otp_sectors = self.memories['OTP Memory']['sectors']
|
|
63
|
+
otp_sector = [s for s in otp_sectors if s['addr'] == 0x1fff7800][0]
|
|
64
|
+
self.select_memory('OTP Memory')
|
|
65
|
+
self.clear_status()
|
|
66
|
+
otp_data = self.read_sector(otp_sector)
|
|
67
|
+
if otp_data[0] == 0:
|
|
68
|
+
otp_data = otp_data[16:]
|
|
69
|
+
if otp_data[0] == 0xfe:
|
|
70
|
+
self.board = HwVersion(otp_data[3], otp_data[4], otp_data[5])
|
|
71
|
+
else:
|
|
72
|
+
self.board = HwVersion(3, 0, 0)
|
|
73
|
+
else:
|
|
74
|
+
if ask:
|
|
75
|
+
guess = HwVersion(4, 4, 58) if 'OTP Memory' in self.memories else HwVersion(5, 2, 0)
|
|
76
|
+
if not odrive.utils.yes_no_prompt(f'Hardware version detection not yet supported for this ODrive. Is this an {guess.display_name}?', False):
|
|
77
|
+
raise DfuError("Could not determine hardware version.")
|
|
78
|
+
self.board = guess
|
|
79
|
+
else:
|
|
80
|
+
self.board = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DfuDeviceDiscovery():
|
|
84
|
+
def __init__(self):
|
|
85
|
+
self._executor = None
|
|
86
|
+
self._usb_lock = threading.Lock()
|
|
87
|
+
|
|
88
|
+
def _get_devices(self):
|
|
89
|
+
# Note that this function can interfere with DFU if run at the same time
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
import libusb_package
|
|
93
|
+
backend = libusb_package.get_libusb1_backend()
|
|
94
|
+
except ModuleNotFoundError:
|
|
95
|
+
backend = None # use PyUSB discovery
|
|
96
|
+
|
|
97
|
+
with self._usb_lock:
|
|
98
|
+
all_devices = {}
|
|
99
|
+
for dev in usb.core.find(idVendor=0x0483, idProduct=0xdf11, find_all=True, backend=backend):
|
|
100
|
+
try:
|
|
101
|
+
serial_number = dev.serial_number
|
|
102
|
+
except ValueError:
|
|
103
|
+
print("found device but could not check serial number (retrying in 1s)")
|
|
104
|
+
continue
|
|
105
|
+
all_devices[serial_number] = ODriveInDfuMode(dev)
|
|
106
|
+
return all_devices
|
|
107
|
+
|
|
108
|
+
async def __aenter__(self):
|
|
109
|
+
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1).__enter__()
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
113
|
+
self._executor.__exit__(exc_type, exc_val, exc_tb)
|
|
114
|
+
|
|
115
|
+
async def discover(self, serial_number: str):
|
|
116
|
+
while True:
|
|
117
|
+
devices = await asyncio.get_running_loop().run_in_executor(self._executor, self._get_devices)
|
|
118
|
+
|
|
119
|
+
if serial_number is None and len(devices) > 0:
|
|
120
|
+
return next(iter(devices.values()))
|
|
121
|
+
if (not serial_number is None) and (serial_number in devices):
|
|
122
|
+
return devices[serial_number]
|
|
123
|
+
|
|
124
|
+
await asyncio.sleep(1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def enter_dfu_mode(device, dfu_discoverer: DfuDeviceDiscovery) -> ODriveInDfuMode:
|
|
128
|
+
"""
|
|
129
|
+
Puts the specified device into (legacy) DFU mode.
|
|
130
|
+
"""
|
|
131
|
+
if not hasattr(device, "enter_dfu_mode"):
|
|
132
|
+
raise Exception(
|
|
133
|
+
"The firmware on device {:08X} cannot soft enter DFU mode.\n"
|
|
134
|
+
"Please remove power, put the DFU switch into DFU mode,\n"
|
|
135
|
+
"then apply power again. Then try again.\n"
|
|
136
|
+
"If it still doesn't work, you can try to use the DeFuse app or \n"
|
|
137
|
+
"dfu-util, see the odrive documentation.\n"
|
|
138
|
+
"You can also flash the firmware using STLink (`make flash`)"
|
|
139
|
+
.format(device.serial_number)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
serial_number = "{:08X}".format(device.serial_number)
|
|
143
|
+
print("Putting device {:08X} into DFU mode...".format(device.serial_number))
|
|
144
|
+
|
|
145
|
+
# If the new DFU system is already installed, need to disable bootloader to
|
|
146
|
+
# be able to enter legacy DFU mode.
|
|
147
|
+
# A warning will have been shown earlier in this case.
|
|
148
|
+
if hasattr(device, 'disable_bootloader'):
|
|
149
|
+
device.disable_bootloader()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = device.enter_dfu_mode()
|
|
153
|
+
except fibre.ObjectLostError:
|
|
154
|
+
result = True # this is expected because the device reboots
|
|
155
|
+
if not result:
|
|
156
|
+
raise DfuError("Could not put device into legacy DFU mode.")
|
|
157
|
+
|
|
158
|
+
dfu_device = dfu_discoverer.discover(serial_number)
|
|
159
|
+
|
|
160
|
+
if platform.system() == "Windows":
|
|
161
|
+
async with _show_deferred_message(10, "Still waiting for the device to reappear.\n" "Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32."):
|
|
162
|
+
return await dfu_device
|
|
163
|
+
else:
|
|
164
|
+
return await dfu_device
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def write_firmware(device, firmware: odrive.firmware.FirmwareFile, dfu_discoverer: DfuDeviceDiscovery, erase_all: bool, logger, installing_bootloader: bool):
|
|
168
|
+
"""
|
|
169
|
+
Puts the device into DFU mode (if it's not already in DFU mode), writes
|
|
170
|
+
the specified firmware file and then takes the device out of DFU mode.
|
|
171
|
+
|
|
172
|
+
erase_all: If True, the entire flash memory is erased, including NVM.
|
|
173
|
+
If False, only sectors that are overwritten is erased.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
if not isinstance(device, ODriveInDfuMode):
|
|
177
|
+
device = await enter_dfu_mode(device, dfu_discoverer)
|
|
178
|
+
|
|
179
|
+
device.init(logger, ask=False)
|
|
180
|
+
if device.board == HwVersion(3, 0, 0):
|
|
181
|
+
# Jump to application
|
|
182
|
+
device.jump_to_application(0x08000000)
|
|
183
|
+
raise DfuError(
|
|
184
|
+
"Could not determine hardware version. Flashing precompiled "
|
|
185
|
+
"firmware could lead to unexpected results. Please use an "
|
|
186
|
+
"STLink/2 to force-update the firmware anyway. Refer to "
|
|
187
|
+
"https://docs.odriverobotics.com/developer-guide for details.")
|
|
188
|
+
|
|
189
|
+
logger.debug("Memories on device: ")
|
|
190
|
+
for k, mem in device.memories.items():
|
|
191
|
+
logger.debug("{} sectors:".format(k))
|
|
192
|
+
for sector in mem['sectors']:
|
|
193
|
+
logger.debug(" {:08X} to {:08X}".format(
|
|
194
|
+
sector['addr'],
|
|
195
|
+
sector['addr'] + sector['len'] - 1))
|
|
196
|
+
|
|
197
|
+
# fill sectors with data
|
|
198
|
+
sections = list(firmware.get_flash_sections())
|
|
199
|
+
for name, addr, content in sections:
|
|
200
|
+
logger.debug(f"loading section {name} to 0x{addr:08x} ... 0x{(addr+len(content)):08x}")
|
|
201
|
+
|
|
202
|
+
touched_sectors = list(_populate_sectors(device.memories['Internal Flash']['sectors'], sections))
|
|
203
|
+
logger.debug("The following sectors will be flashed: ")
|
|
204
|
+
for sector,_ in touched_sectors:
|
|
205
|
+
logger.debug(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + sector['len'] - 1))
|
|
206
|
+
|
|
207
|
+
device.select_memory('Internal Flash')
|
|
208
|
+
device.clear_status()
|
|
209
|
+
|
|
210
|
+
# Erase
|
|
211
|
+
try:
|
|
212
|
+
internal_flash_sectors = device.memories['Internal Flash']['sectors']
|
|
213
|
+
if erase_all:
|
|
214
|
+
erase_sectors = internal_flash_sectors
|
|
215
|
+
else:
|
|
216
|
+
erase_sectors = [s for s, d in touched_sectors]
|
|
217
|
+
for i, sector in enumerate(erase_sectors):
|
|
218
|
+
print("Erasing... (sector {}/{}) \r".format(i, len(erase_sectors)), end='', flush=True)
|
|
219
|
+
device.erase_sector(sector)
|
|
220
|
+
print('Erasing... done \r', end='', flush=True)
|
|
221
|
+
finally:
|
|
222
|
+
print('', flush=True)
|
|
223
|
+
|
|
224
|
+
# Flash
|
|
225
|
+
try:
|
|
226
|
+
for i, (sector, data) in enumerate(touched_sectors):
|
|
227
|
+
print("Flashing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True)
|
|
228
|
+
device.write_sector(sector, data)
|
|
229
|
+
print('Flashing... done \r', end='', flush=True)
|
|
230
|
+
finally:
|
|
231
|
+
print('', flush=True)
|
|
232
|
+
|
|
233
|
+
# Verify
|
|
234
|
+
try:
|
|
235
|
+
for i, (sector, expected_data) in enumerate(touched_sectors):
|
|
236
|
+
print("Verifying... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True)
|
|
237
|
+
observed_data = device.read_sector(sector)
|
|
238
|
+
mismatch_pos = _get_first_mismatch_index(observed_data, expected_data)
|
|
239
|
+
if not mismatch_pos is None:
|
|
240
|
+
mismatch_pos -= mismatch_pos % 16
|
|
241
|
+
observed_snippet = ' '.join('{:02X}'.format(x) for x in observed_data[mismatch_pos:mismatch_pos+16])
|
|
242
|
+
expected_snippet = ' '.join('{:02X}'.format(x) for x in expected_data[mismatch_pos:mismatch_pos+16])
|
|
243
|
+
raise RuntimeError("Verification failed around address 0x{:08X}:\n".format(sector['addr'] + mismatch_pos) +
|
|
244
|
+
" expected: " + expected_snippet + "\n"
|
|
245
|
+
" observed: " + observed_snippet)
|
|
246
|
+
print('Verifying... done \r', end='', flush=True)
|
|
247
|
+
finally:
|
|
248
|
+
print('', flush=True)
|
|
249
|
+
|
|
250
|
+
# Jump to application
|
|
251
|
+
device.jump_to_application(0x08000000)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def unlock_device(serial_number, cancellation_token):
|
|
255
|
+
# TODO: this function is outdated
|
|
256
|
+
|
|
257
|
+
print("Looking for ODrive in DFU mode...")
|
|
258
|
+
print("If the program hangs at this point, try to set the DFU switch to \"DFU\" and power cycle the ODrive.")
|
|
259
|
+
|
|
260
|
+
stm_device = find_device_in_dfu_mode(serial_number, cancellation_token)
|
|
261
|
+
dfudev = DfuDevice(stm_device)
|
|
262
|
+
|
|
263
|
+
print("Unlocking device (this may take a few seconds)...")
|
|
264
|
+
dfudev.unprotect()
|
|
265
|
+
print("done")
|
|
266
|
+
print("")
|
|
267
|
+
print("Now do the following:")
|
|
268
|
+
print(" 1. Put the DFU switch on the ODrive to \"DFU\"")
|
|
269
|
+
print(" 2. Power-cycle the ODrive")
|
|
270
|
+
print(" 3. Run \"odrivetool dfu\" (or any third party DFU tool)")
|
|
271
|
+
print(" 4. Put the DFU switch on the ODrive to \"RUN\"")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def launch_dfu(serial_number: Optional[str], path: Optional[str], channel: Optional[str], version: Optional[str], erase_all: bool, logger, force: bool = False, installing_bootloader: bool = False, release_type: str = 'firmware'):
|
|
275
|
+
"""
|
|
276
|
+
Runs the complete interactive DFU process:
|
|
277
|
+
|
|
278
|
+
1. Wait for device in either DFU mode or normal mode. If `serial_number` is
|
|
279
|
+
None, the first discovered device is selected, otherwise only the
|
|
280
|
+
specified device is accepted.
|
|
281
|
+
|
|
282
|
+
2. If `path` is None, check for the latest firmware, present it to the user
|
|
283
|
+
and ask whether to continue. Otherwise don't ask and always continue.
|
|
284
|
+
|
|
285
|
+
3. If the device is in normal mode, put it into DFU mode.
|
|
286
|
+
|
|
287
|
+
4. Write flash memory.
|
|
288
|
+
|
|
289
|
+
5. Exit DFU mode.
|
|
290
|
+
|
|
291
|
+
path: Path to a .elf path or None to check online.
|
|
292
|
+
channel: Channel on which to check for firmware (master, devel, ...)
|
|
293
|
+
version: Exact firmware version
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
assert sum([bool(path), bool(channel), bool(version)]) == 1
|
|
297
|
+
|
|
298
|
+
async with DfuDeviceDiscovery() as dfu_discoverer:
|
|
299
|
+
logger.info("Waiting for ODrive...")
|
|
300
|
+
|
|
301
|
+
# Wait for device either in DFU mode or in normal mode, whichever is
|
|
302
|
+
# found first.
|
|
303
|
+
done, pending = await asyncio.wait([
|
|
304
|
+
asyncio.create_task(odrive.find_any_async(serial_number=serial_number)),
|
|
305
|
+
asyncio.create_task(dfu_discoverer.discover(serial_number=serial_number))
|
|
306
|
+
], return_when=asyncio.FIRST_COMPLETED)
|
|
307
|
+
for t in pending:
|
|
308
|
+
t.cancel()
|
|
309
|
+
try:
|
|
310
|
+
await t
|
|
311
|
+
except asyncio.CancelledError:
|
|
312
|
+
pass
|
|
313
|
+
device = (await asyncio.gather(*done))[0]
|
|
314
|
+
|
|
315
|
+
found_in_dfu = isinstance(device, ODriveInDfuMode)
|
|
316
|
+
|
|
317
|
+
if not found_in_dfu:
|
|
318
|
+
current_fw_version = (
|
|
319
|
+
(device.fw_version_major, device.fw_version_minor, device.fw_version_revision)
|
|
320
|
+
if hasattr(device, "fw_version_major") and hasattr(device, "fw_version_minor") and hasattr(device, "fw_version_revision")
|
|
321
|
+
else (0, 0, 0)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
hw_version_major = device.hw_version_major if hasattr(device, "hw_version_major") else 0
|
|
325
|
+
|
|
326
|
+
buggy_firmwares = {(0, 6, 5), (0, 6, 6)}
|
|
327
|
+
|
|
328
|
+
if hw_version_major == 0 or current_fw_version == (0, 0, 0) or (hw_version_major == 4 and (current_fw_version in buggy_firmwares)):
|
|
329
|
+
raise DfuError(
|
|
330
|
+
"The firmware on device {:08X} does not support starting a firmware update from\n"
|
|
331
|
+
"RUN mode. Please remove power, put the DFU switch into DFU mode, then apply power\n"
|
|
332
|
+
"again. Then try again.\n"
|
|
333
|
+
"Refer to https://docs.odriverobotics.com/v/latest/guides/firmware-update.html for details."
|
|
334
|
+
.format(device.serial_number)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
has_new_dfu = hasattr(device, 'bootloader_version') and device.bootloader_version >= 0x00010000
|
|
338
|
+
if has_new_dfu and installing_bootloader:
|
|
339
|
+
if not odrive.utils.yes_no_prompt("Bootloader is already installed in this device. Do you want to re-install it?", True):
|
|
340
|
+
return
|
|
341
|
+
elif has_new_dfu:
|
|
342
|
+
if not odrive.utils.yes_no_prompt("This device is set up to use the new DFU system but you're running the legacy DFU command. "
|
|
343
|
+
"Try `odrivetool new-dfu` or https://gui.odriverobotics.com/dfu instead. "
|
|
344
|
+
"Do you want to proceed anyway with the legacy DFU system?", False):
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
if path:
|
|
348
|
+
assert os.path.isfile(path)
|
|
349
|
+
assert channel is None
|
|
350
|
+
file = odrive.firmware.FirmwareFile.from_file(path)
|
|
351
|
+
else:
|
|
352
|
+
if isinstance(device, ODriveInDfuMode):
|
|
353
|
+
device.init(logger, ask=True)
|
|
354
|
+
board = device.board if isinstance(device, ODriveInDfuMode) else device._board
|
|
355
|
+
build_id_short: Optional[str] = "{:08x}".format(device.commit_hash) if hasattr(device, 'commit_hash') else None
|
|
356
|
+
file = await get_firmware(board, build_id_short, channel, version, logger, not force, release_type)
|
|
357
|
+
|
|
358
|
+
may_have_config = found_in_dfu or (hasattr(device, 'user_config_loaded') and device.user_config_loaded)
|
|
359
|
+
if may_have_config and not force:
|
|
360
|
+
if not odrive.utils.yes_no_prompt("The device may have user configuration that will be lost after the firmware upgrade. If you want to back this up please run `odrivetool backup-config` first. Do you want to continue anyway?", True):
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
await write_firmware(device, file, dfu_discoverer, erase_all, logger, installing_bootloader)
|
|
364
|
+
|
|
365
|
+
if installing_bootloader:
|
|
366
|
+
logger.success("Bootloader upload successful.")
|
|
367
|
+
logger.info("To complete the setup, run `odrivetool new-dfu` or go to https://gui.odriverobotics.com/dfu.")
|
|
368
|
+
elif not found_in_dfu:
|
|
369
|
+
logger.info("Waiting for the device to reappear...")
|
|
370
|
+
device = await odrive.find_any_async(odrive.default_usb_search_path, serial_number)
|
|
371
|
+
logger.success("Device firmware update successful.")
|
|
372
|
+
else:
|
|
373
|
+
logger.success("Firmware upload successful.")
|
|
374
|
+
logger.info("To complete the firmware update, set the DFU switch to \"RUN\" and power cycle the board.")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _populate_sectors(sectors, sections: List[Tuple[int, bytes]]):
|
|
378
|
+
"""
|
|
379
|
+
Checks for which on-device sectors there is data in the hex file and
|
|
380
|
+
returns a (sector, data) tuple for each touched sector where data
|
|
381
|
+
is a byte array of the same size as the sector.
|
|
382
|
+
"""
|
|
383
|
+
for sector in sectors:
|
|
384
|
+
sector_addr = sector['addr']
|
|
385
|
+
sector_content = bytes([0xff]) * sector['len']
|
|
386
|
+
|
|
387
|
+
# check if any segment from the hexfile overlaps with this sector
|
|
388
|
+
touched = False
|
|
389
|
+
for section_name, section_addr, section_content in sections:
|
|
390
|
+
if section_addr + len(section_content) <= sector_addr:
|
|
391
|
+
continue # section is completely before current sector
|
|
392
|
+
if section_addr >= sector_addr + len(sector_content):
|
|
393
|
+
continue # section is completely after current sector
|
|
394
|
+
|
|
395
|
+
# prune start and end of section
|
|
396
|
+
if section_addr < sector_addr:
|
|
397
|
+
section_content = section_content[(sector_addr - section_addr):]
|
|
398
|
+
section_addr = sector_addr
|
|
399
|
+
if section_addr + len(section_content) > sector_addr + len(sector_content): # prune end
|
|
400
|
+
section_content = section_content[:(sector_addr + len(sector_content) - section_addr)]
|
|
401
|
+
|
|
402
|
+
# insert section data into sector
|
|
403
|
+
sector_content = sector_content[:(section_addr - sector_addr)] + section_content + sector_content[(section_addr - sector_addr + len(section_content)):]
|
|
404
|
+
touched = True
|
|
405
|
+
|
|
406
|
+
if touched:
|
|
407
|
+
yield (sector, sector_content)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _get_first_mismatch_index(array1, array2):
|
|
411
|
+
"""
|
|
412
|
+
Compares two arrays and returns the index of the
|
|
413
|
+
first unequal item or None if both arrays are equal
|
|
414
|
+
"""
|
|
415
|
+
if len(array1) != len(array2):
|
|
416
|
+
raise Exception("arrays must be same size")
|
|
417
|
+
for pos in range(len(array1)):
|
|
418
|
+
if (array1[pos] != array2[pos]):
|
|
419
|
+
return pos
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _show_deferred_message(delay: float, msg: str):
|
|
424
|
+
async def msg_loop():
|
|
425
|
+
while True:
|
|
426
|
+
await asyncio.sleep(delay)
|
|
427
|
+
print(msg)
|
|
428
|
+
|
|
429
|
+
class MsgLoopCtx():
|
|
430
|
+
def __init__(self):
|
|
431
|
+
self._task = None
|
|
432
|
+
async def __aenter__(self):
|
|
433
|
+
self._task = asyncio.create_task(msg_loop())
|
|
434
|
+
return self
|
|
435
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
436
|
+
self._task.cancel()
|
|
437
|
+
try:
|
|
438
|
+
await self._task
|
|
439
|
+
except asyncio.CancelledError:
|
|
440
|
+
pass # expected due to cancel() call above
|
|
441
|
+
|
|
442
|
+
return MsgLoopCtx()
|
|
Binary file
|