Robots
A Robot is the on-robot node in the Solarian three-node architecture. It runs on the robot itself and is responsible for:
- Owning the hardware (every Module is spawned as its own OS process by the robot).
- Receiving incoming Actions from the relay server.
- Routing each action to either the right module's action queue or a robot-level
@action_handler. - Publishing the latest Observation back out to the relay server at the configured frame rate.
Defined in solarian/core/robot.py.
Defining a Robot
Subclass Robot and attach Module instances as class attributes. The attribute name is the canonical module ID — it's used to look up conf/modules/<name>.yaml, to key the Observation and Action schemas, and to address per-module action queues.
from solarian.core import Robot
from solarian_ur5.module import UR5ArmModule
from solarian_feetech.module import FeetechGripperModule
class MyRobot(Robot):
arm = UR5ArmModule()
gripper = FeetechGripperModule()
That's it — no __init__, no boilerplate. Robot.__init__ walks cls.__dict__, finds every Module instance, loads its YAML, instantiates the module, and wires up shared-memory ring buffers.
Robot is a singleton. The first instantiation wins; subsequent calls return the same object. This is what makes it safe to reference MyRobot.arm.shoulder_pan.pos from your Observation and Action schemas at import time.
Registering your Robot class
Solarian needs to know which Robot subclass to instantiate when solarian run-robot runs. Set it in your project's settings.py:
ROBOT_CLASS = 'myrobot.robot.MyRobot'
CONTROLLER_CLASS = 'myrobot.controller.MyController'
SERVER_CLASS = 'myrobot.server.MyServer'
The CLI reads ROBOT_CLASS, imports it, and feeds it the parsed RobotConfig from conf/robot.yaml.
Configuration
Each robot loads conf/robot.yaml (the default config name for solarian run-robot):
connection: ${deployment.connection}
img_compression_quality: 70
fps: 30
The fields map to RobotConfig:
- Name
fps- Type
- int
- Description
Rate at which the robot publishes observations to the relay server.
- Name
img_compression_quality- Type
- int
- Description
JPEG quality (0–100) for any
ImageDatafield that gets compressed before publishing.
- Name
connection- Type
- ConnectionConfig
- Description
ZMQ ports and IPs — read from
conf/deployment/<name>.yaml. See Server.
Every individual module attached to the robot also reads its own conf/modules/<attr>.yaml. So MyRobot.arm reads conf/modules/arm.yaml, MyRobot.gripper reads conf/modules/gripper.yaml, etc. See Modules and Project Configs.
Lifecycle
When you run solarian run-robot, the framework calls these methods in order:
Robot(cfg) # __init__ — discovers modules, builds shared memory
│
├── connect() # connect every module to its hardware (in the parent)
├── initialize() # call module.initialize() on every module
└── start() # disconnect parent-side, spawn module processes,
# start observation sender thread, then control_loop
connect()opens hardware buses (USB, serial, sockets, cameras) and runs each module'sconnect(). Calibration is loaded here — and the interactive sweep starts if the file is missing.initialize()runs each module'sinitialize()hook. Use this for one-time setup like setting torque, operating mode, or homing.start()disconnects in the parent process (so each module process can re-open the bus on its own), spawns one OS process perModule, starts shared bus servers for any port shared by multiple modules, and finally enterscontrol_loop().
A separate _observation_sender thread runs at ~200 Hz, reading the latest observation from each module's shared-memory ObservationBuffer and publishing it as a single combined message at cfg.fps.
Action routing
The control loop pulls Action messages off the ZMQ socket and dispatches them based on action.action_type:
Action received
│
├── action_type == MODULE_ACTION → push each ModuleAction onto
│ module.action_queue (shared memory)
│
└── otherwise → look up @action_handler(action_type)
on the Robot subclass and call it
You can declare robot-level handlers for custom action types — see Custom action types:
from solarian.core import Robot
from solarian.core.action import action_handler, ActionType
class MyActionType(ActionType):
EMERGENCY_STOP = ActionType.CUSTOM_START + 1
class MyRobot(Robot):
arm = ArmModule()
@action_handler(MyActionType.EMERGENCY_STOP)
def _on_emergency_stop(self, action, t_now):
for module in self.modules.values():
module.action_queue.clear()
return False # return False to stop the control loop
Events on the robot
Routing actions is not the whole tick. Before dispatching, control_loop drains its modules' events and ticks any inline module so it publishes observations. A Robot is an event host in exactly the way a Controller is, sharing the same EventHost machinery and the same @event_handler decorator, so a limit switch tripping reaches a robot-side handler on the next tick rather than waiting on a round trip to the controller.
A robot-side skill is started by name. Every Robot handles START_SKILL and STOP_SKILL generically: the handler looks the name up among the installed skill plugins and the robot's own skills tuple, constructs the class with the params that came over the wire, and starts it through start_skill() — which wires it to the robot's observation buffers, action queues and event inbox, and resolves the module names it was given back into real modules so its action builders work. So a project makes a skill startable by publishing or declaring it, not by writing a matching action type and handler on both sides of the link.
Multi-module shared ports
If two modules share a hardware port (for example, a serial bus with multiple Dynamixel servos addressed by ID), Solarian automatically starts a BusServer in the parent process and gives each module a BusClient proxy. From your code's perspective the bus still looks like a normal ModuleBus — you don't have to coordinate access yourself.
See also
- Modules — how to declare per-module behavior.
- Calibration — limit sweep, IK alignment, and latency measurements before first
run-robot. - Server — the relay node sitting between robots and controllers.
- Controllers — how to send actions and consume observations.
- Project Configs — the conventions that bind YAML files to module attributes.
- Actions — declaring custom routes for non-standard actions.
- Simulation — run the same robot in MuJoCo with
is_sim = True, in a scene you build.