ROBOT_ADAPTER_ONBOARDING - zero1ne-26/doc GitHub Wiki

๋กœ๋ด‡ Adapter ๊ฐœ๋ฐœ ์˜จ๋ณด๋”ฉ โ€” HAยทMQTT ๊ณ„์ • ๋“ฑ๋ก๊ณผ ๋ฉ”์‹œ์ง€ ์†ก์ˆ˜์‹ 

mcp-robot-adapter ๊ฐœ๋ฐœ์— ์ฐฉ์ˆ˜ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ MQTT ๊ธฐ์ดˆ ๊ฐœ๋… โ†’ broker(Mosquitto) ์‚ฌ์šฉ์ž ๋“ฑ๋ก โ†’ Home Assistant ์—ฐ๋™ โ†’ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰/๊ตฌ๋…๊นŒ์ง€ ํ•œ ๋ฒˆ์— ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ์กด ์„ค๊ณ„ ๋ฌธ์„œ์˜ ๊ด€๋ จ ๋‚ด์šฉ์„ ๋ชจ์€ ์‹ค๋ฌด ๊ฐ€์ด๋“œ๋‹ค. MQTT๋ฅผ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ์‚ฌ๋žŒ์„ ๊ธฐ์ค€์œผ๋กœ ์“ฐ์˜€๋‹ค โ€” ์ด๋ฏธ ์ต์ˆ™ํ•˜๋‹ค๋ฉด ยง1์€ ๊ฑด๋„ˆ๋›ฐ์–ด๋„ ๋œ๋‹ค. ๋ณธ ๋ฌธ์„œ๋Š” ์š”์•ฝ/์•ˆ๋‚ด์šฉ์ด๋ฉฐ, ์ถฉ๋Œ ์‹œ ์›๋ณธ ์„ค๊ณ„ ๋ฌธ์„œ๊ฐ€ ์šฐ์„ ํ•œ๋‹ค โ€” ํ† ํ”ฝยทํŽ˜์ด๋กœ๋“œ๋Š” 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 ๊ฐ™์€ ์—”ํ‹ฐํ‹ฐ๋กœ ํก์ˆ˜ํ•œ๋‹ค (์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋ฌด์—‡์ธ์ง€๋Š” ยง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 ๊ถŒํ•œ ์ž์ฒด๊ฐ€ ์—†๋‹ค.


1. MQTT ๊ธฐ์ดˆ ๊ฐœ๋… โ€” ์ฒ˜์Œ์ด๋ผ๋ฉด ์—ฌ๊ธฐ๋ถ€ํ„ฐ

1.1 MQTT๋ž€ ๋ฌด์—‡์ธ๊ฐ€

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ยท๋””๋ฒ„๊น… ํ„ฐ๋ฏธ๋„ ๋“ฑ ๊ตฌ๋… ์ค‘์ธ ๋ชจ๋‘๊ฐ€ ๋™์‹œ์— ๋ฐ›๋Š”๋‹ค. ๊ตฌ๋…์ž๊ฐ€ ๋Š˜์–ด๋‚˜๋„ ๋ฐœํ–‰ ์ฝ”๋“œ๋Š” ํ•œ ์ค„๋„ ๋ฐ”๋€Œ์ง€ ์•Š๋Š”๋‹ค.

1.2 topic โ€” "๋“ฑ๋ก"์ด๋ผ๋Š” ์ ˆ์ฐจ๋Š” ์‚ฌ์‹ค ์—†๋‹ค (์ค‘์š”)

์ฒ˜์Œ ์ ‘ํ•˜๋ฉด "ํ† ํ”ฝ์„ ์–ด๋””์— ๋“ฑ๋กํ•˜์ง€?"๋ถ€ํ„ฐ ์ฐพ๊ฒŒ ๋˜๋Š”๋ฐ, MQTT ํ”„๋กœํ† ์ฝœ ์ž์ฒด์—๋Š” topic์„ ๋ฏธ๋ฆฌ ๋“ฑ๋กยท์ƒ์„ฑํ•˜๋Š” ์ ˆ์ฐจ๊ฐ€ ์—†๋‹ค. topic์€ ๋ˆ„๊ตฐ๊ฐ€ ๊ทธ ์ด๋ฆ„์œผ๋กœ publish ํ•˜๋Š” ์ˆœ๊ฐ„ ์กด์žฌํ•˜๊ณ , ์•„๋ฌด๋„ ์•ˆ ์“ฐ๋ฉด ๊ทธ๋ƒฅ ์—†๋Š” ๊ฒƒ์ด๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์ฒ˜๋Ÿผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด ๋‘๋Š” ๊ฐœ๋…์ด ์•„๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์ด ํ”„๋กœ์ ํŠธ์—์„œ "ํ† ํ”ฝ ๋“ฑ๋ก"์ด๋ผ ๋ถ€๋ฅด๋Š” ๊ฒƒ์€ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€๋ฅผ ๋œปํ•œ๋‹ค:

  1. ๋ฌธ์„œ ๋“ฑ๋ก โ€” MQTT_SCHEMA.md์˜ ํ† ํ”ฝ ์นดํƒˆ๋กœ๊ทธ์— ํ† ํ”ฝ๋ช…ยทpayload ํ˜•์‹ยทQoSยทretain์„ ์ •์˜ํ•œ๋‹ค. ๋ชจ๋“  ์„œ๋น„์Šค๊ฐ€ ์ด ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  ๋งž์ถ˜๋‹ค (๋‹จ์ผ ์†Œ์Šค).
  2. ๊ถŒํ•œ ๋“ฑ๋ก โ€” 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 ์ด๋ฆ„์œผ๋กœ ๋ฐœํ–‰ํ•œ๋‹ค.

1.3 QoSยทretainยทLWT โ€” ๋ฐœํ–‰ํ•  ๋•Œ ์ •ํ•˜๋Š” 3๊ฐ€์ง€ ์˜ต์…˜

publish ํ•  ๋•Œ๋งˆ๋‹ค topicยทpayload ์™ธ์— ๋‹ค์Œ ์˜ต์…˜์„ ํ•จ๊ป˜ ์ง€์ •ํ•œ๋‹ค. ์–ด๋–ค ๊ฐ’์„ ์“ธ์ง€๋Š” ํ† ํ”ฝ๋งˆ๋‹ค MQTT_SCHEMA.md ยง2์— ์ด๋ฏธ ์ •ํ•ด์ ธ ์žˆ๋‹ค โ€” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž„์˜๋กœ ๊ณ ๋ฅด์ง€ ์•Š๋Š”๋‹ค.

QoS (Quality of Service) โ€” ์ „๋‹ฌ ๋ณด์žฅ ์ˆ˜์ค€

QoS ๋ณด์žฅ ๋น„์œ  ์ด ํ”„๋กœ์ ํŠธ์˜ ์šฉ๋„
0 ์ตœ๋Œ€ 1ํšŒ โ€” ๋ณด๋‚ด๊ณ  ๋, ์œ ์‹ค ๊ฐ€๋Šฅ ์ผ๋ฐ˜ ์šฐํŽธ robot/+/state (1์ดˆ๋งˆ๋‹ค ๋‹ค์‹œ ๋ณด๋‚ด๋‹ˆ ํ•œ ๋ฒˆ ๋น ์ ธ๋„ ๋ฌด๋ฐฉ)
1 ์ตœ์†Œ 1ํšŒ โ€” ๋„์ฐฉ ํ™•์ธ, ์ค‘๋ณต ๊ฐ€๋Šฅ ๋“ฑ๊ธฐ ์šฐํŽธ robot/+/commandยทackยทfault (๋†“์น˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ)
2 ์ •ํ™•ํžˆ 1ํšŒ โ€” ๊ฐ€์žฅ ๋ฌด๊ฒ๋‹ค ๋‚ด์šฉ์ฆ๋ช… safety/estop ๋‹จ ํ•˜๋‚˜ (์ ˆ๋Œ€ ์œ ์‹คยท์ค‘๋ณต ๊ธˆ์ง€)

QoS 1์€ "์ค‘๋ณต ๋„์ฐฉ ๊ฐ€๋Šฅ"์ด๋ฏ€๋กœ ๊ตฌ๋…์ž๋Š” envelope์˜ id(ยง4.4)๋กœ ๊ฐ™์€ ๋ฉ”์‹œ์ง€๋ฅผ ๋‘ ๋ฒˆ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค โ€” ์ด๊ฒƒ์„ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ(idempotent) ๋ผ ํ•œ๋‹ค.

retain โ€” broker์— "๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€" ๋ณด๊ด€

๋ณดํ†ต ๋ฉ”์‹œ์ง€๋Š” ๊ทธ ์ˆœ๊ฐ„์˜ ๊ตฌ๋…์ž์—๊ฒŒ๋งŒ ์ „๋‹ฌ๋˜๊ณ  ์‚ฌ๋ผ์ง„๋‹ค. retain=true๋กœ ๋ฐœํ–‰ํ•˜๋ฉด broker๊ฐ€ ๊ทธ topic์˜ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ 1๊ฐœ๋ฅผ ๋ณด๊ด€ํ•ด ๋‘์—ˆ๋‹ค๊ฐ€, ๋‚˜์ค‘์— ๊ตฌ๋…์„ ์‹œ์ž‘ํ•˜๋Š” client์—๊ฒŒ ์ฆ‰์‹œ ๊ฑด๋„ค์ค€๋‹ค.

  • ์˜ˆ: HA๊ฐ€ ์žฌ์‹œ์ž‘๋ผ๋„ robot/spot-01/state๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ์ˆœ๊ฐ„ ๋งˆ์ง€๋ง‰ ๋กœ๋ด‡ ์ƒํƒœ๋ฅผ ๋ฐ”๋กœ ๋ฐ›๋Š”๋‹ค โ€” ๋‹ค์Œ 1Hz ๋ฐœํ–‰์„ ๊ธฐ๋‹ค๋ฆด ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ๋ถ€์ž‘์šฉ: ๋ฐœํ–‰์ž๊ฐ€ ์ฃฝ์–ด๋„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜์›ํžˆ ๋‚จ๋Š”๋‹ค(stale). ๊ทธ๋ž˜์„œ โ†“ LWT๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

LWT (Last Will and Testament, ์œ ์–ธ์žฅ)

client๊ฐ€ broker์— ์ ‘์†ํ•  ๋•Œ ๋ฏธ๋ฆฌ ๋งก๊ฒจ๋‘๋Š” ๋ฉ”์‹œ์ง€. client๊ฐ€ ๋น„์ •์ƒ ์ข…๋ฃŒ(๋„คํŠธ์›Œํฌ ๋‹จ์ ˆ, ํฌ๋ž˜์‹œ)๋˜๋ฉด broker๊ฐ€ ๋Œ€์‹  ์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•ด ์ค€๋‹ค.

  • adapter๋Š” connection: "offline"์ด ๋‹ด๊ธด state ๋ฉ”์‹œ์ง€๋ฅผ ์œ ์–ธ์žฅ์œผ๋กœ ๋งก๊ธด๋‹ค โ†’ adapter๊ฐ€ ์ฃฝ์œผ๋ฉด broker๊ฐ€ ์ž๋™์œผ๋กœ "๋กœ๋ด‡ ์˜คํ”„๋ผ์ธ"์„ ๋ฐœํ–‰ โ†’ HA ๋Œ€์‹œ๋ณด๋“œ์— ์ฆ‰์‹œ ๋ฐ˜์˜๋˜๊ณ , retain ๋œ stale "online" ์ƒํƒœ๊ฐ€ ๋‚จ๋Š” ๋ฌธ์ œ๋„ ํ•ด๊ฒฐ๋œ๋‹ค.
  • connect ์ „์— ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค (ํ”„๋กœํ† ์ฝœ์ƒ CONNECT ํŒจํ‚ท์— ์‹ค๋ ค ๊ฐ€๊ธฐ ๋•Œ๋ฌธ).

2. MQTT ์‚ฌ์šฉ์ž ๋“ฑ๋ก (Mosquitto)

Mosquitto๋Š” allow_anonymous false(์ต๋ช… ์ ‘์† ๊ธˆ์ง€) + password_file + acl_file ๊ตฌ์„ฑ์ด๋‹ค (SERVICES.md ยง1). adapter๋Š” robot-adapter ์‚ฌ์šฉ์ž๋กœ ์ ‘์†ํ•œ๋‹ค.

2.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.secret์˜ MOSQUITTO_PWD_ROBOT_ADAPTER์— ๊ธฐ์ž…ํ•œ๋‹ค (gitignore, chmod 600). adapter ์ปจํ…Œ์ด๋„ˆ๋Š” env_file๋กœ ์ด ๊ฐ’์„ ๋ฐ›๋Š”๋‹ค.
  • ํšŒ์ „ ์ฃผ๊ธฐ๋Š” ๋ถ„๊ธฐ 1ํšŒ โ€” ์ ˆ์ฐจ๋Š” DEPLOYMENT.md ยง9.2 ์ฐธ๊ณ .

2.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์ด๋‹ค.

2.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 ๊ณ ์ • ๊ถŒ์žฅ

3. Home Assistant ์ชฝ ๋“ฑ๋ก

adapter ๊ฐœ๋ฐœ์ž๊ฐ€ HA์—์„œ ํ•ด์•ผ ํ•  ์ผ์€ ๋‘ ๊ฐ€์ง€๋ฟ์ด๋‹ค. ๊ทธ ์ „์—, ์ด ์ ˆ ์ „์ฒด์—์„œ ๊ณ„์† ๋‚˜์˜ค๋Š” "์—”ํ‹ฐํ‹ฐ"๊ฐ€ ๋ฌด์—‡์ธ์ง€๋ถ€ํ„ฐ ์งš๊ณ  ๊ฐ„๋‹ค.

3.0 ๋จผ์ € โ€” HA "์—”ํ‹ฐํ‹ฐ(entity)"๋ž€ ๋ฌด์—‡์ด๊ณ  ์™œ ํ•„์š”ํ•œ๊ฐ€

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๊ฐ€ ์ด๊ฒƒ์„ ์—”ํ‹ฐํ‹ฐ๋กœ ํก์ˆ˜ํ•˜๋Š” ์ˆœ๊ฐ„, ์ถ”๊ฐ€ ์ฝ”๋“œ ์—†์ด ๋‹ค์Œ์„ ์ „๋ถ€ ์–ป๋Š”๋‹ค:

  1. ์–ธ์ œ๋“  ์กฐํšŒ โ€” orchestrator๊ฐ€ LLM ๊ฒฐ์ • ์ง์ „์— GET /api/states/sensor.spot_01_battery๋กœ ์ตœ์‹  ๊ฐ’์„ ์ฝ๋Š”๋‹ค (ยง7.2)
  2. ํžˆ์Šคํ† ๋ฆฌ โ€” recorder๊ฐ€ SQLite์— ์ž๋™ ๊ธฐ๋ก โ†’ ๋ฐฐํ„ฐ๋ฆฌ ๊ทธ๋ž˜ํ”„๊ฐ€ ๊ณต์งœ
  3. ๋Œ€์‹œ๋ณด๋“œ โ€” Lovelace ์นด๋“œ์— ์˜ฌ๋ ค ์šด์˜์ž๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณธ๋‹ค
  4. ์ž๋™ํ™” ํŠธ๋ฆฌ๊ฑฐ/์กฐ๊ฑด โ€” ํด๋ฐฑ ์ž๋™ํ™”๊ฐ€ 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๋ผ๋Š” ์—”ํ‹ฐํ‹ฐ๋กœ ๊ด€๋ฆฌํ•ด๋ผ"๋ผ๊ณ  ์•Œ๋ ค์ฃผ๋Š” ์ž‘์—…์ด๋‹ค.

3.1 HA์˜ MQTT integration ๊ณ„์ • (1ํšŒ, ์ธํ”„๋ผ ์ž‘์—…)

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

homeassistant ์‚ฌ์šฉ์ž์˜ ACL(read robot/+/stateยทrobot/+/ackยทrobot/+/fault, write robot/+/command ๋“ฑ)๋„ SERVICES.md ยง1์— ์ด๋ฏธ ์ •์˜๋˜์–ด ์žˆ๋‹ค.

3.2 ๋กœ๋ด‡ ์—”ํ‹ฐํ‹ฐ ๋“ฑ๋ก โ€” MQTT Discovery

adapter๊ฐ€ ๊ธฐ๋™ ์‹œ discovery config๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด HA๊ฐ€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค (OSS_INTEGRATION.md ยง3.2). ์•„๋ž˜์— ๋™์ž‘ ์›๋ฆฌ๋ถ€ํ„ฐ ์˜ˆ์ œ ์ฝ”๋“œ๊นŒ์ง€ ์ฐจ๋ก€๋กœ ์„ค๋ช…ํ•œ๋‹ค.

๋™์ž‘ ์›๋ฆฌ โ€” "์—”ํ‹ฐํ‹ฐ ์„ค์ •์„ MQTT ๋ฉ”์‹œ์ง€๋กœ ๋ณด๋‚ธ๋‹ค"

์›๋ž˜ HA์— ์„ผ์„œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด HA ์ชฝ YAML ํŒŒ์ผ์„ ๊ณ ์น˜๊ณ  HA๋ฅผ ์žฌ์‹œ์ž‘ํ•ด์•ผ ํ•œ๋‹ค. MQTT Discovery๋Š” ์ด ์„ค์ • ์ž์ฒด๋ฅผ MQTT ๋ฉ”์‹œ์ง€๋กœ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹์ด๋‹ค:

  1. HA๋Š” MQTT integration์„ ์ผœ๋Š” ์ˆœ๊ฐ„(ยง3.1์˜ discovery: true) homeassistant/+/+/config ํŒจํ„ด์„ ํ•ญ์ƒ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋‹ค.
  2. ์–ด๋–ค client๋“  ์ด ํŒจํ„ด์— ๋งž๋Š” topic์œผ๋กœ "์—”ํ‹ฐํ‹ฐ ์„ค์ • JSON"์„ ๋ฐœํ–‰ํ•˜๋ฉด,
  3. HA๊ฐ€ ๊ทธ JSON์„ ์ฝ๊ณ  ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฆ‰์‹œ ์ƒ์„ฑํ•œ๋‹ค โ€” HA ์žฌ์‹œ์ž‘๋„, YAML ์ˆ˜์ •๋„ ํ•„์š” ์—†๋‹ค.

์ฆ‰ "๋กœ๋ด‡์ด ๋Š˜์–ด๋‚˜๋ฉด HA ์„ค์ •์„ ๊ณ ์นœ๋‹ค"๊ฐ€ ์•„๋‹ˆ๋ผ, **"adapter๊ฐ€ ์ž๊ธฐ ๋กœ๋ด‡์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์Šค์Šค๋กœ ์‹ ๊ณ ํ•œ๋‹ค"**๊ฐ€ ๋œ๋‹ค. config/robots.yaml์— ๋กœ๋ด‡์ด ์ถ”๊ฐ€๋ผ๋„ HA ์ชฝ ์ž‘์—…์ด 0์ด ๋˜๋Š” ๊ฒƒ์ด ์ด ๋ฐฉ์‹์„ ์“ฐ๋Š” ์ด์œ ๋‹ค.

config topic์˜ ํ˜•์‹

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 ์‚ญ์ œ์™€ ๊ฐ™์€ ๊ทœ์•ฝ).

