antioch-py 2.0.6__py3-none-any.whl
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.
Potentially problematic release.
This version of antioch-py might be problematic. Click here for more details.
- antioch/__init__.py +0 -0
- antioch/message.py +87 -0
- antioch/module/__init__.py +53 -0
- antioch/module/clock.py +62 -0
- antioch/module/execution.py +278 -0
- antioch/module/input.py +127 -0
- antioch/module/module.py +218 -0
- antioch/module/node.py +357 -0
- antioch/module/token.py +42 -0
- antioch/session/__init__.py +150 -0
- antioch/session/ark.py +504 -0
- antioch/session/asset.py +65 -0
- antioch/session/error.py +80 -0
- antioch/session/record.py +158 -0
- antioch/session/scene.py +1521 -0
- antioch/session/session.py +220 -0
- antioch/session/task.py +323 -0
- antioch/session/views/__init__.py +40 -0
- antioch/session/views/animation.py +189 -0
- antioch/session/views/articulation.py +245 -0
- antioch/session/views/basis_curve.py +186 -0
- antioch/session/views/camera.py +92 -0
- antioch/session/views/collision.py +75 -0
- antioch/session/views/geometry.py +74 -0
- antioch/session/views/ground_plane.py +63 -0
- antioch/session/views/imu.py +73 -0
- antioch/session/views/joint.py +64 -0
- antioch/session/views/light.py +175 -0
- antioch/session/views/pir_sensor.py +140 -0
- antioch/session/views/radar.py +73 -0
- antioch/session/views/rigid_body.py +282 -0
- antioch/session/views/xform.py +119 -0
- antioch_py-2.0.6.dist-info/METADATA +115 -0
- antioch_py-2.0.6.dist-info/RECORD +99 -0
- antioch_py-2.0.6.dist-info/WHEEL +5 -0
- antioch_py-2.0.6.dist-info/entry_points.txt +2 -0
- antioch_py-2.0.6.dist-info/top_level.txt +2 -0
- common/__init__.py +0 -0
- common/ark/__init__.py +60 -0
- common/ark/ark.py +128 -0
- common/ark/hardware.py +121 -0
- common/ark/kinematics.py +31 -0
- common/ark/module.py +85 -0
- common/ark/node.py +94 -0
- common/ark/scheduler.py +439 -0
- common/ark/sim.py +33 -0
- common/assets/__init__.py +3 -0
- common/constants.py +47 -0
- common/core/__init__.py +52 -0
- common/core/agent.py +296 -0
- common/core/auth.py +305 -0
- common/core/registry.py +331 -0
- common/core/task.py +36 -0
- common/message/__init__.py +59 -0
- common/message/annotation.py +89 -0
- common/message/array.py +500 -0
- common/message/base.py +517 -0
- common/message/camera.py +91 -0
- common/message/color.py +139 -0
- common/message/frame.py +50 -0
- common/message/image.py +171 -0
- common/message/imu.py +14 -0
- common/message/joint.py +47 -0
- common/message/log.py +31 -0
- common/message/pir.py +16 -0
- common/message/point.py +109 -0
- common/message/point_cloud.py +63 -0
- common/message/pose.py +148 -0
- common/message/quaternion.py +273 -0
- common/message/radar.py +58 -0
- common/message/types.py +37 -0
- common/message/vector.py +786 -0
- common/rome/__init__.py +9 -0
- common/rome/client.py +430 -0
- common/rome/error.py +16 -0
- common/session/__init__.py +54 -0
- common/session/environment.py +31 -0
- common/session/sim.py +240 -0
- common/session/views/__init__.py +263 -0
- common/session/views/animation.py +73 -0
- common/session/views/articulation.py +184 -0
- common/session/views/basis_curve.py +102 -0
- common/session/views/camera.py +147 -0
- common/session/views/collision.py +59 -0
- common/session/views/geometry.py +102 -0
- common/session/views/ground_plane.py +41 -0
- common/session/views/imu.py +66 -0
- common/session/views/joint.py +81 -0
- common/session/views/light.py +96 -0
- common/session/views/pir_sensor.py +115 -0
- common/session/views/radar.py +82 -0
- common/session/views/rigid_body.py +236 -0
- common/session/views/viewport.py +21 -0
- common/session/views/xform.py +39 -0
- common/utils/__init__.py +4 -0
- common/utils/comms.py +571 -0
- common/utils/logger.py +123 -0
- common/utils/time.py +42 -0
- common/utils/usd.py +12 -0
common/utils/comms.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from threading import Event
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
import zenoh
|
|
9
|
+
|
|
10
|
+
from common.message import Message
|
|
11
|
+
|
|
12
|
+
DEFAULT_COMMS_SUBSCRIBER_RING_DEPTH = 4096
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound=Message)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommsSession:
|
|
18
|
+
"""
|
|
19
|
+
Lightweight, synchronous communication session built on top of Zenoh.
|
|
20
|
+
|
|
21
|
+
This session provides send/publish operations, subscriber creation, and
|
|
22
|
+
advanced features like queryables and direct publisher management.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""
|
|
27
|
+
Create a new communication session.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Get router endpoint from environment or use localhost default
|
|
31
|
+
router_endpoint = os.environ.get("ZENOH_ROUTER_ENDPOINT", "tcp/127.0.0.1:7447")
|
|
32
|
+
|
|
33
|
+
self.config = zenoh.Config()
|
|
34
|
+
self.config.insert_json5("mode", '"peer"')
|
|
35
|
+
self.config.insert_json5("listen/endpoints", '["tcp/0.0.0.0:0"]')
|
|
36
|
+
self.config.insert_json5("scouting/multicast/enabled", "false")
|
|
37
|
+
self.config.insert_json5("connect/endpoints", f'["{router_endpoint}"]') # Discovery via router
|
|
38
|
+
self.session = zenoh.open(self.config)
|
|
39
|
+
|
|
40
|
+
def __del__(self) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Ensure the session is closed when garbage collected.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
self.close()
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> CommsSession:
|
|
48
|
+
"""
|
|
49
|
+
Enter the context manager.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Exit the context manager and close the session.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
def get_session_info(self) -> dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Get information about the session.
|
|
64
|
+
|
|
65
|
+
:return: Information about the Zenoh session.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"zid": self.session.info.zid(),
|
|
70
|
+
"routers": self.session.info.routers_zid(),
|
|
71
|
+
"peers": self.session.info.peers_zid(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def declare_subscriber(self, path: str) -> CommsSubscriber:
|
|
75
|
+
"""
|
|
76
|
+
Create a new subscriber for the given path.
|
|
77
|
+
|
|
78
|
+
:param path: The path to subscribe to.
|
|
79
|
+
:return: A new CommsSubscriber instance.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
return CommsSubscriber(self.session, path)
|
|
83
|
+
|
|
84
|
+
def declare_async_subscriber(self, path: str) -> CommsAsyncSubscriber:
|
|
85
|
+
"""
|
|
86
|
+
Create a new async subscriber that uses callbacks for event-driven reception.
|
|
87
|
+
|
|
88
|
+
:param path: The path to subscribe to.
|
|
89
|
+
:return: A new CommsAsyncSubscriber instance.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
return CommsAsyncSubscriber(self.session, path)
|
|
93
|
+
|
|
94
|
+
def declare_callback_subscriber(
|
|
95
|
+
self,
|
|
96
|
+
path: str,
|
|
97
|
+
callback: Callable[[zenoh.Sample], None],
|
|
98
|
+
) -> zenoh.Subscriber[None]:
|
|
99
|
+
"""
|
|
100
|
+
Create a new callback subscriber for the given path.
|
|
101
|
+
|
|
102
|
+
:param path: The path to subscribe to.
|
|
103
|
+
:param callback: Callback function to handle incoming messages.
|
|
104
|
+
:return: A new zenoh.Subscriber instance.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
return self.session.declare_subscriber(path, callback)
|
|
108
|
+
|
|
109
|
+
def declare_queryable(
|
|
110
|
+
self,
|
|
111
|
+
path: str,
|
|
112
|
+
complete: bool = True,
|
|
113
|
+
) -> CommsQueryable:
|
|
114
|
+
"""
|
|
115
|
+
Create a synchronous queryable endpoint for request-response patterns.
|
|
116
|
+
|
|
117
|
+
Returns a queryable wrapper that provides a clean API for receiving queries.
|
|
118
|
+
|
|
119
|
+
:param path: The path pattern to respond to queries on.
|
|
120
|
+
:param complete: Whether this queryable provides complete information.
|
|
121
|
+
:return: A new CommsQueryable instance.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return CommsQueryable(self.session, path, complete)
|
|
125
|
+
|
|
126
|
+
def declare_callback_queryable(
|
|
127
|
+
self,
|
|
128
|
+
path: str,
|
|
129
|
+
callback: Callable[[zenoh.Query], None],
|
|
130
|
+
complete: bool = True,
|
|
131
|
+
) -> zenoh.Queryable[None]:
|
|
132
|
+
"""
|
|
133
|
+
Create a callback-based queryable for async request-response patterns.
|
|
134
|
+
|
|
135
|
+
The callback is invoked by Zenoh automatically for each query, allowing
|
|
136
|
+
asynchronous handling without manual recv() calls.
|
|
137
|
+
|
|
138
|
+
:param path: The path pattern to respond to queries on.
|
|
139
|
+
:param callback: Callback function that receives zenoh.Query objects directly.
|
|
140
|
+
:param complete: Whether this queryable provides complete information.
|
|
141
|
+
:return: A new zenoh.Queryable instance.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
return self.session.declare_queryable(path, callback, complete=complete)
|
|
145
|
+
|
|
146
|
+
def declare_publisher(self, path: str) -> CommsPublisher:
|
|
147
|
+
"""
|
|
148
|
+
Create a dedicated publisher for a path.
|
|
149
|
+
|
|
150
|
+
:param path: The path to publish to.
|
|
151
|
+
:return: A CommsPublisher instance.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
return CommsPublisher(self.session, path)
|
|
155
|
+
|
|
156
|
+
def query(
|
|
157
|
+
self,
|
|
158
|
+
path: str,
|
|
159
|
+
response_type: type[T],
|
|
160
|
+
request: Message | bytes | None = None,
|
|
161
|
+
target: zenoh.QueryTarget = zenoh.QueryTarget.ALL,
|
|
162
|
+
timeout: float = 10.0,
|
|
163
|
+
) -> T:
|
|
164
|
+
"""
|
|
165
|
+
Send a typed query and wait for the first response.
|
|
166
|
+
|
|
167
|
+
:param path: The path to query.
|
|
168
|
+
:param response_type: The expected message type class for the response.
|
|
169
|
+
:param request: Optional request message to send with the query.
|
|
170
|
+
:param target: The target of the query.
|
|
171
|
+
:param timeout: Maximum time to wait for a reply in seconds.
|
|
172
|
+
:return: The first successful response message.
|
|
173
|
+
:raises TimeoutError: When the query times out.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
payload = None
|
|
177
|
+
if request is not None:
|
|
178
|
+
if isinstance(request, Message):
|
|
179
|
+
request = request.pack()
|
|
180
|
+
payload = request
|
|
181
|
+
|
|
182
|
+
# Send query with payload
|
|
183
|
+
replies = self.session.get(
|
|
184
|
+
path,
|
|
185
|
+
target=target,
|
|
186
|
+
payload=payload,
|
|
187
|
+
timeout=timeout,
|
|
188
|
+
consolidation=zenoh.ConsolidationMode.NONE,
|
|
189
|
+
congestion_control=zenoh.CongestionControl.DROP,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Get first valid response
|
|
193
|
+
for reply in replies:
|
|
194
|
+
result = reply.result
|
|
195
|
+
if isinstance(result, zenoh.ReplyError):
|
|
196
|
+
error = result.payload.to_string()
|
|
197
|
+
if "timeout" in error.lower():
|
|
198
|
+
raise TimeoutError(f"Query {path} timed out")
|
|
199
|
+
raise RuntimeError(f"Query {path} failed: {error}")
|
|
200
|
+
return response_type.unpack(result.payload.to_bytes())
|
|
201
|
+
|
|
202
|
+
# Raise error if no response was received
|
|
203
|
+
raise RuntimeError(f"No response received for query {path}")
|
|
204
|
+
|
|
205
|
+
def close(self) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Close the communication session.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if not self.session.is_closed():
|
|
211
|
+
self.session.close()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class CommsSubscriber:
|
|
215
|
+
"""
|
|
216
|
+
Communication subscriber for handling message reception.
|
|
217
|
+
|
|
218
|
+
Provides methods for receiving messages from a specific communication path.
|
|
219
|
+
Each subscriber operates independently with its own message buffer.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, session: zenoh.Session, path: str) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Initialize the subscriber.
|
|
225
|
+
|
|
226
|
+
:param session: The Zenoh session.
|
|
227
|
+
:param path: The path to subscribe to.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
handler = zenoh.handlers.RingChannel(DEFAULT_COMMS_SUBSCRIBER_RING_DEPTH)
|
|
231
|
+
self._subscriber = session.declare_subscriber(path, handler)
|
|
232
|
+
|
|
233
|
+
def recv(self, message_cls: type[T]) -> T:
|
|
234
|
+
"""
|
|
235
|
+
Receive the next message, blocking until one is available.
|
|
236
|
+
|
|
237
|
+
:param message_cls: The expected message type class.
|
|
238
|
+
:return: The deserialized message.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
sample = self._subscriber.handler.recv()
|
|
242
|
+
data = sample.payload.to_bytes()
|
|
243
|
+
return message_cls.unpack(data)
|
|
244
|
+
|
|
245
|
+
def recv_latest(self, message_cls: type[T]) -> T:
|
|
246
|
+
"""
|
|
247
|
+
Drain all messages and return the latest one, blocking if none available.
|
|
248
|
+
|
|
249
|
+
This method prevents the "drain bug" by ensuring we always get the most
|
|
250
|
+
recent message, discarding any stale messages that may have accumulated.
|
|
251
|
+
|
|
252
|
+
:param message_cls: The expected message type class.
|
|
253
|
+
:return: The latest deserialized message.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
latest_sample = None
|
|
257
|
+
|
|
258
|
+
# First, drain all available messages without blocking
|
|
259
|
+
# We don't deserialize here - just keep the raw samples
|
|
260
|
+
while True:
|
|
261
|
+
sample = self._subscriber.handler.try_recv()
|
|
262
|
+
if sample is None:
|
|
263
|
+
break
|
|
264
|
+
latest_sample = sample
|
|
265
|
+
|
|
266
|
+
# If we have a latest sample, deserialize and return it
|
|
267
|
+
if latest_sample is not None:
|
|
268
|
+
data = latest_sample.payload.to_bytes()
|
|
269
|
+
return message_cls.unpack(data)
|
|
270
|
+
|
|
271
|
+
# Otherwise, wait for a new message
|
|
272
|
+
return self.recv(message_cls)
|
|
273
|
+
|
|
274
|
+
def try_recv(self, message_cls: type[T]) -> T | None:
|
|
275
|
+
"""
|
|
276
|
+
Try to receive a message without blocking.
|
|
277
|
+
|
|
278
|
+
:param message_cls: The expected message type class.
|
|
279
|
+
:return: The deserialized message if available, None otherwise.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
sample = self._subscriber.handler.try_recv()
|
|
283
|
+
if sample is None:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
data = sample.payload.to_bytes()
|
|
287
|
+
return message_cls.unpack(data)
|
|
288
|
+
|
|
289
|
+
def try_recv_sample(self) -> zenoh.Sample | None:
|
|
290
|
+
"""
|
|
291
|
+
Try to receive a raw sample without blocking.
|
|
292
|
+
|
|
293
|
+
:return: The raw Zenoh sample if available, None otherwise.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
return self._subscriber.handler.try_recv()
|
|
297
|
+
|
|
298
|
+
def try_recv_latest(self, message_cls: type[T]) -> T | None:
|
|
299
|
+
"""
|
|
300
|
+
Try to drain all messages and return the latest one without blocking.
|
|
301
|
+
|
|
302
|
+
This is the non-blocking version of recv_latest. It prevents the "drain bug"
|
|
303
|
+
by ensuring we always get the most recent message if any are available.
|
|
304
|
+
|
|
305
|
+
:param message_cls: The expected message type class.
|
|
306
|
+
:return: The latest deserialized message if available, None otherwise.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
latest_sample = None
|
|
310
|
+
|
|
311
|
+
# Drain all available messages without blocking
|
|
312
|
+
# We don't deserialize here - just keep the raw samples
|
|
313
|
+
while True:
|
|
314
|
+
sample = self._subscriber.handler.try_recv()
|
|
315
|
+
if sample is None:
|
|
316
|
+
break
|
|
317
|
+
latest_sample = sample
|
|
318
|
+
|
|
319
|
+
# If we have a latest sample, deserialize and return it
|
|
320
|
+
if latest_sample is not None:
|
|
321
|
+
data = latest_sample.payload.to_bytes()
|
|
322
|
+
return message_cls.unpack(data)
|
|
323
|
+
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
def drain(self) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Drain all queued messages without processing them.
|
|
329
|
+
|
|
330
|
+
:return: True if any messages were drained, False otherwise.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
drained_any = False
|
|
334
|
+
while self._subscriber.handler.try_recv() is not None:
|
|
335
|
+
drained_any = True
|
|
336
|
+
|
|
337
|
+
return drained_any
|
|
338
|
+
|
|
339
|
+
def close(self) -> None:
|
|
340
|
+
"""
|
|
341
|
+
Close the subscriber and release resources.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
self._subscriber.undeclare()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class CommsAsyncSubscriber:
|
|
348
|
+
"""
|
|
349
|
+
Async subscriber that uses callbacks for event-driven message reception.
|
|
350
|
+
|
|
351
|
+
Provides blocking recv with timeout and automatic wake-up on message arrival.
|
|
352
|
+
Useful for event-driven loops that need to respond to both messages and shutdown events.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __init__(self, session: zenoh.Session, path: str) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Initialize the async subscriber.
|
|
358
|
+
|
|
359
|
+
:param session: The Zenoh session.
|
|
360
|
+
:param path: The path to subscribe to.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
self._message_available = Event()
|
|
364
|
+
self._latest_sample: zenoh.Sample | None = None
|
|
365
|
+
self._subscriber = session.declare_subscriber(path, self._on_message)
|
|
366
|
+
|
|
367
|
+
def recv_timeout(self, message_cls: type[T], timeout: float | None = None) -> T | None:
|
|
368
|
+
"""
|
|
369
|
+
Receive a message with optional timeout.
|
|
370
|
+
|
|
371
|
+
Blocks until a message arrives or timeout expires. Uses event-driven
|
|
372
|
+
approach to wake up immediately when messages arrive.
|
|
373
|
+
|
|
374
|
+
:param message_cls: The expected message type class.
|
|
375
|
+
:param timeout: Timeout in seconds (None for infinite).
|
|
376
|
+
:return: The deserialized message if available, None on timeout.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
# Wait for message with timeout
|
|
380
|
+
if not self._message_available.wait(timeout):
|
|
381
|
+
return None
|
|
382
|
+
if self._latest_sample is None:
|
|
383
|
+
raise RuntimeError("No message available")
|
|
384
|
+
|
|
385
|
+
# Get the message and reset event
|
|
386
|
+
data = self._latest_sample.payload.to_bytes()
|
|
387
|
+
self._latest_sample = None
|
|
388
|
+
self._message_available.clear()
|
|
389
|
+
return message_cls.unpack(data)
|
|
390
|
+
|
|
391
|
+
def close(self) -> None:
|
|
392
|
+
"""
|
|
393
|
+
Close the subscriber and release resources.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
self._subscriber.undeclare()
|
|
397
|
+
|
|
398
|
+
def _on_message(self, sample: zenoh.Sample) -> None:
|
|
399
|
+
"""
|
|
400
|
+
Callback invoked when a message arrives.
|
|
401
|
+
|
|
402
|
+
:param sample: The received sample.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
self._latest_sample = sample
|
|
406
|
+
self._message_available.set()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class CommsPublisher:
|
|
410
|
+
"""
|
|
411
|
+
Dedicated publisher for efficient message publication to a single path.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
def __init__(self, session: zenoh.Session, path: str) -> None:
|
|
415
|
+
"""
|
|
416
|
+
Initialize the publisher.
|
|
417
|
+
|
|
418
|
+
:param session: The Zenoh session.
|
|
419
|
+
:param path: The path to publish to.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
self._publisher = session.declare_publisher(
|
|
423
|
+
path,
|
|
424
|
+
congestion_control=zenoh.CongestionControl.DROP,
|
|
425
|
+
reliability=zenoh.Reliability.BEST_EFFORT,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def publish(self, data: bytes | Message) -> None:
|
|
429
|
+
"""
|
|
430
|
+
Publish data to the publisher's path.
|
|
431
|
+
|
|
432
|
+
:param data: Raw bytes or a Message that will be serialized.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
if isinstance(data, Message):
|
|
436
|
+
data = data.pack()
|
|
437
|
+
self._publisher.put(data)
|
|
438
|
+
|
|
439
|
+
def close(self) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Close the publisher and release resources.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
self._publisher.undeclare()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class Query:
|
|
448
|
+
"""
|
|
449
|
+
Communication query for handling response to a query request.
|
|
450
|
+
|
|
451
|
+
Important: make sure you call drop() (or use the context manager) to make sure
|
|
452
|
+
the query is finalized!
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def __init__(self, query: zenoh.Query) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Initialize the query.
|
|
458
|
+
|
|
459
|
+
:param query: The underlying Zenoh query.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
self._query = query
|
|
463
|
+
|
|
464
|
+
def __enter__(self) -> Query:
|
|
465
|
+
"""
|
|
466
|
+
Enter the context manager.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
self._query.__enter__()
|
|
470
|
+
return self
|
|
471
|
+
|
|
472
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
473
|
+
"""
|
|
474
|
+
Exit the context manager and drop the query.
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
self._query.__exit__(exc_type, exc_val, exc_tb)
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def key_expr(self) -> str:
|
|
481
|
+
"""
|
|
482
|
+
Get the key expression of the query.
|
|
483
|
+
|
|
484
|
+
:return: The key expression.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
return str(self._query.key_expr)
|
|
488
|
+
|
|
489
|
+
def payload(self, message_cls: type[T]) -> T:
|
|
490
|
+
"""
|
|
491
|
+
Get the payload of the query.
|
|
492
|
+
|
|
493
|
+
:param message_cls: The message type to unpack the payload into.
|
|
494
|
+
:return: The unpacked message.
|
|
495
|
+
:raises RuntimeError: If the query has no payload.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
if self._query.payload is None:
|
|
499
|
+
raise RuntimeError("Query has no payload")
|
|
500
|
+
return message_cls.unpack(self._query.payload.to_bytes())
|
|
501
|
+
|
|
502
|
+
def reply(self, data: Message | bytes) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Send a reply to the query.
|
|
505
|
+
|
|
506
|
+
:param data: Response data. Raw bytes or a Message that will be serialized.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
if isinstance(data, Message):
|
|
510
|
+
data = data.pack()
|
|
511
|
+
self._query.reply(
|
|
512
|
+
self._query.key_expr,
|
|
513
|
+
payload=data,
|
|
514
|
+
congestion_control=zenoh.CongestionControl.DROP,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def drop(self) -> None:
|
|
518
|
+
"""
|
|
519
|
+
Drop the query.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
self._query.drop()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class CommsQueryable:
|
|
526
|
+
"""
|
|
527
|
+
Lightweight wrapper around a Zenoh queryable for request-response patterns.
|
|
528
|
+
|
|
529
|
+
Provides a clean API for receiving and processing queries without manual unwrapping.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
def __init__(self, session: zenoh.Session, path: str, complete: bool = True) -> None:
|
|
533
|
+
"""
|
|
534
|
+
Initialize the queryable wrapper.
|
|
535
|
+
|
|
536
|
+
:param session: The Zenoh session.
|
|
537
|
+
:param path: The path pattern to respond to queries on.
|
|
538
|
+
:param complete: Whether this queryable provides complete information.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
handler = zenoh.handlers.RingChannel(DEFAULT_COMMS_SUBSCRIBER_RING_DEPTH)
|
|
542
|
+
self._queryable = session.declare_queryable(path, handler, complete=complete)
|
|
543
|
+
|
|
544
|
+
def recv(self) -> Query:
|
|
545
|
+
"""
|
|
546
|
+
Receive the next query, blocking until one is available.
|
|
547
|
+
|
|
548
|
+
:return: The query wrapped in our Query helper.
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
zenoh_query = self._queryable.handler.recv()
|
|
552
|
+
return Query(zenoh_query)
|
|
553
|
+
|
|
554
|
+
def try_recv(self) -> Query | None:
|
|
555
|
+
"""
|
|
556
|
+
Try to receive a query without blocking.
|
|
557
|
+
|
|
558
|
+
:return: The query wrapped in our Query helper, or None if no query available.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
zenoh_query = self._queryable.handler.try_recv()
|
|
562
|
+
if zenoh_query is None:
|
|
563
|
+
return None
|
|
564
|
+
return Query(zenoh_query)
|
|
565
|
+
|
|
566
|
+
def close(self) -> None:
|
|
567
|
+
"""
|
|
568
|
+
Close the queryable and release resources.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
self._queryable.undeclare()
|
common/utils/logger.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from common.message import Log, LogLevel, Message
|
|
2
|
+
from common.utils.comms import CommsSession
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Logger:
|
|
6
|
+
"""
|
|
7
|
+
Logger that publishes structured logs to the communication system.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
comms: CommsSession,
|
|
13
|
+
base_channel: str | None = None,
|
|
14
|
+
debug: bool = False,
|
|
15
|
+
print_logs: bool = False,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the logger.
|
|
19
|
+
|
|
20
|
+
:param comms: Comms session.
|
|
21
|
+
:param base_channel: Optional base channel for logs and telemetry.
|
|
22
|
+
:param debug: Whether to run in debug mode.
|
|
23
|
+
:param print_logs: Whether to print logs to stdout.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
self._log_publisher = comms.declare_publisher("_logs")
|
|
27
|
+
self._base_channel = base_channel
|
|
28
|
+
self._debug = debug
|
|
29
|
+
self._print_logs = print_logs
|
|
30
|
+
self._let_us: int = 0
|
|
31
|
+
|
|
32
|
+
def set_let(self, let_us: int) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Set the logical execution time.
|
|
35
|
+
|
|
36
|
+
:param let_us: Logical execution time in microseconds.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
self._let_us = let_us
|
|
40
|
+
|
|
41
|
+
def debug(self, message: str) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Log a debug message.
|
|
44
|
+
|
|
45
|
+
:param message: Log message.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
if self._debug:
|
|
49
|
+
self._log(LogLevel.DEBUG, message)
|
|
50
|
+
|
|
51
|
+
def info(self, message: str) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Log an info message.
|
|
54
|
+
|
|
55
|
+
:param message: Log message.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
self._log(LogLevel.INFO, message)
|
|
59
|
+
|
|
60
|
+
def warning(self, message: str) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Log a warning message.
|
|
63
|
+
|
|
64
|
+
:param message: Log message.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
self._log(LogLevel.WARNING, message)
|
|
68
|
+
|
|
69
|
+
def error(self, message: str) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Log an error message.
|
|
72
|
+
|
|
73
|
+
:param message: Log message.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
self._log(LogLevel.ERROR, message)
|
|
77
|
+
|
|
78
|
+
def telemetry(self, channel: str, telemetry: Message | dict) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Record telemetry data from a Message or a JSON-serializable dictionary.
|
|
81
|
+
|
|
82
|
+
:param channel: Telemetry channel (alphanumeric, underscore, hyphen, period, slash).
|
|
83
|
+
:param telemetry: The message or dict to record.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if not all(c.isalnum() or c in ("_", "-", ".", "/") for c in channel):
|
|
87
|
+
raise ValueError(f"Invalid channel format: {channel}")
|
|
88
|
+
|
|
89
|
+
# Pack telemetry data based on type
|
|
90
|
+
if isinstance(telemetry, Message):
|
|
91
|
+
packed_data = telemetry.pack()
|
|
92
|
+
elif isinstance(telemetry, dict):
|
|
93
|
+
packed_data = Message.pack_json(telemetry)
|
|
94
|
+
|
|
95
|
+
self._log_publisher.publish(
|
|
96
|
+
Log(
|
|
97
|
+
level=LogLevel.INFO,
|
|
98
|
+
message=None,
|
|
99
|
+
channel=f"{self._base_channel}/{channel}" if self._base_channel else channel,
|
|
100
|
+
let_us=self._let_us,
|
|
101
|
+
telemetry=packed_data,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _log(self, level: LogLevel, message: str) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Send a log message through Zenoh.
|
|
108
|
+
|
|
109
|
+
:param level: The log level.
|
|
110
|
+
:param message: The log message.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
self._log_publisher.publish(
|
|
114
|
+
Log(
|
|
115
|
+
level=level,
|
|
116
|
+
message=message,
|
|
117
|
+
channel=f"{self._base_channel}/logs" if self._base_channel else None,
|
|
118
|
+
let_us=self._let_us,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if self._print_logs:
|
|
123
|
+
print(f"[{level.value.upper()}] {message}")
|