Command Line Interface

Solarian ships two console scripts:

  • solarian — the main CLI. Every project workflow (running a node, recording, training, …) goes through this command.
  • create-solarian — a one-shot project scaffolder. See Creating a Project.

Run solarian --help to print the grouped command list.

Global flags

Every solarian subcommand accepts these top-level flags before the subcommand name:

  • Name
    --conf
    Type
    string
    Description

    Path to the Hydra config directory. Defaults to conf. You can also pass dir/name to override the config name (for example, --conf conf/robot_dev).

  • Name
    --deployment, -d
    Type
    string
    Description

    Deployment profile in conf/deployment/, supplying the relay address and device ports for this host layout. Defaults to $SOLARIAN_DEPLOYMENT, then localhost. See Deployments.

  • Name
    --settings
    Type
    string
    Description

    Path to your project's settings.py. Auto-discovered when omitted (looks for {package_name}/settings.py based on pyproject.toml, then conf/settings.py).

  • Name
    --version, -V
    Type
    flag
    Description

    Print the installed Solarian version and exit. Works on its own, without a subcommand.

Hydra-style overrides on the command line (key=value) are forwarded to the underlying Hydra compose() call, so anything in your YAML can be overridden ad hoc.

solarian run-robot fps=60 connection.ip=192.168.1.20

Some commands also take parameters that are not YAML keys at all — role, module, sweep, axis. Hydra composes in struct mode and would normally reject those, so a command declares them in a params tuple and they are seeded before your overrides are applied:

class CalibrateCameraLatencyCommand(BaseCommand):
    params = ("role", "monitor_hz", "qr_size", "n_frames", "plot", "allow_unsynced")

You then write them plainly, role=display, with no + prefix. A name that is neither a YAML key nor a declared parameter is still rejected, which is what catches frps=60 before it silently does nothing.

Project commands

create-project

solarian create-project <project_name> [--force]

Scaffolds a new project. Prefer the standalone create-solarian <name> script — it's the same code, but works before a Solarian project exists in the current directory.

install

solarian install <package-or-path>

Installs a plugin into the current Python environment, adds it to the project's pyproject.toml, and copies its config template into conf/ when that file is not already there. A name is resolved on PyPI first (ur5 installs solarian-ur5). If PyPI has no such project, the command looks in the current directory and then in plugins/. A path installs that directory editable and skips PyPI.

templates/<folder>/<file>.yaml is copied to conf/<folder>/<file>.yaml, so a module plugin's templates/modules/ur5.yaml lands at conf/modules/ur5.yaml, and the attribute on your Robot has to match that filename. See Installing a plugin and Project Configs.

solarian install solarian-move-skill
solarian install plugins/solarian-umi-slim
solarian install ur5

add-op

solarian add-op <name> [--force]

Writes <package>/dataset_ops/<name>.py, a dataset op stub copied from the same template as the drop_short_episodes.py example every new project gets. Nothing needs registering: each file in dataset_ops/ is an op, named after the file with _ turned into - (split_by_success.py is split-by-success), unless its class sets name. --force overwrites the file. See Dataset ops.

solarian add-op split-by-success
solarian dataset ops                 # lists it, with source project:so106/dataset_ops/split_by_success.py

Robot commands

These commands run on the robot machine. They import the modules listed under setup_modules=("robot", "observation", "action").

run-robot

solarian run-robot

Loads conf/robot.yaml, instantiates your Robot subclass, spawns one process per Module, and starts the action receiver / observation publisher. See Robots.

calibrate

solarian calibrate

Interactive bring-up for one module. Four steps on an IK arm: Step 1: Joint limits, Step 2: Home position (match the static model-q=0 sim), Step 3: Sim mapping (highlighted joint, Y/N), Output. The live mirror then stays open with on-screen nrm / rad numbers so you can check the mapping. Answer ENTER / Y / N in the sim window. Grippers stop after Step 1 plus one physical closed pose. See Calibration.

calibrate-arm

solarian calibrate-arm [axis=z] [amplitude=0.02] [freq=1.25] [poses=3] [distance=0.06]

Measures the three numbers that describe an ArmModule in time, and writes each into the arm's module yaml as it goes. You pick the arm (auto-selected when there is only one) and a mode:

  • all — the three below, in order.
  • speed — short moves at rising speeds from several poses → max_speed[0].
  • action — sweep a Cartesian sinusoid on one axis, record the measured pose, and recover the command-to-motion delay by cross-correlation → action_latency. Shows alignment plots.
  • obs — time a few hundred pose reads and report the mean → receive_latency.

