Turtlebot3 AWS Warehouse InfluxDB

dc_bridge blesses exactly five Destination types — postgres, s3, file, console, vector (see Destinations) — and InfluxDB is not one of them. This demo is not a peer of the PostgreSQL/RustFS demos: it exists to show how to reach a destination dc_bridge doesn't bless directly, via the ADR-0003 passthrough escape hatch — a raw Vector sink config loaded through custom_config_files, consuming the same public dc.<tag> routes a blessed Destination would. Read Destinations's "Passthrough" section first if you haven't already; this page only covers what's specific to InfluxDB.

You will also need 3 terminal windows, to:

  1. Run the Nav2 turtlebot3 launchfile: it starts localization, navigation and RViz
  2. Run navigation inspection demo
  3. Run DC

Using a different terminal window for DC helps reading its information.

Setup the environment

Python dependencies

For this tutorial, we will need to install all dependencies, the demo dashboard's included (uv owns them; see Setup):

uv sync

Setup the infrastructure

InfluxDB

InfluxDB will be used to store our data and timestamps. To start it, follow the steps (tools/infrastructure/docker/docker-compose.influxdb.yaml, unchanged by the DC 2.0 rework — this demo still needs a real InfluxDB instance to point the passthrough sink at).

One-time: install the passthrough sink config

dc_bridge has no InfluxDB Destination to configure through ROS parameters, so the Vector sink itself ships as a plain file in this package, dc_demos/config/tb3_simulation_influxdb_sink.toml, installed to the package's share directory. Copy it into place once before the first launch:

mkdir -p ~/.dc
cp "$(ros2 pkg prefix dc_demos)/share/dc_demos/config/tb3_simulation_influxdb_sink.toml" ~/.dc/

dc_params_file's custom_config_files (see below) points at this path.

Setup simulation environment

In the terminal 1, source your environment:

source /opt/ros/jazzy/setup.bash
source install/setup.bash

Nothing else has to be exported. dc_simulation puts its own models and worlds on GZ_SIM_RESOURCE_PATH through its environment hook.

Info

This demo used to run aws_robomaker_small_warehouse_world under Gazebo Classic. That package has no Jazzy release — its own jazzy branch still hard-depends on gazebo_ros, which was never published for Jazzy. The warehouse now comes from dc_simulation instead, which vendors the same AWS RoboMaker props (shelves, clutter, trash cans) into a gz-sim world of its own.

Terminal 1: Start Navigation

Then, in the same terminal (1), start the simulation and Nav2. DC comes up separately in terminal 2, so turn it off here:

ros2 launch dc_demos tb3_qrcodes.launch.py \
    headless:=False \
    use_dc:=False

RViz and gz-sim will start: now you see the robot in the warehouse, and the map on RViz. AMCL sets the initial pose itself, so there is no need to click "2D Pose Estimate".

Warning

The warehouse world is heavy — 317 model instances. Expect a slow start and a real-time factor well under 1 without a GPU; see dc_simulation/README.md for measured numbers.

RViz Gazebo

Terminal 2: Start DC

Run colcon build to compile the workspace:

colcon build

Now, start the demo:

ros2 launch dc_demos tb3_simulation_influxdb.launch.py use_sim_time:=True

Warning

use_sim_time defaults to False — without the override above, DC's Position measurement looks up TF transforms on the wall clock while the simulation publishes them on Gazebo's sim clock, which throws "extrapolation into the past"/"transform does not exist" errors and can crash measurement_server outright once the two clocks drift far enough apart.

The robot will start collecting data.

Known issue: a field can silently stop landing

InfluxDB 1.x's line protocol locks a field's type from its first write for the life of the database. cpu.average is 0 (a bare JSON integer, not 0.0) on its very first sample — before the plugin has two polls to compute a delta from — so Vector's influxdb_logs sink writes it as an integer; every later float sample then fails with field type conflict: ... is type float, already exists as type integer and is silently dropped, forever, for that field in that database. Verified: after a full demo run, SELECT count("/cpu/average") FROM dc returns nothing at all. This isn't DC-specific — it's InfluxDB 1.x line protocol meeting JSON's 0/0.0 ambiguity — and the only fix is dropping and recreating the dc database (influx -execute "DROP DATABASE dc; CREATE DATABASE dc") before a run where you need that field.

Terminal 3: Start autonomous navigation

Execute

ros2 run nav2_simple_commander demo_security

The robot will start moving and you will be able to see all visualizations activated in RViz:

RViz-moving

Visualize the data

Grafana's own datasource is PostgreSQL now (see PostgreSQL/RustFS demo), and every dashboard shipped with the DC 2.0 infrastructure — Home, Robot, KPI and Fast DDS statistics — is backed by it: there is no Grafana dashboard for this demo's data, since it never touches PostgreSQL. To look at what landed in InfluxDB, use InfluxDB's own tooling instead:

influx -database dc -execute "SELECT * FROM dc ORDER BY time DESC LIMIT 20"

or query its HTTP API directly:

curl -G 'http://127.0.0.1:8086/query' --data-urlencode "db=dc" --data-urlencode "q=SELECT * FROM dc ORDER BY time DESC LIMIT 20"

Understanding the configuration

Info

The full configuration file can be found here, and the passthrough sink config here.

Measurement server

Measurements

measurement_plugins sets which plugin to load. We collect

System measurements:

  1. CPU
  2. OS
  3. Memory
  4. Uptime

Robot measurements:

  1. Camera images
  2. Command velocities
  3. Distance traveled
  4. Positions
  5. Speed

