Skip to content

Mechanical mounting

attach_tool records an assembly: which product sits on which flange, and at what transform. bt.mounting.report reads what the loaded products' installation documents say about that assembly:

import botrail as bt

result = bt.mounting.report(robot)   # also accepts a Scene
print(result.to_markdown())
result.save("mounting.json")
for item in result.blockers():
    print(item.target, item.status, item.message, item.next_action)

Each item has an id, its target (the same instance names the BOM uses), a status of pass, fail or unknown, a message, a next_action and the evidence it was read from. ready is true when no item failed or stayed unknown, blockers() lists the ones that did, and the report renders as Markdown or JSON (to_markdown(), to_json(), save(path)) alongside the assemblies it walked and the kits it found. scene.check() includes failed items as warnings; this report also retains unknowns and their evidence.

What is checked

Product and assembly declarations:

  • required:<id> — a part the product's installation documents require between it and the robot flange, and whether that part is actually on the attachment path. A bare Robotiq 2F-85 needs its ES-062 coupling (manual, section 6.1.1); translating the gripper by the coupling's thickness does not supply it, and a coupling attached somewhere else does not count.
  • kit_host / kit_composition — whether a purchase kit is documented for the robot it is mounted on, and whether it is still assembled the way the manufacturer's part number describes.

These checks are evaluated alongside the drawing checks below. Having the right part or a manufacturer-supported kit does not resolve missing hole, fastener or clearance data. Payload and electrical compatibility are separate.

Drawing-level fit

Each actual tool or arm attachment, including a rigid vehicle-mounted arm, also produces these items:

Item What it checks
requirements_complete An evidenced declaration that the installation lists all required separate parts. Individual required:<id> checks still examine their actual path and quantity.
interface Agreement of the two evidenced bare-interface identifiers; matching labels do not replace dimensional checks or manufacturer host approval.
declared_pose The actual flange-to-mount transform against explicit, evidenced allowed_poses. Omission is unknown.
geometry Coincident, opposed mating planes; corresponding clearance/threaded holes and cylindrical locating features across their diameter, position-tolerance and depth bounds.
fasteners Selected screws: nominal thread diameter, pitch and handedness, head standard, property class, declared length/torque limits, engagement and bottoming.
clearance Declared conservative body boxes and tool-access boxes for the two adjacent parts in the actual assembly pose.

These consume the catalog-builder vocabulary on mounting.interfaces[]: geometry, fasteners, clearance and requirements_complete. No dimensions are inferred from model meshes. Sources must be cited by index and document section. Drawing frames require frame_verified: true; complete: true declares coverage, not a substitute for individual missing fields or evidence. Missing declarations produce unknown, including in old projects. A part check or vehicle mount_pose can pass while the overall report stays unresolved.

Drawing lengths use millimetres, torque uses N·m, and allowed_poses.position uses metres. Dimension ranges are {min, max}; they are kept as ranges through loading, saving and generated Python. A pass requires every value in the stated bounds to satisfy the condition. Disjoint incompatible bounds fail; partial overlap is unknown. No default position tolerance, thread pitch, minimum engagement or torque is invented.

Holes are paired by transformed position and complementary type, not their names or list order. Uncertain correspondence stays unknown and a feature cannot satisfy two holes. Pin/hole and boss/recess checks include radial fit and usable depth. This is a planar threaded-hole/clearance-hole contract; nut fastening, clamps, interference fits and strength calculations are outside its scope.

For a selected screw on the clearance-hole side:

tip depth  = screw length - grip - washer thickness
engagement = tip depth - unthreaded lead

grip_mm is bearing-surface-to-mating-face thickness, excluding washers. depth_mm is usable tip depth including the drawing's bottom allowance. Both faces' fastener_rules apply. The one documented default is omitted thread_start_mm meaning zero unthreaded lead; missing usable depth or grip remains unknown. The selected fasteners[] records one screw for each listed clearance hole, with its length, washer, thread, grade and tightening torque. This checks a declared assembly selection, not measured installation torque.

clearance.solids conservatively covers each body excluding the separately checked mating features. clearance.access reserves tool space. Overlap means a violation of these declared envelopes, not proof that the detailed CAD surfaces collide. Both tool-access directions are checked; touching box boundaries are allowed. Nonadjacent parts, approach motion and the surrounding cell require separate collision checks.

JSON evidence includes the loaded identities, complete face declarations, source references and individual checks with numerical bounds. ready now requires these drawing items to pass as well as the part and kit checks; existing callers that used part presence alone must inspect that item's status instead. It remains limited to the report's scope and is not hardware certification or a strength/load assessment.

