moteus 0.3.89__py3-none-any.whl → 0.3.91__py3-none-any.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.
- moteus/command.py +8 -2
- moteus/device_info.py +61 -0
- moteus/export.py +14 -7
- moteus/fdcanusb.py +12 -222
- moteus/fdcanusb_device.py +355 -0
- moteus/moteus.py +113 -546
- moteus/moteus_tool.py +162 -112
- moteus/protocol.py +429 -0
- moteus/pythoncan.py +6 -137
- moteus/pythoncan_device.py +244 -0
- moteus/transport.py +692 -7
- moteus/transport_device.py +194 -0
- moteus/transport_factory.py +188 -0
- moteus/transport_wrapper.py +51 -0
- moteus/version.py +1 -1
- {moteus-0.3.89.dist-info → moteus-0.3.91.dist-info}/METADATA +1 -1
- moteus-0.3.91.dist-info/RECORD +29 -0
- moteus/router.py +0 -60
- moteus-0.3.89.dist-info/RECORD +0 -23
- {moteus-0.3.89.dist-info → moteus-0.3.91.dist-info}/WHEEL +0 -0
- {moteus-0.3.89.dist-info → moteus-0.3.91.dist-info}/entry_points.txt +0 -0
- {moteus-0.3.89.dist-info → moteus-0.3.91.dist-info}/top_level.txt +0 -0
moteus/command.py
CHANGED
@@ -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.
|
moteus/device_info.py
ADDED
@@ -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()
|
moteus/export.py
CHANGED
@@ -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
|
moteus/fdcanusb.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright
|
1
|
+
# Copyright 2025 mjbots Robotic Systems, LLC. info@mjbots.com
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -12,229 +12,19 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
import
|
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
|
15
|
+
import typing
|
23
16
|
|
24
|
-
from . import
|
17
|
+
from .fdcanusb_device import FdcanusbDevice
|
18
|
+
from .transport_wrapper import TransportWrapper
|
25
19
|
|
26
|
-
|
27
|
-
def
|
28
|
-
return ''.join(['{:02X}'.format(x) for x in data])
|
29
|
-
|
30
|
-
|
31
|
-
def _dehexify(data):
|
32
|
-
result = b''
|
33
|
-
for i in range(0, len(data), 2):
|
34
|
-
result += bytes([int(data[i:i + 2], 16)])
|
35
|
-
return result
|
36
|
-
|
37
|
-
|
38
|
-
class CanMessage:
|
39
|
-
arbitration_id = 0
|
40
|
-
is_extended_id = False
|
41
|
-
dlc = 0
|
42
|
-
data = b''
|
43
|
-
is_fd = False
|
44
|
-
bitrate_switch = False
|
45
|
-
|
46
|
-
|
47
|
-
class Fdcanusb:
|
48
|
-
"""Connects to a single mjbots fdcanusb."""
|
49
|
-
|
50
|
-
def __init__(self, path=None, debug_log=None, disable_brs=False):
|
51
|
-
"""Constructor.
|
52
|
-
|
53
|
-
Arguments:
|
54
|
-
path: serial port where fdcanusb is located
|
55
|
-
"""
|
20
|
+
class Fdcanusb(TransportWrapper):
|
21
|
+
def __init__(self, path=None, *args, **kwargs):
|
56
22
|
if path is None:
|
57
|
-
path =
|
58
|
-
|
59
|
-
# A fdcanusb ignores the requested baudrate, so we'll just
|
60
|
-
# pick something nice and random.
|
61
|
-
self._serial = aioserial.AioSerial(port=path, baudrate=9600)
|
62
|
-
self._stream_data = b''
|
63
|
-
|
64
|
-
self._cycle_lock = asyncio.Lock()
|
65
|
-
|
66
|
-
self._send_flags = (
|
67
|
-
' b' if disable_brs else '')
|
68
|
-
|
69
|
-
self._debug_log = None
|
70
|
-
if debug_log:
|
71
|
-
self._debug_log = open(debug_log, 'wb')
|
72
|
-
|
73
|
-
def __enter__(self):
|
74
|
-
return self
|
75
|
-
|
76
|
-
def __exit__(self, type, value, traceback):
|
77
|
-
self._serial.close()
|
78
|
-
|
79
|
-
async def _readline(self, stream):
|
80
|
-
while True:
|
81
|
-
offset = min((self._stream_data.find(c) for c in b"\r\n"
|
82
|
-
if c in self._stream_data), default=None)
|
83
|
-
if offset is not None:
|
84
|
-
to_return, self._stream_data = (
|
85
|
-
self._stream_data[0:offset+1],
|
86
|
-
self._stream_data[offset+1:])
|
87
|
-
if offset > 0:
|
88
|
-
return to_return
|
89
|
-
else:
|
90
|
-
continue
|
91
|
-
else:
|
92
|
-
self._stream_data += await stream.read(8192, block=False)
|
93
|
-
|
94
|
-
def detect_fdcanusb(self):
|
95
|
-
if sys.platform == 'win32':
|
96
|
-
return self.win32_detect_fdcanusb()
|
97
|
-
|
98
|
-
if os.path.exists('/dev/fdcanusb'):
|
99
|
-
return '/dev/fdcanusb'
|
100
|
-
maybe_list = glob.glob('/dev/serial/by-id/*fdcanusb*')
|
101
|
-
if len(maybe_list):
|
102
|
-
return sorted(maybe_list)[0]
|
103
|
-
|
104
|
-
return self.pyserial_detect_fdcanusb()
|
105
|
-
|
106
|
-
def win32_detect_fdcanusb(self):
|
107
|
-
import serial.tools.list_ports
|
108
|
-
ports = serial.tools.list_ports.comports()
|
109
|
-
for port in ports:
|
110
|
-
if port.vid == 0x0483 and port.pid == 0x5740:
|
111
|
-
return port.name
|
112
|
-
|
113
|
-
raise RuntimeError('Could not detect fdcanusb')
|
114
|
-
|
115
|
-
def pyserial_detect_fdcanusb(self):
|
116
|
-
ports = serial.tools.list_ports.comports()
|
117
|
-
for port in ports:
|
118
|
-
if port.vid == 0x0483 and port.pid == 0x5740:
|
119
|
-
return port.device
|
120
|
-
|
121
|
-
raise RuntimeError('Could not detect fdcanusb')
|
122
|
-
|
123
|
-
async def cycle(self, commands):
|
124
|
-
"""Request that the given set of commands be sent to the fdcanusb, and
|
125
|
-
any responses collated and returned, after being parsed by
|
126
|
-
their command specific parsers.
|
127
|
-
|
128
|
-
Each command instance must model moteus.Command
|
129
|
-
"""
|
130
|
-
|
131
|
-
# Since the fdcanusb has state, we need to ensure it is kept
|
132
|
-
# consistent. Only one call to cycle at a time please.
|
133
|
-
async with self._cycle_lock:
|
134
|
-
# Since the fdcanusb can't send multiple things at once, we
|
135
|
-
# just go through the commands one at a time and handle them
|
136
|
-
# individually.
|
137
|
-
return [await self._do_command(x) for x in commands]
|
138
|
-
|
139
|
-
def _parse_message(self, line):
|
140
|
-
fields = line.split(b" ")
|
141
|
-
message = CanMessage()
|
142
|
-
message.data = _dehexify(fields[2])
|
143
|
-
message.arbitration_id = int(fields[1], 16)
|
144
|
-
|
145
|
-
flags = fields[3] if len(fields) > 3 else ''
|
146
|
-
if b'E' in flags:
|
147
|
-
message.is_extended_id = True
|
148
|
-
if b'B' in flags:
|
149
|
-
message.bitrate_switch = True
|
150
|
-
if b'F' in flags:
|
151
|
-
message.is_fd = True
|
152
|
-
|
153
|
-
return message
|
154
|
-
|
155
|
-
async def _do_command(self, command):
|
156
|
-
await self.write(command)
|
157
|
-
reply_required = command.reply_required
|
158
|
-
|
159
|
-
# Get the OK response.
|
160
|
-
while True:
|
161
|
-
ok_response = await self._readline(self._serial)
|
162
|
-
if ok_response.startswith(b"OK"):
|
163
|
-
break
|
164
|
-
# Ignore spurious responses until we get an OK.
|
165
|
-
|
166
|
-
while reply_required:
|
167
|
-
line = await self._readline(self._serial)
|
168
|
-
|
169
|
-
if not line.startswith(b"rcv"):
|
170
|
-
raise RuntimeError("unexpected fdcanusb response, got: " +
|
171
|
-
line.decode('latin1'))
|
172
|
-
|
173
|
-
if self._debug_log:
|
174
|
-
self._debug_log.write(f'{time.time()} < '.encode('latin1') +
|
175
|
-
line.rstrip() + b'\n')
|
176
|
-
|
177
|
-
message = self._parse_message(line)
|
178
|
-
|
179
|
-
moteus_id = (message.arbitration_id >> 8) & 0x7f
|
180
|
-
|
181
|
-
if command.raw or moteus_id == command.destination:
|
182
|
-
return command.parse(message)
|
183
|
-
|
184
|
-
# We are not raw and the message wasn't from the device we
|
185
|
-
# were writing to, so just loop and try some more.
|
186
|
-
|
187
|
-
async def write(self, command):
|
188
|
-
# This merely sends a command and doesn't even wait for an OK
|
189
|
-
# to come back. It can *not* be intermixed with calls to
|
190
|
-
# 'cycle'.
|
191
|
-
bus_id = None
|
192
|
-
if command.raw:
|
193
|
-
bus_id = command.arbitration_id
|
194
|
-
else:
|
195
|
-
bus_id = (command.destination |
|
196
|
-
(0x8000 if command.reply_required else 0) |
|
197
|
-
(command.can_prefix << 16))
|
198
|
-
hexdata = _hexify(command.data)
|
199
|
-
on_wire_size = self._round_up_dlc(len(command.data))
|
200
|
-
hexdata += '50' * (on_wire_size - len(command.data))
|
201
|
-
cmd = "can send {:04x} {}{}\n".format(
|
202
|
-
bus_id, hexdata, self._send_flags).encode('latin1')
|
203
|
-
self._serial.write(cmd)
|
204
|
-
if self._debug_log:
|
205
|
-
self._debug_log.write(f'{time.time()} > '.encode('latin1') +
|
206
|
-
cmd.rstrip() + b'\n')
|
207
|
-
await self._serial.drain()
|
208
|
-
|
209
|
-
async def read(self):
|
210
|
-
# Read a single CAN message and do not parse it.
|
211
|
-
while True:
|
212
|
-
line = await self._readline(self._serial)
|
213
|
-
if not line.startswith(b"rcv"):
|
214
|
-
continue
|
215
|
-
|
216
|
-
if self._debug_log:
|
217
|
-
self._debug_log.write(f'{time.time()} < '.encode('latin1') +
|
218
|
-
line.rstrip() + b'\n')
|
23
|
+
path = Fdcanusb.detect_fdcanusb()
|
219
24
|
|
220
|
-
|
221
|
-
|
25
|
+
device = FdcanusbDevice(path, *args, **kwargs)
|
26
|
+
super().__init__(device)
|
222
27
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
if size <= 12:
|
227
|
-
return 12
|
228
|
-
if size <= 16:
|
229
|
-
return 16
|
230
|
-
if size <= 20:
|
231
|
-
return 20
|
232
|
-
if size <= 24:
|
233
|
-
return 24
|
234
|
-
if size <= 32:
|
235
|
-
return 32
|
236
|
-
if size <= 48:
|
237
|
-
return 48
|
238
|
-
if size <= 64:
|
239
|
-
return 64
|
240
|
-
return size
|
28
|
+
@staticmethod
|
29
|
+
def detect_fdcanusb():
|
30
|
+
return FdcanusbDevice.detect_fdcanusb()
|