로봇은 관측한 사실을 발행하고, 임계 판정·집계·경보는 관제가 수행합니다. 로봇은 다음 값을 싣지 않습니다.
| 구분 | 채널 | 내용 |
|---|---|---|
| 상태 확장 | r2s/{robot_id}/state | 위경도·방위각 · 속도 · 내부 온도 · 센서 상태 · 통신 품질 · 맵 버전 · 누적 주행 (§2) |
| 접속 유지 | r2s/{robot_id}/state | MQTT keepalive · LWT 두절 판정 (§3) |
| 보행자 안전 | r2s/{robot_id}/safety | 인지 · 회피 · 정지 · 재가동 (§4) |
| 충전 | r2s/{robot_id}/state | 도킹 충전기 식별 · 배터리 상세 (§5) |
| 미션 예외 | r2s/{robot_id}/goals | 실패 · 거절 · 중단 사유 (§6) |
| 미션 거리 | r2s/{robot_id}/goals | 단계별 이동 거리 (§7) |
| 소프트웨어 | r2s/{robot_id}/software | 컴포넌트별 설치 버전 (§8) |
r2s/{robot_id}/state기존 상태 발행(API 문서 §4.1) 페이로드에 아래 필드를 추가합니다. 발행 방식은 기존과 동일하게 변동 필드 이벤트 단위이며 retain=true입니다.
{
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": {
"location": {
"x": 0.0, "y": 0.0, "th": 0.0, "map_id": "",
"lat": 37.5004000, "lng": 126.8556000, "heading": 273.4
},
"speed": 0.0,
"internal_temp": 0.0,
"sensors": {
"lidar": { "status": "ok", "errors_1h": 0 },
"camera": { "status": "ok", "errors_1h": 0 },
"imu": { "status": "ok", "errors_1h": 0 }
},
"comm": {
"uplink": "cellular",
"wifi_rssi": -58, "wifi_ssid": "ZWS-AP-01",
"net_type": "5G", "cellular_rsrp": -95, "cellular_level": "good"
},
"map": { "version": "", "synced_at": "2026-06-04T09:00:00+09:00" },
"odometer": 0,
"uptime": 0
}
}
| 필드 | 타입 | 단위 | 설명 |
|---|---|---|---|
payload.location.lat | number | deg | WGS84 위도. 소수점 7자리 권장 |
payload.location.lng | number | deg | WGS84 경도 |
payload.location.heading | number | deg | 로봇 전면이 향하는 방위. 진북 0°, 시계방향 증가(동 90 · 남 180 · 서 270), 0 이상 360 미만 |
payload.speed | number | km/h | 주행 속도 |
payload.internal_temp | number | °C | 본체 내부 온도 |
payload.sensors.{lidar,camera,imu}.status | enum | - | ok · degraded · fault |
payload.sensors.{lidar,camera,imu}.errors_1h | integer | 건 | 직전 1시간 오류 발생 수 |
payload.comm.uplink | enum | - | 실제 트래픽이 나가는 경로. wifi · cellular |
payload.comm.wifi_rssi | integer | dBm | Wi-Fi 수신 세기 (미연결 시 null) |
payload.comm.wifi_ssid | string | - | 접속 중인 Wi-Fi SSID (미연결 시 null) |
payload.comm.net_type | string | - | 셀룰러 접속 방식. 5G · LTE |
payload.comm.cellular_rsrp | number | dBm | 셀룰러 수신 전력. 로봇이 측정한 LTE RSRP 또는 NR RSRP 를 그대로 싣습니다. 미등록 시 null |
payload.comm.cellular_level | enum | - | excellent · good · fair · poor · none |
payload.map.version | string | - | 로봇이 현재 보유·사용 중인 맵 버전 |
payload.map.synced_at | string | ISO8601 | 해당 맵을 마지막으로 반영한 시각 |
payload.odometer | integer | m | 누적 주행 거리 |
payload.uptime | integer | s | 부팅 후 누적 가동 시간 |
location 확장기존 location(API 문서 §4.1의 x·y·th·map_id)에 위경도와 방위각을 같은 객체로 싣습니다. 같은 시각·같은 자세를 서로 다른 기준계로 표현한 값입니다.
x·y·th·map_id) 미션·배차·노드 정합에 사용합니다. 기존 규약 그대로이며, 본 문서는 이 필드들의 정의·단위·기준을 바꾸지 않습니다.lat·lng·heading) 단지 전체를 한 화면에서 보는 지도 표시에 사용합니다. 맵 로컬 값을 대체하지 않고 병기합니다.둘은 같은 발행에 함께 싣습니다. location을 발행할 때 한쪽만 갱신하면 같은 로봇이 화면마다 다른 위치·방향을 가리키게 됩니다.
heading 규약기존 location_score(위치 신뢰도)와 ems(비상정지 소스)는 관제가 저장·표시하므로 값이 변할 때마다 발행합니다.
comm 확장comm은 로봇의 통신 상태를 실제 트래픽 경로·Wi-Fi 세기·셀룰러 신호로 나눠 싣습니다.
uplink은 "붙어 있는가"가 아니라 "나가는가"입니다. 관제는 이 값만으로 통신 채널을 판정합니다. wifi_rssi가 실려 있어도 uplink가 cellular이면 셀룰러로 판정합니다 — Wi-Fi 연결 여부와 트래픽 경로는 별개의 사실입니다.cellular_rsrp와 wifi_rssi는 단위가 같아도 같은 축이 아닙니다. Wi-Fi RSSI 는 통상 -30~-90dBm, RSRP 는 -44~-140dBm 로 분포가 달라 관제가 별도 지표·별도 임계로 다룹니다. 한쪽 값으로 다른 쪽을 채워 보내지 마십시오 — 두 값이 한 축에 섞이면 양호한 셀룰러 신호가 약신호로 집계됩니다. 로봇은 측정값을 등급으로 환산하거나 반올림하지 않고 그대로 싣습니다.net_type은 cellular_rsrp와 같은 발행에 싣습니다. RSRP 의 해석 기준이 LTE 와 NR 에서 다르므로, net_type 없이 온 RSRP 는 등급으로 환산할 수 없습니다.wifi_rssi·wifi_ssid는 null입니다. wifi_ssid에 빈 문자열을 싣지 마십시오.관제는 아래 메커니즘으로 통신 두절을 판정합니다.
online: false를 will retain으로 등록합니다. 정상 종료 시에는 종료 전 online: false를 직접 발행합니다.r2s/{robot_id}/safety보행자 인지·회피 상황을 이벤트로 발행합니다. 임계 초과 판정과 위험 영역 진입 판정은 관제가 수행하므로 로봇은 발행하지 않습니다.
occurredAt으로 판단합니다.payload.events 배열에 복수 이벤트를 함께 실을 수 있습니다.{
"occurredAt": "2026-06-04T10:12:03.412+09:00",
"payload": {
"events": [
{
"eventId": "9f1c2a7e-3b41-4d0e-a7c5-1e2f9b6d8c04",
"type": "avoid",
"at": "2026-06-04T10:12:03.400+09:00",
"location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
"floor": "B1",
"trackId": "p-8821",
"distanceCm": 42,
"stopReason": null,
"detail": { "sensor": "fusion", "targets": 2, "class": "person" }
}
]
}
}
| 필드 | 타입 | 단위 | 설명 |
|---|---|---|---|
events[].eventId | string (UUID) | - | 이벤트 고유 식별자. 재전송 시 동일 값을 유지합니다(관제 중복 제거 키). |
events[].type | enum | - | detect · avoid · stop · resume |
events[].at | string | ISO8601 | 이벤트 발생 시각(밀리초 권장). 묶음 발행 시 항목별로 싣습니다. |
events[].location | object | - | x · y · th · map_id. 상태 발행의 location과 동일 좌표계입니다. |
events[].floor | string | - | 층 식별자. 실내 이벤트에 싣습니다. |
events[].trackId | string | - | 인지 대상 추적 식별자. 동일 대상인 동안 같은 값을 유지합니다. |
events[].distanceCm | integer | cm | 보행자와의 이격 거리. 정의는 §4.4를 따릅니다. |
events[].stopReason | enum | - | 정지 사유. type=stop에만 싣습니다. |
events[].trigger | enum | - | 재가동 주체. type=resume에만 싣습니다. |
events[].detail | object | - | 부가 정보(센서·대상 수·분류 등). 스키마를 강제하지 않습니다. |
○ 필수 · △ 선택 · — 미사용
| 필드 | detect | avoid | stop | resume |
|---|---|---|---|---|
eventId · type · at · location | ○ | ○ | ○ | ○ |
floor | 실내 이벤트 ○ · 실외 — | |||
trackId | ○ | ○ | △ | — |
distanceCm | △ | ○ | △ | — |
stopReason | — | — | ○ | — |
trigger | — | — | — | ○ |
detail | △ | △ | △ | △ |
이격 거리 distanceCm
null.정지 사유 stopReason
| 값 | 설명 |
|---|---|
pedestrian-near | 보행자 근접 |
crossing | 보행자 교차 진입 |
congestion | 통로 정체 |
emergency-stop | 비상 정지 |
obstacle | 장애물 |
재가동 주체 trigger — auto(로봇 자체 판단) · operator(운영자 지시)
추적 식별자 trackId — 동일 대상을 추적하는 동안 같은 값을 유지하고, 추적이 끊긴 뒤 재획득하면 새 값을 부여합니다. 로봇 내부에서만 유일하면 되며 전역 유일성은 요구하지 않습니다.
| 타입 | 발행 시점 | 억제 규칙 |
|---|---|---|
detect | 보행자를 반경 3m 이내에서 인지한 시점 | 동일 trackId는 30초 내 재발행하지 않습니다. |
avoid | 회피 기동 종료 시점(최소 이격 확정 후) — 기동 1회당 1건 | - |
stop | 보행자 요인으로 정지한 즉시 | - |
resume | 정지 상태에서 재가동한 즉시 | - |
10:12:01 detect trackId p-8821 distanceCm 280
10:12:03 avoid trackId p-8821 distanceCm 42
10:12:09 avoid trackId p-8830 distanceCm 95
10:12:31 avoid trackId p-8830 distanceCm 88
10:12:48 stop stopReason pedestrian-near
10:13:20 resume trigger auto
충전기 점유 현황을 관제가 파악할 수 있도록, 도킹한 충전기를 state에 싣습니다.
{
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": {
"mode": "charging",
"charging": { "charger_code": "CS-001", "docked": true, "docked_at": "2026-06-04T10:00:00+09:00" }
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
payload.charging.charger_code | string | 도킹한 충전기 식별자. 관제가 부여한 충전기 코드를 사용합니다. |
payload.charging.docked | boolean | 도킹 여부. 해제 시 false로 발행합니다. |
payload.charging.docked_at | string | 도킹·해제 시각(ISO8601) |
API 문서 §4.1의 battery 필드 중 아래는 반드시 전송합니다.
| 필드 | 전송 시점 |
|---|---|
soc | 상시 (변동 시) |
voltage · current · temperature | 상시 (변동 시) |
est_charge_complete | 충전 중 |
soh | 상시 (변동 시) |
r2s/{robot_id}/goalsgoal·mission이 canceled로 전이할 때 사유를 함께 발행합니다. 관제는 이 사유로 재배차 여부를 판단합니다.
{
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": {
"goals": [
{
"id": "goal-1",
"type": "customer",
"status": "canceled",
"reason": "path-blocked",
"reasonMsg": "",
"mission": [
{ "id": "m-1", "type": "dropoff", "status": "canceled", "reason": "door-not-opened" }
]
}
]
}
}
| 값 | 설명 |
|---|---|
path-blocked | 경로 차단 — 우회 불가 |
robot-fault | 로봇 이상 — 수행 불가 |
battery-low | 배터리 부족 |
elevator-unavailable | 엘리베이터 사용 불가 |
door-not-opened | 출입문·박스 개방 실패 |
timeout | 대기 시간 초과 |
operator-cancel | 운영자 지시로 중단 |
unknown | 그 외 — reasonMsg에 원문을 함께 전달 |
r2s/{robot_id}/goals단계(mission[])가 done으로 전이할 때 그 단계에서 이동한 거리를 함께 발행합니다. 관제는 이 값을 단계의 orderId로 묶어 주문별 배달 거리를 산출합니다.
{
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": {
"goals": [
{
"id": "goal-1",
"type": "customer",
"status": "done",
"mission": [
{ "id": "goal-1_00_move", "type": "move", "orderId": "order-1", "status": "done", "distance": 137 },
{ "id": "goal-1_01_elevatorCall", "type": "elevatorCall", "orderId": "order-1", "status": "done" },
{ "id": "goal-1_02_move", "type": "move", "orderId": "order-1", "status": "done", "distance": 24 },
{ "id": "m-1", "type": "dropoff", "orderId": "order-1", "status": "done", "distance": 0 }
]
}
]
}
}
| 필드 | 타입 | 단위 | 설명 |
|---|---|---|---|
payload.goals[].mission[].distance | integer | m | 그 단계에서 이동한 거리. 누적이 아닌 구간 값입니다 |
odometer(전원 투입 이래 누적 주행)와 다른 값입니다. 단계마다 그 단계에서만 이동한 거리를 싣습니다.done에서 확정하고 이후 바꾸지 않습니다. goals는 같은 내용이 반복 발행되므로 관제는 마지막 값으로 덮어씁니다 — 확정 후 값이 달라지면 거리가 변경된 것으로 읽힙니다. standby·doing 단계에는 싣지 않아도 됩니다.auth·elevatorCall·sourceFloorGetOn·gate_private 등)는 생략할 수 있고 관제는 0으로 읽습니다. 반대로 이동이 있었던 단계는 반드시 싣습니다 — 빠지면 그만큼 주문 거리가 줄어듭니다.로봇은 여러 주문의 goal 을 한 메시지에 묶어 발행하며(API 문서 §5.2), 주문 식별자는 goal 이 아니라 단계에 실립니다(mission[].orderId). 거리를 단계에 싣는 이유가 이것입니다 — goal 단위 값으로는 한 goal 안에 여러 주문의 단계가 섞였을 때 나눌 수 없습니다. 로봇은 지금처럼 각 단계에 orderId를 채워 보내면 됩니다.
r2s/{robot_id}/software로봇이 탑재한 컴포넌트별 버전을 발행합니다. MQTT 접속 직후 1회와 버전 변경 시 발행하며, retain=true입니다.
{
"occurredAt": "2026-06-04T10:00:00+09:00",
"payload": {
"components": [
{ "name": "navi", "version": "1.4.2" },
{ "name": "gui", "version": "2.0.1" },
{ "name": "robots", "version": "3.1.0" },
{ "name": "sub", "version": "0.9.7" }
]
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
payload.components[].name | string | 컴포넌트 식별자 (navi · gui · robots · sub 등) |
payload.components[].version | string | 설치 버전 문자열 |
state.geofence · r2s/{robot_id}/geofence관제가 배포한 영역(지오펜스)을 로봇이 내려받아 보유하고, 주행 중 스스로 판정합니다. 로봇이 발행하는 것은 두 가지입니다 — 적용 버전 회신(§9.1)과 우회·차단 이벤트(§9.2). 영역 진입·이탈 판정은 관제가 수행하므로 로봇은 발행하지 않습니다.
영역을 내려받는 절차(버전 알림 · 조회 · 적용 시점)는 시나리오 문서의 지오펜스 절을 따릅니다.
state.geofence새 토픽을 만들지 않고 기존 상태 발행(r2s/{robot_id}/state)에 geofence 객체를 얹습니다. map과 같은 자리입니다.
{
"geofence": {
"version": 42,
"applied_at": "2026-09-22T10:00:05+09:00"
}
}
| 필드 | 타입 | 단위 | 설명 |
|---|---|---|---|
geofence.version | integer | - | 로봇이 현재 적용 중인 단지 지오펜스 버전. 내려받기만 하고 아직 적용하지 않았다면 이전 값을 유지합니다. |
geofence.applied_at | string | ISO8601 | 그 버전을 실제로 적용한 시각. |
version은 0으로 발행합니다.r2s/{robot_id}/geofence영역 때문에 경로를 바꾸거나 진행할 수 없게 된 경우에 발행합니다. 관제는 로봇의 위치만으로는 "영역 때문에 돌아간 것"과 "원래 그 경로"를 구분할 수 없으므로, 이 판단은 로봇만 보고할 수 있습니다.
detour){
"occurredAt": "2026-09-22T10:04:45.120+09:00",
"payload": {
"eventId": "3f9a1c02-77d5-4a11-9c3e-5b0e1f7a2d48",
"type": "detour",
"geofenceId": "GF-01a0c31f",
"missionId": "MSN-20260922-0031",
"at": "2026-09-22T10:04:45.100+09:00",
"extraDistanceM": 340,
"extraSeconds": 95
}
}
blocked){
"occurredAt": "2026-09-22T10:06:11.480+09:00",
"payload": {
"eventId": "b1d77c54-2e08-4f93-8a6d-0c4e91f3b5a7",
"type": "blocked",
"geofenceId": "GF-01a0c318",
"missionId": "MSN-20260922-0031",
"at": "2026-09-22T10:06:11.450+09:00",
"hasAlternative": false,
"location": { "x": 61.0, "y": 171.5, "th": 0.0, "map_id": "ground" }
}
}
| 필드 | 타입 | 단위 | 설명 |
|---|---|---|---|
eventId | string (UUID) | - | 이벤트 고유 식별자. 재전송 시 동일 값을 유지합니다. |
type | enum | - | detour · blocked |
geofenceId | string | - | 원인이 된 영역 식별자. 조회 응답의 id와 같은 값입니다. |
missionId | string | - | 수행 중이던 미션. 미션 밖 주행이면 null. |
at | string | ISO8601 | 판단이 성립한 시각(밀리초 권장). |
extraDistanceM | number | m | detour 필수. 영역을 피하지 않았을 경우의 경로 대비 추가 이동 거리. |
extraSeconds | number | 초 | detour 필수. 같은 기준의 추가 소요 시간(추정값 허용). |
hasAlternative | boolean | - | blocked 필수. 목적지까지 다른 경로가 존재하는지. 존재하나 손실이 과도해 포기한 경우 true. |
location | object | - | blocked 필수. 진행 불가로 멈춘 지점. 상태 발행의 location과 동일 좌표계입니다. |
extraDistanceM · extraSeconds 는 생략하지 않습니다. 이 두 값이 없으면 우회 횟수만 남아 해당 영역의 운영 영향을 판단할 수 없습니다. 정확한 비교 경로를 산출하기 어려우면 추정값을 싣고 그대로 발행합니다.hasAlternative 의 구분 — false는 목적지에 도달할 경로가 물리적으로 없음을 뜻합니다. 우회로가 있으나 손실이 커서 포기한 경우는 true입니다. 관제의 후속 처리가 갈립니다.avoid 회피는 detour, no-go 진행 불가는 blocked입니다. hazard는 통과가 허용되므로 발행하지 않습니다.no-go를 적용받은 경우 — 최단 경로로 벗어난 뒤 blocked를 발행합니다.아래 항목은 채널·담당 경계를 확정한 뒤 본 문서에 규격을 추가합니다.
| 항목 | 내용 | 선행 조건 |
|---|---|---|
| 경로 이탈 · 리플래닝 보고 | 영역 외 사유(장애물·통행 불가 등)의 우회 발생·사유·지연 예상. 영역에 의한 우회는 §9.2 | 보고 단위(이벤트 / goals 확장) 확정 |
| 펌웨어 OTA 진행 보고 | 수락·거절·진행률·성공·실패·롤백 | OTA 담당 경계(관제 배포 / 제로웍스 자체 갱신) 확정 |
| 원격 명령 응답 | 긴급정지·복귀·속도제한 명령의 수신·수락·적용 회신 | 원격 명령 채널(s2r) 확정 |
| 적재함 재실 감지 | 칸별 물품 유무·잠금 상태 | 감지 수단 확정 (현재 door[] 개폐만 수신) |
| 자가진단 결과 | 부팅 점검 항목·통과 여부 | 점검 항목 목록 확정 |
| 라이브 영상 | 원격 제어 영상 스트림 | 스트리밍 방식 확정 |