lifecore_ros2: composing reusable ROS 2 Python lifecycle components

Hi ROS community,

I would like to share a small ROS 2 project I have been building around lifecycle nodes in Python:

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:

  • LifecycleComponentNode
  • LifecycleComponent
  • lifecycle-aware publisher components
  • lifecycle-aware subscriber components
  • lifecycle-aware timer components
  • lifecycle-aware service server/client components
  • a when_active guard 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:

  1. plain ROS 2 node
  2. classic ROS 2 lifecycle node
  3. lifecore_ros2 component-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+
  • rclpy
  • uv for 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.

2 Likes