pamoja-serial 0.1.18__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.
@@ -0,0 +1,13 @@
1
+ # Native extensions are built per platform, not committed.
2
+ *.so
3
+ *.pyd
4
+ *.dylib
5
+
6
+ # Build and packaging output.
7
+ dist/
8
+ wheels/
9
+ target/
10
+
11
+ # Local virtual environments.
12
+ .venv/
13
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Wiedman
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,101 @@
1
+ Metadata-Version: 2.5
2
+ Name: pamoja-serial
3
+ Version: 0.1.18
4
+ Summary: SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets.
5
+ Project-URL: Repository, https://github.com/molexxxx/pamoja
6
+ Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/serial.html
7
+ Author: molexxxx
8
+ License: MIT
9
+ License-File: LICENSE-MIT
10
+ Keywords: iot,pamoja,robotics,serial
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: pamoja-native==0.1.18
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pamoja-serial
20
+
21
+ SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
22
+
23
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/serial.html)
24
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
25
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html)
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pip install pamoja-serial
31
+ ```
32
+
33
+ ```python
34
+ from pamoja import serial
35
+ ```
36
+
37
+ This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
38
+
39
+ ## Example
40
+
41
+ The script the test suite runs, spliced here as it ran.
42
+
43
+ From [`bindings/python/guides/serial.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/serial.py):
44
+
45
+ ```python
46
+ from pamoja.serial import COBS_DELIMITER, SLIP_END, SLIP_ESC, SlipDecoder, cobs, slip
47
+
48
+ # A UART carries bytes, not packets, so a framing has to mark where one packet ends. SLIP
49
+ # reserves two byte values for that, and the package names both: the end byte closes a
50
+ # frame, the escape byte carries a value that would otherwise look like one.
51
+ payload = b"lvl=" + bytes([SLIP_END, SLIP_ESC])
52
+ framed = slip.encode(payload)
53
+ print(f"slip {len(payload)} payload bytes framed as {len(framed)}")
54
+
55
+ # Decoding gives the payload back unchanged, reserved bytes and all.
56
+ restored = slip.decode(framed)
57
+ print(f"slip decoded back to {len(restored)} bytes")
58
+
59
+ # COBS trades that escaping for one code byte per run of up to 254 non-zero bytes, each
60
+ # run led by its own length, so a frame never grows by more than a byte per 254. Zero is
61
+ # the delimiter, and never appears inside a frame.
62
+ packet = b"lvl=" + bytes([COBS_DELIMITER]) + b"7"
63
+ cobs_framed = cobs.encode(packet)
64
+ print(f"cobs {len(packet)} payload bytes framed as {len(cobs_framed)}")
65
+
66
+ # A read from a port returns whatever arrived, which is rarely one whole frame. This chunk
67
+ # holds two good frames with a truncated one between them; the decoder hands over the good
68
+ # ones and discards only the bad frame.
69
+ decoder = SlipDecoder()
70
+ chunk = (
71
+ b"ok"
72
+ + bytes([SLIP_END])
73
+ + bytes([SLIP_ESC]) # a frame that ends before its escape pair completes
74
+ + bytes([SLIP_END])
75
+ + b"go"
76
+ + bytes([SLIP_END])
77
+ )
78
+ frames = decoder.feed(chunk)
79
+ for frame in frames:
80
+ print(f"received {frame.decode()}")
81
+ print(f"discarded {decoder.discarded} frame the stream mangled")
82
+ ```
83
+
84
+ ## The same capability in every language
85
+
86
+ | Language | Package | Reference |
87
+ | --- | --- | --- |
88
+ | Rust | [`pamoja-serial`](https://crates.io/crates/pamoja-serial) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_serial/index.html), [docs.rs](https://docs.rs/pamoja-serial), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-serial) |
89
+ | TypeScript | [`@pamoja/serial`](https://www.npmjs.com/package/@pamoja/serial) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_serial.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-serial) |
90
+ | Python | [`pamoja-serial`](https://pypi.org/project/pamoja-serial/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-serial) |
91
+ | C# | [`Pamoja.Serial`](https://www.nuget.org/packages/Pamoja.Serial) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Serial.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-serial) |
92
+
93
+ ## Documentation
94
+
95
+ - [`pamoja.serial` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html), every class and function in this module.
96
+ - [The Serial framing guide](https://pamoja.molex.cloud/docs/guides/serial.html), with the same example in Rust, TypeScript, and C#.
97
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,83 @@
1
+ # pamoja-serial
2
+
3
+ SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
4
+
5
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/serial.html)
6
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
7
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pip install pamoja-serial
13
+ ```
14
+
15
+ ```python
16
+ from pamoja import serial
17
+ ```
18
+
19
+ This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
20
+
21
+ ## Example
22
+
23
+ The script the test suite runs, spliced here as it ran.
24
+
25
+ From [`bindings/python/guides/serial.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/serial.py):
26
+
27
+ ```python
28
+ from pamoja.serial import COBS_DELIMITER, SLIP_END, SLIP_ESC, SlipDecoder, cobs, slip
29
+
30
+ # A UART carries bytes, not packets, so a framing has to mark where one packet ends. SLIP
31
+ # reserves two byte values for that, and the package names both: the end byte closes a
32
+ # frame, the escape byte carries a value that would otherwise look like one.
33
+ payload = b"lvl=" + bytes([SLIP_END, SLIP_ESC])
34
+ framed = slip.encode(payload)
35
+ print(f"slip {len(payload)} payload bytes framed as {len(framed)}")
36
+
37
+ # Decoding gives the payload back unchanged, reserved bytes and all.
38
+ restored = slip.decode(framed)
39
+ print(f"slip decoded back to {len(restored)} bytes")
40
+
41
+ # COBS trades that escaping for one code byte per run of up to 254 non-zero bytes, each
42
+ # run led by its own length, so a frame never grows by more than a byte per 254. Zero is
43
+ # the delimiter, and never appears inside a frame.
44
+ packet = b"lvl=" + bytes([COBS_DELIMITER]) + b"7"
45
+ cobs_framed = cobs.encode(packet)
46
+ print(f"cobs {len(packet)} payload bytes framed as {len(cobs_framed)}")
47
+
48
+ # A read from a port returns whatever arrived, which is rarely one whole frame. This chunk
49
+ # holds two good frames with a truncated one between them; the decoder hands over the good
50
+ # ones and discards only the bad frame.
51
+ decoder = SlipDecoder()
52
+ chunk = (
53
+ b"ok"
54
+ + bytes([SLIP_END])
55
+ + bytes([SLIP_ESC]) # a frame that ends before its escape pair completes
56
+ + bytes([SLIP_END])
57
+ + b"go"
58
+ + bytes([SLIP_END])
59
+ )
60
+ frames = decoder.feed(chunk)
61
+ for frame in frames:
62
+ print(f"received {frame.decode()}")
63
+ print(f"discarded {decoder.discarded} frame the stream mangled")
64
+ ```
65
+
66
+ ## The same capability in every language
67
+
68
+ | Language | Package | Reference |
69
+ | --- | --- | --- |
70
+ | Rust | [`pamoja-serial`](https://crates.io/crates/pamoja-serial) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_serial/index.html), [docs.rs](https://docs.rs/pamoja-serial), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-serial) |
71
+ | TypeScript | [`@pamoja/serial`](https://www.npmjs.com/package/@pamoja/serial) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_serial.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-serial) |
72
+ | Python | [`pamoja-serial`](https://pypi.org/project/pamoja-serial/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-serial) |
73
+ | C# | [`Pamoja.Serial`](https://www.nuget.org/packages/Pamoja.Serial) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Serial.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-serial) |
74
+
75
+ ## Documentation
76
+
77
+ - [`pamoja.serial` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html), every class and function in this module.
78
+ - [The Serial framing guide](https://pamoja.molex.cloud/docs/guides/serial.html), with the same example in Rust, TypeScript, and C#.
79
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,187 @@
1
+ """Idiomatic serial-framing facade.
2
+
3
+ A serial line is a stream of bytes with no packet boundaries, so something has to
4
+ mark where one message ends and the next begins. SLIP and COBS are the two ways to
5
+ do that, and each is offered both as a one-shot call over a complete frame and as
6
+ a streaming decoder for the arbitrary chunks a port delivers.
7
+
8
+ The streaming decoders are what a real read loop uses. A corrupt frame does not
9
+ raise, because the frames around it are still good; it is dropped and counted on
10
+ :attr:`SlipDecoder.discarded`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Protocol
16
+
17
+ from pamoja._native import CobsDecoder as _NativeCobsDecoder
18
+ from pamoja._native import SlipDecoder as _NativeSlipDecoder
19
+ from pamoja._native import cobs_decode as _cobs_decode
20
+ from pamoja._native import cobs_encode as _cobs_encode
21
+ from pamoja._native import cobs_max_encoded_len as _cobs_max_encoded_len
22
+ from pamoja._native import serial_framing_bytes as _serial_framing_bytes
23
+ from pamoja._native import slip_decode as _slip_decode
24
+ from pamoja._native import slip_encode as _slip_encode
25
+ from pamoja._native import slip_max_encoded_len as _slip_max_encoded_len
26
+
27
+ __all__ = [
28
+ "COBS_DELIMITER",
29
+ "CobsDecoder",
30
+ "Framing",
31
+ "SLIP_END",
32
+ "SLIP_ESC",
33
+ "SLIP_ESC_END",
34
+ "SLIP_ESC_ESC",
35
+ "SlipDecoder",
36
+ "cobs",
37
+ "slip",
38
+ ]
39
+
40
+
41
+ class Framing(Protocol):
42
+ """One of the two byte-stuffing framings this module offers."""
43
+
44
+ def encode(self, payload: bytes) -> bytes:
45
+ """Frame a payload for the wire.
46
+
47
+ :param payload: The bytes to send.
48
+ :returns: The frame, delimiter included.
49
+ """
50
+
51
+ def decode(self, frame: bytes) -> bytes:
52
+ """Read the payload back out of a complete frame.
53
+
54
+ :param frame: The frame as it arrived.
55
+ :returns: The payload.
56
+ :raises PamojaError: If the frame is corrupt.
57
+ """
58
+
59
+ def max_encoded_len(self, payload_len: int) -> int:
60
+ """Return the largest frame a payload of this length can produce.
61
+
62
+ :param payload_len: The payload length in bytes.
63
+ :returns: The worst-case frame length.
64
+ """
65
+
66
+
67
+ _END, _ESC, _ESC_END, _ESC_ESC, _DELIMITER = _serial_framing_bytes()
68
+
69
+ #: The SLIP byte that ends a frame (RFC 1055).
70
+ SLIP_END = _END
71
+ #: The SLIP byte that escapes a reserved value inside a frame (RFC 1055).
72
+ SLIP_ESC = _ESC
73
+ #: The byte that follows an escape to stand for a literal end byte.
74
+ SLIP_ESC_END = _ESC_END
75
+ #: The byte that follows an escape to stand for a literal escape byte.
76
+ SLIP_ESC_ESC = _ESC_ESC
77
+ #: The byte that delimits a COBS frame, which never appears inside one.
78
+ COBS_DELIMITER = _DELIMITER
79
+
80
+
81
+ class _Slip:
82
+ """SLIP (RFC 1055): an ``END`` byte ends a packet, and an escape pair carries it."""
83
+
84
+ __slots__ = ()
85
+
86
+ def encode(self, payload: bytes) -> bytes:
87
+ """Frame a payload as a SLIP packet."""
88
+ return _slip_encode(bytes(payload))
89
+
90
+ def decode(self, frame: bytes) -> bytes:
91
+ """Read the payload back out of a SLIP frame."""
92
+ return _slip_decode(bytes(frame))
93
+
94
+ def max_encoded_len(self, payload_len: int) -> int:
95
+ """Return the worst-case SLIP frame length for a payload."""
96
+ return _slip_max_encoded_len(payload_len)
97
+
98
+
99
+ class _Cobs:
100
+ """COBS: removes the zero byte so one zero delimits packets unambiguously."""
101
+
102
+ __slots__ = ()
103
+
104
+ def encode(self, payload: bytes) -> bytes:
105
+ """Frame a payload as a COBS packet."""
106
+ return _cobs_encode(bytes(payload))
107
+
108
+ def decode(self, frame: bytes) -> bytes:
109
+ """Read the payload back out of a COBS frame."""
110
+ return _cobs_decode(bytes(frame))
111
+
112
+ def max_encoded_len(self, payload_len: int) -> int:
113
+ """Return the worst-case COBS frame length for a payload."""
114
+ return _cobs_max_encoded_len(payload_len)
115
+
116
+
117
+ #: SLIP framing, the simplest there is.
118
+ slip: Framing = _Slip()
119
+
120
+ #: COBS framing, for links where the overhead has to stay small and predictable.
121
+ cobs: Framing = _Cobs()
122
+
123
+
124
+ class SlipDecoder:
125
+ """Reassembles whole SLIP frames from the chunks a serial port delivers.
126
+
127
+ Example::
128
+
129
+ decoder = SlipDecoder()
130
+ while True:
131
+ for frame in decoder.feed(port.read(256)):
132
+ handle(frame)
133
+ """
134
+
135
+ __slots__ = ("_native",)
136
+
137
+ def __init__(self) -> None:
138
+ """Create an empty decoder, ready for the first chunk."""
139
+ self._native = _NativeSlipDecoder()
140
+
141
+ def feed(self, chunk: bytes) -> list[bytes]:
142
+ """Feed a chunk of the stream.
143
+
144
+ :param chunk: The bytes just read from the port.
145
+ :returns: Every frame this chunk completed, in order, which is often none.
146
+ """
147
+ return self._native.feed(bytes(chunk))
148
+
149
+ @property
150
+ def discarded(self) -> int:
151
+ """How many corrupt frames this decoder has discarded."""
152
+ return self._native.discarded
153
+
154
+ def reset(self) -> None:
155
+ """Discard any partly assembled frame."""
156
+ self._native.reset()
157
+
158
+
159
+ class CobsDecoder:
160
+ """Reassembles whole COBS frames from the chunks a serial port delivers.
161
+
162
+ The counterpart to :class:`SlipDecoder`, for links where the framing overhead
163
+ has to stay small and predictable.
164
+ """
165
+
166
+ __slots__ = ("_native",)
167
+
168
+ def __init__(self) -> None:
169
+ """Create an empty decoder, ready for the first chunk."""
170
+ self._native = _NativeCobsDecoder()
171
+
172
+ def feed(self, chunk: bytes) -> list[bytes]:
173
+ """Feed a chunk of the stream.
174
+
175
+ :param chunk: The bytes just read from the port.
176
+ :returns: Every frame this chunk completed, in order.
177
+ """
178
+ return self._native.feed(bytes(chunk))
179
+
180
+ @property
181
+ def discarded(self) -> int:
182
+ """How many corrupt frames this decoder has discarded."""
183
+ return self._native.discarded
184
+
185
+ def reset(self) -> None:
186
+ """Discard any partly assembled frame."""
187
+ self._native.reset()
File without changes
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pamoja-serial"
7
+ version = "0.1.18"
8
+ description = "SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ license-files = ["LICENSE-MIT"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "molexxxx" }]
14
+ keywords = ["pamoja", "iot", "robotics", "serial"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "pamoja-native==0.1.18",
23
+ ]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/molexxxx/pamoja"
27
+ Documentation = "https://pamoja.molex.cloud/docs/guides/serial.html"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["pamoja"]