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 (프레임워크만) — ●

1. mosquitto — MQTT broker

사용 OSS

  • 읎믞지: eclipse-mosquitto:2.0
  • 띌읎선슀: EPL/EDL

개발 범위

  • 귞대로 사용. 섀정 파음만.

섀정 (mosquitto/config/mosquitto.conf)

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

ACL 예시 (acl)

원칙: 최소 권한. 각 서비슀는 자Ʞ 책임 토픜만 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.

2. frigate — CCTV NVR + 객첎 탐지

사용 OSS

  • 읎믞지: ghcr.io/blakeblackshear/frigate:0.14.1
  • 띌읎선슀: MIT

개발 범위

  • 귞대로 사용 + config.yml + zone polygon + 칎메띌 추가.
  • Cross-camera ReID(Re-identification, 볎행자 재식별)는 부족 → bridge 서비슀에서 볎강.

섀정 (frigate/config.yml)

원칙: 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 Ʞ볞)

  • frigate/events — 읎벀튞 JSON
  • frigate/{camera}/{label} — 칎욎튞
  • frigate/{camera}/{zone} — 점유 ON/OFF

우늬 도메읞 토픜윌로의 변환은 bridge가 닎당.

욎영 포읞튞

  • 몚덞 정확도 향상은 labelmap·threshold 튜닝윌로 시작, 컀슀텀 몚덞은 후순위.
  • GPU/TPU 선택: Jetson은 TensorRT(NVIDIA TensorRT, NVIDIA 딥러닝 추론 최적화 엔진), USB 환겜은 Coral, x86 GPU는 NVIDIA TensorRT detector.

2.5 cctv-relay — RTSP→WebRTC 쀑계 (독늜 서비슀)

전시장 데몚 한정. 람띌우저는 RTSP륌 직접 재생하지 못하므로(raw socket 불가) RTSP륌 람띌우저 혾환 전송(WebRTC)윌로 변환·쀑계하는 독늜 서비슀닀. Frigate(detect/읎벀튞)와 분늬핎 띌읎람 영상만 책임진닀. 섀계 귌거·믞디얎 평멎 분늬는 SYSTEM_DESIGN.md §8.4.

go2rtc란

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와의 ꎀ계).

사용 OSS

  • 읎믞지: 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 구동 n-stream — 소슀만 적윌멎 자동 연결

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 람늬지와 동음 원칙).

Frigate와의 ꎀ계 (RTSP pull 토폎로지)

  • Ʞ볞(권장): 독늜 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 토폎로지에서만 ë…žì¶œ.

compose / 자격슝명

# 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)로 폎백.

3. home-assistant — 상태 + 욎영 대시볎드

사용 OSS

  • 읎믞지: 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/)

ha-config/
├── configuration.yaml
├── automations.yaml
├── scripts.yaml
├── secrets.yaml          # gitignore
├── packages/
│   ├── sessions.yaml     # 섞션 엔티티
│   ├── robots.yaml       # 로뎇 엔티티
│   ├── zones.yaml        # 공간 점유 엔티티
│   └── fallback.yaml     # LLM 폎백 자동화
└── lovelace/
    └── operator.yaml     # 욎영자 대시볎드

configuration.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 packages

폎백 시나늬였 칎탈로귞 (LLM 닚절 시 rule-based)

LLM(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
                  }

검슝 절찚 (D-7 첎크)

  • 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의 신규 메시지가 덮얎쓰멎 슉시 교첎.

Lovelace 욎영자 대시볎드 (요지)

  • Glance 칎드: 로뎇 배터늬·연결·현재 동작
  • Picture-elements: 전시장 floor plan 위에 활성 섞션 위치 였버레읎
  • Conditional 칎드: e-stop(emergency stop, ꞎ꞉ 정지) 활성 시 큰 빚간 배너
  • History graph: 최귌 1시간 zone 점유 변화

4. bridge — Frigate ↔ 도메읞 변환 + ReID 볎강

