OSS_INTEGRATION - zero1ne-26/doc GitHub Wiki

OSS ๋ฐฑ๋ณธ ํ†ตํ•ฉ ๊ฐ€์ด๋“œ

SYSTEM_DESIGN.md์˜ ์ถ”์ƒ ์„ค๊ณ„๋ฅผ ์‹ค์ œ OSS๋กœ ๊ตฌํ˜„ํ•  ๋•Œ, ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋–ค ํ”„๋กœํ† ์ฝœยทํ† ํ”ฝยท์ด๋ฒคํŠธ๋กœ ์—ฐ๊ฒฐ๋˜๋Š”์ง€๋ฅผ ์ •๋ฆฌํ•œ ๋ฌธ์„œ.


1. ์ฑ…์ž„ ๋ถ„๋‹ด

์ปดํฌ๋„ŒํŠธ ์ฑ„ํƒ OSS ๋‹ด๋‹น
Vision (CCTV โ†’ ์ด๋ฒคํŠธ) Frigate RTSP(Real Time Streaming Protocol, ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”„๋กœํ† ์ฝœ) ์ŠคํŠธ๋ฆผ ์ž…๋ ฅ, ๊ฐ์ฒด ํƒ์ง€ยท์ถ”์ , MQTT(Message Queuing Telemetry Transport, ๋ฉ”์‹œ์ง€ ํ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์ „์†ก ํ”„๋กœํ† ์ฝœ) ์ด๋ฒคํŠธ ๋ฐœํ–‰
๋ฉ”์‹œ์ง• ๋ฐฑ๋ณธ Mosquitto (๋˜๋Š” HA ๋‚ด์žฅ broker) MQTT broker โ€” ๋ชจ๋“  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์˜ ์ค‘์•™
์ƒํƒœ ์ €์žฅยท๋Œ€์‹œ๋ณด๋“œ Home Assistant (HA) ์‚ฌ์šฉ์ž/๊ณต๊ฐ„/๋กœ๋ด‡ ์—”ํ‹ฐํ‹ฐ, SQLite recorder, Lovelace UI
LLM(Large Language Model, ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ) ์˜์‚ฌ๊ฒฐ์ • LangGraph + Claude Agent SDK(Software Development Kit, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ํ‚คํŠธ) ์ปจํ…์ŠคํŠธ ์–ด์…ˆ๋ธ” โ†’ Claude tool use โ†’ ์•ก์…˜ ๊ฒฐ์ •
๋„๊ตฌ ํ‘œ์ค€ํ™” MCP(Model Context Protocol, ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ) SPOTยทํƒœ๋ธ”๋ฆฟยทHA ์„œ๋น„์Šค๋ฅผ LLM ๋„๊ตฌ๋กœ ๋…ธ์ถœ
๋กœ๋ด‡ ์ œ์–ด Boston Dynamics SDK + (๋ฏธ์ • ๋กœ๋ด‡) MCP server ๋„๊ตฌ ํ˜ธ์ถœ โ†’ ์‹ค์ œ ๋กœ๋ด‡ ๋ช…๋ น
๋กœ๋ด‡ ๋””๋ฒ„๊น… (๋ณด์กฐ) Foxglove Bridge ๋กœ๋ด‡ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์‹œ๊ฐํ™”
ํƒœ๋ธ”๋ฆฟ UI ์ž์ฒด ์ œ์ž‘ (React Native) HA REST(Representational State Transfer, ํ‘œํ˜„ ์ƒํƒœ ์ „์ด)/WebSocket API ๋˜๋Š” ์ง์ ‘ MQTT ๊ตฌ๋…

2. ์ „์ฒด ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    RTSP    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” MQTT (frigate/events)  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ CCTV (n)  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ Frigate โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                        โ”‚              โ”‚
                                                            โ”‚  Mosquitto   โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  Wi-Fi/SDK  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  MQTT             โ”‚  (MQTT       โ”‚
โ”‚ SPOT      โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Robot Adapterโ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚   broker)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚ (MCP server) โ”‚                    โ”‚              โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ”‚              โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  WSS/REST   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                   โ”‚              โ”‚
โ”‚ Tablet x3 โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Home         โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚ Assistant    โ”‚   MQTT             โ”‚              โ”‚
                         โ”‚ (state/UI)   โ”‚                    โ”‚              โ”‚
                         โ”‚ + SQLite     โ”‚                    โ”‚              โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                                                    โ”‚ MQTT
                                                                    โ–ผ
                                                            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                                            โ”‚ LangGraph     โ”‚
                                                            โ”‚ Orchestrator  โ”‚
                                                            โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
                                                            โ”‚  โ”‚ Claude   โ”‚ โ”‚
                                                            โ”‚  โ”‚ (tool    โ”‚ โ”‚
                                                            โ”‚  โ”‚  use)    โ”‚ โ”‚
                                                            โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
                                                            โ”‚  via MCP:     โ”‚
                                                            โ”‚   โ€ข robot.*   โ”‚
                                                            โ”‚   โ€ข tablet.*  โ”‚
                                                            โ”‚   โ€ข ha.*      โ”‚
                                                            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ํ•ต์‹ฌ ์›์น™:

  • MQTT broker๊ฐ€ ๋ชจ๋“  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์˜ hub. ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” broker๋กœ๋งŒ ํ†ต์‹ .
  • HA์˜ SQLite๊ฐ€ ์˜์† ์ƒํƒœ. Context Server๋ฅผ ๋ณ„๋„๋กœ ๋‘์ง€ ์•Š๊ณ  HA์˜ entity/state ๋จธ์‹ ์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ.
  • LangGraph๋Š” MQTT ๊ตฌ๋…์ž + Claude ํ˜ธ์ถœ์ž. ์ž์‹ ์˜ ์ƒํƒœ๋Š” ๊ฑฐ์˜ ๊ฐ–์ง€ ์•Š์Œ(stateless graph + ์™ธ๋ถ€ ์ƒํƒœ๋Š” HA์—์„œ ์กฐํšŒ).