config payload์˜ ์ฃผ์š” ํ•„๋“œ

{
  "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์ด ๋ชจ์ž„)

๋ฐœํ–‰ ์‹œ์  โ€” ๊ธฐ๋™ ์‹œ 1ํšŒ + HA ์žฌ์‹œ์ž‘ ๊ฐ์ง€

  • ๊ธฐ๋™ ์‹œ(connect ์งํ›„) 1ํšŒ: retain=true๋กœ ๋ฐœํ–‰ํ•ด ๋‘๋ฉด ํ‰์ƒ์‹œ์—” ์ด๊ฑธ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค.
  • HA ์žฌ์‹œ์ž‘ ๊ฐ์ง€(๊ถŒ์žฅ): HA๋Š” ์ž์‹ ์ด ๊ธฐ๋™๋˜๋ฉด homeassistant/status topic์— 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.


4. adapter๊ฐ€ topic์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ• โ€” ์ˆœ์„œ๋Œ€๋กœ

์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€์˜ ํ๋ฆ„์€ 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] ํ™•์ธ โ€” ํ„ฐ๋ฏธ๋„ ๊ตฌ๋…์œผ๋กœ ์‹ค์ œ ๋„์ฐฉ ๊ฒ€์ฆ

STEP 1 โ€” ์ ‘์† ์ •๋ณด ์ค€๋น„

