asyncio-for-robotics 1.4.0__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.4.0/LICENSE +21 -0
- asyncio_for_robotics-1.4.0/PKG-INFO +283 -0
- asyncio_for_robotics-1.4.0/README.md +234 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/__init__.py +15 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/__init__.py +15 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/_compat.py +13 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/_logger.py +125 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/scope.py +379 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/sub.py +382 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/utils.py +161 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/__init__.py +0 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/custom_stdout.py +44 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/delete_me.py +32 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/python_discussion.py +116 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_discussion.py +113 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_double_listener.py +78 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_double_talker.py +108 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_event_callback.py +153 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_listener.py +24 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_pubsub.py +58 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_service_client.py +33 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_service_server.py +29 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_talker.py +35 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/zenoh_discussion.py +68 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/py.typed +1 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/__init__.py +46 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/future.py +47 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/service.py +317 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/session.py +106 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/session_types.py +281 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/sub.py +107 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/utils.py +45 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/textio/__init__.py +12 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/textio/sub.py +117 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/__init__.py +33 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/session.py +113 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/sub.py +75 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/PKG-INFO +283 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/SOURCES.txt +51 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/dependency_links.txt +1 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/requires.txt +22 -0
- asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/top_level.txt +1 -0
- asyncio_for_robotics-1.4.0/pyproject.toml +68 -0
- asyncio_for_robotics-1.4.0/setup.cfg +4 -0
- asyncio_for_robotics-1.4.0/tests/test_core.py +39 -0
- asyncio_for_robotics-1.4.0/tests/test_ros2_pubsub_sync.py +54 -0
- asyncio_for_robotics-1.4.0/tests/test_ros2_pubsub_thr.py +73 -0
- asyncio_for_robotics-1.4.0/tests/test_ros2_serv.py +319 -0
- asyncio_for_robotics-1.4.0/tests/test_ros2_stdout_echo.py +180 -0
- asyncio_for_robotics-1.4.0/tests/test_scope.py +179 -0
- asyncio_for_robotics-1.4.0/tests/test_textio.py +69 -0
- asyncio_for_robotics-1.4.0/tests/test_utils.py +64 -0
- asyncio_for_robotics-1.4.0/tests/test_zenoh_pubsub.py +65 -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,283 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asyncio-for-robotics
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Asyncio interface for ROS 2, Zenoh, and other robotic middlewares.
|
|
5
|
+
Author-email: Elian NEPPEL <elian.dev@posteo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/2lian/asyncio-for-robotics
|
|
8
|
+
Project-URL: Issues, https://github.com/2lian/asyncio-for-robotics/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/2lian/asyncio-for-robotics
|
|
10
|
+
Keywords: asyncio,publisher,robotics,ros2,subscriber,zenoh
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering
|
|
27
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
28
|
+
Classifier: Topic :: System :: Networking
|
|
29
|
+
Requires-Python: <3.15,>=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: async-timeout; python_version < "3.11"
|
|
33
|
+
Requires-Dist: exceptiongroup; python_version < "3.11"
|
|
34
|
+
Requires-Dist: taskgroup; python_version < "3.11"
|
|
35
|
+
Requires-Dist: typing-extensions>=4; python_version < "3.11"
|
|
36
|
+
Provides-Extra: benchmarks
|
|
37
|
+
Requires-Dist: pyfiglet; extra == "benchmarks"
|
|
38
|
+
Requires-Dist: uvloop; extra == "benchmarks"
|
|
39
|
+
Provides-Extra: build
|
|
40
|
+
Requires-Dist: build; extra == "build"
|
|
41
|
+
Requires-Dist: twine; extra == "build"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: colorama; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
46
|
+
Provides-Extra: zenoh
|
|
47
|
+
Requires-Dist: eclipse-zenoh>=1; extra == "zenoh"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# Asyncio For Robotics
|
|
51
|
+
| Requirements | Compatibility | Tests |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| [](https://pypi.org/project/asyncio_for_robotics/)<br>[](https://opensource.org/license/mit) | [](https://github.com/ros2)<br>[](https://zenoh.io/) | [](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.yml)<br>[](https://github.com/2lian/asyncio-for-robotics/actions/workflows/ros-pytest.yml) |
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
- Better syntax.
|
|
58
|
+
- Only native python: Better docs and support.
|
|
59
|
+
- Simplifies testing.
|
|
60
|
+
|
|
61
|
+
*Will this make my code slower?* [Likely not.](https://github.com/2lian/asyncio-for-robotics/tree/main/README.md#about-speed)
|
|
62
|
+
|
|
63
|
+
*Will this make my code faster?* No. However, `asyncio` will help YOU write
|
|
64
|
+
better, faster code.
|
|
65
|
+
|
|
66
|
+
> [!TIP]
|
|
67
|
+
> `asyncio_for_robotics` interfaces do not replace their primary interfaces! We add capabilities, giving you more choices, not less.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
### Barebone
|
|
73
|
+
|
|
74
|
+
Compatible with ROS 2 (`jazzy`,`humble` and newer) out of the box. This library is pure python (>=3.10), so it installs easily.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install asyncio_for_robotics
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Along with Zenoh
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install asyncio_for_robotics eclipse-zenoh
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Read more
|
|
87
|
+
|
|
88
|
+
- [Detailed ROS 2 tutorial](https://github.com/2lian/asyncio-for-robotics/blob/main/using_with_ros.md)
|
|
89
|
+
- [Lifetime with `afor.Scope`](https://github.com/2lian/asyncio-for-robotics/blob/main/using_scope.md) and [Backend's Sessions](https://github.com/2lian/asyncio-for-robotics/blob/main//using_session.md)
|
|
90
|
+
- [Detailed examples](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example)
|
|
91
|
+
- [no talking 🦍 show me code 🦍](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example/ros2_pubsub.py)
|
|
92
|
+
- [Cross-Platform deployment even with ROS](https://github.com/2lian/asyncio-for-robotics/blob/main/cross_platform.md) [](https://pixi.sh)
|
|
93
|
+
- [Usage for software testing](https://github.com/2lian/asyncio-for-robotics/blob/main/tests)
|
|
94
|
+
|
|
95
|
+
## Available interfaces:
|
|
96
|
+
- **Rate**: Every tick of a clock. (native)
|
|
97
|
+
- **TextIO**: `stdout` lines of a `Popen` process (and other `TextIO` files). (native)
|
|
98
|
+
- **ROS 2**: Subscriber, Service Client, Service Server.
|
|
99
|
+
- **Zenoh**: Subscriber.
|
|
100
|
+
- [Implement your own interface!](https://github.com/2lian/asyncio-for-robotics/blob/main/own_proto_example.md)
|
|
101
|
+
|
|
102
|
+
### Additional Projects and Interfaces
|
|
103
|
+
- **[gogo_keyboard](https://github.com/2lian/gogo_keyboard)**: Subscribe to keyboard key presses and release.
|
|
104
|
+
- **[asyncio_gazebo](https://github.com/2lian/asyncio-gazebo)**: Subscribe to Gazebo transport.
|
|
105
|
+
|
|
106
|
+
> [!TIP]
|
|
107
|
+
> An interface is not required for every operation. ROS 2 native publishers and
|
|
108
|
+
> nodes work just fine. Furthermore, advanced behavior can be composed of
|
|
109
|
+
> generic `afor` object (see [ROS2 Event Callback
|
|
110
|
+
> Example](./asyncio_for_robotics/example/ros2_event_callback.py)).
|
|
111
|
+
>
|
|
112
|
+
## Code sample
|
|
113
|
+
|
|
114
|
+
Syntax is identical between ROS 2, Zenoh, TextIO, Rate...
|
|
115
|
+
|
|
116
|
+
### Wait for messages one by one
|
|
117
|
+
|
|
118
|
+
Application:
|
|
119
|
+
- Get the latest sensor data
|
|
120
|
+
- Get clock value
|
|
121
|
+
- Wait for trigger
|
|
122
|
+
- Wait for next tick of the Rate
|
|
123
|
+
- Wait for system to be operational
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
sub = afor.Sub(...)
|
|
127
|
+
|
|
128
|
+
# Get the latest message
|
|
129
|
+
latest = await sub.wait_for_value()
|
|
130
|
+
|
|
131
|
+
# Get a new message
|
|
132
|
+
new = await sub.wait_for_new()
|
|
133
|
+
|
|
134
|
+
# Get the next message received
|
|
135
|
+
next = await sub.wait_for_next()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Continuously listen to a data stream
|
|
139
|
+
|
|
140
|
+
Application:
|
|
141
|
+
- Process a whole data stream
|
|
142
|
+
- React to changes in sensor data
|
|
143
|
+
- Execute on every tick of the Rate
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Continuously process the latest messages
|
|
147
|
+
async for msg in sub.listen():
|
|
148
|
+
status = foo(msg)
|
|
149
|
+
if status == DONE:
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
# Continuously process all incoming messages
|
|
153
|
+
async for msg in sub.listen_reliable():
|
|
154
|
+
status = foo(msg)
|
|
155
|
+
if status == DONE:
|
|
156
|
+
break
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Scope and Lifetimes
|
|
160
|
+
|
|
161
|
+
Application:
|
|
162
|
+
- Destroy subscribers automatically when leaving a block
|
|
163
|
+
- Keep ownership visible
|
|
164
|
+
- Group several `afor` objects under one lifetime
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
async with afor.Scope():
|
|
168
|
+
sub1 = afor.ros2.Sub(String, "/chatter1")
|
|
169
|
+
sub2 = afor.ros2.Sub(String, "/chatter2")
|
|
170
|
+
|
|
171
|
+
# sub1 and sub2 are alive inside this scope/codeblock
|
|
172
|
+
await sub1.wait_for_value()
|
|
173
|
+
...
|
|
174
|
+
# exiting the scope cleans up resources of sub1 and sub2
|
|
175
|
+
# here ROS 2 subscription are destroyed from the transport
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
See [Lifetime and `afor.Scope`](https://github.com/2lian/asyncio-for-robotics/blob/main/using_scope.md) for details.
|
|
179
|
+
|
|
180
|
+
### Improved Services / Queryable for ROS 2
|
|
181
|
+
|
|
182
|
+
> [!NOTE]
|
|
183
|
+
> This is only for ROS 2.
|
|
184
|
+
|
|
185
|
+
Application:
|
|
186
|
+
- Client request reply from a server.
|
|
187
|
+
- Servers can delay their response without blocking (not possible in native ROS 2)
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Server is once again a afor subscriber, but generating responder objects
|
|
191
|
+
server = afor.Server(...)
|
|
192
|
+
|
|
193
|
+
# processes all requests.
|
|
194
|
+
# listen_reliable method is recommanded as it cannot skip requests
|
|
195
|
+
async for responder in server.listen_reliable():
|
|
196
|
+
if responder.request == "PING!":
|
|
197
|
+
reponder.response = "PONG!"
|
|
198
|
+
await asyncio.sleep(...) # reply can be differed
|
|
199
|
+
reponder.send()
|
|
200
|
+
else:
|
|
201
|
+
... # reply is not necessary
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# the client implements a async call method
|
|
206
|
+
client = afor.Client(...)
|
|
207
|
+
|
|
208
|
+
response = await client.call("PING!")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Process for the right amount of time
|
|
212
|
+
|
|
213
|
+
Application:
|
|
214
|
+
- Test if the system is responding as expected
|
|
215
|
+
- Run small tasks with small and local code
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Listen with a timeout
|
|
219
|
+
data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
|
|
220
|
+
if isinstance(data, TimeoutError):
|
|
221
|
+
pytest.fail(f"Failed to get new data in under 1 second")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Process a codeblock with a timeout
|
|
225
|
+
async with afor.soft_timeout(1):
|
|
226
|
+
sum = 0
|
|
227
|
+
total = 0
|
|
228
|
+
async for msg in sub.listen_reliable():
|
|
229
|
+
number = process(msg)
|
|
230
|
+
sum += number
|
|
231
|
+
total += 1
|
|
232
|
+
|
|
233
|
+
last_second_average = sum/total
|
|
234
|
+
assert last_second_average == pytest.approx(expected_average)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Apply pre-processing to a data-stream
|
|
238
|
+
|
|
239
|
+
Application:
|
|
240
|
+
- Parse payload of different transport into a common type.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# ROS2 String type afor subscriber
|
|
244
|
+
inner_sub: Sub[String] = afor.ros2.Sub(String, "topic_name")
|
|
245
|
+
# converted into a subscriber generating python `str`
|
|
246
|
+
ros2str_func = lambda msg: msg.data
|
|
247
|
+
sub: BaseSub[str] = afor.ConverterSub(sub=inner_sub, convert_func=ros2str_func)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## About Speed
|
|
251
|
+
|
|
252
|
+
The obvious question is whether this adds latency compared to native ROS 2.
|
|
253
|
+
|
|
254
|
+
In this benchmark, the answer is: a little on ROS 2, very little on Zenoh.
|
|
255
|
+
|
|
256
|
+
- On ROS 2 Jazzy with `SingleThreadedExecutor` and `rmw_zenoh_cpp`, trip
|
|
257
|
+
duration increases from 70 μs to 140 μs when using afor, for an added
|
|
258
|
+
overhead of about 70 μs.
|
|
259
|
+
- On Zenoh, `afor` adds only about 7 μs over the native path.
|
|
260
|
+
This suggests that most of the ROS 2 cost comes from cross-thread operations
|
|
261
|
+
with the `rclpy` machinery.
|
|
262
|
+
- Even with this added overhead, Zenoh + `afor` remains about an order of
|
|
263
|
+
magnitude faster than ROS 2 + `rclpy` in this benchmark.
|
|
264
|
+
- For many Python robotics applications, an extra few dozen microseconds is
|
|
265
|
+
negligible relative to the benefits of a uniform `asyncio` interface.
|
|
266
|
+
- This benchmark measures latency; it **does not measure throughput**. A *2x*
|
|
267
|
+
latency increase, does not imply a *2x* throughput decrease.
|
|
268
|
+
|
|
269
|
+
| Backend | Interface | Latency (μs) |
|
|
270
|
+
| :--------- | :------------ | -----------: |
|
|
271
|
+
| No-backend | `afor` | 4 |
|
|
272
|
+
| Zenoh | *native* | 3 |
|
|
273
|
+
| Zenoh | `afor` | 10 |
|
|
274
|
+
| ROS Single Thrd | *native* | 70 |
|
|
275
|
+
| ROS Single Thrd | `afor` | 136 |
|
|
276
|
+
| ROS Multi Thrd | *native* | 125 |
|
|
277
|
+
| ROS Multi Thrd | `afor` | 217 |
|
|
278
|
+
| [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | [`afor`](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/ros2/session.py#L211C7-L211C25) | 280 |
|
|
279
|
+
|
|
280
|
+
Benchmark code is available at
|
|
281
|
+
[https://github.com/2lian/afor_benchmarks](https://github.com/2lian/afor_benchmarks).
|
|
282
|
+
The benchmark uses two pub/sub pairs that continuously echo messages on
|
|
283
|
+
localhost, with a single participant and a local Zenoh router.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Asyncio For Robotics
|
|
2
|
+
| Requirements | Compatibility | Tests |
|
|
3
|
+
|---|---|---|
|
|
4
|
+
| [](https://pypi.org/project/asyncio_for_robotics/)<br>[](https://opensource.org/license/mit) | [](https://github.com/ros2)<br>[](https://zenoh.io/) | [](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.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?* [Likely not.](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
|
+
> [!TIP]
|
|
18
|
+
> `asyncio_for_robotics` interfaces do not replace their primary interfaces! We add capabilities, giving you more choices, not less.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
### Barebone
|
|
24
|
+
|
|
25
|
+
Compatible with ROS 2 (`jazzy`,`humble` and newer) out of the box. This library is pure python (>=3.10), so it installs easily.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install asyncio_for_robotics
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Along with Zenoh
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install asyncio_for_robotics eclipse-zenoh
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Read more
|
|
38
|
+
|
|
39
|
+
- [Detailed ROS 2 tutorial](https://github.com/2lian/asyncio-for-robotics/blob/main/using_with_ros.md)
|
|
40
|
+
- [Lifetime with `afor.Scope`](https://github.com/2lian/asyncio-for-robotics/blob/main/using_scope.md) and [Backend's Sessions](https://github.com/2lian/asyncio-for-robotics/blob/main//using_session.md)
|
|
41
|
+
- [Detailed examples](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example)
|
|
42
|
+
- [no talking 🦍 show me code 🦍](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/example/ros2_pubsub.py)
|
|
43
|
+
- [Cross-Platform deployment even with ROS](https://github.com/2lian/asyncio-for-robotics/blob/main/cross_platform.md) [](https://pixi.sh)
|
|
44
|
+
- [Usage for software testing](https://github.com/2lian/asyncio-for-robotics/blob/main/tests)
|
|
45
|
+
|
|
46
|
+
## Available interfaces:
|
|
47
|
+
- **Rate**: Every tick of a clock. (native)
|
|
48
|
+
- **TextIO**: `stdout` lines of a `Popen` process (and other `TextIO` files). (native)
|
|
49
|
+
- **ROS 2**: Subscriber, Service Client, Service Server.
|
|
50
|
+
- **Zenoh**: Subscriber.
|
|
51
|
+
- [Implement your own interface!](https://github.com/2lian/asyncio-for-robotics/blob/main/own_proto_example.md)
|
|
52
|
+
|
|
53
|
+
### Additional Projects and Interfaces
|
|
54
|
+
- **[gogo_keyboard](https://github.com/2lian/gogo_keyboard)**: Subscribe to keyboard key presses and release.
|
|
55
|
+
- **[asyncio_gazebo](https://github.com/2lian/asyncio-gazebo)**: Subscribe to Gazebo transport.
|
|
56
|
+
|
|
57
|
+
> [!TIP]
|
|
58
|
+
> An interface is not required for every operation. ROS 2 native publishers and
|
|
59
|
+
> nodes work just fine. Furthermore, advanced behavior can be composed of
|
|
60
|
+
> generic `afor` object (see [ROS2 Event Callback
|
|
61
|
+
> Example](./asyncio_for_robotics/example/ros2_event_callback.py)).
|
|
62
|
+
>
|
|
63
|
+
## Code sample
|
|
64
|
+
|
|
65
|
+
Syntax is identical between ROS 2, Zenoh, TextIO, Rate...
|
|
66
|
+
|
|
67
|
+
### Wait for messages one by one
|
|
68
|
+
|
|
69
|
+
Application:
|
|
70
|
+
- Get the latest sensor data
|
|
71
|
+
- Get clock value
|
|
72
|
+
- Wait for trigger
|
|
73
|
+
- Wait for next tick of the Rate
|
|
74
|
+
- Wait for system to be operational
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
sub = afor.Sub(...)
|
|
78
|
+
|
|
79
|
+
# Get the latest message
|
|
80
|
+
latest = await sub.wait_for_value()
|
|
81
|
+
|
|
82
|
+
# Get a new message
|
|
83
|
+
new = await sub.wait_for_new()
|
|
84
|
+
|
|
85
|
+
# Get the next message received
|
|
86
|
+
next = await sub.wait_for_next()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Continuously listen to a data stream
|
|
90
|
+
|
|
91
|
+
Application:
|
|
92
|
+
- Process a whole data stream
|
|
93
|
+
- React to changes in sensor data
|
|
94
|
+
- Execute on every tick of the Rate
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Continuously process the latest messages
|
|
98
|
+
async for msg in sub.listen():
|
|
99
|
+
status = foo(msg)
|
|
100
|
+
if status == DONE:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
# Continuously process all incoming messages
|
|
104
|
+
async for msg in sub.listen_reliable():
|
|
105
|
+
status = foo(msg)
|
|
106
|
+
if status == DONE:
|
|
107
|
+
break
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Scope and Lifetimes
|
|
111
|
+
|
|
112
|
+
Application:
|
|
113
|
+
- Destroy subscribers automatically when leaving a block
|
|
114
|
+
- Keep ownership visible
|
|
115
|
+
- Group several `afor` objects under one lifetime
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
async with afor.Scope():
|
|
119
|
+
sub1 = afor.ros2.Sub(String, "/chatter1")
|
|
120
|
+
sub2 = afor.ros2.Sub(String, "/chatter2")
|
|
121
|
+
|
|
122
|
+
# sub1 and sub2 are alive inside this scope/codeblock
|
|
123
|
+
await sub1.wait_for_value()
|
|
124
|
+
...
|
|
125
|
+
# exiting the scope cleans up resources of sub1 and sub2
|
|
126
|
+
# here ROS 2 subscription are destroyed from the transport
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See [Lifetime and `afor.Scope`](https://github.com/2lian/asyncio-for-robotics/blob/main/using_scope.md) for details.
|
|
130
|
+
|
|
131
|
+
### Improved Services / Queryable for ROS 2
|
|
132
|
+
|
|
133
|
+
> [!NOTE]
|
|
134
|
+
> This is only for ROS 2.
|
|
135
|
+
|
|
136
|
+
Application:
|
|
137
|
+
- Client request reply from a server.
|
|
138
|
+
- Servers can delay their response without blocking (not possible in native ROS 2)
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Server is once again a afor subscriber, but generating responder objects
|
|
142
|
+
server = afor.Server(...)
|
|
143
|
+
|
|
144
|
+
# processes all requests.
|
|
145
|
+
# listen_reliable method is recommanded as it cannot skip requests
|
|
146
|
+
async for responder in server.listen_reliable():
|
|
147
|
+
if responder.request == "PING!":
|
|
148
|
+
reponder.response = "PONG!"
|
|
149
|
+
await asyncio.sleep(...) # reply can be differed
|
|
150
|
+
reponder.send()
|
|
151
|
+
else:
|
|
152
|
+
... # reply is not necessary
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# the client implements a async call method
|
|
157
|
+
client = afor.Client(...)
|
|
158
|
+
|
|
159
|
+
response = await client.call("PING!")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Process for the right amount of time
|
|
163
|
+
|
|
164
|
+
Application:
|
|
165
|
+
- Test if the system is responding as expected
|
|
166
|
+
- Run small tasks with small and local code
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# Listen with a timeout
|
|
170
|
+
data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
|
|
171
|
+
if isinstance(data, TimeoutError):
|
|
172
|
+
pytest.fail(f"Failed to get new data in under 1 second")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Process a codeblock with a timeout
|
|
176
|
+
async with afor.soft_timeout(1):
|
|
177
|
+
sum = 0
|
|
178
|
+
total = 0
|
|
179
|
+
async for msg in sub.listen_reliable():
|
|
180
|
+
number = process(msg)
|
|
181
|
+
sum += number
|
|
182
|
+
total += 1
|
|
183
|
+
|
|
184
|
+
last_second_average = sum/total
|
|
185
|
+
assert last_second_average == pytest.approx(expected_average)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Apply pre-processing to a data-stream
|
|
189
|
+
|
|
190
|
+
Application:
|
|
191
|
+
- Parse payload of different transport into a common type.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# ROS2 String type afor subscriber
|
|
195
|
+
inner_sub: Sub[String] = afor.ros2.Sub(String, "topic_name")
|
|
196
|
+
# converted into a subscriber generating python `str`
|
|
197
|
+
ros2str_func = lambda msg: msg.data
|
|
198
|
+
sub: BaseSub[str] = afor.ConverterSub(sub=inner_sub, convert_func=ros2str_func)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## About Speed
|
|
202
|
+
|
|
203
|
+
The obvious question is whether this adds latency compared to native ROS 2.
|
|
204
|
+
|
|
205
|
+
In this benchmark, the answer is: a little on ROS 2, very little on Zenoh.
|
|
206
|
+
|
|
207
|
+
- On ROS 2 Jazzy with `SingleThreadedExecutor` and `rmw_zenoh_cpp`, trip
|
|
208
|
+
duration increases from 70 μs to 140 μs when using afor, for an added
|
|
209
|
+
overhead of about 70 μs.
|
|
210
|
+
- On Zenoh, `afor` adds only about 7 μs over the native path.
|
|
211
|
+
This suggests that most of the ROS 2 cost comes from cross-thread operations
|
|
212
|
+
with the `rclpy` machinery.
|
|
213
|
+
- Even with this added overhead, Zenoh + `afor` remains about an order of
|
|
214
|
+
magnitude faster than ROS 2 + `rclpy` in this benchmark.
|
|
215
|
+
- For many Python robotics applications, an extra few dozen microseconds is
|
|
216
|
+
negligible relative to the benefits of a uniform `asyncio` interface.
|
|
217
|
+
- This benchmark measures latency; it **does not measure throughput**. A *2x*
|
|
218
|
+
latency increase, does not imply a *2x* throughput decrease.
|
|
219
|
+
|
|
220
|
+
| Backend | Interface | Latency (μs) |
|
|
221
|
+
| :--------- | :------------ | -----------: |
|
|
222
|
+
| No-backend | `afor` | 4 |
|
|
223
|
+
| Zenoh | *native* | 3 |
|
|
224
|
+
| Zenoh | `afor` | 10 |
|
|
225
|
+
| ROS Single Thrd | *native* | 70 |
|
|
226
|
+
| ROS Single Thrd | `afor` | 136 |
|
|
227
|
+
| ROS Multi Thrd | *native* | 125 |
|
|
228
|
+
| ROS Multi Thrd | `afor` | 217 |
|
|
229
|
+
| [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | [`afor`](https://github.com/2lian/asyncio-for-robotics/blob/main/asyncio_for_robotics/ros2/session.py#L211C7-L211C25) | 280 |
|
|
230
|
+
|
|
231
|
+
Benchmark code is available at
|
|
232
|
+
[https://github.com/2lian/afor_benchmarks](https://github.com/2lian/afor_benchmarks).
|
|
233
|
+
The benchmark uses two pub/sub pairs that continuously echo messages on
|
|
234
|
+
localhost, with a single participant and a local Zenoh router.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .core.scope import AUTO_SCOPE, Scope, ScopeBreak, scoped
|
|
2
|
+
from .core.utils import soft_timeout, soft_wait_for, Rate
|
|
3
|
+
from .core.sub import BaseSub, ConverterSub
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AUTO_SCOPE",
|
|
7
|
+
"Scope",
|
|
8
|
+
"ScopeBreak",
|
|
9
|
+
"scoped",
|
|
10
|
+
"soft_wait_for",
|
|
11
|
+
"soft_timeout",
|
|
12
|
+
"Rate",
|
|
13
|
+
"BaseSub",
|
|
14
|
+
"ConverterSub",
|
|
15
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .scope import AUTO_SCOPE, Scope, ScopeBreak, scoped
|
|
2
|
+
from .utils import soft_timeout, soft_wait_for, Rate
|
|
3
|
+
from .sub import BaseSub, ConverterSub
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AUTO_SCOPE",
|
|
7
|
+
"Scope",
|
|
8
|
+
"ScopeBreak",
|
|
9
|
+
"scoped",
|
|
10
|
+
"BaseSub",
|
|
11
|
+
"ConverterSub",
|
|
12
|
+
"soft_wait_for",
|
|
13
|
+
"soft_timeout",
|
|
14
|
+
"Rate",
|
|
15
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from asyncio import TaskGroup
|
|
3
|
+
except ImportError:
|
|
4
|
+
try:
|
|
5
|
+
from taskgroup import TaskGroup
|
|
6
|
+
except ImportError:
|
|
7
|
+
TaskGroup = NotImplemented
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
BaseExceptionGroup = BaseExceptionGroup
|
|
11
|
+
except NameError:
|
|
12
|
+
# BaseExceptionGroup = NotImplemented
|
|
13
|
+
from exceptiongroup import BaseExceptionGroup
|