Menlo

Move the Robot

stand, balance, set_velocity, the check each command runs first, and the velocity limits.

Walking is one verb, set_velocity(vx, vy, vyaw), held for a duration or until you end it. Everything else on this page is about getting the robot into the robot mode that accepts it, and ending a walk safely.

Robot modeWhat the robot doesVerb that reaches it
DAMPevery actuator compliant; the robot rests on a bench, hangs from its gantry hook, or folds to the grounddamp()
STANDjoints stiffened into the standing pose; no balance loopstand()
MOVEthe walking policy balances and walksbalance() from an armed STAND

The firmware accepts MOVE from STAND only once the robot is armed: STAND held upright for 0.5 s. In DAMP, the robot drops velocity commands and nothing reports it.

A walking robot needs clear floor

Support the robot for a first run, hanging from its gantry hook with both feet on the floor, and keep 2 m of clear floor ahead. Keep Asimov Manager open at the E-Stop; nothing in the SDK is an emergency stop. Work through Before You Run a Script and Standing and Balancing first.

Stand Up

stand() puts the robot in STAND: the actuators hold a standing pose by position control alone, without a balance loop. Use it only to bring the robot out of DAMP. Call it on a robot in DAMP that hangs from its gantry hook with both feet on the floor, as in Stand Up and Walk, and never after a walk.

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}")

stand() checks the robot before it sends: the state is fresh, no fault is latched, the robot is not in MOVE, and battery and actuator temperatures are within limits. It raises NotReadyError with nothing sent otherwise, and the script prints the message: what is wrong and what fixes it. Once sent, stand() reads the robot's own report and returns when the robot is armed. It raises WaitTimeoutError when that takes more than 10 s and RobotFaultedError when a fault latches meanwhile.

Balance

balance() puts the robot in MOVE at zero velocity: the walking policy balances it in place, and this is the only robot mode in which the robot stands free. Call it with the robot still supported, and slacken the gantry only after it returns.

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")

From STAND, balance() runs preflight("move") first. Before the arming hold has passed it waits, up to timeout (5 s by default), then sends zero velocity, which is what puts the robot in MOVE, and returns once the robot reports MOVE; WaitTimeoutError when it does not within timeout. In DAMP, with a fault latched or with a low battery it raises NotReadyError at once and sends nothing. In MOVE, balance() is never checked: it ends any held velocity and sends zero, so it is also how a walk ends.

The firmware reports STAND at once but accepts MOVE only after STAND has been held upright, gravity z below -0.87, for 0.5 s. A velocity sent before that is neither refused nor reported: the robot stays in STAND. robot.armed is the arming test as a property: True once armed or in MOVE, False in DAMP or before the hold has passed, None with no state, when the Robot is closed, in robot mode UNKNOWN, or in STAND when the robot reports no gravity. Without gravity, the check in STAND passes with an unknown_gravity warning: the SDK cannot see arming, so it does not claim it, and the zero velocity goes out unverified.

Walk

set_velocity() runs in MOVE only. It checks the robot first and raises NotReadyError with nothing sent when it is in DAMP (stand() it first) or STAND (balance() it first), when a fault is latched, or when the battery or an actuator is out of limits. The codes it can carry are in Check Before You 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")

Axes: vx forward in m/s, vy left in m/s, vyaw counter-clockwise in rad/s. A walk is set_velocity(vx=0.2, duration=3.0), a strafe set_velocity(vy=0.15, duration=3.0), a turn on the spot set_velocity(vyaw=0.3, duration=3.0); combine them in one call.

Hold a Velocity

By default set_velocity() needs a duration: the SDK re-sends the velocity at 10 Hz for that long, sends zero, and returns once the zero has gone out (wait=True). With wait=False it returns at once with a Sent and the hold runs in the background, with or without a duration, until something ends it:

The hold ends whenWhat is sent
duration seconds passzero velocity, by the SDK
balance()zero velocity
another set_velocity()the new velocity
stand(), damp(), trajectory(), set_joints()that verb
close(), or the end of the with blockzero velocity, then the link drops
the link is lost (no state for 2 s)zero velocity; every verb raises until close() and connect()
the firmware latches a fault or restartsnothing; the hold is dropped