ยง2์—์„œ ๋งŒ๋“  ๊ณ„์ •๊ณผ ยง2.3์˜ ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค. ์ฝ”๋“œ์— ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

STEP 2 โ€” client ๊ฐ์ฒด ์ƒ์„ฑ + ์ธ์ฆ ์ •๋ณด ์„ค์ •

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๋กœ ๋‘ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ ‘์†ํ•˜๋ฉด ๋จผ์ € ๋ถ™์€ ์ชฝ์ด ๋Š๊ธด๋‹ค โ€” ๋””๋ฒ„๊น… ์ค‘ ๋กœ์ปฌ์—์„œ ํ•˜๋‚˜ ๋” ๋„์šธ ๋•Œ ์ฃผ์˜.

STEP 3 โ€” LWT(์œ ์–ธ์žฅ) ๋“ฑ๋ก

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
)

STEP 4 โ€” connect: broker์— ์ ‘์†

client.connect("mosquitto", 1883)   # host, port
client.loop_start()                 # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋„คํŠธ์›Œํฌ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
  • connect() ์‹œ์ ์— broker๊ฐ€ passwd ํŒŒ์ผ๋กœ ๊ณ„์ •์„ ๊ฒ€์ฆํ•œ๋‹ค. ๊ณ„์ •์ด ํ‹€๋ฆฌ๋ฉด ์—ฌ๊ธฐ์„œ ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€๋œ๋‹ค.
  • loop_start()๋Š” ์†ก์ˆ˜์‹ ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๋ฅผ ๋„์šด๋‹ค โ€” ์ด๊ฑธ ๋นผ๋จน์œผ๋ฉด publish๊ฐ€ ์ „์†ก๋˜์ง€ ์•Š๊ณ  subscribe ์ฝœ๋ฐฑ๋„ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค (์ดˆ์‹ฌ์ž๊ฐ€ ๊ฐ€์žฅ ์ž์ฃผ ๋น ์ง€๋Š” ํ•จ์ •).

