Aerostack2

DocsPlugins

State estimator

The ground truth, raw odometry and EKF plugins, and the settings they take.

Authored by
  • @alvgaona

The state estimator tells the rest of the stack where the drone is, and its plugin decides where that comes from. What each plugin is for and how the TF tree fits together is on the state estimator concept page.

PluginReadsWhere the pose goes
ground_truthAn exact pose, or a motion capture rigid bodyodom → base_link, with map → odom fixed
raw_odometryAny nav_msgs/msg/Odometry, and optionally GPSodom → base_link, with map → odom fixed
simple_ekfThe IMU, plus any number of pose and velocity sourcesDrifting sources in odom → base_link, absolute ones in map → odom

The node publishes self_localization/pose in earth and self_localization/twist in base_link. Both start once the plugin has reported every transform it owns and a velocity.

Running it

Pass the plugin's settings as plugin_config_file. Your file is merged over the plugin's defaults, so it only needs what you change:

config/state_estimator.yaml
/**:
  ros__parameters:
    raw_odometry:
      odom_sub_topic: "/lio/odometry"
ros2 launch as2_state_estimator state_estimator_launch.py \
  namespace:=drone0 \
  plugin_name:=raw_odometry \
  plugin_config_file:=config/state_estimator.yaml

Add use_sim_time:=true in simulation.

Check what was applied

Plugin settings can also go in config_file, under the node's name, as the example projects' config.yaml does. Either way, check what the plugin read with ros2 param get /drone0/state_estimator raw_odometry.odom_sub_topic, or in the startup log, which lists every setting.

ParameterDefaultWhat it does
plugin_nameThe plugin to load. Required
publish_hz0.0How often the state is published, in Hz. 0 publishes on every update
<plugin>.debug_publish_hz-1.0How often the plugin's own estimate is published under state_estimation/<plugin>/. -1 publishes on every update, and 0 turns it off

The frame names are the ones every Aerostack2 node shares, described under frame names.

Ground truth

ground_truth takes an exact pose in the earth frame, from a topic or from one rigid body of a mocap4r2 motion capture system. Poses on the topic have to be stamped in earth, and poses in any other frame are dropped. Motion capture poses are always taken as earth.

Each setting goes under ground_truth, such as ground_truth.twist_sub_topic:

ParameterDefaultWhat it does
pose_sub_topicground_truth/poseThe pose topic, as geometry_msgs/msg/PoseStamped
mocap_sub_topicemptyA mocap4r2_msgs/msg/RigidBodies topic. When set, it's used instead of the pose topic
rigid_body_name33The rigid body to track, when reading motion capture
twist_sub_topicemptyA geometry_msgs/msg/TwistStamped topic in base_link. Empty differentiates the pose instead
twist_smooth_filter_cte1.0Smooths the differentiated velocity, above 0 and up to 1. 1 means no smoothing
reject_repeated_positionsfalseDrop poses that repeat the last position
earth_map_transform.set_earth_mapfalsePlace map with the pose below. When false, map sits where the first pose is
earth_map_transform.position.x, .y, .z0.0Where map sits in earth, in meters
earth_map_transform.orientation.roll, .pitch, .yaw0.0How map is turned in earth, in radians

Set twist_sub_topic only to a topic that's published. The pose is only published together with a velocity, so a silent velocity topic means no pose.

Raw odometry

raw_odometry takes an odometry as it is. Its pose becomes odom → base_link, and map → odom stays at zero. With GPS, it also places the drone on the globe and offers the set_origin and get_origin services.

Each setting goes under raw_odometry, such as raw_odometry.use_gps:

ParameterDefaultWhat it does
odom_sub_topicsensor_measurements/odomThe odometry topic, as nav_msgs/msg/Odometry
earth_map_transform.set_earth_mapfalsePlace map with the pose below. When false, map sits at the origin of earth, or at the first GPS fix when use_gps is on
earth_map_transform.position.x, .y, .z0.0Where map sits in earth, in meters
earth_map_transform.orientation.roll, .pitch, .yaw0.0How map is turned in earth, in radians
use_gpsfalseUse GPS to set the origin, from sensor_measurements/gps
gps_origin.set_originfirst_gpsWhere the origin comes from. first_gps, manual or service
gps_origin.coordinates.latitude, .longitude, .altitude0.0The origin, in manual mode

With GPS on, the pose starts once the first fix arrives, and in service mode once set_origin has been called too. The origin can only be set once.

Simple EKF

simple_ekf is a 15-state extended Kalman filter, with position, velocity, attitude and the IMU biases. It predicts with every IMU message and corrects with each source listed in update_topics.

The filter starts once map is placed. That happens with the first pose of a source whose set_earth_map is true, or with the first IMU message when no source has it. The pose that places map must be stamped exactly earth or map, or the filter never starts.

Each setting goes under simple_ekf, such as simple_ekf.timer_hz:

ParameterDefaultWhat it does
predict_topicsensor_measurements/imuThe IMU topic, assumed to be in base_link
update_topics["posestamped1"]The names of the correction sources, each with its own block
platform_topicplatform/infoWhere to read whether the drone is flying. Empty counts it as always flying
use_armfalseCount the drone as flying once armed, instead of once in offboard
gravity9.81Gravity, in m/s²
timer_hz100.0How often the filter smooths map → odom and runs its checks, in Hz
map_odom_alpha0.1How quickly map → odom follows corrections, above 0 and up to 1. 1 turns the smoothing off
max_update_latency_ms1000.0The oldest correction the filter accepts, in milliseconds
max_imu_dt_ms200.0The longest gap between IMU messages it predicts over, in milliseconds
unobserved_variance100.0The variance used for a part of a message its source doesn't measure, marked with a variance of 0 or less. Keep it at or below 1e4
initial_covariance.position, .velocity, .orientation0.0The initial uncertainty of each state
initial_covariance.bias_acc, .bias_gyro1e-8The initial uncertainty of the IMU biases
imu_params.accelerometer_noise_density1e-3The accelerometer noise
imu_params.gyroscope_noise_density1e-4The gyroscope noise
imu_params.accelerometer_random_walk1e-4How fast the accelerometer bias wanders
imu_params.gyroscope_random_walk1e-5How fast the gyroscope bias wanders
earth_map_transform.static_tftruePublish earth → map once, as a static transform
earth_map_transform.set_earth_mapfalsePlace map with a fixed pose. Every source must then have set_earth_map false, or its first pose replaces it
preflight_correction.variance1e-5How firmly the state is held at the start pose until the drone first flies
preflight_correction.position.x, .y, .z0.0The start position in map, in meters
preflight_correction.orientation.roll, .pitch, .yaw0.0The start orientation in map, in radians
verbosetrueLog the filter's status
debug_verbosefalseLog every step, for tuning
internal_ekf_debug_topicsemptyA topic prefix for the filter's internal state. Empty turns it off

The default source, posestamped1, reads a PoseStamped on ground_truth/pose and places map.

When the IMU stops, the filter can predict from the commands sent to the drone instead, with the imu_model_fallback settings. They're described in the plugin's README.

Correction sources

Each name in update_topics has a block of its own, such as simple_ekf.mocap.topic:

ParameterDefaultWhat it does
topicThe topic. Required
typeThe full message type, such as geometry_msgs/msg/PoseStamped. Required
set_earth_mapPlace map with this source's first pose. Required
use_message_covarianceUse the covariance in the messages instead of the fixed values below. Required
is_odometrytrue for nav_msgs/msg/Odometry, false for the restCorrect odom → base_link for a source that drifts, or map → odom for an absolute one
position_covariance, orientation_covariance1e-4 and 1e-5 on each axisThe fixed noise of a pose source
position_multiplier, orientation_multiplier1.0 on each axisScale the message covariance of a pose source
linear_covariance1e-2 on each axisThe fixed noise of a velocity source
linear_multiplier1.0 on each axisScale the message covariance of a velocity source
is_body_frametrueRead a velocity source in base_link. When false, the frame comes from the message
rigid_body_nameThe rigid body, for motion capture. Required for that type
update_rate_hz0.0The most corrections per second from this source. 0 means no limit
innovation_gate0.0Reject corrections more than this many standard deviations from the estimate. 0 turns it off
innovation_gate_timeout1.0After this many seconds of rejections, accept the source again
reject_repeated_positionstrue for motion captureDrop poses that repeat the last position
repeated_position_threshold1e-6How close a position must be to count as repeated, in meters

The sources can be geometry_msgs/msg/PoseStamped, geometry_msgs/msg/PoseWithCovarianceStamped, nav_msgs/msg/Odometry, mocap4r2_msgs/msg/RigidBodies and geometry_msgs/msg/TwistWithCovarianceStamped, written in full. Any other type is ignored. The plugin's README explains how to tune the filter.

Last updated on November 3, 2026

On this page

Last updated on November 3, 2026