Menlo

Quickstart

Install menlo-sdk, save the robot, check it, then stand, balance, walk for 3 s and damp from Python.

By the end of this page the robot stands up from DAMP, balances on its own, walks forward for 3 s and rests again, from five short scripts.

Before the first walk

For a first run, hang the robot from its gantry hook with both feet on the floor and 2 m of clear floor ahead. Keep Asimov Manager open at the E-Stop, or be ready to cut power at the battery unit: nothing in the SDK is an emergency stop. Work through Before You Run a Script and Standing and Balancing first.

Install

Python 3.12 or newer.

pip install menlo-sdk

or, in a project managed by uv, uv add menlo-sdk. One package drives every connection mode; there are no extras.

Get the Examples

The scripts on this page are in the examples directory of the menlo-sdk repository on GitHub. Clone it and run them from there:

git clone https://github.com/menloresearch/menlo-sdk
cd menlo-sdk/examples

Each code block below shows the main part of a script. The full script also imports the SDK, sets its constants and connects with with Robot().connect() as robot:, which waits for the robot's first state.

Get an SDK Credential

The hybrid and livekit connection modes join the robot's LiveKit room with an SDK credential from Asimov Manager; livekit also carries commands through it. Skip this step for udp, which needs no credential.

  1. In Asimov Manager, open Developer in the sidebar. Under How a script connects, note the Manager URL: it is the address the SDK uses to reach the robot.

    You do not need the LiveKit URL or the Room. The SDK asks the robot for both each time it connects, and when the LiveKit URL reads localhost, the SDK uses the robot's address instead.

  2. Under Issue a credential, enter a Name for the computer that will use it, such as my-laptop. Issue one per computer, so you can revoke one without cutting off the others.

  3. Under What it may do, choose Control to drive the robot from your computer. Observe watches the camera and listens to the microphone only; the robot drops its commands.

  4. Select Issue credential, then copy the credential with the copy button next to it. It is shown only this once: the robot keeps the credential's ID, not the credential, so it cannot show it again. If you lose it, revoke it and issue another.

  5. The credential appears under Issued credentials with its role and ID. To take it back, select Revoke next to it; the computer holding it can no longer connect, and a session that is already running keeps working until its LiveKit token expires, at most 12 hours.

Save the Robot

menlo setup

The wizard asks for a name, a connection mode, and what the mode needs: the robot's address for udp, the Manager URL and the credential for livekit, both for hybrid. It checks each field against the robot before it saves, and every script below then finds the robot with Robot() and no arguments.

udp and hybrid carry commands and state on the robot's network, and the robot sends UDP state to one computer. In Asimov Manager, open Advanced Settings and, under Asimov Edge, set udp-control to on and udp-state-host to your computer's address, then select Save & Restart Asimov Edge. Without a saved robot, export MENLO_UDP_HOST=<robot address> points the SDK at the robot for one shell. To build the connection in code, see Connection Modes.

Check

check.py connects, prints what stands in the way of a stand, a walk and a trajectory, and sends nothing. Run it before anything that moves the robot:

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
python check.py

Every problem has a stable code and a one-line message; the codes and what clears each one are in Check Before You Move. stand(), balance() in STAND, set_velocity(), set_joints() and trajectory() run the same check before they send anything, so the scripts below do not repeat it.

Stand

With the robot in DAMP, hanging from its gantry hook with both feet on the floor:

examples/stand.pyView on GitHub ↗
try:
    # Checks the robot first. Returns once it reports STAND and has been upright for
    # 0.5 s (armed): the firmware accepts MOVE only after that.
    robot.stand()
except (NotReadyError, WaitTimeoutError) as exc:
    print(exc)  # what is wrong and what fixes it
    sys.exit(1)
print(f"robot mode {robot.get_state().mode.name}, armed {robot.armed}")
python stand.py

