Skip to content

Bosch BNO085 + u-blox ZED-F9P (outdoor, differential drive)

Platform: Differential drive outdoor robots Status: Community contributed — running on Oakland University Robotics Association "Erdferkel" (IGVC competition, Michigan) and Agroecology Lab agricultural robot (UK). Field validation results pending.


Sensors

Sensor Model Notes
IMU Bosch BNO085 9-axis (accel + gyro + magnetometer). Typically 100 Hz.
GPS u-blox ZED-F9P Standard GPS (~2m CEP) or RTK float/fixed. 5-10 Hz.
Wheel odometry Platform-specific nav_msgs/Odometry via differential drive controller.

IMU datasheet specs used: - Gyro noise density: 0.014 dps/√Hz → σ ≈ 0.005 rad/s at 100 Hz - Accel noise density: 120 μg/√Hz → σ ≈ 0.1 m/s² at 100 Hz (conservative for vibration)

Note on the BNO085 magnetometer: The onboard magnetometer can provide immediate yaw initialization without needing GPS motion first, but magnetic interference from motors and wiring makes it unreliable on most robot platforms. Leave imu.has_magnetometer: false unless you've calibrated and verified your magnetic environment is clean.


Config

fusioncore:
  ros__parameters:
    base_frame: base_link
    odom_frame: odom
    publish_rate: 100.0
    publish.force_2d: true

    motion_model: "DifferentialDrive"

    # Bosch BNO085: 9-axis consumer IMU.
    # Most platforms benefit from leaving the magnetometer off due to motor interference.
    imu.has_magnetometer: false
    imu.gyro_noise: 0.005           # rad/s : 0.014 dps/sqrt(Hz) at 100 Hz
    imu.accel_noise: 0.1            # m/s^2 : conservative for wheeled robot vibration
    imu.remove_gravitational_acceleration: false  # most BNO085 drivers output raw
    imu.frame_id: "imu_link"

    # Generic outdoor differential drive encoders.
    # Tighten vel_noise if your encoders are high resolution (>= 500 CPR).
    encoder.vel_noise: 0.05         # m/s
    encoder.yaw_noise: 0.02         # rad/s

    # u-blox ZED-F9P: CEP depends on fix type.
    # Standard GPS (no corrections): CEP ~2.0m -> base_noise_xy: 2.0
    # RTK float (NTRIP corrections):  CEP ~0.4m -> base_noise_xy: 0.5, min_fix_type: 3
    # RTK fixed:                       CEP ~0.01m -> base_noise_xy: 0.015, min_fix_type: 4
    # Start with standard GPS values. Tighten when corrections are stable.
    gnss.base_noise_xy: 2.0         # m : F9P autonomous CEP
    gnss.base_noise_z: 4.0          # m
    gnss.max_hdop: 3.5              # F9P tracks GPS + GLONASS + Galileo: HDOP usually good
    gnss.min_satellites: 4
    gnss.min_fix_type: 1            # 1=GPS, 3=RTK_FLOAT, 4=RTK_FIXED

    # Measure from base_link to GPS antenna phase center: x=forward, y=left, z=up.
    gnss.lever_arm_x: 0.0
    gnss.lever_arm_y: 0.0
    gnss.lever_arm_z: 0.0

    gnss.fix2_topic: ""
    gnss.heading_topic: ""          # set if using dual-antenna F9P for heading
    gnss.azimuth_topic: ""

    # Raw magnetometer heading: fuses sensor_msgs/MagneticField directly.
    # The BNO085 publishes this on /imu/mag at 100 Hz via the Hillcrest driver.
    # Requires hard/soft iron calibration (use imu_calib or magneto).
    # Useful for GPS-denied phases where track heading is unavailable.
    magnetometer.enabled: false
    magnetometer.topic: "/imu/mag"
    magnetometer.noise_rad: 0.05
    magnetometer.chi2_threshold: 9.21
    magnetometer.declination_rad: 0.0    # look up at magnetic-declination.com
    magnetometer.hard_iron: [0.0, 0.0, 0.0]
    magnetometer.soft_iron: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]

    outlier_rejection: true
    outlier_threshold_gnss: 16.27   # chi2(3, 0.999)
    outlier_threshold_enc: 11.34    # chi2(3, 0.999)
    outlier_threshold_imu: 15.09    # chi2(6, 0.999)

    gnss.coast_n: 3
    gnss.coast_q_factor: 10.0
    gnss.coast_timeout_s: 30.0

    adaptive.imu: true
    adaptive.encoder: true
    adaptive.gnss: true
    adaptive.window: 50
    adaptive.alpha: 0.01

    ukf.q_position: 0.01
    ukf.q_orientation: 1.0e-9
    ukf.q_velocity: 0.1
    ukf.q_angular_vel: 0.1
    ukf.q_acceleration: 1.0
    ukf.q_gyro_bias: 1.0e-5
    ukf.q_accel_bias: 1.0e-5
    ukf.q_encoder_wz_bias: 1.0e-7

    input.gnss_crs: "EPSG:4326"
    output.crs: "EPSG:4978"
    output.convert_to_enu_at_reference: true
    reference.use_first_fix: true

Topic remaps

Topic names vary by driver. Common setups:

# Adafruit BNO085 breakout + ublox_ros2_driver
ros2 launch fusioncore_ros fusioncore.launch.py \
  fusioncore_config:=/path/to/this-config.yaml \
  --ros-args \
  -r /imu/data:=/bno085/imu \
  -r /gnss/fix:=/fix \
  -r /odom/wheels:=/diff_controller/odom

Common BNO085 drivers and their default topics: - bno085_ros2 package: publishes at /bno085/imu → remap -r /imu/data:=/bno085/imu - Adafruit CircuitPython bridge: check your specific node's topic

Common F9P drivers: - ublox_ros2_driver: publishes NavSatFix at /fix → remap -r /gnss/fix:=/fix - ublox_gps_node: check your configuration

Wheel odometry (depends on your motor controller): - ROS 2 Control diff_drive_controller: /diff_controller/odom - Nav2 default: /odom - Custom: whatever your node publishes


RTK upgrade

When NTRIP corrections are available and the F9P reaches RTK float or fixed:

# RTK float (NTRIP corrections, carrier phase partial):
gnss.base_noise_xy: 0.5
gnss.min_fix_type: 3

# RTK fixed (centimeter-level, all carrier phases resolved):
gnss.base_noise_xy: 0.015
gnss.min_fix_type: 4

The F9P publishes fix quality in NavSatFix.status.status. FusionCore's min_fix_type maps to that field: 1=GPS, 2=DGPS/SBAS, 3=RTK_FLOAT, 4=RTK_FIXED.


Deployers

  • Zachary Lain (@ZacharyLain), Oakland University Robotics Association: "Erdferkel" differential-drive robot, u-blox F9P + BNO085, IGVC competition Michigan.
  • Sam (@samuk), Agroecology Lab: outdoor agricultural tracked robot, dual u-blox F9P RTK + BNO085, ROS 2 Humble, UK.

Contributing a field validation result? Open a pull request or discussion with your rosbag or trajectory plot.