A C++20 Linux user-space driver-style project that communicates with a simulated embedded peripheral through a compact binary request/response protocol.
The hardware endpoint is implemented in Python and exposed through a UNIX domain socket, so the complete command path can be built, tested and demonstrated on a normal Linux workstation without physical hardware. The C++ driver depends only on the ITransport abstraction, which keeps the protocol and high-level device API independent from the development transport.
- Modern C++20 with RAII and
std::span. - A transport-independent
DeviceDriverAPI. - Binary big-endian frame encoding and decoding.
- CRC-32 integrity checking.
- Request/response correlation with command and sequence validation.
- Bounded payload sizes.
- End-to-end read timeouts for stream framing.
- Retry handling for timeouts, CRC failures and invalid correlated responses.
- Linux UNIX domain socket I/O.
- Deterministic mock-based unit tests.
- Python peripheral simulation.
- CMake + CTest integration.
- Optional AddressSanitizer + UndefinedBehaviorSanitizer builds.
- GitHub Actions CI with GCC, Clang and sanitizers.
This repository is intentionally a user-space driver simulation, not a Linux kernel module and not a real UART, SPI or I2C implementation.
The UNIX domain socket is the reproducible development transport. A real serial, SPI, I2C or other hardware transport can be added later by implementing lhbd::transport::ITransport without changing the high-level driver API or binary codec.
.
├── .github/workflows/ci.yml
├── examples/demo_commands.md
├── include/lhbd/
│ ├── core/DeviceDriver.hpp
│ ├── protocol/
│ │ ├── Crc32.hpp
│ │ ├── Frame.hpp
│ │ ├── FrameCodec.hpp
│ │ └── Protocol.hpp
│ ├── transport/
│ │ ├── ITransport.hpp
│ │ ├── MockTransport.hpp
│ │ └── UnixSocketTransport.hpp
│ └── utils/Logger.hpp
├── scripts/run_integration_test.py
├── simulator/device_simulator.py
├── src/
│ ├── apps/device_cli.cpp
│ ├── core/DeviceDriver.cpp
│ ├── protocol/
│ │ ├── Crc32.cpp
│ │ └── FrameCodec.cpp
│ ├── transport/
│ │ ├── MockTransport.cpp
│ │ └── UnixSocketTransport.cpp
│ └── utils/Logger.cpp
├── tests/test_main.cpp
├── CMakeLists.txt
├── Makefile
└── README.md
Recommended environment:
- Linux;
- CMake 3.18 or newer;
- a C++20 compiler such as GCC or Clang;
- Python 3.10 or newer for the simulator and integration test;
makeif you want to use the convenience Makefile;ziponly formake zip.
No third-party C++ library is required.
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DLHBD_WARNINGS_AS_ERRORS=ON
cmake --build build --parallel 2
ctest --test-dir build --output-on-failureWhen Python 3.10+ is available, CTest registers both:
lhbd_unit_tests;lhbd_integration_test.
If Python is not available, the C++ unit test is still built and registered.
make testUseful targets:
make build
make unit-test
make integration
make sanitize
make clean
make helpmake sanitize builds a separate build-san/ tree with AddressSanitizer and UndefinedBehaviorSanitizer enabled.
Build the project first, then use two terminals.
Terminal 1:
python3 simulator/device_simulator.py --socket /tmp/lhbd_device.sock
Terminal 2:
./build/lhbd_cli --socket /tmp/lhbd_device.sock --demo
Typical output:
PING: OK
WRITE REG 0x0010: OK
READ REG 0x0010: 305419896
TEMPERATURE: 22.04...
STATUS: mode=1 flags=0 uptime=...
The exact sensor value and uptime vary between runs.
lhbd_cli [--socket <path>] --demo
lhbd_cli [--socket <path>] --ping
lhbd_cli [--socket <path>] --read-register <address>
lhbd_cli [--socket <path>] --write-register <address> <value>
lhbd_cli [--socket <path>] --sensor temperature|voltage|current|gyro_x
lhbd_cli [--socket <path>] --status
lhbd_cli [--socket <path>] --reset
Examples:
./build/lhbd_cli --socket /tmp/lhbd_device.sock --ping ./build/lhbd_cli --socket /tmp/lhbd_device.sock --read-register 0x0001 ./build/lhbd_cli --socket /tmp/lhbd_device.sock --write-register 0x0010 0x12345678 ./build/lhbd_cli --socket /tmp/lhbd_device.sock --sensor voltage ./build/lhbd_cli --socket /tmp/lhbd_device.sock --status ./build/lhbd_cli --socket /tmp/lhbd_device.sock --reset
Register addresses are limited to 16 bits and register values to 32 bits. Invalid numeric input and unknown sensor names are rejected by the CLI instead of being silently truncated or mapped to another sensor.
Each frame uses a fixed 16-byte header followed by an optional payload and a 4-byte CRC-32.
Offset Size Field
0 4 Magic = 0x4C484244 ("LHBD")
4 1 Protocol version
5 1 Command
6 1 Status
7 1 Reserved
8 2 Sequence number
10 2 Reserved
12 4 Payload length
16 N Payload
16+N 4 CRC-32 over header + payload
All multi-byte integer fields are encoded in big-endian order. Payloads are limited to 4096 bytes.
The implemented commands are:
Ping;ReadRegister;WriteRegister;ReadSensor;GetStatus;Reset.
The simulator exposes four sensor IDs:
- temperature;
- voltage;
- current;
- gyro X.
DeviceDriver assigns a sequence number to each logical command. The same sequence is retained when the command is retried.
A response is accepted only when:
- the frame structure is valid;
- the CRC is valid;
- the response sequence matches the request sequence;
- the response command matches the request command.
Timeouts, CRC failures and invalid correlated responses are retried up to DriverConfig::maxRetries.
The UNIX socket transport applies one absolute timeout to the complete frame read, including header, payload and CRC. A peer that sends only a partial frame therefore cannot leave the read blocked indefinitely.
The simulator can deliberately delay, drop or corrupt responses:
python3 simulator/device_simulator.py \ --socket /tmp/lhbd_device.sock \ --delay-ms 50 \ --drop-rate 0.3 \ --corrupt-rate 0.2
--drop-rate and --corrupt-rate must be between 0.0 and 1.0, and --delay-ms must be non-negative.
This is useful for observing timeout, retry and CRC statistics from the CLI.
The integration script launches the Python simulator, waits for its socket, executes the C++ CLI demo, validates representative output and cleans up the process/socket afterward.
Run it directly with:
python3 scripts/run_integration_test.py \ --binary build/lhbd_cli \ --socket /tmp/lhbd_integration.sock
Or simply run:
make integration
#include "lhbd/core/DeviceDriver.hpp" #include "lhbd/transport/UnixSocketTransport.hpp" #include <memory> int main() { auto transport = std::make_unique<lhbd::transport::UnixSocketTransport>( "/tmp/lhbd_device.sock"); lhbd::core::DeviceDriver driver(std::move(transport)); if (!driver.connect()) { return 1; } if (!driver.writeRegister(0x0010, 0x12345678)) { return 2; } const auto value = driver.readRegister(0x0010); const auto temperature = driver.readSensor( lhbd::protocol::SensorId::Temperature); return value && temperature ? 0 : 3; }
- the standard CRC-32 reference vector;
- frame encode/decode round trips;
- CRC corruption detection;
- malformed and oversized frames;
- ping requests;
- register reads and writes;
- sensor and status decoding;
- reset handling;
- timeout retries;
- CRC retries;
- response-command mismatch detection and retry;
- driver configuration edge cases;
- the complete Python simulator + C++ CLI path.
.github/workflows/ci.yml builds and tests the project on GitHub Actions with:
- GCC;
- Clang;
- warnings treated as errors;
- AddressSanitizer + UndefinedBehaviorSanitizer.
The current implementation deliberately remains small and demonstrative:
- communication is synchronous and request/response oriented;
- one
DeviceDriverinstance is not designed for concurrent transactions from multiple threads; - the simulator is a development fixture, not a security boundary;
- no physical UART/SPI/I2C backend is included;
- retries reuse the same sequence number, which is suitable for the current idempotent simulated operations but should be reconsidered before adding non-idempotent commands.
Those constraints keep the repository focused on protocol framing, driver abstraction, error handling and testability rather than board-specific hardware setup.