moteus 0.3.89__tar.gz → 0.3.91__tar.gz
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.
- {moteus-0.3.89 → moteus-0.3.91}/PKG-INFO +1 -1
- {moteus-0.3.89 → moteus-0.3.91}/moteus/command.py +8 -2
- moteus-0.3.91/moteus/device_info.py +61 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/export.py +14 -7
- moteus-0.3.91/moteus/fdcanusb.py +30 -0
- moteus-0.3.91/moteus/fdcanusb_device.py +355 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/moteus.py +113 -546
- {moteus-0.3.89 → moteus-0.3.91}/moteus/moteus_tool.py +162 -112
- moteus-0.3.91/moteus/protocol.py +429 -0
- moteus-0.3.89/moteus/transport.py → moteus-0.3.91/moteus/pythoncan.py +7 -14
- moteus-0.3.91/moteus/pythoncan_device.py +244 -0
- moteus-0.3.91/moteus/transport.py +713 -0
- moteus-0.3.91/moteus/transport_device.py +194 -0
- moteus-0.3.91/moteus/transport_factory.py +188 -0
- moteus-0.3.91/moteus/transport_wrapper.py +51 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/version.py +1 -1
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/PKG-INFO +1 -1
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/SOURCES.txt +7 -1
- {moteus-0.3.89 → moteus-0.3.91}/setup.py +1 -1
- moteus-0.3.89/moteus/fdcanusb.py +0 -240
- moteus-0.3.89/moteus/pythoncan.py +0 -152
- moteus-0.3.89/moteus/router.py +0 -60
- {moteus-0.3.89 → moteus-0.3.91}/README.md +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/__init__.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/aioserial.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/aiostream.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/calibrate_encoder.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/multiplex.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/posix_aioserial.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/reader.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/regression.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus/win32_aioserial.py +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/dependency_links.txt +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/entry_points.txt +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/requires.txt +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/moteus.egg-info/top_level.txt +0 -0
- {moteus-0.3.89 → moteus-0.3.91}/setup.cfg +0 -0
@@ -13,8 +13,11 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
15
|
|
16
|
+
from .device_info import DeviceAddress
|
17
|
+
|
18
|
+
|
16
19
|
class Command():
|
17
|
-
destination = 1
|
20
|
+
destination = DeviceAddress(can_id=1)
|
18
21
|
source = 0
|
19
22
|
reply_required = False
|
20
23
|
data = b''
|
@@ -26,7 +29,10 @@ class Command():
|
|
26
29
|
# non-moteus devices).
|
27
30
|
raw = False
|
28
31
|
arbitration_id = 0 # this is the name python-can gives
|
29
|
-
|
32
|
+
|
33
|
+
# The channel can be specified to direct this to a particular
|
34
|
+
# transport device.
|
35
|
+
channel = None
|
30
36
|
|
31
37
|
def parse(self, message):
|
32
38
|
# By default, we just return the message as is.
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Copyright 2025 mjbots Robotic Systems, LLC. info@mjbots.com
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from dataclasses import dataclass
|
16
|
+
import typing
|
17
|
+
import uuid
|
18
|
+
|
19
|
+
from .transport_device import TransportDevice
|
20
|
+
|
21
|
+
@dataclass(frozen=True)
|
22
|
+
class DeviceAddress:
|
23
|
+
"""The minimal set of information necessary to communicate with a
|
24
|
+
device in a system. It may be just a CAN ID if that is unique, or
|
25
|
+
it may be a UUID prefix. It may also include a transport device,
|
26
|
+
although that is not required."""
|
27
|
+
can_id: typing.Optional[int] = None
|
28
|
+
uuid: typing.Optional[bytes] = None
|
29
|
+
transport_device: typing.Optional[TransportDevice] = None
|
30
|
+
|
31
|
+
def __repr__(self):
|
32
|
+
if self.can_id:
|
33
|
+
return f'DeviceAddress(can_id={self.can_id}, td={self.transport_device})'
|
34
|
+
uuid_bytes = self.uuid.hex() if self.uuid else None
|
35
|
+
return f'DeviceAddress(uuid={uuid_bytes}, td={self.transport_device})'
|
36
|
+
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class DeviceInfo:
|
40
|
+
"""This describes a device that was discovered on the CAN bus. It
|
41
|
+
includes the full available addressing information, as well as the
|
42
|
+
minimal DeviceAddress structure necessary to address it in the
|
43
|
+
current system."""
|
44
|
+
|
45
|
+
can_id: int = 1
|
46
|
+
uuid: typing.Optional[bytes] = None
|
47
|
+
transport_device: typing.Optional[TransportDevice] = None
|
48
|
+
address: typing.Optional[DeviceAddress] = None
|
49
|
+
|
50
|
+
def __repr__(self):
|
51
|
+
uuid_bytes = uuid.UUID(bytes=self.uuid) if self.uuid else None
|
52
|
+
return f'DeviceInfo(can_id={self.can_id}, uuid={uuid_bytes}, td={self.transport_device})'
|
53
|
+
|
54
|
+
def _cmp_key(self):
|
55
|
+
return (self.can_id, self.uuid or b'')
|
56
|
+
|
57
|
+
def __lt__(self, other):
|
58
|
+
if not isinstance(other, DeviceInfo):
|
59
|
+
return NotImplemented
|
60
|
+
|
61
|
+
return self._cmp_key() < other._cmp_key()
|
@@ -18,8 +18,10 @@ controller."""
|
|
18
18
|
ALL = [
|
19
19
|
'aiostream',
|
20
20
|
'make_transport_args', 'get_singleton_transport',
|
21
|
-
'
|
22
|
-
'
|
21
|
+
'DeviceAddress', 'DeviceInfo',
|
22
|
+
'Frame', 'FrameFilter', 'TransportDevice',
|
23
|
+
'Fdcanusb', 'FdcanusbDevice', 'Controller', 'Register', 'Transport', 'TransportWrapper',
|
24
|
+
'PythonCan', 'PythonCanDevice',
|
23
25
|
'Mode', 'QueryResolution', 'PositionResolution', 'Command', 'CommandError',
|
24
26
|
'Stream',
|
25
27
|
'TRANSPORT_FACTORIES',
|
@@ -28,19 +30,24 @@ ALL = [
|
|
28
30
|
'RegisterParser', 'QueryParser',
|
29
31
|
]
|
30
32
|
from moteus.command import Command
|
33
|
+
from moteus.device_info import DeviceAddress, DeviceInfo
|
31
34
|
from moteus.fdcanusb import Fdcanusb
|
32
|
-
from moteus.
|
35
|
+
from moteus.fdcanusb_device import FdcanusbDevice
|
33
36
|
from moteus.transport import Transport
|
37
|
+
from moteus.transport_wrapper import TransportWrapper
|
34
38
|
from moteus.pythoncan import PythonCan
|
39
|
+
from moteus.pythoncan_device import PythonCanDevice
|
40
|
+
from moteus.multiplex import (INT8, INT16, INT32, F32, IGNORE,
|
41
|
+
RegisterParser, QueryParser)
|
42
|
+
from moteus.transport_device import Frame, FrameFilter, TransportDevice
|
43
|
+
import moteus.reader as reader
|
44
|
+
import moteus.aiostream as aiostream
|
45
|
+
|
35
46
|
from moteus.moteus import (
|
36
47
|
CommandError,
|
37
48
|
Controller, Register, Mode, QueryResolution, PositionResolution, Stream,
|
38
49
|
make_transport_args, get_singleton_transport,
|
39
50
|
TRANSPORT_FACTORIES)
|
40
|
-
from moteus.multiplex import (INT8, INT16, INT32, F32, IGNORE,
|
41
|
-
RegisterParser, QueryParser)
|
42
|
-
import moteus.reader as reader
|
43
|
-
import moteus.aiostream as aiostream
|
44
51
|
|
45
52
|
try:
|
46
53
|
from moteus.version import VERSION
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Copyright 2025 mjbots Robotic Systems, LLC. info@mjbots.com
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import typing
|
16
|
+
|
17
|
+
from .fdcanusb_device import FdcanusbDevice
|
18
|
+
from .transport_wrapper import TransportWrapper
|
19
|
+
|
20
|
+
class Fdcanusb(TransportWrapper):
|
21
|
+
def __init__(self, path=None, *args, **kwargs):
|
22
|
+
if path is None:
|
23
|
+
path = Fdcanusb.detect_fdcanusb()
|
24
|
+
|
25
|
+
device = FdcanusbDevice(path, *args, **kwargs)
|
26
|
+
super().__init__(device)
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def detect_fdcanusb():
|
30
|
+
return FdcanusbDevice.detect_fdcanusb()
|
@@ -0,0 +1,355 @@
|
|
1
|
+
# Copyright 2025 mjbots Robotic Systems, LLC. info@mjbots.com
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import asyncio
|
16
|
+
import glob
|
17
|
+
import os
|
18
|
+
import serial
|
19
|
+
import serial.tools
|
20
|
+
import serial.tools.list_ports
|
21
|
+
import sys
|
22
|
+
import time
|
23
|
+
import typing
|
24
|
+
|
25
|
+
from . import aioserial
|
26
|
+
from .transport_device import Frame, FrameFilter, TransportDevice
|
27
|
+
|
28
|
+
|
29
|
+
def _hexify(data):
|
30
|
+
return ''.join(['{:02X}'.format(x) for x in data])
|
31
|
+
|
32
|
+
|
33
|
+
def _dehexify(data):
|
34
|
+
result = b''
|
35
|
+
for i in range(0, len(data), 2):
|
36
|
+
result += bytes([int(data[i:i + 2], 16)])
|
37
|
+
return result
|
38
|
+
|
39
|
+
|
40
|
+
def _find_serial_number(path):
|
41
|
+
"""Attempt to find the USB serial number for a given device path.
|
42
|
+
|
43
|
+
This function handles both direct device paths and symlinks to the
|
44
|
+
actual device, like custom udev rules that create /dev/fdcanusb -> /dev/serial/by-id/foo.
|
45
|
+
"""
|
46
|
+
if not path:
|
47
|
+
return None
|
48
|
+
|
49
|
+
try:
|
50
|
+
# Resolve symlinks to get the actual device path
|
51
|
+
real_path = os.path.realpath(path)
|
52
|
+
|
53
|
+
# Get all ports including symlinks
|
54
|
+
ports = serial.tools.list_ports.comports(include_links=True)
|
55
|
+
|
56
|
+
for port in ports:
|
57
|
+
# Check if either the device path or the resolved path matches
|
58
|
+
if port.device == path or port.device == real_path:
|
59
|
+
# Return serial number if available
|
60
|
+
if hasattr(port, 'serial_number') and port.serial_number:
|
61
|
+
return port.serial_number
|
62
|
+
|
63
|
+
except Exception:
|
64
|
+
# If anything goes wrong, just return None
|
65
|
+
pass
|
66
|
+
|
67
|
+
return None
|
68
|
+
|
69
|
+
|
70
|
+
class FdcanusbDevice(TransportDevice):
|
71
|
+
"""Connects to a single mjbots fdcanusb."""
|
72
|
+
|
73
|
+
def __init__(self, path=None, debug_log=None, disable_brs=False, **kwargs):
|
74
|
+
"""Constructor.
|
75
|
+
|
76
|
+
Arguments:
|
77
|
+
path: serial port where fdcanusb is located
|
78
|
+
"""
|
79
|
+
super(FdcanusbDevice, self).__init__(**kwargs)
|
80
|
+
|
81
|
+
self._disable_brs = disable_brs
|
82
|
+
|
83
|
+
# A fdcanusb ignores the requested baudrate, so we'll just
|
84
|
+
# pick something nice and random.
|
85
|
+
self._serial = aioserial.AioSerial(port=path, baudrate=9600)
|
86
|
+
|
87
|
+
# Attempt to discover the USB serial number associated with
|
88
|
+
# this device for pretty-printing.
|
89
|
+
self._serial_number = _find_serial_number(path)
|
90
|
+
|
91
|
+
self._stream_data = b''
|
92
|
+
|
93
|
+
self._ok_waiters = []
|
94
|
+
|
95
|
+
self._reader_task = None
|
96
|
+
self._running = False
|
97
|
+
|
98
|
+
self._debug_log = debug_log
|
99
|
+
|
100
|
+
# Start the reader if we can.
|
101
|
+
self._start_reader()
|
102
|
+
|
103
|
+
def __repr__(self):
|
104
|
+
if self._serial_number:
|
105
|
+
return f"Fdcanusb(sn='{self._serial_number}')"
|
106
|
+
else:
|
107
|
+
return 'Fdcanusb()'
|
108
|
+
|
109
|
+
def close(self):
|
110
|
+
if self._reader_task and not self._reader_task.done():
|
111
|
+
self._reader_task.cancel()
|
112
|
+
|
113
|
+
if hasattr(self._serial, 'close'):
|
114
|
+
self._serial.close()
|
115
|
+
|
116
|
+
self._running = False
|
117
|
+
|
118
|
+
def empty_bus_tx_safe(self):
|
119
|
+
return True
|
120
|
+
|
121
|
+
async def __aenter__(self):
|
122
|
+
return self
|
123
|
+
|
124
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
125
|
+
self.close()
|
126
|
+
return False
|
127
|
+
|
128
|
+
def _start_reader(self):
|
129
|
+
self._running = True
|
130
|
+
try:
|
131
|
+
self._reader_task = asyncio.create_task(self._reader_loop())
|
132
|
+
except RuntimeError:
|
133
|
+
self._reader_task = None
|
134
|
+
|
135
|
+
async def _ensure_reader_started(self):
|
136
|
+
if self._reader_task is None and self._running:
|
137
|
+
self._reader_task = asyncio.create_task(self._reader_loop())
|
138
|
+
|
139
|
+
async def _reader_loop(self):
|
140
|
+
try:
|
141
|
+
while self._running:
|
142
|
+
try:
|
143
|
+
line = await self._readline(self._serial)
|
144
|
+
if not line:
|
145
|
+
continue
|
146
|
+
|
147
|
+
if self._debug_log:
|
148
|
+
self._write_log(b'< ' + line.rstrip())
|
149
|
+
|
150
|
+
if line.startswith(b'rcv'):
|
151
|
+
frame = self._parse_frame(line)
|
152
|
+
await self._handle_received_frame(frame)
|
153
|
+
elif line.startswith(b'OK'):
|
154
|
+
await self._handle_ok_response(line)
|
155
|
+
else:
|
156
|
+
await self._handle_other_response(line)
|
157
|
+
except asyncio.CancelledError:
|
158
|
+
break
|
159
|
+
except Exception as e:
|
160
|
+
raise
|
161
|
+
# Log and continue running.
|
162
|
+
if self._debug_log:
|
163
|
+
self._write_log(f'ERROR: {str(e)}'.encode('latin1'))
|
164
|
+
|
165
|
+
# Sleep briefly to prevent a tight error loop.
|
166
|
+
await asyncio.sleep(0.01)
|
167
|
+
finally:
|
168
|
+
self._running = False
|
169
|
+
|
170
|
+
async def _handle_ok_response(self, line):
|
171
|
+
# Just notify the first non-done OK waiter. An OK received
|
172
|
+
# with no waiters is assumed to be stale.
|
173
|
+
#
|
174
|
+
# Callers are *required* to enqueue their self._ok_waiters
|
175
|
+
# *before* sending anything that could result in an OK being
|
176
|
+
# emitted.
|
177
|
+
for waiter in self._ok_waiters:
|
178
|
+
if not waiter.done():
|
179
|
+
waiter.set_result(None)
|
180
|
+
return
|
181
|
+
|
182
|
+
async def _handle_other_response(self, line):
|
183
|
+
raise RuntimeError(f'{self} received error {line}')
|
184
|
+
|
185
|
+
async def _readline(self, stream):
|
186
|
+
while True:
|
187
|
+
offset = min((self._stream_data.find(c) for c in b"\r\n"
|
188
|
+
if c in self._stream_data), default=None)
|
189
|
+
if offset is not None:
|
190
|
+
to_return, self._stream_data = (
|
191
|
+
self._stream_data[0:offset+1],
|
192
|
+
self._stream_data[offset+1:])
|
193
|
+
if offset > 0:
|
194
|
+
return to_return
|
195
|
+
else:
|
196
|
+
continue
|
197
|
+
else:
|
198
|
+
data = await stream.read(8192, block=False)
|
199
|
+
if not data:
|
200
|
+
# If for some reason we got a completely empty
|
201
|
+
# response, which shouldn't happen, ensure we
|
202
|
+
# yield control to somebody else so that hopefully
|
203
|
+
# we eventually make forward progress.
|
204
|
+
await asyncio.sleep(0)
|
205
|
+
else:
|
206
|
+
self._stream_data += data
|
207
|
+
|
208
|
+
def _parse_frame(self, line):
|
209
|
+
fields = line.split(b" ")
|
210
|
+
frame = Frame()
|
211
|
+
frame.data = _dehexify(fields[2])
|
212
|
+
frame.dlc = len(frame.data)
|
213
|
+
frame.arbitration_id = int(fields[1], 16)
|
214
|
+
frame.channel = self
|
215
|
+
|
216
|
+
for flag in fields[3:]:
|
217
|
+
if flag == b'E':
|
218
|
+
frame.is_extended_id = True
|
219
|
+
if flag == b'B':
|
220
|
+
frame.bitrate_switch = True
|
221
|
+
if flag == b'F':
|
222
|
+
frame.is_fd = True
|
223
|
+
|
224
|
+
return frame
|
225
|
+
|
226
|
+
async def _write_send_frame(self, frame):
|
227
|
+
actual_brs = frame.bitrate_switch and not self._disable_brs
|
228
|
+
|
229
|
+
hexdata = _hexify(frame.data)
|
230
|
+
on_wire_size = self._round_up_dlc(len(frame.data))
|
231
|
+
hexdata += self._padding_hex * (on_wire_size - len(frame.data))
|
232
|
+
|
233
|
+
flags = ''
|
234
|
+
if actual_brs:
|
235
|
+
flags += 'B'
|
236
|
+
else:
|
237
|
+
flags += 'b'
|
238
|
+
if frame.is_fd:
|
239
|
+
flags += 'F'
|
240
|
+
|
241
|
+
cmd = "can send {:04x} {}{}{}\n".format(
|
242
|
+
frame.arbitration_id,
|
243
|
+
hexdata,
|
244
|
+
' ' if len(flags) > 0 else '',
|
245
|
+
flags).encode('latin1')
|
246
|
+
|
247
|
+
self._serial.write(cmd)
|
248
|
+
if self._debug_log:
|
249
|
+
self._write_log(b'> ' + cmd.rstrip())
|
250
|
+
|
251
|
+
async def send_frame(self, frame: Frame):
|
252
|
+
await self._ensure_reader_started()
|
253
|
+
|
254
|
+
try:
|
255
|
+
ok_waiter = asyncio.Future()
|
256
|
+
self._ok_waiters.append(ok_waiter)
|
257
|
+
|
258
|
+
await self._write_send_frame(frame)
|
259
|
+
await self._serial.drain()
|
260
|
+
|
261
|
+
await ok_waiter
|
262
|
+
finally:
|
263
|
+
self._ok_waiters = [w for w in self._ok_waiters
|
264
|
+
if w != ok_waiter]
|
265
|
+
|
266
|
+
async def receive_frame(self):
|
267
|
+
await self._ensure_reader_started()
|
268
|
+
|
269
|
+
return await super(FdcanusbDevice, self).receive_frame()
|
270
|
+
|
271
|
+
async def transaction(
|
272
|
+
self,
|
273
|
+
requests: typing.List[TransportDevice.Request],
|
274
|
+
**kwargs):
|
275
|
+
|
276
|
+
# We do not support child devices.
|
277
|
+
assert not any([request.child_device is not None
|
278
|
+
for request in requests])
|
279
|
+
|
280
|
+
def make_subscription(request):
|
281
|
+
future = asyncio.Future()
|
282
|
+
|
283
|
+
async def handler(frame, request=request, future=future):
|
284
|
+
if future.done():
|
285
|
+
# Stick it in our receive queue so that it isn't
|
286
|
+
# lost.
|
287
|
+
self._receive_queue.append(frame)
|
288
|
+
|
289
|
+
return
|
290
|
+
|
291
|
+
request.responses.append(frame)
|
292
|
+
# While we may receive more than one frame for a given
|
293
|
+
# request, we only wait for one.
|
294
|
+
future.set_result(None)
|
295
|
+
|
296
|
+
return self._subscribe(request.frame_filter, handler), future
|
297
|
+
|
298
|
+
subscriptions = [
|
299
|
+
make_subscription(request)
|
300
|
+
for request in requests
|
301
|
+
if request.frame_filter is not None
|
302
|
+
]
|
303
|
+
|
304
|
+
try:
|
305
|
+
# Now we will send all our requests.
|
306
|
+
ok_waiters = set([asyncio.Future()
|
307
|
+
for _ in range(len(requests))])
|
308
|
+
try:
|
309
|
+
self._ok_waiters.extend(list(ok_waiters))
|
310
|
+
|
311
|
+
# TODO: Provide control over the amount of pipelining
|
312
|
+
# like the C++ class.
|
313
|
+
for request in requests:
|
314
|
+
await self._write_send_frame(request.frame)
|
315
|
+
|
316
|
+
await self._serial.drain()
|
317
|
+
|
318
|
+
await asyncio.gather(*ok_waiters)
|
319
|
+
finally:
|
320
|
+
self._ok_waiters = [w for w in self._ok_waiters
|
321
|
+
if w not in ok_waiters]
|
322
|
+
|
323
|
+
# If there any responses to wait for, do so.
|
324
|
+
if subscriptions:
|
325
|
+
await asyncio.gather(*[x[1] for x in subscriptions])
|
326
|
+
finally:
|
327
|
+
# Clean up all our subscriptions.
|
328
|
+
for x in subscriptions:
|
329
|
+
x[0].cancel()
|
330
|
+
|
331
|
+
def _write_log(self, output: bytes):
|
332
|
+
assert self._debug_log is not None
|
333
|
+
self._debug_log.write(f'{time.time():.6f}/{self._serial_number} '.encode('latin1') + output + b'\n')
|
334
|
+
|
335
|
+
@staticmethod
|
336
|
+
def detect_fdcanusbs():
|
337
|
+
if sys.platform == 'win32':
|
338
|
+
return FdcanusbDevice.pyserial_detect_fdcanusbs()
|
339
|
+
|
340
|
+
maybe_list = glob.glob('/dev/serial/by-id/*fdcanusb*')
|
341
|
+
if len(maybe_list):
|
342
|
+
return sorted(maybe_list)
|
343
|
+
|
344
|
+
return FdcanusbDevice.pyserial_detect_fdcanusbs()
|
345
|
+
|
346
|
+
@staticmethod
|
347
|
+
def detect_fdcanusb():
|
348
|
+
return FdcanusbDevice.detect_fdcanusbs()[0]
|
349
|
+
|
350
|
+
@staticmethod
|
351
|
+
def pyserial_detect_fdcanusbs():
|
352
|
+
ports = serial.tools.list_ports.comports()
|
353
|
+
|
354
|
+
return [x.device for x in ports
|
355
|
+
if x.vid == 0x0483 and x.pid == 0x5740]
|