DVX_GUI/packet/README.md
2026-03-20 20:00:05 -05:00

428 lines
14 KiB
Markdown

# 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)