Skip to content

VSSUT-Robotics-Society/PacketProtocol

Repository files navigation

PacketProtocol Protocol Library - Complete Architecture

Date: 2026-02-01
Status: Design Complete (v1 scope)
References: protocols.h, packets.h, status.h (v2+), cobs.h design documents


Overview

PacketProtocol is a reliable serial transport library for Arduino ↔ RPi/Host communication. It provides:

  • Automatic frame synchronization via COBS encoding
  • Deterministic packet structure with sequence tracking
  • Hardwired RESET handshake for clean state machine
  • Best-effort sensor data (freshness > completeness)
  • Reliable control (CONTROL packets preempt everything)

Architecture: Single serialized channel (UART/USB CDC) with strict ordering, no parallel data streams.


1. Protocol Layers (Bottom to Top)

┌─────────────────────────────────────────┐
│  Application Layer (ROS2 Node / Sketch) │
├─────────────────────────────────────────┤
│  Protocol Engine (State Machine)        │  ← ok() pattern (v2+)
│  - Handshake (RESET/RESET_ACK)          │  ← ConnectionState
│  - SEQ validation                       │  ← DESYNC recovery
│  - Watchdog (timeout → RESET)           │
├─────────────────────────────────────────┤
│  Packet Layer                           │
│  - build() / parse()                    │  ← protocols.h types
│  - SEQ management (currentSeq/advanceSeq) │
├─────────────────────────────────────────┤
│  COBS Framing                           │
│  - encode() / decode()                  │  ← 0x00 frame delimiters
│  - Automatic sync on frame boundary     │
├─────────────────────────────────────────┤
│  Serial Transport (Platform-Specific)   │
│  - Arduino Serial / POSIX termios       │
│  - Byte-level I/O                       │
└─────────────────────────────────────────┘

2. Module Dependencies

┌─────────────────┐
│  protocols.h    │  (enums, constants, types)
│ - PacketType    │
│ - ControlPayload│
│ - ConnectionState
│ - StatusCode    │
└────────┬────────┘
         │
    ┌────┴─────────────────────┬──────────┐
    │                          │          │
┌───▼──────┐  ┌──────────┐  ┌─▼────┐  ┌─▼──────┐
│ packets.h │  │ status.h │  │ cobs.h  │ (v1 only)
│ - Packet  │  │ - Status │  │         │
│ - build() │  │ - ok()   │  │ - encode│
│ - parse() │  │ - get()  │  │ - decode│
│ - SEQ mgmt│  │ - set()  │  │         │
└─────────┘  └──────────┘  └────────┘
     ▲                          ▲
     └──────────┬───────────────┘
                │
         ┌──────▼──────────┐
         │ Protocol Engine │
         │ (RX + TX)       │
         └─────────────────┘

3. V1 Scope (This Release)

Included

protocols.h - Packet types, CONTROL/DATA/COMMAND, SEQ semantics
packets.h - Build/parse wire format, SEQ management (pure build, advanceSeq)
cobs.h - Encode/decode, automatic frame sync

Deferred to V2+

status.h - Full ok() pattern, StatusCode tracking
watchdog - Timeout detection (can use simple timer instead)
Fragmentation - Payloads > 255 bytes split across packets

Why v1 is complete: RESET/RESET_ACK handshake works without status tracking. RX thread can drop malformed packets silently. No explicit state monitoring needed for basic operation.


4. Wire Format

Frame Structure

[ COBS-Encoded Packet ] [ 0x00 Frame Delimiter ]

Packet Structure (inside COBS)

[ TYPE ][ LEN ][ SEQ ][ PAYLOAD... ]

Byte 0    : TYPE (0x00=CONTROL, 0x01=DATA, 0x02=COMMAND)
Byte 1    : LEN (0-255, payload length)
Byte 2    : SEQ (0-255, wraps, auto-managed)
Byte 3-N  : PAYLOAD (0-255 bytes)

Example: RESET Packet

Raw (pre-COBS):
[ 0x00 ][ 0x01 ][ 0x00 ][ 0x01 ]
  TYPE    LEN     SEQ     VERSION

