Sugarcoat Event-Driven Architecture

Interactive visual reference for event routing and execution flows

System Architecture

A Sugarcoat application runs across two kinds of processes: the main process (hosting the Launcher and Monitor node) and one or more component processes. Select a flow below to highlight which parts of the architecture are involved.

Main Process
L Launcher
  • Orchestrates the ROS2 launch system
  • Routes event/action pairs by ownership
  • Registers OnInternalEvent handlers
  • Executes inline recipe methods & ROS launch actions
  • Creates bridge topics for callable → component flows
M Monitor (ROS2 Node)
  • Subscribes to event topics
  • Maintains the event blackboard
  • Evaluates topic-based conditions
  • Polls callable conditions via timers
  • Executes system-level actions (publish, srv, action goal)
  • Emits InternalEvent back to Launcher context
Component Process
C Component Node (LifecycleNode)
  • Subscribes to its routed event topics
  • Subscribes to bridge topics (/event_bridge/...)
  • Maintains its own event blackboard
  • Evaluates topic-based conditions
  • Executes @component_action methods
Component Process
C Component Node (LifecycleNode)
  • Same event infrastructure as any component
  • Independent blackboard and subscriptions

Execution Flows

Select a flow to see the step-by-step execution path and highlight the involved architecture components above.

Topic Monitor Topic Condition → Monitor Action

The Monitor subscribes to the event topic, evaluates the condition on each incoming message, and directly executes system-level actions (e.g. publish_message).

ROS Topic New message Monitor Sub __event_topic_callback Blackboard + lazy expiration check_condition() Evaluate expression ThreadPoolExecutor publish_message / srv / action goal

Topic Component Topic Condition → Component Action

The event is routed directly to the component. The component subscribes to the topic itself, evaluates the condition, and executes the @component_action method.

ROS Topic New message Component Sub __event_topic_callback Blackboard + lazy expiration check_condition() Evaluate expression ThreadPoolExecutor @component_action method

Topic Launcher Topic Condition → Launcher Action

The Monitor subscribes to the event topic and evaluates the condition. On trigger, it emits an InternalEvent to the ROS2 launch context. The Launcher's OnInternalEvent handler matches it and executes the inline recipe method or ROS launch action.

ROS Topic New message Monitor Sub check_condition() _on_internal_event() Emit to launch context IPC OnInternalEvent Match + inject topics Execute Launch Entity OpaqueFunction (inline method) or ROS launch action

Callable Monitor Callable Condition → Monitor Action

The Monitor polls the user-supplied callable at check_rate Hz via a timer. When it returns True, the registered system-level action is executed directly.

Timer Tick check_rate Hz callable() Returns True check_action _condition() ThreadPoolExecutor publish / srv / action goal

Callable Bridge Component Callable Condition → Component Action (Bridge)

The callable lives in the recipe (main process) but the action must run inside the component (separate process). A bridge topic (/event_bridge/e_{id}_{component}) carries a Bool message across the process boundary.

MONITOR (main process) COMPONENT (separate process) Timer check_rate Hz callable() Returns True publish_message() Bool(True) → /event_bridge/e_... ROS Topic Subscription callback Bridge topic listener Blackboard update on-any → True ThreadPoolExecutor @component_action method Why a bridge? The callable condition is a Python function defined in the recipe (main process). The @component_action runs inside the component's node (separate process). The bridge topic provides safe cross-process communication via ROS2 pub/sub.

Callable Launcher Callable Condition → Launcher Action

The Monitor polls the callable. On trigger, it emits an InternalEvent to the launch context, where the Launcher executes the inline recipe method or ROS launch action.

Timer Tick check_rate Hz callable() Returns True _on_internal_event() Emit to context IPC OnInternalEvent Match event_name Execute Inline recipe method

Action Routing Decision Tree

The Launcher inspects each action and routes it based on ownership. Hover over destination boxes to see the responsible source file and method.

Event / Action Pair Callable-based condition? No (topic-based) Yes (callable) @component_action? Yes Lifecycle action? Yes No No System-level action? Yes No Action owned by component? No Yes Launcher _ros_events_actions Component _components_events_actions Monitor _monitor_events_actions Launcher _ros_events_actions Monitor direct exec Bridge Monitor → Topic → Component Routing: launcher.py → __rewrite_actions_for_components() / __route_action_based_event() Emission: launch_actions.py → _on_internal_event()

Blackboard & Condition Evaluation

Both the Monitor and each Component maintain their own event blackboard. Click on a blackboard entry to see how it is handled during evaluation.

Event Blackboard

Dict[str, EventBlackboardEntry] — one entry per subscribed topic

FRESH
/sensor/data
msg: Float32(data=7.2)  |  age: 0.02s  |  id: a3f8...
EXPIRED
/camera/image
msg: Image(...)  |  age: 3.1s  |  id: c72b...
ALREADY PROCESSED
/odom
msg: Odometry(...)  |  age: 0.01s  |  id: e91d...

Evaluation Pipeline

1
Message arrives
__event_topic_callback(topic, msg) updates the blackboard entry with a fresh timestamp and new UUID.
2
Lookup dependent events
__events_per_topic[topic_name] returns all events that reference this topic.
3
Build clean cache subset
For each topic the event needs: check TTL (time-to-live) and idempotency (stale ID). Only valid entries pass through.
4
Evaluate condition tree
event.check_condition(subset) — applies on_change rising edge, handle_once guard, and keep_event_delay throttle.
5
Execute actions
If triggered, submit all registered actions to the shared ThreadPoolExecutor (10 workers). The ROS callback thread is never blocked.

Internal Data Flow Summary

How event/action data moves through the system from user configuration to runtime execution.

CONFIGURATION launcher.add_pkg() events_actions={...} __rewrite_actions _for_components() Route by ownership STORAGE _monitor_events_actions Dict[Event, List[Action]] System-level + callable bridge actions _components_events_actions Dict[str(JSON), List[Action]] Serialized events → component actions _ros_events_actions Dict[str(ID), List[LaunchEntity]] + _internal_events (Event objects) RUNTIME Monitor Node __reconstruct_monitor_actions() → resolve method refs _activate_event_monitoring() → create subscriptions + timers __event_topic_callback() / check_action_condition() Component Node Deserialize events from JSON → Event objects _turn_on_events_management() → subscriptions + register actions Launcher (ROS Launch Context) _setup_internal_events_handlers() → OnInternalEvent handlers ComponentLaunchAction.execute() → register emit callbacks InternalEvent emitted → handler matches → entity executes emit InternalEvent bridge