3. ์ปดํฌ๋„ŒํŠธ๋ณ„ ํ†ตํ•ฉ ์ƒ์„ธ

3.1 Frigate โ†” MQTT broker

Frigate๋Š” ๊ฐ์ฒด ํƒ์ง€ ์‹œ ์ž๋™์œผ๋กœ MQTT ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.

frigate.yml (์š”์ง€):

mqtt:
  host: mosquitto
  port: 1883
  topic_prefix: frigate
  client_id: frigate

cameras:
  cafe_cam_01:
    ffmpeg:
      inputs:
        - path: rtsp://camera-cafe-01/stream
          roles: [detect, record]
    detect: { width: 1280, height: 720, fps: 5 }
    objects:
      track: [person]
    zones:
      cafe_zone:
        coordinates: "0,0,1280,0,1280,720,0,720"

๋ฐœํ–‰๋˜๋Š” MQTT ํ† ํ”ฝ:

  • frigate/events โ€” ๊ฐ์ฒด ์ง„์ž…/์ดํƒˆ/์—…๋ฐ์ดํŠธ (JSON payload, before/after ์Šค๋ƒ…์ƒท ํฌํ•จ)
  • frigate/{camera}/person โ€” ์นด๋ฉ”๋ผ๋ณ„ person ์นด์šดํŠธ
  • frigate/{camera}/{zone} โ€” ์กด ์ ์œ  ์—ฌ๋ถ€ (ON/OFF)

์šฐ๋ฆฌ ๋„๋ฉ”์ธ ํ† ํ”ฝ์œผ๋กœ์˜ ๋งคํ•‘: HA ์ž๋™ํ™” ๋˜๋Š” ์ž‘์€ bridge ์„œ๋น„์Šค๊ฐ€ frigate/events โ†’ vision/zone/enter / vision/zone/exit๋กœ ๋ณ€ํ™˜.

# bridge.py (์˜์‚ฌ์ฝ”๋“œ, 30๋ผ์ธ ์ •๋„)
@mqtt.on("frigate/events")
def on_frigate_event(msg):
    e = json.loads(msg.payload)
    if e["type"] == "new":
        publish("vision/zone/enter", {
            "session_id": resolve_session(e["after"]),  # ReID ๋งค์นญ
            "zone_id": map_camera_to_zone(e["after"]["camera"]),
            "confidence": e["after"]["score"],
            "ts": e["after"]["frame_time"],
        })

Cross-camera ReID(Re-identification, ๋ณดํ–‰์ž ์žฌ์‹๋ณ„)๋Š” Frigate ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๋ถ€์กฑ โ†’ BoxMOT/Norfair ๋ณด๊ฐ• ๋˜๋Š” ๋‹จ์ˆœ ์™ธํ˜• ์ž„๋ฒ ๋”ฉ ๋งค์นญ์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„.

3.1.1 cctv-relay (go2rtc) โ†” ๋ธŒ๋ผ์šฐ์ € โ€” WebRTC ๋ผ์ด๋ธŒ ์˜์ƒ (๋…๋ฆฝ ์„œ๋น„์Šค)

์ „์‹œ์žฅ ๋ฐ๋ชจ ํ•œ์ •. ๋ธŒ๋ผ์šฐ์ €๋Š” RTSP๋ฅผ ์ง์ ‘ ์žฌ์ƒํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ RTSP๋ฅผ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜ ์ „์†ก์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์ค‘๊ณ„์„œ๋ฒ„(relay) ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ด๋ฅผ ๋…๋ฆฝ ์„œ๋น„์Šค cctv-relay(go2rtc ๊ธฐ๋ฐ˜) ๋กœ ๋‘”๋‹ค โ€” Frigate(detect/์ด๋ฒคํŠธ)์™€ ๋ถ„๋ฆฌํ•ด ๋ผ์ด๋ธŒ ์˜์ƒ๋งŒ ์ฑ…์ž„์ง„๋‹ค. ๋ฐฐ๊ฒฝยท๋ถˆ๋ณ€์‹ ์™„ํ™” ๊ทผ๊ฑฐ๋Š” SYSTEM_DESIGN.md ยง8.4, ์„œ๋น„์Šค ์ƒ์„ธ๋Š” SERVICES.md ยง2.5.

์ค‘๊ณ„ ํ๋ฆ„:

