codewitch-honey-crisis/htcw_frame_arq

0.1.1

Latest
uploaded 11 hours ago

Readme

# frame_arq

Provides transport framing, error checking, and **guaranteed delivery** for dirty comms lines.

This library accepts simple read and write callbacks to read or write bytes to and from a transport. On top of plain framing it adds a stop-and-wait ARQ (Automatic Repeat reQuest) layer: each frame is acknowledged, corrupt or missing frames are detected, and delivery is retried until it succeeds.

This is a fork of `htcw_frame`. The wire format is **not** compatible with `htcw_frame` — it adds a sequence byte and a real CRC — so the two ends of a link must both use `htcw_frame_arq`.

## Wire format

Each frame is framed using 8 identical marker bytes, followed by a single sequence/type byte, a 4-byte unsigned little-endian payload length, a 4-byte little-endian CRC, and then the payload itself.

- **Marker bytes:** For a data frame the marker is the command value plus 128, so a command of 1 through 127 becomes 129 through 255 on the wire. This keeps markers out of the ASCII range and prevents collisions with log text. Command 0 is reserved — its marker value (128) identifies a **control frame** (an ACK or NACK).
- **Sequence/type byte:** The high 2 bits are the frame type (data, ACK, or NACK); the low 6 bits are the sequence number (0-63). Stop-and-wait only needs enough sequence space to spot duplicates, so ACKs and NACKs ride entirely in this byte with no payload — no command values are stolen from your 1-127 range.
- **CRC:** A standard IEEE CRC-32. Unlike the original library, the CRC now covers the sequence byte and the length field in addition to the payload, so a corrupted header is caught too.

The full header is `FRAME_ARQ_HEADER_LENGTH` (17) bytes.

## Setup

Setting it up involves giving `frame_arq_create()` a couple of read/write callbacks and a max payload size.

Here's an example with Arduino:
```cpp
#include <Arduino.h>
#include "frame_arq.h"
#define PAYLOAD_MAX_SIZE (1024)
static frame_arq_handle_t frame_handle = NULL;
static int serial_read(void* state) {
    (void)state;
    return Serial.read();
}
static int serial_write(uint8_t value, void* state) {
    (void)state;
    Serial.write((uint8_t)value);
    return 0;
}
void setup() {
    Serial.begin(115200);
    frame_handle = frame_arq_create(PAYLOAD_MAX_SIZE,serial_read,NULL,serial_write,NULL);
}
```

Once it's configured and bound to a transport, you read and write frames using `frame_arq_get()` and `frame_arq_put()`.

## Reading frames

`frame_arq_get()` drives the ARQ state machine. It pumps whatever bytes are available and returns immediately (it never blocks waiting for a frame), so call it repeatedly in a loop. When it receives a valid frame it automatically sends the appropriate ACK or NACK for you before returning.

```cpp
void* ptr;
size_t length;
int cmd = frame_arq_get(frame_handle, &ptr, &length);
// cmd  > 0 : a payload was delivered. cmd is the command (1-127);
//            ptr points to the payload and length is its size in bytes.
// cmd == 0 : nothing to do — no frame yet, or an ACK/duplicate/control
//            frame was handled internally.
// cmd == FRAME_ARQ_RESEND_NEEDED (-1) : a NACK arrived from the peer;
//            call frame_arq_resend(). This is a notification, not an error.
// cmd  < -1 : a genuine error (e.g. FRAME_ARQ_ERROR_CRC).
```

The error codes are ordered so that `FRAME_ARQ_RESEND_NEEDED` is `-1` and every real failure is `< -1`. That lets you write `if (cmd < -1)` to test for a genuine error while treating a resend request as the non-fatal notification it is.

`ptr` points into internal storage that is overwritten on the next call to `frame_arq_get()`, so consume the payload before calling again.

## Writing frames

Unlike reading, you provide the buffer. `frame_arq_put()` copies the frame internally (so your buffer need not stay valid after the call) and retains it for possible retransmission.

```cpp
// typedef struct { ... } data_t; data_t my_data;
int res = frame_arq_put(frame_handle, my_cmd, &my_data, sizeof(my_data));
// FRAME_ARQ_SUCCESS on success.
// FRAME_ARQ_ERROR_BUSY if a previous frame is still awaiting its ACK.
// < -1 on other errors.
```

Because this is stop-and-wait, only one frame may be outstanding at a time. If you call `frame_arq_put()` while a previously sent frame has not yet been acknowledged, it returns `FRAME_ARQ_ERROR_BUSY` rather than blocking. You can check this ahead of time with `frame_arq_awaiting_ack()`.

## Guaranteed delivery

Delivery is guaranteed by two mechanisms working together, and retransmission timing is left to you so you keep control over timeouts and retry counts:

