Robots: ROS 2, Gazebo, MuJoCo and plants
A robot's software as ROS 2 nodes, its world in Gazebo or MuJoCo, and the plants the simulation runs beside the model.
ROS 2#
A deployment that says packaging ros2 runs its Linux binaries as ROS 2 nodes:
archec gen main.arche --target ros2 --deployment VehicleRos2 writes the messages as the
interface package ros2/ (<project>_msgs) and, in ros2_nodes/, an ament_cmake package per
binary on a Linux processor, with an rclcpp executable per process (or one for the binary)
and a node per part it runs. The node wraps the part's C module (--target c: its state
machine and functions) and gives it:
- a publisher per port and message it sends; its C send function converts the message and publishes it;
- a subscription per link that comes to it, which fires the state machine, calls the
port's
handlerwith the message's fields of the same names, then itson-receive; - a service per operation of an interface the port
provides, answered by the function of the same name, and a client for one itrequires; - a timer on ROS time (the simulator's clock with
use_sim_time) that calls a periodic thread'sstep, and parameters from the definition's and the part'sprops.
Each model thread is a std::thread with its own executor, with its policy, priority
and core on Linux. A topic is <part>/<port>, with /<message> after it when the port
sends several, unless the link or the port gives a topic. The QoS comes from the link and
the receiving port: buffer sample keeps 1, depth n keeps n, and reliability carries
over. The message's period becomes the QoS deadline on both ends, so they stay
compatible, and the link's deadline or stale-after becomes the lifespan. ROS 2 QoS has
no latency budget, so latency is not carried. ros2_nodes/<deployment>_bringup launches
every executable in the deployment's namespace. Your code in the nodes goes in their USER CODE regions
(the includes, the end of each node's constructor, its members, main before the threads
start, the CMakeLists), which archec gen keeps (regeneration). Binaries on an RTOS stay firmware: the
generator lists the topics that need a bridge (CAN) and those no node publishes (sensors,
or the simulator).
Gazebo#
archec gen main.arche --target gazebo puts the plant of that deployment in Gazebo (gz
sim, Harmonic), so its nodes run against a simulated vehicle. The output folder gazebo/
is an ament package that holds:
worlds/<system>.sdf, a skeleton you finish (written once). It has a world (physics at the parts'solverstep, a ground, a sun) and a model of the system: abase_linkand a link per part that is a sensor, an actuator the nodes command, or has amass. Each link is named after its part and fixed to the base, with its pose a TODO. A sensor gets the Gazebo sensor it is, where one exists: agpu_lidar(itschannelsprop as rings), acamera, animuor anavsat, at the rate of what it sends.config/bridge.yamlfor ros_gz_bridge:/clock, each sensor to its standard ROS type (PointCloud2, Image, Imu, NavSatFix), and each float field of a command to agz.msgs.Doubletopic of its link, for a plugin to apply.scripts/sim_adapter.py(rclpy, written once). It turns Gazebo's messages into the model's on the topics the nodes subscribe to, with the deadline they ask for. It fills what it can (stamps, sequence numbers, image and cloud sizes, the nearest lidar return, latitude and longitude) and leaves a TODO for the rest. It also passes the nodes' commands on to Gazebo.launch/<system>_sim.launch.py, which starts gz sim with the world, the bridge, the adapter and the deployment's launch file withuse_sim_time.
Shapes, poses, joints and the vehicle's dynamics are not in the model and stay yours. A sensor Gazebo does not simulate (a radar) is a note. CARLA is not generated: its ROS 2 bridge publishes the same standard types, so the adapter can take its topics instead of Gazebo's.
MuJoCo#
archec gen main.arche --target mujoco puts the plant of a system in
MuJoCo, from what its parts say about their bodies:
component Link "Arm link" {
physical {
mass 1kg
shape capsule 0.04m, 0.5m at 0, 0, -0.25m
}
}
system Arm {
part upper: Link {
joint hinge y at 0, 0, 1.5m range -2..2 = 0.3
}
}
shape:boxx, y, z (full sizes),spherer,capsuleorcylinderr, length (along z), andat x, y, z, where its centre is in the part's frame,rot, how it is turned there (rot y 90deg: a capsule along x).joint: how the part moves in the part it is in (the world for a top part):hingeorslidealongx,y,z(or-x, or[x, y, z]),ball,free, orfixed(the default);at x, y, z, where its frame is in its parent's,rot, how it is turned there (roll, pitch, yawrot 0, 0, 90deg, orrot z 90deg: the body'squat; its axis, shape and parts are in the turned frame);range lo..hi, its limits (rad, or m);= value, where it starts.
The output folder mujoco/ holds, written again each time:
<system>.xml, the MJCF. A part with amassor ashapeis a body, inside the nearest body around it; its geom has the part's mass (MuJoCo derives the inertia), and a part with a mass and no shape is a 0.1 m box. Akind sensorpart is a sensor of the body it is in, on a site at itsjoint at: its name says which (imu: an accelerometer and a gyro; encoder or angle: jointpos; velocity: jointvel; force, torque, touch, range, gps; attitude or ahrs: its orientation, a quaternion w, x, y, z) or else the unit of what it sends (rad, rad/s, m/s², N, N·m). Akind actuatorpart drives the hinge or slide of the body it is in: amotor, or aposition(rad, m) orvelocity(rad/s, m/s) servo from the unit of the field that commands it, with the field'srangeas its control range andprop kp/prop kvas its gains. On ajoint freebody (a drone) it is a thruster: a motor on a site at itsjoint atthat pushes along z (or itsjoint slideaxis), and turns the body back about that axis byprop reactionN·m per N (a propeller's drag). The timestep is the parts'solverstep (1 ms by default); a keyframe holds the joints' starting values.plant_bridge.py, which runs it. A command message sets its actuator'sdata.ctrlfrom its arrival; each sensor's message is filled fromdata.sensordata(its float fields in order; astampfield gets the time) at the message'speriod.--stdio: lockstep witharchec sim --plant mujoco/plant_bridge.py, which runs the rest of the model (its controllers) against the plant. The protocol is JSON lines with the simulator's ownsendevents: everyperiod, the plant's messages go to the parts linked to the sensors, the simulator runs a period, and what it sent to the actuators goes back.MUJOCO_PYTHONnames the Python that hasmujoco(elsepython3).--csv out.csv [--control seq.csv] [--for 2s]: a fixed control sequence (a row per time in seconds, a column per actuator) and the readings, in CSV.--viewer: MuJoCo's viewer, when there is one (on macOS, run it withmjpython).--ros2 [--namespace NS] [--rate 1] [--for T]: a ROS 2 node (rclpy), for the nodes ofarchec gen --target ros2: each sensor's message published on its port's topic (cart/encoder/position, or the link's or port'stopic) at itsperiod(its QoS deadline, as the nodes ask), each actuator's command taken from its sender's topic as it comes, with the types of<project>_msgs;/clockis the plant's time, for the nodes'use_sim_time. The namespace is the deployment's (packaging ros2);--rate 0runs as fast as it can,--rate 1with the wall clock. Without--forit runs until stopped. It needs a sourced ROS 2 (rclpy, rosgraph_msgs) and the built<project>_msgs.
$ archec gen examples/arm/main.arche --target mujoco -o out
$ archec sim examples/arm/main.arche --for 3s --plant out/mujoco/plant_bridge.py
archec check says what does not read (body) and, in a system whose parts say their
shape or joint, what MuJoCo cannot place (body-unmapped): a mass with no shape, a
sensor it has no model of, an actuator with no joint to drive. The ground, colours and
contacts beyond MuJoCo's defaults are not in the model.
How the sensors read and the actuators act is the model's (spec):
a sensor's noise (its σ in the MJCF; the bridge draws it from --seed with the generator
archec sim uses), its field's range (MuJoCo's cutoff) and its port's latency
(delay); an actuator's command range (ctrlrange), force-range (forcerange), latency
(delay), and with a gain or a lag a <general> actuator (gainprm, dyntype filterexact). Each keyframe of the system is a <key> — its hinges', slides' and balls'
positions, its free bodies' places and turns, their velocities (qpos, qvel: see
keyframes) — and plant_bridge.py --keyframe K
(or archec sim --plant … --keyframe K) starts from it.
Plants#
Four plants ready to run, each a project to start from (archec new my-arm --template two-link-arm) and, in a store, the collection plants: a plant in MuJoCo (its parts'
mass, shape and joint, its sensors and actuators), a controller in the action
language, the system that wires them, requirements (with an EARS sentence or a formal
property), a test plan and a card. The test plan runs against the plant:
archec gen main.arche --target mujoco -o out
archec sim main.arche --plant out/mujoco/plant_bridge.py --test
| Template | The plant | The controller | Measured in MuJoCo 4.0 |
|---|---|---|---|
two-link-arm |
two hinged links, a torque motor and an encoder at each | gravity compensation and PD | within 0.02 rad of its pose at 0.78 s |
cart-pole |
a cart on a rail, a pole with a bob, 0.1 rad off upright | LQR, its gain from archec physics --linearize --lqr |
the pole within 0.1 rad, upright within 0.02 rad at 1.08 s |
diff-drive-rover |
a free chassis, two wheels, a caster, encoders, IMU, GPS | odometry, heading and speed loops through three waypoints | at the last at 10.4 s, 0.073 m off at most |
quadrotor |
a free body, four thrusters with their drag torque, IMU, attitude, position | place, attitude and altitude loops, the mixer | a 0.5 m climb within 0.05 m in 0.4 s |
examples/cart-pole-mpc balances the same cart-pole
with a model predictive controller (mpc.ts): every 5 ms, the forces of the next 0.1 s
planned within ±3 N on the cart-pole that archec physics --linearize --lqr gives (A, B,
and the LQR's cost to go P as the horizon's end), the first one sent. Its test plan passes
against MuJoCo; with only a force limit, a clamped LQR does about as well there.