Modular Data Injection Architecture for Embedded Systems
Published at September 5, 2025 · 4 min read · Tags: architecture embedded interface modular iot rtos zephyr c++ embedded-linux medical-devices
Modular Data Injection Architecture for Embedded Systems
Published at September 5, 2025 · 4 min read · Tags: architecture embedded interface modular iot rtos zephyr c++ embedded-linux medical-devices
Designing complex embedded systems β such as IoT devices, medical products, or industrial controllers β often requires supporting multiple hardware platforms: MCUs, Embedded Linux systems, and sometimes even mock environments for testing.
To achieve clean separation between core logic and platform-specific implementations, I designed a modular data injection architecture based on virtual interfaces and dynamic backend registration.
This post summarizes the approach and presents a practical C++ implementation of this architecture.
The main goal was to define a clear separation between:
system/
βββ core/
β βββ include/core/
β β βββ ISystemComponent.hpp
β β βββ IAlgo.hpp
β β βββ IController.hpp
β β βββ registry.hpp
β β βββ version.hpp
β βββ src/
β βββ startup.cpp
βββ backends/
β βββ component_linux/
β β βββ SystemImpl.cpp
β β βββ CMakeLists.txt
β βββ component_mock/
β βββ SystemImpl.cpp
βββ main.cpp
βββ CMakeLists.txt
ISystemComponent.hpp
#pragma once
namespace core {
class ISystemComponent {
public:
virtual ~ISystemComponent() = default;
virtual void configure(int profile_id) = 0;
virtual void start() = 0;
virtual void stop() = 0;
};
}
To allow runtime selection of implementation, we use a global factory registry.
registry.hpp
:#pragma once
#include <functional>
#include <unordered_map>
#include <string>
namespace core {
class IController;
using Factory = std::function<IController*()>;
inline std::unordered_map<std::string, Factory>& registry() {
static std::unordered_map<std::string, Factory> map;
return map;
}
inline void register_factory(const std::string& name, Factory f) {
registry()[name] = std::move(f);
}
}
Backends register themselves on static initialization.
#include "core/ISystemComponent.hpp"
#include "core/registry.hpp"
class LinuxComponent : public core::ISystemComponent {
public:
void configure(int profile_id) override { /* Linux-specific config */ }
void start() override { /* start component */ }
void stop() override { /* stop component */ }
};
namespace {
struct AutoRegister {
AutoRegister() {
core::register_factory("component@linux", []() -> core::IController* {
return new LinuxComponent();
});
}
} instance;
}
This backend implements ISystemComponent
and registers itself under "component@linux"
.
#include "core/ISystemComponent.hpp"
#include "core/registry.hpp"
int main() {
std::unique_ptr<core::IController> ctrl(
core::registry()["component@linux"]()
);
auto component = dynamic_cast<core::ISystemComponent*>(ctrl.get());
if (component) {
component->configure(42);
component->start();
component->stop();
}
}
The main()
can be compiled once and run against any backend.
Goal: Build a flexible, modular software system that separates logic from implementation.
Core Principles:
- One Core API defines WHAT the system does
- Multiple Backends define HOW it is done
- Supported Platforms: MCU, Embedded Linux, Mock, RTOS (Zephyr)
β
Easier Testing
β
Easier Integration
β
Easier Growth
This architecture is useful for any modern embedded software stack. We can:
If youβre working on modular embedded design for IoT, medical, or industrial products β this approach can simplify your architecture dramatically.
In many embedded systems β especially those using Jetson, robotics, or medical imaging platforms β it is useful to expose internal components as ROS 2 nodes for modular control, diagnostics, and inter-process communication.
ROS 2 provides:
rqt
, rviz
, and ros2 topic
Assume you have a backend implementation like LinuxComponent
. You can wrap it inside a ROS 2 node:
#include "rclcpp/rclcpp.hpp"
#include "core/ISystemComponent.hpp"
#include "core/registry.hpp"
class ComponentNode : public rclcpp::Node {
public:
ComponentNode()
: Node("component_node")
{
using namespace std::chrono_literals;
component_ = std::unique_ptr<core::ISystemComponent>(
dynamic_cast<core::ISystemComponent*>(
core::registry()["component@linux"]())
);
component_->configure(1);
timer_ = this->create_wall_timer(1000ms, [this]() {
component_->start(); // Or collect data/status
});
}
private:
std::unique_ptr<core::ISystemComponent> component_;
rclcpp::TimerBase::SharedPtr timer_;
};
Feature | Benefit |
---|---|
Topic exposure | Allows other components to subscribe to telemetry |
Service API | Enables external tools to trigger configure() or start() |
Standard tooling | Easier diagnostics via ros2 topic echo , rqt_graph , etc. |
Scalability | Runs locally or distributed (DDS middleware) |
You can place the ROS 2 node wrapper in a separate ros2/
subfolder with its own CMakeLists.txt
and package.xml
:
ros2/
βββ component_node/
β βββ src/
β β βββ component_node.cpp
β βββ CMakeLists.txt
β βββ package.xml
Link against your core and backend libraries, and expose only the wrapper via ROS 2 interfaces.
The architecture described in this post is ROS 2βready by design.
Each backend component can be integrated as a ROS 2 node with minimal glue code, enabling:
This makes the approach suitable not only for embedded firmware but also for larger robotic and automation platforms.