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.
Files changed (41) hide show
  1. asyncio_for_robotics-1.0.4/LICENSE +21 -0
  2. asyncio_for_robotics-1.0.4/PKG-INFO +261 -0
  3. asyncio_for_robotics-1.0.4/README.md +217 -0
  4. asyncio_for_robotics-1.0.4/asyncio_for_robotics/__init__.py +7 -0
  5. asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/__init__.py +0 -0
  6. asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/_logger.py +125 -0
  7. asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/sub.py +238 -0
  8. asyncio_for_robotics-1.0.4/asyncio_for_robotics/core/utils.py +114 -0
  9. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/__init__.py +0 -0
  10. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/custom_stdout.py +42 -0
  11. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/python_discussion.py +116 -0
  12. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_discussion.py +127 -0
  13. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_double_listener.py +83 -0
  14. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_double_talker.py +107 -0
  15. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_listener.py +30 -0
  16. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_pubsub.py +55 -0
  17. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_service_client.py +42 -0
  18. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_service_server.py +35 -0
  19. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/ros2_talker.py +41 -0
  20. asyncio_for_robotics-1.0.4/asyncio_for_robotics/example/zenoh_discussion.py +76 -0
  21. asyncio_for_robotics-1.0.4/asyncio_for_robotics/py.typed +1 -0
  22. asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/__init__.py +29 -0
  23. asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/service.py +263 -0
  24. asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/session.py +307 -0
  25. asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/sub.py +78 -0
  26. asyncio_for_robotics-1.0.4/asyncio_for_robotics/ros2/utils.py +45 -0
  27. asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/__init__.py +14 -0
  28. asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/session.py +42 -0
  29. asyncio_for_robotics-1.0.4/asyncio_for_robotics/zenoh/sub.py +63 -0
  30. asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/PKG-INFO +261 -0
  31. asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/SOURCES.txt +39 -0
  32. asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/dependency_links.txt +1 -0
  33. asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/requires.txt +18 -0
  34. asyncio_for_robotics-1.0.4/asyncio_for_robotics.egg-info/top_level.txt +1 -0
  35. asyncio_for_robotics-1.0.4/pyproject.toml +61 -0
  36. asyncio_for_robotics-1.0.4/setup.cfg +4 -0
  37. asyncio_for_robotics-1.0.4/tests/test_ros2_pubsub_sync.py +70 -0
  38. asyncio_for_robotics-1.0.4/tests/test_ros2_pubsub_thr.py +220 -0
  39. asyncio_for_robotics-1.0.4/tests/test_ros2_serv.py +82 -0
  40. asyncio_for_robotics-1.0.4/tests/test_utils.py +48 -0
  41. 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
+ | [![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) | [![zenoh](https://img.shields.io/badge/Zenoh-%3E%3D1.0-blue)](https://zenoh.io/)<br>[![ros](https://img.shields.io/badge/ROS_2-Humble%20%7C%20Jazzy-blue?logo=ros)](https://github.com/ros2) | [![Python](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.yml/badge.svg)](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.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) |
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
+ | [![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) | [![zenoh](https://img.shields.io/badge/Zenoh-%3E%3D1.0-blue)](https://zenoh.io/)<br>[![ros](https://img.shields.io/badge/ROS_2-Humble%20%7C%20Jazzy-blue?logo=ros)](https://github.com/ros2) | [![Python](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.yml/badge.svg)](https://github.com/2lian/asyncio-for-robotics/actions/workflows/python-package.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?* [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.
@@ -0,0 +1,7 @@
1
+ from asyncio_for_robotics.core.utils import soft_timeout, soft_wait_for, Rate
2
+
3
+ __all__ = [
4
+ "soft_wait_for",
5
+ "soft_timeout",
6
+ "Rate",
7
+ ]
@@ -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
+