Repository navigation
MQTT_SCHEMA SPOT
| 항목 | 내용 |
|---|---|
| 제공값 |
session_id (오케스트레이션 서버에서 발급) |
| 작성 주체 | SPOT 개발 담당 |
| 최종 수정일 | 2026-08-30 |
| 상태 | 초안 — 협의 필요. 시나리오별 흐름·정책은 sequence 페이지, 합의·미합의 사항은 8절 |
오케스트레이션 서버 ↔ SPOT 사이에서 오가는 것만 적습니다.
서버가 정하는 것 무엇을 할 것인가 "p2 로 가라" "안내 시나리오를 실행하라"
SPOT 이 정하는 것 어떻게 할 것인가 어느 경로로, 몇 도로 돌아서, 어떤 자세로
자세(pose)·경로·정밀정렬 같은 동작 파라미터는 이 문서에 없습니다. SPOT 내부에서 처리합니다. 서버가 그 값을 알 필요도, 보낼 필요도 없습니다.
이 원칙은 오케스트레이션 서버 담당자 요청("포즈나 이동 로직은 로봇에서 자체 처리")을 그대로 반영한 것입니다.
| 구분 | 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 를 쓰지 않습니다. 로봇이 재접속할 때 브로커가 옛 명령을 다시 배달하면 아무도 시키지 않았는데 로봇이 움직입니다.
모든 메시지는 CloudEvents 1.0 봉투를 씁니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
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가 하나도 안 왔는지) 먼저 확인해주세요.
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" }
}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 이 알아서 지나갑니다. 서버는 최종 목적지만 지정합니다.
scenarios 토픽(5절)의 variables 는 키가 변수명, 값이
기본값입니다. 서버가 보내는 variables 는 그 기본값을 덮어씁니다.
-
생략해도 됩니다. 기본값으로 실행되며
BAD_PARAM이 나지 않습니다 - 일부만 보내도 됩니다. 보낸 키만 덮어씁니다
- 목적지를 뜻하는 변수의 값은
waypoints의label이어야 합니다 — 그 외에는 스텝이 실행될 때REJECTED(UNKNOWN_WAYPOINT)로 실패합니다
scenarios의unsupported_steps가 비어 있지 않으면 그 시나리오는 아직 못 하는 동작을 포함합니다. 실행하면 그 스텝에서 멈춥니다.
로봇 담당자가 없어도 "지금 시나리오를 돌려도 되는가" 를 확인하고, 안 되면 무엇을 해야 하는지 받는 명령입니다. 관리자 페이지의 버튼 하나로 쓰는 것을 전제로 합니다.
언제 누르나 — 전시 시작 전, 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은 그 결과를 확인하고 빠진 것을 채우는 용도입니다.
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 로 돌아갑니다.
한 명령에 응답이 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_state6개. 거부일 때만fault_detail이 더 붙습니다② 완료 위 표 10개 전부. 값이 없으면 null이며 키를 생략하지 않습니다접수 ack 에서
location·map_id·posture를 읽으면undefined입니다 — 아직 시작도 안 한 명령이라 값이 없는 게 맞습니다.
| 값 | 단계 | 뜻 | 서버가 할 일 |
|---|---|---|---|
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로 끝날 수도 있다고 보고 종결 처리해주세요.
| 필드 | 타입 | 언제 |
|---|---|---|
code |
string | 항상 (아래 표) |
message |
string | 항상. 사람이 읽는 문장 |
failed_step |
number |
run_scenario·플랜 실패 시에만. 실패한 스텝의 0-베이스 첨자 |
plan_id |
string | 〃 |
| 코드 | 언제 | 서버가 할 일 |
|---|---|---|
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" } }{
"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 를 덮어씁니다. "죽기 직전 배터리가 몇 %였나" 는 브로커에 남지 않습니다 — 필요하면 서버가 마지막 값을 보관해주세요.
갈 수 있는 곳 목록. 맵을 새로 만들 때마다 갱신됩니다.
{
"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절) — 메뉴를 만들기 전에 중복을 걸러주세요.
실행 가능한 시나리오 목록. 관리자 UI 의 편집기가 저장할 때 발행합니다.
{
"updated_at": "2026-08-08T…Z",
"scenarios": [
{ "id": "guide", "label": "전시 안내",
"variables": { "spot": "p2" }, // 실행 시 채울 값
"unsupported_steps": [] } // 아직 못 하는 동작이 있으면 여기 표시
]
}스텝 내용은 실려 있지 않습니다. 서버는 id 와 label 만 알면 되고,
그 시나리오가 어떤 동작들로 이루어졌는지는 SPOT 이 압니다.
{ "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
|
비상정지가 발생했을 때와 해제됐을 때 발행합니다.
{ "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 |
|---|---|
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_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 서버의 판단입니다.
시퀀스 다이어그램과 시나리오별 정책 제안은 sequence 페이지에 있습니다. 같은 내용을 두 곳에 두면 한쪽만 고쳐져 어긋나므로 링크로 둡니다.
| 시나리오 | 쓰는 action
|
특히 볼 것 |
|---|---|---|
| 지점 이동 | navigate_to |
NOT_LOCALIZED 는 재시도해도 안 풀립니다 |
| 시나리오 실행 | run_scenario |
이름만 보내면 됩니다. 진행률은 plan 토픽 |
| 사람 추종 | follow |
|
| 로봇팔 연동 | navigate_to |
완료(SUCCESS)를 받고 팔에 지시해주세요 |
| 비상정지·복구 | stop |
estop 토픽이 retained 로 옵니다 |
| 제어권 인수·인계 | — |
state.commands_accepted 로 판단 |
| # | 항목 | 결론 |
|---|---|---|
| 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 은 오케스트레이션 서버가 릴레이하지 않아 아무도 받지 못합니다. 그쪽을 구독 중인 서비스가 있으면 알려주세요 — 양쪽 발행으로 바꿀 수 있습니다 |
시나리오를 가리지 않고 적용되는 것들입니다.
SPOT 은 명령을 하나씩만 실행합니다. 실행 중 새 명령은 BUSY 로 거절합니다 (STOP 제외).
제안 — 서버가 큐를 관리해주세요. 앞 명령의 완료 응답(SUCCESS/FAULT_*)을 받은 뒤
다음을 보내는 방식이 가장 단순합니다.
| 수준 | 무슨 일이 일어나나 | 누가 | 서버 대응 제안 |
|---|---|---|---|
| 35% 이하 | 상태에 표시만 | — | 긴 시나리오를 새로 시작하지 않기 |
| 32% 이하 |
스스로 충전소로 복귀합니다. 막히면 30% 까지 재시도하고, 그동안 fault 토픽에 BATTERY_LOW 를 냅니다 |
SPOT | 진행 중인 시나리오가 중단됩니다 |
| 30% 이하 | 복귀를 포기하고 그 자리에 앉은 뒤 모션을 잠급니다. 이때 commands_accepted 가 false 로 내려가고, 이동 명령은 REJECTED(BATTERY_LOW)로 거부됩니다 |
SPOT | 운영자 호출. 충전 전까지 안 풀립니다 |
| 30% 미만 | 전원 인가(power_on)가 거부됩니다 |
로봇 펌웨어 | 우리가 못 바꿉니다. 위 두 줄이 여기 닿지 않으려는 장치입니다 |
| 25% 미만 | 잠기지 않았더라도 commands_accepted 를 false 로 내립니다 (마지막 방어선) |
SPOT | 명령을 보내지 마세요 |
3분 동안 명령이 없으면 SPOT 이 스스로 앉습니다. 다음 명령에 자동으로 다시 일어섭니다.
서 있는 것만으로 배터리가 빠르게 닳기 때문입니다(실측: 세션 소비의 대부분이 기립 유지). 서버가 따로 할 일은 없습니다.
state 토픽에 connection:"offline" 이 뜹니다. SPOT 이 발행하는 게 아니라
브로커가 대신 발행합니다(MQTT LWT).
브로커가 알아채는 데 최대 60초 걸리므로, timestamp 를 같이 보고 오래된 값인지
판단해주세요.
| 항목 | 값 | 이유 |
|---|---|---|
| 명령 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
|
⬜ 구현됨, 실물 보정 미검증 |
- Google ADK 검토 (프레임워크·모델 비교)
- 페르소나 + 상황 카드 모델 (2차 시나리오 구동 방식)
- Mac Studio 로컬 LLM + Claude Opus 하이브리드 라우팅 전략
- 개발 일정·할 일 점검표 (단계별 진행 점검)
- 로봇 Adapter 개발 온보딩 (HA·MQTT 계정 등록, 토픽 적용, 테스트 송수신, 상태 확인)
- LG ThinQ 가전 연동 (HA 통합 · REST 데이터 취득 · 제어)
- 장소 도착 자동화 (태스크/이벤트/장소 규칙 → SPOT·로봇팔·반려로봇·Hub 자동 명령)
- 엔딩 화면 데이터 출처 점검 (차량 3필드 · 세션 종료 엔딩 5화면)
{ "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": { /* 아래 절들 */ } }