Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cpp-embedded-device-driver-simulator

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.

What this project demonstrates

  • Modern C++20 and RAII-based resource management.
  • Linux AF_UNIX / SOCK_STREAM communication.
  • 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 BUSY device status.
  • Structured exception handling for transport, protocol and device failures.
  • std::unique_ptr ownership, std::optional, std::variant, std::chrono and 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.

Simulated device

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.

Architecture

                         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.

Repository layout

.
├── 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

Requirements

  • 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).

Build

From the repository root:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

Run the unit tests:

ctest --test-dir build --output-on-failure

The default configuration builds:

build/edds_cli
build/tests/edds_tests
build/libedds_driver.a

CMake options

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-failure

Quick start

The simulator and CLI use /tmp/edds_device.sock by default.

Terminal 1 - start the simulated device

python3 simulator/device_simulator.py \
  --socket /tmp/edds_device.sock \
  --verbose

Terminal 2 - query all sensors

./build/edds_cli \
  --socket /tmp/edds_device.sock \
  --command all

The CLI connects to the simulator, exchanges binary frames, validates the response CRC and sequence number, decodes the typed payloads and prints the resulting values.

CLI commands

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 reset

Because the CLI and simulator share the same default socket path, --socket /tmp/edds_device.sock can be omitted when the default is used.

Polling

Run the same command several times through one driver instance:

./build/edds_cli \
  --command temperature \
  --poll 5 \
  --sleep-ms 250

Timeout and retry policy

./build/edds_cli \
  --command all \
  --timeout-ms 500 \
  --retries 3

CLI 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

Logging

Enable debug logs:

./build/edds_cli --command all --debug

Write logs to a file as well:

./build/edds_cli \
  --command all \
  --debug \
  --log-file edds.log

Disable driver logs:

./build/edds_cli --command all --quiet

Display the complete CLI help:

./build/edds_cli --help

Fault injection

The 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 --help

Binary protocol

Every 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 0x01 and responses use kind 0x02;
  • 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.

Reliability and error handling

A transaction uses a bounded number of attempts: DriverConfig::retries + 1.

The driver retries when:

  • a TimeoutError occurs;
  • a ProtocolError occurs, including a malformed/corrupted frame;
  • the device returns BUSY and 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

Testing

The repository contains a small dependency-free C++ test harness and a deterministic MockTransport implementation.

Run through CTest:

ctest --test-dir build --output-on-failure

Or execute the test binary directly:

./build/tests/edds_tests

The 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.

C++ API example

#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.

Protocol frame decoder

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 edd501010100010000002b553b66

The 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.

Technical documentation

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

Design choices

RAII for Linux resources

UnixSocketTransport owns the socket file descriptor and closes it automatically. Resource lifetime is therefore tied to object lifetime, including exceptional control paths.

Dependency inversion

DeviceDriver knows only ITransport. Socket-specific code remains in UnixSocketTransport, while tests can provide MockTransport.

Strongly typed public API

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.

Nullable GPS data

GpsFix uses std::optional for position values that are unavailable when the device reports no fix.

Heterogeneous sensor formatting

The CLI uses the Reading std::variant abstraction to handle different sensor result types through a common formatting path.

Scope and limitations

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.

License

MIT. See LICENSE.

About

Créer un faux périphérique matériel et écrire côté C++ une couche “driver” qui communique avec lui.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages