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 Modes

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

ModeDescription
DAMPMotors go compliant (limp). Safe default. The robot enters DAMP automatically on: boot, fall detected, motor overtemp, trajectory session timeout (200 ms), 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 200 ms during an active session, the robot auto-DAMPs. Every value in positions[25] actively drives its motor — 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. 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.
DAMPMOVE/TRAJECTORYTrajectoryRequestDirect joint control. No ModeCommand(STAND) needed. Direct PD engages immediately — the robot transitions from compliant (limp) to actively driven on the first packet, so your first target should be a sensible pose.
MOVESTANDModeCommand(STAND)Returns to default standing pose.

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 200 ms, the robot auto-DAMPs. This catches client disconnects (tab close, WiFi drop, grip release) — it does not apply to velocity or mode commands.

Velocity commands persist: once you've sent a VelocityCommand, the robot keeps walking at that velocity until you send a new velocity, switch modes, or send DAMP. There is no user-facing "must stream" requirement for velocity.


Commands

Velocity (Walk)

import itertools
import time
from edge.generated.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
)
FieldRangeUnitDescription
vx-2.0 to 2.0m/sForward / backward
vy-1.0 to 1.0m/sLeft / right
vyaw-2.0 to 2.0rad/sYaw rotation

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 motor — 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 edge.generated.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
positions25motor-specificradiansTarget positions in firmware order. Every entry drives its motor — no skip sentinel.
kp250 – 500Position gain (optional — edge fills defaults if omitted)
kd250 – 5.0Velocity 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 edge.generated.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)
)

# Emergency stop (damp)
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
ModeCommandnever dropped — always accepted

Velocity values outside the published ranges are clamped, not dropped. KP/KD arrays of any length other than 25 are not rejected — edge silently substitutes its own defaults.


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_currentfloat25Motor current (amps)
joint_tempfloat25Motor 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 (motor 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 edge.generated.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 motor 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_Pitch, Neck_Yaw

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