Skip to content

Changelog ​

All notable changes to this project are documented here.

[Unreleased] ​

Added ​

  • The result --json writes carries the robot as the analysis put it together, of a check too and of a program a rule refuses: parts, each component's part with its category, where its visual is drawn and, for a part with links of its own, where each link's frame stands with every servo at its hold; and facts, what compile.json says of it, which now also gives the height of the ground the robot stands on (ground_z, where the targets stand it), a legged robot's feet, the parts' declared ground contacts and the walker it walks by. A tool draws the robot from either without the compiler's internals.

  • A library may register walkers: a policy trained for exactly one robot assembled from a Legged body and legs that are parts of their own, with the body, each leg's part and glue, the command it takes, and every servo it moves, in its order. A program whose body and legs are a walker's, each leg placed within a millimetre of where the analysis places the walker's by its glue and turned within a milliradian of it, walks by it, whatever order it declares them in: the mjcf target writes its policy as the robot's drive, its joints named by the program's components. Two walkers for one robot are refused (assembly.one_walker). A body that walks by walkers alone needs no controller of its own.

  • A gait eases each change of its command and stops only once the robot has settled. The command it follows moves toward the one it is told by at most gait.slew each second (1 m/s ahead and aside, 2 rad/s in turn), and the gait goes on stepping while that command moves the robot and until, at a period's workings of the gait in a row, its centre of mass has moved along the floor no faster than gait.settle.speed (0.05 m/s) and the chassis has turned no faster than gait.settle.turn (0.1 rad/s). Told to stop at once from a run, a robot braked as hard as its feet let it, stood on them still moving when its gait stopped, and fell a moment later; the library's Go1 on four legs now walks and stops at every corner of its ranges, and the test quadruped stops from 0.5 m/s backward, wherever in its stride it is told, without stepping again. drive.json gives the rates as gait.slew and the two as gait.settle.

  • A leg may be a part of its own, of category leg, used as a Leg: its links, on hinges its servos turn, end in the feet it states, and it is placed by glue on a Legged chassis that has no legs of its own. Such a chassis is a body that states its gait and walks by a gait, or by a walker trained for it on its legs, with no feet of its own: links of its own, a head or arms, are held where their servos hold them. A policy in its own controller, trained on a part's own legs, is refused on it, and a controller that states neither a policy nor a gait is refused as such. The robot stands on the feet of all its legs, which the ground, the statics and the clearance rules take as they take a chassis's own. assembly.legs_on_a_legged_chassis asks that every leg be placed on such a chassis, hanging from no connector of it that steers, and that it carry legs, and kinematics.gait_walks_the_feet asks of the robot's feet what the loader asks of a chassis's own: an even number, four or more, each stepping by at least two servos of its own. The mjcf target names each foot after its leg, and drive.json gives each foot its leg's hinges, from the leg's frame down.

  • A legged base may walk by a gait, which needs no training: its part's controller states gait, the period of a step, the part of it each foot is on the floor (duty, more than half) and how high a foot is lifted (step_height). The legs, taken in turn about the chassis's middle, step every other one half a period apart; the feet on the floor carry the body level at its standing height and at the speed it is told, a balance of its whole mass and inertia shared among them, and each foot in the air lands where that speed needs it. The part stands on an even number of feet, four or more, and each foot steps by at least two servos. The mjcf target writes such a base as it writes one that walks by a policy, but stepping as its other models do, with no IMU and nothing beside the model, and carrying what is mounted on the chassis; drive.json says "kinematics": "gait" and gives the gait, the standing height and each foot with its leg's hinges, its friction and its offset in the cycle. A policy and a gait are not stated together. Every foot is on the floor together for at least 8 ms of each half period, when the gait may stop, and each foot is in the air for at least 40 ms of each period; each foot steps by servos of its own, none above two feet; and the mjcf target refuses a gait in a world whose gravity is not straight down.

  • A legged base walks in the mjcf target, by the walking controller its chassis part states as controller: a policy, an ONNX file in the part's directory, called every period seconds with what it observes, in its observation's order (the IMU's linear and angular velocity and gravity's direction in its frame, the command, the servos' angles from their holds and their speeds, the last action, and the gait's phase), the servos of hidden read as still; each servo's target is its hold plus action_scale times the action; and simulator states what it was trained in, as MuJoCo's <option> does, with any of MuJoCo's integrators but RK4, under which the model could not be stepped in two halves as a page steps it. The model steps as the controller was trained, its feet alone meet the floor, each by a contact pair with its friction, as in the models such controllers are trained on (a stand-in in compile.json), its IMU is a site read by a velocimeter and a gyro, and drive.json says "kinematics": "policy" and how to call the policy, which goes beside the model as policy.onnx. The model's free body is the chassis itself, with no footprint body below it, and every number of a part's link is written exactly, as its range and its hold were. A legged base without a walking controller, its part's own or a walker its library trained for its body and legs, is written standing, the model such a walker is trained on: as under a gait, its feet alone meeting the floor, no IMU and nothing beside it, from the keyframe that holds its servos, and drive.json says "kinematics": "stand" and gives no command. The ROS targets still refuse a legged base for its links.

  • The Legged chassis type: a base on legs that are its chassis part's own links. The part lists Legged among its profiles and states its feet, the shapes on its links it stands on with their friction, and its gait, the speeds ahead and aside and the turn its walking controller takes. The robot stands where its servos hold its legs: the ground is where its feet's lowest points stand, each point within 2 mm of a foot's lowest standing with it, the statics rule takes the feet as its supports, and assembly.feet_touch_the_ground asks that they all touch, pointing at the foot and the servos that hold it. A wheel or a caster under it is judged against the plane its feet stand on, and the new assembly.links_clear_the_ground asks of any part's links that no shape but a foot reach below the ground. The gait says which way and how fast the base goes: kinematics.spin_supported and kinematics.strafe_supported ask that a Spin or a Strafe go a way the gait goes, and a Strafe no faster than it takes, and the new kinematics.walk_supported the same of an Advance, a BackUp and a Navigate, which also needs a turn either way. The feet and the gait are the Legged type's: a part whose profiles list another type too stands, used as that type, on its wheels or tracks. A Legged chassis has no wheel or track mounts; the wheel rules say so of a wheel or a motor placed on it. The test library has a quadruped of plain shapes, quad_test.

  • Parts with links of their own, the part legged bases need. A chassis or an attachment may state links: bodies below its frame, each hanging from the part frame or from a link above it, on a revolute joint turned by the position servo it states (kp, damping, armature, frictionloss, effort, and the angle it holds when nothing commands it, hold) or on a fixed one, each with its mass, its mesh and the shapes it collides by, which may be capsules. The robot's mass and centre of mass, its footprint and the clearance between its parts take each link where its servo holds it, the part's own inertial being its base alone; a sphere and a capsule are judged for clearance as they are; a part's own links are not judged against each other or against its base. The mjcf target writes them, below; ros-gz-nav2 and ros-mujoco-nav2 refuse such a part with target.unsupported. A moving joint on a connector a mating uses, which the targets wrote as a fixed joint without a word, is refused too: on the mating part's own connector unless the part is a wheel, and on the connector it mates onto whatever mates there. A connector no mating uses is not checked.

  • The mjcf target writes a part's own links: below the part's body, each a body <component>/<link> where its joint puts it, a revolute joint a hinge within its range with the damping, armature and friction its servo states and a position actuator of the servo's stiffness, bounded by its effort, a fixed joint no joint, the link's inertial as its principal moments about its principal axes, and its shapes, a capsule as MuJoCo's. With servos, the keyframe hold has each servo's joint and command at its hold, and drive.json names the servos; it names the links' bodies always. Stand-ins said in compile.json: the links' shapes collide with MuJoCo's default friction, and a link lighter than MuJoCo's least mass, or with a principal moment less than its least, is written raised to them, as MuJoCo raises every body, and said from the numbers written. The joint's and the servo's ranges, the keyframe's holds and a link's mass and moments are written exactly; the largest moment as the sum of the other two where the numbers to write put it above. The library finds a tensor's principal moments and axes by Jacobi's rotations (principal_frame, after Numerical Recipes), in place of a closed form that lost digits where two moments are nearly equal. The XML the targets write, and the mjcf target's exact numbers, give a number nearer zero than a float's least normal one, which MuJoCo's parser refuses, as 0.

  • The mjcf target: the robot alone, for MuJoCo without ROS. scene.xml is the robot on its floor as one MJCF, which MuJoCo opens by itself and which names no file, and drive.json says how a base motion becomes each wheel's and kingpin's command, or each belt's surface motion. The robot is the one ros-mujoco-nav2 gives MuJoCo, as MuJoCo's URDF reader makes the robot description into a model, written out with what that target's node adds at run time: the free joint at the spawn pose, the contacts' friction, the floor, the step, the integrator, the actuators and the node's lighting. It is the model that target's node builds in MuJoCo 3.15 of that target's output: the tests compare the two in the fields of their bodies, joints, geoms, actuators, light and sky, leaving out the world's entities, the cameras of the robot's sensors, the offsets that place elements in the models' arrays and the frame MuJoCo chooses for a mesh, whose triangles and normals are compared in its body's frame instead, for differential bases (with and without motor ratings, and with a camera), two skid-steer bases, an Ackermann, a four-wheel-drive Ackermann and a tricycle base, and bases with a hull and with a declared skid. A robot with two hull parts differs: MuJoCo's URDF reader names a mesh by its file's name, the same for every hull, so that target's model collides every hull part by the first one's mesh. A differential or skid-steer base is driven as that target's diff_drive_controller drives it, and a car and a tricycle by the runtime's steering drive, which drives a car there; a tricycle there is driven by tricycle_controller, and the difference is declared. The world's entities, ROS and Nav2 are left out, and so is what a sensor senses. A base on tracks is driven by its belts' surfaces, as that target's node moves them, and without the odometry its track drive makes there; a world without a floor is refused. It is for the Studio, which is to drive the robot in the browser.

  • Environments from a library. A scenario's ref names a library environment by its id and takes it whole: Scenario room(ref = "room_6x4") { ... }. The vocabulary called ref "the base environment" and both targets refused every value but "empty", so each program that wanted walls wrote them out as its own boxes. An environment is an entry of a component library with the category environment: whether it brings its floor, ground_plane, and its entities, each stated with the parameters a .world entity takes (entity for its type, then type with size or the ref of a library object, pos, collision). Lowering makes them the scenario's own entities, before the program's: they are names of the world, so a goal targets a room's dock and a program's entity cannot take one of their names; the map, the sensors and a run's verdict take them as they take any entity, and the targets no longer refuse a base environment, there being nothing of one left for them to build. The entity type and the object an environment's entity names are resolved when a program takes it, with diagnostics that point into its file. A world has one ground plane: ground_plane = true is type.second_ground_plane where an environment of any of the world's scenarios brings its floor, and also where another scenario already lays it, which two scenarios could each do before without a word; the floors two environments bring are the one plane. New diagnostics: name.unknown_environment, name.ambiguous_environment, name.not_an_environment; while a scenario's environment is not found, a world name the program does not declare is not reported as unknown. compile.json lists the environments taken. The example library holds a storeroom, room_6x4, furnished with the objects of the next entry.

  • Objects from a library in the world. An entity's ref names a library object by its id, as a component's names its part: Obstacle table(ref = "table_80", pos = (...)). A world had only boxes, spheres and cylinders to be made of, and ref, though the vocabulary named it, was refused by both targets. An object is an entry of a component library with the category object: it states the mesh it is shown by and, as collision, the list of shapes it collides by (box, sphere, cylinder, each with size, origin_xyz and rpy), and none of what a part states to be assembled and moved. An object collides by those shapes and never by its mesh, which the compiler does not open and MuJoCo could neither read (glTF) nor collide as drawn. The analysis projects each shape onto the ground where the entity stands, so a table is its top and its legs and a gate its two posts on the navigation map and in a run's verdict; facts.world.regions and task/evaluation.json hold a list of regions for each entity where they held one (regions for region), and world/mujoco.json a list of shapes for each entity where it held primitive and size. Both targets copy the mesh to world/meshes/<object-id>/ and show it. The Gazebo world names it by its path from the world file and holds a collision for each shape. MuJoCo decodes no glTF, so its node reads the mesh itself, the triangles of a .glb or a .gltf where the file's nodes place them, each in its material's base colour with the PNG picture laid on it, shows it to the cameras, and builds the shapes for what is met and for the lasers' rays; world/mujoco.json holds a visual for such an entity, and compile.json records the stand-in. A mesh that is not glTF is not read there: the object's shapes are shown in its place, and an object that only shows by such a mesh is target.unsupported for that target. compile.json lists the placed objects with their libraries. An object that declares no collision is only shown: it can be a Marker and not an Obstacle or a SupportSurface. size beside ref is refused, since an object is as large as its entry says. New diagnostics: name.unknown_object, name.ambiguous_object, name.not_an_object, type.size_needs_type, type.object_without_collision; a component whose ref names an object is name.part_category_mismatch. The example library holds eight objects. Six are models that collections publish for simulators, converted to glTF with their units, scales and poses applied, and their collision stated by hand: a pallet rack, a work table and a pallet jack from the small warehouse world AWS published for RoboMaker (MIT-0), and a traffic cone, a cardboard box and a pallet from Gazebo Fuel (CC0); each folder's README says where its model came from and what was changed. The cone's model states that its material gives off light (0.2), which Gazebo adds to every pixel of the object and the MuJoCo node does not show: Gazebo's camera read its white 151 of 255 and MuJoCo's 96. The objects are built without such light, and the cone reads 94 and 96. A ramp and a mark painted on the floor are drawn plainly.

  • A part's collision may be a convex hull: type: hull with vertices and faces (triangles, wound either way), refused unless every corner lies on or inside every face's plane. A hull follows a tapered or slanted part where a box reaches over its neighbour. The analysis judges hulls as it does boxes, through the separating-axis test, or through GJK and EPA where two hulls' edges would make too many axes; the footprint reaches a hull's corners; the targets write a hull as a binary STL, robot/hulls/<part>_hull.stl, and collide through it in both simulators, and the evaluation plan carries its outline from above as a polygon.

  • assembly.parts_clear_each_other, a warning: two parts fixed to the base whose declared shapes overlap deeper than 2 mm, and not within a well on a connector of either, are named with the depth and the way out. A lidar and a computer put 3 cm apart on a Romi crossed by 18 mm and compiled clean. A warning rather than a refusal while most parts carry boxes, which overlap where the parts fit; a part in a pocket of its host declares the pocket as a well. Analysis lists the pairs in the facts (Crossing). The example and the test libraries declare the wells their mounted parts' pockets need.

  • assembly.wheels_clear_other_parts: a part whose declared collision box reaches into the space a wheel turns through, over its steering lock, or into a track's unit, is refused; the finding names both parts and says how deep and which way to move the one in the way. A steered wheel is judged over its whole lock: a stretch of it is proved from its middle, where the wheel grown by as far as any point of it moves over half the stretch holds the wheel at every angle of the stretch; what cannot be proved is halved, and what the analysis cannot settle, within its budget or at its finest stretch, leaves the rule undecided, never passed. No pair of parts is let off for being mated: a part declares the void in its box where a wheel turns or a hub sits as a well on the connector, a cylinder of a radius and a width about the connector's axis, and the rule lets in exactly that. A part may come within 2 mm, since boxes are bounds; a part without a box leaves the rule undecided. Found when an arm's base put on a Strafer's deck connector stood 24 mm into a mecanum wheel, compiled clean, and would have driven on in a simulator that does not collide a robot with itself. Analysis lists the overlaps and the unsettled stretches in the facts (Overlap, Unsettled). The example library's chassis, motor and servo declare the wells their boxes need.

