DVX_GUI/packet
2026-03-20 20:00:05 -05:00
..
Makefile Reliable, secure, serial link protocol added. 2026-03-10 21:05:52 -05:00
packet.c Unicode removed from source. Lots of bugs fixed. Close to Alpha 2! 2026-03-20 19:10:53 -05:00
packet.h Unicode removed from source. Lots of bugs fixed. Close to Alpha 2! 2026-03-20 19:10:53 -05:00
README.md Updated docs. 2026-03-20 20:00:05 -05:00

Packet -- Reliable Serial Transport with HDLC Framing

Packetized serial transport providing reliable, ordered delivery over an unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT error detection, and a Go-Back-N sliding window protocol for automatic retransmission.

This layer sits on top of an already-open rs232 COM port. It does not open or close the serial port itself.

Architecture

Application
    |
    |  pktSend()     queue a packet for reliable delivery
    |  pktPoll()     receive, process ACKs/NAKs, check retransmit timers
    |
[Packet Layer]       framing, CRC, sequencing, sliding window ARQ
    |
[rs232]              raw byte I/O via ISR-driven ring buffers
    |
UART

The packet layer adds framing, error detection, and reliability to the raw byte stream provided by rs232. The caller provides a receive callback that is invoked synchronously from pktPoll() for each complete, CRC-verified, in-order data packet.

Frame Format

Before byte stuffing, each frame has the following layout:

[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
Field Size Description
0x7E 1 byte Flag byte -- frame delimiter
SEQ 1 byte Sequence number (wrapping uint8_t, 0-255)
TYPE 1 byte Frame type (DATA, ACK, NAK, RST)
LEN 1 byte Payload length (0-255)
Payload 0-255 Application data
CRC 2 bytes CRC-16-CCITT, little-endian, over SEQ..payload

The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte payload) is 260 bytes before byte stuffing.

Frame Types

Type Value Description
DATA 0x00 Carries application payload
ACK 0x01 Cumulative acknowledgment -- next expected sequence
NAK 0x02 Negative ack -- request retransmit from this seq
RST 0x03 Connection reset -- clear all state

Byte Stuffing

HDLC transparency encoding ensures the flag byte (0x7E) and escape byte (0x7D) never appear in the frame body. Within the frame data (everything between flags), these two bytes are escaped by prefixing with 0x7D and XORing the original byte with 0x20:

  • 0x7E becomes 0x7D 0x5E
  • 0x7D becomes 0x7D 0x5D

In the worst case, every byte in the frame is escaped, doubling the wire size. In practice, these byte values are uncommon in typical data and the overhead is minimal.

Why HDLC Framing

HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit variable-length frames on a raw byte stream. The 0x7E flag byte unambiguously marks frame boundaries. This is proven, lightweight, and requires zero buffering at the framing layer.

The alternative -- length-prefixed framing -- is fragile on noisy links because a corrupted length field permanently desynchronizes the receiver. With HDLC framing, the receiver can always resynchronize by hunting for the next flag byte.

CRC-16-CCITT

Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value 0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is stored little-endian in the frame (CRC low byte first, then CRC high byte).

The CRC is computed via a 256-entry lookup table (512 bytes of .rodata). Table-driven CRC is approximately 10x faster than bit-by-bit computation on a 486 -- a worthwhile trade for a function called on every frame transmitted and received.

Go-Back-N Sliding Window Protocol

Why Go-Back-N

Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not need an out-of-order reassembly buffer and only tracks a single expected sequence number. This works well for the low bandwidth-delay product of a serial link. On a 115200 bps local connection, the round-trip time is negligible, so the window rarely fills.

Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy links, but serial links are nearly lossless. The CRC check is primarily a safety net for electrical noise, not a routine error recovery mechanism.

Protocol Details

The sliding window is configurable from 1 to 8 slots (default 4). Sequence numbers are 8-bit unsigned integers that wrap naturally at 256. The sequence space (256) is much larger than 2x the maximum window (16), so there is no ambiguity between old and new frames.

Sender behavior:

  • Assigns a monotonically increasing sequence number to each DATA frame
  • Retains a copy of each sent frame in a retransmit slot until it is acknowledged
  • When the window is full (txCount >= windowSize), blocks or returns PKT_ERR_TX_FULL depending on the block parameter

