motor-python 0.0.9__tar.gz → 0.0.11__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.
- {motor_python-0.0.9 → motor_python-0.0.11}/PKG-INFO +1 -1
- {motor_python-0.0.9 → motor_python-0.0.11}/pyproject.toml +1 -1
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/__init__.py +1 -1
- motor_python-0.0.11/src/motor_python/can_utils.py +275 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/cube_mars_motor_can.py +37 -10
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/definitions.py +12 -0
- motor_python-0.0.9/src/motor_python/can_utils.py +0 -88
- {motor_python-0.0.9 → motor_python-0.0.11}/.gitignore +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/LICENSE +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/README.md +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/scripts/README.md +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/__main__.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/base_motor.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/can_protocol.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/cube_mars_motor.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/examples.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/examples_can.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/motor_control_using_pid.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/motor_manager.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/motor_status_parser.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/pid_controller.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/second_order_low_pass_filter.py +0 -0
- {motor_python-0.0.9 → motor_python-0.0.11}/src/motor_python/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: motor_python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
4
4
|
Summary: CubeMars motor module for Aries exosuits.
|
|
5
5
|
Project-URL: homepage, https://github.com/TUM-Aries-Lab/motor-module
|
|
6
6
|
Author-email: Tsmorz <tony.smoragiewicz@tum.de>, Hannes Nguyen <hannes.nguyen@tum.de>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "motor_python"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.11"
|
|
4
4
|
description = "CubeMars motor module for Aries exosuits."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Tsmorz", email = "tony.smoragiewicz@tum.de"},{ name = "Hannes Nguyen", email = "hannes.nguyen@tum.de" }]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""CAN Network Utilities for Linux (Jetson).
|
|
2
|
+
|
|
3
|
+
Three operations on the interface, in increasing order of violence:
|
|
4
|
+
|
|
5
|
+
* :func:`get_can_state` reads the controller's error state and counters;
|
|
6
|
+
* :func:`ensure_can_interface` raises the link if it is down, and leaves it
|
|
7
|
+
strictly alone if it is already up at the right bitrate;
|
|
8
|
+
* :func:`reset_can_interface` reloads the kernel module, which is the only
|
|
9
|
+
thing that clears latched error counters on the Orin's mttcan controller.
|
|
10
|
+
|
|
11
|
+
Reach for the lightest one that answers the problem. Reconfiguring a working
|
|
12
|
+
bus is not free: taking the link down and up resets the controller and can
|
|
13
|
+
drop a motor into BUS-OFF.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
from motor_python.definitions import CAN_DEFAULTS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_can_state(interface: str = "can0") -> dict:
|
|
26
|
+
"""Parse CAN controller state from the kernel via `ip` command.
|
|
27
|
+
|
|
28
|
+
:param interface: CAN interface name (e.g. 'can0').
|
|
29
|
+
:return: Dict with keys 'state', 'tx_err', 'rx_err'.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
["ip", "-details", "-statistics", "link", "show", interface],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=5,
|
|
37
|
+
)
|
|
38
|
+
output = result.stdout
|
|
39
|
+
state = "UNKNOWN"
|
|
40
|
+
tx_err = rx_err = 0
|
|
41
|
+
for line in output.splitlines():
|
|
42
|
+
if "state" in line and "berr-counter" in line:
|
|
43
|
+
for part in line.split():
|
|
44
|
+
if part in (
|
|
45
|
+
"ERROR-ACTIVE",
|
|
46
|
+
"ERROR-PASSIVE",
|
|
47
|
+
"ERROR-WARNING",
|
|
48
|
+
"BUS-OFF",
|
|
49
|
+
):
|
|
50
|
+
state = part
|
|
51
|
+
if "tx" in line:
|
|
52
|
+
idx = line.index("tx")
|
|
53
|
+
tx_err = int(line[idx:].split()[1])
|
|
54
|
+
if "rx" in line:
|
|
55
|
+
idx = line.index("rx")
|
|
56
|
+
rx_err = int(line[idx:].split()[1].rstrip(")"))
|
|
57
|
+
return {"state": state, "tx_err": tx_err, "rx_err": rx_err}
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.debug(f"Could not read CAN state: {e}")
|
|
60
|
+
return {"state": "UNKNOWN", "tx_err": 0, "rx_err": 0}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reset_can_interface(interface: str = "can0", bitrate: int = 1000000) -> bool:
|
|
64
|
+
"""Fully reset the CAN interface by reloading the mttcan kernel module.
|
|
65
|
+
|
|
66
|
+
``ip link set down/up`` does NOT reset TX/RX error counters on the
|
|
67
|
+
Jetson Orin Nano mttcan controller and causes a CAN bus disruption
|
|
68
|
+
that can push the motor into BUS-OFF (completely silent). This method
|
|
69
|
+
instead:
|
|
70
|
+
|
|
71
|
+
1. Takes the interface down.
|
|
72
|
+
2. Unloads the mttcan module — the bus is silent during unload, giving
|
|
73
|
+
the motor's CAN controller time to see the 128×11 recessive bits
|
|
74
|
+
required by the CAN spec to recover from BUS-OFF.
|
|
75
|
+
3. Reloads the module and brings the interface back up.
|
|
76
|
+
|
|
77
|
+
:param interface: CAN interface name (e.g. 'can0').
|
|
78
|
+
:param bitrate: Desired bitrate (e.g. 1000000).
|
|
79
|
+
:return: True if successful, False if the commands fail (e.g. no sudo).
|
|
80
|
+
"""
|
|
81
|
+
logger.warning(
|
|
82
|
+
f"Performing full kernel-level reset of {interface} to clear error states..."
|
|
83
|
+
)
|
|
84
|
+
cmds = [
|
|
85
|
+
f"sudo ip link set {interface} down",
|
|
86
|
+
"sudo rmmod mttcan",
|
|
87
|
+
"sleep 0.5", # Let bus stay recessive so nodes can recover
|
|
88
|
+
"sudo modprobe mttcan",
|
|
89
|
+
# Keep runtime reset behavior aligned with setup_can.sh.
|
|
90
|
+
f"sudo ip link set {interface} up type can bitrate {bitrate} berr-reporting on restart-ms 100",
|
|
91
|
+
f"sudo ip link set {interface} txqueuelen 1000",
|
|
92
|
+
]
|
|
93
|
+
try:
|
|
94
|
+
for cmd in cmds:
|
|
95
|
+
if cmd == "sleep 0.5":
|
|
96
|
+
time.sleep(0.5)
|
|
97
|
+
else:
|
|
98
|
+
subprocess.run(cmd.split(), check=True)
|
|
99
|
+
time.sleep(0.1)
|
|
100
|
+
logger.success(f"CAN interface {interface} reset successfully.")
|
|
101
|
+
return True
|
|
102
|
+
except subprocess.CalledProcessError as e:
|
|
103
|
+
logger.error(f"Failed to reset CAN interface: {e}")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run(command: list[str], timeout: float) -> subprocess.CompletedProcess | None:
|
|
108
|
+
"""Run a command, returning None if it could not be run at all.
|
|
109
|
+
|
|
110
|
+
:param command: Argument vector.
|
|
111
|
+
:param timeout: Seconds to wait before giving up.
|
|
112
|
+
:return: The finished process, or None if it could not run.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
# check=False: a non-zero exit is information here, not an exception.
|
|
116
|
+
# The caller reads returncode and reports it with the interface's name.
|
|
117
|
+
return subprocess.run(
|
|
118
|
+
command, capture_output=True, text=True, timeout=timeout, check=False
|
|
119
|
+
)
|
|
120
|
+
except (OSError, subprocess.SubprocessError) as err:
|
|
121
|
+
logger.debug(f"Could not run {' '.join(command)}: '{err}'.")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_can_interface(
|
|
126
|
+
interface: str = CAN_DEFAULTS.interface,
|
|
127
|
+
) -> tuple[bool, bool, int | None]:
|
|
128
|
+
"""Report what the kernel currently thinks of a CAN interface.
|
|
129
|
+
|
|
130
|
+
:param interface: CAN interface name (e.g. 'can0').
|
|
131
|
+
:return: ``(exists, is_up, bitrate)``; bitrate is None when unreadable.
|
|
132
|
+
:rtype: tuple[bool, bool, int | None]
|
|
133
|
+
"""
|
|
134
|
+
result = _run(
|
|
135
|
+
["ip", "-details", "link", "show", interface],
|
|
136
|
+
CAN_DEFAULTS.can_command_timeout_s,
|
|
137
|
+
)
|
|
138
|
+
if result is None or result.returncode != 0:
|
|
139
|
+
return False, False, None
|
|
140
|
+
|
|
141
|
+
output = result.stdout
|
|
142
|
+
# The flags live in angle brackets on the first line. "UP" also appears in
|
|
143
|
+
# "state UP" further along, but the flag is the authoritative one -- an
|
|
144
|
+
# interface can carry the UP flag while its state reads UNKNOWN, which is
|
|
145
|
+
# normal for CAN.
|
|
146
|
+
first_line = output.splitlines()[0] if output else ""
|
|
147
|
+
is_up = "UP" in first_line.split("<")[-1].split(">")[0].split(",")
|
|
148
|
+
|
|
149
|
+
bitrate = None
|
|
150
|
+
fields = output.split()
|
|
151
|
+
if "bitrate" in fields:
|
|
152
|
+
try:
|
|
153
|
+
bitrate = int(fields[fields.index("bitrate") + 1])
|
|
154
|
+
except (IndexError, ValueError):
|
|
155
|
+
bitrate = None
|
|
156
|
+
return True, is_up, bitrate
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def ensure_can_interface(
|
|
160
|
+
interface: str = CAN_DEFAULTS.interface,
|
|
161
|
+
bitrate: int = CAN_DEFAULTS.bitrate,
|
|
162
|
+
) -> bool:
|
|
163
|
+
"""Make sure a CAN interface is up at the given bitrate, raising it if not.
|
|
164
|
+
|
|
165
|
+
Callers otherwise have to remember ``sudo ./setup_can.sh`` after every power
|
|
166
|
+
cycle, and forgetting it presents as a motor that will not answer -- which
|
|
167
|
+
sends you to the wiring for a fault that is one command away.
|
|
168
|
+
|
|
169
|
+
**Idempotent**: an interface already up at the right bitrate is left
|
|
170
|
+
untouched. That is the point of the check rather than a shortcut through
|
|
171
|
+
it, because bringing a working link down and up resets the controller and
|
|
172
|
+
can drop a motor into BUS-OFF.
|
|
173
|
+
|
|
174
|
+
This does not reload the ``mttcan`` module; see
|
|
175
|
+
:func:`reset_can_interface` for that, which is the right tool once error
|
|
176
|
+
counters have latched and the wrong one to use unasked at startup.
|
|
177
|
+
|
|
178
|
+
:param interface: CAN interface name (e.g. 'can0').
|
|
179
|
+
:param bitrate: Desired bitrate in bits/sec.
|
|
180
|
+
:return: True if the interface is usable afterwards.
|
|
181
|
+
:rtype: bool
|
|
182
|
+
"""
|
|
183
|
+
if shutil.which("ip") is None:
|
|
184
|
+
logger.debug(
|
|
185
|
+
"No 'ip' command, so this is not a Linux host. Skipping CAN setup."
|
|
186
|
+
)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
exists, is_up, current_bitrate = read_can_interface(interface)
|
|
190
|
+
if not exists:
|
|
191
|
+
logger.error(
|
|
192
|
+
f"No CAN interface '{interface}'. The mttcan kernel module is "
|
|
193
|
+
f"probably not loaded; run setup_can.sh once, which modprobes it."
|
|
194
|
+
)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
if is_up and current_bitrate == bitrate:
|
|
198
|
+
logger.info(
|
|
199
|
+
f"CAN interface '{interface}' is already up at {bitrate} bps. "
|
|
200
|
+
f"Leaving it alone."
|
|
201
|
+
)
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
reason = "down" if not is_up else f"at {current_bitrate} bps rather than {bitrate}"
|
|
205
|
+
logger.warning(f"CAN interface '{interface}' is {reason}. Bringing it up.")
|
|
206
|
+
return _bring_up_can_interface(interface, bitrate)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _bring_up_can_interface(interface: str, bitrate: int) -> bool:
|
|
210
|
+
"""Configure and raise the interface.
|
|
211
|
+
|
|
212
|
+
``sudo -n`` throughout: a control process must not stop at a password
|
|
213
|
+
prompt, so a host without passwordless sudo fails immediately and says so
|
|
214
|
+
rather than hanging until something times out.
|
|
215
|
+
|
|
216
|
+
:param interface: CAN interface name.
|
|
217
|
+
:param bitrate: Desired bitrate in bits/sec.
|
|
218
|
+
:return: True if the interface came up at the requested bitrate.
|
|
219
|
+
:rtype: bool
|
|
220
|
+
"""
|
|
221
|
+
timeout = CAN_DEFAULTS.can_command_timeout_s
|
|
222
|
+
commands = [
|
|
223
|
+
# Down first: "up type can bitrate ..." is rejected on an interface that
|
|
224
|
+
# is already up, which is the case when only the bitrate is wrong.
|
|
225
|
+
["sudo", "-n", "ip", "link", "set", interface, "down"],
|
|
226
|
+
[
|
|
227
|
+
"sudo",
|
|
228
|
+
"-n",
|
|
229
|
+
"ip",
|
|
230
|
+
"link",
|
|
231
|
+
"set",
|
|
232
|
+
interface,
|
|
233
|
+
"up",
|
|
234
|
+
"type",
|
|
235
|
+
"can",
|
|
236
|
+
"bitrate",
|
|
237
|
+
str(bitrate),
|
|
238
|
+
"berr-reporting",
|
|
239
|
+
"on",
|
|
240
|
+
"restart-ms",
|
|
241
|
+
str(CAN_DEFAULTS.can_restart_ms),
|
|
242
|
+
],
|
|
243
|
+
[
|
|
244
|
+
"sudo",
|
|
245
|
+
"-n",
|
|
246
|
+
"ip",
|
|
247
|
+
"link",
|
|
248
|
+
"set",
|
|
249
|
+
interface,
|
|
250
|
+
"txqueuelen",
|
|
251
|
+
str(CAN_DEFAULTS.can_tx_queue_length),
|
|
252
|
+
],
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for command in commands:
|
|
256
|
+
result = _run(command, timeout)
|
|
257
|
+
if result is None or result.returncode != 0:
|
|
258
|
+
detail = (result.stderr or result.stdout).strip() if result else "not run"
|
|
259
|
+
logger.error(
|
|
260
|
+
f"Could not bring up '{interface}': '{detail}'. Run "
|
|
261
|
+
f"scripts/allow_can_bringup_without_password.sh once to fix this "
|
|
262
|
+
f"permanently, or setup_can.sh with sudo for just this boot."
|
|
263
|
+
)
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
_, is_up, current_bitrate = read_can_interface(interface)
|
|
267
|
+
if not (is_up and current_bitrate == bitrate):
|
|
268
|
+
logger.error(
|
|
269
|
+
f"'{interface}' did not come up as asked: up={is_up}, "
|
|
270
|
+
f"bitrate={current_bitrate}."
|
|
271
|
+
)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
logger.success(f"CAN interface '{interface}' up at {bitrate} bps.")
|
|
275
|
+
return True
|
|
@@ -1282,10 +1282,17 @@ class CubeMarsBaseCAN(BaseMotor):
|
|
|
1282
1282
|
return None
|
|
1283
1283
|
|
|
1284
1284
|
def send_neutral_command(self) -> None:
|
|
1285
|
-
"""
|
|
1285
|
+
"""Send the enter-motor-mode frame.
|
|
1286
1286
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1287
|
+
The name and the docstring this replaced were both wrong -- it was
|
|
1288
|
+
described as returning telemetry, copied from get_status(). What it
|
|
1289
|
+
sends is FF..FC, the MIT enter-motor-mode helper, which is why calling
|
|
1290
|
+
it was the difference between a motor that produced torque and one
|
|
1291
|
+
that accepted every command and produced none.
|
|
1292
|
+
|
|
1293
|
+
Nothing here is neutral. Prefer enable_mit_mode(), which sends the
|
|
1294
|
+
same frame and reports whether the motor answered; this remains for
|
|
1295
|
+
callers that want the frame alone.
|
|
1289
1296
|
"""
|
|
1290
1297
|
if not self.connected:
|
|
1291
1298
|
return
|
|
@@ -1412,13 +1419,13 @@ class CubeMarsBaseCAN(BaseMotor):
|
|
|
1412
1419
|
|
|
1413
1420
|
self._mit_enabled = False
|
|
1414
1421
|
neutral = self._mit_neutral_payload()
|
|
1415
|
-
feedback_before = self._last_feedback_monotonic
|
|
1416
|
-
sent = self._send_mit_payload(neutral, capture_response=True)
|
|
1417
|
-
if sent and self._last_feedback_monotonic > feedback_before:
|
|
1418
|
-
self._mit_enabled = True
|
|
1419
|
-
logger.info("MIT mode ready via direct MIT command handshake")
|
|
1420
|
-
return
|
|
1421
1422
|
|
|
1423
|
+
# The helper frame is what actually enters motor mode. It is sent
|
|
1424
|
+
# first, not as a fallback, because fresh feedback cannot tell an
|
|
1425
|
+
# enabled motor from a disabled one: the motor answers an MIT-format
|
|
1426
|
+
# frame either way. Concluding from that alone reported success
|
|
1427
|
+
# against a motor that then produced no torque -- commands accepted,
|
|
1428
|
+
# position and speed reported, torque frozen at one value.
|
|
1422
1429
|
helper_frames: list[tuple[str, bytes]] = []
|
|
1423
1430
|
if self._helper_policy in {"fcfd", "legacy"}:
|
|
1424
1431
|
helper_frames.append(("FF..FC", self._CAN_HELPER_ENABLE))
|
|
@@ -1427,10 +1434,14 @@ class CubeMarsBaseCAN(BaseMotor):
|
|
|
1427
1434
|
|
|
1428
1435
|
for label, frame in helper_frames:
|
|
1429
1436
|
logger.debug(f"Attempting MIT handshake with helper frame {label}")
|
|
1437
|
+
# No response captured: the helper is a fire-and-forget command
|
|
1438
|
+
# and proves nothing by replying. The MIT payload below is what
|
|
1439
|
+
# the enable is verified against, so demanding a reply here would
|
|
1440
|
+
# only add a second thing to go wrong.
|
|
1430
1441
|
helper_sent = self._send_raw(
|
|
1431
1442
|
arbitration_id=self.motor_can_id,
|
|
1432
1443
|
data=frame,
|
|
1433
|
-
capture_response=
|
|
1444
|
+
capture_response=False,
|
|
1434
1445
|
)
|
|
1435
1446
|
if not helper_sent:
|
|
1436
1447
|
continue
|
|
@@ -1442,6 +1453,22 @@ class CubeMarsBaseCAN(BaseMotor):
|
|
|
1442
1453
|
logger.info(f"MIT mode ready after helper frame {label}")
|
|
1443
1454
|
return
|
|
1444
1455
|
|
|
1456
|
+
# No helper frame is available under the strict policy, and under the
|
|
1457
|
+
# others none got through. Falling back to the direct handshake keeps
|
|
1458
|
+
# those callers working, but it proves only that the motor is
|
|
1459
|
+
# answering -- so say so rather than logging readiness.
|
|
1460
|
+
feedback_before = self._last_feedback_monotonic
|
|
1461
|
+
sent = self._send_mit_payload(neutral, capture_response=True)
|
|
1462
|
+
if sent and self._last_feedback_monotonic > feedback_before:
|
|
1463
|
+
self._mit_enabled = True
|
|
1464
|
+
logger.warning(
|
|
1465
|
+
"MIT mode assumed ready from a direct command handshake: the "
|
|
1466
|
+
"motor is answering, but nothing confirms it entered motor "
|
|
1467
|
+
"mode, and a disabled motor answers identically. Expect "
|
|
1468
|
+
"commands to be accepted and produce no torque if it did not."
|
|
1469
|
+
)
|
|
1470
|
+
return
|
|
1471
|
+
|
|
1445
1472
|
can_state = self._read_can_state(force=True)
|
|
1446
1473
|
raise RuntimeError(
|
|
1447
1474
|
"Failed to enable MIT mode: no fresh feedback observed after MIT handshake "
|
|
@@ -98,6 +98,18 @@ class CANDefaults:
|
|
|
98
98
|
retry_backoff: float = 0.01 # Base backoff time for retries (seconds)
|
|
99
99
|
can_reset_pause: float = 0.1 # Small Pause after CAN bus reset (seconds)
|
|
100
100
|
|
|
101
|
+
# Link settings used when raising the interface, matching setup_can.sh.
|
|
102
|
+
# berr-reporting and restart-ms are what let the controller restart itself
|
|
103
|
+
# out of BUS-OFF -- a single unacknowledged frame is enough to cause it, and
|
|
104
|
+
# without these the interface goes permanently silent instead of recovering.
|
|
105
|
+
can_restart_ms: int = 100
|
|
106
|
+
# The kernel default of 10 is too shallow for a 100 Hz control loop driving
|
|
107
|
+
# two motors.
|
|
108
|
+
can_tx_queue_length: int = 1000
|
|
109
|
+
# Per `ip` invocation. Generous for a command that returns immediately, and
|
|
110
|
+
# short enough that a host which cannot run it does not stall startup.
|
|
111
|
+
can_command_timeout_s: float = 5.0
|
|
112
|
+
|
|
101
113
|
|
|
102
114
|
@dataclass(frozen=True)
|
|
103
115
|
class MITModeLimits:
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"""CAN Network Utilities for Linux (Jetson)."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
from loguru import logger
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_can_state(interface: str = "can0") -> dict:
|
|
10
|
-
"""Parse CAN controller state from the kernel via `ip` command.
|
|
11
|
-
|
|
12
|
-
:param interface: CAN interface name (e.g. 'can0').
|
|
13
|
-
:return: Dict with keys 'state', 'tx_err', 'rx_err'.
|
|
14
|
-
"""
|
|
15
|
-
try:
|
|
16
|
-
result = subprocess.run(
|
|
17
|
-
["ip", "-details", "-statistics", "link", "show", interface],
|
|
18
|
-
capture_output=True,
|
|
19
|
-
text=True,
|
|
20
|
-
timeout=5,
|
|
21
|
-
)
|
|
22
|
-
output = result.stdout
|
|
23
|
-
state = "UNKNOWN"
|
|
24
|
-
tx_err = rx_err = 0
|
|
25
|
-
for line in output.splitlines():
|
|
26
|
-
if "state" in line and "berr-counter" in line:
|
|
27
|
-
for part in line.split():
|
|
28
|
-
if part in (
|
|
29
|
-
"ERROR-ACTIVE",
|
|
30
|
-
"ERROR-PASSIVE",
|
|
31
|
-
"ERROR-WARNING",
|
|
32
|
-
"BUS-OFF",
|
|
33
|
-
):
|
|
34
|
-
state = part
|
|
35
|
-
if "tx" in line:
|
|
36
|
-
idx = line.index("tx")
|
|
37
|
-
tx_err = int(line[idx:].split()[1])
|
|
38
|
-
if "rx" in line:
|
|
39
|
-
idx = line.index("rx")
|
|
40
|
-
rx_err = int(line[idx:].split()[1].rstrip(")"))
|
|
41
|
-
return {"state": state, "tx_err": tx_err, "rx_err": rx_err}
|
|
42
|
-
except Exception as e:
|
|
43
|
-
logger.debug(f"Could not read CAN state: {e}")
|
|
44
|
-
return {"state": "UNKNOWN", "tx_err": 0, "rx_err": 0}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def reset_can_interface(interface: str = "can0", bitrate: int = 1000000) -> bool:
|
|
48
|
-
"""Fully reset the CAN interface by reloading the mttcan kernel module.
|
|
49
|
-
|
|
50
|
-
``ip link set down/up`` does NOT reset TX/RX error counters on the
|
|
51
|
-
Jetson Orin Nano mttcan controller and causes a CAN bus disruption
|
|
52
|
-
that can push the motor into BUS-OFF (completely silent). This method
|
|
53
|
-
instead:
|
|
54
|
-
|
|
55
|
-
1. Takes the interface down.
|
|
56
|
-
2. Unloads the mttcan module — the bus is silent during unload, giving
|
|
57
|
-
the motor's CAN controller time to see the 128×11 recessive bits
|
|
58
|
-
required by the CAN spec to recover from BUS-OFF.
|
|
59
|
-
3. Reloads the module and brings the interface back up.
|
|
60
|
-
|
|
61
|
-
:param interface: CAN interface name (e.g. 'can0').
|
|
62
|
-
:param bitrate: Desired bitrate (e.g. 1000000).
|
|
63
|
-
:return: True if successful, False if the commands fail (e.g. no sudo).
|
|
64
|
-
"""
|
|
65
|
-
logger.warning(
|
|
66
|
-
f"Performing full kernel-level reset of {interface} to clear error states..."
|
|
67
|
-
)
|
|
68
|
-
cmds = [
|
|
69
|
-
f"sudo ip link set {interface} down",
|
|
70
|
-
"sudo rmmod mttcan",
|
|
71
|
-
"sleep 0.5", # Let bus stay recessive so nodes can recover
|
|
72
|
-
"sudo modprobe mttcan",
|
|
73
|
-
# Keep runtime reset behavior aligned with setup_can.sh.
|
|
74
|
-
f"sudo ip link set {interface} up type can bitrate {bitrate} berr-reporting on restart-ms 100",
|
|
75
|
-
f"sudo ip link set {interface} txqueuelen 1000",
|
|
76
|
-
]
|
|
77
|
-
try:
|
|
78
|
-
for cmd in cmds:
|
|
79
|
-
if cmd == "sleep 0.5":
|
|
80
|
-
time.sleep(0.5)
|
|
81
|
-
else:
|
|
82
|
-
subprocess.run(cmd.split(), check=True)
|
|
83
|
-
time.sleep(0.1)
|
|
84
|
-
logger.success(f"CAN interface {interface} reset successfully.")
|
|
85
|
-
return True
|
|
86
|
-
except subprocess.CalledProcessError as e:
|
|
87
|
-
logger.error(f"Failed to reset CAN interface: {e}")
|
|
88
|
-
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|