Adding an Injector
An injector is the transport-specific part of the framework. Topic injectors subscribe to a raw input topic, apply active faults, and republish the result on the consumer-facing output topic. Service injectors proxy a ROS service and can alter the response path.
Use this guide when adding support for a new ROS message type, service type, or other injectable ROS interface.
1. Pick the Injector Type
Choose a short type string for YAML scenarios. Existing examples are:
odom
scan
joint_state
imu
tf
trigger_service
The type should describe the ROS interface being faulted, not the fault itself. For example, prefer battery_state over battery_voltage_drop.
2. Create the Typed Injector Class
Create a header in:
include/ros2_fault_injection/injectors/
and a source file in:
src/injectors/
The class should derive from FaultInjectorBase:
class BatteryStateFaultInjector : public FaultInjectorBase {
public:
explicit BatteryStateFaultInjector(rclcpp::Node& node, const InjectorConfig& config);
private:
void on_battery_state(const sensor_msgs::msg::BatteryState::SharedPtr msg);
rclcpp::Subscription<sensor_msgs::msg::BatteryState>::SharedPtr sub_;
rclcpp::Publisher<sensor_msgs::msg::BatteryState>::SharedPtr pub_;
};
The constructor should create the subscription and publisher using config.topic->input_topic, config.topic->output_topic, and config.topic->qos_depth.
For service injectors, use the service-specific configuration instead of topic fields. For example, a trigger-service injector uses config.trigger_service->proxy_service and config.trigger_service->target_service.
3. Follow the Proxy Pattern
The callback should copy the incoming message, apply common faults, apply message-specific faults, then publish:
void BatteryStateFaultInjector::on_battery_state(
const sensor_msgs::msg::BatteryState::SharedPtr msg) {
std::lock_guard<std::mutex> lock(mutex_);
if (should_drop()) {
return;
}
auto output = *msg;
apply_voltage_bias(output);
apply_voltage_noise(output);
pub_->publish(output);
}
If the message type supports delay, follow the existing delayed-message pattern used by odom, scan, joint state, and IMU injectors.
4. Add Fault Config Keys
Add the new keys to fault_config_schema so validation, services, and docs agree on what the injector accepts.
Example:
{"battery_state",
{"voltage_bias", "voltage_noise_stddev", "drop_probability", "delay_ms"}}
Use the common keys when they make sense:
drop_probability
delay_ms
Add value validation in scenario_validator for keys with constraints, such as non-negative standard deviations or probabilities in the range 0.0 to 1.0.
5. Add a Plugin Wrapper
Injectors are discovered through pluginlib. The factory does not need a new if branch for each injector type.
For a built-in injector, add a small wrapper class to:
include/ros2_fault_injection/core/builtin_fault_injector_plugin.hpp
src/core/builtin_fault_injector_plugin.cpp
The wrapper is default-constructible for pluginlib, then creates the real injector when the framework passes in the node and injector configuration:
class BatteryStateFaultInjectorPlugin : public FaultInjectorPlugin {
public:
std::string type() const override { return "battery_state"; }
std::shared_ptr<FaultInjector> create(
rclcpp::Node& node, const InjectorConfig& config) override {
return std::make_shared<BatteryStateFaultInjector>(node, config);
}
};
Export the wrapper in the plugin source file:
PLUGINLIB_EXPORT_CLASS(
ros2_fault_injection::BatteryStateFaultInjectorPlugin,
ros2_fault_injection::FaultInjectorPlugin)
Then register it in fault_injector_plugins.xml:
<class
name="ros2_fault_injection/BatteryStateFaultInjectorPlugin"
type="ros2_fault_injection::BatteryStateFaultInjectorPlugin"
base_class_type="ros2_fault_injection::FaultInjectorPlugin">
<description>Battery state fault injector.</description>
</class>
External packages can provide their own wrappers too. They should depend on ros2_fault_injection, implement FaultInjectorPlugin, and export their own plugin XML with:
pluginlib_export_plugin_description_file(ros2_fault_injection your_plugins.xml)
6. Update CMake
Add the new source file to the core library:
add_library(${PROJECT_NAME}_core
...
src/injectors/battery_state_fault_injector.cpp
)
If you add the plugin wrapper to a new source file, add that source file to the core library too.
If the injector needs a new ROS package dependency, add it to:
CMakeLists.txtpackage.xml
Most sensor message injectors use sensor_msgs, which is already present.
7. Add Example YAML
Add either a focused config file in config/ or extend multi_injector_faults.yaml:
injectors:
- id: battery
type: battery_state
topic:
input_topic: /battery_state_raw
output_topic: /battery_state
qos_depth: 10
faults:
- id: battery_voltage_bias
injector_id: battery
active_on_startup: false
config:
voltage_bias: -1.5
8. Add Tests
At minimum, add tests for:
schema accepts the new config keys
validator rejects invalid values
the injector mutates messages as expected
FaultInjectorFactorycan create the injector through pluginlibservice config updates work for the new keys, if they are runtime configurable
Prefer small focused tests. Use fake injectors for service and scheduler behavior, and typed injector tests only when checking message mutation.
9. Update Documentation
Update:
README.mdthis guide if the process changes
Doxygen comments on the new public header
Then regenerate API docs:
cd /home/reece/fault_injection_ws
source /opt/ros/jazzy/setup.bash
colcon build --symlink-install --packages-select ros2_fault_injection --cmake-target generate_doxygen_api
Checklist
New injector header and source file added
Plugin wrapper added
Plugin XML exports the wrapper
Factory creates the injector through pluginlib
CMake builds the source file
package.xmlhas any new dependenciesconfig schema includes new keys
validator checks key values where needed
example YAML added
README updated
tests added
Doxygen docs regenerate successfully