The order in all is the point of merging them. The sinusoid's peak velocity has to stay inside max_speed, because PoseTrajectoryInterpolator stretches any waypoint it cannot reach in time and a stretched sweep measures the speed limit rather than the pipeline — so the speed is measured first and the sweep is sized from the result. Run all on new hardware; the individual modes are for re-measuring one number later.

A figure that is not a measurement is not written: a speed the arm never fell behind is only a lower bound, and a negative latency means the sweep did not track.

Speed mode free-drives the arm to each pose in turn, through take-hold / release-torque / lock / let-go prompts. At the first direction it ramps the commanded speed until the measured motion stops keeping up, then checks whether every other pose and axis direction can hold that same speed, lowering it whenever one cannot. Between directions the arm drives back to the pose you chose rather than reversing the offset it just made, so a move that fell short does not shift where the next one begins. What it reports is the slowest sample, since the Jacobian means a single global limit is only safe if it is the minimum over the region the arm works in. Directions without room for the full test distance are skipped rather than measured short. The moves deliberately run above the configured limit — that is the only way to see past it — with a ceiling of four times the configured value.

Action mode asks for one more placement, then verifies the model can travel the full amplitude both ways from that pose and aborts naming the joint in the way, because an out-of-reach sweep comes back clipped and the clip is what gets measured. The arm returns to where it was when the command started, including when the run aborts partway. See Latency & Synchronization for what each number corrects and why.

calibrate-gripper

solarian calibrate-gripper [amplitude=8] [freq=1.25]

The same four modes for any GripperModule (UMI calibrate_gripper_latency, plus the speed ramp UMI had no equivalent for), writing max_speed, action_latency and receive_latency into the gripper's module yaml.

The gripper's speed is a genuine hardware constant: one prismatic degree of freedom, no Jacobian, so a single measurement holds everywhere. It strokes between the ends of travel (staying a tenth clear of the hard stops) at rising speed and reports the rate the jaws hold, along with whether the configured value was optimistic, pessimistic, or fair.

Getting this order wrong is not hypothetical. UMI swept the full stroke at about 1 Hz, which peaks at 300 mm/s — fine for the WSG50 they ran, twice what a 150 mm/s gripper can follow. Run that sweep against the smaller gripper and every waypoint is stretched, the jaws crawl, and the cross-correlation returns a lag with no physical meaning. The sweep now sizes itself from max_speed and refuses an explicit amplitude/freq pair that would exceed it.

calibrate-gripper-ranges

solarian calibrate-gripper-ranges

Sweeps the servo across its normalized range and asks you for a ruler reading (mm) at each stop, then writes the pairs to aperture_table in the module yaml (UMI calibrate_gripper_ranges). That table is what GripperModule interpolates to turn a commanded aperture into a servo value. Without it the module assumes the opening is a straight line between aperture_closed_mm and aperture_open_mm, which a parallel gripper's linkage does not follow.

calibrate-camera-latency

# on the machine with a monitor
solarian calibrate-camera-latency role=display [monitor_hz=60] [qr_size=720]

# on the robot, camera pointed at that monitor
solarian calibrate-camera-latency role=capture monitor_hz=60 [n_frames=120] [plot=true]

Measures the camera's receive_latency with UMI's QR-timestamp method, and writes it to the camera module's yaml: one role fills a screen with a QR code encoding the instant it was drawn, the other films it and subtracts. monitor_hz is required on capture, where display refresh enters the arithmetic, and optional on display, where it only paces the redraw. Both roles sync to the relay clock, so the screen and the camera can be on different machines — which is the better setup, though both can share one machine if the robot has a screen. See Latency & Synchronization.

All four calibrate-* commands above talk to the module in-process (same as solarian calibrate), not through the robot's action queues. Run them on the machine the device is wired to, with run-robot stopped — the only exception is calibrate-camera-latency role=display, which just draws on a screen and belongs on the machine with the monitor. See Latency & Synchronization.

setup-module

solarian setup-module module_name=<attr>

Interactive helper for setting up a hardware module — assigning servo IDs, picking a port, and writing the resulting values into conf/modules/<attr>.yaml.

calibrate-servo

solarian calibrate-servo

Prepares one servo for use, before it is a module component: pick the brand, the port and the servo, then set its ID, reset the comms and position registers, and park the shaft at mid-encoder so you can fit the horn with the mechanism at mid-travel. Needs no project and no config.

