// Packetized serial transport with HDLC-style framing and sliding window // // Frame format (before byte stuffing): // [0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI] // // The leading 0x7E is the frame flag. A trailing 0x7E closes the frame and // also serves as the flag for the next frame (back-to-back). SEQ is an 8-bit // sequence number that wraps naturally. TYPE identifies DATA/ACK/NAK/RST. // LEN is the payload byte count (0-255). CRC covers SEQ through PAYLOAD. // // Byte stuffing (transparency): // 0x7E -> 0x7D 0x5E (flag byte escaped) // 0x7D -> 0x7D 0x5D (escape byte itself escaped) // XOR with 0x20 is the standard HDLC transparency method. // // CRC-16-CCITT (polynomial 0x1021) over SEQ+TYPE+LEN+PAYLOAD. // Initial value 0xFFFF. Stored little-endian in the frame. #include #include #include #include #include #include #include "packet.h" #include "../rs232/rs232.h" // ======================================================================== // Internal defines // ======================================================================== #define FLAG_BYTE 0x7E #define ESC_BYTE 0x7D #define ESC_XOR 0x20 // Frame types #define FRAME_DATA 0x00 #define FRAME_ACK 0x01 #define FRAME_NAK 0x02 #define FRAME_RST 0x03 // Header size: SEQ + TYPE + LEN #define HEADER_SIZE 3 // CRC size #define CRC_SIZE 2 // Minimum frame size (header + CRC, no payload) #define MIN_FRAME_SIZE (HEADER_SIZE + CRC_SIZE) // Maximum raw frame size (header + max payload + CRC) #define MAX_FRAME_SIZE (HEADER_SIZE + PKT_MAX_PAYLOAD + CRC_SIZE) // Maximum stuffed frame size (worst case: every byte stuffed + flag) #define MAX_STUFFED_SIZE (1 + MAX_FRAME_SIZE * 2) // Receive buffer must hold at least one max-size stuffed frame #define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64) // Retransmit timeout. 500ms is conservative for a local serial link (RTT // is < 1ms) but accounts for the remote side being busy. On a real BBS // connection through the proxy, the round-trip includes TCP latency. #define RETRANSMIT_TIMEOUT_MS 500 // Receive state machine: three states for HDLC deframing. // HUNT: discarding bytes until a flag (0x7E) is seen — sync acquisition. // ACTIVE: accumulating frame bytes, watching for flag (end of frame) or // escape (next byte is XOR'd). // ESCAPE: the previous byte was 0x7D; XOR this byte with 0x20 to recover // the original value. #define RX_STATE_HUNT 0 #define RX_STATE_ACTIVE 1 #define RX_STATE_ESCAPE 2 // ======================================================================== // Types // ======================================================================== // Transmit window slot: retains a copy of sent data so we can retransmit // on NAK or timeout without the caller keeping its buffer alive. The timer // tracks when this slot was last (re)transmitted for timeout detection. typedef struct { uint8_t data[PKT_MAX_PAYLOAD]; int len; uint8_t seq; clock_t timer; } TxSlotT; // Connection state. txSlots is a circular window indexed by [0..txCount-1], // where slot 0 is the oldest unacked frame (sequence txAckSeq) and // slot txCount-1 is the newest. When an ACK advances txAckSeq, we shift // slots down (implicitly, by incrementing txAckSeq and decrementing txCount). struct PktConnS { int com; int windowSize; PktRecvCallbackT callback; void *callbackCtx; // Transmit state (Go-Back-N sender) uint8_t txNextSeq; // next sequence number to assign uint8_t txAckSeq; // oldest unacknowledged sequence TxSlotT txSlots[PKT_MAX_WINDOW]; int txCount; // number of slots in use // Receive state (Go-Back-N receiver: only accepts in-order frames) uint8_t rxExpectSeq; // next expected sequence number uint8_t rxState; // RX_STATE_* uint8_t rxFrame[MAX_FRAME_SIZE]; int rxFrameLen; }; // ======================================================================== // CRC-16-CCITT lookup table // ======================================================================== static const uint16_t sCrcTable[256] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x54A5, 0xA54A, 0xB56B, 0x8508, 0x9529, 0xE5CE, 0xF5EF, 0xC58C, 0xD5AD, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x4864, 0x5845, 0x6826, 0x7807, 0x08E0, 0x18C1, 0x28A2, 0x38A3, 0xC94C, 0xD96D, 0xE90E, 0xF92F, 0x89C8, 0x99E9, 0xA98A, 0xB9AB, 0x5A55, 0x4A74, 0x7A17, 0x6A36, 0x1AD1, 0x0AF0, 0x3A93, 0x2AB2, 0xDB5D, 0xCB7C, 0xFB1F, 0xEB3E, 0x9BD9, 0x8BF8, 0xBB9B, 0xAB9A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x06B1, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 }; // ======================================================================== // Static prototypes (alphabetical) // ======================================================================== static uint16_t crcCalc(const uint8_t *data, int len); static void processFrame(PktConnT *conn, const uint8_t *frame, int len); static void retransmitCheck(PktConnT *conn); static void rxProcessByte(PktConnT *conn, uint8_t byte); static void sendAck(PktConnT *conn, uint8_t seq); static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); static void sendNak(PktConnT *conn, uint8_t seq); static void sendRst(PktConnT *conn); static int seqInWindow(uint8_t seq, uint8_t base, int size); static int txSlotIndex(PktConnT *conn, uint8_t seq); // ======================================================================== // CRC computation // ======================================================================== // Table-driven CRC-16-CCITT. Processing one byte per iteration with a // 256-entry table is ~10x faster than bit-by-bit on a 486. The table // costs 512 bytes of .rodata — a worthwhile trade for a function called // on every frame received and transmitted. static uint16_t crcCalc(const uint8_t *data, int len) { uint16_t crc = 0xFFFF; for (int i = 0; i < len; i++) { crc = (crc << 8) ^ sCrcTable[(crc >> 8) ^ data[i]]; } return crc; } // ======================================================================== // Frame transmission // ======================================================================== // Build a raw frame, compute CRC, byte-stuff it, and transmit. // The stuffed buffer can be up to 2x the raw size (every byte might need // escaping) plus the flags. This is stack-allocated because frames are small // and we're not in a deeply recursive call path. static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { uint8_t raw[MAX_FRAME_SIZE]; uint8_t stuffed[MAX_STUFFED_SIZE]; int rawLen; uint16_t crc; int out; // Build raw frame: SEQ + TYPE + LEN + PAYLOAD raw[0] = seq; raw[1] = type; raw[2] = (uint8_t)len; if (payload && len > 0) { memcpy(&raw[3], payload, len); } rawLen = HEADER_SIZE + len; // Append CRC crc = crcCalc(raw, rawLen); raw[rawLen] = (uint8_t)(crc & 0xFF); raw[rawLen + 1] = (uint8_t)((crc >> 8) & 0xFF); rawLen += CRC_SIZE; // Byte-stuff into output buffer with leading flag out = 0; stuffed[out++] = FLAG_BYTE; for (int i = 0; i < rawLen; i++) { if (raw[i] == FLAG_BYTE) { stuffed[out++] = ESC_BYTE; stuffed[out++] = FLAG_BYTE ^ ESC_XOR; } else if (raw[i] == ESC_BYTE) { stuffed[out++] = ESC_BYTE; stuffed[out++] = ESC_BYTE ^ ESC_XOR; } else { stuffed[out++] = raw[i]; } } // Trailing flag to close the frame immediately stuffed[out++] = FLAG_BYTE; // Send via serial port (blocking write) rs232Write(conn->com, (const char *)stuffed, out); } static void sendAck(PktConnT *conn, uint8_t seq) { sendFrame(conn, seq, FRAME_ACK, 0, 0); } static void sendNak(PktConnT *conn, uint8_t seq) { sendFrame(conn, seq, FRAME_NAK, 0, 0); } static void sendRst(PktConnT *conn) { sendFrame(conn, 0, FRAME_RST, 0, 0); } // ======================================================================== // Sequence number helpers // ======================================================================== // Check if a sequence number falls within a window starting at base. // Works correctly with 8-bit wrap-around because unsigned subtraction // wraps mod 256 — if seq is "ahead" of base by less than size, diff // will be a small positive number. static int seqInWindow(uint8_t seq, uint8_t base, int size) { uint8_t diff = seq - base; return diff < (uint8_t)size; } static int txSlotIndex(PktConnT *conn, uint8_t seq) { uint8_t diff = seq - conn->txAckSeq; if (diff >= (uint8_t)conn->txCount) { return -1; } return diff; } // ======================================================================== // Frame processing // ======================================================================== // Handle a complete, de-stuffed frame. CRC is verified first; on failure, // we NAK to request retransmission of the frame we actually expected. // // For DATA frames: Go-Back-N receiver logic — only accept if seq matches // rxExpectSeq (strictly in-order). Out-of-order frames within the window // trigger a NAK; duplicates and out-of-window frames are silently dropped. // // For ACK frames: cumulative acknowledgement. The ACK carries the next // expected sequence number, so we free all slots up to that point. // // For NAK frames: the receiver wants us to retransmit from a specific // sequence. Go-Back-N retransmits that frame AND all subsequent ones. static void processFrame(PktConnT *conn, const uint8_t *frame, int len) { uint8_t seq; uint8_t type; int payloadLen; uint16_t rxCrc; uint16_t calcCrc; if (len < MIN_FRAME_SIZE) { return; } // Verify CRC calcCrc = crcCalc(frame, len - CRC_SIZE); rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8); if (calcCrc != rxCrc) { // CRC mismatch — request retransmit of what we expect sendNak(conn, conn->rxExpectSeq); return; } seq = frame[0]; type = frame[1]; payloadLen = frame[2]; // Validate payload length against actual frame size if (payloadLen + MIN_FRAME_SIZE != len) { return; } switch (type) { case FRAME_DATA: if (seq == conn->rxExpectSeq) { // In-order delivery if (conn->callback) { conn->callback(conn->callbackCtx, &frame[HEADER_SIZE], payloadLen); } conn->rxExpectSeq++; sendAck(conn, conn->rxExpectSeq); } else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) { // Out of order but in window — NAK the one we want sendNak(conn, conn->rxExpectSeq); } // else: duplicate or out of window, silently discard break; case FRAME_ACK: { // ACK carries the next expected sequence number (cumulative) // Advance txAckSeq and free all slots up to seq while (conn->txCount > 0 && conn->txAckSeq != seq) { conn->txAckSeq++; conn->txCount--; } break; } case FRAME_NAK: { // Retransmit from the requested sequence int idx = txSlotIndex(conn, seq); if (idx >= 0) { // Retransmit this slot and all after it (go-back-N) for (int i = idx; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[i]; sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); slot->timer = clock(); } } break; } case FRAME_RST: // Remote requested reset — clear state and respond with RST conn->txNextSeq = 0; conn->txAckSeq = 0; conn->txCount = 0; conn->rxExpectSeq = 0; sendRst(conn); break; } } // ======================================================================== // Receive byte processing (state machine) // ======================================================================== // Feed one byte from the serial port into the HDLC deframing state machine. // The flag byte (0x7E) serves double duty: it ends the current frame AND // starts the next one. This means back-to-back frames share a single flag // byte, saving bandwidth. A frame is only processed if it meets the minimum // size requirement (header + CRC), so spurious flags between frames are // harmless (they just produce zero-length "frames" that are discarded). static void rxProcessByte(PktConnT *conn, uint8_t byte) { switch (conn->rxState) { case RX_STATE_HUNT: if (byte == FLAG_BYTE) { conn->rxState = RX_STATE_ACTIVE; conn->rxFrameLen = 0; } break; case RX_STATE_ACTIVE: if (byte == FLAG_BYTE) { // End of frame (or start of next) if (conn->rxFrameLen >= MIN_FRAME_SIZE) { processFrame(conn, conn->rxFrame, conn->rxFrameLen); } // Reset for next frame conn->rxFrameLen = 0; } else if (byte == ESC_BYTE) { conn->rxState = RX_STATE_ESCAPE; } else { if (conn->rxFrameLen < MAX_FRAME_SIZE) { conn->rxFrame[conn->rxFrameLen++] = byte; } else { // Frame too large, discard and hunt for next flag conn->rxState = RX_STATE_HUNT; } } break; case RX_STATE_ESCAPE: conn->rxState = RX_STATE_ACTIVE; byte ^= ESC_XOR; if (conn->rxFrameLen < MAX_FRAME_SIZE) { conn->rxFrame[conn->rxFrameLen++] = byte; } else { conn->rxState = RX_STATE_HUNT; } break; } } // ======================================================================== // Retransmit check // ======================================================================== // Timer-based retransmission for slots that haven't been ACK'd within the // timeout. This handles the case where an ACK or NAK was lost — without // this, the connection would stall forever. Each slot is retransmitted // independently and its timer is reset, creating exponential backoff // behavior naturally (each retransmit resets the timer). static void retransmitCheck(PktConnT *conn) { clock_t now = clock(); clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000; for (int i = 0; i < conn->txCount; i++) { TxSlotT *slot = &conn->txSlots[i]; if (now - slot->timer >= timeout) { sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); slot->timer = now; } } } // ======================================================================== // Public functions (alphabetical) // ======================================================================== void pktClose(PktConnT *conn) { if (conn) { free(conn); } } bool pktCanSend(PktConnT *conn) { if (!conn) { return false; } return conn->txCount < conn->windowSize; } int pktGetPending(PktConnT *conn) { if (!conn) { return PKT_ERR_INVALID_PARAM; } return conn->txCount; } PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { PktConnT *conn; if (windowSize <= 0) { windowSize = PKT_DEFAULT_WINDOW; } if (windowSize > PKT_MAX_WINDOW) { windowSize = PKT_MAX_WINDOW; } conn = (PktConnT *)calloc(1, sizeof(PktConnT)); if (!conn) { return 0; } conn->com = com; conn->windowSize = windowSize; conn->callback = callback; conn->callbackCtx = callbackCtx; conn->txNextSeq = 0; conn->txAckSeq = 0; conn->txCount = 0; conn->rxExpectSeq = 0; conn->rxState = RX_STATE_HUNT; conn->rxFrameLen = 0; return conn; } // Main polling function — must be called frequently (each iteration of the // app's main loop or event loop). It drains the serial port's RX buffer, // feeds bytes through the deframing state machine, and checks for // retransmit timeouts. The callback is invoked synchronously for each // complete, verified, in-order data frame, so the caller should be prepared // for re-entrant calls to pktSend from within the callback. int pktPoll(PktConnT *conn) { char buf[128]; int nRead; int delivered = 0; if (!conn) { return PKT_ERR_INVALID_PARAM; } // Read available serial data and feed to state machine while ((nRead = rs232Read(conn->com, buf, sizeof(buf))) > 0) { for (int i = 0; i < nRead; i++) { uint8_t prevExpect = conn->rxExpectSeq; rxProcessByte(conn, (uint8_t)buf[i]); if (conn->rxExpectSeq != prevExpect) { delivered++; } } } // Detect disconnected socket/port if (nRead < 0) { return PKT_ERR_DISCONNECTED; } // Check for retransmit timeouts retransmitCheck(conn); return delivered; } int pktReset(PktConnT *conn) { if (!conn) { return PKT_ERR_INVALID_PARAM; } conn->txNextSeq = 0; conn->txAckSeq = 0; conn->txCount = 0; conn->rxExpectSeq = 0; conn->rxState = RX_STATE_HUNT; conn->rxFrameLen = 0; sendRst(conn); return PKT_SUCCESS; } // Send a data packet. If block=true and the window is full, polls in a // tight loop until space opens up (an ACK arrives). The data is copied // into a retransmit slot before sending so the caller can reuse its buffer // immediately. The window slot tracks the sequence number and timestamp // for retransmission. int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { TxSlotT *slot; if (!conn) { return PKT_ERR_INVALID_PARAM; } if (!data || len <= 0 || len > PKT_MAX_PAYLOAD) { return PKT_ERR_INVALID_PARAM; } // Wait for window space if blocking if (block) { while (conn->txCount >= conn->windowSize) { if (pktPoll(conn) == PKT_ERR_DISCONNECTED) { return PKT_ERR_DISCONNECTED; } } } else { if (conn->txCount >= conn->windowSize) { return PKT_ERR_TX_FULL; } } // Store in retransmit buffer slot = &conn->txSlots[conn->txCount]; memcpy(slot->data, data, len); slot->len = len; slot->seq = conn->txNextSeq; slot->timer = clock(); conn->txCount++; conn->txNextSeq++; // Transmit sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); return PKT_SUCCESS; }