VDA5050 state 메시지 필드 해설: nodeStates·actionStates·errors와 발행 시점
이 글의 핵심
state는 AGV가 관제에 자신의 상태를 알리는 하나뿐인 토픽입니다. 이벤트가 있을 때 또는 늦어도 30초마다 발행되고, 남은 노드·엣지, 마지막으로 지난 노드, 액션 상태, 적재물, 배터리, 오류가 한 메시지에 모입니다. 표준 문서의 필드 정의를 읽는 순서대로 풀고, libVDA5050++ 위의 시뮬레이터가 실제로 내보낸 메시지로 확인합니다.
state는 VDA5050의 여섯 토픽 중 관제가 가장 많이 읽는 메시지입니다. 차량이 어디까지 갔고, 무엇을 하고 있고, 무엇이 잘못됐는지가 모두 여기에 있습니다. 이 글은 VDA5050 2.0.0 문서(공식 저장소 2.0.0 태그의 VDA5050_EN_V1.md, 머리말에는 “RELEASE CANDIDATE, FOR REVIEW”로 표기)의 6.10절을 필드 단위로 풀고, libVDA5050++ v2.11.3 위에 만든 시뮬레이터가 실제로 내보낸 state와 대조합니다. 환경은 Ubuntu 24.04 aarch64 컨테이너와 eclipse-mosquitto:2입니다. 표준이 정한 내용과, 이 라이브러리·어댑터의 동작은 구분해서 적었습니다.
order 쪽 규칙은 order 수명주기, 액션 상태 전이는 pick 액션 처리와 instantActions 총정리에서 다룹니다. 라이브러리로 state를 채우는 법은 libVDA5050++ 어댑터 만들기에 있습니다.
state는 언제 발행되나
표준은 AGV의 상태를 토픽 하나로만 보내게 합니다. order, 배터리, 오류를 따로 보내면 브로커와 관제의 처리량이 늘고 정보가 서로 어긋날 수 있어서 한 메시지로 묶었다고 설명합니다. 발행 시점은 “관련 이벤트가 있을 때, 늦어도 30초마다”입니다. 이벤트로 꼽은 것은 아래와 같습니다.
- order 수신, order update 수신
- 적재 상태의 변화
- 오류나 경고
- 노드 통과
- 운영 모드 전환
driving필드의 변화nodeStates,edgeStates,actionStates의 변화
표준은 통신량을 줄이려 노력하라고 적고, 서로 연관된 이벤트(새 order를 받으면 노드·엣지 상태가 함께 바뀌는 것, 노드를 지나면서 그 상태가 바뀌는 것)는 여러 번이 아니라 한 번의 state로 묶는 것이 합리적이라고 합니다. 토픽 이름은 <interface>/<버전>/<manufacturer>/<serialNumber>/state이고, QoS 0(최대 한 번)을 쓰도록 정해져 있습니다.
최소 예제 어댑터(기본 설정)에서 3m를 이동하는 동안 받은 state는 order를 받았을 때와 노드에 도착했을 때 두 번뿐이었습니다. 이동 중의 위치는 visualization 토픽으로 따로 옵니다. 표준에서 visualization은 구현이 선택이고 위치를 더 자주 내보내는 시각화 전용 토픽입니다. libVDA5050++의 state 최대 주기 기본값은 30000ms로, 표준의 30초와 같습니다.
실제 state 한 건
아래는 n0(0,0)에서 n1(3,0)로 가서 pick하고, 그다음 n2(3,2)는 horizon(released: false)으로 둔 order를 보낸 직후의 state입니다. 길이를 줄이려고 batteryState, safetyState 등 뒤에서 다룰 부분은 생략했습니다.
{
"orderId": "st-1",
"orderUpdateId": 0,
"lastNodeId": "n0",
"lastNodeSequenceId": 0,
"driving": true,
"agvPosition": { "x": 0.051, "y": 0.0, "theta": 0.0, "mapId": "map", "positionInitialized": true },
"nodeStates": [
{ "nodeId": "n1", "sequenceId": 2, "released": true,
"nodePosition": { "x": 3.0, "y": 0.0, "mapId": "map", "allowedDeviationXY": 0.5, "allowedDeviationTheta": 3.14 } },
{ "nodeId": "n2", "sequenceId": 4, "released": false,
"nodePosition": { "x": 3.0, "y": 2.0, "mapId": "map", "allowedDeviationXY": 0.5, "allowedDeviationTheta": 3.14 } }
],
"edgeStates": [
{ "edgeId": "e01", "sequenceId": 1, "released": true },
{ "edgeId": "e12", "sequenceId": 3, "released": false }
],
"actionStates": [ { "actionId": "a_pick", "actionType": "pick", "actionStatus": "WAITING" } ],
"loads": [],
"operatingMode": "AUTOMATIC",
"errors": [],
"information": []
}
이 한 건에서 읽을 수 있는 것은 이렇습니다. 차량은 n0(seq 0)에 있었고 움직이는 중입니다. 남은 노드는 n1(확정)과 n2(예고)이고, 남은 엣지는 e01(확정)과 e12(예고)입니다. pick 액션은 n1에 도착해야 시작되므로 WAITING입니다.
order를 식별하는 필드
orderId: 현재 order 또는 직전에 끝난 order의 ID입니다. 새 order를 받을 때까지 유지되고, 한 번도 order가 없으면 빈 문자열입니다. 시뮬레이터에서 order가 없을 때는""였고,cancelOrder뒤에도 값이 유지됐습니다(표준이 취소 뒤orderId와orderUpdateId를 유지한다고 정한 것과 맞습니다).orderUpdateId: 차량이 받아들인 order update의 번호입니다. 없으면 0입니다. 관제는 이 값으로 “내가 보낸 update가 반영됐는가”를 확인합니다.zoneSetId: 선택 필드입니다. 차량이 경로 계획에 쓰는 zone set의 ID이고, order의 값과 다르면 차량이 order를 거절해야 합니다. 시뮬레이터는 zone을 쓰지 않아 이 필드를 내보내지 않았습니다.
어디까지 갔는가: lastNodeId와 nodeStates, edgeStates
표준의 핵심 규칙은 “남은 것만 담는다”입니다. nodeStates와 edgeStates에는 차량이 앞으로 통과해야 할 노드와 엣지만 들어 있습니다. 노드를 지나면 차량은 그 노드의 nodeState를 목록에서 지우고 lastNodeId와 lastNodeSequenceId를 그 노드의 값으로 바꿉니다. 노드 통과는 곧 앞선 엣지를 벗어났다는 뜻이라 엣지도 edgeStates에서 지워지고, 그 엣지에서 돌던 액션은 끝나야 합니다. 같은 순간에 다음 엣지에 들어가므로 그 엣지의 액션이 트리거됩니다. 다만 차량이 엣지 위에서 멈춰 있어야 한다면(소프트 또는 하드 블로킹 엣지 등) 다시 움직이기 시작할 때 엣지에 들어간 것으로 칩니다.
노드를 “통과했다”고 판단하는 주체는 차량입니다. 표준은 일반적으로 차량의 제어점이 노드의 deviationRangeXY 안에 있고 방향이 deviationRangeTheta 안에 있을 때라고 적습니다. lastNodeSequenceId가 필요한 이유는 같은 nodeId가 order 안에 여러 번 나올 수 있기 때문입니다.
시뮬레이터의 타임라인으로 확인하면 이렇습니다.
| 시점 | lastNodeId | nodeStates | edgeStates | 비고 |
|---|---|---|---|---|
| order 수락 직후 | n0 (seq 0) | n1(확정), n2(예고) | e01(확정), e12(예고) | driving: true |
| n1 도착 | n1 (seq 2) | n2(예고) | e12(예고) | driving: false, pick RUNNING |
| pick 종료 | n1 (seq 2) | n2(예고) | e12(예고) | pick FINISHED, loads에 L1 |
n1에서 차량이 멈춘 채 더 가지 않은 것은 다음 노드 n2가 예고(released: false)이기 때문입니다. 확정된 구간 끝에서 서고, order update로 horizon이 풀려야 다시 갑니다. base, horizon, update 규칙은 order 수명주기에 있습니다.
지금 어디에 있고 움직이는가
agvPosition: 지도 위 위치입니다. 자기 위치를 파악할 수 없는 차량(예: 라인 추종)은 생략할 수 있는 선택 필드입니다.positionInitialized가 false면 위치가 초기화되지 않은 것이고,localizationScore(0.0이 위치 불명, 1.0이 확실)와deviationRange는 선택이며 로그와 시각화용입니다.mapId는 층마다 따로입니다. 엘리베이터를 타면 출발 층 지도에서 사라지고 도착 층의 엘리베이터 노드에 나타난다고 표준이 설명합니다.velocity: 차량 좌표계의 속도(vx,vy,omega)입니다. 선택입니다. 시뮬레이터는 이 필드를 내보내지 않았습니다.driving: 차량이 이동하거나 회전하는 중이면 true입니다. 리프트 동작 같은 다른 움직임은 포함하지 않습니다. 이 값은 라이브러리가 알 수 없어서 어댑터가 직접 알려야 하고, 어댑터 가이드에서 알리지 않으면 이동 중에도 false로 나오는 것을 확인했습니다.paused: 하드웨어 버튼이나 instantActionstartPause로 일시정지 중이면 true입니다. 차량은 order를 재개할 수 있습니다.newBaseRequest: 차량이 base의 끝에 거의 도달해 새 base가 오지 않으면 속도를 줄여야 할 때 true입니다. 관제가 base를 더 보내라는 신호이고, 불필요한 감속을 막는 용도라고 6.10.3절이 설명합니다. 시뮬레이터는 이 필드를 내보내지 않았습니다.distanceSinceLastNode: 라인 추종 차량이lastNodeId를 지난 뒤 주행한 거리(m)입니다.
actionStates
차량이 받은 모든 액션(order의 node·edge에 붙은 것과 instantAction)은 actionStates의 항목으로 나타나고, 새 order를 받을 때까지 유지됩니다. 완료될 때마다 actionStatus를 FINISHED로 바꾼 state를 한 번 더 발행하고, 해당되면 resultDescription을 함께 보냅니다. 위 타임라인에서도 pick이 WAITING에서 RUNNING, FINISHED로 바뀔 때마다 state가 나왔습니다. 시뮬레이터가 보낸 항목은 actionId, actionType, actionStatus 세 필드였고, 표준상 actionType은 정보 제공이나 시각화용 선택 필드입니다(order에서 이미 알려 준 값이라서). 각 상태의 의미와 전이는 instantActions 총정리의 actionStatus 절에 있습니다.
loads
차량이 지금 싣고 있는 짐입니다. 선택 필드이고, 표준은 두 경우를 구분합니다. 적재 상태를 판단할 수 없는 차량은 배열을 아예 생략하고, 판단할 수 있는데 배열이 비어 있으면 짐이 없는 것으로 간주합니다. 시뮬레이터는 pick 전에는 "loads": [], pick 뒤에는 [{"loadId": "L1", "loadType": "EPAL"}]를 내보냈습니다. 반면 어댑터 가이드의 최소 예제는 적재 정보를 한 번도 넣지 않았더니 state에 loads 키 자체가 없었습니다. 관제가 loads 없음과 loads: []를 같은 뜻으로 처리하면 안 되는 이유입니다. loadId는 바코드나 RFID 같은 고유 식별자, loadPosition은 차량의 어느 적재 위치인지, weight는 kg입니다. 바운딩 박스(boundingBoxReference, loadDimensions)도 선택으로 정의돼 있습니다.
batteryState와 operatingMode
batteryState에는 batteryCharge(%, 단계값만 제공하는 차량은 나쁨 20, 좋음 80으로 표시), charging, 그리고 선택으로 batteryVoltage, batteryHealth(0 이상 100 이하), reach(m)가 있습니다. 시뮬레이터는 batteryCharge와 charging만 내보냈고 충전 시작과 종료에 따라 charging이 바뀌는 것을 instantActions 글에서 확인했습니다.
operatingMode는 다섯 값입니다.
| 값 | 의미 |
|---|---|
| AUTOMATIC | 관제가 차량을 완전히 제어. order와 액션을 받아 수행 |
| SEMIAUTOMATIC | 관제가 제어하되 속도는 HMI가 조절(자동 모드의 속도를 넘을 수 없음). 조향은 자동 |
| MANUAL | 관제가 제어하지 않음. HMI로 조향·속도·핸들링 장치를 조작. 이 모드에 들어가거나 나올 때 차량은 모든 order를 즉시 지움(안전한 HMI 필요) |
| SERVICE | 관제가 제어하지 않음. 권한 있는 인원이 차량을 재설정 |
| TEACHIN | 관제가 제어하지 않음. 차량이 지도 작성 같은 교육을 받는 중 |
MANUAL에서 order가 지워진다는 규칙은 어댑터 쪽 reset() 호출과도 이어집니다. libVDA5050++는 운영 모드가 MANUAL로 바뀌었다고 알려 주면 핸들러의 reset()을 부릅니다(라이브러리 퀵스타트 문서의 설명).
errors와 information
errors에는 지금 활성인 오류를 모두 담아야 하고, 빈 배열이면 활성 오류가 없다는 뜻입니다. 항목은 errorType, errorLevel, errorDescription, errorReferences입니다. 수준은 두 가지입니다.
| errorLevel | 의미 |
|---|---|
| WARNING | 스스로 해소되는 오류이거나 차량이 출발할 수 있는 상태(예: 유지보수 주기 경고, 필드 침범) |
| FATAL | 차량이 운행할 수 없는 상태. 사람의 개입이 필요(예: 레이저 스캐너 오염) |
errorReferences는 referenceKey와 referenceValue 쌍으로 원인을 가리킵니다(headerId, orderId, actionId 등). 시뮬레이터에 일부러 잘못된 order를 보내자 이렇게 나왔습니다. n1 이후 sequence를 건너뛴 새 order였고, 앞의 order는 horizon에서 멈춘 채 끝나지 않은 상태였습니다.
"errors": [
{
"errorType": "orderError",
"errorLevel": "WARNING",
"errorDescription": "The order skips sequence ids",
"errorReferences": [
{ "referenceKey": "order.orderId", "referenceValue": "st-2" },
{ "referenceKey": "order.orderUpdateId", "referenceValue": "0" }
]
},
{
"errorType": "orderUpdateError",
"errorLevel": "WARNING",
"errorDescription": "Order ID does not match the current (unfinished) one",
"errorReferences": [
{ "referenceKey": "order.orderId", "referenceValue": "st-2" },
{ "referenceKey": "state.orderId", "referenceValue": "st-1" }
]
}
]
하나의 order가 두 가지 이유로 동시에 거절돼 오류가 둘 쌓였습니다. 두 번째 오류가 말하듯, 앞의 order가 horizon에서 멈춘 채 끝나지 않았기 때문에 다른 orderId의 order는 받아들여지지 않습니다. 이런 차량을 풀려면 같은 orderId로 update를 보내거나 cancelOrder를 먼저 보내야 합니다. 이 오류들은 다음 order가 수락될 때까지 errors에 남았습니다. errorType의 이름은 표준이 validationError, orderError, orderUpdateError, noOrderToCancel을 정해 두었고, 이 글의 시뮬레이터에서 본 unknownActionType, instantActionError는 표준 문서에 없는 라이브러리의 이름이었습니다.
information은 임의의 추가 정보를 관제에 보내는 배열입니다. 항목은 infoType, infoDescription, infoLevel(DEBUG 또는 INFO), infoReferences입니다. 표준은 information을 시각화와 디버깅에만 쓰고 관제의 로직에는 쓰지 말라고 분명히 적었습니다. 얼마나 오래 보고할지는 차량이 정합니다.
safetyState
safetyState는 eStop과 fieldViolation으로 이루어집니다. eStop은 비상정지의 해제 방식을 나타냅니다.
| eStop | 의미 |
|---|---|
| AUTOACK | 범퍼나 보호 필드 등으로 활성화되는 자동 해제 가능 비상정지 |
| MANUAL | 차량에서 수동으로 해제해야 하는 비상정지 |
| REMOTE | 시설 비상정지. 원격에서 해제해야 함 |
| NONE | 비상정지 없음 |
fieldViolation은 보호 필드가 침범됐는지입니다. 시뮬레이터는 항상 NONE과 false였습니다.
관제 쪽에서 조심할 점
아래는 표준의 정의와 위 관찰에서 도출한 것이고, 내 해석이 들어 있습니다.
- 선택 필드는 없을 수 있습니다.
velocity,newBaseRequest,zoneSetId,loads는 시뮬레이터에서 나오지 않거나 비어 있었습니다. 없는 것과 빈 값은 의미가 다를 수 있으므로(특히loads) 필드 존재를 먼저 확인합니다. nodeStates가 비어 있다고 order가 끝난 것은 아닙니다. 표준은 비어 있는 것을 “대기 중(idle)“으로 정의하지만, 액션이 아직 실행 중일 수 있어서actionStates도 함께 봅니다. 시뮬레이터에서 마지막 노드 도착 직후nodeStates는 비었고 drop은 아직 RUNNING이었습니다.- state의 발행 간격에 로직을 걸지 않습니다. QoS 0이라 메시지가 사라질 수 있고, 표준이 정한 상한은 30초입니다. 놓쳤다고 의심되면 instantAction
stateRequest로 즉시 요청할 수 있습니다. - 위치를 state에서 자주 갱신해 표시하려 하지 않습니다. 이동 중 state는 이벤트 때만 오므로
visualization을 쓰거나, 시뮬레이터처럼 최대 주기를 줄이는 설정이 필요합니다. - 오류는
errors로 판단합니다.information은 같은 내용을 담더라도 로직에 쓰지 않습니다. - 같은 순간에 여러 오류가 쌓일 수 있고, 새 order가 수락될 때까지 남을 수 있습니다. 화면에 오래된 오류가 보이는 것은 정상일 수 있습니다.
한계
표준은 2.0.0 문서(릴리스 후보 표기)를 기준으로 했고, 그림으로만 설명된 부분(그림 12, 13)은 옮기지 않았습니다. 이후 버전에서 필드가 추가되거나 바뀌었는지는 확인하지 않았습니다. trajectory(NURBS), nodeDescription, edgeDescription, 적재물의 바운딩 박스는 표준 정의만 읽었고 시뮬레이터에서 실행해 보지 않았습니다. 실행 결과는 로컬 컨테이너의 가상 차량에서 얻은 것이라 실제 장비의 발행 빈도와 지연은 반영하지 않습니다.