Menlo
Asimov API

Robot Control

Control modes, state machine, commands, and telemetry for the Asimov robot.

Send commands on the commands DataChannel, receive telemetry on the telemetry DataTrack. See the Asimov API overview for connection setup.

Control requires commissioned hardware

Use these commands only on a commissioned robot with version-matched software, validated support and operating procedures, and an independent hardware emergency stop. DAMP is a software control state, not an emergency-stop system.

Control Modes

See the state machine diagram on the overview page for a visual reference.

ModeDescription
DAMPActuators go compliant (limp). Safe default. The robot enters DAMP automatically on: boot, fall detected, actuator overtemp, trajectory session timeout (2 s), or explicit ModeCommand(DAMP).
STANDRobot ramps to a standing pose over ~2 s. The ramp is advisory - the firmware accepts MOVE commands the moment they arrive. If you want a settled pose before commanding MOVE, wait ~2 s after sending ModeCommand(STAND).
MOVE/POLICYNeural network locomotion at 50 Hz. Send VelocityCommand(vx, vy, vyaw) to control walking speed and direction.
MOVE/TRAJECTORYDirect PD control of joints. No neural network. Requires continuous streaming (~50 Hz) - if trajectory packets stop for 2 s during an active session, the robot auto-DAMPs. Every value in positions[25] actively drives its actuator - there is no skip sentinel. For joints you don't want to actively command, send a hold target of your choice (e.g. the joint's current value read from EdgeTelemetry.joint_pos).

Allowed Transitions

FromToCommandNotes
AnyDAMPModeCommand(DAMP)Always allowed as a software stop request; not a hardware emergency stop.
DAMPSTANDModeCommand(STAND)Robot ramps to standing pose over ~2 s. Ramp is advisory - wait ~2 s before sending MOVE if you want a settled pose.
STANDMOVE/POLICYVelocityCommandSending velocity auto-enters POLICY.
STANDMOVE/TRAJECTORYTrajectoryRequestSending trajectory auto-enters TRAJECTORY.
MOVESTANDModeCommand(STAND)Returns to default standing pose.

There is no DAMP -> MOVE shortcut. While the firmware reports FW_MODE_DAMP, both VelocityCommand and TrajectoryRequest are dropped at the edge. Pass through ModeCommand(STAND) first, for velocity and trajectory alike.

Trajectory session timeout: while a trajectory teleop session is active, your application must stream TrajectoryRequest packets at ~50 Hz. If no trajectory packet arrives for 2 s, the robot auto-DAMPs. This catches client disconnects (tab close, WiFi drop, grip release).

Velocity staleness: velocity has its own watchdog. If 2 s pass with no new nonzero VelocityCommand, the edge issues a zero velocity - the robot holds in MOVE at a standstill rather than DAMPing or returning to STAND. Clients that deduplicate a held control must re-send the held nonzero command periodically (the reference clients use a 500 ms keepalive) or the robot will stop under them.

On the cloud path this watchdog is suppressed while the operator is still confirmed present in the session. The moment presence cannot be confirmed, the 2 s rule applies.


Commands

Velocity (Walk)

import itertools
import time
from asimov_protocol.v1.edge_cloud_pb2 import CloudCommand, VelocityCommand

# Monotonic sequence counter shared by all examples on this page.
_seq = itertools.count(1)
def seq() -> int:
    return next(_seq)


cmd = CloudCommand(
    timestamp_us=int(time.time() * 1e6),
    sequence=seq(),
    velocity=VelocityCommand(vx=0.5, vy=0.0, vyaw=0.0)
)
await room.local_participant.publish_data(
    cmd.SerializeToString(), topic="commands", reliable=True
)
FieldEnvelopeUnitDescription
vx-1.0 to 1.0m/sForward (+) / backward (-)
vy-1.0 to 1.0m/sStrafe left (+) / right (-)
vyaw-1.5 to 1.5rad/sTurn left (+) / right (-)

The edge does not clamp velocity. It rejects non-finite values and forwards everything else verbatim; the firmware applies its own limits. The envelope above is the ceiling the reference operator clients pin every command to before sending, and it is the envelope this documentation is written against. Clamp on your side - do not rely on the robot to catch a runaway command.

Send a ModeCommand(STAND) first. VelocityCommand packets received while the robot is in FW_MODE_DAMP are silently dropped - no error event is emitted. Wait until telemetry reports FW_MODE_STAND (or FW_MODE_MOVE) before sending velocity.

Trajectory (Direct Joint Control)

Send 25 positions in firmware order. Every value actively drives its actuator - there is no skip sentinel. For joints you don't want to actively move, send a hold target of your choice (typical pattern: read EdgeTelemetry.joint_pos and override only the joints you're commanding).

Each TrajectoryRequest is applied as an instantaneous PD target - there is no in-firmware interpolation between segments. Clients are responsible for any smoothing across packets.

import time
from asimov_protocol.v1.edge_cloud_pb2 import (
    CloudCommand, TrajectoryRequest, FullTrajectory, JointSegment, EdgeTelemetry
)

# Keep a reference to the latest telemetry frame; update it from your
# telemetry subscription handler (see "Telemetry" below).
latest_telemetry: EdgeTelemetry | None = None

# ... once `latest_telemetry` has been populated by your subscription handler:
assert latest_telemetry is not None, "wait for the first telemetry frame"

# Seed positions from the latest telemetry pose so unspecified joints
# hold their current value; override only the ones you're driving.
positions = list(latest_telemetry.joint_pos)   # 25 floats
positions[12] = 0.5           # L_Shoulder_Pitch
positions[13] = -0.2          # L_Shoulder_Roll
positions[15] = 1.2           # L_Elbow

