motor-python 0.0.10__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.
Files changed (23) hide show
  1. {motor_python-0.0.10 → motor_python-0.0.11}/PKG-INFO +1 -1
  2. {motor_python-0.0.10 → motor_python-0.0.11}/pyproject.toml +1 -1
  3. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/__init__.py +1 -1
  4. motor_python-0.0.11/src/motor_python/can_utils.py +275 -0
  5. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/definitions.py +12 -0
  6. motor_python-0.0.10/src/motor_python/can_utils.py +0 -88
  7. {motor_python-0.0.10 → motor_python-0.0.11}/.gitignore +0 -0
  8. {motor_python-0.0.10 → motor_python-0.0.11}/LICENSE +0 -0
  9. {motor_python-0.0.10 → motor_python-0.0.11}/README.md +0 -0
  10. {motor_python-0.0.10 → motor_python-0.0.11}/scripts/README.md +0 -0
  11. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/__main__.py +0 -0
  12. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/base_motor.py +0 -0
  13. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/can_protocol.py +0 -0
  14. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/cube_mars_motor.py +0 -0
  15. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/cube_mars_motor_can.py +0 -0
  16. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/examples.py +0 -0
  17. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/examples_can.py +0 -0
  18. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/motor_control_using_pid.py +0 -0
  19. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/motor_manager.py +0 -0
  20. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/motor_status_parser.py +0 -0
  21. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/pid_controller.py +0 -0
  22. {motor_python-0.0.10 → motor_python-0.0.11}/src/motor_python/second_order_low_pass_filter.py +0 -0
  23. {motor_python-0.0.10 → 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.10
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.10"
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" }]
@@ -5,7 +5,7 @@ Legacy UART interface: CubeMarsAK606v3.
5
5
  Base class: BaseMotor (for shared interface & safety logic).
6
6
  """
7
7
 
8
- __version__ = "0.0.10"
8
+ __version__ = "0.0.11"
9
9
 
10
10
  from typing import Literal
11
11
 
@@ -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
@@ -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