STEP 5 โ€” subscribe: ๋ฐ›์„ topic ๋“ฑ๋ก

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์ด ์ง€๋‚œ ์ฑ„ ํ์— ๋‚จ์€ ๋ช…๋ น์€ ํ๊ธฐํ•œ๋‹ค.

STEP 6 โ€” payload ๋งŒ๋“ค๊ธฐ: CloudEvents envelope

์ž์ฒด ๋ฐœํ–‰ํ•˜๋Š” ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋Š” 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" | ...)์œผ๋กœ ํƒ€์ž…์„ ์ขํžˆ์ง€ ์•Š๋Š”๋‹ค.

STEP 7 โ€” publish: ๋ฐœํ–‰

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

STEP 8 โ€” ํ™•์ธ: ์‹ค์ œ๋กœ ๋„์ฐฉํ–ˆ๋Š”์ง€ ๋ณธ๋‹ค

publish๋Š” ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ (ACL ๋•Œ๋ฌธ์—) broker๊ฐ€ ๋ฒ„๋ ธ์„ ์ˆ˜๋„ ์žˆ๋‹ค โ€” ๋ฐœํ–‰ ์ฝ”๋“œ๋งŒ ๋ฏฟ์ง€ ๋ง๊ณ  ๋ฐ˜๋“œ์‹œ ๊ตฌ๋… ํ„ฐ๋ฏธ๋„๋กœ ํ™•์ธํ•œ๋‹ค. ๋ฐฉ๋ฒ•์€ ยง6, HA ์—”ํ‹ฐํ‹ฐ๊นŒ์ง€์˜ ํ™•์ธ์€ ยง7.


