Reference
The public surface of menlo-sdk: Robot, connection configs, preflight, Sent, State, media, the store, transports and errors.
Everything here is importable from menlo.asimov unless a module is named.
menlo.__version__ is the distribution version. Units are metres per second, radians per
second, radians, degrees Celsius.
Where to Start
| Do this | Use |
|---|---|
| say where the robot is | Connection, Robot |
| check it is ready | preflight, armed |
| balance in place, walk, strafe, turn | balance, set_velocity |
| stand up, or go compliant | stand, damp |
| pose a joint | set_joints, trajectory |
| wait for something | wait_until; stand, balance and damp wait for their robot mode themselves |
| read what it reports | State, get_state |
| check what it can do | has, require, RobotInfo |
| see what went out | Sent, Outcomes |
| pictures and sound | Media |
| record a run | record |
| save a robot for next time | Store and CLI |
| handle failure | Errors |
Connection
ConnectionConfig(udp=None, livekit=None, limits=None, mode=None, name=None, hints={})| Member | |
|---|---|
udp: UdpConfig | None | the robot's address on the local network |
livekit: ManagerConfig | LiveKitConfig | None | the room, through Asimov Manager or directly |
limits: Limits | None | velocity limits for this robot |
mode: ConnectMode | None | the connection mode connect() uses when given none |
name | the saved robot this config came from, if any |
available_modes() -> tuple[ConnectMode, ...] | which of "udp", "hybrid", "livekit" this config can reach |
default_mode() -> ConnectMode | mode when set, else only_mode() |
only_mode() -> ConnectMode | the mode the set slots imply: udp alone is udp, livekit alone is livekit, both are hybrid; ConnectError with none set |
ConnectionConfig.from_environment(environ=None, *, robot=None) | from MENLO_*, else the saved robot |
transport_for(mode, ...) | builds the transport for a mode |
| Config | |
|---|---|
UdpConfig(host, command_port=8850, state_bind=("0.0.0.0", 8851), state_source=None) | commands to host:8850, state on state_bind. state_source pins the one address state may come from; without it, samples that do not match this robot are dropped |
ManagerConfig(url, credential, label=None, timeout=5.0) | Asimov Manager mints the room and a join token per connect. .host; .resolve(); .check() -> ManagerGrant(url, room, identity, role) validates the credential without joining |
LiveKitConfig(url, room, token) | room details you hold; token is a string or a callable minting one per join. One token is one participant |
default_label() | the label the SDK uses for this host |
The LiveKit identity is sdk-<credential id>-<host>-<6 random hex>; robot.info.endpoint
reads room@url as <identity> on livekit and host:8850 + room@url as <identity> on
hybrid. Two sessions with the same label evict each other.
Robot
Robot(source: ConnectionConfig | Transport | None = None, *, limits=None, link_timeout=2.0)None resolves the robot from the environment, else the saved robot. limits here wins
over MENLO_LIMITS, which wins over the saved robot's limits, which win over Limits().
connect
connect(mode=None, *, timeout=5.0, media_timeout=3.0, connect_timeout=10.0,
allow_version_skew=False, require_state=True, persist=None) -> RobotAttach on "udp", "hybrid" or "livekit"; mode may be left out when the config names
one or can reach only one. Returns once the first State has arrived and its protocol
version matches. Raises ConnectError before touching the network when the config lacks
what the mode needs, naming the menlo robots add command that fixes it. media_timeout
bounds the wait for camera, microphone and speaker tracks. require_state=False returns as
soon as the connection is open. persist=True, or MENLO_PERSIST=1, saves the robot after
a successful connect with a ManagerConfig. close() then connect() again to switch
modes.
open(*, timeout=5.0, allow_version_skew=False, require_state=True) does the same over a
Transport passed to the constructor. RuntimeError on an open robot.
Preflight
Action = Literal["stand", "move", "trajectory"]
preflight(action: Action = "move") -> Preflight
armed: bool | Nonepreflight reads the latest state, sends nothing and never raises for a robot condition.
stand() runs it for "stand", balance() from STAND and set_velocity() for "move",
set_joints() and trajectory() for "trajectory", before they send; see Verbs. armed is True in MOVE and in
STAND once the robot has been upright (gravity z below -0.87) for 0.5 s since STAND began;
False in DAMP or before; None with no state, closed, in UNKNOWN, or in STAND with no
gravity.
Preflight member | |
|---|---|
action | the action checked |
problems: tuple[Problem, ...] | blocking first |
state: State | None | the sample the check read |
armed: bool | None | |
ok: bool | no blocking problem |
blocking: tuple[Problem, ...] | |
has(code) -> bool | |
str() | ready to move, or not ready to move: followed by one - code: message line each, warnings suffixed (warning) |
explain() | one line: not ready to move: message (code); fix. ..., blocking problems only; the message of the NotReadyError a command raises |
Problem(code, message, blocking); str() is code: message. The codes and their meaning are in
Check Before You Move.
The thresholds: state older than 0.5 s is stale, battery below 20 %, a joint at or above
60 °C, armed means gravity z below -0.87 for 0.5 s.
Verbs
Each returns a Sent. The robot reports no per-command verdict: Sent.outcome stays
None and wait_outcome() returns Unknown in every mode. Read the effect from
robot.get_state().
set_velocity, stand, balance from STAND, set_joints and trajectory run
preflight() for their action before they send. A passing check reads one cached sample and
adds no delay. A blocking problem that clears on its own (no_state, stale_state,
not_armed, or DAMP still reported just after a stand()) is waited out for up to
timeout seconds; any other raises NotReadyError at once, a latched fault
RobotFaultedError, and nothing is sent. balance in MOVE and damp are never checked.
set_velocity
set_velocity(vx=0.0, vy=0.0, vyaw=0.0, *, duration=None, wait=None, hold=True,
timeout=None) -> SentWalk in MOVE. Checks "move": from STAND it raises NotReadyError (wrong_mode;
balance() it first), from DAMP the same (stand() it first), and nothing is sent; a
stale_state is waited out up to timeout (5 s by default). Clamped to limits; re-sent
at 10 Hz until balance(), another verb, close(), or duration seconds pass, when zero
is sent. wait defaults to True: block until the hold has ended and its zero has gone
out, or another verb took over; that needs a duration, else ValueError. wait=False
returns once sent and keeps the velocity until the next command, or until duration.
hold=False sends exactly one packet and holds nothing, for a loop that clocks its own
commands; the check is made once, as in trajectory(), and duration or wait=True with
it is a ValueError. ValueError on a non-finite value or a non-positive duration. See
Move the Robot.
balance
balance(*, timeout=5.0, wait=True) -> SentZero velocity: the robot is in MOVE and the walking policy balances it in place. In MOVE,
never checked; it ends any velocity hold and sends zero, so this is how a walk ends. In
STAND, checks "move" first, waiting up to timeout for not_armed to clear, then sends
zero velocity, which puts the robot in MOVE; with wait, returns once the robot reports
MOVE, WaitTimeoutError when the whole call passes timeout. In DAMP, FAULT_DAMP or
UNKNOWN it raises NotReadyError (RobotFaultedError for a latched fault) and sends
nothing. Not an emergency stop.
stand
stand(*, timeout=10.0, wait=True) -> SentPut the robot in STAND: the actuators hold the standing pose, without a balance loop. Checks
"stand": from MOVE it raises NotReadyError (wrong_mode) and sends nothing, since a
free-standing robot tips over. Sends STAND once and, with wait, returns when the robot
reports STAND and is armed; WaitTimeoutError when the whole call passes timeout,
RobotFaultedError when a fault latches meanwhile. wait=False returns once sent. Ends a
running set_joints() and a held velocity.
damp
damp(*, timeout=5.0, wait=True) -> SentEvery actuator compliant now. A standing robot folds to the ground. Never checked: DAMP is
sent whatever the robot's state. With wait, returns once the robot reports DAMP or
FAULT_DAMP, WaitTimeoutError after timeout. Not an emergency stop.
set_joints
set_joints(positions, *, duration=2.0, hz=50.0, kp=None, kd=None,
wait=True, tolerance=0.05, timeout=None) -> SentChecks "trajectory" (MOVE or an armed STAND; a stale pose is waited out), then
interpolates from the last reported pose to positions over duration, clocking
trajectory() setpoints from a thread at hz, then holds the target until another verb.
Any verb from any thread ends it before its next setpoint leaves. wait=True blocks until
every joint is within tolerance rad, WaitTimeoutError otherwise. An ankle target more
than 0.02 rad past the firmware's limits (0.35 rad pitch, 0.1 rad roll) is a ValueError
before anything is sent. timeout bounds the
whole call; left None, the check may wait 5 s and the target duration + 2 s. See
Control Joints.
trajectory
trajectory(positions, *, kp=None, kd=None, timeout=0.0) -> SentOne setpoint, radians, firmware order; clock these yourself or use set_joints. Checks
"trajectory" once, with no delay when it passes, so a loop at 50 Hz can call this. When
the robot is not ready it raises NotReadyError at once and sends nothing; pass timeout
to wait that long instead.
ValueError unless len(positions) == info.dof, or when only one of kp and kd is
given. A gain of 0 is not "no gain": the firmware substitutes its damping constants, so the
joint does not hold a position. None applies the robot's per-joint gain table. The robot
enters DAMP 2 s after the last setpoint.
Waits
Every wait reads the robot's own report, checks for a fault before its predicate, and
refuses to succeed on a stale stream. stand(), balance(), damp() and
set_joints(wait=True) wait this way for their own effect.
wait_until
wait_until(predicate, *, timeout=10.0, stale_after=None, poll=0.05) -> StateBlocks until predicate(state) is true and returns that first State. Raises
WaitTimeoutError (the message names the robot mode and the age of the last sample),
StateStaleError when the stream goes quiet for stale_after, and RobotFaultedError
when a fault latches. stale_after defaults to link_timeout. A wait for one robot mode is
wait_until(lambda s: s.mode is Mode.MOVE).
Properties and Callbacks
| Name | Type | Notes |
|---|---|---|
config | ConnectionConfig | what this Robot was bound to |
info | RobotInfo | transport, endpoint, dof, joint names, protocol version, limits, capabilities |
has(cap), require(*caps) | bool, raises UnsupportedError | this connection's set: drive, state, battery, camera, microphone, speaker |
camera, microphone, speaker | Camera, Microphone, Speaker | see Media; UnsupportedError when the connection does not carry them |
record(path) | Recording | context manager: every state sample and every command as JSON lines |
get_state() | State | the latest sample; NotConnectedError before the first |
armed | bool | None | see Preflight |
connected | bool | open and not LinkLostError |
outcomes() | Iterator[Refused] | received refusals, oldest first; empty with this robot |
on_state | Callable[[State], None] | None | every accepted sample, on the transport thread |
on_alert | Callable[[Alert, str], None] | None | an alert "raised" or "cleared" |
on_mode_change | Callable[[Mode, Mode], None] | None | before, after |
on_refused | Callable[[Refused], None] | None | never fires with this robot |
on_link_lost | Callable[[LinkLostError], None] | None | |
close() | zero velocity if held, drop the link; idempotent; never raises |
Robot is a context manager; __exit__ calls close().
Sent
| Member | Type | Meaning |
|---|---|---|
name | str | the verb: set_velocity, balance, stand, damp, trajectory; set_joints() sends its setpoints as trajectory |
sequence | int | the command's sequence number |
command | Velocity | ModeCommand | Trajectory | what went out, after clamping |
clamped | bool | command differs from what you asked |
sent_at | float | time.monotonic() |
outcome | Applied | Refused | None | non-blocking; None while pending |
wait_outcome(timeout=None) | Applied | Refused | Unknown | None uses the transport default |
require(timeout=None, *, unknown_ok=True) | as above | raises CommandRefusedError; with unknown_ok=False also OutcomeUnknownError |
Outcomes
| Type | Fields |
|---|---|
Applied | sequence |
Refused | sequence, reason: Refusal, detail: str (diagnostic text; never branch on it), verb |
Unknown | sequence, waited_s, verb |
Refusal (IntEnum): UNSPECIFIED, FW_DAMPED, FAULT_DAMPED, GATE, UNSUPPORTED_SOURCE,
BAD_LENGTH, EMPTY_TRAJECTORY, NON_FINITE, PARSE, UNKNOWN_COMMAND, UNKNOWN_MODE,
SHUTTING_DOWN, NO_CAMERA, NOT_ACTIVE, UNRECOGNIZED; retryable property;
Refusal.from_wire().
State
@dataclass(frozen=True)
class State:
mode: Mode # DAMP | STAND | MOVE | FAULT_DAMP | UNKNOWN
joints: tuple[Joint, ...] # firmware order
gravity: tuple[float, float, float] | None # body frame; z near -1 when upright
gyro: tuple[float, float, float] | None # rad/s
quat: tuple[float, float, float, float] | None # w, x, y, z
error_flags: int
alerts: tuple[Alert, ...]
sequence: int
fw_timestamp_us: int
protocol_version: int
battery: Battery | None # None when the robot reports no battery
received_at: float
edge_timestamp_us: int # Asimov Edge's wall clock (µs); livekit only, 0 on udp and hybrid
# properties
age_s: float
upright: bool | None # gravity z < -0.8; None without gravity
euler: tuple[float, float, float] | None # roll, pitch, yaw (rad) from quat
yaw: float | None # rad, counter-clockwise, (-pi, pi]
faulted: bool # FAULT_DAMP, error_flags or any critical alert
joint_pos: tuple[float, ...]
def joint(self, name: str) -> Joint # KeyError on an unknown nameJoint(name, pos, vel, current, temp): vel, current and temp are None when the robot
did not report them. Alert(id, severity, value, threshold, source_id, first_set_us=0) with
critical (severity == 0) and name. Battery(voltage_v, current_a, soc_percent, max_cell_temp_c, protection: BatteryProtection) with charging and protecting;
BatteryProtection is an IntFlag.
RobotInfo(transport, endpoint, dof, joint_names, protocol_version, limits, capabilities)
with joint_index(name), has(capability) and a readable str().
Limits(vx=0.4, vy=0.4, vyaw=0.8): the defaults are the firmware caps (0.4 m/s, 0.4 m/s,
0.8 rad/s).
Limits.parse("0.3,0.3,0.6"). Negative or non-finite values raise ValueError.
Media
robot.camera.photo(*, timeout=5.0) -> Frame # one fresh frame; WaitTimeoutError when quiet
robot.camera.latest() -> Frame | None # newest frame; None before the first
robot.camera.frames(*, timeout=5.0) # iterator, always the newest frame
robot.camera.subscribe(cb) # cb(Frame) on the transport thread
robot.camera.stream(*, timeout=5.0) # every frame, in order
robot.camera.capture_clip(seconds, audio=True) -> Clip
robot.microphone.chunks(*, timeout=5.0) # every AudioChunk in order; .dropped counts loss
robot.speaker.play(chunk)
robot.speaker.play_pcm(pcm_s16le, sample_rate_hz=16000, channels=1)Frame(width, height, encoding, data, stride_bytes, key_frame, frame_id, timestamp_ns, sequence) with shape, age_s, to_numpy(), to_jpeg(quality=85).
AudioChunk(sample_rate_hz, channels, samples_per_channel, encoding, data, stream_id, timestamp_ns, sequence) with duration_s, to_numpy(). Clip(frames, audio, started_at)
with duration_s, fps, save_wav(path) -> Path, save_frames(dir),
save_mp4(path, *, fps=None) -> Path, frames_as_numpy(). numpy, Pillow and OpenCV are
needed only at the call that uses them, and named in the ImportError.
Recording
with robot.record("run.jsonl") as rec:
...
rec.samples, rec.commands_written
from menlo.asimov import recording
for line in recording.load("run.jsonl"): # dicts with kind "state" | "sent", t, mode
...Store and CLI
~/.menlo/robots.toml (MENLO_HOME moves it), 0600 in a 0700 directory.
RobotStore() | get(name=None), put(robot, *, default=None, allow_manager_change=False), remove(name), use(name), names, save(); len(), iteration and in |
StoredRobot(name, manager_url=None, credential=None, room=None, mode=None, udp_host=None, limits=None) | resolved_mode(), missing(mode), fix_command(mode), manager(label=), connection(label=) |
menlo.asimov.store.persist(config, room, *, name=None, mode=None) | what connect(persist=True) calls: records the manager, credential, room, udp_host, mode and limits; ValueError without a ManagerConfig |
| Command | |
|---|---|
menlo setup [--no-check] | wizard: name, mode, that mode's fields, live checks |
menlo robots [--json] | the saved robots |
menlo robots add NAME --mode --udp --manager --credential --limits VX,VY,VYAW [--default] [--no-check] [--no-input] | add or update; unspecified values are kept |
menlo robots remove NAME, menlo robots use NAME | forget, make default |
menlo status [--watch] [--json] | readiness, robot mode, battery, hottest joint, faults, state rate |
menlo stand [-y], menlo balance [-y], menlo walk --vx --vy --vyaw --duration S [-y], menlo damp [-y] | the verbs; each prints a plan and asks Proceed? [y/N], -y/--yes goes ahead without asking. stand, balance and walk run the readiness check first and print Not feasible: when it refuses; balance in MOVE sends at once without asking; damp is not gated |
menlo --robot NAME --mode MODE, menlo --version |
Environment: MENLO_ROBOT, MENLO_MODE, MENLO_UDP_HOST, MENLO_MANAGER_URL,
MENLO_CREDENTIAL, MENLO_LIMITS, MENLO_HOME, MENLO_PERSIST. MENLO_MANAGER_URL and
MENLO_CREDENTIAL go together; one alone is a ConnectError. Environment wins over the
saved robot and is not merged with it. See Command Line.
Transports
UdpTransport(host, *, command_port, state_bind, state_source),
LiveKitTransport(url, room, *, token, media_timeout, connect_timeout),
HybridTransport(host, *, livekit_url, room, token, command_port, state_bind, ...). All
implement Transport; a ConnectionConfig builds the right one for the mode.
LiveKitTransport.identity and .room read back what the token claimed.
Errors
MenloError
ConnectError
ProtocolMismatchError(expected, observed)
NotConnectedError
LinkLostError
NotReadyError(message, *, action, preflight) # .problems, .has(code)
RobotFaultedError(message, *, state, sent=None) # plus action, preflight
WaitTimeoutError(message, *, last, sent=None) # also TimeoutError
StateStaleError
CommandRefusedError(refused)
OutcomeUnknownError(unknown) # also TimeoutError
UnsupportedError(capability, transport)NotReadyError reads not ready to <action>: <message> (<code>); <fix>. ..., with
(waited <t> s) appended when a transient problem did not clear in time; nothing was sent.
RobotFaultedError is raised by a command's check with nothing sent, and by a wait after a
command went out, when sent is set. WaitTimeoutError.sent is the command whose effect
did not arrive. Caller
mistakes are builtins: ValueError (non-finite velocity, limit or duration, wrong trajectory
length, a lone kp or kd, an unknown connection mode), KeyError (unknown joint name),
RuntimeError (open() on an open robot). A mode the config cannot reach raises
ConnectError.
Threading
Robot is synchronous and thread-safe. Callbacks run on the transport thread, the one that
delivers state: keep them short. robot.damp() from a callback, such as on_alert damping
on an alert, sends DAMP and returns at once. The other commands that wait for the robot, and
wait_until(), raise RuntimeError there with nothing sent; pass wait=False, or call them
from your own thread.
Per-Robot Tables
menlo.asimov.robots.ASIMOV_1_BIPED_JOINTS is the 25-actuator order of the Asimov 1 biped
firmware profile, chosen by the reported joint count. menlo.asimov.robots.PROTOCOL_VERSION
is the protocol version the SDK was built against; connect() compares it with what the robot
echoes and raises ProtocolMismatchError on a difference.
The Protocol
Commands on udp go to port 8850 and state arrives on 8851, each datagram one bare
asimov.io.RobotCommand or RobotState. On livekit, commands are data messages on the
commands topic and state is the state data track. Every command carries
protocol_version = 1, a sequence number and timestamp_us. UDP commands are not
authenticated; the SDK credential and its role apply to the LiveKit room: every command on
livekit, and the camera and audio on hybrid. Without the SDK
and the livekit_raw/ examples speak it with the livekit and asimov-protocol packages
alone.
How is this guide?