Do this on every new servo. A horn fitted at an arbitrary shaft angle can put the 0/4095 encoder seam inside the joint's travel, which Calibration has to reject — the servo cannot be commanded across the seam. The brand menu lists every installed actuator bus plugin; a brand whose plugin has no scanning support says so instead of failing.

write-robot-calibration

solarian write-robot-calibration

Writes the calibration values listed in your YAML into the actuators / sensors themselves (e.g. servo offsets stored on the device EEPROM). Re-pushes an existing calibration file without re-sweeping.

verify-calibration

solarian verify-calibration module=<attr>

Compares each joint's calibrated span (in radians) to the IK model's jnt_range and prints a per-joint PASS/FAIL table. On an arm it then opens the live sim mirror so you can move the real arm and confirm the sim tracks. See Calibration.

derive-model-limits

solarian derive-model-limits module=<attr> [sweep=true]

Prints MJCF-ready joint range="lo hi" values (radians) derived from the real arm's mechanical limits — from the existing calibration, or a guided live sweep with sweep=true. For model authors keeping jnt_range equal to the real hard-stops. See Calibration.

Controller commands

These run on the controller machine. They import setup_modules=("controller", "observation", "action").

run-controller

solarian run-controller

Loads conf/controller.yaml, instantiates your Controller subclass, and starts the action sender / observation receiver. See Controllers.

calibrate-controller

solarian calibrate-controller

Runs controller-side calibration. Used for leader-arm setups where the controller hardware also needs zeroing.

write-controller-calibration

solarian write-controller-calibration

Writes the calibration values from YAML into the controller hardware.

run-service

solarian run-service <conf_or_service> [--checkpoint PATH] [--host HOST] [--port PORT] [server args...]

Runs a service: a tracker, a depth model, or a policy server. Name a conf in conf/services/ or conf/skills/ with a server: block to run the server it describes, with its checkpoint, port and args, or name an installed service to run it with your own. --checkpoint, --host and --port override the conf, and every other argument goes to the server after the conf's args. So solarian run-service bbox_tracker starts the SAM 2 tracker conf/services/bbox_tracker.yaml calls, solarian run-service umi_target --checkpoint models/pick/checkpoints/latest.ckpt the diffusion policy conf/skills/umi_target.yaml drives, and solarian run-service sam2-tracker --model small the tracker with your arguments. An unknown name lists the installed services. To serve on Solarian Base's GPUs instead, see Services, Policies & Models.

run-server

solarian run-server

Starts a RelayServer using conf/server.yaml. Run this on the relay machine — typically a small box on the same network as both the robot and the controller.

Recording & replay commands

record

solarian record num_episodes=10 episode_time_s=20

The same teleop loop as run-controller, cut into episodes: reset the scene, record for episode_time_s, seal, repeat. Press delete during an episode to throw it away and record it again — the index is reused, so a bad take leaves no gap — r to seal early, and quit the UI to end the session.

conf/commands/record.yaml holds only the shape of the session (name, num_episodes, episode_time_s, reset_time_s, tasks). The recording: block in conf/controller.yaml picks which sides record: the robot, into recordings/robot/, and the controller, into recordings/controller/. The robot's conf/robot.yaml recording: block holds how the robot stores its episodes. The command refuses to start when neither side would record. See Recording.

replay

solarian replay episodes=[0,1,2]

Replays a previously recorded episode by streaming its actions back through the standard action path. Useful for debugging deterministic behavior.

Training & data commands

export-dataset

solarian export-dataset recordings/robot/0003_7f3a9c_cup_stacking

Turns a recorded session — archive mp4s plus one parquet per episode, all at native rates — into a LeRobot v2.1 dataset: every signal resampled onto one grid, every frame warped through the camera's optical remap and downscaled to the observation resolution. The session is never modified, so recalibrate a camera or change your mind about the rate and export again from the same recording. --fps, --episodes A,B,C, --no-remap and --overwrite cover the rest. See Recording.

dataset

solarian dataset ops [--json]
solarian dataset new <source> <dir>
solarian dataset apply <dir>[@VERSION] <op> [--param KEY=VALUE]... [--review | --online]
solarian dataset log <dir>

Runs dataset ops on recorded sessions without changing them. new starts a computed dataset over a session; apply adds a version made by an op on another version (by default the only leaf, or @source). redo, accept, close, drop, export and rebuild cover the rest, and solarian dataset --help lists them all. --project DIR picks the project whose dataset_ops/ load, by default the one around the current directory.

