Skip to content
Docs menu

Connect your robot

Pair a robot from its page with a code, and write a lucen-device adapter for your own robot: the plugin entry point, the protocols, and the fail-safe the driver insists on.

lucen-device is the robot-side half of the consent layer: it runs on the robot's own computer, verifies a human's signed approval offline, pulls the exact bytes, refuses a policy whose contract does not fit this robot — and then hands the policy to an adapter. The adapter is the only part that knows your robot. This page is how to write one, install it, and what the driver will and will not trust it with.

From the robot's page

Connecting a robot starts on the hub, not here — step 4 of the Quickstart. Open the robot's page → Connected → Connect a robot: the page shows a short code and the lines to paste on the robot's computer (this hub's installer, the PATH line, then lucen-device pair with the code and lucen-device serve), a signed-in person confirms the robot that claimed it — comparing the key fingerprint the robot prints — and no API key ever touches the robot. A policy then reaches it only through Deploy to robot on the model's page, held by a signed-in person, or a request an agent filed and a person approved in the Inbox; Stop now on the live page ends a run early, and Disconnect on My robots revokes the robot's credential.

With a built-in adapter that is the whole job. Read on when your robot needs an adapter of its own: pair takes the same --adapter and --adapter-config as the register command shown below, which remains for a box you set up by hand with an API key.

Adapters

Two adapters ship with the driver. virtual is a clock at the policy's control rate — no physics, no inference, no actuators — so the whole path can be rehearsed without hardware. command runs the start / stop commands you configure. Everything else is a plugin, and yours is about 140 lines.

Nothing on this page has been run on a physical robot. The example drives a fake one, and says so in every heartbeat.

Install the driver with an adapter

An adapter is an ordinary Python distribution that registers a class in the entry-point group lucen_device.adapters. It must be installed into the environment lucen-device runs from — with uv tool, that is one more --with:

uv tool install --index lucen=https://api.mousemouse.ai/install/simple/ --with ./examples/device-adapter lucen-device

lucen-device comes from this hub's own package index; ./examples/device-adapter (in the source repository) is the example this page is about — copy the directory, rename it, and replace its FakeRobot with your robot's SDK.

lucen-device adapters

lists every adapter an installed distribution registers, with the distribution and version it comes from. It imports nothing — it reads package metadata — because importing an adapter is running its code.

lucen-device adapters --check example-arm

imports that one adapter, reports which protocols its class implements, and probes its fail-safe (below). Use it while you write yours.

Loading an adapter is code execution

An adapter runs on the robot's computer, as the user the driver runs as, every time an approval arrives. The driver keeps that deliberate:

  • You name the adapter at register, and the driver loads that one and no other. Listing never imports. serve loads exactly what device.json records.
  • A name must resolve to one place. If two installed distributions register the same adapter name — including something registering virtual — the driver refuses and names both.
  • What was registered is what runs. serve refuses (adapter_changed) when the registered name now resolves to a different distribution or class. A new version of the same distribution is allowed, and reported.
  • The trail says what ran. The distribution, version and class go into the device's adapter_info on the hub and into every signed started event.

The hub cannot see your code and does not pretend to vouch for it. What it holds is the human's approval, the digest, the contract fingerprint — and your box's own statement of what it loaded.

The protocols

No import from lucen_device is needed; the methods are duck-typed. Implement what your robot can do.

ProtocolMethodsWhat it is for
policystart(model_path, manifest, seconds), stop(), status(), is_running()Run an approved ONNX on the robot — the reflex layer, which is never served over a network.
chunksbegin(contract, seconds), observe(), apply(chunk, received_at=…), hold(reason), resume(), stop(), status(), is_running()Consume a hub-served skill endpoint: observations out, action chunks in.
staged_start (optional)stage(name, interface)Called with "safe" and then "default_pose" before start / begin: reach a safe state, move to the robot card's default pose, only then the policy.
safe_stop (optional)set_safe_stop(mode)The robot card's interface.safety.safe_stop — hold, zero_torque or default_pose — decides what your hold() and stop() must mean. Raise if you cannot do it; nothing starts.

The constructor receives one dict: your own settings (lucen-device register --adapter-config FILE.json, kept in device.json, never sent to the hub), plus approval_id and the robot card's interface.

Anything your adapter raises before motion starts is a clean refusal (adapter_refused) on the hub's trail, followed by stop().

The fail-safe is not optional

A device may consume a hub endpoint only if its adapter implements chunks and its hold() works. The driver establishes that itself, from your code:

  1. the class has every method of the chunk protocol, as a real callable;
  2. on a fresh instance, hold("probe") and then stop() return without raising, within five seconds.

So: hold() and stop() must be safe to call in any state — before begin(), twice in a row — and must never raise. They are the two calls the driver makes when everything else has already gone wrong. The probe runs at register and again before every endpoint session. An adapter whose hold() is missing, raises, or hangs registers as not endpoint-capable, and the hub refuses to put an endpoint approval in front of a human for it.

While a session runs, the driver's loop enforces the rest on the robot's own clock: a chunk that arrives after its deadline is dropped, never applied; two missed deadlines or a lost link call hold() and post degraded; chunks flowing again call resume() and post recovered. If hold() raises or does not return in one second at that moment, the driver calls stop() instead, says hold_failed in the trail, and ends the run rather than reconnect onto a robot it cannot hold. A loop that stops making progress for ten seconds — an apply() that never returns — gets hold() then stop() from a watchdog thread. That watchdog is a coarse backstop in a Python process, not a real-time guarantee: your robot's own controller must also stop when commands stop arriving.

Register

The policy a device accepts is read off the ONNX's own stamped contract, so pull one the robot runs — lucen/laika-stand-v1b, the standing policy in service on Laika — and point --accept-from at it:

lucen pull lucen/laika-stand-v1b ./stand
lucen-device register --robot lucen/laika --name lab-arm --adapter example-arm --accept-from ./stand/stand_v1b.onnx

register loads the adapter, says on stderr that it is third-party code and where it came from, probes the fail-safe, pins the hub's signing key and the robot card's embodiment interface, and sends the hub the adapter's name, its provenance and the declared capability. An unknown name fails here, before anything is sent, and lists what is installed.

A class nobody packaged can be named directly instead:

--adapter my_robot.adapter:MyArm          an importable module and a class
--adapter ./my_arm.py:MyArm               a file and a class

Such a class sets its own name attribute — the identifier the hub records (lower-case letters, digits, ., _, -; up to 64 characters) — and may not call itself by an installed adapter's name. A file adapter's "version" on the trail is the sha256 of the file.

lucen-device serve --once

handles the approvals a person has signed for this box, and exits. Nothing runs until a signed-in person approves — Deploy to robot on the model's page, or a request in the Inbox; no API key of any scope can.

The robot card's interface, on the box

When the robot repo's card declares an embodiment interface, the driver pins it and its fingerprint at register (like the signing key: refreshed only on purpose, with lucen-device interface --refresh) and uses it three ways:

  • safety.safe_stop is passed to set_safe_stop(). If the card asks for something other than hold and the adapter has no set_safe_stop(), nothing starts — a fail-safe that ignores the robot's declared safe stop would be pretending.
  • default_pose reaches stage("default_pose", interface).
  • A policy stamped with an embodiment_fingerprint that differs from the pinned one is refused (embodiment_mismatch). Most policies carry no such stamp yet; the started event then records embodiment_check: policy_unstamped — the check is recorded as not made, never assumed to have passed.

The command adapter

For a robot that already has a start script, --adapter command runs it. The commands come from this box — --adapter-config, or the LUCEN_DEVICE_START_CMD / LUCEN_DEVICE_STOP_CMD / LUCEN_DEVICE_STATUS_CMD environment variables — and never from the hub.

{
  "start_cmd": "/opt/robot/run_policy.sh {model_path} --seconds {seconds} --safe-stop {safe_stop}",
  "stop_cmd": "/opt/robot/stop.sh {safe_stop}",
  "status_cmd": ["/opt/robot/status.sh"]
}
PlaceholderExpands to
{model_path}the verified ONNX
{manifest_path}its lucen_manifest, as JSON, written beside it
{interface_path}the robot card's interface as JSON; empty when the card has none
{seconds}the remaining approved window, whole seconds
{approval_id}the approval this run belongs to
{safe_stop}hold, zero_torque or default_pose from the robot card

A string runs under sh -c; a JSON list runs as is. A literal brace is {{ or }}, and an unknown placeholder is refused before anything starts.

Stopping, when the window ends or the hub refuses started: stop_cmd runs first (30 s cap; its exit code is kept in the status), then the start command's whole process group gets SIGTERM — it runs in its own session, so a runner forked by sh -c is reached too — then, after 10 s, SIGKILL. command has no chunk interface, so a device driving it cannot consume a hub endpoint. lucen-legged is the old name for the same adapter and keeps working.