5. ํ† ํ”ฝ์„ ์ˆ˜์ •ยท์ถ”๊ฐ€ํ•˜๊ณ  ์ ์šฉํ•˜๋Š” ์ ˆ์ฐจ

ยง1.2์—์„œ ์„ค๋ช…ํ–ˆ๋“ฏ MQTT ์ž์ฒด์—๋Š” topic ์ƒ์„ฑ ์ ˆ์ฐจ๊ฐ€ ์—†๋‹ค โ€” ์ด ํ”„๋กœ์ ํŠธ์—์„œ "ํ† ํ”ฝ ๋“ฑ๋ก"์€ ๋ฌธ์„œยท์ฝ”๋“œยท๊ถŒํ•œ์„ ๋งž์ถ”๋Š” ์ผ์ด๋‹ค. ๋‹จ์ผ ์†Œ์Šค๋Š” ๋‘ ๊ณณ: ๋ฌธ์„œ๋Š” MQTT_SCHEMA.md, ์ฝ”๋“œ๋Š” packages/topics/ (SERVICES.md ยง9.2). ์–ด๋А ์ชฝ๋„ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์„œ๋น„์Šค ์ฝ”๋“œ์— ํ† ํ”ฝ ๋ฌธ์ž์—ด์„ inline์œผ๋กœ ๋ฐ•๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ํ”ํ•œ ๊ฒฐํ•จ์ด๋‹ค.

