VDA5050으로 AGV·AMR 관제 통신하기: MQTT 토픽, order·state·instantActions, libVDA5050++ C++ 구현
이 글의 핵심
VDA5050은 서로 다른 제조사의 AGV·AMR이 하나의 관제 시스템(Master Control)과 MQTT로 통신할 수 있도록 만든 개방형 표준입니다. order·state·instantActions 같은 메시지 구조와 Topic 규칙, 그리고 Fraunhofer IML이 공개한 C++ 구현체 libVDA5050++를 이용한 통합 방법을 다룹니다.
로봇마다 프로토콜이 다르면 생기는 문제
물류 창고나 제조 현장에 AGV(Automated Guided Vehicle)나 AMR(Autonomous Mobile Robot)을 도입할 때 흔히 마주치는 문제가 있습니다. A사의 무인지게차와 B사의 자율주행 로봇을 같은 현장에서 동시에 운용하려고 하면, 두 로봇이 서로 다른 벤더 전용 프로토콜을 쓰기 때문에 하나의 관제 시스템(Fleet Manager, VDA5050에서는 Master Control이라고 부릅니다)으로 통합 관제가 되지 않습니다. 그래서 벤더마다 별도의 관제 소프트웨어를 두거나, 벤더별 어댑터를 그때그때 직접 만들어야 했습니다.
VDA5050은 이 문제를 표준화로 풀기 위해 등장했습니다. 독일 자동차산업협회(VDA, Verband der Automobilindustrie)와 독일 기계공업연맹(VDMA)이 카를스루에 공과대학(KIT) 물류연구소(IFL) 및 산업 파트너와 함께 개발한 개방형 인터페이스 표준으로, 제조사와 무관하게 AGV·AMR이 관제 시스템과 통신할 수 있는 환경을 목표로 합니다. 표준을 따르는 로봇이라면 원칙적으로 어떤 제조사의 것이든 같은 관제 시스템에 연결할 수 있습니다.
VDA5050이란 무엇인가
VDA5050은 Master Control(관제 시스템)과 AGV/AMR 사이에 주고받을 정보의 구조와, 그 정보를 어떤 MQTT Topic으로 전달할지를 정의한 표준입니다.
전송 계층은 MQTT를 전제로 하며, 하나의 브로커에 관제 시스템과 여러 대의 AGV가 클라이언트로 접속합니다. 모든 메시지는 JSON이고, 메시지 타입마다 공개된 JSON 스키마가 있습니다. 표준은 로봇의 내부 제어 로직이나 항법 알고리즘에는 관여하지 않고, 관제와 로봇 사이의 인터페이스만 정의합니다. 버전은 1.x(2019년 1.1)에서 시작해 2022년 2.0, 이후 2.1로 개정되어 왔고, 다음 메이저 버전 작업도 진행되어 왔으므로 실제 도입 시점의 최신 버전은 공식 저장소에서 확인하는 것이 안전합니다.
flowchart LR
MC["Master Control<br/>(Fleet Manager)"] <-->|MQTT| Broker(("MQTT Broker"))
Broker <--> AGV1["AGV #1<br/>(제조사 A)"]
Broker <--> AGV2["AGV #2<br/>(제조사 B)"]
Broker <--> AGV3["AMR #3<br/>(제조사 C)"]
MQTT Topic 구조
VDA5050은 Topic 이름에 어떤 인터페이스·버전·제조사·차량인지가 드러나도록 계층 규칙을 정해 두었습니다.
{interfaceName}/{majorVersion}/{manufacturer}/{serialNumber}/{topic}
예를 들어 제조사 RobotCompany의 시리얼 번호 0001 차량이 state를 발행한다면 다음과 같은 Topic이 됩니다.
uagv/v2/RobotCompany/0001/state
각 계층의 의미는 다음과 같습니다.
interfaceName: 보통uagv(unified AGV)를 사용합니다.majorVersion:v1,v2처럼 메이저 버전만 표기합니다. 메이저 버전이 다르면 스키마 호환을 보장하지 않으므로 Topic 자체를 분리합니다.manufacturer,serialNumber: 차량을 유일하게 식별하는 값으로, 관제 시스템이 여러 제조사의 차량을 동시에 구독·발행할 때 이 값으로 라우팅합니다.topic: 아래에서 다루는order,state,instantActions,connection,visualization,factsheet중 하나입니다.
MQTT Topic 이름은 대소문자를 구분하므로 instantactions와 instantActions는 서로 다른 Topic입니다. 클라이언트를 직접 구현할 때 이 철자 차이 때문에 명령이 전달되지 않는 경우가 있으니 주의해야 합니다.
핵심 메시지 타입
| Topic | 방향 | 역할 | QoS | Retain |
|---|---|---|---|---|
order | Master Control → AGV | 노드·엣지로 구성된 이동 경로와 수행할 액션을 전달 | 0 | 아니오 |
instantActions | Master Control → AGV | 정지·일시정지·재개 등 즉시 실행할 단발성 명령 | 0 | 아니오 |
state | AGV → Master Control | 현재 위치, 주문 진행 상황, 배터리, 에러 등 전체 상태 | 0 | 아니오 |
connection | AGV → Master Control | 온라인/오프라인/연결 끊김 상태 (Last Will 활용) | 1 | 예 |
visualization | AGV → Master Control | 관제 화면에서 실시간 위치를 그리기 위한 경량 상태 | 0 | 아니오 |
factsheet | AGV → Master Control | 차량 제원, 지원 기능, 제약 조건 (정적에 가까운 정보) | 0 | 예 |
order와 instantActions는 Retain하지 않아야 합니다. Retain 메시지로 남겨 두면 차량이 재접속할 때마다 브로커가 예전 명령을 다시 전달해, 차량이 이미 끝난 주문을 또 실행하려 드는 오작동으로 이어질 수 있기 때문입니다. 반대로 connection과 factsheet는 Retain해 두어야 관제 시스템이나 새로 접속한 클라이언트가 즉시 최신 상태를 알 수 있습니다.
Order 메시지: Node·Edge·Action 모델
VDA5050의 order는 이동 경로를 그래프로 표현합니다. 정지 지점인 Node와 Node를 잇는 구간인 Edge의 배열로 경로 전체를 기술하고, 각 Node·Edge에는 그 지점에서 수행할 Action(예: 화물 픽업)을 붙일 수 있습니다.
{
"headerId": 42,
"timestamp": "2026-09-07T09: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는 Node에 짝수, Edge에 홀수를 번갈아 부여해 전체 순서를 명시합니다. 차량은 released: true인 구간(base)까지만 실제로 진행하고, released: false인 뒷부분(horizon)은 앞으로 갈 예정 경로로만 참고합니다. 관제 시스템은 같은 orderId에 orderUpdateId를 올린 주문을 보내 horizon을 점진적으로 release할 수 있으므로, 장거리 경로를 처음부터 모두 확정하지 않아도 됩니다. 업데이트의 첫 노드를 직전 base의 마지막 노드와 맞춰 이어붙이는 규칙, 로봇이 주문을 받아들이거나 거부하는 판단 순서, nodeStates·lastNodeId로 진행을 보고하는 방법은 VDA5050 order 수명주기에서 예제 메시지로 자세히 다룹니다.
sequenceDiagram participant MC as Master Control participant Br as MQTT Broker participant AGV as AGV MC->>Br: PUBLISH order (nodes/edges/actions) Br->>AGV: order 전달 AGV->>Br: PUBLISH state (driving, nodeStates 갱신) Br->>MC: state 전달 Note over AGV: node-B 도착, pick 액션 시작 AGV->>Br: PUBLISH state (actionStates: RUNNING → FINISHED) Br->>MC: state 전달
state 메시지는 nodeStates, edgeStates, actionStates, batteryState, errors, driving 같은 필드로 현재 진행 상황을 보고합니다. 정확한 필드 구성은 VDA5050 버전에 따라 다르므로, 구현할 때는 공식 JSON 스키마 저장소를 함께 확인해야 합니다.
C++로 구현하기: libVDA5050++
VDA5050 표준은 구현 언어를 규정하지 않기 때문에 ROS 2용 vda5050_connector, TypeScript/Node.js용 vda-5050-lib 등 여러 구현체가 있습니다. C++ 쪽의 대표적인 오픈소스 라이브러리가 libVDA5050++입니다. Fraunhofer IML(프라운호퍼 물류연구소)이 Silicon Economy 연구 프로젝트의 일환으로 개발해 Open Logistics Foundation을 통해 공개했습니다.
아래 인터페이스 이름과 코드는 라이브러리 1.x 계열(VDA5050 1.1 기준) API를 바탕으로 한 개념 스케치입니다. 이후 메이저 버전에서 VDA5050 2.x 지원이 추가되면서 클래스 구성과 초기화 방식이 바뀌었으므로, 실제 코드는 사용하는 라이브러리 버전의 문서와 예제를 기준으로 작성해야 합니다.
아키텍처: 두 개의 어댑터로 분리
libVDA5050++는 VDA5050 프로토콜 로직을 라이브러리 내부에 캡슐화하고, 사용자는 양쪽 끝의 어댑터 인터페이스만 구현하도록 설계되어 있습니다. Master Control 쪽 어댑터는 관제 시스템에서 order·instantActions를 만들고 state·visualization을 수신하는 역할을 합니다. AGV 쪽 어댑터는 라이브러리가 전달하는 주행·액션 명령을 받아 로봇 하드웨어나 ROS 같은 미들웨어로 연결합니다.
flowchart TB
subgraph "관제 시스템 프로세스"
MC["Fleet Manager 로직"] --> MCA["Master Control Adapter"]
end
subgraph "libVDA5050++"
MCA <--> Core["VDA5050 프로토콜 코어<br/>(order 검증·상태 관리·타이밍 감시)"]
Core <--> AGVA["AGV Adapter"]
end
subgraph "차량 제어 프로세스"
AGVA --> Nav["ActionHandler / NavigationHandler 구현체"]
Nav --> HW["실제 로봇 하드웨어·ROS"]
end
Core <-->|MQTT| Broker(("MQTT Broker"))
이 구조에서 로봇 제조사는 자사 하드웨어와 연결되는 액션·주행 핸들러만 구현하면 되고, 메시지 파싱·검증·타이밍 감시·Topic 발행 같은 반복적인 프로토콜 로직은 라이브러리가 처리합니다.
구현해야 하는 인터페이스
AGV 쪽을 구현한다면 라이브러리가 요구하는 핸들러를 채워 넣는 방식으로 개발합니다. 1.x 계열 기준으로 정리하면 다음과 같습니다.
| 인터페이스 | 역할 |
|---|---|
ActionHandler | start/pause/resume/stop — order·instantActions로 지정된 액션 실행 |
StepBasedNavigationHandler | 노드를 하나씩 순서대로 밟아가는(line-guided) 차량의 주행 처리 |
ContinuousNavigationHandler | 여러 노드를 함께 고려하는 자유 주행(AMR) 차량의 주행 처리 |
PauseResumeHandler | 일시정지·재개 요청을 실제 주행 제어와 연동 |
Connector / ConnectorPassive | MQTT 송수신을 별도 스레드로 처리할지, 폴링 방식으로 처리할지 선택 |
// AGV 쪽에서 구현하는 ActionHandler 예시 (개념 스케치)
#include <vda5050++/interface_agv/action_handler.h>
class MyActionHandler : public vda5050pp::interface_agv::ActionHandler {
public:
void start(const vda5050pp::Action &action) override {
// action.actionType (예: "pick")을 보고 실제 하드웨어 제어 API 호출
// 완료되면 라이브러리에 actionState를 RUNNING -> FINISHED로 보고
}
void pause(const vda5050pp::Action &action) override { /* 일시정지 처리 */ }
void resume(const vda5050pp::Action &action) override { /* 재개 처리 */ }
void stop(const vda5050pp::Action &action) override { /* 중단 처리 */ }
};
차량 설명 정보는 AGVDescription 구조체로 구성해 라이브러리에 전달하고, 라이브러리 핸들은 스레드 모드를 선택해 실행합니다.
// library_handle_ptr 은 라이브러리 초기화 과정에서 생성한 핸들
library_handle_ptr->spinParallel(4); // 내부 스레드 4개로 비동기 실행
// 또는
library_handle_ptr->spinOnce(); // 이벤트 루프에 직접 통합(폴링 방식)
spinParallel은 라이브러리가 자체 스레드에서 메시지를 처리하므로 핸들러 코드가 다른 스레드에서 호출된다는 점을 고려해 공유 상태를 보호해야 합니다. spinOnce는 기존 제어 루프에 넣어 호출 시점을 직접 통제할 수 있는 대신, 루프가 오래 막히면 메시지 처리도 함께 지연됩니다.
CMake로 통합하기
libVDA5050++는 CMake 패키지로 배포되며, 필요한 커넥터·로거 모듈을 타깃 단위로 링크합니다. 타깃 이름은 라이브러리 버전에 따라 다를 수 있으므로 설치한 버전의 README를 확인해야 합니다.
find_package(libvda5050++ REQUIRED)
target_link_libraries(${PROJECT_NAME} PUBLIC
libvda5050++::console_logger # 콘솔 로깅 모듈
libvda5050++::mqtt_connector # MQTT 송수신 커넥터
)
전송 계층이 커넥터 모듈로 분리되어 있으므로, MQTT 외의 전송 수단이 필요하면 커넥터를 교체하는 방식으로 대응할 수 있습니다.
다른 언어 구현체
C++ 외에도 VDA5050을 채택한 생태계마다 구현체가 있습니다. ROS 2 기반이라면 vda5050_connector가 ROS 토픽·액션과 VDA5050 MQTT 메시지를 서로 변환해 주므로 가장 접근하기 쉽습니다. TypeScript/Node.js의 vda-5050-lib는 Master Control 쪽과 AGV 쪽 컨트롤러 클래스를 모두 제공해, 웹 기반 관제 도구나 가상 AGV를 만들 때 쓸 수 있습니다. 어떤 언어로 직접 구현하든 가장 먼저 확인할 출처는 JSON 스키마와 스펙 문서가 정리된 공식 저장소 VDA5050/VDA5050입니다.
도입 시나리오
VDA5050이 가장 큰 효과를 내는 경우는 이기종 AGV 플릿 통합입니다. 서로 다른 제조사의 지게차형 AGV와 자율주행 카트를 하나의 관제 소프트웨어로 운용할 때 VDA5050을 공통 인터페이스로 정해 두면, 새 차량을 들일 때 해당 차량이 표준을 지원하는지만 확인하면 됩니다.
이미 운용 중인 로봇이 VDA5050을 지원하지 않는다면, 로봇의 기존 API를 감싸는 얇은 브리지 프로세스를 만들어 state를 발행하고 order/instantActions를 구독하게 하는 방식으로 점진적으로 옮겨 갈 수 있습니다. 실제 로봇 없이 관제 시스템을 개발할 때도 VDA5050 메시지를 주고받는 가상 AGV를 MQTT 클라이언트로 띄워 통합 테스트를 진행할 수 있습니다.
트러블슈팅
| 증상 | 흔한 원인 | 대응 |
|---|---|---|
| 재접속한 차량이 예전 주문을 다시 실행함 | order/instantActions를 Retain으로 발행 | 두 Topic은 Retain하지 않기 |
| 관제 화면에서 차량의 연결 끊김이 감지되지 않음 | connection 토픽에 Last Will 미설정 | MQTT 연결 시 Last Will을 connection Topic에 CONNECTIONBROKEN으로 등록 |
| 서로 다른 제조사 차량 간 Topic이 충돌함 | manufacturer/serialNumber 값이 중복되거나 형식이 다름 | 식별자 네이밍 규칙을 먼저 합의 |
수정한 order가 반영되지 않음 | orderUpdateId를 올리지 않고 같은 주문을 재전송 | 주문 내용이 바뀔 때마다 orderUpdateId를 증가 |
| 여러 관제 시스템이 한 차량을 동시에 제어하려 함 | Master Control을 이중화했는데 조율 로직이 없음 | 액티브-스탠바이 구조로 두고 한 번에 하나의 Master Control만 order를 발행하도록 강제 |