Export¶
Deliverables leave botrail in open formats: USD for anyone with a viewer, CSV/JSON for your own pipeline, vendor robot programs for the controller, and Python for the next author. Nothing round-trips through a proprietary project file. The engineering documents a cell hands over come out of the same script — the I/O list, the bill of materials, the layout sheet and the cell report are derived from the scene, so they cannot disagree with it or with each other. The Hand over the cell tutorial writes the whole set.
USD animation¶
scene.export_usd("cell.usda") # the cell as it stands, no motion
scene.export_usd("motion.usda", traj, fps=60) # one planned trajectory
tl.export_usd("cycle.usda", fps=60) # a whole baked cycle
tl.export_usd("cycle.usdc", fps=60) # same layer, binary crate file
tl.export_usd("takt.usdc", fps=24, start=62.3, end=86.9) # one takt of a line
Without a trajectory the export is the static cell: robots at their current joint positions, every visible obstacle at its pose, toolpaths and cameras — the layer a layout is handed around as, and what the browser demo is served. Equipment ordered from the catalog comes out as ordinary prims, so a cell whose belt and guarding are generated at run time still ships as one self-contained USD file.
start/end clip the export to a window, and on a line that is the
difference between shippable and not: a full run is mostly repetition, so
one steady-state takt carries the whole story. The two-station weld line
measures 45.9 MB as a whole run at 30 fps and 16.4 MB as one takt at 24
fps — see scripts/export_line_recording.py, which picks the window
where the pipeline is fullest. (A binary recording keeps its prim names
out of reach of text sniffing, so examples/export/play_record.py takes an
explicit --cell for those.)
Both bake to a USD layer that plays in usdview, Omniverse, or Blender with no
botrail installed: link motion as timeSamples, every obstacle as prims,
grasped objects riding, releasing, resting exactly as simulated. USD-sourced
robots reference their original stage at full visual fidelity (assets are
copied to a sibling <stem>_assets/ directory); URDF robots are authored from
the model's visuals, and a visual whose OBJ names an mtllib keeps its
authored colors — one displayColor per face, so a catalog arm looks like
the machine rather than like a palette. A sole robot exports under the
historical Robot prim;
with several, each lands at /World/<sanitized instance name> — the
convention playback relies on. Exporters return their warnings as a list.
The extension picks the serialization: .usda writes text (diffable, but
large — timeSamples dominate), .usdc or .usd writes the binary crate
format at roughly half the size, byte-for-byte the same composed result.
.usdz is rejected on purpose: it is an asset package, not a layer, and
packaging the referenced robot assets is a different operation.
Cameras in the scene export as
UsdGeomCamera prims under /World/Cameras, their world pose sampled
frame by frame through their mount (a wrist camera rides the arm, a
vehicle camera its machine) and their optics carried as
focal-length/aperture — so View → Camera in usdview or Omniverse frames
exactly what the studio's picture-in-picture shows.
The reverse direction — playing recordings back into the studio, including
Isaac Sim captures — is
play_usd_animation; the
Export and replay USD tutorial covers both ways.
Camera video¶
scene.add_camera("overview", position=(2.0, -1.6, 1.4), look_at=(0, 0, 0.4))
scene.simulate_sequence("cycle") # the bake to film
from botrail import capture
capture.record_camera(scene, "overview", "cycle.mp4") # or .webm / .gif
Films the baked cycle through a camera and
writes a video — the shareable artifact for people who will not open a USD
stage. Under the hood it is the studio's own ⤓ cam exporter driven by a
headless browser: the cycle is re-walked on a fixed fps grid rather than
captured in real time, so no frame is ever dropped and the same bake always
produces the same file, pixel for pixel — on CI's software renderer too.
.webm (VP9) comes straight from the browser; .mp4 and .gif are
converted with ffmpeg (system, or pip install imageio-ffmpeg). Needs
playwright with a fetched Chromium (pip install playwright && python -m
playwright install chromium); both are checked when called, not at import.
CSV and JSON¶
traj.export_csv("motion.csv", dt=0.008) # resampled uniformly; dt=None writes
# the internal waypoints
traj.export_json("motion.json") # {joint_names, times, positions, velocities}
t,shoulder_pan,shoulder_lift,elbow,wrist_1,wrist_2,wrist_3
0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
0.008000,0.002194,-0.001280,0.001646,0.000000,0.000549,0.000000
A whole cycle exports the same way: tl.robot_trajectory("far") is a
Trajectory, with step boundaries in segment_ends.
Robot programs¶
traj.export_script("pick.script", dialect="urscript")
print(traj.to_script()) # same thing, as a string
def pick():
# Generated by botrail (units: rad, m, s)
# joints: shoulder_pan, shoulder_lift, elbow, wrist_1, wrist_2, wrist_3
movej([0, 0, 0, 0, 0, 0], a=4, v=2, r=0)
movej([1.2, -0.7, 0.9, 0, 0.3, 0], a=4, v=2, r=0)
end
The script replays the sparse planned waypoints (traj.segments) as
vendor move commands — one movej/movel per waypoint, Cartesian-line
segments as linear moves — and leaves time parameterization to the robot
controller, which is where it belongs on a real machine. Speeds derive from
the joint limits scaled by speed_scale; linear-move speed is tcp_speed.
Two honest limits: the current dialect list is "urscript" (a 6-axis
format — exporting a 4-DOF arm is a clean error), and blend_radius defaults
to 0 for a reason — overlapping blends abort some controllers, so raise it
only after verifying on yours. botrail's own CI harness can replay exported
scripts against a URSim controller simulator.
The control logic itself goes to the PLC IDE as PLCopen XML
(scene.export_plcopen("cell.plcopen.xml") — see
Offline commissioning), and the controller's
log comes back through tl.diff(trace).
A baked sequence exports too (tl.to_script()), with the sensor
contacts and coils it uses on numbered digital I/O. The ports come from
the I/O map: bind the points on a robot_controller node
(bt.io.ur_standard() gives a UR its DI0-7 / DO0-7) and the script picks
them up; inputs= / outputs= dicts still override per key, io= projects
a newer assignment onto an older bake.
Python¶
A script that rebuilds the scene through the botrail API — the same content as the studio's Export .py button. This is the studio-to-code exit: build interactively, export, and the cell becomes reviewable text. For a bundled, loadable artifact instead, see Projects.