Vehicle-mounted arms

Give mount_robot the loaded carrier to record its catalog identity, revision, source documents and mounting frame:

carrier = bt.Robot.from_catalog("rb-kairos", format="usd")
# After adding the arm and the vehicle named "amr":
scene.mount_robot("amr", carrier=carrier)
print(bt.mounting.report(scene).to_markdown())

With no offset arguments, the first mount-side allowed_poses entry is used when declared; otherwise the carrier's declared flange and the arm's declared mount (or root) coincide, including their rotations. The vehicle frame must be the carrier model's root frame. Only frames fixed to their model roots are accepted. flange= and mount= accept exact link names; selecting a different frame from the declared one remains unverified.

An explicit offset_position / offset_quaternion keeps the existing meaning: vehicle frame to arm root. It is retained even if it differs from the catalog frame, so an existing layout can be audited without moving it:

scene.mount_robot("amr", carrier=carrier,
                 offset_position=(-0.2423, -0.1765, 0.7041))
for item in bt.mounting.report(scene).items:
    if item.key == "mount_pose":
        print(item.status, item.message, item.evidence)

mount_pose reports pass for a matching reference pose and fail for a position or orientation discrepancy. It uses the same allowed_poses format, source checks and quaternion comparison as tool mounting; an explicit pose declaration without supporting evidence remains unknown. Its evidence includes the expected and actual offsets, translation/rotation errors, and captured carrier provenance. For multiple permitted poses, a match uses that pose; a failure reports the difference from the first pose and lists all permitted poses. Missing carrier information, a non-catalog carrier or arm, or missing sources for the carrier or a distinct arm mounting face produces unknown; changing a BOM label cannot supply this information. An arm's model root is a defined coordinate origin and can be compared without a separate mounting-face declaration. Evidence labels that basis as model_root and retains the arm's sources; it does not verify that origin as a physical mounting face. Existing offset-only mounts still load and move as before. Gait and rotor-spin mounts describe locomotion rather than a bolted arm and are excluded from this check.

This item's scope is catalog frame alignment. A deliberate adapter or custom bracket may offset those frames; a failure does not mean such an assembly is physically impossible. A pass does not establish approved host combinations, hole/fastener fit or load capacity. The numerical comparison tolerance is not a manufacturing tolerance. ready only means no reported item is failed or unknown.

The reference survives snapshots, .botrail files and generated Python, including embed_catalog=True. It contains frame data and provenance, so replaying it needs no carrier download or duplicate carrier geometry. The captured mounting declarations and order requirements participate in the same drawing and required-part checks as tools. If the carrier is itself an uncaptured composite or kit, or an older snapshot lacks path completeness, absence of a required part on that incomplete path stays unknown. To review each adapter's two interfaces, model that chain with actual Robot attachments. The AMR example uses the catalog frame directly, with no corner offset or invented adapter plate. Its RB-KAIROS arm root is at (0, 0, 0.688620001) m in the carrier frame. The bench, deck and belt poses and travelling fold are taught for that placement, including the unrotated holonomic arrival. The CLI reports mount_pose and the overall unresolved mounting count separately: matching the frame does not supply missing mechanical drawings.

Manufacturer kits

A kind: kit catalog entry is a purchase unit. Loading it assembles its component packages with their mounting frames and TCP:

arm = bt.Robot.from_catalog("universal_robots/ur/ur5e/r2")
kit = bt.Robot.from_catalog("robotiq/2f/2f-85-ur-es-062-kit/r2")
robot = arm.attach_tool(kit)   # the coupling and the hand, once each, under `kit_`
scene = bt.Scene(robot)

purchase = scene.bom().rows[-1]
print(purchase["qty"], purchase["order"]["part_number"])   # 1 kit SKU
print(purchase["order"]["includes"])                       # per kit, not extra purchases

for entry in bt.mounting.report(scene).kits:
    print(entry["target"], entry["host"],
          entry["manufacturer_support"]["status"], entry["composition"]["status"])

A kit lands under the kit_ prefix unless you pass prefix= (its coupling exposes a flange link, as the arm does), so its links read kit_gripper/tcp and so on. The BOM shows the kit as one row with its order.part_number and order.includes; the included parts are not additional purchases. Each entry of report.kits names its target, catalog ID and host, with manufacturer_support (status, message) and composition (status).

The ES-062 kit is AGC-ES-UR-KIT-85 as documented in the Robotiq quick-start guide; its coupling is GRP-ES-CPL-062, and the bare gripper keeps its own catalog ID. Robotiq distinguishes ES-062 and ES-077 wrists, so check which one the robot has.