A C++ lifecycle orchestration layer for ROS 2 on resource-constrained embedded systems.
⚡ 81.3% faster boot time on real robot hardware
💾 38.7% lower RAM usage vsros2 launch(Stable)
🧠 Deterministic ROS 2 lifecycle orchestration in C++
Validated on a commercial robot platform (IFA 2025 showcase).
- Boot time: 60.0s → 11.2s (-81.3%)
- RAM usage (Stable): 442MB → 271MB (~38.7% reduction)
- Avg RAM during boot: 229MB → 201MB
📌 Tested on low-end embedded SoCs (e.g., 1GB RAM, Cortex-A35/A53)
📌 Boot time is defined as the time from process start until all required nodes reach the ACTIVE lifecycle state.
📌 Measurements are based on lifecycle activation logs and OS-level memory metrics collected under identical workloads.
👉 These results suggest a practical deployment path for ROS 2 on highly constrained embedded hardware.
ROS 2 can remain challenging to deploy on low-cost embedded robots when memory budgets and startup constraints are tight.
This project demonstrates that ROS 2 can be:
- deployed on 1GB-class hardware
- booted with deterministic lifecycle coordination
- used in real production environments
| Approach | Strengths | Limitations |
|---|---|---|
| ros2 launch (Python) | Flexible, easy to use | High RAM usage, non-deterministic startup |
| systemd | Stable process management | No ROS 2 lifecycle awareness |
| ROS 2 Composition | Efficient intra-process execution | No system-level orchestration |
| micro-ROS | Optimized for MCUs | Not for Linux-based systems |
| LifecycleManager | Deterministic, low-overhead, lifecycle-aware | More integration effort than default launch-based workflows |
🚧 [NOTICE] Project Status
This repository currently provides architecture documentation, YAML configuration examples, measurement methodology, and empirical validation results for the Deterministic Lifecycle Manager.
These materials are intended to make the design and observed system behavior technically reviewable before the full implementation is publicly released.
A partial or full source release under Apache License 2.0 is being prepared through internal compliance review.
- Overview - "What & Why?"
- Structural Pain Points in Production Systems - "Pain Point"
- Architecture & LifecycleManager - "Solution"
- Deterministic Boot Flow - "Deep-dive"
- Metrics & Validation - "Validation"
- Source, Build & License - "Open-Source Status"
This document introduces the "Deterministic Lifecycle Manager", a C++‑based lifecycle orchestration service designed to run ROS 2 reliably on low‑end embedded robotic platforms. The target systems are cost-constrained commercial robotic platforms built on entry-level SoCs such as Rockchip PX30, LG DQ1, or other Cortex-A35-class architectures, typically equipped with less than 1GB of RAM. On these platforms, boot-time behavior and peak resource usage critically impact system stability.
During system integration and production‑equivalent platform evaluation, we repeatedly encountered critical issues when using the standard Python‑based ros2 launch workflow on resource‑constrained hardware.
The most common problems were:
- High baseline RAM usage before application logic starts
- Frequent OOM (Out Of Memory) kills during early boot
- Unstable and non-deterministic startup sequences in production images Similar issues were repeatedly observed during evaluation on multiple low-end platforms, with the detailed quantitative results in this repository collected on the LG DQ1-based test platform. In our evaluated configuration, these issues were not sufficiently resolved through parameter tuning, launch configuration changes, or partial optimization. Our working conclusion was that orchestration overhead in the evaluated Python-based launch path was a major contributor under tight memory constraints.
To address these limitations, we implemented the Deterministic Lifecycle Manager as a minimal, native C++ service that launches and supervises ROS 2 nodes directly as OS-level processes. Key characteristics include:
- No Python-based launch dependency on the target system
- ROS 2 nodes built and executed as native C++ binaries
- Hardware-Aware Concurrent Spawning: Utilizes C++ threads alongside standard POSIX primitives (
fork(),execvp(),SIGCHLD) to balance parallel execution. By throttling simultaneous process launches based on system hardware concurrency (CPU cores), it mitigates excessive CPU contention and OS scheduler overhead to ensure system stability. - Dual-Launch "Benchmark Mode": Supports toggling between native C++ spawning and legacy script-based execution (
std::system()) to allow direct A/B performance comparisons (e.g., Python vs. C++ native). This design preserves standard ROS 2 lifecycle semantics and DDS-based communication while reducing the runtime overhead associated with the evaluated Python-based launch path.
💡 Note: Deterministic Lifecycle Manager is not a replacement for ROS 2. It is a focused orchestration layer that provides deterministic and resource-efficient boot and lifecycle management, specifically tailored for low-end embedded systems used in cost-constrained production robots.
This section explains why the standard Python‑based ros2 launch workflow becomes a reliability bottleneck on low‑end SoCs, based on issues repeatedly observed during production‑equivalent system integration.
ros2 launch relies on Python processes that are loaded and initialized during system boot.
- On low-cost hardware with 1 GB or less of RAM, this introduces substantial overhead before any application logic starts.
- As the number of nodes increases, Python interpreter initialization and runtime management consume a significant portion of system resources, frequently leading to memory pressure and OOM (Out Of Memory) events during early boot.
- Although
ros2 launchsupports concurrent spawning, it does not enforce strict OS‑level startup ordering or readiness guarantees. - As a result, dependent nodes may start before prerequisite nodes are fully initialized, leading to race conditions and unstable boot behavior in production environments.
The launch system focuses on process creation and parameter loading.
- After startup, lifecycle state transitions are not centrally coordinated.
- In systems managing many nodes, this results in fragmented lifecycle handling, uncoordinated state transitions, and the absence of a single authoritative component responsible for global system state—an important weakness on resource‑constrained platforms.
ROS 2 lifecycle states are intentionally minimal and low‑level, while production robots operate in mission‑level modes such as standby or navigation.
- Without centralized orchestration, mapping mission‑level behavior to coordinated lifecycle transitions across multiple nodes becomes error‑prone and difficult to validate, especially on low‑end SoCs where deterministic behavior is critical.
💡 Summary
On low‑end embedded platforms, the Python‑based launch system introduces overhead and non‑determinism during boot and state transitions. In our evaluated setup, these limitations were not sufficiently mitigated through launch configuration alone. This motivated a native and deterministic lifecycle orchestration approach.
The Lifecycle Manager is a multi-threaded C++ ROS 2 node that acts as a centralized lifecycle coordinator. It utilizes a specialized Dual-Thread Architecture:
- Spin Thread: Dedicated to handling ROS 2 communications and service callbacks.
- Main Thread: Manages the core orchestration loop, including package spawning and the
processQueue()mechanism. This non-blocking queue ensures that state transition requests are serialized and processed deterministically.
It operates as a ROS 2-native orchestration component within a standard ROS 2 system, while avoiding dependence on the Python-based launch path in the evaluated deployment mode.
The system is structured around five core modules:
flowchart TD
App["<b>APPLICATION LAYER</b><br/>(Requests device state changes)"]
YAML["<b>Configuration YAML</b><br/>(Source of Truth)"]
subgraph Manager ["LIFECYCLE MANAGER (Native C++)"]
direction TB
SL["Service Layer<br/>(Queue Manager)"]
Conf["Configuration<br/>(YAML Parser)"]
Core["Orchestration Core<br/>(Coordinates Launcher & Engine)"]
NL["Node Launcher<br/>(fork/exec/SIGCHLD)"]
TE["Transition Engine<br/>(State Machine Logic)"]
LC["Lifecycle Client<br/>(Service Interface)"]
SL --> Core
Conf --> Core
Core --> NL
Core --> TE
TE --> LC
end
subgraph Nodes ["MANAGED ROS 2 NODES"]
direction LR
NA["Node A"]
NB["Node B"]
NN["Node N"]
end
App -- "ROS 2 Service" --> SL
YAML --> Conf
NL -- "Native Execution" --> Nodes
LC -- "Get/ChangeState" --> Nodes
style App fill:#f8f9fa,stroke:#343a40,stroke-width:2px
style Manager fill:#f8f9fa,stroke:#343a40,stroke-width:2px
style Nodes fill:#f8f9fa,stroke:#343a40,stroke-width:2px
style SL fill:#fff,stroke:#343a40
style Conf fill:#fff,stroke:#343a40
style Core fill:#fff,stroke:#343a40
style TE fill:#fff,stroke:#343a40
style NL fill:#fff,stroke:#343a40
style LC fill:#fff,stroke:#343a40
style YAML fill:#fff,stroke:#343a40,stroke-width:2px
style NA fill:#fff,stroke:#343a40
style NB fill:#fff,stroke:#343a40
style NN fill:#fff,stroke:#343a40
- Service Layer – Exposes the
/lifecycle_transition_deviceROS 2 service and manages a thread-safe work queue. It supports string-based state transition requests (e.g., "NORMAL", "SLEEP") for improved human readability and CLI usability. - Config Engine – A centralized YAML parser that serves as the single source of truth. It supports professional name-based device state definitions, removing the need for fragile numeric indexing.
- Transition Engine – The central orchestrator that coordinates complex lifecycle state machines across multiple packages. It implements the serialization of state transition requests to prevent race conditions during concurrent updates.
- Node Launcher – Handles native process spawning via POSIX
fork/execand monitors child process health usingSIGCHLD. It manages dynamic path resolution and per-process log redirection.- Benchmarking Mode: Includes a specialized execution path for legacy
.py/.shlaunch scripts (viasystem()), enabling deterministic A/B testing against standard Python launches.
- Benchmarking Mode: Includes a specialized execution path for legacy
- Lifecycle Client – Interfaces with managed nodes using standard ROS 2
GetStateandChangeStateservices, featuring robust retry mechanisms and health monitoring.
Architecture Independence – By relying exclusively on POSIX standard system calls (fork, execvp, sigaction) and standard ROS 2 APIs, the Manager is designed to be portable across common Linux targets such as ARM64 and x86_64, subject to standard ROS 2 and platform integration constraints. This helps maintain consistent orchestration behavior across development and deployment platforms.
The following diagram illustrates the fundamental architectural shift from a heavy Python interpreter to our lightweight C++ native orchestrator.
flowchart TD
subgraph Old [❌ Before: Python-Based ros2 launch]
P_Interpreter["Python Interpreter (Heavy Base RAM)"] --> P_Launch{"launch.py"}
P_Launch -- "1. High-Overhead Launch" --> P_N1[ROS 2 Node Process A]
P_Launch -- "2. High-Overhead Launch" --> P_N2[ROS 2 Node Process B]
P_Launch -- "3. High-Overhead Launch" --> P_N3[ROS 2 Node Process C]
P_Note["🚨 Sequential Bottleneck & OOM"] -.-> P_Launch
end
subgraph New [✅ After: C++ Lifecycle Manager]
C_Manager{"C++ LifecycleManager (Native OS Process, Ultra-Low RAM)"}
C_Manager == "Concurrent fork() & execvp()" ==> C_N1[Node Process A]
C_Manager == "Concurrent fork() & execvp()" ==> C_N2[Node Process B]
C_Manager == "Concurrent fork() & execvp()" ==> C_N3[Node Process C]
C_N1 -. "Lifecycle State: Active" .-> C_Manager
C_N2 -. "Lifecycle State: Active" .-> C_Manager
C_N3 -. "Lifecycle State: Active" .-> C_Manager
C_Note["💡 Safe Concurrency & Determinism"] -.-> C_Manager
end
style P_Interpreter fill:#ffebee,stroke:#c62828,stroke-width:2px
style P_Launch fill:#ffcdd2,stroke:#c62828,stroke-width:2px
style C_Manager fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
All orchestration behavior is defined declaratively in a single YAML file:
# Example: Configuration with Professional State Names
LIFECYCLE_MANAGER_CONFIG:
DEVICE_STATE_NAMES: ["NORMAL", "SLEEP", "POWERSAVE"]
USE_LAUNCH_SCRIPT: false
NODE_TRANSITION_STRATEGY: "parallel"
PACKAGE_slam_package:
PACKAGE_ENABLE: true
NODE_slam_node:
EXECUTABLE: slam_node
ARGUMENT: ["--param_a", "value_a"]
DEPENDENCY: ["lidar_package,lidar_node"]
DEVICE_INIT: ACTIVE
DEVICE_STATE_NORMAL: ACTIVE
DEVICE_STATE_SLEEP: INACTIVE
DEVICE_STATE_POWERSAVE: FINALIZEDKey configuration capabilities:
- Launch mode (
USE_LAUNCH_SCRIPT): Selects between native binary spawning (fork/exec) or script-based execution (benchmarking) - Transition strategy (
NODE_TRANSITION_STRATEGY): Parallel (multi-threaded) or sequential execution - Executable arguments (
ARGUMENT): Command-line arguments for the native process - Dependency declaration (
DEPENDENCY): Inter-node startup ordering and readiness polling - Initialization target (
DEVICE_INIT): Specific lifecycle state for initial boot - Device state mapping (
DEVICE_STATE_<NAME>): Per-node lifecycle targets for each robot mission state
The ROS 2 lifecycle standard does not allow direct transitions between certain primary states. The Lifecycle Manager resolves all intermediate steps automatically:
UNCONFIGURED ➔ ACTIVE : Configure ➔ Activate (two-step)
ACTIVE ➔ UNCONFIGURED : Deactivate ➔ Cleanup (two-step)
UNCONFIGURED ➔ INACTIVE : Configure
INACTIVE ➔ ACTIVE : Activate
ACTIVE ➔ INACTIVE : Deactivate
Any state ➔ FINALIZED : Appropriate shutdown transition
The application layer simply declares a target state; the Lifecycle Manager resolves and executes all intermediate transitions transparently, each wrapped with configurable retry and timeout policies.
To bridge the semantic gap between robot missions and low-level lifecycle states, the Lifecycle Manager introduces a "Device State" abstraction.
| Node \ Device State | NORMAL | SLEEP | POWERSAVE |
|---|---|---|---|
slam_node |
ACTIVE | INACTIVE | FINALIZED |
lidar_node |
ACTIVE | INACTIVE | FINALIZED |
navigation_node |
ACTIVE | INACTIVE | FINALIZED |
motor_node |
ACTIVE | INACTIVE | FINALIZED |
camera_node |
ACTIVE | ACTIVE | INACTIVE |
diagnostic_node |
ACTIVE | ACTIVE | ACTIVE |
To switch the robot from "NORMAL" to "SLEEP", just call:
ros2 service call /lifecycle_transition_device lifecycle_manager_msgs/srv/TransitionDevice "{request: 'SLEEP'}"This single call automatically transitions each node to its matching target state — navigation and motor stop, while camera and diagnostics stay running. This design completely decouples mission logic from lifecycle management.
The Lifecycle Manager follows a rigorous, deterministic sequence to ensure all nodes are prepared and synchronized.
By identifying independent node groups at runtime from YAML dependency declarations (e.g., DEPENDENCY: ["pkg_name,node_name"]), the system initializes multiple packages concurrently — optimizing the theoretical boot time from O(N) sequential initialization to O(Depth(G)), where Depth(G) is the longest dependency path in the package graph.
In other words, boot time becomes bounded by the longest dependency chain rather than the total number of nodes.
flowchart TD
Start("[ SYSTEM STARTUP ]") --> YAML
YAML["<b>YAML Configuration</b><br/>(Source of Truth)"] --> Exec
Exec["<b>Execution Strategy</b><br/>(Parallel vs Sequential)"] --> Path
subgraph Path ["[ ORCHESTRATOR PATH ] - (Parallel / Seq Loop)"]
direction TB
Check["<b>Check if Enabled</b><br/>(f_packageLaunch flag)"] --> Launch
Launch["<b>Package Launch</b><br/>(Native fork/exec)"] --> Dep
Dep["<b>Dependency & State Check</b><br/>(GetState + Dep Polling)"] --> Trans
Trans["<b>State Transition</b><br/>(ChangeState Client)"]
end
Path --> Ready("[ SYSTEM READY ]")
style Path fill:#f8f9fa,stroke:#343a40,stroke-width:2px,stroke-dasharray: 5 5
style Start fill:#e7f3ff,stroke:#007bff,stroke-width:2px
style Ready fill:#d4edda,stroke:#28a745,stroke-width:2px
style YAML fill:#fff,stroke:#343a40,stroke-width:1px
style Exec fill:#fff,stroke:#343a40,stroke-width:1px
style Check fill:#fff,stroke:#343a40,stroke-width:1px
style Launch fill:#fff,stroke:#343a40,stroke-width:1px
style Dep fill:#fff,stroke:#343a40,stroke-width:1px
style Trans fill:#fff,stroke:#343a40,stroke-width:1px
- Load YAML Configuration — Reads all package/node definitions, dependencies, and device-state mappings from a single YAML file.
- Select Execution Strategy — Determines parallel (multi-threaded) or sequential mode per YAML configuration.
- Native Process Spawning — Each package binary is launched via
fork/execwith executable paths resolved dynamically byament_index_cpp. Per-process log files are created with timestamps. - Dependency Wait — For each node, polls the managed-state of declared dependency nodes until they reach
ACTIVE(30-second timeout). - Lifecycle State Polling — Calls
GetStateservice with retry until the node responds, confirming it is alive and ready for state transitions. - Initial State Transition — Applies the
DEVICE_INITtarget lifecycle state via the multi-step state machine (e.g., triggeringConfigure ➔ Activatefor anACTIVEtarget).
After all nodes reach their initial states, the system enters the operational phase — waiting for Device State requests via /lifecycle_transition_device and applying per-node lifecycle targets through the TransitionEngine.
This section validates the impact of Deterministic Lifecycle Manager using measurements collected on a pre-production commercial cleaning robot platform publicly showcased at IFA 2025.
For evaluation purposes, the product's software stack was ported to ROS 2, system interfaces were redesigned, and the C++ DLM was deployed directly on the target hardware. The goal was realistic system-level evaluation under severe production constraints (memory pressure and strict boot-time requirements). All measurements were performed on identical hardware using the same ROS 2 node set.
| ITEM | Specification |
|---|---|
| HW Platform (AP) | LG DQ1 (Cortex-A53x4 @ 1GHz) |
| RAM | 1GB |
| eMMC | 4GB |
| ROS 2 Distribution | ROS 2 Humble |
| Managed ROS 2 Nodes | 14 nodes + lifecycle node |
| OS | Yocto based ROS 2 |
| Yocto Version | Kirkstone |
Two boot configurations were evaluated:
- Baseline: ROS 2 default Python-based launch
ROS 2 nodes were started using the standard
ros2 launchworkflow, which loads the Python interpreter and initializes the launch framework during system boot. - Modified: Binary-based launch using Deterministic Lifecycle Manager All ROS 2 nodes were executed directly as OS processes under DLM control, without involving the Python runtime.
No other system components or ROS 2 node implementations were changed between the two configurations.
The measured results are summarized in the data below.
| Metric | Python-based launch | C++ DLM (Proposed) |
|---|---|---|
| Boot time (s) | 59.96 ± 0.70 | 11.20 ± 0.68 |
| Avg RAM during boot (MB) | 228.95 ± 1.80 | 201.28 ± 8.59 |
| Stable RAM 5 s after boot (MB) | 441.97 ± 1.41 | 270.86 ± 1.83 |
Values are averaged over ten repeated runs (Mean ± Std. Dev.).
- Boot completion time was reduced from 59.96s to 11.20s, corresponding to an 81.3% reduction.
- Stable memory usage after startup decreased from 441.97MB to 270.86MB, corresponding to a 38.7% reduction.
| Legacy Sequential Python Launch | Proposed Parallel Native C++ DLM |
|---|---|
![]() |
![]() |
Left: Legacy Sequential Python Launch | Right: Proposed Parallel Native C++ DLM
-
⏱️ Execution Time Comparison (Boot Speed)
- Python Launch:
59.96s - Binary (C++):
11.20s - Improvement: ↓ 81.3%
(59.96s ➔ 11.20s)
- Python Launch:
-
💾 Stable Memory Usage Comparison (RAM)
- Python Launch:
441.97MB - Binary (C++):
270.86MB - Improvement: ↓ 38.7%
(441.97MB ➔ 270.86MB)
- Python Launch:
All experiments were conducted on identical hardware and software configurations.
- Same nodes and execution graph
- Same workload
- Only the orchestration mechanism was changed (Python-based ros2 launch vs LifecycleManager)
Detailed configuration and setup are available in this repository.
A side-by-side boot comparison (Legacy Python vs. Native C++ DLM) is shown in the Performance Charts section above.
💡 Benchmark Reproducibility:
The 81.3% performance gain was verified using the built-in Benchmark Mode. By toggling theuse_launch_scriptflag in the same LifecycleManager instance, we compared identical node sets launched via Python scripts vs. direct C++ native spawning, supporting the interpretation that a large portion of the improvement is attributable to orchestration-path differences.
Conclusion: By removing Python from the evaluated runtime path, startup memory pressure was reduced, and OOM events observed in the baseline configuration were not reproduced in the evaluated DLM configuration. These improvements were achieved without modifying the ROS 2 nodes themselves and resulted in a stable and repeatable boot sequence on the target hardware.
- Architecture:
x86_64(Development/PC) andARM64/AArch64(Embedded Target) - OS: Ubuntu 22.04 (Jammy) or later / Linux (Yocto-based)
- ROS 2: Humble, Iron, or Jazzy
- Compiler: C++17 or higher (Required for
<filesystem>and modern C++ features) - Dependencies:
rclcpp,lifecycle_msgs,lifecycle_manager_msgs,yaml-cpp
The full implementation is not yet publicly available in this repository.
Build and run instructions will be added once the source release is completed.
Currently available in this repository:
- architecture documentation
- YAML configuration examples
- measurement methodology
- empirical validation results
This project is licensed under the Apache License 2.0, allowing internal commercial use as well as future open-source contributions.
As described at the top of this document, this repository currently provides architectural documentation, configuration examples, and empirical validation results that fully describe the execution model and lifecycle behavior of the system.
The complete C++ source code is planned to be released under the same license following completion of internal compliance procedures.

