Adding a Custom Sensor Plugin to a Gazebo Robot Without the Load-Failure Guesswork
Build, declare, and verify a custom Gazebo sensor plugin as a shared library — with a concrete SDF example, load-failure diagnosis table, and a negative test to prove your checks work.
11 Nov 2025, 03:18 UTC

What you're building and why it fails so often
You want a piece of your own C++ code running inside the simulation: reading a sensor each step, publishing custom data, or tweaking model behavior. In Gazebo that means a plugin — a shared library (.so) declared in your SDF file that Gazebo loads when the world starts. The frustrating part is that almost every failure happens at load time: the simulator either can't find the library, or finds one compiled against the wrong Gazebo version and crashes or silently skips it. This guide walks through the full loop — build, declare, verify, recover — so a failed load is a diagnosable event, not a mystery.
Before you start
- A working Gazebo installation. Know which generation you have. Classic Gazebo (gazebo11) and modern Gazebo (formerly Ignition, e.g., Fortress, Harmonic) have different plugin APIs, CMake package names, and environment variables. Code from a tutorial written for one will not compile against the other. Check with
gazebo --version(classic) orgz sim --version(modern). - A C++ toolchain, CMake, and the Gazebo development headers matching your installed major version exactly.
- Basic familiarity with SDF: plugins are declared inside
<model>or<sensor>elements, so you need to know where your robot's model file lives.
Step 1: Write the plugin class
Create a class inheriting from the appropriate base. For classic Gazebo, a model plugin looks like this:
// my_sensor_plugin.cc (classic Gazebo / gazebo11)
#include <gazebo/gazebo.hh>
#include <gazebo/physics/physics.hh>
namespace gazebo {
class MySensorPlugin : public ModelPlugin {
public:
void Load(physics::ModelPtr model, sdf::ElementPtr sdf) override {
gzmsg << "MySensorPlugin loaded on " << model->GetName() << "\n";
// Read custom SDF parameters here, e.g.:
// double rate = sdf->Get<double>("publish_rate", 10.0).first;
}
};
GZ_REGISTER_MODEL_PLUGIN(MySensorPlugin)
}The debug line in Load() is deliberate — it becomes your primary load-confirmation signal later. For modern Gazebo you instead write a system implementing ISystemConfigure and register it with GZ_ADD_PLUGIN; the structure is analogous but the headers and macros differ.
Step 2: Build it as a shared library
A minimal CMakeLists.txt for classic Gazebo:
cmake_minimum_required(VERSION 3.10)
project(my_sensor_plugin)
find_package(gazebo REQUIRED)
include_directories(${GAZEBO_INCLUDE_DIRS})
add_library(my_sensor_plugin SHARED my_sensor_plugin.cc)
target_link_libraries(my_sensor_plugin ${GAZEBO_LIBRARIES})Build in a separate directory: mkdir build && cd build && cmake .. && make. This produces libmy_sensor_plugin.so. You need write access only to your own project directory; no root required.
Step 3: Make the library discoverable and declare it in SDF
Gazebo finds plugins through a search path environment variable — GAZEBO_PLUGIN_PATH in classic Gazebo, GZ_SIM_SYSTEM_PLUGIN_PATH in modern Gazebo. Point it at your build output:
export GAZEBO_PLUGIN_PATH=$GAZEBO_PLUGIN_PATH:/home/you/my_plugin/buildThen reference the plugin in your model's SDF, inside the <model> element:
<plugin name="my_sensor_plugin" filename="libmy_sensor_plugin.so"/>The filename must match the actual library file name, including the lib prefix and .so suffix. A mismatch here is the single most common cause of "failed to load plugin" errors.
Step 4: Verify the load — don't assume it worked
Launch a minimal world containing only the plugin-tagged model, with verbose output so load errors surface:
gazebo --verbose minimal.world # classic
gz sim -v 4 minimal.sdf # modernExpected checks, in order:
- Your
gzmsgline fromLoad()appears in the console. If it doesn't, the plugin never loaded — stop here and go to recovery. - If your plugin publishes, list topics (
gz topic -l, orrostopic listunder ROS integration) and echo the output topic to confirm data flows at the rate you configured. - Negative test: temporarily change the SDF filename to something wrong and relaunch. You should see a clear "failed to load plugin" message. If you don't, your verification method isn't actually detecting failures — fix that before trusting check 1.
When it fails: reading the error
| Symptom | Likely cause | Fix |
|---|---|---|
| "Failed to load plugin libmy_sensor_plugin.so" | Not on the plugin path, or filename mismatch | Check echo $GAZEBO_PLUGIN_PATH and ls the directory; compare the SDF filename byte-for-byte |
| Undefined symbol / missing symbol errors | Compiled against different Gazebo headers than the installed runtime (ABI mismatch) | Rebuild against the dev package matching the installed major version; never mix major versions |
| Gazebo crashes on launch with no clear message | Plugin code faulting in Load() | Remove the <plugin> tag; if the world launches cleanly, the crash is in your code — add logging or run under a debugger |
| Plugin loads but no topic data | Update/publish logic not wired, or wrong topic name | Confirm the topic name in code vs. what you echo; add a counter log in the publish path |
Limits and cautions
Keep per-step work small: anything in a plugin's update callback runs inside the simulation loop, and heavy computation drags down the real-time factor for the whole world. Also treat version mixing as a hard rule — a plugin built for gazebo11 will not load safely under Harmonic, and vice versa; the failure mode is often a crash rather than a clean error. Finally, note that exact environment variable names and CMake package names differ between Gazebo generations and are worth confirming against the documentation for your specific installed release before scripting around them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.