PPNet

Hex.pmHex DocsLicenseChangelog

Message protocol with error correction (Reed-Solomon) and framing (COBS).

Installation

If available on Hex, add the dependency to your list in mix.exs:

def deps do
[
{:pp_net, "~> 0.1.4"}
]
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 (8 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).

StepEncodeDecode
Frametype + body
RS+ 8 parity bytescorrection
COBSbyte stuffingunstuff
Stream…payload…0x00split by 0x00

Frame structure (after Reed-Solomon)

All messages share the same logical layout before COBS:

FieldTypeBytes
typeuint81
bodybinaryvariable

The full block (type + body) is protected by 8 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).

FieldType
unique_idstring
board_identifierstring
versioninteger
board_versioninteger
boot_idinteger
ppnet_versioninteger
datetimeinteger (Unix ts)

Backward compatibility: A 6-element list without datetime is still accepted; datetime will be nil.


Type 2 — SingleCounter

Body: MessagePack array.

FieldType
kindstring
valueany
pulsesinteger
duration_msinteger
datetimeinteger (Unix ts)

Backward compatibility: A 4-element list without datetime is still accepted; datetime will be nil.


Type 3 — Ping

Body: MessagePack array with 11 elements in order:

FieldTypeValidationWire format / notes
session_idstringUUID formatList of 16 integers (bytes); decoded to UUID string
temperaturefloat
uptime_msinteger
locationlistlat/lon: float, accuracy: integer[lat, lon, accuracy]
cpufloat0.0 <= cpu <= 1.0
tpu_memory_percentinteger0 <= value <= 100% of TPU memory
tpu_ping_msintegerTPU ping time (ms)
wifilistmax 10 entriesList of 7-byte binaries: 6 bytes MAC (raw) + 1 byte RSSI (signed int8, dBm)
storagelisttotal/used: integer[total, used] (2 integers, kilobytes — clients must convert before sending; receivers always assume KB)
datetimeintegerUnix timestamp (seconds)
extramapOptional key/value data

Backward compatibility: A 9-element list without session_id is still accepted; session_id will be nil. A 10-element list without datetime is still accepted; datetime will be nil.

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, datetime].

FieldTypeNotes
kindinteger1 = detection
datamapExample payload: {"image_id" => <16-byte UUID binary>, "d" => [...]}
datetimeintegerUnix timestamp (seconds)

Backward compatibility: A 2-element list without datetime is still accepted; datetime will be nil.


Type 5 — Image

Body: fixed header + raw image data.

FieldTypeBytes
idbinary16 (UUIDv4)
formatuint81 (1=jpeg, 2=webp, 3=png, 4=h264)
datetimeuint32 (Unix)4
data_sizeuint324
databinarydata_size

H.264 (4):data must be an Annex-B byte stream — NAL units prefixed with a 00 00 01 or 00 00 00 01 start code. pack/1 rejects other framings (e.g. AVCC/length-prefixed) with %PPNet.PackError{reason: :not_annex_b}.

Backward compatibility: The old format (id + format + data without datetime/data_size) is still accepted; datetime will be nil.

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.

FieldTypeBytes
message_moduleuint81
transaction_iduint324
datetimeuint32 (Unix)4
total_chunksuint162

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


Type 7 — ChunkedMessageBody (fragment)

FieldTypeBytes
transaction_iduint324
datetimeuint32 (Unix)4
chunk_indexuint162
chunk_sizeuint81
chunk_databinarychunk_size

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

Backward compatibility: The old format without datetime is still accepted; datetime will be nil.


Usage

Example (chunked image):

image = %PPNet.Message.Image{data: raw_binary, format: :webp, datetime: DateTime.utc_now()}
[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])