cmd = CloudCommand(
    timestamp_us=int(time.time() * 1e6),
    sequence=seq(),
    trajectory=TrajectoryRequest(
        full=FullTrajectory(segments=[
            JointSegment(positions=positions)
        ])
    )
)
await room.local_participant.publish_data(
    cmd.SerializeToString(), topic="commands", reliable=True
)
FieldCountRangeUnitDescription
positions25actuator-specificradiansTarget positions in firmware order. Every entry drives its actuator - no skip sentinel.
kp250 - 500-Position gain (optional - edge fills defaults if omitted)
kd250 - 5.0-Velocity damping gain (optional - edge fills defaults if omitted)

KP/KD overrides are all-or-nothing. To override gains, send exactly 25 values in kp and/or kd. Any other count (including partial overrides like 23 or 24 values) is silently replaced with the edge defaults.

Mode Switch

import time
from asimov_protocol.v1.edge_cloud_pb2 import CloudCommand, ModeCommand, Mode


# Stand up
cmd = CloudCommand(
    timestamp_us=int(time.time() * 1e6),
    sequence=seq(),
    mode=ModeCommand(mode=Mode.MODE_STAND)
)

# Request software DAMP (not a hardware emergency stop)
cmd = CloudCommand(
    timestamp_us=int(time.time() * 1e6),
    sequence=seq(),
    mode=ModeCommand(mode=Mode.MODE_DAMP)
)

Command Validation

The robot silently drops malformed or safety-gated commands - no EdgeError event is emitted on the system channel when this happens. Validate inputs client-side before sending. The current drop conditions:

CommandDropped when...
VelocityCommandany of vx, vy, vyaw is non-finite (NaN/Inf), or firmware is in FW_MODE_DAMP
TrajectoryRequestJointSegment.positions length != 25, or any position is non-finite, or FullTrajectory.segments is empty, or the request uses the unimplemented id source (LUT name) instead of full, or firmware is in FW_MODE_DAMP
ModeCommand(STAND)firmware is fault-DAMPed - it entered DAMP by itself with error_flags set or a critical alert active. The robot will not stand back up into a fault.
ModeCommand(DAMP)never dropped - always accepted

Any command is also dropped if it arrives from a source that is not the arbiter's active controller.

Velocity values are not clamped by the edge - only non-finite values are rejected. KP/KD arrays of any length other than 25 are not rejected - edge silently substitutes its own defaults.

A failed STAND is not always a connectivity problem. Check error_flags and active_alerts before retrying. Some firmware faults - a detected fall in particular - latch and clear only on a firmware restart; no command recovers them, and the locomotion policy has no get-up behaviour.


Telemetry

The robot streams telemetry at 10 Hz on the telemetry DataTrack (lossy).

FieldTypeCountDescription
timestamp_usuint641Edge clock (microseconds)
fw_timestamp_usuint641Firmware clock (microseconds) - use with timestamp_us for latency measurement
sequenceuint321Monotonic counter
fw_modeFirmwareMode1FW_MODE_DAMP=0, FW_MODE_STAND=1, FW_MODE_MOVE=2
joint_posfloat25Joint positions (radians)
joint_velfloat25Joint velocities (rad/s)
joint_currentfloat25Actuator current (amps)
joint_tempfloat25Actuator temperature (celsius)
imu_quatfloat4Orientation [w, x, y, z]
imu_gyrofloat3Angular velocity (rad/s)
imu_gravityfloat3Projected gravity vector
error_flagsuint321Active errors bitfield
active_alertsFirmwareAlertrepeatedCurrent hardware alerts (actuator overtemp, fall detect, CAN faults)
fw_age_msuint321How stale the firmware data was when forwarded (milliseconds)
last_video_timestamp_usuint641Timestamp of the last video frame sent (for media sync)
last_audio_timestamp_usuint641Timestamp of the last audio frame sent (for media sync)
import asyncio
from asimov_protocol.v1.edge_cloud_pb2 import EdgeTelemetry


async def read_telemetry(track):
    stream = track.subscribe()
    async for frame in stream:
        t = EdgeTelemetry.FromString(frame.payload)
        print(f"Mode: {t.fw_mode}, Joints: {list(t.joint_pos)}")


# Subscribe to the robot's telemetry DataTrack
@room.on("data_track_published")
def on_data_track(track):
    asyncio.create_task(read_telemetry(track))

Joint Order

All trajectory commands use firmware order - 25 joints matching the CAN bus actuator layout. These are the indices for the positions array.

GroupIndicesJoints
Left Leg0-5L_Hip_Pitch, L_Hip_Roll, L_Hip_Yaw, L_Knee, L_Ankle_A, L_Ankle_B
Right Leg6-11R_Hip_Pitch, R_Hip_Roll, R_Hip_Yaw, R_Knee, R_Ankle_A, R_Ankle_B
Left Arm12-16L_Shoulder_Pitch, L_Shoulder_Roll, L_Shoulder_Yaw, L_Elbow, L_Wrist_Yaw
Right Arm17-21R_Shoulder_Pitch, R_Shoulder_Roll, R_Shoulder_Yaw, R_Elbow, R_Wrist_Yaw
Waist + Neck22-24Waist_Yaw, Neck_Yaw, Neck_Pitch

PD Gains

KP/KD gains are injected automatically if you don't provide them. To override, send exactly 25 values in kp and/or kd matching the joint order above. For reference:

ParameterRangeTypical
Kp (position gain)0 - 50040-150
Kd (velocity gain)0 - 5.02.0-5.0

How is this guide?

On this page