VDA5050 order 거절 오류 모음: 어떤 메시지가 어떤 오류를 만드는가

이 글의 핵심

order가 거절되면 차량은 order를 받아들이지 않고 state의 errors에 이유를 남깁니다. 표준은 validationError, orderError, orderUpdateError 세 가지를 정의하는데, 실제 구현은 더 많은 이름을 씁니다. libVDA5050++ v2.11.3 위의 시뮬레이터에 잘못된 order를 하나씩 보내 오류 이름, 설명, errorReferences를 기록하고, 오류 없이 통과하는 경우와 프로세스가 종료되는 경우도 함께 정리합니다.

order를 보냈는데 차량이 움직이지 않을 때 가장 먼저 볼 곳은 state의 errors입니다. 이 글은 일부러 잘못된 order를 하나씩 보내 어떤 오류가 어떤 모양으로 남는지 기록한 것입니다. 표준 문서는 VDA5050 2.0.0(공식 저장소 2.0.0 태그의 VDA5050_EN_V1.md, 머리말에 “RELEASE CANDIDATE, FOR REVIEW”로 표기)이고, 실행 대상은 libVDA5050++ v2.11.3 위에 만든 시뮬레이터입니다. 환경은 Ubuntu 24.04 aarch64 컨테이너와 eclipse-mosquitto:2입니다. 표준이 정한 것과 이 라이브러리가 실제로 한 것은 구분해서 적었습니다.

각 시나리오는 시뮬레이터를 재시작한 직후, 차량이 n0(0,0)에 서 있는 깨끗한 상태에서 order 하나만 보내 측정했습니다. 오류는 다음 order가 수락될 때까지 errors에 남기 때문에, 앞 실험의 오류가 섞이지 않게 하기 위해서입니다. state의 구조는 state 메시지 해설, order의 sequenceId와 update 규칙은 order 수명주기에서 다룹니다.

표준이 정한 거절 규칙

6.6.4절은 order를 거절해야 하는 경우와 그때의 동작을 세 가지로 나눕니다.

상황차량의 동작오류 이름
형식이 잘못된 order내부 버퍼로 받아들이지 않음. 새 order를 받을 때까지 경고를 계속 보고validationError
차량이 수행할 수 없는 액션이나 쓸 수 없는 필드가 있는 order(예: 최대 리프팅 높이 초과, 리프트가 없는데 리프팅 액션)받아들이지 않음. 잘못된 필드를 error reference로orderError
같은 orderId인데 orderUpdateId가 현재보다 낮은 order받아들이지 않고 이전 order를 유지하며 계속 실행orderUpdateError

같은 orderId와 orderUpdateId를 두 번 받으면 두 번째는 무시된다고 표준이 따로 적습니다. 관제가 state 응답이 늦어서 order를 다시 보낸 경우를 이유로 듭니다. 취소할 order가 없을 때의 noOrderToCancel은 instantActions 총정리에서 다뤘습니다.

errors 항목 읽는 법

항목마다 errorType, errorLevel(WARNING 또는 FATAL), errorDescription, errorReferences(referenceKey와 referenceValue 쌍)가 있습니다. 이 글에서 본 거절 오류는 모두 WARNING이었습니다. 표준은 8.1절에서 order 때문에 생긴 오류라면 headerId, 토픽(order 또는 instantAction), orderId와 orderUpdateId, 액션이 원인이면 actionId를 레퍼런스에 담으라고 권합니다. 아래 표의 레퍼런스는 이 라이브러리가 실제로 넣은 키입니다.

실험 결과

헤더와 형식

보낸 ordererrorTypeerrorDescriptionerrorReferences
serialNumber가 다른 값validationErrorHeader.serialNumber is "other999", requires "blog001"header.headerId, header.serialNumber
manufacturer가 다른 값validationErrorHeader.manufacturer is "acme", requires "sim"header.headerId, header.manufacturer
version이 1.1.0validationErrorHeader.version is 1.1.0, requires one of {"2.0.0"}header.headerId, header.version
JSON이 중간에 끊김validationErrorDeserialzation Error: MqttModule could not parse message on topic "uagv/v2/sim/blog001/order" ([json.exception.parse_error.101] ...)없음
nodes 필드 없음validationError같은 Deserialzation Error, 끝에 key 'nodes' not found없음
액션 파라미터 값의 타입이 틀림(loadType에 숫자 5)validationError[json.exception.type_error.302] type must be string, but is number없음