Changed ​

  • What a part file may state, a change to the library format. Every number is 0 or between 1e-100 and 1e30 in size (library.invalid_value): no quantity of a part is nearer zero or larger, and what the targets work out from such a number overflowed or was lost to zero. A number written nearer zero than any float, such as 1e-400, is out of it too, where it read as 0, and converting a schema-3 library skips a part that states one as a YAML float, or a sexagesimal float that reads as 0.0 with a part lost so, and its report says why, where it wrote 0.0; one written as 1e-400, which YAML 1.1 reads as text, goes over as written, for the loader to refuse. A zero that a computation left a rounding off, such as a product of inertia of -2.7e-32, is within it. An inertia tensor is judged exactly on its numbers as stated: positive definite by Sylvester's criterion, and its principal moments within the triangle inequality by the mass's second moment, half the trace less the tensor, being positive semidefinite, allowing for the rounding of the stated decimals to the nearest doubles. A part's own tensor, which the MuJoCo targets hand on as it is stated, is also held as MuJoCo holds it, its two lesser principal moments adding up in floats to at least the largest; a link's, which the mjcf target writes within the inequality, is not. A float's eigenvalues decided both before: a link's thin plate turned off its axes was refused, and a tensor a hair from positive definite was taken. No part of the example, test and component libraries changes its verdict.
  • statics.mass_inside_the_wheel_contacts is unknown while a wheel, an end of a track's run or a foot stands off the plane the others share, which assembly.wheels_touch_one_plane or assembly.feet_touch_the_ground fails: which of them bear the robot is not known. It counted each where it was placed, and passed a quadruped one of whose feet stood 7.8 mm below the others.
  • A mesh goes into an output with everything that lies beside it. A glTF keeps its buffers and textures, and an OBJ its materials, in files of their own and names them by relative path; the compiler copied the one file a declaration names, so a model.gltf compiled and reached the output without its model.bin, where Gazebo could not import it and its lasers saw nothing there. The loader now lists the directory the mesh lies in, and the targets copy it whole, for a part of the robot as for an object of the world. That directory is the mesh's own: a visual.file beside its declaration, and not in a directory such as assets/, is library.invalid_value.

