SERVICES - zero1ne-26/doc GitHub Wiki
ê° ìë¹ì€ê° ìŽë€ OSS륌 ìŽë»ê² íì©íê³ , ìŽëê¹ì§ ì§ì ë§ë€ìŽìŒ íëì§ ì 늬. ë©ìì§ ì€í€ë§ ììžë MQTT_SCHEMA.md ì°žì¡°.
| ìë¹ì€ | OSS ê·žëë¡ | ì€ì ìì± | ìœë ìì± |
|---|---|---|---|
mosquitto |
â | â | â |
frigate |
â | â | â |
cctv-relay (go2rtc) |
â | â | â |
home-assistant |
â | â | â |
bridge |
(ëŒìŽëžë¬ëЬë§) | â | â |
orchestrator |
(ëŒìŽëžë¬ëЬë§) | â | â |
mcp-robot-adapter |
(ëŒìŽëžë¬ëЬë§) | â | â |
mcp-tablet-gateway |
(ëŒìŽëžë¬ëЬë§) | â | â |
tablet-app |
(íë ììí¬ë§) | â | â |
- ìŽë¯žì§:
eclipse-mosquitto:2.0 - ëŒìŽì ì€: EPL/EDL
- ê·žëë¡ ì¬ì©. ì€ì íìŒë§.
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/acl
persistence true
persistence_location /mosquitto/data/
log_dest stdout
# ë©ìì§ ë³Žì¡Ž ì ì±
(ì ì ìŽì ì€ ìì )
max_inflight_messages 100
max_queued_messages 1000ìì¹: ìµì ê¶í. ê° ìë¹ì€ë ìêž° ì±
ì í íœë§ write, íìí í íœë§ read. 몚ë ì첎 ìë¹ì€ë system/heartbeat/<ìêž°ìŽëŠ> ë°í ê¶íì ê°ì§ë€.
# ââââââââââââââââââââââââââââââââââââââââââ
# frigate: ì첎 í íœë§ ë°í
# ââââââââââââââââââââââââââââââââââââââââââ
user frigate
topic write frigate/#
# ââââââââââââââââââââââââââââââââââââââââââ
# bridge: frigate/# 구ë
, vision/# + session/+ + safety/proximity ë°í
# ââââââââââââââââââââââââââââââââââââââââââ
user bridge
topic read frigate/#
topic write vision/zone/enter
topic write vision/zone/exit
topic write vision/position/update
topic write session/start
topic write safety/proximity
topic read session/end # ìë² ë© íêž° ížëŠ¬ê±° ìì (SCRUB)
topic write system/heartbeat/bridge
# ââââââââââââââââââââââââââââââââââââââââââ
# orchestrator: ëª
ë ¹ì MCPë¡ ì ë¬ (MQTT ë°í X)
# ââââââââââââââââââââââââââââââââââââââââââ
user orchestrator
topic read vision/zone/enter
topic read vision/zone/exit
topic read vision/position/update
topic read robot/+/state
topic read robot/+/ack
topic read robot/+/fault
topic read tablet/+/intent
topic read tablet/+/ack
topic read context/+/+/update
topic read context/environmental/update
topic read safety/+
topic read session/start
topic read session/end
topic write tablet/+/show_options
topic write tablet/+/show_message
topic write context/user/+/update
topic write session/end # timeout ížëŠ¬ê±°
topic write system/heartbeat/orchestrator
# ââââââââââââââââââââââââââââââââââââââââââ
# mcp-robot-adapter: ë¡ëŽ ì ì©
# ââââââââââââââââââââââââââââââââââââââââââ
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
# ââââââââââââââââââââââââââââââââââââââââââ
# mcp-tablet-gateway: íëžëŠ¿ ì ì©
# ââââââââââââââââââââââââââââââââââââââââââ
user tablet-gateway
topic write tablet/+/intent
topic write tablet/+/ack
topic read tablet/+/show_options
topic read tablet/+/show_message
topic read session/end # SCRUB ížëŠ¬ê±° ìì (í í° ë¬Žíší·WSS ì¢
ë£)
topic write system/heartbeat/tablet-gateway
# ââââââââââââââââââââââââââââââââââââââââââ
# home-assistant: ëŒìŽëž 몚ëí°ë§Â·íŽë°± ìëí·ìŽìì ëª
ë ¹
# ââââââââââââââââââââââââââââââââââââââââââ
user homeassistant
topic read vision/#
topic read robot/+/state
topic read robot/+/ack
topic read robot/+/fault
topic read tablet/+/intent
topic read tablet/+/ack
topic read context/+/+/update
topic read safety/#
topic read session/+
topic read system/heartbeat/+
topic write context/environmental/update
topic write safety/estop # ìŽìì e-stop ìœì
topic write robot/+/command # LLM íŽë°± 겜ë¡
topic write tablet/+/show_options # íŽë°± ì ìµì
ë
žì¶
topic write tablet/+/show_message
topic write session/end # ìŽìì ì¢
ë£ ížëŠ¬ê±°
topic write system/heartbeat/homeassistant
ë¹ë°ë²íž íìŒ ê¶í:
passwdíìŒìchmod 600, ownerë mosquitto 컚í ìŽë uid(1883). ížì€íž ëë í°ëЬ(./mosquitto/config)ë ëìŒíê² ì í.
- ë¹ë°ë²ížë
mosquitto_passwd -c passwd <user>ë¡ ìì±,.env.secretì ë³ŽêŽ X (íìŒ ìì²Žê° ë¹ë°). - TLS(Transport Layer Security, ì ì¡ ê³ìžµ 볎ì)ë ëšìŒ ì¬ìŽíž LAN ëŽë¶ìì íì ìë. ìžë¶ ë žì¶ ì 8883 + cert.
- ìŽë¯žì§:
ghcr.io/blakeblackshear/frigate:0.14.1 - ëŒìŽì ì€: MIT
-
ê·žëë¡ ì¬ì© +
config.yml+ zone polygon + 칎ë©ëŒ ì¶ê°. - Cross-camera ReID(Re-identification, 볎íì ì¬ìë³)ë ë¶ì¡± â
bridgeìë¹ì€ìì 볎ê°.
ìì¹: config.ymlì gitì ì»€ë° ê°ë¥íŽìŒ íë¯ë¡ ì격ìŠëª
ì í묞ìŒë¡ ëì§ ìëë€. Frigate 0.13+ ë {ENV_VAR} ííë¡ í겜ë³ì ì¹íì ì§ìíë©°, 컚í
ìŽë í겜ë³ì(env_file)ë¡ ì£Œì
íë€.
mqtt:
host: mosquitto
port: 1883
user: frigate
password: '{FRIGATE_MQTT_PASSWORD}' # í겜ë³ì ì¹í (Frigate 0.13+)
topic_prefix: frigate
detectors:
coral:
type: edgetpu
device: usb
# ëë NVIDIA: type: tensorrt
cameras:
cafe_cam_01:
ffmpeg:
inputs:
- path: rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@10.0.1.11:554/stream
roles: [detect, record]
detect:
width: 1280
height: 720
fps: 5
objects:
track: [person]
zones:
cafe_zone:
coordinates: 0,720,0,0,1280,0,1280,720
objects: [person]
record:
enabled: false # ìì ì ì¥ ì íš (ê°ìžì 볎)
snapshots:
enabled: false
park_cam_01: { ... }
vehicle_cam_01: { ... }
home_cam_01: { ... }composeìì 죌ì :
# deploy/docker-compose.yml (frigate ìë¹ì€)
frigate:
image: ghcr.io/blakeblackshear/frigate:0.14.1
env_file: [./.env, ./.env.secret] # FRIGATE_MQTT_PASSWORD, FRIGATE_RTSP_USER, FRIGATE_RTSP_PASSWORD
....env.secret ì ì (gitignore):
FRIGATE_MQTT_PASSWORD=...
FRIGATE_RTSP_USER=admin
FRIGATE_RTSP_PASSWORD=...칎ë©ëŒë§ë€ ì격ìŠëª ìŽ ë€ë¥Žë©Ž
FRIGATE_RTSP_PASSWORD_CAFE_01ë±ìŒë¡ ë¶ëЬ. Frigateë ìì í겜ë³ìëª ì 몚ë ì¹ííë€.
ëžëŒì°ì ëŒìŽëž ìì(WebRTC)ì Frigateê° ìëëŒ **ë 늜 ìë¹ì€
cctv-relay**ê° ëŽë¹íë€ â §2.5. Frigateë detect/ìŽë²€íž ì ì©ìŒë¡ ì ì§íë€.
-
frigate/eventsâ ìŽë²€íž JSON -
frigate/{camera}/{label}â 칎ìŽíž -
frigate/{camera}/{zone}â ì ì ON/OFF
- ëªšëž ì íë í¥ìì labelmap·threshold íëìŒë¡ ìì, 컀ì€í 몚ëžì íìì.
- GPU/TPU ì í: Jetsonì TensorRT(NVIDIA TensorRT, NVIDIA ë¥ë¬ë ì¶ë¡ ìµì í ìì§), USB í겜ì Coral, x86 GPUë NVIDIA TensorRT detector.
ì ìì¥ ë°ëªš íì . ëžëŒì°ì ë RTSP륌 ì§ì ì¬ìíì§ ëª»íë¯ë¡(raw socket ë¶ê°) RTSP륌 ëžëŒì°ì íží ì ì¡(WebRTC)ìŒë¡ ë³í·ì€ê³íë ë 늜 ìë¹ì€ë€. Frigate(detect/ìŽë²€íž)ì ë¶ëŠ¬íŽ ëŒìŽëž ììë§ ì± ìì§ë€. ì€ê³ 귌거·믞ëìŽ í멎 ë¶ëЬë SYSTEM_DESIGN.md §8.4.
go2rtcë 칎ë©ëŒ ì€ížëŠŒì ì¬ë¬ íë¡í ìœë¡ ì€ìê° ë³í·ì€ê³íë ëšìŒ ë°ìŽë늬 ì€ížëŠ¬ë° ìë²ë€(Goë¡ ìì±, MIT). íëì RTSP/RTMP/HTTP ìì€ë¥Œ ë°ì WebRTC·MSE·HLS·RTSP ë± íŽëŒìŽìžížê° ì구íë ì ì¡ ë°©ììŒë¡ ì¬íší€ì§(restream) íŽ ëŽë³Žëžë€ â 볞 íë¡ì ížìì íìí "ëžëŒì°ì ê° ëª» ë°ë RTSP â ëžëŒì°ì ê° ë°ë WebRTC" ë³íìŽ ì íí íµì¬ êž°ë¥ìŽë€.
| í¹ì§ | 볞 íë¡ì ížììì ì믞 |
|---|---|
| WebRTC + WHEP ëŽì¥ | ëžëŒì°ì ê° íì€ WHEP(SDP offer/answer)ë¡ ë°ë¡ ì°ê²° â ë³ë ìê·žëë§ ìë² ë¶íì |
| zero-copy íšì€ì€ë£š | ìì€ê° H.264멎 ížëì€ìœë© ììŽ ê·žëë¡ ì ë¬(ì ì§ì°Â·ì ë¶í). H.265 ë± ë¹íží ìœë±ë§ ffmpeg: producerë¡ ë³í |
| config 구ë |
streams: YAML ë§µì ìì€ í ì€ ì¶ê°ë¡ ì€ížëŠŒ ë±ë¡, 첫 consumer ì°ê²° ì lazy ì°ê²° â ì첎 ìœë ë¶íì |
| ëšìŒ ë°ìŽë늬(~50MB) | ìì¡Žì± ìë ìì 컚í ìŽë, Frigateì ë¶ëŠ¬íŽ ë 늜 ë°°í¬ ì©ìŽ |
| ì ì¥ ì íš | Ʞ볞ì ìŒë¡ ë ¹í·ì€ë ì· ììŽ ëŒìŽëž íµê³Œë§ â ê°ìžì 볎 ì ì± (§11.3)곌 묎충ë |
ì°žê³ : go2rtcë Frigateì ëŽì¥ë ê²ê³Œ ëìŒí ìì§ìŽë€(Frigateê° ëŒìŽëž 볎Ʞì©ìŒë¡ go2rtc륌 ë²ë€). 볞 íë¡ì ížë detect íìŽíëŒìžê³Œì ì¥ì 격늬륌 ìíŽ ìŽë¥Œ ë²ë€ ëì ë 늜 ìë¹ì€ë¡ ëŒìŽ ìŽì©íë€(§2.5 Frigateìì êŽê³).
- ìŽë¯žì§:
ghcr.io/alexxit/go2rtc:1.9.14(ë²ì í) - ëŒìŽì ì€: MIT
- íë¡ì íž: https://github.com/AlexxIT/go2rtc
- ëì: MediaMTX(구 rtsp-simple-server) â WHEP ì§ìíë 볞 íë¡ì ížë go2rtc íì€ ì±í.
-
ê·žëë¡ ì¬ì© +
config/cctv-relay.yamlìì±. RTSPâWebRTC ë³í·ICE·WHEPë go2rtcê° ì ëŽíë¯ë¡ ì첎 ìœë ìì. - ì í: ëšìŒ ìì€(
config/cameras.yaml)ìì ìŽ ì€ì ì ìì±íë ë¹ë ì€í¬ëŠœíž(ìë "ëšìŒ ìì€" ì°žì¡°).
config/cctv-relay.yaml (컚í
ìŽëì ë§ìŽíž). streams: ë§µì ì볞 RTSP 죌ì륌 í ì€ ì¶ê°í멎 ê·ž ì€ížëŠŒìŽ ìë ë±ë¡Â·ì°ê²°ëë€. 칎ë©ëŒ nëë í목 nê°ë¡ íì¥ëë€.
# config/cctv-relay.yaml (go2rtc ì€ì )
streams:
cafe_cam_01: rtsp://${RTSP_USER}:${RTSP_PASSWORD}@10.0.1.11:554/stream # ì볞 ìì€ë§ ì ìŒë©Ž ë
park_cam_01: rtsp://${RTSP_USER}:${RTSP_PASSWORD}@10.0.1.12:554/stream
vehicle_cam_01: rtsp://${RTSP_USER}:${RTSP_PASSWORD}@10.0.1.13:554/stream
home_cam_01: rtsp://${RTSP_USER}:${RTSP_PASSWORD}@10.0.1.14:554/stream
# H.265 칎ë©ëŒë ëžëŒì°ì WebRTC íží(H.264)ìŒë¡ ížëì€ìœë:
# lobby_cam_01:
# - rtsp://${RTSP_USER}:${RTSP_PASSWORD}@10.0.1.15:554/stream
# - 'ffmpeg:lobby_cam_01#video=h264#audio=opus'
webrtc:
candidates:
- 10.0.1.10:8555 # ì ìì¥ LANìì ëžëŒì°ì ê° ë¿ë ížì€íž IP:í¬íž (host candidate)
api:
listen: ":1984" # WHEP/REST (BFF ìê·žëë§ì©, ìžë¶ ë¹ê³µê°)-
ìë ì°ê²°: go2rtcë
streams:í목ì 첫 consumer(WHEP ì°ê²°) ì lazyë¡ ìë ì°ê²°íë€. í목 ì¶ê° í ìë¹ì€ reload(ëë ì¬ìì)ë§ í멎 ì 칎ë©ëŒê° ìŠì ì¬ì© ê°ë¥ â ìœë ë³ê²œ ë¶íì. íì ì°ê²°(í늬ìë°)ìŽ íìí멎 íŽë¹ ì€ížëŠŒìffmpeg:...producer륌 ëìŽ ìì íì±ìŒë¡ ë§ë ë€. -
ì격ìŠëª
:
${RTSP_USER}/${RTSP_PASSWORD}ë go2rtcì env ì¹í. í묞 êžì§, 컚í ìŽë env(.env.secret)ë¡ ì£Œì .
ëšìŒ ìì€(ì í): RTSP 죌ì륌 Frigateì ì€ë³µ êŽëЬíì§ ììŒë €ë©Ž, SYSTEM_DESIGN.md §8.3.3ì zone ëšìŒ-ìì€ íšíŽê³Œ ëìŒíê² config/cameras.yaml(칎ë©ëŒë¹ rtsp/zone/codec) íëìì scripts/gen-cctv-relay.pyê° config/cctv-relay.yamlì, scripts/gen-frigate-config.pyê° Frigate cameras:륌 ê°ê° ìì±íë€.
- ëžëŒì°ì ë same-originìŒë¡ tablet-fe BFF(
/api/cctv/webrtc/<cam>)ì WHEP offer(SDP)륌 볎ëžë€ â BFFê°cctv-relayì go2rtc API(/api/webrtc?src=<cam>,:1984)ë¡ íë¡ìíê³ answer륌 ë°í. - íì í 믞ëìŽ(SRTP)ë ëžëŒì°ì â cctv-relay ICEë¡ ì§ì (P2P) â Node BFFë ìê·žëë§ë§ ì€ê³, 믞ëìŽë ê±°ì¹ì§ ìëë€. LAN íì ìŽëŒ STUN/TURN ë¶íì.
- RTSP ì격ìŠëª ·relay 죌ìë ìë²ìë§ ë³Žì â ëžëŒì°ì ë²ë€ ë žì¶ êžì§(CCTV SSE ëžëЬì§ì ëìŒ ìì¹).
-
Ʞ볞(ê¶ì¥): ë
늜 pull â
cctv-relayê° ì¹Žë©ëŒìì ì§ì RTSP륌 ê°ì žìšë€. Frigate detect íìŽíëŒìžê³Œ ìì ë¶ëЬ â í쪜 ì¥ì ê° ë€ë¥ž 쪜ì ì íëì§ ìì. ë¹ì©: 칎ë©ëŒë¹ ì°ê²° 2ê°(detect + relay), ì ë ¥ ëìí ì€ë³µ(§DEPLOYMENT 0.4). - ëì(ëšìŒ pull): Frigate detect ì
ë ¥ì
rtsp://cctv-relay:8554/<cam>ë¡ ëìŽ ì¹Žë©ëŒ ì°ê²°ì 1ê°ë¡ ì€ìŒ ì ììŒë, Frigateê° relay ê°ì©ì±ì ì¢ ìëë¯ë¡ ë¹ì íìŽíëŒìž ìì ì±ê³Œ ížë ìŽëì€í. 칎ë©ëŒ ëì ì°ê²° ì íìŽ ë¹¡ë¹¡í ëë§ ì±í.
-
8555/tcp+udpâ WebRTC 믞ëìŽ(ëžëŒì°ì âcctv-relay). -
1984/tcpâ go2rtc API/WHEP(BFF ìê·žëë§ì©, ìžë¶ ë¹ê³µê°). -
8554/tcpâ (ì í) RTSP restream, ëšìŒ-pull í íŽë¡ì§ììë§ ë žì¶.
# deploy/docker-compose.yml (cctv-relay ìë¹ì€)
cctv-relay:
image: ghcr.io/alexxit/go2rtc:1.9.14
# command 믞ì§ì â ìŽë¯žì§ Ʞ볞 Cmd(go2rtc -config /config/go2rtc.yaml) ì¬ì©.
# command륌 ë®ìŽì°ë©Ž go2rtc ë°ìŽëëŠ¬ê° ì§ìì ž tiniê° ìžìë§ exec â FATAL.
volumes: ["./config/cctv-relay.yaml:/config/go2rtc.yaml:ro"] # 컚í
ìŽë Ʞ볞 겜ë¡ë¡ ë§ìŽíž
env_file: [./.env.secret] # RTSP_USER, RTSP_PASSWORD
ports: ["8555:8555/tcp", "8555:8555/udp"] # 1984ì LAN ëŽë¶(BFF)ë§ â ìžë¶ 믞ë
žì¶
restart: unless-stopped- ìì ë¹ì ì¥: go2rtcë Ʞ볞ì ìŒë¡ ë ¹ííì§ ìì â ëŒìŽëž íµê³Œë§(ê°ìžì 볎 ì ì± Â§11.3ì 묎충ë).
-
ìœë± íìž ì ê²°: WebRTCë H.264ë§ ì€ì§ ì§ì(HEVC ë¶ê°). ì ê· ì¹Žë©ëŒë
ffprobe rtsp://...ë¡ íìž í, H.265멎 ì transcode í목 ì¶ê°. -
íŽë°±: WebRTC íì ì€íš ì MSE(fMP4 over WS, go2rtc
/api/ws)ë¡ íŽë°±.
- ìŽë¯žì§:
ghcr.io/home-assistant/home-assistant:2026.1 - ëŒìŽì ì€: Apache 2.0
-
ê·žëë¡ ì¬ì© + ë€ìì ì°ëŠ¬ê° ìì±:
- MQTT(Message Queuing Telemetry Transport, ë©ìì§ í í ë ë©ížëЬ ì ì¡ íë¡í ìœ) integration íì±í
- ìí°í° ì ì (template sensor, MQTT discovery)
- ìëí (rule-based íŽë°± ìë늬ì€)
- Lovelace ëì볎ë YAML
- Zone playbookì© input_select / input_boolean
ha-config/
âââ configuration.yaml
âââ automations.yaml
âââ scripts.yaml
âââ secrets.yaml # gitignore
âââ packages/
â âââ sessions.yaml # ìžì
ìí°í°
â âââ robots.yaml # ë¡ëŽ ìí°í°
â âââ zones.yaml # ê³µê° ì ì ìí°í°
â âââ fallback.yaml # LLM íŽë°± ìëí
âââ lovelace/
âââ operator.yaml # ìŽìì ëì볎ë
mqtt:
broker: mosquitto
username: !secret mqtt_user
password: !secret mqtt_password
discovery: true
discovery_prefix: homeassistant
recorder:
purge_keep_days: 3
exclude:
domains: [automation, updater]
http:
use_x_forwarded_for: true
trusted_proxies: [127.0.0.1]
homeassistant:
packages: !include_dir_named packagesLLM(Large Language Model, ëê·ëªš ìžìŽ ëªšëž)ìŽ ìëµ ëª» íë ëììë ìì°ìŽ ëêž°ì§ ìëë¡ ëªšë zoneì ì ì ìë늬ì€ë¥Œ ì¬ì ì ìíë€. ížëŠ¬ê±°ë ê³µíµ â "orchestrator heartbeat 30ìŽ ìŽì 믞ìì + zone enter ëë tablet intent ë°ì".
| Zone | ížëŠ¬ê±° | íŽë°± ì¡ì (ë¡ëŽ + íëžëŠ¿) |
|---|---|---|
cafe |
vision/zone/enter |
⢠SPOT look_at (ì¬ì©ì) ⢠íëžëŠ¿: ìµì ["ì»€íŒ ì¶ì² (ì€íëŒìž)", "ì¡°ì©í ëë¬ë³Žêž°"]
|
park |
vision/zone/enter |
⢠SPOT gesture: wave ⢠íëžëŠ¿: ë©ìì§ "ê³µì ì°ì± ë¡ë¥Œ ë°ëŒ ìŽëíŽë³Žìžì" + ìµì ["ë¡ëŽê³Œ ì°ì±
", "íŒì ì°ì±
"]
|
vehicle |
vision/zone/enter |
⢠ë¡ëŽ ëª
ë ¹ ìì (ì°šë ëŽë¶ íì) ⢠íëžëŠ¿: ë©ìì§ "ìŽì ìì ìì볎ìžì" + ìµì ["ì€ëì€ ë°ëªš", "ëì€íë ìŽ ë°ëªš"]
|
home |
vision/zone/enter |
⢠SPOT sit (ì€ëŽ ìì ììž) ⢠íëžëŠ¿: ìµì ["ê±°ì€ ë¶ìêž°", "죌방 ë¶ìêž°", "ì¹šì€ ë¶ìêž°"]
|
| any |
tablet/+/intent (ìµì
ì í) |
⢠íëžëŠ¿: ë©ìì§ "ì í ê°ì¬í©ëë€. ìŽìììê² ìëŽë늎ê²ì." (ì ì ìëµ) |
| any | safety/estop active=true |
⢠몚ë íì± íëžëŠ¿: ë©ìì§ "ì ì ìì°ì ë©ì¶ììµëë€" íì |
packages/fallback.yaml (몚ë zone ê³µíµ í¬íŒ):
# orchestrator íì± ì¬ë¶ sensor
template:
- binary_sensor:
- name: orchestrator_alive
state: >
{{ (now() - states.sensor.orchestrator_heartbeat.last_updated).total_seconds() < 30 }}
availability: "{{ states('sensor.orchestrator_heartbeat') not in ['unknown', 'unavailable'] }}"
# ê³µíµ trigger·condition ë³ìë¡ ë¬¶ì
input_text:
fallback_last_session:
name: "ë§ì§ë§ íŽë°± ë°ë ìžì
"zoneë³ ìëí (4ê°):
automation:
- alias: "Fallback - cafe enter"
trigger:
- platform: mqtt
topic: vision/zone/enter
condition:
- condition: state
entity_id: binary_sensor.orchestrator_alive
state: 'off'
- condition: template
value_template: "{{ trigger.payload_json.data.zone_id == 'cafe' }}"
action:
- service: mqtt.publish
data:
topic: "robot/spot-01/command"
payload_template: >
{
"command_id": "{{ now().timestamp() | int | string }}",
"robot_id": "spot-01",
"capability": "look_at",
"params": {"target_session_id": "{{ trigger.payload_json.data.session_id }}"},
"priority": "operator",
"source": {"service": "home-assistant", "trace_id": "fallback-{{ now().timestamp() | int }}"},
"expires_at": null
}
qos: 1
- service: mqtt.publish
data:
topic: "tablet/{{ states('input_text.session_to_tablet_' ~ trigger.payload_json.data.session_id) }}/show_options"
payload: >
{
"tablet_id": "...",
"message_id": "fallback-cafe-{{ now().timestamp() | int }}",
"options": [
{"id": "fallback_drink", "label": "ì»€íŒ ì¶ì² (ì€íëŒìž)"},
{"id": "fallback_quiet", "label": "ì¡°ì©í ëë¬ë³Žêž°"}
],
"ttl_s": 60,
"timestamp": "{{ now().isoformat() }}"
}
qos: 1
- alias: "Fallback - park enter"
# ëìŒ íšíŽ, capability=gesture, params.name=wave, ìµì
ë³ê²œ
...
- alias: "Fallback - vehicle enter"
# ë¡ëŽ ëª
ë ¹ ìì, íëžëŠ¿ ë©ìì§ë§
...
- alias: "Fallback - home enter"
# capability=sit, ìµì
3ê°
...
- alias: "Fallback - tablet intent (LLM down)"
trigger:
- platform: mqtt
topic: tablet/+/intent
condition:
- condition: state
entity_id: binary_sensor.orchestrator_alive
state: 'off'
action:
- service: mqtt.publish
data:
topic: "tablet/{{ trigger.topic.split('/')[1] }}/show_message"
payload: >
{
"message_id": "fallback-ack-{{ now().timestamp() | int }}",
"content": {"text": "ì í ê°ì¬í©ëë€. ìŽìììê² ìëŽë늎ê²ì.", "media": [], "speaker": "system"},
"duration_s": 5,
"timestamp": "{{ now().isoformat() }}"
}
- alias: "Fallback - estop broadcast"
trigger:
- platform: mqtt
topic: safety/estop
condition:
- condition: template
value_template: "{{ trigger.payload_json.data.active == true }}"
action:
# 몚ë íì± íëžëŠ¿ì broadcast (ìžì
목ë¡ì HA entityë¡ ë³Žì )
- repeat:
for_each: "{{ state_attr('sensor.active_sessions', 'tablet_ids') | default([]) }}"
sequence:
- service: mqtt.publish
data:
topic: "tablet/{{ repeat.item }}/show_message"
payload: >
{
"content": {"text": "ì ì ìì°ì ë©ì¶ììµëë€", "speaker": "system"},
"duration_s": null
}- orchestrator 컚í
ìŽë륌
docker compose stopìŒë¡ ê°ì ì¢ ë£. - 30ìŽ í zone enter ížëŠ¬ê±° â ì 4ê° ìëí ê°ê° ë°ëëëì§ ëŒìŽëž í
ìŒë¡ íìž:
docker exec mosquitto mosquitto_sub -t '#' -v
- ê° zone ìë늬ì€ì ë©ìì§Â·ë¡ëŽ ëì 몚ë ê²ìŠ í orchestrator ì¬êž°ë.
- LLMìŽ ë³µê·í멎
binary_sensor.orchestrator_aliveê°onìŽ ëìŽ ìëí condition ì°šëš â ì¶ê° íŽë°± ë°ë ì íš. - ì§í ì€ìŽë íŽë°± ë©ìì§ë ì첎 TTL(60s)ë¡ ìì° ì멞. orchestratorì ì ê· ë©ìì§ê° ë®ìŽì°ë©Ž ìŠì êµì²Ž.
- Glance 칎ë: ë¡ëŽ ë°°í°ëŠ¬Â·ì°ê²°Â·íì¬ ëì
- Picture-elements: ì ìì¥ floor plan ìì íì± ìžì ìì¹ ì€ë²ë ìŽ
- Conditional 칎ë: e-stop(emergency stop, êžŽêž ì ì§) íì± ì í° ë¹šê° ë°°ë
- History graph: ìµê·Œ 1ìê° zone ì ì ë³í
- ë² ìŽì€:
python:3.12-slim -
paho-mqtt: MQTT íŽëŒìŽìžíž -
pydantic: ì€í€ë§ ê²ìŠ -
numpy,scipy: ìë² ë© ë§€ì¹ - (ì í)
boxmot: cross-camera ReID ìê³ ëŠ¬ìŠ â AGPL 죌ì
ì ë¶ ì첎 ê°ë°. ìžë¶ OSSë ëŒìŽëžë¬ëЬ ìì€ë§ íì©.
-
frigate/events구ë â ì°ëЬvision/zone/{enter|exit}í íœìŒë¡ ë³í - ìžì â track ID ë§€í: íëžëŠ¿ íìŽë§ ì 캡ì²í ìží ìë² ë©ê³Œ Frigate ì¶ì 결곌륌 ë§€ì¹
- Cross-camera ReID: 칎ë©ëŒ ê° ëìŒ ìžë¬Œ ë§€ì¹ (Frigate 믞ì§ì ìì)
- Position update륌 5Hzë¡ ë°í
Frigate frigate/eventsë ê°ì²Ž ëšìë¡ ë°íëë©°, ëìŒ personìŽ zone 겜ê³ìì ê¹ë¹¡ìŽê±°ë 칎ë©ëŒ ì í ì ë
žìŽìŠê° ë°ìíë€. ëšì ë³í ì ë€ì 묞ì ê° ìê¹:
- ê°ì personìŽ cafeâpark ìŽë ì€ ì ê¹ ë ë€ ON â ì€ë³µ enter.
- personìŽ zone 겜ê³ìì 뚞묎륎며 ON/OFF ë°ë³µ â ê¹ë¹¡ê±°ëЬë enter/exit.
- cross-camera ì í ì Frigate track ID ë³ê²œ â exit í ìŠì enter.
ë°ëŒì session_id ëšìë¡ "íì¬ zone" ìí륌 bridgeê° ë³Žì íê³ hysteresis(ìŽë ¥)ì ëìŽ íì íë€.
# bridge/zone_tracker.py
@dataclass
class SessionZoneState:
session_id: str
current_zone: ZoneId | None
candidate_zone: ZoneId | None # ì ìŽ ì€ í볎
candidate_since: datetime | None # í볎 ì§ì
ìê°
last_seen: datetime| ì ë ¥ | 조걎 | ì¶ë ¥ |
|---|---|---|
frigate/eventsì entered_zonesì ì zone Z ë±ì¥ (íì¬ ë€ë¥ž zone) |
Z != current_zone |
candidate_zone=Z, candidate_since=now |
candidate_zoneìŽ ENTER_HOLD_MS(800ms) ëì ì ì§ |
timer ë§ë£ |
vision/zone/exit (current_zone) â vision/zone/enter (Z) â current_zone=Z |
| candidate_zoneìŽ hold ì€ ë€ì ì¬ëŒì§ | flicker | candidate íêž° (ìŽì enterë 묎ì) |
frigate/eventsì person ì¬ëŒì§ (current_zones ë¹) |
last_seen ê°±ì ë§ | (ìŠì exit ì íš) |
last_seenìŽ EXIT_GRACE_MS(3000ms) ìŽì stale |
timer ë§ë£ |
vision/zone/exit (current_zone) â current_zone=None |
| cross-camera ì íìŒë¡ Frigate track ID ë³ê²œ | ReIDë¡ ê°ì session_id ë§€ì¹ | ëìŒ sessionìŒë¡ ì²ëЬ â exit/enter ë°í ì íš |
zone_tracker:
enter_hold_ms: 800 # ì§§ìŒë©Ž ê¹ë¹¡ê±°ëŠŒ, Ꞟ멎 NFR(1.5s) ìë°
exit_grace_ms: 3000 # ì§§ìŒë©Ž ì ê¹ ê°ëа ìžë¬Œë exit, Ꞟ멎 zone ì¬ìŽ ë¹ ê³µê° êžžìŽì§
stale_timeout_ms: 30000 # ìŽ ìê° í session ì첎륌 droppedë¡ ì²ëЬ(ë€ë¥ž ë©ì»€ëìŠ)async def on_frigate_event(event):
person = event["after"]
session_id = registry.match(person) # ReID
if not session_id:
return # íìŽë§ ì ë ìžë¬Œ â 묎ì
zones_now = set(person["current_zones"]) # ì: {"cafe_zone"}
domain_zone = first_match(zones_now, ZONE_MAP) # 'cafe' ë±
state = states.get(session_id)
state.last_seen = now()
if domain_zone and domain_zone != state.current_zone:
if state.candidate_zone != domain_zone:
state.candidate_zone = domain_zone
state.candidate_since = now()
elif now() - state.candidate_since >= ENTER_HOLD_MS:
await commit_transition(state, domain_zone)
elif not domain_zone:
# zone ìì â exit í볎 (timerë ë³ë tickìì ì²ëЬ)
state.candidate_zone = None
# ë³ë tick (100ms 죌Ʞ)
async def tick():
for state in states.values():
if not state.current_zone:
continue
if now() - state.last_seen >= EXIT_GRACE_MS:
await publish_exit(state.session_id, state.current_zone)
state.current_zone = None-
vision/zone/enterë°í ì§í ëìŒ (session_id,zone_id) ì€ë³µ enter 1ìŽ ëŽ ë°í êžì§. - envelope
idë íì ì ULID(Universally Unique Lexicographically Sortable Identifier, ë²ì© ê³ ì ì ë ¬ ê°ë¥ ìë³ì) â 구ë ìê° ê°ì ë©ìì§ë¥Œ ë ë² ì²ëЬíì§ ìëë¡.
services/bridge/
âââ Dockerfile
âââ requirements.txt
âââ pyproject.toml
âââ src/
â âââ bridge/
â âââ __init__.py
â âââ main.py # ìížëЬí¬ìžíž
â âââ mqtt_client.py # paho-mqtt ëíŒ
â âââ frigate_adapter.py # frigate/events íì
â âââ reid.py # ìë² ë© ë§€ì¹, cross-cam
â âââ session_registry.py # session_id â track_id
â âââ topics.py # í íœ ìì
â âââ schemas.py # pydantic 몚ëž
âââ tests/
âââ test_frigate_adapter.py
âââ test_reid.py
âââ fixtures/
âââ frigate_events/ # ìí JSON
# src/bridge/main.py
from bridge.mqtt_client import Mqtt
from bridge.frigate_adapter import handle_frigate_event
from bridge.session_registry import registry
mqtt = Mqtt(host="mosquitto", user="bridge", password=os.environ["MQTT_PWD"])
@mqtt.on("frigate/events")
def on_frigate(msg):
domain_events = handle_frigate_event(msg, registry)
for ev in domain_events:
mqtt.publish(ev.topic, ev.payload, qos=1)
@mqtt.on("session/end")
def on_session_end(msg):
registry.unbind(session_id=msg["session_id"]) # SCRUB: ìë² ë© ì°žì¡° íŽì
mqtt.run_forever()-
ì ë:
frigate_adapterì íœì€ì² JSON ì ë ¥ â ëë©ìž ìŽë²€íž ì¶ë ¥ ê²ìŠ. - íµí©: ìë² ëë mosquitto + Frigate ìŽë²€íž ì뮬ë ìŽí°ë¡ E2E.
- ì±ë¥: 10ëª ëì ì¶ì ì latency p95 < 100ms ê²ìŠ.
- ë² ìŽì€:
python:3.12-slim -
anthropic: Claude SDK(Software Development Kit, ìíížìšìŽ ê°ë° í€íž) -
langgraph: ê·žëí ì€ìŒì€ížë ìŽì -
mcpPython SDK: MCP(Model Context Protocol, ëªšëž ì»ší ì€íž íë¡í ìœ) ìë² ì°ê²° -
paho-mqtt,pydantic,httpx(HA REST(Representational State Transfer, íí ìí ì ìŽ)),prometheus-client
ì ë¶ ì첎 ê°ë°. LangGraphë 곚격, ë žëì zone playbookì ì°ëŠ¬ê° ìì±.
- MQTT ížëŠ¬ê±° 구ë
(
vision/zone/enter,tablet/+/intent,context/+/+/update,context/environmental/update) - HA REST/WebSocketìŒë¡ 컚í ì€íž ìŽì ëž
- Claude tool use ížì¶ (MCP ìë² ë žì¶)
- ê°ëë ìŒ (ì€í€ë§Â·whitelist·hard clamp)
- ë구 ížì¶ 결곌륌 MQTT publish ëë MCP dispatch
- íŽë°± 몚ë (Claude ì€íš ì rule-based)
services/orchestrator/
âââ Dockerfile
âââ pyproject.toml
âââ src/
â âââ orchestrator/
â âââ main.py
â âââ graph.py # LangGraph ì ì
â âââ nodes/
â â âââ assemble.py
â â âââ call_claude.py
â â âââ guardrail.py
â â âââ dispatch.py
â â âââ fallback.py
â âââ ha_client.py # HA REST/WS
â âââ mcp_clients.py # robot/tablet/ha MCP ì°ê²°
â âââ prompts/
â â âââ system.md
â â âââ playbooks/
â â âââ cafe.md
â â âââ park.md
â â âââ vehicle.md
â â âââ home.md
â âââ tools.py # Claude tool ì€í€ë§
â âââ metrics.py # Prometheus
â âââ topics.py
âââ tests/
âââ test_graph.py
âââ test_guardrail.py
âââ test_playbooks.py
TOOLS = [
{
"name": "robot_command",
"description": "ë¡ëŽìê² capability ëª
ë ¹ì ë°í. ìì ìží°ë¡ íµê³Œ ììë§ ì€íëš.",
"input_schema": {
"type": "object",
"properties": {
"robot_id": {"type": "string"}, # ë°íìì ROBOT_REGISTRYë¡ ê²ìŠ (config/robots.yaml)
"capability": {"type": "string",
"enum": ["goto", "follow", "stop", "look_at", "gesture"]},
"params": {"type": "object"},
},
"required": ["robot_id", "capability"],
},
},
{
"name": "show_options",
"description": "첎íì íëžëŠ¿ì ì íì§ë¥Œ íì.",
"input_schema": {...},
},
# ...
]def validate_tool_calls(state: FlowState) -> dict:
accepted, rejected = [], []
for call in state["tool_calls"]:
# 1. JSON schema
if not validate_schema(call):
rejected.append((call, "schema"))
continue
# 2. capability whitelist
if call["input"]["capability"] not in ALLOWED_CAPS:
rejected.append((call, "whitelist"))
continue
# 3. zone polygon clamp
if "params" in call["input"] and "target_pos" in call["input"]["params"]:
if not in_zone_polygon(call["input"]["params"]["target_pos"]):
rejected.append((call, "zone_fence"))
continue
# 4. velocity clamp
if "velocity" in call["input"].get("params", {}):
call["input"]["params"]["velocity"] = min(
call["input"]["params"]["velocity"], MAX_VELOCITY
)
accepted.append(call)
REJECT_COUNTER.inc(len(rejected))
return {"tool_calls": accepted, "rejected_calls": rejected}ìì€í í롬ííž + zone playbookì stable â Anthropic prompt cache íì©.
- ë žë ëšì: ê° LangGraph ë žë ì ì¶ë ¥ ê²ìŠ.
- ê°ëë ìŒ fuzz: ìëì ìŒë¡ ì못ë ë구 ížì¶ 1000걎 â 100% ê±°ë¶ íìž.
- ì뮬ë ìŽì : ê°ì§ MQTT ìŽë²€íž stream â ê·žëí ì¢ ëš ëì ê²ìŠ.
- ë² ìŽì€:
python:3.12-slim -
bosdyn-client,bosdyn-api: SPOT SDK (Boston Dynamics ë³ë EULA) - (믞ì ë¡ëŽ ê²°ì í) ROS2
rclpyëë íŽë¹ SDK -
mcpPython SDK: MCP server ë žì¶ -
paho-mqtt,pydantic
ì ë¶ ì첎 ê°ë°. SPOT SDKë ëŒìŽëžë¬ëЬ ížì¶ ìì€.
- ìŽì¢ ë¡ëŽ ì¶ìí: Capability Model ìží°íìŽì€ë¥Œ SPOT/믞ì ë¡ëŽ ìŽëí°ê° 구í
-
MCP ë구 ë
žì¶: orchestratorê° ížì¶í
robot_commandë구 - ìì ìží°ë¡: Hardware e-stop · zone polygon · proximity guard · watchdog
- ëª ë ¹ í: priority(safety > operator > llm) FIFO
-
í
ë ë©ížëЬ: 1Hz
robot/{id}/stateë°í -
Fault ê°ì§:
robot/{id}/faultë°í
services/mcp-robot-adapter/
âââ Dockerfile
âââ pyproject.toml
âââ src/
â âââ adapter/
â âââ main.py # MCP server + MQTT bridge
â âââ capability_model.py # ì¶ì ìží°íìŽì€
â âââ adapters/
â â âââ spot.py # bosdyn-client ëí
â â âââ generic_ros2.py # 믞ì ë¡ëŽì©
â âââ safety/
â â âââ interlock.py # íµí© ê²ìŽíž
â â âââ estop.py
â â âââ zone_fence.py
â â âââ proximity.py
â â âââ watchdog.py
â âââ command_queue.py
â âââ telemetry.py
â âââ topics.py
âââ tests/
âââ test_safety.py
âââ test_capability_model.py
âââ test_command_queue.py
# capability_model.py
from abc import ABC, abstractmethod
from typing import Literal
Capability = Literal["goto", "follow", "stop", "sit", "stand", "gesture", "look_at"]
class RobotAdapter(ABC):
@abstractmethod
async def execute(self, capability: Capability, params: dict) -> dict: ...
@abstractmethod
async def get_state(self) -> dict: ...
@abstractmethod
async def emergency_stop(self) -> None: ...# adapters/spot.py
from bosdyn.client import create_standard_sdk
from bosdyn.client.robot_command import RobotCommandClient, RobotCommandBuilder
class SpotAdapter(RobotAdapter):
def __init__(self, hostname, username, password):
sdk = create_standard_sdk("zerone-spot")
self.robot = sdk.create_robot(hostname)
self.robot.authenticate(username, password)
self.cmd_client = self.robot.ensure_client(RobotCommandClient.default_service_name)
async def execute(self, capability, params):
if capability == "stop":
cmd = RobotCommandBuilder.stop_command()
elif capability == "goto":
cmd = RobotCommandBuilder.synchro_se2_trajectory_point_command(
goal_x=params["x"], goal_y=params["y"], goal_heading=params.get("yaw", 0),
frame_name=ODOM_FRAME_NAME,
)
elif capability == "look_at":
cmd = self._build_look_at(params["target"])
else:
raise ValueError(f"Unknown capability: {capability}")
future = self.cmd_client.robot_command_async(cmd)
return {"command_id": future.command_id}# safety/interlock.py
class SafetyGate:
def __init__(self, estop, fence, proximity, watchdog):
self.checks = [estop, fence, proximity, watchdog]
async def allow(self, robot_id, capability, params) -> tuple[bool, str | None]:
for check in self.checks:
ok, reason = await check.evaluate(robot_id, capability, params)
if not ok:
return False, reason
return True, None- ì ë: ê° ìì 첎í¬ê° ì íí 조걎ìì ê±°ë¶íëì§.
- ê³ìœ í ì€íž: SPOT ì뮬ë ìŽí°(BDìì ì ê³µ) ëë mock ìŽëí°ë¡ capability ížì¶ ê²ìŠ.
- 칎ì€ì€: e-stop ìŽë²€íž ë°ì ì íì ëšì 몚ë ëª ë ¹ ê±°ë¶ íìž.
- ë² ìŽì€:
node:20-alpine -
ws: WebSocket ìë² -
mqtt: MQTT íŽëŒìŽìžíž -
@modelcontextprotocol/sdk: MCP server (TypeScript) -
zod: ì€í€ë§ ê²ìŠ
ì ë¶ ì첎 ê°ë°.
- íëžëŠ¿ WSS(WebSocket Secure, 볎ì ì¹ììŒ) ì°ê²° êŽëЬ (ì¬ì°ê²°Â·ìžì íìŽë§)
- MQTT
tablet/{id}/show_options·show_message구ë â WSS push - WSS ì
ë ¥ â MQTT
tablet/{id}/intentë°í - MCP ë구ë¡
show_options·show_message륌 ìžë¶ì ë žì¶ (orchestrator ì§ì ížì¶ì©) - ë©ìì§ ID ì¶ì + ACK
services/mcp-tablet-gateway/
âââ Dockerfile
âââ package.json
âââ tsconfig.json
âââ src/
â âââ main.ts
â âââ wss-server.ts
â âââ mqtt-bridge.ts
â âââ mcp-server.ts
â âââ session-pairing.ts # QR í í° ë°êž
â âââ schemas.ts
â âââ topics.ts
âââ tests/
âââ wss.test.ts
âââ mqtt-bridge.test.ts
import { WebSocketServer } from "ws";
import mqtt from "mqtt";
const wss = new WebSocketServer({ port: 8080 });
const mq = mqtt.connect("mqtt://mosquitto");
const tablets = new Map<TabletId, WebSocket>();
wss.on("connection", (ws, req) => {
const tabletId = authenticate(req);
tablets.set(tabletId, ws);
mq.subscribe(`tablet/${tabletId}/+`);
ws.on("message", (raw) => {
const intent = parseIntent(raw); // zod ê²ìŠ
mq.publish(`tablet/${tabletId}/intent`, JSON.stringify(intent), { qos: 1 });
});
ws.on("close", () => tablets.delete(tabletId));
});
mq.on("message", (topic, payload) => {
const [, tabletId, type] = topic.split("/");
const ws = tablets.get(tabletId);
if (!ws) return;
ws.send(JSON.stringify({ type, ...JSON.parse(payload.toString()) }));
});ì ì : ì ìì¥ ì 구ì íìŽë§ 칎ë©ëŒ 1ë(ëë ì 구 zone 칎ë©ëŒ 겞ì©)ê° íì ìŒì ž ìê³ , Frigateê° person ì¶ì ì€. íëžëŠ¿ì ë¶í í ìŽììê° ì²Žíììê² ì ë¬.
Tablet tablet-gateway Frigate bridge orchestrator
â â â â â
â POST /pair â â â â
âââââââââââââââââââââºâ â â â
â â generate â â â
â â - session_id (ULID) â â
â â - pairing_token (32B random) â â
â â - QR(payload=session_id) â â
â 200 { session_id, token, qr_png } â â â
âââââââââââââââââââââ†â â â
â â â â â
â display QR + "ì ìë§ ì¹Žë©ëŒë¥Œ ëŽì£Œìžì" â â â
â â â â â
â âââ ìŽììê° íëžëŠ¿ì íìŽë§ 칎ë©ëŒ ììŒë¡ âââ â â
â â â â â
â â â person ê°ì§ â â
â â â + QR ëìœë© â â
â â â (zone=pairing)â â
â â â â â
â â ââMQTT frigate/eventsââ⺠â
â â â â â
â â â â ë§€ì¹: â
â â â â - person bboxììâ
â â â â OSNet ìë² ë© ì¶ì¶â
â â â â - QR payloadìì â
â â â â session_id ì¶ì¶ â
â â â â - registry.bind() â
â â â â â
â â â â MQTT session/startâ
â â â ââââââââââââââââââââºâ
â â â â â assemble + welcome
â â â â â Claude tool call:
â â â â â show_message("íìí©ëë€")
â WSS upgrade Auth: Bearer <token> â â â
âââââââââââââââââââââºâ verify token â â â
â â session_id ë§€í â â â
â WSS open â â â â
âââââââââââââââââââââ†â â â
â â â â â
â {type:"show_message", text:"íì..."} â â â
ââââââââââââââââââââââ€âââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â â â â
-
mcp-tablet-gatewayPOST /pair:-
session_idULID ìì±,pairing_token32-byte random. - QR ìŽë¯žì§ ìì±: payload = session_id (URL-safe base64).
- ìëµ:
{session_id, token, qr_png_base64}. í í° TTL 4ìê°. - í í°ì in-memory Mapì
{token: session_id}ì ì¥.
-
- íëžëŠ¿: QR íì€í¬ëа + ìì± ìëŽ "칎ë©ëŒë¥Œ ëŽì£Œìžì". 5ìŽ ì¹ŽìŽížë€ìŽ.
-
Frigate: íìŽë§ 칎ë©ëŒ(ì:
pairing_cam_01)ìì person ê°ì§ + QR ëìœë (zone polygonpairing_zone). ëìœë 결곌륌 attributeë¡ ìŽë²€íž ë°í. -
bridge:-
frigate/events구ë . -
current_zonesìpairing_zoneí¬íš + QR attributeìì session_id ì¶ì¶ ì:- person bbox ìììì OSNet(Omniscale Feature Network, ìŽëì€ìŒìŒ í¹ì§ ë€ížìí¬) ìë² ë© ì¶ì¶ (BoxMOT ëë ì첎 몚ëž).
-
registry.bind(session_id, embedding). -
session/startMQTT publish ({session_id, paired_tablet_id, visual_embedding_ref, started_at}).
-
-
orchestrator:session/startìì â íì ìëëŠ¬ì€ ì€í (Claudeshow_message). - íëžëŠ¿: í í°ìŒë¡ WSS ì°ê²° â orchestrator ë©ìì§ ìì ìì.
| ìí© | ëì |
|---|---|
| íëžëŠ¿ìŽ QR íì í 60ìŽ ëŽ ì¹Žë©ëŒ ìžì 못 íš | íëžëŠ¿ ìëìŒë¡ íìŽë§ í멎 ì¬ìì, í í° íêž° |
| Frigateê° personì 볎ì§ë§ QR ëìœë ì€íš | bridgeê° ë¬Žì (session_id ììŒë©Ž bind ì íš) |
| ê°ì session_idë¡ ë€ë¥ž ìë² ë© ì¬ë°ìžë© ìë | Ʞ졎 ìë² ë© ë®ìŽì°êž° (ì¬íìŽë§ ìë) + warn ë¡ê·ž |
| WSS ì°ê²° ì í í° ë§ë£ | 401 ìëµ â íëžëŠ¿ ì¬íìŽë§ |
- íìŽë§ 칎ë©ëŒ ìì¹: ì 구·íëžëŠ¿ ìë ¹ë êž°ì€ 1.5m 거늬, 뚞늬·ìë°ì ì 볎ìŽê².
- 첎íììê² "QRìŽ ì¬ëŒì§ ëê¹ì§ 칎ë©ëŒë¥Œ ëŽì£Œìžì" ìëŽ.
- ëì íìŽë§ ííŒ: í ë²ì í ëª ì© ì¹Žë©ëŒ ììŒë¡.
- ì ë: zod ì€í€ë§, íìŽë§ í í° ìì±/ë§ë£.
- íµí©: ê°ì§ mosquitto + WSS íŽëŒìŽìžížë¡ ë©ìì§ round-trip.
- íë ììí¬: React Native (ê¶ì¥Â·êž°ë³ž). Flutterë ê°ë¥íë 볞 ìì€í ì RN ìíê³ êž°ì€ìŒë¡ ê°ìŽë â ë³ê²œ ì ë³ë PoC íì.
-
ws: WSS ì°ê²° (mcp-tablet-gatewayì íµì ). -
react-native-vision-camera: QR ì€ìº. -
zustand: ìí êŽëЬ.
ì ë¶ ì첎 ê°ë°.
- ë¶í â íìŽë§ ëêž°: QR íì (ëë ìŽìì ì€ìº 몚ë).
-
ìžì
ì§í: orchestratorê° pushí
show_options/show_message륌 ëì ìŒë¡ ë ë. -
ìží°ëì
: ì¬ì©ìê° ìµì
ì í·ìì í
ì€íž ì
ë ¥ â
tablet/{id}/intentpublish. - ìžì ì¢ ë£: ìŽìì ížëŠ¬ê±° ëë timeout.
- UI: NativeBase / React Native Paper
- ì ëë©ìŽì : Reanimated 3
- 믞ëìŽ: react-native-video (LLMìŽ ìì ìëµ ì)
apps/tablet/
âââ App.tsx
âââ src/
â âââ screens/
â â âââ PairingScreen.tsx
â â âââ SessionScreen.tsx
â â âââ EndScreen.tsx
â âââ components/
â â âââ OptionsCard.tsx
â â âââ MessageBubble.tsx
â âââ stores/
â â âââ session.ts
â âââ transport/
â â âââ wss.ts
â â âââ auth.ts
â âââ schemas.ts
âââ ios|android/
- ìží°ëì ëšìí멎 Home Assistant Lovelace íì€í¬ëа + 컀ì€í 칎ëë¡ë ê°ë¥ (ê°ë° ë¹ì© â, íí ìì ë â).
zerone/
âââ deploy/ # ìŽì docker-compose, .env, configs
â âââ docker-compose.yml
â âââ frigate/config.yml
â âââ ha-config/
â âââ mosquitto/
â âââ prometheus/
âââ services/
â âââ bridge/
â âââ orchestrator/
â âââ mcp-robot-adapter/
â âââ mcp-tablet-gateway/
âââ apps/
â âââ tablet/
âââ packages/ # ê³µì ëŒìŽëžë¬ëЬ
â âââ topics/ # í íœ ìì·ì€í€ë§ (TS/Python ëìŒ)
âââ .github/workflows/
âââ docs/ # SYSTEM_DESIGN.md, MQTT_SCHEMA.md, ...
packages/topicsì TypeScriptì Pythonìì 몚ë import ê°ë¥í ííë¡ ì ì (Pydantic â Zod ëêž°í). ë³ê²œ ì ì쪜 ìë ë°ìíëë¡ codegen.
몚ë ìë¹ì€ê° ê³µì :
MQTT_HOST=mosquitto
MQTT_PORT=1883
LOG_LEVEL=info
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
SERVICE_NAME=<each>
몚ë ì첎 ìë¹ì€ë GET /healthz ë
žì¶ â Docker HEALTHCHECKì Prometheus probeìì ì¬ì©.
- 구조í JSON (
structlogPython,pinoNode). -
trace_idíë íì â orchestratorê° ë°í, 몚ë ë€ìŽì€ížëŠŒìŽ ì í.
MQTT envelopeì traceparentë W3C Trace Context íì. 몚ë ì첎 ìë¹ì€ë ë€ì íšíŽìŒë¡ ì¶ì¶Â·ì ííë€.
ì졎ì±:
opentelemetry-api ~= 1.27
opentelemetry-sdk ~= 1.27
opentelemetry-exporter-otlp-proto-http ~= 1.27
opentelemetry-instrumentation-paho-mqtt ~= 0.48b0
ë¶ížì€ížë©:
# packages/topics/tracing.py (몚ë Python ìë¹ì€ ê³µíµ)
from opentelemetry import trace
from opentelemetry.propagate import extract, inject
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
def init_tracing(service_name: str):
provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) # OTEL_EXPORTER_OTLP_ENDPOINT
trace.set_tracer_provider(provider)ë°íì íšíŽ:
tracer = trace.get_tracer(__name__)
def publish_with_trace(topic: str, data: dict, span_name: str):
with tracer.start_as_current_span(span_name) as span:
carrier = {}
inject(carrier) # carrier["traceparent"] = "00-..."
envelope = wrap(
type_=topic.replace("/", "."),
data=data,
source=f"/services/{SERVICE_NAME}",
traceparent=carrier.get("traceparent"),
)
mqtt.publish(topic, json.dumps(envelope), qos=1)구ë ì íšíŽ:
def on_message(topic, payload):
envelope = json.loads(payload)
ctx = extract({"traceparent": envelope.get("traceparent", "")})
with tracer.start_as_current_span("handle_" + envelope["type"], context=ctx) as span:
span.set_attribute("event.id", envelope["id"])
span.set_attribute("event.type", envelope["type"])
process(envelope["data"])ì졎ì±:
"@opentelemetry/api": "^1.9",
"@opentelemetry/sdk-node": "^0.55",
"@opentelemetry/exporter-trace-otlp-http": "^0.55"import { trace, context, propagation } from "@opentelemetry/api";
const tracer = trace.getTracer("mcp-tablet-gateway");
function publishWithTrace(topic: string, data: unknown, spanName: string) {
return tracer.startActiveSpan(spanName, (span) => {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
const envelope = wrap({ type: topic.replaceAll("/", "."), data, traceparent: carrier.traceparent });
mqtt.publish(topic, JSON.stringify(envelope), { qos: 1 });
span.end();
});
}-
Anthropic API: orchestratorê° ížì¶ ì íì¬ spanì traceparent륌 HTTP
traceparentí€ëë¡ ìë 죌ì (OTLPInstrumentationì¬ì© ì). -
HA REST:
httpxinstrumentor íì±í â span ìë ì¶ê°. - MCP ížì¶ (orchestrator â mcp-robot-adapter): MCP transportê° HTTP/SSE멎 ëìŒíê² traceparent í€ë 죌ì .
- í겜ë³ì:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318(ëë ì§ì Tempo). - ìŽììì Tempo ëë Grafana Cloud Tracesë¡ export. ë¯žë¬ í겜ìì Jaeger UI(
localhost:16686)ë¡ ìê°í.
[Frigate event] â bridge.handle_frigate_event
ââ bridge.publish: vision/zone/enter (traceparent ì ê· ìì±)
ââ orchestrator.handle_vision_zone_enter (extract + ëìŒ trace ê³ì)
ââ orchestrator.assemble_context
â ââ http GET /api/states/... (HA REST)
â ââ http GET /api/states/...
ââ orchestrator.call_claude (Anthropic API)
ââ orchestrator.guardrail
ââ orchestrator.dispatch
ââ mcp-robot-adapter.robot_command (MCP ížì¶, ê°ì trace)
ââ adapter.execute_spot (SDK ížì¶)
í ì€ë¡ ìŽìŽì§ë trace륌 Grafanaìì íŒì³ latency ë³ëª©Â·ì€ë¥ ì¶ì .
-
mcp-robot-adapter,mcp-tablet-gatewayë MCP spec(JSON-RPC 2.0 over stdio/SSE) ì€ì. - ë구 ì ì ì
inputSchemaJSON Schema ì íí ëª ì â Claudeê° ì íí ížì¶.
- Python:
ruff+mypy --strict - TypeScript:
eslint+tsc --strict - 몚ë PRì CIìì lint·type·test íµê³Œ íì.
Ʞ졎 ìœëê° ììŒë¯ë¡ N/A. ì²ìë¶í° ì êµ¬ì¡°ë¡ ìì ê¶ì¥.
ì ê· í©ë¥ììê² ë³Žì¬ì€ ì 묞 겜ë¡:
- SYSTEM_DESIGN.md â ì 첎 귞늌
- OSS_INTEGRATION.md â ìŽë»ê² 묶ìŽëê°
- ìŽ ë¬žì (SERVICES.md) â ìŽëë¶í° ìœë©íëê°
- MQTT_SCHEMA.md â ìží°íìŽì€ ê³ìœ
- DEPLOYMENT.md â ìŽë»ê² 굎늬ëê°