DocsDevelop
Your project with Pixi
Start a Pixi workspace that installs the released Aerostack2, and build a C++ package of your own on top of it.
Most work on top of Aerostack2 doesn't change Aerostack2 itself. You write a node, a plugin or a mission of your own, and run it next to the stock components. This guide sets up a Pixi workspace for that. Pixi installs Aerostack2 already built, so the only code it compiles is yours, and it runs on Linux and on macOS with Apple silicon.
You'll build altitude_monitor, a small C++ node that warns when the drone flies above a set
altitude.
Before you start
- Pixi 0.83 or later. See Install Pixi.
- Some ROS 2. You should know what a package, a node and a topic are.
Where the packages come from
Your workspace takes packages from three channels.
| Channel | What it holds |
|---|---|
aerostack2/aerostack2-humble or aerostack2/aerostack2-jazzy on prefix.dev | Every Aerostack2 package, built for one distro |
robostack-humble or robostack-jazzy | ROS 2, from RoboStack |
conda-forge | Everything else, like compilers and libraries |
The Aerostack2 packages keep their ROS names, like as2_core and as2_python_api. The
aerostack2 package installs all of them at once.
Create the workspace
Make a folder for the workspace, with a folder inside it for your package:
mkdir -p my_drone/altitude_monitor/src
cd my_droneBy the end of the guide it holds these files:
- my_drone/
- pixi.tomlThe workspace, its channels and what it installs
- altitude_monitor/
- pixi.tomlHow Pixi builds the package
- package.xml
- CMakeLists.txt
- src/
- altitude_monitor_node.cpp
Create the workspace's pixi.toml for your distro:
[workspace]
name = "my_drone"
preview = ["pixi-build"]
channels = [
"https://prefix.dev/aerostack2/aerostack2-humble",
"robostack-humble",
"conda-forge",
]
platforms = [
{ platform = "linux-64", glibc = "2.34" },
{ platform = "linux-aarch64", glibc = "2.34" },
"osx-arm64",
]
# libcxx 23 dropped std::wstring_convert, which the ROS 2 message headers still use.
[workspace.target.osx-arm64.build-variants]
c_compiler_version = ["22"]
cxx_compiler_version = ["22"]
[dependencies]
altitude_monitor = { path = "altitude_monitor" }
ros-humble-ros2run = "*"
ros-humble-ros2launch = "*"
ros-humble-ros2topic = "*"
ros-humble-ros2param = "*"altitude_monitor is a path dependency, so Pixi builds it from the folder rather than downloading
it. The ros2 commands are there to run and inspect the node. Aerostack2 itself isn't listed,
because your package pulls in what it needs.
Write the node
The node subscribes to the drone's pose and warns, at most once a second, when it's above
max_altitude:
#include <memory>
#include <as2_core/names/topics.hpp>
#include <as2_core/node.hpp>
#include <geometry_msgs/msg/pose_stamped.hpp>
#include <rclcpp/rclcpp.hpp>
class AltitudeMonitor : public as2::Node
{
public:
AltitudeMonitor()
: as2::Node("altitude_monitor")
{
max_altitude_ = this->declare_parameter("max_altitude", 5.0);
pose_sub_ = this->create_subscription<geometry_msgs::msg::PoseStamped>(
as2_names::topics::self_localization::pose,
as2_names::topics::self_localization::qos,
[this](const geometry_msgs::msg::PoseStamped & msg) {
if (msg.pose.position.z > max_altitude_) {
RCLCPP_WARN_THROTTLE(
this->get_logger(), *this->get_clock(), 1000,
"Altitude %.2f m is above the %.2f m limit", msg.pose.position.z, max_altitude_);
}
});
}
private:
double max_altitude_;
rclcpp::Subscription<geometry_msgs::msg::PoseStamped>::SharedPtr pose_sub_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<AltitudeMonitor>());
rclcpp::shutdown();
return 0;
}as2::Node is a ROS 2 node that also knows the drone's frame names. The topic name and its QoS come
from as2_core, so the node listens where the
state estimator publishes. The name is relative, so the node
follows its namespace, like /drone0/self_localization/pose for drone0.
Describe the package
The package.xml and CMakeLists.txt are the same as in any ROS 2 package:
<?xml version="1.0"?>
<package format="3">
<name>altitude_monitor</name>
<version>0.1.0</version>
<description>Warns when the drone flies above a set altitude.</description>
<maintainer email="you@example.com">You</maintainer>
<license>BSD-3-Clause</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>geometry_msgs</depend>
<depend>as2_core</depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>cmake_minimum_required(VERSION 3.8)
project(altitude_monitor)
set(CMAKE_CXX_STANDARD 17)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(as2_core REQUIRED)
add_executable(altitude_monitor_node src/altitude_monitor_node.cpp)
ament_target_dependencies(altitude_monitor_node rclcpp geometry_msgs as2_core)
install(TARGETS altitude_monitor_node DESTINATION lib/${PROJECT_NAME})
ament_package()Tell Pixi how to build it
Each package in the workspace needs a pixi.toml of its own:
[package]
name = "altitude_monitor"
[package.build]
backend = { name = "pixi-build-ros", version = "0.7.*" }
[package.build.config]
extra-package-mappings = [{ as2_core = { conda = ["as2_core"] } }]The pixi-build-ros backend reads the package.xml and builds the package from it. It takes the
name, the version and the dependencies from there, so you don't repeat them.
It also turns each dependency into a RoboStack package name, so rclcpp becomes
ros-humble-rclcpp. Aerostack2 is published under its own names instead, so
extra-package-mappings maps as2_core to the as2_core package. Add an entry for every
Aerostack2 package your package.xml depends on.
Build
pixi installPixi downloads ROS 2 and Aerostack2, already built, then compiles altitude_monitor and installs
it into the environment. Only your package is compiled, so it's done in about a minute.
Run it
Start the node in the drone0 namespace, with a limit of 2 m:
pixi run ros2 run altitude_monitor altitude_monitor_node \
--ros-args -r __ns:=/drone0 -p max_altitude:=2.0In a second terminal, in the same folder, publish a pose 3.5 m up in place of the state estimator:
pixi run ros2 topic pub --qos-reliability best_effort -r 5 \
/drone0/self_localization/pose geometry_msgs/msg/PoseStamped \
'{pose: {position: {z: 3.5}}}'The node warns once a second:
[WARN] [drone0.altitude_monitor]: Altitude 3.50 m is above the 2.00 m limitStop both with Ctrl+C.
Change the code
Edit the node, then run it again. pixi run sees that the package changed and rebuilds it before it
starts, in a few seconds. pixi shell does the same when it opens, and pixi install rebuilds
without running anything.
Add more packages
Each package gets its own folder with a pixi.toml, and a path dependency in the workspace's
pixi.toml. When one of your packages depends on another, list it as a host dependency of the
package that uses it, and map it to nothing so Pixi doesn't look for it in a channel:
[package.build.config]
extra-package-mappings = [{ as2_core = { conda = ["as2_core"] }, limits = { conda = [] } }]
[package.host-dependencies]
limits = { path = "../limits" }Pixi builds limits first, and rebuilds altitude_monitor whenever limits changes.
Fly with it
To run the node next to a drone, add it to a project. The
multirotor simulator project is a
Pixi workspace too, so copy your package's folder into it and add a path dependency to the
project's pixi.toml:
[dependencies]
altitude_monitor = { path = "altitude_monitor" }Then start the drone as the project explains, and run your node in its namespace with pixi run.
Next steps
Last updated on November 3, 2026