Fixed ​

  • A hull whose triangles did not close one solid loaded: a triangle left out, a triangle given twice, a third triangle on an edge, or corners in one plane. The targets wrote it as it was, an open robot/hulls/<part>_hull.stl for ros-gz-nav2's collision, and the analysis read its edges from those triangles. Such a hull is refused now, the diagnostic pointing at the triangle that shows it, and a flat or bulging triangle's diagnostic points at that triangle too, not at the whole list.

  • A part's mesh, its policy, or a file beside its mesh that was a link to a file outside the library was read as the part's own and copied into the output: a compile for ros-gz-nav2 wrote the outside file under robot/meshes/<part>/. Such a part is refused (library.invalid_value, saying where the link leads), and a link that stays inside the part is still read. A path is also written plainly now: ./, a backslash or an empty segment is refused as .. is.

  • A part file with an alias inside the value its anchor names (description: &d [*d]), values nested thousands deep, or aliases of aliases that expand a few hundred bytes to billions of values stopped the whole compile with a RecursionError, or ran without end, though no program used the part. Such a file is refused with library.yaml_syntax, which says which of the three it is, and the rest of its library loads; values nested more than 64 levels deep and more than 200,000 values once aliases are expanded are refused alike. An alias otherwise reads as before.

  • A part whose glue matched two connectors only once a later part was placed came out of the placements alone: the wheel on its shaft stayed placed on a motor that sat nowhere, and the motor stayed in the mass, the footprint and the clearance checks. Such a part now takes every part that hangs from it out with it, each said to mate onto a part that is not placed, and the parts it is found for are found against the same placements, whatever order the program declares them in.

  • A driver that regulates what it feeds its motors gave them its voltage whether or not anything fed the driver: a bus board that regulates to 11.1 V, names no battery and states no input range of its own passed its servos in both voltage rules. A supply now also says the battery the power comes from, and a regulated output with none behind it leaves its motors undecided, as a part that names no supply does; a motor that does not accept the regulated voltage is still refused. compile.json gives the battery of each supply.

  • A motor whose glue matched no connector dropped out of the motors the analysis knows, and with it out of the power and command rules: a supply it could not take went unsaid beside the glue that missed. What a motor is rated at, what feeds it and what commands it are now known whether or not it is placed; where it sits, its shaft and what it turns only once it is.

  • A rule that failed for one part made what it could not decide for another a refusal too: when boards that take at most 10 V were refused a pack sold as 11.1 V, the servos on the same pack, which the rule cannot judge without the pack's range, were refused as well. Each finding now carries its own answer, and an undecided one stays a warning (cannot decide: ...) beside the error. The two clearance rules, which returned with the parts in the way alone, also say which parts they could not judge when another part is in the way: those without a shape and the stretches of a steering lock not settled.

  • A link's cylinder 1e-12 m across, a capsule as thin and 10 m long, or a hull with an edge of 1e-13 m beside faces metres across stopped the analysis with a ValueError: an edge's direction was taken by normalizing the difference of its corners, shorter than normalize takes, and a long, thin face had passed the flat-face bound by its area. Each side of a cylinder's prism now takes its direction from its angle, and each edge of a hull, a capsule's polytope among them, from its difference by its own length, an edge of no length having none; no test program's output or validation changes.

  • A vector's length was the square root of the sum of its squares, which lose digits for components under about 1e-154, vanish under about 1e-162 and overflow over about 1e154: a hull's edge 1e-163 m long was dropped, and one 1e-161 m long came out 0.994 of its length, its direction 1.006 long and hiding another edge's; a capsule 1e155 m long stopped the analysis with a ValueError. A length and a direction are now taken from the components scaled first by a power of two, which leaves a length the same to the last digit wherever no square or sum of squares overflows or falls below the normal floats. A library's numbers are bounded, so none states such sizes; a length and a direction are right for any float.

  • A Check_distance from a part shaped by a hull failed the evaluation with KeyError: 'center': the plan carried the hull's outline from above without the point a distance is measured from. It now carries the middle of the rectangle around the outline, as it does a box's.

  • In MuJoCo, two parts with hulls collide each by its own. Every hull's STL was named collision.stl, in its part's directory, and MuJoCo's URDF reader names a mesh by its file's stem and reuses a mesh of a stem and scale it has read, every hull being at unit scale: the second hull's geom took the first hull's collision mesh, so that a block on a chassis with a hull collided through the chassis's 0.30 m hull. A hull's file is now robot/hulls/<part>_hull.stl, in both targets, apart from the parts' copied meshes, so that no visual file of a part takes its place.

  • In MuJoCo, a light two-wheel base keeps to its commands, and a heavy base drives, turns and stops. Every driven wheel had a velocity servo of gain 0.5 N m s/rad on a joint of only its own inertia. Told to turn in place, the Pololu 3pi+ (0.15 kg, wheels of 3 g) turned 2.65 times as far and the Romi 1.13 times, and microros_esp32, told 0.2 m/s at once, tipped over. The heaviest bases barely moved: the Husarion Panther (55 kg) and Lynx (39 kg) went 0.215 and 0.309 m of 0.4 m and did not turn. Each driven wheel's joint now carries an assumed 1e-4 kg m² of its motor's inertia behind its gearbox, which no part states, and its gain is its share of the base's weight at its rim over 20 ms where that is more than 0.5. Of the component library's 34 examples that the targets drive in MuJoCo, every two-wheel differential base now goes 0.389 to 0.397 m of 0.4 m and turns 0.98 to 1.01 times the turn asked; the Panther and the Lynx go 0.394 m, and told to turn at 0.5 rad/s while driving at 0.2 m/s, turn at 0.25 and 0.14 of it. In the image, Core's own motion check now passes the 3pi+, which it failed, and keeps the Romi's heading, 0.58 rad off after its straight leg before; the Panther and the Lynx, which slid 0.24 and 0.14 m on after each stop, stop within 2 cm. ros-mujoco-nav2's world/mujoco.json gives each actuator's armature, which its node sets on the joint, and the mjcf target writes it on the joint in scene.xml.

  • A colour camera sees one world alike in the two simulators. MuJoCo's own light is a lamp on each camera under a black sky, and its node laid no picture on a mesh: the robot's camera read grey shapes on a floor that faded to black, where Gazebo's read a lit floor, a sky and each object as it looks. In Gazebo a box a program states had no material and was black wherever the sun did not reach it, a tenth of the same picture. Both targets now state one light (world/world.sdf, and light in world/mujoco.json: ambient light, a sun, a sky, a grey floor, a mid grey for a stated shape). The MuJoCo node sets it up, with the colours encoded as Gazebo's picture encodes them and the sun at half its strength, at which the camera read the sky (217, 223, 235) and (214, 221, 234) of 255 in the two, the lit floor 188 to 209 and 196, and a stated box's shaded side 71 and 74. It lays the picture of a mesh's material on it as a MuJoCo texture, which must be PNG, the only form MuJoCo decodes: a picture's red read (76, 5, 8) and (79, 14, 15). A traffic cone and two cartons taken from Gazebo's collection look in MuJoCo's camera as they do in Gazebo's. The sun's shadows are drawn in four times the depth MuJoCo would, without which the lit floor showed dark specks short of the horizon. A camera draws within its range in both: a Gazebo camera shows the sky beyond the far end of its range, also in the colour picture of one that measures depth, so that in a room 6 m long its picture had no far wall and MuJoCo's had; and MuJoCo drew nothing nearer than a hundredth of the world's extent, which in a world 80 m across took the floor's nearest 0.8 m from the robot's camera. The node now draws from the near end of a camera's range and paints the sky over what lies beyond its far end. That end is a distance from a camera that measures depth, in its picture and its depth image, and a distance ahead of a plain one, as Gazebo has them: of crates in a row 4.8 m ahead of a camera that reaches 5 m, Gazebo shows and measures those 4.95 m off and not those 5.2 m off. A point of the node's cloud lies where the middle of its pixel looks; it lay half a pixel off, 2 cm at 5 m.

  • In MuJoCo a Marker that does not collide is in the cameras' pictures. The robot description has MuJoCo discard what is only shown, the robot's visuals being meshes MuJoCo cannot decode, and at compile time that option also discarded every geom nothing meets, so a mark was not in the model at all; and a camera drew MuJoCo's default groups, which a mark's is not among. A disc on the floor or a post that only shows was in no camera's picture there, while Gazebo showed it. The node now keeps what it adds to be shown and draws it: the cloud a depth camera makes of a post a metre ahead marks the local costmap at the post in both simulators. No ray returns from such a mark in MuJoCo, whose node casts its rays at what collides, where Gazebo's lasers see what is shown; compile.json lists each such mark among the approximations.

  • In MuJoCo a laser and a range finder see an entity that stands turned. An entity states no mass, and the robot description has MuJoCo infer none from shapes, so MuJoCo filled the body's inertial frame in with the body's pose in the world; and MuJoCo 3.15's mj_multiRay loses rays at a body whose inertial frame is both off it and turned, which mj_ray finds. Of a fan of rays at a box 1.6 m from the world's origin, 5 of 8 returned when the box was turned by 5 degrees and none from 10 degrees on, while a ball dropped on the box came to rest on it. The node now states each entity's inertial frame as the body's own.

  • The launch starts Nav2's lifecycle manager without autostart. The task runtime asks it to start the servers, 3 s of wall time or more after the manager first answered it, and waits until it reports them active (run_task.py --start-lifecycle, --start-after). With autostart the manager calls the servers as soon as it has made its clients of them, and rmw_fastrtps gives a server's reply up when that client has not matched it within the time the reply may block ("failed to send response to /map_server/change_state (timeout): client will not receive response"). Nav2 Jazzy's manager waits for that reply without a limit, and the task ended not_started after 120 s of simulated time without the robot moving, in 1 of 24 bring-ups of a minimal program, four at a time. With the wait, 72 of 72 came up; the median bring-up had Nav2 active 2.1 s later than with autostart. The wait is a measured remedy, not a guarantee. After a start that failed the manager reports its servers not active, so the task does not start; when the runtime receives the manager's answer that it could not start its servers, the run ends with status not_started without waiting for the 120 s.

  • The depth and RGBD cameras' stand-in is 160 by 120 pixels in both simulators, where it was 320 by 240; a plain Camera stays at 320 by 240. In Gazebo, the cloud of 76800 points was published at its 10 Hz, but ROS lost about three clouds in four between the bridge and the stack, the controller's transforms fell behind, and a depth-camera robot's Navigate aborted on "Transform data too old when converting from odom to map" in 4 of 6 runs; at 160 by 120, in none of 6.

  • In Gazebo a skid-steer base turns in place about its middle while every wheel carries at least half the average wheel load. With rigid sideways grip the more heavily loaded axle held while the other slid, and the base swung about it: the AgileX LIMO PRO, its centre of mass 2.3 mm behind its middle, ended a half turn 0.19 m from where it began, and the demo frame on plain wheels, its battery at the rear, moved 0.25 m in a 3 rad turn. Now the LIMO PRO ends its half turn 1 mm from where its odometry puts it, and the demo frame does not move. Its wheels get Gazebo's WheelSlip system with the previous compiler's sideways slip compliance, 2, multiplied by (wheelbase / track)² for a base longer than it is wide, at an equal share of the robot's weight, and state their grip across first (fdir1 along the axle), as WheelSlip reads it. robot/robot.urdf gains the WheelSlip plugin; compile.json lists the compliance among the approximations.

  • In Gazebo a wheel grips with the friction the target states for it. DART gives a contact the smaller of its two surfaces' coefficients, and the ground plane stated none, so its default 1 held every wheel to 1: a skid-steer base's wheels, stated at 3 along and 1 across, gripped 1 and 1, and the AgileX LIMO PRO, a four-wheel skid-steer base 200 mm long and 173 mm wide, did not turn in place at all while its odometry reported the turn and the run passed. The ground plane and each SupportSurface in world/world.sdf now state mu = mu2 = 3, the highest coefficient the target's contact models state, so the wheels of two-wheel, tricycle and Ackermann bases grip with their stated 3 as well; compile.json lists the surfaces among the approximations.

  • The task starts once it has received from Nav2's local and global costmaps their map and the robot's footprint (run_task.py --wait-costmap), not as soon as Nav2's lifecycle manager reports its servers active, which it does without waiting for them to have received either. A goal sent before they had failed at once and the robot never moved: in 5 of 20 runs of the in-image bumper test DriveOnHeading ended the ram on "Costmap is not available" or on a transform it did not yet have; with the wait it passed 10 runs of 10. The wait makes it likely, not certain, that Nav2's servers have the costmaps when the first goal arrives.

