Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@

extensions = ['sphinx_honu']
honu_github = ('HonuRobotics', 'holybro_models')

# Anchors for headings down to ###, so a page can link to a section
# (`page.md#section-title`) in a way GitHub renders too.
myst_heading_anchors = 3
2 changes: 1 addition & 1 deletion docs/getting-started/first-simulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gazebo opens with the X500 resting on its landing gear:
- the GPS mast

That is the default configuration; no configuration was involved. The
model carries no control layer, so the rotors idle until an autopilot
model carries no control layer, so the rotors idle until something
publishes on the motor bus
([Actuators](../vehicles/x500/actuators.md)).

Expand Down
59 changes: 56 additions & 3 deletions docs/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,62 @@ ros2 run x500_gazebo configure_vehicle.py --config $HOME/my_x500/my_loadout.yaml
```

That writes `x500.urdf`, `model.sdf`, `model.config`,
`ros_gz_bridge.yaml` and `robot_description.yaml`. Because the directory
holds a `model.sdf` next to a `model.config`, it works as a `model://`
root on `GZ_SIM_RESOURCE_PATH`.
`ros_gz_bridge.yaml` and `robot_description.yaml`, plus `vehicle.yaml`,
the config as used. Because the directory holds a `model.sdf` next to a
`model.config`, it works as a `model://` root on `GZ_SIM_RESOURCE_PATH`.
Add `--name <name>` to generate an instance under another name.

## Several vehicles

Every instance of the vehicle goes by one name, which is at once its
Gazebo model name, its topic namespace (`/<name>/...` on both the Gazebo
and the ROS side, joint states included) and its TF prefix (`<name>/`
in front of every frame and every sensor `frame_id`). A name is letters,
digits and underscores, starting with a letter; the generator refuses
anything else.

The launch argument is the name. `sim.launch.xml` always passes `name:=`,
`x500` by default, to `configure_vehicle.py --name`, which applies it to the
config before generating the artifacts, so the model name, the plugin
topics, the frames and the bridge cannot disagree. A `topic_namespace` in a
custom config is therefore overridden by the launch; it only names the
instance when the generator runs without `--name`. `x`, `y`, `z`, `roll`,
`pitch` and `yaw` place the instance:

```bash
ros2 launch x500_gazebo sim.launch.xml name:=uav_b x:=2 yaw:=1.57
```

Per part `topic`, `gz_topic` and `ros_topic` overrides go under the
instance name too, so two instances of one config never share a topic; an
override that starts with a slash is used as given and is then shared.
`extra_bridge_topics` are appended to the bridge config verbatim, so they
are shared by every instance too.

`sim.launch.xml` starts a Gazebo server of its own, so running it twice
gives two simulations, not two quads. Several instances share one world
through gz-maritime, whose simulation launch names no vehicle and whose
spawn launch adds any vehicle by name and pose, running this generator for
it:

```bash
ros2 launch kai_bringup simulation.launch.xml world:=$(ros2 pkg prefix --share x500_gazebo)/worlds/x500_ground.sdf
```

then, one command per quad:

```bash
x500="$(ros2 pkg prefix x500_gazebo)/lib/x500_gazebo/configure_vehicle.py --config $(ros2 pkg prefix --share x500_description)/config/x500.yaml"
ros2 launch kai_bringup spawn_vehicle.launch.xml name:=uav_a y:=2 z:=0.25 generator:="$x500"
ros2 launch kai_bringup spawn_vehicle.launch.xml name:=uav_b y:=-2 z:=0.25 generator:="$x500"
```

Each quad gets its topics under its name, its own bridge and
`robot_state_publisher` in its namespace and its own TF prefix; the clock
is bridged once by the simulation launch, which drops the `/clock` entry
this bridge config carries. To run several **simulations** side by side
instead, give each Gazebo instance its own `GZ_PARTITION` and each ROS
graph its own `ROS_DOMAIN_ID`.

To confirm a config and an assembled URDF still agree:

Expand Down
8 changes: 5 additions & 3 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ half assembled vehicle.
## Topics

See the X500 [Sensors](../vehicles/x500/sensors.md) and
[Actuators](../vehicles/x500/actuators.md) pages. `/clock` and
`/joint_states` are always bridged; every part topic follows
`/<namespace>/<instance>/<suffix>` on both sides.
[Actuators](../vehicles/x500/actuators.md) pages. `<name>` is the
instance name. `/clock`, `/<name>/joint_states` and
`/<name>/command/motor_speed` are always bridged; every part topic follows
`/<name>/<part>/<suffix>` on both sides, and a per part override goes
under the name unless it starts with a slash.

