pycyphal2 2.0.0.dev0__py3-none-any.whl → 2.0.0.dev1__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.
- pycyphal2/__init__.py +81 -7
- pycyphal2/_api.py +15 -1
- pycyphal2/_node.py +47 -19
- pycyphal2/can/__init__.py +1 -1
- {pycyphal2-2.0.0.dev0.dist-info → pycyphal2-2.0.0.dev1.dist-info}/METADATA +6 -7
- {pycyphal2-2.0.0.dev0.dist-info → pycyphal2-2.0.0.dev1.dist-info}/RECORD +9 -9
- {pycyphal2-2.0.0.dev0.dist-info → pycyphal2-2.0.0.dev1.dist-info}/WHEEL +0 -0
- {pycyphal2-2.0.0.dev0.dist-info → pycyphal2-2.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {pycyphal2-2.0.0.dev0.dist-info → pycyphal2-2.0.0.dev1.dist-info}/top_level.txt +0 -0
pycyphal2/__init__.py
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
[Cyphal](https://opencyphal.org) in Python —
|
|
3
3
|
decentralized real-time pub/sub with tunable reliability, service discovery, and zero configuration.
|
|
4
|
-
Works anywhere,
|
|
4
|
+
Works anywhere, [including baremetal MCUs](https://github.com/OpenCyphal-Garage/cy).
|
|
5
5
|
|
|
6
6
|
Supports various transports such as Ethernet (UDP) and CAN FD with optional redundancy.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Optional features inside the brackets can be removed if not needed; see `pyproject.toml` for the full list:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install pycyphal2[udp,pythoncan]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
7
18
|
Set up a transport, make a node, publish and subscribe:
|
|
8
19
|
|
|
9
20
|
```python
|
|
@@ -25,22 +36,46 @@ All public symbols live at the top level — just `import pycyphal2`.
|
|
|
25
36
|
Transport modules (`pycyphal2.udp`, `pycyphal2.can`) are imported separately
|
|
26
37
|
so that only the needed dependencies are pulled in.
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
### Name resolution
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
The topic naming system shares many similarities with ROS.
|
|
42
|
+
A valid name contains printable ASCII characters except space (ASCII codes [33, 126]).
|
|
43
|
+
Normalized names do not have leading or trailing segment separators `/` and do not have consecutive separators.
|
|
44
|
+
Every node should have a unique name, which is called its *home*; home substitution is done via `~/`.
|
|
45
|
+
|
|
46
|
+
| Input name | Namespace | Home | Remap | Resolved name | Note |
|
|
47
|
+
| ----------------- | --------- | ---- | ------------------ | --------------------- | -------------------------------- |
|
|
48
|
+
| `foo/bar` | `ns` | `me` | | `ns/foo/bar` | Relative name |
|
|
49
|
+
| `/foo//bar/` | `ns` | `me` | | `foo/bar` | Absolute name; namespace ignored |
|
|
50
|
+
| `~/foo/bar` | `ns` | `me` | | `me/foo/bar` | Homeful name |
|
|
51
|
+
| `sensor/*/temp` | `diag` | `me` | | `diag/sensor/*/temp` | Pattern with `*` |
|
|
52
|
+
| `/sensor/>` | `diag` | `me` | | `sensor/>` | Pattern with trailing `>` |
|
|
53
|
+
| `foo/bar` | `ns` | `me` | `foo/bar=~/zoo` | `me/zoo` | Remap first, then resolve |
|
|
54
|
+
|
|
55
|
+
Only exact `~` or `~/...` is homeful; `~ns` is literal. A matching remap overrides pinning.
|
|
56
|
+
Pins are allowed only on verbatim names, not on patterns.
|
|
57
|
+
|
|
58
|
+
Environment variables that control name remapping:
|
|
31
59
|
|
|
32
60
|
- `CYPHAL_NAMESPACE` — default namespace prepended to relative topic names.
|
|
33
61
|
- `CYPHAL_REMAP` — topic name remappings (`from=to` pairs, whitespace-separated).
|
|
34
62
|
|
|
35
|
-
|
|
63
|
+
See also :meth:`Node.remap`.
|
|
64
|
+
|
|
65
|
+
### Publish
|
|
66
|
+
|
|
67
|
+
Publication is best-effort by default. Pass `reliable=True` when publishing to retry delivery until
|
|
36
68
|
acknowledged by every known subscriber or until the deadline; if the remote side does not acknowledge in time,
|
|
37
69
|
:class:`DeliveryError` is raised.
|
|
38
70
|
|
|
39
71
|
```python
|
|
72
|
+
pub = node.advertise("sensor/temperature")
|
|
40
73
|
await pub(Instant.now() + 1.0, b"payload", reliable=True)
|
|
41
74
|
```
|
|
42
75
|
|
|
43
|
-
|
|
76
|
+
### Subscribe
|
|
77
|
+
|
|
78
|
+
Subscriptions normally yield messages as soon as they arrive. Set `reordering_window` [seconds] on
|
|
44
79
|
:meth:`Node.subscribe` to allow delaying out-of-order messages to reconstruct the original publication order.
|
|
45
80
|
This is useful for sensor feeds and state estimators.
|
|
46
81
|
|
|
@@ -48,6 +83,20 @@ This is useful for sensor feeds and state estimators.
|
|
|
48
83
|
sub = node.subscribe("sensor/temperature", reordering_window=0.1)
|
|
49
84
|
```
|
|
50
85
|
|
|
86
|
+
Pattern matching is supported: use `*` to match one name segment (e.g., `sensor/*/temperature`)
|
|
87
|
+
and a trailing `>` to match zero or more trailing segments (e.g., `sensor/>`).
|
|
88
|
+
Pattern subscribers automatically join matching topics as they appear, and unsubscribe as they disappear.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
sub = node.subscribe("sensor/*/temperature")
|
|
92
|
+
async for arrival in sub:
|
|
93
|
+
topic = arrival.breadcrumb.topic
|
|
94
|
+
captures = sub.substitutions(topic)
|
|
95
|
+
print(topic.name, captures) # [('engine', 1)], where 1 is the pattern segment index
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### RPC & streaming
|
|
99
|
+
|
|
51
100
|
RPC is layered directly on top of pub/sub. Use :meth:`Publisher.request` to publish a message that expects
|
|
52
101
|
responses, and use :attr:`Arrival.breadcrumb` on the subscriber side to send a unicast reply back to the requester.
|
|
53
102
|
One request may yield responses from multiple subscribers.
|
|
@@ -66,8 +115,33 @@ await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-1", reliable=True)
|
|
|
66
115
|
await arrival.breadcrumb(Instant.now() + 1.0, b"chunk-2", reliable=True)
|
|
67
116
|
```
|
|
68
117
|
|
|
118
|
+
### Topic pinning
|
|
119
|
+
|
|
120
|
+
Topics may be pinned to a specific subject-ID using `name#1234` to bypass automatic assignment.
|
|
121
|
+
This is useful for applications where a high degree of determinism is required and for Cyphal/CAN v1.0 interoperability.
|
|
122
|
+
Pattern names (e.g., `sensor/*/temperature/>`) cannot be pinned.
|
|
123
|
+
|
|
124
|
+
To join a Cyphal/CAN v1.0 subject, use topic name of the form `subject_id#subject_id`; e.g., `7509#7509`.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
pub = node.advertise("motor/status#1234")
|
|
128
|
+
sub = node.subscribe("1234#1234")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Old Cyphal/CAN v1.0 nodes do not participate in the topic discovery protocol,
|
|
132
|
+
so topics joined only by such nodes are not discoverable by pattern subscribers.
|
|
133
|
+
|
|
134
|
+
## Remarks
|
|
135
|
+
|
|
69
136
|
Cyphal does not define a serialization format. Previous versions used to define the DSDL format but it has been
|
|
70
137
|
extracted into an independent project, and Cyphal was made serialization-agnostic in v1.1+.
|
|
138
|
+
|
|
139
|
+
PyCyphal v2 is published on PyPI as [`pycyphal2`](https://pypi.org/project/pycyphal2/)
|
|
140
|
+
to enable coexistence with the original [`pycyphal` v1](https://pypi.org/project/pycyphal/)
|
|
141
|
+
in the same Python environment.
|
|
142
|
+
The two packages have radically different APIs but are wire-compatible on Cyphal/CAN.
|
|
143
|
+
The maintenance of the original `pycyphal` package will eventually cease;
|
|
144
|
+
existing applications leveraging `pycyphal` should upgrade to the new API of `pycyphal2`.
|
|
71
145
|
"""
|
|
72
146
|
|
|
73
147
|
from __future__ import annotations
|
|
@@ -77,7 +151,7 @@ from ._transport import Transport as Transport
|
|
|
77
151
|
from ._transport import TransportArrival as TransportArrival
|
|
78
152
|
from ._transport import SubjectWriter as SubjectWriter
|
|
79
153
|
|
|
80
|
-
__version__ = "2.0.0.
|
|
154
|
+
__version__ = "2.0.0.dev1"
|
|
81
155
|
|
|
82
156
|
# pdoc needs __all__ to display re-exported members.
|
|
83
157
|
__all__ = [
|
pycyphal2/_api.py
CHANGED
|
@@ -131,7 +131,6 @@ class Closable(ABC):
|
|
|
131
131
|
class Topic(ABC):
|
|
132
132
|
"""
|
|
133
133
|
Topics are managed automatically by the library, created and destroyed as necessary.
|
|
134
|
-
This is just a compact view to expose some auxiliary information.
|
|
135
134
|
"""
|
|
136
135
|
|
|
137
136
|
@property
|
|
@@ -144,6 +143,16 @@ class Topic(ABC):
|
|
|
144
143
|
def name(self) -> str:
|
|
145
144
|
raise NotImplementedError
|
|
146
145
|
|
|
146
|
+
@property
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def evictions(self) -> int:
|
|
149
|
+
raise NotImplementedError
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
def subject_id(self, modulus: int) -> int:
|
|
153
|
+
"""The modulus can be obtained from :attr:`Transport.subject_id_modulus`."""
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
147
156
|
@abstractmethod
|
|
148
157
|
def match(self, pattern: str) -> list[tuple[str, int]] | None:
|
|
149
158
|
"""
|
|
@@ -475,6 +484,11 @@ class Node(Closable, ABC):
|
|
|
475
484
|
def namespace(self) -> str:
|
|
476
485
|
raise NotImplementedError
|
|
477
486
|
|
|
487
|
+
@property
|
|
488
|
+
@abstractmethod
|
|
489
|
+
def transport(self) -> Transport:
|
|
490
|
+
raise NotImplementedError
|
|
491
|
+
|
|
478
492
|
@abstractmethod
|
|
479
493
|
def remap(self, spec: str | dict[str, str]) -> None:
|
|
480
494
|
"""
|
pycyphal2/_node.py
CHANGED
|
@@ -306,6 +306,7 @@ class _TopicFlyweight(Topic):
|
|
|
306
306
|
|
|
307
307
|
_topic_hash: int
|
|
308
308
|
_name: str
|
|
309
|
+
_evictions: int
|
|
309
310
|
|
|
310
311
|
@property
|
|
311
312
|
def hash(self) -> int:
|
|
@@ -315,6 +316,13 @@ class _TopicFlyweight(Topic):
|
|
|
315
316
|
def name(self) -> str:
|
|
316
317
|
return self._name
|
|
317
318
|
|
|
319
|
+
@property
|
|
320
|
+
def evictions(self) -> int:
|
|
321
|
+
return self._evictions
|
|
322
|
+
|
|
323
|
+
def subject_id(self, modulus: int) -> int:
|
|
324
|
+
return compute_subject_id(self._topic_hash, self._evictions, modulus)
|
|
325
|
+
|
|
318
326
|
def match(self, pattern: str) -> list[tuple[str, int]] | None:
|
|
319
327
|
return match_pattern(pattern, self._name)
|
|
320
328
|
|
|
@@ -364,7 +372,7 @@ class TopicImpl(Topic):
|
|
|
364
372
|
self._node = node
|
|
365
373
|
self._name = name
|
|
366
374
|
self._topic_hash = rapidhash(name)
|
|
367
|
-
self.
|
|
375
|
+
self._evictions = evictions
|
|
368
376
|
self.ts_origin = now
|
|
369
377
|
self.ts_animated = now
|
|
370
378
|
self._pub_tag_baseline = int.from_bytes(os.urandom(8), "little")
|
|
@@ -392,14 +400,20 @@ class TopicImpl(Topic):
|
|
|
392
400
|
def name(self) -> str:
|
|
393
401
|
return self._name
|
|
394
402
|
|
|
403
|
+
@property
|
|
404
|
+
def evictions(self) -> int:
|
|
405
|
+
return self._evictions
|
|
406
|
+
|
|
407
|
+
def set_evictions(self, evictions: int) -> None:
|
|
408
|
+
self._evictions = evictions
|
|
409
|
+
|
|
410
|
+
def subject_id(self, modulus: int) -> int:
|
|
411
|
+
return compute_subject_id(self._topic_hash, self._evictions, modulus)
|
|
412
|
+
|
|
395
413
|
def match(self, pattern: str) -> list[tuple[str, int]] | None:
|
|
396
414
|
return match_pattern(pattern, self._name)
|
|
397
415
|
|
|
398
416
|
# -- Internal --
|
|
399
|
-
@property
|
|
400
|
-
def subject_id(self) -> int:
|
|
401
|
-
return compute_subject_id(self._topic_hash, self.evictions, self._node.transport.subject_id_modulus)
|
|
402
|
-
|
|
403
417
|
def lage(self, now: float) -> int:
|
|
404
418
|
return log_age(self.ts_origin, now)
|
|
405
419
|
|
|
@@ -426,14 +440,14 @@ class TopicImpl(Topic):
|
|
|
426
440
|
|
|
427
441
|
def ensure_writer(self) -> SubjectWriter:
|
|
428
442
|
if self.pub_writer is None:
|
|
429
|
-
sid = self.subject_id
|
|
443
|
+
sid = self.subject_id(self._node.transport.subject_id_modulus)
|
|
430
444
|
self.pub_writer = self._node.acquire_subject_writer(self, sid)
|
|
431
445
|
_logger.info("Writer acquired for '%s' sid=%d", self._name, sid)
|
|
432
446
|
return self.pub_writer
|
|
433
447
|
|
|
434
448
|
def ensure_listener(self) -> None:
|
|
435
449
|
if self.sub_listener is None and self.couplings:
|
|
436
|
-
sid = self.subject_id
|
|
450
|
+
sid = self.subject_id(self._node.transport.subject_id_modulus)
|
|
437
451
|
self.sub_listener = self._node.acquire_subject_listener(self, sid)
|
|
438
452
|
_logger.info("Listener acquired for '%s' sid=%d", self._name, sid)
|
|
439
453
|
|
|
@@ -441,12 +455,12 @@ class TopicImpl(Topic):
|
|
|
441
455
|
if self.couplings:
|
|
442
456
|
self.ensure_listener()
|
|
443
457
|
elif self.sub_listener is not None:
|
|
444
|
-
self._node.release_subject_listener(self, self.subject_id)
|
|
458
|
+
self._node.release_subject_listener(self, self.subject_id(self._node.transport.subject_id_modulus))
|
|
445
459
|
self.sub_listener = None
|
|
446
460
|
_logger.info("Listener released for '%s'", self._name)
|
|
447
461
|
|
|
448
462
|
def release_transport_handles(self) -> None:
|
|
449
|
-
sid = self.subject_id
|
|
463
|
+
sid = self.subject_id(self._node.transport.subject_id_modulus)
|
|
450
464
|
if self.pub_writer is not None:
|
|
451
465
|
self._node.release_subject_writer(self, sid)
|
|
452
466
|
self.pub_writer = None
|
|
@@ -487,7 +501,7 @@ def left_wins(l_lage: int, l_hash: int, r_lage: int, r_hash: int) -> bool:
|
|
|
487
501
|
|
|
488
502
|
class NodeImpl(Node):
|
|
489
503
|
def __init__(self, transport: Transport, *, home: str, namespace: str) -> None:
|
|
490
|
-
self.
|
|
504
|
+
self._transport = transport
|
|
491
505
|
self._home = home
|
|
492
506
|
self._namespace = namespace
|
|
493
507
|
self._remaps: dict[str, str] = {}
|
|
@@ -555,6 +569,10 @@ class NodeImpl(Node):
|
|
|
555
569
|
def namespace(self) -> str:
|
|
556
570
|
return self._namespace
|
|
557
571
|
|
|
572
|
+
@property
|
|
573
|
+
def transport(self) -> Transport:
|
|
574
|
+
return self._transport
|
|
575
|
+
|
|
558
576
|
def remap(self, spec: str | dict[str, str]) -> None:
|
|
559
577
|
if isinstance(spec, str):
|
|
560
578
|
spec = dict(x.split("=", 1) for x in spec.split() if "=" in x)
|
|
@@ -573,7 +591,12 @@ class NodeImpl(Node):
|
|
|
573
591
|
topic.pub_count += 1
|
|
574
592
|
topic.sync_implicit()
|
|
575
593
|
topic.ensure_writer()
|
|
576
|
-
_logger.info(
|
|
594
|
+
_logger.info(
|
|
595
|
+
"Advertise '%s' -> '%s' sid=%d",
|
|
596
|
+
name,
|
|
597
|
+
resolved,
|
|
598
|
+
topic.subject_id(self.transport.subject_id_modulus),
|
|
599
|
+
)
|
|
577
600
|
return PublisherImpl(self, topic)
|
|
578
601
|
|
|
579
602
|
def subscribe(self, name: str, *, reordering_window: float | None = None) -> Subscriber:
|
|
@@ -662,29 +685,34 @@ class NodeImpl(Node):
|
|
|
662
685
|
self.couple_topic_root(topic, root)
|
|
663
686
|
topic.sync_listener()
|
|
664
687
|
self.notify_implicit_gc()
|
|
665
|
-
_logger.info(
|
|
688
|
+
_logger.info(
|
|
689
|
+
"Topic created '%s' hash=%016x sid=%d",
|
|
690
|
+
name,
|
|
691
|
+
topic.hash,
|
|
692
|
+
topic.subject_id(self.transport.subject_id_modulus),
|
|
693
|
+
)
|
|
666
694
|
return topic
|
|
667
695
|
|
|
668
696
|
def topic_allocate(self, topic: TopicImpl, new_evictions: int, now: float) -> None:
|
|
669
697
|
"""Iterative subject-ID allocation with collision resolution. Mirrors topic_allocate() in cy.c."""
|
|
670
698
|
# Work queue: list of (topic, new_evictions) pairs to process.
|
|
699
|
+
modulus = self.transport.subject_id_modulus
|
|
671
700
|
work: list[tuple[TopicImpl, int]] = [(topic, new_evictions)]
|
|
672
701
|
while work:
|
|
673
702
|
t, ev = work.pop(0)
|
|
674
703
|
# Remove from subject-ID index first.
|
|
675
|
-
old_sid = t.subject_id
|
|
704
|
+
old_sid = t.subject_id(modulus)
|
|
676
705
|
if old_sid in self.topics_by_subject_id and self.topics_by_subject_id[old_sid] is t:
|
|
677
706
|
del self.topics_by_subject_id[old_sid]
|
|
678
707
|
|
|
679
708
|
if ev >= EVICTIONS_PINNED_MIN:
|
|
680
709
|
# Pinned topic: no collision detection, shared subject-IDs are fine.
|
|
681
710
|
t.release_transport_handles()
|
|
682
|
-
t.
|
|
711
|
+
t.set_evictions(ev)
|
|
683
712
|
t.sync_listener()
|
|
684
713
|
self.schedule_gossip_urgent(t)
|
|
685
714
|
continue
|
|
686
715
|
|
|
687
|
-
modulus = self.transport.subject_id_modulus
|
|
688
716
|
new_sid = compute_subject_id(t.hash, ev, modulus)
|
|
689
717
|
collider = self.topics_by_subject_id.get(new_sid)
|
|
690
718
|
|
|
@@ -694,14 +722,14 @@ class NodeImpl(Node):
|
|
|
694
722
|
if collider is None:
|
|
695
723
|
# No collision, install.
|
|
696
724
|
t.release_transport_handles()
|
|
697
|
-
t.
|
|
725
|
+
t.set_evictions(ev)
|
|
698
726
|
self.topics_by_subject_id[new_sid] = t
|
|
699
727
|
t.sync_listener()
|
|
700
728
|
self.schedule_gossip_urgent(t)
|
|
701
729
|
elif left_wins(t.lage(now), t.hash, collider.lage(now), collider.hash):
|
|
702
730
|
# Our topic wins: take the slot, evict the collider.
|
|
703
731
|
t.release_transport_handles()
|
|
704
|
-
t.
|
|
732
|
+
t.set_evictions(ev)
|
|
705
733
|
del self.topics_by_subject_id[new_sid]
|
|
706
734
|
self.topics_by_subject_id[new_sid] = t
|
|
707
735
|
if collider.pub_writer is not None:
|
|
@@ -1293,7 +1321,7 @@ class NodeImpl(Node):
|
|
|
1293
1321
|
self._notify_monitors(topic)
|
|
1294
1322
|
else:
|
|
1295
1323
|
self.on_gossip_unknown(hdr.topic_hash, hdr.topic_evictions, hdr.topic_log_age, ts)
|
|
1296
|
-
self._notify_monitors(_TopicFlyweight(hdr.topic_hash, name))
|
|
1324
|
+
self._notify_monitors(_TopicFlyweight(hdr.topic_hash, name, hdr.topic_evictions))
|
|
1297
1325
|
|
|
1298
1326
|
def on_gossip_known(
|
|
1299
1327
|
self,
|
|
@@ -1430,7 +1458,7 @@ class NodeImpl(Node):
|
|
|
1430
1458
|
self.decouple_topic_root(topic, topic.couplings[0].root, sync_lifecycle=False)
|
|
1431
1459
|
self.topics_by_name.pop(name, None)
|
|
1432
1460
|
self.topics_by_hash.pop(topic.hash, None)
|
|
1433
|
-
sid = topic.subject_id
|
|
1461
|
+
sid = topic.subject_id(self.transport.subject_id_modulus)
|
|
1434
1462
|
if self.topics_by_subject_id.get(sid) is topic:
|
|
1435
1463
|
del self.topics_by_subject_id[sid]
|
|
1436
1464
|
topic.associations.clear()
|
pycyphal2/can/__init__.py
CHANGED
|
@@ -12,7 +12,7 @@ transport = CANTransport.new(SocketCANInterface("can0"))
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Python-CAN is useful when the application runs not on GNU/Linux or already uses `python-can` or needs
|
|
15
|
-
|
|
15
|
+
[one of its *many* hardware backends](https://python-can.readthedocs.io/en/stable/interfaces.html)
|
|
16
16
|
-- GS-USB, SLCAN, PCAN, etc:
|
|
17
17
|
|
|
18
18
|
```python
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycyphal2
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev1
|
|
4
4
|
Summary: Pure-Python implementation of Cyphal -- a simple and robust real-time publish/subscribe stack that runs anywhere.
|
|
5
5
|
Author-email: Pavel Kirienko and OpenCyphal team <pavel@opencyphal.org>
|
|
6
6
|
License: MIT
|
|
@@ -38,6 +38,7 @@ _pub/sub without steroids_
|
|
|
38
38
|
|
|
39
39
|
[](https://opencyphal.org/)
|
|
40
40
|
[](https://forum.opencyphal.org)
|
|
41
|
+
[](https://pypi.org/project/pycyphal2/)
|
|
41
42
|
[](https://opencyphal.github.io/pycyphal)
|
|
42
43
|
|
|
43
44
|
</div>
|
|
@@ -46,12 +47,10 @@ _pub/sub without steroids_
|
|
|
46
47
|
|
|
47
48
|
Python implementation of the [Cyphal](https://opencyphal.org) stack that runs on GNU/Linux, Windows, and macOS.
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
pip install pycyphal2[udp,pythoncan]
|
|
54
|
-
```
|
|
50
|
+
PyCyphal v2 is published on PyPI as `pycyphal2` to enable coexistence with v1 `pycyphal` in the same Python environment.
|
|
51
|
+
The two packages have radically different APIs but are wire-compatible on Cyphal/CAN.
|
|
52
|
+
The maintenance of the original `pycyphal` package will eventually cease;
|
|
53
|
+
existing applications leveraging `pycyphal` should upgrade to the new API of `pycyphal2`.
|
|
55
54
|
|
|
56
55
|
📚 **Read the docs** at <https://opencyphal.github.io/pycyphal>.
|
|
57
56
|
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
pycyphal2/__init__.py,sha256=
|
|
2
|
-
pycyphal2/_api.py,sha256=
|
|
1
|
+
pycyphal2/__init__.py,sha256=nxnNW6FRdNG9SkboIR3MvzZMmWi4IWw_iOq_0dSuvuA,6780
|
|
2
|
+
pycyphal2/_api.py,sha256=fYtM9J0RmaEp30iSzebi6a0kLeINPRM0UvvAjevEV6E,22688
|
|
3
3
|
pycyphal2/_hash.py,sha256=qdQ3A35oFy5mxPNO873OlLntVsCuOak6ckJf8r5j254,10621
|
|
4
4
|
pycyphal2/_header.py,sha256=12r_jQQ1t3rrNG6si5C3-vNcUSkvYQCBo8YhG16g19Q,10448
|
|
5
|
-
pycyphal2/_node.py,sha256=
|
|
5
|
+
pycyphal2/_node.py,sha256=8MOV6GoROC55bczN8kcYi19IrdD4UF-U7mUaTAaPCJQ,57586
|
|
6
6
|
pycyphal2/_publisher.py,sha256=e8ydodDeLTqrgeNnBj4hjz2gWqkGgODmUGER9YgyY10,15868
|
|
7
7
|
pycyphal2/_subscriber.py,sha256=_BOeyjzSJel58GYRlfud0qpilFuaZEgBzBCGRFf4NtI,15721
|
|
8
8
|
pycyphal2/_transport.py,sha256=C0-hMukqJ_wUbNkXMBpMSp78Rf9FE7rcBfZpLDMkNF4,3492
|
|
9
9
|
pycyphal2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
pycyphal2/udp.py,sha256=xGrXQZQf-Tqo6em2rBxUTn373HYhaqb0B5nsf0b6SLQ,42389
|
|
11
|
-
pycyphal2/can/__init__.py,sha256=
|
|
11
|
+
pycyphal2/can/__init__.py,sha256=db9Miv0bKV591WiajAVijef2x7J5Y9-gu0G2mm7hHU4,1553
|
|
12
12
|
pycyphal2/can/_interface.py,sha256=u2YmS-fUkThE75IRKmF_VoA_YCTMM4ADkfMAi25cOQY,4369
|
|
13
13
|
pycyphal2/can/_reassembly.py,sha256=tDh0LubxANkOVIRCHG6zbABJEQD7WegxQS-UPqg__6E,5682
|
|
14
14
|
pycyphal2/can/_transport.py,sha256=sWNoyUnVJnUuhQ8pEtzAkfJSuqvI0RzMVVDKA5j1NOk,21036
|
|
15
15
|
pycyphal2/can/_wire.py,sha256=OSStXci7qzwnbz0YZ0H07rNU5bdi5fCLRNKVZmDOtdw,13696
|
|
16
16
|
pycyphal2/can/pythoncan.py,sha256=-zgFhoKDvihlRpVZg3OV12ClBTsf5MNLf1eD9_HxDus,10845
|
|
17
17
|
pycyphal2/can/socketcan.py,sha256=nBYzbAcoVXeGoRrovpJnCbTqs4MBabpt27QJw4JqUYc,8529
|
|
18
|
-
pycyphal2-2.0.0.
|
|
19
|
-
pycyphal2-2.0.0.
|
|
20
|
-
pycyphal2-2.0.0.
|
|
21
|
-
pycyphal2-2.0.0.
|
|
22
|
-
pycyphal2-2.0.0.
|
|
18
|
+
pycyphal2-2.0.0.dev1.dist-info/licenses/LICENSE,sha256=ILoAsB6eavnHqYkHMZt5JIp4-2oIMglw7vQr2aizB8w,1101
|
|
19
|
+
pycyphal2-2.0.0.dev1.dist-info/METADATA,sha256=QoXOiDmVMmg9p4swuXqlFSpeBSAApWNC_HmenAI7xok,2602
|
|
20
|
+
pycyphal2-2.0.0.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
pycyphal2-2.0.0.dev1.dist-info/top_level.txt,sha256=m3-ZKFH4OwPKxa94jzEP4OCKx7B7qnONx3GYXgaw8BU,10
|
|
22
|
+
pycyphal2-2.0.0.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|