[0.2.0] - 2026-10-08 ​

Changed ​

  • A motor is declared, not required. A wheel or track on a mount the chassis type always drives is driven with no Motor declared for it: the kit's motors are then part of its chassis, as in every library built before motors were parts, and the targets drive it within no stated limits and record a wheel joint without effort or velocity limits (a belt without a speed limit) with the reason. Declared motors, drivelines, batteries and drivers are judged as before, and assembly.wheel_mounts_filled still refuses a motor off a drive mount, a wheel beside a motor and a shaft with nothing on it; its evidence counts the wheels driven with no motor declared. The rules on supply and command stay undecided for a robot that declares no motor. A car's steered pair, which the type only lets drive, is driven only by a motor. Libraries whose kits keep their motors inside the chassis compile without being split first.

  • The ros-mujoco-nav2 target runs on MuJoCo 3.11 or later; its test image has 3.15.0 where it had 3.3.5. A track's belt is simulated by the surface velocity of a geom, which MuJoCo has had since 3.11, and mj_multiRay, which casts every laser's rays, takes its arguments differently since then: the node now says which MuJoCo it needs instead of stopping at its first scan.

  • The in-image motion check judges a turn by the way it was commanded. Judged by its size alone, a base that turned the wrong way round passed: the NXT Tri-bot, rearing on its rear support in MuJoCo, turned -1.8 rad of the +3 commanded and the check called it a turn.

  • The in-image motion check times its legs by the simulation's clock. Timed by the wall clock, a leg came up short whenever the simulator fell behind it: the rear-driven car once covered 0.596 m of 1.2 m in MuJoCo on a busy machine and 1.200 m in each of three reruns.

  • A steering base's minimum turning radius is the tightest circle its kingpins allow: a kingpin's distance ahead of the unsteered axle over the tangent of its limit, plus its distance from the centre line. It was the bicycle model's wheelbase over the tangent of the limit, which for a car promised the planner a turn its inner wheel cannot make (0.44 m for the test car, whose inner kingpin holds it to 0.57 m).

  • A part on a steered mount turns about the kingpin, the chassis connector it is mated onto. It turned about its own origin, which is elsewhere when the part's mating connector is.

  • A chassis profile no longer takes port_roles. The roles steered_drive and steered_free were checked when a library loaded and read by nothing after that: which mounts steer is the chassis type's to say and the connector's steering block, and which are driven is what the program places. A library drops the line.

  • A message that names a chassis type or a category takes the article the name needs: "an Ackermann chassis", "an attachment".

  • The demo program and the parts of the documentation's example robots live in examples/ (examples/demo, examples/library) instead of among the test fixtures; the parts only the tests use (the other drive and sensor types) stay in tests/library/fixtures/test_parts, and the tests load both libraries. The tests, the getting-started guide and the README read the same four demo files, so the guide's copy can no longer drift from the tests' (it had: the tests still stated is_static on the demo's obstacles).

  • The documentation is a VitePress site in docs/, laid out as a guide (Introduction, Basics, Advanced), the reference specification, and the development notes, after Lucide's documentation. The guide is new: a first program from source to a run, one page per file and per concern, and the deeper pages on parts, drive types, sensors, validation, judging a run and the two simulators, with the language highlighted. Every example in it compiles, and the getting-started run was carried out as written. The generated vocabulary page also stops splitting a row at a choice's |. The site is published inside the project site at https://mit-cdfg.github.io/robotdsl-site/docs/, in the site's type and colour, and the site's own page is its home.

  • A scene entity stays in place unless the program says otherwise: is_static defaults to true, and both targets refuse an entity declared movable (is_static = false), since an entity states no mass to move it with. The Gazebo target gave a movable entity SDF's default inertial, 1 kg and a unit inertia, and a Marker without the flag, having no collision, fell through the floor; the evaluator also judges contact against each entity's declared pose. All 145 entities in robotdsl-experiment's 53 worlds already declare is_static = true, so their output does not change.

  • Where each world name is and what each entity covers seen from above are analysis facts, which the map, the mission and the evaluator read instead of each reading the world on its own. task/mission.json holds every goal and every entity under locations (it held goals); an entity in task/evaluation.json carries the region it covers, a circle or a convex polygon, in place of its shape, pose and size; compile.json lists locations in place of goal_poses. A Marker's collision default is settled once, in lowering.

