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.serveloads exactly whatdevice.jsonrecords. - 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.
serverefuses (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_infoon the hub and into every signedstartedevent.
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.
| Protocol | Methods | What it is for |
|---|---|---|
policy | start(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. |
chunks | begin(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:
- the class has every method of the chunk protocol, as a real callable;
- on a fresh instance,
hold("probe")and thenstop()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_stopis passed toset_safe_stop(). If the card asks for something other thanholdand the adapter has noset_safe_stop(), nothing starts — a fail-safe that ignores the robot's declared safe stop would be pretending.default_posereachesstage("default_pose", interface).- A policy stamped with an
embodiment_fingerprintthat differs from the pinned one is refused (embodiment_mismatch). Most policies carry no such stamp yet; thestartedevent then recordsembodiment_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"]
}
| Placeholder | Expands 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.