Skip to content

MQTT_SCHEMA SPOT

Joo edited this page Oct 4, 2026 · 16 revisions

SPOT 제어 MQTT 스키마

항목 내용
제공값 session_id (오케스트레이션 서버에서 발급)
작성 주체 SPOT 개발 담당
최종 수정일 2026-08-30
상태 초안 — 협의 필요. 시나리오별 흐름·정책은 sequence 페이지, 합의·미합의 사항은 8절

0. 이 문서의 범위

오케스트레이션 서버 ↔ SPOT 사이에서 오가는 것만 적습니다.

서버가 정하는 것     무엇을 할 것인가      "p2 로 가라"  "안내 시나리오를 실행하라"
SPOT 이 정하는 것    어떻게 할 것인가      어느 경로로, 몇 도로 돌아서, 어떤 자세로

자세(pose)·경로·정밀정렬 같은 동작 파라미터는 이 문서에 없습니다. SPOT 내부에서 처리합니다. 서버가 그 값을 알 필요도, 보낼 필요도 없습니다.

이 원칙은 오케스트레이션 서버 담당자 요청("포즈나 이동 로직은 로봇에서 자체 처리")을 그대로 반영한 것입니다.


1. 토픽 목록

구분 Topic 방향 QoS Retained 설명
REQUEST robot/spot-01/command OC 서버 → SPOT 1 ✕ 제어 명령
RESPONSE robot/spot-01/ack SPOT → 구독자 1 ✕ 접수·완료 보고
상태 robot/spot-01/state SPOT → 구독자 1 ○ 1Hz 상태
목적지 robot/spot-01/waypoints SPOT → 구독자 1 ○ 갈 수 있는 곳 목록
시나리오 robot/spot-01/scenarios 관리자UI → 구독자 1 ○ 실행 가능한 시나리오 목록 (로봇팀 UI 가 발행)
안전 robot/spot-01/estop SPOT → 구독자 2 ○ 비상정지 발생·해제
진행 robot/spot-01/plan SPOT → 구독자 1 ✕ 시나리오 스텝 진행상황
장애 robot/spot-01/fault SPOT → 구독자 1 ○ 장애 알림 (BATTERY_LOW 등)
세션 server/session/start OC 서버 → SPOT(구독) 1 ✕ 세션 시작 → SPOT 이 home 으로 이동 (§3-1)
세션 server/session/end OC 서버 → SPOT(구독) 1 ✕ 세션 종료 → SPOT 이 충전 위치로 복귀 (§3-1)

로봇 토픽에 명령을 발행하는 것은 OC 서버뿐입니다. 태블릿·관리자 화면·도착 자동화가 각각 서버에 요청하고, 서버가 로봇별 큐로 순서를 정해 이 토픽으로 냅니다 (2026-08-10 합의, §8).

무엇이 언제 명령을 만드는지는 이 문서의 범위가 아닙니다. 서버가 관리하며 LOCATION_AUTOMATION_DESIGN 에 있습니다. 그 흐름은 서버 사정으로 계속 바뀌므로(2026-08-22 에 도착 자동화와 디바이스 큐가 들어왔다) 여기 옮겨 적으면 곧 어긋납니다. 이 문서는 로봇이 무엇을 받고 무엇을 돌려주는지만 다룹니다 — 그건 서버가 바뀌어도 그대로입니다.

명령만 retained 를 쓰지 않습니다. 로봇이 재접속할 때 브로커가 옛 명령을 다시 배달하면 아무도 시키지 않았는데 로봇이 움직입니다.


2. 공통 페이로드

모든 메시지는 CloudEvents 1.0 봉투를 씁니다.

{
  "specversion": "1.0",
  "type": "robot.command",          // 메시지 종류
  "source": "/services/orchestration-server",
  "id": "0d3f…",                    // 메시지 고유 id (중복 수신 판별용)
  "time": "2026-08-08T04:12:03Z",
  "datacontenttype": "application/json",
  "subject": "robot/spot-01",
  "data": { /* 아래 절들 */ }
}

data 공통 필드 (REQUEST)

필드 타입 필수 설명
session_id string Y 오케스트레이션 서버 발급. SPOT 은 해석하지 않고 응답에 그대로 실어 되돌려줍니다 (형식 제약 없음)
command_id string Y 명령 한 건의 고유 id. 응답 매칭 키
action string Y 무엇을 할 것인가 (3절)
params object N action 별 파라미터

command_id 가 왜 필요한가 — 명령을 보내고 결과가 한참 뒤에 따로 옵니다. 여러 명령이 동시에 돌 수 있어서, 이 값이 없으면 어느 응답이 어느 명령의 것인지 알 수 없습니다.

중복 억제는 봉투의 id 로만 합니다 (command_id 가 아닙니다). 최근 512건을 기억하며, 로봇이 재시작하면 그 기억은 사라집니다.

⚠️ 그래서 같은 command_id 를 새 봉투 id 로 다시 보내면 로봇은 또 실행합니다. QoS 1 재전송(브로커가 같은 봉투를 다시 배달)은 걸러지지만, 서버가 직접 만드는 재시도는 걸러지지 않습니다. 재시도할 때는 로봇이 정말 못 받았는지 (ack 가 하나도 안 왔는지) 먼저 확인해주세요.


3. REQUEST (태블릿·OC → SPOT)

Topic: robot/spot-01/command

