Repository navigation
Releases: menloresearch/menlo-sdk
Release list
menlo-sdk 0.1.0
Added
connect()checks the robot's model and Robot OS version before any control opens: over
udp and hybrid from Asimov Edge's/api/version, on livekit from Asimov Edge in the room.
This SDK supportsasimov_1on the Robot OS0.2line.UdpConfig(version_port=..., version_timeout=...)points the check at another port and sets its time budget;
connect(allow_unsupported_target=True)waives an unsupported model or Robot OS line
during development, not a missing or unreadable version.CompatibilityError, aConnectErrorsubclass, for a robot whose model or Robot OS
facts are malformed or unsupported; failing to fetch them raisesConnectError.menlo setupandmenlo robots addrecordCompatibilityErroras a failed connection
check, so the setup wizard can offerSave anyway?and the add command can show its
--no-checkhint.
Changed
- Breaking: update the robot to Robot OS 0.2.3 or newer before upgrading the SDK. An older
robot has no version endpoint, soconnect()raisesConnectError, and
allow_unsupported_targetdoes not waive that.
Install
pip install menlo-sdk==0.1.0Commits since v0.1.0rc10
- release(sdk): 0.1.0 (menloresearch/robot#171) (568385a)
- sdk(changelog): the Robot OS preflight entries belong to Unreleased (menloresearch/robot#169) (906bfe4)
- fix(sdk): raise compatibility failures to callers (menloresearch/robot#162) (a79860b)
- feat(sdk): forward-port Robot OS compatibility preflight (menloresearch/robot#161) (16e0342)
Full diff: v0.1.0rc10...v0.1.0
menlo-sdk 0.1.0rc10
Added
- On
livekit, a non-zero velocity and a trajectory travel as frames on the SDK's own data
trackcommandswhen the robot says its Asimov Edge reads it (participant attribute
asimov.commands_track = "1"). A lost frame is replaced by the next one instead of holding
the stream back behind a resend: at 10 % loss and 50 Hz, the oldest velocity the robot took
was 43 ms old on the track and 3 s old as packets. Mode commands and a zero velocity stay
reliable packets. A robot without the attribute gets every command as a packet, as before.
Changed
LICENSEcarries the copyright line, Copyright 2026 Menlo Research Pte. Ltd.- CI no longer runs the integration job that needed access to Asimov Edge's source.
examples/guard.pyis an example only: the motion examples no longer call it by default.
Each carriesfrom guard import guardandguard(robot)commented out, to uncomment.
ItsMAX_JOINT_TEMP_Cdefault is 80 C, where the firmware latches DAMP.
Install
pip install menlo-sdk==0.1.0rc10Commits since v0.1.0rc9
- release(sdk): 0.1.0rc10 (menloresearch/robot#142) (58318b9)
- livekit: streamed setpoints on the client's own data track (menloresearch/robot#137) (285318d)
- sdk: copyright in LICENSE; the public CI no longer clones Asimov Edge (menloresearch/robot#136) (701b52b)
- sdk(examples): guard.py is an example, off by default in the motion scripts (menloresearch/robot#124) (12e9e06)
Full diff: v0.1.0rc9...v0.1.0rc10
menlo-sdk 0.1.0rc9
Added
- The usage guide is an agent skill,
skills/menlo-sdk/SKILL.md, in the Agent Skills format.
Claude Code installs it from this repository's marketplace
(/plugin marketplace add menloresearch/menlo-sdk, then/plugin install menlo-sdk@menlo);
Codex, pi and other agents that read Agent Skills install it with
npx skills add menloresearch/menlo-sdk. It links the documentation and
https://docs.menlo.ai/llms.txt. - Examples
guard.py(an optional guard on the user's side: joint temperature, battery, a
latched fault and active alerts, with limits at the top of the file; it prints what it
finds and exits non-zero when the rule fails, and the motion examples call it) and
rest.py(MOVE, then STAND, then DAMP, after asking whether the robot is on its gantry
hook or seated on a stool or bench). preflight()reports active firmware alerts by name (alerts), and themenlo stand,
balance,walkanddampplans show the robot's facts: robot mode, armed, faults,
active alerts, the hottest joint and the battery.menlo status --jsonhasalerts.
Changed
- Breaking: guards move to the user side. Safety is the firmware's job and command handling
is Asimov Edge's; the SDK reports facts and no longer refuses a command because of what the
robot reports.stand(),balance(),set_velocity()(includinghold=False),
trajectory()andset_joints()are sent in any robot mode and the firmware decides:
stand()from MOVE sends STAND (support the robot first),set_velocity()in STAND is
sent and an armed robot enters MOVE,balance()sends zero velocity in any mode. The only
refusal left is no live state:no_stateandstale_statepast the command'stimeout,
raisingNotReadyErrorwith nothing sent. A closed Robot raisesNotConnectedError, a
lost linkLinkLostErrorand a protocol mismatchProtocolMismatchError, at once. In a
require_state=Falsesession these verbs wait for the first sample up to their
timeout, where they raisedNotConnectedErrorat once. - Breaking: a latched fault no longer refuses a command before it is sent. The command is
sent, and the wait that sees the fault raisesRobotFaultedErrorwith.sentset. - Breaking:
balance()sends as soon as there is live state; it no longer waits for the
robot to arm. From DAMP it raisesWaitTimeoutErrorat once ("the robot is in DAMP:
stand() first"); from a STAND that was not armed it times out saying so. - Breaking:
preflight()is a report. Onlynot_connected,no_stateandstale_stateare
blocking;faulted,alertsandnot_armedare information, andPreflight.okmeans
live state. menlo stand,balance,walkanddampaskProceed? [y/N](unless--yes) after
showing the facts, and printNot feasible:only without live state.menlo standfrom
MOVE warns to hang the robot from its gantry hook or seat it on a stool or bench first.
Exit 3 covers no live state and a command that was sent and did not get there.menlo standon a robot already in STAND that has not armed sends nothing, waits up to
10 s for it to arm, and exits 3 when it does not; it printed "nothing sent" and exited 0.- Migration: catch fewer
NotReadyErrors (only no live state, andRobotFaultedErrorfrom
a wait); a script that relied on the SDK to refuse a hot, faulted or flat robot, or a
wrong robot mode, writes that rule itself fromrobot.get_state();examples/guard.py
is a starting point. - The skill moved from
docs/SKILL.mdtoskills/menlo-sdk/SKILL.md, and its links to the
examples point at GitHub so they work wherever the skill is installed.
Removed
- The SDK's state-based checks and their codes:
wrong_mode,joint_hot,battery_low,
battery_protecting,unknown_battery,unknown_joint_tempandunknown_gravity, the
refusal of a latched fault before sending, and the wait for arming before a velocity. JOINT_HOT_CandBATTERY_LOW_PERCENTfrommenlo.asimov._preflight: the SDK holds no
thresholds.
Install
pip install menlo-sdk==0.1.0rc9Commits since v0.1.0rc8
- release(sdk): 0.1.0rc9 (menloresearch/robot#117) (0f536c6)
- sdk: guards move to the user side; the SDK reports facts and refuses only without live state (menloresearch/robot#112) (7b8619e)
- sdk: the usage guide is an agent skill, installable in Claude Code, Codex and pi (menloresearch/robot#88) (ba9cf40)
- feat(control): synchronize Edge and Manager controller state (menloresearch/robot#13) (8f2fdde)
Full diff: v0.1.0rc8...v0.1.0rc9
menlo-sdk 0.1.0rc8
Added
balance(timeout=5.0, wait=True): MOVE at zero velocity, where the walking policy balances
the robot in place. From an armed STAND it enters MOVE and returns once the robot reports
MOVE, waiting for arming up totimeout; in MOVE it ends any velocity hold and sends zero,
never checked, from any thread; in DAMP it raisesNotReadyError(stand()it first) or
RobotFaultedError, and sends nothing. It is the only way from STAND into MOVE.set_velocity(hold=False): one velocity packet per call, nothing re-sent, for a loop that
clocks its own commands. It ends any running hold, is checked once without waiting, and
takes nodurationand nowait=True(ValueError).- Open robots are closed at interpreter exit, so a script that ends without
close()still
sends zero velocity after a held velocity. menlo balance: from STAND it asks, then enters MOVE; in MOVE it sends zero velocity at
once.- Examples
balance.py(betweenstand.pyandwalk.py),wait_until.py(act the moment
a joint passes an angle, mid-move),stream_velocity.py(a 50 Hz velocity loop),
record_audio.py(the microphone to a WAV file) andplay_audio.py(a WAV file on the
speaker). Mode.FAULT_DAMP: robot mode 5, the firmware's latched emergency damping.state.faulted
is true in it,preflight()reportsfaulted, a held velocity is released, waits raise
RobotFaultedError, androbot.armedisFalse.Preflight.explain(): the blocking problems in one sentence each, with what fixes them.NotReadyError.action,.problemsand.has(code);RobotFaultedError.sent;
WaitTimeoutError.sent.
Changed
goto()isset_joints(), with the same parameters and behaviour.trajectory()is
unchanged.set_velocity()works in MOVE only: from STAND it raisesNotReadyError(wrong_mode,
"balance() it first") and sends nothing, so nothing leaves STAND for MOVE implicitly.set_velocity()waits by default: it returns once thedurationhas ended and zero
velocity is sent. Without adurationit raisesValueError; passwait=Falseto keep
the velocity until the next command.timeoutdefaults toNone(5 s with a hold).walk.pyno longer enters MOVE; runbalance.pyfirst.keyboard.py: space balances
(from STAND it enters MOVE).menlo walkneeds the robot in MOVE (menlo balancefirst) and ends balancing in place.trajectory()checks readiness once and raises at once when the robot is not ready, so a
streaming loop fails fast instead of stalling; passtimeout=to wait.stand(),set_velocity(),trajectory()andset_joints()check the robot before they send.
When it is ready the check adds no delay. They wait up to theirtimeoutfor a problem
that clears on its own (not_armedright after a stand,stale_state,no_state), and
otherwise raiseNotReadyError, orRobotFaultedErrorfor a latched fault, with nothing
sent.balance()in MOVE anddamp()are never checked.stand(timeout=10.0, wait=True)returns once the robot reports STAND and is armed, so the
nextbalance()enters MOVE. It raisesWaitTimeoutErrorwhen the robot does not arm in
time andRobotFaultedErrorfor a fault while standing. In MOVE it raises
NotReadyError(wrong_mode) and sends nothing.damp(timeout=5.0, wait=True)returns once the robot reports DAMP, and raises
WaitTimeoutErrorwhen it does not.set_velocity()takestimeout=for the check: 5 s by default with a hold, 0 (check once)
withhold=False.trajectory()takestimeout=too, default 0: a streaming loop fails
fast.set_joints()'stimeoutbounds the whole call, the check and the wait for the
target.RobotFaultedErroris a subclass ofNotReadyError, soexcept NotReadyErrorcatches
every "the robot cannot do that now".NotReadyErrormessages name every blocking problem
and what fixes it.set_joints()from a stale pose raisesNotReadyError(stale_state) after its timeout, where
it raisedStateStaleError.wait_until()'sWaitTimeoutErrormessage names the timeout, the robot mode and the age
of the last state sample.menlo standandmenlo walkstill check before the plan; after the answer the SDK
command checks again and a refusal printsNot feasible:.menlo standexits3when the
robot does not arm within 10 s.- The examples call the commands and print the
NotReadyErrorthey raise.check.pyshows
preflight()on its own. connect()with no mode on a config that has bothudpandlivekitconnects in
hybrid. Before, it raisedValueError.connect("udp")andconnect("livekit")still
pick one of the two.- The SDK is licensed under the Apache License 2.0. Releases up to 0.1.0rc7 were MIT.
- The package description is "Python SDK for Menlo robots. Supports Asimov 1." The README,
which is also the PyPI page, is short: requirements, installation, usage, and links to the
documentation.
Fixed
balance()that refuses outside MOVE after a nonzero velocity leavesclose()the zero it
owes for that velocity; the refusal itself still sends nothing.damp()called from a state callback (on_state,on_alert,on_mode_change) sends DAMP
and returns at once. It used to wait on the thread that delivers state, which held up
every sample for up to 5 s. The other verbs that wait, andwait_until(), raise
RuntimeErrorthere with nothing sent;wait=Falseworks.balance()after a fault-DAMP ended aset_velocity(hold=False)stream raises
RobotFaultedErrorand sends nothing. It used to send a zero velocity to the DAMPed robot.set_velocity(wait=True)raisesRobotFaultedError(withsent) when the robot
fault-DAMPs during the walk; it used to return as if the walk had run its course.- The
LinkLostErrorfor a state stream that went quiet says a zero velocity was sent only
when one was: with nothing held, nothing is sent. menlo standon a robot already in STAND waits for the new session to see it armed
before it says "armed" or "not armed"; an armed robot was reported "not armed".balance()outside MOVE runs its check whatever this session sent before. After a
set_velocity(wait=False)orhold=False, a robot another controller put in DAMP or STAND
got a MOVE-at-zero with no check; now the hold ends, nothing more is re-sent, and the check
raisesNotReadyErrorwhen the robot cannot enter MOVE.set_joints()with gains it rejects (ValueError: a lonekporkd, or a wrong
length) leaves the velocity or motion in force untouched. It used to end a held velocity
first, soclose()no longer sent its zero.- A state sample repeated with the same sequence and firmware clock is dropped. Each copy
used to count as a fresh observation: it refreshedage_s, the checks and the waits, and
counted toward arming, with nothing new observed. A stream with no stamps at all is still
followed sample by sample. set_velocity(duration=..., wait=True)at zero velocity ends early as a walk's wait
does: another verb returns it, a fault-DAMP raisesRobotFaultedError, a lost link
LinkLostError,close()NotConnectedError. It used to sleep out the duration and
return aSentwhatever happened.set_velocity(duration=..., wait=True)whose keepalive thread stalls (a recording write
that blocks) raisesWaitTimeoutErroronce its time budget is spent, after ending the
hold and sending the zero itself, past the recording hook so a blocked write does not
hold it up. It used to return as if the hold had ended, with the
velocity still held and no zero sent.- A
connect()oropen()while another is still opening the sameRobotraises
RuntimeError. Two racing connects both used to succeed, andclose()left the first
transport open and its keepalive thread running. - A
UdpTransportreopened from its own state callback (close()thenopen()in
on_state) ends the old reader thread. The old reader used to keep running on the
closed socket, retrying failed reads in a busy loop beside the new one. stand(wait=True)returns only once the robot reports STAND and is armed, as documented.
A MOVE reported after the STAND went out (another controller's) used to count as
standing; now the wait runs toWaitTimeoutError, which names the robot mode it saw.close()thenopen()on the same transport starts with no camera frame and no queued
microphone audio. The previous session's last frame and unread audio used to come back
as the new session's, until new media arrived.- Leaving a LiveKit room waits, up to 2 s, for audio already handed to the speaker to play
out. The last second ofspeaker.play_pcm()audio (LiveKit's queue) used to be cut off
when a script closed the robot right after it, asplay_audio.pyand
camera_and_audio.pydo. - A verb called from a state callback while a recording runs leaves that callback marked
as one: a wait later in the same callback raisesRuntimeError. The recording hook's own
callback used to clear the mark, so the wait blocked the thread that delivers state. play_audio.pyplays a one-second 440 Hz tone when there is nohello.wavin the
directory it runs in, instead of failing withFileNotFoundError.move_joints.pyandwait_until.pyrefuse, send nothing and exit 1 unless the robot
reports STAND. They used to run in MOVE too, where the robot usually stands free and a
trajectory turns the walking policy off, so it falls.damp.pyprints theWaitTimeoutErrormessage, which names the E-Stop in Asimov Manager,
and exits 1 when DAMP is not reported in time. It used to end in a traceback.
Removed
Robot.stop():balance()replaces it.menlo stop:menlo balancereplaces it.Robot.goto(): renamedset_joints().Robot.wait_ready(): the commands check readiness themselves;preflight()is the...
menlo-sdk 0.1.0rc7
Changed
-
The package description is "Python SDK for Menlo robots: Asimov 1." The README is shorter:
installation, a quickstart in plain Python, the examples, safety, connection modes, and the
command line near the end. -
New examples, one purpose each, with settings as constants at the top of the file and no
command-line arguments:check.py,connect.py,read_state.py,stand.py,walk.py,
damp.py,keyboard.py,move_joints.py,camera_and_audio.pyand
record_and_replay.py. Every script that moves the robot runsrequire_ready()from
check.pyfirst.keyboard.pydrives in short bounded holds that stop when you let go. -
livekit_raw/replaces the single raw LiveKit example: a sharedmanager_token.py, and
send_commands.py,read_state.py,camera.pyandaudio.py. -
apps/follow_the_ball.pyandapps/agent_room.pymove nothing until you press g; space
pauses, b damps after asking, x quits. -
walk.pywalks at 0.3 m/s;keyboard.pydrives at 0.3 m/s forward and sideways and turns
at 0.6 rad/s.move_joints.pyusesgoto()'s own 0.05 rad tolerance. -
Preflightreads "not ready to run a trajectory" for the"trajectory"action, and so do
theNotReadyErrormessages. -
With a latched fault,
preflight("move")andpreflight("trajectory")reportfaulted
alone, withoutwrong_modetelling you to stand: standing does not clear a latch. -
menlo dampsays the robot must be supported, and names both emergency stops: the E-Stop in
Asimov Manager, or cutting power at the battery unit. -
A connect that hears no state says that Asimov Edge also sends none while the robot's
firmware is not reporting.
Fixed
trajectory()andgoto()on the biped moved both ankles when asked to hold them. The
state reports each ankle as its two motors (A, B), but the firmware reads a trajectory's
ankle entries as the ankle's pitch and roll. The SDK now sends the pitch and roll that put
the motors where you asked, so a trajectory of a reported pose within the firmware's ankle
limits holds the robot still, andgoto()reaches its target instead of timing out.trajectory()andgoto()raiseValueErrorfor a biped ankle target more than 0.02 rad
past the firmware's ankle limits (pitch 0.35 rad, roll 0.1 rad), before anything is sent.
The firmware would clamp such a target, so the ankle would not go where it was sent.close()on livekit and hybrid stopped its event loop without waiting for the camera,
microphone and state readers, so a stream still closing was left open. It now waits, up to
2 s, for them to close their streams. Command packets already queued, such as the zero
velocityclose()sends, are delivered before the SDK leaves the room.
Removed
- The numbered examples
01_connect_udp.pyto11_raw_livekit.py.
Install
pip install menlo-sdk==0.1.0rc7Commits since v0.1.0rc6
- release(sdk): 0.1.0rc7 (menloresearch/robot#75) (95a5646)
- sdk: one-purpose examples tested end to end, ankle trajectory fix, shorter README (menloresearch/robot#74) (9d3abc4)
- sdk(examples): state each example's pre-run requirement the manual's way (menloresearch/robot#72) (c8829e5)
Full diff: v0.1.0rc6...v0.1.0rc7
menlo-sdk 0.1.0rc6
Added
robot.preflight(action)checks the latest state before a stand, a walk ("move") or a
trajectory and returns aPreflight:ok, andProblems with stable codes
(stale_state,faulted,battery_low,joint_hot,wrong_mode,not_armed, ...).
A field the robot does not report is a warning, never a guess. It sends nothing.robot.wait_ready(action, timeout=5.0)blocks until the check passes, and raises
NotReadyError(carrying thePreflight) on timeout or at once on a latched fault.robot.armed: whether the firmware accepts MOVE now. From STAND it does only once the
robot has been upright for 0.5 s; a velocity sent earlier leaves the robot in STAND.
Callwait_ready("move")betweenstand()and the firstset_velocity.- Saved robots hold the whole connection:
mode,udp_host,manager_url,credential,
roomand an optional[robots.NAME.limits]table.connect()with no argument uses the
saved connection mode. A mode whose fields are missing fails atconnect(), before any
network I/O, with themenlo robots addcommand that fixes it. - Environment variables
MENLO_UDP_HOST,MENLO_MODEandMENLO_LIMITS, beside
MENLO_MANAGER_URL,MENLO_CREDENTIAL,MENLO_ROBOT,MENLO_HOMEandMENLO_PERSIST. ManagerConfig.check()tests an SDK credential with one token request and returns a
ManagerGrant(room, identity, role).- The
menlocommand:setup(a wizard with live checks),robots(list,add,remove,
use),status(READY, NOT READY or FAULTED;--watch,--json; sends nothing), and
stand,walk --duration,stop,damp.--robotand--modework with every command. menlo stand,walkanddampprint a one-line plan (the robot, its connection mode and
address, its state, and what will happen, with any clamped speed) and ask
Proceed? [y/N].-y/--yesgoes ahead without asking; with no terminal and no--yes
they exit 2 and send nothing. Whenpreflight()refuses,standandwalkprint
Not feasible:with the robot mode, the reason and the fix, and exit 3 without asking;
walkchecks again after the answer; a fault that ends a walk early is named and exits 3.
stopnever asks, and repeats its zero only while the robot is still reported in MOVE.
Exit codes: 0 done, 1 error, 2 usage, 3 not feasible, 4 cancelled, 130 interrupted.- Examples
01_connect_udp.pyto11_raw_livekit.pyandapps/, indexed in
examples/README.mdand run by the test suite.
Changed
Limits()defaults to the firmware's velocity caps, 0.4 m/s, 0.4 m/s and 0.8 rad/s, so
Sent.clampedis true exactly when the robot would not walk at the speed asked for.
Values above the caps are sent as asked and the firmware clamps them.pip install menlo-sdkinstallslivekit,questionaryandrichand drives every
connection mode; the[livekit]extra is gone.import menlo.asimovand
connect("udp")still load none of them.- Docstrings,
docs/REFERENCE.mdanddocs/SKILL.mdstate what Asimov Edge and the
firmware do:Sent.wait_outcome()isUnknownon every connection mode, no command
timestamp is checked, UDP commands are not authenticated, a latched fault keeps
error_flagsset until the firmware restarts, and nothing in the SDK is an emergency stop.
Removed
menlo login,menlo logoutandmenlo use: usemenlo setup,menlo robots add,
menlo robots removeandmenlo robots use.- The old examples,
examples/demos/andexamples/checkout.py.
Fixed
ALERT_NAMESnames alert 14,INFERENCE_FAILURE.
Install
pip install menlo-sdk==0.1.0rc6Commits since v0.1.0rc5
- release(sdk): 0.1.0rc6 (menloresearch/robot#69) (0fc8882)
- sdk: preflight, saved connection modes, the menlo CLI, new examples and README (menloresearch/robot#60) (899e826)
Full diff: v0.1.0rc5...v0.1.0rc6
menlo-sdk 0.1.0rc5
No change to the SDK itself: this release exercises the new release preparation.
Changed
RELEASING.md: the release PR is opened by a workflow that sets the version and this
changelog section, and refuses a version that is not above the current one, already tagged or
already on PyPI.
Install
pip install menlo-sdk==0.1.0rc5Commits since v0.1.0rc4
- release(sdk): 0.1.0rc5 (menloresearch/robot#62) (de34261)
- release: prepare-release workflow and tools/release/bump.py for the sdk and the protocol (menloresearch/robot#59) (72f2fdb)
Full diff: v0.1.0rc4...v0.1.0rc5
menlo-sdk 0.1.0rc4
0.1.0rc3 was tagged but never published: the release workflow refused its own tag. rc4 is the
same SDK, released with the workflow fixed.
Changed
- Releases are cut from Menlo's internal repository, the SDK's source, and reach this
repository as a mirroredv*tag with a GitHub Release carrying the files uploaded to PyPI
(RELEASING.md). This repository no longer publishes by itself.
Fixed
- The error raised when the wire bindings cannot be imported names the module that failed
and installs both runtime dependencies,asimov-protocolandprotobuf; it used to call
protobuf the only one.
Added
- README links
SECURITY.mdfor private vulnerability reports.
Install
pip install menlo-sdk==0.1.0rc4Commits since v0.1.0rc2
- ci(sdk-release): fix the first-parent check; release(sdk): 0.1.0rc4 (menloresearch/robot#36) (9df3186)
- release(sdk): 0.1.0rc3 (menloresearch/robot#34) (7593015)
- docs(sdk): name both runtime dependencies; link SECURITY.md; ignore .tokamak/ (menloresearch/robot#31) (380b8ed)
- ci: mirror robot directories with Copybara; release the SDK from sdk/v* tags (menloresearch/robot#24) (9a4d511)
Full diff: v0.1.0rc2...v0.1.0rc4