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.

Where to go next

  • Calibrate the hardware with solarian calibrate before first run-robot on 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 into dataset_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.

Was this page helpful?