제로웍스 연동 개발 시나리오

제로웍스 로봇이 비스캣 관제와 결합해 한 사이클을 도는 동작 흐름입니다. 토픽·페이로드 상세는 연동 API 문서를, 메인은 개발 가이드를 참조하세요.

2026-09-03 개정 요약

1. 등록·인증 (최초 1회)

비스캣 관제에 robotCode(예: R-001)가 사전 등록된 뒤, 로봇은 robotCode와 공개키를 제출합니다. 관제는 robotCode로 로봇을 조회해 내부 식별자 robotId(UUID)를 회신하고 HMAC 비밀키를 발급하며, 로봇은 X.509 인증서 번들(다운로드 URL)을 내려받습니다. 이후 로봇은 인증·MQTT에 robotId를 사용합니다. 등록·인증 요청은 REST로 진행하고, 인증서를 설치한 뒤 MQTT에 연결합니다. 이후 미션·상태·알람 메시지는 모두 MQTT로 주고받습니다.

sequenceDiagram participant R as 로봇(제로웍스) participant C as 비스캣 관제 Note over R,C: robotCode는 관제에 사전 등록된 상태 R->>C: POST /v1/robot/key (robotCode, publicKey) C-->>R: robotId(UUID) + 공개키로 암호화된 HMAC 비밀키 R->>C: POST /v1/robot/cert (HMAC 서명) C-->>R: 인증서 번들 다운로드 URL 목록 Note over R: robotId·비밀키·인증서 보관 (TPM 권장)

2. 맵 등록 (REST · 로봇 UI 버튼)

로봇이 보유한 지도와 의미 거점 노드(매장·하역장·충전소·대기 등 goal.type 거점)를 siteId 기준으로 관제에 등록합니다. 운영자가 로봇 UI의 업로드 버튼을 누르면 로봇이 보유 맵을 자동 업로드합니다. 등록 개시 시 siteId·제시 mapId와 업로드할 파일 목록을 선언하면 관제가 파일별 업로드 URL을 발급하고, 로봇은 지도 이미지·메타(yml)·노드(json)를 각각 해당 URL로 올립니다(압축하지 않음). 흐름은 등록 개시 → 파일 업로드 → 등록 확정 3단계입니다.

sequenceDiagram participant R as 로봇(제로웍스) participant C as 비스캣 관제 participant U as 업로드 URL Note over R: 운영자가 로봇 UI 업로드 버튼 클릭 R->>C: ① POST /v1/robot/maps (siteId, mapId, files[]) Note over C: mapVersion 발급, 파일별 업로드 URL 생성 C-->>R: mapVersion, uploads[](파일별 URL), uploadExpiresIn R->>U: ② 각 파일(이미지·yml·json) 개별 업로드 U-->>R: 200 OK R->>C: ③ POST /v1/robot/maps/{mapId}/commit Note over C: 선언 파일 전부 업로드 확인 + 검증
→ 활성화(이전 버전 비활성)
누락 시 오류, 비선언 파일 미반영·삭제 C-->>R: isActive=true, activatedAt
  1. 등록 개시: 로봇이 siteId·제시 mapId와 업로드할 파일 목록을 관제에 전송. 관제가 mapVersion과 파일별 업로드 URL을 발급.
  2. 파일 업로드: 로봇이 지도 이미지·메타(yml)·노드(json)를 각 업로드 URL로 개별 전송(압축하지 않음). uploadExpiresIn 내 완료.
  3. 등록 확정: 로봇이 commit 호출 → 관제가 선언 파일이 모두 업로드됐는지 확인하고 검증해 통과 시 활성화(이전 버전 비활성). 누락 시 오류, 선언에 없는 파일은 미반영·삭제. 이후 MQTT location.map_id가 활성 mapId를 가리키고, 관제는 등록된 노드 좌표로 goal을 내립니다.

맵 버전·노드

재매핑 시 (siteId, mapId)당 mapVersion이 증가하며 이력을 보관합니다(롤백·감사 가능). 노드(json)에서 Place 객체를 가진 노드가 목적지로 등록되며, 관제는 그 좌표로 goal 의 location을 구성합니다.

노드 idx는 필수이며, 관제의 목적지 키(place_id)가 됩니다. idx가 없으면 배열 순번으로 대체되는데, 다음 등록에서 노드가 추가·삭제되면 키가 통째로 밀려 기존 주소 매핑이 다른 세대를 가리킵니다. 포맷 상세는 API 문서 §7.

3. MQTT 입찰·배차 (offer → bid → result)

미션이 생기면 관제가 수행 가능한 후보 로봇에게 미션(goal 배열)을 제시합니다. 흐름은 입찰 요청 → 입찰 응답 → 배차 결과 3단계입니다. 각 로봇은 예상 도착시간을 입찰하고, 어느 로봇이 수행할지의 최종 배차 결정은 관제가 단독으로 내려(최소 도착시간 기준, 추후 적재공간 포함) result로 통보합니다.