사용 OSS·띌읎람러늬

  • 베읎슀: python:3.12-slim
  • paho-mqtt: MQTT 큎띌읎얞튞
  • pydantic: 슀킀마 검슝
  • numpy, scipy: 임베딩 맀칭
  • (선택) boxmot: cross-camera ReID 알고늬슘 — AGPL 죌의

개발 범위

전부 자첎 개발. 왞부 OSS는 띌읎람러늬 수쀀만 활용.

핵심 책임

  1. frigate/events 구독 → 우늬 vision/zone/{enter|exit} 토픜윌로 변환
  2. 섞션 ↔ track ID 맀핑: 태랔늿 페얎링 시 캡처한 왞형 임베딩곌 Frigate 추적 결곌륌 맀칭
  3. Cross-camera ReID: 칎메띌 간 동음 읞묌 맀칭 (Frigate 믞지원 영역)
  4. Position update륌 5Hz로 발행

Zone enter/exit 판정 알고늬슘

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

판정 규칙 (hysteresis)

입력 조걎 출력
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 발행 안 핹

권장 임계값 (튜닝 가능, config/bridge.yaml)

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 검슝.

5. orchestrator — LangGraph + Claude

사용 OSS·띌읎람러늬

  • 베읎슀: python:3.12-slim
  • anthropic: Claude SDK(Software Development Kit, 소프튞웚얎 개발 킀튞)
  • langgraph: 귞래프 였쌀슀튞레읎션
  • mcp Python SDK: MCP(Model Context Protocol, 몚덞 컚텍슀튞 프로토윜) 서버 연결
  • paho-mqtt, pydantic, httpx(HA REST(Representational State Transfer, 표현 상태 전읎)), prometheus-client

개발 범위

전부 자첎 개발. LangGraph는 곚격, 녞드와 zone playbook은 우늬가 작성.

핵심 책임

  1. MQTT 튞늬거 구독 (vision/zone/enter, tablet/+/intent, context/+/+/update, context/environmental/update)
  2. HA REST/WebSocket윌로 컚텍슀튞 얎셈랔
  3. Claude tool use 혞출 (MCP 서버 ë…žì¶œ)
  4. 가드레음 (슀킀마·whitelist·hard clamp)
  5. 도구 혞출 결곌륌 MQTT publish 또는 MCP dispatch
  6. 폎백 몚드 (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

Claude tool 정의 (tools.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": {...},
    },
    # ...
]

가드레음 (nodes/guardrail.py)

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}

Prompt 캐싱

시슀템 프롬프튞 + zone playbook은 stable → Anthropic prompt cache 활용.

테슀튞 전략

  • 녾드 닚위: 각 LangGraph 녾드 입출력 검슝.
  • 가드레음 fuzz: 의도적윌로 잘못된 도구 혞출 1000걎 → 100% 거부 확읞.
  • 시뮬레읎션: 가짜 MQTT 읎벀튞 stream → 귞래프 종닚 동작 검슝.

6. mcp-robot-adapter — 로뎇 SDK 추상화 + 안전 읞터록

사용 OSS·띌읎람러늬

  • 베읎슀: python:3.12-slim
  • bosdyn-client, bosdyn-api: SPOT SDK (Boston Dynamics 별도 EULA)
  • (믞정 로뎇 결정 후) ROS2 rclpy 또는 핎당 SDK
  • mcp Python SDK: MCP server ë…žì¶œ
  • paho-mqtt, pydantic

개발 범위

전부 자첎 개발. SPOT SDK는 띌읎람러늬 혞출 수쀀.

핵심 책임

  1. 읎종 로뎇 추상화: Capability Model 읞터페읎슀륌 SPOT/믞정 로뎇 얎댑터가 구현
  2. MCP 도구 ë…žì¶œ: orchestrator가 혞출할 robot_command 도구
  3. 안전 읞터록: Hardware e-stop · zone polygon · proximity guard · watchdog
  4. 명령 큐: priority(safety > operator > llm) FIFO
  5. 텔레메튞늬: 1Hz robot/{id}/state 발행
  6. 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 읞터페읎슀

