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
odrive/database.py ADDED
@@ -0,0 +1,184 @@
1
+ import json
2
+ import os
3
+ from odrive.hw_version import HwVersion
4
+
5
+ script_dir = os.path.dirname(os.path.realpath(__file__))
6
+
7
+ class NotFoundError(Exception):
8
+ pass
9
+
10
+ class Database():
11
+ def __init__(self, data):
12
+ self._data = data
13
+
14
+ def get_odrive_versions(self):
15
+ """
16
+ Returns all known ODrive board versions and their data as a collection
17
+ of tuples.
18
+ """
19
+ print("TODO: deprecated")
20
+ return self._data['odrives'].items()
21
+
22
+ def get_products(self):
23
+ """
24
+ Returns all known ODrive Robotics product line and version combinations.
25
+ The return type is a generator of tuples of the form
26
+ (product_name, product_data).
27
+ """
28
+ return [(k, v) for k, v in self._data['odrives'].items()]
29
+
30
+ def get_product(self, board: HwVersion):
31
+ """
32
+ Loads data for a particular ODrive Robotics board.
33
+ board: e.g. (4, 4, 58)
34
+ """
35
+ assert isinstance(board, HwVersion)
36
+ key = board
37
+ if not key in self._data['odrives']:
38
+ raise NotFoundError(f"{key} not found in product database")
39
+ return self._data['odrives'][key]
40
+
41
+ def get_motor(self, name: str):
42
+ """
43
+ Loads data for a particular motor model.
44
+ name: e.g. "D6374-150KV"
45
+ """
46
+ return self._data['motors'][name]
47
+
48
+ def get_brakeR(self, name: str):
49
+ """
50
+ Loads data for a particular brake resistor.
51
+ name: e.g. "500w2rj"
52
+ """
53
+ return self._data['brakeRs'][name]
54
+
55
+ def get_encoder(self, name: str):
56
+ """
57
+ Loads data for a particular encoder model.
58
+ name: e.g. "AMT10x"
59
+ """
60
+ return self._data['encoders'][name]
61
+
62
+ def get_encoders(self):
63
+ return list(self._data['encoders'].keys())
64
+
65
+
66
+ def _process_motor(motor):
67
+ if "kv" in motor:
68
+ motor["torque_constant"] = 8.27 / motor["kv"]
69
+ else:
70
+ motor["kv"] = 8.27 / motor["torque_constant"]
71
+
72
+ def _process_nothing(x):
73
+ pass
74
+
75
+
76
+ def load(path = None, validate = False):
77
+ """
78
+ path: Path of the database folder. If none, the path is detected automatically.
79
+ validate: Validates all JSON files that are being loaded against their schema.
80
+ If this feature is used jsonschema must be installed.
81
+ """
82
+
83
+ db_dir0 = os.path.join(script_dir, 'data') # When running from pip install
84
+ db_dir1 = os.path.join(os.path.dirname(os.path.dirname(script_dir)), 'data') # When running from Git repo
85
+
86
+ if path is None:
87
+ if os.path.isdir(db_dir0):
88
+ path = db_dir0
89
+ elif os.path.isdir(db_dir1):
90
+ path = db_dir1
91
+ else:
92
+ raise Exception("Database not found.")
93
+
94
+ data = {
95
+ 'odrives': {},
96
+ 'drvs': {},
97
+ 'motors': {},
98
+ 'encoders': {},
99
+ 'brakeRs': {}
100
+ }
101
+
102
+ loaders = {
103
+ 'odrive': [_process_nothing, None],
104
+ 'drv': [_process_nothing, None],
105
+ 'motor': [_process_motor, None],
106
+ 'encoder': [_process_nothing, None],
107
+ 'brakeR': [_process_nothing, None]
108
+ }
109
+
110
+ if validate:
111
+ import jsonschema
112
+ with open(os.path.join(path, "schema.json")) as fp:
113
+ schema = json.load(fp)
114
+ loaders['odrive'][1] = jsonschema.Draft4Validator({**schema, **{"$ref": "#/$defs/odrive"}})
115
+ loaders['drv'][1] = jsonschema.Draft4Validator({**schema, **{"$ref": "#/$defs/drv"}})
116
+ loaders['motor'][1] = jsonschema.Draft4Validator({**schema, **{"$ref": "#/$defs/motor"}})
117
+ loaders['encoder'][1] = jsonschema.Draft4Validator({**schema, **{"$ref": "#/$defs/encoder"}})
118
+ loaders['brakeR'][1] = jsonschema.Draft4Validator({**schema, **{"$ref": "#/$defs/brakeR"}})
119
+
120
+
121
+ for file in os.listdir(path):
122
+ name, ext = os.path.splitext(file)
123
+ file = os.path.join(path, file)
124
+ if os.path.isfile(file) and ext.lower() == '.json':
125
+ for k, (processor, validator) in loaders.items():
126
+ try:
127
+ if name.startswith(k + '-') and ext.lower() == '.json':
128
+ with open(file) as fp:
129
+ item = json.load(fp)
130
+ items = {item['name']: item}
131
+
132
+ elif name == k + 's':
133
+ with open(file) as fp:
134
+ items = json.load(fp)
135
+ assert(isinstance(items, dict))
136
+
137
+ else:
138
+ continue
139
+
140
+ if validate:
141
+ for item in items.values():
142
+ validator.validate(item)
143
+
144
+ for name, item in items.items():
145
+ processor(item)
146
+ data[k + 's'][name] = item
147
+
148
+ except Exception as ex:
149
+ raise Exception("error while processing " + file) from ex
150
+
151
+ # Postprocessing: load metadata of gate driver chip for each inverter
152
+ for odrive in data['odrives'].values():
153
+ for inv in odrive['inverters']:
154
+ if len(inv['drv'].keys()) == 1 and '$ref' in inv['drv'].keys():
155
+ inv['drv'] = data['drvs'][inv['drv']['$ref']]
156
+
157
+ # Postprocessing: transform product string keys to board version triplets
158
+ data['odrives'] = {
159
+ {
160
+ 'ODrive v3.6-24V': HwVersion(3, 6, 24),
161
+ 'ODrive v3.6-56V': HwVersion(3, 6, 56),
162
+ 'ODrive Pro v4.2-58V': HwVersion(4, 2, 58),
163
+ 'ODrive Pro v4.3-58V': HwVersion(4, 3, 58),
164
+ 'ODrive Pro v4.4-58V': HwVersion(4, 4, 58),
165
+ 'ODrive S1 X1': HwVersion(5, 0, 0),
166
+ 'ODrive S1 X3': HwVersion(5, 1, 0),
167
+ 'ODrive S1 X4': HwVersion(5, 2, 0),
168
+ 'ODrive Micro X1': HwVersion(6, 0, 0),
169
+ 'ODrive Micro X3': HwVersion(6, 1, 0),
170
+ 'ODrive Micro X4': HwVersion(6, 2, 0),
171
+ }[k]: v
172
+ for k, v in data['odrives'].items()
173
+ }
174
+
175
+ # Postprocessing: include inherited properties for each encoder
176
+ for key, encoder in list(data['encoders'].items()):
177
+ while 'inherits' in encoder:
178
+ inherited_encoder = data['encoders'][encoder['inherits']]
179
+ encoder.pop('inherits')
180
+ encoder = {**inherited_encoder, **encoder}
181
+ data['encoders'][key] = encoder
182
+
183
+ return Database(data)
184
+
odrive/dfu.py ADDED
@@ -0,0 +1,195 @@
1
+ import aiohttp
2
+ import asyncio
3
+ import os
4
+ from typing import Optional
5
+
6
+ import fibre
7
+ import odrive
8
+ import odrive.firmware
9
+ from odrive.release_api import VersionRelationship, format_version
10
+ from .hw_version import HwVersion
11
+ from .libodrive import Interface, LibODrive, Device, Firmware
12
+
13
+ class DfuError(Exception):
14
+ pass
15
+
16
+ async def enter_dfu_mode(device, discoverer: Interface) -> Device:
17
+ """
18
+ Puts the specified device into (new) DFU mode.
19
+ """
20
+ serial_number = "{:08X}".format(device.serial_number)
21
+ print("Putting device {:08X} into DFU mode...".format(device.serial_number))
22
+ try:
23
+ result = device.enter_dfu_mode2()
24
+ except fibre.ObjectLostError:
25
+ result = True # this is expected because the device reboots
26
+ if not result:
27
+ raise DfuError("Failed to enter DFU mode.")
28
+
29
+ device = await discoverer.wait_for(serial_number, is_bootloader=True)
30
+ await device.connect_bootloader()
31
+ return device
32
+
33
+ async def write_firmware(device: Device, fw: Firmware, erase_all: bool):
34
+ def print_progress(new_action_group: bool, action_string: str, action_index: int, n_actions: int):
35
+ if new_action_group and action_index != 0:
36
+ print()
37
+ print(f"DFU: {action_string} ", end='\r')
38
+ try:
39
+ await device.run_installation(fw, erase_all, print_progress)
40
+ finally:
41
+ print()
42
+
43
+ async def await_first(tasks):
44
+ """
45
+ Awaits the first of several tasks or futures and cancels the others.
46
+ """
47
+ # TODO: is there any standard way to do this in one line?
48
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
49
+ for t in pending:
50
+ t.cancel()
51
+ for t in pending:
52
+ try:
53
+ await t
54
+ except asyncio.CancelledError:
55
+ pass
56
+ return (await asyncio.gather(*done))[0]
57
+
58
+ async def get_firmware(board: HwVersion, current_build_id_short: Optional[str], channel: Optional[str], version: Optional[str], logger, interactive: bool, release_type: str = 'firmware'):
59
+ async with aiohttp.ClientSession() as session:
60
+ if channel:
61
+ logger.info(f"Checking online for latest {board.display_name} {release_type} on channel {channel}...")
62
+ else:
63
+ logger.info(f"Checking online for {board.display_name} {release_type} version {format_version(version)}...")
64
+ api_client = odrive.api_client.ApiClient(session)
65
+ release_api = odrive.release_api.ReleaseApi(api_client)
66
+
67
+ firmware_index = await release_api.get_index(release_type)
68
+
69
+ # If we're fetching normal firmware, use whatever file the release
70
+ # server returns as preferred file URL.
71
+ # If we're fetching the bootloader, need to select between multiple
72
+ # files on the release server.
73
+ file = 'bootloader_installer.elf' if release_type == 'bootloader' else None
74
+
75
+ try:
76
+ if channel:
77
+ manifest = firmware_index.get_latest(channel, app='default', board=board, file=file)
78
+ else:
79
+ manifest = firmware_index.get_version(version, app='default', board=board, file=file)
80
+ except odrive.release_api.ChannelNotFoundError as ex:
81
+ raise DfuError(ex)
82
+ except odrive.release_api.FirmwareNotFoundError:
83
+ raise DfuError(f"No {release_type} found matching the specified criteria.")
84
+
85
+ if interactive:
86
+ version_relationship = firmware_index.compare(current_build_id_short, manifest['commit_hash'], channel, app='default', board=board)
87
+ prompt = {
88
+ VersionRelationship.UNKNOWN: "Found compatible firmware ({to_version}). Install now?",
89
+ VersionRelationship.EQUAL: "Your current firmware ({to_version}) is up to date. Do you want to reinstall this version?",
90
+ VersionRelationship.UPGRADE: "Found new firmware ({from_hash} => {to_version}). Install now?",
91
+ VersionRelationship.DOWNGRADE: "Found older firmware ({from_hash} => {to_version}). Install now?",
92
+ }[version_relationship]
93
+
94
+ if not odrive.utils.yes_no_prompt(prompt.format(from_hash=current_build_id_short, to_version=format_version(manifest['commit_hash'])), True):
95
+ raise odrive.utils.OperationAbortedException()
96
+
97
+ logger.info(f"Downloading {release_type}...")
98
+ return odrive.firmware.FirmwareFile.from_file(await release_api.load(manifest))
99
+
100
+ async def run_dfu(libodrive: LibODrive, dfu_discoverer: Interface, serial_number: Optional[str], path: Optional[str], channel: Optional[str], version: Optional[str], erase_all: bool, logger, interactive: bool = True):
101
+ """
102
+ See dfu_ui for description.
103
+ """
104
+ assert sum([bool(path), bool(channel), bool(version)]) == 1
105
+
106
+ logger.info("Waiting for ODrive...")
107
+
108
+ # Wait for device either in DFU mode or in normal mode, whichever is
109
+ # found first.
110
+ device = await await_first([
111
+ asyncio.create_task(odrive.find_any_async(serial_number=serial_number)),
112
+ asyncio.create_task(dfu_discoverer.wait_for(serial_number=serial_number, is_bootloader=True))
113
+ ])
114
+
115
+ found_in_dfu = isinstance(device, Device)
116
+
117
+ if not found_in_dfu:
118
+ bootloader_version = device.bootloader_version if hasattr(device, "bootloader_version") else 0
119
+ if bootloader_version == 0:
120
+ raise DfuError(
121
+ "New DFU system not installed on device {:08X}.\n"
122
+ "Please follow instructions for one-time setup here:\n"
123
+ "https://docs.odriverobotics.com/v/latest/guides/new-dfu.html\n"
124
+ "or use the legacy DFU system (odrivetool legacy-dfu)."
125
+ .format(device.serial_number)
126
+ )
127
+
128
+ # Note: we don't do ahead of time compatibility check until the
129
+ # bootloader version has stabilized. Just-in-time checking is
130
+ # handled in libodrive (when bootloader is already started).
131
+
132
+ if found_in_dfu:
133
+ await device.connect_bootloader()
134
+
135
+ if path:
136
+ assert os.path.isfile(path)
137
+ assert channel is None
138
+ file = odrive.firmware.FirmwareFile.from_file(path)
139
+ else:
140
+ board: HwVersion = device.hw_version if isinstance(device, Device) else device._board
141
+ build_id_short: Optional[str] = "{:08x}".format(device.commit_hash) if hasattr(device, 'commit_hash') else None
142
+ file = await get_firmware(board, build_id_short, channel, version, logger, interactive)
143
+
144
+ with libodrive.open_firmware(file.as_buffer()) as firmware:
145
+ print("loaded firmware: ")
146
+ print(" Version: " + str(".".join(str(n) for n in firmware.fw_version)))
147
+ print(" Build ID: " + "".join(f"{b:02x}" for b in firmware.build))
148
+ print(" Hardware: " + firmware.hw_version.display_name)
149
+
150
+ if not isinstance(device, Device):
151
+ device = await enter_dfu_mode(device, dfu_discoverer)
152
+
153
+ assert device.is_bootloader
154
+ await write_firmware(device, firmware, erase_all)
155
+
156
+ logger.info("Waiting for the device to reappear...")
157
+ device = await odrive.find_any_async(odrive.default_usb_search_path, serial_number)
158
+ logger.success("Device firmware update successful.")
159
+
160
+
161
+ async def dfu_ui(serial_number: Optional[str], path: Optional[str], channel: Optional[str], version: Optional[str], erase_all: bool, logger, interactive: bool = True):
162
+ """
163
+ Runs the complete interactive DFU process:
164
+
165
+ 1. Wait for device in either DFU mode or normal mode. If `serial_number` is
166
+ None, the first discovered device is selected, otherwise only the
167
+ specified device is accepted.
168
+
169
+ 2. If `path` is None, check for the latest or specified firmware, present it
170
+ to the user and ask whether to continue. Otherwise don't ask and always
171
+ continue.
172
+
173
+ 3. If the device is in normal mode, put it into DFU mode.
174
+
175
+ 4. Erase, write and verify flash memory.
176
+
177
+ 5. Exit DFU mode.
178
+
179
+ Parameters
180
+ ----------
181
+ path: Path to a .elf path or None to check online.
182
+ channel: Channel on which to check for firmware (master, devel, ...)
183
+ version: Exact firmware version
184
+ """
185
+ libodrive = LibODrive(loop=asyncio.get_running_loop())
186
+
187
+ try:
188
+ with libodrive.start_usb_discovery() as dfu_discoverer:
189
+ libodrive._worker_thread.start()
190
+ try:
191
+ await run_dfu(libodrive, dfu_discoverer, serial_number, path, channel, version, erase_all, logger, interactive)
192
+ finally:
193
+ libodrive.stop_thread()
194
+ finally:
195
+ libodrive.deinit()
@@ -0,0 +1,297 @@
1
+ import usb.util
2
+ import time
3
+ import fractions
4
+ import array
5
+ from enum import Enum
6
+ import time
7
+ import math
8
+
9
+ class RequestType(Enum):
10
+ SEND = 0x21
11
+ RECEIVE = 0xa1
12
+
13
+ class Request(Enum):
14
+ DETACH = 0x00
15
+ DNLOAD = 0x01
16
+ UPLOAD = 0x02
17
+ GETSTATUS = 0x03
18
+ CLRSTATUS = 0x04
19
+ GETSTATE = 0x05
20
+ ABORT = 0x06
21
+
22
+ class Command(Enum):
23
+ SET_ADDRESS_POINTER = 0x21
24
+ ERASE = 0x41
25
+ READ_UNPROTECT = 0x92
26
+
27
+ class DfuState(Enum):
28
+ APP_IDLE = 0x00
29
+ APP_DETACH = 0x01
30
+ DFU_IDLE = 0x02
31
+ DFU_DOWNLOAD_SYNC = 0x03
32
+ DFU_DOWNLOAD_BUSY = 0x04
33
+ DFU_DOWNLOAD_IDLE = 0x05
34
+ DFU_MANIFEST_SYNC = 0x06
35
+ DFU_MANIFEST = 0x07
36
+ DFU_MANIFEST_WAIT_RESET = 0x08
37
+ DFU_UPLOAD_IDLE = 0x09
38
+ DFU_ERROR = 0x0a
39
+
40
+ class DfuStatus(Enum):
41
+ OK = 0x00
42
+ ERROR_TARGET = 0x01
43
+ ERROR_FILE = 0x02
44
+ ERROR_WRITE = 0x03
45
+ ERROR_ERASE = 0x04
46
+ ERROR_CHECK_ERASED = 0x05
47
+ ERROR_PROG = 0x06
48
+ ERROR_VERIFY = 0x07
49
+ ERROR_ADDRESS = 0x08
50
+ ERROR_NOTDONE = 0x09
51
+ ERROR_FIRMWARE = 0x0a
52
+ ERROR_VENDOR = 0x0b
53
+ ERROR_USBR = 0x0c
54
+ ERROR_POR = 0x0d
55
+ ERROR_UNKNOWN = 0x0e
56
+ ERROR_STALLEDPKT = 0x0f
57
+
58
+
59
+ SIZE_MULTIPLIERS = {' ': 1, 'K': 1024, 'M' : 1024*1024}
60
+ MAX_TRANSFER_SIZE = 2048
61
+
62
+ # Order is LSB first
63
+ def _address_to_4bytes(a):
64
+ return [ a % 256, (a >> 8)%256, (a >> 16)%256, (a >> 24)%256 ]
65
+
66
+ def _get_dfu_functional_descriptors(interfaces):
67
+ for intf in interfaces:
68
+ pos = 0
69
+ desc = intf.extra_descriptors
70
+ while len(desc):
71
+ current_desc = desc[:desc[0]]
72
+ desc = desc[desc[0]:]
73
+ if current_desc[1] == 0x21:
74
+ yield current_desc
75
+
76
+
77
+ class DfuError(Exception):
78
+ def __init__(self, message: str, status: DfuStatus, state: DfuState, text: str):
79
+ super().__init__(message + " Device responded with {}, {}, \"{}\".".format(status, state, text))
80
+
81
+ class DfuDevice:
82
+ def __init__(self, device):
83
+ self._dev = device
84
+ self._memory = None
85
+
86
+ def init(self):
87
+ cfg = self._dev[0]
88
+ cfg.set()
89
+
90
+ dfu_desc = list(_get_dfu_functional_descriptors(cfg.interfaces()))
91
+ assert len(dfu_desc) == 1
92
+ dfu_desc = dfu_desc[0]
93
+
94
+ self._max_transfer_size = dfu_desc[5] + (dfu_desc[6] << 8)
95
+ assert self._max_transfer_size <= MAX_TRANSFER_SIZE
96
+
97
+ self.memories = {}
98
+ for intf in cfg.interfaces():
99
+ # example for intf_name:
100
+ # '@Internal Flash /0x08000000/04*016Kg,01*064Kg,07*128Kg'
101
+ intf_name = usb.util.get_string(self._dev, intf.iInterface)
102
+ if intf_name.count('/') != 2:
103
+ raise Exception(f"invalid interface name for interface {intf.iInterface}: {intf_name}")
104
+ label, baseaddr, layout = intf_name.split('/')
105
+ baseaddr = int(baseaddr, 0) # convert hex to decimal
106
+ addr = baseaddr
107
+
108
+ memory = {
109
+ 'intf': intf,
110
+ 'alt': intf.bAlternateSetting,
111
+ 'sectors': []
112
+ }
113
+
114
+ for sector in layout.split(','):
115
+ repeat, size = map(int, sector[:-2].split('*'))
116
+ size *= SIZE_MULTIPLIERS[sector[-2].upper()]
117
+ mode = sector[-1]
118
+
119
+ while repeat > 0:
120
+ # TODO: verify if the section is writable
121
+ memory['sectors'].append({
122
+ 'addr': addr,
123
+ 'len': size,
124
+ 'mode': mode
125
+ })
126
+
127
+ addr += size
128
+ repeat -= 1
129
+
130
+ name = label.rstrip().lstrip('@')
131
+ self.memories[name] = memory
132
+
133
+ def select_memory(self, name):
134
+ self._memory = self.memories[name]
135
+ self._memory['intf'].set_altsetting()
136
+
137
+ def _control_msg(self, requestType: RequestType, request: Request, value: int, buffer):
138
+ if self._memory is None:
139
+ raise Exception("no memory selected")
140
+ return self._dev.ctrl_transfer(requestType.value, request.value, value, self._memory['intf'].bInterfaceNumber, buffer, timeout=60000)
141
+
142
+ def _detach(self, timeout):
143
+ """
144
+ timeout: Timeout in [??]
145
+ """
146
+ return self._control_msg(RequestType.SEND, Request.DETACH, timeout, None)
147
+
148
+ def _dnload(self, blockNum, data):
149
+ """
150
+ The device's current state must be dfuIDLE or dfuDNLOAD-IDLE state for
151
+ this command to work (see STM AN3156 Fig 5).
152
+
153
+ blockNum: block to write to
154
+ data: byte-array-like data to write
155
+ Returns: number of bytes that were written.
156
+ """
157
+ cnt = self._control_msg(RequestType.SEND, Request.DNLOAD, blockNum, list(data))
158
+ return cnt
159
+
160
+ def _upload(self, blockNum, size):
161
+ """
162
+ The device's current state must be dfuIDLE or dfuUPLOAD-IDLE state for
163
+ this command to work (see STM AN3156 Fig 3).
164
+
165
+ blockNum: block to read from
166
+ size: number of bytes to read
167
+ Returns: byte array containing the data that was read
168
+ """
169
+ return self._control_msg(RequestType.RECEIVE, Request.UPLOAD, blockNum, size)
170
+
171
+ def _get_status(self):
172
+ """
173
+ Returns: A tuple of the form (status: DfuStatus, state: DfuState, text: str)
174
+ """
175
+ msg = self._control_msg(RequestType.RECEIVE, Request.GETSTATUS, 0, 6)
176
+
177
+ status = DfuStatus(msg[0])
178
+ state = DfuState(msg[4])
179
+ poll_timeout_ms = msg[1] + (msg[2] << 8) + (msg[3] << 16)
180
+ text = usb.util.get_string(self._dev, msg[5])
181
+
182
+ if poll_timeout_ms > 10000:
183
+ raise Exception("Device requested an unreasonable timeout: {} ms".format(poll_timeout_ms))
184
+
185
+ time.sleep(float(poll_timeout_ms) / 1000.0)
186
+ return status, state, text
187
+
188
+ def clear_status(self):
189
+ if self._get_state() == DfuState.DFU_ERROR:
190
+ self._control_msg(RequestType.SEND, Request.CLRSTATUS, 0, None)
191
+
192
+ def _get_state(self):
193
+ msg = self._control_msg(RequestType.RECEIVE, Request.GETSTATE, 0, 1)
194
+ return DfuState(msg[0])
195
+
196
+ def _abort(self):
197
+ self._control_msg(RequestType.SEND, Request.ABORT, 0, None)
198
+
199
+ def _read(self, block, size):
200
+ return self._upload(block + 2, size)
201
+
202
+ def _write(self, block, data):
203
+ return self._dnload(block + 2, data)
204
+
205
+ def _set_address(self, addr):
206
+ self._dnload(0x0, [Command.SET_ADDRESS_POINTER.value] + _address_to_4bytes(addr))
207
+ self._expect_state(
208
+ [DfuState.DFU_DOWNLOAD_BUSY], [DfuState.DFU_DOWNLOAD_IDLE],
209
+ "Failed to set address 0x{:08x}.".format(addr)
210
+ )
211
+
212
+ def unprotect(self):
213
+ self._dnload(0x0, [Command.READ_UNPROTECT])
214
+ self._expect_state(
215
+ [DfuState.DFU_DOWNLOAD_BUSY], [DfuState.DFU_DOWNLOAD_IDLE],
216
+ "Failed to unprotect."
217
+ )
218
+
219
+ def _erase(self, addr):
220
+ self._dnload(0x0, [Command.ERASE.value] + _address_to_4bytes(addr))
221
+ self._expect_state(
222
+ [DfuState.DFU_DOWNLOAD_BUSY], [DfuState.DFU_DOWNLOAD_IDLE],
223
+ "Failed to erase sector at 0x{:08x}.".format(addr)
224
+ )
225
+
226
+ def _leave(self):
227
+ return self._dnload(0x0, []) # Just send an empty data.
228
+
229
+ def _expect_state(self, busy_states, target_states, error_text):
230
+ while True:
231
+ status, state, text = self._get_status()
232
+ if state in target_states:
233
+ return
234
+ if state not in busy_states:
235
+ raise DfuError(error_text, status, state, text)
236
+
237
+ def erase_sector(self, sector):
238
+ self._expect_state([], [DfuState.DFU_IDLE, DfuState.DFU_DOWNLOAD_IDLE], "Cannot erase sector")
239
+ self._erase(sector['addr'])
240
+
241
+ def write_sector(self, sector, data):
242
+ self._expect_state([], [DfuState.DFU_IDLE, DfuState.DFU_DOWNLOAD_IDLE], "Cannot write sector")
243
+
244
+ status, state, text = self._get_status()
245
+ if state not in [DfuState.DFU_IDLE, DfuState.DFU_DOWNLOAD_IDLE]:
246
+ raise DfuError("Cannot write sector.", status, state, text)
247
+
248
+ self._set_address(sector['addr'])
249
+
250
+ transfer_size = math.gcd(sector['len'], self._max_transfer_size)
251
+
252
+ for blocknum in range(int(sector['len'] / transfer_size)):
253
+ block = data[(blocknum * transfer_size):((blocknum + 1) * transfer_size)]
254
+ n_written = self._write(blocknum, block)
255
+ assert n_written == len(block), n_written
256
+ self._expect_state(
257
+ [DfuState.DFU_DOWNLOAD_BUSY], [DfuState.DFU_DOWNLOAD_IDLE],
258
+ "Failed to write sector at 0x{:08x}, block {}".format(sector['addr'], blocknum)
259
+ )
260
+
261
+ def read_sector(self, sector):
262
+ """
263
+ Reads data from the specified sector
264
+ Returns: a byte array containing the data
265
+ """
266
+ self._expect_state([], [DfuState.DFU_IDLE, DfuState.DFU_DOWNLOAD_IDLE], "Cannot read sector.")
267
+ self._set_address(sector['addr'])
268
+
269
+ self._abort() # exit DNLOAD_IDLE state
270
+
271
+ transfer_size = math.gcd(sector['len'], self._max_transfer_size)
272
+
273
+ # Device's current state must be dfuIDLE or dfuUPLOAD-IDLE for the subsequent read() commands to work.
274
+
275
+ data = array.array(u'B')
276
+ for blocknum in range(int(sector['len'] / transfer_size)):
277
+ device_block = self._read(blocknum, transfer_size)
278
+ assert len(device_block) == transfer_size, len(device_block)
279
+ data.extend(device_block)
280
+
281
+ self._abort() # take device into DFU_IDLE
282
+ return data
283
+
284
+ def jump_to_application(self, address):
285
+ self._set_address(address)
286
+ self._leave()
287
+
288
+ try:
289
+ self._expect_state(
290
+ [DfuState.DFU_MANIFEST_SYNC], [DfuState.DFU_MANIFEST],
291
+ "Failed to exit DFU mode."
292
+ )
293
+ # on ODrive v3.x this passes
294
+ # on ODrive v4.x it throws a usb.core.USBError
295
+ except usb.core.USBError:
296
+ pass # expected
297
+
@@ -0,0 +1 @@
1
+ from .DfuDevice import DfuDevice, DfuState, DfuStatus