-
Notifications
You must be signed in to change notification settings - Fork 0
ROBOT_ADAPTER_ONBOARDING
mcp-robot-adapter개발에 착수하는 개발자가 MQTT broker(Mosquitto) 사용자 등록 → Home Assistant 연동 → 메시지 발행/구독까지 한 번에 따라갈 수 있도록 기존 설계 문서의 관련 내용을 모은 실무 가이드다. 본 문서는 요약/안내용이며, 충돌 시 원본 설계 문서가 우선한다 — 토픽·페이로드는 MQTT_SCHEMA.md, 서비스 책임·ACL은 SERVICES.md, 시크릿 절차는 DEPLOYMENT.md.
orchestrator ──(MCP tool call, 주 경로)──▶ mcp-robot-adapter ──(bosdyn-client)──▶ SPOT
home-assistant ─(MQTT robot/+/command, 폴백 경로)─▶ mcp-robot-adapter
mcp-robot-adapter ─(MQTT robot/+/state·ack·fault)─▶ mosquitto ─▶ HA·orchestrator 구독
-
adapter ↔ HA 직접 연결은 없다. HA는 MQTT integration으로
robot/+/state등을 구독해sensor.spot_01_battery같은 엔티티로 흡수한다. adapter가 HA REST API를 호출하거나HA_TOKEN을 가질 필요가 없다. - 명령 주 경로는 MCP tool call(MQTT 미경유), MQTT
robot/{robot_id}/command는 HA 폴백 경로 전용이다. 두 entry point는 adapter 내부에서 동일한 priority queue + safety interlock으로 수렴한다. 상세: MQTT_SCHEMA.md §3.4.
⚠️ adapter 개발 중에도 orchestrator가robot/+/command를 발행하도록 만들지 않는다 — ACL에서 orchestrator에게 이 토픽 write 권한 자체가 없다.
Mosquitto는 allow_anonymous false + password_file + acl_file 구성이다 (SERVICES.md §1). adapter는 robot-adapter 사용자로 접속한다.
# 최초 1회 — passwd 파일 생성과 함께 (기존 파일 있으면 -c 빼고 실행)
mosquitto_passwd -c ./mosquitto/config/passwd robot-adapter # 대화형으로 비밀번호 입력
# 자동화 스크립트에서 쓸 때
mosquitto_passwd -b ./mosquitto/config/passwd robot-adapter '<password>'
# 파일 권한 — mosquitto 컨테이너 uid는 1883
chown 1883:1883 ./mosquitto/config/passwd && chmod 0600 ./mosquitto/config/passwd
# 변경 반영 (재시작 없이 reload)
docker kill -s HUP mosquitto- 비밀번호 원문은
deploy/.env.secret의MOSQUITTO_PWD_ROBOT_ADAPTER에 기입한다 (gitignore,chmod 600). adapter 컨테이너는env_file로 이 값을 받는다. - 회전 주기는 분기 1회 — 절차는 DEPLOYMENT.md §9.2 참고.
mosquitto/config/acl에 아래 블록이 이미 정의되어 있다 (SERVICES.md §1 ACL 예시). 새 토픽이 필요하면 MQTT_SCHEMA.md에 먼저 추가한 뒤 ACL을 갱신한다.
user robot-adapter
topic write robot/+/state
topic write robot/+/ack
topic write robot/+/fault
topic read robot/+/command # HA 폴백 경로 수신
topic read safety/estop
topic read safety/proximity
topic write safety/estop # 자체 감지 시 발행
topic read vision/position/update # proximity guard
topic write system/heartbeat/robot-adapter
원칙은 최소 권한 — adapter는 robot 도메인 + safety + 자기 heartbeat만 만진다. 권한 밖 publish는 broker가 조용히 drop하므로, 개발 중 "메시지가 안 보인다" 증상의 1순위 점검 항목이 ACL이다.
| 항목 | 값 |
|---|---|
| host / port |
mosquitto : 1883 (같은 compose network edge) |
| username | robot-adapter |
| password | env MOSQUITTO_PWD_ROBOT_ADAPTER
|
| TLS | 단일 사이트 LAN 내부에선 미사용 (외부 노출 시 8883 + cert) |
| client_id |
mcp-robot-adapter 고정 권장 |
adapter 개발자가 HA에서 해야 할 일은 두 가지뿐이다.
HA 자체도 Mosquitto의 한 클라이언트다. homeassistant 사용자를 §1.1과 같은 방법으로 passwd에 등록하고, ha-config/configuration.yaml에서 참조한다:
mqtt:
broker: mosquitto
username: !secret mqtt_user # = homeassistant
password: !secret mqtt_password
discovery: true
discovery_prefix: homeassistanthomeassistant 사용자의 ACL(read robot/+/state·robot/+/ack·robot/+/fault, write robot/+/command 등)도 SERVICES.md §1에 이미 정의되어 있다.
adapter가 기동 시 discovery config를 발행하면 HA가 엔티티를 자동 생성한다 (OSS_INTEGRATION.md §3.2):
Topic: homeassistant/sensor/spot_01_battery/config (retain=true)
Payload: {
"name": "SPOT-01 Battery",
"state_topic": "robot/spot-01/state",
"value_template": "{{ value_json.data.battery_pct }}",
"unit_of_measurement": "%",
"device": { "identifiers": ["spot-01"], "name": "SPOT 01" }
}
결과 엔티티: sensor.spot_01_battery, sensor.spot_01_pose, binary_sensor.spot_01_online. 또는 discovery 대신 ha-config/packages/robots.yaml에 수동 정의해도 된다 — 어느 쪽이든 상태의 단일 소스는 robot/{robot_id}/state 토픽이다.
참고: HA long-lived access token(
HA_TOKEN)은 orchestrator 전용이다 (HA REST/WS 호출용). adapter에는 불필요하므로 발급받지 않는다. 발급 절차가 필요하면 DEPLOYMENT.md §9.2.
자체 발행하는 모든 메시지는 CloudEvents 1.0 envelope으로 감싼다 (MQTT_SCHEMA.md §1). 토픽은 / 구분, envelope type은 . 구분이다 (robot/spot-01/state ↔ robot.state).
{
"specversion": "1.0",
"type": "robot.state",
"source": "/services/mcp-robot-adapter",
"id": "01HW9XJ1A2B3C4D5E6F7G8H9JK",
"time": "2026-05-04T10:00:00.123Z",
"datacontenttype": "application/json",
"subject": "robot/spot-01",
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
"data": { "robot_id": "spot-01", "battery_pct": 87, "connection": "online", "...": "..." }
}-
id는 ULID — 구독자 멱등 처리(dedup) 키. -
time은now()가 아닌 이벤트 원천 시각 (텔레메트리 캡처 시각). - envelope 생성/파싱은
packages/topics/의 wrapper를 사용한다 — 토픽 문자열·스키마를 서비스 코드에 inline 재정의하지 않는다.
| 토픽 | type | QoS | retain | 비고 |
|---|---|---|---|---|
robot/{robot_id}/state |
robot.state |
0 | true | 1Hz 텔레메트리. pose(world 좌표)·battery·current_action·faults·connection |
robot/{robot_id}/ack |
robot.ack |
1 | false | 같은 command_id로 accepted → started → completed/failed/rejected 순차 발행 |
robot/{robot_id}/fault |
robot.fault |
1 | true | 해제 시 동일 토픽에 active: false를 retain false로 발행 |
safety/estop |
safety.estop |
2 | true | 자체 감지 시에만 |
system/heartbeat/robot-adapter |
system.heartbeat |
0 | true | 라이브니스 |
필드 정의 전문은 MQTT_SCHEMA.md §3.4 참조. robot_id는 string + config/robots.yaml 레지스트리 런타임 검증 — literal union("spot-01" | ...)으로 타입을 좁히지 않는다.
mqtt.subscribe("robot/+/command", qos=1) # HA 폴백 경로 명령
mqtt.subscribe("safety/estop", qos=2) # 절대 누락 금지
mqtt.subscribe("safety/proximity", qos=1)
mqtt.subscribe("vision/position/update", qos=0) # proximity guard 입력 (5Hz)수신한 robot.command는 MCP 경로 명령과 같은 priority queue(safety > operator > llm) + safety interlock을 통과시킨다. expires_at이 지난 채 큐에 남은 명령은 폐기한다.
robot/+/state는 retain=true라 발행자가 비정상 종료하면 stale 상태가 영구 잔존한다. connect 전에 LWT 등록이 필수다 (MQTT_SCHEMA.md §4.5).
# 예시 — services/mcp-robot-adapter/src/adapter/main.py (발췌 수준)
import json, os
import paho.mqtt.client as mqtt
from topics.envelope import wrap # packages/topics — inline 재정의 금지
client = mqtt.Client(client_id="mcp-robot-adapter")
client.username_pw_set("robot-adapter", os.environ["MOSQUITTO_PWD_ROBOT_ADAPTER"])
# 1) LWT 등록 (connect 전) — 비정상 종료 시 connection: offline 으로 덮어씀
will = wrap("robot.state", {"robot_id": "spot-01", "connection": "offline",
"timestamp": now_rfc3339()},
source="/services/mcp-robot-adapter", subject="robot/spot-01")
client.will_set("robot/spot-01/state", json.dumps(will), qos=1, retain=True)
# 2) 연결 + 구독
client.connect("mosquitto", 1883)
client.subscribe("robot/+/command", qos=1)
client.subscribe("safety/estop", qos=2)
# 3) 1Hz 텔레메트리 발행
state = wrap("robot.state", telemetry.snapshot(),
source="/services/mcp-robot-adapter", subject="robot/spot-01")
client.publish("robot/spot-01/state", json.dumps(state), qos=0, retain=True)
# 4) graceful shutdown — 정상 종료 시에도 offline state를 직접 publish 후 disconnect
def shutdown_handler(*_):
client.publish("robot/spot-01/state", json.dumps(will), qos=1, retain=True)
client.disconnect()토픽·스키마의 단일 소스는 두 곳이다: 문서는 MQTT_SCHEMA.md, 코드는 packages/topics/ (SERVICES.md §9.2). 어느 쪽도 거치지 않고 서비스 코드에 토픽 문자열을 inline으로 박는 것이 가장 흔한 결함이다.
| 단계 | 작업 | 위치 |
|---|---|---|
| 1 |
토픽 카탈로그에 정의 — 토픽명, type, 발행자/구독자, QoS, retain, payload 인터페이스, JSON 예시 |
MQTT_SCHEMA.md §3 + §4 발행자/구독자 매트릭스 |
| 2 | 스키마 코드 반영 — 상수·Pydantic 모델 추가 후 codegen으로 TS(Zod)↔Python 동기화 | packages/topics/ |
| 3 |
ACL 갱신 — 발행자에 topic write, 구독자에 topic read 추가 |
mosquitto/config/acl |
| 4 | broker 적용 — 재시작 없이 reload | docker kill -s HUP mosquitto |
| 5 | retain=true 토픽이면 LWT 정책 추가 | MQTT_SCHEMA.md §4.5 표 |
⚠️ ACL 갱신을 빠뜨리면 broker가 publish를 에러 없이 조용히 drop한다. "발행했는데 구독자에 안 보임" → 가장 먼저 ACL을 의심할 것.
MQTT_SCHEMA.md §7 그대로:
- 새 필드는 optional로 추가 — 기존 필드 삭제·타입 변경 금지 (backward 호환).
-
breaking change는
type버전화:robot.state.v2처럼.v2suffix → v1·v2 병행 발행 → 모든 구독자 마이그레이션 후 v1 폐기. - QoS·retain 변경도 스키마 변경으로 취급 — MQTT_SCHEMA.md §2 표를 함께 갱신.
전부 broker 컨테이너에 들어 있는 mosquitto_sub / mosquitto_pub으로 가능하다 — 별도 도구 설치 불필요. 인증이 켜져 있으므로 -u/-P는 항상 필요하다.
# 터미널 1: 모든 토픽 라이브 테일 (토픽명 -v 표시)
docker exec -it mosquitto mosquitto_sub -h localhost -u robot-adapter -P '<pwd>' -t '#' -v
# robot 도메인만
docker exec -it mosquitto mosquitto_sub -h localhost -u robot-adapter -P '<pwd>' \
-t 'robot/#' -v
# retained 메시지만 확인 (현재 로봇 상태 스냅샷)
docker exec -it mosquitto mosquitto_sub -h localhost -u robot-adapter -P '<pwd>' \
-t 'robot/+/state' -v --retained-only
# 특정 trace_id 추적
docker exec -it mosquitto mosquitto_sub -h localhost -u robot-adapter -P '<pwd>' -t '#' -v \
| grep '0af7651916cd43dd8448eb211c80319c'HA 폴백 경로를 흉내내 robot/{robot_id}/command를 발행한다. homeassistant 계정을 사용해야 한다 — robot-adapter는 이 토픽의 write 권한이 없다 (ACL이 의도대로 동작하는지의 검증이기도 하다).
docker exec -it mosquitto mosquitto_pub -h localhost -u homeassistant -P '<pwd>' \
-t 'robot/spot-01/command' -q 1 -m '{
"specversion": "1.0", "type": "robot.command",
"source": "/services/home-assistant", "id": "01HW9X...",
"time": "2026-05-04T10:00:00Z", "datacontenttype": "application/json",
"data": {
"command_id": "01HW9X...", "robot_id": "spot-01",
"capability": "look_at", "params": { "target_session_id": "01HW8M..." },
"priority": "operator",
"source": { "service": "home-assistant", "trace_id": "manual-test" },
"expires_at": "2026-05-04T10:00:05Z"
}
}'기대 결과 — 터미널 1(robot/# 구독)에 adapter가 발행한 ack 시퀀스가 보인다:
robot/spot-01/ack {... "data": {"command_id": "01HW9X...", "status": "accepted", ...}}
robot/spot-01/ack {... "status": "started" ...}
robot/spot-01/ack {... "status": "completed" ...}
rejected가 오면 reason 필드에 safety interlock 거부 사유(estop/fence/proximity/watchdog)가 들어 있다. ACL 위반이면 아무것도 안 온다 (broker silent drop).
테스트 중 만든 retained 메시지는 null payload로 지운다:
docker exec mosquitto mosquitto_pub -h localhost -u robot-adapter -P '<pwd>' \
-t 'robot/spot-01/state' -r -n # -r -n: retain + null payload → 삭제HA 쪽에서도 같은 테스트를 할 수 있다 — 개발자 도구 → 액션에서 mqtt.publish 호출 (운영자 대시보드의 수동 명령 버튼이 쓰는 것과 같은 경로다).
adapter가 robot/{robot_id}/state를 발행하면 HA 엔티티(§2.2)가 갱신된다. 확인 방법은 세 가지.
HA 접속: 로컬 PC 브라우저에서 http://<로컬PC>:8123 (컨테이너 내부에서는 http://host.docker.internal:8123).
-
개발자 도구 → 상태:
sensor.spot_01_battery검색 → 현재 값·attributes·last_updated 확인. 발행 직후 값이 바뀌는지 본다. -
개발자 도구 → MQTT → "Listen to a topic":
robot/+/state입력 → HA가 실제 수신하는 raw payload를 그대로 본다. 엔티티는 안 바뀌는데 여기엔 보인다면value_template문제, 여기도 안 보이면 broker/ACL 문제로 구분할 수 있다. -
기록(History): 엔티티 클릭 → 그래프. recorder가 SQLite에 자동 기록한다 (
purge_keep_days: 3).
HA_TOKEN(long-lived access token, 발급 절차는 DEPLOYMENT.md §9.2)으로 조회한다:
curl -s -H "Authorization: Bearer $HA_TOKEN" \
http://host.docker.internal:8123/api/states/sensor.spot_01_battery | jq{
"entity_id": "sensor.spot_01_battery",
"state": "87",
"attributes": { "unit_of_measurement": "%", "friendly_name": "SPOT-01 Battery" },
"last_updated": "2026-05-04T10:00:01.456Z"
}orchestrator가 컨텍스트 조회에 쓰는 것과 동일한 경로이므로, 여기서 보이는 값이 곧 LLM이 보는 값이다.
- 터미널에서
mosquitto_sub -t 'robot/+/state'로 발행 자체 확인 (§5.1) - HA 개발자 도구 → MQTT listen으로 HA 수신 확인
- 개발자 도구 → 상태에서 엔티티 값 갱신 확인
-
curl /api/states/...로 REST 조회 확인 — 여기까지 통과하면 orchestrator·운영자 대시보드 모두에서 보인다
흔한 함정: discovery config나
packages/robots.yaml의value_template이 envelope을 고려하지 않은 경우. payload는 CloudEvents envelope으로 감싸여 있으므로{{ value_json.battery_pct }}가 아니라 **{{ value_json.data.battery_pct }}**다.
-
mosquitto_passwd로robot-adapter등록,passwd권한1883:1883 / 0600, broker reload -
acl에 robot-adapter 블록 확인 (신규 토픽은 §4 절차: MQTT_SCHEMA.md →packages/topics→ ACL → reload) -
deploy/.env.secret에MOSQUITTO_PWD_ROBOT_ADAPTER,SPOT_USERNAME/PASSWORD기입 - SPOT service account는 dedicated 계정(
zerone-spot-svc, Operator+EStop, Admin 금지) — DEPLOYMENT.md §9.2 - connect 전 LWT 등록 + graceful shutdown에서 offline state 발행
- 모든 발행 메시지 CloudEvents envelope +
packages/topics/wrapper 사용 -
robot/+/command수신분이 MCP 경로와 같은 큐·interlock으로 수렴하는지 확인 - HA에서
sensor.spot_01_battery등 엔티티가 보이는지 확인 —value_template은value_json.data.*(§6.3 E2E 한 바퀴) -
safety/estop수신 시 큐 fence + 운영자 reset까지 유지 (safety > operator > llm)
서비스 전체 책임·디렉터리 구조·테스트 전략은 SERVICES.md §6 참고.