action params 설명
navigate_to { "destination": "p2" } 이름 붙인 지점으로 이동
run_scenario { "scenario_id": "guide", "variables": {…} } 시나리오 실행 (아래)
follow { "mode": "start" | "stop" } 사람 추종 시작·정지
return_home {} 충전 위치로 복귀 후 착석, 모터 전원 내림 — 다음 이동 명령에서 자동으로 다시 켜짐 (2026-10-04)
stop {} 즉시 정지. 어떤 상태에서도 수용
stand {} 일어서기 — 모터도 함께 켜짐 (2026-09-06 계약 승격)
sit {} 앉기 (2026-09-06 계약 승격)
power_off {} 앉히고 모터 전원 내림 — 다시 움직이려면 stand (2026-09-06 신규)
warm_up { "rotate": false, "take_lease": true } 준비 점검·복구. 연결·시간동기·E-Stop·제어권·맵·위치추정을 한 번에 확인하고 결과를 돌려줌. 기본은 로봇이 움직이지 않음 (아래) (2026-09-29 신규)

🚨 stop 은 서버 큐를 우회해 즉시 발행해주세요. 로봇은 실행 중에도 stop 만은 BUSY 로 거절하지 않지만, 그건 명령이 도착했을 때의 이야기입니다. 앞 명령의 종결 ack 를 기다리는 큐에 stop 이 들어가면 주행이 끝날 때까지 정지가 나가지 않습니다 — 안전 경로가 조용히 무력화됩니다.

위 9개가 계약의 전부입니다. (2026-09-06: stand·sit이 관리자 전용에서 계약으로 승격되고 power_off가 신설됐습니다. 2026-09-29: warm_up 이 신설됐습니다.) 로봇은 이 밖에도 relocalize · pose · execute_plan · align_tag · take_control/release_control 을 받지만 관리자 UI 전용이라 계약이 아닙니다. 예고 없이 바뀌므로 서버는 쓰지 마세요.

예시

{
  "session_id": "01HW9X8M2K",
  "command_id": "cmd-0042",
  "action": "navigate_to",
  "params": { "destination": "p2" }
}

destination 값

robot/spot-01/waypoints (5절)에 실려 오는 label 중 하나입니다. 자동 생성된 경유지는 목적지가 될 수 없습니다 — 그건 길이지 장소가 아닙니다. 목록에 없는 값을 보내면 REJECTED 로 거부하고, fault_detail.code 에 이유를, fault_detail.message 에 갈 수 있는 곳 이름들을 적어 돌려줍니다.

⚠️ 그 목록은 사람이 읽는 문장 안에 들어 있습니다 ("'x' is not in the map. known: origin, p2, p3"). 배열 필드가 아니므로 파싱하지 마세요 — 목록이 필요하면 waypoints 토픽을 읽어주세요.

⚠️ 같은 label 이 두 개 이상이면 그 이름은 목적지로 쓸 수 없습니다. 어느 쪽인지 정할 수 없어 AMBIGUOUS_WAYPOINT 로 거부합니다. 말없이 하나를 고르면 엉뚱한 곳으로 갑니다 — 현장 맵의 p3 두 개가 2.7m 떨어져 있었습니다. 목록에 같은 이름이 둘 보이면 버튼을 만들지 마세요.

중간 경유지는 SPOT 이 알아서 지나갑니다. 서버는 최종 목적지만 지정합니다.

variables 값

scenarios 토픽(5절)의 variables 는 키가 변수명, 값이 기본값입니다. 서버가 보내는 variables 는 그 기본값을 덮어씁니다.

  • 생략해도 됩니다. 기본값으로 실행되며 BAD_PARAM 이 나지 않습니다
  • 일부만 보내도 됩니다. 보낸 키만 덮어씁니다
  • 목적지를 뜻하는 변수의 값은 waypoints 의 label 이어야 합니다 — 그 외에는 스텝이 실행될 때 REJECTED(UNKNOWN_WAYPOINT)로 실패합니다

scenarios 의 unsupported_steps 가 비어 있지 않으면 그 시나리오는 아직 못 하는 동작을 포함합니다. 실행하면 그 스텝에서 멈춥니다.

warm_up — 준비 점검

로봇 담당자가 없어도 "지금 시나리오를 돌려도 되는가" 를 확인하고, 안 되면 무엇을 해야 하는지 받는 명령입니다. 관리자 페이지의 버튼 하나로 쓰는 것을 전제로 합니다.

언제 누르나 — 전시 시작 전, SPOT 전원을 켠 뒤, 명령이 안 먹을 때.

param 타입 기본 설명
rotate bool false true 면 태그가 안 보일 때 일어서서 제자리 회전하며 위치를 찾습니다. 로봇이 움직이므로 옆에 사람이 있을 때만. false 면 로봇은 움직이지 않습니다
take_lease bool true 제어권이 없으면 가져옵니다. false 면 가져오지 않고 누가 쥐고 있는지만 알려줍니다

둘 다 생략할 수 있습니다 ("params": {}).

하는 일 (순서대로, 기본은 전부 무모션)

순서 항목 내용
1 연결·시간동기 로봇이 안 닿으면 다시 접속하고, 시간 동기는 새 응답을 받을 때까지 확인
2 E-Stop 등록 시도 (모터가 꺼져 있을 때만 됩니다)
3 제어권 없으면 가져옴 (take_lease)
4 맵 로봇 안의 맵이 비어 있으면 다시 올림 (SPOT 재부팅 시 비워집니다)
5 위치추정 태그가 보이면 움직이지 않고 잡음. 안 보이면 rotate: true 일 때만 회전 탐색
6 결과 보고 준비 여부와 막힌 이유, 다음에 할 일

