본문으로 건너뛰기 VDA5050 Protocol Complete Guide | AGV/AMR Fleet Interoper...

VDA5050 Protocol Complete Guide | AGV/AMR Fleet Interoperability· C++ Implementation

VDA5050 Protocol Complete Guide | AGV/AMR Fleet Interoperability· C++ Implementation

이 글의 핵심

VDA5050 is the open interface standard that lets AGVs and AMRs from different manufacturers talk to a single fleet manager over MQTT. This guide covers the topic structure, the order/state/instantActions message types, the Node-Edge-Action graph model that drives every mission, a C++ implementation walkthrough with libVDA5050++, and production deployment patterns.

Why a Warehouse Full of Robots Needs a Common Language

Walk onto the floor of any modern logistics center or manufacturing plant that has scaled past its first pilot deployment, and you’ll usually find more than one type of mobile robot. A fleet of forklift-style AGVs (Automated Guided Vehicles) from one vendor handles pallet moves along fixed lanes, while a newer batch of AMRs (Autonomous Mobile Robots) from a different vendor navigates dynamically around people and obstacles for piece-picking. Each of them, out of the box, speaks its own proprietary protocol to its own proprietary fleet controller.

That’s a problem the moment you want a single dashboard, a single dispatch algorithm, or a single safety-zone coordinator across the whole floor. Before an open standard existed, integrators solved this the expensive way: run parallel fleet management systems side by side, or hand-roll a bespoke adapter for every vendor API a customer happened to buy. Neither approach scales, and both turn “add one more robot vendor” into a multi-month integration project.

VDA5050 exists to close that gap. It’s an open interface standard developed by the German Association of the Automotive Industry (VDA) and the German Mechanical Engineering Industry Association (VDMA), together with the Institute for Material Handling and Logistics (IFL) at the Karlsruhe Institute of Technology and a group of industrial partners. The goal is plug-and-play interoperability: any AGV or AMR that implements VDA5050 correctly should be connectable to any fleet manager that also implements it, regardless of who manufactured the vehicle.

If you’re a robotics engineer evaluating whether to adopt VDA5050 for a new integration, or you’ve inherited a fleet manager codebase and need to add support for a new vehicle type, this guide walks through the standard end to end — the message model, the wire format, a real C++ implementation, and the operational pitfalls that show up once robots are actually moving pallets.

What VDA5050 Actually Standardizes

VDA5050 defines two things: the structure of the information exchanged between a fleet manager (called Master Control in the spec) and a vehicle, and the MQTT topic naming scheme used to route that information. A few design decisions shape everything downstream:

  • Transport: MQTT is the assumed transport layer. A single broker sits in the middle; Master Control and every AGV in the fleet connect to it as MQTT clients.
  • Message format: every message is JSON, and every message type has a publicly documented JSON Schema.
  • Vendor neutrality: VDA5050 says nothing about a robot’s internal navigation stack, motor control, or SLAM implementation. It standardizes only the interface between the fleet manager and the vehicle — what happens inside either side is entirely up to the implementer.
  • Version history: the standard shipped its first release (1.1) in 2019, moved through 2.0 and 2.1 (January 2025), and has continued evolving toward 3.0.0 to better support large, heterogeneous fleets.
