A C++20 userspace driver-style library that communicates with a simulated embedded device over a custom binary protocol on a Linux Unix domain socket.
The project is designed as a small but realistic embedded-device integration exercise: the C++ side owns the transport, protocol codec, CRC validation, typed device API, retry policy and logging, while a Python process behaves like the firmware endpoint. No physical hardware is required.
- Modern C++20 and RAII-based resource management.
- Linux
AF_UNIX/SOCK_STREAMcommunication. - A transport interface (
ITransport) decoupled from the driver logic. - A binary request/response protocol with CRC-32 integrity checking.
- Little-endian serialization and typed payload decoding.
- Bounded request timeouts and retries.
- Recovery from timeouts, malformed/corrupted responses and
BUSYdevice status. - Structured exception handling for transport, protocol and device failures.
std::unique_ptrownership,std::optional,std::variant,std::chronoand mutex-based request serialization.- Deterministic unit testing through a mock transport.
- Fault injection through a configurable Python device simulator.
- CMake/CTest integration without external test-framework dependencies.
This is a userspace driver-style abstraction, not a Linux kernel module.
The simulator models a composite embedded unit exposing:
- temperature measurements;
- battery voltage and state-of-charge information;
- IMU acceleration and gyroscope samples;
- GPS position/fix information;
- radio link status and packet counters;
- device configuration and reset commands.
C++ application / CLI
|
v
+---------------+
| DeviceDriver |
+-------+-------+
|
+---------------+---------------+
| |
v v
+----------------+ +----------------+
| ProtocolCodec | | ITransport |
| + CRC-32 | +--------+-------+
+----------------+ |
+----------+----------+
| |
v v
+--------------------+ +----------------+
| UnixSocketTransport| | MockTransport |
| AF_UNIX/SOCK_STREAM| | unit tests |
| poll() timeouts | | fault control |
+---------+----------+ +----------------+
|
v
/tmp/edds_device.sock
|
v
+--------------------+
| Python simulator |
| simulated firmware |
+--------------------+
DeviceDriver depends on the abstract ITransport interface rather than directly on the Unix socket implementation. The same driver logic can therefore run against the real Linux transport or against MockTransport in unit tests.
.
├── apps/
│ └── edds_cli.cpp # Command-line client
├── cmake/
│ └── README.md
├── docs/
│ ├── DEVICE_SIMULATOR.md
│ ├── PROTOCOL.md
│ ├── TECHNICAL_DESIGN.md
│ └── TESTING.md
├── include/edds/ # Public C++ API
│ ├── crc32.hpp
│ ├── device_driver.hpp
│ ├── errors.hpp
│ ├── format.hpp
│ ├── logger.hpp
│ ├── protocol.hpp
│ ├── transport.hpp
│ ├── types.hpp
│ └── unix_socket_transport.hpp
├── simulator/
│ └── device_simulator.py # Simulated firmware endpoint
├── src/ # Driver implementation
├── tests/ # Unit tests + MockTransport
├── tools/
│ ├── decode_frame.py
│ └── run_demo.sh
├── CMakeLists.txt
├── LICENSE
└── README.md
- Linux;
- CMake 3.16 or newer;
- a compiler with C++20 support, such as GCC or Clang;
- Python 3 for the simulator and protocol helper script;
- POSIX threads (detected by CMake through
Threads::Threads).
From the repository root:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallelRun the unit tests:
ctest --test-dir build --output-on-failureThe default configuration builds:
build/edds_cli
build/tests/edds_tests
build/libedds_driver.a
| Option | Default | Purpose |
|---|---|---|
EDDS_BUILD_CLI |
ON |
Build edds_cli |
EDDS_BUILD_TESTS |
ON |
Build and register the unit tests |
EDDS_WARNINGS_AS_ERRORS |
OFF |
Add -Werror to project targets |
Example:
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DEDDS_WARNINGS_AS_ERRORS=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failureThe simulator and CLI use /tmp/edds_device.sock by default.
python3 simulator/device_simulator.py \
--socket /tmp/edds_device.sock \
--verbose./build/edds_cli \
--socket /tmp/edds_device.sock \
--command allThe CLI connects to the simulator, exchanges binary frames, validates the response CRC and sequence number, decodes the typed payloads and prints the resulting values.
The --command option accepts exactly the following values:
| Command | Action |
|---|---|
all |
Ping the device and read all five sensor/status groups |
ping |
Read firmware, serial number and device name |
temperature |
Read the temperature sample |
battery |
Read battery voltage, percentage and flags |
imu |
Read accelerometer and gyroscope data |
gps |
Read GPS fix information |
radio |
Read radio RSSI, link state and packet counters |
sample-rate |
Set the simulated device sample rate |
reset |
Reset the simulated device state |
Examples:
./build/edds_cli --command ping
./build/edds_cli --command temperature
./build/edds_cli --command battery
./build/edds_cli --command imu
./build/edds_cli --command gps
./build/edds_cli --command radio
./build/edds_cli --command sample-rate --sample-rate 50
./build/edds_cli --command resetBecause the CLI and simulator share the same default socket path, --socket /tmp/edds_device.sock can be omitted when the default is used.
Run the same command several times through one driver instance:
./build/edds_cli \
--command temperature \
--poll 5 \
--sleep-ms 250./build/edds_cli \
--command all \
--timeout-ms 500 \
--retries 3CLI limits enforced by the argument parser:
| Option | Accepted values | Default |
|---|---|---|
--timeout-ms |
1..60000 |
250 |
--retries |
0..10 |
2 |
--poll |
1..1000000 |
1 |
--sleep-ms |
0..60000 |
500 |
--sample-rate |
1..1000 Hz |
10 |
Enable debug logs:
./build/edds_cli --command all --debugWrite logs to a file as well:
./build/edds_cli \
--command all \
--debug \
--log-file edds.logDisable driver logs:
./build/edds_cli --command all --quietDisplay the complete CLI help:
./build/edds_cli --helpThe Python simulator can deliberately degrade communication so that timeout, retry and validation paths can be exercised without physical hardware.
python3 simulator/device_simulator.py \
--socket /tmp/edds_device.sock \
--delay-ms 20 \
--drop-rate 0.05 \
--error-rate 0.02 \
--busy-rate 0.05 \
--seed 7 \
--verbose| Simulator option | Effect |
|---|---|
--delay-ms N |
Adds N milliseconds of response latency |
--drop-rate P |
Drops responses with probability P, simulating timeouts |
--error-rate P |
Corrupts encoded response bytes with probability P |
--busy-rate P |
Returns protocol status BUSY with probability P |
--seed N |
Sets the random seed used by fault injection |
--backlog N |
Sets the Unix socket listen backlog |
--verbose |
Prints received commands and injected faults |
The three probability options accept values from 0.0 to 1.0.
Display simulator help:
python3 simulator/device_simulator.py --helpEvery message uses the following frame structure:
Offset Size Field
0 2 MAGIC = ED D5
2 1 VERSION
3 1 KIND
4 2 SEQUENCE
6 1 COMMAND
7 1 STATUS
8 2 PAYLOAD_LENGTH
10 N PAYLOAD
10+N 4 CRC32
Properties:
- all multi-byte integers are little-endian;
- maximum payload size is 4096 bytes;
- CRC-32 covers the 10-byte header and payload, excluding the CRC field itself;
- requests use kind
0x01and responses use kind0x02; - the sequence identifier is checked against the outstanding request;
- the command identifier in the response must match the request.
See docs/PROTOCOL.md for command IDs, status codes and exact payload schemas.
A transaction uses a bounded number of attempts: DriverConfig::retries + 1.
The driver retries when:
- a
TimeoutErroroccurs; - a
ProtocolErroroccurs, including a malformed/corrupted frame; - the device returns
BUSYand retry attempts remain.
For timeout and protocol failures, the transport is closed and reconnected before the next attempt.
Other non-OK device statuses are exposed as DeviceError.
The exception hierarchy is:
DriverError
├── TimeoutError
├── TransportError
├── ProtocolError
└── DeviceError
The repository contains a small dependency-free C++ test harness and a deterministic MockTransport implementation.
Run through CTest:
ctest --test-dir build --output-on-failureOr execute the test binary directly:
./build/tests/edds_testsThe current suite contains 11 test cases covering:
- the standard CRC-32 test vector;
- protocol encode/decode round trips;
- CRC mismatch rejection;
- invalid magic rejection;
- little-endian binary helpers;
- PING request generation and response decoding;
- temperature and battery payload decoding;
- retry after a read timeout;
- retry after
BUSY; - non-OK device status propagation;
- mismatched response sequence rejection.
The driver tests do not require the Python simulator or a real Unix socket because they inject MockTransport through ITransport.
#include "edds/device_driver.hpp"
#include "edds/unix_socket_transport.hpp"
#include <chrono>
#include <iostream>
#include <memory>
int main()
{
auto transport = std::make_unique<edds::UnixSocketTransport>(
"/tmp/edds_device.sock"
);
edds::DriverConfig config;
config.request_timeout = std::chrono::milliseconds(250);
config.retries = 2;
edds::DeviceDriver driver(std::move(transport), config);
driver.connect();
const auto info = driver.ping();
const auto temperature = driver.read_temperature();
std::cout << info.device_name
<< " temperature="
<< temperature.celsius
<< " C\n";
}DeviceDriver owns the transport through std::unique_ptr<ITransport>. The application therefore works with typed device operations while socket and binary-frame details remain encapsulated inside the library.
tools/decode_frame.py can inspect a frame represented as hexadecimal bytes.
The following example is a valid PING request frame:
python3 tools/decode_frame.py edd501010100010000002b553b66The decoder displays:
- protocol version;
- message kind;
- sequence;
- command;
- status;
- payload size;
- payload bytes;
- CRC contained in the frame;
- CRC recalculated by the tool;
- checksum validity.
| Document | Contents |
|---|---|
docs/TECHNICAL_DESIGN.md |
Architecture, modules, ownership and retry strategy |
docs/PROTOCOL.md |
Binary frame format, command IDs and payload schemas |
docs/DEVICE_SIMULATOR.md |
Simulator behavior and fault injection |
docs/TESTING.md |
Test architecture and mock strategy |
UnixSocketTransport owns the socket file descriptor and closes it automatically. Resource lifetime is therefore tied to object lifetime, including exceptional control paths.
DeviceDriver knows only ITransport. Socket-specific code remains in UnixSocketTransport, while tests can provide MockTransport.
Binary payloads are converted into C++ types such as:
DeviceInfo;TemperatureSample;BatteryStatus;ImuSample;GpsFix;RadioStatus.
Binary representation therefore remains internal to the protocol/driver layers.
GpsFix uses std::optional for position values that are unavailable when the device reports no fix.
The CLI uses the Reading std::variant abstraction to handle different sensor result types through a common formatting path.
The project intentionally focuses on the software architecture around embedded-device communication.
It does not attempt to emulate:
- electrical characteristics;
- hard real-time scheduling;
- physical sensor noise with hardware-level fidelity;
- a Linux kernel driver.
The current physical-transport substitute is a Unix domain stream socket.
The ITransport boundary is intended to make future UART, SPI, TCP or other transport implementations possible without changing the high-level device API.
MIT. See LICENSE.