응답 — 다른 명령처럼 2번 옵니다. 보통 2~5초 안에 완료 응답이 옵니다.

완료 status 뜻 관리자 페이지가 할 일
SUCCESS 준비 완료. 시나리오를 돌려도 됩니다 "준비 완료" 표시
FAULT_HW + fault_detail.code = "NOT_READY" 미준비. 로봇 고장이 아니라 조치가 필요한 상태 fault_detail.message 를 그대로 화면에 보여주세요

warm_up 은 실행 중인 다른 warm_up 이 있으면 BUSY 를 돌려줍니다. 충전 중에도 보낼 수 있습니다 — 점검만 하고 로봇을 일으키지 않습니다.

미준비일 때의 실제 응답 (충전 케이블이 꽂힌 상태, 2026-09-29 현장):

{
  "command_id": "cmd-c902af65",
  "status": "FAULT_HW",
  "fault_detail": {
    "code": "NOT_READY",
    "message": "충전 케이블을 뽑으세요",
    "report": {
      "connection": "online",
      "timesync": "ready(fresh)",
      "estop": "registered",
      "lease": "owned",
      "map": "present (40 waypoints)",
      "localization": "ok",
      "commands_accepted": false,
      "block_reasons": ["CHARGING"],
      "localized": true,
      "is_lost": false,
      "battery_pct": 89,
      "charging_state": "CHARGING",
      "ready": false,
      "next": "충전 케이블을 뽑으세요"
    }
  }
}

fault_detail.message 는 사람이 읽는 조치 안내입니다. 여러 개면 / 로 이어집니다.

막힌 이유 (report.block_reasons) message 예
CHARGING 충전 케이블을 뽑으세요
ESTOP_HARDWARE 로봇 뒷면의 빨간 MOTOR LOCKOUT 버튼을 확인하세요
ESTOP_NOT_REGISTERED 로봇을 앉혀 모터를 끈 뒤 20초 기다렸다 다시
FAULT_LEASE 제어권이 없습니다 — 다시 누르거나 태블릿 조종을 끝내세요
BATTERY_LOW 배터리 부족 — 충전하세요
TIME_SYNC_NOT_READY 시간 동기 대기 중 — 10초 뒤 다시
(위치추정 안 됨) 태그가 보이게 하거나 rotate: true 로 다시
(로봇이 안 닿음) 로봇 전원·유선 케이블을 확인하세요

⚠️ message 문구는 바뀔 수 있습니다. 파싱하지 말고 그대로 보여주세요. 프로그램으로 판단하려면 report.ready 와 report.block_reasons 를 쓰세요.

ℹ️ warm_up 을 누르지 않아도 SPOT 은 전원이 켜지면 연결·맵·위치추정을 스스로 복구합니다(움직이지 않는 것만). warm_up 은 그 결과를 확인하고 빠진 것을 채우는 용도입니다.


3-1. 세션 토픽 (SPOT 이 구독)

OC 서버가 Vision 에 세션 시작·종료를 알리는 토픽(ORCHESTRATION_SERVER)을 SPOT 도 같이 구독해서 스스로 움직입니다. robot/spot-01/command 로 따로 명령을 보내지 않아도 됩니다.

Topic Payload SPOT 동작
server/session/start { "session_id": "…" } 하던 동작을 멈추고 home 으로 이동. 도착 후 감정 표현 허용
server/session/end { "session_id": "…" } 감정 표현 중단 → 충전 위치로 복귀 → 착석 → 모터 전원 내림

⚠️ server/session/end 가 오지 않으면 SPOT 은 복귀하지 않습니다. 마지막 자리에 서서 계속 기다리고, 시간이 지나도 스스로 돌아가지 않습니다 (2026-10-04 전시장: 종료 메시지가 오지 않아 9분간 대기, 관리자 콘솔의 return_home 으로 수동 복귀). 세션이 끝나는 모든 경로(사용자 종료·타임아웃·관리자 강제 종료·새 세션으로 교체)에서 발행해 주세요. 발행이 어려운 경로가 있으면 대신 return_home 명령을 보내면 됩니다.

서버 답(2026-10-04, orchestration-server 148): 네 경로 모두에서 낸다 — 태블릿 종료 · 관리자 강제 종료 · 같은 태블릿의 새 세션(이전 세션 end → 새 start 순) · 유휴 타임아웃(앱 설정 session_idle_timeout_minutes, 기본 15분). 10-04 의 9분 대기는 태블릿이 종료 없이 세션을 버린 경우로, 그때는 24시간 뒤에야 DB 만 닫혔다. 이제 활동이 15분 멈추면 서버가 end 를 낸다.

세부 규칙

상황 SPOT 동작
retained 로 온 메시지 무시. 재접속 때 옛 세션이 되살아나지 않게 합니다 — retained 로 발행하지 마세요
세션 중에 다른 session_id 의 start 새 세션으로 갈아탑니다. 진행 중이던 명령은 ABORTED
지금 세션이 아닌 session_id 의 end 무시 (늦게 온 이전 세션의 종료가 지금 응대를 끊지 않게)
복귀 중에 같은 end 가 또 옴 무시 (진행 중인 복귀 유지)
start 시점에 충전 케이블이 꽂혀 있음 뽑히기를 최대 30초 대기 → 뽑히면 3초 뒤 출발, 안 뽑히면 그 세션은 이동 없이 취소
end 시점에 충전 중 이동하지 않음 (이미 충전 위치로 간주)
복귀가 FAULT_NAV 로 실패 (사람에 막힘 등) SPOT 이 10초 뒤 스스로 재시도, 최대 3회. 그 사이 다른 명령·세션·STOP 이 오면 재시도 취소