The actuators hold a standing pose and stand() returns once the robot is armed: STAND held upright for 0.5 s. It sends nothing else. When the robot is not ready, stand() raises NotReadyError with nothing sent, and the script prints why; when the robot does not arm within 10 s, it raises WaitTimeoutError. Call stand() only from DAMP, never after a walk: STAND has no balance loop.

Balance

With the robot armed and still on its gantry hook:

examples/balance.pyView on GitHub ↗
try:
    # Checks the robot first when it is in STAND. Returns once it reports MOVE.
    robot.balance()
except (NotReadyError, WaitTimeoutError) as exc:
    print(exc)  # what is wrong and what fixes it
    sys.exit(1)
print(f"robot mode {robot.get_state().mode.name}, balancing in place")
python balance.py

balance() sends zero velocity, which puts an armed robot in MOVE: the walking policy balances it in place, and this is the only robot mode in which it stands free. It returns once the robot reports MOVE, or raises WaitTimeoutError after 5 s. Once it has returned, slacken the gantry gradually until the robot carries its own weight.

Walk

With the robot balancing in MOVE:

examples/walk.pyView on GitHub ↗
try:
    # Checks the robot first: it must be in MOVE. Held and re-sent at 10 Hz for
    # DURATION_S, then zero velocity; returns after that.
    sent = robot.set_velocity(vx=VX, vy=VY, vyaw=VYAW, duration=DURATION_S)
except NotReadyError as exc:  # nothing was sent, or (RobotFaultedError) a fault ended it
    print(exc)
    sys.exit(1)
if sent.clamped:
    print("the SDK reduced the speed to the robot's limits:", sent.command)
print(f"robot mode {robot.get_state().mode.name}, balancing in place")
python walk.py

What each line does:

LineEffect
set_velocity(vx=VX, vy=VY, vyaw=VYAW, duration=DURATION_S)Checks that the robot is in MOVE and ready to walk, then walks at the set velocity for the duration; the SDK re-sends the velocity while it is held, sends zero velocity when the duration ends, and returns after that
except NotReadyErrorThe check failed and nothing was sent, or a fault ended the walk (a RobotFaultedError with .sent set); the message says what is wrong and what fixes it
sent.clampedTrue when the SDK reduced the speed to the robot's limits; sent.command is what went out

After the walk the robot stays in MOVE, balancing in place. The velocity and duration are constants at the top of the file. Leaving the with block closes the connection; a held velocity gets a zero first. The script never changes the robot mode: on a robot in STAND it prints not ready to move: the robot is in STAND; balance() it first (wrong_mode) and exits.

Put the Robot Down

damp() makes every actuator compliant. A standing robot folds, so support it first: take up the gantry's slack, or seat the robot on a bench.

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}")
python damp.py

The script asks for confirmation unless YES is set to True at the top of the file. damp() is not an emergency stop; for that, use the E-Stop in Asimov Manager, or cut power at the battery unit (Stopping the Robot).

What Ready Means

A command that moves the robot checks it first and raises NotReadyError instead of guessing. The message names each blocking problem, its code and what fixes it, and the exception carries the problems. Read it before you retry:

from menlo.asimov import NotReadyError, Robot

with Robot().connect() as robot:
    try:
        robot.set_velocity(vx=0.3, duration=3.0)
    except NotReadyError as e:
        print(e)             # not ready to move: the robot is in DAMP; stand() it first (wrong_mode)
        print(e.preflight)   # one line per problem, blocking ones first

A problem that clears on its own, such as a robot that is in STAND but has not held it upright for 0.5 s, is waited out for up to timeout seconds (5 s by default) before the command raises. balance() in MOVE and damp() are never refused. The full picture is in Check Before You Move.

Next Steps

  • Move the Robot: turning, holding a velocity, limits
  • Safety: what the SDK does and does not do for you
  • Examples: every script in the repository, including keyboard.py for driving from the keyboard
  • Command Line: menlo setup to save a robot, menlo status --watch while you develop

How is this guide?

On this page