Metadata-Version: 2.5
Name: pamoja-serial
Version: 0.2.0
Summary: SLIP and COBS byte stuffing with streaming decoders, so a UART byte stream carries discrete packets.
Project-URL: Repository, https://github.com/molexxxx/pamoja
Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/serial.html
Author: molexxxx
License: MIT
License-File: LICENSE-MIT
Keywords: iot,pamoja,robotics,serial
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pamoja-native==0.2.0
Description-Content-Type: text/markdown

# pamoja-serial

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#.

[![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/serial.html)
[![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
[![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html)

## Install

```sh
pip install pamoja-serial
```

```python
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`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/serial.py):

```python
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`](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) |
| 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) |
| 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) |
| 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) |

## Documentation

- [`pamoja.serial` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/serial.html), every class and function in this module.
- [The Serial framing guide](https://pamoja.molex.cloud/docs/guides/serial.html), with the same example in Rust, TypeScript, and C#.
- [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).

## License

MIT
