asyncio_for_robotics 1.0.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 (40) hide show
  1. asyncio_for_robotics-1.0.0/LICENSE +21 -0
  2. asyncio_for_robotics-1.0.0/PKG-INFO +255 -0
  3. asyncio_for_robotics-1.0.0/README.md +218 -0
  4. asyncio_for_robotics-1.0.0/asyncio_for_robotics/__init__.py +7 -0
  5. asyncio_for_robotics-1.0.0/asyncio_for_robotics/core/__init__.py +0 -0
  6. asyncio_for_robotics-1.0.0/asyncio_for_robotics/core/_logger.py +125 -0
  7. asyncio_for_robotics-1.0.0/asyncio_for_robotics/core/sub.py +238 -0
  8. asyncio_for_robotics-1.0.0/asyncio_for_robotics/core/utils.py +114 -0
  9. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/__init__.py +0 -0
  10. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/custom_stdout.py +42 -0
  11. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/python_discussion.py +116 -0
  12. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_discussion.py +120 -0
  13. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_double_listener.py +78 -0
  14. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_double_talker.py +100 -0
  15. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_listener.py +22 -0
  16. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_service_client.py +41 -0
  17. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_service_server.py +36 -0
  18. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/ros2_talker.py +39 -0
  19. asyncio_for_robotics-1.0.0/asyncio_for_robotics/example/zenoh_discussion.py +69 -0
  20. asyncio_for_robotics-1.0.0/asyncio_for_robotics/py.typed +1 -0
  21. asyncio_for_robotics-1.0.0/asyncio_for_robotics/ros2/__init__.py +29 -0
  22. asyncio_for_robotics-1.0.0/asyncio_for_robotics/ros2/service.py +263 -0
  23. asyncio_for_robotics-1.0.0/asyncio_for_robotics/ros2/session.py +307 -0
  24. asyncio_for_robotics-1.0.0/asyncio_for_robotics/ros2/sub.py +78 -0
  25. asyncio_for_robotics-1.0.0/asyncio_for_robotics/ros2/utils.py +45 -0
  26. asyncio_for_robotics-1.0.0/asyncio_for_robotics/zenoh/__init__.py +14 -0
  27. asyncio_for_robotics-1.0.0/asyncio_for_robotics/zenoh/session.py +42 -0
  28. asyncio_for_robotics-1.0.0/asyncio_for_robotics/zenoh/sub.py +63 -0
  29. asyncio_for_robotics-1.0.0/asyncio_for_robotics.egg-info/PKG-INFO +255 -0
  30. asyncio_for_robotics-1.0.0/asyncio_for_robotics.egg-info/SOURCES.txt +38 -0
  31. asyncio_for_robotics-1.0.0/asyncio_for_robotics.egg-info/dependency_links.txt +1 -0
  32. asyncio_for_robotics-1.0.0/asyncio_for_robotics.egg-info/requires.txt +18 -0
  33. asyncio_for_robotics-1.0.0/asyncio_for_robotics.egg-info/top_level.txt +1 -0
  34. asyncio_for_robotics-1.0.0/pyproject.toml +54 -0
  35. asyncio_for_robotics-1.0.0/setup.cfg +4 -0
  36. asyncio_for_robotics-1.0.0/tests/test_ros2_pubsub_sync.py +70 -0
  37. asyncio_for_robotics-1.0.0/tests/test_ros2_pubsub_thr.py +220 -0
  38. asyncio_for_robotics-1.0.0/tests/test_ros2_serv.py +82 -0
  39. asyncio_for_robotics-1.0.0/tests/test_utils.py +48 -0
  40. asyncio_for_robotics-1.0.0/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,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: asyncio_for_robotics
