From 5f27e00ee30580287f35eedb4f54384756c331d0 Mon Sep 17 00:00:00 2001 From: alkonosst Date: Wed, 24 Jun 2026 10:31:16 -0400 Subject: [PATCH 1/3] test: Change platformio.ini config to support native tests, add more tests to improve coverage --- platformio.ini | 109 ++++++++++++------ scripts/coverage.py | 38 ++++++ scripts/require-example.py | 36 ++++++ test/test_bytepack.cpp | 229 +++++++++++++++++++++++++++---------- 4 files changed, 317 insertions(+), 95 deletions(-) create mode 100644 scripts/coverage.py create mode 100644 scripts/require-example.py diff --git a/platformio.ini b/platformio.ini index 6eeb690..4155d46 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 @@ -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/; 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/; 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} diff --git a/scripts/coverage.py b/scripts/coverage.py new file mode 100644 index 0000000..a197f3a --- /dev/null +++ b/scripts/coverage.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 Maximiliano Ramirez +# 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/", +) diff --git a/scripts/require-example.py b/scripts/require-example.py new file mode 100644 index 0000000..0b7c109 --- /dev/null +++ b/scripts/require-example.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 Maximiliano Ramirez +# 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/ and needs to know which one.\n" + " Define EXAMPLE before 'pio run', for example:\n" + ' PowerShell: $env:EXAMPLE="examples/"\n' + ' bash/WSL: export EXAMPLE="examples/"\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/)\n" + " and make sure the folder exists.\n" + "==================================================================================\n" + ) + env.Exit(1) diff --git a/test/test_bytepack.cpp b/test/test_bytepack.cpp index 8b836b7..ab42952 100644 --- a/test/test_bytepack.cpp +++ b/test/test_bytepack.cpp @@ -31,9 +31,10 @@ * truncation of valid messages and single-byte corruption. */ -#include +#ifdef ARDUINO +# include +#endif -#define UNITY_INCLUDE_DOUBLE #include #include @@ -133,6 +134,25 @@ struct MsgNested { } }; +// Named dispatch handler reused by every dispatch test. Each distinct lambda type is its own +// dispatch<>/tryDispatch<> instantiation, so funneling the tests through a single handler type +// keeps the template's branch coverage tractable (one instantiation per message, fully exercised). +struct DispatchRecorder { + bool ping_handled = false; + bool data_handled = false; + uint8_t ping_counter = 0; + uint16_t data_value = 0; + + void operator()(const MsgPing& msg) { + ping_handled = true; + ping_counter = msg.counter; + } + void operator()(const MsgData& msg) { + data_handled = true; + data_value = msg.value; + } +}; + /* ---------------------------------------------------------------------------------------------- */ /* Custom types */ /* ---------------------------------------------------------------------------------------------- */ @@ -206,6 +226,10 @@ void test_type_quant_saturation() { q = -400.0f; TEST_ASSERT_EQUAL_INT16(-32768, q.getRaw()); + // Negative, non-saturating value: exercises the "round half away from zero" negative branch + q = -12.34f; + TEST_ASSERT_EQUAL_INT16(-1234, q.getRaw()); + // NaN -> 0 q = NAN; TEST_ASSERT_EQUAL_INT16(0, q.getRaw()); @@ -399,6 +423,10 @@ void test_writer_custom_types() { w.reset(); TEST_ASSERT_TRUE(w.isOk()); TEST_ASSERT_EQUAL(0, w.getSize()); + + // set() rejects a payload larger than this capacity (Bytes) + uint8_t oversized[6] = {}; + TEST_ASSERT_FALSE(msg.b.set(oversized, sizeof(oversized))); } void test_writer_overflow() { @@ -427,6 +455,24 @@ void test_writer_overflow() { TEST_ASSERT_EQUAL(0, w.getSize()); } +void test_writer_bytes_overflow() { + // A Bytes payload whose length prefix fits but whose data does not must fail without writing a + // partial payload; and the writer must stay in error if it is already failed when it reaches the + // Bytes field. + Msg2 msg{}; + msg.q = 1.0f; + uint8_t src[5] = {1, 2, 3, 4, 5}; + TEST_ASSERT_TRUE(msg.b.set(src, sizeof(src))); + + // Quant (2) + length prefix (1) fit in 4 bytes, but the 5 data bytes overflow + uint8_t small[4] = {}; + TEST_ASSERT_EQUAL(0, serialize(msg, small, sizeof(small))); + + // Buffer too small even for the Quant: the writer is already in error when it reaches the Bytes + uint8_t tiny[1] = {}; + TEST_ASSERT_EQUAL(0, serialize(msg, tiny, sizeof(tiny))); +} + /* ---------------------------------------------------------------------------------------------- */ /* Reader */ /* ---------------------------------------------------------------------------------------------- */ @@ -614,6 +660,25 @@ void test_serialize() { TEST_ASSERT_EQUAL(0, serialize(big, small, sizeof(small))); } +void test_serialize_overflow() { + // serialize() must report 0 when the destination cannot hold the message. Exercising it for every + // message type also covers the failure side of serialize()'s "isOk() ? size : 0" per instantiation. + uint8_t buffer[1] = {}; + + MsgPing ping{}; + TEST_ASSERT_EQUAL(0, serialize(ping, buffer, 0)); // needs 1 byte + + Msg2 custom{}; + custom.q = 1.0f; + TEST_ASSERT_EQUAL(0, serialize(custom, buffer, sizeof(buffer))); // needs >= 3 bytes + + MsgArray arr{}; + TEST_ASSERT_EQUAL(0, serialize(arr, buffer, sizeof(buffer))); // needs 6 bytes + + MsgNested nested{}; + TEST_ASSERT_EQUAL(0, serialize(nested, buffer, sizeof(buffer))); // needs 2 bytes +} + void test_deserialize() { const uint8_t buffer[1] = {77}; @@ -655,6 +720,21 @@ void test_serialize_with_header() { TEST_ASSERT_EQUAL(expected[i], buffer[i]); } + // deserializeWithHeader round-trip and every header-mismatch path (covers the MsgData header check) + MsgData out{}; + TEST_ASSERT_TRUE(deserializeWithHeader(out, buffer, written)); + TEST_ASSERT_EQUAL_UINT16(0x1234, out.value); + + buffer[0] = 0x00; // wrong ID + TEST_ASSERT_FALSE(deserializeWithHeader(out, buffer, written)); + buffer[0] = MsgData::ID; + + buffer[1] = 0x00; // wrong VERSION + TEST_ASSERT_FALSE(deserializeWithHeader(out, buffer, written)); + buffer[1] = MsgData::VERSION; + + TEST_ASSERT_FALSE(deserializeWithHeader(out, buffer, 1)); // too short for the header + // Buffer too small for header + fields -> returns 0 TEST_ASSERT_EQUAL(0, serializeWithHeader(in, buffer, 3)); } @@ -700,54 +780,69 @@ void test_peek_id_and_version() { TEST_ASSERT_FALSE(peekVersion(buffer, 1, version)); } +// Runs every dispatch path for a given handler: each candidate matching and not matching, unknown +// ID, wrong VERSION (per message), truncated body (per message), the fold short-circuit and the +// too-short frame. After it returns, a MsgPing and a MsgData frame have each been routed once. +template +void exerciseAllDispatchBranches(Handler& handler) { + uint8_t ping_frame[3] = {MsgPing::ID, MsgPing::VERSION, 7}; + uint8_t data_frame[8] = {MsgData::ID, MsgData::VERSION, 0x09, 0x03, 0, 0, 0, 0}; // value = 777 + + // MsgData matches (first candidate fails the ID check, second matches and deserializes) + TEST_ASSERT_TRUE((dispatch(data_frame, sizeof(data_frame), handler))); + + // MsgPing matches (first candidate matches, short-circuiting the fold before MsgData is tried) + TEST_ASSERT_TRUE((dispatch(ping_frame, sizeof(ping_frame), handler))); + + // Unknown ID -> no candidate matches + data_frame[0] = 0x00; + TEST_ASSERT_FALSE((dispatch(data_frame, sizeof(data_frame), handler))); + data_frame[0] = MsgData::ID; + + // Right ID, wrong VERSION (MsgData) -> rejected + data_frame[1] = 0xFF; + TEST_ASSERT_FALSE((dispatch(data_frame, sizeof(data_frame), handler))); + data_frame[1] = MsgData::VERSION; + + // Matching header but truncated body (MsgData needs 6 body bytes) -> rejected + TEST_ASSERT_FALSE((dispatch(data_frame, 3, handler))); + + // Right ID, wrong VERSION (MsgPing) -> rejected + ping_frame[1] = MsgPing::VERSION + 1; + TEST_ASSERT_FALSE((dispatch(ping_frame, sizeof(ping_frame), handler))); + ping_frame[1] = MsgPing::VERSION; + + // Matching header but truncated body (MsgPing needs 1 body byte) -> rejected + TEST_ASSERT_FALSE((dispatch(ping_frame, 2, handler))); + + // Too short for the header -> rejected + TEST_ASSERT_FALSE((dispatch(data_frame, 1, handler))); +} + void test_dispatch() { - FloatInt f_union; - f_union.f = 0.5f; + DispatchRecorder rec; + exerciseAllDispatchBranches(rec); - // [ID][VERSION][value LE][ratio LE] - uint8_t buffer[8] = { - MsgData::ID, - MsgData::VERSION, - 0x09, // value = 777 (LSB) - 0x03, // value (MSB) - uint8_t(f_union.u & 0xFF), - uint8_t((f_union.u >> 8) & 0xFF), - uint8_t((f_union.u >> 16) & 0xFF), - uint8_t((f_union.u >> 24) & 0xFF), - }; + TEST_ASSERT_TRUE(rec.ping_handled); + TEST_ASSERT_TRUE(rec.data_handled); + TEST_ASSERT_EQUAL_UINT8(7, rec.ping_counter); + TEST_ASSERT_EQUAL_UINT16(777, rec.data_value); +} +void test_dispatch_overloaded() { + // Overloaded is the ergonomic public handler; exercise every dispatch branch through it so both + // the API and its template instantiation are fully covered. bool ping_handled = false; bool data_handled = false; - - auto handler = Overloaded{ + auto handler = Overloaded{ [&](const MsgPing&) { ping_handled = true; }, - [&](const MsgData& m) { data_handled = (m.value == 777); }, + [&](const MsgData&) { data_handled = true; }, }; - // Matching ID and VERSION -> the right handler runs - TEST_ASSERT_TRUE((dispatch(buffer, sizeof(buffer), handler))); - TEST_ASSERT_FALSE(ping_handled); - TEST_ASSERT_TRUE(data_handled); + exerciseAllDispatchBranches(handler); - // Unknown ID -> no handler runs - data_handled = false; - buffer[0] = 0xFF; - TEST_ASSERT_FALSE((dispatch(buffer, sizeof(buffer), handler))); - TEST_ASSERT_FALSE(ping_handled); - TEST_ASSERT_FALSE(data_handled); - buffer[0] = MsgData::ID; - - // Wrong VERSION -> no handler runs - buffer[1] = MsgData::VERSION + 1; - TEST_ASSERT_FALSE((dispatch(buffer, sizeof(buffer), handler))); - buffer[1] = MsgData::VERSION; - - // Known header but truncated body -> rejected - TEST_ASSERT_FALSE((dispatch(buffer, sizeof(buffer) - 1, handler))); - - // Empty / header-only input -> rejected - TEST_ASSERT_FALSE((dispatch(buffer, 0, handler))); - TEST_ASSERT_FALSE((dispatch(buffer, 2, handler))); + TEST_ASSERT_TRUE(ping_handled); + TEST_ASSERT_TRUE(data_handled); } /* ---------------------------------------------------------------------------------------------- */ @@ -856,15 +951,11 @@ void test_round_trip_with_header_and_dispatch() { TEST_ASSERT_EQUAL_UINT16(in.value, out.value); TEST_ASSERT_EQUAL_FLOAT(in.ratio, out.ratio); - // Dispatch to the right handler - bool handled = false; - TEST_ASSERT_TRUE((dispatch(buffer, - written, - Overloaded{ - [&](const MsgPing&) {}, - [&](const MsgData& m) { handled = (m.value == 777); }, - }))); - TEST_ASSERT_TRUE(handled); + // Dispatch to the right handler (shared recorder type, see test_dispatch) + DispatchRecorder rec; + TEST_ASSERT_TRUE((dispatch(buffer, written, rec))); + TEST_ASSERT_TRUE(rec.data_handled); + TEST_ASSERT_EQUAL_UINT16(777, rec.data_value); } /* ---------------------------------------------------------------------------------------------- */ @@ -887,6 +978,7 @@ void test_stress_garbage_input() { stress_state = 0xA5A5A5A5; uint8_t buffer[24]; + DispatchRecorder rec; for (uint32_t i = 0; i < STRESS_ITERATIONS; i++) { const size_t len = nextRandom() % (sizeof(buffer) + 1); // 0..24 @@ -900,12 +992,7 @@ void test_stress_garbage_input() { deserialize(msg, buffer, len); TEST_ASSERT_TRUE(msg.b.getLength() <= msg.b.getCapacity()); - dispatch(buffer, - len, - Overloaded{ - [](const MsgPing&) {}, - [](const MsgData&) {}, - }); + dispatch(buffer, len, rec); } } @@ -1014,13 +1101,18 @@ void test_stress_truncate_and_corrupt() { } /* ---------------------------------------------------------------------------------------------- */ -/* setup / loop */ +/* Runners */ /* ---------------------------------------------------------------------------------------------- */ -void setup() { - Serial.begin(115200); - delay(2000); +void setUp(void) { + // set stuff up here +} + +void tearDown(void) { + // clean stuff up here +} +int runUnityTests(void) { UNITY_BEGIN(); // Custom types @@ -1034,6 +1126,7 @@ void setup() { RUN_TEST(test_writer_common_types); RUN_TEST(test_writer_custom_types); RUN_TEST(test_writer_overflow); + RUN_TEST(test_writer_bytes_overflow); // Reader RUN_TEST(test_reader_initial_state); @@ -1046,11 +1139,13 @@ void setup() { // Helpers / dispatch RUN_TEST(test_serialize); + RUN_TEST(test_serialize_overflow); RUN_TEST(test_deserialize); RUN_TEST(test_serialize_with_header); RUN_TEST(test_deserialize_with_header); RUN_TEST(test_peek_id_and_version); RUN_TEST(test_dispatch); + RUN_TEST(test_dispatch_overloaded); // Round-trip RUN_TEST(test_round_trip_common_types); @@ -1063,7 +1158,17 @@ void setup() { RUN_TEST(test_stress_round_trip); RUN_TEST(test_stress_truncate_and_corrupt); - UNITY_END(); + return UNITY_END(); } -void loop() {} \ No newline at end of file +// For native +int main(void) { return runUnityTests(); } + +// For Arduino framework +#ifdef ARDUINO +void setup() { + delay(2000); + runUnityTests(); +} +void loop() {} +#endif \ No newline at end of file From 80d630c300725237647605e1a323b8bf247d7937 Mon Sep 17 00:00:00 2001 From: alkonosst Date: Wed, 24 Jun 2026 12:09:07 -0400 Subject: [PATCH 2/3] feat: Add CMake support and basic native example --- CMakeLists.txt | 14 +++++ README.md | 23 +++++++- examples/BasicNative/BasicNative.cpp | 81 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 examples/BasicNative/BasicNative.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8b052ac --- /dev/null +++ b/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/README.md b/README.md index 1123a17..cc09fb3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ PlatformIO Registry