CCTV n โ”€โ”€RTSP(config ๊ตฌ๋™ ์ž๋™ ์—ฐ๊ฒฐ)โ”€โ”€โ–บ cctv-relay(go2rtc) โ”€โ”€โ–บ WebRTC/MSE restream
CCTV n โ”€โ”€RTSPโ”€โ”€โ–บ Frigate โ”€โ”€โ–บ detect ํŒŒ์ดํ”„๋ผ์ธ (frigate/events)     โ€ป ๋น„์ „์€ ๋ณ„๋„ ๊ฒฝ๋กœ
๋ธŒ๋ผ์šฐ์ €(tablet-fe) โ”€โ”€โ‘  WHEP offer(SDP)โ”€โ”€โ–บ tablet-fe BFF(same-origin) โ”€โ”€โ–บ cctv-relay /api/webrtc
๋ธŒ๋ผ์šฐ์ € โ—„โ•โ•โ‘ก ๋ฏธ๋””์–ด(SRTP, ICE P2P)โ•โ•โ–บ cctv-relay   (Node BFF๋Š” ์‹œ๊ทธ๋„๋ง๋งŒ ์ค‘๊ณ„, ๋ฏธ๋””์–ด ๋ฏธ๊ฒฝ์œ )
  • n-stream config ๊ตฌ๋™: config/cctv-relay.yaml์˜ streams: ๋งต์— ์›๋ณธ RTSP ์ฃผ์†Œ๋ฅผ ํ•œ ์ค„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ž๋™ ๋“ฑ๋กยท์—ฐ๊ฒฐ(์ฝ”๋“œ ๋ณ€๊ฒฝ ๋ถˆํ•„์š”).
  • ์‹œ๊ทธ๋„๋ง์€ BFF ํ”„๋ก์‹œ: ๋ธŒ๋ผ์šฐ์ €๋Š” ์นด๋ฉ”๋ผ ์ž๊ฒฉ์ฆ๋ช…ยทrelay ์ฃผ์†Œ๋ฅผ ๋ชจ๋ฅธ ์ฑ„ same-origin(/api/cctv/webrtc/<cam>)์œผ๋กœ๋งŒ WHEP offer๋ฅผ ๋ณด๋‚ธ๋‹ค โ†’ ์ž๊ฒฉ์ฆ๋ช…์€ ์„œ๋ฒ„(cctv-relay/BFF)์—๋งŒ(ยง3.1์˜ {ENV_VAR} ์น˜ํ™˜๊ณผ ๋™์ผ ์›์น™).
  • ๋ฏธ๋””์–ด๋Š” P2P: SDP ํ˜‘์ƒ ํ›„ ์˜์ƒ์€ ๋ธŒ๋ผ์šฐ์ €โ†”cctv-relay ICE๋กœ ์ง์ ‘. LAN ๋‚ด๋ถ€์ด๋ฏ€๋กœ STUN/TURN ๋ถˆํ•„์š”(host candidate IP:8555).
  • Frigate์™€ ๋ถ„๋ฆฌ: ๊ธฐ๋ณธ์€ cctv-relay๊ฐ€ ์นด๋ฉ”๋ผ์—์„œ ๋…๋ฆฝ pull โ†’ ๋น„์ „ ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ์žฅ์•  ๊ฒฉ๋ฆฌ(๋‹จ์ผ-pull ๋ณ€ํ˜• ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋Š” SERVICES ยง2.5).
  • ์ฝ”๋ฑ: WebRTC๋Š” H.264๋งŒ โ†’ H.265 ์นด๋ฉ”๋ผ๋Š” go2rtc transcode ํ•„์š”(CPU/GPU ๋น„์šฉ).

3.2 Home Assistant โ†” MQTT broker

HA๋Š” MQTT integration์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด broker์˜ ํ† ํ”ฝ์„ entity์— ์ž๋™ ๋งคํ•‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

MQTT Discovery๋กœ ์—”ํ‹ฐํ‹ฐ ์ž๋™ ๋“ฑ๋ก (LangGraph ๋˜๋Š” Robot Adapter๊ฐ€ ๋ฐœํ–‰):