3
+ Version: 1.0.0
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: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development :: Embedded Systems
18
+ Classifier: Topic :: System :: Networking
19
+ Classifier: Framework :: AsyncIO
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: typing_extensions>=4.0; python_version < "3.11"
24
+ Requires-Dist: async-timeout; python_version < "3.11"
25
+ Provides-Extra: zenoh
26
+ Requires-Dist: eclipse-zenoh>=1.0.0; extra == "zenoh"
27
+ Provides-Extra: dev
28
+ Requires-Dist: colorama; extra == "dev"
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: pytest-asyncio; extra == "dev"
31
+ Requires-Dist: pyfiglet; extra == "dev"
32
+ Requires-Dist: uvloop; extra == "dev"
33
+ Provides-Extra: build
34
+ Requires-Dist: build; extra == "build"
35
+ Requires-Dist: twine; extra == "build"
36
+ Dynamic: license-file
37
+
38
+ # Asyncio For Robotics
39
+ | Requirements | Compatibility | Tests |
40
+ |---|---|---|
41
+ | [![python](https://img.shields.io/badge/Python-3.10_\|_3.11_\|_3.12_\|_3.13_\|_3.14-%20blue)](https://www.python.org/)<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)](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) |
42
+
43
+ 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.
44
+
45
+ - Better syntax.
46
+ - Only native python: Better docs and support.
47
+ - No ROS 2 executor.
48
+ - Simplifies testing.
49
+
50
+ *Will this make my code slower?* [No.](https://github.com/2lian/asyncio-for-robotics/tree/main/README.md#about-speed)
51
+
52
+ *Will this make my code faster?* No. However `asyncio` will help YOU write
53
+ better, faster code.
54
+
55
+ *Does it replace ROS 2? Is this a wrapper?* No. It is a tool adding async capabilities. It gives you more choices, not less.
56
+
57
+ ## Install
58
+
59
+ ### Barebone
60
+
61
+ ```bash
62
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git
63
+ ```
64
+
65
+ ### For ROS 2
66
+
67
+ Compatible with: `jazzy`,`humble` and newer. This library is pure python (>=3.10), so it installs easily.
68
+
69
+ ```bash
70
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git
71
+ ```
72
+
73
+ ### For Zenoh
74
+
75
+ ```bash
76
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git[zenoh]
77
+ ```
78
+
79
+ ## Read more
80
+
81
+ - [Detailed ROS 2 tutorial](./using_with_ros.md)
82
+ - [Detailed examples](./asyncio_for_robotics/example)
83
+ - [Usage for software testing](./tests)
84
+ - [Implement your own protocol](./own_proto_example.md)
85
+
86
+ ## Code sample
87
+
88
+ Syntax is identical between ROS 2 and Zenoh.
89
+
90
+ ### Wait for messages one by one
91
+
92
+ Application:
93
+ - Get the latest sensor data
94
+ - Get clock value
95
+ - Wait for trigger
96
+ - Wait for system to be operational
97
+
98
+ ```python
99
+ sub = afor.Sub(...)
100
+
101
+ # get the latest message
102
+ latest = await sub.wait_for_value()
103
+
104
+ # get a new message
105
+ new = await sub.wait_for_new()
106
+
107
+ # get the next message received
108
+ next = await sub.wait_for_next()
109
+ ```
110
+
111
+ ### Continuously listen to a data stream
112
+
113
+ Application:
114
+ - Process a whole data stream
115
+ - React to changes in sensor data
116
+
117
+ ```python
118
+ # Continuously process the latest messages
119
+ async for msg in sub.listen():
120
+ status = foo(msg)
121
+ if status == DONE:
122
+ break
123
+
124
+ # Continuously process all incoming messages
125
+ async for msg in sub.listen_reliable():
126
+ status = foo(msg)
127
+ if status == DONE:
128
+ break
129
+ ```
130
+
131
+ ### Reliable, non-drifting Rate
132
+
133
+ Application:
134
+ - Periodic updates and actions
135
+
136
+ ```python
137
+ # Rate is simply a subscriber triggering on every tick
138
+ rate = afor.Rate(frequency=0.01, time_source=time.time_ns)
139
+
140
+ # Wait for the next tick
141
+ await rate.wait_for_new()
142
+
143
+ # Executes after a tick
144
+ async for _ in rate.listen():
145
+ foo(...)
146
+
147
+ # Reliably executes for every tick
148
+ async for _ in sub.listen_reliable():
149
+ foo(...)
150
+ ```
151
+
152
+ ### Improved Services / Queryable for ROS 2
153
+
154
+ Services are needlessly convoluted in ROS 2 and intrinsically not async
155
+ (because the server callback function MUST return a response). `afor` overrides
156
+ the ROS behavior, allowing for the response to be sent later. Implementing
157
+ similar systems for a transport protocol (that is not suffering from skill
158
+ issues) should be very easy: The server is just a
159
+ `asyncio_for_robotics.core.BaseSub` generating responder objects.
160
+
161
+ Application:
162
+ - Client request reply from a server.
163
+ - Servers can delay their response without blocking (not possible in ROS 2)
164
+
165
+ ```python
166
+ # Server is once again a afor subscriber, but generating responder objects
167
+ server = afor.Server(...)
168
+
169
+ # processes all requests.
170
+ # listen_reliable method is recommanded as it cannot skip requests
171
+ async for responder in server.listen_reliable():
172
+ if responder.request == "PING!":
173
+ reponder.response = "PONG!"
174
+ await asyncio.sleep(...) # reply can be differed
175
+ reponder.send()
176
+ else:
177
+ ... # reply not necessary
178
+ ```
179
+
180
+ ```python
181
+ # the client implements a async call method
182
+ client = afor.Client(...)
183
+
184
+ response = await client.call("PING!")
185
+ ```
186
+
187
+ ### Process for the right amount of time
188
+
189
+ Application:
190
+ - Test if the system is responding as expected
191
+ - Run small tasks with small and local code
192
+
193
+ ```python
194
+ # Listen with a timeout
195
+ data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
196
+ if isinstance(data, TimeoutError):
197
+ pytest.fail(f"Failed to get new data in under 1 second")
198
+
199
+
200
+ # Process a codeblock with a timeout
201
+ async with afor.soft_timeout(1):
202
+ sum = 0
203
+ total = 0
204
+ async for msg in sub.listen_reliable():
205
+ number = process(msg)
206
+ sum += number
207
+ total += 1
208
+
209
+ last_second_average = sum/total
210
+ assert last_second_average == pytest.approx(expected_average)
211
+ ```
212
+
213
+ ## About Speed
214
+
215
+ The inevitable question: *“But isn’t this slower than the ROS 2 executor? ROS 2 is the best!”*
216
+
217
+ In short: `rclpy`'s executor is the bottleneck.
218
+ - Comparing to the best ROS 2 Jazzy can do (`SingleThreadedExecutor`), `afor` increases latency from 110us to 150us.
219
+ - Comparing to other execution methods, `afor` is equivalent if not faster.
220
+ - If you find it slow, you should use C++ or Zenoh (or contribute to this repo?).
221
+
222
+ Benchmark code is available in [`./tests/bench/`](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.
223
+
224
+ | With `afor` | Transport | Executor | | Frequency (kHz) | Latency (ms) |
225
+ |:----------:|:----------|:----------------------------------|-|---------:|---------:|
226
+ | ✔️ | Zenoh | None | | **95** | **0.01** |
227
+ | ✔️ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | **17** | **0.06** |
228
+ | ❌ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | 13 | 0.08 |
229
+ | ❌ | ROS 2 | SingleThreaded | | 9 | 0.11 |
230
+ | ✔️ | ROS 2 | SingleThreaded | | **7** | **0.15** |
231
+ | ✔️ | ROS 2 | MultiThreaded | | **3** | **0.3** |
232
+ | ❌ | ROS 2 | MultiThreaded | | **3** | **0.3** |
233
+ | ✔️ | ROS 2 | [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | | 3 | 0.3 |
234
+
235
+
236
+ Details:
237
+ - `uvloop` was used, replacing the asyncio executor (more or less doubles the performances for Zenoh)
238
+ - RMW was set to `rmw_zenoh_cpp`
239
+ - ROS2 benchmarks uses `afor`'s `ros2.ThreadedSession` (the default in `afor`).
240
+ - Only the Benchmark of the [`ros_loop` method](https://github.com/m2-farzan/ros2-asyncio) uses `afor`'s second type of session: `ros2.SynchronousSession`.
241
+ - ROS 2 executors can easily be changed in `afor` when creating a session.
242
+ - 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).
243
+ - If there is interest in those benchmarks I will improve them, so others can run them all easily.
244
+
245
+ Analysis:
246
+ - Zenoh is extremely fast, proving that `afor` is not the bottleneck.
247
+ - This `AsyncioExecutor` having better perf when using `afor` is interesting, because `afor` does not bypass code.
248
+ - I think this is due to `AsyncioExecutor` having some overhead that affects its own callback.
249
+ - Without `afor` the ROS 2 callback executes some code and publishes.
250
+ - With `afor` the ROS 2 callback returns immediately, and fully delegates execution to `asyncio`.
251
+ - 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.
252
+ - `AsyncioExecutor` does not have such thread, thus can directly communicate.
253
+ - Zenoh has its own thread, however it is built exclusively for multi-thread operations, without any executor. Thus achieves far superior performances.
254
+ - `MultiThreadedExecutor` is just famously slow.
255
+ - 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,218 @@
1
+ # Asyncio For Robotics
2
+ | Requirements | Compatibility | Tests |
3
+ |---|---|---|
4
+ | [![python](https://img.shields.io/badge/Python-3.10_\|_3.11_\|_3.12_\|_3.13_\|_3.14-%20blue)](https://www.python.org/)<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)](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
+ - No ROS 2 executor.
11
+ - Simplifies testing.
12
+
13
+ *Will this make my code slower?* [No.](https://github.com/2lian/asyncio-for-robotics/tree/main/README.md#about-speed)
14
+
15
+ *Will this make my code faster?* No. However `asyncio` will help YOU write
16
+ better, faster code.
17
+
18
+ *Does it replace ROS 2? Is this a wrapper?* No. It is a tool adding async capabilities. It gives you more choices, not less.
19
+
20
+ ## Install
21
+
22
+ ### Barebone
23
+
24
+ ```bash
25
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git
26
+ ```
27
+
28
+ ### For ROS 2
29
+
30
+ Compatible with: `jazzy`,`humble` and newer. This library is pure python (>=3.10), so it installs easily.
31
+
32
+ ```bash
33
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git
34
+ ```
35
+
36
+ ### For Zenoh
37
+
38
+ ```bash
39
+ pip install git+https://github.com/2lian/asyncio-for-robotics.git[zenoh]
40
+ ```
41
+
42
+ ## Read more
43
+
44
+ - [Detailed ROS 2 tutorial](./using_with_ros.md)
45
+ - [Detailed examples](./asyncio_for_robotics/example)
46
+ - [Usage for software testing](./tests)
47
+ - [Implement your own protocol](./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 a transport protocol (that is not suffering from skill
121
+ issues) should be very easy: The server is just a
122
+ `asyncio_for_robotics.core.BaseSub` generating responder objects.
123
+
124
+ Application:
125
+ - Client request reply from a server.
126
+ - Servers can delay their response without blocking (not possible in ROS 2)
127
+
128
+ ```python
129
+ # Server is once again a afor subscriber, but generating responder objects
130
+ server = afor.Server(...)
131
+
132
+ # processes all requests.
133
+ # listen_reliable method is recommanded as it cannot skip requests
134
+ async for responder in server.listen_reliable():
135
+ if responder.request == "PING!":
136
+ reponder.response = "PONG!"
137
+ await asyncio.sleep(...) # reply can be differed
138
+ reponder.send()
139
+ else:
140
+ ... # reply not necessary
141
+ ```
142
+
143
+ ```python
144
+ # the client implements a async call method
145
+ client = afor.Client(...)
146
+
147
+ response = await client.call("PING!")
148
+ ```
149
+
150
+ ### Process for the right amount of time
151
+
152
+ Application:
153
+ - Test if the system is responding as expected
154
+ - Run small tasks with small and local code
155
+
156
+ ```python
157
+ # Listen with a timeout
158
+ data = await afor.soft_wait_for(sub.wait_for_new(), timeout=1)
159
+ if isinstance(data, TimeoutError):
160
+ pytest.fail(f"Failed to get new data in under 1 second")
161
+
162
+
163
+ # Process a codeblock with a timeout
164
+ async with afor.soft_timeout(1):
165
+ sum = 0
166
+ total = 0
167
+ async for msg in sub.listen_reliable():
168
+ number = process(msg)
169
+ sum += number
170
+ total += 1
171
+
172
+ last_second_average = sum/total
173
+ assert last_second_average == pytest.approx(expected_average)
174
+ ```
175
+
176
+ ## About Speed
177
+
178
+ The inevitable question: *“But isn’t this slower than the ROS 2 executor? ROS 2 is the best!”*
179
+
180
+ In short: `rclpy`'s executor is the bottleneck.
181
+ - Comparing to the best ROS 2 Jazzy can do (`SingleThreadedExecutor`), `afor` increases latency from 110us to 150us.
182
+ - Comparing to other execution methods, `afor` is equivalent if not faster.
183
+ - If you find it slow, you should use C++ or Zenoh (or contribute to this repo?).
184
+
185
+ Benchmark code is available in [`./tests/bench/`](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.
186
+
187
+ | With `afor` | Transport | Executor | | Frequency (kHz) | Latency (ms) |
188
+ |:----------:|:----------|:----------------------------------|-|---------:|---------:|
189
+ | ✔️ | Zenoh | None | | **95** | **0.01** |
190
+ | ✔️ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | **17** | **0.06** |
191
+ | ❌ | ROS 2 | [Experimental Asyncio](https://github.com/ros2/rclpy/pull/1399) | | 13 | 0.08 |
192
+ | ❌ | ROS 2 | SingleThreaded | | 9 | 0.11 |
193
+ | ✔️ | ROS 2 | SingleThreaded | | **7** | **0.15** |
194
+ | ✔️ | ROS 2 | MultiThreaded | | **3** | **0.3** |
195
+ | ❌ | ROS 2 | MultiThreaded | | **3** | **0.3** |
196
+ | ✔️ | ROS 2 | [`ros_loop` Method](https://github.com/m2-farzan/ros2-asyncio) | | 3 | 0.3 |
197
+
198
+
199
+ Details:
200
+ - `uvloop` was used, replacing the asyncio executor (more or less doubles the performances for Zenoh)
201
+ - RMW was set to `rmw_zenoh_cpp`
202
+ - ROS2 benchmarks uses `afor`'s `ros2.ThreadedSession` (the default in `afor`).
203
+ - Only the Benchmark of the [`ros_loop` method](https://github.com/m2-farzan/ros2-asyncio) uses `afor`'s second type of session: `ros2.SynchronousSession`.
204
+ - ROS 2 executors can easily be changed in `afor` when creating a session.
205
+ - 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).
206
+ - If there is interest in those benchmarks I will improve them, so others can run them all easily.
207
+
208
+ Analysis:
209
+ - Zenoh is extremely fast, proving that `afor` is not the bottleneck.
210
+ - This `AsyncioExecutor` having better perf when using `afor` is interesting, because `afor` does not bypass code.
211
+ - I think this is due to `AsyncioExecutor` having some overhead that affects its own callback.
212
+ - Without `afor` the ROS 2 callback executes some code and publishes.
213
+ - With `afor` the ROS 2 callback returns immediately, and fully delegates execution to `asyncio`.
214
+ - 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.
215
+ - `AsyncioExecutor` does not have such thread, thus can directly communicate.
216
+ - Zenoh has its own thread, however it is built exclusively for multi-thread operations, without any executor. Thus achieves far superior performances.
217
+ - `MultiThreadedExecutor` is just famously slow.
218
+ - 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
+