After COBS encode (no zeros to escape):
[ 0x05 ][ 0x00 ][ 0x01 ][ 0x00 ][ 0x01 ]

On wire:
[ 0x05 ][ 0x00 ][ 0x01 ][ 0x00 ][ 0x01 ][ 0x00 ]
                                           ▲
                                    frame delimiter

5. Startup Sequence (Handshake)

Host (RPi/Server) Side

1. Build RESET packet
   - Type: CONTROL
   - Payload: VERSION (1 byte)
   - SEQ: 0 (auto-managed)

2. Encode with COBS, add 0x00 delimiter, write to serial

3. Advance SEQ (only on successful write)
   - pkt.advanceSeq()  (SEQ: 0 → 1)

4. Wait for RESET_ACK (blocking with timeout)

5. On receive RESET_ACK:
   - Parse packet
   - Verify VERSION matches
   - Transition to ACTIVE state
   - status.set(OK) [v2+]

6. Send application data (DATA/COMMAND packets)

Device (Arduino) Side

1. RX byte pump running
   - Accumulate bytes until 0x00
   - COBS decode
   - Dispatch by TYPE

2. Receive RESET packet
   - Parse: TYPE=CONTROL, payload[0]=VERSION
   - Reset protocol state
   - pkt.resetSeq()  (SEQ → 0)
   - Clear any pending data

3. Build RESET_ACK packet
   - Type: CONTROL, Payload: VERSION
   - SEQ: 0 (after reset)

4. Encode and send RESET_ACK
   - pkt.advanceSeq()  (SEQ: 0 → 1)

5. Enter ACTIVE state, accept DATA/COMMAND

6. Normal Operation (Data Flow)

TX Path (Host → Device)

Application
    ↓
    Build packet (TYPE, PAYLOAD)
    ├─ pkt.build(type, payload)  [pure, no side effects]
    ├─ Returns: [ TYPE ][ LEN ][ SEQ ][ PAYLOAD ]
    ↓
    COBS encode
    ├─ encoded = cobs_encode(raw_pkt)
    ├─ Add 0x00 frame delimiter
    ↓
    Serial write
    ├─ written = serial.write(encoded)
    ├─ if (written == len(encoded))
    │    pkt.advanceSeq()  ← Only advance on full write
    └─ else
         log warn, retry or discard

RX Path (Device → Host)

Serial read (byte pump thread)
    ↓
    Accumulate bytes until 0x00
    ├─ If 0x00: frame boundary found
    ├─ If buffer empty: noise, discard
    ↓
    COBS decode
    ├─ try: raw_pkt = cobs_decode(buffer)
    ├─ catch: malformed, drop frame, continue
    ↓
    Parse packet
    ├─ info = pkt.parse(raw_pkt)
    ├─ Extract: TYPE, SEQ, PAYLOAD
    ↓
    Dispatch by TYPE
    ├─ CONTROL: Protocol layer (RESET_ACK, EVENT, etc)
    ├─ DATA: Application (sensor data)
    └─ COMMAND: Application (command handler)
    
    Protocol layer validates SEQ
    ├─ delta = recv_seq - expected_seq
    ├─ if (delta < 0 || delta > DESYNC_THRESHOLD)
    │    status.set(DESYNC) [v2+] or send RESET
    └─ else
         update expected_seq for next packet

7. Control Flow (CONTROL Packet Handling)

RESET/RESET_ACK (Handshake)

┌─────────┐         RESET          ┌─────────┐
│  IDLE   │────────────────────→   │WAIT_ACK │
└─────────┘  (seq=0, version)      └────┬────┘
                                         │
                                    RESET_ACK
                                    (seq=0, version)
                                         │
                                    ┌────▼────┐
                                    │ ACTIVE  │
                                    └─────────┘

HEARTBEAT (Optional Liveness)

Every 5 seconds (or less frequent):
Host → Device: HEARTBEAT packet
  - Type: CONTROL
  - Payload: empty (LEN=0)
  - Shows liveness
  
Device processes and continues (no response needed)

EVENT (Noteworthy Condition)

Device detects something noteworthy (low battery, sensor cal needed)
Device → Host: EVENT packet
  - Type: CONTROL
  - Payload: EVENT_CODE (1 byte)
  - Protocol stays ACTIVE
  
