feathersdk 0.0.8__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.
- feathersdk-0.0.8/PKG-INFO +31 -0
- feathersdk-0.0.8/README.md +3 -0
- feathersdk-0.0.8/pyproject.toml +9 -0
- feathersdk-0.0.8/setup.cfg +31 -0
- feathersdk-0.0.8/setup.py +22 -0
- feathersdk-0.0.8/src/feathersdk/__init__.py +7 -0
- feathersdk-0.0.8/src/feathersdk/comms/__init__.py +4 -0
- feathersdk-0.0.8/src/feathersdk/comms/comms_manager.py +399 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/__init__.py +384 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/recompilation.py +122 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_error.h +93 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_poll.h +267 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_result.h +41 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_sockets.h +148 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_source_hash.h +2 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/sct_write.h +114 -0
- feathersdk-0.0.8/src/feathersdk/comms/socketcan_tcp/socketcan_tcp.c +47 -0
- feathersdk-0.0.8/src/feathersdk/comms/system.py +299 -0
- feathersdk-0.0.8/src/feathersdk/comms/wirehawk.py +215 -0
- feathersdk-0.0.8/src/feathersdk/robot/__init__.py +3 -0
- feathersdk-0.0.8/src/feathersdk/robot/battery_system.py +130 -0
- feathersdk-0.0.8/src/feathersdk/robot/instance_lock.py +239 -0
- feathersdk-0.0.8/src/feathersdk/robot/manipulation_platform.py +145 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/MMS760200-48-C2-1.eds +5172 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/MMS760400-48-C2-1.eds +5172 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/__init__.py +3 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/feetech_simple.py +175 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/motor.py +6 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/motors_manager.py +710 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/myactuator.py +84 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/robstride/__init__.py +7 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/robstride/admin.py +262 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/robstride/message_handler.py +111 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/robstride/params.py +442 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/robstride/robstride_motor.py +465 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/sim/__init__.py +0 -0
- feathersdk-0.0.8/src/feathersdk/robot/motors/sim/virtual_robstride.py +391 -0
- feathersdk-0.0.8/src/feathersdk/robot/navigation_platform.py +1003 -0
- feathersdk-0.0.8/src/feathersdk/robot/neck_platform.py +364 -0
- feathersdk-0.0.8/src/feathersdk/robot/pinocchio_model.py +201 -0
- feathersdk-0.0.8/src/feathersdk/robot/rh56f1_hand/rh56f1_wrapper.h +203 -0
- feathersdk-0.0.8/src/feathersdk/robot/robot.py +154 -0
- feathersdk-0.0.8/src/feathersdk/robot/robstride_neck_platform.py +390 -0
- feathersdk-0.0.8/src/feathersdk/robot/self_collision_checker.py +436 -0
- feathersdk-0.0.8/src/feathersdk/robot/steppable_system.py +57 -0
- feathersdk-0.0.8/src/feathersdk/robot/test_system.py +23 -0
- feathersdk-0.0.8/src/feathersdk/robot/torso_platform.py +519 -0
- feathersdk-0.0.8/src/feathersdk/robot/vision_platform.py +117 -0
- feathersdk-0.0.8/src/feathersdk/utils/__init__.py +1 -0
- feathersdk-0.0.8/src/feathersdk/utils/common.py +292 -0
- feathersdk-0.0.8/src/feathersdk/utils/constants.py +13 -0
- feathersdk-0.0.8/src/feathersdk/utils/feathertypes.py +4 -0
- feathersdk-0.0.8/src/feathersdk/utils/files.py +33 -0
- feathersdk-0.0.8/src/feathersdk/utils/format.py +63 -0
- feathersdk-0.0.8/src/feathersdk/utils/hydra_helper.py +21 -0
- feathersdk-0.0.8/src/feathersdk/utils/logger.py +179 -0
- feathersdk-0.0.8/src/feathersdk/utils/perf_logger.py +13 -0
- feathersdk-0.0.8/src/feathersdk/utils/telemetry.py +141 -0
- feathersdk-0.0.8/src/feathersdk/utils/timeout.py +65 -0
- feathersdk-0.0.8/src/feathersdk/utils/trajectory.py +8 -0
- feathersdk-0.0.8/src/feathersdk/utils/urdf_convex_hull.py +650 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/PKG-INFO +31 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/SOURCES.txt +66 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/dependency_links.txt +1 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/not-zip-safe +1 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/requires.txt +7 -0
- feathersdk-0.0.8/src/feathersdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: feathersdk
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: Feather Python SDK Library
|
|
5
|
+
Author: Feather Robotics Inc.
|
|
6
|
+
License: "MIT"
|
|
7
|
+
Platform: unix
|
|
8
|
+
Platform: linux
|
|
9
|
+
Platform: osx
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: canopen
|
|
19
|
+
Requires-Dist: typing_extensions
|
|
20
|
+
Requires-Dist: filelock
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: psutil
|
|
23
|
+
Requires-Dist: smbus2
|
|
24
|
+
Requires-Dist: hydra-core
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
|
|
29
|
+
# Feather Python SDK Library
|
|
30
|
+
|
|
31
|
+
Feather Code!
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = feathersdk
|
|
3
|
+
version = 0.0.8
|
|
4
|
+
description = Feather Python SDK Library
|
|
5
|
+
readme = README.md
|
|
6
|
+
author = Feather Robotics Inc.
|
|
7
|
+
platforms = unix, linux, osx
|
|
8
|
+
classifiers =
|
|
9
|
+
Programming Language :: Python :: 3
|
|
10
|
+
Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Programming Language :: Python :: 3.9
|
|
12
|
+
Programming Language :: Python :: 3.10
|
|
13
|
+
Programming Language :: Python :: 3.11
|
|
14
|
+
Programming Language :: Python :: 3.12
|
|
15
|
+
license = "MIT"
|
|
16
|
+
license-files = ["LICEN[CS]E*"]
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
packages =
|
|
20
|
+
feathersdk
|
|
21
|
+
install_requires =
|
|
22
|
+
numpy>=1.10
|
|
23
|
+
python_requires = >=3.9
|
|
24
|
+
package_dir =
|
|
25
|
+
=src
|
|
26
|
+
zip_safe = no
|
|
27
|
+
|
|
28
|
+
[egg_info]
|
|
29
|
+
tag_build =
|
|
30
|
+
tag_date = 0
|
|
31
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
readme_path = os.path.join(os.path.dirname(__file__), 'README.md')
|
|
6
|
+
setup(
|
|
7
|
+
packages=find_packages(where='./src'),
|
|
8
|
+
package_data={
|
|
9
|
+
"feathersdk": ["**/*.c", "**/*.h", "**/*.eds"],
|
|
10
|
+
},
|
|
11
|
+
long_description=open(readme_path).read(),
|
|
12
|
+
long_description_content_type='text/markdown',
|
|
13
|
+
install_requires=[
|
|
14
|
+
'canopen',
|
|
15
|
+
'typing_extensions',
|
|
16
|
+
'filelock',
|
|
17
|
+
'numpy',
|
|
18
|
+
'psutil',
|
|
19
|
+
'smbus2',
|
|
20
|
+
'hydra-core',
|
|
21
|
+
],
|
|
22
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Suppress all logging from canopen. TODO: Filter the messages to keep the ones we want to worry about.
|
|
4
|
+
# Note: there are no CRITICAL logging messages in canopen 2.4.1, so this should disable all sdo.client/server messages.
|
|
5
|
+
import logging
|
|
6
|
+
logging.getLogger("canopen.sdo.client").setLevel(logging.CRITICAL)
|
|
7
|
+
logging.getLogger("canopen.sdo.server").setLevel(logging.CRITICAL)
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import traceback
|
|
3
|
+
from feathersdk.utils.feathertypes import Optional, Callable, Any, Union, Self
|
|
4
|
+
from .system import is_can_interface, enable_can_interface, is_can_enabled
|
|
5
|
+
from .socketcan_tcp import _subscribe_multi, _cansend, _tcpsend_modbus, SocketResult, _stop_polling, \
|
|
6
|
+
_clear_all_sockets, _get_err_str
|
|
7
|
+
from feathersdk.utils.common import make_exception_pickleable, FeatherThreadLock, exponential_timeout, currtime, \
|
|
8
|
+
timediff, TimestampedMixin
|
|
9
|
+
import math
|
|
10
|
+
from collections import namedtuple
|
|
11
|
+
from feathersdk.utils.logger import warning, debug
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MotorUIDType = str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SocketCANLibError(Exception):
|
|
19
|
+
"""Error raised when the socketcan library returns a non-zero error code."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class CanOverloadError(Exception):
|
|
23
|
+
"""Error raised when the CAN bus is overloaded."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@make_exception_pickleable
|
|
27
|
+
class UnknownInterfaceError(Exception):
|
|
28
|
+
"""Error raised when the interface is not an interface, or is not being tracked by CommsManager."""
|
|
29
|
+
def __init__(self, *, interface: str):
|
|
30
|
+
super().__init__(f"Interface \"{interface}\" is not an interface, or is not being tracked by CommsManager")
|
|
31
|
+
|
|
32
|
+
@make_exception_pickleable
|
|
33
|
+
class CommsManagerNotRunningError(Exception):
|
|
34
|
+
"""Error raised when the comms manager is not running."""
|
|
35
|
+
def __init__(self):
|
|
36
|
+
super().__init__("CommsManager is not running")
|
|
37
|
+
|
|
38
|
+
@make_exception_pickleable
|
|
39
|
+
class CanNotEnabledError(Exception):
|
|
40
|
+
"""Error raised when a CAN interface is not enabled."""
|
|
41
|
+
def __init__(self, *, interface: str):
|
|
42
|
+
super().__init__(f"CAN interface \"{interface}\" is not enabled")
|
|
43
|
+
|
|
44
|
+
@make_exception_pickleable
|
|
45
|
+
class MissingMotorUIDError(Exception):
|
|
46
|
+
"""Error raised when a motor UID is required but not provided."""
|
|
47
|
+
def __init__(self):
|
|
48
|
+
super().__init__(f"`motor_uid` is required in single message mode")
|
|
49
|
+
|
|
50
|
+
@make_exception_pickleable
|
|
51
|
+
class PendingReceiveMessageError(Exception):
|
|
52
|
+
"""Error raised when a message is to be sent to a motor when we are awaiting a response in single message mode."""
|
|
53
|
+
def __init__(self, *, motor_uid: MotorUIDType):
|
|
54
|
+
super().__init__(f"Message to motor \"{motor_uid}\" is pending, cannot send more until response is received")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# We calculate the maximum number of messages that can be sent over a CAN bus per second using math similar to:
|
|
58
|
+
# https://electronics.stackexchange.com/questions/121329/whats-the-maximum-can-bus-frame-message-rate-at-125-kbit-s
|
|
59
|
+
#
|
|
60
|
+
# Including all the extra overhead, CRC, bit stuffing, etc, we get a theoretical 144 bits per frame. At 1Mbs, this
|
|
61
|
+
# would be a theoretical max of 6944 frames per second. Since most all messages we send will have 1 reply from the
|
|
62
|
+
# device, we divide by 2 to get 3472 messages-sent per second. To give us ~10% leeway, we limit to 3000 messages-sent
|
|
63
|
+
# per second on average.
|
|
64
|
+
#
|
|
65
|
+
# Using our exponential decay formula of N_{x+1} = N_x * e^(-t * decay_rate) + decay_scale, and a decay rate of 0.01,
|
|
66
|
+
# experiments (see /playground/justin_exp_decay_math.py) show that at a rate of 3000 messages-sent per second, the
|
|
67
|
+
# decayed-count maxes out at ~300. So, we use this limit here.
|
|
68
|
+
CAN_BUS_DECAY_LIMIT: float = 300.0;
|
|
69
|
+
CAN_BUS_DECAY_RATE: float = 0.01;
|
|
70
|
+
CAN_BUS_DECAY_SCALE: float = 10.0; # Controls how quickly we reach our limit. Higher=more quicker.
|
|
71
|
+
# Value of 10.0 = ~30 messages before limit at full speed, ~50 at 100us/message
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
LoadAndTime = namedtuple("LoadAndTime", ["load", "last_message_time"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class SendCanMessage(TimestampedMixin):
|
|
79
|
+
"""Data class representing a CAN message that is about to be sent. Only used for WireHawk"""
|
|
80
|
+
iface: str
|
|
81
|
+
extended: bool
|
|
82
|
+
can_id: int
|
|
83
|
+
data: bytes
|
|
84
|
+
motor_uid: Optional[MotorUIDType] = None
|
|
85
|
+
|
|
86
|
+
def __hash__(self) -> int:
|
|
87
|
+
return hash((self.iface, self.extended, self.can_id, self.data, self.motor_uid))
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class SendTCPMessage(TimestampedMixin):
|
|
91
|
+
"""Data class representing a TCP message that is about to be sent. Only used for WireHawk"""
|
|
92
|
+
ip: str
|
|
93
|
+
tid: int
|
|
94
|
+
uid: int
|
|
95
|
+
fcode: int
|
|
96
|
+
reg_addr: int
|
|
97
|
+
reg_val: int
|
|
98
|
+
|
|
99
|
+
def __hash__(self) -> int:
|
|
100
|
+
return hash((self.ip, self.tid, self.uid, self.fcode, self.reg_addr, self.reg_val))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
SendCallbackParamType = Union[SendCanMessage, SendTCPMessage]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CommsManager:
|
|
107
|
+
"""Handles sending and receiving messages over CAN and TCP.
|
|
108
|
+
|
|
109
|
+
Provides a single point of entry for sending and receiving messages over CAN and TCP. Any new instances of this
|
|
110
|
+
class will point to the same instance.
|
|
111
|
+
|
|
112
|
+
The manager also checks for message overload on the CAN bus and will raise an error if the message rate is too high.
|
|
113
|
+
|
|
114
|
+
You can
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
_instance: Optional[Self] = None
|
|
118
|
+
"""The singleton instance of the CommsManager. Do not modify at runtime!!!"""
|
|
119
|
+
|
|
120
|
+
_lock: FeatherThreadLock = FeatherThreadLock("CommsManager")
|
|
121
|
+
|
|
122
|
+
def __new__(cls) -> Self:
|
|
123
|
+
with cls._lock:
|
|
124
|
+
if cls._instance is None:
|
|
125
|
+
cls._instance = super(CommsManager, cls).__new__(cls)
|
|
126
|
+
cls._instance.__reset()
|
|
127
|
+
return cls._instance
|
|
128
|
+
|
|
129
|
+
def __reset(self) -> None:
|
|
130
|
+
"""Reset the comms manager to a clean state. Assumes you have the lock already."""
|
|
131
|
+
if hasattr(self, "thread") and self.is_running():
|
|
132
|
+
raise ValueError("Cannot reset CommsManager while it is running")
|
|
133
|
+
|
|
134
|
+
self.thread = None
|
|
135
|
+
self._thread_error: Optional[Exception] = None
|
|
136
|
+
self._thread_traceback: Optional[str] = None
|
|
137
|
+
|
|
138
|
+
self.is_dry = False
|
|
139
|
+
self._callbacks: list[Callable[[SocketResult], None]] = []
|
|
140
|
+
self.endpoints: list[str] = []
|
|
141
|
+
self._can_loads: dict[str, LoadAndTime] = {}
|
|
142
|
+
self.__overload_check = True
|
|
143
|
+
|
|
144
|
+
self._pending_msgs: dict[str, set[MotorUIDType]] = {}
|
|
145
|
+
self._single_msg_checkers: dict[str, Callable[[SocketResult], MotorUIDType]] = {}
|
|
146
|
+
self._single_msg_mode: bool = False
|
|
147
|
+
|
|
148
|
+
# Do not override any existing send callbacks since they're used by WireHawk
|
|
149
|
+
send_callbacks = [] if not hasattr(self, "_send_callbacks") else self._send_callbacks
|
|
150
|
+
self._send_callbacks: list[Callable[[SendCallbackParamType], None]] = send_callbacks
|
|
151
|
+
|
|
152
|
+
def _DANGEROUS_disable_overload_check(self) -> None:
|
|
153
|
+
"""WARNING: Only use this for testing!"""
|
|
154
|
+
with self._lock:
|
|
155
|
+
self.__overload_check = False
|
|
156
|
+
|
|
157
|
+
def set_single_message_mode(self, single_message_mode: bool) -> None:
|
|
158
|
+
"""Set the comms manager to single message mode.
|
|
159
|
+
|
|
160
|
+
In single message mode, the comms manager will only send one message at a time to any one motor.
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
self._single_msg_mode = single_message_mode
|
|
164
|
+
|
|
165
|
+
def set_is_dry(self, is_dry: bool) -> None:
|
|
166
|
+
"""Enable/disable 'dry run' mode.
|
|
167
|
+
|
|
168
|
+
In dry run mode, the comms manager will not send any messages, and instead just print info about them
|
|
169
|
+
"""
|
|
170
|
+
with self._lock:
|
|
171
|
+
self.is_dry = is_dry
|
|
172
|
+
|
|
173
|
+
def start(self, endpoints: list[str], enable_cans: bool = True, allow_no_enable_can: bool = False) -> None:
|
|
174
|
+
"""Start the comms manager
|
|
175
|
+
|
|
176
|
+
This will start a background thread that will poll all the endpoints and call the callback for each message.
|
|
177
|
+
|
|
178
|
+
Will call `enable_can_interface` for each endpoint that is a CAN interface with default bitrate of 1Mbs. Will
|
|
179
|
+
create vCAN interfaces for each endpoint that is a vCAN interface.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
endpoints: list of endpoints to subscribe to, eg: ["can0", "vcan1", "192.168.11.210"], etc.
|
|
183
|
+
enable_cans: If True, will attempt to enable any CAN interface that is not already enabled.
|
|
184
|
+
allow_no_enable_can: If True, will not raise an error if any of the CAN interfaces are not enabled.
|
|
185
|
+
"""
|
|
186
|
+
with self._lock:
|
|
187
|
+
if self.is_running():
|
|
188
|
+
raise ValueError("CommsManager is already running")
|
|
189
|
+
|
|
190
|
+
for ep in endpoints:
|
|
191
|
+
if is_can_interface(ep):
|
|
192
|
+
try:
|
|
193
|
+
if not is_can_enabled(ep, bitrate=1_000_000):
|
|
194
|
+
if not enable_cans:
|
|
195
|
+
raise CanNotEnabledError(interface=ep)
|
|
196
|
+
enable_can_interface(ep, bitrate=1_000_000)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
if not allow_no_enable_can:
|
|
199
|
+
raise e
|
|
200
|
+
else:
|
|
201
|
+
warning(f"Could not enable CAN interface {ep}: {e}, for the robot to work properly, "
|
|
202
|
+
f"run `sudo ip link set {ep} up`")
|
|
203
|
+
|
|
204
|
+
_get_err_str() # Just to make sure library is compiled and ready
|
|
205
|
+
|
|
206
|
+
self.endpoints = endpoints
|
|
207
|
+
self._can_loads = {ep: LoadAndTime(0.0, 0.0) for ep in endpoints if is_can_interface(ep)}
|
|
208
|
+
|
|
209
|
+
self.thread = threading.Thread(target=self._thread_func, daemon=True)
|
|
210
|
+
self._thread_error = None
|
|
211
|
+
self._thread_traceback = None
|
|
212
|
+
self.thread.start()
|
|
213
|
+
|
|
214
|
+
def _thread_func(self) -> None:
|
|
215
|
+
"""Thread function for the comms manager."""
|
|
216
|
+
try:
|
|
217
|
+
_subscribe_multi(self.endpoints, self.__main_callback)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self._thread_error = e
|
|
220
|
+
self._thread_traceback = traceback.format_exc()
|
|
221
|
+
warning(f"Error in comms manager thread:\n{self._thread_traceback}")
|
|
222
|
+
|
|
223
|
+
def restart_with_added_endpoints(self, endpoints: Union[str, list[str]], start_ok: bool = False) -> None:
|
|
224
|
+
"""Add endpoints to the (running) comms manager and restart it
|
|
225
|
+
|
|
226
|
+
Note: if the endpoint is already in the comms manager, this will do nothing.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
endpoints: The endpoint(s) to add. Can be string or multiple strings.
|
|
230
|
+
start_ok: By default, an error will be raised if the comms manager is not running. If True, will start the
|
|
231
|
+
comms manager if it is not running.
|
|
232
|
+
"""
|
|
233
|
+
if isinstance(endpoints, str):
|
|
234
|
+
endpoints = [endpoints]
|
|
235
|
+
|
|
236
|
+
if not self.is_running():
|
|
237
|
+
if start_ok:
|
|
238
|
+
return self.start(endpoints)
|
|
239
|
+
raise CommsManagerNotRunningError()
|
|
240
|
+
|
|
241
|
+
with self._lock:
|
|
242
|
+
new_endpoints = list(set(endpoints).union(set(self.endpoints)))
|
|
243
|
+
|
|
244
|
+
if len(new_endpoints) > 0:
|
|
245
|
+
self.close()
|
|
246
|
+
self.start(new_endpoints)
|
|
247
|
+
|
|
248
|
+
def add_callback(self, callback: Callable[[SocketResult], None]) -> None:
|
|
249
|
+
"""Add a callback to be called for each message."""
|
|
250
|
+
with self._lock:
|
|
251
|
+
self._callbacks.append(callback)
|
|
252
|
+
|
|
253
|
+
def _add_send_callback(self, callback: Callable[[SendCallbackParamType], None]) -> None:
|
|
254
|
+
"""Add a callback to be called for each message that is about to be sent."""
|
|
255
|
+
with self._lock:
|
|
256
|
+
self._send_callbacks.append(callback)
|
|
257
|
+
|
|
258
|
+
def remove_callback(self, callback: Callable[[SocketResult], None]) -> None:
|
|
259
|
+
"""Remove a callback from being called for each message."""
|
|
260
|
+
with self._lock:
|
|
261
|
+
self._callbacks.remove(callback)
|
|
262
|
+
|
|
263
|
+
def close(self, timeout: float = 0.1) -> None:
|
|
264
|
+
"""Close the comms manager.
|
|
265
|
+
|
|
266
|
+
This will stop the background thread and close all the endpoints.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
timeout: The timeout in seconds to wait for the comms manager to ensure closed.
|
|
270
|
+
"""
|
|
271
|
+
with self._lock:
|
|
272
|
+
if not self.is_running():
|
|
273
|
+
raise CommsManagerNotRunningError()
|
|
274
|
+
|
|
275
|
+
_stop_polling()
|
|
276
|
+
|
|
277
|
+
def _clear_all_sockets_and_reset():
|
|
278
|
+
"""Clear all sockets and reset the comms manager.
|
|
279
|
+
|
|
280
|
+
Used within exponential_timeout. Checks to make sure the thread is actually stopped, then clears and
|
|
281
|
+
resets the comms manager. We need to do it this way because we can get into deadlocks if we keep the lock
|
|
282
|
+
the entire time and a message comes in to the handler.
|
|
283
|
+
"""
|
|
284
|
+
with self._lock:
|
|
285
|
+
if self.is_running():
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
_clear_all_sockets()
|
|
289
|
+
self.__reset()
|
|
290
|
+
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
# Make sure the thread is actually stopped
|
|
294
|
+
try:
|
|
295
|
+
exponential_timeout(timeout, _clear_all_sockets_and_reset)
|
|
296
|
+
except TimeoutError:
|
|
297
|
+
raise SystemError("CommsManager is still running after closing")
|
|
298
|
+
|
|
299
|
+
def register_single_message_checker(self, iface: str, func: Callable[[SocketResult], Optional[MotorUIDType]]) -> None:
|
|
300
|
+
"""Register a function to check for expected responses in single message mode.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
func: A function that takes a SocketResult and returns a string unique to the motor that sent the message,
|
|
304
|
+
or None if the message is not expected and should be ignored.
|
|
305
|
+
"""
|
|
306
|
+
with self._lock:
|
|
307
|
+
self._single_msg_checkers[iface] = func
|
|
308
|
+
|
|
309
|
+
def __main_callback(self, result: SocketResult) -> None:
|
|
310
|
+
"""Called by libsocketcantcp library to notify us of a new message."""
|
|
311
|
+
if result.is_error():
|
|
312
|
+
warning(f"Error in comms manager callback: {result.err_type} - {result.err_msg}")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# Check for expected responses in single message mode
|
|
316
|
+
with self._lock:
|
|
317
|
+
if self._single_msg_mode and result.socket_name in self._pending_msgs:
|
|
318
|
+
if (res := self._single_msg_checkers[result.socket_name](result)) is not None:
|
|
319
|
+
if not isinstance(res, MotorUIDType):
|
|
320
|
+
raise TypeError(f"Expected MotorUIDType, got {type(res)}")
|
|
321
|
+
self._pending_msgs[result.socket_name].discard(res)
|
|
322
|
+
|
|
323
|
+
for callback in self._callbacks:
|
|
324
|
+
callback(result)
|
|
325
|
+
|
|
326
|
+
def is_running(self) -> bool:
|
|
327
|
+
"""Check if the background polling thread is currently running."""
|
|
328
|
+
return self.thread is not None and self.thread.is_alive()
|
|
329
|
+
|
|
330
|
+
def cansend(
|
|
331
|
+
self,
|
|
332
|
+
interface: str,
|
|
333
|
+
extended: bool,
|
|
334
|
+
can_id: int,
|
|
335
|
+
data: bytes,
|
|
336
|
+
motor_uid: Optional[MotorUIDType] = None
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Send a CAN message.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
interface: The interface to send the message on.
|
|
342
|
+
extended: Whether the message is an extended (29-bit) CAN message, or a standard (11-bit) CAN message.
|
|
343
|
+
can_id: The CAN ID of the message. Must be in the range [0-0x1FFFFFFF] for extended messages, or
|
|
344
|
+
[0-0x7FF] for standard messages.
|
|
345
|
+
data: The data to send in the message. Must be 8 bytes long.
|
|
346
|
+
motor_uid: The unique identifier of the motor that sent the message. Only needed in single message mode.
|
|
347
|
+
"""
|
|
348
|
+
for callback in self._send_callbacks:
|
|
349
|
+
callback(SendCanMessage(iface=interface, extended=extended, can_id=can_id, data=data, motor_uid=motor_uid))
|
|
350
|
+
|
|
351
|
+
if self.is_dry:
|
|
352
|
+
debug("Dry cansend: ", interface, can_id, data)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
with self._lock:
|
|
356
|
+
# Make sure we are not in single message mode and have a pending message for this interface
|
|
357
|
+
if self._single_msg_mode:
|
|
358
|
+
if interface not in self._pending_msgs:
|
|
359
|
+
self._pending_msgs[interface] = set()
|
|
360
|
+
if motor_uid is None:
|
|
361
|
+
raise MissingMotorUIDError()
|
|
362
|
+
if not isinstance(motor_uid, MotorUIDType):
|
|
363
|
+
raise TypeError(f"Expected MotorUIDType, got {type(motor_uid)}")
|
|
364
|
+
if motor_uid in self._pending_msgs[interface]:
|
|
365
|
+
raise PendingReceiveMessageError(motor_uid=motor_uid)
|
|
366
|
+
|
|
367
|
+
# We are good to send this message, mark it as pending
|
|
368
|
+
self._pending_msgs[interface].add(motor_uid)
|
|
369
|
+
|
|
370
|
+
# Make sure we are not overloading the CAN bus
|
|
371
|
+
if interface not in self._can_loads:
|
|
372
|
+
raise UnknownInterfaceError(interface=interface)
|
|
373
|
+
|
|
374
|
+
if self.__overload_check:
|
|
375
|
+
dt_ms = timediff(self._can_loads[interface].last_message_time) * 1000.0 * CAN_BUS_DECAY_SCALE
|
|
376
|
+
new_load = self._can_loads[interface].load * math.exp(-dt_ms * CAN_BUS_DECAY_RATE) + CAN_BUS_DECAY_SCALE
|
|
377
|
+
if new_load > CAN_BUS_DECAY_LIMIT:
|
|
378
|
+
raise CanOverloadError(f"CAN bus {interface} is overloaded")
|
|
379
|
+
|
|
380
|
+
self._can_loads[interface] = LoadAndTime(load=new_load, last_message_time=currtime())
|
|
381
|
+
|
|
382
|
+
self._run_and_check(_cansend, interface, extended, can_id, data)
|
|
383
|
+
|
|
384
|
+
def tcpsend_modbus(self, ip: str, tid: int, uid: int, fcode: int, reg_addr: int, reg_val: int) -> None:
|
|
385
|
+
"""Send a Modbus TCP message."""
|
|
386
|
+
for callback in self._send_callbacks:
|
|
387
|
+
callback(SendTCPMessage(ip=ip, tid=tid, uid=uid, fcode=fcode, reg_addr=reg_addr, reg_val=reg_val))
|
|
388
|
+
|
|
389
|
+
if self.is_dry:
|
|
390
|
+
debug("dry tcpsend", ip, tid, uid, fcode, reg_addr, reg_val)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
self._run_and_check(_tcpsend_modbus, ip, tid, uid, fcode, reg_addr, reg_val)
|
|
394
|
+
|
|
395
|
+
def _run_and_check(self, func: Callable[[Any], None], *args: Any) -> None:
|
|
396
|
+
"""Run a library function and check the result. If the result is non-zero, print a warning."""
|
|
397
|
+
if not self.is_running():
|
|
398
|
+
raise CommsManagerNotRunningError()
|
|
399
|
+
func(*args)
|