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 passdir/nameto 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, thenlocalhost. See Deployments.
- Name
--settings- Type
- string
- Description
Path to your project's
settings.py. Auto-discovered when omitted (looks for{package_name}/settings.pybased onpyproject.toml, thenconf/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.