Topic: homeassistant/sensor/spot_01_battery/config
Payload: {
  "name": "SPOT-01 Battery",
  "state_topic": "robot/spot-01/state",
  "value_template": "{{ value_json.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
  • sensor.session_a_zone, sensor.session_a_mood
  • binary_sensor.cafe_occupied (Frigate zone์—์„œ ์ž๋™ ์ƒ์„ฑ)

์ƒํƒœ ์˜์†: HA ๊ธฐ๋ณธ recorder๊ฐ€ SQLite์— ์ž๋™ ๊ธฐ๋ก โ€” ๋ณ„๋„ Context Server ๋ถˆํ•„์š”.

๋Œ€์‹œ๋ณด๋“œ: Lovelace YAML๋กœ ์šด์˜์ž ์ฝ˜์†” ์ฆ‰์‹œ ๊ตฌ์„ฑ ๊ฐ€๋Šฅ (๊ทธ๋ž˜ํ”„, ์นด๋ฉ”๋ผ ์ŠคํŠธ๋ฆผ, ๋กœ๋ด‡ ์ƒํƒœ ์นด๋“œ).

์„œ๋น„์Šค ํ˜ธ์ถœ (HA โ†’ ์™ธ๋ถ€): mqtt.publish ์„œ๋น„์Šค๋กœ ์ž„์˜ ํ† ํ”ฝ ๋ฐœํ–‰ ๊ฐ€๋Šฅ โ†’ ์šด์˜์ž๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ ๋ฒ„ํŠผ์œผ๋กœ ๋น„์ƒ ์ •์ง€ยท๋กœ๋ด‡ ํ˜ธ์ถœ ๋“ฑ ์ˆ˜๋™ ๋ช…๋ น.

3.3 LangGraph + Claude Agent SDK โ†” MQTT + MCP

LangGraph ์„œ๋น„์Šค์˜ ์ฑ…์ž„์€ ๋‘˜:

  1. MQTT ๊ตฌ๋…: vision/zone/+, tablet/+/intent, context/+/+/update ๋“ฑ์— ๋ฐ˜์‘.
  2. Claude tool use ํ˜ธ์ถœ: ์ปจํ…์ŠคํŠธ๋ฅผ ๋ชจ์•„ Anthropic API ํ˜ธ์ถœ, ๋„๊ตฌ ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋ฅผ MQTT publish ๋˜๋Š” MCP ํ˜ธ์ถœ๋กœ ์‹คํ–‰.

LangGraph ๋…ธ๋“œ ๊ตฌ์กฐ:

from langgraph.graph import StateGraph
from anthropic import Anthropic

class FlowState(TypedDict):
    trigger_event: dict
    context_snapshot: dict
    llm_decisions: list[dict]

graph = StateGraph(FlowState)
graph.add_node("assemble_context", assemble_from_ha)   # HA REST API๋กœ ํ˜„์žฌ ์ƒํƒœ ์กฐํšŒ
graph.add_node("call_claude", call_with_tools)         # Claude Agent SDK ํ˜ธ์ถœ
graph.add_node("dispatch", dispatch_tool_calls)        # MCP ๋˜๋Š” MQTT publish
graph.add_node("guardrail", validate_actions)          # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธยทhard clamp

graph.set_entry_point("assemble_context")
graph.add_edge("assemble_context", "call_claude")
graph.add_edge("call_claude", "guardrail")
graph.add_edge("guardrail", "dispatch")

Claude Agent SDK ํ˜ธ์ถœ (๋„๊ตฌ๋Š” MCP๋กœ ๋“ฑ๋ก):

from anthropic import Anthropic
client = Anthropic()

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    system=ZONE_PLAYBOOK_SYSTEM_PROMPT,  # prompt cache ์ ์šฉ
    tools=[
        {"type": "mcp", "server": "robot-adapter"},
        {"type": "mcp", "server": "tablet-gateway"},
        {"type": "mcp", "server": "home-assistant"},
    ],
    messages=[{"role": "user", "content": context_message}],
)

3.4 MCP ์„œ๋ฒ„๋กœ ๋„๊ตฌ ํ‘œ์ค€ํ™”

๋„๊ตฌ๋ฅผ LangGraph ์•ˆ์— ์ง์ ‘ ํ•จ์ˆ˜๋กœ ๋‘์ง€ ์•Š๊ณ  MCP ์„œ๋ฒ„ 3๊ฐœ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด ์žฌ์‚ฌ์šฉ์„ฑยทํ…Œ์ŠคํŠธยท๊ถŒํ•œ ๋ถ„๋ฆฌ๊ฐ€ ์šฉ์ด.

MCP ์„œ๋ฒ„ ๊ตฌ์„ฑ:

mcp-robot-adapter (Python) โ€” SPOT SDK ๋ž˜ํ•‘

# ๋…ธ์ถœ ๋„๊ตฌ (์˜ˆ์‹œ)
@mcp.tool
def robot_command(robot_id: str,   # ๋Ÿฐํƒ€์ž„์— config/robots.yaml ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋กœ ๊ฒ€์ฆ
                  capability: Literal["goto", "follow", "stop", "look_at", "gesture"],
                  params: dict) -> dict:
    """๋กœ๋ด‡์— capability ๋ช…๋ น์„ ๋ฐœํ–‰. ์•ˆ์ „ ์ธํ„ฐ๋ก์„ ํ†ต๊ณผํ•ด์•ผ ์‹คํ–‰."""
    if not safety_interlock.allow(robot_id, capability, params):
        return {"status": "rejected", "reason": "safety_interlock"}
    return adapters[robot_id].execute(capability, params)

mcp-tablet-gateway (Node.js) โ€” ํƒœ๋ธ”๋ฆฟ์— ์˜ต์…˜ยท๋ฉ”์‹œ์ง€ push

@tool
async function showOptions(tabletId: string, options: Option[]): Promise<Ack> {
  await ws.send(tabletId, { type: "show_options", options });
  return { ok: true };
}

mcp-home-assistant โ€” HA ์ƒํƒœ ์กฐํšŒยท์„œ๋น„์Šค ํ˜ธ์ถœ (์ปค๋ฎค๋‹ˆํ‹ฐ ๊ตฌํ˜„์ฒด ํ™œ์šฉ ๊ฐ€๋Šฅ)

  • ha.get_state("sensor.session_a_mood")
  • ha.call_service("light.turn_on", { ... })

LangGraph๋Š” ์œ„ MCP ์„œ๋ฒ„ 3๊ฐœ๋ฅผ Claude์— ๋„๊ตฌ๋กœ ๋…ธ์ถœํ•ด, Claude๊ฐ€ ํ˜ธ์ถœํ•˜๋ฉด ์‹ค์ œ ์•ก์…˜์ด ์‹คํ–‰๋œ๋‹ค.

3.5 Robot Adapter โ†” MQTT (ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ๋ฐœํ–‰)

