Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.14)
project(ByteFrame VERSION 0.2.0 LANGUAGES CXX)

# Create an INTERFACE library.
add_library(ByteFrame INTERFACE)

# Alias with author prefix. Consumers link "alkonosst::ByteFrame".
add_library(alkonosst::ByteFrame ALIAS ByteFrame)

# Specify the include directories for the library.
target_include_directories(ByteFrame INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src)

# Set a C++ standard requirement for the library. This will also propagate to consumers of the library.
target_compile_features(ByteFrame INTERFACE cxx_std_11)
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<img src="https://badges.registry.platformio.org/packages/alkonosst/library/ByteFrame.svg" alt="PlatformIO Registry">
</a>
<br><br>
<a href="https://codecov.io/github/alkonosst/ByteFrame">
<img src="https://img.shields.io/codecov/c/github/alkonosst/ByteFrame?style=for-the-badge&logo=codecov&logoColor=white&labelColor=F01F7A" alt="Coverage">
</a>
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&color=blue" alt="License">
</a>
Expand All @@ -35,6 +38,7 @@
- [Installation](#installation)
- [PlatformIO](#platformio)
- [Arduino IDE](#arduino-ide)
- [CMake](#cmake)
- [Usage](#usage)
- [Including the library](#including-the-library)
- [Namespace](#namespace)
Expand All @@ -54,7 +58,7 @@

# Description

**ByteFrame** is a header-only C++11 Arduino library that delimits packets over raw byte streams (UART, RS-485, radios, raw TCP...). It answers the question a stream cannot: _where does each packet start and end, and did it arrive intact?_
**ByteFrame** is a header-only C++11 embedded/native library that delimits packets over raw byte streams (UART, RS-485, radios, raw TCP...). It answers the question a stream cannot: _where does each packet start and end, and did it arrive intact?_

Each frame is the payload plus a selectable CRC, encoded with [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) and terminated by a single `0x00` delimiter. COBS guarantees the delimiter never appears inside a frame, so the decoder can always resynchronize after corruption or after joining a stream mid-frame. The payload is just bytes: ByteFrame does not care if it is a [BytePack](https://github.com/alkonosst/BytePack) message, a protobuf, or a raw struct.

Expand Down Expand Up @@ -135,6 +139,23 @@ lib_deps =
3. Search for **"ByteFrame"**.
4. Click **Install**.

## CMake

For desktop C++ projects, pull the library with `FetchContent` and link the `alkonosst::ByteFrame`
target:

```cmake
include(FetchContent)
FetchContent_Declare(
ByteFrame
GIT_REPOSITORY https://github.com/alkonosst/ByteFrame.git
GIT_TAG vx.y.z # pin a release tag (recommended), or a branch/commit
)
FetchContent_MakeAvailable(ByteFrame)

target_link_libraries(your_app PRIVATE alkonosst::ByteFrame)
```

# Usage

## Including the library
Expand Down
81 changes: 81 additions & 0 deletions examples/BasicNative/BasicNative.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* SPDX-FileCopyrightText: 2026 Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
*
* SPDX-License-Identifier: MIT
*/

/**
* Basic Native Example Overview:
* - Mirrors the Basic Arduino example, swapping Serial for printf. Useful for native builds.
* - Build and run locally:
* PowerShell: $env:EXAMPLE="examples/BasicNative"; pio run -e native-example -t exec
* bash/WSL : export EXAMPLE="examples/BasicNative"; pio run -e native-example -t exec
*/

#include <cstdio>

#include <ByteFrame.h>

// Maximum payload this link accepts; both the TX buffer and the Decoder derive from it
constexpr size_t MAX_PAYLOAD = 32;

ByteFrame::Decoder<MAX_PAYLOAD> decoder;

static void printHex(const uint8_t* data, const size_t len) {
for (size_t i = 0; i < len; ++i)
printf("%02X ", data[i]);
printf("\n");
}

int main() {
printf("--------------------------------\n");
printf("ByteFrame - Basic Native Example\n");
printf("--------------------------------\n");

// Any bytes work as payload: a BytePack message, a string, a raw struct...
const uint8_t payload[] = {'H', 'i', ' ', 0x00, 0x42, 0xFF}; // zeros are fine: COBS handles them

// TX buffer sized at compile time for the worst case (CRC + COBS overhead + delimiter)
uint8_t frame[ByteFrame::getMaxEncodedSize(MAX_PAYLOAD)] = {};

const size_t frame_size = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
if (frame_size == 0) {
printf("Encoding failed: frame buffer too small\n");
return 1;
}

printf("Payload (%zu bytes):\n", sizeof(payload));
printHex(payload, sizeof(payload));
printf("\n");

printf("Encoded frame (%zu bytes, ends with the 0x00 delimiter):\n", frame_size);
printHex(frame, frame_size);
printf("\n");

// Feed the frame byte by byte, as it would arrive from a UART
for (size_t i = 0; i < frame_size; i++) {
if (decoder.feed(frame[i])) {
printf("Decoded payload (%zu bytes):\n", decoder.getPayloadSize());
printHex(decoder.getPayload(), decoder.getPayloadSize());
printf("\n");
}
}

// Corrupt one byte: the CRC rejects the frame and the counter registers it
frame[2] ^= 0x55;

for (size_t i = 0; i < frame_size; i++) {
decoder.feed(frame[i]);
}

const auto& stats = decoder.getStats();

printf("After feeding a corrupted copy of the frame:\n");
printf("- Frame available: %s\n", decoder.isFrameAvailable() ? "yes" : "no");
printf("- Frames OK: %u\n", stats.frames_ok);
printf("- CRC errors: %u\n", stats.crc_errors);
printf("- Malformed errors: %u\n", stats.malformed);
printf("- Overflow errors: %u\n", stats.overflows);

return 0;
}
5 changes: 3 additions & 2 deletions examples/CrcSelection/CrcSelection.ino
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ constexpr size_t MAX_PAYLOAD = 16;

// Encode the payload with policy Crc, decode it back with the same policy, and report the result.
// The buffer size adapts to the policy through getMaxEncodedSize<Crc>().
template <class Crc>
void showPolicy(const char* name, const uint8_t* payload, const size_t payload_size) {
// clang-format off
template <class Crc> void showPolicy(const char* name, const uint8_t* payload, const size_t payload_size) {
// clang-format on
uint8_t frame[ByteFrame::getMaxEncodedSize<Crc>(MAX_PAYLOAD)] = {};
const size_t frame_size = ByteFrame::encode<Crc>(payload, payload_size, frame, sizeof(frame));

Expand Down
4 changes: 2 additions & 2 deletions library.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ByteFrame",
"version": "0.1.0",
"version": "0.2.0",
"authors": {
"name": "Maximiliano Ramirez",
"email": "maximiliano.ramirezbravo@gmail.com"
Expand All @@ -10,7 +10,7 @@
"url": "https://github.com/alkonosst/ByteFrame.git"
},
"description": "Header-only C++11 framing library with zero dynamic allocation. Delimits packets on raw byte streams with COBS, a selectable CRC (none/CRC8/CRC16/CRC32) and a 0x00 delimiter. Incremental fixed-buffer decoder. Pairs with BytePack for type-safe payloads.",
"keywords": "framing, frame, cobs, crc, crc8, crc16, crc32, delimiter, packet, stream, serial, uart, rs485, protocol, header-only, embedded, arduino, esp32, static",
"keywords": "framing, frame, cobs, crc, crc8, crc16, crc32, delimiter, packet, stream, serial, uart, rs485, protocol, header-only, embedded, native, arduino, esp32, static",
"license": "MIT",
"frameworks": "*",
"headers": ["ByteFrame.h"]
Expand Down
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=ByteFrame
version=0.1.0
version=0.2.0
author=Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
maintainer=Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
sentence=Header-only frame delimiting library with zero dynamic memory allocation.
Expand Down
111 changes: 75 additions & 36 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,35 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[platformio]
lib_dir = .
src_dir = examples/Basic
; src_dir = examples/StreamingEncoder
; src_dir = examples/CrcSelection
; src_dir = examples/ChunkedStream
; src_dir = examples/OneShotDecode
; ------------------------------------------------------------------------------------------------ ;
; Common configurations ;
; ------------------------------------------------------------------------------------------------ ;

; Common configuration for all environments.
[common]
; Tests
; Ignore Unity library to avoid scanning .pio/libdeps/Unity/examples/ as part of lib_dir = .
; Add Unity src path manually so unity.h is still found during test compilation
lib_ignore = Unity
build_src_flags = -Wall -Wextra -Werror

; Flags
build_flags =
; All warning as errors
-Wall
-Wextra
-Werror

[env:uno_wifi_rev2]
; Common configuration for compiling examples.
; It's necessary to set the EXAMPLE env variable when running. Example:
; - Windows: $env:EXAMPLE="examples/basic"; pio run -e native-example
; - Linux : export EXAMPLE="examples/basic"; pio run -e native-example
[example]
extends = common
platform = atmelmegaavr@1.9.0
framework = arduino
board = uno_wifi_rev2
build_src_filter = +<*> +<../${sysenv.EXAMPLE}>
extra_scripts = pre:scripts/require-example.py

[env:stm32f103c8]
; Common configuration for compiling tests.
[test]
extends = common
platform = ststm32@19.6.0
framework = arduino
board = bluepill_f103c8
test_build_src = yes
build_flags = -DUNITY_INCLUDE_DOUBLE

[env:esp32-s3]
extends = common
; Common configuration for native builds.
[native]
platform = native

; Common configuration for esp32-s3.
[esp32-s3]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
board = esp32-s3-devkitc-1
framework = arduino
Expand All @@ -61,23 +55,68 @@ upload_speed = 921600
monitor_echo = true
monitor_filters =
esp32_exception_decoder
send_on_enter
log2file

; Flags
build_flags =
${common.build_flags}

; Unity include path (lib_ignore = Unity prevents it from being auto-added)
-I.pio/libdeps/esp32-s3/Unity/src

; Enable debug (ESP-IDF logs)
; -DUSE_ESP_IDF_LOG
; -DCORE_DEBUG_LEVEL=5
; -DCONFIG_LOG_COLORS

; Enable PSRAM
-DBOARD_HAS_PSRAM
; -DBOARD_HAS_PSRAM

; Enable USB CDC on boot
-DARDUINO_USB_CDC_ON_BOOT=1

; ------------------------------------------------------------------------------------------------ ;
; Environments ;
; ------------------------------------------------------------------------------------------------ ;

; Native example compilation.
; Usage: export EXAMPLE=examples/<example>; pio run -e native-example -t exec
[env:native-example]
extends = native, example

; Native tests without sanitizers or code coverage for quick tests.
; Usage: pio test -e native-test
[env:native-test]
extends = native, test

; Native tests with sanitizers (ASan + UBSan).
; Usage: pio test -e native-san-test
[env:native-san-test]
extends = native, test
build_type = debug
build_flags =
${test.build_flags}
-fsanitize=address,undefined
-fno-sanitize-recover=all
-fno-omit-frame-pointer

; Native tests with code coverage (gcov).
; Usage: pio test -e native-cov-test
; - coverage.py passes --coverage to the linker (SCons does not forward it) and adds a
; custom "coverage" target that runs the tests and builds the gcovr report
; (pio run -e native-cov-test -t coverage).
[env:native-cov-test]
extends = native, test
build_type = debug
build_flags =
${test.build_flags}
-fno-inline
--coverage
extra_scripts = pre:scripts/coverage.py

; ESP32-S3 example compilation.
; Usage: export EXAMPLE=examples/<example>; pio run -e esp32-s3-example -t upload -t monitor
[env:esp32-s3-example]
extends = esp32-s3, example

; ESP32-S3 tests.
; Usage: pio test -e esp32-s3-test
[env:esp32-s3-test]
extends = esp32-s3, test
build_flags =
${esp32-s3.build_flags}
${test.build_flags}
38 changes: 38 additions & 0 deletions scripts/coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2026 Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
# SPDX-License-Identifier: MIT

# Coverage setup for the native-cov-test environment. Does two things:
# 1) Pass --coverage to the linker. PlatformIO/SCons (4.8.1) forwards -fsanitize to the linker
# automatically, but NOT --coverage (it has no special case in SCons ParseFlags, so it only reaches
# the compiler). Without this, the gcov runtime symbols (__gcov_*) are undefined at link time.
# 2) Add a custom "coverage" target that runs the tests and builds the report with gcovr.
# Use: pio run -e native-cov-test -t coverage (output in coverage/ as XML and HTML).
import platform

Import("env")

env.Append(LINKFLAGS=["--coverage"])

env_name = env["PIOENV"]

coverage_dir_cmd = ""
platform_name = platform.system()
if platform_name == "Windows":
coverage_dir_cmd = "if not exist coverage mkdir coverage"
else:
coverage_dir_cmd = "mkdir -p coverage"

env.AddCustomTarget(
name="coverage",
dependencies=None,
actions=[
"pio test -e " + env_name,
coverage_dir_cmd,
"gcovr --root . --filter src/ .pio/build/"
+ env_name
+ " --print-summary"
+ " --xml coverage/coverage.xml --html-details coverage/index.html",
],
title="Local coverage report",
description="Unit tests + gcovr report (XML/HTML) in coverage/",
)
Loading