결과 보고

세션 토픽으로 시작된 이동(홈 이동·충전 복귀)의 완료는 robot/spot-01/ack 로 나갑니다. OC 가 보낸 명령이 아니므로 command_id 는 SPOT 이 만든 cmd-xxxxxxxx, session_id 는 null 입니다 — OC 의 대기 목록과 매칭되지 않으니 무시하면 됩니다. 접수(ACCEPTED) ack 는 없고 완료(SUCCESS·FAULT_NAV·ABORTED)만 나갑니다.

감정 표현(cctv/person/emotion 직접 구독)의 완료도 같은 형식으로 나갑니다.

같은 동작을 명령으로 보내려면

robot/spot-01/command 에 action: "session_start"(params.destination 생략 시 home) · "session_end" 를 보내도 같은 처리입니다. 이 경우 일반 명령처럼 접수·완료 ack 가 요청의 command_id 로 돌아갑니다.


4. RESPONSE (SPOT → 구독자)

Topic: robot/spot-01/ack

한 명령에 응답이 2번 옵니다.

① 접수 응답   즉시      "받았다" 또는 "못 받는다"
② 완료 응답   나중에    "다 했다" 또는 "이래서 실패했다"
필드 타입 필수 설명
command_id string Y 요청의 값
session_id string | null Y 요청의 값을 그대로 되돌려줍니다. 로봇이 그 명령을 모르면 null
robot_id string Y spot-01
status string Y 아래 표
timestamp string Y 발행 시각 (UTC, ISO 8601)
fsm_state string Y 보고 시점의 로봇 상태
posture string Y stand | sit | unknown
location string | null Y 완료 시점의 웨이포인트 이름. 모르면 null
map_id string | null Y 로드된 맵. 없으면 null
fault_detail object | null Y 실패 시 아래 표. 성공이어도 키는 옵니다 (값이 null)

⚠️ ①접수와 ②완료는 실리는 필드가 다릅니다.

실리는 필드
① 접수 command_id · session_id · robot_id · status · timestamp · fsm_state 6개. 거부일 때만 fault_detail 이 더 붙습니다
② 완료 위 표 10개 전부. 값이 없으면 null 이며 키를 생략하지 않습니다

접수 ack 에서 location·map_id·posture 를 읽으면 undefined 입니다 — 아직 시작도 안 한 명령이라 값이 없는 게 맞습니다.

status

값 단계 뜻 서버가 할 일
ACCEPTED ① 접수됨. 실행 시작 완료 응답을 기다립니다
BUSY ① 다른 명령 실행 중 재시도 또는 대기. 완료 응답은 오지 않습니다
REJECTED ① 지금 상태에서 불가 재계획. 완료 응답은 오지 않습니다
SUCCESS ② 정상 완료 다음 단계
ABORTED ② STOP 으로 중단됨 사람이 개입한 것
FAULT_NAV ② 경로 실패·위치 상실 NOT_LOCALIZED 면 재시도 금지 (아래 참고)
FAULT_LEASE ② 제어권을 뺏김 state.commands_accepted 가 true 로 돌아올 때까지 대기
FAULT_HW ② 하드웨어 문제 운영자 호출
TIMEOUT ② 제한 시간 초과 재시도

⚠️ navigate_to 는 TIMEOUT 을 내지 않습니다. 로봇은 도착하거나 실패할 때까지 계속 갑니다. 서버가 자체 상한을 걸어주세요 — 주행 속도가 0.15 m/s 이므로 경로 길이(m) ÷ 0.15 에 여유를 더한 값이 기준이 됩니다. 상한을 넘겼다고 판단하면 같은 명령을 재시도하지 말고 stop 을 먼저 보내주세요. 로봇은 아직 그 명령을 실행 중이라 새 명령은 BUSY 로 거절됩니다.

orchestration-server 가 거는 상한은 앱 설정 spot_command_timeout_seconds (기본 30초) 입니다. 백오피스 설정 화면에서 바꿀 수 있고, 큐에 이미 들어간 명령도 발행 직전에 다시 읽으므로 고치면 바로 다음 명령부터 먹습니다. 자동화 액션에 timeout_ms 를 직접 적어 둔 경우에는 그 값이 우선합니다. 상한에 걸려 멈춘 명령은 스태프 화면 /staff/spot 에 뜨고, 거기서 즉시 정지 → 재시도 순으로 처리합니다.

⚠️ BUSY·REJECTED 는 완료 응답이 오지 않습니다. 무조건 기다리면 영원히 블로킹됩니다. ACCEPTED 를 받은 명령만 기다려주세요.

⚠️ 예외 하나 — run_scenario 안에서는 REJECTED 가 완료 응답으로 옵니다. 시나리오는 스텝을 시작한 뒤에야 목적지 이름을 풀기 때문에, 그 이름이 틀렸으면 접수는 ACCEPTED 로 끝나고 실패가 ②단계로 옵니다 (fault_detail.failed_step 으로 어느 스텝인지 알 수 있습니다). ACCEPTED 를 받은 명령은 REJECTED 로 끝날 수도 있다고 보고 종결 처리해주세요.

fault_detail 구조

필드 타입 언제
code string 항상 (아래 표)
message string 항상. 사람이 읽는 문장
failed_step number run_scenario·플랜 실패 시에만. 실패한 스텝의 0-베이스 첨자
plan_id string 〃

fault_detail.code