Receiver behavior:

  • Accepts frames strictly in order (seq == rxExpectSeq)
  • On in-order delivery, increments rxExpectSeq and sends an ACK carrying the new expected sequence number (cumulative acknowledgment)
  • Out-of-order frames within the window trigger a NAK for the expected sequence number
  • Duplicate and out-of-window frames are silently discarded

ACK processing:

  • ACKs carry the next expected sequence number (cumulative)
  • On receiving an ACK, the sender frees all retransmit slots with sequence numbers less than the ACK's sequence number

NAK processing:

  • A NAK requests retransmission from a specific sequence number
  • The sender retransmits that frame AND all subsequent unacknowledged frames (the Go-Back-N property)
  • Each retransmitted slot has its timer reset

RST processing:

  • Resets all sequence numbers and buffers to zero on both sides
  • The remote side also sends a RST in response

Timer-Based Retransmission

Each retransmit slot tracks the time it was last (re)transmitted. If 500ms elapses without an ACK, the slot is retransmitted and the timer is reset. This handles the case where an ACK or NAK was lost on the wire -- without this safety net, the connection would stall permanently.

The 500ms timeout is conservative for a local serial link (RTT is under 1ms) but accounts for the remote side being busy processing. On BBS connections through the Linux proxy, the round-trip includes TCP latency, making the generous timeout appropriate.

Receive State Machine

Incoming bytes from the serial port are fed through a three-state HDLC deframing state machine:

State Description
HUNT Discarding bytes until a flag (0x7E) is seen
ACTIVE Accumulating frame bytes; flag ends frame, ESC escapes
ESCAPE Previous byte was 0x7D; XOR this byte with 0x20

The flag byte serves double duty: it ends the current frame AND starts the next one. Back-to-back frames share a single flag byte, saving bandwidth. A frame is only processed if it meets the minimum size requirement (5 bytes), so spurious flags produce harmless zero-length "frames" that are discarded.

API Reference

Types

// Receive callback -- called for each verified, in-order data packet
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);

// Opaque connection handle
typedef struct PktConnS PktConnT;

Constants

Name Value Description
PKT_MAX_PAYLOAD 255 Maximum payload bytes per packet
PKT_DEFAULT_WINDOW 4 Default sliding window size
PKT_MAX_WINDOW 8 Maximum sliding window size
PKT_SUCCESS 0 Success
PKT_ERR_INVALID_PORT -1 Invalid COM port
PKT_ERR_NOT_OPEN -2 Connection not open
PKT_ERR_ALREADY_OPEN -3 Connection already open
PKT_ERR_WOULD_BLOCK -4 Operation would block
PKT_ERR_OVERFLOW -5 Buffer overflow
PKT_ERR_INVALID_PARAM -6 Invalid parameter
PKT_ERR_TX_FULL -7 Transmit window full (non-blocking)
PKT_ERR_NO_DATA -8 No data available
PKT_ERR_DISCONNECTED -9 Serial port disconnected or error

Functions

pktOpen

PktConnT *pktOpen(int com, int windowSize,
                  PktRecvCallbackT callback, void *callbackCtx);

Creates a packetized connection over an already-open COM port.

  • com -- RS232 port index (RS232_COM1 through RS232_COM4)
  • windowSize -- sliding window size (1-8), or 0 for the default (4)
  • callback -- called from pktPoll() for each received, verified, in-order data packet. The data pointer is valid only during the callback.
  • callbackCtx -- user pointer passed through to the callback

Returns a connection handle, or NULL on failure (allocation error).

pktClose

void pktClose(PktConnT *conn);

Frees the connection state. Does not close the underlying COM port. The caller is responsible for calling rs232Close() separately.

pktSend

int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);

Sends a data packet. len must be in the range 1 to PKT_MAX_PAYLOAD (255). The data is copied into a retransmit slot before transmission, so the caller can reuse its buffer immediately.

  • block = true -- If the transmit window is full, polls internally (calling pktPoll() in a tight loop) until an ACK frees a slot. Returns PKT_ERR_DISCONNECTED if the serial port drops during the wait.
  • block = false -- Returns PKT_ERR_TX_FULL immediately if the window is full.

