# Supply Chain Partners Asimov is an open source humanoid robot with a global customer base across research, education, and commercial applications. We are looking for manufacturing and component partners. A full body humanoid is closer in part count and complexity to an EV car than a robotic arm. Review the [Asimov 1 overview](/asimov/1/overview) and [System Tour](/asimov/1/overview/system-tour) before deciding if this is a fit. *** How It Works [#how-it-works] Open supply chain. Similar to LeRobot and OpenArm. | | | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **Margins** | 100% Yours. | | **Component pricing** | Volume pricing on actuators, controllers, and other key parts. | | **Prototype support** | We work closely with you to build your first unit. | | **Validation** | Our engineering team validates your prototype against the reference design and signs off when it meets spec. We share findings directly with you. | *** Getting Started [#getting-started] Review the platform [#review-the-platform] Read the [Asimov 1 overview](/asimov/1/overview) and [System Tour](/asimov/1/overview/system-tour). Reach out [#reach-out] [Contact us](https://tally.so/r/J9O5BJ) with your manufacturing capabilities and region. Build a prototype [#build-a-prototype] We work closely with you to build your first unit, providing technical support and guidance. Get validated [#get-validated] Our engineering team reviews your prototype against the reference design and signs off when it meets spec. *** What We Expect [#what-we-expect] * Customer service, quality, and reliability * (optionally) warranty support with added cost for your units * Adherence to Asimov safety and quality standards (provided during onboarding) * Sustainable materials and ethical labor practices * Direct communication *** Resources [#resources] # Resources The following are key resources for partners supplying Asimov. Actuators [#actuators] You can use the code `Asimov❤️Encos` to get discounts on actuators from Encos, our verified supplier. More coming soon # Menlo Platform Menlo Platform is coming soon. It will provide the cloud layer for connecting robots, managing deployments, accessing telemetry, and operating robot applications across development and physical systems. Asimov 1 does not require Menlo Platform for its current locomotion, directional driving, direct actuator control, telemetry, or media interfaces. Use the [Asimov 1 API](/asimov/1/program/api) for the currently available robot interface. The Python SDK for Asimov hardware, tentatively named **Asimov Client SDK**, is also in development and is separate from Menlo Platform. # Command Line `pip install menlo-sdk` installs the `menlo` command. It saves robots for the SDK and drives one through the same verbs a script uses. `stand`, `balance`, `walk` and `damp` show what they are about to do, with the robot's facts, and ask before they send anything. The facts are for you to judge: no command refuses because of them, and the firmware decides what a command does. The one refusal is no live state. `balance` on a robot already in MOVE sends at once without asking. Every command is sent to the robot; none is an emergency stop. ```bash menlo --robot NAME --mode {udp,hybrid,livekit} menlo --version menlo -h ``` `--robot` picks a saved robot, `--mode` overrides its connection mode for this command. Without them, the command uses `MENLO_ROBOT`, then the default saved robot, then the only saved robot. Setup [#setup] ```bash menlo setup ``` The wizard asks for a name, a connection mode, and only the fields that mode needs: the robot's address for `udp`, the Asimov Manager URL and an SDK credential for `livekit`, both for `hybrid`. The credential is typed masked and never printed back. Each field is checked live before it is saved: | Check | What passes | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Credential** | `accepted, role control, room robot-...`; a credential with the Observe role saves with a warning: it can watch, not drive over livekit | | **udp** | state arrives from the address; the message names the robot mode it saw | | **livekit** | the room is joined; the message lists the tracks it found | A failed check asks "Save anyway?". When another robot is already the default, the wizard asks whether this one should be. `--no-check` skips the live checks. The wizard needs a terminal; without one it exits 2. SDK credentials come from the Developer page in Asimov Manager; [Drive from Python](/asimov/1/operate/drive/python-sdk#get-an-sdk-credential) shows the steps. Saved robots live in `~/.menlo/robots.toml`, readable only by you. Robots [#robots] ```bash menlo robots # table: default mark, name, mode, address, manager, room menlo robots add lab --mode hybrid --udp 192.168.1.20 --manager https://manager.example --credential ... menlo robots add lab --limits 0.3,0.3,0.6 menlo robots use lab menlo robots remove lab ``` `add` on an existing name keeps the saved values it is not given, so the second `add` above only sets limits. Flags it needs and does not have are asked for in a terminal; with `--no-input`, missing flags exit 2. `--default` makes the robot the default; `--no-check` skips the live check. With every flag it needs, or with `--no-input`, a failed check saves nothing and exits 1; in the wizard, you can choose to save despite a failed check. `use` and `remove` exit 1 on a name that is not saved. Status [#status] ```bash menlo status menlo status --watch ``` Connects, reads state for a second, and prints a panel: the robot, its connection mode and address; a badge, NOT READY when there is no live state, FAULTED when the firmware latched DAMP, READY otherwise, with the reason; the robot mode and whether it is armed; battery; the hottest joint; faults; alerts; the state rate; and whether the state is fresh. It sends nothing. `--watch` refreshes until `q` or Ctrl-C. Stand, Balance, Walk, Damp [#stand-balance-walk-damp] ```bash menlo stand menlo balance menlo walk --vx 0.2 --duration 3 menlo walk --vyaw 0.3 --duration 2 menlo damp ``` | Command | What it does | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `stand` | from any robot mode. A robot already in STAND is sent nothing; if it has not armed, `menlo stand` waits up to 10 s for it to arm and exits 3 when it does not. From MOVE it first warns to hang the robot from its gantry hook or seat it on a stool or bench: STAND has no balance loop. Asks, sends `stand()` and waits up to 10 s for the robot to report STAND and arm, then prints `Armed: ready to balance, menlo balance`. Exit 3 when it does not arm | | `balance` | outside MOVE, asks, sends zero velocity and waits up to 5 s for the robot to report MOVE, then prints `Balancing in MOVE: ready to walk, e.g. menlo walk --vx 0.3 --duration 3`. From DAMP or an unarmed STAND it exits 3 saying why. In MOVE, never asks: sends zero velocity twice, 0.1 s apart, and the robot stays in MOVE, balancing in place. A script that holds a velocity re-sends it at 10 Hz and must itself be stopped | | `walk` | `--duration` is required, above 0 and at most 10 s; at least one of `--vx`, `--vy`, `--vyaw` must be nonzero. Asks, holds the velocity for the duration, then sends zero velocity: the robot stays in MOVE, balancing in place. The firmware walks in MOVE only: a walk that leaves the robot outside MOVE exits 3 with `Run menlo balance first`. Ctrl-C sends zero velocity and exits 130. A fault that ends the walk early is named, with a note that it latches until the firmware restarts, and exits 3 | | `damp` | asks, sends `damp()` and waits up to 10 s for the robot to report DAMP, then prints `Robot mode DAMP.`, or the fault that holds it. Hang the robot from its gantry hook or seat it on a stool or bench first | DAMP makes every actuator compliant: a standing robot folds to the ground, so hang it from its gantry hook or seat it on a stool or bench first. In an emergency use the **E-Stop** in Asimov Manager, or cut power at the battery unit; see [Stopping the Robot](/asimov/1/operate/safety/stopping). Confirm Before Sending [#confirm-before-sending] `stand`, `balance` outside MOVE, `walk` and `damp` print one line: the robot, its connection mode and address, the robot's facts (robot mode, armed, faults, active alerts, the hottest joint, battery), and what will happen. Then they ask `Proceed? [y/N]`. ```text $ menlo walk --vx 0.2 --duration 3 lab (hybrid, 192.168.22.32) · MOVE · armed · faults none · alerts none · hottest joint 41 C (L_Knee) · battery 82 % → walk vx 0.20 m/s, vy 0.00 m/s, vyaw 0.00 rad/s for 3.0 s, then balance in place Proceed? [y/N] y Walking. Ctrl-C ends the walk. Walk done. Robot mode MOVE, balancing in place. ``` ```text $ menlo stand lab (udp, 192.168.22.32) · DAMP · faults none · alerts none · hottest joint 38 C (L_Knee) · battery 82 % → stand, then wait until armed Proceed? [y/N] y Standing. Waiting for the robot to arm (0.5 s upright)... Armed: ready to balance, menlo balance ``` ```text $ menlo balance lab (udp, 192.168.22.32) · STAND · armed · faults none · alerts none · hottest joint 41 C (L_Knee) · battery 82 % → balance: MOVE at zero velocity, the walking policy balances the robot Proceed? [y/N] y Balancing in MOVE: ready to walk, e.g. menlo walk --vx 0.3 --duration 3 ``` ```text $ menlo damp lab (udp, 192.168.22.32) · MOVE · armed · faults none · alerts none · hottest joint 45 C (L_Knee) · battery 80 % → damp: every actuator stops holding its position and a standing robot falls, so the robot must be supported. Not an emergency stop: use the E-Stop in Asimov Manager, or cut power at the battery unit. Proceed? [y/N] ``` . Read the line and judge the facts: a fault, an alert, a hot joint or a low battery does not stop the command. A speed above the robot's velocity limits shows the value that is sent, then the one you asked for, for example `vx 0.40 m/s (asked 0.60)`. . Type `y` and press Enter to go ahead. Enter alone, or anything else, cancels: the command prints `Cancelled; nothing sent.` and exits 4. `-y` or `--yes` prints the line and goes ahead without asking. With no terminal to ask on and no `--yes`, the command sends nothing and exits 2. Not Feasible [#not-feasible] The one refusal is no live state. Before the plan, `stand`, `balance` and `walk` check for a fresh state sample. Without one they do not ask: they print `Not feasible:` on stderr, send nothing and exit 3: ```text Not feasible: no fresh state from lab: the latest state is 1.2 s old (limit 0.5 s). Check the link with `menlo status`. ``` On `udp` or `hybrid`, a robot that never reports state is often a firewall; see [No State on udp or hybrid](/sdk/troubleshooting#no-state-on-udp-or-hybrid). A command that was sent and did not get there also exits 3, with what the robot reports: `balance` in DAMP (`the robot is in DAMP: stand() first`), `balance` on a robot that has not armed, `walk` that left the robot outside MOVE, a `stand` that does not arm within 10 s, and a fault during a command. For Scripts and Other Programs [#for-scripts-and-other-programs] `menlo robots --json` and `menlo status --json` print one JSON object and nothing else. `--yes` runs `stand`, `balance`, `walk` and `damp` without a question; branch on the exit code. A script or another program runs `menlo status --json` first, judges the facts itself, then runs `menlo balance --yes` and `menlo walk ... --yes`: ```bash menlo status --json | jq '.ready, .robot_mode, .faults, .alerts, .max_joint_temp_c' menlo walk --vx 0.2 --duration 3 --yes ``` `status` carries `robot`, `mode`, `host`, `verdict`, `ready` (there is live state), `problems` (each with `code`, `message`, `blocking`), `robot_mode`, `armed`, `battery_percent`, `faults`, `alerts`, `error_flags`, `max_joint_temp_c`, `state_rate_hz`, `state_age_s` and `fresh`. `robots` carries `default` and a `robots` list with `name`, `default`, `mode`, `udp_host`, `manager_url`, `room`, `credential_saved` and `limits`; the credential itself is never printed. Exit Codes [#exit-codes] | Code | Meaning | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **0** | done | | **1** | an error; the message is `menlo : ` on stderr | | **2** | usage: a missing flag, a bad value, a wizard without a terminal, or a question with no terminal and no `--yes` | | **3** | not feasible or not reached: no live state (`Not feasible:` is printed, nothing sent), or the command was sent and the robot did not get there (not armed in time, still in DAMP, a fault) | | **4** | cancelled: you answered no; nothing was sent | | **130** | interrupted with Ctrl-C | Related [#related] * [Connection Modes](/sdk/connection-modes): what each mode needs * [Safety](/sdk/safety#standing-and-balancing): the stand, balance, walk, damp sequence and why * [What the SDK Reports](/sdk/safety#what-the-sdk-reports): every code `preflight()` reports * [SDK Reference](/sdk/reference#store-and-cli): the store the CLI writes # Coding Agents A coding agent writes better scripts for Asimov when it knows how the SDK behaves: which command does what, what the robot must be doing first, and that guards are the script's job. Two things give it that: the **menlo-sdk skill**, a usage guide the agent loads when you ask it for robot code, and the docs as plain text in **llms.txt**. A script an agent writes moves the robot like any other script. Read it before you run it, and follow [Safety](/sdk/safety): support the robot, hanging from its gantry hook or seated on a stool or bench, and keep the E-Stop in Asimov Manager open, or be ready to cut power at the battery unit. Install the Skill [#install-the-skill] The skill is in the [skills/menlo-sdk](https://github.com/menloresearch/menlo-sdk/tree/main/skills/menlo-sdk) folder of the SDK repository, in the Agent Skills format, so most coding agents can load it. Claude Code [#claude-code] Add the repository as a plugin marketplace, then install the plugin: ```text /plugin marketplace add menloresearch/menlo-sdk /plugin install menlo-sdk@menlo ``` `/plugin` then lists `menlo-sdk@menlo`, and the skill is `menlo-sdk:menlo-sdk`. To get a newer version later, run `/plugin update menlo-sdk@menlo`. Codex, pi, oh-my-pi and Other Agents [#codex-pi-oh-my-pi-and-other-agents] Install it with the `skills` installer: ```bash npx skills add menloresearch/menlo-sdk ``` It copies the skill to `~/.agents/skills/menlo-sdk`, the folder Codex, pi and oh-my-pi read, and links it for the other agents it finds on your computer. Add `-g -y` to skip the questions. By Hand [#by-hand] Copy the `skills/menlo-sdk` folder from the repository into the skills folder your agent reads: `~/.agents/skills/` for Codex, pi and oh-my-pi, or `~/.claude/skills/` for Claude Code. Try It [#try-it] Ask your agent for a script, for example: ```text Using menlo-sdk, walk my Asimov forward at 0.3 m/s for 3 s and end balancing in place. ``` With the skill, the answer uses the SDK's own calls: ```python robot.set_velocity(vx=0.3, duration=3.0) # walks for 3 s, then sends zero velocity robot.balance() # stays in MOVE, balancing in place ``` What the Skill Teaches [#what-the-skill-teaches] * How to connect in each [connection mode](/sdk/connection-modes) and what each one carries. * The commands, what each waits for, and the errors they raise. * That the SDK does not refuse a command because of what the robot reports, so a script writes its own guard from `robot.get_state()`, as [guard.py](/sdk/safety#write-your-own-guard) does. * The order to bring the robot up and down: support it before MOVE to STAND and before DAMP. * Where the full documentation is. Give an Agent the Docs [#give-an-agent-the-docs] For an agent without the skill, or for questions the skill does not cover, point it at the docs as plain text: | File | What it holds | | ---------------------------------------------------- | -------------------------------------------------------------------- | | [llms.txt](https://docs.menlo.ai/llms.txt) | Every docs page, one line each; the SDK pages are under "Python SDK" | | [llms-full.txt](https://docs.menlo.ai/llms-full.txt) | The full text of every docs page in one file | Related [#related] * [Quickstart](/sdk/quickstart): the first walk, step by step * [Safety](/sdk/safety): what to check before a script moves the robot, and writing a guard * [Examples](/sdk/examples): the scripts the skill points to # Connection Modes The SDK reaches the robot in one of three connection modes. The `Robot` object and every verb are the same in all three; the mode decides what the connection carries and where it works from. | Mode | Control and state | Camera and audio | Works from | | --------- | -------------------------- | ---------------- | ------------------------------------ | | `udp` | UDP on the robot's network | none | the robot's network | | `hybrid` | UDP on the robot's network | a LiveKit room | the robot's network | | `livekit` | a LiveKit room | a LiveKit room | wherever Asimov Manager is reachable | A saved robot carries a mode, so `Robot().connect()` needs no argument. Pass one to override it for a session: `connect("udp")`. In Code [#in-code] `connect.py` builds the connection in code. Set `MODE` at the top of the file to `"udp"`, `"hybrid"` or `"livekit"`; the three `ConnectionConfig` shapes sit side by side: ```python lineNumbers=20 # udp: control and state over UDP on the robot's network. No camera or audio. udp = ConnectionConfig(udp=UdpConfig(host=ROBOT_ADDRESS)) # hybrid: control and state over UDP, camera and audio over LiveKit. hybrid = ConnectionConfig( udp=UdpConfig(host=ROBOT_ADDRESS), livekit=ManagerConfig(url=MANAGER_URL, credential=CREDENTIAL), ) # livekit: everything through the robot's LiveKit room, wherever Asimov Manager is reachable. livekit = ConnectionConfig(livekit=ManagerConfig(url=MANAGER_URL, credential=CREDENTIAL)) config = {"udp": udp, "hybrid": hybrid, "livekit": livekit}[MODE] with Robot(config).connect(MODE) as robot: state = robot.get_state() print(f"connected over {MODE} to {robot.info.endpoint}") print(f"robot mode {state.mode.name}, armed {robot.armed}") if state.battery is not None: print(f"battery {state.battery.soc_percent:.0f} %") else: print("battery not reported") capabilities = ("drive", "state", "battery", "camera", "microphone", "speaker") print("capabilities:", ", ".join(c for c in capabilities if robot.has(c))) ``` The other example scripts call `Robot()` with no config and read the environment or a saved robot instead; see [How the SDK Picks a Connection](#how-the-sdk-picks-a-connection). UDP [#udp] Commands go to the robot on UDP port 8850 and state comes back on port 8851. There is no media and no sign-in: anyone on the robot's network who can reach port 8850 can command it, so trust the network before you turn it on. The robot sends UDP state to one host. In Asimov Manager, set the Asimov Edge parameters `udp-control` to on and `udp-state-host` to your computer's address ([Asimov Manager](/asimov/1/operate/asimov-manager#overview)). One client per state port: a second script on the same computer binds `UdpConfig(state_bind=("0.0.0.0", ))` and the robot's `udp-state-port` matches it. Hybrid [#hybrid] Control and state travel as in udp, so a velocity command does not cross the internet. The camera, microphone and speaker travel over the robot's LiveKit room, which the SDK joins with an SDK credential from Asimov Manager. Use hybrid on the robot's network when a script needs the camera or sound. It needs the robot's address, its Asimov Manager URL and an SDK credential. Issue the credential on the **Developer** page of Asimov Manager ([Get an SDK Credential](/sdk/quickstart#get-an-sdk-credential)). The credential's role applies to the LiveKit room: a **Control** credential can talk through the speaker, an **Observe** credential watches and listens only. Commands in hybrid travel over UDP, which has no sign-in, so the role does not limit driving. LiveKit [#livekit] Everything, commands included, goes through the robot's LiveKit room. Commands travel on the data topic `commands` and state on the data track `state`, at about 10 Hz. Use livekit away from the robot's network, or when an agent framework joins the same room and the SDK is one more participant in it. It needs the Asimov Manager URL and an SDK credential ([Get an SDK Credential](/sdk/quickstart#get-an-sdk-credential)). The SDK asks Asimov Manager for a room token, joins, and reports `robot.info.endpoint` as `room@url as `. The credential's role applies: a **Control** credential drives, an **Observe** credential watches and listens, and the robot drops its commands. The Asimov Manager URL is the one you open in the browser: `http://192.168.22.32`, `http://asimov.local:8080` or a bare host name. The LiveKit URL Asimov Manager hands out is the one the robot itself uses; when it says `localhost`, the SDK substitutes the manager's host. A manager that redirects is reported as a `ConnectError`, not followed. Every SDK session joins the room as its own participant, `sdk---<6 hex>`. `ManagerConfig(label="...")` fixes the suffix so a restarted script replaces its previous session instead of joining beside it. To join with a token you minted yourself, pass `LiveKitConfig(url, room, token)` instead of `ManagerConfig`; one token is one participant. To use the room without the SDK at all, see [Without the SDK](/sdk/without-the-sdk). How the SDK Picks a Connection [#how-the-sdk-picks-a-connection] `Robot()` with no argument builds its connection from the environment or a saved robot, in this order: . Environment variables: `MENLO_UDP_HOST`, or `MENLO_MANAGER_URL` and `MENLO_CREDENTIAL`, or both pairs. They are not merged with a saved robot. . A saved robot from `~/.menlo/robots.toml`: the one named by `MENLO_ROBOT`, else the file's default, else the only one. . Otherwise `ConnectError`, naming `menlo setup` and the variables. `MENLO_MODE` sets the mode `connect()` uses with no argument and `MENLO_LIMITS` the velocity limits, whichever source supplied the connection. `MENLO_HOME` moves the store directory. A saved robot holds everything one mode needs, and can hold more than one mode's fields: ```toml title="~/.menlo/robots.toml" default = "lab" [robots.lab] mode = "hybrid" udp_host = "192.168.22.32" manager_url = "http://192.168.22.32" credential = "..." room = "robot-menlo-0001" [robots.lab.limits] vx = 0.3 ``` `udp` needs `udp_host`; `hybrid` needs `udp_host`, `manager_url` and `credential`; `livekit` needs `manager_url` and `credential`. A mode whose field is missing raises `ConnectError` naming the field and the `menlo robots add` command that fills it. Save and change robots with the [command line](/sdk/cli); the file is created with mode 0600 because it holds SDK credentials. What Connect Waits For [#what-connect-waits-for] `connect()` returns once the robot has reported state, so `robot.get_state()` is valid on the next line. It raises `ConnectError` after `timeout` (5 s by default) without state. On hybrid and livekit the media tracks are awaited for `media_timeout` (3 s); the capabilities the SDK claims are the tracks that arrived. `connect(require_state=False)` opens the room without waiting for the firmware: camera, microphone and speaker work at once. State unlocks when the first state frame arrives; a motion verb sent before then waits for it up to its `timeout`. A protocol version other than the one the SDK speaks raises `ProtocolMismatchError`. `allow_version_skew=True` connects anyway, and the robot may misread the commands the SDK sends. Update the SDK or the robot instead. After 2 s without state the SDK raises `LinkLostError`, sends zero velocity if it holds one, and every verb raises until you `close()` and `connect()` again. On `udp` and `hybrid`, the robot also zeroes a velocity 2 s after the last command it received. On `livekit`, Asimov Edge stops a held velocity when the SDK sends zero or leaves the room; see [Watchdogs](/sdk/safety#watchdogs). Related [#related] * [Quickstart](/sdk/quickstart): point the SDK at the robot and run the first scripts * [Command Line](/sdk/cli): `menlo setup` and `menlo robots` * [Drive from Python](/asimov/1/operate/drive/python-sdk): the SDK credential and the UDP settings, step by step * [Asimov Manager](/asimov/1/operate/asimov-manager): the robot's settings * [SDK Reference](/sdk/reference#connection): `ConnectionConfig`, `UdpConfig`, `ManagerConfig`, `LiveKitConfig` # Examples Each file is one short, runnable script, kept in the [examples](https://github.com/menloresearch/menlo-sdk/tree/main/examples) directory of the SDK repository. Every script except `connect.py` and the `livekit_raw/` scripts connects to the saved robot (`menlo setup`), or to the robot the `MENLO_*` environment variables describe; set `MENLO_ROBOT=NAME` to pick another saved robot. The settings a script uses, such as a speed, a duration or a joint name, are constants at the top of the file. Read the docstring at the top of a file before you run it: it says what the robot must be doing. Before a script that moves the robot, work through [Before You Run a Script](/sdk/safety#before-you-run-a-script). The SDK sends what a script asks and reports what the robot says; it refuses a command only when there is no live state. A guard is yours: the motion scripts call `guard(robot)` from `guard.py` before they send anything, and it stops them when the robot reports what your rule will not drive through. Edit its limits, or delete the call. Nothing here is an emergency stop: use the E-Stop in Asimov Manager, or cut power at the battery unit. ```bash pip install menlo-sdk git clone https://github.com/menloresearch/menlo-sdk.git cd menlo-sdk python examples/check.py ``` Check and Guard [#check-and-guard] | Script | What it shows | | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [check.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/check.py) | the robot's facts (robot mode, armed, faults, alerts, the hottest joint, battery, state age) and `preflight()` for stand, move and trajectory; sends nothing | | [guard.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/guard.py) | an optional guard of your own: joint temperature, battery, a latched fault and active alerts, with limits at the top of the file; prints what it finds and exits with status 1 when the rule fails. The motion scripts import it | Connect [#connect] | Script | What it shows | | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [connect.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/connect.py) | the `udp`, `hybrid` and `livekit` configs side by side in code; set `MODE` at the top and it prints the endpoint, robot mode, arming, battery and capabilities | State [#state] | Script | What it shows | | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | [read\_state.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/read_state.py) | a tour of `robot.get_state()`: mode, arming, faults, battery, the hottest actuators, gravity, alerts, state age and rate, and the mode and alert callbacks | Stand, Balance, Walk, Rest, Damp [#stand-balance-walk-rest-damp] Run them in this order, with the robot on its feet, hanging from its gantry hook. | Script | What it shows | | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [stand.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/stand.py) | `stand()` from any robot mode; it returns once the robot is armed, and the script prints the `NotReadyError` or `WaitTimeoutError` otherwise. From MOVE, hang the robot from its gantry hook or seat it on a stool or bench first | | [balance.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/balance.py) | `balance()` from an armed STAND: the robot enters MOVE and balances in place; the script prints the `NotReadyError` or `WaitTimeoutError` otherwise | | [walk.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/walk.py) | a bounded `set_velocity(duration=3.0)` in MOVE: the SDK holds the velocity, sends zero, and the robot balances in place; reports `sent.clamped`. Walking in MOVE only is the script's own rule | | [stream\_velocity.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/stream_velocity.py) | `set_velocity(hold=False)` from your own 50 Hz loop, one packet per tick, then `balance()`; MOVE only, the script's own rule | | [rest.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/rest.py) | MOVE to STAND to DAMP, after asking whether the robot is on its gantry hook or seated on a stool or bench; prints each robot mode | | [damp.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/damp.py) | `damp()` on a supported robot, after asking; it returns once the robot reports DAMP | Keyboard [#keyboard] | Script | What it shows | | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [keyboard.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/keyboard.py) | drive from the keyboard with short bounded holds: `w`/`s` walk, `a`/`d` strafe, `q`/`e` turn, space `balance()`, `t` stand (asks first in MOVE), `b` damp after asking, `x` quit | Joints [#joints] | Script | What it shows | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [move\_joints.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/move_joints.py) | `set_joints()` on one named joint, on a supported robot; exits unless `ROBOT_SUPPORTED = True` is set in the file; the robot enters DAMP 2 s after the script exits | | [wait\_until.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/wait_until.py) | `set_joints(wait=False)`, then `wait_until()` acts the moment the joint passes an angle in the robot's own report, on a supported robot; exits unless `ROBOT_SUPPORTED = True` | Media [#media] | Script | What it shows | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | [camera\_and\_audio.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/camera_and_audio.py) | a photo, a short clip saved as JPEG frames with its sound as WAV, and a tone on the speaker; `hybrid` or `livekit` | | [record\_audio.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/record_audio.py) | three seconds of the microphone saved as WAV | | [play\_audio.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/play_audio.py) | a WAV file played on the robot's speaker, one second at a time | Recording [#recording] | Script | What it shows | | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | [record\_and\_replay.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/record_and_replay.py) | `robot.record()` for three seconds, then reading the file back with `recording.load()` | Without the SDK [#without-the-sdk] The scripts in `livekit_raw/` use only the `livekit` and `asimov-protocol` packages; see [Without the SDK](/sdk/without-the-sdk). They read `MENLO_CREDENTIAL` and the `MANAGER_URL` set in `manager_token.py`. | Script | What it shows | | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | [livekit\_raw/manager\_token.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/livekit_raw/manager_token.py) | a join token from Asimov Manager; the other scripts in the folder import it | | [livekit\_raw/read\_state.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/livekit_raw/read_state.py) | `RobotState` from the `state` data track for 5 s | | [livekit\_raw/send\_commands.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/livekit_raw/send_commands.py) | its own guard by hand, then one STAND `RobotCommand` on the `commands` topic; the robot must hang from its gantry hook | | [livekit\_raw/camera.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/livekit_raw/camera.py) | one frame from the video track, saved as PPM | | [livekit\_raw/audio.py](https://github.com/menloresearch/menlo-sdk/blob/main/examples/livekit_raw/audio.py) | the microphone track, saved as WAV | Related [#related] * [Quickstart](/sdk/quickstart): check, stand, balance, walk and damp, step by step * [Safety](/sdk/safety#write-your-own-guard): writing your own guard, and bringing the robot to rest * [Command Line](/sdk/cli): the same verbs without writing a script # Overview `menlo-sdk` is the Python SDK for Menlo robots, and supports Asimov 1. It drives the robot with the robot's own verbs, `stand`, `set_velocity`, `damp` and `trajectory`, plus two the SDK adds: `balance` and `set_joints`. The package is `menlo`; the robot is the `menlo.asimov` subpackage. It is on [PyPI](https://pypi.org/project/menlo-sdk/) and its source is on [GitHub](https://github.com/menloresearch/menlo-sdk). Everything the SDK sends passes through the same arbiter and safety layer on the robot as the Cockpit and the gamepad; nothing in it bypasses the robot. For a first run, hang the robot from its gantry hook with both feet on the floor and give it 2 m of clear floor ahead. Nothing in the SDK is an emergency stop: keep the E-Stop in Asimov Manager open, or be ready to cut power at the battery unit. Read [Safety](/sdk/safety) first. ```python lineNumbers=24 guard(robot) # your rule, in guard.py; delete this line to walk without it mode = robot.get_state().mode if mode is not Mode.MOVE: # this script's own rule: walk from MOVE only print(f"robot mode {mode.name}: walk.py walks in MOVE only; run balance.py first") sys.exit(1) try: # 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: # no live state (nothing sent), or a fault ended the walk 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") ``` That is the walk from the [Quickstart](/sdk/quickstart). This guide has the detail behind every line of it; [Drive from Python](/asimov/1/operate/drive/python-sdk) gets you from `pip install` to the first walk, step by step, and the [Python SDK](/asimov/1/program/sdk) page is the overview. What It Does [#what-it-does] | You want to | The SDK gives you | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Walk, strafe, turn** | `set_velocity(vx, vy, vyaw)`, held for a duration or until `balance()`; the robot walks in MOVE | | **Change robot mode** | `stand()`, `balance()`, `damp()` | | **Move joints** | `set_joints(pose)` and `trajectory(setpoint)` with the walking policy off | | **Read the robot** | `robot.get_state()`: mode, joints, IMU, battery, alerts; waits that read it | | **Report the facts** | `preflight()` and `menlo status` report robot mode, arming, faults and alerts; a command refuses only without live state (`NotReadyError`), and the firmware decides the rest | | **Guard your script** | `guard.py`, an optional guard of your own with limits you edit, which the motion examples call before they send | | **See and hear** | camera frames, microphone chunks, a speaker to play into | | **Keep a record** | every state sample and every command as JSON lines | Connection Modes [#connection-modes] | Mode | Control and state | Camera and audio | Works from | | --------- | -------------------------- | ---------------- | ------------------------------------ | | `udp` | UDP on the robot's network | none | the robot's network | | `hybrid` | UDP on the robot's network | a LiveKit room | the robot's network | | `livekit` | a LiveKit room | a LiveKit room | wherever Asimov Manager is reachable | The `Robot` object is the same in all three. A saved robot remembers its mode; see [Connection Modes](/sdk/connection-modes). Before You Start [#before-you-start] The SDK drives a robot that is already assembled, powered and set up. These pages cover the parts it depends on: * [Start the robot](/asimov/1/operate/power#start-the-robot) and [turn it off](/asimov/1/operate/power#turn-the-robot-off) * [Asimov Manager](/asimov/1/operate/asimov-manager): SDK credentials and the robot's settings * [Stand Up and Walk](/asimov/1/operate/drive/stand-up): what the robot does in each robot mode * [Standing and Balancing](/sdk/safety#standing-and-balancing): the stand, balance, walk, damp sequence and why * [Stopping the Robot](/asimov/1/operate/safety/stopping): the E-Stop and cutting power. Nothing in the SDK is an emergency stop * [Before You Run a Script](/sdk/safety#before-you-run-a-script): the checklist for every script that moves the robot Pages [#pages] Requirements [#requirements] Python 3.12 or newer. `pip install menlo-sdk` installs `asimov-protocol`, `protobuf`, `livekit`, `questionary` and `rich`. numpy, Pillow and OpenCV are optional; a method that needs one names it in its `ImportError`. Related [#related] * [Drive from Python](/asimov/1/operate/drive/python-sdk): the hands-on path from a built robot to its first walk * [Python SDK](/asimov/1/program/sdk): the overview in the program section * [API](/asimov/1/program/api): the HTTP and WebSocket interface of Asimov Manager * [menlo-sdk on PyPI](https://pypi.org/project/menlo-sdk/) * [menlo-sdk on GitHub](https://github.com/menloresearch/menlo-sdk) # Control Joints Two verbs put the actuators under position control. `set_joints(positions)` moves every joint to a pose for you; `trajectory(positions)` sends one setpoint and expects you to clock the next. While a `set_joints()` or a `trajectory()` is in force, nothing balances the robot. Use them only with the robot supported, hanging from its gantry hook or seated on a stool or bench. A fall latches DAMP until the firmware restarts. Work through [Before You Run a Script](/sdk/safety#before-you-run-a-script) first. Move to a Pose [#move-to-a-pose] `set_joints` interpolates from the pose the robot last reported to the one you ask for, over `duration` seconds, and clocks the setpoints at `hz` from a thread. It returns once every joint is within `tolerance` radians of its target, then holds the target by re-sending it until another verb takes over. `move_joints.py` bends one elbow (`JOINT = "L_Elbow"`) and moves nothing else. Run `stand.py` first, keep the robot supported, and read what happens when the script exits before you run it. The script exits unless you set `ROBOT_SUPPORTED = True` at the top of the file, and calls `guard(robot)`, your own rule from `guard.py`, before it moves anything: ```python lineNumbers=28 guard(robot) # your rule, in guard.py; delete this line to move without it state = robot.get_state() # one sample: the pose to move from and back to start = state.joint_pos home = state.joint(JOINT).pos target = list(start) target[robot.info.joint_index(JOINT)] += TURN_RAD try: # Moves from the current pose over DURATION_S and returns within 0.05 rad of the # target. set_joints() holds it until the next verb. robot.set_joints(target, duration=DURATION_S) except (NotReadyError, WaitTimeoutError) as exc: # no live state, or the joints did not follow print(exc) sys.exit(1) print(f"{JOINT} moved from {home:.2f} to {robot.get_state().joint(JOINT).pos:.2f} rad") robot.set_joints(start, duration=DURATION_S) print(f"{JOINT} back at {robot.get_state().joint(JOINT).pos:.2f} rad") ``` `positions` is one radian value per actuator, `robot.info.dof` of them, in firmware order. `robot.info.joint_index(name)` gives the index of a named joint and `robot.info.joint_names` lists them all. `set_joints()` is sent in any robot mode; the firmware follows a trajectory in MOVE or in an armed STAND, and drops it in DAMP. It waits for live state, which also gives it a fresh pose to plan from; without live state it raises `NotReadyError` with nothing sent, and the script prints the message. In MOVE the robot is usually standing free on its own balance. A `set_joints()` or a `trajectory()` there turns the walking policy off, and a free-standing robot falls. The SDK does not stop it. Before joint control in any robot mode, hang the robot from its gantry hook or seat it on a stool or bench. `wait=False` returns once the plan is running. `timeout` bounds the whole call, the live-state check and the wait; left `None`, the check may wait 5 s and the target `duration + 2` s, and a joint that is not within `tolerance` by then raises `WaitTimeoutError`. The ankles are limited: 0.35 rad of pitch and 0.1 rad of roll. A target more than 0.02 rad past an ankle limit raises `ValueError` before anything is sent. Finish [#finish] When a trajectory is left alone, the robot enters DAMP 2 s after the last setpoint. That is what happens when `move_joints.py` exits: the held target stops being re-sent and every actuator stops holding its position, which is why the robot must be supported. To end joint control on your own terms, send `damp()` yourself, with the robot still supported; see [Shutdown](/sdk/safety#shutdown). Stream Your Own Setpoints [#stream-your-own-setpoints] `trajectory` is the raw form. Each call is one setpoint; you keep the clock, bound the loop, and end in DAMP: ```python import time deadline = time.monotonic() + 10.0 try: for target in controller: # one pose per tick, robot.info.dof values each if time.monotonic() > deadline: break robot.trajectory(target) # NotReadyError, nothing sent, on a stale stream time.sleep(0.02) # 50 Hz finally: robot.damp() # the robot is supported ``` Every `trajectory()` checks for live state once; on a live stream it reads one cached sample and adds no delay to the loop. The robot enters DAMP 2 s after the last setpoint, so a stalled loop leaves the robot in DAMP, not frozen in its last pose. `ValueError` when `len(positions) != robot.info.dof`, and for a target past an ankle limit. Gains [#gains] Without `kp` and `kd`, the robot applies its own per-joint gain table. Pass both to override them, one value per joint; a lone `kp` or `kd` is a `ValueError`. A gain of 0 is not "no gain": the firmware substitutes its damping constants, so the joint does not hold a position. Which Command Is in Effect [#which-command-is-in-effect] The firmware obeys whichever command arrived last. A `set_joints()` yields to any other verb: `damp()`, `stand()`, `balance()`, a velocity or a `trajectory()` from any thread ends it before its next setpoint leaves. A loop you clock with `trajectory()` does not. While it streams, a verb sent from another thread can be overwritten by your next setpoint, and nothing raises. Stop your loop, then send the verb. Wait for a Joint [#wait-for-a-joint] `wait_until.py` starts a `set_joints(wait=False)` and acts once the elbow has bent part of the way, with `robot.wait_until` reading the robot's own report. Like `move_joints.py`, it needs the robot supported and `ROBOT_SUPPORTED = True`, and calls `guard(robot)` first: ```python lineNumbers=33 guard(robot) # your rule, in guard.py; delete this line to move without it state = robot.get_state() # one sample: the pose to move from and back to start, home = state.joint_pos, state.joint(JOINT).pos target = list(start) target[robot.info.joint_index(JOINT)] += TURN_RAD try: robot.set_joints(target, duration=DURATION_S, wait=False) # returns at once, moving began = time.monotonic() passed = robot.wait_until(lambda s: s.joint(JOINT).pos >= home + PASS_RAD, timeout=5.0) except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or never got there print(exc) last = getattr(exc, "last", None) # the last state a wait saw, when there was one if last is not None and last.mode is Mode.DAMP: print("robot mode DAMP: the joints do not follow in DAMP, run stand.py first") sys.exit(1) took = time.monotonic() - began print(f"{JOINT} at {passed.joint(JOINT).pos:.2f} rad after {took:.1f} s of {DURATION_S} s") # A new set_joints() ends the first move where it is: back, and wait until there. robot.set_joints(start, duration=DURATION_S) print(f"{JOINT} back at {robot.get_state().joint(JOINT).pos:.2f} rad") ``` Related [#related] * [Safety](/sdk/safety): faults, watchdogs, and what a fall latches * [Read State](/sdk/state): `state.joints`, `state.joint(name)`, `info.joint_names` * [SDK Reference](/sdk/reference#set_joints): `set_joints` and `trajectory` signatures # Camera and Audio The robot publishes its camera and microphone as tracks in its LiveKit room and plays an audio track you publish. The SDK carries them in the `hybrid` and `livekit` connection modes; `udp` carries drive and state only. One `pip install menlo-sdk` covers all of it. Ask First [#ask-first] Capabilities are what this connection carries, so test rather than assume: ```python if robot.has("camera"): frame = robot.camera.photo() robot.require("drive", "camera") # UnsupportedError when one is missing robot.info.capabilities # frozenset({'drive', 'state'}) on udp ``` The full set is `drive`, `state`, `battery`, `camera`, `microphone` and `speaker`. A capability is claimed from a track that arrived within `media_timeout` (3 s) of connecting. A room that publishes no video makes `robot.has("camera")` `False` and `robot.camera` raise `UnsupportedError`, rather than hand out a stream that never yields. A Photo, a Clip and a Tone [#a-photo-a-clip-and-a-tone] `camera_and_audio.py` saves a photo and a short clip with sound, then plays a tone on the speaker. It moves nothing and needs the `hybrid` or `livekit` connection mode: ```python lineNumbers=26 if robot.has("camera"): photo = robot.camera.photo() # the next fresh frame, rgb8 Path(PHOTO).write_bytes(photo.to_jpeg()) print(f"saved {PHOTO}, {photo.width}x{photo.height}") clip = robot.camera.capture_clip(CLIP_S, audio=robot.has("microphone")) frames = clip.save_frames(CLIP_DIR) print(f"saved {len(frames)} frames to {CLIP_DIR}/ at {clip.fps:.1f} fps") if clip.audio: print("saved", clip.save_wav(CLIP_WAV)) else: print("this connection carries no camera; use hybrid or livekit") if robot.has("speaker"): # 16-bit mono PCM: a sine wave at TONE_HZ for TONE_S. tone = b"".join( struct.pack(" `photo()` returns the next fresh `Frame`, rgb8, and raises `WaitTimeoutError` when the camera is quiet for `timeout` (5 s). `frame.to_jpeg(quality=85)` needs Pillow and `frame.to_numpy()` needs numpy; each names the package in its `ImportError`. `capture_clip(seconds, audio=True)` returns a `Clip` with `frames`, `audio`, `duration_s` and `fps`, and `save_wav`, `save_frames` (Pillow) and `save_mp4` (OpenCV) to write it out. `play_pcm(pcm_s16le, sample_rate_hz=16000, channels=1)` takes raw 16-bit samples; `play(chunk)` takes an `AudioChunk` as the microphone yields them. A Stream [#a-stream] ```python robot.camera.latest() # newest Frame or None; never blocks for frame in robot.camera.frames(timeout=5.0): # latest-wins iterator ... robot.camera.subscribe(on_frame) # one call per Frame, on the transport thread for chunk in robot.microphone.chunks(): # pcm_s16le AudioChunks, in order chunk.data robot.microphone.dropped # chunks a slow consumer lost ``` `Frame.age_s` says how old a frame is. A control loop that steers from the camera should treat an old frame as no frame, send each velocity with a short `duration` so a stalled loop ends the walk, and call `balance()` whenever it loses its target. Such a loop drives the robot: it needs a robot balancing in MOVE with clear floor around it, and the [checklist](/sdk/safety#before-you-run-a-script) before it runs. Record the Microphone [#record-the-microphone] `record_audio.py` reads the microphone for 3 s and writes `microphone.wav`. It moves nothing: ```python lineNumbers=18 if not robot.has("microphone"): print("this connection carries no microphone; use hybrid or livekit") sys.exit(1) # Every chunk in order, as 16-bit PCM. chunks() raises WaitTimeoutError when the # microphone says nothing for its timeout. chunks: list[AudioChunk] = [] recorded_s = 0.0 for chunk in robot.microphone.chunks(timeout=5.0): chunks.append(chunk) recorded_s += chunk.duration_s if recorded_s >= SECONDS: break with wave.open(PATH, "wb") as wav: wav.setnchannels(chunks[0].channels) wav.setsampwidth(2) # 16-bit samples wav.setframerate(chunks[0].sample_rate_hz) wav.writeframes(b"".join(c.data for c in chunks)) print(f"saved {PATH}, {recorded_s:.1f} s at {chunks[0].sample_rate_hz} Hz") ``` `chunks()` yields `AudioChunk`s in order, 16-bit PCM, each with `data`, `sample_rate_hz`, `channels` and `duration_s`; it raises `WaitTimeoutError` when the microphone is quiet for `timeout` (5 s). A consumer that falls behind loses chunks, counted in `microphone.dropped`. Play a Sound [#play-a-sound] `play_audio.py` plays a WAV file, `hello.wav` in the directory you run it from, on the robot's speaker, one second at a time. With no `hello.wav` there, it plays a one-second 440 Hz tone: ```python lineNumbers=24 if not robot.has("speaker"): print("this connection carries no speaker; use hybrid or livekit") sys.exit(1) if os.path.exists(PATH): with wave.open(PATH, "rb") as wav: if wav.getsampwidth() != 2: print(f"{PATH} is not 16-bit PCM; convert it first") sys.exit(1) rate, channels = wav.getframerate(), wav.getnchannels() pcm = wav.readframes(wav.getnframes()) name = PATH else: # 16-bit mono PCM: one second of a 440 Hz sine wave rate, channels, name = 16_000, 1, f"a 440 Hz tone (no {PATH} here)" pcm = b"".join( struct.pack(" The file must be 16-bit PCM; `play_pcm` takes its samples with their sample rate and channel count and returns once they are handed to the room. Media Before the Firmware [#media-before-the-firmware] `connect(require_state=False)` opens the room without waiting for the robot to report state. Camera, microphone and speaker work whether or not the firmware is running; `robot.get_state()` and `damp()` raise `NotConnectedError` until the first state frame arrives. `stand()`, `balance()`, `set_velocity()`, `set_joints()` and `trajectory()` wait up to their `timeout` for it, then raise `NotReadyError` with nothing sent. Agents in the Same Room [#agents-in-the-same-room] A voice or vision agent framework subscribes to the robot's tracks the way it subscribes to a person's webcam, and the SDK joins the same room as one more participant to do the driving, on the `livekit` connection mode. Give the agent a walk bounded by a `duration` and a `balance()` as tools, and keep the same [checklist](/sdk/safety#before-you-run-a-script) for the agent as for a script. Related [#related] * [Connection Modes](/sdk/connection-modes): hybrid and livekit, and the SDK credential they need * [Without the SDK](/sdk/without-the-sdk): the same tracks with the `livekit` package alone * [Examples](/sdk/examples#media): every media script * [Safety](/sdk/safety): what to check before a camera-driven loop moves the robot * [SDK Reference](/sdk/reference#media): `Frame`, `AudioChunk`, `Clip` # Move the Robot 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 mode | What the robot does | Verb that reaches it | | ---------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------- | | **DAMP** | every actuator compliant; the robot rests on a stool or bench, hangs from its gantry hook, or folds to the ground | `damp()` | | **STAND** | joints stiffened into the standing pose; no balance loop | `stand()` | | **MOVE** | the walking policy balances and walks | `balance()` from an armed STAND | Every verb is sent in any robot mode, and the firmware decides what it does. The firmware enters 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. The SDK refuses a command only without live state; a rule about the robot mode, a fault or a hot actuator is your script's, as in [Write Your Own Guard](/sdk/safety#write-your-own-guard). 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](/sdk/safety#before-you-run-a-script) and [Standing and Balancing](/sdk/safety#standing-and-balancing) first. Stand Up [#stand-up] `stand()` puts the robot in STAND: the actuators hold a standing pose by position control alone, without a balance loop. 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](/asimov/1/operate/drive/stand-up). `stand()` is also sent from MOVE: hang the robot from its gantry hook or seat it on a stool or bench first, or it tips over; see [Bringing the Robot to Rest](/sdk/safety#bringing-the-robot-to-rest). ```python lineNumbers=19 guard(robot) # your rule, in guard.py; delete this line to stand without it try: # Returns once the robot reports STAND and has been upright for 0.5 s (armed): the # firmware enters MOVE only after that. robot.stand() except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or not armed print(exc) # what the robot reports and what to do sys.exit(1) print(f"robot mode {robot.get_state().mode.name}, armed {robot.armed}") ``` `stand.py` calls `guard(robot)` first, your own rule from `guard.py`; delete the line to stand without it. `stand()` waits for live state, then sends STAND; without live state it raises `NotReadyError` with nothing sent. Once sent, it 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] `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. ```python lineNumbers=18 guard(robot) # your rule, in guard.py; delete this line to balance without it try: robot.balance() # returns once the robot reports MOVE except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or not MOVE print(exc) # what the robot reports and what to do sys.exit(1) print(f"robot mode {robot.get_state().mode.name}, balancing in place") ``` Outside MOVE, `balance()` sends zero velocity as soon as there is live state, which is what puts an armed robot in MOVE, and returns once the robot reports MOVE; `WaitTimeoutError` when it does not within `timeout` (5 s by default), saying so when the robot was not armed. From DAMP it raises `WaitTimeoutError` at once: `the robot is in DAMP: stand() first`. In MOVE, `balance()` is sent at once: it ends any held velocity and sends zero, so it is also how a walk ends. The firmware reports STAND at once but enters 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. `stand()` returns only once the robot is armed, so `stand()` then `balance()` enters MOVE. `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. Walk [#walk] `set_velocity()` is sent in any robot mode: in MOVE the robot walks, an armed STAND enters MOVE, and in DAMP the robot drops it. Without live state it raises `NotReadyError` with nothing sent. `walk.py` keeps a rule of its own: it walks in MOVE only, and exits naming `balance.py` otherwise. ```python lineNumbers=24 guard(robot) # your rule, in guard.py; delete this line to walk without it mode = robot.get_state().mode if mode is not Mode.MOVE: # this script's own rule: walk from MOVE only print(f"robot mode {mode.name}: walk.py walks in MOVE only; run balance.py first") sys.exit(1) try: # 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: # no live state (nothing sent), or a fault ended the walk 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 [#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 when | What is sent | | --------------------------------------------------- | ---------------------------------------------------------------- | | `duration` seconds pass | zero 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` block | zero 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 restarts | nothing; 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 live-state check runs once per call, so a live stream 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](/sdk/safety#watchdogs). Drive from the Keyboard [#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 MOVE it asks first whether the robot is on its gantry hook or seated on a stool or bench), `b` damps after asking, and `x` or Ctrl-C quits with zero velocity and `close()`. Every key is sent in any robot mode, and the status line shows what the robot reports. `guard(robot)` runs before the first key. 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. ```python lineNumbers=181 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, next_key) elif key == "b": last = confirm_and_damp(robot, next_key) ``` End a Walk [#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 a free-standing robot falls; the SDK sends it as asked, so support the robot first. To finish a session in DAMP, support the robot first, hanging from its gantry hook or seated on a stool or bench, then run `rest.py` (MOVE, STAND, then DAMP, asking first) or `damp.py`; see [Bringing the Robot to Rest](/sdk/safety#bringing-the-robot-to-rest). Turn by Heading [#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: ```python 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) # NotReadyError only without live state 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 [#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: ```python 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`. Related [#related] * [Safety](/sdk/safety#standing-and-balancing): the stand, balance, walk, damp sequence and why * [Read State](/sdk/state): `robot.armed`, `wait_until`, faults * [Stand Up and Walk](/asimov/1/operate/drive/stand-up): the same robot modes from the Cockpit * [SDK Reference](/sdk/reference#set_velocity): `set_velocity`, `balance`, `stand`, `damp`, `Limits` * [Troubleshooting](/sdk/troubleshooting): `NotReadyError`, `WaitTimeoutError` and `RobotFaultedError` # Quickstart 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. 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](/sdk/safety#before-you-run-a-script) and [Standing and Balancing](/sdk/safety#standing-and-balancing) first. Install [#install] Python 3.12 or newer. ```bash 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 [#get-the-examples] The scripts on this page are in the `examples` directory of the [menlo-sdk repository on GitHub](https://github.com/menloresearch/menlo-sdk/tree/main/examples). Clone it and run them from there: ```bash 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 [#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. The Developer page's connection details: Manager URL, LiveKit URL and Room 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. The Issue a credential form: a name, and the Observe and Control choice 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. A newly issued credential, shown once with its copy button 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. Issued credentials, each with its role, ID, issue time and a Revoke button Save the Robot [#save-the-robot] ```bash 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=` points the SDK at the robot for one shell. To build the connection in code, see [Connection Modes](/sdk/connection-modes). Check [#check] `check.py` connects, prints what the robot reports (robot mode, armed, faults, alerts, the hottest joint, battery and the state's age) and `preflight()` for a stand, a walk and a trajectory, and sends nothing. Run it before anything that moves the robot: ```python lineNumbers=16 # The SDK counts the 0.5 s a robot in STAND must be upright to arm from its own samples. time.sleep(0.6) s = robot.get_state() # one sample: every fact below comes from it print(f"robot mode {s.mode.name}, armed {robot.armed}, faulted {s.faulted}") print(f"alerts {', '.join(a.name for a in s.alerts) or 'none'}") temps = [j.temp for j in s.joints if j.temp is not None] print(f"hottest joint {max(temps):.0f} C" if temps else "joint temperatures not reported") print(f"battery {s.battery.soc_percent:.0f} %" if s.battery else "battery not reported") print(f"state {s.age_s:.2f} s old") for action in ("stand", "move", "trajectory"): print(robot.preflight(action)) # "ready to move" means live state, and the facts ``` ```bash python check.py ``` The SDK does not refuse a command because of what the robot reports: a fault, an alert, a hot actuator or a low battery is yours to judge, and the firmware decides what a command does. The one refusal is no live state. The scripts below call `guard(robot)` from `guard.py` before they send anything: an optional guard of your own that stops the script when the robot is faulted, has a warning or critical alert, has an actuator at 60 °C or more, or has a battery below 20 %. Edit its limits, or delete the `guard(robot)` line to drive without it; see [Write Your Own Guard](/sdk/safety#write-your-own-guard). Stand [#stand] With the robot in DAMP, hanging from its gantry hook with both feet on the floor: ```python lineNumbers=19 guard(robot) # your rule, in guard.py; delete this line to stand without it try: # Returns once the robot reports STAND and has been upright for 0.5 s (armed): the # firmware enters MOVE only after that. robot.stand() except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or not armed print(exc) # what the robot reports and what to do sys.exit(1) print(f"robot mode {robot.get_state().mode.name}, armed {robot.armed}") ``` ```bash 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. Without live state, `stand()` raises `NotReadyError` with nothing sent, and the script prints why; when the robot does not arm within 10 s, it raises `WaitTimeoutError`. `stand()` is sent from any robot mode, but STAND has no balance loop: after a walk, hang the robot from its gantry hook or seat it on a stool or bench before you call it. Balance [#balance] With the robot armed and still on its gantry hook: ```python lineNumbers=18 guard(robot) # your rule, in guard.py; delete this line to balance without it try: robot.balance() # returns once the robot reports MOVE except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or not MOVE print(exc) # what the robot reports and what to do sys.exit(1) print(f"robot mode {robot.get_state().mode.name}, balancing in place") ``` ```bash 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 [#walk] With the robot balancing in MOVE: ```python lineNumbers=24 guard(robot) # your rule, in guard.py; delete this line to walk without it mode = robot.get_state().mode if mode is not Mode.MOVE: # this script's own rule: walk from MOVE only print(f"robot mode {mode.name}: walk.py walks in MOVE only; run balance.py first") sys.exit(1) try: # 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: # no live state (nothing sent), or a fault ended the walk 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") ``` ```bash python walk.py ``` What each line does: | Line | Effect | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `guard(robot)` | Your own rule from `guard.py`: exits when the robot reports what it will not drive through; delete the line to walk without it | | `if mode is not Mode.MOVE` | This script's own rule: it walks in MOVE only | | `set_velocity(vx=VX, vy=VY, vyaw=VYAW, duration=DURATION_S)` | 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 NotReadyError` | There was no live state and nothing was sent, or a fault ended the walk (a `RobotFaultedError` with `.sent` set); the message says what the robot reports and what to do | | `sent.clamped` | `True` 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 `robot mode STAND: walk.py walks in MOVE only; run balance.py first` and exits. That rule is the script's, not the SDK's: `set_velocity()` is sent in any robot mode. Put the Robot Down [#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 stool or bench. `rest.py` goes from MOVE to STAND to DAMP instead, and asks first; see [Bringing the Robot to Rest](/sdk/safety#bringing-the-robot-to-rest). ```python lineNumbers=17 # 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}") ``` ```bash 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](/asimov/1/operate/safety/stopping)). What Ready Means [#what-ready-means] Ready means live state. A command refuses only without it: it waits up to its `timeout` for a fresh sample (by default 10 s for `stand()`, 5 s for `balance()`, `set_velocity()` and `set_joints()`; `trajectory()` checks once), then raises `NotReadyError` with nothing sent. Every other fact is reported, not acted on: ```python from menlo.asimov import NotReadyError, Robot with Robot().connect() as robot: print(robot.preflight("move")) # the facts: blocking only without live state try: robot.set_velocity(vx=0.3, duration=3.0) except NotReadyError as e: print(e) # not ready to move: the latest state is 1.2 s old (limit 0.5 s) (stale_state); ... ``` `balance()` in MOVE and `damp()` are sent at once. The full picture is in [What the SDK Reports](/sdk/safety#what-the-sdk-reports). Next Steps [#next-steps] * [Move the Robot](/sdk/move): turning, holding a velocity, limits * [Safety](/sdk/safety): what the SDK does and does not do for you * [Examples](/sdk/examples): every script in the repository, including `keyboard.py` for driving from the keyboard * [Command Line](/sdk/cli): `menlo setup` to save a robot, `menlo status --watch` while you develop Related [#related] * [Drive from Python](/asimov/1/operate/drive/python-sdk): the same path with the robot's settings, video and sound * [Stand Up and Walk](/asimov/1/operate/drive/stand-up): the same sequence from the Cockpit * [menlo-sdk on PyPI](https://pypi.org/project/menlo-sdk/) * [menlo-sdk on GitHub](https://github.com/menloresearch/menlo-sdk) # Record a Session `robot.record(path)` writes every state sample the robot sent and every command the script sent to one file, one JSON object per line, in order. Nothing is sampled or averaged. `record_and_replay.py` records a few seconds of state, then reads the file back. It sends nothing to the robot: ```python lineNumbers=18 # Every state sample and every command sent while the block runs goes to PATH. with Robot().connect() as robot, robot.record(PATH) as recording: time.sleep(SECONDS) print(f"recorded {recording.samples} states and {recording.commands_written} commands") # Each line is one JSON object; "kind" says whether it is a state or a command. lines = list(load(PATH)) states = [line for line in lines if line["kind"] == "state"] print("robot modes seen:", dict(Counter(s["mode"] for s in states))) if len(states) > 1: rate = (len(states) - 1) / (states[-1]["t"] - states[0]["t"]) print(f"state rate {rate:.0f} Hz") ``` `record` is a context manager: recording stops when the block exits, including when it exits because of an exception, which is the run you want the file for. `recording.samples` and `recording.commands_written` count what went in. Read It Back [#read-it-back] `menlo.asimov.recording.load(path)` yields one dict per line. `kind` is `"state"` or `"sent"`, `t` is `time.monotonic()` when the line was written (subtract the first line's `t` for the time since the recording began), and a state line carries the robot mode as `mode` beside the fields of `State`. What It Is Good For [#what-it-is-good-for] | Use | Why | | ------------------- | ------------------------------------------------------------------------------------------------ | | **A bug report** | a fall or a robot that did not move is easier to read from the file than to describe | | **Comparing runs** | the same script on two robots, or before and after a change, gives two files with the same shape | | **Checking a loop** | the `t` of the `sent` lines shows whether a control loop held its rate | Related [#related] * [Read State](/sdk/state): the fields in a state line * [SDK Reference](/sdk/reference#recording): `record`, `Recording` and `load` # SDK Reference Everything here is importable from `menlo.asimov` unless a module is named. `menlo.__version__` is the distribution version. Units are metres per second, radians per second, radians, degrees Celsius. Where to Start [#where-to-start] | Do this | Use | | ------------------------------------ | ----------------------------------------------------------------------------------------------- | | say where the robot is | [`Connection`](#connection), [`Robot`](#robot) | | read the facts | [`preflight`, `armed`](#preflight) | | balance in place, walk, strafe, turn | [`balance`](#balance), [`set_velocity`](#set_velocity) | | stand up, or go compliant | [`stand`](#stand), [`damp`](#damp) | | pose a joint | [`set_joints`](#set_joints), [`trajectory`](#trajectory) | | wait for something | [`wait_until`](#wait_until); `stand`, `balance` and `damp` wait for their robot mode themselves | | read what it reports | [`State`](#state), [`get_state`](#properties-and-callbacks) | | check what it can do | [`has`, `require`](#properties-and-callbacks), [`RobotInfo`](#state) | | see what went out | [`Sent`](#sent), [`Outcomes`](#outcomes) | | pictures and sound | [`Media`](#media) | | record a run | [`record`](#recording) | | save a robot for next time | [`Store and CLI`](#store-and-cli) | | handle failure | [`Errors`](#errors) | Connection [#connection] ```python ConnectionConfig(udp=None, livekit=None, limits=None, mode=None, name=None, hints={}) ``` | Member | | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `udp: UdpConfig \| None` | the robot's address on the local network | | `livekit: ManagerConfig \| LiveKitConfig \| None` | the room, through Asimov Manager or directly | | `limits: Limits \| None` | velocity limits for this robot | | `mode: ConnectMode \| None` | the connection mode `connect()` uses when given none | | `name` | the saved robot this config came from, if any | | `available_modes() -> tuple[ConnectMode, ...]` | which of `"udp"`, `"hybrid"`, `"livekit"` this config can reach | | `default_mode() -> ConnectMode` | `mode` when set, else `only_mode()` | | `only_mode() -> ConnectMode` | the mode the set slots imply: `udp` alone is udp, `livekit` alone is livekit, both are `hybrid`; `ConnectError` with none set | | `ConnectionConfig.from_environment(environ=None, *, robot=None)` | from `MENLO_*`, else the saved robot | | `transport_for(mode, ...)` | builds the transport for a mode | | Config | | | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UdpConfig(host, command_port=8850, state_bind=("0.0.0.0", 8851), state_source=None)` | commands to `host:8850`, state on `state_bind`. `state_source` pins the one address state may come from; without it, samples that do not match this robot are dropped | | `ManagerConfig(url, credential, label=None, timeout=5.0)` | Asimov Manager mints the room and a join token per connect. `.host`; `.resolve()`; `.check() -> ManagerGrant(url, room, identity, role)` validates the credential without joining | | `LiveKitConfig(url, room, token)` | room details you hold; `token` is a string or a callable minting one per join. One token is one participant | | `default_label()` | the label the SDK uses for this host | The LiveKit identity is `sdk---<6 random hex>`; `robot.info.endpoint` reads `room@url as ` on `livekit` and `host:8850 + room@url as ` on `hybrid`. Two sessions with the same label evict each other. `Robot` [#robot] ```python Robot(source: ConnectionConfig | Transport | None = None, *, limits=None, link_timeout=2.0) ``` `None` resolves the robot from the environment, else the saved robot. `limits` here wins over `MENLO_LIMITS`, which wins over the saved robot's limits, which win over `Limits()`. `connect` [#connect] ```python connect(mode=None, *, timeout=5.0, media_timeout=3.0, connect_timeout=10.0, allow_version_skew=False, require_state=True, persist=None) -> Robot ``` Attach on `"udp"`, `"hybrid"` or `"livekit"`; `mode` may be left out when the config names one or can reach only one. Returns once the first `State` has arrived and its protocol version matches. Raises `ConnectError` before touching the network when the config lacks what the mode needs, naming the `menlo robots add` command that fixes it. `media_timeout` bounds the wait for camera, microphone and speaker tracks. `require_state=False` returns as soon as the connection is open. `persist=True`, or `MENLO_PERSIST=1`, saves the robot after a successful connect with a `ManagerConfig`. `close()` then `connect()` again to switch modes. `open(*, timeout=5.0, allow_version_skew=False, require_state=True)` does the same over a `Transport` passed to the constructor. `RuntimeError` on an open robot. Preflight [#preflight] ```python Action = Literal["stand", "move", "trajectory"] preflight(action: Action = "move") -> Preflight armed: bool | None ``` `preflight` is a report of the facts. It reads the latest state, sends nothing, never waits and never raises for a robot condition. `action` is `"stand"`, `"move"` (`balance()` and `set_velocity()`) or `"trajectory"` (`set_joints()` and `trajectory()`). Only the blocking codes, which all mean no live state, stop a command; see [Verbs](#verbs). `armed` is `True` in MOVE and in STAND once the robot has been upright (gravity z below -0.87) for 0.5 s since STAND began; `False` in DAMP or before; `None` with no state, closed, in UNKNOWN, or in STAND with no gravity. | `Preflight` member | | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `action` | the action checked | | `problems: tuple[Problem, ...]` | blocking first, then information | | `state: State \| None` | the sample the check read | | `armed: bool \| None` | | | `ok: bool` | no blocking problem: there is live state | | `blocking: tuple[Problem, ...]` | `not_connected`, `no_state` or `stale_state` | | `has(code) -> bool` | | | `str()` | `ready to move` or `not ready to move:`, followed by one ` - code: message` line per problem, information suffixed ` (information)` | | `explain()` | one line: `not ready to move: message (code); fix. ...`, blocking problems only; the message of the `NotReadyError` a command raises | `Problem(code, message, blocking)`; `str()` is `code: message`. Blocking: `not_connected`, `no_state`, `stale_state`. Information, which nothing in the SDK acts on: `faulted` (the alerts that latched DAMP, named), `alerts` (active firmware alerts, named) and `not_armed` (`move` from STAND before the arming hold). The codes and their meaning are in [What the SDK Reports](/sdk/safety#what-the-sdk-reports). State older than 0.5 s is stale; armed means gravity z below -0.87 for 0.5 s. The SDK holds no joint temperature or battery threshold: write your own rule, as in [Write Your Own Guard](/sdk/safety#write-your-own-guard). Verbs [#verbs] Each returns a `Sent`. The robot reports no per-command verdict: `Sent.outcome` stays `None` and `wait_outcome()` returns `Unknown` in every mode. Read the effect from `robot.get_state()`. Every verb is sent in any robot mode, and the firmware decides what it does. The robot mode, a latched fault, an alert, a joint temperature or the battery never stop one. The one refusal is no live state: `set_velocity`, `stand`, `balance` outside MOVE, `set_joints` and `trajectory` read one cached sample before they send, which adds no delay on a live stream. Without one (`no_state`, `stale_state`) they wait up to `timeout`, then raise `NotReadyError` with nothing sent. After a STAND sent moments ago whose report has not arrived, they wait up to 1 s for that report, then send anyway. A closed `Robot` raises `NotConnectedError`, a lost link `LinkLostError` and a protocol version mismatch `ProtocolMismatchError`, at once. `balance` in MOVE and `damp` are sent at once. A wait that sees a latched fault after the command was sent raises `RobotFaultedError` with `.sent` set. `set_velocity` [#set_velocity] ```python set_velocity(vx=0.0, vy=0.0, vyaw=0.0, *, duration=None, wait=None, hold=True, timeout=None) -> Sent ``` Walk. Sent in any robot mode: in MOVE the robot walks; in an armed STAND it enters MOVE; in DAMP the robot drops it. Without live state it waits up to `timeout` (5 s by default), then raises `NotReadyError` with nothing sent. Clamped to `limits`; re-sent at 10 Hz until `balance()`, another verb, `close()`, or `duration` seconds pass, when zero is sent. `wait` defaults to `True`: block until the hold has ended and its zero has gone out, or another verb took over; that needs a `duration`, else `ValueError`. `wait=False` returns once sent and keeps the velocity until the next command, or until `duration`. `hold=False` sends exactly one packet and holds nothing, for a loop that clocks its own commands; the live-state check is made once, as in `trajectory()`, and `duration` or `wait=True` with it is a `ValueError`. `ValueError` on a non-finite value or a non-positive duration. See [Move the Robot](/sdk/move). `balance` [#balance] ```python balance(*, timeout=5.0, wait=True) -> Sent ``` Zero velocity: the robot is in MOVE and the walking policy balances it in place. In MOVE it is sent at once; it ends any velocity hold and sends zero, so this is how a walk ends. Outside MOVE it sends zero velocity as soon as there is live state, without waiting for arming. From an armed STAND that puts the robot in MOVE; with `wait`, it returns once the robot reports MOVE, and raises `WaitTimeoutError` when the whole call passes `timeout`, saying so when the robot was not armed. From DAMP it raises `WaitTimeoutError` at once: "the robot is in DAMP: stand() first". `RobotFaultedError` when the wait sees a latched fault. Not an emergency stop. `stand` [#stand] ```python stand(*, timeout=10.0, wait=True) -> Sent ``` Put the robot in STAND: the actuators hold the standing pose, without a balance loop. Sent from any robot mode. From MOVE, hang the robot from its gantry hook or seat it on a stool or bench first: a free-standing robot tips over. Sends STAND once and, with `wait`, returns when the robot reports STAND and is armed; `WaitTimeoutError` when the whole call passes `timeout`, `RobotFaultedError` when a fault latches meanwhile. `wait=False` returns once sent. Ends a running `set_joints()` and a held velocity. `damp` [#damp] ```python damp(*, timeout=5.0, wait=True) -> Sent ``` Every actuator compliant now. A standing robot folds to the ground, so hang it from its gantry hook or seat it on a stool or bench first. Sent at once, whatever the robot's state. With `wait`, returns once the robot reports DAMP or FAULT\_DAMP, `WaitTimeoutError` after `timeout`. Not an emergency stop: in an emergency use the E-Stop in Asimov Manager, or cut power at the battery unit. `set_joints` [#set_joints] ```python set_joints(positions, *, duration=2.0, hz=50.0, kp=None, kd=None, wait=True, tolerance=0.05, timeout=None) -> Sent ``` Sent in any robot mode; the firmware follows a trajectory in MOVE or an armed STAND. Waits for live state first (a stale pose is waited out), then interpolates from the last reported pose to `positions` over `duration`, clocking `trajectory()` setpoints from a thread at `hz`, then holds the target until another verb. Any verb from any thread ends it before its next setpoint leaves. `wait=True` blocks until every joint is within `tolerance` rad, `WaitTimeoutError` otherwise. An ankle target more than 0.02 rad past the firmware's limits (0.35 rad pitch, 0.1 rad roll) is a `ValueError` before anything is sent. `timeout` bounds the whole call; left `None`, the live-state check may wait 5 s and the target `duration + 2` s. See [Control Joints](/sdk/joints). `trajectory` [#trajectory] ```python trajectory(positions, *, kp=None, kd=None, timeout=0.0) -> Sent ``` One setpoint, radians, firmware order; clock these yourself or use `set_joints`. Sent in any robot mode. Checks for live state once, with no delay on a live stream, so a loop at 50 Hz can call this. On a stale stream it raises `NotReadyError` at once and sends nothing; pass `timeout` to wait that long instead. `ValueError` unless `len(positions) == info.dof`, or when only one of `kp` and `kd` is given. A gain of 0 is not "no gain": the firmware substitutes its damping constants, so the joint does not hold a position. `None` applies the robot's per-joint gain table. The robot enters DAMP 2 s after the last setpoint. Waits [#waits] Every wait reads the robot's own report, checks for a fault before its predicate, and refuses to succeed on a stale stream. `stand()`, `balance()`, `damp()` and `set_joints(wait=True)` wait this way for their own effect. `wait_until` [#wait_until] ```python wait_until(predicate, *, timeout=10.0, stale_after=None, poll=0.05) -> State ``` Blocks until `predicate(state)` is true and returns that first `State`. Raises `WaitTimeoutError` (the message names the robot mode and the age of the last sample), `StateStaleError` when the stream goes quiet for `stale_after`, and `RobotFaultedError` when a fault latches. `stale_after` defaults to `link_timeout`. A wait for one robot mode is `wait_until(lambda s: s.mode is Mode.MOVE)`. Properties and Callbacks [#properties-and-callbacks] | Name | Type | Notes | | --------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | | `config` | `ConnectionConfig` | what this Robot was bound to | | `info` | `RobotInfo` | transport, endpoint, dof, joint names, protocol version, limits, capabilities | | `has(cap)`, `require(*caps)` | `bool`, raises `UnsupportedError` | this connection's set: `drive`, `state`, `battery`, `camera`, `microphone`, `speaker` | | `camera`, `microphone`, `speaker` | `Camera`, `Microphone`, `Speaker` | see Media; `UnsupportedError` when the connection does not carry them | | `record(path)` | `Recording` | context manager: every state sample and every command as JSON lines | | `get_state()` | `State` | the latest sample; `NotConnectedError` before the first | | `armed` | `bool \| None` | see Preflight | | `connected` | `bool` | open and not `LinkLostError` | | `outcomes()` | `Iterator[Refused]` | received refusals, oldest first; empty with this robot | | `on_state` | `Callable[[State], None] \| None` | every accepted sample, on the transport thread | | `on_alert` | `Callable[[Alert, str], None] \| None` | an alert `"raised"` or `"cleared"` | | `on_mode_change` | `Callable[[Mode, Mode], None] \| None` | before, after | | `on_refused` | `Callable[[Refused], None] \| None` | never fires with this robot | | `on_link_lost` | `Callable[[LinkLostError], None] \| None` | | | `close()` | | zero velocity if held, drop the link; idempotent; never raises | `Robot` is a context manager; `__exit__` calls `close()`. `Sent` [#sent] | Member | Type | Meaning | | ------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `name` | `str` | the verb: `set_velocity`, `balance`, `stand`, `damp`, `trajectory`; `set_joints()` sends its setpoints as `trajectory` | | `sequence` | `int` | the command's sequence number | | `command` | `Velocity \| ModeCommand \| Trajectory` | what went out, after clamping | | `clamped` | `bool` | `command` differs from what you asked | | `sent_at` | `float` | `time.monotonic()` | | `outcome` | `Applied \| Refused \| None` | non-blocking; `None` while pending | | `wait_outcome(timeout=None)` | `Applied \| Refused \| Unknown` | `None` uses the transport default | | `require(timeout=None, *, unknown_ok=True)` | as above | raises `CommandRefusedError`; with `unknown_ok=False` also `OutcomeUnknownError` | Outcomes [#outcomes] | Type | Fields | | --------- | ------------------------------------------------------------------------------------------ | | `Applied` | `sequence` | | `Refused` | `sequence`, `reason: Refusal`, `detail: str` (diagnostic text; never branch on it), `verb` | | `Unknown` | `sequence`, `waited_s`, `verb` | `Refusal` (IntEnum): `UNSPECIFIED`, `FW_DAMPED`, `FAULT_DAMPED`, `GATE`, `UNSUPPORTED_SOURCE`, `BAD_LENGTH`, `EMPTY_TRAJECTORY`, `NON_FINITE`, `PARSE`, `UNKNOWN_COMMAND`, `UNKNOWN_MODE`, `SHUTTING_DOWN`, `NO_CAMERA`, `NOT_ACTIVE`, `UNRECOGNIZED`; `retryable` property; `Refusal.from_wire()`. State [#state] ```python @dataclass(frozen=True) class State: mode: Mode # DAMP | STAND | MOVE | FAULT_DAMP | UNKNOWN joints: tuple[Joint, ...] # firmware order gravity: tuple[float, float, float] | None # body frame; z near -1 when upright gyro: tuple[float, float, float] | None # rad/s quat: tuple[float, float, float, float] | None # w, x, y, z error_flags: int alerts: tuple[Alert, ...] sequence: int fw_timestamp_us: int protocol_version: int battery: Battery | None # None when the robot reports no battery received_at: float edge_timestamp_us: int # Asimov Edge's wall clock (µs); livekit only, 0 on udp and hybrid # properties age_s: float upright: bool | None # gravity z < -0.8; None without gravity euler: tuple[float, float, float] | None # roll, pitch, yaw (rad) from quat yaw: float | None # rad, counter-clockwise, (-pi, pi] faulted: bool # FAULT_DAMP, error_flags or any critical alert joint_pos: tuple[float, ...] def joint(self, name: str) -> Joint # KeyError on an unknown name ``` `Joint(name, pos, vel, current, temp)`: `vel`, `current` and `temp` are `None` when the robot did not report them. `Alert(id, severity, value, threshold, source_id, first_set_us=0)` with `critical` (`severity == 0`) and `name`. `Battery(voltage_v, current_a, soc_percent, max_cell_temp_c, protection: BatteryProtection)` with `charging` and `protecting`; `BatteryProtection` is an `IntFlag`. `RobotInfo(transport, endpoint, dof, joint_names, protocol_version, limits, capabilities)` with `joint_index(name)`, `has(capability)` and a readable `str()`. `Limits(vx=0.4, vy=0.4, vyaw=0.8)`: the defaults are the firmware caps (0.4 m/s, 0.4 m/s, 0.8 rad/s). `Limits.parse("0.3,0.3,0.6")`. Negative or non-finite values raise `ValueError`. Media [#media] ```python robot.camera.photo(*, timeout=5.0) -> Frame # one fresh frame; WaitTimeoutError when quiet robot.camera.latest() -> Frame | None # newest frame; None before the first robot.camera.frames(*, timeout=5.0) # iterator, always the newest frame robot.camera.subscribe(cb) # cb(Frame) on the transport thread robot.camera.stream(*, timeout=5.0) # every frame, in order robot.camera.capture_clip(seconds, audio=True) -> Clip robot.microphone.chunks(*, timeout=5.0) # every AudioChunk in order; .dropped counts loss robot.speaker.play(chunk) robot.speaker.play_pcm(pcm_s16le, sample_rate_hz=16000, channels=1) ``` `Frame(width, height, encoding, data, stride_bytes, key_frame, frame_id, timestamp_ns, sequence)` with `shape`, `age_s`, `to_numpy()`, `to_jpeg(quality=85)`. `AudioChunk(sample_rate_hz, channels, samples_per_channel, encoding, data, stream_id, timestamp_ns, sequence)` with `duration_s`, `to_numpy()`. `Clip(frames, audio, started_at)` with `duration_s`, `fps`, `save_wav(path) -> Path`, `save_frames(dir)`, `save_mp4(path, *, fps=None) -> Path`, `frames_as_numpy()`. numpy, Pillow and OpenCV are needed only at the call that uses them, and named in the `ImportError`. Recording [#recording] ```python with robot.record("run.jsonl") as rec: ... rec.samples, rec.commands_written from menlo.asimov import recording for line in recording.load("run.jsonl"): # dicts with kind "state" | "sent", t, mode ... ``` Store and CLI [#store-and-cli] `~/.menlo/robots.toml` (`MENLO_HOME` moves it), `0600` in a `0700` directory. | | | | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RobotStore()` | `get(name=None)`, `put(robot, *, default=None, allow_manager_change=False)`, `remove(name)`, `use(name)`, `names`, `save()`; `len()`, iteration and `in` | | `StoredRobot(name, manager_url=None, credential=None, room=None, mode=None, udp_host=None, limits=None)` | `resolved_mode()`, `missing(mode)`, `fix_command(mode)`, `manager(label=)`, `connection(label=)` | | `menlo.asimov.store.persist(config, room, *, name=None, mode=None)` | what `connect(persist=True)` calls: records the manager, credential, room, `udp_host`, `mode` and `limits`; `ValueError` without a `ManagerConfig` | | Command | | | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `menlo setup [--no-check]` | wizard: name, mode, that mode's fields, live checks | | `menlo robots [--json]` | the saved robots | | `menlo robots add NAME --mode --udp --manager --credential --limits VX,VY,VYAW [--default] [--no-check] [--no-input]` | add or update; unspecified values are kept | | `menlo robots remove NAME`, `menlo robots use NAME` | forget, make default | | `menlo status [--watch] [--json]` | readiness, robot mode, battery, hottest joint, faults, alerts, state rate | | `menlo stand [-y]`, `menlo balance [-y]`, `menlo walk --vx --vy --vyaw --duration S [-y]`, `menlo damp [-y]` | the verbs; each prints a plan with the robot's facts and asks `Proceed? [y/N]`, `-y`/`--yes` goes ahead without asking. `Not feasible:` only without live state; `balance` in MOVE sends at once without asking | | `menlo --robot NAME --mode MODE`, `menlo --version` | | Environment: `MENLO_ROBOT`, `MENLO_MODE`, `MENLO_UDP_HOST`, `MENLO_MANAGER_URL`, `MENLO_CREDENTIAL`, `MENLO_LIMITS`, `MENLO_HOME`, `MENLO_PERSIST`. `MENLO_MANAGER_URL` and `MENLO_CREDENTIAL` go together; one alone is a `ConnectError`. Environment wins over the saved robot and is not merged with it. See [Command Line](/sdk/cli). Transports [#transports] `UdpTransport(host, *, command_port, state_bind, state_source)`, `LiveKitTransport(url, room, *, token, media_timeout, connect_timeout)`, `HybridTransport(host, *, livekit_url, room, token, command_port, state_bind, ...)`. All implement `Transport`; a `ConnectionConfig` builds the right one for the mode. `LiveKitTransport.identity` and `.room` read back what the token claimed. Errors [#errors] ``` MenloError ConnectError ProtocolMismatchError(expected, observed) NotConnectedError LinkLostError NotReadyError(message, *, action, preflight) # .problems, .has(code) RobotFaultedError(message, *, state, sent=None) # plus action, preflight WaitTimeoutError(message, *, last, sent=None) # also TimeoutError StateStaleError CommandRefusedError(refused) OutcomeUnknownError(unknown) # also TimeoutError UnsupportedError(capability, transport) ``` `NotReadyError` means there was no live state to send against (`no_state` or `stale_state`) within the command's `timeout`; nothing was sent. A closed `Robot` raises `NotConnectedError`, a lost link `LinkLostError`, a protocol version mismatch `ProtocolMismatchError`. It reads `not ready to : (); `, with ` (waited s)` appended when the command waited. `RobotFaultedError` is raised by a wait that saw a latched fault after the command went out; `sent` is set. `WaitTimeoutError.sent` is the command whose effect did not arrive. Caller mistakes are builtins: `ValueError` (non-finite velocity, limit or duration, wrong trajectory length, a lone kp or kd, an unknown connection mode), `KeyError` (unknown joint name), `RuntimeError` (`open()` on an open robot). A mode the config cannot reach raises `ConnectError`. Threading [#threading] `Robot` is synchronous and thread-safe. Callbacks run on the transport thread, the one that delivers state: keep them short. `robot.damp()` from a callback, such as `on_alert` damping on an alert, sends DAMP and returns at once. The other commands that wait for the robot, and `wait_until()`, raise `RuntimeError` there with nothing sent; pass `wait=False`, or call them from your own thread. Per-Robot Tables [#per-robot-tables] `menlo.asimov.robots.ASIMOV_1_BIPED_JOINTS` is the 25-actuator order of the Asimov 1 biped firmware profile, chosen by the reported joint count. `menlo.asimov.robots.PROTOCOL_VERSION` is the protocol version the SDK was built against; `connect()` compares it with what the robot echoes and raises `ProtocolMismatchError` on a difference. The Protocol [#the-protocol] Commands on udp go to port 8850 and state arrives on 8851, each datagram one bare `asimov.io.RobotCommand` or `RobotState`. On livekit, commands are data messages on the `commands` topic and state is the `state` data track. Every command carries `protocol_version = 1`, a sequence number and `timestamp_us`. UDP commands are not authenticated; the SDK credential and its role apply to the LiveKit room: every command on livekit, and the camera and audio on hybrid. [Without the SDK](/sdk/without-the-sdk) and the `livekit_raw/` examples speak it with the `livekit` and `asimov-protocol` packages alone. # Safety The SDK is a client of the robot. It sends what you ask, bounded by your limits, and reads back what the robot reports. Safety is the firmware's job, command handling is the robot's, and a guard is your script's: the SDK does not refuse a command because of what the robot reports. When in doubt, open Asimov Manager and press the E-Stop. Before You Run a Script [#before-you-run-a-script] . **Support the robot**, as [Supporting the Robot](/asimov/1/operate/safety#supporting-the-robot) describes: hanging from its gantry hook with both feet on the floor for a first stand and walk, hanging from its gantry hook or seated on a stool or bench for joint control, for STAND from MOVE and for DAMP. A walking robot needs 2 m of clear floor ahead of it. . **Clear the area.** Keep people, cables and furniture out of the robot's reach and keep the robot in sight. . **Have a way to stop the robot open.** Keep Asimov Manager open with its **E-Stop** in the page header or the phone top bar (it needs no drive session), and the battery unit within reach to cut power. [Stopping the Robot](/asimov/1/operate/safety/stopping) explains each. . **Make sure nothing else is driving.** Close any Cockpit session and unpair the gamepad. Both outrank a script, and a paired gamepad holds control even while idle. . **Check the robot.** `python check.py` or `menlo status` reports battery, actuator temperatures, faults, alerts and whether the state stream is fresh. Neither sends anything. Decide for yourself whether to go ahead, or let [your own guard](#write-your-own-guard) decide. . **Start slow.** Pass `Robot(limits=Limits(vx=0.2, vy=0.2, vyaw=0.4))` or save limits on the robot, and use short `duration` values until the script behaves. `balance()`, `damp()` and `close()` are commands sent over a network link. None of them cuts power, and none reaches a robot the link has lost. Keep the **E-Stop** open and the battery unit within reach whenever the robot stands. Standing and Balancing [#standing-and-balancing] The robot has three robot modes, and only one of them keeps it upright on its own. In **DAMP** no actuator holds a position. In **STAND** the actuators hold a standing pose and nothing balances it: a free-standing robot in STAND tips over. In **MOVE** the walking policy balances the robot, at zero velocity or any other. So the robot must be supported in DAMP and STAND, and stands free only once `balance()` has returned and it reports MOVE. This is the sequence for a script, and the order in which the [Quickstart](/sdk/quickstart) runs it as separate files: ```python import sys from menlo.asimov import NotReadyError, Robot, RobotFaultedError, WaitTimeoutError with Robot().connect() as robot: # 1. The robot hangs from its gantry hook with both feet on the floor. try: robot.stand() # DAMP -> STAND; returns once the robot is armed robot.balance() # armed STAND -> MOVE at zero velocity; returns once MOVE is reported except RobotFaultedError as exc: # latched until the firmware restarts sys.exit(f"faulted: {exc}") except NotReadyError as exc: # no live state: nothing was sent sys.exit(str(exc)) except WaitTimeoutError as exc: # sent, but the robot did not arm or reach MOVE in time sys.exit(str(exc)) # 2. Only now: slacken the gantry until the robot carries its own weight. input("Robot balancing in MOVE. Slacken the gantry, then press Enter to walk. ") # 3. Walk. Every set_velocity() ends with zero velocity; the robot stays in MOVE. robot.set_velocity(vx=0.2, duration=3.0) robot.balance() # zero velocity: balancing in place # 4. Support the robot again before it leaves MOVE. input("Take up the gantry's slack so it holds the robot, then press Enter to damp. ") robot.damp() # every actuator stops holding its position ``` Why each step is where it is: . **`stand()` on the gantry hook, both feet on the floor.** STAND holds a pose and does not balance, so the hook carries the robot. `stand()` sends STAND and returns once the robot reports STAND and has been upright for 0.5 s: the firmware enters MOVE only after that, so `stand()` returning means the robot is armed. It raises `NotReadyError` with nothing sent only when there is no live state. . **`balance()` before the support comes off.** From an armed STAND, `balance()` sends zero velocity, which is what puts the robot in MOVE, and returns once the robot reports MOVE. It does not wait for arming: after `stand()` the robot is already armed. MOVE at zero velocity is the only state in which the robot stands free, so the hook keeps carrying the robot until `balance()` has returned: until then the robot is in STAND, and a robot in STAND that loses its support tips over. . **Slacken the gantry only after `balance()` returns.** The SDK cannot see the gantry; the script asks you. Slacken it gradually until the robot carries its own weight, with 2 m of clear floor ahead. . **Walk, then `balance()`.** `set_velocity()` holds the velocity for its `duration` and ends with zero velocity. `balance()` in MOVE is the same zero velocity, sent at once: it ends a walk and the robot balances in place. . **Support the robot before `damp()`.** `damp()` is sent at once, and a standing robot falls when its actuators let go. Take up the gantry's slack, or seat the robot on a stool or bench, first. A script cannot see the support, so this one asks before it damps; to go through STAND first, as the robot's own controls do ([Bring It Back Down](/asimov/1/operate/drive/stand-up#bring-it-back-down)), see [Bringing the Robot to Rest](#bringing-the-robot-to-rest). Catch all three errors around `stand()` and `balance()`. A `RobotFaultedError` means the command was sent and a critical alert, a fall included, latched DAMP until the firmware restarts; a `NotReadyError` means there was no live state and nothing was sent; a `WaitTimeoutError` means the command was sent and the robot did not report the mode in time, so read `robot.get_state().mode` before you decide what to do next. Keep Asimov Manager open at the **E-Stop** throughout: nothing in the SDK is an emergency stop. Bringing the Robot to Rest [#bringing-the-robot-to-rest] The SDK sends every mode change you ask for, in any robot mode, and the firmware decides what it does. Two of them need the robot supported first, because the SDK cannot see the support: * **MOVE to STAND.** STAND has no balance loop: it stiffens the joints into a pose, and a free-standing robot asked to stand from MOVE tips over. `stand()` from MOVE is sent as asked, so hang the robot from its gantry hook or seat it on a stool or bench first. * **Anything to DAMP.** DAMP makes every actuator compliant, so a standing robot falls. Support the robot the same way before `damp()`. `rest.py` goes from MOVE to STAND to DAMP, the order the robot's own controls use. It asks first whether the robot is on its gantry hook or seated on a stool or bench, unless you set `YES = True`; sends STAND and waits until the robot reports it; then sends DAMP and waits until the robot reports that, printing each robot mode. A robot already in DAMP is left as it is: ```python lineNumbers=22 mode = robot.get_state().mode print(f"robot mode {mode.name}") if mode in (Mode.DAMP, Mode.FAULT_DAMP): print("already at rest; nothing sent") sys.exit(0) if not YES: question = "Is the robot on its gantry hook or seated on a stool or bench? [y/N] " if input(question).strip().lower() not in ("y", "yes"): print("nothing sent") sys.exit(0) try: # STAND first: the robot holds its pose, so DAMP does not drop it from a walk. robot.stand(wait=False) robot.wait_until(lambda s: s.mode is Mode.STAND, timeout=STAND_TIMEOUT_S) print(f"robot mode {robot.get_state().mode.name}") robot.damp() # returns once the robot reports DAMP except (NotReadyError, WaitTimeoutError) as exc: # no live state, a fault, or not reported print(exc) sys.exit(1) print(f"robot mode {robot.get_state().mode.name}") ``` `rest.py` is not an emergency stop: for that, use the E-Stop in Asimov Manager, or cut power at the battery unit. If Something Goes Wrong [#if-something-goes-wrong] . **Press the E-Stop in Asimov Manager, or cut power at the battery unit.** The E-Stop damps every actuator at once and a free-standing robot falls; use it when a robot lying on the floor is better than what is happening now. If the robot's software is not running, the E-Stop is disabled: cut power at the battery unit. Do not try to catch a falling robot. . **Stop the script.** Ctrl-C in a script that uses `with Robot().connect()` leaves the block, and `close()` sends zero velocity. A script that keeps running keeps re-sending its velocity at 10 Hz; `menlo balance` from another terminal does not end it. . **Read what the robot reports, and write it down.** `menlo status` shows the robot mode, a **FAULTED** badge and what latched, from the current alerts and the latched `error_flags`. In a script, `state.faulted` and the `faulted` line from `preflight()` name the causes, and the waits of `stand()`, `balance()`, `set_velocity()`, `set_joints()` and `wait_until()` raise `RobotFaultedError`. . **Recover the robot.** Follow [Recovering After an E-Stop or a Fall](/asimov/1/operate/safety/stopping#recovering-after-an-e-stop-or-a-fall): support and inspect the robot. Its **Turn Off Robot** step restarts the firmware, so do step 3 first. . **Restart the firmware, if the recovery did not.** A critical alert holds the robot in DAMP, and `error_flags` set, until the firmware restarts; restarting clears the record of what latched. Use **Restart** on the **Firmware (RPU)** row of the [Troubleshoot page](/asimov/1/operate/troubleshooting), or **Turn Off Robot** and **Start Robot** on Overview. Do not call `stand()` on a free-standing robot that is walking or falling. STAND has no balance loop: it stiffens the joints into a pose, and a free-standing robot in that pose tips over. End a walk with `balance()`, which leaves the robot in MOVE, balancing in place. Who Stops What [#who-stops-what] | Layer | Stops | Does not stop | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **SDK** | a held velocity: zero on `balance()`, `close()`, the end of a `with` block, a lost link | anything after a fault; another program's velocity | | **The robot** | velocity and trajectory in DAMP (dropped); a velocity 2 s after the last command on `udp` and `hybrid`; a trajectory 2 s after the last setpoint (DAMP) | a velocity a script keeps re-sending | | **Firmware** | MOVE before an armed STAND (the robot stays in STAND, nothing reports it); everything, on a critical alert (DAMP, latched) | a command because of where it came from: it obeys whichever arrived last; STAND from MOVE on a free-standing robot | The SDK refuses a command only when there is no live state. It also clamps every velocity to your `Limits`, runs every wait against fresh state, and fences a running `set_joints()` so that any verb ends it. `close()` sends zero velocity only when this `Robot` holds one; a `Robot` that sent nothing does not stop another program. After `close()` stops re-sending a trajectory, the robot enters DAMP 2 s later. Neither the robot nor the firmware checks command timestamps. Who Holds Control [#who-holds-control] The robot gives the Cockpit priority over a paired gamepad, the gamepad over `udp`, and `udp` over `livekit`. The state stream does not say who holds control, so a walk that does not happen, with no fault and no refusal, is the first thing to check. A gamepad paired to the robot holds control while nobody touches it. Every velocity a script sends is dropped without a report. Unpair the gamepad on the **Controller** page of Asimov Manager before you run a script; see [Gamepad](/asimov/1/operate/drive/gamepad#who-drives). Limits [#limits] The firmware caps `vx` and `vy` at 0.4 m/s and `vyaw` at 0.8 rad/s. `Limits()` defaults to those caps and the SDK clamps every velocity to them before it leaves, so `Sent.clamped` is `True` exactly when the robot would not have walked at the speed you asked for. A tighter envelope is `Robot(limits=Limits(vx=0.2, vy=0.2, vyaw=0.4))`, `MENLO_LIMITS="0.2,0.2,0.4"`, or `limits` on a saved robot. Limits above the caps are sent as asked and the firmware clamps them. Arming [#arming] MOVE is entered only from STAND held upright, gravity z below -0.87 (under 30 degrees of tilt), for 0.5 s. A velocity sent earlier is not refused and not reported: the robot stays in STAND. `stand()` returns once the robot is armed, so `stand()` then `balance()` enters MOVE, and `robot.armed` reads the same test the firmware applies. `balance()` and `set_velocity()` send without waiting for arming: a velocity that reaches an armed STAND puts the robot in MOVE, and `balance()` that reaches a STAND that is not armed times out saying so. The SDK counts the 0.5 s from its own samples, so a new session sees an armed robot as armed 0.5 s after it connects. A robot that reports no gravity leaves `robot.armed` as `None`. Faults [#faults] A critical alert, such as a fall, actuator over-temperature, battery protection, a joint past its limit, lost actuator communication or a watchdog, makes the firmware latch DAMP. The latch, and `error_flags`, stay until the firmware restarts. The SDK reports it as `State.faulted` and as `faulted` from `preflight()`, and drops a held velocity without sending anything more. A command is still sent; the wait that sees the fault, in `stand()`, `balance()`, `set_velocity()`, `set_joints()` or `wait_until()`, raises `RobotFaultedError` with `.sent` set. `damp()` returns, since the robot is already in DAMP. Watchdogs [#watchdogs] | Watchdog | Where | What happens | | -------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Velocity** | the robot, `udp` and `hybrid` | zero velocity 2 s after the last velocity command; the robot stays in MOVE, balancing | | **Trajectory** | the robot, every mode | DAMP 2 s after the last setpoint | | **Link** | SDK, every mode | no state for 2 s: zero velocity if one is held, `on_link_lost` fires, every verb raises `LinkLostError` until `close()` and `connect()` | A script that holds a velocity re-sends it at 10 Hz, so the velocity watchdog does not stop it: end it with `balance()`, a `duration`, or `close()`. On a `livekit` connection, Asimov Edge stops a held velocity when the SDK sends zero or leaves the room. A script that hangs while it holds a velocity keeps the robot walking until LiveKit drops the participant. Bound every hold with a `duration`, and prefer `udp` or `hybrid` for a control loop. What the SDK Reports [#what-the-sdk-reports] The SDK does not refuse a command because of what the robot reports. Its robot mode, a latched fault, an alert, an actuator temperature or the battery never stop `stand()`, `balance()`, `set_velocity()`, `trajectory()` or `set_joints()`, and the firmware decides what a command does. The firmware warns at 60 °C and latches DAMP at 80 °C on an actuator, and warns below 20 % battery. The one refusal is no live state. `stand()`, `balance()` outside MOVE, `set_velocity()`, `trajectory()` and `set_joints()` read the latest state sample before they send; on a live stream that adds no delay. Without one (`no_state`, `stale_state`) they wait up to the command's `timeout`, then raise `NotReadyError` with nothing sent. After a STAND sent moments ago whose report has not arrived, they wait up to 1 s for that report, then send anyway. A closed `Robot` raises `NotConnectedError`, a lost link `LinkLostError` and a protocol version mismatch `ProtocolMismatchError`, at once. `trajectory()` and `set_velocity(hold=False)` check once and do not wait unless given a `timeout`. `balance()` in MOVE and `damp()` are sent at once. ```text not ready to move: the latest state is 1.2 s old (limit 0.5 s) (stale_state); check the network link to the robot (waited 5.0 s) ``` Waits stay truthful. `stand()` returns at STAND and armed, `balance()` at MOVE, `damp()` at DAMP. A latched fault seen during a wait raises `RobotFaultedError`, after the command was sent, and a robot that does not get there raises `WaitTimeoutError` saying what it reports: ```python from menlo.asimov import NotReadyError, Robot, RobotFaultedError, WaitTimeoutError with Robot().connect() as robot: try: robot.stand() robot.balance() robot.set_velocity(vx=0.3, duration=3.0) except RobotFaultedError as exc: # sent; latched until the firmware restarts print("faulted:", exc) except NotReadyError as exc: # no live state: nothing was sent print(exc) except WaitTimeoutError as exc: # sent, but the robot did not arm or reach MOVE in time print(exc) ``` `RobotFaultedError` is a `NotReadyError`, so catch it first to tell a latched fault apart. `robot.preflight(action)` lists the facts for `"stand"`, `"move"` (`balance()` and `set_velocity()`) or `"trajectory"` (`trajectory()` and `set_joints()`). It sends nothing, never waits and never raises for a robot condition. `check.ok` is `True` when nothing is blocking, which means there is live state; `check.problems` lists the problems, blocking first. Only the blocking codes stop a command; the others are information, and nothing in the SDK acts on them. `check.py` prints the raw facts and `preflight()` for all three actions: ```python lineNumbers=16 # The SDK counts the 0.5 s a robot in STAND must be upright to arm from its own samples. time.sleep(0.6) s = robot.get_state() # one sample: every fact below comes from it print(f"robot mode {s.mode.name}, armed {robot.armed}, faulted {s.faulted}") print(f"alerts {', '.join(a.name for a in s.alerts) or 'none'}") temps = [j.temp for j in s.joints if j.temp is not None] print(f"hottest joint {max(temps):.0f} C" if temps else "joint temperatures not reported") print(f"battery {s.battery.soc_percent:.0f} %" if s.battery else "battery not reported") print(f"state {s.age_s:.2f} s old") for action in ("stand", "move", "trajectory"): print(robot.preflight(action)) # "ready to move" means live state, and the facts ``` | Code | Blocks | Meaning | What clears it | | --------------- | ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `not_connected` | yes | closed, link lost, or protocol mismatch | `close()` and `connect()` | | `no_state` | yes | the session is open and the robot has not reported state | wait for the firmware; see [No State on udp or hybrid](/sdk/troubleshooting#no-state-on-udp-or-hybrid) | | `stale_state` | yes | latest state older than 0.5 s | a command waits for the next sample; otherwise check the link | | `faulted` | no | a critical alert is latched, or the robot reports FAULT\_DAMP; the alerts that caused it are named | restart the firmware | | `alerts` | no | the firmware reports active alerts, named | | | `not_armed` | no | `move` from STAND before the 0.5 s upright hold | hold STAND upright | Write Your Own Guard [#write-your-own-guard] A rule about what your script will not drive through is yours to write. Read `robot.get_state()` once, decide, and only then send: ```python s = robot.get_state() # one sample; every fact from it if s.faulted or any(j.temp is not None and j.temp >= 60 for j in s.joints): raise SystemExit(f"not driving: robot mode {s.mode.name}, faulted {s.faulted}") ``` `guard.py` is an optional guard to start from. Its limits are constants at the top of the file: `MAX_JOINT_TEMP_C` (60 °C), `MIN_BATTERY_PERCENT` (20 %), `REFUSE_WHEN_FAULTED` and `REFUSE_ON_ALERTS` (warning and critical alerts). The defaults stop at the firmware's own warnings, before its latches. It prints what it finds and exits with status 1 when your rule fails: ```python lineNumbers=24 def guard(robot: Robot) -> None: """Print what the robot reports, and exit with status 1 when your rule fails.""" s = robot.get_state() # one sample: every fact below comes from it stop: list[str] = [] print(f"guard: robot mode {s.mode.name}, armed {robot.armed}, faulted {s.faulted}") if s.faulted and REFUSE_WHEN_FAULTED: stop.append("the firmware latched DAMP; it stays so until the firmware restarts") temps = [(j.temp, j.name or f"joint {i}") for i, j in enumerate(s.joints) if j.temp is not None] if temps: hottest, joint = max(temps) print(f"guard: hottest joint {joint} {hottest:.0f} C") if hottest >= MAX_JOINT_TEMP_C: stop.append(f"{joint} is at {hottest:.0f} C (your limit {MAX_JOINT_TEMP_C:.0f} C)") else: print("guard: joint temperatures not reported") if s.battery is not None: print(f"guard: battery {s.battery.soc_percent:.0f} %") if s.battery.soc_percent < MIN_BATTERY_PERCENT: stop.append( f"battery at {s.battery.soc_percent:.0f} % (your limit {MIN_BATTERY_PERCENT:.0f} %)" ) if s.battery.protecting: stop.append("the battery management system is protecting the pack") else: print("guard: battery not reported") for alert in s.alerts: print(f"guard: alert {alert.name} (severity {alert.severity})") serious = sorted({a.name for a in s.alerts if a.severity <= 1}) # 0 critical, 1 warning if serious and REFUSE_ON_ALERTS: stop.append(f"active alerts: {', '.join(serious)}") if stop: print("guard: not going ahead: " + "; ".join(stop)) print("guard: this is your rule in guard.py; change it, or delete the guard(robot) line") sys.exit(1) ``` The motion examples, `stand.py`, `balance.py`, `walk.py`, `keyboard.py`, `stream_velocity.py`, `move_joints.py` and `wait_until.py`, call `guard(robot)` before they send anything. Change the constants, change `guard()`, or delete the `guard(robot)` line in a script to drive without it. Run on its own, `python guard.py` prints what it finds and sends nothing. Shutdown [#shutdown] Leaving the `with` block calls `close()`: zero velocity if one is held, then the link drops. The robot stays in MOVE, balancing in place, and the Cockpit or the gamepad can take it from there. Call `damp()` at the end of a session only on a supported robot, hanging from its gantry hook or seated on a stool or bench: every actuator stops holding its position and a standing robot falls. `damp.py` asks before it sends, then waits for the robot to report DAMP: ```python lineNumbers=17 # 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}") ``` Turning the robot off is in [Power](/asimov/1/operate/power#turn-the-robot-off). Related [#related] * [Stopping the Robot](/asimov/1/operate/safety/stopping): the E-Stop, pause, damp and cutting power * [Stand Up and Walk](/asimov/1/operate/drive/stand-up): the same sequence from the Cockpit * [Move the Robot](/sdk/move): stand, balance and walk * [Control Joints](/sdk/joints): joint control on a supported robot * [Troubleshooting](/sdk/troubleshooting): each error and its fix # Read State `robot.get_state()` returns the latest sample the robot reported, decoded: robot mode, joints, IMU, battery, alerts. Reading it costs nothing, because the robot streams it anyway. A field the robot does not report is `None`, never a guessed zero. `read_state.py` is a tour of the state: robot mode, arming, what latched, battery, the hottest actuators, gravity, state age and rate, alerts. It sends nothing and works in every connection mode. ```python lineNumbers=36 # robot.get_state() is the latest sample the robot sent. It is a snapshot: read it again # for newer values. A field the robot does not report is None, never a guess. s = robot.get_state() # Robot mode: DAMP (no actuator holds a position), STAND (a held standing pose, no # balance) or MOVE (the walking policy balances the robot, at zero or any velocity). print(f"robot mode {s.mode.name}") # Armed: the firmware accepts MOVE only after STAND has been held upright for 0.5 s. # None means the SDK cannot tell (no gravity vector reported). print(f"armed {robot.armed}") # Faulted: a critical alert latched DAMP. The robot stays in DAMP, and error_flags stay # set, until the firmware restarts. The preflight check names what latched. print(f"faulted {s.faulted}") for problem in robot.preflight("stand").problems: if problem.code == "faulted": print(f" {problem.message}") # Battery, from the battery management system. None when the robot reports none. if s.battery is not None: b = s.battery print( f"battery {b.soc_percent:.0f} %, {b.voltage_v:.1f} V, {b.current_a:+.1f} A, " f"warmest cell {b.max_cell_temp_c:.0f} °C, protecting {b.protecting}" ) else: print("battery not reported") # Actuators: one Joint per actuator, with the firmware's name, position (rad), velocity # (rad/s), current (A) and temperature (°C). The firmware warns at 60 °C and latches # DAMP at 80 °C. temps = sorted(((j.temp, j.name) for j in s.joints if j.temp is not None), reverse=True) if temps: hottest = ", ".join(f"{name} {temp:.0f} °C" for temp, name in temps[:HOTTEST]) print(f"hottest {hottest}") else: print("hottest actuator temperatures not reported") # IMU: gravity as the body sees it. z is close to -1 when the robot is upright; # upright is True below -0.8. euler is (roll, pitch, yaw) in rad from the IMU quaternion. if s.gravity is not None: gx, gy, gz = s.gravity print(f"gravity ({gx:+.2f}, {gy:+.2f}, {gz:+.2f}), upright {s.upright}") if s.euler is not None: roll, pitch, yaw = s.euler print(f"orientation roll {roll:+.2f}, pitch {pitch:+.2f}, yaw {yaw:+.2f} rad") # Alerts the firmware reports now. Severity 0 is critical: it latches DAMP. alerts = [f"{a.name}{' (critical)' if a.critical else ''}" for a in s.alerts] print(f"alerts {', '.join(alerts) or 'none'}") # Freshness: how old the sample is. Decisions to move are made on samples at most # 0.5 s old; robot.preflight() reports stale_state beyond that. print(f"state age {s.age_s * 1000:.0f} ms") # The same fields as a stream, a few times a second. for _ in range(STREAM_LINES): time.sleep(STREAM_PERIOD_S) s = robot.get_state() now = time.monotonic() rate = sum(1 for t in tuple(arrivals) if now - t <= 1.0) battery = f"{s.battery.soc_percent:.0f} %" if s.battery else "n/a" print( f"{s.mode.name:5} armed {robot.armed} upright {s.upright} faulted {s.faulted} " f"battery {battery} age {s.age_s * 1000:.0f} ms rate {rate:.0f} Hz" ) ``` | Field | Meaning | | ------------------------------------------------------ | ----------------------------------------------------------------------------------- | | `state.mode` | `Mode.DAMP`, `Mode.STAND`, `Mode.MOVE`, `Mode.FAULT_DAMP` or `Mode.UNKNOWN` | | `state.joints` | one `Joint(name, pos, vel, current, temp)` per actuator, firmware order | | `state.joint("L_Knee")` | a joint by name; `KeyError` on an unknown name | | `state.joint_pos` | one radian value per actuator | | `state.gravity` | gravity projected into the body frame; z near -1 when upright | | `state.gyro`, `state.quat`, `state.euler`, `state.yaw` | angular rate, orientation, and heading in radians counter-clockwise | | `state.battery` | `Battery(voltage_v, current_a, soc_percent, max_cell_temp_c, protection)` or `None` | | `state.alerts`, `state.error_flags`, `state.faulted` | alerts and faults reported by the firmware | | `state.age_s` | seconds since this sample arrived | Check the Facts [#check-the-facts] `robot.preflight(action)` reads the latest state and lists the facts for `"stand"`, `"move"` or `"trajectory"`: whether there is live state, a latched fault, active alerts, and in STAND whether the robot is armed. It sends nothing, never waits and never raises for a robot condition. `check.py` prints the raw facts and all three: ```python lineNumbers=16 # The SDK counts the 0.5 s a robot in STAND must be upright to arm from its own samples. time.sleep(0.6) s = robot.get_state() # one sample: every fact below comes from it print(f"robot mode {s.mode.name}, armed {robot.armed}, faulted {s.faulted}") print(f"alerts {', '.join(a.name for a in s.alerts) or 'none'}") temps = [j.temp for j in s.joints if j.temp is not None] print(f"hottest joint {max(temps):.0f} C" if temps else "joint temperatures not reported") print(f"battery {s.battery.soc_percent:.0f} %" if s.battery else "battery not reported") print(f"state {s.age_s:.2f} s old") for action in ("stand", "move", "trajectory"): print(robot.preflight(action)) # "ready to move" means live state, and the facts ``` `check.ok` is `True` when nothing is blocking, which means there is live state; `check.problems` lists the problems, blocking first, then information; `check.has(code)` tests one; `str(check)` is readable. Only `not_connected`, `no_state` and `stale_state` block; `faulted`, `alerts` and `not_armed` are information. Each code is in [What the SDK Reports](/sdk/safety#what-the-sdk-reports). `stand()`, `balance()` outside MOVE, `set_velocity()`, `set_joints()` and `trajectory()` wait for live state before they send, up to `timeout` seconds, then raise `NotReadyError`, carrying `.preflight` and `.problems`, with nothing sent. Nothing else stops them: the robot mode, a fault, an alert, a hot actuator or a low battery is for your script to decide on, from `robot.get_state()`, as `guard.py` does; see [Write Your Own Guard](/sdk/safety#write-your-own-guard). `robot.armed` is the arming test on its own: `True` in MOVE, or in STAND once the robot has been upright for 0.5 s; `False` in DAMP or before that; `None` without state, closed, in UNKNOWN, or in STAND with no gravity reported. Wait on the Robot [#wait-on-the-robot] Waits read the robot's own report, never a sleep. The code below moves the robot, so it belongs after the [checklist](/sdk/safety#before-you-run-a-script) and [Standing and Balancing](/sdk/safety#standing-and-balancing): ```python if robot.get_state().mode is Mode.DAMP: robot.stand() # returns once the robot reports STAND and is armed robot.balance() # returns once the robot reports MOVE robot.set_velocity(vx=0.2, duration=2.0) # checks the robot, then sends robot.wait_until(lambda s: s.mode is Mode.MOVE and s.upright, timeout=10.0) ``` `stand()`, `balance()`, `damp()`, `set_joints(wait=True)` and `wait_until` read the robot's report and raise `WaitTimeoutError` (also a `TimeoutError`) when the condition is not met in time, with `.last` holding the last state seen; `.sent` on a command carries what went out. A stream that goes quiet raises `StateStaleError` instead: a robot that did not reach the state and a stream that stopped need different fixes. A fault is checked before the predicate and raises `RobotFaultedError`, so a fall is never read as success. `stale_after` on `wait_until` defaults to the link timeout of 2 s. Upright [#upright] `state.upright` is `True` when gravity z is below -0.8, about 37 degrees of tilt, and `None` when the robot reports no gravity: unknown and fallen are different answers. It is not the arming test; the firmware arms at -0.87, under 30 degrees, and `robot.armed` reads that. Faults [#faults] `state.faulted` is `True` when `error_flags` is set or any alert is critical (severity 0). A critical alert, such as a fall, actuator over-temperature, battery protection, a joint past its limit, lost actuator communication or a watchdog, makes the firmware latch DAMP. It stays latched, and `error_flags` stay set, until the firmware restarts. The robot may report it as `Mode.FAULT_DAMP`. Commands are still sent; the waits of `stand()`, `balance()`, `set_velocity()`, `set_joints()` and `wait_until` raise `RobotFaultedError`; `preflight()` reports `faulted`; nothing a script sends clears it. `alert.name` is the firmware's name for an alert, `alert.critical` its severity test. What the SDK Knows About the Body [#what-the-sdk-knows-about-the-body] ```python info = robot.info info.dof, info.joint_names # 25 and the firmware's actuator order on Asimov 1 info.capabilities # frozenset({'drive', 'state', 'battery', 'camera', ...}) info.protocol_version, info.limits, info.endpoint, info.transport ``` Joint names come from `menlo.asimov.robots.ASIMOV_1_BIPED_JOINTS`, chosen by the reported joint count. `robot.has("camera")` and `robot.require("drive", "battery")` test the capabilities; the set is what this connection carries, so it holds no media on udp. Callbacks [#callbacks] For event-driven code, register a callback instead of polling. Callbacks run on the transport thread, the one that delivers state: keep them short. `robot.damp()` from a callback, such as `on_alert` damping on an alert, sends DAMP and returns at once. The other commands that wait for the robot, and `wait_until()`, raise `RuntimeError` there with nothing sent; pass `wait=False`, or call them from your own thread. | Callback | Arguments | | ---------------------- | ---------------------------------------------------------- | | `robot.on_state` | every accepted `State` | | `robot.on_mode_change` | `(before, after)`, both `Mode` | | `robot.on_alert` | `(alert, change)`, with `change` `"raised"` or `"cleared"` | | `robot.on_link_lost` | the `LinkLostError` | Related [#related] * [Move the Robot](/sdk/move): what to do once the robot is ready * [Safety](/sdk/safety#what-the-sdk-reports): every preflight code * [SDK Reference](/sdk/reference#state): every field and type # Troubleshooting Start with `menlo status`: it connects, reads state for a second, and prints what the robot reports without sending anything. The SDK refuses a command only when there is no live state; everything else the robot reports is for your script to judge. `ConnectError`: Nothing to Connect To [#connecterror-nothing-to-connect-to] `Robot()` found neither `MENLO_UDP_HOST` nor `MENLO_MANAGER_URL` with `MENLO_CREDENTIAL` in the environment, and no saved robot. Run `menlo setup`, or pass a `ConnectionConfig`. With several robots saved and no default, `menlo robots use NAME` picks one, or set `MENLO_ROBOT`. `MENLO_MANAGER_URL` without `MENLO_CREDENTIAL`, or the reverse, is the same error: they go together. `ConnectError`: The Saved Robot Lacks What the Mode Needs [#connecterror-the-saved-robot-lacks-what-the-mode-needs] The message names the fix, such as `menlo robots add lab --udp `. `hybrid` needs the robot's address, the Asimov Manager URL and an SDK credential; `udp` the address; `livekit` the Manager URL and the credential. `cfg.available_modes()` says which modes a config can reach. `ConnectError`: Asimov Manager Refused the Credential [#connecterror-asimov-manager-refused-the-credential] The SDK credential is wrong or revoked, or the URL is not Asimov Manager. Issue a new one on the Developer page of Asimov Manager with the Control role ([Get an SDK Credential](/asimov/1/operate/drive/python-sdk#get-an-sdk-credential)) and run `menlo setup` again. A credential with the Observe role connects but cannot drive on livekit. A manager that redirects is reported rather than followed: use the address it redirected to. `ConnectError`: No State From the Robot on UDP [#connecterror-no-state-from-the-robot-on-udp] The robot never reported in. The robot sends UDP state to one address, so check, in order: . `udp-control` is on in the Asimov Edge parameters in Asimov Manager. . `udp-state-host` is this machine's address as the robot sees it, and `udp-state-port` is the port the SDK binds (8851 by default). . Nothing blocks port 8851 inbound: a firewall or a VPN. See [No State on udp or hybrid](#no-state-on-udp-or-hybrid). `menlo robots add NAME --udp HOST` runs this check and prints the same advice. No State on udp or hybrid [#no-state-on-udp-or-hybrid] On `udp` and `hybrid`, the robot listens for commands on UDP port 8850 and sends its state to one address: the `udp-state-host` and `udp-state-port` (8851 by default) in the Asimov Edge parameters. Commands go out from your computer, but state comes in to it, so a firewall on your computer that blocks inbound UDP 8851 from the robot leaves the SDK with no state: `connect()` raises `ConnectError`, a command raises `NotReadyError` with `no_state` or `stale_state`, and `menlo stand`, `balance` and `walk` print `Not feasible:`. `livekit` is not affected. First check that `udp-state-host` is this computer's address as the robot sees it; the robot sends state to that one host only. Then allow inbound UDP 8851 from the robot: * **Linux** with ufw: `sudo ufw allow from to any port 8851 proto udp`. With firewalld: `sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="" port port="8851" protocol="udp" accept'`, then `sudo firewall-cmd --reload`. * **macOS**: the firewall allows or blocks per application. Open **System Settings > Network > Firewall > Options** and allow incoming connections for the Python you run the SDK with, or answer **Allow** when macOS asks the first time a script connects. * **Windows**: answer **Allow** for private networks when Windows asks the first time a script connects, or in an administrator PowerShell run `New-NetFirewallRule -DisplayName "Asimov state" -Direction Inbound -Protocol UDP -LocalPort 8851 -Action Allow`. Run `menlo status` again: the state rate and a fresh sample show that state arrives. `ConnectError`: Could Not Bind the State Port [#connecterror-could-not-bind-the-state-port] Another process holds port 8851; one client per port on udp. Close it, or use `state_bind=("0.0.0.0", )` and set `udp-state-port` to match. `ProtocolMismatchError` [#protocolmismatcherror] The robot speaks another protocol version than the SDK was built against. Update whichever is behind. `connect(allow_version_skew=True)` proceeds anyway, and the robot may misread the commands the SDK sends. `NotReadyError` [#notreadyerror] There was no live state to send against, and nothing was sent. `stand()`, `balance()` outside MOVE, `set_velocity()`, `set_joints()` and `trajectory()` read the latest state sample before they send. The codes are `no_state` (the robot has not reported) and `stale_state` (the latest sample is older than 0.5 s); both are waited out for up to the command's `timeout` first, and the message then ends in `(waited s)`. A closed `Robot` raises `NotConnectedError`, a lost link `LinkLostError` and a protocol version mismatch `ProtocolMismatchError` instead, at once. `e.problems` carries them, `e.has(code)` tests one and `e.preflight` is the whole report. Check the link with `menlo status`. On `udp` or `hybrid`, see [No State on udp or hybrid](#no-state-on-udp-or-hybrid). The robot mode, a fault, an alert, a hot actuator or a low battery never raise `NotReadyError`: the command is sent and the firmware decides. A rule against them is yours to write; see [Write Your Own Guard](/sdk/safety#write-your-own-guard). `RobotFaultedError` [#robotfaultederror] A wait found `state.faulted` set after the command was sent: a critical alert, such as a fall, over-temperature or battery protection, has latched DAMP. The message and `state.alerts` name it; write it down before anything restarts the firmware, since restarting clears the record of what latched. Nothing a script sends clears it. Then follow [Recovering After an E-Stop or a Fall](/asimov/1/operate/safety/stopping#recovering-after-an-e-stop-or-a-fall) to support and inspect the robot; its **Turn Off Robot** step restarts the firmware. See [If Something Goes Wrong](/sdk/safety#if-something-goes-wrong). `RobotFaultedError` is a `NotReadyError`, so `except NotReadyError` catches it too; catch it first to tell a fault apart. `e.sent` is the command that went out. `WaitTimeoutError` [#waittimeouterror] A command was sent and the robot did not report the effect in time. `e.last` is the last state seen and `e.sent` what went out. * `stand()`: the robot did not report STAND and arming within `timeout` (10 s). See the next section. * `balance()` from STAND: the robot did not report MOVE within `timeout` (5 s) after the zero velocity went out. The message says when the robot was not armed: wait until `robot.armed` is `True`, then `balance()` again. Otherwise another controller may hold it: the Asimov Manager Cockpit or a paired gamepad outranks a script, and a command it outranks has no effect. * `balance()` from DAMP: raised at once, `the robot is in DAMP: stand() first`. * `damp()`: the robot did not report DAMP within `timeout` (5 s). Check that state is still arriving with `menlo status`, and that a `trajectory()` loop is not overwriting the verb. * `set_joints(wait=True)`: a joint did not reach its target within `tolerance`. It may be blocked, or the gains may be too soft to hold it. * `wait_until()`: the predicate stayed false; the message names the robot mode and the age of the last sample. `stand()` Times Out and the Robot Stays DAMP [#stand-times-out-and-the-robot-stays-damp] * **A fault is latched**: `stand()` raises `RobotFaultedError` instead; see above. * **The robot dropped the command**: another source holds control. The Cockpit outranks a paired gamepad, which outranks udp, which outranks livekit, and a paired gamepad holds control while idle. Unpair it and try again. * **The robot reports STAND but is not armed**: it is not upright. The firmware arms after 0.5 s of STAND under 30 degrees of tilt; `robot.armed` reads the test. The Robot Tipped Over After `stand()` in MOVE [#the-robot-tipped-over-after-stand-in-move] `stand()` is sent from any robot mode. STAND has no balance loop, so a free-standing robot asked to stand from MOVE tips over. Before `stand()` in MOVE, hang the robot from its gantry hook or seat it on a stool or bench; `rest.py` asks first. To stand still after a walk, call `balance()`: zero velocity keeps the robot in MOVE, balancing in place. See [Bringing the Robot to Rest](/sdk/safety#bringing-the-robot-to-rest). `set_velocity` Returned but the Robot Did Not Walk [#set_velocity-returned-but-the-robot-did-not-walk] In order of likelihood: . The hold was cut short: with `wait=False`, `set_velocity` returns at once, and the next verb or the end of the `with` block ends the hold. Keep the connection open for the duration, or leave `wait` at its default so the call returns after the hold. . The robot is not in MOVE. `set_velocity()` is sent in any robot mode: in DAMP the robot drops it, and in a STAND that is not armed it leaves the robot in STAND. MOVE is entered only from STAND held upright for 0.5 s, and an early velocity is neither refused nor reported. Check `state.mode` and `robot.armed`, and call `stand()` then `balance()` first. . Another source holds control; see above. The state stream does not say who does. A `stand()` or `damp()` Did Nothing [#a-stand-or-damp-did-nothing] A loop is streaming `trajectory()` setpoints and the next setpoint overwrote the verb. A `stand()` sent into the loop is overwritten this way. A `damp()` usually holds, because the robot drops trajectories once it reports DAMP, but a setpoint that arrives first still wins. Stop the loop, then send the verb. A `set_joints()` does not have this problem: any verb ends it. `StateStaleError` [#statestaleerror] The robot went quiet for longer than `stale_after` (default: the link timeout, 2 s) during `wait_until()` or a command's wait. Treat it as absent, not slow: the connection dropped, the robot's software stopped, or it is reporting to a different machine. `LinkLostError` follows. A stream that is merely late before a command is sent is `stale_state`, waited out and then a `NotReadyError`. `LinkLostError` [#linklosterror] No state for 2 s. The SDK sent zero velocity if it held one, and every verb now raises this until you `close()` and `connect()` again. On udp and hybrid, the robot also zeroes the velocity 2 s after the last command it received. `NotConnectedError` [#notconnectederror] `robot.get_state()` or `damp()` before the robot reported (after `connect(require_state=False)`), or any verb after `close()`. `close()` during a `set_velocity(..., wait=True)` raises this in the waiting thread. `UnsupportedError` [#unsupportederror] This connection does not carry that capability. `camera`, `microphone` and `speaker` need `hybrid` or `livekit`; on those, a capability is claimed only from a track that arrived within `media_timeout` of connecting. Gate with `robot.has("camera")`. `ImportError` Naming Pillow, numpy or OpenCV [#importerror-naming-pillow-numpy-or-opencv] `Frame.to_jpeg`, `to_numpy`, `Clip.save_frames` and `Clip.save_mp4` need those packages at call time. The SDK does not depend on them; install the one named. Every `Sent.wait_outcome()` Returns `Unknown` [#every-sentwait_outcome-returns-unknown] The robot reports no per-command verdict, so `Unknown` is what every command gets and `on_refused` never fires. `Unknown` is neither success nor refusal: read the effect with `wait_until` and `robot.get_state()`, or let `stand()`, `balance()` and `damp()` wait for it. Velocity Is Slower Than Asked [#velocity-is-slower-than-asked] Check `sent.clamped` and `sent.command`. The SDK clamps to `Limits`, whose defaults are the firmware caps of 0.4 m/s and 0.8 rad/s; a robot does not walk faster than that. A tighter limit came from `Robot(limits=)`, `MENLO_LIMITS`, or the saved robot's `limits`. Two Sessions Keep Disconnecting Each Other [#two-sessions-keep-disconnecting-each-other] Both joined the room with the same identity. With `ManagerConfig`, each session gets its own unless you set the same `label`; with `LiveKitConfig`, one token is one participant, so mint two. Related [#related] * [Safety](/sdk/safety): what each layer stops, what the SDK reports, and your own guard * [Troubleshooting the Robot](/asimov/1/operate/troubleshooting): problems that are not the SDK's # Without the SDK The robot's LiveKit room carries everything the `livekit` connection mode uses: state and commands as data messages, the camera and the microphone as tracks. Any LiveKit client can join it. This page shows how, in Python, without `menlo-sdk`; the scripts are in the `livekit_raw/` folder of the [examples](/sdk/examples#without-the-sdk). ```bash pip install livekit asimov-protocol export MENLO_CREDENTIAL=... # an SDK credential from Asimov Manager ``` `asimov-protocol` holds the protobuf messages; `RobotState` and `RobotCommand` are what the SDK sends and reads. Set `MANAGER_URL` in `manager_token.py` to your Asimov Manager address. The role of the credential applies: an Observe credential can read state and the tracks but its commands are dropped ([Connection Modes](/sdk/connection-modes#livekit)). No limits, no link watchdog, no zero velocity on exit and no fence between commands. Run `send_commands.py` only with the robot hanging from its gantry hook, and keep Asimov Manager open at the **E-Stop**. Work through [Before You Run a Script](/sdk/safety#before-you-run-a-script) first. A Join Token [#a-join-token] Asimov Manager issues join tokens at `POST /api/livekit/token` to a request that carries an SDK credential as a Bearer token. The answer names the LiveKit `url`, the `room`, the `token`, your participant `identity` and the credential's `role`. `manager_token.py` fetches one; it refuses redirects, so the credential is never forwarded to another host, and it rewrites a `localhost` LiveKit URL to the manager's host, which is what a robot that runs its own LiveKit server reports. ```python lineNumbers=20 class NoRedirect(urllib.request.HTTPRedirectHandler): """Refuse every redirect: urllib would send the credential on to the new address.""" def redirect_request(self, *args: Any, **kwargs: Any) -> None: return None def fetch_grant(manager: str = MANAGER_URL, credential: str = CREDENTIAL) -> dict[str, Any]: """The LiveKit URL, the robot's room and a join token, from Asimov Manager.""" if not credential: sys.exit("Set MENLO_CREDENTIAL to an SDK credential from Asimov Manager.") request = urllib.request.Request( manager.rstrip("/") + "/api/livekit/token", data=b"{}", method="POST", headers={"Authorization": f"Bearer {credential}", "Content-Type": "application/json"}, ) with urllib.request.build_opener(NoRedirect).open(request, timeout=5) as response: grant: dict[str, Any] = json.load(response) # url, room, token, identity, role # The URL is the one the robot uses itself; localhost there is Asimov Manager's host. url = urlsplit(grant["url"]) if url.hostname in ("localhost", "127.0.0.1"): host = urlsplit(manager).hostname or "" netloc = host if url.port is None else f"{host}:{url.port}" grant["url"] = url._replace(netloc=netloc).geturl() return grant ``` The other scripts in the folder import `fetch_grant()` from this file. Read State [#read-state] The robot publishes `RobotState` on the `state` data track at the firmware's state rate. `read_state.py` joins, decodes each message with `RobotState.FromString` and prints the mode, protocol version and sequence number for 5 s: ```python lineNumbers=18 async def print_states(track: rtc.RemoteDataTrack) -> None: async for frame in track.subscribe(): state = asimov_state_pb2.RobotState.FromString(bytes(frame.payload)) mode = asimov_common_pb2.ControlMode.Name(state.current_mode) print(f"{mode} protocol {state.protocol_version} sequence {state.sequence}") async def main() -> None: grant = fetch_grant() room = rtc.Room() readers: set[asyncio.Task[None]] = set() def on_data_track(track: rtc.RemoteDataTrack) -> None: if track.info.name == "state": task = asyncio.ensure_future(print_states(track)) readers.add(task) # keep a reference so the task is not collected task.add_done_callback(readers.discard) room.on("data_track_published", on_data_track) await room.connect(grant["url"], grant["token"]) try: await asyncio.sleep(SECONDS) finally: await room.disconnect() ``` `current_mode` is the firmware's mode enum, `error_flags` the latched fault bits, and `active_alerts` the current alerts. The field list is in [The Protocol](/sdk/reference#the-protocol). Send a Command [#send-a-command] Commands are `RobotCommand` messages published as reliable data on the `commands` topic. Each carries `protocol_version = 1`, a `sequence` you increment, and `timestamp_us`. The robot drops a command whose protocol version it does not speak, and reports nothing. `send_commands.py` keeps a guard of its own, like `guard.py` in the SDK examples: it listens to state for 2 s and sends nothing when the state is stale, the protocol version is wrong, a critical alert or `error_flags` is set, the battery is protecting itself or below 20 %, or an actuator is at 60 °C or more. The limits are constants at the top of the file; change them as you need. If the robot is in DAMP, it sends one STAND and exits. ```python lineNumbers=85 reason = why_not_stand(*latest) if latest else "no state received" if reason is not None: print("not sending STAND:", reason) return 1 payload = command(asimov_common_pb2.CONTROL_MODE_STAND) await room.local_participant.publish_data(payload, reliable=True, topic="commands") ``` A velocity is the same message with `set_velocity` filled in. Without the SDK, nothing re-sends it, so send zero velocity yourself before you leave the room. Asimov Edge stops a held velocity when a participant sends zero or leaves the room ([Watchdogs](/sdk/safety#watchdogs)). Camera [#camera] The camera is the robot's video track. `camera.py` subscribes, takes one frame from a `VideoStream`, converts it to RGB and writes `frame.ppm`: ```python lineNumbers=19 async def save_one_frame(track: rtc.Track, done: asyncio.Future[str]) -> None: stream = rtc.VideoStream(track) async for event in stream: rgb = event.frame.convert(rtc.VideoBufferType.RGB24) header = f"P6 {rgb.width} {rgb.height} 255\n".encode() Path(PATH).write_bytes(header + bytes(rgb.data)) done.set_result(f"saved {PATH}, {rgb.width}x{rgb.height}") break await stream.aclose() async def main() -> None: grant = fetch_grant() room = rtc.Room() done: asyncio.Future[str] = asyncio.get_running_loop().create_future() tasks: set[asyncio.Task[None]] = set() def on_track(track: rtc.Track, *_: object) -> None: if track.kind == rtc.TrackKind.KIND_VIDEO and not tasks: tasks.add(asyncio.ensure_future(save_one_frame(track, done))) room.on("track_subscribed", on_track) await room.connect(grant["url"], grant["token"]) try: print(await asyncio.wait_for(done, TIMEOUT_S)) finally: await room.disconnect() ``` Audio [#audio] The microphone is the robot's audio track. `audio.py` reads an `AudioStream` for a few seconds and writes `microphone.wav`: ```python lineNumbers=21 async def record(track: rtc.Track, done: asyncio.Future[str]) -> None: stream = rtc.AudioStream(track) chunks: list[bytes] = [] rate = channels = 0 async for event in stream: frame = event.frame rate, channels = frame.sample_rate, frame.num_channels chunks.append(bytes(frame.data)) if sum(len(c) for c in chunks) >= SECONDS * rate * channels * 2: break await stream.aclose() with wave.open(PATH, "wb") as wav: wav.setnchannels(channels) wav.setsampwidth(2) # 16-bit samples wav.setframerate(rate) wav.writeframes(b"".join(chunks)) done.set_result(f"saved {PATH}, {SECONDS:.0f} s at {rate} Hz") async def main() -> None: grant = fetch_grant() room = rtc.Room() done: asyncio.Future[str] = asyncio.get_running_loop().create_future() tasks: set[asyncio.Task[None]] = set() def on_track(track: rtc.Track, *_: object) -> None: if track.kind == rtc.TrackKind.KIND_AUDIO and not tasks: tasks.add(asyncio.ensure_future(record(track, done))) room.on("track_subscribed", on_track) await room.connect(grant["url"], grant["token"]) try: print(await asyncio.wait_for(done, SECONDS + TIMEOUT_S)) finally: await room.disconnect() ``` To play sound on the robot, publish an audio track of your own; the robot plays it on its speaker. Related [#related] * [Connection Modes](/sdk/connection-modes): the `livekit` mode and the SDK credential * [Camera and Audio](/sdk/media): the same tracks through the SDK * [Safety](/sdk/safety): what the SDK adds on top of the wire, and what it does not * [SDK Reference](/sdk/reference#the-protocol): the protocol messages * [Robot API](/asimov/1/program/api): the HTTP API of Asimov Manager # Overview Asimov 2 [#asimov-2] V2 concepts, full specs & shipping date unknown. Interested? [Talk to us](https://tally.so/r/J9O5BJ) # Overview Digital Asimov is a free, browser-based digital twin of the Asimov robot. Drive it, watch its telemetry, and explore what the robot can do — all from a browser tab, with no hardware to set up. It's the quickest way to get a feel for Asimov: a fun test-drive of the robot and a zero-setup sandbox before hardware is available. Access it at [try.menlo.ai](https://try.menlo.ai) — sign in and a robot is created for you automatically. A faithful digital twin [#a-faithful-digital-twin] Digital Asimov isn't an idealized physics demo — it mirrors the real robot: * **Real actuator models** measured from physical hardware, not idealized physics. * **The same control stack** a physical Asimov runs, so what works here works on hardware. * **The same telemetry**, streamed over the same wire format as a physical robot. * **FPV and third-person camera** views, rendered as a live video track. What you can do [#what-you-can-do] # Quickstart Drive a Digital Asimov robot in your browser in under a minute — no hardware, no install. Open the simulator [#open-the-simulator] Go to [try.menlo.ai](https://try.menlo.ai) and sign in with Google. A robot is created for you automatically — no setup required. Start the robot [#start-the-robot] The simulator launches with the robot running. The cockpit interface appears immediately. Control the robot [#control-the-robot] Use keyboard controls for direct teleop: | Key | Action | | ------- | -------------- | | `W` | Forward | | `S` | Backward | | `A` | Strafe left | | `D` | Strafe right | | `Q` | Turn left | | `E` | Turn right | | `Space` | Emergency stop | Each keypress fires one movement command immediately. A green **Command sent** flash confirms each command; a red **Command failed** flash means the session rejected it. For physical robots, see the [Asimov API](/asimov/1/program/api). # Telemetry Once the robot is running, telemetry flows into three panels across the settings page and cockpit. Event log [#event-log] Timestamped stream of everything the robot reports: connection state, firmware messages, alert-rule fires, and errors. Each entry shows a timestamp, message, and source; severity is color-coded (info, warn, error). Use it as the first stop when something looks off — a dropped session, an unresponsive joint, or an alert you didn't expect will all leave a line here. Joints [#joints] Per-joint table with three columns: | Column | Unit | Notes | | ----------- | ---- | --------------------- | | Position | rad | Current joint angle | | Current | A | Actuator current draw | | Temperature | °C | Turns red above 70°C | Click the bell icon on any row to create an alert rule for that joint. When a reading crosses your threshold, the rule fires a browser notification and writes a line to the event log. Subsystem Health [#subsystem-health] One status dot per on-robot subsystem: | Subsystem | Covers | | --------- | ----------------------------------------- | | FW Link | Firmware link to the actuator controllers | | Speaker | On-board speaker | | Camera | On-board camera | | Mic | On-board microphone | | BLE Radio | Bluetooth-LE radio | | Cloud | Cloud connectivity | The **Alerts badge** next to the robot name summarizes this panel — `NO ALERTS` when everything is green, `NN ALERTS` with an unread dot when a subsystem is reporting an error. Click the badge for a popover listing each failure. Subsystem Health only reports real values for physical Asimov robots. Platform UI support for physical robots is coming soon — until then, the panel renders as a layout preview. # Assembly Manual This assembly manual provides a detailed guide to building the Asimov 0 legs, including the required tools and parts, the assembly process for each module, when and how to perform debugging and testing during assembly, and the basic steps for operating the robot. 1\. Tool Lists 🛠️ [#1-tool-lists-️] Allen keys (M2 to M6) [#allen-keys-m2-to-m6] Allen keys (M2 to M6) are used to tighten and loosen hex socket screws during assembly, and this build requires sizes ranging from M2 to M6. Soldering Iron [#soldering-iron] A soldering iron is required to connect the electronic components. Hot air gun [#hot-air-gun] A heat air gun will be used to heat the string. Any standard hot air gun will work, so you can use the one that is most convenient for you. Wire stripper [#wire-stripper] A wire stripper is used to remove insulation from the ends of wires so they can be soldered, crimped, or connected to terminals during humanoid assembly. Any standard wire stripper suitable for the wire gauges used in this build. Label Maker (Optional) [#label-maker-optional] Useful for labeling wires, connectors, and modules to make assembly and debugging easier. CAN-to-USB Adapter (Optional) [#can-to-usb-adapter-optional] A CAN-to-USB adaptor is important for post-assembly debugging, since it allows direct connection to the actuator over CAN bus for testing and verification. You can buy from [Waveshare](https://www.waveshare.com/usb-can-b.htm). Multimeter [#multimeter] A multimeter is used to measure electrical values such as voltage, current, resistance, and continuity. Most multimeters support these basic functions, so any standard one can usually be used. Crimp tool [#crimp-tool] A crimp tool is used to attach connectors securely to wires. 2\. Get the parts 📦 [#2-get-the-parts-] Before you start building, gather all the required components first. Please review the [BOM and sourcing path](/asimov/1/build/assembly-preparations) to make sure you have the required parts. > Tip: Group the parts before you begin, such as screws, bolts, wires, actuators, connectors, and mechanical parts. This will help you build faster. Also make sure you have a basic understanding of the essential parts covered in these hardware chapters: [1. Frame and structural components](/asimov/0/hardware/frame-and-structural-components) [2. Joint design and actuation](/asimov/0/hardware/joint-design-and-actuation) [3. 3D printing and fabrication](/asimov/0/hardware/3d-printing-and-fabrication) 3\. Building the robot 🤖 [#3-building-the-robot-] Release Soon ! # Before You Start This is the right place to start if you are deciding whether Asimov 0 is for you, evaluating the build, or preparing to work through this manual. Asimov 0 is an open-source bipedal legs robot. This manual covers how the legs are designed, assembled, and brought from simulation work to real hardware locomotion. It is part of the broader Asimov project, which you can explore at [asimov.inc](https://asimov.inc). Asimov 0 open-source bipedal robotic legs Expect ambitious design choices, incomplete edges, and a build process that rewards technical judgment. What this is [#what-this-is] This manual is focused on `Asimov 0`, specifically: * the leg hardware architecture * the fabrication and assembly workflow * bring-up and operating context for the legs * the simulation and reinforcement-learning stack used for locomotion What this is not [#what-this-is-not] This is not a complete full-body humanoid operating guide. It is also not a lightweight consumer build booklet. The current emphasis is the lower body, the technical decisions behind it, and the workflow required to take it from components to working hardware. Core resources [#core-resources] If you want the source-of-truth artifacts behind this manual, start here: * [Asimov 0 repository](https://github.com/asimovinc/asimov-v0): the main source repository for the robot, including the published actuator list, mechanical assets, and supporting project files. * [Asimov 0 sim model](https://github.com/asimovinc/asimov-v0/tree/main/sim-model): the simulation-model directory used as the basis for simulator-side work. * [Asimov 0 lower-body 3D file](https://static.asimov.inc/ASV0_LowerBody.HTML): browser-viewable lower-body geometry for inspecting the leg design at a high level. If you are trying to understand whether Asimov 0 matches your needs, those three links usually answer the first serious questions: what the robot is, how the legs are packaged, and where the simulation artifacts live. Who this is for [#who-this-is-for] This manual is intended for readers who want one of two things: * to understand how Asimov 0 is designed and why specific hardware and control choices were made * to build, assemble, and validate the legs themselves It is a good fit for robotics engineers, advanced hobbyists, research teams, and technical builders who are comfortable working across mechanics, electronics, embedded systems, and simulation. Expected effort [#expected-effort] Asimov 0 is a serious hardware project, not a one-hour weekend kit. You should expect meaningful time in: * procurement and part preparation * fabrication and finishing * mechanical assembly * wiring and electronics checks * bring-up, debugging, and calibration * simulation and policy validation before any walking attempt The exact build time and cost will depend on whether you are sourcing parts independently or starting from a kit, how much fabrication you do yourself, and how much prior robotics experience you already have. The DIY Kit is the faster path if your goal is the broader full-body robot. If your goal is specifically `Asimov 0` legs, treat the kit as optional and treat this manual as the primary technical reference. Safety [#safety] Treat Asimov 0 as powered electromechanical hardware, not as a toy. Before working on the robot, make sure you have: * a stable workspace with room to support or suspend the robot safely * a plan for power isolation and emergency shutdown * basic electrical test tools such as a multimeter * a controlled bring-up process for actuators, wiring, and motion tests Do not attempt first power-on, homing, or motion tests casually. Early mistakes in wiring, joint direction, or configuration can damage components or create unsafe motion. Initial bring-up and locomotion tests should only be done in a controlled setup with the robot restrained, supported, or otherwise prevented from falling unexpectedly. What success looks like [#what-success-looks-like] For most builders, success should be evaluated in stages: . You understand the system architecture and have the required parts, tools, and workspace. . The hardware is assembled correctly and passes basic electrical and mechanical checks. . The robot can be powered, homed, and verified safely. . The software and simulation stack are configured well enough to validate the control path. . The robot reaches stable real-world locomotion behavior on hardware. The right first milestone is usually not “make it walk immediately.” The right first milestone is a clean, safe, verifiable bring-up. Where to go next [#where-to-go-next] * Read the [Overview](/asimov/0/overview) for the structure of the manual and the recommended reading order. * Go to [Hardware Design](/asimov/0/hardware) if you want to understand the robot before sourcing parts. * Visit [asimov.inc](https://asimov.inc) for the broader project overview. * Visit the [Asimov DIY Kit](https://asimov.inc/diy-kit) if you are evaluating the broader full-body hardware package. # Introduction This page explains how the manual is organized and what to read first. If you are evaluating the project or starting from scratch, begin with [Before You Start](/asimov/0). That page covers scope, audience, effort, safety, and the core project links. This is the earliest public version of the Asimov manual. It is being released in stages, and several sections will continue to expand as the documentation matures. The manual is organized into three working sections: * [Hardware Design](/asimov/0/hardware) explains how the robot is designed, including the electrical and mechanical structure, major components, and joint architecture. * [Assembly Manual](/asimov/0/assembly-manual) covers the practical build process, required tools and parts, and the path toward bring-up and operation. * [Locomotion Control](/guides/locomotion-training) documents the simulation and reinforcement-learning workflow used to train and deploy walking behavior. Recommended reading order [#recommended-reading-order] If you are new to the project, read the manual in this order: . [Before You Start](/asimov/0) . [Hardware Design](/asimov/0/hardware) . [Assembly Manual](/asimov/0/assembly-manual) . [Locomotion Control](/guides/locomotion-training) That sequence takes you from orientation and decision-making, to system understanding, to build execution, to simulation and deployment. Asimov - Here be Dragons is now available for pre-order at [asimov.inc/diy-kit](https://asimov.inc/diy-kit). # Overview This page explains how the manual is organized and what to read first. If you are evaluating the project or starting from scratch, begin with [Before You Start](/asimov/0). That page covers scope, audience, effort, safety, and the core project links. This is the earliest public version of the Asimov manual. It is being released in stages, and several sections will continue to expand as the documentation matures. The manual is organized into three working sections: * [Hardware Design](/asimov/0/hardware) explains how the robot is designed, including the electrical and mechanical structure, major components, and joint architecture. * [Assembly Manual](/asimov/0/assembly-manual) covers the practical build process, required tools and parts, and the path toward bring-up and operation. * [Locomotion Control](/guides/locomotion-training) documents the simulation and reinforcement-learning workflow used to train and deploy walking behavior. Recommended reading order [#recommended-reading-order] If you are new to the project, read the manual in this order: . [Before You Start](/asimov/0) . [Hardware Design](/asimov/0/hardware) . [Assembly Manual](/asimov/0/assembly-manual) . [Locomotion Control](/guides/locomotion-training) That sequence takes you from orientation and decision-making, to system understanding, to build execution, to simulation and deployment. Asimov - Here be Dragons is now available for pre-order at [asimov.inc/diy-kit](https://asimov.inc/diy-kit). # Overview This collection documents the locomotion stack used to take Asimov from simulation to real-world walking. The current material is centered on the legs-only platform: 12 actuated leg joints with 2 passive toe joints. The same design principles are intended to carry forward into the full-body controller. The locomotion stack is organized around one core idea: successful transfer is determined less by policy novelty and more by whether the policy sees the same type, timing, and quality of data in simulation that it will see on hardware. The chapters in this collection are organized as follows: * [Understanding Your Simulation Environment](/guides/locomotion-training/understanding-your-simulation-environment) defines the simulator assumptions, actuator interfaces, timing model, and the limits of a purely physics-only view of sim2real. * [Reinforcement Learning for Locomotion](/guides/locomotion-training/reinforcement-learning-for-locomotion) describes the policy formulation, actor and critic observations, and the control philosophy used for transfer. * [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification) documents the hardware-to-simulation mapping, actuator parameters, armature values, joint constraints, and other identified quantities that matter for stable behavior. * [Simulation Training Environment](/guides/locomotion-training/reinforcement-learning-simulation-training-environment) covers action and observation interfaces, control rates, delays, filters, and environment structure. * [Reward Design](/guides/locomotion-training/reinforcement-learning-reward-design) explains which rewards were kept, which ones were removed, and which ones were modified for Asimov hardware. * [Domain Randomization](/guides/locomotion-training/reinforcement-learning-domain-randomization) describes the targeted randomization strategy used for sim2real transfer. * [Policy Deployment](/guides/locomotion-training/reinforcement-learning-policy-deployment) documents the real firmware loop, processor-in-the-loop validation, and the practical issues encountered during bring-up on hardware. This collection should be read together with the hardware chapters, especially the discussions of the parallel ankle mechanism and passive toes in [Joint Design and Actuation](/asimov/0/hardware/joint-design-and-actuation). # Deep Dive: System Identification This chapter documents the hardware-to-simulation quantities that were identified or constrained for stable locomotion transfer. 1\. Hardware mapping comes first [#1-hardware-mapping-comes-first] Before tuning rewards or training parameters, the joint-level hardware mapping must be correct. For Asimov legs, the following items were especially important: * the ankle is not directly driven * ankle pitch and roll are produced through a parallel mechanism * toe behavior is passive and spring-driven * leg joints use different actuator families with different reflected inertia and torque-speed limits Errors in this mapping produced unstable or seemingly random policy behavior. 2\. Armature as reflected rotor inertia [#2-armature-as-reflected-rotor-inertia] In this stack, joint armature should not be interpreted as the literal electrical armature. It is used as a reflected inertia term that captures how the rotor and gearbox appear at the joint. This distinction matters because armature strongly affects closed-loop behavior and stability. Representative identified values are: | Joint family | Example value | Notes | | ------------ | ------------- | ----------------------------------------------------------- | | hip pitch | `0.095625` | From actuator datasheet and transmission mapping | | knee | `0.0339552` | From actuator datasheet and transmission mapping | | ankle | `0.0565056` | Doubled to reflect two actuators driving the parallel ankle | The ankle value required special treatment because pitch and roll are driven by two actuators through the RSU ankle mechanism. 3\. KP/KD consistency between sim and hardware [#3-kpkd-consistency-between-sim-and-hardware] Even with calculated KP/KD values, the robot exhibited vibration on startup. After analysis, the root cause was not a hardware limitation — the real actuator controllers worked fine. The problem was that the **simulation KP/KD values produced an underdamped system**, and the policy learned to behave accordingly. The policy is simply an MLP. Its job is to model some non-linear function based on the data provided to it and how its weights get updated. When trained on data from an underdamped simulated system, the policy learned to behave like an underdamped controller. And how does an underdamped control system respond to an impulse? It oscillates. This was verified mathematically: the policy's output behavior matched the impulse response of an underdamped second-order system. The failure chain is: . simulation KP/KD values produce underdamped dynamics . the policy trains on this data and learns to behave like an underdamped controller . on real hardware, the gains are fine — but the policy's learned behavior is already underdamped . the policy's corrections overshoot, and each overshoot triggers a larger correction on the next cycle, exciting sustained oscillation This realization was critical because it reframed the locomotion problem: > How do I make the domain of data between sim and real match as closely as possible? The practical lesson is not just about constraining gains to hardware limits — it is about ensuring the simulated dynamics produce training data that matches the real system's response characteristics. If the sim data domain diverges from the real data domain, the policy will learn behavior that does not transfer, regardless of whether the individual parameter values are physically plausible. 4. Actuator model details that mattered [#4-actuator-model-details-that-mattered] The actuator model includes more than simple PD control. The simulation stack models: * per-joint stiffness and damping * effort limits * speed-torque saturation * reflected inertia through armature * static and dynamic friction * explicit action delay This richer actuator model was a significant part of the sim2real improvement. Representative actuator parameters for the legs stack include: | Parameter | Example value | Note | | ----------------- | ------------- | ---------------------------------------- | | stiffness | `65.0` | chosen as a safe deployable value | | damping | `5.0` | tuned to match real system response | | effort limit | `39.40` | peak torque for the modeled joint family | | saturation effort | `120.0` | speed-torque saturation behavior | | velocity limit | `12.57 rad/s` | from actuator specification | | friction static | `1.30` | static friction term | | friction dynamic | `0.100` | Coulomb-like dynamic friction term | The simulated actuator path is then wrapped in an explicit delay model with `delay_min_lag=0` and `delay_max_lag=1`. 5\. Delay is part of identification [#5-delay-is-part-of-identification] Actuator delay was not treated as a generic nuisance term. It was modeled from the observed timing behavior of the real firmware and communication path. The training model therefore includes: * action delay on the actuator path * grouped observation delay on the sensing path * real CAN timing structure rather than perfectly synchronized joint state These delays are part of the identified system, not just regularization noise. This same reasoning also motivated the move away from an overly pristine built-in actuator interpretation toward a control path that better reflected what the policy would actually see at IO rate. 6. Toe model identification [#6-toe-model-identification] The toe joint is passive, but it still affects whole-body stability through contact and push-off. The simulator therefore needs: * toe stiffness * toe damping * toe limits * toe collision geometry * toe-ground contact behavior In practice, toe resistance had to be increased relative to early assumptions because insufficient toe support caused the policy to ignore the toe during learning. Toe state was exposed to the critic, not the actor. This allowed training to capture the stabilizing effect of the toe without introducing a deploy-time dependency on unmeasured joint state. 7\. Collision geometry is also system identification [#7-collision-geometry-is-also-system-identification] Contact behavior is highly sensitive to geometry. The locomotion environment therefore replaced detailed mesh collision with simpler capsule-based foot and toe geometry. This choice improved determinism and reduced the risk of learning artifacts from unstable mesh contact. The identified contact model includes: * multiple foot and toe capsules * explicit foot-ground contact sensing * toe contact sensing * tuned friction and contact dimensions on foot and toe geoms The final contact configuration emphasized repeatability: | Contact setting | Value / choice | | ------------------- | ----------------------------------- | | contact primitive | capsules instead of mesh collision | | foot / toe friction | `0.6` | | contact dimension | `condim=3` on foot and toe geometry | | capsule radius | approximately `12 mm` | In practice, multiple heel, midfoot, and toe capsules were used so the support polygon was more stable than a single coarse collision shape. 8\. Soft limits and deployable ranges [#8-soft-limits-and-deployable-ranges] The policy is not trained to use the full hard-stop hardware range. Instead, training uses soft joint limits, typically at `0.9` of the hardware range. This reduces: * hard-stop impacts * unrealistic exploitation of boundary states * deployment-time shock loads near limit boundaries The soft-limit factor used in training was approximately `0.9` of the hardware range. 9\. Geometry errors can invalidate learning [#9-geometry-errors-can-invalidate-learning] System identification also includes checking the geometry itself. One important example was toe alignment: when the toes were accidentally tilted relative to the intended flat contact pose, the policy stopped learning effective forward-balance recovery. This is a useful reminder that a locomotion policy can fail even when gains and rewards are reasonable, simply because the physical model is not internally consistent. # Domain Randomization This chapter describes the domain randomization strategy used for Asimov locomotion. 1\. Targeted, not broad [#1-targeted-not-broad] The randomization strategy is intentionally selective. The goal is not to randomize every quantity in the simulator. The goal is to randomize the quantities that are known to vary between simulation and hardware. This chapter should therefore be read with the following principle in mind: > Randomize what is known to vary. Do not randomize what has already been measured with sufficient accuracy. 2\. Quantities that are randomized [#2-quantities-that-are-randomized] Representative randomized terms include: | Parameter | Range | Reason | | ----------------------------- | ---------------------------------------------- | ----------------------------- | | encoder zero offset (`qpos0`) | `+/-0.02 rad` | Calibration error | | PD gains | `x0.9 - x1.1` | Actuator response variation | | toe stiffness | `3.5 - 5.5 Nm/rad` | Spring variation | | foot friction | `1.0 - 1.5` | Surface variation | | observation delay | `0-2` steps | CAN timing jitter | | action delay | `0-1` steps | Command latency | | push disturbance | `+/-0.5 m/s` class disturbances | External perturbations | | reset base orientation | `yaw+/-180°, pitch+/-0.15 rad, roll+/-0.1 rad` | Initial orientation variation | | joint velocity noise | `+/-0.1 rad/s` | Encoder velocity noise | | IMU angular velocity noise | `+/-0.01 rad/s` | Gyro measurement noise | These randomizations are tied directly to known sources of mismatch. 3\. Quantities intentionally not randomized [#3-quantities-intentionally-not-randomized] Some quantities are intentionally left fixed during initial training. | Parameter | Reason | | ------------ | --------------------------------------------------------------------- | | body mass | broad randomization reduced learning stability during initial walking | | link lengths | CAD and URDF geometry were already close to hardware | | gravity | deployment environment does not vary meaningfully | This prevents training from spending capacity on unlikely or unnecessary variability. 4\. Delay randomization is not generic noise [#4-delay-randomization-is-not-generic-noise] Observation and actuator delay randomization are especially important in this stack. These delays are not abstract robustness noise — they reflect the real CAN polling structure and firmware timing described in [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification). The randomization ranges in the table above correspond directly to the measured variation in those timing paths. 5\. Contact-side randomization [#5-contact-side-randomization] Foot friction and contact-dependent terms are also randomized because walking quality depends strongly on floor condition and contact consistency. These terms help the policy remain usable across: * slightly different surfaces * moderate contact-model mismatch * unit-to-unit variation in toe and foot response 6\. Randomization still depends on an accurate base model [#6-randomization-still-depends-on-an-accurate-base-model] Domain randomization is not a substitute for system identification. The stack first requires: * correct hardware mapping * realistic actuator parameters * stable contact geometry * deployable observation design Only after those are in place does targeted randomization improve robustness in a meaningful way. # Reinforcement Learning for Locomotion This chapter describes the policy design used for Asimov locomotion and the reasoning behind its observation interface. Asimov legs locomotion overview *Figure: Asimov legs during early locomotion development. This image anchors the policy chapter to the real hardware platform the controller was trained for, rather than to a generic humanoid benchmark or a simulator-only setup.* 1\. Locomotion as a data-interface problem [#1-locomotion-as-a-data-interface-problem] The locomotion policy is a standard feedforward neural network. The central design question is not the novelty of the network itself, but whether the policy receives the correct information at the correct time. For this reason, the locomotion problem is framed as: > How can the domain of data in simulation be made to match the domain of data on hardware as closely as possible? This framing leads to several design choices: * avoid observations that are unavailable on the robot * model timing skew and delay explicitly * give the critic access to privileged training-only information * treat actuator behavior as part of the learning problem Asimov legs walking animation *Figure: Qualitative walking result from the trained legs policy. The motion shown here is useful as a visual reference for the kind of gait the observation design, critic structure, and actuator model were intended to produce on hardware.* 2\. Actor observations [#2-actor-observations] The actor is limited to signals that can be produced on the real robot. The policy observation vector has 45 dimensions. | Observation term | Dimensions | Notes | | ------------------------- | ---------- | --------------------------------------------------- | | base angular velocity | 3 | IMU angular velocity | | projected gravity | 3 | Orientation proxy used instead of ground-truth pose | | command | 3 | Target `v_x`, `v_y`, `w_z` | | joint position groups 1-3 | 12 | Grouped by CAN timing | | joint velocity groups 1-3 | 12 | Grouped by CAN timing | | previous actions | 12 | Smoothed control history | The observation design intentionally excludes base linear velocity. 3\. No ground-truth linear velocity [#3-no-ground-truth-linear-velocity] Many locomotion baselines feed ground-truth base linear velocity into the policy. Asimov does not. The reason is simple: * the real robot does not measure ground-truth base velocity * the robot has an IMU and encoder-derived joint state * training with unavailable information encourages brittle policies If the actor depends on an observation that disappears at deployment time, transfer quality degrades immediately. 4\. Asymmetric actor-critic [#4-asymmetric-actor-critic] Training uses an asymmetric actor-critic structure. The actor is restricted to deployable observations, while the critic receives additional privileged information that improves value estimation. The critic receives everything the actor sees, plus: | Privileged term | Dimensions | Purpose | | -------------------- | ---------- | ----------------------------------- | | base linear velocity | 3 | Ground-truth motion during training | | foot height | 2 | Contact and swing-state context | | foot air time | 2 | Step timing context | | foot contact | 2 | Binary contact state | | foot contact forces | 6 | Ground interaction | | toe joint position | 2 | Passive toe state | | toe joint velocity | 2 | Passive toe dynamics | This setup allows the critic to learn from simulator-only signals without forcing the actor to depend on unavailable hardware data. 5\. Why toe state belongs in the critic [#5-why-toe-state-belongs-in-the-critic] The passive toes affect support, push-off, and recovery from forward pitching. However, they are not actively actuated and are not instrumented like the main leg joints. Toe state is therefore exposed to the critic only. This allows the training process to capture the relationship between toe deflection and stability, while still requiring the actor to infer toe behavior indirectly from: * ankle motion * IMU state * body response during stance and push-off 6\. Contact force as privileged information [#6-contact-force-as-privileged-information] Contact force is also useful during training. It helps the critic evaluate whether the robot is loading the ground in a stable way, even though the actor does not receive direct force measurements as a deployment input. In practice, this improves: * stance stability * push-off behavior * foot placement quality The resulting policy does not react to contact changes as aggressively as force-rich commercial systems, but it achieves useful and stable behavior without relying on a direct force-sensing action policy. 7\. Network structure [#7-network-structure] The policy uses a straightforward multilayer perceptron. | Network | Structure | | ------- | --------------------------------------------- | | Actor | `45 -> 512 -> 256 -> 128 -> 12` | | Critic | `(45 + privileged) -> 512 -> 256 -> 128 -> 1` | Additional settings: * activation: ELU * observation normalization: enabled * initial policy noise standard deviation: 1.0 The network design is intentionally simple. Sim2real performance came primarily from the observation and actuation interface, not from architectural novelty. # Practical Issues in Policy Deployment This chapter describes how the locomotion policy is deployed and validated on real hardware, and which practical issues mattered most during sim2real transfer. 1\. Processor-in-the-loop validation [#1-processor-in-the-loop-validation] Before deploying to hardware, the locomotion stack is validated through the processor-in-the-loop path described in [Understanding Your Simulation Environment](/guides/locomotion-training/understanding-your-simulation-environment). This section focuses on the deployment-specific validation that path enables. 2\. Jitter and delay must be exercised before deployment [#2-jitter-and-delay-must-be-exercised-before-deployment] Actuator timing on paper is not the same as actuator timing in a running system. The deployment path therefore validates controller behavior under injected delay and jitter. The actuator emulator is used to insert randomized response timing so that the stack can be tested against: * late actuator responses * race conditions in the control loop * protocol parsing issues under load * timing mismatch between sensor and control computation This turns delay handling into an integration test rather than an assumption. Representative injected delay was drawn over a range spanning approximately `0.4 ms` to `2 ms`, which was enough to expose timing assumptions in the firmware and policy loop before hardware tests. 3\. CAN ordering mattered [#3-can-ordering-mattered] One of the important deployment issues was the ordering of actuator-state data on the CAN path. Early behavior allowed requests to be issued in sequence while responses could arrive in different order. Even though this is logically acceptable at the communication layer, it created a control problem: * the policy interpreted stale and reordered state as a real physical deviation * corrective action was applied too aggressively * the resulting impulses excited oscillations across the legs The fix was to make the real IO sampling path behave more like the training environment by sampling actuators at the intended rate and waiting for the expected packet order. The important lesson is that conceptual correctness at the communication layer is not sufficient. If the data arrives in a different temporal structure than the policy expects, the control loop can still fail. 4\. Oscillation should be treated as a systems problem [#4-oscillation-should-be-treated-as-a-systems-problem] Severe startup oscillation was not solved by adding more rewards. It was the result of two interacting causes: . **Underdamped policy behavior** — the simulation KP/KD values produced underdamped dynamics, causing the policy (an MLP) to learn underdamped control behavior that it carried onto the real robot. The full failure chain is documented in [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification). . **Stale and reordered actuator-state data** — CAN packet ordering (described in Section 3 above) meant the policy was correcting against state that no longer reflected reality, amplifying the oscillation on every cycle. Both issues had to be resolved together. Fixing the gains alone was insufficient while the policy was still consuming misordered state, and fixing CAN ordering alone was insufficient while the controller was underdamped. 5\. Re-homing still matters [#5-re-homing-still-matters] Even with a trained locomotion policy, real-robot alignment before execution remains important. In practice: * small asymmetries in the real robot can produce visible drift * a careful home pose reduces bias before walking * policy quality should not be judged independently of robot setup quality This is particularly important for narrow-stance walking where small geometric biases can affect lateral balance. 6\. Resulting deployment behavior [#6-resulting-deployment-behavior] With the final stack, the legs locomotion policy achieved real-world behaviors including: * forward walking * backward walking * lateral walking * balance recovery under external pushes The same underlying policy design was used across these cases, demonstrating that the transfer strategy was robust enough for more than a single scripted motion. Deployment graph on real robot versus simulation *Figure: Joint trajectory comparison between real deployment and simulation. This comparison is included to show that sim2real success was evaluated not only by visual walking quality, but also by whether the commanded and observed motion patterns remained consistent across both domains.* 7\. Carryover to the full-body stack [#7-carryover-to-the-full-body-stack] The legs-only deployment work establishes the main ingredients that should carry into the full-body controller: * the same leg actuator architecture * the same staggered observation-delay philosophy * the same privileged toe and contact information for training * the same targeted randomization strategy The full-body system introduces more joints and more coordination demands, but the sim2real foundation remains the same. # Reward Design This chapter documents the reward design used for Asimov locomotion and the main differences from common open-source baselines. 1\. Reward design was not the main bottleneck [#1-reward-design-was-not-the-main-bottleneck] The locomotion policy did not become deployable through reward shaping alone. Stable transfer depended more strongly on: * actuator modeling * observation timing * deployable observation design * real controller constraints Reward design still matters, but it should be understood as one component of the stack rather than the sole driver of performance. The practical lesson from the legs stack is that reward changes alone did not solve transfer. The walking policy became deployable only after the actuator model, timing model, and observation interface were brought closer to hardware. 2\. Core rewards kept from existing baselines [#2-core-rewards-kept-from-existing-baselines] The Asimov reward set was heavily influenced by open-source humanoid locomotion work, especially Booster-style reward structure. Representative retained terms include: | Reward | Weight | Purpose | | ------------------ | -------------------------------- | ------------------------------------------- | | `tracking_lin_vel` | `+1.0` (base, curriculum-scaled) | Follow commanded linear velocity | | `tracking_ang_vel` | `+0.5` (base, curriculum-scaled) | Follow commanded yaw rate | | `orientation` | `-5.0` | Penalize deviation from upright orientation | | `upright` | curriculum | Maintain stable torso posture | | `action_rate` | `-1.0` | Smooth action changes | | `torques` | `-2e-4` | Encourage efficient actuation | 3\. No gait clock [#3-no-gait-clock] Some locomotion baselines provide an explicit gait phase clock to the policy. Asimov does not. This choice was made because: * Asimov kinematics are not identical to baseline robots * the ankle range is limited by the parallel mechanism * the policy should discover a gait that fits this hardware rather than follow a hand-imposed gait phase This makes the policy less prescriptive and more hardware-specific. 4\. Asymmetric pose tolerances [#4-asymmetric-pose-tolerances] Uniform pose tolerances across all joints are not appropriate for Asimov. The legs use different tolerances depending on the joint and the hardware structure. Representative walking tolerances are: | Joint | Typical tolerance | | ----------- | ----------------- | | hip pitch | `0.5` | | hip roll | `0.25` | | hip yaw | `0.2` | | knee | `0.5` | | ankle pitch | `0.2` | | ankle roll | `0.12` | The ankle tolerances are tight because the real ankle range is limited. 5\. Narrow-stance stability penalties [#5-narrow-stance-stability-penalties] Asimov has a narrower stance than many humanoid baselines. This increases lateral balance sensitivity and motivates stronger stability penalties. Representative terms include: | Reward | Weight | | ------------------ | ------- | | `body_ang_vel` | `-0.08` | | `angular_momentum` | `-0.03` | These terms help reduce large pelvis rotation and unstable whole-body motion. 6\. Contact-force limits [#6-contact-force-limits] The reward set penalizes excessive ground reaction forces. This serves two purposes: * it discourages aggressive stomping behavior * it protects the real robot from unnecessary impact loading Representative terms include: | Reward | Weight | Note | | -------------------------- | ------- | ----------------------------------------------------- | | `feet_contact_force_limit` | `-5e-4` | penalizes forces above approximately `350 N` | | `feet_stumble` | `-1.25` | penalizes large horizontal-to-vertical contact ratios | 7\. Air-time reward [#7-air-time-reward] Asimov legs are light enough to support dynamic walking with noticeable swing and brief unloaded phases. An air-time reward is therefore used to discourage shuffling behavior. Representative term: | Reward | Weight | | ---------- | ------ | | `air_time` | `+0.5` | This reward encourages dynamic gait emergence rather than static stepping. 8\. Consolidated reward table [#8-consolidated-reward-table] The legs policy used a compact reward set rather than a large collection of highly specialized terms. | Reward | Weight | Role | | -------------------------- | -------------------------- | ---------------------------------- | | `tracking_lin_vel` | `+1.0` (curriculum-scaled) | commanded linear velocity tracking | | `tracking_ang_vel` | `+0.5` (curriculum-scaled) | commanded yaw tracking | | `orientation` | `-5.0` | penalize orientation deviation | | `air_time` | `+0.5` | dynamic stepping | | `action_rate` | `-1.0` | smooth action changes | | `torques` | `-2e-4` | efficient actuation | | `pose` | curriculum | posture shaping | | `upright` | curriculum | torso stability | | `body_ang_vel` | `-0.08` | pelvis rotation penalty | | `angular_momentum` | `-0.03` | global stability penalty | | `self_collisions` | `-1.0` | reject self-contact | | `feet_stumble` | `-1.25` | discourage unstable foot strikes | | `feet_contact_force_limit` | `-5e-4` | discourage excessive ground impact | 9\. Practical lesson [#9-practical-lesson] The most important lesson from this stack is that reward design should remain consistent with the hardware interface. It is counterproductive to reward behaviors that require: * unavailable sensors * unrealistic joint range * unrealistically fast force response * contact conditions that the deployed robot cannot reproduce For Asimov, reward design works best when it reflects the real limitations and affordances of the leg hardware. # Simulation Training Environment This chapter documents the training environment used for Asimov locomotion, including policy rate, observation timing, and actuator delay. 1\. Training environment structure [#1-training-environment-structure] The training stack is organized around a MuJoCo-based environment with: * 200 Hz physics integration * 200 Hz IO-side state handling * 50 Hz policy execution * delayed actuator and observation paths * asymmetric actor-critic observations * passive toe dynamics in the plant model The environment is intentionally designed to avoid an idealized control path. In this chapter, `IO-side state handling` means the observation and actuator-side update loop. It carries raw actuator-state timing, observation delay, and related control-path computations before the policy runs. Representative physics settings inherited by the legs stack include: | Setting | Value | | ----------------- | ------- | | physics timestep | `5 ms` | | policy decimation | `4` | | policy rate | `50 Hz` | | solver iterations | `10` | 2\. Observation timing and grouping [#2-observation-timing-and-grouping] Joint observations are grouped to reflect the real CAN polling order. This means the policy does not receive all joint states as equally fresh data. | Observation group | Typical freshness | | ----------------- | ----------------- | | group 1 | oldest | | group 2 | intermediate | | group 3 | freshest | Representative delay settings are: | Group | Delay range | | ------- | ----------- | | group 1 | `0-2` steps | | group 2 | `0-1` steps | | group 3 | `0` steps | This grouped delay structure is one of the main sim2real features of the stack. In implementation terms, the oldest joint group reflects the earliest CAN reads, the middle group reflects intermediate bus timing, and the freshest group reflects the last actuator-state packets available in the loop. 3\. Observation noise [#3-observation-noise] The observation model includes moderate noise terms that reflect sensor and estimation uncertainty. Representative values include: | Quantity | Noise | | -------------------- | -------------- | | IMU angular velocity | `+/-0.01` | | projected gravity | `+/-0.05` | | joint position | `+/-0.01 rad` | | joint velocity | `+/-0.1 rad/s` | The goal is not to flood the policy with noise. The goal is to expose it to realistic sensing quality. 4\. Built-in versus IO-rate actuator computation [#4-built-in-versus-io-rate-actuator-computation] One practical lesson from early experiments was that a pristine built-in actuator path can expose the policy to cleaner data than the real system will ever produce. In contrast, the final control path intentionally allowed the policy to experience: * IO-rate computation at `200 Hz` * slower policy output at `50 Hz` * numerical roughness from the actual action and observation update path This was important because the policy needed to tolerate the same class of stale, imperfect signals it would see during deployment. 5\. Actuator interface [#5-actuator-interface] The action interface is a joint-space command interface over the 12 actuated leg joints. Important characteristics: * DC actuator model with per-joint parameters * explicit actuator delay * torque-speed saturation * friction model * policy actions applied through a slower policy loop than the physics loop Actuator delay settings and the full actuator model are documented in [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification). The action scaling rule preserved in the stack is: `action_scale = 0.30 * effort / stiffness` Another important implementation detail is that actuator damping comes from the controller path rather than from fixed XML damping on actuated joints. Default XML damping and friction-loss terms are removed for those joints to avoid double-counting dissipation. 6\. Contact and toe observables [#6-contact-and-toe-observables] The environment tracks foot contact state, contact forces, foot air time, and toe position/velocity. These signals are used by the critic and reward system even when not exposed to the deployable actor. The toe model and its role in training are described in [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification). Representative contact thresholds: | Quantity | Threshold | | -------------------------- | ------------------------------ | | foot contact observation | `5 N` vertical-force threshold | | reward-side contact helper | `10 N` threshold | 7\. Commands and operating envelope [#7-commands-and-operating-envelope] The nominal command envelope for the legs locomotion stack is conservative: | Command | Range | | ----------- | ------------- | | `lin_vel_x` | `(-0.8, 0.8)` | | `lin_vel_y` | `(-0.6, 0.6)` | | `ang_vel_z` | `(-0.6, 0.6)` | This operating envelope is appropriate for early sim2real transfer and hardware bring-up. 8\. Disturbances, resets, and play mode [#8-disturbances-resets-and-play-mode] The training environment also includes controlled perturbations and reset variation. Representative settings include: | Item | Representative setting | | --------------------------- | -------------------------------------------------------------------- | | push disturbance timing | every `1-3 s` | | push magnitude | approximately `+/-0.5 m/s` in planar velocity, `+/-1.5 m/s` in pitch | | reset yaw perturbation | `+/-pi` | | reset pitch perturbation | `+/-0.15 rad` | | reset roll perturbation | `+/-0.1 rad` | | bad-orientation termination | approximately `45 deg` tilt | Play mode disables policy corruption and push disturbance while preserving the rest of the deployment-relevant control path. 9\. PPO configuration [#9-ppo-configuration] The training setup uses PPO with a conventional configuration. | Parameter | Value | | ------------------- | ------ | | learning rate | `1e-3` | | gamma | `0.99` | | lambda | `0.95` | | clip parameter | `0.2` | | entropy coefficient | `0.01` | | learning epochs | `5` | | mini-batches | `4` | | desired KL | `0.01` | | max grad norm | `1.0` | | rollout length | `24` | The optimizer configuration is not the main differentiator of the stack. The environment fidelity and observation design are more important to transfer quality. # Understanding Your Simulation Environment This chapter describes what the locomotion simulator is expected to represent, what it intentionally does not represent, and why those boundaries matter for sim2real transfer. 1\. Simulation is part of the control stack [#1-simulation-is-part-of-the-control-stack] For Asimov, the simulator is not treated as an isolated physics sandbox. It is treated as one component in a larger control stack that includes: * the robot kinematic and dynamic model * actuator behavior and delay * sensor noise and observation timing * the firmware path used to compute control-relevant signals * the policy observation and action interface This viewpoint is important because many real deployment failures are not caused by large physics mismatches. They are caused by timing skew, stale observations, bus jitter, or control signals that are computed differently in simulation and on hardware. 2\. Scope of the locomotion model [#2-scope-of-the-locomotion-model] The current locomotion stack is built around the legs-only robot: * 12 actuated joints across both legs * 2 passive toe joints * a parallel ankle mechanism rather than a simple directly driven serial ankle The simulator uses rigid-body dynamics, but needs to capture more than simple serial-chain kinematics. It also needs to reflect: * ankle pitch and roll mapping through the parallel mechanism * passive toe compliance and toe-ground interaction * actuator saturation, friction, and delay * contact geometry under the foot and toe 3\. Simulator rates and control rates [#3-simulator-rates-and-control-rates] The training environment uses separate rates for physics, IO, and policy execution. Here, `IO` means the observation and actuator-side update path, not policy inference itself. It is the loop that carries raw actuator-state timing, observation delay, and control-path bookkeeping before the policy consumes the resulting state. | Layer | Rate | Role | | --------------------- | ------ | ------------------------------------------------------------------------------------ | | Physics step | 200 Hz | Integrates robot dynamics | | Observation / IO path | 200 Hz | Updates actuator-state and actuator-side signals, including timing and delay effects | | Policy step | 50 Hz | Produces commanded joint targets from the latest available IO state | These separate rates matter because the policy is not trained on infinitely fresh simulator state. It is trained on data that already reflects timing artifacts in the control loop. 4\. Do not trust pristine simulator data [#4-do-not-trust-pristine-simulator-data] A default simulator exposes perfectly synchronized state: * all joint observations arrive at once * sensor values are available without bus latency * actuators respond at fixed timing * projected gravity and orientation can be computed from ideal simulator state This is useful for debugging, but it is not the data that real hardware provides. The locomotion stack therefore avoids building the policy around privileged measurements that do not exist on the robot. 5\. Processor-in-the-loop environment [#5-processor-in-the-loop-environment] Asimov extends the simulation boundary beyond rigid-body dynamics by running the real firmware path inside the validation loop. Processor-in-the-loop architecture *Figure: Processor-in-the-loop architecture used for sim2real validation. The important point is that the control loop is closed through the real firmware path, with MuJoCo providing simulated IMU signals and the communication stack preserving the same software interfaces used on the robot.* The processor-in-the-loop path includes: * virtual CAN on Linux through the same SocketCAN software interface used by the actuator stack * a MuJoCo bridge that reads `imu_ang_vel` and `imu_lin_acc` * UDP transport of simulated IMU data into firmware * the same FusionX path used on hardware to compute projected gravity for the policy This design avoids a common failure mode where simulation uses a simplified software path that cannot exist on the real robot. Injected CAN bus jitter *Figure: CAN bus delay and jitter injection used to test timing robustness before hardware deployment. This figure is included here because locomotion transfer depended not only on rigid-body physics, but also on whether the simulated control path exposed the same latency variation and stale-data behavior seen on the real system.* 6\. Contact geometry and collision stability [#6-contact-geometry-and-collision-stability] Foot-ground contact must be stable and interpretable. For this reason, the locomotion environment uses explicit collision primitives instead of relying on detailed mesh collision for learning. Key choices include: * capsule-based foot and toe contact geometry * explicit toe and foot contact points * contact tuning on foot and toe geometry * conservative, repeatable contact behavior rather than maximum geometric fidelity This reduces the risk that the policy learns from unstable collision artifacts. 7\. Hardware structure still constrains the simulator [#7-hardware-structure-still-constrains-the-simulator] The simulation environment is specific to the Asimov leg design, not a generic humanoid simulator. The hardware constraints that shape the simulation — the parallel ankle mechanism, passive toe joints, actuator limits, and joint ranges — are documented in [Joint Design and Actuation](/asimov/0/hardware/joint-design-and-actuation) and [Deep Dive: System Identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification). # Asimov Manager Asimov Manager is the console that runs on the robot's Raspberry Pi 5 — think of it as the robot's router login page. You use it to set up, start, drive, update and troubleshoot Asimov 1 from a browser. There is no app to install and no cloud account. Open it at `http://.local`, where `` is the hostname you set when flashing the Pi 5. The console serves plain HTTP on port 80. Until you set a password, the login is pre-filled, so the first sign-in is one click — see [Connect & Sign In](/asimov/1/operate/setup/connect). In the console, **RPU** means the Robot Processing Unit in the torso, whose Radxa CM5 runs the motion-control firmware. Asimov Manager's Overview: the main action, the subsystem strip and the actuator roster Pages [#pages] | Group | Page | What it's for | | ----------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | — | **Overview** | The robot's vital signs and its main action — see [Overview](#overview) | | — | **Cockpit** | Drive the robot — see [Stand Up and Walk](/asimov/1/operate/drive/stand-up) | | Robot | **Actuators** | Assign IDs, check and calibrate actuators — see [Calibrate Actuators](/asimov/1/operate/setup/calibrate-actuators) | | Robot | **Firmware** | Confirm assembly, start and stop the firmware, Start on boot and the RPU link — see [First Start](/asimov/1/operate/setup/first-start) | | Connections | **Wi-Fi** | Join and forget networks — see [Connect & Sign In](/asimov/1/operate/setup/connect#join-your-wi-fi) | | Connections | **Controller** | Pair a Bluetooth gamepad — see [Gamepad](/asimov/1/operate/drive/gamepad) | | System | **Updates** | Install a software update — see [Software Updates](/asimov/1/operate/software-updates) | | System | **Developer** | Issue credentials for the SDK — see [Developer Credentials](#developer-credentials) | | System | **Advanced Settings** | Every setting on the robot, raw. Prefer the dedicated pages where they exist | | System | **Troubleshoot** | Health checks, service restarts and logs — see [Troubleshooting](/asimov/1/operate/troubleshooting) | | User menu | **Account** | Password, appearance, version and settings reset — see [Account and Access](#account-and-access) | On a phone, the sidebar becomes a menu behind **Open Menu** in the top bar. The sidebar's device card shows the hostname, overall health, battery and any connected gamepad on every page. The E-Stop is in the page header of Overview, Firmware, Wi-Fi, Controller, Account and Troubleshoot on a desktop, and in the top bar of every page on a phone — see [Stopping the Robot](/asimov/1/operate/safety/stopping#e-stop). Overview [#overview] Overview has one main action, which depends on the robot's state: | Firmware | Main action | | ----------------------------------- | ---------------------------------------- | | Stopped | **Start Robot** | | Running | **Drive Robot**, plus **Turn Off Robot** | | RPU unreachable or not controllable | None — fix the link first | Its panels: | Panel | What to look for | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Battery | A percentage, amber below 50% and red below 20%. A blank reading means Asimov Edge isn't running | | Subsystem strip | **Firmware** (**Running**, **No RPU control** or **RPU unreachable**), **Controller** (the paired gamepad, or **Not connected**) and **Wi-Fi** (your network's name) | | Actuator roster | Each joint's result from the last actuator check, such as **25 of 25 responding · checked** and when. **not current** means the result is old, not that anything failed | | Software & services | The installed release, and **Video streaming** — whether the robot's video service (LiveKit) is running. The Cockpit doesn't show video in this release | | Logs | The newest lines across services, with **View logs** for the full viewer | The Setup Checklist [#the-setup-checklist] Until the robot is fully set up, the sidebar shows a **Finish setting up** checklist. [The Setup Checklist](/asimov/1/operate/setup#the-setup-checklist) explains each row. Account and Access [#account-and-access] Password [#password] Change it on **Account**, from the user menu — see [Set a Password](/asimov/1/operate/setup/connect#set-a-password). Forgotten Password [#forgotten-password] Asimov Manager has no reset link. Over [SSH](#command-line), clear the stored password: ```bash sudo asimovctl config reset manager.password_hash ``` The login goes back to the pre-filled default. Sign in and set a new password straight away. Reset All Settings [#reset-all-settings] **Reset All Settings**, under Advanced on the Account page, returns the robot's RPU, video (LiveKit) and Asimov Edge settings to factory defaults. Asimov Edge's parameters, including the robot's serial, go back to their defaults too. It keeps your login, saved Wi-Fi networks, SDK credentials, the assembly confirmation, and actuator IDs and calibration. {/* TODO(manager): the confirmation says "the robot's identity is kept", but the reset restores asimov_edge.params, including serial = "MENLO-0001". Fix the copy or the scope, then update this section. */} Developer Credentials [#developer-credentials] A developer credential lets a computer running the [Python SDK](/sdk/connection-modes) join the robot's LiveKit room. You need one to connect the SDK in its **hybrid** or **livekit** connection mode, which is also how the SDK sees the camera and hears the microphone. The Cockpit needs no credential, and neither does the SDK's **udp** mode. The robot runs its own LiveKit server and is set up to use it from the first boot, so there is nothing to configure before you issue a credential. **Issue a Credential** Open **Developer** in the sidebar. Under **Issue a credential**, name the computer that will use it, choose **Observe** or **Control**, and select **Issue credential**. The credential is shown once, so copy it then; if you lose it, revoke it under **Issued credentials** and issue another. [Drive from Python](/asimov/1/operate/drive/python-sdk#get-an-sdk-credential) walks through the page step by step and connects the SDK with `menlo setup`. **Observe and Control** | Role | What it allows in the LiveKit room | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Observe** (default) | Watch the camera and listen to the microphone. The LiveKit server refuses its commands, whatever program holds it | | **Control** | Everything Observe allows, plus sending commands and talking through the speaker. Commands still pass through the robot's safety layer, and the Cockpit keeps priority | The role limits driving only in the **livekit** mode. In **hybrid**, commands travel over UDP on the robot's network, which has no sign-in, so an Observe credential still gets the camera and microphone and the SDK can still drive. Hybrid also needs the robot to accept UDP commands and send state to your computer; see [Hybrid](/sdk/connection-modes#hybrid). **Revoke a Credential** Under **Issued credentials**, select **Revoke** next to the credential and confirm. The computer holding it can no longer connect. A session that is already running keeps working until its current LiveKit token expires, at most 12 hours. **Reset All Settings** keeps issued credentials. **The LiveKit Secret Stays on the Robot** The credential is not the LiveKit key or secret. Each time the SDK connects, it trades the credential for a short-lived LiveKit token from Asimov Manager, so the key and secret never leave the robot and you never need them. To use a LiveKit server other than the robot's own, set **LiveKit URL**, **LiveKit key** and **LiveKit secret** under **Asimov Edge** in **Advanced Settings**, and select **Save & Restart Asimov Edge**. Saving checks that the server accepts the key and secret first. If the **LiveKit URL** row on the Developer page reads **not set**, the robot publishes no video or audio until you set it here. Advanced Settings, Asimov Edge: LiveKit URL, LiveKit key and LiveKit secret Command Line [#command-line] Everything the console does is also available over SSH on the Pi 5. Sign in with the username and password you set in Raspberry Pi Imager when [flashing it](/asimov/1/build/assembly-preparations/software-setup/flash-raspberry-pi), then use `asimovctl` with `sudo`: ```bash asimovctl status # services, RPU link and setup state asimovctl start|stop|restart # asimov-edge, asimov-firmware or livekit-server asimovctl enable|disable # start on boot asimovctl config show # every setting, secrets masked asimovctl config set asimovctl config reset asimovctl apply # write settings to the services asimovctl reset-setup --yes # undo Confirm assembly; stops the firmware asimovctl update status # the current software update ``` The browser and the command line control the same services, so anything you start from one you can stop from the other. What It Manages [#what-it-manages] Asimov Manager and Asimov Edge run on the Pi 5. Asimov Edge carries commands and telemetry between the console and the RPU, whose Radxa CM5 runs the motion-control firmware. The Pi 5 and RPU share a private Ethernet link, which the console reports as two checks: **Network** (the RPU answers on the wire) and **Control** (the console can manage it). An unplugged cable and a stopped service never look the same. What It Doesn't Do [#what-it-doesnt-do] * **Run the control loop.** The firmware on the CM5 does; the console starts it, stops it and relays your drive input. * **Command actuators while the firmware runs.** The Actuators page works only with the firmware stopped. * **Show video.** The Cockpit's camera panel reads **No video** in this release. * **Restart or shut down the robot's computers.** * **Manage more than one robot.** There is no fleet view. * **Flash boards.** It is installed and updated as part of the robot's software. # Power On and Off A session with a commissioned robot runs: power on, start the robot, [drive](/asimov/1/operate/drive/stand-up), turn the robot off, power off. Power On [#power-on] . Support the robot on its gantry hook or a bench. . Power it on at the battery unit. . Wait about two minutes. The Raspberry Pi 5 joins a saved Wi-Fi network, or starts its setup hotspot if it can't, and its link to the Robot Processing Unit (RPU) comes up. . Open `http://.local`. {/* TODO(hardware): confirm the power-on control on an assembled robot (the battery unit's button?) and add a photo. */} The firmware starts by itself only if its **Start on boot** switch is on, which it isn't by default. Otherwise the actuators stay unpowered and the robot stays limp until you start it. Start the Robot [#start-the-robot] . Check that the robot is supported and everyone is clear of it. . On **Overview**, select **Start Robot** and confirm. Start Robot starts Asimov Edge if it isn't running, then the firmware. The actuators come on in Damp: powered, but limp. If **Start Robot** is greyed out, its tooltip says why — most often the RPU is unreachable, or assembly was never confirmed (confirm it on the **Firmware** page, as in [First Start](/asimov/1/operate/setup/first-start#confirm-assembly)). While an actuator check is running, the button reads **Waiting for actuator check…**. Once the firmware is running, Overview's main action becomes **Drive Robot**. Before you drive, glance at Overview: a battery percentage, **Running** for the firmware, and **25 of 25 responding**. [Overview](/asimov/1/operate/asimov-manager#overview) explains every panel. Turn the Robot Off [#turn-the-robot-off] . If the robot is walking, bring it back down first — see [Bring It Back Down](/asimov/1/operate/drive/stand-up#bring-it-back-down). . Make sure it is supported. . On **Overview**, select **Turn Off Robot** and confirm. This stops the firmware and nothing else. All 25 actuators go offline and driving stops; Asimov Edge and the console stay up, so the robot is still reachable. **Stop Firmware** on the Firmware page does the same, and works whenever the firmware is running. Power Off [#power-off] . Turn the robot off, as above, with it seated or hanging from its gantry hook. . Press the button on the battery unit. Asimov Manager can't restart or shut down the robot's computers. {/* TODO(hardware): confirm whether the Pi 5 needs a clean shutdown before power is removed. */} Start on Boot [#start-on-boot] The **Start on boot** switch on the Firmware page starts the firmware automatically whenever the robot powers on. It is off by default, and available once assembly is confirmed. Leave it off unless the robot lives somewhere it is always safely supported. With it on, the actuators power up after every power cut, with nobody there to check. The Firmware page with the firmware running: Restart, Stop Firmware and the Start on boot switch Battery and Charging [#battery-and-charging] The battery level shows in the sidebar's device card, the phone top bar, Overview and the Cockpit header. It turns amber below 50% and red below 20%. The Cockpit's Telemetry tab has the detail: charge, voltage, current, maximum cell temperature and protection flags. Asimov Manager doesn't warn you or act on a low battery, so keep an eye on the level. {/* TODO(hardware): charging procedure — connector, whether it can charge while running, storage charge level, runtime, and what the robot does at low battery (Asimov Edge drives a battery-warning buzzer; threshold unknown). */} # Software Updates Asimov 1 updates as one unit: a single signed file updates the robot's apps and the Radxa CM5 firmware for the Robot Processing Unit (RPU) together. You install it from the **Updates** page in Asimov Manager, with no terminal needed. The Updates page with an update ready: Update Now, the installed versions, and the ways to get an update Get the Update File [#get-the-update-file] Updates come as a signed `.mender` file of up to 2 GB. Fetching updates online isn't available yet — **Check Now** is disabled — so add the file by hand. {/* TODO: say where owners download the .mender update file. */} Install an Update [#install-an-update] . Support the robot and [turn it off](/asimov/1/operate/power#turn-the-robot-off). The update restarts the RPU. . Open **Updates**, select **Choose File…** and pick the `.mender` file. . Start the update. You can leave the page and come back — the progress card tracks the update either way. An update isn't finished when it installs: the robot runs a health check on its services first, then reports the result. | Result | Meaning | | ---------------------------------------- | --------------------------------------------------------------------------------- | | **Updated to Robot Software \** | Done. The services stayed healthy for the whole check | | **Update rolled back** | Something failed the health check, and the robot returned to the previous version | | **Update undone** or **Update closed** | The update was reversed or abandoned before it finished | | **Update failed** | It didn't install. The log on the page says why | Select **OK** to dismiss the result. If the page instead says an update was interrupted — for example **The robot restarted during an update** — select **Finish Update** to run the health check again. Messages such as **The controller kept the new version** point to recovery steps that are run over SSH. If you see one without having done anything unusual, note the exact message and contact support rather than improvising. Check Versions [#check-versions] * **Updates → Installed** lists **Robot Apps** and **Controller (RPU)** — the CM5 firmware. * Overview's **Software** row shows the installed release. * **Account** shows the console's own version, which matches the release. Updates don't change actuator IDs, calibration or the assembly confirmation. # Troubleshooting Start from the symptom. Most fixes involve the **Troubleshoot** page, which shows the robot's health checks — the Robot Processing Unit (RPU) link, the video service and the Bluetooth radio — with a **Restart** button for each service. **Check again** runs every check again. **Restart** on the **Firmware (RPU)** row restarts the firmware immediately, which takes the actuators offline. Support the robot before using it. The Troubleshoot page on a healthy robot: RPU reachable, connectivity and per-service restarts The Console Will Not Load [#the-console-will-not-load] | Symptom | What to do | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `http://.local` doesn't answer | Allow two minutes from power-on — longer if the robot falls back to its hotspot — and check that your device is on the same network as the robot | | `.local` doesn't resolve | Common on Android. On the setup hotspot, use `http://10.42.0.1`. On your own network, use the address your router assigned the robot | | It loads on the hotspot but not on your Wi-Fi | Some routers, especially guest networks, stop devices from reaching each other. Use your main network | | The robot never joins a 5 GHz network | Try a 2.4 GHz network, or a 5 GHz channel from 36 to 48 | | The setup hotspot never appears | It starts only when the robot has nothing to join: at power-on, after a failed join, or after you forget its last network | | The robot dropped off your Wi-Fi | The hotspot doesn't return on its own once the robot has booted. Power-cycle the robot so it rejoins or starts the hotspot | The RPU Is Unreachable [#the-rpu-is-unreachable] Nothing that moves the robot works until this link is healthy. The **Firmware** page splits it into two checks: | Failing check | Meaning | What to do | | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Network** | The RPU isn't answering on the wire | Check the Ethernet cable between the Raspberry Pi 5 and the Radxa Carrier Board, and power to the Head Compute Unit and RPU. Power down before reseating connections | | **Control** | The RPU answers, but the console can't manage it | The RPU is still booting or its services are down. Wait a minute and reload the page; if it persists, power-cycle the robot | While the RPU is unreachable, the setup checklist greys out the rows that depend on it ("Waiting on the RPU"), and the Actuators and Firmware pages lock their tools. That is expected, not a second fault. The Firmware Will Not Start [#the-firmware-will-not-start] | Symptom | What to do | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | **Start Robot** or **Start Firmware** is greyed out | Read its tooltip, which names what's missing | | Assembly was never confirmed | Confirm it on the **Firmware** page — see [First Start](/asimov/1/operate/setup/first-start#confirm-assembly) | | **Waiting for actuator check…** | An actuator check is using the CAN buses for about 15 seconds. Wait, then try again | | The RPU is unreachable | Fix the link first — see above | The Actuators Page Is Locked [#the-actuators-page-is-locked] Its tools work only with the firmware stopped and the RPU reachable. The lock pill names the cause — for example **Stop the firmware to change actuators**. Follow its **Open Firmware ›** link, fix the cause, then select **Retry**. The Robot Will Not Move [#the-robot-will-not-move] See [If You Cannot Drive](/asimov/1/operate/drive/cockpit#if-you-cannot-drive). The usual causes: no session, the robot isn't in Walk, Asimov Edge isn't running, or a gamepad has control. The Robot Stopped or Fell on Its Own [#the-robot-stopped-or-fell-on-its-own] The firmware damps the robot when it detects a fault. Look for **fault N** on the Cockpit's status line and for **Error flags** in its Telemetry tab, then check the **Firmware** log for that moment. An overheating actuator and a lost command stream look the same from outside, and completely different in the log. To get going again, follow [Recovering After an E-Stop or a Fall](/asimov/1/operate/safety/stopping#recovering-after-an-e-stop-or-a-fall). The Robot Drifts, Stumbles or Leans [#the-robot-drifts-stumbles-or-leans] Suspect a zero point, especially after a repair — see [Recalibration After Repairs](/asimov/1/operate/setup/calibrate-actuators#recalibration-after-repairs). Rule out wiring and actuator faults first. You Forgot the Password [#you-forgot-the-password] See [Forgotten Password](/asimov/1/operate/asimov-manager#forgotten-password). Reading the Logs [#reading-the-logs] The **Logs** viewer on the Troubleshoot page shows five sources — **Edge**, **LiveKit**, **RPi kernel**, **Firmware** and **RPU kernel** — newest first, updating live. Scroll down for older entries. **RPi kernel** is the Pi 5's kernel log; **RPU kernel** is the kernel log from the Radxa CM5 in the RPU. Asimov Manager keeps the newest 4,000 entries per source. Older history survives a restart only if persistent logging is enabled on that computer. There is no export, so when you report a problem, copy the relevant lines or take a screenshot while they are still there. The Cockpit's Logs tab shows the Edge and Firmware logs without leaving the Cockpit, and Overview's Logs panel shows the newest lines across services. # Overview Asimov 1 represents a more open approach to humanoid robotics, giving researchers and developers the freedom to understand, repair, customize, and develop the robot across its full stack. Asimov 1 humanoid robot, front view | Specification | Asimov 1 | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Height | Approximately 1.2 m (3.94 ft) | | Weight | Approximately 35 kg (77 lb) | | Powered joints | 25 | | Legs | 6 powered joints per leg | | Arms | 5 powered joints per arm | | Body and neck | 1 waist joint and 2 neck joints | | Compute | Radxa CM5 for motion control; Raspberry Pi 5 for media and networking | | Sensing and media | IMU, monocular camera, stereo microphone array, and speaker | | Power | Rechargeable 13S4P lithium-ion battery system | The Compute Module consists of two connected stacks, the **Head Compute Unit** and the **Robot Processing Unit (RPU)**: * The Head Compute Unit contains the Pi 5, Head Board, and Media HAT Board. The Media HAT Board provides the buzzer and brownout protection. * The RPU contains the Radxa Carrier Board, CM5, and Power Distribution Board (PDB). The Carrier Board with the CM5 mounted forms the **Motion Control Board (MCB)**; the bare Radxa Carrier Board is one component, not the complete MCB. Key Features [#key-features] A Fully Open Platform [#a-fully-open-platform] Asimov 1 makes its mechanical design, electrical design, simulation models, firmware, and bill of materials available directly to builders. View on GitHub ↗ Coming soon Bill of Materials [#bill-of-materials] Enter your email below to receive the Asimov 1 Bill of Materials. Software Availability [#software-availability] | Capability | Status | | -------------------------------- | ----------------------------------------------- | | Full-body locomotion policy | Available in the supplied firmware | | Directional remote teleoperation | Available | | Direct actuator control | Available through the Asimov Edge or Asimov SDK | | Robot telemetry | Available at 10 Hz | | MuJoCo and URDF models | Provided with the Asimov 1 files | | Full-body teleoperation | In development | | Asimov Client SDK | In development | | Menlo Platform | Coming soon | *** # Tools & Supplies These are the tools you'll need to assemble an Asimov 1, along with some optional extras. Use this guide to prepare your workbench, see what each tool is for, and find the products we use ourselves. The linked products are references, not a requirement to buy a particular brand. None of these links are affiliate links or advertisements; they're simply what we use, so feel free to choose suitable tools from your favourite brand instead. Required Tools and Supplies [#required-tools-and-supplies] These tools and supplies are needed for standard assembly, including the wire joins required at certain actuator joints and basic electrical checks.