## Packages

Expand Down
43 changes: 33 additions & 10 deletions docs/vehicles/x500/actuators.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,45 @@ following the [Add a part](../../how-to/index.md) guide.

## Actuators ROS API

There is none, deliberately. On a multicopter the per rotor outputs only
make sense downstream of an attitude controller, so the autopilot (PX4
SITL, ArduPilot SITL or a custom controller) is the control layer and it
talks to the Gazebo motor bus directly. The marine vehicles (BlueBoat,
BlueROV2) expose a normalized -1..1 `throttle` topic per thruster, the
ArduPilot output convention; the X500's equivalent normalized layer is
the autopilot itself. Rotor joint states are bridged to `/joint_states`
so RViz animates the props.
The four rotors are driven together over one motor command bus:

| ROS Topic | Description | Message type |
|---|---|---|
| `/<name>/command/motor_speed` | Angular velocity per rotor (rad/s), one entry per rotor number, indexed from 0 | [actuator_msgs/msg/Actuators](https://github.com/rudislabs/actuator_msgs/blob/main/msg/Actuators.msg) |

`<name>` is the instance name, `x500` for the default instance, which the
commands below use, or whatever the quad was spawned as.

The `velocity` array carries one angular velocity per rotor, in rad/s,
where entry `i` drives `rotor_i`. Each motor plugin ramps its rotor to the
commanded speed with the part's time constants and holds it until the next
message, so always send all four; a stopped rotor is a `0`. To spin all
four by hand:

```bash
ros2 topic pub --once /x500/command/motor_speed actuator_msgs/msg/Actuators "{velocity: [700, 700, 700, 700]}"
```

and to stop them:

```bash
ros2 topic pub --once /x500/command/motor_speed actuator_msgs/msg/Actuators "{velocity: [0, 0, 0, 0]}"
```

The bus is a low level interface: it sets rotor speeds and nothing more,
and keeping the quad level and steering it is the job of a controller
above it. Rotor joint
states are bridged to `/<name>/joint_states` so RViz animates the props.

## Gazebo transport API

| gz Topic | Description | Message type |
|---|---|---|
| `/x500/command/motor_speed` | Angular velocity per rotor (rad/s), indexed by the rotor number | `gz.msgs.Actuators` |
| `/<name>/command/motor_speed` | The same bus on the Gazebo side | `gz.msgs.Actuators` |

To spin the rotors by hand, with no autopilot:
The motor plugins build the topic from the instance name, so a quad spawned
as `uav_b` listens on `/uav_b/command/motor_speed`. The bridge maps the ROS
messages above onto it; the same command can be sent from the Gazebo side:

```bash
gz topic -t /x500/command/motor_speed -m gz.msgs.Actuators -p 'velocity: [700, 700, 700, 700]'
Expand Down
3 changes: 1 addition & 2 deletions docs/vehicles/x500/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ Launch the X500 without a config and you get the airframe with:
- **the 4S battery**.
- **the GPS mast**.

That is enough to fly once an autopilot is attached or you provide
control code. Two more parts sit in the catalog unfitted, the companion
That is enough to fly once a controller drives the motor bus. Two more parts sit in the catalog unfitted, the companion
computer and the gimbal camera, and nothing above is fixed: fit an
optional part, swap a default for another accepted type, or leave a
default off entirely. See [Configuration](configuration.md).
21 changes: 18 additions & 3 deletions docs/vehicles/x500/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ Launches the vehicle in its default configuration: every artifact of the
loadout is generated at start (URDF, composed model, bridge config, into
a directory under `$ROS_HOME`), the model is spawned as `x500` into the
ground world and the ROS bridge comes up with it. The vehicle rests on
its landing gear; the rotors idle until an autopilot publishes on the
motor bus ([Actuators](actuators.md)).
its landing gear; the rotors idle until something publishes on the motor
bus ([Actuators](actuators.md)).

![](../../img/x500.png)

To run a custom vehicle instead, pass a loadout file with `config_file:=`;
the [configuration page](configuration.md) lists the slots and
[Change the loadout](../../how-to/index.md) walks through writing one.

| Argument | Default | Meaning |
|---|---|---|
| `config_file` | the shipped config | Vehicle loadout, expanded at launch. |
| `name` | `x500` | Instance name: the Gazebo model name, the topic namespace (`/<name>/...`) and the TF prefix; letters, digits and underscores. See [Several vehicles](../../how-to/index.md#several-vehicles). |
| `x`, `y`, `z` | `0`, `0`, `0.25` | Spawn position (m); the default drops onto the landing gear. |
| `roll`, `pitch`, `yaw` | `0` | Spawn orientation (rad). |
| `world` | the ground world | Vehicle free world SDF to spawn into. |
| `gui` | `true` | Start the Gazebo GUI. |
| `use_composition` | `true` | Run the server, the bridge and the state publisher in one process. |

## Choosing the world

By default the vehicle is spawned into the vehicle free ground world
Expand All @@ -32,10 +42,15 @@ ros2 launch x500_gazebo sim.launch.xml world:="$(ros2 pkg prefix --share x500_ga

## In RViz

To see the model in RViz:
To see the model in RViz, without Gazebo:

```bash
ros2 launch x500_description display.launch.xml
```

In a simulation the frames carry the instance name (`<name>/base_link`)
and joint states arrive as `/<name>/joint_states`, so RViz next to
`sim.launch.xml` needs its fixed frame, its RobotModel TF prefix and its
joint states pointed at the instance.

![](../../img/rviz.png)
29 changes: 15 additions & 14 deletions docs/vehicles/x500/sensors.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@ guide.

| ROS Topic | Description | Message type |
|---|---|---|
| `/x500/imu` | Body IMU | [sensor_msgs/msg/Imu](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/Imu.html) |
| `/x500/air_pressure` | Barometer | [sensor_msgs/msg/FluidPressure](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/FluidPressure.html) |
| `/x500/mag` | Magnetometer | [sensor_msgs/msg/MagneticField](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/MagneticField.html) |
| `/<name>/imu` | Body IMU | [sensor_msgs/msg/Imu](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/Imu.html) |
| `/<name>/air_pressure` | Barometer | [sensor_msgs/msg/FluidPressure](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/FluidPressure.html) |
| `/<name>/mag` | Magnetometer | [sensor_msgs/msg/MagneticField](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/MagneticField.html) |

### GPS

| ROS Topic | Description | Message type |
|---|---|---|
| `/x500/gps/fix` | Position fix | [sensor_msgs/msg/NavSatFix](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/NavSatFix.html) |

These topic names are not fixed strings. Every part topic is built as
`/<namespace>/<instance>/<suffix>`, so the fix above is the `gps`
instance publishing `fix` under the `x500` namespace. Change the vehicle
and the topics change with it:

- Empty the GPS slot and `/x500/gps/fix` goes away with the part.
- Rename the instance and the topic follows the new name.
- Set `topic`, `gz_topic` or `ros_topic` on the part in the config to
rename the base yourself.
| `/<name>/gps/fix` | Position fix | [sensor_msgs/msg/NavSatFix](https://docs.ros.org/en/rolling/p/sensor_msgs/interfaces/msg/NavSatFix.html) |

`<name>` is the instance name, `x500` for the default instance, or
whatever the quad was spawned as ([Several vehicles](../../how-to/index.md#several-vehicles)).
A part topic follows the part's own name (`gps` above), so a part renamed
or removed in the config moves or drops its topics
([Configuration](configuration.md)).

Sensor messages carry the sensor's frame under the instance name
(`<name>/base_link` for the flight sensors, `<name>/gps_antenna` for the
GPS), which is the frame `robot_state_publisher` publishes for the
instance.
2 changes: 2 additions & 0 deletions x500_description/config/x500.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ base: {type: x500_airframe, name: base_link}
# topic: base for BOTH the Gazebo and ROS sides
# gz_topic: Gazebo side base (overrides `topic`)
# ros_topic: ROS side base (overrides `topic`)
# A base goes under /<topic_namespace>/ unless it starts with a slash,
# which is then used as given (and shared by every instance).
# bridge: dict of NATIVE ros_gz_bridge keys merged into that part's
# generated bridge entries
#
Expand Down
2 changes: 2 additions & 0 deletions x500_gazebo/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog for package x500_gazebo

Forthcoming
-----------
* The motor command bus is bridged to ROS as actuator_msgs/msg/Actuators on /<name>/command/motor_speed.
* One name per instance: configure_vehicle.py --name and the launch pose (x y z roll pitch yaw); joint states, topics, overrides and sensor frame ids under the instance name.
* Motors and sensors placed by the assembly resolution; launch time loadouts via configure_vehicle.py; model://x500.
* Initial import: generated composed model (four rotor actuators, flight
sensors), aerial playground world, sim launch and generated ros_gz bridge.
30 changes: 23 additions & 7 deletions x500_gazebo/launch/sim.launch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
config that does not match the parts (a slot that does not exist, a type
that does not fit) fails the launch with the reason. The world is vehicle
free (x500_ground.sdf by default) and the generated model is spawned into
it as "x500". With use_composition (default), gz_server, the bridge and
it under its instance name ("x500" unless name:= says otherwise), at the
pose x y z roll pitch yaw. With use_composition (default), gz_server, the bridge and
robot_state_publisher run as composable nodes in ONE container process;
the Gazebo GUI runs as a separate process. No control layer: the quad
rests on its landing gear until something publishes motor commands.
Expand All @@ -28,12 +29,18 @@
default="$(find-pkg-share x500_description)/config/x500.yaml"
description="Vehicle loadout config, expanded at launch."/>
<arg name="name" default="x500"
description="Model name to spawn as."/>
description="Instance name: the Gazebo model name, the topic namespace (/&lt;name&gt;/...) and the TF prefix; letters, digits and underscores. Give each copy of the vehicle in one world its own."/>
<arg name="x" default="0" description="Spawn position x (m)."/>
<arg name="y" default="0" description="Spawn position y (m)."/>
<arg name="z" default="0.25" description="Spawn height (m); drops onto the landing gear."/>
<arg name="roll" default="0" description="Spawn roll (rad)."/>
<arg name="pitch" default="0" description="Spawn pitch (rad)."/>
<arg name="yaw" default="0" description="Spawn heading (rad)."/>

<!-- Every artifact of this loadout, generated now from config_file. -->
<!-- Every artifact of this instance, generated now from config_file and
name. -->
<let name="vehicle_dir"
value="$(command '$(find-pkg-prefix x500_gazebo)/lib/x500_gazebo/configure_vehicle.py --config $(var config_file) --cache')"/>
value="$(command '$(find-pkg-prefix x500_gazebo)/lib/x500_gazebo/configure_vehicle.py --config $(var config_file) --name $(var name) --cache')"/>

<!-- Gazebo server (composable) creates the shared container. -->
<gz_server world_sdf_file="$(var world)"
Expand All @@ -45,8 +52,10 @@
<executable cmd="gz sim -g" output="screen" if="$(var gui)"/>

<!-- The vehicle, spawned from the generated model into the (only) world. -->
<node pkg="ros_gz_sim" exec="create" name="spawn_x500" output="screen"
args="-name $(var name) -z $(var z) -file $(var vehicle_dir)/model.sdf"/>
<node pkg="ros_gz_sim" exec="create" name="spawn_$(var name)" output="screen"
args="-name $(var name) -file $(var vehicle_dir)/model.sdf
-x $(var x) -y $(var y) -z $(var z)
-R $(var roll) -P $(var pitch) -Y $(var yaw)"/>

<!-- ros_gz bridge (composable) joins the container. -->
<ros_gz_bridge bridge_name="ros_gz_bridge"
Expand All @@ -55,20 +64,27 @@
create_own_container="false"
use_composition="$(var use_composition)"/>

<!-- robot_state_publisher: composable node in the container when composing... -->
<!-- robot_state_publisher, reading the instance's joint states and
prefixing every TF frame with the instance name, so its frames match
the frame ids the sensors publish: a composable node in the container
when composing... -->
<load_composable_node target="ros_gz_container" if="$(var use_composition)">
<composable_node pkg="robot_state_publisher"
plugin="robot_state_publisher::RobotStatePublisher"
name="robot_state_publisher">
<param from="$(var vehicle_dir)/robot_description.yaml"/>
<param name="frame_prefix" value="$(var name)/" type="str"/>
<param name="use_sim_time" value="true" type="bool"/>
<remap from="joint_states" to="/$(var name)/joint_states"/>
</composable_node>
</load_composable_node>

<!-- ...or a plain node when composition is disabled. -->
<node pkg="robot_state_publisher" exec="robot_state_publisher"
name="robot_state_publisher" unless="$(var use_composition)">
<param from="$(var vehicle_dir)/robot_description.yaml"/>
<param name="frame_prefix" value="$(var name)/"/>
<param name="use_sim_time" value="true"/>
<remap from="joint_states" to="/$(var name)/joint_states"/>
</node>
</launch>
Loading