Creating a Project
This guide walks through creating a brand-new Solarian project, adding a hardware module from a plugin, and running the three nodes — relay server, robot, and controller — for the first time. By the end you'll have a working keyboard teleop session against either a real arm or a simulated module.
1. Scaffold a project
create-solarian is shipped as a separate entry point so it works before you have a project on disk.
create-solarian myrobot
cd myrobot
The scaffolder produces a project with this layout:
myrobot/
├── conf/
│ ├── controller.yaml # `solarian run-controller` config
│ ├── robot.yaml # `solarian run-robot` config
│ ├── server.yaml # `solarian run-server` config
│ ├── record.yaml # `solarian record` config
│ ├── replay.yaml # `solarian replay` config
│ ├── train.yaml # `solarian train` config
│ └── deployment/
│ └── localhost.yaml # relay IP/ports and device paths per host
├── myrobot/
│ ├── __init__.py
│ ├── settings.py # ROBOT_CLASS / CONTROLLER_CLASS / SERVER_CLASS
│ ├── robot.py # MyrobotRobot(Robot)
│ ├── controller.py # MyrobotController(Controller)
│ ├── server.py # MyrobotServer(RelayServer)
│ ├── observation.py # Observation/ModuleObs schema
│ ├── action.py # Action/ModuleAction schema
│ ├── modules/
│ │ └── example_module.py
│ └── dataset_ops/
│ └── drop_short_episodes.py # an example dataset op
└── pyproject.toml
The generated settings.py wires the project together by class path:
ROBOT_CLASS = 'myrobot.robot.MyrobotRobot'
CONTROLLER_CLASS = 'myrobot.controller.MyrobotController'
SERVER_CLASS = 'myrobot.server.MyrobotServer'
Install your project so the package is importable:
pip install -e .
2. Add a hardware module
Solarian hardware drivers ship as standalone solarian-<name> packages. solarian install installs the plugin and copies its default YAML config into your project's conf/ directory.
solarian install ur5
This installs solarian-ur5 and creates conf/modules/ur5.yaml from the plugin's templates/modules/ur5.yaml. The filename is the attribute name you'll use on your Robot. This guide calls the module arm, so rename the file to match:
mv conf/modules/ur5.yaml conf/modules/arm.yaml
See Core Plugins for the drivers to start with.
Edit myrobot/robot.py to attach the module:
from solarian.core import Robot
from solarian_ur5.module import UR5ArmModule
class MyrobotRobot(Robot):
arm = UR5ArmModule()
The attribute name arm matches conf/modules/arm.yaml — that's how Solarian routes per-module configuration. See Project Configs for details.
3. Define an observation and an action schema
Solarian uses Pydantic-typed Observation and Action classes to describe what flows over the wire. The scaffolder generates a starting point you can edit:
from solarian.core.observation import SensorField, ModuleObs, Observation
from myrobot.robot import MyrobotRobot
class ArmObs(ModuleObs):
shoulder_pan_pos = SensorField(MyrobotRobot.arm.shoulder_pan.pos)
class MyrobotObservation(Observation):
arm = ArmObs()
from solarian.core.action import ModuleAction, Action, ActuatorField
from myrobot.robot import MyrobotRobot
class ArmAction(ModuleAction):
shoulder_pan = ActuatorField(MyrobotRobot.arm.shoulder_pan.pos)
class MyrobotAction(Action):
arm = ArmAction()
4. Wire up keyboard teleop in the controller
Every controller comes with a keyboard module that turns key transitions into events. Handle them with @event_handler, and return an action from control_step:
from solarian.core import Controller
from solarian.core.action import ActionType
from solarian.core.event import Event, EventType, event_handler
from myrobot.action import MyrobotAction
class MyrobotController(Controller):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.target = MyrobotAction.zeros()
@event_handler(EventType.KEY_DOWN)
def on_key_down(self, event: Event):
if event.data["key"] == 'a':
self.target.arm.shoulder_pan.value -= 0.05
elif event.data["key"] == 'd':
self.target.arm.shoulder_pan.value += 0.05
def control_step(self) -> MyrobotAction:
self.target.action_type = ActionType.MODULE_ACTION
return self.target
Handlers run on the control thread when the tick drains, so they never race the control loop. For keys you hold rather than tap, read self.held_keys in control_step instead.
See Controllers for the full lifecycle, Events for other input devices, and Action types for the enum.
5. Run the three nodes
For local development you can run all three nodes on the same machine using the default conf/deployment/localhost.yaml:
solarian run-server
To split nodes across machines, add conf/deployment/lab.yaml with the relay's IP and each machine's device paths, then launch every node with --deployment lab. See Deployments.
6. Confirm everything works
In the controller window, press a and d to nudge shoulder_pan. You should see the corresponding actuator move. The relay server window logs every action and observation it forwards.
If something doesn't move, double-check that the module attribute name (arm) matches the YAML filename (conf/modules/arm.yaml) and the Observation/Action attribute names. Solarian validates these at class definition time and will raise a TypeError if they disagree.
Where to go next
- Calibrate the hardware with
solarian calibratebefore firstrun-roboton a real arm or gripper — see Calibration. - Add more modules with
solarian install <plugin>— see Core Plugins. - Record a dataset of teleoperated demonstrations:
solarian record. - Curate it with dataset ops, your own included:
solarian add-op <name>writes one intodataset_ops/— see Dataset ops. - Replace the controller with a learned policy, served with
solarian run-service <skill conf>and driven by a skill, or with the AI Controller for VLM-driven control. - Train a policy with
solarian train.