๋„๊ตฌ๋กœ ๋ช…๋ น์„ ๋ฐ›๋Š” ๊ฒƒ๊ณผ ๋ณ„๋„๋กœ, ๋กœ๋ด‡์€ ์ž๊ธฐ ์ƒํƒœ๋ฅผ 1Hz๋กœ MQTT์— ๋ฐœํ–‰ํ•ด์•ผ HAยทFrigateยทLangGraph๊ฐ€ ๋ชจ๋‘ ๋ณธ๋‹ค.

Topic: robot/spot-01/state
Payload: {
  "robot_id": "spot-01",
  "pose": { "x": 2.4, "y": 1.1, "yaw": 0.78, "zone_id": "cafe" },
  "battery_pct": 87,
  "current_action": { "type": "look_at", "started_at": "2026-05-04T10:00:00Z" },
  "faults": [],
  "connection": "online",
  "updated_at": "2026-05-04T10:00:01Z"
}

์ด ํ† ํ”ฝ์€ HA MQTT Discovery๋กœ ์ž๋™ entityํ™”๋˜์–ด Lovelace์— ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋œ๋‹ค.

3.6 ํƒœ๋ธ”๋ฆฟ โ†” HA / MQTT

๋‘ ๊ฐ€์ง€ ํ†ตํ•ฉ ์˜ต์…˜:

์˜ต์…˜ A โ€” HA ๋„ค์ดํ‹ฐ๋ธŒ: ํƒœ๋ธ”๋ฆฟ์ด HA ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ’€์Šคํฌ๋ฆฐ์œผ๋กœ ํ‘œ์‹œ. ๋น ๋ฅด์ง€๋งŒ ์ปค์Šคํ…€ ์ธํ„ฐ๋ž™์…˜ ํ•œ๊ณ„.

์˜ต์…˜ B โ€” ์ž์ฒด ์•ฑ: React Native ์•ฑ์ด HA WebSocket API์— ์ธ์ฆ ํ† ํฐ์œผ๋กœ ์ ‘์†, ์—”ํ‹ฐํ‹ฐ ๋ณ€๊ฒฝ์„ ๊ตฌ๋…ํ•˜๊ณ  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ MQTT๋กœ publish.

// ํƒœ๋ธ”๋ฆฟ ์•ฑ (์˜ต์…˜ B)
import { connectToHA } from "ha-websocket";
const ha = await connectToHA(URL, TOKEN);
ha.subscribe("state_changed", (e) => {
  if (e.entity_id === `sensor.session_${sessionId}_options`) {
    setOptions(JSON.parse(e.new_state.state));
  }
});

// ์‚ฌ์šฉ์ž๊ฐ€ ์˜ต์…˜ ์„ ํƒ ์‹œ
mqtt.publish(`tablet/${tabletId}/intent`, { option_id: "drink_americano" });

๊ถŒ์žฅ: ์˜ต์…˜ B โ€” ์ธํ„ฐ๋ž™์…˜ ์ž์œ ๋„๊ฐ€ ๋†’๊ณ , HA๋Š” ๋ฐฑ์—”๋“œ ์ƒํƒœ ์ €์žฅ๋งŒ ๋‹ด๋‹น.

3.7 Foxglove (๋ณด์กฐ ๋””๋ฒ„๊น…)

๋กœ๋ด‡ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ(ํŠนํžˆ SPOT์˜ ์นด๋ฉ”๋ผ/IMU(Inertial Measurement Unit, ๊ด€์„ฑ ์ธก์ • ์žฅ์น˜)/๊ด€์ ˆ ์ƒํƒœ)๋ฅผ ์‹œ๊ฐํ™”ํ•  ๋•Œ ์‚ฌ์šฉ. ROS 2๋ฅผ ๋„์ž…ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ€์น˜ ์ œํ•œ์ ์ด๋ฏ€๋กœ ์šด์˜ ๋‹จ๊ณ„ ์ง„์ž… ํ›„ ํ•„์š” ์‹œ ์ถ”๊ฐ€.

์—ฐ๊ฒฐ ์˜ต์…˜: foxglove-bridge ๋˜๋Š” MQTT-to-foxglove ์–ด๋Œ‘ํ„ฐ.


4. ํ•ต์‹ฌ ์‹œํ€€์Šค (์‹ค์ œ ํ† ํ”ฝ์œผ๋กœ)

4.1 ์‚ฌ์šฉ์ž ์นดํŽ˜ ์ง„์ž… โ†’ SPOT ์ธ์‚ฌ

1. CCTV(cafe_cam_01) โ†’ Frigate
2. Frigate publish โ†’ mqtt:frigate/events
   { type:"new", after:{ camera:"cafe_cam_01", label:"person", id:"track_42" } }
3. bridge.py ๋ณ€ํ™˜ โ†’ mqtt:vision/zone/enter
   { session_id:"sess_A", zone_id:"cafe", confidence:0.97 }
4. HA ์ž๋™ํ™” ํŠธ๋ฆฌ๊ฑฐ โ†’ sensor.session_a_zone ์ƒํƒœ ๊ฐฑ์‹ 
   ๋™์‹œ์— LangGraph ์„œ๋น„์Šค๊ฐ€ vision/zone/enter ๊ตฌ๋… ์ฝœ๋ฐฑ ์‹คํ–‰