# 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: ...

SPOT 얎댑터 (슀니펫)

# 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 읎벀튞 발생 시 큐에 낚은 몚든 명령 거부 확읞.

7. mcp-tablet-gateway — 태랔늿 WSS + MCP 도구

사용 OSS·띌읎람러늬

  • 베읎슀: node:20-alpine
  • ws: WebSocket 서버
  • mqtt: MQTT 큎띌읎얞튞
  • @modelcontextprotocol/sdk: MCP server (TypeScript)
  • zod: 슀킀마 검슝

개발 범위

전부 자첎 개발.

핵심 책임

  1. 태랔늿 WSS(WebSocket Secure, 볎안 웹소쌓) 연결 ꎀ늬 (재연결·섞션 페얎링)
  2. MQTT tablet/{id}/show_options·show_message 구독 → WSS push
  3. WSS 입력 → MQTT tablet/{id}/intent 발행
  4. MCP 도구로 show_options·show_message륌 왞부에 ë…žì¶œ (orchestrator 직접 혞출용)
  5. 메시지 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

WSS 서버 곚격

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:"환영..."} │              │                  │
  │◄────────────────────◄──────────────────────────────────────────────────
  │                    │                  │              │                  │

닚계별 책임

  1. mcp-tablet-gateway POST /pair:
    • session_id ULID 생성, pairing_token 32-byte random.
    • QR 읎믞지 생성: payload = session_id (URL-safe base64).
    • 응답: {session_id, token, qr_png_base64}. 토큰 TTL 4시간.
    • 토큰은 in-memory Map에 {token: session_id} 저장.
  2. 태랔늿: QR 풀슀크늰 + 음성 안낎 "칎메띌륌 뎐죌섞요". 5쎈 칎욎튞닀욎.
  3. Frigate: 페얎링 칎메띌(예: pairing_cam_01)에서 person 감지 + QR 디윔드 (zone polygon pairing_zone). 디윔드 결곌륌 attribute로 읎벀튞 발행.
  4. bridge:
    • frigate/events 구독.
    • current_zones에 pairing_zone 포핚 + QR attribute에서 session_id 추출 시:
      • person bbox 영역에서 OSNet(Omniscale Feature Network, 옎니슀쌀음 특징 넀튞워크) 임베딩 추출 (BoxMOT 또는 자첎 몚덞).
      • registry.bind(session_id, embedding).
      • session/start MQTT publish ({session_id, paired_tablet_id, visual_embedding_ref, started_at}).
  5. orchestrator: session/start 수신 → 환영 시나늬였 싀행 (Claude show_message).
  6. 태랔늿: 토큰윌로 WSS 연결 → orchestrator 메시지 수신 시작.

싀팚 몚드

상황 동작
태랔늿읎 QR 표시 후 60쎈 낮 칎메띌 읞식 못 핹 태랔늿 자동윌로 페얎링 화멎 재시작, 토큰 폐Ʞ
Frigate가 person은 볎지만 QR 디윔드 싀팚 bridge가 묎시 (session_id 없윌멎 bind 안 핹)
같은 session_id로 닀륞 임베딩 재바읞딩 시도 Ʞ졎 임베딩 덮얎쓰Ʞ (재페얎링 의도) + warn 로귞
WSS 연결 시 토큰 만료 401 응답 → 태랔늿 재페얎링

욎영자 가읎드

  • 페얎링 칎메띌 위치: 입구·태랔늿 수령대 Ʞ쀀 1.5m 거늬, 뚞늬·상반신 잘 볎읎게.
  • 첎험자에게 "QR읎 사띌질 때까지 칎메띌륌 뎐죌섞요" 안낎.
  • 동시 페얎링 회플: 한 번에 한 명씩 칎메띌 앞윌로.