표준이 말한 “형식이 잘못된 order는 validationError”와 맞습니다. 눈여겨볼 점이 두 가지 있습니다. 첫째, 파싱 단계에서 걸린 오류는 errorReferences가 비어 있어서 어떤 order 때문인지 알 수 없습니다. 이 글의 Deserialzation은 철자가 틀린 채로 나오는 라이브러리의 실제 문자열이므로, 오류 이름이나 설명을 문자열로 매칭하는 관제라면 그대로 써야 합니다. 둘째, 헤더 검사는 토픽이 아니라 메시지 본문의 manufacturer와 serialNumber를 봅니다. 토픽은 맞게 구독하고 본문만 다른 값을 담아 보내도 차량이 거절합니다.

sequenceId와 그래프

보낸 ordererrorTypeerrorDescriptionerrorReferences
노드 sequenceId가 홀수(1, 3)orderErrorThe order contains a node with an odd sequence idorderId, orderUpdateId, node.nodeId, node.sequenceId
위와 같은 메시지orderUpdateErrorCould not stitch order due to invalid sequence idsorderId, orderUpdateId, order.node.sequenceId, state.baseSequenceId(<none>)
노드 sequenceId 중복(0, 0)orderErrorThe order contains duplicate sequence idsorderId, orderUpdateId, node.nodeId, node.sequenceId
노드 seq 0 다음이 4(2를 건너뜀)orderErrorThe order skips sequence idsorderId, orderUpdateId

홀수 sequenceId 하나가 오류 두 개를 만들었습니다. 새 order로 받으려다 걸린 orderError와, 이어붙이기로 해석하려다 걸린 orderUpdateError입니다. 같은 order가 여러 오류를 동시에 남길 수 있으므로, errors는 첫 항목만 보지 말고 전체를 읽어야 합니다.

edge의 startNodeId/endNodeId가 노드 목록과 맞지 않는 order(e01의 시작 노드를 존재하지 않는 n9로)는 오류 없이 수락되었고 차량이 n0에서 n1까지 이동했습니다. edge는 startNodeId와 endNodeId로 두 노드를 가리키는데, 이 라이브러리는 그 값이 노드 목록과 맞는지 거르지 않았습니다. 표준은 노드 목록 순서가 통과 순서를 정하고 엣지 수는 노드 수보다 하나 적어야 한다고 적습니다(6.6.1절). 이 엣지 수 규칙을 라이브러리가 검사하는지는 시험하지 않았습니다. 관제가 order를 만드는 쪽에서 직접 검증해야 합니다.

액션

보낸 ordererrorTypeerrorDescriptionerrorReferences
알 수 없는 actionType(doBackflip)unknownActionTypeUnknown action Type doBackflip encounteredaction.actionType, action.actionId
같은 actionId를 두 노드에서 사용orderErrorThe order contains duplicate action ids.order.action.actionId, order.action.actionType
pick의 blockingType을 NONE으로orderErrorBlockingType "NONE" is not allowed, expected one of {HARD}action.blockingType, allowedBlockingTypes
pick의 blockingType에 없는 값(MEDIUM)orderError위와 같은 BlockingType "NONE" is not allowed...위와 같음
pick에서 필수 파라미터 loadType 누락orderErrorMissing action parameters: ["loadType"]확인하지 않음

세 가지를 짚습니다. 첫째, 알 수 없는 액션은 표준이 말한 orderError가 아니라 라이브러리가 붙인 unknownActionType입니다. 표준은 6.6.4.2에서 차량이 수행할 수 없는 액션이 있는 order를 orderError로 정했으므로, 표준의 오류 이름만 기준으로 분기하는 관제는 이 이름을 따로 처리해야 합니다. 둘째, 허용되지 않는 blockingType 값 MEDIUM을 보내면 라이브러리는 이를 NONE으로 읽은 것처럼 보고합니다. 잘못된 값이 조용히 기본값으로 바뀐다는 뜻이라서, 오류 메시지의 값이 보낸 값과 다르더라도 놀라지 않도록 알아 두어야 합니다. 셋째, 액션마다 허용하는 blockingType이 다릅니다. 이 어댑터에서 pick은 HARD만 허용했습니다(액션 선언에서 오는 값입니다).

