One stop, three faults, two runtimes

An AMR is driving across a warehouse when a person steps into its path. It stops in time. A second later, three faults show up in medkit, from two different runtimes:

[
  {
    "fault_code": "PROTECTIVE_STOP",
    "severity": 2,
    "reporting_sources": ["safety_gate"]
  },
  {
    "fault_code": "ACTION_NAVIGATE_TO_POSE_ABORTED",
    "severity": 2,
    "reporting_sources": ["/bt_navigator"]
  },
  {
    "fault_code": "NAVIGATION_GOAL_ABORTED",
    "severity": 2,
    "reporting_sources": ["/bt_navigator"]
  }
]

PROTECTIVE_STOP comes from a safety function on RTMaps 5. The other two are Nav2, on ROS 2. One physical event, two runtimes, and all three landing in the same fault tree. The rest of this is how that tree gets built, and why the stop runs where it runs.

Safety on RTMaps 5, navigation on ROS 2

Nav2 plans routes and drives the robot, and it is good at it. Guaranteeing the robot stops in time is a different problem: you need a bounded reaction time on every cycle, not most of them. ROS 2 on Linux can't give you that. The scheduler, the network stack, and the rest of a general-purpose OS all sit between a LiDAR return and a brake command, so the response is quick on average, and average is not the bar a safety function is held to. Fine for navigation, wrong for the last line of defense.

So the stop runs on RTMaps 5, Intempora's real-time runtime, certified to ISO 26262 (ASIL-B). It runs on its own schedule, independent of the ROS 2 process, and the same component can move onto a safety RTOS like eSOL eMCOS later. We are not the only ones drawing this line. QNX's 2026 survey of 1,000 robotics developers found 91% still run safety-critical work on a general-purpose OS, and NVIDIA's Halos splits it the same way: Linux for perception, a certified RTOS for the safety functions.

The gate itself is small. It reads the LiDAR /scan and the velocity Nav2 wants to send, checks the front sector (0.8 m), and clamps the speed to zero if anything is inside it. If the scan goes stale, it fails closed and stops anyway. A handful of blocks in an RTMaps diagram, and that is the point: keep it out of Nav2, so you can change the navigation stack without touching the safety side. This is a mixed-criticality robot, a safety function on an ASIL-B runtime alongside a quality-managed navigation stack, and running them on separate runtimes, eventually separate partitions on a safety RTOS, is what gives the safety function freedom from interference: the navigation side cannot disturb its timing or starve its schedule.

Two medkit gateways, one SOVD tree

Split the runtime and you split the diagnostics. The protective stop lives in RTMaps; the aborted goal lives in ROS 2. medkit stitches them back together over SOVD, Service-Oriented Vehicle Diagnostics (ISO 17978-3), and it runs the same on both sides:

  • ros2_medkit (open source) serves SOVD for the ROS 2 stack: Nav2, the controllers, the sensors, every node that publishes diagnostics.
  • rtmaps_medkit serves SOVD from inside the RTMaps 5 runtime. It reads the live RTMaps graph and exposes its faults, live data, and freeze frames over the same REST tree.

Both speak SOVD instead of some private log format, so medkit federates them into one tree behind one API. The protective stop lands next to every Nav2 node, on a single timeline.

The federated SOVD entity tree in medkit: ROS 2 areas and the RTMaps Safety Island in one tree

The Safety Island is the RTMaps 5 side; everything else comes from ROS 2. You drill into either without knowing, or caring, which runtime it lives on.

Pull the protective stop out of the live fault list over REST:

curl http://robot-01:8080/api/v1/faults \
  | jq '.items[] | select(.fault_code == "PROTECTIVE_STOP")'
{
  "fault_code": "PROTECTIVE_STOP",
  "fault_name": "RTMaps fault PROTECTIVE_STOP",
  "severity": 2,
  "severity_label": "error",
  "status": { "aggregatedStatus": "active", "confirmedDTC": "1", "testFailed": "1" },
  "reporting_sources": ["safety_gate"],
  "entity_id": "safety_gate",
  "environment_data": {
    "snapshots": [
      {
        "type": "freeze_frame",
        "name": "RTMaps freeze-frame",
        "data": {
          "commanded_vx": { "value": 0.0, "history": [[35333591782, 0.0]] },
          "min_range_m": {
            "value": 5.0,
            "history": [[35333291470, 2.749], [35333862814, 0.11]]
          },
          "stop_threshold_m": { "value": 0.8, "history": [[35333591782, 0.8]] }
        }
      }
    ]
  }
}

The freeze frame is a short rolling window of the gate's own inputs, each port carrying a value and a (timestamp, value) history. commanded_vx stays pinned at 0.0, the gate is holding the robot, while min_range_m drops to 0.11 m, well inside the 0.8 m stop_threshold_m, as the person crosses the sector. medkit keeps a black-box recording of the same window. When the person moves away and the range climbs back over the threshold, the fault heals, which is how the operator knows it is safe to move again.

Architecture and data flow

One robot, two runtimes, one diagnostic view

What happens, start to finish, from someone stepping into the path to a fleet reroute:

  1. The LiDAR sees something in the front sector closer than 0.8 m.
  2. The RTMaps 5 safety gate clamps the commanded velocity to zero and raises PROTECTIVE_STOP with a freeze frame.
  3. With the robot held stopped, Nav2 can't make progress and gives up on the goal: ACTION_NAVIGATE_TO_POSE_ABORTED, then NAVIGATION_GOAL_ABORTED.
  4. rtmaps_medkit and ros2_medkit each expose their faults over SOVD.
  5. medkit federates both into one entity tree, and all three faults sit on one timeline.
  6. A VDA 5050 agent maps the stop to a fleet error, so the fleet manager can route other robots around the aisle.
  7. The engineer reads the full record over REST or the web UI, freeze frame and recording included.

Steps 1 to 3 are real-time, on the robot. Steps 4 to 7 are diagnostics, and not one of them needs an SSH session.

Watch it run

A short run on the simulated AMR: the stop, the three faults from both runtimes in one medkit tree, and the freeze-frame chart of the LiDAR distance dropping through the threshold.

What this means

This is the open-core model doing its job. ros2_medkit is open source under Apache 2.0 and already gives any ROS 2 robot SOVD diagnostics. rtmaps_medkit carries the same tree into RTMaps 5, the safety-certified runtime where the protective functions actually run, so one SOVD API covers the whole robot instead of just the Linux half. We built the gate and the RTMaps 5 integration together with the team at Intempora.

The contract is SOVD, not the runtime. The safety island can be RTMaps 5 today and a different certified runtime tomorrow, and nothing changes about how you observe the robot. If your factory also runs PLCs, the OPC-UA bridge pulls them into the same tree; for the fleet side, there is VDA 5050 + SOVD.