5. LangGraph: assemble_context
   - HA REST: GET /api/states/sensor.session_a_mood โ†’ "neutral"
   - HA REST: GET /api/states/sensor.weather_condition โ†’ "sunny"
   - ๋กœ๋ด‡ ์ƒํƒœ: GET /api/states/sensor.spot_01_pose
6. LangGraph: Claude tool use ํ˜ธ์ถœ (mcp-robot-adapter, mcp-tablet-gateway ๋…ธ์ถœ)
7. Claude ์‘๋‹ต โ†’ tool_calls:
   - robot_command(robot_id="spot-01", capability="look_at", params={target:"sess_A"})
   - showOptions(tablet_id="t01", options=["์ปคํ”ผ ์ถ”์ฒœ","์Œ์•… ์ถ”์ฒœ"])
8. guardrail ํ†ต๊ณผ โ†’ MCP ํ˜ธ์ถœ โ†’ ์‹ค์ œ ์‹คํ–‰
9. SPOT ๋™์ž‘, ํƒœ๋ธ”๋ฆฟ UI ๊ฐฑ์‹ , robot/spot-01/state 1Hz publish
10. HA ๋Œ€์‹œ๋ณด๋“œ์— ๋ชจ๋“  ๊ฒƒ์ด ๋ผ์ด๋ธŒ๋กœ ํ‘œ์‹œ

4.2 ํƒœ๋ธ”๋ฆฟ ์˜ต์…˜ ์„ ํƒ โ†’ ๋กœ๋ด‡ ๋”ฐ๋ผ์˜ค๊ธฐ

1. ์‚ฌ์šฉ์ž "๊ณต์›์—์„œ ์‚ฐ์ฑ… ๊ฐ™์ด ํ•˜๊ธฐ" ์„ ํƒ
2. ํƒœ๋ธ”๋ฆฟ โ†’ mqtt:tablet/t01/intent { option:"walk_with_me" }
3. LangGraph ๊ตฌ๋… ์ฝœ๋ฐฑ โ†’ assemble_context (ํ˜„ zone=park)
4. Claude โ†’ robot_command(spot-01, capability="follow", params={target:"sess_A", max_distance:2.0})
5. mcp-robot-adapter: safety_interlock ๊ฒ€์ฆ (proximity, zone polygon) โ†’ SPOT SDK ํ˜ธ์ถœ
6. 1Hz robot/spot-01/state ๋ฐœํ–‰ โ†’ ํƒœ๋ธ”๋ฆฟ ์ง„ํ–‰๋„ ํ‘œ์‹œ

5. ํŒจํ‚ค์ง• (docker-compose ๊ณจ๊ฒฉ)

services:
  mosquitto:
    image: eclipse-mosquitto:2.0
    ports: ["1883:1883"]
    volumes: ["./mosquitto:/mosquitto/config"]

  frigate:
    image: ghcr.io/blakeblackshear/frigate:stable
    devices: ["/dev/dri:/dev/dri"]   # ๋˜๋Š” NVIDIA GPU
    volumes:
      - ./frigate.yml:/config/config.yml
      - frigate_media:/media/frigate
    ports: ["5000:5000"]

  home-assistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    volumes: ["./ha-config:/config"]
    network_mode: host

  bridge:
    build: ./services/bridge        # frigate.events โ†’ vision.* ๋ณ€ํ™˜
    depends_on: [mosquitto]

  mcp-robot-adapter:
    build: ./services/mcp-robot-adapter
    devices: ["/dev/spot:/dev/spot"]    # ํ•„์š” ์‹œ
    environment:
      - SPOT_HOSTNAME=...
      - SPOT_USERNAME=...

  mcp-tablet-gateway:
    build: ./services/mcp-tablet-gateway

  langgraph-orchestrator:
    build: ./services/orchestrator
    extra_hosts:
      - "host.docker.internal:host-gateway"     # HA(host network) ๋„๋‹ฌ
    environment:
      - ANTHROPIC_API_KEY=...
      - HA_URL=http://host.docker.internal:8123
      - HA_TOKEN=...
    depends_on: [mosquitto, mcp-robot-adapter, mcp-tablet-gateway]

6. ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋Š” ๋ถ€๋ถ„ (์ข์•„์ง„ ๋ฒ”์œ„)

OSS ๋ฐฑ๋ณธ์„ ์ฑ„ํƒํ•˜๋ฉด ์ž์ฒด ๊ตฌํ˜„ ํ•„์š” ์˜์—ญ์€ ๋‹ค์Œ 4๊ฐœ๋กœ ์ถ•์†Œ๋œ๋‹ค.

๋ชจ๋“ˆ ์ถ”์ • ๊ทœ๋ชจ ํ•ต์‹ฌ ์ฑ…์ž„
services/bridge ~200 LoC(Lines of Code, ์ฝ”๋“œ ๋ผ์ธ ์ˆ˜) Frigate ์ด๋ฒคํŠธ โ†’ ๋„๋ฉ”์ธ ํ† ํ”ฝ ๋ณ€ํ™˜, ReID ์„ธ์…˜ ๋งค์นญ
services/mcp-robot-adapter ~1.5K LoC SPOT/๋ฏธ์ • ๋กœ๋ด‡ SDK ๋ž˜ํ•‘, ์•ˆ์ „ ์ธํ„ฐ๋ก, MCP ๋„๊ตฌ ๋…ธ์ถœ
services/mcp-tablet-gateway ~500 LoC ํƒœ๋ธ”๋ฆฟ WSS(WebSocket Secure, ๋ณด์•ˆ ์›น์†Œ์ผ“), ์˜ต์…˜ push, intent ์ˆ˜์‹ 
services/orchestrator ~800 LoC LangGraph ๊ทธ๋ž˜ํ”„ ์ •์˜, Claude ํ˜ธ์ถœ, prompt/playbook
apps/tablet ~3K LoC React Native UI, ์ธํ„ฐ๋ž™์…˜