sequenceDiagram participant C as 관제 participant A as 로봇 A participant B as 로봇 B Note over C: 미션 생성 → goals 구성
후보 = 단지 내 online 로봇 전원 par 입찰 요청 (offer · goal 전체 정의) C-->>A: goals[] (id·PIN·door·orderId 포함) and C-->>B: goals[] end Note over A,B: 각 로봇이 goal별 도착 예상 시각 자체 계산 A-->>C: 입찰 (bid) goals[]{id, available, estArrival="…T10:12:00+09:00", reason} B-->>C: 입찰 (bid) goals[]{id, available, estArrival="…T10:08:00+09:00", reason} Note over C: 관제가 남은 시간(estArrival−occurredAt) 최소로 배차 결정
(무응답·타임아웃 정책 적용) par 입찰 결과 (result) C-->>A: claimed=false, reason C-->>B: claimed=true, reason=null end Note over B: 낙찰(claimed=true) 받은 B만 수행 시작
  1. 입찰 요청(offer): 관제가 후보 로봇에게 goal 전체 정의를 제시합니다(API 문서 §5.1 본문). 낙찰 로봇은 이 본문을 그대로 수행하므로, offer 는 사실상 조건부 goal 부여입니다. 식별은 관제가 발급한 goal id 단위입니다.
  2. 입찰 응답(bid): 각 로봇이 goal별 수행 가능 여부(available)와 도착 예상 시각(estArrival — occurredAt과 동일 포맷의 절대 시각, API 문서 §5.2 규약)을 자체 계산해 회신. 본문은 견적 응답(quote/bid)과 동일 스키마입니다.
  3. 배차 결과(result): 관제가 입찰을 받아 남은 시간(마지막 goal 도착 예상 시각 기준) 최소로 배차를 결정하고(적재 칸을 배정할 수 없는 로봇은 채택 단계에서 제외) goal별 claimed·reason으로 통보. claimed=true 통보가 곧 주행 개시 신호이며, 관제는 별도의 출발 지시를 보내지 않습니다.

단발 라운드 · 배차 불가 처리

일부 로봇이 무응답이어도 bid 타임아웃(초기값 3초, 견적과 공용) 시점에 관제가 모인 입찰로 배차를 결정하고, 마감 후 도착한 지각 bid는 배제합니다. 라운드는 단발이며 재오퍼하지 않습니다 — 입찰 가능한 로봇이 없으면 해당 미션은 수행 불가로 종료됩니다. mission.atomic이 true면 해당 미션 goal들의 순서가 보존되어(거리 최적화 재정렬 시에도 묶음 유지) 다른 미션 goal이 사이에 끼지 못합니다.

offer 대상은 단지 내 online: true 로봇 전원입니다(모드·적재 여유로 미리 걸러내지 않습니다 — 적재 만재 판정은 관제가 채택 단계에서 합니다). result는 bid 를 보낸 로봇에게만 발송되므로, offer 를 받고 응답하지 않았다면 해당 goal 은 스스로 폐기해야 합니다.

3.1 배송 가능여부 견적 (quote) 운영 적용됨