Removed ​

  • The phases root_validation, elaboration and normalization, which no code ran: the JSON result listed them as not_run even for a clean compile. The compiler failure domain and the compiler_failed status, which nothing produced, are gone from the diagnostics specification, and its examples are now real output of the current language.

Fixed ​

  • A base no longer runs on when what drives it has stopped. Nothing after the runtime's drive node ends a command: a group controller holds the last one it was given, so a car whose steering drive was killed at 0.3 m/s went on at that speed, 0.60 m in the next two seconds in either simulator; an omni base is driven through the same controller. The same held for any base when Gazebo's bridge ended, since the controllers' clock comes through it, and in MuJoCo when the controller manager ended, since the node kept the last joint command. The launch now ends the run when one of these ends before it does, and says which; such a run writes no result.
  • statics.mass_inside_the_wheel_contacts counts a declared skid or ball a little above the wheels' plane once the base has tipped onto it: with its wheels on one line, the base tips about them towards its centre of mass until that contact touches, and the rule reports the tilt. LEGO's EV3 Driving Base, whose steel ball sits 3 mm above its wheels' plane, warned that it could not be judged, though it rests 1.7 degrees back on the ball in both simulators. A raised contact on the side the base tips away from still counts for nothing.
  • The range finder node no longer makes numpy warn at every cloud: a beam without a return is infinite or not a number, and it was carried into the transform that finds each return's height.
  • A third review found places where a program compiled but its run failed or was judged wrongly. An entity's region follows its full pose: a cylinder lying on its side was a circle at its centre, so the map left its length free and a robot parked on it was judged 0.5 m clear. Navigate and GoalReached reach an entity as well as a goal, as the vocabulary says: the mission held only goals and the run failed on an entity. A goal on an entity widens the map like any other: a goal 20 m away sat outside a map 9 m wide. A task halted before Nav2 accepted its goal cancels it once it is accepted: the cancel was dropped and the goal ran on; the runtime now waits for canceled goals to stop and names any that did not. The output is written by replacing each file, never through a link, and a linked directory in it is refused: an output robot.urdf linked to the program's .core overwrote the source. --warnings-as-errors blocks when the warning is found, so nothing is written; it failed the compile after 30 files had been. An empty Sequence, Fallback or Parallel and a task without a node are refused (type.children_required) instead of being refused by the runtime, and a number beyond a float, such as 1e999, is syntax.number_out_of_range instead of a crash while drawing the map.
  • What a run is judged against, what navigation plans with, and what the simulators are given agree with the robot the simulators collide (reported in a second review). The evaluator measures each ground contact a part declares as part of it and each wheel as the circle of its sphere: a skid reaching 1 cm into an obstacle behind the chassis was judged 9.5 cm clear, with no contact. Nav2's footprint covers the wheel spheres: a two-wheel base planned 0.27 m wide collided 0.32 m wide. MuJoCo gets each scene entity as a body of its own, <name>::link: an obstacle named like a link compiled and the node stopped on repeated name; the Gazebo target, whose world holds the robot and the floor as models, refuses an entity named like either. And the library checks an inertia tensor by its principal moments: a chassis with ixy = 0.02 loaded and MuJoCo refused it (inertia must have positive eigenvalues); the 385 parts of the component catalog still load.
  • A ground contact a part declares (contacts) is modelled consistently (reported in review). Its link no longer adds mass in MuJoCo, which had inferred one from the sphere, 65 g for the test library's skid and 4.2 kg for a 0.1 m ball: the description's compiler block sets inertiafromgeom="false", so each body weighs what the library states. The footprint reaches around a contact that sticks out past the chassis, where it had stayed at the box (a skid at x = -0.5 m left it at -0.17). A contact that reaches below the wheels' plane fails statics.mass_inside_the_wheel_contacts instead of being counted as raised: 0.1 m below the wheels the rule passed and the base came to rest tipped 33 degrees in MuJoCo. And a component named like a link the target adds, a ground contact's <link>_contact_<n> or a steered wheel's knuckle <wheel>_steer, is refused as base_link already was: two links of one name compiled and MuJoCo then refused the description.
  • The ros-mujoco-nav2 target builds the world the program states. The node ran every world with MuJoCo's own gravity and a ground plane, and held every entity in place: a world in free fall without a floor ran as the demo room, and a movable obstacle became a fixed one. The scene now carries the world's gravity and whether it has a ground plane; an entity declared movable is refused, as is a base environment other than "empty".
  • The MuJoCo node's IMU reads linear acceleration. MuJoCo fills body accelerations only when a model has acceleration sensors, and the node computes its sensors itself, so a body pushed at 3 m/s^2 read zero while gravity was added by hand. The node now fills them before reading, and a container check reads an IMU at rest and pushed against the known answer.
  • A ground contact a part declares under contacts is simulated: both targets give it a sphere collision on a fixed link of its own that slides like a caster. The statics rule counted such a skid as a support while neither simulator had it, so a two-wheel base on a molded skid passed the rule and then rested tipped 10 degrees onto its chassis in Gazebo and MuJoCo alike. The motion checks now also require every base to rest level.
  • The ros-mujoco-nav2 target refuses a part that declares no collision box. The robot description collides such a part through its Collada visual mesh, which MuJoCo cannot read, so the program compiled and then stopped at start-up. In the format-1 catalog 36 of 73 chassis and 7 of 9 sensors declare no collision box.