+ + Coverage + License @@ -35,6 +38,7 @@ - [Installation](#installation) - [PlatformIO](#platformio) - [Arduino IDE](#arduino-ide) + - [CMake](#cmake) - [Usage](#usage) - [Including the library](#including-the-library) - [Namespace](#namespace) @@ -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. @@ -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 diff --git a/examples/BasicNative/BasicNative.cpp b/examples/BasicNative/BasicNative.cpp new file mode 100644 index 0000000..24ea362 --- /dev/null +++ b/examples/BasicNative/BasicNative.cpp @@ -0,0 +1,81 @@ +/** + * SPDX-FileCopyrightText: 2026 Maximiliano Ramirez + * + * 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 + +#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 + 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()] = {}; + + // 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; +} From 5053066e31ec88bc752c14bda51db3b846a742fe Mon Sep 17 00:00:00 2001 From: alkonosst Date: Wed, 24 Jun 2026 12:10:51 -0400 Subject: [PATCH 3/3] chore: Format tests --- test/test_bytepack.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_bytepack.cpp b/test/test_bytepack.cpp index ab42952..f25790e 100644 --- a/test/test_bytepack.cpp +++ b/test/test_bytepack.cpp @@ -662,7 +662,8 @@ void test_serialize() { void test_serialize_overflow() { // serialize() must report 0 when the destination cannot hold the message. Exercising it for every - // message type also covers the failure side of serialize()'s "isOk() ? size : 0" per instantiation. + // message type also covers the failure side of serialize()'s "isOk() ? size : 0" per + // instantiation. uint8_t buffer[1] = {}; MsgPing ping{}; @@ -720,7 +721,8 @@ void test_serialize_with_header() { TEST_ASSERT_EQUAL(expected[i], buffer[i]); } - // deserializeWithHeader round-trip and every header-mismatch path (covers the MsgData header check) + // deserializeWithHeader round-trip and every header-mismatch path (covers the MsgData header + // check) MsgData out{}; TEST_ASSERT_TRUE(deserializeWithHeader(out, buffer, written)); TEST_ASSERT_EQUAL_UINT16(0x1234, out.value);