5.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์„ ์˜์‹ฌํ•  ๊ฒƒ.

5.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 ํ‘œ๋ฅผ ํ•จ๊ป˜ ๊ฐฑ์‹ .

6. ํ…Œ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ณ  ๋ฐ›๋Š” ๋ฐฉ๋ฒ•

์ „๋ถ€ broker ์ปจํ…Œ์ด๋„ˆ์— ๋“ค์–ด ์žˆ๋Š” ๋ช…๋ น์ค„ ๋„๊ตฌ mosquitto_sub(๊ตฌ๋…) / mosquitto_pub(๋ฐœํ–‰) ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๋‹ค โ€” ๋ณ„๋„ ๋„๊ตฌ ์„ค์น˜ ๋ถˆํ•„์š”. ์ธ์ฆ์ด ์ผœ์ ธ ์žˆ์œผ๋ฏ€๋กœ -u(username) / -P(password)๋Š” ํ•ญ์ƒ ํ•„์š”ํ•˜๋‹ค.

6.1 ๋ฐ›๊ธฐ โ€” ๊ตฌ๋… ํ„ฐ๋ฏธ๋„์„ ๋จผ์ € ์—ฐ๋‹ค

ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ณธ ์ž์„ธ: ๊ตฌ๋… ํ„ฐ๋ฏธ๋„์„ ๋จผ์ € ์—ด์–ด ๋‘๊ณ , ๊ทธ ๋‹ค์Œ ๋ฐœํ–‰ํ•œ๋‹ค. (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'

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

6.3 ์ •๋ฆฌ โ€” stale retain ์‚ญ์ œ

ํ…Œ์ŠคํŠธ ์ค‘ ๋งŒ๋“  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 ํ˜ธ์ถœ (์šด์˜์ž ๋Œ€์‹œ๋ณด๋“œ์˜ ์ˆ˜๋™ ๋ช…๋ น ๋ฒ„ํŠผ์ด ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ฒฝ๋กœ๋‹ค).


7. HA์—์„œ ์—…๋ฐ์ดํŠธํ•œ ์ƒํƒœ๊ฐ’ ํ™•์ธํ•˜๊ธฐ

adapter๊ฐ€ robot/{robot_id}/state๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด HA ์—”ํ‹ฐํ‹ฐ(ยง3.2)๊ฐ€ ๊ฐฑ์‹ ๋œ๋‹ค. ํ™•์ธ ๋ฐฉ๋ฒ•์€ ์„ธ ๊ฐ€์ง€.

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

7.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์ด ๋ณด๋Š” ๊ฐ’์ด๋‹ค.

7.3 ๊ฒ€์ฆ ์ˆœ์„œ ์ •๋ฆฌ (E2E ํ•œ ๋ฐ”ํ€ด)

  1. ํ„ฐ๋ฏธ๋„์—์„œ mosquitto_sub -t 'robot/+/state'๋กœ ๋ฐœํ–‰ ์ž์ฒด ํ™•์ธ (ยง6.1)
  2. HA ๊ฐœ๋ฐœ์ž ๋„๊ตฌ โ†’ MQTT listen์œผ๋กœ HA ์ˆ˜์‹  ํ™•์ธ
  3. ๊ฐœ๋ฐœ์ž ๋„๊ตฌ โ†’ ์ƒํƒœ์—์„œ ์—”ํ‹ฐํ‹ฐ ๊ฐ’ ๊ฐฑ์‹  ํ™•์ธ
  4. 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 }}**๋‹ค.