umi-slam

solarian umi-slam datasets/cup_session

Turns a directory of GoPro or Lumi collector MP4s into dataset.zarr.zip for UMI training. This is not a solarian record session. ORB-SLAM3 runs in Docker image chicheng/orb_slam3:latest and GoPro IMU extraction in chicheng/openicc:latest. If an image is not already on this machine, the command asks before downloading it. The copied pipeline is from Universal Manipulation Interface (MIT). Add the dataset it writes to task.dataset_mix in a training file, conf/training/<name>.yaml. See Training.

train

solarian train --conf conf/training/cup_arrange

Runs one training file with the trainer its trainer: key names. There is no default file. A trainer plugin seeds conf/training/<name>.yaml on install; copy that file per run. For UMI (trainer: umi), each copy writes models/<job_name>/checkpoints/latest.ckpt, and the same UMISkill loads that checkpoint from the matching skill yaml. See Training.

base

solarian base deploy
solarian base train --conf conf/training/cup_arrange --dataset task.dataset_mix.0.path=3f2a9c1e
solarian base runs logs -f 1a2b3c4d
solarian base endpoints start umi_target --checkpoint 1a2b3c4d

From the solarian-base plugin. Links the project to Solarian Base (login, link), deploys its code (deploy), manages the secrets its jobs get (env), trains on cloud GPUs and follows the run (train), reads runs (runs ls|show|logs), lists the checkpoints its jobs wrote (models ls), and serves the project's services and policies on cloud GPUs (endpoints ls|start|stop|logs). Every action takes --json. See the Solarian Base CLI reference for every flag, and Solarian Base for what Base does.

Utility commands

detect-device

solarian detect-device

Lists USB / serial devices currently connected to the host (port path and name). Handy when you need to fill in a port: field in a module config.

list-cameras

solarian list-cameras

Detects and lists every camera available to the system (V4L2 on Linux, AVFoundation on macOS).

onshape-export

solarian onshape-export <assembly_dir> [--clean-only] [--keys-file PATH] [--update-yaml PATH]

Exports an Onshape CAD assembly into an attachable MuJoCo (MJCF) model for simulation. <assembly_dir> holds a config.json describing the assembly and a solarian block controlling the export. With --update-yaml conf/modules/<module>.yaml, joint ranges from the cleaned MJCF are synced into that module conf's sim_range_min/sim_range_max and into config.json joint_properties.*.limits. See Models from Onshape for the full workflow. Part of the [sim] extra.

shell

solarian shell

Opens an IPython shell with solarian.conf.settings already loaded and your project on sys.path. Useful for poking at your Robot/Controller classes without spinning up the full pipeline.

Authoring a custom command

Add a new file under solarian/cli/commands/ (or your project's own command package) and register it with CommandRegistry:

from omegaconf import DictConfig
from pydantic import BaseModel

from solarian.cli.base import BaseCommand, CommandRegistry


class MyCommandConfig(BaseModel):
    target: str
    speed: float = 1.0


@CommandRegistry.register('my-command')
class MyCommand(BaseCommand):
    category = "Robot"
    help = "Run the my-command demo."

    def get_config_class(self):
        return MyCommandConfig

    def handle(self, cfg: DictConfig) -> None:
        print(f"Running {cfg.target} at speed {cfg.speed}")

category is the heading solarian --help lists the command under. Set uses_config = False when the command does not read a project yaml (no settings.py, and the global --conf / --deployment flags are hidden from its help).

A command that takes positionals or flags implements parse_args. It stores what it parsed on self, returns [], and handle reads those attributes. The default returns the argv unchanged, and Hydra composes those tokens as key=value overrides.

To make the command discoverable without forcing an import on every CLI startup, register it lazily. Repeat help and category here: the command list is built from this record, so it can group commands without importing the module.

CommandRegistry.register_lazy(
    "my-command",
    module_path="myproject.commands.my_command",
    help="Run the my-command demo.",
    config_name="commands/my_command", # picks conf/commands/my_command.yaml
    setup_modules=("robot",),          # which project modules to auto-import
    category="Robot",
)

The command then runs as:

solarian my-command target=arm speed=2.0

If config_name is set, the command receives a parsed DictConfig from conf/<config_name>.yaml (commands/record is conf/commands/record.yaml); otherwise it gets an empty DictConfig. If get_config_class() returns a Pydantic model, Solarian validates the config and prints friendly errors on failure.

Was this page helpful?