์ž์ฒด ์ธํ”„๋ผ(MQTT, ์ƒํƒœ ์ €์žฅ, ๋Œ€์‹œ๋ณด๋“œ, NVR(Network Video Recorder, ๋„คํŠธ์›Œํฌ ๋น„๋””์˜ค ๋ ˆ์ฝ”๋”), ๊ฐ์ฒด ํƒ์ง€)๋Š” ๋ชจ๋‘ OSS๊ฐ€ ์ œ๊ณต.


7. ํ†ตํ•ฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ

  • MQTT QoS(Quality of Service, ์„œ๋น„์Šค ํ’ˆ์งˆ ๋“ฑ๊ธ‰): ๋ช…๋ น(robot/.../command)์€ QoS 1 ๋˜๋Š” 2, ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ(robot/.../state)๋Š” 0 ๋˜๋Š” 1.
  • HA recorder retention: ๊ธฐ๋ณธ 10์ผ์ด์ง€๋งŒ ์ด ์‹œ์Šคํ…œ์€ ํ†ต๊ณ„ ๋ณด์กด์ด ๋ชฉ์  ์•„๋‹ˆ๋ฏ€๋กœ 2~3์ผ๋กœ ์ถ•์†Œ (recorder.purge_keep_days: 3).
  • MCP ๊ถŒํ•œ: mcp-robot-adapter๋Š” mTLS(mutual TLS, ์ƒํ˜ธ ์ธ์ฆ TLS) + ํ† ํฐ. LangGraph๋งŒ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•˜๊ฒŒ ACL(Access Control List, ์ ‘๊ทผ ์ œ์–ด ๋ชฉ๋ก).
  • Frigate ReID ํ•œ๊ณ„: ๋‹จ์ผ ์นด๋ฉ”๋ผ ์ถ”์ ์€ ๊ฐ•ํ•˜์ง€๋งŒ cross-camera๋Š” ๋ณด๊ฐ• ํ•„์š” โ€” services/bridge์—์„œ ์ž„๋ฒ ๋”ฉ ๋งค์นญ ๋ณด๊ฐ• ๋˜๋Š” BoxMOT ์‚ฌ์ด๋“œ์นด.
  • HA ์ž๋™ํ™” vs LangGraph ์ฑ…์ž„ ๊ฒฝ๊ณ„: ๋‹จ์ˆœ ๋ฃฐ(์˜ˆ: ๋กœ๋ด‡ ๋ฐฐํ„ฐ๋ฆฌ < 20% ์‹œ ์•Œ๋ฆผ)์€ HA ์ž๋™ํ™”๋กœ, ์ปจํ…์ŠคํŠธ ์˜์กด ์˜์‚ฌ๊ฒฐ์ •์€ LangGraph๋กœ. ๊ฐ™์€ ๊ฒฐ์ •์„ ๋‘ ๊ณณ์—์„œ ํ•˜๋ฉด ์ถฉ๋Œ๋‚œ๋‹ค.
  • prompt cache: zone playbook + system prompt๋Š” stable โ†’ Anthropic prompt cache๋กœ ๋น„์šฉยท์ง€์—ฐ ์ ˆ๊ฐ.

8. ์‚ฌ์ „ ํ•ฉ์˜ ์‚ฌํ•ญ (์ž์ฃผ ๋ฌป๋Š” ๊ฒฐ์ • ์‚ฌํ•ญ)

Q1. Frigate native zone vs bridge vision/zone/enter โ€” ์—ญํ•  ๋ถ„๋ฆฌ

  • Frigate native (frigate/{cam}/{zone} ON/OFF): ๋‹จ์ˆœ zone ์ ์œ  ํ‘œ์‹œ์šฉ. HA๊ฐ€ binary_sensor.<zone>_occupied๋กœ ์ž๋™ ํก์ˆ˜. ์šด์˜์ž ๋Œ€์‹œ๋ณด๋“œ ์ƒ๋‹จ "ํ˜„์žฌ ์ ์œ  zone" ์นด๋“œ์šฉ์œผ๋กœ๋งŒ ์‚ฌ์šฉ.
  • bridge vision/zone/enter|exit: session_id ๋งค์นญ๋œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ. orchestrator ํŠธ๋ฆฌ๊ฑฐยท์„ธ์…˜ zone history ์ถ”์ ์šฉ.
  • ๋‘ ๊ฒฝ๋กœ๊ฐ€ ์ •๋ณด๊ฐ€ ๊ฒน์น˜๋Š” ๋“ฏํ•˜์ง€๋งŒ ์†Œ๋น„์ž๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ์˜๋„๋œ ์ค‘๋ณต. orchestrator๊ฐ€ Frigate native ํ† ํ”ฝ์„ ์ง์ ‘ ๊ตฌ๋…ํ•˜์ง€ ์•Š๊ฒŒ ACL๋กœ ์ฐจ๋‹จ.