위치와 시작 노드

보낸 ordererrorTypeerrorDescription
차량이 n0에 있는데 첫 노드를 n2로orderErrorThe AGV is not in the deviation radius of node n2 (last node: )
첫 노드에 nodePosition이 없음orderErrorNode n0 is not the last node of the AGV (no position data available for deviation check)

첫 오류의 errorReferences에는 차량의 현재 위치(state.agvPosition.x/y/theta), order 첫 노드의 위치와 허용 오차(order.nodes[0].nodePosition.*), 마지막 노드가 들어 있어서 어느 값이 어긋났는지 바로 비교할 수 있습니다. 다만 마지막 노드를 가리키는 키가 state.lastNodeId가 아니라 state.laseNodeId로 나왔습니다(철자 오류로 보입니다). 이 키로 값을 읽는 코드가 있다면 실제 문자열을 확인해야 합니다. 두 번째 오류는 위치 정보가 없는 노드에서 시작하는 order를 거절한 경우로, 새 order의 첫 노드는 차량의 마지막 노드이거나 허용 오차 안에 있어야 한다는 표준 규칙(취소 뒤에 새 order를 시작하는 방법을 설명한 6.6.3.1절)과 이어집니다. 표준의 두 방법(차량 위치의 임시 노드, 또는 큰 허용 오차)이 이 규칙을 만족시키는 길입니다.

orderId와 orderUpdateId

보낸 ordererrorTypeerrorDescription
같은 orderId에 낮은 orderUpdateIdorderUpdateError6.6.4.3 - Cannot accept decreasing orderUpdateId
새 order인데 orderUpdateId가 0이 아님(3)orderUpdateErrorCannot accept orderUpdateId > 0 for new order
끝나지 않은 order가 있는데 다른 orderIdorderUpdateErrorOrder ID does not match the current (unfinished) one
같은 orderId와 orderUpdateId 재전송없음오류도 동작도 없이 무시됨

세 번째 줄의 레퍼런스는 새 order의 order.orderId와 지금 진행 중인 state.orderId여서 어느 order가 막고 있는지 알 수 있습니다. 앞선 order가 horizon에서 멈춘 채 끝나지 않았을 때 특히 자주 보게 됩니다. 풀려면 같은 orderId로 update를 보내거나 cancelOrder를 먼저 보냅니다. 표준이 정한 “낮은 orderUpdateId는 orderUpdateError”는 그대로 맞고, 나머지 두 경우는 같은 이름을 이 라이브러리가 넓게 쓴 것입니다.

order update(이어붙이기)

base에서 멈춘 order(n0, n1 확정, n2 예고)에 update를 보낸 결과입니다.

update결과
n1(seq 2)부터 시작해 n2(seq 4)를 확정정상. 차량이 n2까지 이동하고 orderUpdateId가 1이 됨
n1(seq 2)부터 시작하는데 다음 노드를 seq 6으로(4를 건너뜀)orderError: The order skips sequence ids. orderUpdateId는 0 그대로
첫 노드가 n0(seq 0)이고 n1, n2를 이어서 확정오류 없음. orderUpdateId가 1로 올라갔지만 n2는 계속 예고 상태이고 차량은 움직이지 않음

마지막 줄이 가장 조심해야 할 경우입니다. 수락된 것처럼 orderUpdateId가 올라가는데 실제로는 horizon이 풀리지 않았습니다. 이 동작의 원인은 확인하지 못했습니다. 관제가 orderUpdateId가 올라갔다는 것만으로 update가 적용됐다고 판단하면 안 되고, 확정한 노드가 nodeStates에서 released: true로 바뀌었는지, 차량이 움직였는지를 함께 봐야 합니다.

프로세스가 종료되는 경우: 빈 nodes