Added ​

  • A base on tracks. The chassis type Tracked has two mounts, left_track and right_track, and the component type Track fills them where a wheeled base has wheels: on the shaft of the motor on the mount, or on the mount itself with a motor turning it through a driveline. A track part, of the new category track, is the whole unit and states its belt in a track block: travel_per_turn_m, how far the belt travels per turn of the hub the part mates by, its width, and ground, the two ends of its run on the ground. The travel is stated and not a radius, since a link tread moves its sprocket's teeth times its links' pitch per turn, 6 to 7% less than the sprocket's outline gives on LEGO's 42065 and 42095. A part whose maker states no travel leaves it out: kinematics.tracks_state_their_travel then cannot decide, and the targets refuse to drive the base. The assembly, statics and kinematics rules judge a track as they judge a wheel; a tracked base spins in place and cannot strafe. Each simulator moves the surface of a box on the belt's run: Gazebo by a TrackController on the track's link, told its speed and the base's centre of rotation by Gazebo's own TrackedVehicle, MuJoCo by the surface velocity of the box. A belt is no joint, so nothing of ros2_control runs for a tracked base: runtime/run_track_drive.py holds /cmd_vel to what the belts' motors reach, hands the simulator the motion the belts then carry the base in (/tracks/cmd_vel), and publishes the odometry from the belts' speeds. Commanded 1.2 m forward and 3 rad on the spot, the test base went 1.200 m and turned 3.05 rad in Gazebo, 1.200 m and 3.00 rad in MuJoCo. Recorded in compile.json: the belts carry the base about its turning centre without scrubbing sideways, since a part states neither a belt's pressure nor its grip; a belt is held to its motor's no-load speed, not to its stall torque; and a joint check on a Track is refused, since no hub turns. A chassis whose origin is not midway between its two tracks is refused.

  • A motor may turn wheels it does not carry, and a car may drive its steered wheels. A chassis part states a driveline on the connector where a motor seats: the connectors whose wheels that motor turns through the chassis' own shafts, and the reduction when its maker states one. The wheels then sit on their mounts and the motor on its seat, so a car's axle with one motor and a differential is declared as it is built. The chassis type says which wheels one motor may turn together: an axle's two on an Ackermann base, or both axles; a side's two on a Diff_drive base. An Ackermann base may drive its front mounts too, both or neither, by a motor on each or through a driveline; until now such a car's front motors were refused. Each wheel's joint is limited to the motor's speed over the reduction and to an equal share of its torque times the reduction. Both targets drive every wheel the program drives: ros2_controllers' Ackermann controller takes exactly two traction joints, so an Ackermann base is bound to group controllers fed by the runtime's steering drive (runtime/run_steering_drive.py, robot/steering_drive.json), which gives each driven wheel the speed of its own circle about the turning centre and each kingpin its own angle, and takes the odometry from the rear wheels' measured speeds. Pushed against an obstacle in either simulator, the four-wheel-drive test car carries about 0.4 N m on each of its four wheels, the rear-driven one on its rear pair and none on its front wheels. A binding that would command fewer joints than the program drives is refused.

  • MotorDriver, the third of three electrical parts, and the command connection. A motor driver is a part on any free connector whose driver block states what it drives: channels, the motors it drives from outputs of its own, with the motor supply it accepts and the current one channel delivers continuously and at its peak; buses, the buses it commands motors over that carry their driver; and output_voltage_v when it regulates what it feeds its motors. A MotorDriver names the battery that feeds it with power, and a Motor the driver that drives it with command. A motor without a bus is powered through the channel that commands it and names no power; a motor with one, a smart servo, names both and needs no second driver. Every motor's supply is traced through its driver to its battery. command.driver_commands_its_motors asks that the driver have a channel for the motor or speak its bus. Three advisory rules warn of what a program may still mean: a supply whose range does not include the voltage a motor's ratings hold at (power.rated_voltage_in_supply), a channel that cannot deliver the current its motor draws stalled (power.driver_delivers_stall_current), and motors that share a channel (command.driver_has_a_channel_for_each_motor). The demo gains two dual driver boards on its deck, and a 3S pack in place of a 2S one, on which its 12 V motors never reached the voltage their ratings hold at.

  • Battery, the second of three electrical parts, and the first electrical rule. A battery is a part on any free connector; its part states what it delivers in a battery block, voltage_nominal_v and, when the maker states them, voltage_min_v and voltage_max_v together and capacity_ah. A power connection is not a place, so the part that draws power names its supply by a reference, power = @core/<battery>. power.supply_voltage_accepted checks that every motor and motor driver whose part states the supply range it accepts is fed within it, run down and full; a part that names no supply leaves it undecided, and a pack sold by its nominal voltage alone leaves it undecided with a warning, since nothing is inferred from a nominal. compile.json lists the batteries, the drivers, and each motor's supply.

  • Motor, the first of three electrical parts. A motor is a part on a drive mount of the chassis, and a wheel is driven by being glued onto the motor's shaft connector; a wheel glued straight onto the chassis rolls freely. Its part states its ratings in a motor block: rated_voltage_v, and at that voltage no_load_speed_rad_s, stall_torque_nm and stall_current_a. The speed and the torque become the wheel joint's velocity and effort limits in the robot description and bound the MuJoCo actuator's command and force; until now the targets drove every wheel with a gain the library never stated. The supply range a motor accepts, voltage_min_v and voltage_max_v, is stated only when its maker states one, since makers state a motor's ratings at one voltage: of Pololu's 626 motor pages one states a range. A glue may now coincide with a connector of a placed part, not only of the chassis, so a wheel finds its motor's shaft; one that coincides with two connectors is refused. assembly.wheel_mounts_filled asks for a motor with one wheel on each drive mount and one free wheel on each other wheel mount. The example and test chassis seat their motors 0.02 m inward of the wheels, which stay where they were. Kits whose motors are built into the chassis are to be split the same way; until they are, their wheels are driven within no stated limits (see "A motor is declared, not required" above).

  • A range finder that measures with light, Range, for infrared triangulation and time-of-flight distance sensors, which had only Ultrasonic to be declared as. A Range part states what it measures in a range block, min_m, max_m, field_of_view_rad and rate_hz, since the products it stands for reach from 0.6 m to 4 m in cones of different widths and no default could stand in; an Ultrasonic part may state one too. Equal limits make a fixed-distance sensor, which reports -inf or +inf as sensor_msgs/Range defines it. Both targets simulate it as a cone of beams whose nearest return is the reading on /range, INFRARED, and, unlike an ultrasonic sensor's, its floor returns are kept: light comes back from the floor. The advisory rule sensing.range_clears_the_ground warns when a Range sensor's cone meets the ground within its range, where the costmaps would mark the floor as an obstacle; a rule can now be advisory, its fail a warning; the summary line counts it (1 advised against) and its entry in the JSON result says "advisory": true. Checked in both simulators: a narrow infrared sensor takes the differential robot to its goal, and a level time-of-flight sensor 0.12 m up reads the floor 0.49 m away, where the rule says its cone meets it, and the local costmap marks an obstacle there; a fixed-distance sensor answers +inf in an open room and -inf to a bar within its 10 cm.

  • provenance ignores the keys it does not know. A library may keep what its own tools need with the part (a price, how many a purchase supplies, a maker's catalog number) without the compiler knowing them; the four keys the compiler reads are checked as before. Everywhere else a declaration still has no free-form fields.

  • Add architecture, language, and developer onboarding documentation.

  • Add the mobiledsl package skeleton, the import-boundary tests that enforce the documented dependency direction, and CI.

  • Add the shared diagnostic, span, and source-map types (support/), taken from MobileDSL-Compiler.

  • Add the frontend for the four source forms (.core, .world, .task, .eval): one grammar with a start rule per form, a source-anchored syntax tree, and syntax diagnostics. Compared with the previous compiler, component types, task node types and check names are ordinary names resolved later instead of grammar keywords; poses and vectors are tuples instead of strings; references are @world/goal instead of "@world/goal"; # comments are gone.

  • Render diagnostics in the terminal the way rustc and ruff do: a heading with the code, an excerpt with the spans underlined, help and note lines, and color when writing to a terminal.

  • Add the component library loader: library.yaml plus parts/<id>/component.yaml, read as spanned YAML so that every problem points into the library file. Compared with the previous compiler's part.yaml: model is visual, geometry is wheel, the joint moves into the connector that creates it, and catalog images are no longer checked by the compiler.

  • Add the standard vocabulary (semantic/model/vocabulary.py): the component types, entity types, task nodes and checks a program may name, with their parameters. The specification page is generated from it. The task vocabulary names what the robot must do (Navigate, Spin, Wait, ...) and nothing about how a navigation stack does it.

  • Add lowering (semantic/lower.py): resolve the four syntax trees against the vocabulary and the loaded libraries into one typed program, reporting every unknown name, ill-typed argument, unresolved reference and structural problem in one pass, with suggestions for near misses.

  • Add analysis (semantic/analysis/): mate each component onto the chassis connector its glue names, and derive placements, wheel contacts, mass and centre of mass, the footprint from declared collision shapes, drive facts, sensors, goal poses and task use. A glue that matches no connector is a fact (unplaced), not a guess.

  • Add validation (validation/): rules that answer pass, fail, unknown or not applicable over the facts, with evidence and findings that point at the program. Standard rules cover glue-to-connector matching, connector occupancy, wheel mounts and classes, horizontal axles, a common contact plane, footprint coverage, and whether the task's Spin and Strafe suit the base. The rule table is rendered into the specification.

  • Add the driver and the mobiledsl compile command: the four files as arguments, --library directories, --check, --json, --warnings-as-errors and --color. Diagnostics and a summary go to stderr, colored on a terminal; the structured result goes to stdout. No target exists yet, so --target reports that.

  • Specify the grammar, the vocabulary, the component library format, the validation rules, the diagnostic contract, and the output contract.

  • Add the target interface (targets/api.py): a target turns the program and its facts into exact file bytes plus copies of library assets, declares every stand-in it makes as an approximation, and refuses what it cannot realize with a target.unsupported diagnostic that points at the source.

  • Add the first target, ros-gz-nav2, composed of a Gazebo adapter (URDF links, joints and inertials from the analysis placements, meshes copied from the libraries, sphere wheel contacts with class-based friction, simulated laser, IMU and camera sensors, a ground-truth pose publisher, the world SDF), a ROS 2 adapter (ros2_control interfaces, the mecanum and differential drive controllers fed by the analysis kinematics, one /cmd_velTwistStamped entry point) and the composition (topic bridge, mission file, bringup.launch.py). Skid-steer, tricycle, Ackermann and omni bases are refused until a controller is bound and verified.

  • Add the output write phase to the driver: --target NAME --output DIR writes one directory, refuses an output that is, holds or sits inside a source or library, overwrites only its own files, and adds compile.json with the facts, validation results, approximations and provenance.

  • Add the in-image test tiers: pytest -m container checks the emitted files with the target's own tools, pytest -m simulation launches the directory and judges motion by the simulator's poses.

  • Add navigation and task execution to ros-gz-nav2: nav/nav2.base.yaml (the target's Nav2 policy) and nav/nav2.robot.yaml (footprint, inflation, the path follower for the base's kinematics, the laser the costmaps watch), task/behavior_tree.xml (the task in BehaviorTree.CPP layout over the vocabulary), and runtime/ (a task executor that binds each node to Nav2 actions, /cmd_vel, odometry and TF, and writes run/task_result.json). The launch starts Nav2 after the drive controller and the task after Nav2, and ends when the task ends. The simulation tier runs the whole demo and checks the task result against the simulator's pose at the goal.

  • Execute the .eval file: the target emits task/evaluation.json (the checks and the planar collision shapes of parts and entities), the runtime records the simulator's pose and the joint states while the task runs, and run/evaluation.json gives three verdicts from independent evidence: the task's own status, the physical outcome from ground truth (goal reached within twice the controller's tolerance, no obstacle touched), and every check with the value it was judged on. The vocabulary states what each check's bounds mean; Check_contact does not count a support surface.

  • Draw the map from the world: every obstacle is rasterized into nav/map.pgm for the map server, the global costmap gets a static layer, and a robot with a laser localizes against the map with AMCL from its spawn pose. A robot without a laser keeps the map frame pinned to odometry.

  • Bound the vocabulary to what the compiler acts on: Arm, Gripper and Tray are one Attachment type, Ultrasonic is gone until a target simulates it, and an architecture test checks that every component type, task node, check and entity type has a consumer in analysis, validation or a target.

  • Support a depth camera: simulated in Gazebo, its image and cloud bridged, and, on a robot without a laser, its cloud projected onto the scan that the costmaps and AMCL read (runtime/run_depth_scan.py).

  • Require Python 3.12, the version CI and the target image already use; the runtime payload's numpy stubs need it for type checking.

  • Name a part's profiles after the component types: a library entry lists the types the part can be used as (profiles: {Lidar: ...}), and the separate lower-case profile names are gone. The vocabulary table is Type, Category, Meaning; the chassis table names chassis and wheel types. Facts, rules and diagnostics say "chassis" and "wheel type" where they said "profile" and "class" (assembly.wheel_type_matches_chassis, name.type_not_supported).

  • Drive a Diff_drive (four-wheel skid-steer) base: the differential controller takes both wheels of a side, and each wheel's sphere contact grips along the rolling direction and slides across it.

  • Drive tricycle and Ackermann bases: a chassis declares the kingpin of a steered mount (steering: {axis, max_angle_rad}), the analysis reports which wheels drive, steer or only roll and the base's minimum turning radius, a rule asks for an upright kingpin on every steered mount, the robot description hangs steered wheels from knuckle links, the tricycle and Ackermann steering controllers are bound, and such a base plans with Smac Hybrid-A* and follows with MPPI's Ackermann model. The launch takes navigation:=false to bring up only the simulator, the robot and its drive controller.

  • Sense with a spinning lidar (Lidar3D: a 16-ring gpu_lidar whose middle ring is bridged as a planar scan and whose cloud is bridged as points) and an RGBD camera (RGBD: colour and depth images and a cloud). Every point cloud is an observation source of both costmaps (points 0.05 to 2 m above the ground); the localizer reads the first laser, else the first spinning lidar's ring, else a scan projected from the first depth or RGBD camera's cloud.

  • Sense with an ultrasonic range finder (Ultrasonic): the simulator has no such sensor, so a gpu_lidar of 9 by 9 beams fills its cone and runtime/run_range_sensor.py reports the nearest return above the floor as a sensor_msgs/msg/Range, which the costmaps' range layer marks. A robot with only range finders navigates on the known map from its spawn pose.

  • Sense with a bumper (Bumper): an ideal contact sensor on the part's collision box, bridged as ros_gz_interfaces/msg/Contacts. The task runtime records its reports, and Check_contact on a bumper is judged from them (seconds pressed, entities that pressed it) instead of from the geometry.

  • Drive omni bases (Omni_three_wheel, Omni_drive) in any layout: the analysis reads each wheel's position and rolling direction, the rule kinematics.omni_wheels_span_the_plane asks that the directions span the plane, each wheel's contact grips along its roll and slides along its axle, and the base is bound to a joint group velocity controller fed by runtime/run_omni_drive.py, which computes the wheel speeds and the odometry from the layout. ros2_controllers' omni controller models only wheels evenly spaced on a circle; the catalog's four-wheel omni bases sit on rectangles with their axles along the diagonals.

  • Prepare the targets for a second simulator: the mission and evaluation data every runtime reads move to targets/mission, the runtime files every target copies are assembled by targets/payload, the robot description can be built without Gazebo's blocks, and the simulated sensors' stand-in specifications are public.

  • A second target, ros-mujoco-nav2: MuJoCo simulates the robot through the runtime's own node (runtime/run_mujoco_sim.py, world/mujoco.json) and ros2_control's topic-based hardware; the robot description, controllers, navigation, task runtime and evaluation are the Gazebo target's. Lasers, spinning lidars and range finders cast rays, cameras render offscreen, the IMU and the bumper read the physics state. Bases on mecanum or omni wheels are refused: MuJoCo's contact friction is isotropic; for the same reason a skid-steer base turns at about a third of the commanded rate, which the target declares as an approximation. The test image is robotdsl/ros-mujoco-nav2:jazzy (docker/ros-mujoco-nav2/jazzy).

  • A statics rule, statics.mass_inside_the_wheel_contacts: the robot's centre of mass must fall strictly inside the polygon of its ground contacts, the wheels and the skids or balls a part declares under contacts (now placed into the facts), or the robot tips at rest. When the contacts lie on one line the rule is undecided and asks for the skid or a caster. Found when the demo two-wheel base, whose part declared its mass over the axle with nothing on the caster, tipped onto its nose under braking in both simulators and its depth camera saw the floor as a wall; the rule reports the margin, and points at the chassis with every contact.

  • Convert the previous compiler's schema-3 component library into this format with tools/convert-library (mobiledsl.library.convert): renamed fields, the joint moved onto the wheel's connector, profiles named after component types, meshes copied under assets/, and a report of what was dropped or left absent. The real catalog converts to 248 loadable parts out of 254; the six the loader refuses are gaps in the data (placeholder inertials without a basis, an id declared in two categories, a tricycle without a kingpin).

  • A defective part declaration no longer blocks every program that uses the library: the part is left out with a warning, and a program that names it gets a name-resolution error saying why it could not be loaded. Found when the converted catalog's four incomplete parts blocked robots built from the 248 complete ones.