Used for most of the robot's assembly. Make sure you have 2, 2.5, 3, 4, 5, and 6 mm keys available.

As an Asimov customer, you do not need to buy this. A metric Allen-key set is included free with your Asimov order or preorder and arrives with your robot delivery.

Used to secure the battery switch to the battery casing. Choose a Phillips bit that fits the screw head correctly.

Used to trim wire ends or small parts where an assembly step calls for cutting. Check the cutter's material and wire-size rating before use.

Used to remove insulation from wires before connecting actuators at certain joints.

Used to shrink heat-shrink tubing over completed wire joins. Control the heat and keep it away from nearby plastics and other heat-sensitive parts.

Used with solder to join wire ends at the joint locations identified in the assembly instructions. Use a suitable tip and temperature for the wire and solder.

Holds wire ends steady while you solder them together. Position the clamps so they do not damage the insulation or strain the wire.

Needed for continuity checks after making wire connections to confirm the intended electrical paths are connected. Also used for voltage checks when debugging the electrical system. Perform continuity checks only with the battery disconnected and the circuit unpowered.

Used with the soldering iron to make the documented wire joins. Lead-free solder still requires suitable fume control.

Insulates soldered wire joins. Select tubing that fits over the join and shrinks to a secure fit; slide it onto the wire before soldering when the join would prevent fitting it afterward.

