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/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# We want to use the fibre package that is included with the odrive package
|
|
6
|
+
# in order to avoid any version mismatch issues,
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "pyfibre"))
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import concurrent
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .version import __version__
|
|
16
|
+
from .utils import get_serial_number_str, get_serial_number_str_sync, attach_metadata
|
|
17
|
+
|
|
18
|
+
# Backwards compatibility with Python 3.6 (default on Ubuntu 18.04)
|
|
19
|
+
if sys.version_info < (3, 7):
|
|
20
|
+
asyncio.get_running_loop = asyncio.get_event_loop
|
|
21
|
+
|
|
22
|
+
def asyncio_run(coro):
|
|
23
|
+
loop = asyncio.get_event_loop()
|
|
24
|
+
return loop.run_until_complete(coro)
|
|
25
|
+
asyncio.run = asyncio_run
|
|
26
|
+
|
|
27
|
+
default_usb_search_path = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0'
|
|
28
|
+
default_search_path = default_usb_search_path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_discovery_lock = threading.Lock()
|
|
32
|
+
_discovery_started = [False]
|
|
33
|
+
_discovery_path = [None]
|
|
34
|
+
|
|
35
|
+
connected_devices = []
|
|
36
|
+
connected_devices_changed = concurrent.futures.Future()
|
|
37
|
+
|
|
38
|
+
def start_discovery(path):
|
|
39
|
+
"""
|
|
40
|
+
Starts device discovery in a background thread. This function returns
|
|
41
|
+
immediately.
|
|
42
|
+
If discovery was already started, this function does nothing.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Start backend if it's not already started
|
|
46
|
+
with _discovery_lock:
|
|
47
|
+
if _discovery_started[0]:
|
|
48
|
+
if path != _discovery_path[0]:
|
|
49
|
+
raise Exception("Cannot change discovery path between multiple find_any() "
|
|
50
|
+
"calls: {} != {}. Use fibre.Domain() directly for finer "
|
|
51
|
+
"grained discovery control.".format(path, _discovery_path))
|
|
52
|
+
return # discovery already started
|
|
53
|
+
_discovery_started[0] = True
|
|
54
|
+
_discovery_path[0] = path
|
|
55
|
+
|
|
56
|
+
async def discovered_object(obj):
|
|
57
|
+
def lost_object(_):
|
|
58
|
+
connected_devices.remove(obj)
|
|
59
|
+
|
|
60
|
+
# indicate that connected_devices changed
|
|
61
|
+
global connected_devices_changed
|
|
62
|
+
signal = connected_devices_changed
|
|
63
|
+
connected_devices_changed = concurrent.futures.Future()
|
|
64
|
+
signal.set_result(None)
|
|
65
|
+
|
|
66
|
+
await attach_metadata(obj)
|
|
67
|
+
|
|
68
|
+
with _discovery_lock:
|
|
69
|
+
connected_devices.append(obj)
|
|
70
|
+
obj._on_lost.add_done_callback(lost_object)
|
|
71
|
+
|
|
72
|
+
# indicate that connected_devices changed
|
|
73
|
+
global connected_devices_changed
|
|
74
|
+
signal = connected_devices_changed
|
|
75
|
+
connected_devices_changed = concurrent.futures.Future()
|
|
76
|
+
signal.set_result(None)
|
|
77
|
+
|
|
78
|
+
def domain_thread():
|
|
79
|
+
_domain_termination_token = concurrent.futures.Future() # unused
|
|
80
|
+
|
|
81
|
+
import fibre
|
|
82
|
+
with fibre.Domain(path) as domain:
|
|
83
|
+
discovery = domain.run_discovery(discovered_object)
|
|
84
|
+
_domain_termination_token.result()
|
|
85
|
+
discovery.stop()
|
|
86
|
+
|
|
87
|
+
threading.Thread(target=domain_thread, daemon=True).start()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_any(path: str = default_search_path,
|
|
91
|
+
serial_number: str = None,
|
|
92
|
+
cancellation_token: concurrent.futures.Future = None,
|
|
93
|
+
timeout: float = None):
|
|
94
|
+
"""
|
|
95
|
+
Blocks until the first matching ODrive object is connected and then returns
|
|
96
|
+
that object.
|
|
97
|
+
|
|
98
|
+
If find_any() is called multiple times, the same object may be returned
|
|
99
|
+
(depending on the serial_number argument).
|
|
100
|
+
|
|
101
|
+
The first call to find_any() will start a background thread that handles
|
|
102
|
+
the backend. This background thread will keep running until the program is
|
|
103
|
+
terminated.
|
|
104
|
+
|
|
105
|
+
If you want finer grained control over object discovery
|
|
106
|
+
consider using fibre.Domain directly.
|
|
107
|
+
"""
|
|
108
|
+
assert cancellation_token is None or isinstance(cancellation_token, concurrent.futures.Future)
|
|
109
|
+
if cancellation_token is None:
|
|
110
|
+
cancellation_tokens = []
|
|
111
|
+
else:
|
|
112
|
+
cancellation_tokens = [cancellation_token]
|
|
113
|
+
|
|
114
|
+
start_discovery(path)
|
|
115
|
+
|
|
116
|
+
wait_start = time.monotonic()
|
|
117
|
+
while True:
|
|
118
|
+
signal = connected_devices_changed
|
|
119
|
+
with _discovery_lock:
|
|
120
|
+
for obj in connected_devices:
|
|
121
|
+
if serial_number is None or obj._serial_number == serial_number:
|
|
122
|
+
return obj
|
|
123
|
+
|
|
124
|
+
current_timeout = None if timeout is None else max(0, timeout - (time.monotonic() - wait_start))
|
|
125
|
+
wait_result = concurrent.futures.wait([signal] + cancellation_tokens, timeout=current_timeout, return_when=concurrent.futures.FIRST_COMPLETED)
|
|
126
|
+
|
|
127
|
+
if (not cancellation_token is None) and (cancellation_token in wait_result.done):
|
|
128
|
+
raise concurrent.futures.CancelledError()
|
|
129
|
+
elif (not signal in wait_result.done):
|
|
130
|
+
raise TimeoutError()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def find_any_async(path: str = default_search_path, serial_number: Optional[str] = None):
|
|
134
|
+
start_discovery(path)
|
|
135
|
+
|
|
136
|
+
while True:
|
|
137
|
+
signal = connected_devices_changed
|
|
138
|
+
with _discovery_lock:
|
|
139
|
+
for obj in connected_devices:
|
|
140
|
+
if serial_number is None or obj._serial_number == serial_number:
|
|
141
|
+
return obj
|
|
142
|
+
await asyncio.shield(asyncio.wrap_future(signal))
|
odrive/api_client.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import odrive.crypto as crypto
|
|
7
|
+
from odrive.crypto import b64encode, b64decode
|
|
8
|
+
|
|
9
|
+
API_BASE_ADDR = 'https://api.odriverobotics.com'
|
|
10
|
+
|
|
11
|
+
class ApiClient():
|
|
12
|
+
def __init__(self, session, api_base_addr: str = API_BASE_ADDR, key: Union[str, bytes, None] = None):
|
|
13
|
+
self._session = session
|
|
14
|
+
self._api_base_addr = api_base_addr
|
|
15
|
+
if key is None and 'ODRIVE_API_KEY' in os.environ:
|
|
16
|
+
key = os.environ['ODRIVE_API_KEY']
|
|
17
|
+
self._key = None if key is None else crypto.load_private_key(key if isinstance(key, bytes) else b64decode(key))
|
|
18
|
+
|
|
19
|
+
async def call(self, method: str, endpoint: str, inputs=None):
|
|
20
|
+
url = self._api_base_addr + endpoint
|
|
21
|
+
content = json.dumps(inputs or {}).encode('utf-8')
|
|
22
|
+
headers = {
|
|
23
|
+
'content-type': 'application/json; charset=utf-8'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if not self._key is None:
|
|
27
|
+
url_bytes = endpoint.encode('utf-8')
|
|
28
|
+
timestamp_bytes = int(datetime.datetime.now().timestamp()).to_bytes(8, 'little', signed=False)
|
|
29
|
+
message = url_bytes + b':' + timestamp_bytes + b':' + content
|
|
30
|
+
signature = crypto.sign(self._key, message)
|
|
31
|
+
headers['Authorization'] = 'hmacauth ' + ':'.join([
|
|
32
|
+
b64encode(crypto.get_public_bytes(self._key.public_key())),
|
|
33
|
+
b64encode(timestamp_bytes),
|
|
34
|
+
b64encode(signature)
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
async with self._session.request(method, url, headers=headers, data=content, ) as response:
|
|
38
|
+
if response.status != 200:
|
|
39
|
+
try:
|
|
40
|
+
ex_data = await response.json()
|
|
41
|
+
except:
|
|
42
|
+
ex_raw = await response.read()
|
|
43
|
+
raise Exception(f"Server failed with {response.status} ({response.reason}): {ex_raw}")
|
|
44
|
+
else:
|
|
45
|
+
tb = ''.join(ex_data['traceback'])
|
|
46
|
+
raise Exception(f"Server failed with {response.status} ({response.reason}): {ex_data['message']} in \n{tb}")
|
|
47
|
+
|
|
48
|
+
return await response.json()
|
|
49
|
+
|
|
50
|
+
async def download(self, url: str):
|
|
51
|
+
async with self._session.get(url) as response:
|
|
52
|
+
if response.status != 200:
|
|
53
|
+
raise Exception(f"Server failed with {response.status} ({response.reason})")
|
|
54
|
+
return await response.read()
|