코드 언제 서버가 할 일
UNKNOWN_ACTION 모르는 action 오타이거나 미지원. 재시도 무의미
BAD_PARAM 필수 파라미터 없음 빠진 이름이 message 에 적힙니다
UNKNOWN_WAYPOINT destination 이 목록에 없음 waypoints 를 다시 읽으세요
AMBIGUOUS_WAYPOINT 같은 이름의 지점이 둘 이상 그 이름은 쓸 수 없습니다 (3절)
NOT_A_DESTINATION 자동 생성 경유지를 목적지로 지정 길이지 장소가 아닙니다
NOT_LOCALIZED 로봇이 자기 위치를 모름 재시도 금지. 위치추정부터
BATTERY_LOW 배터리 잠금 — 복귀 후 착석 상태 (9.2절) 충전 전까지 안 풀립니다. 운영자 호출
MOTION_DISABLED 운영자가 모션을 전면 차단함 사람이 풀어야 합니다. stop 은 계속 먹습니다
DISPATCH_ERROR 로봇 내부 예외 로그 확인이 필요합니다

ack 와 plan 에는 요청의 session_id 를 그대로 실어 되돌려줍니다. OC 서버가 이 값을 재발행 토픽 이름 안의 라우팅 키로 쓰기 때문입니다 (orchestration/{session_id}/spot/ack). 이 값이 없으면 재발행 자체가 일어나지 않아 화면에 아무것도 뜨지 않습니다 — 메시지 유실처럼 보이지만 라우팅 실패입니다.

예시

// ① 접수
{ "command_id": "cmd-0042", "session_id": "01HW9X8M2K", "robot_id": "spot-01",
  "status": "ACCEPTED", "timestamp": "2026-08-08T04:12:03Z",
  "fsm_state": "NAVIGATING" }

// ② 완료 (성공)
{ "command_id": "cmd-0042", "session_id": "01HW9X8M2K", "robot_id": "spot-01",
  "status": "SUCCESS", "timestamp": "2026-08-08T04:14:11Z",
  "fsm_state": "IDLE", "posture": "stand",
  "location": "p2", "map_id": "map_0807_2112", "fault_detail": null }

// ② 완료 (실패)
{ "command_id": "cmd-0042", "session_id": "01HW9X8M2K", "robot_id": "spot-01",
  "status": "FAULT_NAV", "timestamp": "2026-08-08T04:13:40Z",
  "fsm_state": "IDLE", "posture": "stand",
  "location": null, "map_id": "map_0807_2112",
  "fault_detail": { "code": "NOT_LOCALIZED",
                    "message": "robot does not know where it is" } }

5. 상태 · 목록 토픽

robot/spot-01/state — 1Hz, retained

{
  "connection": "online",              // offline 이면 SPOT 이 죽은 것 (아래 참고)
  "timestamp": "2026-08-08T04:12:03Z",
  "fsm_state": "IDLE",                 // IDLE|NAVIGATING|FOLLOWING|EXECUTING_PLAN
                                       // |POSING|INSPECTING|RELOCALIZING
  "battery_pct": 54,
  "battery_runtime_min": 93,
  "power_state": "ON",                 // ON|OFF  (모터 전원)
  "posture": "stand",                  // stand|sit|unknown — 전원과 별개입니다
  "shore_power": "OFF",                // 충전 케이블 ON|OFF
  "battery_status": "DISCHARGING",     // CHARGING|DISCHARGING
  "estop_state": "NONE",               // NONE|SOFTWARE|HARDWARE
  "lease_owned": true,                 // SPOT 서버가 제어권을 쥐고 있나
  "lease_owner": "orchestration",      // orchestration | operator | none
  "commands_accepted": true,           // ★ false 면 지금 명령을 보내도 실패합니다
  "localized": true,                   // 자기 위치를 아나
  "is_lost": false,                    // true 면 이동 명령이 실패합니다
  "current_waypoint_label": "p2",      // 사람이 읽는 이름
  "current_waypoint_id": "papery-toucan-iQ1q…",   // 그래프 상의 고유 id
  "target_waypoint_label": null,       // 이동 중이면 목적지
  "target_waypoint_id": null,
  "map_id": "map_0807_2112",
  "current_command_id": null,          // 실행 중인 명령. 없으면 null
  "seed_tform_body": { "x": 1.46, "y": 0.24, "z": -0.42, "yaw_deg": 55.0 },
  "world_position_cm": null,           // 보정되면 { "x": 812.4, "y": -230.1 } (cm, z·yaw 없음)
  "anchor_offset": null,               // 위 변환의 근거 {rotation_deg, tx_cm, ty_cm}
  "faults": []                         // 있으면 [{ "name": "…", "severity": 2 }]
}

commands_accepted 를 보고 발행 여부를 판단해주세요. false 가 되는 경우는 관리자가 현장에서 제어권을 가져갔을 때, 비상정지 중일 때, 배터리가 바닥이거나 잠겼을 때(9.2절), 그리고 로봇이 관전 모드로 붙어 있을 때입니다. 앞의 것들은 lease_owner · estop_state · battery_pct 로 구분할 수 있습니다.

관전 모드는 SPOT 서버가 E-Stop 등록을 못 한 상태입니다 — 상태·카메라·위치는 전부 정상이고 모션만 불가하며, 조건이 풀리면 스스로 복구됩니다. state 만으로는 이 경우를 따로 구분할 수 없으니, 다른 셋이 정상인데 commands_accepted 가 false 면 이것이라고 보시면 됩니다.

진단용 필드가 몇 개 더 실립니다 (temp_c · joints · joint_max_load · joints_detail). 계약이 아니라 예고 없이 바뀌므로 화면에서 쓰지 마세요.

