diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..66d5e4e
--- /dev/null
+++ b/CMakeLists.txt
@@ -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)
\ No newline at end of file
diff --git a/README.md b/README.md
index f0d9d01..607303d 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@
+
+
+
@@ -35,6 +38,7 @@
- [Installation](#installation)
- [PlatformIO](#platformio)
- [Arduino IDE](#arduino-ide)
+ - [CMake](#cmake)
- [Usage](#usage)
- [Including the library](#including-the-library)
- [Namespace](#namespace)
@@ -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.
@@ -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
diff --git a/examples/BasicNative/BasicNative.cpp b/examples/BasicNative/BasicNative.cpp
new file mode 100644
index 0000000..de57baa
--- /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
+
+// Maximum payload this link accepts; both the TX buffer and the Decoder derive from it
+constexpr size_t MAX_PAYLOAD = 32;
+
+ByteFrame::Decoder 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;
+}
diff --git a/examples/CrcSelection/CrcSelection.ino b/examples/CrcSelection/CrcSelection.ino
index 8900928..d27f36f 100644
--- a/examples/CrcSelection/CrcSelection.ino
+++ b/examples/CrcSelection/CrcSelection.ino
@@ -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().
-template
-void showPolicy(const char* name, const uint8_t* payload, const size_t payload_size) {
+// clang-format off
+template void showPolicy(const char* name, const uint8_t* payload, const size_t payload_size) {
+ // clang-format on
uint8_t frame[ByteFrame::getMaxEncodedSize(MAX_PAYLOAD)] = {};
const size_t frame_size = ByteFrame::encode(payload, payload_size, frame, sizeof(frame));
diff --git a/library.json b/library.json
index 70cb8ba..571cd1c 100644
--- a/library.json
+++ b/library.json
@@ -1,6 +1,6 @@
{
"name": "ByteFrame",
- "version": "0.1.0",
+ "version": "0.2.0",
"authors": {
"name": "Maximiliano Ramirez",
"email": "maximiliano.ramirezbravo@gmail.com"
@@ -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"]
diff --git a/library.properties b/library.properties
index bd76193..311efef 100644
--- a/library.properties
+++ b/library.properties
@@ -1,5 +1,5 @@
name=ByteFrame
-version=0.1.0
+version=0.2.0
author=Maximiliano Ramirez
maintainer=Maximiliano Ramirez
sentence=Header-only frame delimiting library with zero dynamic memory allocation.
diff --git a/platformio.ini b/platformio.ini
index 0ffded2..4155d46 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -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
@@ -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/; 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/src/ByteFrame.h b/src/ByteFrame.h
index bee1f79..3e0aabb 100644
--- a/src/ByteFrame.h
+++ b/src/ByteFrame.h
@@ -458,9 +458,12 @@ class Decoder {
for (size_t i = 0; i < payload_size; i++)
crc = Crc::update(crc, _buffer[i]);
- if (Crc::finalize(crc) != received) {
- _stats.crc_errors++;
- return;
+ // With NoCrc (Crc::SIZE == 0) both received and Crc::finalize() are always 0, so this check can
+ // never fail: the branch and its body are unreachable for that policy and excluded from
+ // coverage
+ if (Crc::finalize(crc) != received) { // GCOVR_EXCL_BR_LINE
+ _stats.crc_errors++; // GCOVR_EXCL_LINE
+ return; // GCOVR_EXCL_LINE
}
_payload_size = payload_size;
@@ -596,18 +599,24 @@ size_t decode(const uint8_t* frame, const size_t frame_size, uint8_t* payload,
// literal block bytes
for (size_t i = append_zero ? 0 : 1; i <= block; i++) {
const uint8_t byte = (i == 0) ? uint8_t(0x00) : frame[in++];
- if (produced < payload_size) {
+
+ // With NoCrc (Crc::SIZE == 0) payload_size == total, so produced never reaches it: the else
+ // (rebuilding the received CRC) is unreachable for that policy and excluded from coverage
+ if (produced < payload_size) { // GCOVR_EXCL_BR_LINE
payload[produced] = byte;
crc = Crc::update(crc, byte);
} else {
- received = crc_type(received | (crc_type(byte) << (8 * (produced - payload_size))));
+ // clang-format off
+ received = crc_type( received | (crc_type(byte) << (8 * (produced - payload_size)))); // GCOVR_EXCL_LINE
+ // clang-format on
}
produced++;
}
append_zero = (code != 0xFF);
}
- if (Crc::finalize(crc) != received) return 0;
+ // Unreachable with NoCrc (Crc::SIZE == 0): received and Crc::finalize() are both 0 (see above)
+ if (Crc::finalize(crc) != received) return 0; // GCOVR_EXCL_BR_LINE
return payload_size;
}
diff --git a/test/test_byteframe.cpp b/test/test_byteframe.cpp
index 22404e9..faaabdc 100644
--- a/test/test_byteframe.cpp
+++ b/test/test_byteframe.cpp
@@ -28,10 +28,14 @@
* garbage input (no crash, invariants hold), single-byte corruption of valid frames.
*/
-#include
+#ifdef ARDUINO
+# include
+#endif
+
#include
#include
+using namespace ByteFrame;
/* ---------------------------------------------------------------------------------------------- */
/* Helpers */
@@ -40,7 +44,7 @@
// Feeds a whole buffer byte by byte and returns true if exactly one valid frame came out,
// completed by the very last byte
template
-bool feedAll(ByteFrame::Decoder& decoder, const uint8_t* data, const size_t size) {
+bool feedAll(Decoder& decoder, const uint8_t* data, const size_t size) {
for (size_t i = 0; i < size; i++) {
const bool frame = decoder.feed(data[i]);
if (frame) return i == size - 1;
@@ -63,11 +67,11 @@ typename Crc::value_type crcOf(const uint8_t* data, const size_t size) {
void test_get_max_encoded_size() {
// data = payload + 2 (CRC); frame = data + ceil(data / 254) (COBS) + 1 (delimiter)
- constexpr size_t size_1 = ByteFrame::getMaxEncodedSize(1); // 3 + 1 + 1
- constexpr size_t size_10 = ByteFrame::getMaxEncodedSize(10); // 12 + 1 + 1
- constexpr size_t size_252 = ByteFrame::getMaxEncodedSize(252); // 254 + 1 + 1
- constexpr size_t size_253 = ByteFrame::getMaxEncodedSize(253); // 255 + 2 + 1
- constexpr size_t size_300 = ByteFrame::getMaxEncodedSize(300); // 302 + 2 + 1
+ constexpr size_t size_1 = getMaxEncodedSize(1); // 3 + 1 + 1
+ constexpr size_t size_10 = getMaxEncodedSize(10); // 12 + 1 + 1
+ constexpr size_t size_252 = getMaxEncodedSize(252); // 254 + 1 + 1
+ constexpr size_t size_253 = getMaxEncodedSize(253); // 255 + 2 + 1
+ constexpr size_t size_300 = getMaxEncodedSize(300); // 302 + 2 + 1
TEST_ASSERT_EQUAL(5, size_1);
TEST_ASSERT_EQUAL(14, size_10);
@@ -87,8 +91,8 @@ void test_encode_known_vector() {
const uint8_t expected[13] =
{0x0C, '1', '2', '3', '4', '5', '6', '7', '8', '9', 0xB1, 0x29, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_EQUAL(sizeof(expected), written);
TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, frame, sizeof(expected));
@@ -97,7 +101,7 @@ void test_encode_known_vector() {
void test_encode_payload_with_zeros() {
// data = [0x11, 0x00, 0x22, crc_lo, crc_hi]: the zero splits the data in two COBS blocks
const uint8_t payload[3] = {0x11, 0x00, 0x22};
- const uint16_t crc = crcOf(payload, sizeof(payload));
+ const uint16_t crc = crcOf(payload, sizeof(payload));
const uint8_t crc_lo = uint8_t(crc & 0xFF);
const uint8_t crc_hi = uint8_t(crc >> 8);
@@ -107,8 +111,8 @@ void test_encode_payload_with_zeros() {
const uint8_t expected[7] = {0x02, 0x11, 0x04, 0x22, crc_lo, crc_hi, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_EQUAL(7, written);
TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, frame, 7);
@@ -117,7 +121,7 @@ void test_encode_payload_with_zeros() {
void test_encode_leading_zero() {
// data = [0x00, crc_lo, crc_hi]: a leading zero produces an empty first block (code 0x01)
const uint8_t payload[1] = {0x00};
- const uint16_t crc = crcOf(payload, sizeof(payload));
+ const uint16_t crc = crcOf(payload, sizeof(payload));
const uint8_t crc_lo = uint8_t(crc & 0xFF);
const uint8_t crc_hi = uint8_t(crc >> 8);
@@ -126,8 +130,8 @@ void test_encode_leading_zero() {
const uint8_t expected[5] = {0x01, 0x03, crc_lo, crc_hi, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_EQUAL(5, written);
TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, frame, 5);
@@ -141,7 +145,7 @@ void test_encode_block_boundary() {
payload[i] = uint8_t(1 + (i % 255)); // never zero
}
- const uint16_t crc = crcOf(payload, sizeof(payload));
+ const uint16_t crc = crcOf(payload, sizeof(payload));
const uint8_t crc_lo = uint8_t(crc & 0xFF);
const uint8_t crc_hi = uint8_t(crc >> 8);
@@ -149,8 +153,8 @@ void test_encode_block_boundary() {
TEST_ASSERT_NOT_EQUAL(0x00, crc_lo);
TEST_ASSERT_NOT_EQUAL(0x00, crc_hi);
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_EQUAL(256, written); // 1 (code) + 254 (data) + 1 (delimiter)
TEST_ASSERT_EQUAL_HEX8(0xFF, frame[0]);
@@ -165,11 +169,11 @@ void test_encode_block_boundary() {
payload2[i] = uint8_t(1 + (i % 255));
}
- uint8_t frame2[ByteFrame::getMaxEncodedSize(sizeof(payload2))] = {};
- const size_t written2 = ByteFrame::encode(payload2, sizeof(payload2), frame2, sizeof(frame2));
+ uint8_t frame2[getMaxEncodedSize(sizeof(payload2))] = {};
+ const size_t written2 = encode(payload2, sizeof(payload2), frame2, sizeof(frame2));
TEST_ASSERT_TRUE(written2 > 0);
- TEST_ASSERT_TRUE(written2 <= ByteFrame::getMaxEncodedSize(sizeof(payload2)));
+ TEST_ASSERT_TRUE(written2 <= getMaxEncodedSize(sizeof(payload2)));
TEST_ASSERT_EQUAL_HEX8(0xFF, frame2[0]);
}
@@ -177,11 +181,11 @@ void test_encode_structural_properties() {
// Whatever the payload, a frame ends with exactly one delimiter and contains no other zero
const uint8_t payload[6] = {0x00, 0xFF, 0x00, 0x00, 0x01, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- TEST_ASSERT_TRUE(written <= ByteFrame::getMaxEncodedSize(sizeof(payload)));
+ TEST_ASSERT_TRUE(written <= getMaxEncodedSize(sizeof(payload)));
TEST_ASSERT_EQUAL_HEX8(0x00, frame[written - 1]);
for (size_t i = 0; i < written - 1; i++) {
TEST_ASSERT_NOT_EQUAL(0x00, frame[i]);
@@ -193,14 +197,14 @@ void test_encode_rejects_invalid_input() {
const uint8_t payload[4] = {1, 2, 3, 4};
// Empty payload
- TEST_ASSERT_EQUAL(0, ByteFrame::encode(payload, 0, frame, sizeof(frame)));
+ TEST_ASSERT_EQUAL(0, encode(payload, 0, frame, sizeof(frame)));
// Exact fit works, one byte less does not (and returns 0)
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- TEST_ASSERT_EQUAL(written, ByteFrame::encode(payload, sizeof(payload), frame, written));
- TEST_ASSERT_EQUAL(0, ByteFrame::encode(payload, sizeof(payload), frame, written - 1));
- TEST_ASSERT_EQUAL(0, ByteFrame::encode(payload, sizeof(payload), frame, 0));
+ TEST_ASSERT_EQUAL(written, encode(payload, sizeof(payload), frame, written));
+ TEST_ASSERT_EQUAL(0, encode(payload, sizeof(payload), frame, written - 1));
+ TEST_ASSERT_EQUAL(0, encode(payload, sizeof(payload), frame, 0));
}
/* ---------------------------------------------------------------------------------------------- */
@@ -212,13 +216,12 @@ void test_encoder_streaming_matches_one_shot() {
// bytes as the one-shot encode() and decode back to the original
const uint8_t payload[9] = {0x11, 0x00, 0x22, 0x00, 0xAB, 0xCD, 0x00, 0xEE, 0xFF};
- uint8_t one_shot[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t one_shot_size =
- ByteFrame::encode(payload, sizeof(payload), one_shot, sizeof(one_shot));
+ uint8_t one_shot[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t one_shot_size = encode(payload, sizeof(payload), one_shot, sizeof(one_shot));
TEST_ASSERT_TRUE(one_shot_size > 0);
- uint8_t streamed[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- ByteFrame::Encoder<> encoder(streamed, sizeof(streamed));
+ uint8_t streamed[getMaxEncodedSize(sizeof(payload))] = {};
+ Encoder<> encoder(streamed, sizeof(streamed));
encoder.feed(payload, 3); // first chunk
encoder.feed(payload + 3, 4); // second chunk
encoder.feed(payload + 7, 2); // third chunk
@@ -227,7 +230,7 @@ void test_encoder_streaming_matches_one_shot() {
TEST_ASSERT_EQUAL(one_shot_size, streamed_size);
TEST_ASSERT_EQUAL_HEX8_ARRAY(one_shot, streamed, one_shot_size);
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
TEST_ASSERT_TRUE(feedAll(decoder, streamed, streamed_size));
TEST_ASSERT_EQUAL(sizeof(payload), decoder.getPayloadSize());
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), sizeof(payload));
@@ -241,12 +244,11 @@ void test_encoder_streaming_block_boundary() {
payload[i] = uint8_t(1 + (i % 255)); // never zero
}
- uint8_t one_shot[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t one_shot_size =
- ByteFrame::encode(payload, sizeof(payload), one_shot, sizeof(one_shot));
+ uint8_t one_shot[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t one_shot_size = encode(payload, sizeof(payload), one_shot, sizeof(one_shot));
- uint8_t streamed[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- ByteFrame::Encoder<> encoder(streamed, sizeof(streamed));
+ uint8_t streamed[getMaxEncodedSize(sizeof(payload))] = {};
+ Encoder<> encoder(streamed, sizeof(streamed));
encoder.feed(payload, 100);
encoder.feed(payload + 100, sizeof(payload) - 100);
const size_t streamed_size = encoder.finalize();
@@ -258,8 +260,8 @@ void test_encoder_streaming_block_boundary() {
void test_encoder_reuse_and_limits() {
const uint8_t payload[4] = {0x01, 0x02, 0x03, 0x04};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- ByteFrame::Encoder<> encoder(frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ Encoder<> encoder(frame, sizeof(frame));
// finalize() with nothing fed yields no frame
TEST_ASSERT_EQUAL(0, encoder.finalize());
@@ -277,8 +279,8 @@ void test_encoder_reuse_and_limits() {
TEST_ASSERT_EQUAL(size1, size2);
// A capacity one byte too small overflows: finalize() returns 0 and isOk() turns false
- uint8_t tight[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- ByteFrame::Encoder<> small(tight, size1 - 1);
+ uint8_t tight[getMaxEncodedSize(sizeof(payload))] = {};
+ Encoder<> small(tight, size1 - 1);
small.feed(payload, sizeof(payload));
TEST_ASSERT_EQUAL(0, small.finalize());
TEST_ASSERT_FALSE(small.isOk());
@@ -289,7 +291,7 @@ void test_encoder_reuse_and_limits() {
/* ---------------------------------------------------------------------------------------------- */
void test_decoder_initial_state() {
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
TEST_ASSERT_FALSE(decoder.isFrameAvailable());
TEST_ASSERT_EQUAL(0, decoder.getPayloadSize());
@@ -303,11 +305,11 @@ void test_decoder_initial_state() {
void test_decoder_byte_feed_round_trip() {
const uint8_t payload[5] = {0xDE, 0xAD, 0xBE, 0xEF, 0x42};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// The frame becomes available exactly at the delimiter, not before
for (size_t i = 0; i < written - 1; i++) {
@@ -335,17 +337,15 @@ void test_decoder_chunk_feed_multiple_frames() {
const uint8_t payload2[4] = {0xAA, 0x00, 0xBB, 0x00};
// Two frames back to back in a single buffer, as they would arrive from a stream
- uint8_t stream[ByteFrame::getMaxEncodedSize(sizeof(payload1)) +
- ByteFrame::getMaxEncodedSize(sizeof(payload2))] = {};
+ uint8_t stream[getMaxEncodedSize(sizeof(payload1)) + getMaxEncodedSize(sizeof(payload2))] = {};
- const size_t size1 = ByteFrame::encode(payload1, sizeof(payload1), stream, sizeof(stream));
+ const size_t size1 = encode(payload1, sizeof(payload1), stream, sizeof(stream));
TEST_ASSERT_TRUE(size1 > 0);
- const size_t size2 =
- ByteFrame::encode(payload2, sizeof(payload2), stream + size1, sizeof(stream) - size1);
+ const size_t size2 = encode(payload2, sizeof(payload2), stream + size1, sizeof(stream) - size1);
TEST_ASSERT_TRUE(size2 > 0);
const size_t total = size1 + size2;
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// First call consumes up to and including the delimiter of the first frame
size_t consumed = decoder.feed(stream, total);
@@ -363,7 +363,7 @@ void test_decoder_chunk_feed_multiple_frames() {
}
void test_decoder_max_payload() {
- ByteFrame::Decoder<16> decoder;
+ Decoder<16> decoder;
// A payload of exactly MaxPayload bytes is accepted
uint8_t payload[17];
@@ -371,21 +371,21 @@ void test_decoder_max_payload() {
payload[i] = uint8_t(i + 1);
}
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- size_t written = ByteFrame::encode(payload, 16, frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ size_t written = encode(payload, 16, frame, sizeof(frame));
TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
TEST_ASSERT_EQUAL(16, decoder.getPayloadSize());
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), 16);
TEST_ASSERT_EQUAL(0, decoder.getStats().overflows);
// One byte more overflows: no frame, counted once, parser recovers afterwards
- written = ByteFrame::encode(payload, 17, frame, sizeof(frame));
+ written = encode(payload, 17, frame, sizeof(frame));
TEST_ASSERT_FALSE(feedAll(decoder, frame, written));
TEST_ASSERT_FALSE(decoder.isFrameAvailable());
TEST_ASSERT_EQUAL(1, decoder.getStats().overflows);
// A valid frame right after the oversized one decodes normally
- written = ByteFrame::encode(payload, 16, frame, sizeof(frame));
+ written = encode(payload, 16, frame, sizeof(frame));
TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
TEST_ASSERT_EQUAL(16, decoder.getPayloadSize());
TEST_ASSERT_EQUAL(1, decoder.getStats().overflows);
@@ -394,28 +394,28 @@ void test_decoder_max_payload() {
void test_decoder_crc_error() {
const uint8_t payload[4] = {0x10, 0x20, 0x30, 0x40};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
// Corrupt one payload byte inside the frame, keeping it non-zero so the COBS structure (and
// therefore the frame boundary) is preserved: the CRC must catch it
frame[2] = (frame[2] == 0x7F) ? 0x7E : 0x7F;
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
TEST_ASSERT_FALSE(feedAll(decoder, frame, written));
TEST_ASSERT_FALSE(decoder.isFrameAvailable());
TEST_ASSERT_EQUAL(1, decoder.getStats().crc_errors);
TEST_ASSERT_EQUAL(0, decoder.getStats().malformed);
// The decoder keeps working after the error
- const size_t written2 = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ const size_t written2 = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(feedAll(decoder, frame, written2));
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), sizeof(payload));
}
void test_decoder_malformed_frames() {
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// Code byte announces 4 data bytes but the delimiter arrives mid-block
TEST_ASSERT_FALSE(decoder.feed(uint8_t(0x05)));
@@ -435,11 +435,11 @@ void test_decoder_malformed_frames() {
}
void test_decoder_idle_delimiters_ignored() {
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// Idle delimiters (empty frames) are not data and not errors
for (int i = 0; i < 10; i++) {
- TEST_ASSERT_FALSE(decoder.feed(ByteFrame::DELIMITER));
+ TEST_ASSERT_FALSE(decoder.feed(DELIMITER));
}
TEST_ASSERT_EQUAL(0, decoder.getStats().crc_errors);
@@ -450,11 +450,11 @@ void test_decoder_idle_delimiters_ignored() {
void test_decoder_resync_mid_stream() {
const uint8_t payload[6] = {0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// Join the stream in the middle of a frame: the partial frame is dropped (one error of some
// kind), and the next complete frame decodes normally
@@ -471,11 +471,11 @@ void test_decoder_resync_mid_stream() {
void test_decoder_reset() {
const uint8_t payload[4] = {0x11, 0x22, 0x33, 0x44};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
// Feed half a frame, reset, then feed a complete frame: it must decode cleanly with no errors
for (size_t i = 0; i < written / 2; i++) {
@@ -496,10 +496,10 @@ void test_decoder_reset() {
void test_round_trip_all_sizes() {
constexpr size_t MAX_PAYLOAD = 64;
- ByteFrame::Decoder decoder;
+ Decoder decoder;
uint8_t payload[MAX_PAYLOAD];
- uint8_t frame[ByteFrame::getMaxEncodedSize(MAX_PAYLOAD)] = {};
+ uint8_t frame[getMaxEncodedSize(MAX_PAYLOAD)] = {};
for (size_t size = 1; size <= MAX_PAYLOAD; size++) {
// Pattern with zeros sprinkled in to exercise the COBS paths
@@ -507,9 +507,9 @@ void test_round_trip_all_sizes() {
payload[i] = (i % 3 == 0) ? 0x00 : uint8_t(i);
}
- const size_t written = ByteFrame::encode(payload, size, frame, sizeof(frame));
+ const size_t written = encode(payload, size, frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
- TEST_ASSERT_TRUE(written <= ByteFrame::getMaxEncodedSize(size));
+ TEST_ASSERT_TRUE(written <= getMaxEncodedSize(size));
TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
TEST_ASSERT_EQUAL(size, decoder.getPayloadSize());
@@ -529,33 +529,33 @@ void test_round_trip_all_sizes() {
void test_crc_policies_check_values() {
// Standard CRC check values over the ASCII string "123456789"
const uint8_t data[9] = {'1', '2', '3', '4', '5', '6', '7', '8', '9'};
- TEST_ASSERT_EQUAL_HEX8(0xF4, crcOf(data, sizeof(data)));
- TEST_ASSERT_EQUAL_HEX16(0x29B1, crcOf(data, sizeof(data)));
- TEST_ASSERT_EQUAL_HEX32(0xCBF43926, crcOf(data, sizeof(data)));
+ TEST_ASSERT_EQUAL_HEX8(0xF4, crcOf(data, sizeof(data)));
+ TEST_ASSERT_EQUAL_HEX16(0x29B1, crcOf(data, sizeof(data)));
+ TEST_ASSERT_EQUAL_HEX32(0xCBF43926, crcOf(data, sizeof(data)));
}
void test_decode_one_shot() {
const uint8_t payload[7] = {0x11, 0x00, 0x22, 0xFF, 0x00, 0xAB, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
// Round-trips: only the payload is written out, so the buffer is just the payload size
uint8_t out[sizeof(payload)] = {};
- TEST_ASSERT_EQUAL(sizeof(payload), ByteFrame::decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL(sizeof(payload), decode(frame, written, out, sizeof(out)));
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, sizeof(payload));
// The trailing delimiter is optional: decoding the frame without it yields the same payload
- TEST_ASSERT_EQUAL(sizeof(payload), ByteFrame::decode(frame, written - 1, out, sizeof(out)));
+ TEST_ASSERT_EQUAL(sizeof(payload), decode(frame, written - 1, out, sizeof(out)));
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, sizeof(payload));
// A buffer too small for the payload is rejected
- TEST_ASSERT_EQUAL(0, ByteFrame::decode(frame, written, out, sizeof(out) - 1));
+ TEST_ASSERT_EQUAL(0, decode(frame, written, out, sizeof(out) - 1));
// Corrupting a payload byte (kept non-zero) is caught by the CRC
frame[1] = (frame[1] == 0x7F) ? 0x7E : 0x7F;
- TEST_ASSERT_EQUAL(0, ByteFrame::decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL(0, decode(frame, written, out, sizeof(out)));
}
// Encodes a fixed payload and decodes it back through both decode paths with a given CRC policy
@@ -563,38 +563,258 @@ template
void roundTripWithCrc() {
const uint8_t payload[7] = {0x11, 0x00, 0x22, 0xFF, 0x00, 0xAB, 0x00};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
// One-shot decode
uint8_t out[sizeof(payload)] = {};
- TEST_ASSERT_EQUAL(sizeof(payload), ByteFrame::decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL(sizeof(payload), decode(frame, written, out, sizeof(out)));
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, sizeof(payload));
// Streaming decoder with the same policy
- ByteFrame::Decoder<32, Crc> decoder;
+ Decoder<32, Crc> decoder;
TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
TEST_ASSERT_EQUAL(sizeof(payload), decoder.getPayloadSize());
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), sizeof(payload));
}
void test_crc_variants_round_trip() {
- roundTripWithCrc();
- roundTripWithCrc();
- roundTripWithCrc();
- roundTripWithCrc();
+ roundTripWithCrc();
+ roundTripWithCrc();
+ roundTripWithCrc();
+ roundTripWithCrc();
// The CRC width drives the encoded size: a wider CRC yields a larger frame
- const size_t no_crc = ByteFrame::getMaxEncodedSize(8);
- const size_t crc8 = ByteFrame::getMaxEncodedSize(8);
- const size_t crc16 = ByteFrame::getMaxEncodedSize(8);
- const size_t crc32 = ByteFrame::getMaxEncodedSize(8);
+ const size_t no_crc = getMaxEncodedSize(8);
+ const size_t crc8 = getMaxEncodedSize(8);
+ const size_t crc16 = getMaxEncodedSize(8);
+ const size_t crc32 = getMaxEncodedSize(8);
TEST_ASSERT_TRUE(no_crc < crc8);
TEST_ASSERT_TRUE(crc8 < crc16);
TEST_ASSERT_TRUE(crc16 < crc32);
}
+/* ---------------------------------------------------------------------------------------------- */
+/* All paths across CRC policies */
+/* ---------------------------------------------------------------------------------------------- */
+
+/**
+ * Encoder, Decoder, encode(), decode() and getMaxEncodedSize() are templates: gcov tracks every CRC
+ * instantiation separately, so a branch exercised only for the default Crc16CcittFalse still counts
+ * as uncovered for NoCrc, Crc8Smbus and Crc32IsoHdlc. The templated helpers below drive every error
+ * and edge path and are instantiated once per policy so each instantiation reaches full coverage.
+ */
+
+// Encode a payload that crosses a full COBS block and contains zeros into every output-buffer
+// capacity from 0 to the worst case. The full capacity exercises the happy path; each smaller
+// capacity trips a different overflow branch (literal byte, encoded zero, full-block successor, CRC
+// bytes, trailing delimiter). Every frame that is produced must round-trip back to the payload.
+template
+void exerciseEncoderOverflow() {
+ constexpr size_t N = 300; // > 254 so the payload crosses a full COBS block boundary
+ uint8_t payload[N];
+ for (size_t i = 0; i < N; i++) {
+ payload[i] = uint8_t(1 + (i % 254)); // never zero
+ }
+ payload[260] = 0x00; // zeros past the first block boundary (encoded-zero overflow paths)
+ payload[280] = 0x00;
+
+ uint8_t frame[getMaxEncodedSize(N)] = {};
+ uint8_t out[N] = {};
+ bool produced = false;
+ bool overflowed = false;
+
+ for (size_t cap = 0; cap <= sizeof(frame); cap++) {
+ const size_t written = encode(payload, N, frame, cap);
+ if (written == 0) {
+ overflowed = true;
+ continue;
+ }
+ produced = true;
+ TEST_ASSERT_TRUE(written <= getMaxEncodedSize(N));
+ TEST_ASSERT_EQUAL(N, decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, N);
+ }
+
+ TEST_ASSERT_TRUE(produced);
+ TEST_ASSERT_TRUE(overflowed);
+
+ // finalize() on a fresh encoder with nothing fed yields no frame (the "empty" guard)
+ Encoder empty(frame, sizeof(frame));
+ TEST_ASSERT_EQUAL(0, empty.finalize());
+}
+
+// A payload whose data (payload + CRC) is exactly 254 bytes encodes to a single full COBS block
+// (code 0xFF) with no trailing code byte: this exercises the block-boundary close at finalize().
+template
+void exerciseEncoderFullBlock() {
+ constexpr size_t payload_size = 254 - Crc::SIZE; // data == 254 == one block
+ uint8_t payload[payload_size] = {};
+ uint8_t frame[getMaxEncodedSize(payload_size)] = {};
+
+ size_t written = 0;
+ for (unsigned seed = 0; seed < 256; seed++) {
+ for (size_t i = 0; i < payload_size; i++) {
+ payload[i] = uint8_t(1 + ((i + seed) % 254));
+ }
+ written = encode(payload, payload_size, frame, sizeof(frame));
+ // 1 code byte + 254 data bytes + delimiter == 256: reached only when the last data byte (the
+ // top CRC byte) is non-zero, so the block closes "full" instead of via an encoded zero.
+ if (written == 256 && frame[0] == 0xFF) break;
+ }
+
+ TEST_ASSERT_EQUAL(256, written);
+ TEST_ASSERT_EQUAL_HEX8(0xFF, frame[0]);
+ TEST_ASSERT_EQUAL_HEX8(DELIMITER, frame[written - 1]);
+
+ uint8_t out[payload_size] = {};
+ TEST_ASSERT_EQUAL(payload_size, decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, payload_size);
+}
+
+// Drive every Decoder path: idle delimiter, happy frame, mid-block and too-short malformed frames,
+// overflow on a literal byte and on an implicit inter-block zero, recovery, and a CRC mismatch.
+// Templated on MaxPayload as well, because each Decoder is a distinct gcov
+// instantiation that must be exercised in full.
+template
+void exerciseDecoderPaths() {
+ Decoder decoder;
+
+ // getPayloadSize() before any frame is available -> 0
+ TEST_ASSERT_EQUAL(0, decoder.getPayloadSize());
+
+ // Idle delimiter: an empty frame is ignored, not an error
+ TEST_ASSERT_FALSE(decoder.feed(DELIMITER));
+
+ // Happy path
+ const uint8_t payload[8] = {0x11, 0x00, 0x22, 0xFF, 0x00, 0xAB, 0xCD, 0x00};
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
+ TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
+ TEST_ASSERT_EQUAL(sizeof(payload), decoder.getPayloadSize());
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), sizeof(payload));
+ TEST_ASSERT_EQUAL(1, decoder.getStats().frames_ok);
+
+ // Malformed: the delimiter arrives in the middle of a block
+ decoder.feed(uint8_t(0x05)); // announces 4 data bytes
+ decoder.feed(uint8_t(0x11));
+ decoder.feed(uint8_t(0x22));
+ TEST_ASSERT_FALSE(decoder.feed(DELIMITER));
+ TEST_ASSERT_EQUAL(1, decoder.getStats().malformed);
+
+ // Malformed: a frame decoding to fewer than 1 + Crc::SIZE bytes is too short (only meaningful
+ // when the policy carries a CRC; with NoCrc a single decoded byte is already a valid frame)
+ if (Crc::SIZE > 0) {
+ decoder.feed(uint8_t(0x02)); // announces 1 data byte
+ decoder.feed(uint8_t(0x41));
+ TEST_ASSERT_FALSE(decoder.feed(DELIMITER));
+ TEST_ASSERT_EQUAL(2, decoder.getStats().malformed);
+ }
+
+ // Overflow on a literal byte: a run of non-zero bytes longer than the buffer
+ uint8_t big[MaxPayload + 8];
+ for (size_t i = 0; i < sizeof(big); i++) {
+ big[i] = uint8_t(1 + (i % 254));
+ }
+ uint8_t bigframe[getMaxEncodedSize(sizeof(big))] = {};
+ written = encode(big, sizeof(big), bigframe, sizeof(bigframe));
+ TEST_ASSERT_FALSE(feedAll(decoder, bigframe, written));
+ TEST_ASSERT_EQUAL(1, decoder.getStats().overflows);
+
+ // The decoder recovers after the overflow
+ written = encode(payload, sizeof(payload), frame, sizeof(frame));
+ TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
+
+ // Overflow exactly on an implicit inter-block zero: a zero placed right at the buffer limit, so
+ // the buffer fills on the literal run and the next push is the implied zero between blocks
+ uint8_t big_zero[MaxPayload + 8];
+ for (size_t i = 0; i < sizeof(big_zero); i++) {
+ big_zero[i] = uint8_t(1 + (i % 254));
+ }
+ big_zero[MaxPayload + Crc::SIZE] = 0x00;
+ uint8_t bigzframe[getMaxEncodedSize(sizeof(big_zero))] = {};
+ written = encode(big_zero, sizeof(big_zero), bigzframe, sizeof(bigzframe));
+ const uint32_t before = decoder.getStats().overflows;
+ TEST_ASSERT_FALSE(feedAll(decoder, bigzframe, written));
+ TEST_ASSERT_EQUAL(before + 1, decoder.getStats().overflows);
+
+ // CRC mismatch: corrupt a payload byte, keeping it non-zero so the COBS structure survives (only
+ // a policy that carries a CRC can detect this)
+ if (Crc::SIZE > 0) {
+ written = encode(payload, sizeof(payload), frame, sizeof(frame));
+ frame[1] = (frame[1] == 0x7F) ? 0x7E : 0x7F;
+ const uint32_t crc_before = decoder.getStats().crc_errors;
+ TEST_ASSERT_FALSE(feedAll(decoder, frame, written));
+ TEST_ASSERT_EQUAL(crc_before + 1, decoder.getStats().crc_errors);
+ }
+}
+
+// Drive every one-shot decode() path: happy round-trip, optional trailing delimiter, capacity
+// rejection, truncated COBS, interior zero, too-short frame and CRC mismatch.
+template
+void exerciseDecodePaths() {
+ const uint8_t payload[8] = {0x11, 0x00, 0x22, 0xFF, 0x00, 0xAB, 0xCD, 0x00};
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
+ uint8_t out[sizeof(payload)] = {};
+
+ // Happy path
+ TEST_ASSERT_EQUAL(sizeof(payload), decode(frame, written, out, sizeof(out)));
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, out, sizeof(payload));
+
+ // The trailing delimiter is optional: without it the first/second pass loops exit on frame_size
+ TEST_ASSERT_EQUAL(sizeof(payload), decode(frame, written - 1, out, sizeof(out)));
+
+ // Output buffer too small for the payload
+ TEST_ASSERT_EQUAL(0, decode(frame, written, out, sizeof(out) - 1));
+
+ // Truncated COBS block: the code byte announces more bytes than the frame provides
+ const uint8_t truncated[3] = {0x05, 0x11, 0x22};
+ TEST_ASSERT_EQUAL(0, decode(truncated, sizeof(truncated), out, sizeof(out)));
+
+ // Interior zero: a zero may not appear inside a COBS block
+ const uint8_t interior_zero[3] = {0x03, 0x11, 0x00};
+ TEST_ASSERT_EQUAL(0, decode(interior_zero, sizeof(interior_zero), out, sizeof(out)));
+
+ // Too short: not even one payload byte plus the CRC. With a CRC, a 1-byte decode is too short;
+ // with NoCrc only an empty frame (zero decoded bytes) is too short.
+ if (Crc::SIZE > 0) {
+ const uint8_t too_short[3] = {0x02, 0x41, 0x00}; // decodes to 1 byte < 1 + Crc::SIZE
+ TEST_ASSERT_EQUAL(0, decode(too_short, sizeof(too_short), out, sizeof(out)));
+ } else {
+ const uint8_t empty_frame[1] = {0x00}; // decodes to 0 bytes < 1
+ TEST_ASSERT_EQUAL(0, decode(empty_frame, sizeof(empty_frame), out, sizeof(out)));
+ }
+
+ // CRC mismatch (only a policy that carries a CRC can detect this)
+ if (Crc::SIZE > 0) {
+ uint8_t corrupted[getMaxEncodedSize(sizeof(payload))] = {};
+ for (size_t i = 0; i < written; i++) {
+ corrupted[i] = frame[i];
+ }
+ corrupted[1] = (corrupted[1] == 0x7F) ? 0x7E : 0x7F;
+ TEST_ASSERT_EQUAL(0, decode(corrupted, written, out, sizeof(out)));
+ }
+}
+
+// Run every path helper for one CRC policy. The Decoder paths run for each MaxPayload the suite
+// instantiates (16, 32, 64), since gcov tracks every Decoder separately.
+template
+void exerciseAllPaths() {
+ exerciseEncoderOverflow();
+ exerciseEncoderFullBlock();
+ exerciseDecodePaths();
+ exerciseDecoderPaths<16, Crc>();
+ exerciseDecoderPaths<32, Crc>();
+ exerciseDecoderPaths<64, Crc>();
+}
+
+void test_all_paths_no_crc() { exerciseAllPaths(); }
+void test_all_paths_crc8() { exerciseAllPaths(); }
+void test_all_paths_crc16() { exerciseAllPaths(); }
+void test_all_paths_crc32() { exerciseAllPaths(); }
+
/* ---------------------------------------------------------------------------------------------- */
/* Stress */
/* ---------------------------------------------------------------------------------------------- */
@@ -615,10 +835,10 @@ void test_stress_round_trip_random_chunks() {
stress_state = 0xC0FFEE11;
constexpr size_t MAX_PAYLOAD = 64;
- ByteFrame::Decoder decoder;
+ Decoder decoder;
uint8_t payload[MAX_PAYLOAD];
- uint8_t frame[ByteFrame::getMaxEncodedSize(MAX_PAYLOAD)] = {};
+ uint8_t frame[getMaxEncodedSize(MAX_PAYLOAD)] = {};
for (uint32_t i = 0; i < STRESS_ITERATIONS; i++) {
const size_t size = 1 + (nextRandom() % MAX_PAYLOAD);
@@ -626,7 +846,7 @@ void test_stress_round_trip_random_chunks() {
payload[j] = uint8_t(nextRandom());
}
- const size_t written = ByteFrame::encode(payload, size, frame, sizeof(frame));
+ const size_t written = encode(payload, size, frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
// Feed the frame in random-sized chunks, as a stream would deliver it
@@ -651,7 +871,7 @@ void test_stress_round_trip_random_chunks() {
void test_stress_garbage_input() {
stress_state = 0xA5A5A5A5;
- ByteFrame::Decoder<32> decoder;
+ Decoder<32> decoder;
for (uint32_t i = 0; i < STRESS_ITERATIONS; i++) {
// Random bytes, delimiters included. Random garbage virtually never carries a valid CRC; the
@@ -663,11 +883,11 @@ void test_stress_garbage_input() {
}
// After the garbage, a valid frame still decodes
- const uint8_t payload[3] = {0x01, 0x02, 0x03};
- uint8_t frame[ByteFrame::getMaxEncodedSize(sizeof(payload))] = {};
- const size_t written = ByteFrame::encode(payload, sizeof(payload), frame, sizeof(frame));
+ const uint8_t payload[3] = {0x01, 0x02, 0x03};
+ uint8_t frame[getMaxEncodedSize(sizeof(payload))] = {};
+ const size_t written = encode(payload, sizeof(payload), frame, sizeof(frame));
- decoder.feed(ByteFrame::DELIMITER); // close any partial garbage frame first
+ decoder.feed(DELIMITER); // close any partial garbage frame first
TEST_ASSERT_TRUE(feedAll(decoder, frame, written));
TEST_ASSERT_EQUAL_HEX8_ARRAY(payload, decoder.getPayload(), sizeof(payload));
}
@@ -676,10 +896,10 @@ void test_stress_corruption() {
stress_state = 0xDEADBEEF;
constexpr size_t MAX_PAYLOAD = 32;
- ByteFrame::Decoder decoder;
+ Decoder decoder;
uint8_t payload[MAX_PAYLOAD];
- uint8_t frame[ByteFrame::getMaxEncodedSize(MAX_PAYLOAD)] = {};
+ uint8_t frame[getMaxEncodedSize(MAX_PAYLOAD)] = {};
for (uint32_t i = 0; i < STRESS_ITERATIONS; i++) {
const size_t size = 1 + (nextRandom() % MAX_PAYLOAD);
@@ -687,7 +907,7 @@ void test_stress_corruption() {
payload[j] = uint8_t(nextRandom());
}
- const size_t written = ByteFrame::encode(payload, size, frame, sizeof(frame));
+ const size_t written = encode(payload, size, frame, sizeof(frame));
TEST_ASSERT_TRUE(written > 0);
// Corrupt one random byte of the encoded frame (delimiter excluded), forcing a real change.
@@ -713,18 +933,23 @@ void test_stress_corruption() {
// Restore the frame and close any partial state so iterations stay independent
frame[corrupt_pos] = original;
- decoder.feed(ByteFrame::DELIMITER);
+ decoder.feed(DELIMITER);
}
}
/* ---------------------------------------------------------------------------------------------- */
-/* 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();
// Size helper
@@ -762,12 +987,28 @@ void setup() {
RUN_TEST(test_decode_one_shot);
RUN_TEST(test_crc_variants_round_trip);
+ // All paths across CRC policies
+ RUN_TEST(test_all_paths_no_crc);
+ RUN_TEST(test_all_paths_crc8);
+ RUN_TEST(test_all_paths_crc16);
+ RUN_TEST(test_all_paths_crc32);
+
// Stress
RUN_TEST(test_stress_round_trip_random_chunks);
RUN_TEST(test_stress_garbage_input);
RUN_TEST(test_stress_corruption);
- UNITY_END();
+ return UNITY_END();
}
+// For native
+int main(void) { return runUnityTests(); }
+
+// For Arduino framework
+#ifdef ARDUINO
+void setup() {
+ delay(2000);
+ runUnityTests();
+}
void loop() {}
+#endif