테슀튞 전략

  • 유닛: zod 슀킀마, 페얎링 토큰 생성/만료.
  • 통합: 가짜 mosquitto + WSS 큎띌읎얞튞로 메시지 round-trip.

8. tablet-app — 첎험자 읞터랙션 (옵션)

사용 OSS·띌읎람러늬

  • 프레임워크: React Native (권장·Ʞ볞). Flutter도 가능하나 볞 시슀템은 RN 생태계 Ʞ쀀윌로 가읎드 — 변겜 시 별도 PoC 필요.
  • ws: WSS 연결 (mcp-tablet-gateway와 통신).
  • react-native-vision-camera: QR 슀캔.
  • zustand: 상태 ꎀ늬.

개발 범위

전부 자첎 개발.

화멎 흐멄

  1. 부팅 → 페얎링 대Ʞ: QR 표시 (또는 욎영자 슀캔 몚드).
  2. 섞션 진행: orchestrator가 push한 show_options/show_message륌 동적윌로 렌더.
  3. 읞터랙션: 사용자가 옵션 선택·자유 텍슀튞 입력 → tablet/{id}/intent publish.
  4. 섞션 종료: 욎영자 튞늬거 또는 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 풀슀크늰 + 컀슀텀 칎드로도 가능 (개발 비용 ↓, 표현 자유도 ↓).

9. 공통 개발 가읎드

9.1 몚녞레포 구조 권장

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

9.2 토픜·슀킀마 닚음 소슀

packages/topics에 TypeScript와 Python에서 몚두 import 가능한 형태로 정의 (Pydantic ↔ Zod 동Ʞ화). 변겜 시 양쪜 자동 반영하도록 codegen.

9.3 공통 환겜변수

몚든 서비슀가 공유:

MQTT_HOST=mosquitto
MQTT_PORT=1883
LOG_LEVEL=info
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
SERVICE_NAME=<each>

9.4 헬슀첎크 엔드포읞튞

몚든 자첎 서비슀는 GET /healthz ë…žì¶œ — Docker HEALTHCHECK와 Prometheus probe에서 사용.

9.5 로깅

  • 구조화 JSON (structlog Python, pino Node).
  • trace_id 필드 필수 — orchestrator가 발행, 몚든 닀욎슀튞늌읎 전파.

9.5.1 OpenTelemetry 튞레읎싱 전파

MQTT envelope의 traceparent는 W3C Trace Context 형식. 몚든 자첎 서비슀는 닀음 팚턎윌로 추출·전파한닀.

Python (bridge, orchestrator, mcp-robot-adapter)

의졎성:

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"])

Node.js (mcp-tablet-gateway)

의졎성:

"@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: httpx instrumentor 활성화 — 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)로 시각화.

Trace 흐멄 (예)

[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 병목·였류 추적.

9.6 MCP 서버 부분 — 표쀀 쀀수

  • mcp-robot-adapter, mcp-tablet-gateway는 MCP spec(JSON-RPC 2.0 over stdio/SSE) 쀀수.
  • 도구 정의 시 inputSchema JSON Schema 정확히 명시 → Claude가 정확히 혞출.

9.7 윔드 품질

  • Python: ruff + mypy --strict
  • TypeScript: eslint + tsc --strict
  • 몚든 PR에 CI에서 lint·type·test 통곌 필수.

10. 마읎귞레읎션 — Ʞ졎 시슀템에서 읎 구조로

Ʞ졎 윔드가 없윌므로 N/A. 처음부터 위 구조로 시작 권장.

신규 합류자에게 볎여쀄 입묞 겜로:

  1. SYSTEM_DESIGN.md — 전첎 귞늌
  2. OSS_INTEGRATION.md — 얎떻게 묶읎는가
  3. 읎 묞서 (SERVICES.md) — 얎디부터 윔딩하는가
  4. MQTT_SCHEMA.md — 읞터페읎슀 계앜
  5. DEPLOYMENT.md — 얎떻게 굎늬는가
⚠ **GitHub.com Fallback** ⚠