Architecture Overview
ROS Base splits a robot application into four fixed roles:
BaseManager: lifecycle and main loopBaseNode: communication and data cachingBaseAgent: algorithms and computationBaseHandlers: scheduling and state machines
The key idea is this: in attached mode, nodes and agents are registered into the same manager and share one ROS main-process context.
graph LR
WorldIn["External Environment / ROS2 Inputs"] --> Callback["1. Subscription Callback"]
subgraph ManagerProcess["BaseManager Process"]
Callback --> NodeCache["2. BaseNode Caches Data"]
Timer["Manager Timer"] --> Handler["3. BaseHandlers.handle()"]
NodeCache --> Handler
Handler --> Agent["4. BaseAgent Computation"]
Agent --> Handler
Handler --> NodePub["5. BaseNode Publishes Results"]
end
NodePub --> WorldOut["6. External Environment / Actuators"]
1. The actual relationships in code
BaseManager
BaseManager directly inherits rclpy.node.Node, so it is the object that actually participates in ROS2 spinning. During construction it performs three registrations:
- Register
nodes_dict - Register
agents_dict - Register
handlers_class
During registration, the manager passes itself to each object through manager=self.
BaseNode
BaseNode is not itself a Node subclass. It is a wrapper layer:
- When
managerexists, methods such ascreate_subscription()are forwarded to the manager. - When
manageris absent, it creates and owns a temporaryrclpy.node.Node.
That is where the dual-mode behavior comes from.
BaseAgent
BaseAgent does not create publishers or subscriptions. Its focus is computation and internal state. After registration into a manager, it can access context through:
self.nodesself.agentsself.stateself.timestamp
BaseHandlers
BaseHandlers.handle() is the default business entry point inside the main loop. It can either:
- Maintain its own internal state machine
- Or read and write
manager.statedirectly throughself.state
Both styles are used in the current example repositories.
2. What happens after startup
graph LR
A["Registry"] --> B["Handshake"]
B --> C["create_timer"]
C --> D["rclpy.spin"]
D --> E["release_resources + shutdown"]
Stage 1: Registry
Inside BaseManager.__init__():
super().__init__(
node_name="PickPlaceOrchestrator",
nodes_dict=nodes_dict,
agents_dict=agents_dict,
handlers_class=PickPlaceFSMHandlers,
node_freq_hz=10,
)
The manager instantiates each component in sequence:
self.nodes[node_name] = node_class(manager=self, *args, **kwargs)
self.agents[agent_name] = agent_class(manager=self, *args, **kwargs)
self.handlers = handlers_class(manager=self, *args, **kwargs)
Stage 2: Handshake
Before creating the timer, start_main_loop_timer() repeatedly checks handshake rules:
As long as any condition is not satisfied, the manager keeps calling spin_once(self, timeout_sec=0.1) until external messages provide the required data.
Stage 3: Main loop
After the handshake passes, the manager creates a fixed-rate timer and runs the following sequence inside main_loop():
- Optional manager-level state transitions
handlers.handle()timestamp += 1- Optional frequency logging
Stage 4: Shutdown
start_main_loop_timer() also handles the full shutdown sequence:
- Stop child processes
- Call
release_resources() - Call
destroy_node() - Call
rclpy.shutdown()
Because of that, it is usually not recommended to call rclpy.spin(manager) again from outside.
3. Two common state-machine styles
Style A: the handler owns a fine-grained FSM
SigLoMa-VLM uses this pattern:
manager.statestores only a coarse state or the startup statePickPlaceFSMHandlersmaintainscurrent_stateandprev_stateinternally
Advantages:
- State definitions can be more fine-grained
- It fits task-oriented FSMs well
Style B: the handler directly drives manager.state
quad_deploy uses this pattern:
self.statemaps directly tomanager.state- State values are global strings such as
"cold_start","turn", and"navigation"
Advantages:
- Global state stays centralized
- It fits control-state switching and cross-module broadcasting well
4. When to split into multiple processes
ROS Base does not require everything to stay inside one manager.
Good candidates to keep inside the same manager:
- High-frequency shared context
- Lightweight callback caches
- Business flows that need to read and write the same state directly
Good candidates to move into separate processes:
- Camera SDKs or blocking external commands
- Heavy computation that holds the GIL for long periods
- External toolchains that should not disturb the main control loop