-
-
Notifications
You must be signed in to change notification settings - Fork 109
Expand file tree
/
Copy path__init__.py
More file actions
302 lines (213 loc) · 12.1 KB
/
Copy path__init__.py
File metadata and controls
302 lines (213 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
"""
[Cyphal](https://opencyphal.org) in Python —
decentralized real-time pub/sub with tunable reliability, service discovery, and zero configuration.
Works anywhere, [including baremetal MCUs](https://github.com/OpenCyphal-Garage/cy).
Supports various transports such as Ethernet (UDP) and CAN FD with optional redundancy.
# Installation
Optional features inside the brackets can be removed if not needed; see `pyproject.toml` for the full list:
```
pip install 'pycyphal2[udp,pythoncan]'
```
# Usage
Set up a transport, make a node, publish and subscribe:
```python
import asyncio
from pycyphal2 import Node, Instant
from pycyphal2.udp import UDPTransport
async def main():
node = Node.new(UDPTransport.new(), "my_node")
pub = node.advertise("sensor/temperature")
await pub(Instant.now() + 1.0, b"21.5")
sub = node.subscribe("sensor/temperature")
async for arrival in sub:
print(arrival.message)
if __name__ == "__main__":
asyncio.run(main())
```
All public symbols live at the top level — just `import pycyphal2`.
Transport modules (`pycyphal2.udp`, `pycyphal2.can`) are imported separately
so that only the needed dependencies are pulled in.
## Name resolution
The topic naming system shares many similarities with [ROS Names](https://wiki.ros.org/Names).
Name resolution is the process by which a topic name passed to `node.advertise()` or `node.subscribe()` is resolved to a topic name as used on the Cyphal network.
There exist 4 kinds of topic names in Cyphal:
<details markdown="1">
<summary>Relative Name</summary>
A relative name is a name that does not start with `/` or `~/`.
```
sensor/temperature
cmd_vel
camera/image_raw
```
Its resolved name is prefixed with the node namespace.
| Input name | Namespace | Home | Resolved name |
| ----------------- | --------- | ---- | --------------------- |
| `foo` | `ns` | `me` | `ns/foo` |
| `foo/bar` | `ns` | `me` | `ns/foo/bar` |
*Use case:* Use relative names for topics that are specific to a node, but might be reused across multiple nodes.
*Example:* A robot contains 4 motor controllers of the same type, each has its own namespace (`motor_1`, `motor_2`, `motor_3`, `motor_4`).
Using relative names, the application code is the same for all 4 motor controllers, however the topics are resolved differently based on the node namespace.
| Input name | Namespace | Home | Resolved name |
| ----------------- | --------- | ------- | ---------------------------- |
| `speed` | `motor_1` | `robot` | `motor_1/speed` |
| `speed` | `motor_2` | `robot` | `motor_2/speed` |
| `speed` | `motor_3` | `robot` | `motor_3/speed` |
| `speed` | `motor_4` | `robot` | `motor_4/speed` |
</details>
<details markdown="1">
<summary>Absolute Name</summary>
An absolute name starts with `/`.
```
/temperature
/diagnostics/status
```
Its resolved name is simply the same as the input name, ignoring both the node namespace and home.
| Input name | Namespace | Home | Resolved name |
| ----------------- | --------- | ---- | ------------------ |
| `foo` | `ns` | `me` | `foo` |
| `foo/bar` | `ns` | `me` | `foo/bar` |
Use case: Use absolute names for topics that are *not* specific to a node and might be reused across multiple nodes.
Example: Shared system topics like `/log`, since multiple nodes may publish to the same topic.
Conversely, topics like `/battery_voltage` that might be sourced from multiple nodes but need one single source of truth for other nodes like the motor controllers to subscribe to.
| Input name | Namespace | Home | Resolved name |
| --------------------- | --------------- | ------- | ------------------ |
| `/log` | `cpu` | `robot` | `/log` |
| `/battery_voltage` | `battery_1` | `robot` | `/battery_voltage` |
</details>
<details markdown="1">
<summary>Homeful Name</summary>
A homeful name starts with `~` of `~/`.
```
~/config
```
Its resolved name consists of the home and the input name, with the node namespace ignored. Note that `~foo` is not homeful and resolves as relative name to `ns/~foo` (this is confusing so don't use this).
Proposal: both `~` and `~foo` should not be allowed.
| Input name | Namespace | Home | Resolved name |
| --------------------- | --------------- | ------- | ------------------ |
| `~` | `ns` | `me` | `me` |
| `~/config` | `ns` | `me` | `me/config` |
| `~config` | `ns` | `me` | `ns/~config` |
Use case: For topics tied to specific nodes. The most common use case is configuring a node's parameters or settings.
Example: We want to configure an antenna to transmit at a specific frequency. The antenna node has a `~/config/frequency` topic that we can publish to.
</details>
<details markdown="1">
<summary>Pattern Name (only for subscribing)</summary>
A pattern name contains wildcard `*` (matches any _single_ name segment)
```
*/speed # matches any topic under `speed` (e.g. `motor_1/speed`, `motor_2/speed/`, `motor_3/value`, ...)
```
or `>` (matches _zero or more_ trailing segments)
```
sensor/> # matches any topic under `sensor` (e.g. `sensor/`, `sensor/temperature`, `sensor/pressure/`, ...)
```
Use case: Use `*` to subscribe to a _specific_ topic coming from a undetermined number of nodes.
Use `>` to subscribe to _multiple_ topics under a given namespace.
Example: `*/battery_pct` to subscribe to all nodes publishing battery data, of which there may be multiple per vehicle.
'logs/>' to subscribe to all topics publishing under '/logs' which may contain 'log_info', 'log_warning', 'log_error' topics.
</details>
Used effectively they allow to split up complex systems into smaller sub-systems simplifying development and debugging.
### Extra functions
*Topping* is the process by which a unique subject ID is assigned upon initialization.
For some applications that require a high level of reliability, determinism is required and can be achieved by using `#` to pin a topic to a specific subject ID.
```
motor/speed#1234
```
The resolved topic name is 'motor/speed' and the subject ID is fixed to 1234.
*Remapping* lets a node replace one name with another before final resolution.
This can be useful when trying to match the expected topic name from one node to another (when integrating multiple subsystems).
```
node.remap({"sensor/temperature": "temp"})
```
Now any topic name matching `sensor/temperature` will be remapped to `temp` before final resolution.
A valid name contains printable ASCII characters except space (ASCII codes [33, 126]).
Normalized names do not have leading or trailing segment separators `/` and do not have consecutive separators.
Every node should have a unique name, which is called its *home*; home substitution is done via `~/`.
| Input name | Namespace | Home | Remap | Resolved name | Note |
| ----------------- | --------- | ---- | ------------------ | --------------------- | -------------------------------- |
| `foo/bar` | `ns` | `me` | | `ns/foo/bar` | Relative name |
| `/foo//bar/` | `ns` | `me` | | `foo/bar` | Absolute name; namespace ignored |
| `~/foo/bar` | `ns` | `me` | | `me/foo/bar` | Homeful name |
| `sensor/*/temp` | `diag` | `me` | | `diag/sensor/*/temp` | Pattern with `*` |
| `/sensor/>` | `diag` | `me` | | `sensor/>` | Pattern with trailing `>` |
| `foo/bar` | `ns` | `me` | `foo/bar=~/zoo` | `me/zoo` | Remap first, then resolve |
Only exact `~` or `~/...` is homeful; `~ns` is literal. A matching remap overrides pinning.
Pins are allowed only on verbatim names, not on patterns.
Environment variables that control name remapping:
- `CYPHAL_NAMESPACE` — default namespace prepended to relative topic names.
- `CYPHAL_REMAP` — topic name remappings (`from=to` pairs, whitespace-separated).
See also :meth:`Node.remap`.
## Publish
Publication is best-effort by default. Pass `reliable=True` when publishing to retry delivery until
acknowledged by every known subscriber or until the deadline; if the remote side does not acknowledge in time,
:class:`DeliveryError` is raised.
```python
pub = node.advertise("sensor/temperature")
await pub(Instant.now() + 1.0, b"payload", reliable=True)
```
## Subscribe
Subscriptions normally yield messages as soon as they arrive. Set `reordering_window` [seconds] on
:meth:`Node.subscribe` to allow delaying out-of-order messages to reconstruct the original publication order.
This is useful for sensor feeds and state estimators.
```python
sub = node.subscribe("sensor/temperature", reordering_window=0.1)
```
Pattern matching is supported: use `*` to match one name segment (e.g., `sensor/*/temperature`)
and a trailing `>` to match zero or more trailing segments (e.g., `sensor/>`).
Pattern subscribers automatically join matching topics as they appear, and unsubscribe as they disappear.
```python
sub = node.subscribe("sensor/*/temperature")
async for arrival in sub:
topic = arrival.breadcrumb.topic
captures = sub.substitutions(topic)
print(topic.name, captures) # [('engine', 1)], where 1 is the pattern segment index
```
## RPC & streaming
RPC is layered directly on top of pub/sub. Use :meth:`Publisher.request` to publish a message that expects
responses, and use :attr:`Arrival.breadcrumb` on the subscriber side to send a unicast reply back to the requester.
One request may yield responses from multiple subscribers.
```python
stream = await pub.request(Instant.now() + 1.0, 0.5, b"read")
async for response in stream:
print(response.message)
```
Streaming is just repeated replying on the same breadcrumb. The requester consumes such replies through
:class:`ResponseStream`; each responder numbers its own responses from zero upward.
```python
await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-1", reliable=True)
await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-2", reliable=True)
```
## Topic pinning
Topics may be pinned to a specific subject-ID using `name#1234` to bypass automatic assignment.
This is useful for applications where a high degree of determinism is required and for Cyphal/CAN v1.0 interoperability.
Pattern names (e.g., `sensor/*/temperature/>`) cannot be pinned.
To join a Cyphal/CAN v1.0 subject, use topic name of the form `subject_id#subject_id`; e.g., `7509#7509`.
```python
pub = node.advertise("motor/status#1234")
sub = node.subscribe("1234#1234")
```
Old Cyphal/CAN v1.0 nodes do not participate in the topic discovery protocol,
so topics joined only by such nodes are not discoverable by pattern subscribers.
# Remarks
Cyphal does not define a serialization format. Previous versions used to define the DSDL format but it has been
extracted into an independent project, and Cyphal was made serialization-agnostic in v1.1+.
PyCyphal v2 is published on PyPI as [`pycyphal2`](https://pypi.org/project/pycyphal2/)
to enable coexistence with the original [`pycyphal` v1](https://pypi.org/project/pycyphal/)
in the same Python environment.
The two packages have radically different APIs but are wire-compatible on Cyphal/CAN.
The maintenance of the original `pycyphal` package will eventually cease;
existing applications leveraging `pycyphal` should upgrade to the new API of `pycyphal2`.
"""
from __future__ import annotations
from ._api import *
from ._transport import SubjectWriter as SubjectWriter
from ._transport import Transport as Transport
from ._transport import TransportArrival as TransportArrival
__version__ = "2.0.0.dev2"
# pdoc needs __all__ to display re-exported members.
__all__ = [
_k
for _k, _v in vars().items()
if not _k.startswith("_")
and _k not in {"annotations", "TYPE_CHECKING"}
and (getattr(_v, "__module__", None) or "").startswith(__name__)
]