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.
| Mode | Description |
|---|---|
| DAMP | Motors 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). |
| STAND | Robot 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/POLICY | Neural network locomotion at 50 Hz. Send VelocityCommand(vx, vy, vyaw) to control walking speed and direction. |
| MOVE/TRAJECTORY | Direct 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
| From | To | Command | Notes |
|---|---|---|---|
| Any | DAMP | ModeCommand(DAMP) | Always allowed. Emergency stop. |
| DAMP | STAND | ModeCommand(STAND) | Robot ramps to standing pose over ~2 s. Ramp is advisory — wait ~2 s before sending MOVE if you want a settled pose. |
| STAND | MOVE/POLICY | VelocityCommand | Sending velocity auto-enters POLICY. |
| STAND | MOVE/TRAJECTORY | TrajectoryRequest | Sending trajectory auto-enters TRAJECTORY. |
| DAMP | MOVE/TRAJECTORY | TrajectoryRequest | Direct 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. |
| MOVE | STAND | ModeCommand(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
)| Field | Range | Unit | Description |
|---|---|---|---|
vx | -2.0 to 2.0 | m/s | Forward / backward |
vy | -1.0 to 1.0 | m/s | Left / right |
vyaw | -2.0 to 2.0 | rad/s | Yaw rotation |
Send a
ModeCommand(STAND)first.VelocityCommandpackets received while the robot is inFW_MODE_DAMPare silently dropped — no error event is emitted. Wait until telemetry reportsFW_MODE_STAND(orFW_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
)| Field | Count | Range | Unit | Description |
|---|---|---|---|---|
positions | 25 | motor-specific | radians | Target positions in firmware order. Every entry drives its motor — no skip sentinel. |
kp | 25 | 0 – 500 | — | Position gain (optional — edge fills defaults if omitted) |
kd | 25 | 0 – 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
kpand/orkd. 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:
| Command | Dropped when… |
|---|---|
VelocityCommand | any of vx, vy, vyaw is non-finite (NaN/Inf), or firmware is in FW_MODE_DAMP |
TrajectoryRequest | JointSegment.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 |
ModeCommand | never 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).
| Field | Type | Count | Description |
|---|---|---|---|
timestamp_us | uint64 | 1 | Edge clock (microseconds) |
fw_timestamp_us | uint64 | 1 | Firmware clock (microseconds) — use with timestamp_us for latency measurement |
sequence | uint32 | 1 | Monotonic counter |
fw_mode | FirmwareMode | 1 | FW_MODE_DAMP=0, FW_MODE_STAND=1, FW_MODE_MOVE=2 |
joint_pos | float | 25 | Joint positions (radians) |
joint_vel | float | 25 | Joint velocities (rad/s) |
joint_current | float | 25 | Motor current (amps) |
joint_temp | float | 25 | Motor temperature (celsius) |
imu_quat | float | 4 | Orientation [w, x, y, z] |
imu_gyro | float | 3 | Angular velocity (rad/s) |
imu_gravity | float | 3 | Projected gravity vector |
error_flags | uint32 | 1 | Active errors bitfield |
active_alerts | FirmwareAlert | repeated | Current hardware alerts (motor overtemp, fall detect, CAN faults) |
fw_age_ms | uint32 | 1 | How stale the firmware data was when forwarded (milliseconds) |
last_video_timestamp_us | uint64 | 1 | Timestamp of the last video frame sent (for media sync) |
last_audio_timestamp_us | uint64 | 1 | Timestamp 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.
| Group | Indices | Joints |
|---|---|---|
| Left Leg | 0–5 | L_Hip_Pitch, L_Hip_Roll, L_Hip_Yaw, L_Knee, L_Ankle_A, L_Ankle_B |
| Right Leg | 6–11 | R_Hip_Pitch, R_Hip_Roll, R_Hip_Yaw, R_Knee, R_Ankle_A, R_Ankle_B |
| Left Arm | 12–16 | L_Shoulder_Pitch, L_Shoulder_Roll, L_Shoulder_Yaw, L_Elbow, L_Wrist_Yaw |
| Right Arm | 17–21 | R_Shoulder_Pitch, R_Shoulder_Roll, R_Shoulder_Yaw, R_Elbow, R_Wrist_Yaw |
| Waist + Neck | 22–24 | Waist_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:
| Parameter | Range | Typical |
|---|---|---|
| Kp (position gain) | 0 – 500 | 40–150 |
| Kd (velocity gain) | 0 – 5.0 | 2.0–5.0 |
How is this guide?