8. ์ฐฉ์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  1. mosquitto_passwd๋กœ robot-adapter ๋“ฑ๋ก, passwd ๊ถŒํ•œ 1883:1883 / 0600, broker reload
  2. acl์— robot-adapter ๋ธ”๋ก ํ™•์ธ (์‹ ๊ทœ ํ† ํ”ฝ์€ ยง5 ์ ˆ์ฐจ: MQTT_SCHEMA.md โ†’ packages/topics โ†’ ACL โ†’ reload)
  3. deploy/.env.secret์— MOSQUITTO_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 ๋ฐœํ–‰ (ยง4 STEP 3ยท7)
  6. ๋ชจ๋“  ๋ฐœํ–‰ ๋ฉ”์‹œ์ง€ CloudEvents envelope + packages/topics/ wrapper ์‚ฌ์šฉ (ยง4 STEP 6)
  7. robot/+/command ์ˆ˜์‹ ๋ถ„์ด MCP ๊ฒฝ๋กœ์™€ ๊ฐ™์€ ํยทinterlock์œผ๋กœ ์ˆ˜๋ ดํ•˜๋Š”์ง€ ํ™•์ธ
  8. HA์—์„œ sensor.spot_01_battery ๋“ฑ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋ณด์ด๋Š”์ง€ ํ™•์ธ โ€” value_template์€ value_json.data.* (ยง7.3 E2E ํ•œ ๋ฐ”ํ€ด)
  9. safety/estop ์ˆ˜์‹  ์‹œ ํ fence + ์šด์˜์ž reset๊นŒ์ง€ ์œ ์ง€ (safety > operator > llm)

์„œ๋น„์Šค ์ „์ฒด ์ฑ…์ž„ยท๋””๋ ‰ํ„ฐ๋ฆฌ ๊ตฌ์กฐยทํ…Œ์ŠคํŠธ ์ „๋žต์€ SERVICES.md ยง6 ์ฐธ๊ณ .

โš ๏ธ **GitHub.com Fallback** โš ๏ธ