이 토픽의 발행자는 SPOT 서버 하나입니다 — 봉투의 source 는 항상 /services/spot-mcp-server 입니다.

current_command_id 는 ack 를 놓쳤을 때의 복구 단서입니다. 완료 ack 는 retained 가 아니라, 서버가 재시작하거나 브로커 연결이 끊긴 사이에 오면 유실됩니다. 그때 이 값을 보면 로봇이 아직 그 명령을 실행 중인지 알 수 있습니다 — null 인데 서버 큐에 pending 이 남아 있다면 그 명령은 이미 끝난 것입니다.

접수 ACCEPTED 시점에 실리고 완료 ack 시점에 내려갑니다. MQTT 로 받은 명령만 실립니다 — 관리자 UI 가 MCP 로 직접 시킨 동작 중에는 null 이고, 그때는 fsm_state 가 IDLE 이 아닌 것으로 판단해주세요.

state 페이로드에는 robot_id 가 없습니다. 어느 로봇인지는 토픽 경로의 {id} 세그먼트로 판단해주세요 (ack 에는 robot_id 가 실립니다).

world_position_cm 은 앵커 태그로 보정한 뒤에만 값이 있습니다. 그 전에는 null 입니다. 콘솔이 이 좌표를 지도에 되찍으려면 역변환이 필요한데, 근거는 anchor_offset 뿐입니다. 맵을 다시 녹화하면 둘 다 null 로 돌아갑니다.

connection: offline 은 SPOT 이 발행하지 않습니다. SPOT 이 죽으면 브로커가 대신 발행합니다(MQTT LWT). retained 라 늦게 붙은 구독자도 즉시 압니다. 다만 브로커가 알아채는 데 최대 60초 걸리므로, timestamp 를 같이 보고 오래된 값인지 판단해주세요.

그때 오는 것은 위 전문이 아니라 아래 3필드짜리입니다 (브로커가 미리 등록된 페이로드를 그대로 뱉기 때문입니다):

{ "specversion": "1.0", "type": "robot.state",
  "source": "/services/spot-mcp-server", "subject": "robot/spot-01",
  "data": { "robot_id": "spot-01", "connection": "offline",
            "timestamp": "2026-08-08T04:20:00Z" } }

⚠️ 같은 토픽에 스키마가 둘이라는 뜻입니다. battery_pct·fsm_state 를 필수로 파싱하면 로봇이 죽는 순간 파서가 깨집니다. connection 을 먼저 보고 분기해주세요.

⚠️ 이 메시지도 retained 라 직전의 정상 state 를 덮어씁니다. "죽기 직전 배터리가 몇 %였나" 는 브로커에 남지 않습니다 — 필요하면 서버가 마지막 값을 보관해주세요.

robot/spot-01/waypoints — retained

갈 수 있는 곳 목록. 맵을 새로 만들 때마다 갱신됩니다.

{
  "map_id": "map_0807_2112",
  "updated_at": "2026-08-07T12:54:48Z",
  "waypoints": [
    { "label": "origin", "fiducials": [1] },
    { "label": "p2",     "fiducials": [0, 2] },
    { "label": "p3",     "fiducials": [0] }
  ]
}

retained 라서 서버가 언제 켜지든 접속 즉시 목록을 받습니다. 태블릿 목적지 메뉴를 이걸로 채우면 됩니다.

⚠️ label 은 유일하지 않습니다. 맵을 녹화할 때 같은 이름을 두 번 붙이면 그대로 둘 다 실려 옵니다. 그 이름은 목적지로 못 씁니다(3절) — 메뉴를 만들기 전에 중복을 걸러주세요.

robot/spot-01/scenarios — retained

실행 가능한 시나리오 목록. 관리자 UI 의 편집기가 저장할 때 발행합니다.

{
  "updated_at": "2026-08-08T…Z",
  "scenarios": [
    { "id": "guide", "label": "전시 안내",
      "variables": { "spot": "p2" },        // 실행 시 채울 값
      "unsupported_steps": [] }             // 아직 못 하는 동작이 있으면 여기 표시
  ]
}

스텝 내용은 실려 있지 않습니다. 서버는 id 와 label 만 알면 되고, 그 시나리오가 어떤 동작들로 이루어졌는지는 SPOT 이 압니다.

robot/spot-01/plan — 시나리오 진행 상황

{ "session_id": "01HW9X8M2K", "command_id": "cmd-0050",
  "plan_id": "p-1", "scenario_id": "guide", "action": "navigate_to",
  "step_index": 0, "total_steps": 3,
  "status": "SUCCESS", "timestamp": "2026-08-08T04:12:03Z" }

여러 단계짜리 시나리오가 어디까지 갔는지 보여줍니다. 태블릿 진행 표시에 쓸 수 있습니다.

  • step_index 는 0부터 셉니다 (0 ≤ step_index < total_steps). 완료 ack 의 fault_detail.failed_step 도 같은 기준입니다. 어긋나면 같은 스텝이 토픽마다 다른 번호로 나갑니다.
  • 스텝당 1건, 그 스텝이 끝난 시점에만 발행합니다. 시작 이벤트는 없습니다. 진행 중인 스텝은 "마지막 발행 + 1" 로 보면 됩니다.
  • 실패한 스텝도 1건 발행됩니다 — status 가 SUCCESS 가 아니고 aborted: true 가 붙습니다. 그 뒤 스텝은 발행되지 않습니다.
  • total_steps 는 항상 실립니다. 스텝이 실패하면 거기서 멈추고 나머지는 발행되지 않으므로, 이 값이 없으면 "3개 중 1개만 왔다" 와 "1개짜리가 다 왔다" 를 구분할 수 없습니다.
  • 중단되면 aborted: true 가 붙습니다. 평소에는 키 자체가 없습니다.