flowchart LR
  MC["Master Control
(Fleet Manager)"] <-->|MQTT| Broker(("MQTT Broker")) Broker <--> AGV1["AGV #1
(Vendor A)"] Broker <--> AGV2["AGV #2
(Vendor B)"] Broker <--> AGV3["AMR #3
(Vendor C)"]

Because the broker is the single point of contact, adding a fourth vendor to this picture doesn’t require touching Master Control’s code at all — it just means one more client connecting with the right topic names and message schemas.

MQTT Topic Structure

VDA5050 topic names are deliberately self-describing: the interface, protocol version, manufacturer, and vehicle serial number are all embedded directly in the path.

{interfaceName}/{majorVersion}/{manufacturer}/{serialNumber}/{topic}

For example, a vehicle from manufacturer RobotCompany with serial number 0001 publishing its state message produces this topic:

uagv/v2/RobotCompany/0001/state
  • interfaceName is conventionally uagv (unified AGV).
  • majorVersion is expressed as just the major number (v1, v2, …). Because a major version bump can break schema compatibility, the topic path itself segregates traffic by major version — a v1 client and a v2 client never accidentally cross-subscribe.
  • manufacturer / serialNumber uniquely identify the vehicle. This is what lets Master Control subscribe to a wildcard pattern and route incoming messages to the correct vehicle record internally.
  • topic is one of the six message types covered below: order, state, instantActions, connection, visualization, or factsheet.

One detail that trips up people writing their own MQTT client from scratch: topic segments are case-sensitive. instantactions and instantActions are two different topics as far as the broker is concerned, and a typo here fails silently — the subscriber just never receives anything, with no error to point you at the cause.

The Six Core Message Types

TopicDirectionPurposeQoSRetained
orderMaster Control → AGVThe route (nodes + edges) and actions the vehicle should execute0No
instantActionsMaster Control → AGVOne-off commands to execute immediately — stop, pause, resume0No
stateAGV → Master ControlFull status: position, order progress, battery, errors0No
connectionAGV → Master ControlOnline / offline / connection-broken status, driven by MQTT Last Will1Yes
visualizationAGV → Master ControlLightweight position updates for real-time map rendering0No
factsheetAGV → Master ControlVehicle specs, supported features, physical constraints — near-static0Yes

The retained-flag column matters more than it looks. order and instantActions must never be published as retained messages. If they were, a vehicle reconnecting after a network blip would immediately receive the broker’s cached copy of the last order it was sent — including orders it already completed — and could attempt to re-execute a mission that’s long finished. That’s not a hypothetical edge case; it’s one of the first things new implementers get bitten by. connection and factsheet, on the other hand, should be retained precisely so that Master Control (or any newly connected monitoring client) can learn a vehicle’s current status immediately, without waiting for the vehicle to publish again on its own schedule.

The Order Model: Nodes, Edges, and Actions

The heart of VDA5050 is how it describes a mission. Instead of a single “go to X” command, an order message describes a route graph: an ordered sequence of Nodes (stop points) connected by Edges (the segments between them), where each Node or Edge can carry one or more Actions — pick up a pallet, wait for a traffic light, open a door — to execute at that point in the route.

{
  "headerId": 42,
  "timestamp": "2026-09-11T09:12:00.000Z",
  "version": "2.1.0",
  "manufacturer": "RobotCompany",
  "serialNumber": "0001",
  "orderId": "order-8841",
  "orderUpdateId": 0,
  "nodes": [
    {
      "nodeId": "node-A",
      "sequenceId": 0,
      "released": true,
      "nodePosition": { "x": 12.4, "y": 3.1, "mapId": "warehouse-1f" },
      "actions": []
    },
    {
      "nodeId": "node-B",
      "sequenceId": 2,
      "released": true,
      "nodePosition": { "x": 18.9, "y": 3.1, "mapId": "warehouse-1f" },
      "actions": [
        {
          "actionId": "pick-001",
          "actionType": "pick",
          "blockingType": "HARD",
          "actionParameters": [{ "key": "lhd", "value": "pallet-77" }]
        }
      ]
    }
  ],
  "edges": [
    {
      "edgeId": "edge-A-B",
      "sequenceId": 1,
      "released": true,
      "startNodeId": "node-A",
      "endNodeId": "node-B",
      "actions": []
    }
  ]
}

sequenceId alternates even numbers for Nodes and odd numbers for Edges, which pins down the overall traversal order unambiguously. Only the portion of the route with released: true is safe for the vehicle to actually drive — Master Control can leave the tail of a long route unreleased and fill it in later by re-publishing the order with a higher orderUpdateId. This matters operationally: it means Master Control doesn’t have to fully plan a kilometer-long route before dispatching a vehicle; it can release the route incrementally as downstream planning resolves.

sequenceDiagram
  participant MC as Master Control
  participant Br as MQTT Broker
  participant AGV as AGV

  MC->>Br: PUBLISH order (nodes/edges/actions)
  Br->>AGV: deliver order
  AGV->>Br: PUBLISH state (driving, nodeStates updated)
  Br->>MC: deliver state
  Note over AGV: arrives at node-B, starts pick action
  AGV->>Br: PUBLISH state (actionStates: RUNNING -> FINISHED)
  Br->>MC: deliver state

The state message mirrors this structure back: nodeStates, edgeStates, and actionStates arrays report exactly how far along the route and its actions the vehicle currently is, alongside batteryState, errors, and a driving flag. Exact field composition drifts slightly between major versions, so always cross-check field names against the official JSON Schema repository rather than relying on memory or an older integration’s code.

Action Status: A State Machine, Not a Boolean

It’s tempting to treat an action as a simple “started / done” flag, but VDA5050 defines a proper lifecycle for it via the actionStatus field: WAITING, INITIALIZING, RUNNING, PAUSED, FINISHED, FAILED. A vehicle implementation must report exactly one of these values per action, per state publish, and — critically — the transitions between them need to be driven by explicit triggers rather than left implicit.

stateDiagram-v2
  [*] --> WAITING: order received, action's node/edge not yet reached
  WAITING --> INITIALIZING: execution condition met (node arrival, blockingType order reached)
  INITIALIZING --> RUNNING: hardware control call issued
  RUNNING --> PAUSED: pause requested via instantActions (optional)
  PAUSED --> RUNNING: resume requested via instantActions
  RUNNING --> FINISHED: hardware reports success
  RUNNING --> FAILED: hardware error or timeout
  INITIALIZING --> FAILED: parameter validation or hardware init failure
  FINISHED --> [*]
  FAILED --> [*]

A subtlety worth calling out: WAITING isn’t strictly required by the spec, but reporting it explicitly at least once is cheap and pays off operationally. Without it, a fleet manager UI can’t distinguish “this action is queued and the vehicle knows about it” from “the vehicle never acknowledged this action at all” — a distinction that matters a great deal when you’re debugging a stalled mission at 2 AM.

blockingType: A Safety Constraint, Not Metadata

blockingType is one of three values — NONE, SOFT, HARD — and it isn’t decorative. It’s a constraint the vehicle’s motion controller must actively enforce:

  • NONE: the action runs entirely independent of driving. The vehicle can move on to the next edge regardless of whether the action has finished. Good for side effects like flashing a warning light.
  • SOFT: the vehicle can start moving before the action completes, but the action must remain active until the vehicle fully leaves that node — it’s a “you can start leaving, but don’t cut the action off mid-flight” constraint.
  • HARD: the vehicle must not leave the node — must not advance onto the next released edge — until the action reaches FINISHED or FAILED. Physical, state-changing actions like pick and drop are almost always HARD.

The reasoning for pick being HARD is concrete: if the vehicle starts moving before the forks have fully engaged and lifted a pallet, the load can shift, fall, or collide with the rack. Because the cost of getting this wrong is a safety incident, HARD blocking should be enforced twice — once in the action state machine, and again independently in the low-level motion controller, so that even if the higher-level logic misinterprets a “released” flag, the motion controller’s own “a HARD action is in progress” flag physically refuses the move command.

When multiple actions land on the same node, the actions array’s order is the source of truth for execution order: HARD actions run strictly sequentially in array order; NONE actions can run concurrently with anything; a SOFT action doesn’t block a following HARD action from starting, but the vehicle can’t leave the node until both are done — effectively max(SOFT finish time, HARD finish time).

Implementing VDA5050 in C++: libVDA5050++

VDA5050 doesn’t mandate an implementation language — there’s a Python/ROS package (vda5050_connector) and a TypeScript/Node.js library (vda-5050-lib) among others — but the most mature C++ option is libVDA5050++, released by Fraunhofer IML (Fraunhofer Institute for Material Flow and Logistics) as part of its Silicon Economy research program and published through the Open Logistics Foundation.

Architecture: Two Adapters, One Core

libVDA5050++ encapsulates all of the protocol logic — JSON parsing, schema validation, timing supervision, topic publication — inside the library itself. What you implement is a pair of Adapter interfaces at the two edges of the system:

  • Master Control Adapter (vda5050pp::interface_mc): the fleet-manager-side interface for constructing order/instantActions messages and consuming incoming state/visualization data.
  • AGV Adapter (vda5050pp::interface_agv): the vehicle-side interface that receives driving and action commands from the library and connects them to the robot’s actual hardware or middleware (ROS, a proprietary motor controller, etc.).
flowchart TB
  subgraph "Fleet manager process"
    MC["Your fleet management logic"] --> MCA["Master Control Adapter"]
  end
  subgraph "libVDA5050++"
    MCA <--> Core["VDA5050 protocol core
(order validation, state management, timing supervision)"] Core <--> AGVA["AGV Adapter"] end subgraph "Vehicle control process" AGVA --> Nav["ActionHandler / NavigationHandler implementations"] Nav --> HW["Robot hardware / ROS"] end Core <-->|MQTT| Broker(("MQTT Broker"))

The payoff of this split is that a vehicle manufacturer only needs to implement a handful of hardware-facing handlers — ActionHandler, NavigationHandler — while the library absorbs the repetitive, easy-to-get-wrong parts of the protocol: message parsing, schema validation, timing supervision, and topic publication.

The Interfaces You Actually Implement

If you’re building the AGV side, development mostly consists of filling in the handlers the library expects:

InterfaceResponsibility
ActionHandlerstart/pause/resume/stop — execute the actions specified by order and instantActions
StepBasedNavigationHandlerDriving logic for vehicles that traverse nodes one at a time (line-guided vehicles)
ContinuousNavigationHandlerDriving logic for vehicles that consider multiple nodes concurrently (free-roaming AMRs)
PauseResumeHandlerWires pause/resume requests into the actual motion controller
Connector / ConnectorPassiveChoose whether MQTT send/receive runs on its own thread or is driven by polling
// AGV-side ActionHandler implementation (conceptual sketch)
#include <vda5050++/interface_agv/action_handler.h>

class MyActionHandler : public vda5050pp::interface_agv::ActionHandler {
public:
  void start(const vda5050pp::Action &action) override {
    // Inspect action.actionType (e.g. "pick") and call the real hardware API.
    // Once complete, report the actionState transition RUNNING -> FINISHED
    // back to the library.
  }
  void pause(const vda5050pp::Action &action) override { /* handle pause */ }
  void resume(const vda5050pp::Action &action) override { /* handle resume */ }
  void stop(const vda5050pp::Action &action) override { /* handle abort */ }
};

Vehicle description data is assembled into a vda5050pp::interface_agv::agv_description::AGVDescription struct and handed to the library at startup. The library handle is then driven with one of two threading modes:

// library_handle_ptr was created during library initialization
library_handle_ptr->spinParallel(4); // run asynchronously across 4 internal threads
// or
library_handle_ptr->spinOnce();      // integrate directly into your own event loop (polling)

spinParallel is the lower-friction option for a standalone AGV control process. spinOnce is what you want if the library needs to coexist with an existing event loop — a ROS 2 executor, for example — where spawning independent threads for the protocol stack would fight the rest of your architecture for CPU affinity and scheduling priority.

Wiring It Up with CMake

libVDA5050++ ships as a CMake package, and you link the connector and logging modules you need at the target level:

find_package(libvda5050++ REQUIRED)

target_link_libraries(${PROJECT_NAME} PUBLIC
  libvda5050++::console_logger   # console logging module
  libvda5050++::mqtt_connector   # MQTT send/receive connector
)

For a broader CMake reference (target-based linking, find_package, generator expressions), see the CMake build system guide — the patterns there apply directly to structuring a project around a third-party protocol library like this one.

Because the library is middleware-neutral by design, swapping mqtt_connector for a different connector module is the intended path if a future project needs a non-MQTT transport (OPC UA, for instance) without rewriting the protocol logic itself.

Version caveat: per the library’s own README, its current implementation targets VDA5050 1.1.0, while both the standard and the library’s public interfaces continue to evolve. If your project needs current 2.x/3.x fields, verify field-level support before committing to the library for production use.

Beyond C++: Other Language Implementations

C++ isn’t the only ecosystem with mature VDA5050 tooling:

  • Python / ROS: vda5050_connector runs as a ROS 2 node and translates between ROS actions/topics and VDA5050 MQTT messages. If you’re already building on ROS 2, this is usually the path of least resistance.
  • TypeScript / Node.js: vda-5050-lib focuses on the Master Control side, which pairs naturally with a web-based fleet management dashboard.
  • The official spec repository: VDA5050/VDA5050 is the canonical source for the JSON Schemas and markdown spec documents, independent of language — it’s the first place to check no matter which stack you’re implementing against.

From libVDA5050++ to a Working Pick Action: Where the Real Complexity Lives

Understanding the JSON schema and wiring up ActionHandler is necessary but not sufficient. In practice, the gap between “the schema is implemented” and “the pick action reliably works on the warehouse floor” is where most of the engineering effort actually goes. A few of the failure modes worth designing around up front:

  • Non-blocking hardware calls. ActionHandler::start() must never block waiting for the gripper or fork to finish. A synchronous call there stalls the entire action state machine — including unrelated NONE actions and the state publish cycle — for as long as the hardware takes to respond. The right shape is an async call (a std::future, a callback, or an event-loop-driven state machine) with an explicit watchdog timer, transitioning to FAILED with an errorType like pickTimeout if the hardware doesn’t respond in time.
  • Completion means physically verified, not just acknowledged. Treating “hardware accepted the command” as equivalent to “the pallet is actually secured” is a common and costly mistake. FINISHED should only be reported after verifying the physical result — a load-cell reading confirming weight, for instance — not merely after the drive command returns.
  • Idempotent action handling. Under MQTT QoS 0 or after a reconnect, Master Control can re-publish an order, or a reconnecting vehicle can receive an order it was already partway through. Track processed actionId values on the vehicle side, and if an already-RUNNING or already-FINISHED actionId arrives again, re-publish the last known actionState instead of re-triggering the hardware.
  • Atomic load updates. The moment a pick action reaches FINISHED, the vehicle’s state.load array needs to reflect the newly acquired cargo — loadId, loadType, loadPosition, weight — in the same update cycle as the actionState transition. If the two updates land in separate, non-atomic publishes, there’s a window where Master Control sees a FINISHED pick but an empty load array, and can incorrectly dispatch the vehicle for another pick while it’s still carrying cargo.

If you’re building a production pick-and-place fleet, these action-execution details deserve as much design attention as the message schema itself — the linked Korean deep dive below on the pick-action state machine walks through a complete C++ implementation of this pattern, including the watchdog-timer and load-reporting logic in full.

Real-World Deployment Scenarios

  • Heterogeneous fleet consolidation: when forklift-style AGVs from one vendor and free-roaming AMRs from another need to run under a single fleet management system, adopting VDA5050 as the common interface means each new vendor only requires a new adapter, not a new fleet manager.
  • Bridging legacy robots: for a robot that predates VDA5050 adoption, a thin bridge process that wraps its existing API — publishing state and subscribing to order/instantActions — lets you migrate incrementally instead of replacing the vehicle’s control stack outright.
  • Simulation and integration testing: fleet manager development and testing doesn’t require physical hardware. A simulated AGV that speaks VDA5050 over MQTT lets you exercise order dispatch, edge cases, and failure handling entirely in CI before any hardware is involved.

Best Practices and Troubleshooting

SymptomCommon CauseFix
A reconnecting vehicle re-executes an old orderorder/instantActions published as retainedNever retain these two topics — the spec explicitly prohibits it
Fleet dashboard shows a vehicle as permanently offlineNo MQTT Last Will configured on the connection topicRegister a Last Will publishing CONNECTIONBROKEN to connection at connect time
Topic collisions between vehicles from different vendorsDuplicate or inconsistently formatted manufacturer/serialNumber valuesAgree on an identifier naming convention across the team before onboarding new vendors
An order update never takes effectorderUpdateId wasn’t incremented on resendIncrement orderUpdateId every time order content changes
Multiple fleet managers fight for control of one vehicleMaster Control is redundant but has no coordination logicRun an active-standby topology and ensure only one instance publishes order at a time

Wrapping Up

VDA5050 standardizes the interface between AGVs/AMRs and a fleet manager as MQTT-transported JSON messages, which is what makes it possible to operate a genuinely multi-vendor fleet under one control system. Once you understand the topic structure and the core message types — order (route plus actions), state (current status), instantActions (one-off commands), and the retained connection/factsheet pair — you have enough to start integrating against existing open-source implementations rather than building the protocol layer from scratch. For C++ projects specifically, Fraunhofer IML’s libVDA5050++ cleanly separates Master Control Adapter and AGV Adapter responsibilities behind a middleware-neutral core, which meaningfully reduces the amount of custom adapter code a new vehicle integration requires.