Menlo

Troubleshooting

What each SDK error means and what to do about it.

Start with menlo status: it connects, runs the readiness check, and prints the first blocking reason without sending anything.

ConnectError: Nothing to Connect To

Robot() found neither MENLO_UDP_HOST nor MENLO_MANAGER_URL with MENLO_CREDENTIAL in the environment, and no saved robot. Run menlo setup, or pass a ConnectionConfig. With several robots saved and no default, menlo robots use NAME picks one, or set MENLO_ROBOT. MENLO_MANAGER_URL without MENLO_CREDENTIAL, or the reverse, is the same error: they go together.

ConnectError: The Saved Robot Lacks What the Mode Needs

The message names the fix, such as menlo robots add lab --udp <robot address>. hybrid needs the robot's address, the Asimov Manager URL and an SDK credential; udp the address; livekit the Manager URL and the credential. cfg.available_modes() says which modes a config can reach.

ConnectError: Asimov Manager Refused the Credential

The SDK credential is wrong or revoked, or the URL is not Asimov Manager. Issue a new one on the Developer page of Asimov Manager with the Control role (Get an SDK Credential) and run menlo setup again. A credential with the Observe role connects but cannot drive on livekit. A manager that redirects is reported rather than followed: use the address it redirected to.

ConnectError: No State From the Robot on UDP

The robot never reported in. The robot sends UDP state to one address, so check, in order:

  1. udp-control is on in the Asimov Edge parameters in Asimov Manager.
  2. udp-state-host is this machine's address as the robot sees it, and udp-state-port is the port the SDK binds (8851 by default).
  3. Nothing blocks port 8851 inbound: a firewall or a VPN.

menlo robots add NAME --udp HOST runs this check and prints the same advice.

ConnectError: Could Not Bind the State Port

Another process holds port 8851; one client per port on udp. Close it, or use state_bind=("0.0.0.0", <other port>) and set udp-state-port to match.

ProtocolMismatchError

The robot speaks another protocol version than the SDK was built against. Update whichever is behind. connect(allow_version_skew=True) proceeds anyway, and the robot may misread the commands the SDK sends.

NotReadyError

The check that stand(), balance() from STAND, set_velocity(), set_joints() or trajectory() runs before it sends found a blocking problem, and nothing was sent, except when a wait raised a RobotFaultedError after the command went out; see the next section. The message names each problem, its code and what fixes it; e.problems carries them, e.has(code) tests one and e.preflight is the whole check. Each code and its fix is in Check Before You Move.

A problem that clears on its own (no_state, stale_state, not_armed) is waited out for up to timeout seconds first; the message then ends in (waited <timeout> s). Every other problem raises at once: a robot in DAMP asked to walk needs stand(), a robot in STAND asked to walk needs balance(), a robot in MOVE asked to stand needs balance() instead, a low battery needs charging, a hot actuator needs to cool.

RobotFaultedError

A command's check, or a wait, found state.faulted set: a critical alert, such as a fall, over-temperature or battery protection, has latched DAMP. The message and state.alerts name it; write it down before anything restarts the firmware, since restarting clears the record of what latched. Nothing a script sends clears it. Then follow Recovering After an E-Stop or a Fall to support and inspect the robot; its Turn Off Robot step restarts the firmware. See If Something Goes Wrong. RobotFaultedError is a NotReadyError, so except NotReadyError catches it too; e.sent is set when the fault latched after a command went out.

WaitTimeoutError

A command was sent and the robot did not report the effect in time. e.last is the last state seen and e.sent what went out.

  • stand(): the robot did not report STAND and arming within timeout (10 s). See the next section.
  • balance() from STAND: the robot did not report MOVE within timeout (5 s) after the zero velocity went out. Another controller may hold it: the Asimov Manager Cockpit or a paired gamepad outranks a script, and a command it outranks has no effect.
  • damp(): the robot did not report DAMP within timeout (5 s). Check that state is still arriving with menlo status, and that a trajectory() loop is not overwriting the verb.
  • set_joints(wait=True): a joint did not reach its target within tolerance. It may be blocked, or the gains may be too soft to hold it.
  • wait_until(): the predicate stayed false; the message names the robot mode and the age of the last sample.

stand() Times Out and the Robot Stays DAMP

  • A fault is latched: stand() raises RobotFaultedError instead; see above.
  • The robot dropped the command: another source holds control. The Cockpit outranks a paired gamepad, which outranks udp, which outranks livekit, and a paired gamepad holds control while idle. Unpair it and try again.
  • The robot reports STAND but is not armed: it is not upright. The firmware arms after 0.5 s of STAND under 30 degrees of tilt; robot.armed reads the test.

stand() Raises NotReadyError in MOVE

stand() refuses from MOVE with wrong_mode: STAND has no balance loop and a free-standing robot tips over. Zero velocity keeps the firmware in MOVE, so balance() is how to stand still after a walk. stand() is for waking a robot up from DAMP.

set_velocity() Raises NotReadyError in STAND

set_velocity() runs in MOVE only and refuses from STAND with wrong_mode. Call balance() first: from an armed STAND it puts the robot in MOVE at zero velocity and returns once the robot reports MOVE. See Standing and Balancing.

set_velocity Returned but the Robot Did Not Walk

In order of likelihood:

  1. The hold was cut short: with wait=False, set_velocity returns at once, and the next verb or the end of the with block ends the hold. Keep the connection open for the duration, or leave wait at its default so the call returns after the hold.
  2. The robot is not in MOVE: balance() from STAND sends zero velocity without a report of the effect when the robot reports no gravity, since the check then passes with an unknown_gravity warning and the SDK cannot see arming. MOVE is accepted only from STAND held upright for 0.5 s, and an early velocity is neither refused nor reported. Check state.gravity and state.mode.
  3. Another source holds control; see above. The state stream does not say who does.

A robot in DAMP or STAND is not on this list: set_velocity() raises NotReadyError there and sends nothing.

A stand() or damp() Did Nothing

A loop is streaming trajectory() setpoints and the next setpoint overwrote the verb. A stand() sent into the loop is overwritten this way. A damp() usually holds, because the robot drops trajectories once it reports DAMP, but a setpoint that arrives first still wins. Stop the loop, then send the verb. A set_joints() does not have this problem: any verb ends it.

StateStaleError

The robot went quiet for longer than stale_after (default: the link timeout, 2 s) during wait_until() or a command's wait. Treat it as absent, not slow: the connection dropped, the robot's software stopped, or it is reporting to a different machine. LinkLostError follows. A stream that is merely late before a command is sent is stale_state in the check, waited out and then a NotReadyError.

LinkLostError

No state for 2 s. The SDK sent zero velocity if it held one, and every verb now raises this until you close() and connect() again. On udp and hybrid, the robot also zeroes the velocity 2 s after the last command it received.

NotConnectedError

A verb or robot.get_state() before the robot reported (after connect(require_state=False)), or after close(). close() during a set_velocity(..., wait=True) raises this in the waiting thread.

UnsupportedError

This connection does not carry that capability. camera, microphone and speaker need hybrid or livekit; on those, a capability is claimed only from a track that arrived within media_timeout of connecting. Gate with robot.has("camera").

ImportError Naming Pillow, numpy or OpenCV

Frame.to_jpeg, to_numpy, Clip.save_frames and Clip.save_mp4 need those packages at call time. The SDK does not depend on them; install the one named.

Every Sent.wait_outcome() Returns Unknown

The robot reports no per-command verdict, so Unknown is what every command gets and on_refused never fires. Unknown is neither success nor refusal: read the effect with wait_until and robot.get_state(), or let stand(), balance() and damp() wait for it.

Velocity Is Slower Than Asked

Check sent.clamped and sent.command. The SDK clamps to Limits, whose defaults are the firmware caps of 0.4 m/s and 0.8 rad/s; a robot does not walk faster than that. A tighter limit came from Robot(limits=), MENLO_LIMITS, or the saved robot's limits.

Two Sessions Keep Disconnecting Each Other

Both joined the room with the same identity. With ManagerConfig, each session gets its own unless you set the same label; with LiveKitConfig, one token is one participant, so mint two.

How is this guide?

On this page