The next verb or the end of the with block cuts an unexpired wait=False hold short, so a script that calls set_velocity(duration=3.0, wait=False) and returns does not walk for 3 s.

set_velocity(hold=False) sends exactly one packet and re-sends nothing: your own loop is the clock, as in stream_velocity.py. It takes no duration and no wait, and the check runs once per call, so a ready robot adds no delay between packets.

On udp and hybrid, the robot zeroes the velocity 2 s after the last packet it received; it stays in MOVE, balancing. On livekit, Asimov Edge stops a held velocity when the SDK sends zero or leaves the room, so always bound a hold with a duration; see Watchdogs.

Drive from the Keyboard

keyboard.py turns key presses into short bounded holds of about 0.3 s, so releasing a key stops the robot. w/s walk forward and back, a/d strafe, q/e turn, space sends balance(), t stands (from DAMP only; a refusal shows on the status line), b damps after asking, and x or Ctrl-C quits with zero velocity and close(). From DAMP, t then space brings the robot to STAND and then to MOVE. A one-line status shows the robot mode, arming, battery and the last command.

The keyboard walks the robot backward, sideways and round as well as forward: keep clear floor all around it, not only ahead.

examples/keyboard.pyView on GitHub ↗
while True:
    show(robot, last)
    key = next_key(TICK_S)
    if key is None:
        continue
    if key in QUIT:
        break
    if key in MOVES:
        last = move(robot, key)
    elif key == " ":
        last = balance(robot)
    elif key == "t":
        last = stand(robot)
    elif key == "b":
        last = confirm_and_damp(robot, next_key)

End a Walk

balance() sends zero velocity. The robot stays in MOVE and the walking policy keeps it balanced in place. End a walk this way; a set_velocity() with a duration does the same when the duration runs out. stand() after walking removes the balance loop and the robot falls; the SDK refuses it in MOVE.

To finish a session in DAMP, support the robot first, hanging from its gantry hook or seated on a bench, then run damp.py; see Shutdown.

Turn by Heading

State.yaw is the heading in radians, counter-clockwise. Turn until the heading has changed by an angle instead of guessing a duration:

import math

from menlo.asimov import Robot

with Robot().connect() as robot:
    before = robot.get_state().yaw
    robot.set_velocity(vyaw=0.3, duration=15.0, wait=False)  # checks the robot first; NotReadyError when it cannot walk
    robot.wait_until(
        lambda s: s.yaw is not None
        and abs(math.remainder(s.yaw - before, math.tau)) >= math.radians(90),
        timeout=15.0,
    )
    robot.balance()

math.remainder keeps the difference in (-pi, pi], so the wrap at pi does not count as a full turn. wait_until checks for a fault before it evaluates the predicate and raises StateStaleError when the stream goes quiet, so a robot that fell or stopped talking is never read as "turned".

Limits

The Motion Control Board firmware caps velocity at 0.4 m/s forward and sideways and 0.8 rad/s turning. Limits() defaults to those caps, so Sent.clamped is True exactly when the robot would not have walked at the speed you asked for; sent.command is the velocity that went out. Lower limits keep a script inside a smaller envelope:

from menlo.asimov import Limits, Robot

robot = Robot(limits=Limits(vx=0.2, vy=0.2, vyaw=0.4))

Robot(limits=) wins over MENLO_LIMITS="vx,vy,vyaw", which wins over the saved robot's [robots.NAME.limits], which wins over the defaults. Values above the firmware caps are sent as asked and the firmware clamps them. Negative or non-finite limits raise ValueError.

  • Safety: the stand, balance, walk, damp sequence and why
  • Read State: robot.armed, wait_until, faults
  • Stand Up and Walk: the same robot modes from the Cockpit
  • Reference: set_velocity, balance, stand, damp, Limits
  • Troubleshooting: NotReadyError, WaitTimeoutError and RobotFaultedError

How is this guide?

On this page