Environment measurements:

  1. Map

Infrastructure measurements:

  1. InfluxDB health, a TCPHealth check against InfluxDB's own port (8086) — unrelated to the passthrough mechanism, this is the same kind of infrastructure health-check measurement the PostgreSQL/RustFS demo uses for its own destinations.

None of this changed from the destination-agnostic measurement configuration used elsewhere in DC 2.0 — nested/flatten still shape the JSON for InfluxDB's line-protocol-oriented storage, and images (map, camera) are still stored as base64 strings since that's the only field type Grafana (or any consumer reading straight out of InfluxDB) can render from a database column. What changed is only how the Records reach InfluxDB in the first place — see the Destination section below.

measurement_server:
  ros__parameters:
    ...
    camera:
      plugin: "dc_measurements/Camera"
      topic_output: "/dc/measurement/camera"
      save_raw_base64: true
      nested: true
      flatten: true
      ...
    influxdb_health:
      plugin: "dc_measurements/TCPHealth"
      topic_output: "/dc/measurement/influxdb_health"
      polling_interval: 5000
      host: "127.0.0.1"
      port: 8086
      name: "InfluxDB"
      include_measurement_plugin: true
      nested: true
      flatten: true

An example camera Record, now without a tags field (that mechanism no longer exists — a Destination's inputs list decides routing instead). nested: true + flatten: true together produce /-prefixed dotted-path keys such as /camera/camera_name, which is also exactly what lands as the InfluxDB column name, since nothing downstream renames them:

{
  "/camera/base64/raw": "/9j/4AAQSkZJRgABAQAA...",
  "/camera/camera_name": "Intel Realsense",
  "custom_keys": ["robot_name", "id"],
  "date": 1677668926.700422,
  "flattened": true,
  "host": "127.0.0.1",
  "id": "be781e5ffb1e7ee4f817fe7b63e92c32",
  "name": "camera",
  "nested": true,
  "robot_name": "Turtlebot",
  "run_id": "218",
  "source_type": "fluent",
  "tag": "dc.measurement.camera",
  "timestamp": "2026-09-04T00:00:40.252Z"
}

Conditions

We also initialize conditions:

  1. min_distance_traveled
  2. max_distance_traveled

They are used in the distance traveled measurement to only take values in a certain range.

Destination: the passthrough

There is no InfluxDB Destination to bless, but destinations is not empty — it names a file Destination whose inputs are what put these topics on the dc.<tag> routes the snippet consumes:

dc_bridge:
  ros__parameters:
    shipper:
      data_dir: "$HOME/.dc/buffer"
    destinations: ["records_log"]
    records_log:
      type: file
      receives: records
      inputs: ["/dc/measurement/cpu", "/dc/measurement/memory", ...]
      path: "/tmp/dc/tb3_simulation_influxdb_records.ndjson"
      time_key: "date"
      time_format: "double"
    custom_config_files: ["$HOME/.dc/tb3_simulation_influxdb_sink.toml"]
    vector_forward_host: "127.0.0.1"
    vector_forward_port: 24224

dc_bridge derives both its ROS subscriptions and its dc.<tag> route branches from destinations, and never reads a passthrough snippet's inputs — so a passthrough always accompanies at least one blessed Destination covering the same topics. file is the anchor in every passthrough demo in this repo now, including this one — blessed console moved to a passthrough recipe of its own (ADR-0003, Destinations: Recipes) — but it would have been the wrong anchor for this demo regardless, even back when it was still blessed: this demo collects base64 camera and map images, which make for unreadable terminal output. The Elasticsearch tutorial explains the file anchor mechanics in more detail.

Warning

destinations: [] does not work as a way to say "passthrough only". rclcpp cannot load an empty YAML sequence (no inferable element type), so the Bridge dies at startup with parameter_value_from failed for parameter 'destinations': No parameter value set and never reads custom_config_files at all.

custom_config_files lists raw Vector config snippets that are merged as-is alongside whatever dc_bridge itself renders (here, nothing) — see Destinations's passthrough section for the full contract (naming collisions, vector validate as a startup backstop, etc.). The snippet installed above:

# ~/.dc/tb3_simulation_influxdb_sink.toml
[sinks.influxdb]
type = "influxdb_logs"
inputs = [
  "dc.dc.measurement.cpu",
  "dc.dc.measurement.memory",
  "dc.dc.measurement.os",
  "dc.dc.measurement.uptime",
  "dc.dc.measurement.camera",
  "dc.dc.measurement.cmd_vel",
  "dc.dc.measurement.distance_traveled",
  "dc.dc.measurement.position",
  "dc.dc.measurement.speed",
  "dc.dc.measurement.map",
  "dc.dc.measurement.influxdb_health",
]
endpoint = "http://127.0.0.1:8086"
measurement = "dc"

[sinks.influxdb.influxdb1_settings]
database = "dc"
username = "dc"

Each inputs entry is one of the stable dc.<tag> routes dc_bridge exposes for every topic that appears in any Destination's inputs or, as here, any custom_config_files snippet's inputs — the Tag is the topic name with the leading / dropped and the rest of the /s turned into . (/dc/measurement/cpudc.measurement.cpu), and the route is dc.<tag> (so dc.dc.measurement.cpu). Vector's influxdb_logs sink type is what actually understands InfluxDB 1.x's write API; nothing in this snippet is DC-specific beyond the dc.<tag> inputs.

Warning

Set real InfluxDB credentials (or an auth token, depending on your InfluxDB version) in the snippet before pointing this at anything but the demo's own local, unauthenticated-by-default instance.