asyncio_for_robotics 1.0.4__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.
- asyncio_for_robotics-1.0.4/LICENSE +21 -0
- asyncio_for_robotics-1.0.4/PKG-INFO +261 -0
- asyncio_for_robotics-1.0.4/README.md +217 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/__init__.py +7 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/__init__.py +0 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/_logger.py +125 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/sub.py +238 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/utils.py +114 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/__init__.py +0 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/custom_stdout.py +42 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/python_discussion.py +116 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_discussion.py +127 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_double_listener.py +83 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_double_talker.py +107 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_listener.py +30 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_pubsub.py +55 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_service_client.py +42 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_service_server.py +35 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_talker.py +41 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/zenoh_discussion.py +76 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/py.typed +1 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/__init__.py +29 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/service.py +263 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/session.py +307 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/sub.py +78 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/utils.py +45 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/__init__.py +14 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/session.py +42 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/sub.py +63 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/PKG-INFO +261 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/SOURCES.txt +39 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/dependency_links.txt +1 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/requires.txt +18 -0
- asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/top_level.txt +1 -0
- asyncio_for_robotics-1.0.4/pyproject.toml +61 -0
- asyncio_for_robotics-1.0.4/setup.cfg +4 -0
- asyncio_for_robotics-1.0.4/tests/test_ros2_pubsub_sync.py +70 -0
- asyncio_for_robotics-1.0.4/tests/test_ros2_pubsub_thr.py +220 -0
- asyncio_for_robotics-1.0.4/tests/test_ros2_serv.py +82 -0
- asyncio_for_robotics-1.0.4/tests/test_utils.py +48 -0
- asyncio_for_robotics-1.0.4/tests/test_zenoh_pubsub.py +203 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Elian NEPPEL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asyncio_for_robotics
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Asyncio interface for ROS 2, Zenoh, and other robotic middlewares.
|
|
5
|
+
Author-email: Elian <no@no.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/2lian/asyncio-for-robotics
|
|
8
|
+
Project-URL: Repository, https://github.com/2lian/asyncio-for-robotics
|
|
9
|
+
Project-URL: Issues, https://github.com/yourusername/asyncio-for-robotics/issues
|
|
10
|
+
Keywords: asyncio,robotics,ros2,zenoh,publisher,subcriber
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
25
|
+
Classifier: Topic :: System :: Networking
|
|
26
|
+
Classifier: Framework :: AsyncIO
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: typing_extensions>=4.0; python_version < "3.11"
|
|
31
|
+
Requires-Dist: async-timeout; python_version < "3.11"
|
|
32
|
+
Provides-Extra: zenoh
|
|
33
|
+
Requires-Dist: eclipse-zenoh>=1.0.0; extra == "zenoh"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: colorama; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
38
|
+
Requires-Dist: pyfiglet; extra == "dev"
|
|
39
|
+
Requires-Dist: uvloop; extra == "dev"
|
|
40
|
+
Provides-Extra: build
|
|
41
|
+
Requires-Dist: build; extra == "build"
|
|
42
|
+
Requires-Dist: twine; extra == "build"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Asyncio For Robotics
|
|
46
|
+
| Requirements | Compatibility | Tests |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| [](https://pypi.org/project/asyncio_for_robotics/)<br>[](https://opensource.org/license/mit) | [](https://zenoh.io/)<br>[](https://github.com/ros2) | [](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.yml)<br>[](https://github.com/2lian/asyncio-for-robotics/actions/workflows/ros-pytest.yml) |
|
|
49
|
+
|
|
50
|
+
The Asyncio For Robotics (`afor`) library makes `asyncio` usable with ROS 2, Zenoh and more, letting you write linear, testable, and non-blocking Python code.
|
|
51
|
+
|
|
52
|
+
- Better syntax.
|
|
53
|
+
- Only native python: Better docs and support.
|
|
54
|
+
- Simplifies testing.
|
|
55
|
+
|
|
56
|
+
*Will this make my code slower?* [No.](https://github.com/2lian/asyncio-for-robotics/tree/main/README.md#about-speed)
|
|
57
|
+
|
|
58
|
+
*Will this make my code faster?* No. However `asyncio` will help YOU write
|
|
59
|
+
better, faster code.
|
|
60
|
+
|
|
61
|
+
*Does it replace ROS 2? Is this a wrapper?* No. It is a tool adding async capabilities. It gives you more choices, not less.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
### Barebone
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install asyncio_for_robotics
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### For ROS 2
|
|
72
|
+
|
|
73
|
+
Compatible with: `jazzy`,`humble` and newer. This library is pure python (>=3.10), so it installs easily.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install asyncio_for_robotics
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### For Zenoh
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install asyncio_for_robotics[zenoh]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Read more
|
|
86
|
+
|
|
87
|
+
- [Detailed ROS 2 tutorial](https://github.com/2lian/asyncio-for-robotics/blob/main/using_with_ros.md)
|
|
88
|
+
- [Detailed examples](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example)
|
|
89
|
+
- [no talking 🦍 show me code 🦍](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example/ros2_pubsub.py)
|
|
90
|
+
- [Usage for software testing](https://github.com/2lian/asyncio-for-robotics/blob/main/tests)
|
|
91
|
+
- [Implement your own protocol](https://github.com/2lian/asyncio-for-robotics/blob/main/own_proto_example.md)
|
|
92
|
+
|
|
93
|
+
## Code sample
|
|
94
|
+
|
|
95
|
+
Syntax is identical between ROS 2 and Zenoh.
|
|
96
|
+
|
|
97
|
+
### Wait for messages one by one
|
|
98
|
+
|
|
99
|
+
Application:
|
|
100
|
+
- Get the latest sensor data
|
|
101
|
+
- Get clock value
|
|
102
|
+
- Wait for trigger
|
|
103
|
+
- Wait for system to be operational
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
sub = afor.Sub(...)
|
|
107
|
+
|
|
108
|
+
# get the latest message
|
|
109
|
+
latest = await sub.wait_for_value()
|
|
110
|
+
|
|
111
|
+
# get a new message
|
|
112
|
+
new = await sub.wait_for_new()
|
|
113
|
+
|
|
114
|
+
# get the next message received
|
|
115
|
+
next = await sub.wait_for_next()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Continuously listen to a data stream
|
|
119
|
+
|
|
120
|
+
Application:
|
|
121
|
+
- Process a whole data stream
|
|
122
|
+
- React to changes in sensor data
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Continuously process the latest messages
|
|
126
|
+
async for msg in sub.listen():
|
|
127
|
+
status = foo(msg)
|
|
128
|
+
if status == DONE:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
# Continuously process all incoming messages
|
|
132
|
+
async for msg in sub.listen_reliable():
|
|
133
|
+
status = foo(msg)
|
|
134
|
+
if status == DONE:
|
|
135
|
+
break
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Reliable, non-drifting Rate
|
|
139
|
+
|
|
140
|
+
Application:
|
|
141
|
+
- Periodic updates and actions
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# Rate is simply a subscriber triggering on every tick
|
|
145
|
+
rate = afor.Rate(frequency=0.01, time_source=time.time_ns)
|
|
146
|
+
|
|
147
|
+
# Wait for the next tick
|
|
148
|
+
await rate.wait_for_new()
|
|
149
|
+
|
|
150
|
+
# Executes after a tick
|
|
151
|
+
async for _ in rate.listen():
|
|
152
|
+
foo(...)
|
|
153
|
+
|
|
154
|
+
# Reliably executes for every tick
|
|
155
|
+
async for _ in sub.listen_reliable():
|
|
156
|
+
foo(...)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Improved Services / Queryable for ROS 2
|
|
160
|
+
|
|
161
|
+
Services are needlessly convoluted in ROS 2 and intrinsically not async
|
|
162
|
+
(because the server callback function MUST return a response). `afor` overrides
|
|
163
|
+
the ROS behavior, allowing for the response to be sent later. Implementing
|
|
164
|
+
similar systems for other transport protocol should be very easy: The server is just a
|
|
165
|
+
`asyncio_for_robotics.core.BaseSub` generating responder objects.
|
|
166
|
+
|
|
167
|
+
Application:
|
|
168
|
+
- Client request reply from a server.
|
|
169
|
+
- Servers can delay their response without blocking (not possible in ROS 2)
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Server is once again a afor subscriber, but generating responder objects
|
|
173
|
+
server = afor.Server(...)
|
|
174
|
+
|
|
175
|
+
# processes all requests.
|
|
176
|
+
# listen_reliable method is recommanded as it cannot skip requests
|
|
177
|
+
async for responder in server.listen_reliable():
|
|
178
|
+
if responder.request == "PING!":
|
|
179
|
+
reponder.response = "PONG!"
|
|
180
|
+
await asyncio.sleep(...) # reply can be differed
|
|
181
|
+
reponder.send()
|
|
182
|
+
else:
|
|
183
|
+
... # reply is not necessary
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# the client implements a async call method
|
|
188
|
+
client = afor.Client(...)
|
|
189
|
+
|
|
190
|
+
response = await client.call("PING!")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Process for the right amount of time
|
|
194
|
+
|
|
195
|
+
Application:
|
|
196
|
+
- Test if the system is responding as expected
|
|
197
|
+
- Run small tasks with small and local code
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
# Listen with a timeout
|
|
201
|
+
data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
|
|
202
|
+
if isinstance(data, TimeoutError):
|
|
203
|
+
pytest.fail(f"Failed to get new data in under 1 second")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Process a codeblock with a timeout
|
|
207
|
+
async with afor.soft_timeout(1):
|
|
208
|
+
sum = 0
|
|
209
|
+
total = 0
|
|
210
|
+
async for msg in sub.listen_reliable():
|
|
211
|
+
number = process(msg)
|
|
212
|
+
sum += number
|
|
213
|
+
total += 1
|
|
214
|
+
|
|
215
|
+
last_second_average = sum/total
|
|
216
|
+
assert last_second_average == pytest.approx(expected_average)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## About Speed
|
|
220
|
+
|
|
221
|
+
The inevitable question: *“But isn’t this slower than the ROS 2 executor? ROS 2 is the best!”*
|
|
222
|
+
|
|
223
|
+
In short: `rclpy`'s executor is the bottleneck.
|
|
224
|
+
- Comparing to the best ROS 2 Jazzy can do (`SingleThreadedExecutor`), `afor` increases latency from 110us to 150us.
|
|
225
|
+
- Comparing to other execution methods, `afor` is equivalent if not faster.
|
|
226
|
+
- If you find it slow, you should use C++ or Zenoh (or contribute to this repo?).
|
|
227
|
+
|
|
228
|
+
Benchmark code is available in [`./tests/bench/`](https://github.com/2lian/asyncio-for-robotics/blob/main/tests/bench/), it consists in two pairs of pub/sub infinitely echoing a message (using one single node). The messaging rate, thus measures the request to response latency.
|
|
229
|
+
|
|
230
|
+
| With `afor` | Transport | Executor | | Frequency (kHz) | Latency (ms) |
|
|
231
|
+
|:----------:|:----------|:----------------------------------|-|---------:|---------:|
|
|
232
|
+
| ✔️ | Zenoh | None | | **95** | **0.01** |
|
|
233
|
+
| ✔️ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | **17** | **0.06** |
|
|
234
|
+
| ❌ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | 13 | 0.08 |
|
|
235
|
+
| ❌ | ROS 2 | SingleThreaded | | 9 | 0.11 |
|
|
236
|
+
| ✔️ | ROS 2 | SingleThreaded | | **7** | **0.15** |
|
|
237
|
+
| ✔️ | ROS 2 | MultiThreaded | | **3** | **0.3** |
|
|
238
|
+
| ❌ | ROS 2 | MultiThreaded | | **3** | **0.3** |
|
|
239
|
+
| ✔️ | ROS 2 | [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | | 3 | 0.3 |
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
Details:
|
|
243
|
+
- `uvloop` was used, replacing the asyncio executor (more or less doubles the performances for Zenoh)
|
|
244
|
+
- RMW was set to `rmw_zenoh_cpp`
|
|
245
|
+
- ROS2 benchmarks uses `afor`'s `ros2.ThreadedSession` (the default in `afor`).
|
|
246
|
+
- Only the Benchmark of the [`ros_loop` method](https://github.com/m2-farzan/ros2-asyncio) uses `afor`'s second type of session: `ros2.SynchronousSession`.
|
|
247
|
+
- ROS 2 executors can easily be changed in `afor` when creating a session.
|
|
248
|
+
- The experimental `AsyncioExecutor` PR on ros rolling by nadavelkabets is incredible [https://github.com/ros2/rclpy/pull/1399](https://github.com/ros2/rclpy/pull/1399). Maybe I will add proper support for it (but only a few will want to use an unmerged experimental PR of ROS 2 rolling).
|
|
249
|
+
- If there is interest in those benchmarks I will improve them, so others can run them all easily.
|
|
250
|
+
|
|
251
|
+
Analysis:
|
|
252
|
+
- Zenoh is extremely fast, proving that `afor` is not the bottleneck.
|
|
253
|
+
- This `AsyncioExecutor` having better perf when using `afor` is interesting, because `afor` does not bypass code.
|
|
254
|
+
- I think this is due to `AsyncioExecutor` having some overhead that affects its own callback.
|
|
255
|
+
- Without `afor` the ROS 2 callback executes some code and publishes.
|
|
256
|
+
- With `afor` the ROS 2 callback returns immediately, and fully delegates execution to `asyncio`.
|
|
257
|
+
- The increase of latency on the `SingleThreaded` executors proves that getting data in and out of the `rclpy` executor and thread is the main bottleneck.
|
|
258
|
+
- `AsyncioExecutor` does not have such thread, thus can directly communicate.
|
|
259
|
+
- Zenoh has its own thread, however it is built exclusively for multi-thread operations, without any executor. Thus achieves far superior performances.
|
|
260
|
+
- `MultiThreadedExecutor` is just famously slow.
|
|
261
|
+
- Very surprisingly, the well known `ros_loop` method detailed here [https://github.com/m2-farzan/ros2-asyncio](https://github.com/m2-farzan/ros2-asyncio) is slow.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Asyncio For Robotics
|
|
2
|
+
| Requirements | Compatibility | Tests |
|
|
3
|
+
|---|---|---|
|
|
4
|
+
| [](https://pypi.org/project/asyncio_for_robotics/)<br>[](https://opensource.org/license/mit) | [](https://zenoh.io/)<br>[](https://github.com/ros2) | [](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.yml)<br>[](https://github.com/2lian/asyncio-for-robotics/actions/workflows/ros-pytest.yml) |
|
|
5
|
+
|
|
6
|
+
The Asyncio For Robotics (`afor`) library makes `asyncio` usable with ROS 2, Zenoh and more, letting you write linear, testable, and non-blocking Python code.
|
|
7
|
+
|
|
8
|
+
- Better syntax.
|
|
9
|
+
- Only native python: Better docs and support.
|
|
10
|
+
- Simplifies testing.
|
|
11
|
+
|
|
12
|
+
*Will this make my code slower?* [No.](https://github.com/2lian/asyncio-for-robotics/tree/main/README.md#about-speed)
|
|
13
|
+
|
|
14
|
+
*Will this make my code faster?* No. However `asyncio` will help YOU write
|
|
15
|
+
better, faster code.
|
|
16
|
+
|
|
17
|
+
*Does it replace ROS 2? Is this a wrapper?* No. It is a tool adding async capabilities. It gives you more choices, not less.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
### Barebone
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install asyncio_for_robotics
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### For ROS 2
|
|
28
|
+
|
|
29
|
+
Compatible with: `jazzy`,`humble` and newer. This library is pure python (>=3.10), so it installs easily.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install asyncio_for_robotics
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### For Zenoh
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install asyncio_for_robotics[zenoh]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Read more
|
|
42
|
+
|
|
43
|
+
- [Detailed ROS 2 tutorial](https://github.com/2lian/asyncio-for-robotics/blob/main/using_with_ros.md)
|
|
44
|
+
- [Detailed examples](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example)
|
|
45
|
+
- [no talking 🦍 show me code 🦍](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example/ros2_pubsub.py)
|
|
46
|
+
- [Usage for software testing](https://github.com/2lian/asyncio-for-robotics/blob/main/tests)
|
|
47
|
+
- [Implement your own protocol](https://github.com/2lian/asyncio-for-robotics/blob/main/own_proto_example.md)
|
|
48
|
+
|
|
49
|
+
## Code sample
|
|
50
|
+
|
|
51
|
+
Syntax is identical between ROS 2 and Zenoh.
|
|
52
|
+
|
|
53
|
+
### Wait for messages one by one
|
|
54
|
+
|
|
55
|
+
Application:
|
|
56
|
+
- Get the latest sensor data
|
|
57
|
+
- Get clock value
|
|
58
|
+
- Wait for trigger
|
|
59
|
+
- Wait for system to be operational
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
sub = afor.Sub(...)
|
|
63
|
+
|
|
64
|
+
# get the latest message
|
|
65
|
+
latest = await sub.wait_for_value()
|
|
66
|
+
|
|
67
|
+
# get a new message
|
|
68
|
+
new = await sub.wait_for_new()
|
|
69
|
+
|
|
70
|
+
# get the next message received
|
|
71
|
+
next = await sub.wait_for_next()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Continuously listen to a data stream
|
|
75
|
+
|
|
76
|
+
Application:
|
|
77
|
+
- Process a whole data stream
|
|
78
|
+
- React to changes in sensor data
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Continuously process the latest messages
|
|
82
|
+
async for msg in sub.listen():
|
|
83
|
+
status = foo(msg)
|
|
84
|
+
if status == DONE:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# Continuously process all incoming messages
|
|
88
|
+
async for msg in sub.listen_reliable():
|
|
89
|
+
status = foo(msg)
|
|
90
|
+
if status == DONE:
|
|
91
|
+
break
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Reliable, non-drifting Rate
|
|
95
|
+
|
|
96
|
+
Application:
|
|
97
|
+
- Periodic updates and actions
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Rate is simply a subscriber triggering on every tick
|
|
101
|
+
rate = afor.Rate(frequency=0.01, time_source=time.time_ns)
|
|
102
|
+
|
|
103
|
+
# Wait for the next tick
|
|
104
|
+
await rate.wait_for_new()
|
|
105
|
+
|
|
106
|
+
# Executes after a tick
|
|
107
|
+
async for _ in rate.listen():
|
|
108
|
+
foo(...)
|
|
109
|
+
|
|
110
|
+
# Reliably executes for every tick
|
|
111
|
+
async for _ in sub.listen_reliable():
|
|
112
|
+
foo(...)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Improved Services / Queryable for ROS 2
|
|
116
|
+
|
|
117
|
+
Services are needlessly convoluted in ROS 2 and intrinsically not async
|
|
118
|
+
(because the server callback function MUST return a response). `afor` overrides
|
|
119
|
+
the ROS behavior, allowing for the response to be sent later. Implementing
|
|
120
|
+
similar systems for other transport protocol should be very easy: The server is just a
|
|
121
|
+
`asyncio_for_robotics.core.BaseSub` generating responder objects.
|
|
122
|
+
|
|
123
|
+
Application:
|
|
124
|
+
- Client request reply from a server.
|
|
125
|
+
- Servers can delay their response without blocking (not possible in ROS 2)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Server is once again a afor subscriber, but generating responder objects
|
|
129
|
+
server = afor.Server(...)
|
|
130
|
+
|
|
131
|
+
# processes all requests.
|
|
132
|
+
# listen_reliable method is recommanded as it cannot skip requests
|
|
133
|
+
async for responder in server.listen_reliable():
|
|
134
|
+
if responder.request == "PING!":
|
|
135
|
+
reponder.response = "PONG!"
|
|
136
|
+
await asyncio.sleep(...) # reply can be differed
|
|
137
|
+
reponder.send()
|
|
138
|
+
else:
|
|
139
|
+
... # reply is not necessary
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# the client implements a async call method
|
|
144
|
+
client = afor.Client(...)
|
|
145
|
+
|
|
146
|
+
response = await client.call("PING!")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Process for the right amount of time
|
|
150
|
+
|
|
151
|
+
Application:
|
|
152
|
+
- Test if the system is responding as expected
|
|
153
|
+
- Run small tasks with small and local code
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Listen with a timeout
|
|
157
|
+
data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
|
|
158
|
+
if isinstance(data, TimeoutError):
|
|
159
|
+
pytest.fail(f"Failed to get new data in under 1 second")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Process a codeblock with a timeout
|
|
163
|
+
async with afor.soft_timeout(1):
|
|
164
|
+
sum = 0
|
|
165
|
+
total = 0
|
|
166
|
+
async for msg in sub.listen_reliable():
|
|
167
|
+
number = process(msg)
|
|
168
|
+
sum += number
|
|
169
|
+
total += 1
|
|
170
|
+
|
|
171
|
+
last_second_average = sum/total
|
|
172
|
+
assert last_second_average == pytest.approx(expected_average)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## About Speed
|
|
176
|
+
|
|
177
|
+
The inevitable question: *“But isn’t this slower than the ROS 2 executor? ROS 2 is the best!”*
|
|
178
|
+
|
|
179
|
+
In short: `rclpy`'s executor is the bottleneck.
|
|
180
|
+
- Comparing to the best ROS 2 Jazzy can do (`SingleThreadedExecutor`), `afor` increases latency from 110us to 150us.
|
|
181
|
+
- Comparing to other execution methods, `afor` is equivalent if not faster.
|
|
182
|
+
- If you find it slow, you should use C++ or Zenoh (or contribute to this repo?).
|
|
183
|
+
|
|
184
|
+
Benchmark code is available in [`./tests/bench/`](https://github.com/2lian/asyncio-for-robotics/blob/main/tests/bench/), it consists in two pairs of pub/sub infinitely echoing a message (using one single node). The messaging rate, thus measures the request to response latency.
|
|
185
|
+
|
|
186
|
+
| With `afor` | Transport | Executor | | Frequency (kHz) | Latency (ms) |
|
|
187
|
+
|:----------:|:----------|:----------------------------------|-|---------:|---------:|
|
|
188
|
+
| ✔️ | Zenoh | None | | **95** | **0.01** |
|
|
189
|
+
| ✔️ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | **17** | **0.06** |
|
|
190
|
+
| ❌ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | 13 | 0.08 |
|
|
191
|
+
| ❌ | ROS 2 | SingleThreaded | | 9 | 0.11 |
|
|
192
|
+
| ✔️ | ROS 2 | SingleThreaded | | **7** | **0.15** |
|
|
193
|
+
| ✔️ | ROS 2 | MultiThreaded | | **3** | **0.3** |
|
|
194
|
+
| ❌ | ROS 2 | MultiThreaded | | **3** | **0.3** |
|
|
195
|
+
| ✔️ | ROS 2 | [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | | 3 | 0.3 |
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
Details:
|
|
199
|
+
- `uvloop` was used, replacing the asyncio executor (more or less doubles the performances for Zenoh)
|
|
200
|
+
- RMW was set to `rmw_zenoh_cpp`
|
|
201
|
+
- ROS2 benchmarks uses `afor`'s `ros2.ThreadedSession` (the default in `afor`).
|
|
202
|
+
- Only the Benchmark of the [`ros_loop` method](https://github.com/m2-farzan/ros2-asyncio) uses `afor`'s second type of session: `ros2.SynchronousSession`.
|
|
203
|
+
- ROS 2 executors can easily be changed in `afor` when creating a session.
|
|
204
|
+
- The experimental `AsyncioExecutor` PR on ros rolling by nadavelkabets is incredible [https://github.com/ros2/rclpy/pull/1399](https://github.com/ros2/rclpy/pull/1399). Maybe I will add proper support for it (but only a few will want to use an unmerged experimental PR of ROS 2 rolling).
|
|
205
|
+
- If there is interest in those benchmarks I will improve them, so others can run them all easily.
|
|
206
|
+
|
|
207
|
+
Analysis:
|
|
208
|
+
- Zenoh is extremely fast, proving that `afor` is not the bottleneck.
|
|
209
|
+
- This `AsyncioExecutor` having better perf when using `afor` is interesting, because `afor` does not bypass code.
|
|
210
|
+
- I think this is due to `AsyncioExecutor` having some overhead that affects its own callback.
|
|
211
|
+
- Without `afor` the ROS 2 callback executes some code and publishes.
|
|
212
|
+
- With `afor` the ROS 2 callback returns immediately, and fully delegates execution to `asyncio`.
|
|
213
|
+
- The increase of latency on the `SingleThreaded` executors proves that getting data in and out of the `rclpy` executor and thread is the main bottleneck.
|
|
214
|
+
- `AsyncioExecutor` does not have such thread, thus can directly communicate.
|
|
215
|
+
- Zenoh has its own thread, however it is built exclusively for multi-thread operations, without any executor. Thus achieves far superior performances.
|
|
216
|
+
- `MultiThreadedExecutor` is just famously slow.
|
|
217
|
+
- Very surprisingly, the well known `ros_loop` method detailed here [https://github.com/m2-farzan/ros2-asyncio](https://github.com/m2-farzan/ros2-asyncio) is slow.
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import copy
|
|
3
|
+
import logging.config
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import colorama
|
|
9
|
+
from colorama import Fore, Style, init
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsonLineFormatter(logging.Formatter):
|
|
13
|
+
"""Outputs log records as JSON lines."""
|
|
14
|
+
|
|
15
|
+
def format(self, record):
|
|
16
|
+
log_record = {
|
|
17
|
+
"time": datetime.fromtimestamp(record.created).isoformat(),
|
|
18
|
+
"level": record.levelname,
|
|
19
|
+
"logger": record.name,
|
|
20
|
+
"message": record.getMessage(),
|
|
21
|
+
"module": record.module,
|
|
22
|
+
"funcName": record.funcName,
|
|
23
|
+
"line": record.lineno,
|
|
24
|
+
}
|
|
25
|
+
if record.exc_info:
|
|
26
|
+
log_record["exc_info"] = self.formatException(record.exc_info)
|
|
27
|
+
return json.dumps(log_record)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LEVEL_COLORS = {
|
|
31
|
+
"DEBUG": Fore.BLUE,
|
|
32
|
+
"INFO": Fore.CYAN,
|
|
33
|
+
"WARNING": Fore.YELLOW,
|
|
34
|
+
"ERROR": Fore.RED,
|
|
35
|
+
"CRITICAL": Fore.BLACK + Style.BRIGHT + colorama.Back.RED,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OnlyLevelFilter(logging.Filter):
|
|
40
|
+
"""Only allow a single level through this handler."""
|
|
41
|
+
def __init__(self, level):
|
|
42
|
+
self.level = level
|
|
43
|
+
def filter(self, record):
|
|
44
|
+
return record.levelno == self.level
|
|
45
|
+
|
|
46
|
+
class ColoredFormatter(logging.Formatter):
|
|
47
|
+
"""Adds colors to levelname in logs."""
|
|
48
|
+
|
|
49
|
+
def format(self, record: logging.LogRecord):
|
|
50
|
+
record = copy.deepcopy(record)
|
|
51
|
+
levelname = record.levelname
|
|
52
|
+
if levelname in LEVEL_COLORS:
|
|
53
|
+
record.levelname = (
|
|
54
|
+
f"{LEVEL_COLORS[levelname]}{record.levelname[0]}{Style.RESET_ALL}"
|
|
55
|
+
)
|
|
56
|
+
record.msg = f"{LEVEL_COLORS[levelname]}{record.msg}{Style.RESET_ALL}"
|
|
57
|
+
return super().format(record)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_logger(debug_path: Optional[str] = None):
|
|
61
|
+
handlers = ["stdout", "stderr"]
|
|
62
|
+
if debug_path:
|
|
63
|
+
handlers.append("json")
|
|
64
|
+
handlers.append("userlog")
|
|
65
|
+
cfg = {
|
|
66
|
+
"version": 1,
|
|
67
|
+
"disable_existing_loggers": False,
|
|
68
|
+
"formatters": {
|
|
69
|
+
"user": {
|
|
70
|
+
"()": ColoredFormatter,
|
|
71
|
+
"format": "%(levelname)s| %(message)s",
|
|
72
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S",
|
|
73
|
+
},
|
|
74
|
+
"json": {
|
|
75
|
+
"()": JsonLineFormatter, # custom formatter
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"filters": {"only_info": {"()": OnlyLevelFilter, "level": logging.INFO}},
|
|
79
|
+
"handlers": {
|
|
80
|
+
"stdout": {
|
|
81
|
+
"class": "logging.StreamHandler",
|
|
82
|
+
"level": "INFO",
|
|
83
|
+
"formatter": "user",
|
|
84
|
+
"filters": ["only_info"],
|
|
85
|
+
"stream": "ext://sys.stdout",
|
|
86
|
+
},
|
|
87
|
+
"stderr": {
|
|
88
|
+
"class": "logging.StreamHandler",
|
|
89
|
+
"level": "WARNING",
|
|
90
|
+
"formatter": "user",
|
|
91
|
+
"stream": "ext://sys.stderr",
|
|
92
|
+
},
|
|
93
|
+
"json": {
|
|
94
|
+
"class": "logging.FileHandler",
|
|
95
|
+
"level": "DEBUG",
|
|
96
|
+
"formatter": "json",
|
|
97
|
+
"filename": (
|
|
98
|
+
os.path.expanduser(debug_path) + "/debug.log.jsonl"
|
|
99
|
+
if debug_path is not None
|
|
100
|
+
else "log.jsonl"
|
|
101
|
+
), # path relative to working dir
|
|
102
|
+
"mode": "w",
|
|
103
|
+
},
|
|
104
|
+
"userlog": {
|
|
105
|
+
"class": "logging.FileHandler",
|
|
106
|
+
"level": "DEBUG",
|
|
107
|
+
"formatter": "user",
|
|
108
|
+
"filename": (
|
|
109
|
+
os.path.expanduser(debug_path) + "/debug.log"
|
|
110
|
+
if debug_path is not None
|
|
111
|
+
else "log"
|
|
112
|
+
), # path relative to working dir
|
|
113
|
+
"mode": "w",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"loggers": {
|
|
117
|
+
"asyncio_for_robotics": {
|
|
118
|
+
"level": "DEBUG",
|
|
119
|
+
"handlers": handlers,
|
|
120
|
+
"propagate": False,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
logging.config.dictConfig(cfg)
|
|
125
|
+
|