Robots
The robots resource keeps the robots your episodes are recorded on and played
on: register one, import its URDF, give it a gripper and line the gripper up on
its arms. It acts in the API key’s own account, and every
method takes a robot as an id, a full accounts/{a}/robots/{r} name or its display
name. A display name is looked up among robots, then gripper designs. get,
update, delete, get_model, import_model, update_model, set_gripper_tcp,
import_gripper_calibration and jaw_holder take kind="robot" or kind="gripper"
to look among that kind only, so pass kind="robot" when you want a robot. A
gripper a robot owns is a copy named like its source: a display name never reaches
it, so name it by its id, and so is a display name with a / in it. copy(gripper=) takes ids and full names only.
A robot owns its gripper. Attaching one copies a gripper’s URDF, TCP and jaw into a gripper of the robot’s own, so lining it up or editing its TCP changes that robot only, and the copy is a snapshot: recalibrating the handheld later does not change it. A robot that carries a gripper never swaps it. To try another gripper, copy the robot with that gripper and keep the original.
Register a robot and import its model
Section titled “Register a robot and import its model”from qualia import Qualia
client = Qualia()
robot = client.robots.create("Lab arm", "franka_fr3") # name, robot type key
# A .urdf, a .zip of the URDF and its meshes, or a directory (zipped for you).model = client.robots.import_model(robot.name, "lab_arm.urdf")print(model.import_state, model.solver, [arm.hand_link for arm in model.arms])import_model asks for a signed upload, PUTs the file (the API key never goes
to storage), starts the import and polls until the server has converted the
meshes. The file is at most 256 MiB. With wait=False it returns the PENDING
model at once; follow it with wait_import(robot). A URDF the server cannot
use (no link reached through three turning joints, for a robot) ends FAILED
and raises QualiaError with the server’s reason. A wait that runs out raises
TimeoutError and leaves the import running.
A directory is zipped for you: a file symlinked within it goes in with its
content, and a link to a file outside it, a link to a directory, a broken link or
a special file is refused by name (ValueError), never dropped or sent. A path
that cannot be imported (a .xacro, a directory without a .urdf, an empty or
oversize file) is refused before anything is sent.
create_from_urdf does both in one call, as the dashboard’s New embodiment does:
robot = client.robots.create_from_urdf("lab_arm.zip", name="Lab arm")design = client.robots.create_from_urdf("parallel_gripper.urdf", kind="gripper")The name defaults to the file’s; a taken name is refused, never renamed. When the
file never reaches an import (the upload or the import call is refused), the
record made for it is deleted again. An import that starts and then fails keeps
the record, as the dashboard does, and the error names it. With request_id= the
call is safe to run again: a retry follows the import its first call started, or
has finished, without uploading anything, and it never deletes the record. It
imports the file only when no import ran or the last one failed. With kind="gripper"
it makes a gripper design, which robots copy; a gripper of one robot’s own is
attach_gripper(urdf=).
list, get, update and delete do what they say:
page = client.robots.list() # oldest first: the first is the default targetrobot = client.robots.get(robot.name) # carried_gripper: its own gripper and mountsclient.robots.update(robot.name, name="Lab arm 2", serial_number="SN-7")client.robots.delete(robot.name, force=True) # force: even with a modellist(kind="gripper") lists the workspace’s gripper designs, such as the
handheld’s, and the copies robots own (each named like its source, its owner the
robot that carries it). A robot’s own is also robot.carried_gripper.gripper, or
client.robots.gripper(robot.name). Delete a copy by its id and it comes off its
robot, as detach_gripper does.
To play a dataset on a robot, set its target robot:
client.datasets.update(dataset_id, target_robot=robot.name).
Edit the model
Section titled “Edit the model”model = client.robots.model(robot.name)client.robots.update_model( robot.name, solver="pyroki", # or "dls" reach_base=[0.0, 0.0, 0.0], # [] clears it arms=[ { "side": "left", "hand_link": "left_hand", "tool": {"xyz": [0, 0, 0.1], "rpy": [0, 0, 0]}, }, {"side": "right", "hand_link": "right_hand"}, ], etag=model.etag, # a stale etag is refused (409))Only the fields you pass change. arms replaces all of them, and arms read from
model.arms can be passed back as they are. get_model is model for a caller
that wants None instead of a 404 when no import ever started.
clear_gripper=True clears the model’s jaw. The jaw lives in the model of the
gripper the robot carries, not always the robot’s: jaw_holder(robot) names the
one to write. Moving an arm’s hand link leaves a gripper mount on the old link
where it is: set the mounts again. qualia robot configure does that for you.
Give a robot a gripper
Section titled “Give a robot a gripper”Copy a gripper design (the handheld’s, or a library gripper), or another robot’s own gripper:
handheld = client.robots.list(kind="gripper").items[0]robot = client.robots.attach_gripper(robot.name, copy_from=handheld.name)print(robot.carried_gripper.display_name, robot.carried_gripper.mounts)Without mounts the copy is mounted at the hand link of every arm whose hand it
has a TCP for, with no offset and guessed set. Or upload a URDF of the
robot’s own (a file, .zip or directory):
robot = client.robots.attach_gripper(robot.name, urdf="parallel_gripper/")That makes an empty gripper the robot owns and imports the URDF into it. It has no
TCP yet, so set one before you mount it. The path is checked first (the same checks
as import_model), so a bad one creates nothing. If a step after the gripper is
created fails, the error is that step’s own (the same QualiaError with its
status, or a ValueError or TimeoutError of the same class; one that cannot
carry the note, such as a UnicodeDecodeError, becomes a plain ValueError or
TimeoutError with the original as its cause; a file error becomes a
QualiaError) with a note naming the gripper that stays attached: import into it
again with import_model(gripper, ...), or detach_gripper(robot) to remove it.
set_gripper(robot, gripper) says the same in one call, for a caller that wants
the robot to end up carrying gripper whatever it carries now:
robot = client.robots.set_gripper(robot.name, handheld.name) # the copy, as aboverobot = client.robots.set_gripper( # the gripper it carries: only its mounts robot.name, robot.carried_gripper.gripper, mounts=[client.robots.mount("left", "link7")])client.robots.set_gripper(robot.name, None) # detach_gripper: deletes its own gripperOn a robot that carries none it is attach_gripper(copy_from=). Naming the gripper
the robot carries sets its mounts; without mounts each arm keeps a mount that
still hangs on its hand link and the rest start from the guess, and when nothing
moves nothing is written. A mount with no pose has no offset. Any other gripper is
refused: a robot never swaps its gripper, so copy the robot with it.
Response
{"name": "accounts/11111111-1111-4111-8111-111111111111/robots/22222222-2222-4222-8222-222222222222","displayName": "Lab arm","hasModel": true,"kind": "ROBOT","etag": "…","carriedGripper": { "gripper": "accounts/11111111-1111-4111-8111-111111111111/robots/33333333-3333-4333-8333-333333333333", "mounts": [ { "side": "SIDE_LEFT", "parentLink": "left_link7", "matrix": { "m": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, "guessed": true } ], "etag": "…"}}Set the TCP
Section titled “Set the TCP”Per hand, a 4x4 matrix in a link of the gripper’s URDF ({side} stands for the
hand’s side), from xyz and rpy (radians, URDF convention) or a matrix:
client.robots.set_gripper_tcp( robot.name, # the robot that carries it, or the gripper itself hands=[ {"side": "left", "link": "gripper_{side}_ee_link", "xyz": [0, 0, 0.12]}, {"side": "right", "link": "gripper_{side}_ee_link", "xyz": [0, 0, 0.12]}, ],)# or a tcp_calibration.json, which also sets what handheld uploads resolve withclient.robots.import_gripper_calibration(robot.name, "tcp_calibration.json")set_gripper_tcp(file=...) takes a JSON file instead (at most 64 KiB, like the
calibration file):
{ "left": { "link": "gripper_{side}_ee_link", "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0.12, 0, 0, 0, 1] }, "right": { "link": "gripper_{side}_ee_link", "matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0.12, 0, 0, 0, 1] }}Line it up and set the jaw
Section titled “Line it up and set the jaw”client.robots.set_mounts( robot.name, [ client.robots.mount("left", "left_hand", xyz=[0, 0, 0.02]), client.robots.mount( "right", "right_hand", xyz=[0, 0, 0.02], rpy=[0, 0, 3.14159] ), ],)client.robots.set_jaw(robot.name, "{side}_finger_joint", open=0.04, shut=0.0)A mount places the gripper’s hand link in a link of the robot (parent_link),
which must be joined to the arm’s hand link by fixed joints only, and the hand
needs a TCP in a link already. A mount with no pose sits on parent_link with no
offset. baked=True says the robot’s own URDF has the gripper built in.
set_mounts replaces all mounts, one per side at most.
set_jaw names the joint that opens the gripper and its position open and shut
(and optionally home, never inferred). Without home it keeps the home the model
already has for the same joint, and sends the etag it read so a concurrent edit is
refused (409). It edits the model the jaw lives in: the carried gripper’s, or the
robot’s own when its URDF has the gripper built in or it carries none.
On a workspace that has not been migrated yet, a robot may still carry a gripper
shared with other robots. set_jaw, set_gripper_tcp and import_gripper_calibration
refuse to edit it by way of a robot (ValueError, naming the gripper), because the
edit would change every robot that carries it; pass the gripper itself to edit it
for all of them. set_mounts is not affected: mounts are the robot’s own.
detach_gripper(robot.name) takes the gripper off, which deletes it: its URDF,
meshes, TCP and jaw.
Try another gripper: copy the robot
Section titled “Try another gripper: copy the robot”other = client.robots.copy(robot.name, "Lab arm, parallel gripper", gripper=gripper.name)A copy takes the robot’s type, URDF and meshes, arms, solver, jaw, reach base and
capsules, and a gripper of its own. It shares nothing with the original afterwards,
and its serial number, datasets, reach results and history are not copied. gripper
is "keep" (the default: a copy of the gripper the robot carries, with its mounts),
"none", or a gripper or robot to copy a gripper from (a second call, so line it
up with set_mounts). A copy is refused while the model is being imported or the
capsules are being fitted: try again in a minute. request_id makes a retry
return the first copy. "keep" and "none" are matched in any case, and any
other gripper must be a robot id (a UUID) or resource name
(accounts/.../robots/...): it is checked before the copy is made.
Copying with another gripper is two calls, the copy and then the attach. If the
attach fails, the gripper-less copy stays, and the error is the attach’s own (same
class, status and code) with a note naming the copy: give it a gripper with
attach_gripper(copy, copy_from=...), or delete it. Retrying with the same
request_id returns that copy and attaches again; a copy that already carries a
gripper is returned as it is.
Every method
Section titled “Every method”The first name in a row is the one this page teaches. Where a row names a second method, the two do the same action.
| Method | Does |
|---|---|
list(kind=, limit=, cursor=) | Robots oldest first (the first is the default target), or designs |
get(robot, kind=) | One robot or gripper |
create(name, type, kind=, owner=, serial_number=, request_id=) | A record with no model |
update(robot, name=, type=, serial_number=, etag=, kind=) | Name, type or serial; "" clears type or serial |
delete(robot, force=False, kind=) | The record, with the gripper it owns |
copy(robot, name, gripper=, request_id=) | A new robot from this one |
model(robot), get_model(robot, kind=) | Import state, arms, solver, jaw; a 404, or None if never imported |
import_model(robot, path, wait=, timeout=, interval=, upload_timeout=, on_progress=, kind=) | Import a URDF, .zip or directory, replacing the model |
wait_import(robot, timeout=, interval=, on_progress=) | Wait for an import under way |
create_from_urdf(path, kind=, name=, type=, serial_number=, request_id=, wait=, timeout=, interval=) | create and import_model in one |
update_model(robot, arms=, solver=, gripper=, clear_gripper=, reach_base=, etag=, kind=) | Arms, IK solver, jaw, reach base |
gripper(robot) | The gripper a robot carries, or None |
attach_gripper(robot, urdf= | copy_from=, mounts=, name=, wait=, etag=) | Give a robot that carries none a gripper of its own |
detach_gripper(robot, etag=) | Take it off, which deletes it |
set_mounts(robot, mounts, etag=) | Where it sits on each arm |
set_gripper(robot, gripper | None, mounts=, etag=) | attach_gripper, set_mounts or detach_gripper in one call |
jaw_holder(robot, kind=), set_jaw(robot, joint, open, shut, home) | The model the jaw lives in; set the jaw |
set_gripper_tcp(gripper, hands= | file=, filename=, etag=, kind=) | A gripper’s TCP as a matrix per hand |
import_gripper_calibration(gripper, path, filename=, etag=, kind=) | A gripper’s TCP from tcp_calibration.json |
Errors
Section titled “Errors”A name the account has is a 409 (ALREADY_EXISTS); a robot of another workspace
is a 404; an edit made on a stale etag is a 409 (ABORTED); a gripper write
without robots:write is a 403. A refused rule (a robot that already carries a
gripper, a mount whose hand has no TCP) is a 400 whose message names it. See
Error handling.