ROBOT_ADAPTER_ONBOARDING - zero1ne-26/doc GitHub Wiki
mcp-robot-adapter๊ฐ๋ฐ์ ์ฐฉ์ํ๋ ๊ฐ๋ฐ์๊ฐ MQTT ๊ธฐ์ด ๊ฐ๋ โ broker(Mosquitto) ์ฌ์ฉ์ ๋ฑ๋ก โ Home Assistant ์ฐ๋ โ ๋ฉ์์ง ๋ฐํ/๊ตฌ๋ ๊น์ง ํ ๋ฒ์ ๋ฐ๋ผ๊ฐ ์ ์๋๋ก ๊ธฐ์กด ์ค๊ณ ๋ฌธ์์ ๊ด๋ จ ๋ด์ฉ์ ๋ชจ์ ์ค๋ฌด ๊ฐ์ด๋๋ค. MQTT๋ฅผ ์ฒ์ ์ ํ๋ ์ฌ๋์ ๊ธฐ์ค์ผ๋ก ์ฐ์๋ค โ ์ด๋ฏธ ์ต์ํ๋ค๋ฉด ยง1์ ๊ฑด๋๋ฐ์ด๋ ๋๋ค. ๋ณธ ๋ฌธ์๋ ์์ฝ/์๋ด์ฉ์ด๋ฉฐ, ์ถฉ๋ ์ ์๋ณธ ์ค๊ณ ๋ฌธ์๊ฐ ์ฐ์ ํ๋ค โ ํ ํฝยทํ์ด๋ก๋๋ 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๊ฐ์ ์ํฐํฐ๋ก ํก์ํ๋ค (์ํฐํฐ๊ฐ ๋ฌด์์ธ์ง๋ ยง3.0์์ ์ค๋ช ). 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 ๊ถํ ์์ฒด๊ฐ ์๋ค.
MQTT(Message Queuing Telemetry Transport)๋ ๊ฐ๋ณ๊ณ ๋จ์ํ ๋ฉ์์ง ์ ๋ฌ ํ๋กํ ์ฝ์ด๋ค. HTTP์ฒ๋ผ "์์ฒญํ๊ณ ์๋ต๋ฐ๋" ๋ฐฉ์์ด ์๋๋ผ, ์ฐํธํจ ์์คํ ์ ๊ฐ๊น๋ค:
๋ฐํ์(publisher) โโ๋ฉ์์งโโโถ [ broker (์ฐ์ฒด๊ตญ) ] โโ๋ฉ์์งโโโถ ๊ตฌ๋
์(subscriber)
mosquitto
| ์ฉ์ด | ๋ป |
|---|---|
| broker | ๋ชจ๋ ๋ฉ์์ง๊ฐ ๊ฑฐ์ณ ๊ฐ๋ ์ค์ ์ค๊ณ ์๋ฒ. ์ด ํ๋ก์ ํธ์์๋ mosquitto๋ผ๋ ์คํ์์ค broker๋ฅผ ์ด๋ค. ๋ฐํ์์ ๊ตฌ๋
์๋ ์๋ก๋ฅผ ๋ชจ๋ฅด๊ณ , ์ค์ง broker๋ง ์๋ค. |
| client | broker์ ์ ์ํ๋ ๋ชจ๋ ํ๋ก๊ทธ๋จ. adapter๋, HA๋, orchestrator๋ ์ ๋ถ client๋ค. ๊ฐ client๋ client_id(์ ์ ์๋ณ ์ด๋ฆ)์ username/password๋ฅผ ๊ฐ์ง๋ค. |
| publish (๋ฐํ) | client๊ฐ broker์๊ฒ "์ด ๋ฉ์์ง๋ฅผ ์ด topic์ผ๋ก ๋ณด๋ด์ค"๋ผ๊ณ ๊ฑด๋ค๋ ํ์. ๋ฐ๋ ์ชฝ ์ฃผ์๋ฅผ ๋ชฐ๋ผ๋ ๋๋ค โ topic ์ด๋ฆ๋ง ์๋ฉด ๋๋ค. |
| subscribe (๊ตฌ๋ ) | client๊ฐ broker์๊ฒ "์ด topic์ผ๋ก ์ค๋ ๋ฉ์์ง๋ฅผ ๋ํํ ์ ๋ฌํด์ค"๋ผ๊ณ ๋ฑ๋กํ๋ ํ์. ์ดํ ํด๋น topic์ ๋ฐํ๋๋ ๋ฉ์์ง๊ฐ ์๋์ผ๋ก ๋์ฐฉํ๋ค. |
| topic | ๋ฉ์์ง์ ์ฃผ์/์ฑ๋ ์ด๋ฆ. robot/spot-01/state์ฒ๋ผ /๋ก ๊ณ์ธต์ ๋๋ ๋ฌธ์์ด์ด๋ค. |
| payload | ๋ฉ์์ง์ ๋ณธ๋ฌธ. MQTT ์ ์ฅ์์ ๊ทธ๋ฅ ๋ฐ์ดํธ ๋ฉ์ด๋ฆฌ์ด๊ณ , ์ด ํ๋ก์ ํธ์์๋ ํญ์ JSON ๋ฌธ์์ด์ ๋ฃ๋๋ค. |
์ด ๊ตฌ์กฐ์ ์ฅ์ : ๋ฐํ์๋ "๋๊ฐ ๋ฐ๋์ง" ์ ๊ฒฝ ์ฐ์ง ์๋๋ค. adapter๊ฐ robot/spot-01/state์ ์ํ๋ฅผ ๋ฐํํ๋ฉด, HAยทorchestratorยท๋๋ฒ๊น
ํฐ๋ฏธ๋ ๋ฑ ๊ตฌ๋
์ค์ธ ๋ชจ๋๊ฐ ๋์์ ๋ฐ๋๋ค. ๊ตฌ๋
์๊ฐ ๋์ด๋๋ ๋ฐํ ์ฝ๋๋ ํ ์ค๋ ๋ฐ๋์ง ์๋๋ค.
์ฒ์ ์ ํ๋ฉด "ํ ํฝ์ ์ด๋์ ๋ฑ๋กํ์ง?"๋ถํฐ ์ฐพ๊ฒ ๋๋๋ฐ, MQTT ํ๋กํ ์ฝ ์์ฒด์๋ topic์ ๋ฏธ๋ฆฌ ๋ฑ๋กยท์์ฑํ๋ ์ ์ฐจ๊ฐ ์๋ค. topic์ ๋๊ตฐ๊ฐ ๊ทธ ์ด๋ฆ์ผ๋ก publish ํ๋ ์๊ฐ ์กด์ฌํ๊ณ , ์๋ฌด๋ ์ ์ฐ๋ฉด ๊ทธ๋ฅ ์๋ ๊ฒ์ด๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์ฒ๋ผ ๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋๋ ๊ฐ๋ ์ด ์๋๋ค.
๋ค๋ง ์ด ํ๋ก์ ํธ์์ "ํ ํฝ ๋ฑ๋ก"์ด๋ผ ๋ถ๋ฅด๋ ๊ฒ์ ๋ค์ ๋ ๊ฐ์ง๋ฅผ ๋ปํ๋ค:
- ๋ฌธ์ ๋ฑ๋ก โ MQTT_SCHEMA.md์ ํ ํฝ ์นดํ๋ก๊ทธ์ ํ ํฝ๋ช ยทpayload ํ์ยทQoSยทretain์ ์ ์ํ๋ค. ๋ชจ๋ ์๋น์ค๊ฐ ์ด ๋ฌธ์๋ฅผ ๋ณด๊ณ ๋ง์ถ๋ค (๋จ์ผ ์์ค).
- ๊ถํ ๋ฑ๋ก โ broker์ ACL(Access Control List, ์ ๊ทผ ์ ์ด ๋ชฉ๋ก) ํ์ผ์ "์ด๋ ์ฌ์ฉ์๊ฐ ์ด๋ topic์ ์ฝ๊ธฐ/์ฐ๊ธฐ ๊ฐ๋ฅํ์ง"๋ฅผ ์ถ๊ฐํ๋ค. ์ด ํ๋ก์ ํธ์ mosquitto๋ ACL์ ์๋ publish๋ฅผ ์๋ฌ๋ ์์ด ์กฐ์ฉํ ๋ฒ๋ฆฐ๋ค(silent drop). ๊ทธ๋์ "๋ถ๋ช ๋ณด๋๋๋ฐ ์ ๋ณด์ธ๋ค"์ 1์์ ์์ธ์ด ACL ๋๋ฝ์ด๋ค.
๊ตฌ์ฒด์ ์ ์ฐจ๋ ยง5์์ ๋ค๋ฃฌ๋ค.
topic ์์ผ๋์นด๋ โ ๊ตฌ๋ ํ ๋๋ง ์ธ ์ ์๋ ํน์ ๋ฌธ์ ๋ ๊ฐ์ง:
| ๋ฌธ์ | ์๋ฏธ | ์ |
|---|---|---|
+ |
ํ ๋จ๊ณ๋ฅผ ์๋ฌด ๊ฐ์ด๋ ๋งค์นญ |
robot/+/state โ robot/spot-01/state, robot/kaist-01/state ๋ชจ๋ ์์ |
# |
๊ทธ ์ดํ ์ ๋ถ ๋งค์นญ (๋งจ ๋์๋ง) |
robot/# โ robot์ผ๋ก ์์ํ๋ ๋ชจ๋ topic ์์ |
๋ฐํํ ๋๋ ์์ผ๋์นด๋๋ฅผ ์ธ ์ ์๋ค โ ํญ์ ๊ตฌ์ฒด์ ์ธ topic ์ด๋ฆ์ผ๋ก ๋ฐํํ๋ค.
publish ํ ๋๋ง๋ค topicยทpayload ์ธ์ ๋ค์ ์ต์ ์ ํจ๊ป ์ง์ ํ๋ค. ์ด๋ค ๊ฐ์ ์ธ์ง๋ ํ ํฝ๋ง๋ค MQTT_SCHEMA.md ยง2์ ์ด๋ฏธ ์ ํด์ ธ ์๋ค โ ๊ฐ๋ฐ์๊ฐ ์์๋ก ๊ณ ๋ฅด์ง ์๋๋ค.
| QoS | ๋ณด์ฅ | ๋น์ | ์ด ํ๋ก์ ํธ์ ์ฉ๋ |
|---|---|---|---|
| 0 | ์ต๋ 1ํ โ ๋ณด๋ด๊ณ ๋, ์ ์ค ๊ฐ๋ฅ | ์ผ๋ฐ ์ฐํธ |
robot/+/state (1์ด๋ง๋ค ๋ค์ ๋ณด๋ด๋ ํ ๋ฒ ๋น ์ ธ๋ ๋ฌด๋ฐฉ) |
| 1 | ์ต์ 1ํ โ ๋์ฐฉ ํ์ธ, ์ค๋ณต ๊ฐ๋ฅ | ๋ฑ๊ธฐ ์ฐํธ |
robot/+/commandยทackยทfault (๋์น๋ฉด ์ ๋๋ ๊ฒ) |
| 2 | ์ ํํ 1ํ โ ๊ฐ์ฅ ๋ฌด๊ฒ๋ค | ๋ด์ฉ์ฆ๋ช |
safety/estop ๋จ ํ๋ (์ ๋ ์ ์คยท์ค๋ณต ๊ธ์ง) |
QoS 1์ "์ค๋ณต ๋์ฐฉ ๊ฐ๋ฅ"์ด๋ฏ๋ก ๊ตฌ๋
์๋ envelope์ id(ยง4.4)๋ก ๊ฐ์ ๋ฉ์์ง๋ฅผ ๋ ๋ฒ ์ฒ๋ฆฌํ์ง ์๊ฒ ํ๋ค โ ์ด๊ฒ์ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ(idempotent) ๋ผ ํ๋ค.
๋ณดํต ๋ฉ์์ง๋ ๊ทธ ์๊ฐ์ ๊ตฌ๋
์์๊ฒ๋ง ์ ๋ฌ๋๊ณ ์ฌ๋ผ์ง๋ค. retain=true๋ก ๋ฐํํ๋ฉด broker๊ฐ ๊ทธ topic์ ๋ง์ง๋ง ๋ฉ์์ง 1๊ฐ๋ฅผ ๋ณด๊ดํด ๋์๋ค๊ฐ, ๋์ค์ ๊ตฌ๋
์ ์์ํ๋ client์๊ฒ ์ฆ์ ๊ฑด๋ค์ค๋ค.
- ์: HA๊ฐ ์ฌ์์๋ผ๋
robot/spot-01/state๋ฅผ ๊ตฌ๋ ํ๋ ์๊ฐ ๋ง์ง๋ง ๋ก๋ด ์ํ๋ฅผ ๋ฐ๋ก ๋ฐ๋๋ค โ ๋ค์ 1Hz ๋ฐํ์ ๊ธฐ๋ค๋ฆด ํ์๊ฐ ์๋ค. - ๋ถ์์ฉ: ๋ฐํ์๊ฐ ์ฃฝ์ด๋ ๋ง์ง๋ง ๋ฉ์์ง๊ฐ ์์ํ ๋จ๋๋ค(stale). ๊ทธ๋์ โ LWT๊ฐ ํ์ํ๋ค.
client๊ฐ broker์ ์ ์ํ ๋ ๋ฏธ๋ฆฌ ๋งก๊ฒจ๋๋ ๋ฉ์์ง. client๊ฐ ๋น์ ์ ์ข ๋ฃ(๋คํธ์ํฌ ๋จ์ , ํฌ๋์)๋๋ฉด broker๊ฐ ๋์ ์ด ๋ฉ์์ง๋ฅผ ๋ฐํํด ์ค๋ค.
- adapter๋
connection: "offline"์ด ๋ด๊ธด state ๋ฉ์์ง๋ฅผ ์ ์ธ์ฅ์ผ๋ก ๋งก๊ธด๋ค โ adapter๊ฐ ์ฃฝ์ผ๋ฉด broker๊ฐ ์๋์ผ๋ก "๋ก๋ด ์คํ๋ผ์ธ"์ ๋ฐํ โ HA ๋์๋ณด๋์ ์ฆ์ ๋ฐ์๋๊ณ , retain ๋ stale "online" ์ํ๊ฐ ๋จ๋ ๋ฌธ์ ๋ ํด๊ฒฐ๋๋ค. - connect ์ ์ ๋ฑ๋กํด์ผ ํ๋ค (ํ๋กํ ์ฝ์ CONNECT ํจํท์ ์ค๋ ค ๊ฐ๊ธฐ ๋๋ฌธ).
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์์ ํด์ผ ํ ์ผ์ ๋ ๊ฐ์ง๋ฟ์ด๋ค. ๊ทธ ์ ์, ์ด ์ ์ ์ฒด์์ ๊ณ์ ๋์ค๋ "์ํฐํฐ"๊ฐ ๋ฌด์์ธ์ง๋ถํฐ ์ง๊ณ ๊ฐ๋ค.
Home Assistant๋ ์๋ ์ค๋งํธํ ํ๋ธ๋ค โ ์ง ์์ ์จ๊ฐ ๊ธฐ๊ธฐ(์ ๊ตฌยท์จ๋๊ณยท์ค์์นยท์นด๋ฉ๋ผ)๋ฅผ ํ ํ๋ฉด์ ๋ชจ์ ๋ณด๊ณ , "ํด ์ง๋ฉด ์ ๊ตฌ ์ผ๊ธฐ" ๊ฐ์ ์๋ํ๋ฅผ ๋๋ฆฌ๋ ์คํ์์ค. HA๋ ์ด ์ธ์์ ๋ชจ๋ ๊ฒ์ ์ํฐํฐ(entity) ๋ผ๋ ๋จ์๋ก ํํํ๋ค.
์ํฐํฐ = "์ด๋ฆ์ด ๋ถ์ ์ํ๊ฐ ํ๋" ๋ค.
| ์ค๋ฌผ | ์ํฐํฐ ID | state (ํ์ฌ ๊ฐ) |
|---|---|---|
| ๊ฑฐ์ค ์ ๊ตฌ | light.living_room |
on |
| ๊ฑฐ์ค ์จ๋๊ณ | sensor.living_room_temp |
23.5 |
| ์ฐ๋ฆฌ ๋ก๋ด์ ๋ฐฐํฐ๋ฆฌ | sensor.spot_01_battery |
87 |
| ์ฐ๋ฆฌ ๋ก๋ด์ ์ฐ๊ฒฐ ์ฌ๋ถ | binary_sensor.spot_01_online |
on |
- ID๋
<domain>.<object_id>๊ตฌ์กฐ๋ค. domain์ด ๊ฐ์ ์ข ๋ฅ๋ฅผ ์ ํ๋ค โsensor๋ ์์ ๊ฐ,binary_sensor๋ on/off ๋ ๊ฐ์ง๋ง. - ์ํฐํฐ๋ state ์ธ์ attributes(๋ถ๊ฐ์ ๋ณด: ๋จ์, friendly_name ๋ฑ)์ last_updated(๋ง์ง๋ง ๊ฐฑ์ ์๊ฐ)๋ฅผ ํจ๊ป ๊ฐ์ง๋ค โ ยง7.2์ REST ์๋ต์ด ์ ํํ ์ด ๊ตฌ์กฐ๋ค.
์ ํ์ํ๊ฐ โ MQTT ๋ฉ์์ง์์ ๊ฒฐ์ ์ ์ฐจ์ด:
| MQTT ๋ฉ์์ง | HA ์ํฐํฐ | |
|---|---|---|
| ์ฑ๊ฒฉ | ํ๋ฌ๊ฐ๋ ์ด๋ฒคํธ โ "๋ฐฉ๊ธ ์ด๋ฐ ์ผ์ด ์์๋ค" | ๋ณด๊ด๋๋ ์ํ โ "์ง๊ธ ๊ฐ์ด ์ด๊ฒ์ด๋ค" |
| ์ฝ๋ ๋ฐฉ๋ฒ | ๋ฐํ๋๋ ์๊ฐ ๊ตฌ๋ ์ค์ด์ด์ผ ๋ฐ๋๋ค | ์ธ์ ๋ ํ์ฌ ๊ฐ์ ์กฐํํ ์ ์๋ค |
| ๊ณผ๊ฑฐ ๊ธฐ๋ก | ๋จ์ง ์๋๋ค (retain 1๊ฐ ์ ์ธ) | recorder๊ฐ SQLite์ ํ์คํ ๋ฆฌ ์๋ ๊ธฐ๋ก |
adapter๊ฐ 1Hz๋ก ๋ฐํํ๋ robot/spot-01/state๋ ๊ทธ ์์ฒด๋ก๋ ํ๋ฌ๊ฐ ๋ฒ๋ฆฌ๋ ์คํธ๋ฆผ์ด๋ค. HA๊ฐ ์ด๊ฒ์ ์ํฐํฐ๋ก ํก์ํ๋ ์๊ฐ, ์ถ๊ฐ ์ฝ๋ ์์ด ๋ค์์ ์ ๋ถ ์ป๋๋ค:
-
์ธ์ ๋ ์กฐํ โ orchestrator๊ฐ LLM ๊ฒฐ์ ์ง์ ์
GET /api/states/sensor.spot_01_battery๋ก ์ต์ ๊ฐ์ ์ฝ๋๋ค (ยง7.2) - ํ์คํ ๋ฆฌ โ recorder๊ฐ SQLite์ ์๋ ๊ธฐ๋ก โ ๋ฐฐํฐ๋ฆฌ ๊ทธ๋ํ๊ฐ ๊ณต์ง
- ๋์๋ณด๋ โ Lovelace ์นด๋์ ์ฌ๋ ค ์ด์์๊ฐ ์ค์๊ฐ์ผ๋ก ๋ณธ๋ค
-
์๋ํ ํธ๋ฆฌ๊ฑฐ/์กฐ๊ฑด โ ํด๋ฐฑ ์๋ํ๊ฐ
binary_sensor.orchestrator_alive๊ฐ์ ์ํฐํฐ๋ฅผ ์กฐ๊ฑด์ผ๋ก ๋์ํ๋ค (SERVICES.md ยง3)
์ด๊ฒ์ด ์ค๊ณ invariant "Home Assistant๊ฐ ์ํ ์ ์ฅ์๋ค" ์ ์ค์ฒด๋ค โ ๋ณ๋์ context serverยทRedisยทDB๋ฅผ ๋ง๋ค์ง ์๊ณ , ์ํฐํฐ + recorder๊ฐ ์์คํ ์ ์์ ๊ณ์ธต ์ญํ ์ ํ๋ค. adapter ๊ฐ๋ฐ์ ์ ์ฅ์์ ํ ๋ฌธ์ฅ์ผ๋ก ์ค์ด๋ฉด: state ๋ฉ์์ง๋ฅผ ๊ท๊ฒฉ๋๋ก ์ ๋ฐํํ๊ธฐ๋ง ํ๋ฉด, ์ ์ฅยท์กฐํยท์๊ฐํยท์๋ํ๋ HA๊ฐ ๋ค ํด์ค๋ค.
๊ทธ๋์ ยง3.2์ "์ํฐํฐ ๋ฑ๋ก"์ด ํ์ํ๋ค โ HA์๊ฒ "robot/spot-01/state topic์์ battery_pct ํ๋๋ฅผ ๋ฝ์ sensor.spot_01_battery๋ผ๋ ์ํฐํฐ๋ก ๊ด๋ฆฌํด๋ผ"๋ผ๊ณ ์๋ ค์ฃผ๋ ์์
์ด๋ค.
HA ์์ฒด๋ Mosquitto์ ํ ํด๋ผ์ด์ธํธ๋ค. homeassistant ์ฌ์ฉ์๋ฅผ ยง2.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). ์๋์ ๋์ ์๋ฆฌ๋ถํฐ ์์ ์ฝ๋๊น์ง ์ฐจ๋ก๋ก ์ค๋ช ํ๋ค.
์๋ HA์ ์ผ์๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด HA ์ชฝ YAML ํ์ผ์ ๊ณ ์น๊ณ HA๋ฅผ ์ฌ์์ํด์ผ ํ๋ค. MQTT Discovery๋ ์ด ์ค์ ์์ฒด๋ฅผ MQTT ๋ฉ์์ง๋ก ๋ณด๋ด๋ ๋ฐฉ์์ด๋ค:
- HA๋ MQTT integration์ ์ผ๋ ์๊ฐ(ยง3.1์
discovery: true)homeassistant/+/+/configํจํด์ ํญ์ ๊ตฌ๋ ํ๊ณ ์๋ค. - ์ด๋ค client๋ ์ด ํจํด์ ๋ง๋ topic์ผ๋ก "์ํฐํฐ ์ค์ JSON"์ ๋ฐํํ๋ฉด,
- HA๊ฐ ๊ทธ JSON์ ์ฝ๊ณ ์ํฐํฐ๋ฅผ ์ฆ์ ์์ฑํ๋ค โ HA ์ฌ์์๋, YAML ์์ ๋ ํ์ ์๋ค.
์ฆ "๋ก๋ด์ด ๋์ด๋๋ฉด HA ์ค์ ์ ๊ณ ์น๋ค"๊ฐ ์๋๋ผ, **"adapter๊ฐ ์๊ธฐ ๋ก๋ด์ ์ํฐํฐ๋ฅผ ์ค์ค๋ก ์ ๊ณ ํ๋ค"**๊ฐ ๋๋ค. config/robots.yaml์ ๋ก๋ด์ด ์ถ๊ฐ๋ผ๋ HA ์ชฝ ์์
์ด 0์ด ๋๋ ๊ฒ์ด ์ด ๋ฐฉ์์ ์ฐ๋ ์ด์ ๋ค.
homeassistant / <component> / <object_id> / config
โโ ยง3.1์ โโ ์ํฐํฐ ์ข
๋ฅ โโ ์ํฐํฐ ๊ณ ์ โโ ๊ณ ์ ์ ๋ฏธ์ฌ
discovery_prefix (sensor, ์ด๋ฆ
binary_sensor, ...)
| ๊ตฌ์ฑ ์์ | ์ค๋ช | ์ |
|---|---|---|
discovery_prefix |
ยง3.1 ์ค์ ๊ฐ. ๊ธฐ๋ณธ homeassistant
|
homeassistant |
component |
HA ์ํฐํฐ ๋๋ฉ์ธ โ sensor(์ซ์/๋ฌธ์ ๊ฐ), binary_sensor(ON/OFF), button, switch ๋ฑ |
sensor |
object_id |
์ด ์ํฐํฐ์ ๊ณ ์ ์ด๋ฆ (HA ๋ด๋ถ ์๋ณ์ฉ) | spot_01_battery |
๋ฐํ ์ retain=true๊ฐ ์ฌ์ค์ ํ์๋ค โ HA๊ฐ ์ฌ์์ํ๋ฉด ๊ตฌ๋
์ ๋ค์ ์์ํ๋๋ฐ, retain ๋์ด ์์ด์ผ ๊ทธ ์๊ฐ broker๊ฐ config๋ฅผ ๋ค์ ๊ฑด๋ค์ฃผ์ด ์ํฐํฐ๊ฐ ๋ณต์๋๋ค(ยง1.3 retain ์ฐธ๊ณ ). retain์ด ์์ผ๋ฉด HA ์ฌ์์ ๋๋ง๋ค ์ํฐํฐ๊ฐ ์ฌ๋ผ์ง๋ค.
์ํฐํฐ ์ญ์ ๋ ๊ฐ์ topic์ผ๋ก ํ๋ค โ ๋น(null) payload๋ฅผ retain์ผ๋ก ๋ฐํํ๋ฉด HA๊ฐ ํด๋น ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๋ค (ยง6.3์ retain ์ญ์ ์ ๊ฐ์ ๊ท์ฝ).
{
"name": "SPOT-01 Battery",
"unique_id": "zerone_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", "manufacturer": "Boston Dynamics" }
}| ํ๋ | ์ค๋ช |
|---|---|
name |
HA UI์ ํ์๋ ์ด๋ฆ |
unique_id |
HA ์ํฐํฐ ๋ ์ง์คํธ๋ฆฌ ๋ฑ๋ก์ฉ ๊ณ ์ ID. ์ด๊ฑธ ๋ฃ์ด์ผ ์ด์์๊ฐ HA UI์์ ์ด๋ฆ ๋ณ๊ฒฝยท๋นํ์ฑํ ๋ฑ์ ๊ด๋ฆฌํ ์ ์๋ค |
state_topic |
HA๊ฐ ๊ฐ์ ์ฝ์ด์ฌ topic. discovery๋ "์ด๋์ ์ฝ์์ง"๋ง ์๋ ค์ฃผ๊ณ , ์ค์ ๊ฐ์ ์ฌ์ ํ robot/{robot_id}/state ๋ฐํ(ยง4 STEP 7)์ด ๊ณต๊ธํ๋ค |
value_template |
state_topic payload(JSON)์์ ๊ฐ์ ๋ฝ์๋ด๋ Jinja2 ํ
ํ๋ฆฟ. value_json์ด ํ์ฑ๋ payload ์ ์ฒด โ envelope ๋๋ฌธ์ value_json.data.โฆ๋ก ์ ๊ทผํด์ผ ํ๋ค (ยง7.3 ํจ์ ์ฐธ๊ณ ) |
device |
๊ฐ์ identifiers๋ฅผ ๊ฐ์ง ์ํฐํฐ๋ค์ HA UI์์ ํ๋์ ๊ธฐ๊ธฐ๋ก ๋ฌถ์ด ๋ณด์ฌ์ค๋ค (SPOT 01 ๊ธฐ๊ธฐ ์๋ batteryยทposeยทonline์ด ๋ชจ์) |
- ๊ธฐ๋ ์(connect ์งํ) 1ํ: retain=true๋ก ๋ฐํํด ๋๋ฉด ํ์์์ ์ด๊ฑธ๋ก ์ถฉ๋ถํ๋ค.
-
HA ์ฌ์์ ๊ฐ์ง(๊ถ์ฅ): HA๋ ์์ ์ด ๊ธฐ๋๋๋ฉด
homeassistant/statustopic์online์ ๋ฐํํ๋ค(birth message). adapter๊ฐ ์ด topic์ ๊ตฌ๋ ํด ๋์๋ค๊ฐonline์์ ์ discovery config๋ฅผ ์ฌ๋ฐํํ๋ฉด, retain์ด ์ด๋ค ์ด์ ๋ก ์ ์ค๋ผ๋(broker ๋ณผ๋ฅจ ์ด๊ธฐํ ๋ฑ) ์ํฐํฐ๊ฐ ์๋ ๋ณต๊ตฌ๋๋ค.
# ์์ โ services/mcp-robot-adapter/src/adapter/ha_discovery.py (๋ฐ์ท ์์ค)
import json
DISCOVERY_PREFIX = "homeassistant"
def _device_block(robot_id: str) -> dict:
# ๊ฐ์ identifiers โ HA UI์์ ์ํฐํฐ๋ค์ด ํ ๊ธฐ๊ธฐ๋ก ๋ฌถ์
return {"identifiers": [robot_id], "name": robot_id.upper(), "manufacturer": "Boston Dynamics"}
def _entity_configs(robot_id: str) -> dict[str, dict]:
"""{config_topic: payload} โ ์ด ๋ก๋ด์ด ์ ๊ณ ํ ์ํฐํฐ ๋ชฉ๋ก.
state_topic์ ์ ๋ถ robot/{robot_id}/state ํ๋๋ค โ 1Hz ํ
๋ ๋ฉํธ๋ฆฌ(ยง4 STEP 7)
ํ ๋ฉ์์ง์์ value_template์ผ๋ก ๊ฐ์ ๋ค๋ฅธ ํ๋๋ฅผ ๋ฝ์ ์ด๋ค.
"""
oid = robot_id.replace("-", "_") # spot-01 โ spot_01 (object_id ๊ท์น: ์์ซ์/_)
state_topic = f"robot/{robot_id}/state"
device = _device_block(robot_id)
return {
f"{DISCOVERY_PREFIX}/sensor/{oid}_battery/config": {
"name": f"{robot_id.upper()} Battery",
"unique_id": f"zerone_{oid}_battery",
"state_topic": state_topic,
"value_template": "{{ value_json.data.battery_pct }}",
"unit_of_measurement": "%",
"device": device,
},
f"{DISCOVERY_PREFIX}/sensor/{oid}_pose/config": {
"name": f"{robot_id.upper()} Pose",
"unique_id": f"zerone_{oid}_pose",
"state_topic": state_topic,
# ๋ฌธ์์ด๋ก ํฉ์ณ ํ์ โ ์ขํ๋ world meters (SYSTEM_DESIGN.md ยง8.3)
"value_template": (
"{{ value_json.data.pose.x | round(2) }}, "
"{{ value_json.data.pose.y | round(2) }}"
),
"device": device,
},
f"{DISCOVERY_PREFIX}/binary_sensor/{oid}_online/config": {
"name": f"{robot_id.upper()} Online",
"unique_id": f"zerone_{oid}_online",
"state_topic": state_topic,
# LWT(ยง4 STEP 3)๊ฐ connection: offline์ ๋ฐํํ๋ฏ๋ก ์ด ํ๋๋ง ๋ณด๋ฉด ๋๋ค
"value_template": "{{ 'ON' if value_json.data.connection == 'online' else 'OFF' }}",
"device_class": "connectivity",
"device": device,
},
}
def publish_discovery(client, robot_id: str) -> None:
"""๊ธฐ๋ ์ 1ํ + HA birth message ์์ ์ ํธ์ถ."""
for topic, payload in _entity_configs(robot_id).items():
client.publish(topic, json.dumps(payload), qos=1, retain=True)
def remove_discovery(client, robot_id: str) -> None:
"""๋ก๋ด ์ฒ ์ ์ โ ๋น payload retain ๋ฐํ์ผ๋ก HA ์ํฐํฐ ์ญ์ ."""
for topic in _entity_configs(robot_id):
client.publish(topic, payload=None, qos=1, retain=True)ยง4์ connect ํ๋ฆ์ ๋ผ์ ๋ฃ๋ ์์น:
# main.py โ STEP 4(connect)์ STEP 5(subscribe) ์ฌ์ด/์งํ
client.connect("mosquitto", 1883)
client.loop_start()
for robot_id in robot_registry: # config/robots.yaml ๋ ์ง์คํธ๋ฆฌ
publish_discovery(client, robot_id) # ๊ธฐ๋ ์ 1ํ
client.subscribe("homeassistant/status", qos=1) # HA ์ฌ์์ ๊ฐ์ง
def on_message(client, userdata, msg):
if msg.topic == "homeassistant/status" and msg.payload == b"online":
for robot_id in robot_registry:
publish_discovery(client, robot_id) # HA๊ฐ ์ฌ๊ธฐ๋ํ์ผ๋ ์ฌ์ ๊ณ
return
... # robot/+/command ๋ฑ ๊ธฐ์กด ์ฒ๋ฆฌ
โ ๏ธ discovery config payload๋ CloudEvents envelope ์์ธ๋ค.homeassistant/.../config์ payload ํ์์ HA๊ฐ ์ ์ํ ๊ฒ์ด๋ฏ๋ก envelope์ผ๋ก ๊ฐ์ธ๋ฉด HA๊ฐ ํด์ํ์ง ๋ชปํ๋ค. envelope ๊ท์น(ยง4 STEP 6)์ ์ฐ๋ฆฌ ๋๋ฉ์ธ ํ ํฝ(robot/,safety/๋ฑ)์๋ง ์ ์ฉ๋๋ค.
โ ๏ธ ACL ์ถ๊ฐ ํ์. SERVICES.md ยง1์ ํ์ฌ ACL์๋ discovery ํ ํฝ ๊ถํ์ด ์๋ค โ discovery๋ฅผ ์ค์ ๋ก ์ฐ๋ ค๋ฉดrobot-adapter์topic write homeassistant/+/+/configยทtopic read homeassistant/status,homeassistant์ฌ์ฉ์์topic read homeassistant/#์ถ๊ฐ๊ฐ ํ์ํ๋ค(ยง5.1 ์ ์ฐจ๋๋ก ์ค๊ณ ๋ฌธ์๋ถํฐ ๊ฐฑ์ ). ACL ๋ณ๊ฒฝ ์์ด ๊ฐ๋ ค๋ฉด ์๋ ์๋ ์ ์ ๋ฐฉ์์ ์ด๋ค.
๊ฒฐ๊ณผ ์ํฐํฐ: sensor.spot_01_battery, sensor.spot_01_pose, binary_sensor.spot_01_online. discovery ๋์ ha-config/packages/robots.yaml์ ์๋ ์ ์ํด๋ ๋๋ค(๋ด์ฉ์ config payload์ ๋์ผํ ํ๋๋ฅผ YAML๋ก ์ ๋ ๊ฒ) โ ๋ก๋ด ๋์๊ฐ ๊ณ ์ ์ด๋ฉด ์ด์ชฝ์ด ๋ ๋จ์ํ๋ค. ์ด๋ ์ชฝ์ด๋ ์ํ์ ๋จ์ผ ์์ค๋ robot/{robot_id}/state ํ ํฝ์ด๋ค.
์ฐธ๊ณ : HA long-lived access token(
HA_TOKEN)์ orchestrator ์ ์ฉ์ด๋ค (HA REST/WS ํธ์ถ์ฉ). adapter์๋ ๋ถํ์ํ๋ฏ๋ก ๋ฐ๊ธ๋ฐ์ง ์๋๋ค. ๋ฐ๊ธ ์ ์ฐจ๊ฐ ํ์ํ๋ฉด DEPLOYMENT.md ยง9.2.
์ฒ์๋ถํฐ ๋๊น์ง์ ํ๋ฆ์ 8๋จ๊ณ๋ค. ๊ฐ STEP์ ์๋์์ ํ๋์ฉ ์ค๋ช ํ๋ค.
[STEP 1] ์ ์ ์ ๋ณด ์ค๋น (hostยท๊ณ์ ) โ ยง2์์ ๋ง๋ ๊ฒ
[STEP 2] client ๊ฐ์ฒด ์์ฑ + ์ธ์ฆ ์ ๋ณด ์ค์
[STEP 3] LWT(์ ์ธ์ฅ) ๋ฑ๋ก โ ๋ฐ๋์ connect "์ "
[STEP 4] connect โ broker์ ์ ์
[STEP 5] subscribe โ ๋ฐ์ topic ๋ฑ๋ก
[STEP 6] payload ๋ง๋ค๊ธฐ โ CloudEvents envelope๋ก ๊ฐ์ผ JSON
[STEP 7] publish โ topicยทpayloadยทQoSยทretain ์ง์ ํด ๋ฐํ
[STEP 8] ํ์ธ โ ํฐ๋ฏธ๋ ๊ตฌ๋
์ผ๋ก ์ค์ ๋์ฐฉ ๊ฒ์ฆ
ยง2์์ ๋ง๋ ๊ณ์ ๊ณผ ยง2.3์ ์ฐ๊ฒฐ ์ ๋ณด๋ฅผ ํ๊ฒฝ๋ณ์๋ก ๋ฐ๋๋ค. ์ฝ๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ํ๋์ฝ๋ฉํ์ง ์๋๋ค.
Python์์๋ paho-mqtt ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด๋ค (์ด ํ๋ก์ ํธ์ ํ์ค ์์กด์ฑ). client ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ client_id์ username/password๋ฅผ ์ค์ ํ๋ค.
import paho.mqtt.client as mqtt
import os
client = mqtt.Client(client_id="mcp-robot-adapter") # broker๊ฐ ์ด ์ด๋ฆ์ผ๋ก ๋๋ฅผ ์๋ณ
client.username_pw_set("robot-adapter", os.environ["MOSQUITTO_PWD_ROBOT_ADAPTER"])-
client_id๋ broker ์ ์ฅ์์์ ๋ด ์ด๋ฆ์ด๋ค. ๊ฐ์ client_id๋ก ๋ ํ๋ก์ธ์ค๊ฐ ์ ์ํ๋ฉด ๋จผ์ ๋ถ์ ์ชฝ์ด ๋๊ธด๋ค โ ๋๋ฒ๊น ์ค ๋ก์ปฌ์์ ํ๋ ๋ ๋์ธ ๋ ์ฃผ์.
robot/+/state๋ retain=true์ด๋ฏ๋ก(ยง1.3), adapter๊ฐ ๋น์ ์ ์ข
๋ฃํ๋ฉด ๋ง์ง๋ง "online" ์ํ๊ฐ broker์ ์์ํ ๋จ๋๋ค. ์ด๋ฅผ ๋ง๊ธฐ ์ํด connect ์ ์ connection: "offline" payload๋ฅผ ์ ์ธ์ฅ์ผ๋ก ๋ฑ๋กํ๋ค (MQTT_SCHEMA.md ยง4.5).
import json
from topics.envelope import wrap # packages/topics โ envelope ์์ฑ ๊ณต์ฉ ํจ์ (STEP 6 ์ฐธ๊ณ )
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( # "๋ด๊ฐ ์ฃฝ์ผ๋ฉด ์ด๊ฑธ ๋์ ๋ฐํํด์ค"
topic="robot/spot-01/state",
payload=json.dumps(will),
qos=1,
retain=True, # ์ฃฝ์ ๋ค์๋ "offline"์ด retain ๋์ด์ผ ํ๋ฏ๋ก true
)client.connect("mosquitto", 1883) # host, port
client.loop_start() # ๋ฐฑ๊ทธ๋ผ์ด๋ ๋คํธ์ํฌ ์ค๋ ๋ ์์-
connect()์์ ์ broker๊ฐpasswdํ์ผ๋ก ๊ณ์ ์ ๊ฒ์ฆํ๋ค. ๊ณ์ ์ด ํ๋ฆฌ๋ฉด ์ฌ๊ธฐ์ ์ฐ๊ฒฐ ๊ฑฐ๋ถ๋๋ค. -
loop_start()๋ ์ก์์ ์ ์ฒ๋ฆฌํ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋๋ฅผ ๋์ด๋ค โ ์ด๊ฑธ ๋นผ๋จน์ผ๋ฉด publish๊ฐ ์ ์ก๋์ง ์๊ณ subscribe ์ฝ๋ฐฑ๋ ํธ์ถ๋์ง ์๋๋ค (์ด์ฌ์๊ฐ ๊ฐ์ฅ ์์ฃผ ๋น ์ง๋ ํจ์ ).
adapter๊ฐ ๋ฐ์์ผ ํ๋ ๋ฉ์์ง๋ฅผ ๊ตฌ๋
ํ๋ค. ์์ผ๋์นด๋ +(ยง1.2)๋ก ๋ชจ๋ ๋ก๋ด ID๋ฅผ ํ ๋ฒ์ ์ปค๋ฒํ๋ค.
client.subscribe("robot/+/command", qos=1) # HA ํด๋ฐฑ ๊ฒฝ๋ก ๋ช
๋ น
client.subscribe("safety/estop", qos=2) # ์ ๋ ๋๋ฝ ๊ธ์ง
client.subscribe("safety/proximity", qos=1)
client.subscribe("vision/position/update", qos=0) # proximity guard ์
๋ ฅ (5Hz)
def on_message(client, userdata, msg):
event = json.loads(msg.payload) # envelope ํ์ฑ
... # ๊ฐ์ priority queue + safety interlock์ผ๋ก ํฌ์
client.on_message = on_message์์ ํ robot.command๋ MCP ๊ฒฝ๋ก ๋ช
๋ น๊ณผ ๊ฐ์ priority queue(safety > operator > llm) + safety interlock์ ํต๊ณผ์ํจ๋ค. expires_at์ด ์ง๋ ์ฑ ํ์ ๋จ์ ๋ช
๋ น์ ํ๊ธฐํ๋ค.
์์ฒด ๋ฐํํ๋ ๋ชจ๋ ๋ฉ์์ง๋ CloudEvents 1.0 envelope์ผ๋ก ๊ฐ์ผ๋ค (MQTT_SCHEMA.md ยง1). envelope์ด๋ ์ค์ ๋ด์ฉ(data)์ ํ์คํ๋ ๋ฉํ๋ฐ์ดํฐ๋ก ๊ฐ์ผ ๋ดํฌ๋ค โ ๋๊ฐ(source)ยท์ธ์ (time)ยท๋ฌด์จ ์ข
๋ฅ(type)์ ๋ฉ์์ง์ธ์ง payload๋ฅผ ์ด์ด๋ณด์ง ์๊ณ ๋ ์ ์ ์๊ฒ ํ๋ค.
topic์ / ๊ตฌ๋ถ, envelope type์ . ๊ตฌ๋ถ์ด๋ค (topic robot/spot-01/state โ type 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", "...": "..." }
}ํ๋๋ณ ์๋ฏธ:
| ํ๋ | ์ค๋ช |
|---|---|
type |
๋ฉ์์ง ์ข ๋ฅ (dot-notation). ํ ํฝ ์นดํ๋ก๊ทธ์ ์ ์๋ ๊ฐ |
source |
๋ฐํํ ์๋น์ค. adapter๋ ํญ์ /services/mcp-robot-adapter
|
id |
ULID โ ์๊ฐ์ ์ ๋ ฌ ๊ฐ๋ฅํ ๊ณ ์ ID. ๊ตฌ๋ ์๊ฐ ์ค๋ณต ์์ ์ ๊ฑธ๋ฌ๋ด๋ ๋ฉฑ๋ฑ ์ฒ๋ฆฌ ํค |
time |
now()๊ฐ ์๋ ์ด๋ฒคํธ ์์ฒ ์๊ฐ (์: ํ
๋ ๋ฉํธ๋ฆฌ๋ฅผ ์บก์ฒํ ์๊ฐ) |
traceparent |
OpenTelemetry ๋ถ์ฐ ํธ๋ ์ด์ฑ ์ฐ๊ฒฐ ๊ณ ๋ฆฌ โ ๊ฐ๋ฅํ๋ฉด ํญ์ ํฌํจ |
data |
์ค์ ๋ด์ฉ. ํ๋ ์ ์๋ MQTT_SCHEMA.md ยง3.4 |
envelope ์์ฑ/ํ์ฑ์ packages/topics/์ ๊ณต์ฉ wrapper(wrap())๋ฅผ ์ฌ์ฉํ๋ค โ ํ ํฝ ๋ฌธ์์ดยท์คํค๋ง๋ฅผ ์๋น์ค ์ฝ๋์ inline์ผ๋ก ์ฌ์ ์ํ์ง ์๋๋ค (ํ๋ก์ ํธ invariant).
robot_id๋ string ํ์
+ config/robots.yaml ๋ ์ง์คํธ๋ฆฌ ๋ฐํ์ ๊ฒ์ฆ์ด๋ค โ literal union("spot-01" | ...)์ผ๋ก ํ์
์ ์ขํ์ง ์๋๋ค.
publish(topic, payload, qos, retain) ๋ค ์ธ์๋ฅผ ์ง์ ํ๋ค. QoSยทretain์ ํ ํฝ๋ณ๋ก ์ ํด์ง ๊ฐ์ ๊ทธ๋๋ก ์ด๋ค:
| 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_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 | ๋ผ์ด๋ธ๋์ค |
# 1Hz ํ
๋ ๋ฉํธ๋ฆฌ ๋ฐํ
state = wrap("robot.state", telemetry.snapshot(),
source="/services/mcp-robot-adapter", subject="robot/spot-01")
client.publish(
topic="robot/spot-01/state", # ์ด๋๋ก โ ๊ตฌ์ฒด์ topic (์์ผ๋์นด๋ ๋ถ๊ฐ)
payload=json.dumps(state), # ๋ฌด์์ โ envelope JSON ๋ฌธ์์ด
qos=0, # ์ด๋ค ๋ณด์ฅ์ผ๋ก โ state๋ 0 (๊ณ ๋น๋, ์ ์ค ํ์ฉ)
retain=True, # ๋ณด๊ด ์ฌ๋ถ โ ์ ๊ตฌ๋
์๊ฐ ์ฆ์ ์ต์ ์ํ ํ๋
)๊ทธ๋ฆฌ๊ณ ์ ์ ์ข ๋ฃ ์์๋ offline state๋ฅผ ์ง์ ๋ฐํํ๊ณ ๋๋๋ค โ ๊ทธ๋์ผ broker๊ฐ ์ ์ธ์ฅ์ ๋ฐ๋ํ์ง ์๊ณ , retain ์ํ๋ ๊นจ๋ํ๊ฒ offline์ผ๋ก ๋จ๋๋ค:
def shutdown_handler(*_):
client.publish("robot/spot-01/state", json.dumps(will), qos=1, retain=True)
client.disconnect()publish๋ ์ฑ๊ณตํ๋๋ฐ (ACL ๋๋ฌธ์) broker๊ฐ ๋ฒ๋ ธ์ ์๋ ์๋ค โ ๋ฐํ ์ฝ๋๋ง ๋ฏฟ์ง ๋ง๊ณ ๋ฐ๋์ ๊ตฌ๋ ํฐ๋ฏธ๋๋ก ํ์ธํ๋ค. ๋ฐฉ๋ฒ์ ยง6, HA ์ํฐํฐ๊น์ง์ ํ์ธ์ ยง7.
ยง1.2์์ ์ค๋ช
ํ๋ฏ MQTT ์์ฒด์๋ topic ์์ฑ ์ ์ฐจ๊ฐ ์๋ค โ ์ด ํ๋ก์ ํธ์์ "ํ ํฝ ๋ฑ๋ก"์ ๋ฌธ์ยท์ฝ๋ยท๊ถํ์ ๋ง์ถ๋ ์ผ์ด๋ค. ๋จ์ผ ์์ค๋ ๋ ๊ณณ: ๋ฌธ์๋ 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(username) / -P(password)๋ ํญ์ ํ์ํ๋ค.
ํ ์คํธ์ ๊ธฐ๋ณธ ์์ธ: ๊ตฌ๋ ํฐ๋ฏธ๋์ ๋จผ์ ์ด์ด ๋๊ณ , ๊ทธ ๋ค์ ๋ฐํํ๋ค. (retain์ด ์๋ ๋ฉ์์ง๋ ๋ฐํ ์๊ฐ์ ๊ตฌ๋ ์์๊ฒ๋ง ๊ฐ๋ฏ๋ก, ์์๊ฐ ๋ฐ๋๋ฉด ์๋ฌด๊ฒ๋ ๋ชป ๋ณธ๋ค.)
# ํฐ๋ฏธ๋ 1: ๋ชจ๋ ํ ํฝ ๋ผ์ด๋ธ ํ
์ผ (-t '#': ์ ์ฒด ๊ตฌ๋
, -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๋ฅผ retain์ผ๋ก ๋ฐํํ๋ฉด ์ง์์ง๋ค (MQTT์ retain ์ญ์ ๊ท์ฝ):
docker exec mosquitto mosquitto_pub -h localhost -u robot-adapter -P '<pwd>' \
-t 'robot/spot-01/state' -r -n # -r: retain, -n: null payload โ broker๊ฐ ๋ณด๊ด๋ถ ์ญ์ HA ์ชฝ์์๋ ๊ฐ์ ํ
์คํธ๋ฅผ ํ ์ ์๋ค โ ๊ฐ๋ฐ์ ๋๊ตฌ โ ์ก์
์์ mqtt.publish ํธ์ถ (์ด์์ ๋์๋ณด๋์ ์๋ ๋ช
๋ น ๋ฒํผ์ด ์ฐ๋ ๊ฒ๊ณผ ๊ฐ์ ๊ฒฝ๋ก๋ค).
adapter๊ฐ robot/{robot_id}/state๋ฅผ ๋ฐํํ๋ฉด HA ์ํฐํฐ(ยง3.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'๋ก ๋ฐํ ์์ฒด ํ์ธ (ยง6.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 ๋ธ๋ก ํ์ธ (์ ๊ท ํ ํฝ์ ยง5 ์ ์ฐจ: 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 ๋ฐํ (ยง4 STEP 3ยท7)
- ๋ชจ๋ ๋ฐํ ๋ฉ์์ง CloudEvents envelope +
packages/topics/wrapper ์ฌ์ฉ (ยง4 STEP 6) -
robot/+/command์์ ๋ถ์ด MCP ๊ฒฝ๋ก์ ๊ฐ์ ํยทinterlock์ผ๋ก ์๋ ดํ๋์ง ํ์ธ - HA์์
sensor.spot_01_battery๋ฑ ์ํฐํฐ๊ฐ ๋ณด์ด๋์ง ํ์ธ โvalue_template์value_json.data.*(ยง7.3 E2E ํ ๋ฐํด) -
safety/estop์์ ์ ํ fence + ์ด์์ reset๊น์ง ์ ์ง (safety > operator > llm)
์๋น์ค ์ ์ฒด ์ฑ ์ยท๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐยทํ ์คํธ ์ ๋ต์ SERVICES.md ยง6 ์ฐธ๊ณ .