Adding your own part
A part the catalog lacks goes into a library of your own: a directory next to your program, named with a second --library.
my_parts/
├── library.yaml
└── parts/
└── lidar_ld06/
├── component.yaml
└── assets/model.dae# my_parts/library.yaml
name: my_parts
parts:
- lidar_ld06A sensor
This is the demo robot's lidar:
schema_version: 1
id: lidar_ld06 # the directory's name
category: sensor
description: LDRobot LD06 2D lidar.
visual:
file: assets/model.dae
xyz: [0.0033, -0.0001, -0.012] # where the mesh sits in the part frame
rpy: [0, 0, -1.5708]
collision:
type: box
size: [0.038, 0.038, 0.033]
origin_xyz: [0, 0, 0.0165]
inertial:
mass: 0.0824982
origin_xyz: [-0.000109908, -0.003282139, 0.01204194]
inertia: {ixx: 3.621828e-05, ixy: 0, ixz: 0, iyy: 4.345764e-05, iyz: 0, izz: 4.733319e-05}
source: cad_measured
connectors:
mount:
pos: [0.0033, -0.0001, -0.024]
dir: [0, 0, 1]
up: [0, -1, 0]
profiles:
Lidar:
mounts: {default: mount}Every length is in metres and every vector is [x, y, z] in the part's own frame: +x forward, +y left, +z up. A sensor looks along its own +x.
The fields that matter most
visual. The mesh that is shown, in a directory of its own inside the part's, assets/. Everything in that directory goes into the output with the mesh, so a mesh made of several files, a model.gltf with its model.bin and textures or an OBJ with its materials, works when they lie there together. A single-file mesh such as a .glb needs nothing beside it.
collision. A shape that stands in for the mesh in every check and in the simulators: a box, {type: box, size: [x, y, z], origin_xyz: [x, y, z]}, or a convex hull, type: hull with vertices, at least four points in the part frame, and faces, triangles that index them. Every corner must lie on or inside every face's plane (within 1 µm), or the loader refuses the hull as not convex; the triangles may be wound either way. A hull follows a tapered or a slanted part where a box would reach over the neighbouring part, and the parts' own clearance rule judges it as it stands. The targets write a hull as a binary STL, robot/hulls/<part>_hull.stl, and collide through it. Leave collision out and the Gazebo target collides through the mesh while the footprint rule warns that it cannot bound the robot; the MuJoCo target refuses the part, since it cannot read the mesh. Declare one.
collision:
type: hull
vertices: [[-0.03, -0.03, 0], [0.03, -0.03, 0], [0.03, 0.03, 0], [-0.03, 0.03, 0], [-0.01, -0.01, 0.04], [0.01, -0.01, 0.04], [0.01, 0.01, 0.04], [-0.01, 0.01, 0.04]]
faces: [[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]inertial. Mass in kilograms, centre of mass, and all six inertia terms. source says where the numbers came from: measured, datasheet, cad_measured, estimated or placeholder. An estimate or a placeholder also needs a basis, a sentence saying how it was chosen, such as solid cylinder at 270 kg/m^3. The compiler never makes a mass up.
connectors. The points where the part mates. pos is the point and dir the mating axis; when two parts mate, their points meet and their directions face each other. up fixes the roll about dir for a part whose orientation matters, such as a camera or a lidar.
profiles. The component types the part can be used as, each mapping the type's logical mounts to connectors. A sensor or a wheel maps default.
A wheel
A wheel states its size, and the connector it mates by carries the axle joint:
schema_version: 1
id: standard_80
category: wheel
description: Plain rubber wheel, radius 0.04 m.
visual: {file: assets/model.dae, xyz: [0, 0, 0], rpy: [0, 0, 0]}
wheel: {radius: 0.04, width: 0.03}
inertial:
mass: 0.1
origin_xyz: [0, 0, 0]
inertia: {ixx: 0.00005, ixy: 0, ixz: 0, iyy: 0.00005, iyz: 0, izz: 0.00008}
source: estimated
basis: solid cylinder
connectors:
mount: {pos: [0, 0, 0], dir: [0, 0, 1], joint: {type: continuous, axis: [0, 0, 1]}}
profiles:
Standard: {mounts: {default: mount}}A wheel declares no collision: the targets derive its rolling contact from radius and width.
A track
A track is one part: the belt with the sprockets and road wheels it runs around. Its frame is at the hub that drives it, +x along the track and +z up, and that hub is its connector, which carries no joint: the unit does not turn about its hub, its belt runs around it. Its track block states the belt from the maker's data:
schema_version: 1
id: track_unit
category: track
description: A link tread around a driven sprocket and road wheels, 38.4 mm wide; its belt travels 0.12 m per turn of the hub and lies on the ground for 0.10 m.
visual: {file: assets/model.dae, xyz: [-0.08, 0, -0.005], rpy: [0, 0, 0]}
collision: {type: box, size: [0.20, 0.0384, 0.05], origin_xyz: [-0.08, 0, -0.005]}
inertial:
mass: 0.06
origin_xyz: [-0.08, 0, -0.005]
inertia: {ixx: 1.99e-05, ixy: 0, ixz: 0, iyy: 2.125e-04, iyz: 0, izz: 2.07e-04}
source: estimated
basis: solid box of 200 by 38.4 by 50 mm at 60 g
track:
travel_per_turn_m: 0.12
width: 0.0384
ground: {from: [-0.03, 0, -0.03], to: [-0.13, 0, -0.03]}
connectors:
hub: {pos: [0, 0, 0], dir: [0, -1, 0], up: [0, 0, 1]}
profiles:
Track: {mounts: {default: hub}}travel_per_turn_m is how far the belt moves for one turn of that hub. For a link tread it is the sprocket's teeth times the links' pitch, 10 teeth of 12 mm here, which is less than the sprocket's outline would give; do not derive it from a radius. Leave it out when the maker's data does not give it, as for a rubber band: the compiler then warns that it cannot tell how far the base moves, and the targets do not drive it. ground is where the belt lies on the ground, which on a unit with road wheels is shorter than the distance between its sprockets. The collision box around the whole unit gives the footprint its reach; in the simulators the track collides through a box on the belt's run instead.
A range finder
A Range part, an infrared or time-of-flight distance sensor, states what it measures in a range block, from its datasheet. Its frame is its emitter: the origin where the beam starts, +x along it.
schema_version: 1
id: tof_vl53l1x
category: sensor
description: A time-of-flight distance sensor on a small carrier board; it looks along its +x axis.
visual: {file: assets/model.dae, xyz: [-0.021, 0, 0], rpy: [0, 0, 0]}
collision: {type: box, size: [0.003, 0.018, 0.013], origin_xyz: [-0.0015, 0, 0]}
inertial:
mass: 0.0005
origin_xyz: [-0.0015, 0, 0]
inertia: {ixx: 2.1e-08, ixy: 0, ixz: 0, iyy: 7.4e-09, iyz: 0, izz: 1.4e-08}
source: estimated
basis: board at 0.5 g
range: {min_m: 0.04, max_m: 4.0, field_of_view_rad: 0.47, rate_hz: 50}
connectors:
mount: {pos: [-0.0015, 0, -0.0065], dir: [0, 0, -1], up: [1, 0, 0]}
profiles:
Range: {mounts: {default: mount}}A sensor that says only whether something is within one distance states it as both min_m and max_m. Sensors says what the targets do with these numbers.
A motor
A motor's frame is the face it seats on the chassis by: the origin at that face's centre, +x along the output shaft. It declares that face as the connector it mates by and the end of its shaft as a second connector, where a wheel mates. Its motor block states its ratings from the datasheet: the voltage they hold at, and at that voltage its no-load speed, its stall torque and the current it draws stalled:
schema_version: 1
id: gearmotor_37d
category: motor
description: A 37 mm brushed gearmotor, 30:1, rated at 12 V; its output shaft reaches 0.02 m past its mounting face.
visual: {file: assets/model.dae, xyz: [-0.03, 0, 0], rpy: [0, 0, 0]}
collision: {type: box, size: [0.06, 0.037, 0.037], origin_xyz: [-0.03, 0, 0]}
inertial:
mass: 0.205
origin_xyz: [-0.03, 0, 0]
inertia: {ixx: 3.5e-05, ixy: 0, ixz: 0, iyy: 7.9e-05, iyz: 0, izz: 7.9e-05}
source: estimated
basis: solid cylinder of 37 mm by 60 mm at 205 g
motor: {rated_voltage_v: 12, no_load_speed_rad_s: 36.6, stall_torque_nm: 0.78, stall_current_a: 5}
connectors:
mount: {pos: [0, 0, 0], dir: [-1, 0, 0]}
shaft: {pos: [0.02, 0, 0], dir: [1, 0, 0]}
profiles:
Motor: {mounts: {default: mount, shaft: shaft}}The shaft connector carries no joint; the wheel that mates onto it brings its continuous one. A motor built into a kit's chassis is a part like this too, seated on a connector the chassis declares inside its body, so that the kit's drive is checked like a built robot's.
A datasheet that gives two voltages gives two sets of ratings; write the set of the voltage the motor is sold for. Add voltage_min_v and voltage_max_v only when the maker states the supply the motor accepts, as the makers of smart servos do and the makers of brushed gearmotors mostly do not. A motor that carries its driver also states the bus it is commanded over, bus: dynamixel_ttl; it then needs no channel of a motor driver, only a board that speaks that bus.
A battery
A battery's frame is the face it sits on: the origin at that face's centre, +z up through the pack. It declares that face as its connector and states what it delivers in a battery block: the voltage it is sold by, and the voltage it delivers run down and full when the maker states them. Write the range only from the datasheet; a lithium pack sold as 11.1 V is 12.6 V full, and the compiler infers nothing from the nominal.
schema_version: 1
id: lipo_3s_2200
category: battery
description: A 3S lithium polymer pack of 2200 mAh, sold as 11.1 V; it delivers 12.6 V full and 9.0 V run down.
visual: {file: assets/model.dae, xyz: [0, 0, 0.012], rpy: [0, 0, 0]}
collision: {type: box, size: [0.034, 0.105, 0.024], origin_xyz: [0, 0, 0.012]}
inertial:
mass: 0.18
origin_xyz: [0, 0, 0.012]
inertia: {ixx: 1.740e-04, ixy: 0, ixz: 0, iyy: 2.598e-05, iyz: 0, izz: 1.827e-04}
source: estimated
basis: solid box of 34 by 105 by 24 mm at 180 g
battery: {voltage_nominal_v: 11.1, voltage_min_v: 9.0, voltage_max_v: 12.6, capacity_ah: 2.2}
connectors:
mount: {pos: [0, 0, 0], dir: [0, 0, 1], up: [1, 0, 0]}
profiles:
Battery: {mounts: {default: mount}}A pack whose page states only its nominal voltage leaves voltage_min_v and voltage_max_v out, and the voltage rule reports that it cannot decide for the parts it feeds.
A motor driver
A motor driver's frame is the face it sits on, like a battery's. Its driver block states what the board drives. A board with outputs of its own states how many motors they drive, channels, the motor supply it accepts, and the current one channel delivers continuously and, when the maker states it, at its peak:
schema_version: 1
id: driver_vnh5019
category: motor_driver
description: A dual brushed motor driver board; each of its two channels delivers 12 A, 30 A at its peak, on a 5.5 to 24 V supply.
visual: {file: assets/model.dae, xyz: [0, 0, 0.005], rpy: [0, 0, 0]}
collision: {type: box, size: [0.065, 0.051, 0.01], origin_xyz: [0, 0, 0.005]}
inertial:
mass: 0.018
origin_xyz: [0, 0, 0.005]
inertia: {ixx: 4.051e-06, ixy: 0, ixz: 0, iyy: 6.488e-06, iyz: 0, izz: 1.024e-05}
source: estimated
basis: solid box of 65 by 51 by 10 mm at the maker's 18 g
driver:
channels: 2
voltage_min_v: 5.5
voltage_max_v: 24
continuous_current_per_channel_a: 12
peak_current_per_channel_a: 30
connectors:
mount: {pos: [0, 0, 0], dir: [0, 0, 1], up: [1, 0, 0]}
profiles:
MotorDriver: {mounts: {default: mount}}A board that commands smart servos states the buses it speaks instead, buses: [dynamixel_ttl, dynamixel_rs485], and may state both channels and buses. A bus name is the library's own, and the compiler only compares it with the bus a motor states, so name the wire and the protocol together. A board that regulates what it feeds its motors, by a boost or a buck converter, states that voltage as output_voltage_v; its motors are then judged against it and not against the battery.
A chassis
A chassis declares a connector for every mount of its chassis type and maps them in its profile. A mount the type drives is where a motor seats, so it sits inward of the wheel by the motor's shaft length. A two-wheel base resting on a skid molded into its body declares the skid under contacts, so that the statics rule counts it and the simulators have it:
connectors:
motor_mount_left: {pos: [0, 0.1, -0.02], dir: [0, 1, 0]}
motor_mount_right: {pos: [0, -0.1, -0.02], dir: [0, -1, 0]}
top_mount: {pos: [0, 0, 0.05], dir: [0, 0, 1], up: [1, 0, 0]}
contacts:
- {xyz: [-0.15, 0, -0.035], radius: 0.025, note: molded skid under the tail}
profiles:
Diff_two_caster:
mounts: {left_wheel: motor_mount_left, right_wheel: motor_mount_right, middle_top: top_mount}Put the chassis' mass where it really is. A base whose centre of mass sits right over its axle leaves its caster unloaded and tips onto its nose when it brakes. The statics rule catches a centre of mass on or outside the edge of the ground contacts, but not one a few millimetres inside it.
A steered wheel mount also states its kingpin, steering: {axis: [0, 0, 1], max_angle_rad: 0.6}; see Drive types.
A collision shape is a bound drawn around the whole part, so a chassis box covers the arches its wheels turn in, and a motor's box the hub its wheel sits against. Declare such a void where it is, as a well on the connector the wheel mates onto or that it turns beside: a cylinder about the connector's axis, centred on the connector, well: {radius: 0.052, width: 0.084}. The clearance rule (assembly.wheels_clear_other_parts) lets a wheel or a track into the part's shape exactly that far and refuses whatever else reaches into the space it moves in; size the well from the wheel the kit ships with, over its steering lock, so that a wider wheel is still refused. The parts fixed to the base are held to the same standard among themselves (assembly.parts_clear_each_other, a warning): a part that sits in a pocket of its host, a motor in a chassis' bay or a board on a sunk deck, is in its host's shape, and the pocket is declared as a well on the connector the part mates onto; the rule lets in what the well holds on either side. Where a box reaches over a neighbour that the part itself would clear, a hull is the tighter shape.
A chassis whose own shafts carry a motor's turning to wheels elsewhere, a car's axle with a differential, declares the hubs the wheels mate onto as its wheel mounts and the motor's seat as a connector with a driveline:
connectors:
hub_rear_left: {pos: [-0.15, 0.13, -0.02], dir: [0, 1, 0]}
hub_rear_right: {pos: [-0.15, -0.13, -0.02], dir: [0, -1, 0]}
motor_seat_rear:
pos: [-0.07, 0, -0.02]
dir: [-1, 0, 0]
driveline: {turns: [hub_rear_left, hub_rear_right], reduction: 2}reduction is the turns of the motor's shaft for one turn of the wheels, counted from the kit's gears or taken from its maker's data. Leave it out when neither gives it: the compiler does not guess one.
An object for the world
A table, a shelf or a ramp that a world places is an entry of the same library, with the category object. This is the example library's work table:
schema_version: 1
id: work_table
category: object
description: A steel work table 0.88 m by 1.56 m whose top is 0.96 m high, with cartons and a monitor on it, 1.44 m high in all; its frame is on the floor under its middle.
visual: {file: assets/model.glb, xyz: [0, 0, 0], rpy: [0, 0, 0]}
collision:
- {type: box, size: [0.88, 1.56, 0.07], origin_xyz: [0, 0, 0.932], rpy: [0, 0, 0]}
- {type: box, size: [0.09, 0.045, 0.9], origin_xyz: [0.327, 0.638, 0.45], rpy: [0, 0, 0]}
- {type: box, size: [0.09, 0.045, 0.9], origin_xyz: [0.327, -0.638, 0.45], rpy: [0, 0, 0]}
- {type: box, size: [0.09, 0.045, 0.9], origin_xyz: [-0.327, 0.638, 0.45], rpy: [0, 0, 0]}
- {type: box, size: [0.09, 0.045, 0.9], origin_xyz: [-0.327, -0.638, 0.45], rpy: [0, 0, 0]}
- {type: box, size: [0.48, 1.34, 0.47], origin_xyz: [0.111, 0.021, 1.205], rpy: [0, 0, 0]}
provenance: {name: Warehouse desk C, manufacturer: 'Amazon.com, Inc. or its affiliates', source_url: 'https://github.com/aws-robotics/aws-robomaker-small-warehouse-world/tree/ee0af733315e78432408c3cd98d378ecee5f767c/models/aws_robomaker_warehouse_DeskC_01', license: MIT-0}visual. The mesh that is shown, as for a part. Put the object's frame where a world will place it by: on the floor under its middle, so that pos = (x, y, 0, ...) stands it on the floor. Draw the mesh as glTF, a .glb or a .gltf with its buffers beside it, in metres with z up, in triangles and without mesh compression, and its pictures as PNG: Gazebo decodes other formats too, but the MuJoCo target's node reads the mesh itself, reads glTF only, and MuJoCo decodes a picture from PNG alone.
collision. A list of shapes, each a box, a sphere or a cylinder with its size, its place origin_xyz and its turn rpy in the object's frame. size reads as a world entity's does: a box's three sides; a sphere's diameter and a cylinder's in x, a cylinder's height in z. An object collides by these shapes and never by its mesh. The compiler opens no mesh, so it could neither draw one onto the map nor judge a run against it; and MuJoCo collides a mesh by its hull, which fills the space under a table. State the shapes a robot can meet: the top and each leg, each post of a gate. A ramp is one slab, tilted by its rpy, whose top face is the surface to drive on.
Leave collision out for an object that is only shown, a mark painted on the floor. Such an object can be placed as a Marker and not as an Obstacle.
provenance. An object is not bought, so it names no product page and no price. When its mesh is a model that someone else drew, a model a collection publishes for simulators, provenance says which model it is (name), whose (manufacturer), where it lies (source_url) and under which licence it is passed on (license).
A model from a collection
A collection such as Gazebo Fuel, or the sample worlds published for a simulator, holds models that look like the things they are: a mesh with pictures laid on it. Such a model is not an object as it comes. Its mesh has a unit of its own (inches, millimetres and centimetres all occur) and its SDF file a scale and a pose for it; its frame is seldom on the floor under its middle; its pictures may be JPEG, which MuJoCo does not decode; its material may state that it gives off light, which Gazebo adds to every pixel of the object and MuJoCo's node does not show; and it collides by a mesh, which an object never does. The six such objects of the example library were made so: the unit, the scale and the pose applied to the mesh, the frame put on the floor, the mesh written as glTF, any pictures as PNG and without that light, and the collision shapes stated by a person; each object's folder has a README that says where its model came from and what was changed. Take only a model whose licence lets it be changed and passed on, and keep that licence in provenance.
An object states no inertial, connectors or profiles: a world holds it where the program places it, nothing mates onto it, and whether the robot may touch it is the entity type's to say in the world that places it.
An environment
A room that several programs drive in is an entry too, with the category environment. It has no mesh of its own: it is a floor and the entities that stand in it.
schema_version: 1
id: room_6x4
category: environment
description: A storeroom 6 m by 4 m with walls 0.5 m high. A loaded pallet rack stands along its north wall and a work table in its north-east corner; by its south wall are a pallet with two cartons and a pallet jack, and in its south-east corner a traffic cone beside a mark on the floor where a robot docks. Its frame is on the floor, 1 m from the west wall and midway between the north and the south.
ground_plane: true
entities:
wall_north: {entity: Obstacle, type: box, size: [6.2, 0.1, 0.5], pos: [2, 2.05, 0.25, 0, 0, 0, 1]}
wall_south: {entity: Obstacle, type: box, size: [6.2, 0.1, 0.5], pos: [2, -2.05, 0.25, 0, 0, 0, 1]}
wall_west: {entity: Obstacle, type: box, size: [0.1, 4.0, 0.5], pos: [-1.05, 0, 0.25, 0, 0, 0, 1]}
wall_east: {entity: Obstacle, type: box, size: [0.1, 4.0, 0.5], pos: [5.05, 0, 0.25, 0, 0, 0, 1]}
rack: {entity: Obstacle, ref: pallet_rack, pos: [2.0, 1.5, 0, 0, 0, 0, 1]}
table: {entity: Obstacle, ref: work_table, pos: [4.5, 1.1, 0, 0, 0, 0, 1]}
pallet: {entity: Obstacle, ref: euro_pallet, pos: [0.2, -1.4, 0, 0, 0, 0.7071068, 0.7071068]}
carton_a: {entity: Obstacle, ref: cardboard_box, pos: [-0.05, -1.4, 0.144, 0, 0, 0, 1]}
carton_b: {entity: Obstacle, ref: cardboard_box, pos: [0.5, -1.35, 0.144, 0, 0, 0.1305262, 0.9914449]}
jack: {entity: Obstacle, ref: pallet_jack, pos: [2.4, -1.6, 0, 0, 0, 0, 1]}
cone: {entity: Obstacle, ref: traffic_cone, pos: [4.72, -1.72, 0, 0, 0, 0, 1]}
dock: {entity: Marker, ref: floor_disc, pos: [4.0, -1.0, 0, 0, 0, 0, 1]}ground_plane. Whether the environment brings its floor: true or false, stated either way.
entities. Each key is an entity's name, and its fields are the parameters a .world entity takes, by the same names: entity is its type (Obstacle, SupportSurface or Marker), then a primitive type with its size or the ref of a library object, its pos as [x, y, z, qx, qy, qz, qw] in the world's frame, and collision when the type's default is not wanted. What is written in a scenario's braces can be moved here line by line. A ref may name an object of another library the program is compiled with. One object may stand in a room several times, each time an entity of its own name: the two cartons are one cardboard_box, set on the pallet by the height of their pos.
Name the places a task will drive to, a dock or a station, as Marker entities: a program's goal targets them by name.
Checking it
A part is checked when a library is loaded. Run --check with a program that uses it:
mobiledsl compile robot.core robot.world robot.task \
--library path/to/catalog --library my_parts --checkA problem points into component.yaml: a missing field, a vector that is not unit length, an inertia that no body could have, a mesh that is not there. A part with a problem is left out of the library with a warning, and a program that names it gets an error repeating why.
An object is checked the same way, by a program whose world places it, and an environment by one whose scenario takes it: the names its entities use, an entity type or an object's id, are resolved then, and a problem with one points into the environment's file.
A chassis, an attachment or a leg may move bodies of its own, such as a pan-tilt mount's head, a legged robot's legs or a leg's thigh and shin: links, each on a revolute joint its servo turns, or fixed, with its mass, its mesh and the shapes it collides by (the library format). The compiler weighs and places each link where its servo holds it. The mjcf target writes the links as bodies, on hinges their servos turn or fixed; the two ROS targets do not write them yet, and refuse such a part. A leg, of category leg, also names the shapes on its links it stands on, its feet, and is placed by glue on a Legged chassis that has no legs of its own (Legs as parts).
The library format specifies every field and every diagnostic.