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.
Files changed (49) hide show
  1. odrive/__init__.py +142 -0
  2. odrive/api_client.py +54 -0
  3. odrive/config.py +1296 -0
  4. odrive/crypto.py +369 -0
  5. odrive/data/brakeRs.json +18 -0
  6. odrive/data/drvs.json +82 -0
  7. odrive/data/encoders.json +86 -0
  8. odrive/data/motors.json +128 -0
  9. odrive/data/odrive-micro-x1.json +48 -0
  10. odrive/data/odrive-micro-x3.json +40 -0
  11. odrive/data/odrive-micro-x4.json +40 -0
  12. odrive/data/odrive-s1-x4.json +52 -0
  13. odrive/data/odrive-v4.4.json +58 -0
  14. odrive/data/schema-brakeR.json +6 -0
  15. odrive/data/schema-drv.json +6 -0
  16. odrive/data/schema-encoders.json +6 -0
  17. odrive/data/schema-motors.json +6 -0
  18. odrive/data/schema-odrive.json +5 -0
  19. odrive/data/schema.json +244 -0
  20. odrive/database.py +184 -0
  21. odrive/dfu.py +195 -0
  22. odrive/dfuse/DfuDevice.py +297 -0
  23. odrive/dfuse/__init__.py +1 -0
  24. odrive/enums.py +415 -0
  25. odrive/firmware.py +102 -0
  26. odrive/hw_version.py +67 -0
  27. odrive/legacy.py +73 -0
  28. odrive/legacy_config.py +123 -0
  29. odrive/legacy_dfu.py +442 -0
  30. odrive/lib/libodrive-windows-x64.dll +0 -0
  31. odrive/libodrive.py +325 -0
  32. odrive/pyfibre/fibre/__init__.py +4 -0
  33. odrive/pyfibre/fibre/libfibre-windows-amd64.dll +0 -0
  34. odrive/pyfibre/fibre/libfibre.py +1078 -0
  35. odrive/pyfibre/fibre/libwinpthread-1.dll +0 -0
  36. odrive/pyfibre/fibre/shell.py +162 -0
  37. odrive/pyfibre/fibre/utils.py +133 -0
  38. odrive/release_api.py +184 -0
  39. odrive/rich_text.py +134 -0
  40. odrive/shell.py +144 -0
  41. odrive/utils.py +881 -0
  42. odrive/version.py +1 -0
  43. odrive-0.6.9.dev0.data/scripts/odrive_demo.py +50 -0
  44. odrive-0.6.9.dev0.data/scripts/odrivetool +263 -0
  45. odrive-0.6.9.dev0.data/scripts/odrivetool.bat +2 -0
  46. odrive-0.6.9.dev0.dist-info/METADATA +22 -0
  47. odrive-0.6.9.dev0.dist-info/RECORD +49 -0
  48. odrive-0.6.9.dev0.dist-info/WHEEL +5 -0
  49. odrive-0.6.9.dev0.dist-info/top_level.txt +1 -0
@@ -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