nodes를 빈 배열([])로 보낸 order(edges도 빈 배열)는 오류를 남기지 않고 차량 프로세스를 종료시켰습니다. 시뮬레이터는 로그에 Handling OrderMessageEvent가 찍힌 직후 종료 코드 139(세그멘테이션 오류)로 끝났고, 라이브러리만 쓰는 최소 예제 어댑터도 같은 order를 받고 종료되었습니다. 세 번 이상 재현되었습니다. 라이브러리가 메시지를 처리하는 경로에서 일어나는 것으로 보이지만 원인 코드를 확인한 것은 아니고, 이후 버전에서 고쳐졌는지도 확인하지 않았습니다. 이 글에 재현 방법을 적는 것은 차량 프로세스를 감시하는 쪽에서 알고 있어야 하는 증상이기 때문입니다.

  • 관제는 order를 만들 때 nodes가 비어 있지 않은지 직접 검증합니다. 표준 6.6.1절은 유효한 order에는 노드가 최소 하나 있어야 한다고 정하므로, 빈 nodes는 표준상 받아들이면 안 되는 order입니다. 차량은 이를 validationError로 거절했어야 하는 경우인데 프로세스가 종료됐습니다.
  • 브로커에서 order 토픽에 게시할 수 있는 클라이언트를 제한합니다(ACL). 이 크래시는 그 토픽에 게시할 수 있는 누구든 일으킬 수 있습니다.
  • 차량 프로세스에는 감시와 자동 재시작을 둡니다. 재시작하면 connection 토픽의 상태가 CONNECTIONBROKEN에서 ONLINE으로 바뀌는 것을 관제가 볼 수 있습니다.

표준과 구현의 차이 요약

항목표준 2.0.0이 라이브러리(v2.11.3)
형식 오류validationError일치. 헤더 값 불일치도 같은 이름
수행 불가 액션orderError알 수 없는 액션은 unknownActionType, 파라미터·blockingType 오류는 orderError
낮은 orderUpdateIdorderUpdateError일치. 같은 이름을 다른 경우에도 사용
같은 id 재전송두 번째는 무시일치(오류 없이 무시)
edge 끝점 불일치규정 없음(전제상 일치해야 함)거르지 않고 수락
빈 nodes유효한 order에는 노드가 최소 하나 필요오류 없이 프로세스 종료

관제 구현에서의 정리

아래는 위 관찰에서 도출한 것이고, 내 해석이 들어 있습니다.

  • 거절 판단은 errorType 하나로 하지 않습니다. 표준의 세 이름 외에 구현이 붙인 이름이 있고, 같은 order가 여러 오류를 남길 수 있습니다. errorLevel, errorDescription, errorReferences의 order.orderId를 함께 봅니다.
  • order를 보낸 뒤에는 state.orderId, orderUpdateId, nodeStates, lastNodeId로 수락 여부를 확인합니다. 오류가 없다는 것이 수락됐다는 뜻은 아닙니다.
  • 오류는 새 order가 수락될 때까지 남으므로, 새 order를 보내기 전의 errors와 보낸 뒤의 errors를 비교해 새로 생긴 오류만 해당 order의 것으로 취급합니다.
  • orderId는 보낼 때마다 새로 만듭니다. 같은 값을 재사용하면 오류 없이 무시되어 원인을 찾기 어렵습니다.
  • order를 보내기 전에 관제 쪽에서 nodes가 비어 있지 않은지, sequenceId가 짝수 노드와 홀수 엣지로 이어지는지, edge 끝점이 노드와 일치하는지를 확인합니다. 차량이 걸러 주지 않는 항목이 있기 때문입니다.
  • errorDescription의 문자열은 구현마다 다르고 철자 오류를 포함할 수 있으니, 사람이 읽는 용도로 쓰고 분기는 errorType과 레퍼런스 키로 합니다.

한계

표준은 2.0.0 문서(릴리스 후보 표기)를 기준으로 했습니다. 실행은 libVDA5050++ v2.11.3 위의 시뮬레이터와 최소 예제 어댑터에서만 했고, 다른 구현이나 다른 버전의 오류 이름과 문구는 다를 수 있습니다. 노드 도착 판정의 허용 오차 설정, zone set 검증, 트래젝토리(NURBS) 관련 오류는 다루지 않았습니다. 빈 nodes 크래시와 첫 노드가 맞지 않는 update의 동작은 원인을 확인하지 못했습니다. 모든 거절 오류는 WARNING 수준으로만 관찰했고 FATAL이 나오는 경우는 시험하지 않았습니다.