lifecore_ros2 is a minimal lifecycle composition library for ROS 2 Jazzy — no hidden state machine.
ROS 2 lifecycle works well for nodes. lifecore_ros2 makes it practical for reusable components.
Raw rclpy lifecycle:
LifecycleNode
├── publishers
├── subscriptions
├── timers
└── lifecycle logic mixed into one class
lifecore_ros2:
LifecycleComponentNode
├── LifecyclePublisherComponent
├── LifecycleSubscriberComponent
└── LifecycleTimerComponent
The node still owns the native ROS 2 lifecycle. The library adds a small composition layer so reusable components can follow the same lifecycle contract.
- not a second lifecycle state machine
- not a plugin system or ROS 2 component container replacement
- not a behavior tree system, orchestration middleware, or launch replacement
- not a replacement for native ROS 2 lifecycle semantics
lifecore_ros2 requires a working ROS 2 Jazzy Python environment. Install and source ROS 2 before importing this package:
source /opt/ros/jazzy/setup.bash
uv add lifecore-ros2rclpy comes from the system ROS installation. It is intentionally not declared as a normal PyPI dependency.
The examples/composed_pipeline.py walk-through highlights the key distinction the library makes explicit: deactivate ≠ cleanup — /pipeline/* topics persist across deactivate and only disappear on cleanup.
Audience. This library is for teams building modular ROS 2 nodes that need reusable lifecycle-aware components, especially in larger robotics stacks, embedded systems, or runtime-orchestrated applications.
Problem framing. ROS 2 provides a powerful managed-node lifecycle (configure → active → deactivate → cleanup). In practice, using it for anything beyond a trivial node leads to recurring problems:
- lifecycle logic gets scattered across monolithic node classes with no clear ownership
- ROS resource setup and teardown (publishers, subscriptions, timers) are easy to make inconsistent — resources allocated in the wrong place or released too late
- runtime gating ("only process messages when active") is hand-rolled differently each time, with no shared, tested pattern
- reusable lifecycle-aware building blocks are awkward in raw
rclpybecause the lifecycle contract is on the node, not on reusable sub-units
lifecore_ros2 solves these four problems with a small, explicit composition layer. It does not replace or extend the ROS 2 lifecycle state machine — it makes the lifecycle contract expressible at the component level.
flowchart LR
Lifecycle[ROS 2 Lifecycle]
Node[LifecycleComponentNode]
Components[LifecycleComponent instances]
Lifecycle <--> Node
Node <--> Components
Lifecycle -. drives .-> Components
A small set of lifecycle-aware building blocks:
| Symbol | Role |
|---|---|
LifecycleComponentNode |
Lifecycle node that owns and drives registered LifecycleComponent instances |
LifecycleComponent |
Base class for a lifecycle-aware managed entity (abstract by convention — override _on_* hooks) |
TopicComponent |
Base class for topic-oriented components (pub/sub) |
LifecyclePublisherComponent |
Lifecycle-gated ROS publisher |
LifecycleSubscriberComponent |
Lifecycle-gated ROS subscriber |
LifecycleTimerComponent |
Lifecycle-gated ROS timer |
ServiceComponent |
Base class for service-oriented components (server/client) |
LifecycleServiceServerComponent |
Lifecycle-gated ROS service server |
LifecycleServiceClientComponent |
Lifecycle-gated ROS service client |
when_active |
Decorator that guards any method to the active state |
LifecoreError and subclasses |
Typed exceptions for boundary violations |
lifecore_ros2.testing |
Reusable fakes, fixtures, assertions, and helpers for lifecycle-focused tests |
The library stays lifecycle-native, keeps ownership in LifecycleComponentNode, and treats component hooks as explicit extension points rather than hidden orchestration.
When sibling components need deterministic ordering, prefer declaring dependencies and priority at add_component(...) so composition intent stays visible in the node assembly code. examples/composed_ordered_pipeline.py shows this pattern without constructor pass-through on library components.
See docs/architecture.rst for lifecycle design rules, docs/patterns.rst for usage patterns, and ROADMAP.md for non-goals and deferred scope. See Examples Repository Plan for the companion repository planning. See CHANGELOG.md for shipped changes or the GitHub Releases page for tagged releases.
- Python 3.12 or newer
- ROS 2 Jazzy installed on the system
uvavailable in the workspace
rclpy is expected to come from the system ROS installation. It is intentionally not declared as a normal PyPI dependency.
Clone the repository, source ROS 2 Jazzy, and sync the local development environment:
git clone https://github.com/apajon/lifecore_ros2.git
cd lifecore_ros2
source /opt/ros/jazzy/setup.bash
uv sync --extra devRun the smallest composed lifecycle example already in the repository:
uv run python examples/minimal_node.pyFrom another terminal in the same ROS 2 environment, drive the node through configure and activate:
source /opt/ros/jazzy/setup.bash
ros2 lifecycle set /minimal_lifecore_node configure
ros2 lifecycle set /minimal_lifecore_node activateFor the full walkthrough, see docs/quickstart.rst. For validation and documentation commands, see docs/getting_started.rst. For the activation-gated subscriber example, continue with examples/minimal_subscriber.py or docs/examples.rst.
The documentation now follows the same lifecycle vocabulary as the library:
- Configure: environment, prerequisites, ROS resource creation model
- Activate: runtime enablement and activation gating
- Run: examples, API usage, composed execution flow
- Transition: ownership, propagation, and error handling rules
- Shutdown: cleanup, release, and lifecycle end-state expectations
Recommended order:
- docs/quickstart.rst
- docs/getting_started.rst
- docs/concepts/mental_model.rst
- docs/architecture.rst
- docs/patterns.rst
- docs/testing.rst
- docs/examples.rst
examples/minimal_subscriber.py is the next runnable example if you want to see activation-gated message delivery after the minimal node quickstart.
See examples/minimal_subscriber.py for the complete runnable file, docs/api_friction_audit.rst for the regression baseline, and docs/examples.rst for the walkthrough.
Component + node definition: 24 lines (regression baseline — see
docs/api_friction_audit.rst).
Run the publisher and observe activation gating:
uv run python examples/minimal_publisher.py
# in another terminal:
ros2 lifecycle set /publisher_demo_node configure
ros2 lifecycle set /publisher_demo_node activate
ros2 topic echo /chatterMessages appear only after activate. Deactivation stops them.
For the subscriber path, use the quickstart above or the full example walkthrough in docs/examples.rst.
All exported symbols and their stability levels are documented in ROADMAP.md.
The extension model and API buckets are defined in docs/architecture.rst and docs/api.rst.
- package metadata uses
Development Status :: 3 - Alphato reflect API stability, not lack of usability - the public API is in the
0.xseries — experimental stability level; minor bumps may include breaking changes - companion examples live in
lifecore_ros2_examples; start with thesensor watchdog lifecycle comparisonfor the first applied example, then seeROADMAP.mdthere for future scenarios
This project is licensed under the Apache-2.0 License — see LICENSE.
Documentation: https://apajon.github.io/lifecore_ros2/
Full documentation lives under docs/ and is built with Sphinx:
uv sync --extra dev --group docs
uv run --group docs python -m sphinx -b html docs docs/_build/htmlKey pages:
docs/getting_started.rst— setup and validation commandsdocs/architecture.rst— lifecycle design rules, error policy, member conventionsdocs/patterns.rst— recommended patterns and anti-patternsdocs/testing.rst— reusable lifecycle test fakes, fixtures, assertions, and helpersdocs/migration_from_rclpy.rst— before/after comparison with raw rclpydocs/api.rst— generated API referencedocs/examples.rst— example walkthroughs
Versioning uses Conventional Commits and python-semantic-release. Preview the next version:
uv run --group release semantic-release version --printRelease (version commit + tag, skip hosted release if no token):
uv run --group release semantic-release version --no-vcs-release
git push origin main --follow-tagsSee ROADMAP.md for promotion-to-1.0.0 criteria.

