Skip to content

ROBOT_ADAPTER_ONBOARDING

playground edited this page Jun 10, 2026 · 4 revisions

로봇 Adapter 개발 온보딩 — HA·MQTT 계정 등록과 메시지 송수신

mcp-robot-adapter 개발에 착수하는 개발자가 MQTT broker(Mosquitto) 사용자 등록 → Home Assistant 연동 → 메시지 발행/구독까지 한 번에 따라갈 수 있도록 기존 설계 문서의 관련 내용을 모은 실무 가이드다. 본 문서는 요약/안내용이며, 충돌 시 원본 설계 문서가 우선한다 — 토픽·페이로드는 MQTT_SCHEMA.md, 서비스 책임·ACL은 SERVICES.md, 시크릿 절차는 DEPLOYMENT.md.

0. 전체 그림 — adapter가 통신하는 상대

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 권한 자체가 없다.


1. MQTT 사용자 등록 (Mosquitto)

Mosquitto는 allow_anonymous false + password_file + acl_file 구성이다 (SERVICES.md §1). adapter는 robot-adapter 사용자로 접속한다.

1.1 계정 생성

# 최초 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.secretMOSQUITTO_PWD_ROBOT_ADAPTER에 기입한다 (gitignore, chmod 600). adapter 컨테이너는 env_file로 이 값을 받는다.
  • 회전 주기는 분기 1회 — 절차는 DEPLOYMENT.md §9.2 참고.

1.2 ACL 등록

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이다.

1.3 연결 정보

항목
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 고정 권장

2. Home Assistant 쪽 등록

adapter 개발자가 HA에서 해야 할 일은 두 가지뿐이다.

2.1 HA의 MQTT integration 계정 (1회, 인프라 작업)

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: homeassistant

homeassistant 사용자의 ACL(read robot/+/state·robot/+/ack·robot/+/fault, write robot/+/command 등)도 SERVICES.md §1에 이미 정의되어 있다.

2.2 로봇 엔티티 등록 — MQTT Discovery

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.


3. 메시지 보내는 방법

3.1 공통 규칙 — CloudEvents envelope

자체 발행하는 모든 메시지는 CloudEvents 1.0 envelope으로 감싼다 (MQTT_SCHEMA.md §1). 토픽은 / 구분, envelope type. 구분이다 (robot/spot-01/staterobot.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) 키.
  • timenow()가 아닌 이벤트 원천 시각 (텔레메트리 캡처 시각).
  • envelope 생성/파싱은 packages/topics/의 wrapper를 사용한다 — 토픽 문자열·스키마를 서비스 코드에 inline 재정의하지 않는다.

3.2 adapter가 발행하는 토픽

토픽 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_idaccepted → started → completed/failed/rejected 순차 발행
robot/{robot_id}/fault robot.fault 1 true 해제 시 동일 토픽에 active: falseretain false로 발행
safety/estop safety.estop 2 true 자체 감지 시에만
system/heartbeat/robot-adapter system.heartbeat 0 true 라이브니스

필드 정의 전문은 MQTT_SCHEMA.md §3.4 참조. robot_idstring + config/robots.yaml 레지스트리 런타임 검증 — literal union("spot-01" | ...)으로 타입을 좁히지 않는다.

3.3 adapter가 구독하는 토픽

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이 지난 채 큐에 남은 명령은 폐기한다.

3.4 연결 + LWT + 발행 — 최소 동작 예시

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()

4. 토픽을 수정·추가하고 적용하는 절차

토픽·스키마의 단일 소스는 두 곳이다: 문서는 MQTT_SCHEMA.md, 코드는 packages/topics/ (SERVICES.md §9.2). 어느 쪽도 거치지 않고 서비스 코드에 토픽 문자열을 inline으로 박는 것이 가장 흔한 결함이다.

4.1 변경 순서 (신규 토픽 / 필드 추가 공통)

단계 작업 위치
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을 의심할 것.

4.2 기존 payload 수정 시 — 스키마 진화 정책

MQTT_SCHEMA.md §7 그대로:

  • 새 필드는 optional로 추가 — 기존 필드 삭제·타입 변경 금지 (backward 호환).
  • breaking change는 type 버전화: robot.state.v2처럼 .v2 suffix → v1·v2 병행 발행 → 모든 구독자 마이그레이션 후 v1 폐기.
  • QoS·retain 변경도 스키마 변경으로 취급 — MQTT_SCHEMA.md §2 표를 함께 갱신.

5. 테스트 메시지 보내고 받는 방법

전부 broker 컨테이너에 들어 있는 mosquitto_sub / mosquitto_pub으로 가능하다 — 별도 도구 설치 불필요. 인증이 켜져 있으므로 -u/-P는 항상 필요하다.

5.1 받기 — 구독 터미널을 먼저 연다

# 터미널 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'

5.2 보내기 — 폴백 명령 수신 테스트

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).

5.3 정리 — stale retain 삭제

테스트 중 만든 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 호출 (운영자 대시보드의 수동 명령 버튼이 쓰는 것과 같은 경로다).


6. HA에서 업데이트한 상태값 확인하기

adapter가 robot/{robot_id}/state를 발행하면 HA 엔티티(§2.2)가 갱신된다. 확인 방법은 세 가지.

6.1 HA UI — 개발자 도구 (가장 빠름)

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).

6.2 HA REST API (스크립트/CI에서)

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이 보는 값이다.

6.3 검증 순서 정리 (E2E 한 바퀴)

  1. 터미널에서 mosquitto_sub -t 'robot/+/state'로 발행 자체 확인 (§5.1)
  2. HA 개발자 도구 → MQTT listen으로 HA 수신 확인
  3. 개발자 도구 → 상태에서 엔티티 값 갱신 확인
  4. curl /api/states/...로 REST 조회 확인 — 여기까지 통과하면 orchestrator·운영자 대시보드 모두에서 보인다

흔한 함정: discovery config나 packages/robots.yamlvalue_template이 envelope을 고려하지 않은 경우. payload는 CloudEvents envelope으로 감싸여 있으므로 {{ value_json.battery_pct }}가 아니라 **{{ value_json.data.battery_pct }}**다.


7. 착수 체크리스트

  1. mosquitto_passwdrobot-adapter 등록, passwd 권한 1883:1883 / 0600, broker reload
  2. acl에 robot-adapter 블록 확인 (신규 토픽은 §4 절차: MQTT_SCHEMA.md → packages/topics → ACL → reload)
  3. deploy/.env.secretMOSQUITTO_PWD_ROBOT_ADAPTER, SPOT_USERNAME/PASSWORD 기입
  4. SPOT service account는 dedicated 계정(zerone-spot-svc, Operator+EStop, Admin 금지) — DEPLOYMENT.md §9.2
  5. connect 전 LWT 등록 + graceful shutdown에서 offline state 발행
  6. 모든 발행 메시지 CloudEvents envelope + packages/topics/ wrapper 사용
  7. robot/+/command 수신분이 MCP 경로와 같은 큐·interlock으로 수렴하는지 확인
  8. HA에서 sensor.spot_01_battery 등 엔티티가 보이는지 확인 — value_templatevalue_json.data.* (§6.3 E2E 한 바퀴)
  9. safety/estop 수신 시 큐 fence + 운영자 reset까지 유지 (safety > operator > llm)

서비스 전체 책임·디렉터리 구조·테스트 전략은 SERVICES.md §6 참고.

Clone this wiki locally