Tutorial 6 — Components
Components are state-machine-scoped objects that provide reusable functionality to behaviors. They live inside a client and survive state transitions. In this tutorial you will study two component patterns from the cl_px4_mr client library: a subscriber data store and an updatable monitor.
ISmaccComponent Basics
All components inherit from smacc2::ISmaccComponent:
class MyComponent : public smacc2::ISmaccComponent
{
public:
void onInitialize() override
{
// Called when the component is created by the client
// Set up subscriptions, publishers, other components
}
};
Key rules:
onInitialize()is called after all components are created — you can safely callrequiresComponent()here to get sibling components.Components live as long as the state machine. They are not destroyed on state transitions.
Components expose
SmaccSignalmembers for behaviors to connect to.
Pattern 1: Subscriber + Data Store
CpVehicleLocalPosition subscribes to a PX4 topic, stores the latest position data behind a mutex, and fires a signal when new data arrives.
// cl_px4_mr/components/cp_vehicle_local_position.hpp
#pragma once
#include <mutex>
#include <px4_msgs/msg/vehicle_local_position.hpp>
#include <rclcpp/rclcpp.hpp>
#include <smacc2/smacc.hpp>
namespace cl_px4_mr
{
class CpVehicleLocalPosition : public smacc2::ISmaccComponent
{
public:
CpVehicleLocalPosition();
virtual ~CpVehicleLocalPosition();
void onInitialize() override;
float getX() const;
float getY() const;
float getZ() const;
float getHeading() const;
bool isValid() const;
smacc2::SmaccSignal<void()> onPositionReceived_;
private:
void onPositionMessage(
const px4_msgs::msg::VehicleLocalPosition::SharedPtr msg);
rclcpp::Subscription<px4_msgs::msg::VehicleLocalPosition>::SharedPtr
subscriber_;
float x_ = 0.0f;
float y_ = 0.0f;
float z_ = 0.0f;
float heading_ = 0.0f;
bool valid_ = false;
mutable std::mutex mutex_;
};
} // namespace cl_px4_mr
Key elements:
ROS subscription — created in
onInitialize()using the state machine’s node handleMutex-protected data — getter methods lock the mutex for thread-safe reads
SmaccSignal —
onPositionReceived_fires in the subscription callback so behaviors can react to position updatesThread safety — the subscription callback runs on a ROS executor thread, so all shared data is mutex-protected
Pattern 2: Updatable Monitor
CpGoalChecker inherits from both ISmaccComponent and ISmaccUpdatable, which gives it a periodic update() method called at ~20 Hz by the SignalDetector.
// cl_px4_mr/components/cp_goal_checker.hpp
#pragma once
#include <cmath>
#include <smacc2/smacc.hpp>
namespace cl_px4_mr
{
class CpVehicleLocalPosition;
class CpGoalChecker : public smacc2::ISmaccComponent,
public smacc2::ISmaccUpdatable
{
public:
CpGoalChecker();
virtual ~CpGoalChecker();
void onInitialize() override;
void update() override;
void setGoal(float x, float y, float z,
float xy_tolerance = 0.5f, float z_tolerance = 0.3f);
void clearGoal();
bool isGoalActive() const;
smacc2::SmaccSignal<void()> onGoalReached_;
private:
CpVehicleLocalPosition * localPosition_ = nullptr;
float goalX_ = 0.0f;
float goalY_ = 0.0f;
float goalZ_ = 0.0f;
float xyTolerance_ = 0.5f;
float zTolerance_ = 0.3f;
bool goalActive_ = false;
};
} // namespace cl_px4_mr
Key elements:
``ISmaccUpdatable`` — the
update()method is called periodically by the SMACC2 SignalDetector``requiresComponent()`` — in
onInitialize(), it gets a pointer toCpVehicleLocalPosition(a sibling component on the same client)Goal checking —
update()reads the current position fromCpVehicleLocalPosition, compares it against the goal, and firesonGoalReached_when within toleranceSignal — behaviors like
CbTakeOffandCbGoToLocationconnect toonGoalReached_to know when to post success
Component-to-Component Dependencies
Components can depend on sibling components within the same client:
void CpGoalChecker::onInitialize()
{
requiresComponent(localPosition_); // Gets CpVehicleLocalPosition
}
This is safe because onInitialize() is called after all components are created by the client’s onComponentInitialization().
Global Component Access from Behaviors
requiresComponent() is also available to client behaviors. While the typical use is a behavior accessing a component on its own client, the search is actually global — it spans all clients across all orthogonals. A behavior on OrKeyboard can reach a component owned by OrData.
void onEntry() override
{
CpMissionData * missionData_ = nullptr;
this->requiresComponent(missionData_);
// missionData_ now points to the component regardless of which
// orthogonal or client owns it
missionData_->initialPosition = Position2D{1.0, 2.0};
}
The sm_data_sharing_1 reference state machine demonstrates this end-to-end: a local ClData client in OrData hosts CpMissionData, and behaviors in all three states access it via requiresComponent() regardless of which orthogonal they belong to. See the source code.
SmaccSignal: Declaring and Connecting
Declare a signal in a component:
// In component header
smacc2::SmaccSignal<void()> onGoalReached_;
Fire the signal:
// In component implementation
onGoalReached_();
Connect to the signal from a behavior:
// In behavior's onEntry()
this->getStateMachine()->createSignalConnection(
goalChecker_->onGoalReached_, &CbTakeOff::onGoalReachedCallback, this);
Always use createSignalConnection() instead of raw boost::signals2::connect(). The framework manages connection lifetimes and automatically disconnects when state-scoped objects (like behaviors) are destroyed on state exit.
Summary
You learned:
ISmaccComponentprovides theonInitialize()lifecycle hookThe subscriber + data store pattern: ROS subscription → mutex-protected data →
SmaccSignalThe updatable monitor pattern:
ISmaccUpdatablefor periodic checking →SmaccSignalComponent-to-component dependencies via
requiresComponent()Signal declaration, firing, and safe connection via
createSignalConnection()
Next Steps
In Tutorial 7 — Creating a Client you will create a complete client that composes components using the orchestrator pattern.