필드 타입 필수 설명
plan_id string Y 실행 한 건의 id. SPOT 이 발급하며 command_id 와 1:1 입니다. 같은 시나리오를 다시 돌리면 새로 발급됩니다
command_id string Y 요청의 값
session_id string | null Y 요청의 값 (재발행 라우팅 키)
robot_id string Y spot-01
scenario_id string | null Y run_scenario 가 아니면 null
action string Y 그 스텝이 무엇이었나 (navigate_to 등)
step_index number Y 0-베이스
total_steps number Y 전체 스텝 수
status string Y SUCCESS | ABORTED | FAULT_NAV | FAULT_LEASE | FAULT_HW | TIMEOUT
timestamp string Y 발행 시각
aborted bool N 중단·실패로 끝났을 때만 true

6. 안전 토픽

robot/spot-01/estop — retained, QoS 2

비상정지가 발생했을 때와 해제됐을 때 발행합니다.

{ "active": true,
  "source": "operator_console",
  "reason": "soft_stop: motion halted, freeze in place",
  "timestamp": "2026-08-08T04:12:03Z" }
reason 무엇이 일어났나
soft_stop: … 움직임만 멈춤. 선 채로 유지
stop_sit: … 멈추고 앉음
hard_estop: … 모터 전원 차단
estop_release: … 해제됨 (active: false)

reason ↔ state.estop_state

reason state.estop_state
soft_stop · stop_sit NONE 그대로 — 로봇의 E-Stop 등록을 건드리지 않고, 제어권과 정지명령만으로 세웁니다
hard_estop NONE 이 아니게 됩니다 (모터 전원 차단)

⚠️ estop.active: true 인데 state.estop_state: NONE 인 상태가 정상입니다. 두 토픽이 어긋난 게 아니라, 소프트 정지는 E-Stop 레이어를 쓰지 않기 때문입니다. 화면에는 estop.active 를 띄우고, estop_state 는 "모터 전원이 끊겼나" 로만 읽어주세요.

누가 푸나

해제는 계약 action 에 없습니다. 운영자가 관리자 UI 또는 태블릿에서 풉니다. 서버는 estop 토픽으로 해제 사실(active: false)을 받을 뿐, 해제를 요청할 수단이 없습니다. 필요하다면 협의 대상입니다(8절).

robot/spot-01/fault — retained, QoS 1

로봇이 스스로 판단한 장애를 알립니다. 시나리오를 중단시키는 사건이 여기로 나옵니다.

{ "robot_id": "spot-01", "code": "BATTERY_LOW", "active": true,
  "message": "battery 32% - returning to charge",
  "battery_pct": 32.0,
  "timestamp": "2026-08-30T06:14:03Z" }
code 언제
BATTERY_LOW 배터리가 하드 리밋에 닿음 (9.2절)
  • message 는 사람이 읽는 문장이며 상황마다 다릅니다. 분기에 쓰지 마세요

  • 코드별 추가 필드가 붙습니다 (BATTERY_LOW 는 battery_pct)

  • active 로 발생과 해제를 가릅니다 — 해제되면 같은 code 에 active: false 가 한 번 더 나갑니다 (estop 토픽과 같은 규약)

  • SPOT 서버가 재시작하면, 지금 아무 장애가 없을 때 해제를 한 번 발행합니다. retained 는 프로세스가 죽어도 브로커에 남아 있어서, 새로 뜬 서버는 무엇이 걸려 있는지 알 수 없기 때문입니다. 발생을 본 적 없는데 해제가 오는 경우가 있다고 보시면 됩니다 — 이미 해제된 상태라면 무시하셔도 됩니다

retained 라서 늦게 붙은 구독자도 즉시 현재 상태를 받습니다. active 를 보고 판단해주세요 — timestamp 만 보면 "오래된 장애" 인지 "오래 지속 중인 장애" 인지 구분할 수 없습니다.

state.faults 는 이것과 다릅니다. 그쪽은 로봇 하드웨어가 보고하는 시스템 폴트 목록({name, severity})이고, 이 토픽은 SPOT 서버의 판단입니다.


7. 시나리오별 흐름

시퀀스 다이어그램과 시나리오별 정책 제안은 sequence 페이지에 있습니다. 같은 내용을 두 곳에 두면 한쪽만 고쳐져 어긋나므로 링크로 둡니다.

시나리오 쓰는 action 특히 볼 것
지점 이동 navigate_to NOT_LOCALIZED 는 재시도해도 안 풀립니다
시나리오 실행 run_scenario 이름만 보내면 됩니다. 진행률은 plan 토픽
사람 추종 follow ⚠️ 구현 진행 중. 방치되면 스스로 끝납니다(시간은 조정 중)
로봇팔 연동 navigate_to 완료(SUCCESS)를 받고 팔에 지시해주세요
비상정지·복구 stop estop 토픽이 retained 로 옵니다
제어권 인수·인계 — state.commands_accepted 로 판단

8. 합의된 것 · 남은 것

합의됨

