pamoja-serial
SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
Install
pip install pamoja-serial
from pamoja import serial
This pulls in pamoja-native, the compiled engine. pip install pamoja is the whole framework in one package.
Example
The script the test suite runs, spliced here as it ran.
From bindings/python/guides/serial.py:
from pamoja.hal import SerialPort, SerialSettings
from pamoja.serial import COBS_DELIMITER, CobsDecoder, cobs, slip
def reading(sequence: int, text: str) -> bytes:
"""A reading: a two-byte sequence number, most significant byte first, then its text."""
return sequence.to_bytes(2, "big") + text.encode()
# The line: 115200 baud, eight data bits, no parity, one stop bit. Ten bits a character.
settings = SerialSettings(115_200)
print(
f"line {settings}, {settings.bits_per_character} bits a character, "
f"{settings.character_nanos / 1_000:.2f} us each"
)
# The two ends of the cable with nothing plugged in. On a Raspberry Pi the gateway's end is
# SerialPort.open("/dev/serial0", settings) and nothing after this statement changes.
gateway, node = SerialPort.pair(settings)
# A UART carries bytes, and nothing marks where a message ends, so the node frames each reading
# with COBS: zero becomes the one byte that ends a frame and never appears inside one, which
# matters here, since the sequence number is full of zeros.
texts = ["wind=12.4", "wind=13.1", "wind=11.8"]
sent = 0
for sequence, text in enumerate(texts, start=1):
frame = cobs.encode(reading(sequence, text))
node.write(frame)
sent += len(frame)
print(
f"node {len(texts)} readings of {len(reading(1, texts[0]))} bytes, "
f"framed as {sent} bytes"
)
# A read returns whatever has arrived, which is rarely one frame: here it is all three. The
# decoder splits the stream back into payloads at each delimiter.
arrived = gateway.read(256, timeout=0.1)
print(f"gateway {len(arrived)} bytes in one read")
decoder = CobsDecoder()
payloads = decoder.feed(arrived)
for payload in payloads:
print(f"reading {int.from_bytes(payload[:2], 'big')} {payload[2:].decode()}")
# What one frame costs on the wire at this speed, start and stop bits included.
frame_length = sent // len(texts)
print(
f"on the wire {settings.transfer_micros(frame_length) / 1_000:.2f} ms "
f"for a {frame_length}-byte frame at {settings}"
)
# The node restarts partway through a frame. As it comes back up it sends a lone delimiter,
# which closes off the half frame, so the gateway drops it rather than gluing it to the next
# one, and then it sends the reading again.
again = cobs.encode(reading(4, "wind=12.9"))
node.write(again[: len(again) // 2])
node.write(bytes([COBS_DELIMITER]))
node.write(again)
dropped_before = decoder.discarded
resent = decoder.feed(gateway.read(256, timeout=0.1))
dropped = decoder.discarded - dropped_before
print(f"restart {dropped} frame cut short and dropped, then {resent[0][2:].decode()}")
# SLIP, the older framing, ends a frame with one reserved byte and escapes that byte and its
# own escape byte inside one. With no reserved bytes in a reading it costs a byte less than
# COBS; a payload full of them costs up to twice its length under SLIP, and never more than
# one byte in 254 over under COBS.
first = reading(1, texts[0])
slip_length = len(slip.encode(first))
cobs_length = len(cobs.encode(first))
print(
f"framing {len(first)} payload bytes: {slip_length} under SLIP, "
f"{cobs_length} under COBS"
)
# The node goes quiet. A read waits for the first byte up to its timeout; on a port with
# nothing plugged in it returns at once and counts the wait instead of sleeping through it, so
# a test of a silent node takes no time.
waited_before = gateway.waited_micros
quiet = gateway.read(256, timeout=0.5)
waited = (gateway.waited_micros - waited_before) // 1_000
print(f"silence {len(quiet)} bytes in {waited} ms, counted and not slept")
The same capability in every language
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-serial |
reference, docs.rs, install |
| TypeScript | @pamoja/serial |
reference, install |
| Python | pamoja-serial |
reference, install |
| C# | Pamoja.Serial |
reference, install |
Documentation
pamoja.serialreference, every class and function in this module.- The Serial framing guide, with the same example in Rust, TypeScript, and C#.
- Every capability, and the install page.
License
MIT
Release files for pamoja-serial 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pamoja_serial-0.2.0.tar.gz | 5.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pamoja_serial-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 11.6 kB
Release files / pamoja_serial-0.2.0.tar.gz
| Download URL | pamoja_serial-0.2.0.tar.gz |
|---|---|
| Size | 5.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7fa2c5d03b48575e9a877d440ab49bba849f964ccbe4b746bbb86750460af05f
|
|
BLAKE2b-256 checksum How to use checksums |
b9678597571337ed249cfd784f13d82e05c43643424d8306465613784350ffc6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / pamoja_serial-0.2.0-py3-none-any.whl
| Download URL | pamoja_serial-0.2.0-py3-none-any.whl |
|---|---|
| Size | 6.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
796f2e5830f5384dc2f6c4fa16ec7689176bab171b09622d9ab00530c689ee7d
|
|
BLAKE2b-256 checksum How to use checksums |
45391b52bc36a25f49585863e128a5d56f4ce0972b45cfcf284fcaf31bd91f93
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|