# 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 ```c // 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 ```c 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 ```c void pktClose(PktConnT *conn); ``` Frees the connection state. Does **not** close the underlying COM port. The caller is responsible for calling `rs232Close()` separately. #### pktSend ```c 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 ```c 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 ```c 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 ```c 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 ```c 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 ```c #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 ```c // 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)