Triggers

Description

A Trigger fires a one-shot signal — a dc_interfaces/msg/FlushEvent — when a composition of Conditions goes from false to true. Measurements listening for that event release the window of recent data they have been holding back, so the seconds leading up to an event are collected too, alongside the seconds after it.

The motivating case is incident review: when an autonomous robot emergency-brakes, what matters is what happened before the brake. A Condition cannot express that — it gates collection going forward, and by the time it turns true the run-up is gone. A Trigger can, because the Measurement was already buffering.

Trigger vs Condition

Both are described with the same if_all/if_any/if_none Condition lists, and a Trigger is built out of Condition plugins — but they answer different questions.

ConditionTrigger
Question it answers"Should this Measurement be collecting right now?""Did something just happen?"
Shape of the signalA level: true for as long as the predicate holdsAn edge: fires once on the false→true transition
Effect on collectionGates live publishing while trueReleases a pre-collected buffer, once
Data it can give youEverything from the moment it turned trueEverything from the buffer_duration_sec before the event, plus post-roll
Where it is configuredPer Measurement (if_all_conditions, …)On the trigger_broadcast_node, once for the whole robot
How Measurements see itDirectly, as their own gateIndirectly, as a FlushEvent on a topic several Measurements can share
ScopeOne MeasurementEvery Measurement subscribed to its flush_topic

They compose: a Trigger is made of Conditions, and a buffering Measurement can still be gated by its own Conditions. Nothing about a Trigger changes what a Condition means.

Trigger is a specific word here

Before this feature existed, "trigger" was loose talk for "a Condition turning on", and the glossary told you to avoid the word. It now names a real, distinct plugin type. Say Condition for a gate that stays true, and Trigger only for the edge-firing plugin documented here.

The flush broadcast

The trigger_broadcast_node loads the Condition plugins named in condition_plugins (the same Condition plugins Measurements use) plus one Trigger plugin, polls the Trigger on a timer, and publishes a FlushEvent on a configurable topic (/dc/flush by default) each time it fires:

                    ┌──────────────────────┐
   /odom, … ───────▶│  Condition plugins   │
                    └──────────┬───────────┘
                               │ if_all / if_any / if_none
                    ┌──────────▼───────────┐
                    │  Trigger (EdgeTrigger)│  false → true?
                    └──────────┬───────────┘
                               │ FlushEvent { incident_id }
                    ┌──────────▼───────────┐
                    │      /dc/flush       │
                    └───┬──────────────┬───┘
                        │              │
              ┌─────────▼────┐  ┌──────▼───────┐
              │ Measurement A│  │ Measurement B│   release their buffered windows,
              │  (buffering) │  │  (buffering) │   both tagged with the same incident_id
              └──────────────┘  └──────────────┘

The node mints a fresh incident_id (a UUID) for every firing; nothing else generates one, so every subscriber of one event shares one ID. Several Measurements can point at the same flush_topic, which is the whole reason the signal is broadcast on a topic rather than wired per Measurement: adding a sensor stream to an existing incident-capture setup needs no change to the Trigger.

Only self-updating Conditions are meaningful here

A Measurement passes its own freshly-collected Record to each Condition it consults, so plugins like Compare read a field out of that Record. A Trigger has no Record of its own: it only makes sense to compose Conditions that maintain their own state from a subscription, such as Moving.

Running the broadcast node

trigger_broadcast_node is a lifecycle node, like the Measurement server, but it is not part of dc_bringup's launch file yet — run it alongside the rest of the stack and transition it yourself (or add it to your own launch file and lifecycle manager node_names):

ros2 run dc_triggers trigger_broadcast_node --ros-args --params-file <your_params.yaml>

A Measurement subscribes to a plain topic, so anything publishing a FlushEvent on flush_topic releases the buffer — including ros2 topic pub, which is how the tools/e2e/scripts/run_incident.sh scenario drives a flush.

FlushEvent

dc_interfaces/msg/FlushEvent is the entire contract between the Trigger side and the Measurement side:

FieldTypeDescription
incident_idstringUUID minted by the broadcast node, one per firing

A Measurement adopts the incident_id it receives; it never generates its own. An event that arrives while a Measurement is already flushing, in post-roll, or in cooldown is ignored.

incident_id

incident_id is a typed field of the dc_interfaces/msg/StringStamped Record envelope, beside group_key — not a key nested inside the measurement's own data. Every Record and File released by one flush cycle carries the same value, so "everything from this one event" is a single query rather than a timestamp range reconstructed by hand:

SELECT * FROM dc_records WHERE incident_id = '3f2b1c7e-…' ORDER BY date;
  • A postgres Destination writes it to its own incident_id column — the column must exist in the table beforehand; see Destinations.
  • A Record collected outside an incident carries an empty incident_id, leaving the column NULL.
  • A Group carries a member's envelope incident_id onto its own output envelope, so grouping does not bury it.
  • Every Destination receives it as a top-level key of the JSON the Bridge ships: the Bridge lifts the envelope field into the payload before handing a Record to its Destinations.

Available plugins

NameDescription
Edge triggerFires once on the false→true rising edge of an if_all/if_any/if_none Condition composition

Node parameters

Parameter nameDescriptionType(s)Default
condition_pluginsName of the condition plugins to loadlist[str][]

Plugin parameters

Every Trigger plugin loaded by this node is namespaced under trigger and shares these parameters:

Parameter nameDescriptionType(s)Default
trigger.pluginName of the trigger plugin to loadstrN/A (mandatory)
trigger.if_all_conditionsFire only once every named Condition is activelist[str][]
trigger.if_any_conditionsFire once any named Condition is activelist[str][]
trigger.if_none_conditionsFire only once no named Condition is activelist[str][]
trigger.topicTopic FlushEvent messages are published onstr"/dc/flush"
trigger.polling_intervalInterval in milliseconds at which the composed Condition state is checkedint100

Measurement parameters

The other half of the feature lives on each Measurement: what it buffers, and what it does once a FlushEvent releases it. All five are optional, and a Measurement that leaves buffer_duration_sec at 0 behaves exactly as it always has — this feature is entirely opt-in. They are documented in full, with the state machine and how Files are staged, in Measurements.

Parameter nameDescriptionType(s)Default
buffer_duration_secSeconds of history to hold instead of publishing live; 0 disables buffering entirelyfloat0
post_roll_duration_secSeconds to keep publishing live after the release finishes, still tagged with the same incident_id; 0 means pre-roll onlyfloat0
cooldown_secSeconds to ignore further FlushEvents once post-roll ends, before buffering re-arms itself; 0 re-arms immediatelyfloat0
max_flush_rate_hzCeiling in Records per second on how fast the released window is emitted; 0 releases it in one burstfloat0
flush_topicTopic the FlushEvent that releases this Measurement is received onstr"/dc/flush"

An armed Measurement moves through four states per incident — Buffering (accumulate, publish nothing) → Flushing (release the window, oldest first, rate-limited) → PostRoll (publish live, same incident_id) → Cooldown (ignore further events) → back to Buffering, with no manual re-arm.

Sizing max_flush_rate_hz

A Measurement's data publisher is KeepLast(1). A whole window published back-to-back in one callback is coalesced down to its last Record before any subscriber runs, so a burst release (max_flush_rate_hz: 0) only delivers the full window to a subscriber that keeps up. Set a rate comfortably above the collection rate being released — 20 Hz for a 2 Hz Measurement — and the window is paced out intact, which is also what keeps a backlog dump from competing with live collection right after an incident.

Example

An emergency-brake capture: the robot buffers the last 10 seconds of uptime data, and the moment it starts moving the buffer is released and collection continues live for 5 more seconds. Swap the moving Condition for whatever expresses your event.

trigger_broadcast_node's parameters:

trigger_broadcast_node:
  ros__parameters:
    condition_plugins: ["moving"]

    moving:
      plugin: "dc_conditions/Moving"
      odom_topic: "/odom"

    trigger:
      plugin: "dc_triggers/EdgeTrigger"
      if_all_conditions: ["moving"]
      topic: "/dc/flush"
      polling_interval: 100

The Measurements listening for it:

measurement_server:
  ros__parameters:
    measurement_plugins: ["uptime", "memory"]

    # Armed: publishes nothing until a FlushEvent arrives on /dc/flush, then releases the
    # last 10 seconds of collection tagged with that event's incident_id.
    uptime:
      plugin: "dc_measurements/Uptime"
      polling_interval: 500
      group_key: "uptime"
      buffer_duration_sec: 10.0
      post_roll_duration_sec: 5.0
      cooldown_sec: 30.0
      max_flush_rate_hz: 20.0
      flush_topic: "/dc/flush"

    # Not armed: collects live as usual, and its Records leave incident_id NULL.
    memory:
      plugin: "dc_measurements/Memory"
      polling_interval: 1000
      group_key: "memory"

A runnable end-to-end version of this — two Measurements, one armed, both landing in the same Postgres table, with the incident_id column asserted by query — is tools/e2e/scripts/run_incident.sh.