Version 1.0.0
SCRAP is a lightweight, packet-based serial communication protocol designed for resource-constrained embedded systems. It provides reliable, structured data exchange over UART, RS-232, RS-485, or virtual serial ports.
Two fully interoperable implementations:
- C (C89): Pure ANSI C, zero dependencies, no
malloc. Runs on anything from bare-metal MCUs to Linux. - Python 3: Zero dependencies (native COBS and CRC). Ideal for PC-side tools and testing.
- Sliding Window (Go-Back-N): Configurable window size for higher throughput (default: Stop-and-Wait)
- Virtual Channels: Multiplex independent data streams on a single link
- Bus Topology: Optional addressing for RS-485 multi-node networks (
SCRAP_ENABLE_ADDRESSING) - Heartbeat: Configurable periodic heartbeat with timeout detection
- Statistics: Built-in packet counters (TX, RX, retransmissions, CRC errors, duplicates)
- Error Callbacks: Get notified on CRC mismatches, decode failures, buffer overflows
- COBS Framing: Robust packet delimiting with Consistent Overhead Byte Stuffing
- CRC16-CCITT: Integrity check on every packet
- Single-Header Option: Auto-generated
dist/scrap.hfor drop-in use (stb-style) - C89 Strict: Compiles with
-std=c89 -Wall -Wextra -Werror -pedantic
P2P mode (7-byte header):
seq(2) | flags(1) | channel(1) | type(1) | payload_len(2) | payload(N) | crc(2)
BUS mode (9-byte header, with SCRAP_ENABLE_ADDRESSING):
src(1) | dst(1) | seq(2) | flags(1) | channel(1) | type(1) | payload_len(2) | payload(N) | crc(2)
The raw packet is COBS-encoded and terminated with a 0x00 delimiter byte.
Copy dist/scrap.h into your project. In one .c file:
#define SCRAP_IMPLEMENTATION
#include "scrap.h"In all other files, just #include "scrap.h".
Add src/scrap.h and src/scrap.c to your build.
#include "scrap.h"
#define MAX_PAYLOAD 128
static Scrap_Handle handle;
static uint8_t rx_buf[SCRAP_BUFFER_SIZE(MAX_PAYLOAD)];
static uint8_t tx_buf[SCRAP_BUFFER_SIZE(MAX_PAYLOAD)];
/* Implement these for your platform */
uint16_t my_write(const uint8_t* data, uint16_t len) { /* ... */ }
uint16_t my_timestamp_ms(void) { /* ... */ }
void on_data(Scrap_Handle* h, const uint8_t* payload, uint16_t len) {
/* Handle received data */
}
int main(void) {
Scrap_Config cfg;
Scrap_Handle* h;
memset(&cfg, 0, sizeof(cfg));
cfg.rx_buffer = rx_buf;
cfg.rx_buffer_size = sizeof(rx_buf);
cfg.tx_buffer = tx_buf;
cfg.tx_buffer_size = sizeof(tx_buf);
cfg.max_payload_size = MAX_PAYLOAD;
cfg.transport.transport_write = my_write;
cfg.transport.get_timestamp_ms = my_timestamp_ms;
cfg.callbacks.data_received = on_data;
h = Scrap_Init(&handle, &cfg);
/* Send data */
Scrap_SendData(h, (const uint8_t*)"Hello", 5, 1 /* reliable */);
/* Main loop */
while (1) {
uint8_t byte = uart_read();
Scrap_ReceiveBytes(h, &byte, 1);
Scrap_Process(h);
}
}All configuration is done via #define before including scrap.h:
| Define | Default | Description |
|---|---|---|
SCRAP_MAX_PACKET_PAYLOAD |
512 | Maximum payload size in bytes |
SCRAP_MAX_CHANNELS |
1 | Number of virtual channels |
SCRAP_MAX_WINDOW_SIZE |
1 | Sliding window size (1 = Stop-and-Wait) |
SCRAP_ENABLE_ADDRESSING |
(undef) | Enable bus topology with src/dst addressing |
SCRAP_U8 |
unsigned char |
8-bit unsigned type override |
SCRAP_U16 |
unsigned short |
16-bit unsigned type override |
SCRAP_U32 |
unsigned long |
32-bit unsigned type override |
SCRAP_LOG |
no-op | Debug logging function |
SCRAP_MEMCPY/MEMSET/MEMMOVE |
stdlib | Memory function overrides |
Scrap_Handle* Scrap_Init(void* h_mem, const Scrap_Config* config);
void Scrap_ReceiveBytes(Scrap_Handle* h, const uint8_t* data, uint16_t len);
void Scrap_Process(Scrap_Handle* h); /* Call periodically */
uint8_t Scrap_IsReadyToSend(Scrap_Handle* h);uint16_t Scrap_SendData(h, payload, len, reliable);
uint16_t Scrap_SendCommand(h, cmd_id, params, params_len, reliable);
uint16_t Scrap_SendDataEx(h, channel, payload, len, reliable);
uint16_t Scrap_SendCommandEx(h, channel, cmd_id, params, params_len, reliable);
uint16_t Scrap_Ping(h);uint8_t Scrap_SetChannelCallbacks(h, channel, callbacks);
uint16_t Scrap_CRC16(data, len);
void* Scrap_GetUserData(h);
const Scrap_Stats* Scrap_GetStats(h);
void Scrap_ResetStats(h);
size_t Scrap_GetHandleSize(void);void (*command_received)(h, cmd_id, params, params_len);
void (*data_received)(h, payload, len);
void (*send_success)(h, sequence_number);
void (*send_failed)(h, sequence_number);
void (*ping_response)(h, rtt_ms);
void (*error)(h, error_code);
void (*heartbeat_received)(h);
void (*heartbeat_timeout)(h);from scrap.protocol import Scrap
# In-memory loopback example
wire = bytearray()
a = Scrap(lambda data: wire.extend(data) or len(data))
b = Scrap(lambda data: None)
b.on_data_received = lambda payload: print(f"Got: {payload}")
a.send_data(b"Hello!")
b.receive_bytes(bytes(wire))
# Output: Got: b'Hello!'For serial communication:
import serial
ser = serial.Serial('/dev/ttyUSB0', 115200)
scrap = Scrap(ser.write, window_size=4)
scrap.on_data_received = lambda p: print(f"RX: {p}")
scrap.send_data(b"Hello!", reliable=True)make # C89 check + unit tests + generate dist/scrap.h
make test # Run C and Python tests only
make dist # Generate dist/scrap.h only
make examples # Build file transfer examples
make clean # Remove build artifactssrc/
scrap.h # Public API header
scrap.c # Implementation
dist/
scrap.h # Auto-generated single-header (stb-style)
pysrc/scrap/ # Python implementation (zero dependencies)
tests/
test_scrap.c # C unit tests (17 tests)
examples/
arduino/ # Arduino sketch example
file_transfer/ # C file transfer (host + device)
python/ # Python loopback test
tools/
amalgamate.py # Generates dist/scrap.h from src/
MIT License. See LICENSE.md.