Returns PKT_SUCCESS on success.

pktPoll

int pktPoll(PktConnT *conn);

The main work function. Must be called frequently (every iteration of your main loop or event loop). It performs three tasks:

  1. Drain serial RX -- reads all available bytes from the rs232 port and feeds them through the HDLC deframing state machine
  2. Process frames -- verifies CRC, handles DATA/ACK/NAK/RST frames, delivers data packets to the callback
  3. Check retransmit timers -- resends any slots that have timed out

Returns the number of DATA packets delivered to the callback this call, or PKT_ERR_DISCONNECTED if the serial port returned an error, or PKT_ERR_INVALID_PARAM if conn is NULL.

The callback is invoked synchronously, so the caller should be prepared for re-entrant calls to pktSend() from within the callback.

pktReset

int pktReset(PktConnT *conn);

Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST frame to the remote side so it resets as well. Useful for recovering from a desynchronized state.

pktCanSend

bool pktCanSend(PktConnT *conn);

Returns true if there is room in the transmit window for another packet. Useful for non-blocking send loops to avoid calling pktSend() when it would return PKT_ERR_TX_FULL.

pktGetPending

int pktGetPending(PktConnT *conn);

Returns the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full). Useful for throttling sends and monitoring link health.

Usage Example

#include "packet.h"
#include "../rs232/rs232.h"

void onPacket(void *ctx, const uint8_t *data, int len) {
    // process received packet -- data is valid only during this callback
}

int main(void) {
    // Open serial port first (packet layer does not manage it)
    rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);

    // Create packet connection with default window size (4)
    PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);

    // Send a packet (blocking -- waits for window space if needed)
    uint8_t msg[] = "Hello, packets!";
    pktSend(conn, msg, sizeof(msg), true);

    // Main loop -- must call pktPoll() frequently
    while (1) {
        int delivered = pktPoll(conn);
        if (delivered == PKT_ERR_DISCONNECTED) {
            break;
        }
        // delivered = number of packets received this iteration
    }

    pktClose(conn);
    rs232Close(RS232_COM1);
    return 0;
}

Non-Blocking Send Pattern

// Send as fast as the window allows, doing other work between sends
while (bytesLeft > 0) {
    pktPoll(conn);  // process ACKs, free window slots

    if (pktCanSend(conn)) {
        int chunk = bytesLeft;
        if (chunk > PKT_MAX_PAYLOAD) {
            chunk = PKT_MAX_PAYLOAD;
        }

        if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
            offset    += chunk;
            bytesLeft -= chunk;
        }
    }

    // do other work here (update UI, check for cancel, etc.)
}

// Drain remaining ACKs
while (pktGetPending(conn) > 0) {
    pktPoll(conn);
}

Internal Data Structures

Connection State (PktConnT)

The connection handle contains:

  • COM port index and window size (configuration)
  • Callback function pointer and context
  • TX state: next sequence to assign, oldest unacked sequence, array of retransmit slots, count of slots in use
  • RX state: next expected sequence, deframing state machine state, frame accumulation buffer

Retransmit Slots (TxSlotT)

Each slot holds a copy of the sent payload, its sequence number, payload length, and a clock_t timestamp of when it was last transmitted. The retransmit timer compares this timestamp against the current time to detect timeout.

Building

make        # builds ../lib/libpacket.a
make clean  # removes objects and library

Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler flags: -O2 -Wall -Wextra -march=i486 -mtune=i586.

Objects are placed in ../obj/packet/, the library in ../lib/.

Requires librs232.a at link time (for rs232Read() and rs232Write()).

Files

  • packet.h -- Public API header (types, constants, function prototypes)
  • packet.c -- Complete implementation (framing, CRC, ARQ, state machine)
  • Makefile -- DJGPP cross-compilation build rules

Dependencies

  • rs232/ -- Serial port I/O (must be linked: -lrs232)

Used By

  • seclink/ -- Secure serial link (adds channel multiplexing and encryption)
  • proxy/ -- Linux serial proxy (uses a socket-based adaptation)