# 항목 결론
1 동시 발행 정리 OC 서버가 로봇별 큐로 순서를 관리합니다 (2026-08-10). 태블릿·관리자 화면·도착 자동화는 서버에 요청하고, 로봇 토픽에 내는 것은 서버뿐입니다. SPOT 은 한 번에 하나만 실행하고 실행 중 새 명령은 BUSY 로 거절하므로(§9.1), 여러 곳이 각자 보내면 뒤엣것이 죽습니다 — 그래서 한 곳으로 모았습니다

로봇 쪽은 이 결정으로 바뀐 것이 없습니다. 받는 토픽·페이로드·ack 규약이 같습니다. 무엇이 언제 명령을 만드는지는 LOCATION_AUTOMATION_DESIGN 을 보세요.

남은 것

# 항목 지금 물어보고 싶은 것
2 시나리오 목록 발행 주체 로봇팀 관리자 UI 가 scenarios 토픽(retained)으로 발행 서버가 시나리오를 만들거나 관리할 계획이 있다면 발행 주체를 옮겨야 합니다. 지금 계획을 알려주세요
3 목적지 라벨 waypoints 토픽의 label 이 정본 전시장 맵을 아직 안 찍어 라벨이 확정되지 않았습니다. 확정되면 device_locations.spot_waypoint_label 에 넣을 값을 전달하겠습니다
4 비상정지 알림 토픽 robot/spot-01/estop 을 정본으로 합니다 (2026-08-30). 전역 MQTT_SCHEMA 의 safety/estop 은 오케스트레이션 서버가 릴레이하지 않아 아무도 받지 못합니다. 그쪽을 구독 중인 서비스가 있으면 알려주세요 — 양쪽 발행으로 바꿀 수 있습니다

9. 공통 정책

시나리오를 가리지 않고 적용되는 것들입니다.

9.1 한 번에 하나

SPOT 은 명령을 하나씩만 실행합니다. 실행 중 새 명령은 BUSY 로 거절합니다 (STOP 제외).

제안 — 서버가 큐를 관리해주세요. 앞 명령의 완료 응답(SUCCESS/FAULT_*)을 받은 뒤 다음을 보내는 방식이 가장 단순합니다.

9.2 배터리

수준 무슨 일이 일어나나 누가 서버 대응 제안
35% 이하 상태에 표시만 — 긴 시나리오를 새로 시작하지 않기
32% 이하 스스로 충전소로 복귀합니다. 막히면 30% 까지 재시도하고, 그동안 fault 토픽에 BATTERY_LOW 를 냅니다 SPOT 진행 중인 시나리오가 중단됩니다
30% 이하 복귀를 포기하고 그 자리에 앉은 뒤 모션을 잠급니다. 이때 commands_accepted 가 false 로 내려가고, 이동 명령은 REJECTED(BATTERY_LOW)로 거부됩니다 SPOT 운영자 호출. 충전 전까지 안 풀립니다
30% 미만 전원 인가(power_on)가 거부됩니다 로봇 펌웨어 우리가 못 바꿉니다. 위 두 줄이 여기 닿지 않으려는 장치입니다
25% 미만 잠기지 않았더라도 commands_accepted 를 false 로 내립니다 (마지막 방어선) SPOT 명령을 보내지 마세요

9.3 유휴 시 자동 착석

3분 동안 명령이 없으면 SPOT 이 스스로 앉습니다. 다음 명령에 자동으로 다시 일어섭니다.

서 있는 것만으로 배터리가 빠르게 닳기 때문입니다(실측: 세션 소비의 대부분이 기립 유지). 서버가 따로 할 일은 없습니다.

9.4 로봇이 죽었을 때

state 토픽에 connection:"offline" 이 뜹니다. SPOT 이 발행하는 게 아니라
브로커가 대신 발행합니다(MQTT LWT).

브로커가 알아채는 데 최대 60초 걸리므로, timestamp 를 같이 보고 오래된 값인지 판단해주세요.


10. QoS / 기타 설정

항목 값 이유
명령 QoS 1 최소 1회 전달 보장. 중복은 봉투의 id 로 걸러냄
명령 retained ✕ 재접속 시 옛 명령이 재실행되면 안 됨
상태 retained ○ 신규 구독자가 즉시 현재 상태 획득
E-Stop QoS 2 정확히 1회. 안전 메시지는 중복·유실 둘 다 곤란
clean session true 오프라인 동안 쌓인 옛 명령을 받지 않음

부록. 실제로 검증된 것

문서 내용 중 실물 로봇으로 확인한 것과 아직 아닌 것을 구분합니다. 안 해본 것을 했다고 적으면 그걸 전제로 개발하시게 되므로 나눠 적었습니다.

항목 상태
navigate_to → 완주 ✅ 2026-08-07 웨이포인트 7개 통과. 2026-08-30 관리자 화면 → OC → MQTT 경로로 재확인
run_scenario ✅ 3스텝 전부 SUCCESS, session_id 그대로 되돌아옴
follow 시작·정지 ⚠️ 구현 진행 중 — 대상 추적이 불안정합니다. 시연에 넣지 마세요
추종 중 STOP ✅ 1초 안에 정지
state 1Hz ✅
stop 지령 수용 ✅ 2026-08-30, 4회 전부 즉시 ACCEPTED (주행 중 실제 정지거리는 2026-08-09 기록)
waypoints retained ✅
state.commands_accepted ✅ 2026-08-30, 실제 게이트로 동작 확인
return_home ✅ 2026-10-04 전시장, 관리자 콘솔 → 길목 경유 복귀·착석·모터 OFF 확인
로봇팔 연동 전체 흐름 ⬜ 미시험
world_position_cm / anchor_offset ⬜ 구현됨, 실물 보정 미검증

Clone this wiki locally