Q2. orchestrator ๋ช…์นญ

  • ์„œ๋น„์Šค ์ด๋ฆ„์€ orchestrator (๋””๋ ‰ํ„ฐ๋ฆฌ services/orchestrator).
  • ๋‚ด๋ถ€์—์„œ LangGraph๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ "LangGraph ์„œ๋น„์Šค"๋ผ ๋ถ€๋ฅด์ง€ ์•Š์Œ โ€” LangGraph๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌยท๊ตฌํ˜„ ๋„๊ตฌ.
  • ๋ฌธ์„œยท๋กœ๊ทธยท๋ฉ”ํŠธ๋ฆญ์—์„œ ์ผ๊ด€๋˜๊ฒŒ orchestrator๋กœ ํ‘œ๊ธฐ.

Q3. MCP transport โ€” HTTP/SSE ๊ถŒ์žฅ

  • MCP ํ‘œ์ค€ transport 3์ข…: stdio, SSE, HTTP.
  • ๋ณธ ์‹œ์Šคํ…œ์€ SSE over HTTP ์ฑ„ํƒ:
    • ์ปจํ…Œ์ด๋„ˆ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด stdio ๋ถ€์ ํ•ฉ.
    • SSE๋Š” ์–‘๋ฐฉํ–ฅ long-lived ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋„๊ตฌ ํ˜ธ์ถœ + ์ง„ํ–‰ ์•Œ๋ฆผ์— ์ ํ•ฉ.
    • HA REST์™€ ๋™์ผ ๋ผ์šฐํŒ…(์—ญํ”„๋ก์‹œ ๋‹จ์ˆœํ™”).
  • mcp-robot-adapterยทmcp-tablet-gateway ๋‘˜ ๋‹ค :8000/mcp ์—”๋“œํฌ์ธํŠธ ๋…ธ์ถœ. orchestrator๊ฐ€ ํด๋ผ์ด์–ธํŠธ๋กœ ์—ฐ๊ฒฐ.
  • ์ธ์ฆ: opaque bearer token (๊ฐ ์„œ๋น„์Šค์˜ .env.secret์— ๋ฐœ๊ธ‰), ์ƒํ˜ธ mTLS๋Š” ๋ฏธ์ ์šฉ(LAN ๋‚ด๋ถ€).

Q4. Zone enter/exit debounce ์ž„๊ณ„๊ฐ’

  • enter hold: 800ms โ€” NFR 1.5s ์ด๋‚ด ๋ณด์žฅ.
  • exit grace: 3000ms โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์ž ๊น ๊ฐ€๋ ค์ ธ๋„ exit ์•ˆ ๋ฐœ๋™.
  • dedup window: ๋™์ผ (session_id, zone_id) enter๋Š” 1000ms ๋‚ด ์žฌ๋ฐœํ–‰ ์ฐจ๋‹จ.
  • ์ž„๊ณ„๊ฐ’์€ config/bridge.yaml์—์„œ ์šด์˜ ์ค‘ ์กฐ์ • ๊ฐ€๋Šฅ (์žฌ๊ธฐ๋™ ํ•„์š”).

Q5. tablet/+/show_options TTL ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ

  • ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ์ž: mcp-tablet-gateway๊ฐ€ ๋‹ด๋‹น.
  • ttl_s ๋งŒ๋ฃŒ ์‹œ ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ํƒœ๋ธ”๋ฆฟ์— {type: "dismiss_options", message_id} ์ž๋™ push โ†’ ํƒœ๋ธ”๋ฆฟ UI๊ฐ€ ์˜ต์…˜ ์˜์—ญ ๋น„์›€.
  • ๋งŒ๋ฃŒ ํ›„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ๊ฒŒ์ดํŠธ์›จ์ด์—์„œ "stale_intent" ์‘๋‹ต์œผ๋กœ ๊ฑฐ๋ถ€ + Loki ๋กœ๊ทธ ํ•œ ์ค„.
  • ๋งŒ๋ฃŒ ์ „ ์ƒˆ show_options(๊ฐ™์€ tablet_id)๊ฐ€ ๋„์ฐฉํ•˜๋ฉด ์ด์ „ message_id๋Š” ์ฆ‰์‹œ ํ๊ธฐ.

9. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งคํ•‘

SYSTEM_DESIGN.md ์ปดํฌ๋„ŒํŠธ โ†’ OSS ๋งคํ•‘:

์„ค๊ณ„ ๋ฌธ์„œ ์ปดํฌ๋„ŒํŠธ OSS ์ฑ„ํƒ
ยง3.1 Central Context Server HA recorder (SQLite) + entity state
ยง3.2 Vision Tracking Frigate + bridge + ReID ๋ณด๊ฐ•
ยง3.3 Robot Adapter ์ž์ฒด ๊ตฌํ˜„ (mcp-robot-adapter)
ยง3.4 LLM Orchestrator LangGraph + Claude Agent SDK
ยง3.5 Tablet Application ์ž์ฒด ๊ตฌํ˜„ (HA WebSocket API ํ™œ์šฉ)
ยง3.6 Monitoring & Dashboard HA Lovelace + Prometheus/Grafana(๋ฉ”ํŠธ๋ฆญ)
ยง6.2 ๋ฉ”์‹œ์ง• (MQTT) Mosquitto (๋˜๋Š” HA ๋‚ด์žฅ)
โš ๏ธ **GitHub.com Fallback** โš ๏ธ