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(BytePack VERSION 0.1.0 LANGUAGES CXX)

# Create an INTERFACE library.
add_library(BytePack INTERFACE)

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

# Specify the include directories for the library.
target_include_directories(BytePack 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(BytePack INTERFACE cxx_std_17)
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/BytePack.svg" alt="PlatformIO Registry">
</a>
<br><br>
<a href="https://codecov.io/github/alkonosst/BytePack">
<img src="https://img.shields.io/codecov/c/github/alkonosst/BytePack?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 @@ -58,7 +62,7 @@

# Description

**BytePack** is a header-only C++17 Arduino library for serializing plain C++ structs into compact, portable byte buffers. A message is any struct that lists its fields in a single `io()` member function; that one function drives serialization, deserialization and compile-time size counting, so the field list is written once and can never get out of sync.
**BytePack** is a header-only C++17 Arduino/Native library for serializing plain C++ structs into compact, portable byte buffers. A message is any struct that lists its fields in a single `io()` member function; that one function drives serialization, deserialization and compile-time size counting, so the field list is written once and can never get out of sync.

The wire format is explicit little-endian with no padding, making it safe to exchange between different architectures (ESP32, ARM, a PC on the other end of a link, etc.). All buffers are caller-provided and statically sized; there is no dynamic memory allocation.

Expand Down Expand Up @@ -151,6 +155,23 @@ lib_deps =
3. Search for **"BytePack"**.
4. Click **Install**.

## CMake

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

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

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

# 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 "BytePack.h"
using namespace BytePack;

// A message is any struct that lists its fields in io()
struct Telemetry {
uint32_t uptime_ms = 0;
int16_t temperature = 0; // centi-degrees Celsius
bool relay_on = false;

template <typename Archive>
constexpr void io(Archive& ar) {
ar(uptime_ms, temperature, relay_on);
}
};

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("BytePack - Basic Native Example\n");
printf("-------------------------------\n");

// Buffer sized at compile time: 4 (uptime) + 2 (temperature) + 1 (relay) = 7 bytes
uint8_t buffer[getMaxPackedSize<Telemetry>()] = {};

// Fill and serialize (e.g. on the transmitting device)
Telemetry tx;
tx.uptime_ms = 123456;
tx.temperature = 2350; // 23.50 C
tx.relay_on = true;

const size_t written = serialize(tx, buffer, sizeof(buffer));
if (written == 0) {
printf("Serialization failed: buffer too small\n");
return 1;
}

printf("Serialized %zu bytes (little-endian):\n", written);
printHex(buffer, written);
printf("\n");

printf("Tx:\n");
printf("- uptime_ms: %u\n", tx.uptime_ms);
printf("- temperature: %d\n", tx.temperature);
printf("- relay_on: %s\n", tx.relay_on ? "true" : "false");
printf("\n");

// Deserialize into a fresh struct (e.g. on the receiving device)
Telemetry rx;
if (!deserialize(rx, buffer, written)) {
printf("Deserialization failed: truncated or invalid input\n");
return 1;
}

printf("Rx:\n");
printf("- uptime_ms: %u\n", rx.uptime_ms);
printf("- temperature: %d\n", rx.temperature);
printf("- relay_on: %s\n", rx.relay_on ? "true" : "false");

return 0;
}
109 changes: 76 additions & 33 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +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/CustomTypes
; src_dir = examples/Dispatch
; src_dir = examples/MessageHeader
; src_dir = examples/NestedMessages
; src_dir = examples/SizeBudget
; src_dir = examples/WriterReader
; ------------------------------------------------------------------------------------------------ ;
; 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:stm32f103c8]
; 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 = ststm32@19.6.0
framework = arduino
board = bluepill_f103c8
build_src_filter = +<*> +<../${sysenv.EXAMPLE}>
extra_scripts = pre:scripts/require-example.py

[env:esp32-s3]
; Common configuration for compiling tests.
[test]
extends = common
test_build_src = yes
build_flags = -DUNITY_INCLUDE_DOUBLE

; 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 @@ -57,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/",
)
36 changes: 36 additions & 0 deletions scripts/require-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2026 Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
# SPDX-License-Identifier: MIT

# Check that the EXAMPLE environment variable is defined and points to an existing folder. This
# variable is used to compile an example from the examples/ directory, and it needs to know which
# one to compile. If the variable is not defined or points to a non-existing folder, an error
# message is printed, and the build process is terminated.
import os
import sys

Import("env")

example = os.environ.get("EXAMPLE")

if not example:
sys.stderr.write(
"\n"
"==================================================================================\n"
" ERROR: the EXAMPLE environment variable is not defined.\n"
" This env compiles an example from examples/<example> and needs to know which one.\n"
" Define EXAMPLE before 'pio run', for example:\n"
' PowerShell: $env:EXAMPLE="examples/<example>"\n'
' bash/WSL: export EXAMPLE="examples/<example>"\n'
"==================================================================================\n"
)
env.Exit(1)
elif not os.path.isdir(os.path.join(env.subst("$PROJECT_DIR"), example)):
sys.stderr.write(
"\n"
"==================================================================================\n"
" ERROR: the example folder does not exist: EXAMPLE=" + example + "\n"
" Check the value (relative path to the project root, e.g., examples/<example>)\n"
" and make sure the folder exists.\n"
"==================================================================================\n"
)
env.Exit(1)
Loading