README.md

# PpNet

Message protocol with error correction (Reed-Solomon) and framing (COBS) for the Pagy Plus stack.

## Installation

If [available on Hex](https://hex.pm/docs/publish), add the dependency to your list in `mix.exs`:

```elixir
def deps do
  [
    {:pp_net, "~> 0.1.1"}
  ]
end
```

## Transport layer: COBS and Reed-Solomon

Each message is encoded in two stages before being sent on the wire:

1. **Frame**  
   Build the frame: `type` (1 byte) + `body` (variable). There is no separate checksum field; Reed-Solomon provides integrity.

2. **Reed-Solomon**  
   The frame is encoded with **Reed-Solomon** (4 parity bytes), allowing up to 4 corrupted bytes in the block to be corrected. Maximum block size is 255 bytes (typical RS limit in GF(2⁸)).

3. **COBS**  
   The result is encoded with **COBS** (_Consistent Overhead Byte Stuffing_): the byte `0x00` is reserved as the frame delimiter, and the payload is escaped so it never contains `0x00`. Frames can thus be delimited reliably in a stream.

4. **Separator**  
   A single `0x00` byte is appended after each encoded message, marking the end of the frame.

**Decoding:** the receiver splits the stream on `0x00`, decodes each block with COBS, applies Reed-Solomon to correct errors, then parses the frame (type + body).

| Step   | Encode           | Decode          |
| ------ | ---------------- | --------------- |
| Frame  | type + body      | —               |
| RS     | + 4 parity bytes | correction      |
| COBS   | byte stuffing    | unstuff         |
| Stream | …payload…`0x00`  | split by `0x00` |

## Frame structure (after Reed-Solomon)

All messages share the same logical layout before COBS:

| Field | Type   | Bytes    |
| ----- | ------ | -------- |
| type  | uint8  | 1        |
| body  | binary | variable |

The full block (type + body) is protected by 4 Reed-Solomon parity bytes.

---

## Message formats (body)

### Types 1–4: MessagePack body

Hello (1), SingleCounter (2), Ping (3), and Event (4) use **MessagePack** for the body.

---

### Type 1 — Hello

Body: MessagePack array (fields in order).

| Field            | Type    |
| ---------------- | ------- |
| unique_id        | string  |
| board_identifier | string  |
| version          | integer |
| board_version    | integer |
| boot_id          | integer |
| ppnet_version    | integer |

---

### Type 2 — SingleCounter

Body: MessagePack array.

| Field       | Type    |
| ----------- | ------- |
| kind        | string  |
| value       | any     |
| pulses      | integer |
| duration_ms | integer |

---

### Type 3 — Ping

Body: MessagePack array. **Minimum format:** `[temperature, uptime_ms]` (2 elements). **Full format:** 9 elements in order:

| Field              | Type    | Wire format / notes                                                              |
| ------------------ | ------- | -------------------------------------------------------------------------------- |
| temperature        | float   | —                                                                                |
| uptime_ms          | integer | —                                                                                |
| location           | map     | `[lat, lon, accuracy]` (3 elements: float, float, integer)                       |
| cpu                | float   | —                                                                                |
| tpu_memory_percent | integer | % of TPU memory                                                                  |
| tpu_ping_ms        | integer | TPU ping time (ms)                                                               |
| wifi               | list    | List of **7-byte binaries**: 6 bytes MAC (raw) + 1 byte RSSI (signed int8, dBm). |
| storage            | map     | `[total, used]` (2 integers, bytes)                                              |
| extra              | map     | Optional key/value data                                                          |

**WiFi encoding:** Each entry is 7 bytes: MAC address as 6 raw bytes (no colon-separated string), then RSSI as one signed byte. This keeps the payload small so the ping stays within a single frame.

---

### Type 4 — Event

Body: MessagePack array `[kind, data]`.

| Field | Type    | Notes                                                                  |
| ----- | ------- | ---------------------------------------------------------------------- |
| kind  | integer | 1 = detection                                                          |
| data  | map     | Example payload: `{"image_id" => <16-byte UUID binary>, "d" => [...]}` |

---

### Type 5 — Image

Body: fixed header + raw image data.

| Field  | Type   | Bytes                     |
| ------ | ------ | ------------------------- |
| id     | binary | 16 (UUIDv4)               |
| format | uint8  | 1 (1=jpeg, 2=webp, 3=png) |
| data   | binary | variable                  |

When the encoded image (or any message) exceeds the channel limit, it is sent as chunked messages (types 6 and 7).

---

### Type 6 — ChunkedMessageHeader (chunked message header)

Used when the payload is too large for a single frame (e.g. image). The body is fixed binary.

| Field               | Type          | Bytes |
| ------------------- | ------------- | ----- |
| message_module_code | uint8         | 1     |
| transaction_id      | uint32        | 4     |
| datetime            | uint32 (Unix) | 4     |
| total_chunks        | uint8         | 1     |

`message_module_code` indicates the original message type (1=Hello, 2=SingleCounter, 3=Ping, 4=Event, 5=Image). Total header body: 10 bytes.

---

### Type 7 — ChunkedMessageBody (fragment)

| Field          | Type   | Bytes      |
| -------------- | ------ | ---------- |
| transaction_id | uint32 | 4          |
| chunk_index    | uint8  | 1          |
| chunk_size     | uint8  | 1          |
| chunk_data     | binary | chunk_size |

`chunk_size` is the length in bytes of `chunk_data`. Fragments are reassembled by `transaction_id` and ordered by `chunk_index`.

---

## Usage

- **Encode** a message: `PPNet.encode_message(message)` returns a single binary, or a list `[header_binary | chunk_binaries]` when the message is chunked (e.g. large image). You can pass `limit: n` to force chunking when the encoded size would exceed `n` bytes (default 254).

- **Parse** a stream: `PPNet.parse(binary)` returns `%{messages: [...], errors: [...]}`. Each element of `messages` is either a decoded message struct (Hello, Ping, etc.) or a `ChunkedMessageHeader` / `ChunkedMessageBody`. Join all frames (e.g. from a stream) and call `parse` on the concatenated binary.

- **Reassemble** chunked payloads: when you have `[%ChunkedMessageHeader{} | chunks]` from `parse`, call `PPNet.Chunked_to_message([header | chunks])` to get `{:ok, message}` (or `{:error, reason}`). The result is the original message type (e.g. `%Image{}`, `%Ping{}`).

Example (chunked image):

```elixir
image = %PPNet.Message.Image{data: raw_binary, format: :webp}
[header_bin | chunk_bins] = PPNet.encode_message(image, limit: 200)
payload = [header_bin | chunk_bins] |> Enum.join()
%{messages: [header | body_messages], errors: []} = PPNet.parse(payload)
{:ok, ^image} = PPNet.Chunked_to_message([header | body_messages])
```