주문이 확정되기 전, 관제가 후보 로봇에게 "지금 이 배송을 수행할 수 있는지"를 미리 물어 가용성과 예상 도착시간을 받아 두는 사전 견적 흐름입니다. 미션(goal)을 만들지 않는 비구속 질의라 배차 입찰(goals/*)과 토픽을 분리해 quote/*로 운영하며, 낙찰(result) 없이 질의 → 견적 2단계로 끝납니다.

sequenceDiagram participant C as 관제 participant A as 로봇 A participant B as 로봇 B Note over C: 배송 가능여부 질의 (주문 전 사전 견적) par 견적 질의 (quote/offer · 축약 본문) C-->>A: quoteId · deliveryType · goals[]{type, location, boxCount} and C-->>B: 동일 본문 end Note over A,B: 각 로봇이 현재 상태로 가용성·ETA 산정 A-->>C: quote/bid goals[]{available=true, estArrival="…T10:09:00+09:00"} B-->>C: quote/bid goals[]{available=true, estArrival="…T10:06:00+09:00"} Note over C: available=true 중 남은 시간 최소 채택
(무응답·타임아웃 = available false)
  1. 견적 질의(offer): 관제가 후보 로봇에게 축약 본문을 제시(s2r/{robot_id}/quote/offer) — quoteId·deliveryType과 목적지 요약(type·location·boxCount)뿐입니다. 아직 주문이 확정되지 않아 goal id·PIN·주문번호가 없습니다. 라운드 상관은 토픽의 robot_id로 합니다.
  2. 견적 응답(bid): 각 로봇이 goal별 수행 가능 여부(available)와 도착 예상 시각(estArrival, 불가 시 null·사유)을 회신(r2s/{robot_id}/quote/bid). 배차 입찰 응답(goals/bid)과 완전히 동일한 스키마이며, id는 빈 문자열로 둡니다.

입찰과의 차이 · 라운드 마감

배차 입찰과 달리 미션을 생성하지 않고 낙찰(result) 단계가 없습니다(2단계). 결과는 배차 확정이 아니라 가용성 집계로, 관제가 available=true 로봇 중 남은 시간(estArrival 환산) 최소를 채택합니다. 단발 라운드(재질의 없음)이며 bid 타임아웃(배차와 동일, 초기값 3초) 내 무응답은 available=false로 간주합니다. 다른 것은 질의 본문뿐이고 응답 스키마는 배차 입찰과 같습니다. 토픽·페이로드 상세는 API 문서 §5.4. 타임아웃 값 협의

적재함 만재는 관제가 판정합니다 — 로봇은 칸 점유를 알 수 없으므로 주행 가능 여부만 답하면 되고, boxCount만큼 칸을 배정할 수 없는 로봇은 관제가 채택 단계에서 제외합니다.

4. 배송 미션 전체 시나리오

배차 이후의 한 사이클입니다(mode: delivery). 한 오더(mission.id)는 매장 goal(type:store, mission.type:pickup)과 고객 goal(type:customer, mission.type:dropoff)로 구성됩니다. 로봇은 goal 배열을 arrivalOrder 순으로 수행하며, 진행 상태를 r2s/{robot_id}/goals로 발행합니다. 상태값은 세 계층 모두 standby·doing·done 한 벌을 씁니다(API 문서 §1). PIN은 mission.PIN으로 선전달되어 로봇이 로컬 검증하고, 사용 결과를 관제에 감사 이벤트로 보고합니다. 열 칸은 mission.door로 관제가 지정합니다.

sequenceDiagram participant R as 로봇 participant C as 관제 participant O as 오토메타(앱 표출) Note over R: 배차 완료 (delivery) · mission.PIN 보유 R->>C: 매장(store) 이동 → goal status=standby→doing R->>C: goals(auth status=doing) — 매장 도착 C->>O: 매장 도착 통지 Note over R: 점주 PIN 입력 → 로봇 로컬 검증
mission.door 칸 개방 R->>C: goals(pickup status=doing) — 박스 열림 Note over R: 점주 상차 → 박스 닫힘 R->>C: goals(pickup status=done) — 상차 완료 / 출발 트리거 C->>O: 배달 출발 알림 R->>C: 세대(customer) 이동 → goal status=doing R->>C: goals(auth status=doing) — 세대 도착 C->>O: 도착 통지 Note over R: 고객 수령 (방식별) R->>C: goals(dropoff status=doing) — 박스 열림 Note over R: 하차 완료 → 박스 닫힘 R->>C: goals(dropoff status=done · goal status=done) · state.status=done C->>O: 하차 완료 통지 Note over R,C: 완료 후 — 연속 배차=mode delivery 유지 / 충전 복귀=mode navi·goal.type charging
  1. 매장 이동: 배차 후 매장(goal.type:store)으로 주행 — goal status가 standby → doing.
  2. 매장 도착: 인증 대기 단계가 시작되면(mission.type:auth · status:doing) 관제가 오토메타에 매장 도착을 통지.
  3. 점주 PIN 검증(로컬): 점주가 PIN 입력 → 로봇이 mission.PIN으로 로컬 검증 → mission.door로 지정된 칸 개방. 개방 시점에 pickup mission 이 doing으로 전이.
  4. 상차·박스 닫힘: 점주 상차 후 박스를 닫으면 pickup mission 이 done으로 전이. 이 전이가 배달 출발 트리거이며, 관제가 오토메타에 배달 출발을 통지. (종전 box/loaded 토픽은 폐지 — API 문서 §6.1)
  5. 세대 이동·도착: 세대(goal.type:customer)로 주행(goal doing), 도착은 매장과 같이 auth:doing으로 판정. 관제가 도착 통지.
  6. 고객 수령: 방식은 mission의 autoDropOff·PIN으로 결정 — 비대면(autoDropOff=true)은 도착 즉시 하차 후 복귀, WEB 버튼 인증(기본)은 관제 → 로봇 문열림 지시로 개방, 로봇 PIN 입력(예외 fallback)은 로컬 검증 후 개방. 개방 시점에 dropoff mission 이 doing.
  7. 완료·복귀: 하차 후 박스를 닫으면 dropoff mission 이 done, 해당 goal 도 done이 됩니다. 미션의 모든 goal·mission 이 done이면 관제가 미션을 종료하고 state.status=done. 이후 처리는 아래 갈래.

도착 판정은 auth:doing, 완료 판정은 전체 done

관제가 의미를 부여하는 미션 단계는 셋뿐입니다 — 도착(auth:doing) · 박스 열림(pickup/dropoff:doing) · 박스 닫힘·지점 완료(pickup/dropoff:done). 그 밖의 단계(move·gate_*·elevatorCall·층별 상태 등)는 done으로 보고되면 이름 그대로 운행 타임라인에 기록됩니다. 어휘 목록은 API 문서 §5 참조.

미션 완료 후 — 연속 배차 · 충전 복귀 · 자동 회차

전 mission이 done이 되어 state.status=done이 되면 다음 처리는 세 갈래이며, 충전 복귀만 mode가 바뀝니다.

즉 복귀=충전은 delivery가 아니라 navi 모드입니다(goal.type=charging). 다음 일감을 바로 받는 경우에만 delivery를 유지합니다. timeout 값은 협의로 확정합니다. timeout 값 협의

열 칸은 관제가 결정

적재함의 어느 칸을 쓸지는 관제가 주문 수량 기반으로 결정해 mission.door(door1~doorN)로 지정합니다. 칸 수 N은 로봇마다 다르며 관제에 로봇별로 등록되어 있습니다(하드웨어 상한 4). 점주가 임의로 칸을 고를 수 없으며, 로봇은 PIN 로컬 검증 후 지정된 도어만 개방합니다. 주문 수량이 적재 용량을 초과하면 배차 단계에서 거부됩니다.

고객 수령 3방식

비대면(autoDropOff:true) / WEB 버튼 인증(기본, PIN:null·autoDropOff:false) / 로봇 PIN 입력(예외 fallback, PIN:랜덤)으로 goal.mission에 전달됩니다. 비대면은 autoDropOff:true인 경우만 해당하므로 (autoDropOff, PIN) 조합으로 3방식이 모두 구분됩니다. 상세는 API 문서 §6.

5. 쓰레기 수거 시나리오

mode: trash_pickup. 흐름은 배송과 동일한 MQTT 입찰·배차·주행 구조(goal 배열)를 따르되, 매장↔세대 대신 세대(goal.type:customer, mission.type:pickup) → 수거장(goal.type:unloading, mission.type:dropoff) 경로입니다.

6. 단지 인프라(E/V·로비폰) 처리

로봇은 엘리베이터·로비폰 업체 API를 직접 호출하지 않습니다 — 관제에 요청합니다

로봇은 필요한 시점에 r2s/{robot_id}/resource로 관제에 요청하고, 관제가 현대엘리베이터(MIRI)·로비폰(inBASE) 연동을 대신 수행한 뒤 s2r/{robot_id}/resource/ready로 진행 신호를 돌려줍니다. 로봇은 업체 API·IP·토큰을 몰라도 됩니다.

요청 순서는 로비폰(건물 진입) → 엘베(상층 이동)입니다. 탑승 호기는 로봇이 고르지 않고 엘베 그룹제어가 배정하므로, 로봇은 elId가 실려 온 뒤 그 호기 앞에서 대기합니다. 페이로드 상세는 API 문서 §5.5.

탑승·하차 완료 보고는 필수입니다 — 누락 시 콜이 취소됩니다

1.5차 PoC는 "로봇 비탑승(사람 동승)" 전제로 운영하지만, 비탑승 전제여도 완료 보고는 보내야 합니다. 엘리베이터는 이 보고를 받아야 도어를 닫고 다음 단계로 넘어가며, 오지 않으면 재확인 3회 뒤 콜을 취소합니다(취소된 콜은 되살릴 수 없어 처음부터 다시 호출해야 합니다).

보고 값은 MIRI serviceStatus 어휘를 그대로 씁니다 — 탑승 완료 sourceFloorGotOn, 하차 완료 destinationFloorGotOff. 이 두 전이는 MIRI 가 push 하지 않아 로봇 보고가 유일한 소식통입니다. 상세는 API 문서 §5.5.

실패 시나리오(호출 실패·문 안 열림·층 오인식) 대응은 별도 협의합니다. 참고로 sourceFloor·destinationFloor를 빈 문자열로 보내면 콜이 계속 실패하며, 로봇이 층을 실을 때까지 재시도가 멈추지 않습니다(2026-08 실사례).

7. 알람·에러·재시도

8. 배송 통신 플로우 예시 (메시지 레벨)

배송 두 케이스의 토픽·메시지 흐름을 단계별로 보입니다 — ① 단건 배송과 ② 주행 중 추가 주문(묶음 배송). 토픽 규약(s2r/r2s)·메시지 envelope·필드 정의는 API 문서 §5를 따릅니다. 등장 주체는 관제·로봇 Robot1이며, 주문 인입·앱 표출(오토메타)은 맥락 노트로만 표시합니다.

mode는 로봇이 어느 서비스로 활용되는지를 나타내는 상태값입니다. 배송 로봇은 배차 전 대기·이동, 그리고 다음 배송으로의 연속 배차까지 mode: delivery를 유지하며, 일감 유무·활동 진행은 state.status·goal.status로 표현합니다(mode로 나타내지 않음). 단 미션 완료 후 충전소로 복귀할 때는 delivery가 아니라 mode: navi + goal.type=charging으로 전이합니다(관제 지시 또는 로봇 timeout 자동 회차). 완료 후 처리 갈래는 §4 참조.

예시 값 주의

아래 JSON의 requestId·좌표·시각·PIN·estArrival 등은 형식 예시입니다. 상태/진행 발행은 변동 필드만 싣는 이벤트 모델이라 단계별로 필요한 필드만 노출했습니다. 적재 칸은 주문 단위로 관제가 배정하며(mission.door), 본 예시는 주문 Order1 → door1, 주문 Order2 → door2를 쓰고 같은 주문의 픽업·하차는 동일 칸을 씁니다.

8.1 단건 배송 (Order1)

고객 Order1 주문 → 입찰 → Robot1 확정 → 매장 도착·상차 → 배송 → Order1 도착·배송 완료. 한 주문(mission_Order1)이 매장 goal(goal_s1, pickup)과 고객 goal(goal_c1, dropoff) 두 goal에 같은 mission.id로 걸칩니다.

sequenceDiagram autonumber participant O as 오토메타 participant C as 관제 participant Robot1 as 로봇 Robot1 O->>C: 고객 Order1 주문 인입 Note over C: 미션 생성(mission_Order1)
goal_s1(store/pickup) + goal_c1(customer/dropoff) C-->>Robot1: s2r/Robot1/goals/offer (goals[]) Robot1-->>C: r2s/Robot1/goals/bid (estArrival) Note over C: 최소 도착시간으로 배차 결정 C-->>Robot1: s2r/Robot1/goals/result (claimed=true) Robot1->>C: r2s/Robot1/state (mode=delivery) Robot1->>C: r2s/Robot1/goals (goal_s1 doing) — 매장 이동 Robot1->>C: r2s/Robot1/goals (auth doing) — 매장 도착 Note over Robot1: 점주 PIN 로컬 검증 → door1 개방 Robot1->>C: r2s/Robot1/goals (pickup doing) — 박스 열림 Robot1->>C: r2s/Robot1/goals (pickup done · goal_s1 done) — 상차 완료 C->>O: 배달 출발 통지 Robot1->>C: r2s/Robot1/goals (goal_c1 doing) — 세대 이동 Robot1->>C: r2s/Robot1/goals (auth doing) — 세대 도착 Note over Robot1: 고객 수령 (WEB 버튼/PIN) Robot1->>C: r2s/Robot1/goals (dropoff done · goal_c1 done) — 배송 완료 C->>O: 배송 완료 통지 Robot1->>C: r2s/Robot1/state (status=done) — 전 미션 완료 Note over C,Robot1: 완료 후 — 연속 배차=mode delivery 유지 / 충전 복귀=mode navi·goal.type charging

① 입찰 요청 (offer) — s2r/Robot1/goals/offer · 관제 → Robot1

관제가 goal 전체(location 포함)를 제시합니다. location 좌표는 로봇이 사전 등록한 맵 노드 기준으로 관제가 채웁니다(§2 맵 등록). 낙찰되면 이 본문을 그대로 수행하므로 goal 재전송은 없습니다.

아래 예시의 goal_s1·mission_Order1은 읽기 쉬운 가짜 값입니다. 실제 발급 형식은 g-{미션UUID}-{goal.type}·m-{미션UUID}이며, 로봇은 받은 id 를 그대로 되돌려 보내야 합니다(API 문서 §5.1).

{
  "requestId": "req-offer-Order1",
  "occurredAt": "2026-06-17T10:00:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "goal_s1", "type": "store", "status": "standby", "arrivalOrder": 1,
        "location": { "x": 12.4, "y": 8.1, "th": 0.0, "map_id": "apt1023-b1" },
        "mission": [
          { "id": "mission_Order1", "type": "pickup", "status": "standby", "atomic": false,
            "PIN": null, "door": [ { "name": "door1", "status": "closed" } ], "orderId": "Order1",
            "address": "", "addressDetail": "", "phoneNumber": "", "message": "" }
        ]
      },
      {
        "id": "goal_c1", "type": "customer", "status": "standby", "arrivalOrder": 2,
        "location": { "x": 40.2, "y": 22.7, "th": 1.57, "map_id": "apt1023-3f" },
        "mission": [
          { "id": "mission_Order1", "type": "dropoff", "status": "standby", "atomic": false,
            "PIN": "1482", "door": [ { "name": "door1", "status": "closed" } ], "orderId": "Order1",
            "address": "103동", "addressDetail": "302호", "phoneNumber": "010-0000-1234", "message": "문 앞" }
        ]
      }
    ]
  }
}

② 입찰 응답 (bid) — r2s/Robot1/goals/bid · Robot1 → 관제

Robot1이 goal별 수행 가능 여부(available)와 도착 예상 시각(estArrival — occurredAt과 동일 포맷의 절대 시각)을 자체 계산해 회신. 식별은 관제가 발급한 goal id이며 그대로 되돌려 보냅니다. 본문 형식은 견적 응답(quote/bid)과 동일합니다.

{
  "requestId": "req-bid-Order1",
  "occurredAt": "2026-06-17T10:00:03+09:00",
  "payload": {
    "goals": [
      { "id": "goal_s1", "arrivalOrder": 1, "available": true, "estArrival": "2026-06-17T10:04:00+09:00", "reason": null },
      { "id": "goal_c1", "arrivalOrder": 2, "available": true, "estArrival": "2026-06-17T10:11:00+09:00", "reason": null }
    ]
  }
}

③ 배차 결과 (result) — s2r/Robot1/goals/result · 관제 → Robot1

{
  "requestId": "req-result-Order1",
  "occurredAt": "2026-06-17T10:00:05+09:00",
  "payload": {
    "goals": [
      { "id": "goal_s1", "claimed": true, "reason": null },
      { "id": "goal_c1", "claimed": true, "reason": null }
    ]
  }
}

④ 수행 — 모드·goal 진행 발행 (r2s/Robot1/state, r2s/Robot1/goals) · Robot1 → 관제

배송 로봇이므로 mode는 delivery로 계속 유지되고, 일감을 잡으면 state.status가 standby → doing으로 전이합니다. 매장 goal 은 standby → doing으로 발행되고, 도착 시 인증 대기 단계(auth)가 doing이 됩니다. (텔레메트리는 requestId 생략)

// r2s/Robot1/state — 모드 전이
{ "occurredAt": "2026-06-17T10:00:06+09:00",
  "payload": { "mode": "delivery", "status": "doing",
    "location": { "x": 12.5, "y": 8.0, "th": 0.0, "map_id": "apt1023-b1" } } }
// r2s/Robot1/goals — 매장 도착(auth doing) → 관제가 매장 도착으로 판정
{ "occurredAt": "2026-06-17T10:04:00+09:00",
  "payload": { "goals": [
    { "id": "goal_s1", "status": "doing", "progress": 100,
      "mission": [
        { "id": "…move…", "type": "move",   "status": "done" },
        { "id": "…auth…", "type": "auth",   "status": "doing" },
        { "id": "mission_Order1", "type": "pickup", "status": "standby" }
      ] }
  ] } }

⑤ 상차·박스 닫힘 → 픽업 완료 (r2s/Robot1/goals) · Robot1 → 관제

점주가 PIN을 입력하면 Robot1이 mission.PIN으로 로컬 검증 후 지정 칸(door1)을 개방하고, pickup mission 을 doing으로 발행합니다 — 관제는 이 전이를 박스 열림으로 처리합니다. 상차 후 박스를 닫으면 같은 mission 이 done이 되고, 이 전이가 배달 출발 트리거입니다. 매장 goal 도 함께 done이 됩니다.

// r2s/Robot1/goals — 박스 닫힘 → 픽업 완료 · 매장 goal 완료
{ "occurredAt": "2026-06-17T10:05:11+09:00",
  "payload": { "goals": [
    { "id": "goal_s1", "status": "done",
      "mission": [ { "id": "mission_Order1", "type": "pickup", "status": "done" } ] }
  ] } }

PIN 사용·검증 결과의 감사 이벤트(r2s) 토픽·페이로드는 여전히 협의 대상입니다(API 문서 §6).

⑥ 배송·완료 (r2s/Robot1/goals) · Robot1 → 관제

세대 goal이 standby → doing, 도착 시 auth:doing, 고객 수령 후 dropoff mission이 done이 되고 goal 도 done이 됩니다. 고객 수령은 기본 WEB 버튼 인증(관제 → 로봇 문열림 지시), 예외 시 로봇 PIN 입력입니다(§4 참조). 미션의 모든 goal·mission 이 done이면 관제가 오토메타에 배송 완료를 통지합니다. 이후 연속 배차가 있으면 mode=delivery를 유지한 채 새 goal을 받고, 없으면 충전소로 복귀합니다(mode=navi·goal.type=charging — §4).

// r2s/Robot1/goals — 하차 완료 → 미션 종료
{ "occurredAt": "2026-06-17T10:10:40+09:00",
  "payload": { "goals": [
    { "id": "goal_s1", "status": "done",
      "mission": [ { "id": "mission_Order1", "type": "pickup",  "status": "done" } ] },
    { "id": "goal_c1", "status": "done",
      "mission": [ { "id": "mission_Order1", "type": "dropoff", "status": "done" } ] }
  ] } }

완료 판정은 그 미션에 속한 goal 이 배열에 모두 실려 전부 done일 때입니다. 여러 미션을 동시에 수행 중이면 한 배열에 함께 실어 보내며, 관제는 goal id 접두로 미션을 갈라 각각 판정합니다.

8.2 주행 중 추가 주문 — 묶음 배송 (Order1 → Order2)

Order1 배차 후 Robot1이 매장1로 이동하는 중 고객 Order2 주문이 들어와 같은 Robot1에 추가됩니다. 낙찰 후 Robot1이 Order1·Order2 goal을 직접 통합·정렬해 매장1 상차 → 매장2 상차 → Order1 배송 → Order2 배송 순서로 수행합니다(관제의 plan 재주입 없음). Order2는 다른 매장(goal_s2)·다른 세대(goal_c2), 적재 칸은 door2를 씁니다.

sequenceDiagram autonumber participant O as 오토메타 participant C as 관제 participant Robot1 as 로봇 Robot1 O->>C: 고객 Order1 주문 C-->>Robot1: offer (mission_Order1 · goal_s1, goal_c1) Robot1-->>C: bid C-->>Robot1: result claimed=true Robot1->>C: r2s/Robot1/goals (goal_s1 doing) — 매장1 이동 중 O->>C: 고객 Order2 주문 (Robot1 주행 중) Note over C: 후보 산정 → Robot1 포함 C-->>Robot1: offer (mission_Order2 · goal_s2, goal_c2) Robot1-->>C: bid (현재 부하 반영) C-->>Robot1: result claimed=true Note over Robot1: Robot1이 Order1+Order2 goal 통합·정렬
(atomic·주문 내 순서 보존)
수행 순서: s1 → s2 → c1 → c2 Robot1->>C: r2s/Robot1/goals (goal_s1 done · pickup_Order1 done) — 매장1 상차 Robot1->>C: r2s/Robot1/goals (goal_s2 doing→done · pickup_Order2 done) — 매장2 상차 Robot1->>C: r2s/Robot1/goals (goal_c1 doing→done · dropoff_Order1 done) C->>O: Order1 배송 완료 Robot1->>C: r2s/Robot1/goals (goal_c2 doing→done · dropoff_Order2 done) C->>O: Order2 배송 완료 Robot1->>C: r2s/Robot1/state (status=done) — 전 미션 완료 Note over C,Robot1: 완료 후 — 연속 배차=mode delivery 유지 / 충전 복귀=mode navi·goal.type charging

① Order1 배차·매장1 이동 — 8.1의 ①~④와 동일

Order1 주문 → offer → bid → result(claimed=true) → Robot1이 goal_s1로 이동(doing). 형식은 8.1과 같아 생략합니다.

② Order2 입찰 요청 (offer) — s2r/Robot1/goals/offer · 관제 → Robot1

Robot1 주행 중 Order2 주문이 인입되면, 관제가 Order2의 두 goal만 제시합니다(Order1 goal은 Robot1이 이미 보유). arrivalOrder는 단건 주문과 동일하게 주문 내 순서(픽업 1·하차 2)로 보내며, 병합 시 관제가 전체 plan 기준으로 재배정합니다(④).

{
  "requestId": "req-offer-Order2",
  "occurredAt": "2026-06-17T10:01:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "goal_s2", "type": "store", "status": "standby", "arrivalOrder": 1,
        "location": { "x": 18.9, "y": 6.4, "th": 0.0, "map_id": "apt1023-b1" },
        "mission": [
          { "id": "mission_Order2", "type": "pickup", "status": "standby", "atomic": false,
            "PIN": null, "door": [ { "name": "door2", "status": "closed" } ], "orderId": "Order2",
            "address": "", "addressDetail": "", "phoneNumber": "", "message": "" }
        ]
      },
      {
        "id": "goal_c2", "type": "customer", "status": "standby", "arrivalOrder": 2,
        "location": { "x": 44.0, "y": 30.1, "th": 3.14, "map_id": "apt1023-5f" },
        "mission": [
          { "id": "mission_Order2", "type": "dropoff", "status": "standby", "atomic": false,
            "PIN": "7035", "door": [ { "name": "door2", "status": "closed" } ], "orderId": "Order2",
            "address": "105동", "addressDetail": "501호", "phoneNumber": "010-0000-5678", "message": "" }
        ]
      }
    ]
  }
}

③ Order2 입찰 응답·배차 결과 (bid, result)

Robot1이 현재 부하(Order1 수행 중)를 반영해 입찰하고, 관제가 낙찰을 통보합니다(형식은 8.1 ②③과 동일, goalId는 goal_s2·goal_c2).

④ Robot1 통합·정렬 (로봇 자체 재계획)

낙찰(③) 후 Robot1은 이미 보유한 Order1 goal과 새로 받은 Order2 goal을 자체적으로 통합·정렬해 수행 순서를 정합니다. goal 정의는 offer 본문으로 이미 전달됐으므로, 관제는 별도 통합 plan을 주입하지 않습니다(goals/set 미사용) — 경로 최적화·재정렬은 로봇이 담당합니다. 각 주문의 내부 순서(픽업 → 하차)와 atomic 제약은 보존되며, Order1·Order2 모두 atomic: false라 픽업끼리·하차끼리 묶는 순서(A,C,B,D)가 허용되어 Robot1은 아래 수행 순서를 잡습니다.

수행 순서goaltypemissiondoor의미
1goal_s1storemission_Order1 · pickupdoor1매장1 상차 (Order1)
2goal_s2storemission_Order2 · pickupdoor2매장2 상차 (Order2)
3goal_c1customermission_Order1 · dropoffdoor1Order1 배송
4goal_c2customermission_Order2 · dropoffdoor2Order2 배송

Robot1이 정한 수행 순서는 r2s/Robot1/goals 진행 발행의 arrivalOrder(1~4)로 반영되어, 관제가 Robot1의 계획을 파악합니다.

⑤ 수행 — 상차 2회 → 배송 2회 (r2s/Robot1/goals) · Robot1 → 관제

Robot1이 arrivalOrder 순으로 수행하며 각 단계 완료를 mission done으로 발행합니다. 매장2 상차 완료 시점에 두 주문(door1·door2)을 모두 적재한 상태가 됩니다.

// 매장1 상차 완료
{ "occurredAt": "2026-06-17T10:04:30+09:00",
  "payload": { "goals": [
    { "id": "goal_s1", "status": "done",
      "mission": [ { "id": "mission_Order1", "type": "pickup", "status": "done", "door": [ { "name": "door1", "status": "closed" } ] } ] }
  ] } }
// 매장2 상차 완료
{ "occurredAt": "2026-06-17T10:07:20+09:00",
  "payload": { "goals": [
    { "id": "goal_s2", "status": "done",
      "mission": [ { "id": "mission_Order2", "type": "pickup", "status": "done", "door": [ { "name": "door2", "status": "closed" } ] } ] }
  ] } }
// Order1 배송 완료 → 관제가 오토메타에 통지
{ "occurredAt": "2026-06-17T10:12:10+09:00",
  "payload": { "goals": [
    { "id": "goal_c1", "status": "done",
      "mission": [ { "id": "mission_Order1", "type": "dropoff", "status": "done", "door": [ { "name": "door1", "status": "closed" } ] } ] }
  ] } }
// Order2 배송 완료 → 관제가 오토메타에 통지. 전 mission done → state.status=done (연속 배차=delivery 유지 / 충전 복귀=navi·charging)
{ "occurredAt": "2026-06-17T10:16:40+09:00",
  "payload": { "goals": [
    { "id": "goal_c2", "status": "done",
      "mission": [ { "id": "mission_Order2", "type": "dropoff", "status": "done", "door": [ { "name": "door2", "status": "closed" } ] } ] }
  ] } }

8.3 상태 전이 요약

단계mode주요 goal.status주요 mission.status
배차 전 대기delivery——
배차 대기delivery——
낙찰·수행 시작deliverygoal_s* standby→doingpickup standby
매장 도착deliverygoal_s* doingauth doing
상차(열림→닫힘)·매장 완료deliverygoal_s* donepickup doing→done
세대 도착deliverygoal_c* doingauth doing
하차(열림→닫힘)·세대 완료deliverygoal_c* donedropoff doing→done
전 goal·mission 완료 (state.status=done)delivery전부 done전부 done
완료 후 — 연속 배차delivery 유지새 goal standby→doing새 mission standby
완료 후 — 충전 복귀 (관제/자동 회차)navicharging goal doing→donecharge

세 계층 모두 standby·doing·done 한 벌을 씁니다 — 종전의 ready·going·arrived·paused 표기는 폐지되었습니다(API 문서 §1). 도착·해당 지점 완료가 goal 의 done이며, 미션 종료는 그 미션에 속한 모든 goal·mission 이 done일 때입니다. 중도 취소는 goal/mission canceled + reason(로봇 발행 항목 §6). 복귀=충전은 delivery가 아니라 navi 모드이며, 다음 일감을 바로 받는 연속 배차에서만 delivery를 유지합니다.

9. 지오펜스 배포·적용

지오펜스는 관제 콘솔에서 단지 운영자가 정의하는 다각형 영역입니다. 로봇은 이 영역을 내려받아 보유하고, 경로 계획·주행 중 스스로 판정합니다. 로봇이 발행하는 항목은 로봇 발행 항목 §9를 따릅니다.

9.1 전체 흐름

#주체동작
1관제영역 등록·수정·삭제·활성 변경 → 해당 단지의 지오펜스 버전 증가
2관제 → 로봇s2r/{robot_id}/geofence/version 발행 (retain)
3로봇수신 버전이 보유 버전과 다르면 GET /v1/robot/geofences 호출
4로봇영역 적용 후 state.geofence.version 회신
5로봇경로 계획·주행 중 영역 판정 (§9.4)
6로봇 → 관제우회·차단 발생 시 r2s/{robot_id}/geofence 발행

영역 진입·이탈 판정은 관제가 위치 발행으로 수행합니다. 로봇은 예방(경로에서 배제·우회·감속)을 담당하고, 진입 기록은 관제가 남깁니다.

9.2 버전 알림 — s2r/{robot_id}/geofence/version

{
  "occurredAt": "2026-09-22T10:00:00+09:00",
  "payload": {
    "siteId": "APT-GOCHEOK-001",
    "version": 42
  }
}

다른 s2r 토픽과 같은 공통 envelope(§1)을 씁니다 — 최상위 occurredAt + payload.

9.3 영역 조회 — GET /v1/robot/geofences

인증은 맵 등록(API 문서 §7)과 동일한 로봇 HMAC 서명 방식을 사용합니다. 요청·응답·에러 코드의 정식 정의는 API 문서 §8입니다.

{
  "result": "SUCCESS",
  "data": {
    "siteId": "APT-GOCHEOK-001",
    "version": 42,
    "geofences": [
      {
        "id": "GF-01a0c3ef",
        "kind": "no-go",
        "env": "indoor",
        "mapId": "ground",
        "vertices": [
          { "x": 46.3, "y": 193.9 },
          { "x": 52.3, "y": 193.9 },
          { "x": 52.3, "y": 199.9 },
          { "x": 46.3, "y": 199.9 }
        ],
        "effectiveFrom": null,
        "effectiveTo": null
      },
      {
        "id": "GF-01a0c318",
        "kind": "hazard",
        "env": "outdoor",
        "vertices": [
          { "lat": 37.4983392, "lng": 126.8599869 },
          { "lat": 37.4978070, "lng": 126.8595129 },
          { "lat": 37.4971505, "lng": 126.8605206 }
        ],
        "effectiveFrom": "2026-09-22T09:00:00+09:00",
        "effectiveTo": "2026-09-30T18:00:00+09:00"
      }
    ]
  }
}
필드타입설명
versioninteger이 응답이 담은 단지 지오펜스 버전. 로봇은 이 값을 적용 버전으로 삼습니다.
geofences[].idstring영역 식별자. 우회·차단 발행 시 그대로 싣습니다.
geofences[].kindenumno-go · avoid · hazard. 거동은 §9.4.
geofences[].envenumindoor · outdoor. 정점 형식이 갈립니다(§9.5).
geofences[].mapIdstringindoor에만 포함. 상태 발행의 location.map_id와 같은 값입니다.
geofences[].verticesarray다각형 정점. 나열 순서가 변의 순서입니다. 3개 이상.
geofences[].effectiveFromstring효력 시작. null은 제한 없음.
geofences[].effectiveTostring효력 종료. null은 무기한.

9.4 영역 종류별 거동 · 적용 시점

종류로봇 거동발행
no-go경로 계획에서 배제합니다. 통과하는 경로를 만들지 않습니다. 이미 내부에 있으면 최단 경로로 벗어난 뒤 정지합니다.blocked
avoid경로 비용에 가중치를 둡니다. 우회가 가능하면 우회하고, 손실이 과도하면 통과를 허용합니다.detour
hazard통과를 허용하되 내부에서 속도를 제한합니다. 제한값 협의없음

적용 시점

효력 기간

effectiveFrom·effectiveTo는 로봇이 자체 시계로 매 판정 시점에 대조합니다. 기간이 시작·종료되어도 버전은 바뀌지 않으므로, 버전 알림만으로는 반영되지 않습니다.

9.5 좌표계

env정점 형식기준
indoorx · y (미터)맵 프레임 좌표. 상태 발행의 location.x·location.y와 같은 좌표계·같은 단위이며, mapId가 location.map_id와 일치하는 영역만 판정 대상입니다.
outdoorlat · lngWGS84. 로봇이 location.lat·location.lng를 발행하는 구간에서만 판정됩니다.

위성 음영 구간(실내·지하)은 indoor 영역으로 등록됩니다. 해당 구간에서 outdoor 영역은 판정되지 않습니다.