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.
Files changed (53) hide show
  1. asyncio_for_robotics-1.4.0/LICENSE +21 -0
  2. asyncio_for_robotics-1.4.0/PKG-INFO +283 -0
  3. asyncio_for_robotics-1.4.0/README.md +234 -0
  4. asyncio_for_robotics-1.4.0/asyncio_for_robotics/__init__.py +15 -0
  5. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/__init__.py +15 -0
  6. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/_compat.py +13 -0
  7. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/_logger.py +125 -0
  8. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/scope.py +379 -0
  9. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/sub.py +382 -0
  10. asyncio_for_robotics-1.4.0/asyncio_for_robotics/core/utils.py +161 -0
  11. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/__init__.py +0 -0
  12. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/custom_stdout.py +44 -0
  13. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/delete_me.py +32 -0
  14. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/python_discussion.py +116 -0
  15. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_discussion.py +113 -0
  16. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_double_listener.py +78 -0
  17. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_double_talker.py +108 -0
  18. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_event_callback.py +153 -0
  19. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_listener.py +24 -0
  20. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_pubsub.py +58 -0
  21. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_service_client.py +33 -0
  22. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_service_server.py +29 -0
  23. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/ros2_talker.py +35 -0
  24. asyncio_for_robotics-1.4.0/asyncio_for_robotics/example/zenoh_discussion.py +68 -0
  25. asyncio_for_robotics-1.4.0/asyncio_for_robotics/py.typed +1 -0
  26. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/__init__.py +46 -0
  27. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/future.py +47 -0
  28. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/service.py +317 -0
  29. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/session.py +106 -0
  30. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/session_types.py +281 -0
  31. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/sub.py +107 -0
  32. asyncio_for_robotics-1.4.0/asyncio_for_robotics/ros2/utils.py +45 -0
  33. asyncio_for_robotics-1.4.0/asyncio_for_robotics/textio/__init__.py +12 -0
  34. asyncio_for_robotics-1.4.0/asyncio_for_robotics/textio/sub.py +117 -0
  35. asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/__init__.py +33 -0
  36. asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/session.py +113 -0
  37. asyncio_for_robotics-1.4.0/asyncio_for_robotics/zenoh/sub.py +75 -0
  38. asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/PKG-INFO +283 -0
  39. asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/SOURCES.txt +51 -0
  40. asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/dependency_links.txt +1 -0
  41. asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/requires.txt +22 -0
  42. asyncio_for_robotics-1.4.0/asyncio_for_robotics.egg-info/top_level.txt +1 -0
  43. asyncio_for_robotics-1.4.0/pyproject.toml +68 -0
  44. asyncio_for_robotics-1.4.0/setup.cfg +4 -0
  45. asyncio_for_robotics-1.4.0/tests/test_core.py +39 -0
  46. asyncio_for_robotics-1.4.0/tests/test_ros2_pubsub_sync.py +54 -0
  47. asyncio_for_robotics-1.4.0/tests/test_ros2_pubsub_thr.py +73 -0
  48. asyncio_for_robotics-1.4.0/tests/test_ros2_serv.py +319 -0
  49. asyncio_for_robotics-1.4.0/tests/test_ros2_stdout_echo.py +180 -0
  50. asyncio_for_robotics-1.4.0/tests/test_scope.py +179 -0
  51. asyncio_for_robotics-1.4.0/tests/test_textio.py +69 -0
  52. asyncio_for_robotics-1.4.0/tests/test_utils.py +64 -0
  53. 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
+ | [![python](https://img.shields.io/pypi/pyversions/asyncio_for_robotics?logo=python&logoColor=white&label=Python&color=%20blue)](https://pypi.org/project/asyncio_for_robotics/)<br>[![mit](https://img.shields.io/badge/License-MIT-gold)](https://opensource.org/license/mit) | [![ros](https://img.shields.io/badge/ROS_2-Humble%20%7C%20Jazzy-blue?logo=ros)](https://github.com/ros2)<br>[![zenoh](https://img.shields.io/badge/Zenoh-%3E%3D1.0-blue)](https://zenoh.io/) | [![Python](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.yml/badge.svg)](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.yml)<br>[![ROS 2](https://github.com/2lian/asyncio-for-robotics/actions/workflows/ros-pytest.yml/badge.svg)](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) [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](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
+ | [![python](https://img.shields.io/pypi/pyversions/asyncio_for_robotics?logo=python&logoColor=white&label=Python&color=%20blue)](https://pypi.org/project/asyncio_for_robotics/)<br>[![mit](https://img.shields.io/badge/License-MIT-gold)](https://opensource.org/license/mit) | [![ros](https://img.shields.io/badge/ROS_2-Humble%20%7C%20Jazzy-blue?logo=ros)](https://github.com/ros2)<br>[![zenoh](https://img.shields.io/badge/Zenoh-%3E%3D1.0-blue)](https://zenoh.io/) | [![Python](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.yml/badge.svg)](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-pytest.yml)<br>[![ROS 2](https://github.com/2lian/asyncio-for-robotics/actions/workflows/ros-pytest.yml/badge.svg)](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) [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](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