- **Fast path (NACK):** When the peer receives a corrupt or out-of-sequence frame it sends a NACK. Your `frame_arq_get()` returns `FRAME_ARQ_RESEND_NEEDED`; you respond by calling `frame_arq_resend()`, which retransmits the outstanding frame with the same sequence number.
- **Total-loss path (timeout):** If a frame is lost so completely that nothing comes back, no NACK is generated. You detect this with your own timer: while `frame_arq_awaiting_ack()` is true, resend if too much time has passed.

A minimal receive/retransmit loop:
```cpp
#define ACK_TIMEOUT_MS 500
static uint32_t last_send_ms = 0;

void loop() {
    void* ptr;
    size_t length;

    int cmd = frame_arq_get(frame_handle, &ptr, &length);
    if (cmd > 0) {
        handle_message(cmd, ptr, length);   // arrived and was ACKed for us automatically
    } else if (cmd == FRAME_ARQ_RESEND_NEEDED) {
        frame_arq_resend(frame_handle);      // the peer NACKed our outstanding frame
        last_send_ms = millis();
    }

    // caller-owned timeout: retransmit if we've waited too long for an ACK
    if (frame_arq_awaiting_ack(frame_handle) &&
        (uint32_t)(millis() - last_send_ms) >= ACK_TIMEOUT_MS) {
        frame_arq_resend(frame_handle);
        last_send_ms = millis();
    }
}
```

And to send, respecting the one-in-flight rule:
```cpp
if (!frame_arq_awaiting_ack(frame_handle)) {
    if (frame_arq_put(frame_handle, my_cmd, &my_data, sizeof(my_data)) == FRAME_ARQ_SUCCESS) {
        last_send_ms = millis();
    }
}
```

Duplicate delivery is handled for you: if an ACK is lost and the peer retransmits, `frame_arq_get()` re-ACKs the duplicate but does not deliver it to you a second time.

The library never abandons a frame or advances the sequence number on its own — it retransmits the same frame until it is acknowledged. If you want to give up on a dead link, do it in your own retry logic and recover by reconnecting (which resets the sequence for a fresh session).

## Other functions

Discard the next waiting incoming frame without reading it:
```cpp
frame_arq_discard(frame_handle); // may never need this
```

Poll the sender state:
```cpp
bool waiting = frame_arq_awaiting_ack(frame_handle);   // a sent frame is unacked
bool resend  = frame_arq_resend_needed(frame_handle);  // a NACK was seen and a resend is pending
```

Free the handle when you're done:
```cpp
frame_arq_destroy(frame_handle);
```

## Zero-allocation creation

If you'd rather not use the heap, `frame_arq_create_za()` lets you supply the storage. It needs a `frame_arq_t` state structure and **two** buffers — one for receiving and one for retaining the last sent frame — each `FRAME_ARQ_HEADER_LENGTH + max_payload_size` bytes long:
```cpp
#define PAYLOAD_MAX_SIZE (1024)
static frame_arq_t     frame_state;
static uint8_t         read_buffer[FRAME_ARQ_HEADER_LENGTH + PAYLOAD_MAX_SIZE];
static uint8_t         retain_buffer[FRAME_ARQ_HEADER_LENGTH + PAYLOAD_MAX_SIZE];

frame_handle = frame_arq_create_za(PAYLOAD_MAX_SIZE,
    &frame_state, read_buffer, retain_buffer,
    serial_read, NULL, serial_write, NULL);
// do not call frame_arq_destroy() on a handle created this way
```

## CRC without a table

By default the CRC-32 uses a 256-entry lookup table, which lives in flash (not RAM) on an MCU. If flash is tight, define `FRAME_ARQ_NO_CRC_TABLE` (e.g. `-DFRAME_ARQ_NO_CRC_TABLE`) to compute the CRC bit-by-bit instead. This removes the table entirely — nothing is precomputed in RAM — at the cost of roughly 8x more CPU per payload byte. Both modes produce identical CRC values, so a tableless build interoperates with a table build.

## PlatformIO

```
; PlatformIO INI entry
[env:node32s]
platform = espressif32
board = node32s
framework = arduino
lib_deps = 
	codewitch-honey-crisis/htcw_frame_arq
```

## About the Demo

The PlatformIO repo portion contains a Demo project under its examples tree which demonstrates using this library in tandem with [htcw_buffers](https://github.com/codewitch-honey-crisis/htcw_buffers) to create a complete, framed, and reliably delivered serial wire protocol for communicating between the SerialFrameDemo C# app running on a Windows PC and a connected ESP32. Because delivery is acknowledged in both directions, the demo recovers from a noisy line — corrupt frames are detected, NACKed, and retransmitted without losing messages.

Links

Supports all targets

To add this component to your project, run:

idf.py add-dependency "codewitch-honey-crisis/htcw_frame_arq^0.1.1"

download archive

Stats

  • Archive size
    Archive size ~ 59.00 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

codewitch-honey-crisis/htcw_frame_arq version: 0.1.1
|