Hi ROS community,
I would like to share a small ROS 2 project I have been building around lifecycle nodes in Python:
- Core library: GitHub - apajon/lifecore_ros2: lifecore_ros2 is a modular ROS 2 framework designed to structure robotic applications around lifecycle-aware components. It provides a clean separation between nodes and components, enforces strict lifecycle management, and enables scalable, maintainable architectures for complex robotic systems. ยท GitHub
- Examples: GitHub - apajon/lifecore_ros2_examples ยท GitHub
The project is called lifecore_ros2.
It is an experimental Python library for ROS 2 Jazzy that helps structure lifecycle nodes as a set of explicit, reusable, lifecycle-aware components.
Why I built this
ROS 2 lifecycle nodes are very useful when a node needs a clear managed state:
unconfigured -> inactive -> active -> finalized
In practice, though, when building Python nodes with rclpy, I often ended up with one large lifecycle node class containing everything:
SensorWatchdogLifecycleNode
โโโ parameters
โโโ publishers
โโโ subscribers
โโโ timers
โโโ service clients
โโโ lifecycle callbacks
โโโ runtime logic
This works, but as the node grows, the lifecycle logic and the application logic can become tightly mixed.
The recurring questions are usually:
- where should this publisher be created?
- should this timer exist while inactive?
- should this subscription callback run while inactive?
- where should cleanup happen?
- how do I reuse this piece of lifecycle-aware logic in another node?
lifecore_ros2 tries to make those boundaries explicit.
The idea
Instead of putting all lifecycle behavior directly into one large node class, the node owns a set of components.
LifecycleComponentNode
โโโ SensorSubscriberComponent
โโโ StatusPublisherComponent
โโโ WatchdogTimerComponent
โโโ DiagnosticsComponent
The node still owns the ROS 2 lifecycle.
Components do not introduce a second state machine. They simply receive lifecycle hooks from the node:
configure -> create ROS resources
activate -> enable runtime behavior
deactivate -> stop/gate runtime behavior
cleanup -> release ROS resources
So the goal is not to replace ROS 2 lifecycle nodes.
The goal is to make lifecycle nodes easier to compose, test, reason about, and reuse across several nodes when they share similar ROS interfaces.
Reusable lifecycle-aware building blocks
Another motivation behind this project is reuse.
In many ROS 2 projects, the same topic-level logic appears in several nodes:
- subscribing to the same sensor topic
- publishing the same command or status topic
- exposing the same diagnostic publisher
- wrapping the same service client
- applying the same lifecycle guard around callbacks
Without a component boundary, this logic often ends up being copied from one node class to another.
lifecore_ros2 is designed to make those pieces reusable and easy to move between nodes.
For example, instead of rewriting the same subscription setup in several lifecycle nodes, you can create a component once:
class BatteryStateSubscriberComponent(LifecycleSubscriberComponent):
...
Then reuse it wherever it is needed:
node.add_component(BatteryStateSubscriberComponent(...))
The same idea applies to publishers, timers, service clients, and small domain-specific pieces of node behavior.
The goal is not only to make one node cleaner.
The goal is to make lifecycle-aware ROS 2 logic portable:
write once -> import in another node -> attach to lifecycle -> reuse
This is useful when several nodes share the same communication contracts but have different responsibilities.
What the library provides
The core package currently includes:
LifecycleComponentNodeLifecycleComponent- lifecycle-aware publisher components
- lifecycle-aware subscriber components
- lifecycle-aware timer components
- lifecycle-aware service server/client components
- a
when_activeguard for callbacks - typed lifecycle/composition errors
- test helpers for lifecycle-oriented unit tests
The pattern is intentionally small.
It is not:
- a replacement for native ROS 2 lifecycle semantics
- a second lifecycle state machine
- a behavior tree system
- a component container replacement
- a launch/orchestration framework
- a plugin framework
It is just a composition layer inside a Python lifecycle node.
Example: sensor watchdog
The examples repository contains a direct comparison of the same node implemented three ways:
- plain ROS 2 node
- classic ROS 2 lifecycle node
lifecore_ros2component-based lifecycle node
The example is a small sensor watchdog:
/sensor/value -> watchdog node -> /sensor/status
The watchdog receives sensor values, checks whether the latest value is fresh, and publishes a status.
The point of the example is not that the node is complex.
The point is that the lifecycle behavior becomes visible:
- with a plain ROS 2 node, everything starts immediately
- with a classic lifecycle node, the lifecycle exists, but guards and resource handling are manual
- with
lifecore_ros2, publishers, subscribers, timers, and runtime behavior are grouped into explicit lifecycle-aware components
Quick start
Core library:
git clone https://github.com/apajon/lifecore_ros2.git
cd lifecore_ros2
source /opt/ros/jazzy/setup.bash
uv sync --extra dev
uv run python examples/minimal_node.py
In another terminal:
source /opt/ros/jazzy/setup.bash
ros2 lifecycle set /minimal_lifecore_node configure
ros2 lifecycle set /minimal_lifecore_node activate
Examples repository:
git clone https://github.com/apajon/lifecore_ros2_examples.git
cd lifecore_ros2_examples
source /opt/ros/jazzy/setup.bash
uv sync --dev
Run the fake sensor publisher:
uv run python examples/lifecycle_comparison/sensor_value_publisher_node.py
Run the lifecore_ros2 watchdog:
uv run python examples/lifecycle_comparison/lifecore_ros2/sensor_watchdog_lifecore_node.py
Then activate it:
ros2 lifecycle set /sensor_watchdog_lifecore configure
ros2 lifecycle set /sensor_watchdog_lifecore activate
ros2 topic echo /sensor/status
Current status
The project currently targets:
- ROS 2 Jazzy
- Python 3.12+
rclpyuvfor local development
The API is still in the 0.x series, so I consider it experimental.
I am sharing it now because I would like feedback before freezing too many design decisions.
Feedback wanted
I would be very interested in feedback from people using lifecycle nodes in real ROS 2 Python projects.
In particular:
- does this component-based lifecycle model make sense to you?
- are lifecycle-aware publishers, subscribers, timers, and services useful primitives?
- would reusable topic-level components be useful in your ROS 2 codebase?
- do you already copy/paste publisher, subscriber, timer, or service client logic between nodes?
- is the ownership model clear?
- does this solve a real pain point, or does it add too much abstraction?
- where does it overlap too much with existing ROS 2 patterns?
- what would you expect before using something like this in a production robot?
Any criticism is welcome.
The goal is to keep the library small and aligned with ROS 2 lifecycle semantics, not to build a new framework on top of ROS 2.
Thanks for reading.