Helps prevent specified bolts from loosening after assembly. Apply to the threads as directed before tightening; do not substitute a high-strength grade or apply it to every fastener indiscriminately.

Secures wires to metal at specified routing locations to help prevent pinching. Check material compatibility and keep adhesive away from connectors and moving joints.

Used for temporary labels, notes, and holding parts, wires, nuts, or bolts in place during assembly. It is not a substitute for electrical insulation or permanent wire retention.

Optional Tools and Workshop Equipment [#optional-tools-and-workshop-equipment] These items can make assembly or troubleshooting easier, but you do not need to buy them for standard assembly. This section also includes optional teleoperation equipment and development and prototyping equipment that we use in our workshop; customers do not need any of it to assemble the robot.

Our open-source crane for lifting and positioning the robot. Build guide, BOM, and design files are on GitHub.

Our open reference design for a sturdy support stool to sit on or position the robot during setup and maintenance.

We will provide the GitHub for this soon.

Can help gently seat a tight-fitting part where the assembly instructions permit it. Check alignment and obstructions first; do not force a defective part or strike an actuator, bearing, or electronics.

Speeds up repetitive tightening and loosening instead of turning every fastener manually. Start threads by hand and use the final-tightening method specified in the assembly step to avoid cross-threading or over-tightening.

Warning: a cordless screwdriver increases the risk of stripping bolt heads or threads. Avoid using one unless you are experienced with powered screwdrivers and torque control. Use hand tools if you are unsure.

Used to visually locate unusually warm areas when investigating possible overheating. It does not replace the robot's temperature monitoring or safety checks.

Helps capture fumes near the soldering work area. This is the desktop unit we use; an appropriate existing extraction setup may serve instead.

Follow the manufacturer's positioning and filter-maintenance instructions, and provide suitable ventilation.

Used with the PICO Motion Tracker for teleoperation and control of Asimov. This headset is optional and is not required to assemble the robot.

Used with the PICO 4 headset for teleoperation and control of Asimov. This tracker is optional and is not required to assemble the robot.

Used for PCB heating during electronics development and prototyping.

A workshop tool for fabrication and prototyping.

Used to machine parts for development and prototyping.

Used to inspect electrical signals during advanced electronics debugging and prototyping.

Used to make prototype parts and iterate on designs.

Used in our workshop for battery inspection and advanced electrical diagnostics.

Capabilities vary across the linked series. Battery DC-current measurements require a DC-capable model.

# Python SDK `menlo-sdk` drives the robot from Python: stand, balance, walk, joints, state, camera and audio, over the robot's network or its LiveKit room. It is a client of Asimov Edge, so every command passes through the robot's safety layer, and the Cockpit and a paired gamepad outrank it. Two guides cover it. [Drive from Python](/asimov/1/operate/drive/python-sdk) is the hands-on path: the robot's settings, a credential, install, and a first walk with video and sound. The [Python SDK](/sdk) section is the depth: every connection mode, verb, error and example. For a first run, hang the robot from its gantry hook with both feet on the floor, and give a walk 2 m of clear floor ahead. Nothing in the SDK is an emergency stop: keep the E-Stop in Asimov Manager open, or be ready to cut power at the battery unit. Read [Safety](/sdk/safety) before the first run. Install [#install] Python 3.12 or newer. ```bash pip install menlo-sdk menlo setup ``` `menlo setup` asks for a connection mode and what it needs, checks them against the robot, and saves the robot, so every script and command below finds it without arguments. The `udp` mode needs the robot to send UDP state to your computer; `hybrid` and `livekit` need an SDK credential from the Developer page in Asimov Manager ([Drive from Python](/asimov/1/operate/drive/python-sdk#get-an-sdk-credential)). Run the Examples [#run-the-examples] ```bash git clone https://github.com/menloresearch/menlo-sdk.git cd menlo-sdk/examples ``` . `python check.py` reports what the robot reports: robot mode, arming, faults, alerts, the hottest joint and the battery. It sends nothing. . `python stand.py` stands the robot up and returns once it is armed. Hang the robot from its gantry hook with both feet on the floor first: STAND has no balance loop. . `python balance.py` puts the robot in MOVE at zero velocity, where the walking policy balances it. Slacken the gantry only after it returns. . `python walk.py` walks forward at 0.3 m/s for 3 s, then balances in place. Give the robot 2 m of clear floor ahead. . `python rest.py` brings the robot to rest: MOVE, then STAND, then DAMP. It asks first whether the robot is hanging from its gantry hook or seated on a stool or bench. `python damp.py` puts the robot in DAMP directly: every actuator stops holding its position, so a standing robot falls. Hang it from its gantry hook or seat it on a stool or bench first. . `python keyboard.py` drives from the keyboard: `w`/`s` walk, `a`/`d` strafe, `q`/`e` turn, space balances in place, `t` stands (asks first in MOVE), `b` damps after asking, `x` quits. It walks the robot backward and sideways too: keep clear floor all around it. The SDK refuses a command only when there is no live state; the firmware decides the rest. Every script that moves the robot calls `guard(robot)` from `guard.py` first, an optional guard of your own that stops the script on a fault, an alert, a hot actuator or a low battery. Edit its limits, or delete the line. The rest of the scripts, including the camera, microphone and speaker, are in [Examples](/sdk/examples); the sequence and why it is ordered this way are in [Standing and Balancing](/sdk/safety#standing-and-balancing). Operate from the Command Line [#operate-from-the-command-line] The same verbs, without a script: ```bash menlo status menlo stand menlo balance menlo walk --vx 0.3 --duration 3 menlo damp ``` `menlo status` sends nothing. `menlo stand`, `menlo balance`, `menlo walk` and `menlo damp` show the robot's facts and ask `Proceed? [y/N]` before they send, unless you pass `-y`. `stand`, `balance` and `walk` refuse only when there is no live state. See [Command Line](/sdk/cli). Learn More [#learn-more] * [Quickstart](/sdk/quickstart): the check, stand, balance, walk and damp scripts, line by line * [Move the Robot](/sdk/move): velocity, holding it, limits, ending a walk * [Connection Modes](/sdk/connection-modes): `udp`, `hybrid` and `livekit` * [SDK Reference](/sdk/reference): every class, verb and error # Resources This section will collect further reading, community and support links, videos, and other useful Asimov 1 material. Content is TBC. # Train Asimov 1 Guidance for deploying a trained policy onto Asimov 1 — simulation-to-hardware transfer, emulator testing, and on-robot rollout — is coming soon. This page is TBC. Training Tutorials [#training-tutorials] The locomotion training tutorials live in the [Training](/guides/locomotion-training) section: * [Understanding your simulation environment](/guides/locomotion-training/understanding-your-simulation-environment) * [Reinforcement learning for locomotion](/guides/locomotion-training/reinforcement-learning-for-locomotion) * [Simulation training environment](/guides/locomotion-training/reinforcement-learning-simulation-training-environment) * [Reward design](/guides/locomotion-training/reinforcement-learning-reward-design) * [Domain randomization](/guides/locomotion-training/reinforcement-learning-domain-randomization) * [System identification](/guides/locomotion-training/reinforcement-learning-deep-dive-system-identification) * [Policy deployment](/guides/locomotion-training/reinforcement-learning-policy-deployment) # 3D Printing and Fabrication This chapter organizes the mechanical build around two fabrication paths: * parts that can be produced with additive manufacturing * parts that should be outsourced because they require higher strength, tighter tolerances, or a different process 1\. Part Naming Convention [#1-part-naming-convention] We named the CAD files so you can quickly identify the required material for each part. Each part name includes a fabrication-code letter indicating its intended manufacturing method and material family: alt text *Figure: Naming convention for the part manufactorying* * `A` parts are not standard 3D printed parts. If a part is labeled as Aluminum 7075, it should be CNC machined. * `B` parts are 3D printable in 316L stainless steel, but only with a metal printing process that supports SLM 316L. * `C` parts are 3D printable in PA12 nylon, but only with a polymer printing process that supports PA12, such as SLS or MJF. * `X` parts are off-the-shelf components and should be purchased directly rather than fabricated. NOTE: . The fabrication method must match the material code in the part name. . Making parts with FDM 3D printing will not guarantee the tolerances and required strength for the system. You can also refer to [Mechanical Design Frame](/asimov/0/hardware/frame-and-structural-components) to identify which module each part belongs to by mapping part indices to the module part lists. After gathering all the parts, you can [start preparing the tools and finally start assembly](/asimov/0/assembly-manual)! # Frame and Structural Components This chapter describes how the Asimov humanoid frame is organized, how major structural modules connect, and why the structural design choices were made.