Modular Data Injection Architecture for Embedded Systems

Modular Data Injection Architecture for Embedded Systems

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.


🧱 Architecture Goals

The main goal was to define a clear separation between:

  • What the system does (via abstract interfaces)
  • How it is implemented (via platform-specific backends)

Key Benefits:

  • Easy mocking of subsystems for testing
  • Code portability across RTOS and Embedded Linux
  • Pluggable architecture with backend auto-registration
  • Shared control logic with per-platform drivers

πŸ—‚οΈ Directory Layout

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

πŸ”Œ Interface Design – Example: 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;
};
}

πŸ“¦ Dynamic Registration System

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.


βš™οΈ Backend Implementation

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


πŸš€ Startup and Usage

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


πŸ“½οΈ Slide Summary (From Internal Presentation)

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

βœ… Summary

This architecture is useful for any modern embedded software stack. We can:

  • Simulate hardware behavior with mocks
  • Replace backend logic without touching the application layer
  • Accelerate testing and reduce cross-platform integration complexity

If you’re working on modular embedded design for IoT, medical, or industrial products β€” this approach can simplify your architecture dramatically.


πŸ€– Optional Integration: Wrapping the Components in ROS 2 Nodes

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.

πŸ”Œ Why ROS 2?

ROS 2 provides:

  • Real-time capable publish/subscribe transport
  • Service calls for control/config APIs
  • Introspection and visualization via rqt, rviz, and ros2 topic

🧩 How to wrap a backend component in ROS 2

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_;
};

πŸ§ͺ Benefits of ROS 2 Wrapping

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)

πŸ“¦ Build System Notes

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.


🌐 Conclusion with ROS 2

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:

  • Real-time monitoring
  • Dynamic configuration
  • Clean system separation

This makes the approach suitable not only for embedded firmware but also for larger robotic and automation platforms.


Comments
comments powered by Disqus