Host logs event, may decide to escalate with RESET if repeated

8. SEQ (Sequence Number) Semantics

What It Does

  • Monotonic counter (0-255, wraps)
  • Detects dropped/reordered packets
  • Enables asymmetric reset detection

What It Doesn't Do

  • No ACKs per packet (CONTROL-only packets ACK'd, not data)
  • No retransmission (drop lost packets)
  • No time alignment (just packet order)

Management Rules

Building:

auto raw = pkt.build(type, payload);  // Uses current SEQ
// pkt.currentSeq() == 0 (for first packet)

encoded = cobs_encode(raw);
written = serial.write(encoded);
if (written == len(encoded)) {
    pkt.advanceSeq();  // Now pkt.currentSeq() == 1
}

Parsing:

auto info = pkt.parse(raw);  // Extract SEQ from packet
uint8_t recv_seq = info.seq;

// Validate
if (recv_seq != expected_seq) {
    if (gap is small)     → tolerate, continue
    if (gap is large)     → send RESET (desync)
    if (backward)         → send RESET (asymmetric)
}

RESET:

pkt.resetSeq();  // SEQ = 0
// Next packet will use SEQ=0

9. Error Recovery

Scenario 1: Bit Corruption in Frame

Corrupt bytes received: 0xAA 0xBB 0xCC 0x00
RX thread tries to decode [0xAA, 0xBB, 0xCC]
→ COBS decode fails (invalid structure)
→ Exception caught, frame dropped
→ Continue waiting for next 0x00

Next good frame arrives: 0x02 0x01 0x02 0x00
RX thread successfully decodes and dispatches

Result: Automatic recovery at frame boundary.

Scenario 2: Device Crash

Host sends packet, device reboots
Host waits for response (timeout 5 sec)
→ No response, status.set(TIMEOUT) [v2+]
→ Host sends RESET

Device boots, receives RESET
→ Resets protocol state (SEQ=0)
→ Sends RESET_ACK

Host receives RESET_ACK
→ status.set(OK) [v2+]
→ Resume normal operation

Scenario 3: Serial Line Noise

Random bytes on line: 0xFF 0x12 0x34 0x00
RX thread tries to decode [0xFF, 0x12, 0x34]
→ offset=0xFF, expects 254 bytes but only 1 left
→ Tries to read past end
→ COBS decode fails
→ Frame dropped

Next real packet comes in cleanly

Result: Noise is contained to single frame, auto-recovered.


10. Threading Model (Deterministic)

Core Rule

Serial read/write happens in exactly one place.

RX Thread (Byte Pump)

while (true) {
    uint8_t byte = serial.read();  // Blocking
    
    if (byte == 0x00) {
        // Frame boundary
        try {
            auto raw = cobs_decode(buffer);
            dispatch_packet(raw);  // No protocol state here!
        } catch (...) {
            // Drop frame
        }
        buffer.clear();
    } else {
        buffer.push_back(byte);
        if (buffer.size() > MAX_ENCODED) {
            buffer.clear();  // Sanity check
        }
    }
}

Properties:

  • No protocol state (just byte pump)
  • Never blocks on application logic
  • Exception-safe (drop frame, continue)

Protocol Thread (Main/Loop)

while (true) {
    // Handle incoming packets (dispatch queue)
    if (packet_available()) {
        auto pkt = packet_queue.pop();
        handle_packet(pkt);  // Updates connection state
    }
    
    // Check watchdog
    if (time_since_last_packet > TIMEOUT) {
        send_reset();
    }
    
    // Send application data (if ACTIVE)
    if (connection_state == ACTIVE && data_available()) {
        auto data = app_queue.pop();
        send_packet(data);
    }
}

Properties:

  • Owns protocol state (ConnectionState, expected SEQ, timeout counter)
  • Owns TX queue
  • Can call pkt.build(), pkt.advanceSeq() safely

TX Queue Policy

Priority:
1. CONTROL packets (RESET, EVENT)
2. DATA & COMMAND (same queue, FIFO)

Rules:
- Only send when ACTIVE (not during handshake)
- After RESET, flush queue
- RESET_ACK flushes pending data

11. Implementation Roadmap (v1)

Phase 1: Foundation

  • Implement protocols.h (enums, constants)
  • Implement packets.h/packets.cpp (build/parse, SEQ)
  • Implement cobs.h/cobs.cpp (encode/decode)
  • Unit tests for above

Phase 2: Serial Integration

  • RX byte pump (accumulate until 0x00, COBS decode, dispatch)
  • TX path (build, encode, write, advanceSeq on success)
  • Simple queue (std::queue for packets)
  • Platform layer (Arduino Serial wrapper, POSIX termios wrapper)

Phase 3: Protocol Engine

  • ConnectionState machine (IDLE → WAIT_ACK → ACTIVE)
  • RESET/RESET_ACK handshake
  • SEQ validation (tolerate small gaps, send RESET on large gap)
  • Simple watchdog (5 sec timeout, send RESET)

Phase 4: Testing

  • Unit tests for each module
  • Integration tests (RX/TX roundtrip)
  • Stress tests (bit corruption, noise)
  • Real hardware tests (Arduino ↔ RPi)

Phase 5+: v2 Features

  • status.h / status.cpp (ok() pattern, StatusCode tracking)
  • Fragmentation (payloads > 255 bytes)
  • Advanced recovery (exponential backoff, per-packet ACKs)
  • Performance optimization (streaming COBS, ring buffers)

12. Quick Reference

Protocol Constants

PROTOCOL_VERSION        = 0x01
MAX_PAYLOAD_SIZE        = 255 (bytes, LEN field is 1 byte)
WATCHDOG_TIMEOUT_MS     = 5000
SEQ_TOLERANCE           = 5 (small drop tolerated)
SEQ_DESYNC_THRESHOLD    = 50 (large jump triggers RESET)

Packet Types

TYPE 0x00 = CONTROL (RESET, RESET_ACK, HEARTBEAT, EVENT)
TYPE 0x01 = DATA (sensor/telemetry, best-effort)
TYPE 0x02 = COMMAND (configuration/control)

Control Opcodes

0x00 = RESET (hard sync)
0x01 = RESET_ACK (acknowledge reset)
0x02 = HEARTBEAT (liveness, optional)
0x03 = EVENT (noteworthy condition)

Connection States

IDLE      = Not connected, no handshake
WAIT_ACK  = Sent RESET, waiting for RESET_ACK
ACTIVE    = Handshake complete, normal ops
ERROR     = Fault detected (v2+, for status tracking)

13. Files Structure

PacketProtocol/
├── include/
│   └── PacketProtocol/
│       ├── protocols.h       (enums, constants)
│       ├── packets.h         (Packet class)
│       ├── status.h          (Status class, v2+)
│       └── cobs.h            (encode/decode)
├── src/
│   ├── packets.cpp           (Packet implementation)
│   ├── status.cpp            (Status implementation, v2+)
│   ├── cobs.cpp              (COBS implementation)
│   └── protocol_engine.cpp   (RX thread, TX thread)
├── platform/
│   ├── arduino/
│   │   └── serial_wrapper.cpp (Arduino-specific)
│   └── linux/
│       └── serial_wrapper.cpp (POSIX-specific)
├── test/
│   ├── test_packets.cpp
│   ├── test_cobs.cpp
│   └── test_protocol.cpp
└── design/
    ├── design_protocols_h.md
    ├── design_packets_h.md
    ├── design_status_h.md
    ├── design_cobs_h.md
    └── ARCHITECTURE.md (this file)

14. Key Design Principles

  1. RESET is authoritative - Only way to change protocol state (besides initial IDLE)
  2. SEQ is observability - No CRC/ACK, SEQ gives visibility into packet loss
  3. COBS gives framing - 0x00 is frame boundary, automatic sync on corruption
  4. Best-effort sensor data - New overwrites old, bounded memory, fresh always
  5. Pure functions - build() and encode() have no side effects, safe to retry
  6. Single writer - Only one thread writes SEQ/state (Protocol thread)
  7. No hidden heuristics - All state transitions explicit, auditable, deterministic

15. References

About

A library for syncing & verifying data sent over unreliable physical mediums like SerialUART.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors