Menlo

Safety

What to check before a script runs, how to stand and balance the robot, what to do when something goes wrong, what the SDK, the robot and the firmware each stop, and the readiness codes.

The SDK is a client of the robot. It sends what you ask, bounded by your limits, and reads back what the robot reports. The stops that matter are in the firmware and on the robot. When in doubt, open Asimov Manager and press the E-Stop.

Before You Run a Script

  1. Support the robot, as Supporting the Robot describes: hanging from its gantry hook with both feet on the floor for a first stand and walk, hanging from its gantry hook or seated on a bench for joint control. A walking robot needs 2 m of clear floor ahead of it.
  2. Clear the area. Keep people, cables and furniture out of the robot's reach and keep the robot in sight.
  3. Have a way to stop the robot open. Keep Asimov Manager open with its E-Stop in the page header or the phone top bar (it needs no drive session), and the battery unit within reach to cut power. Stopping the Robot explains each.
  4. Make sure nothing else is driving. Close any Cockpit session and unpair the gamepad. Both outrank a script, and a paired gamepad holds control even while idle.
  5. Check the robot. python check.py or menlo status reports battery, actuator temperatures, faults and whether the state stream is fresh. Neither sends anything.
  6. Start slow. Pass Robot(limits=Limits(vx=0.2, vy=0.2, vyaw=0.4)) or save limits on the robot, and use short duration values until the script behaves.

Nothing in the SDK is an emergency stop

balance(), damp() and close() are commands sent over a network link. None of them cuts power, and none reaches a robot the link has lost. Keep the E-Stop open and the battery unit within reach whenever the robot stands.

Standing and Balancing

The robot has three robot modes, and only one of them keeps it upright on its own. In DAMP no actuator holds a position. In STAND the actuators hold a standing pose and nothing balances it: a free-standing robot in STAND tips over. In MOVE the walking policy balances the robot, at zero velocity or any other. So the robot must be supported in DAMP and STAND, and stands free only once balance() has returned and it reports MOVE.

This is the sequence for a script, and the order in which the Quickstart runs it as separate files:

import sys

from menlo.asimov import NotReadyError, Robot, RobotFaultedError, WaitTimeoutError

with Robot().connect() as robot:
    # 1. The robot hangs from its gantry hook with both feet on the floor.
    try:
        robot.stand()    # DAMP -> STAND; returns once the robot is armed
        robot.balance()  # armed STAND -> MOVE at zero velocity; returns once MOVE is reported
    except RobotFaultedError as exc:  # latched until the firmware restarts
        sys.exit(f"faulted: {exc}")
    except NotReadyError as exc:  # nothing was sent; the message says what fixes it
        sys.exit(str(exc))
    except WaitTimeoutError as exc:  # sent, but the robot did not arm or reach MOVE in time
        sys.exit(str(exc))
    # 2. Only now: slacken the gantry until the robot carries its own weight.
    input("Robot balancing in MOVE. Slacken the gantry, then press Enter to walk. ")
    # 3. Walk. Every set_velocity() ends with zero velocity; the robot stays in MOVE.
    robot.set_velocity(vx=0.2, duration=3.0)
    robot.balance()  # zero velocity: balancing in place
    # 4. Support the robot again before it leaves MOVE.
    input("Take up the gantry's slack so it holds the robot, then press Enter to damp. ")
    robot.damp()  # every actuator stops holding its position

Why each step is where it is:

  1. stand() on the gantry hook, both feet on the floor. STAND holds a pose and does not balance, so the hook carries the robot. stand() checks the robot first, sends STAND and returns once the robot reports STAND and has been upright for 0.5 s: the firmware accepts MOVE only after that, so stand() returning means the robot is armed. It raises NotReadyError with nothing sent when the robot is not ready, and refuses in MOVE: a walking robot that drops to STAND loses its balance loop.
  2. balance() before the support comes off. From an armed STAND, balance() sends zero velocity, which is what puts the robot in MOVE, and returns once the robot reports MOVE. If the robot is in STAND but not armed, it waits for the arming hold before it sends. MOVE at zero velocity is the only state in which the robot stands free, so the hook keeps carrying the robot until balance() has returned: until then the robot is in STAND, and a robot in STAND that loses its support tips over.
  3. Slacken the gantry only after balance() returns. The SDK cannot see the gantry; the script asks you. Slacken it gradually until the robot carries its own weight, with 2 m of clear floor ahead.
  4. Walk, then balance(). set_velocity() runs in MOVE only, holds the velocity for its duration and ends with zero velocity. balance() in MOVE is the same zero velocity, never checked and never refused: it ends a walk at once and the robot balances in place.
  5. Support the robot before damp(). damp() is never refused, and a standing robot falls when its actuators let go. Take up the gantry's slack, or seat the robot on a bench, first. The robot's own controls go from Walk to Stand before Damp (Bring It Back Down) because an operator sees that the support is holding the robot; a script does not, so it stays in MOVE until it damps.

Catch all three errors around stand() and balance(). A RobotFaultedError means a critical alert, a fall included, latched DAMP until the firmware restarts; a NotReadyError means nothing was sent and its message names what fixes it; a WaitTimeoutError means the command was sent and the robot did not report the mode in time, so read robot.get_state().mode before you decide what to do next. Keep Asimov Manager open at the E-Stop throughout: nothing in the SDK is an emergency stop.

If Something Goes Wrong

  1. Press the E-Stop in Asimov Manager, or cut power at the battery unit. The E-Stop damps every actuator at once and a free-standing robot falls; use it when a robot lying on the floor is better than what is happening now. If the robot's software is not running, the E-Stop is disabled: cut power at the battery unit. Do not try to catch a falling robot.
  2. Stop the script. Ctrl-C in a script that uses with Robot().connect() leaves the block, and close() sends zero velocity. A script that keeps running keeps re-sending its velocity at 10 Hz; menlo balance from another terminal does not end it.
  3. Read what the robot reports, and write it down. menlo status shows the robot mode, a FAULTED badge and what latched, from the current alerts and the latched error_flags. In a script, state.faulted and the faulted message from preflight() name the causes, and stand(), balance(), set_velocity(), set_joints(), trajectory() and wait_until() raise RobotFaultedError.
  4. Recover the robot. Follow Recovering After an E-Stop or a Fall: support and inspect the robot. Its Turn Off Robot step restarts the firmware, so do step 3 first.
  5. Restart the firmware, if the recovery did not. A critical alert holds the robot in DAMP, and error_flags set, until the firmware restarts; restarting clears the record of what latched. Use Restart on the Firmware (RPU) row of the Troubleshoot page, or Turn Off Robot and Start Robot on Overview.

Do not call stand() on a robot that is walking or falling. STAND has no balance loop: it stiffens the joints into a pose, and a free-standing robot in that pose tips over. End a walk with balance(), which leaves the robot in MOVE, balancing in place.

Who Stops What

LayerStopsDoes not stop
SDKa held velocity: zero on balance(), close(), the end of a with block, a lost linkanything after a fault; another program's velocity
The robotvelocity and trajectory in DAMP (dropped); a velocity 2 s after the last command on udp and hybrid; a trajectory 2 s after the last setpoint (DAMP)a velocity a script keeps re-sending
FirmwareMOVE before an armed STAND (the robot stays in STAND, nothing reports it); everything, on a critical alert (DAMP, latched)a command because of where it came from: it obeys whichever arrived last

The SDK also clamps every velocity to your Limits, runs every wait against fresh state, and fences a running set_joints() so that any verb ends it. close() sends zero velocity only when this Robot holds one; a Robot that sent nothing does not stop another program. After close() stops re-sending a trajectory, the robot enters DAMP 2 s later. Neither the robot nor the firmware checks command timestamps.

Who Holds Control

The robot gives the Cockpit priority over a paired gamepad, the gamepad over udp, and udp over livekit. The state stream does not say who holds control, so a walk that does not happen, with no fault and no refusal, is the first thing to check.

An idle paired gamepad locks the SDK out

A gamepad paired to the robot holds control while nobody touches it. Every velocity a script sends is dropped without a report. Unpair the gamepad on the Controller page of Asimov Manager before you run a script; see Gamepad.

Limits

The firmware caps vx and vy at 0.4 m/s and vyaw at 0.8 rad/s. Limits() defaults to those caps and the SDK clamps every velocity to them before it leaves, so Sent.clamped is True exactly when the robot would not have walked at the speed you asked for. A tighter envelope is Robot(limits=Limits(vx=0.2, vy=0.2, vyaw=0.4)), MENLO_LIMITS="0.2,0.2,0.4", or limits on a saved robot. Limits above the caps are sent as asked and the firmware clamps them.

Arming

MOVE is accepted only from STAND held upright, gravity z below -0.87 (under 30 degrees of tilt), for 0.5 s. A velocity sent earlier is not refused and not reported: the robot stays in STAND while the hold runs out. stand() returns once the robot is armed, balance() in STAND waits for arming before it sends, and robot.armed reads the same test the firmware applies. set_velocity() does not wait for arming: it runs in MOVE only, and in STAND it raises NotReadyError naming balance().

Without a gravity vector, arming goes unverified

A robot that reports no gravity gives preflight("move") and preflight("trajectory") the unknown_gravity warning, and they pass in STAND without seeing arming. robot.armed is None, and balance() sends without waiting. Check state.gravity before you rely on the check in that case.

Faults

A critical alert, such as a fall, actuator over-temperature, battery protection, a joint past its limit, lost actuator communication or a watchdog, makes the firmware latch DAMP. The latch, and error_flags, stay until the firmware restarts. The SDK reports it as State.faulted, reports faulted from preflight(), raises RobotFaultedError from every command that checks the robot and from wait_until(), and drops a held velocity without sending anything more.

Watchdogs

WatchdogWhereWhat happens
Velocitythe robot, udp and hybridzero velocity 2 s after the last velocity command; the robot stays in MOVE, balancing
Trajectorythe robot, every modeDAMP 2 s after the last setpoint
LinkSDK, every modeno state for 2 s: zero velocity if one is held, on_link_lost fires, every verb raises LinkLostError until close() and connect()

A script that holds a velocity re-sends it at 10 Hz, so the velocity watchdog does not stop it: end it with balance(), a duration, or close().

On a livekit connection, Asimov Edge stops a held velocity when the SDK sends zero or leaves the room. A script that hangs while it holds a velocity keeps the robot walking until LiveKit drops the participant. Bound every hold with a duration, and prefer udp or hybrid for a control loop.

Check Before You Move

stand(), balance() in STAND, set_velocity(), set_joints() and trajectory() check the robot before they send anything. A ready robot adds no delay. A 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 the command's timeout: 10 s for stand(), 5 s for balance(), set_velocity() and set_joints(). trajectory() and set_velocity(hold=False) check once and do not wait unless given a timeout. Any other blocking problem raises NotReadyError at once, or RobotFaultedError for a latched fault, and nothing is sent. The message names every blocking problem, its code and what fixes it:

not ready to move: the robot is in DAMP; stand() it first (wrong_mode)
not ready to move: the robot is in STAND; balance() it first (wrong_mode)
not ready to move: battery at 12 % (below 20 %) (battery_low); charge the battery
not ready to stand: the firmware latched DAMP (FALL_DETECTED); it stays latched until the firmware restarts (faulted)

balance() in MOVE and damp() are never checked: both are how you make the robot do less.

from menlo.asimov import NotReadyError, Robot, RobotFaultedError, WaitTimeoutError

with Robot().connect() as robot:
    try:
        robot.stand()
        robot.balance()
        robot.set_velocity(vx=0.3, duration=3.0)
    except RobotFaultedError as exc:  # latched until the firmware restarts
        print("faulted:", exc)
    except NotReadyError as exc:  # nothing was sent
        print(exc)
        if exc.has("battery_low"):
            ...
    except WaitTimeoutError as exc:  # sent, but the robot did not arm or reach MOVE in time
        print(exc)

RobotFaultedError is a NotReadyError, so except NotReadyError alone catches every "the robot cannot do that now". exc.problems lists the blocking problems, exc.has(code) tests one, and exc.preflight carries the whole check.

robot.preflight(action) is the same check without a command: it answers whether the robot can "stand", "move" or run a "trajectory" right now, sends nothing and never raises for a robot condition. check.py prints it for all three actions:

examples/check.pyView on GitHub ↗
# The SDK counts the 0.5 s a robot in STAND must be upright to arm from its own samples.
time.sleep(0.6)
print(f"robot mode {robot.get_state().mode.name}, armed {robot.armed}")
for action in ("stand", "move", "trajectory"):
    print(robot.preflight(action))  # "ready to move", or "not ready to move:" and why
CodeBlocksMeaningWhat clears it
not_connectedyesclosed, link lost, or protocol mismatchclose() and connect()
no_stateyesconnected with require_state=False and nothing reportedwait for the firmware
stale_stateyeslatest state older than 0.5 sa command waits for the next sample; otherwise check the link and the robot's UDP state target
faultedyesa critical alert is latched, or the robot reports FAULT_DAMPrestart the firmware
battery_protectingyesthe battery is protecting itselfcharge it or let it cool
battery_lowyesbelow 20 %charge
unknown_batterywarningthe robot reports no battery
joint_hotyesan actuator at or above 60 °Clet it cool
unknown_joint_tempwarningthe robot reports no joint temperatures
wrong_modeyesstand from MOVE or UNKNOWN; move and trajectory from DAMP or UNKNOWN; set_velocity() also from STANDfrom DAMP, stand() first; from STAND, balance() first; in MOVE the robot is already standing, so do not stand it: end a walk with balance()
not_armedyesmove or trajectory in STAND before the 0.5 s upright hold; the message gives the tilt in degreeshold STAND upright; a command waits for it
unknown_gravitywarningthe robot reports no gravity, so arming cannot be seen

stand passes from DAMP or STAND; balance() runs the move check from STAND, which passes once the robot is armed; set_velocity() passes in MOVE only; trajectory passes in MOVE or an armed STAND. A command waits only for no_state, stale_state and not_armed; every other blocking problem fails it at once, since waiting cannot clear it. Blocking problems are listed first; a warning never blocks.

With no state at all, every action reports no_state or not_connected. Robot mode UNKNOWN means the robot reports a mode the SDK does not know; every action then reports wrong_mode.

battery_low at 20 % is stricter than the firmware on purpose: the firmware only warns at that level, and latches DAMP once the battery protects itself (battery_protecting). joint_hot at 60 °C matches the firmware's warning; the firmware latches DAMP at 80 °C.

Shutdown

Leaving the with block calls close(): zero velocity if one is held, then the link drops. The robot stays in MOVE, balancing in place, and the Cockpit or the gamepad can take it from there. Call damp() at the end of a session only on a supported robot, hanging from its gantry hook or seated on a bench: every actuator stops holding its position and a standing robot falls.

damp.py asks before it sends, then waits for the robot to report DAMP:

examples/damp.pyView on GitHub ↗
# damp() is never refused, so there is no check here, only the question.
if not YES:
    mode = robot.get_state().mode.name
    question = f"Robot mode {mode}. Is the robot supported? Damp now? [y/N] "
    if input(question).strip().lower() not in ("y", "yes"):
        print("nothing sent")
        sys.exit(0)
try:
    robot.damp()  # returns once the robot reports DAMP
except WaitTimeoutError as exc:  # sent, but DAMP was not reported: the message says what next
    print(exc)
    sys.exit(1)
print(f"robot mode {robot.get_state().mode.name}")

Turning the robot off is in Power.

How is this guide?

On this page