MQTT_SCHEMA - zero1ne-26/doc GitHub Wiki

MQTT ๋ฉ”์‹œ์ง€ ์Šคํ‚ค๋งˆ ์นดํƒˆ๋กœ๊ทธ

๋ชจ๋“  ํ† ํ”ฝ์˜ payload ์Šคํ‚ค๋งˆยท๋ฐœํ–‰์žยท๊ตฌ๋…์žยทQoS(Quality of Service, ์„œ๋น„์Šค ํ’ˆ์งˆ ๋“ฑ๊ธ‰)๋ฅผ ์ •์˜ํ•˜๋Š” ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›. TypeScript/Python ์–‘์ชฝ ์ฝ”๋“œ ์ƒ์„ฑ์˜ ์†Œ์Šค(packages/topics/)์™€ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•ด์•ผ ํ•จ.


1. ๊ณตํ†ต envelope

๋ชจ๋“  ์ž์ฒด ๋„๋ฉ”์ธ ๋ฉ”์‹œ์ง€๋Š” CloudEvents 1.0 ํ˜ธํ™˜ envelope์œผ๋กœ ๊ฐ์‹ผ๋‹ค. (์™ธ๋ถ€ ์‹œ์Šคํ…œ์ธ Frigate frigate/events๋Š” envelope ์—†์ด ์ž์ฒด ํฌ๋งท์„ ์‚ฌ์šฉ.)

CloudEvents๋ž€? (๋ฐฐ๊ฒฝ)

CloudEvents๋Š” ์ด๋ฒคํŠธ์˜ payload(๋ณธ๋ฌธ) ์™€ ๊ทธ๊ฒƒ์„ ์„ค๋ช…ํ•˜๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(envelope) ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„๋“œ ์ด๋ฆ„์„ ํ‘œ์ค€ํ™”ํ•œ ๋ฒค๋” ์ค‘๋ฆฝ ์ด๋ฒคํŠธ ๋ช…์„ธ๋‹ค. ์„œ๋น„์Šค๋งˆ๋‹ค ์ œ๊ฐ๊ฐ์ธ ์ด๋ฒคํŠธ ํฌ๋งท(eventType vs event_name, ts vs timestamp ๋“ฑ)์„ ํ†ต์ผํ•ด, ๋ฐœํ–‰์žยท๊ตฌ๋…์žยท๋ธŒ๋กœ์ปคยทํŠธ๋ ˆ์ด์‹ฑ ๋„๊ตฌ๊ฐ€ payload๋ฅผ ํ•ด์„ํ•˜์ง€ ์•Š๊ณ ๋„ ์ด๋ฒคํŠธ๋ฅผ ๋ผ์šฐํŒ…ยท์ค‘๋ณต์ œ๊ฑฐ(dedup)ยท์ถ”์ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

  • ํ‘œ์ค€ ์ง€์œ„: CNCF(Cloud Native Computing Foundation) ํ˜ธ์ŠคํŒ… ํ”„๋กœ์ ํŠธ๋กœ, Sandbox(2018) โ†’ Incubating(2019) โ†’ Graduated(2024-01-25) ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์นœ ์กธ์—… ๋“ฑ๊ธ‰ ํ‘œ์ค€์ด๋‹ค. KubernetesยทPrometheusยทOpenTelemetry์™€ ๊ฐ™์€ ์ตœ๊ณ  ์„ฑ์ˆ™๋„ ๋‹จ๊ณ„.
  • ์˜คํ”ˆ์†Œ์Šค: ๋ช…์„ธ์™€ ๋‹ค๊ตญ์–ด SDK(Go/JS/Python/Java/C#/Rust ๋“ฑ) ๋ชจ๋‘ Apache-2.0์œผ๋กœ ๊ณต๊ฐœ. ๋ณธ ํ”„๋กœ์ ํŠธ์˜ packages/topics/ envelope wrapper๋Š” ์ด ๋ช…์„ธ์˜ MQTT Protocol Binding โ€” structured mode(์ „์ฒด envelope์„ JSON ๋ณธ๋ฌธ์œผ๋กœ ์ „์†ก)์— ํ•ด๋‹นํ•œ๋‹ค.
  • ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ์ปค์Šคํ…€: id๋ฅผ ULID๋กœ ๊ณ ์ •(๋ฉฑ๋“ฑ dedup ํ‚ค), traceparent(W3C TraceContext) extension ์†์„ฑ ๋ถ€์ฐฉ(OpenTelemetry ์ „ํŒŒ). ๊ทธ ์™ธ ํ•„์ˆ˜ ์†์„ฑ์€ ํ‘œ์ค€์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฅธ๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ โ€” ๊ณต์‹ ์‚ฌ์ดํŠธ https://cloudevents.io/ ยท ๋ช…์„ธยทSDK https://github.com/cloudevents/spec ยท CNCF ํ”„๋กœ์ ํŠธ ํŽ˜์ด์ง€ https://www.cncf.io/projects/cloudevents/

TypeScript ์ธํ„ฐํŽ˜์ด์Šค

interface CloudEvent<T> {
  specversion: "1.0";
  type: string;                  // "vision.zone.enter" ํ˜•ํƒœ dot-notation
  source: string;                // "/services/bridge", "/services/orchestrator", ...
  id: string;                    // ULID, ๋ฉฑ๋“ฑ์„ฑ dedup ์šฉ
  time: string;                  // RFC3339 (ISO8601), UTC
  datacontenttype: "application/json";
  subject?: string;              // "session/01HXYZ", "robot/spot-01" ๋“ฑ
  traceparent?: string;          // W3C TraceContext (OpenTelemetry ์ „ํŒŒ)
  data: T;                       // ๋ฉ”์‹œ์ง€๋ณ„ payload
}

JSON ์˜ˆ์‹œ

{
  "specversion": "1.0",
  "type": "vision.zone.enter",
  "source": "/services/bridge",
  "id": "01HW9XJ1A2B3C4D5E6F7G8H9JK",
  "time": "2026-05-04T10:00:00.123Z",
  "datacontenttype": "application/json",
  "subject": "session/01HW9X...",
  "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
  "data": { "session_id": "01HW9X...", "zone_id": "cafe", "confidence": 0.97 }
}

๊ทœ์น™

  • id๋Š” ๋ฐœํ–‰์ž๊ฐ€ ULID(Universally Unique Lexicographically Sortable Identifier, ๋ฒ”์šฉ ๊ณ ์œ  ์ •๋ ฌ ๊ฐ€๋Šฅ ์‹๋ณ„์ž)๋กœ ์ƒ์„ฑ. ๋™์ผ id ์žฌ์ˆ˜์‹  ์‹œ ๊ตฌ๋…์ž๋Š” ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ.
  • time์€ ๋ฐœ์ƒ ์‹œ๊ฐ, now()๊ฐ€ ์•„๋‹Œ ์ด๋ฒคํŠธ ์›์ฒœ ์‹œ๊ฐ(์˜ˆ: ํ”„๋ ˆ์ž„ ์บก์ฒ˜ ์‹œ๊ฐ).
  • traceparent๋Š” ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ํ•ญ์ƒ ํฌํ•จ โ€” ํŠธ๋ ˆ์ด์‹ฑ์ด ํ•œ ์ค„๋กœ ์ด์–ด์ง„๋‹ค.
  • subject๋Š” grepยทํ•„ํ„ฐ๋ง ๋ณด์กฐ โ€” ๊ฐ•์ œ ์•„๋‹˜.

์˜ˆ์‹œ ํ‘œ๊ธฐ ๊ด€๋ก€ (์ค‘์š”)

ยง3 ํ† ํ”ฝ ์นดํƒˆ๋กœ๊ทธ์˜ JSON ์˜ˆ์‹œ๋Š” ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด data ๋ถ€๋ถ„๋งŒ ํ‘œ๊ธฐํ•œ๋‹ค. ์‹ค์ œ MQTT(Message Queuing Telemetry Transport, ๋ฉ”์‹œ์ง€ ํ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์ „์†ก ํ”„๋กœํ† ์ฝœ) publish ์‹œ์—๋Š” ์œ„ envelope์œผ๋กœ ๊ฐ์‹ธ์•ผ ํ•œ๋‹ค.

๋Œ€ํ‘œ ์˜ˆ์‹œ โ€” vision/zone/enter์˜ ์‹ค์ œ publish payload:

{
  "specversion": "1.0",
  "type": "vision.zone.enter",
  "source": "/services/bridge",
  "id": "01HW9XJ1A2B3C4D5E6F7G8H9JK",
  "time": "2026-05-04T10:00:00.123Z",
  "datacontenttype": "application/json",
  "subject": "session/01HW9X...",
  "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
  "data": {
    "session_id": "01HW9X...",
    "zone_id": "cafe",
    "camera_id": "cafe_cam_01",
    "confidence": 0.97,
    "position": { "x": 2.4, "y": 1.1 },
    "timestamp": "2026-05-04T10:00:00.123Z"
  }
}

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ๋Š” envelope ์ƒ์„ฑยทํŒŒ์‹ฑ์„ ํ•œ ๊ณณ์— ์บก์Аํ™” ๊ถŒ์žฅ:

# packages/topics/envelope.py (Python)
def wrap(type_: str, data: dict, source: str, subject: str | None = None) -> dict:
    return {
        "specversion": "1.0",
        "type": type_,
        "source": source,
        "id": ulid.new().str,
        "time": datetime.now(timezone.utc).isoformat(),
        "datacontenttype": "application/json",
        **({"subject": subject} if subject else {}),
        **({"traceparent": current_traceparent()} if current_traceparent() else {}),
        "data": data,
    }

์˜ˆ์™ธ: frigate/+(์™ธ๋ถ€ ์‹œ์Šคํ…œ), frigate/{cam}/{label}ยท{zone}(๋‹จ์ˆœ ์นด์šดํŠธ/ONยทOFF) โ€” Frigate๊ฐ€ ์ž์ฒด ํฌ๋งท์œผ๋กœ ๋ฐœํ–‰ํ•˜๋ฏ€๋กœ envelope ๋ฏธ์ ์šฉ. bridge๊ฐ€ ๋ณ€ํ™˜ ์‹œ envelope ๋ถ€์ฐฉ.


2. QoS ยท retain ๊ฐ€์ด๋“œ

์นดํ…Œ๊ณ ๋ฆฌ QoS retain ์ด์œ 
vision/zone/+ 1 false ๋ˆ„๋ฝ ์‹œ ์ธํ„ฐ๋ž™์…˜ ๋ฏธ๋ฐœ๋™ โ†’ ์ตœ์†Œ 1ํšŒ ๋ณด์žฅ
vision/position/update 0 false 5Hz ๊ณ ๋นˆ๋„, ๋ˆ„๋ฝ ํ—ˆ์šฉ
frigate/+ (์™ธ๋ถ€) Frigate ๊ธฐ๋ณธ โ€” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ
session/+ 1 false ์„ธ์…˜ ๋ผ์ดํ”„์‚ฌ์ดํด์€ ๋ˆ„๋ฝ ๊ธˆ์ง€
robot/+/state 0 true ์‹ ๊ทœ ๊ตฌ๋…์ž๊ฐ€ ์ฆ‰์‹œ ์ตœ์‹  ์ƒํƒœ ํš๋“
robot/+/command 1 false ์•ˆ์ „ ๋ช…๋ น์€ ๋ณด์žฅ + ๋ฉฑ๋“ฑ
robot/+/ack 1 false ๋ช…๋ น ๊ฒฐ๊ณผ ์ถ”์ 
robot/+/fault 1 true ์•Œ๋ฆผ ๋ˆ„๋ฝ ๊ธˆ์ง€ + ์‹ ๊ทœ ๊ตฌ๋…์ž๋„ ์ธ์ง€
tablet/+/intent 1 false ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ˆ„๋ฝ ๊ธˆ์ง€
tablet/+/show_options 1 false UI ์ƒํƒœ
tablet/+/show_message 1 false ๋™์ผ
tablet/+/ack 1 false ํ‘œ์‹œ ํ™•์ธ
context/user/+/update 1 true ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง„์‹ค (4-segment)
context/environmental/update 1 true ๋งˆ์ง€๋ง‰ ํ™˜๊ฒฝ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง„์‹ค (3-segment)
safety/estop 2 true ์ ˆ๋Œ€ ๋ˆ„๋ฝ ๊ธˆ์ง€ + ์‹ ๊ทœ ๊ตฌ๋…์ž๋„ ์ธ์ง€
safety/proximity 1 false ์•Œ๋ฆผ์„ฑ
system/heartbeat/+ 0 true ๋ผ์ด๋ธŒ๋‹ˆ์Šค

Mosquitto๋Š” QoS 2๋ฅผ ์ง€์›ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์€ safety/estop์— ํ•œ์ • โ€” ๋น„์šฉ์ด ํฌ๋‹ค.


3. ํ† ํ”ฝ ์นดํƒˆ๋กœ๊ทธ

3.1 ์™ธ๋ถ€ ์‹œ์Šคํ…œ (Frigate)

frigate/events

{
  "type": "new",
  "before": null,
  "after": {
    "id": "1701234567.123456-abc",
    "camera": "cafe_cam_01",
    "frame_time": 1701234567.5,
    "label": "person",
    "score": 0.92,
    "box": [120, 80, 480, 720],
    "current_zones": ["cafe_zone"],
    "entered_zones": ["cafe_zone"]
  }
}

frigate/{camera}/{label} ์นด์šดํŠธ, frigate/{camera}/{zone} ์ ์œ 

  • ๋‹จ์ˆœ ์ •์ˆ˜/ON|OFF ๋ฌธ์ž์—ด. HA๊ฐ€ ์ž๋™ entity๋กœ ํก์ˆ˜.

3.2 vision.* โ€” ๋น„์ „ ๋„๋ฉ”์ธ (bridge๊ฐ€ ๋ณ€ํ™˜ยท๋ฐœํ–‰)

vision/zone/enter

  • type: vision.zone.enter
  • ๋ฐœํ–‰: bridge | ๊ตฌ๋…: orchestrator, home-assistant
  • QoS: 1 / retain: false
interface VisionZoneEnter {
  session_id: string;          // ULID โ€” bridge๊ฐ€ ReID๋กœ ๋งค์นญ
  zone_id: ZoneId;             // 'cafe' | 'park' | 'vehicle' | 'home'
  camera_id: string;           // 'cafe_cam_01'
  confidence: number;          // 0~1
  position: { x: number; y: number } | null;  // ํ‰๋ฉด ์ขŒํ‘œ (homography ๋ณ€ํ™˜)
  timestamp: string;           // ํ”„๋ ˆ์ž„ ์‹œ๊ฐ
}
{
  "session_id": "01HW9X...",
  "zone_id": "cafe",
  "camera_id": "cafe_cam_01",
  "confidence": 0.97,
  "position": { "x": 2.4, "y": 1.1 },
  "timestamp": "2026-05-04T10:00:00.123Z"
}

vision/zone/exit

  • type: vision.zone.exit
  • ์œ„์™€ ๋™์ผ + duration_s.
interface VisionZoneExit extends VisionZoneEnter {
  duration_s: number;          // ์ง„์ž…~์ดํƒˆ ๊ฒฝ๊ณผ
}

vision/position/update

  • type: vision.position.update
  • QoS: 0 / retain: false / ๋นˆ๋„: ์นด๋ฉ”๋ผ๋‹น ~5Hz
interface VisionPositionUpdate {
  session_id: string;
  zone_id: ZoneId;
  position: { x: number; y: number };
  velocity?: { vx: number; vy: number };
  timestamp: string;
}

3.3 session.* โ€” ์„ธ์…˜ ๋ผ์ดํ”„์‚ฌ์ดํด

session/start

  • type: session.start
  • ๋ฐœํ–‰: bridge (ํŽ˜์–ด๋ง ์™„๋ฃŒ ์‹œ OSNet ์ž„๋ฒ ๋”ฉ ๋ฐ”์ธ๋”ฉ ํ›„) | ๊ตฌ๋…: orchestrator, home-assistant
  • QoS: 1
interface SessionStart {
  session_id: string;          // ์‹ ๊ทœ ULID
  paired_tablet_id: string;
  visual_embedding_ref: string;  // bridge๊ฐ€ ReID์— ์‚ฌ์šฉ
  initial_zone: ZoneId | null;
  started_at: string;
}

session/end

  • type: session.end
  • ๋ฐœํ–‰: ์šด์˜์ž(HA) ๋˜๋Š” timeout(orchestrator)
interface SessionEnd {
  session_id: string;
  reason: "user_exit" | "timeout" | "operator_terminate" | "safety";
  duration_s: number;
  zones_visited: ZoneId[];
  ended_at: string;
}

session/end๋Š” SCRUB ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ๊ฒธํ•จ โ€” ๋ณ„๋„ SCRUB ํ† ํ”ฝ์„ ๋‘์ง€ ์•Š๋Š”๋‹ค. ๋ชจ๋“  ๊ตฌ๋…์ž๋Š” ์ˆ˜์‹  ์ฆ‰์‹œ ํ•ด๋‹น session_id์˜ ๋ชจ๋“  in-memory ๋ฐ์ดํ„ฐ(์ž„๋ฒ ๋”ฉยท์บ์‹œยทํŽ˜์–ด๋ง ํ† ํฐ)๋ฅผ ํ๊ธฐํ•ด์•ผ ํ•จ. ์ž์„ธํ•œ ์ฑ…์ž„ ๋ถ„๋‹ด์€ SYSTEM_DESIGN.md ยง11.3 ์ฐธ์กฐ.


3.4 robot.* โ€” ๋กœ๋ด‡ ๋„๋ฉ”์ธ

robot/{robot_id}/state

  • type: robot.state
  • ๋ฐœํ–‰: mcp-robot-adapter (1Hz) | ๊ตฌ๋…: ๋ชจ๋‘
  • QoS: 0 / retain: true
type RobotId = string;   // ๋Ÿฐํƒ€์ž„์— config/robots.yaml ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋กœ ๊ฒ€์ฆ (SYSTEM_DESIGN.md ยง7.2)
type Connection = "online" | "degraded" | "offline";

interface RobotState {
  robot_id: RobotId;
  pose: {
    x: number;                 // meters, ์ „์‹œ์žฅ ํ‰๋ฉด ์ขŒํ‘œ
    y: number;
    yaw: number;               // radians
    zone_id: ZoneId | null;    // pose๊ฐ€ ์–ด๋А zone polygon ๋‚ด๋ถ€์ธ์ง€
  };
  battery_pct: number;         // 0~100
  current_action: {
    type: string;              // 'goto' | 'follow' | ...
    started_at: string;
    eta_s: number | null;
    command_id: string;        // ํŠธ๋ฆฌ๊ฑฐํ•œ command์˜ ID
  } | null;
  faults: Fault[];             // ํ™œ์„ฑ fault ์Šค๋ƒ…์ƒท
  connection: Connection;
  timestamp: string;           // ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์บก์ฒ˜ ์‹œ๊ฐ
}

interface Fault {
  code: string;
  severity: "info" | "warning" | "critical";
  message: string;
  raised_at: string;
}

robot/{robot_id}/command

  • type: robot.command
  • ๋ฐœํ–‰: home-assistant(LLM(Large Language Model, ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ) ํด๋ฐฑ ๊ฒฝ๋กœ + ์šด์˜์ž ์ฝ˜์†” ์ˆ˜๋™ ๋ช…๋ น) | ๊ตฌ๋…: mcp-robot-adapter
  • QoS: 1

๋ช…๋ น ๊ฒฝ๋กœ ์ •์ฑ… (์ค‘์š”):

  • ์ฃผ ๊ฒฝ๋กœ (LLM): orchestrator โ†’ MCP(Model Context Protocol, ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ) tool call โ†’ mcp-robot-adapter ๋‚ด๋ถ€ entry โ†’ ํ โ†’ SDK(Software Development Kit, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ํ‚คํŠธ). MQTT๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์Œ.
  • ํด๋ฐฑ ๊ฒฝ๋กœ (HA): home-assistant ์ž๋™ํ™” โ†’ MQTT publish robot/+/command โ†’ mcp-robot-adapter MQTT subscriber โ†’ ๊ฐ™์€ ํ โ†’ SDK.
  • ๋‘ entry point๋Š” ๋™์ผํ•œ priority queue + safety interlock์œผ๋กœ ์ˆ˜๋ ด โ€” ๊ฒฐ๊ณผยทํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ๋Š” ๋™์ผ ํ† ํ”ฝ์œผ๋กœ ๋ฐœํ–‰.
  • orchestrator๋Š” ์ด ํ† ํ”ฝ์„ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค (ACL(Access Control List, ์ ‘๊ทผ ์ œ์–ด ๋ชฉ๋ก)์—์„œ write ๊ถŒํ•œ๋„ ๋ถ€์—ฌํ•˜์ง€ ์•Š์Œ).
type Capability = "goto" | "follow" | "stop" | "sit" | "stand" | "gesture" | "look_at";
type Priority = "safety" | "operator" | "llm";

interface RobotCommand {
  command_id: string;          // ULID
  robot_id: RobotId;
  capability: Capability;
  params: CapabilityParams;    // capability๋ณ„ ๋‹ค๋ฆ„ (์•„๋ž˜)
  priority: Priority;
  source: {
    service: "home-assistant" | "operator-console";   // MQTT ๊ฒฝ๋กœ๋Š” ํด๋ฐฑ ์ „์šฉ
    trace_id: string;
  };
  expires_at: string | null;   // ์ด ์‹œ์  ์ดํ›„ ํ์— ๋‚จ์•˜์œผ๋ฉด ํ๊ธฐ
}

// capability๋ณ„ params
interface GotoParams   { x: number; y: number; yaw?: number; velocity_max?: number; }
interface FollowParams { target_session_id: string; max_distance_m: number; }
interface StopParams   {} // empty
interface SitParams    {}
interface StandParams  {}
interface GestureParams { name: "wave" | "nod" | "shake"; }
interface LookAtParams { target_session_id?: string; target_pos?: { x: number; y: number }; }

type CapabilityParams =
  | GotoParams | FollowParams | StopParams | SitParams
  | StandParams | GestureParams | LookAtParams;
// ์˜ˆ์‹œ โ€” HA ํด๋ฐฑ ๊ฒฝ๋กœ(์šด์˜์ž ์ž๋™ํ™”)๊ฐ€ ๋ฐœํ–‰ํ•˜๋Š” ๋ช…๋ น
{
  "command_id": "01HW9X...",
  "robot_id": "spot-01",
  "capability": "look_at",
  "params": { "target_session_id": "01HW8M..." },
  "priority": "operator",
  "source": { "service": "home-assistant", "trace_id": "fallback-1714816800" },
  "expires_at": "2026-05-04T10:00:05Z"
}

robot/{robot_id}/ack

  • type: robot.ack
  • ๋ฐœํ–‰: mcp-robot-adapter | ๊ตฌ๋…: orchestrator, home-assistant
  • QoS: 1
type AckStatus = "accepted" | "rejected" | "started" | "completed" | "failed";

interface RobotAck {
  command_id: string;
  robot_id: RobotId;
  status: AckStatus;
  reason: string | null;       // rejected/failed ์‹œ ์›์ธ
  result: Record<string, unknown> | null;  // completed ์‹œ ๊ฒฐ๊ณผ
  timestamp: string;
}

๊ฐ™์€ command_id๋กœ ์‹œ๊ฐ„ ์ˆœ ์—ฌ๋Ÿฌ ack ๋ฐœํ–‰ ๊ฐ€๋Šฅ: accepted โ†’ started โ†’ completed.

robot/{robot_id}/fault

  • type: robot.fault
  • ๋ฐœํ–‰: mcp-robot-adapter | ๊ตฌ๋…: home-assistant(์•Œ๋ฆผ), orchestrator(๋ธ”๋ž™๋ฆฌ์ŠคํŠธ)
  • QoS: 1 / retain: true (ํ™œ์„ฑ ์ƒํƒœ)
interface RobotFaultEvent {
  robot_id: RobotId;
  fault: Fault;
  active: boolean;             // false = cleared
}

Cleared ์‹œ ๋™์ผ ํ† ํ”ฝ์— active: false payload๋ฅผ retain false๋กœ ๋ฐœํ–‰ํ•˜์—ฌ ์ƒํƒœ ๊ฐฑ์‹ .


3.5 tablet.* โ€” ํƒœ๋ธ”๋ฆฟ ๋„๋ฉ”์ธ

tablet/{tablet_id}/intent

  • type: tablet.intent
  • ๋ฐœํ–‰: mcp-tablet-gateway | ๊ตฌ๋…: orchestrator
  • QoS: 1
type IntentType = "option_select" | "free_text" | "mood_input" | "preference_update";

interface TabletIntent {
  tablet_id: string;
  session_id: string;
  intent_type: IntentType;
  payload: IntentPayload;
  timestamp: string;
}

interface OptionSelectPayload   { option_id: string; from_message_id: string; }
interface FreeTextPayload       { text: string; locale?: string; }
interface MoodInputPayload      { mood: "calm" | "excited" | "tired" | "neutral"; }
interface PreferenceUpdatePayload {
  patch: { music_genre?: string; drink?: string; };
}

type IntentPayload =
  | OptionSelectPayload | FreeTextPayload
  | MoodInputPayload | PreferenceUpdatePayload;
{
  "tablet_id": "t01",
  "session_id": "01HW9X...",
  "intent_type": "option_select",
  "payload": { "option_id": "drink_recommend", "from_message_id": "01HW9Y..." },
  "timestamp": "2026-05-04T10:01:00Z"
}

tablet/{tablet_id}/show_options

  • type: tablet.show_options
  • ๋ฐœํ–‰: orchestrator | ๊ตฌ๋…: mcp-tablet-gateway
  • QoS: 1
interface TabletShowOptions {
  tablet_id: string;
  message_id: string;          // ULID, ํ›„์† intent์˜ from_message_id
  options: Option[];
  ttl_s: number | null;        // ๋งŒ๋ฃŒ ์‹œ ์ž๋™ ์ˆจ๊น€
  timestamp: string;
}

interface Option {
  id: string;                  // 'drink_recommend' ๋“ฑ
  label: string;               // ์‚ฌ์šฉ์ž ํ‘œ์‹œ
  icon: string | null;         // material/feather icon name
  description: string | null;
}

tablet/{tablet_id}/show_message

  • type: tablet.show_message
  • ๋ฐœํ–‰/๊ตฌ๋…/QoS ๋™์ผ
interface TabletShowMessage {
  tablet_id: string;
  message_id: string;
  content: {
    text: string;
    media: Media[];
    speaker: string | null;      // robot_id ๊ฐ’ ๋˜๋Š” "system" (๋Ÿฐํƒ€์ž„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฒ€์ฆ)
  };
  duration_s: number | null;   // ์ž๋™ ์‚ฌ๋ผ์ง
  timestamp: string;
}

interface Media {
  type: "image" | "video" | "audio";
  url: string;
}

tablet/{tablet_id}/ack

  • type: tablet.ack
  • ๋ฐœํ–‰: mcp-tablet-gateway | ๊ตฌ๋…: orchestrator
interface TabletAck {
  tablet_id: string;
  message_id: string;
  status: "displayed" | "dismissed" | "failed";
  reason: string | null;
  timestamp: string;
}

3.6 context.* โ€” ์‚ฌ์šฉ์ž/๊ณต๊ฐ„/ํ™˜๊ฒฝ ์ปจํ…์ŠคํŠธ ๋ณ€๊ฒฝ

context/user/{session_id}/update

  • type: context.user.update
  • ๋ฐœํ–‰: orchestrator, home-assistant | ๊ตฌ๋…: orchestrator(์ž๊ธฐ ์ž์‹  ํฌํ•จ, dedup)
  • QoS: 1 / retain: true (๋งˆ์ง€๋ง‰ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง„์‹ค)
interface ContextUserUpdate {
  session_id: string;
  patch: Partial<UserContext>;  // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ
  source: "tablet" | "vision" | "system" | "operator";
  timestamp: string;
}

// UserContext ๋ณธ์ฒด๋Š” SYSTEM_DESIGN.md ยง4.1 ์ฐธ์กฐ

context/environmental/update

  • type: context.environmental.update
  • ๋ฐœํ–‰: home-assistant (๋‚ ์”จ ํ†ตํ•ฉ ๋“ฑ) | ๊ตฌ๋…: orchestrator
  • QoS: 1 / retain: true
interface EnvironmentalUpdate {
  weather: { temp_c: number; condition: string } | null;
  time_of_day: "morning" | "afternoon" | "evening" | "night" | null;
  visitor_count: number | null;     // ์ „์‹œ์žฅ ๋ˆ„์  ์ž…์žฅ์ž
  timestamp: string;
}

๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ(์˜ˆ: ๋‚ ์”จ๋งŒ) ์‹œ์—๋„ ๋ณ€๊ฒฝ ์•ˆ ๋œ ํ•„๋“œ๋Š” null๋กœ ๋ณด๋‚ด์ง€ ๋ง๊ณ  ์ƒ๋žต. ๊ตฌ๋…์ž๋Š” ๋จธ์ง€ ์ฒ˜๋ฆฌ.


3.7 safety.* โ€” ์•ˆ์ „ ์ด๋ฒคํŠธ

safety/estop

  • type: safety.estop
  • ๋ฐœํ–‰: HW e-stop(emergency stop, ๊ธด๊ธ‰ ์ •์ง€) ํ†ตํ•ฉ, mcp-robot-adapter, home-assistant(์šด์˜์ž) | ๊ตฌ๋…: ๋ชจ๋‘
  • QoS: 2 / retain: true (ํ™œ์„ฑ ์‹œ)
interface SafetyEstop {
  source: "hardware_button" | "operator_console" | "auto_proximity" | "auto_fault";
  active: boolean;             // true=engage, false=reset
  reason: string;
  timestamp: string;
}

active: false ๋ฐœํ–‰ ์‹œ retain ๋ฉ”์‹œ์ง€ ์ •๋ฆฌ๋˜์–ด ๋‹ค์Œ ๊ตฌ๋…์ž๋Š” e-stop ํ•ด์ œ ์ƒํƒœ๋กœ ์ธ์ง€.

safety/proximity

  • type: safety.proximity
  • ๋ฐœํ–‰: bridge (vision ๊ธฐ๋ฐ˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ) ๋˜๋Š” mcp-robot-adapter (lidar)
  • QoS: 1
interface SafetyProximity {
  robot_id: RobotId;
  session_id: string | null;
  distance_m: number;
  threshold_m: number;
  violated: boolean;
  timestamp: string;
}

3.8 system.* โ€” ์šด์˜ ํ—ฌ์Šค

system/heartbeat/{service}

  • type: system.heartbeat
  • ๋ฐœํ–‰: ๊ฐ ์„œ๋น„์Šค (10์ดˆ ์ฃผ๊ธฐ) | ๊ตฌ๋…: home-assistant (๋ผ์ด๋ธŒ๋‹ˆ์Šค ์•Œ๋ฆผ)
  • QoS: 0 / retain: true
interface SystemHeartbeat {
  service: string;             // 'orchestrator' | 'bridge' | ...
  version: string;             // ์ด๋ฏธ์ง€ ํƒœ๊ทธ
  uptime_s: number;
  status: "healthy" | "degraded";
  details: Record<string, unknown>;
  timestamp: string;
}

HA์—์„œ last_updated > 30s ๋˜๋ฉด alert.


4. ํ† ํ”ฝ โ†” ๋ฐœํ–‰์ž/๊ตฌ๋…์ž ๋งคํŠธ๋ฆญ์Šค

ํ† ํ”ฝ bridge orch robot-adp tablet-gw HA frigate
frigate/events S โ€” โ€” โ€” (S) P
vision/zone/+ P S โ€” โ€” S โ€”
vision/position/update P S โ€” โ€” โ€” โ€”
session/start P S โ€” โ€” S โ€”
session/end S (P)/S โ€” S P/S โ€”
robot/+/state โ€” S P โ€” S โ€”
robot/+/command โ€” โ€” S โ€” P โ€”
robot/+/ack โ€” S P โ€” S โ€”
robot/+/fault โ€” S P โ€” S โ€”
tablet/+/intent โ€” S โ€” P โ€” โ€”
tablet/+/show_options โ€” P โ€” S โ€” โ€”
tablet/+/show_message โ€” P โ€” S โ€” โ€”
tablet/+/ack โ€” S โ€” P โ€” โ€”
context/user/+/update (P) P/S โ€” โ€” (P) โ€”
context/environmental/update โ€” S โ€” โ€” P โ€”
safety/estop S S P/S S P/S โ€”
safety/proximity P S (P) โ€” S โ€”
system/heartbeat/+ P P P P S โ€”

P = ์ฃผ ๋ฐœํ–‰์ž, S = ์ฃผ ๊ตฌ๋…์ž, (P/S) = ๋ณด์กฐ


4.5 Last Will Testament (LWT) โ€” retain ๋ฉ”์‹œ์ง€ ์ •๋ฆฌ

robot/+/state(retain=true)ยทsafety/estop(retain=true)๋Š” ๋ฐœํ–‰์ž๊ฐ€ ๋น„์ •์ƒ ์ข…๋ฃŒํ•  ๊ฒฝ์šฐ stale ๋ฉ”์‹œ์ง€๊ฐ€ ์˜๊ตฌ ์ž”์กดํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ๋ชจ๋“  retain-true ๋ฐœํ–‰์ž๋Š” mosquitto ์—ฐ๊ฒฐ ์‹œ LWT๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.

๋ฐœํ–‰์ž๋ณ„ LWT ์ •์ฑ…

์„œ๋น„์Šค ํ† ํ”ฝ LWT payload retain
mcp-robot-adapter robot/{robot_id}/state { "robot_id":"spot-01", "connection":"offline", "timestamp":"..." } (envelope ํฌํ•จ) true
home-assistant safety/estop { "active": false, "source": "auto_disconnect", "timestamp":"..." } (ํ•ด์ œ ์˜๋ฏธ) true
๋ชจ๋“  ์ž์ฒด ์„œ๋น„์Šค system/heartbeat/{service} { "service":"...", "status":"degraded", ... } true

safety/estop์˜ LWT๋Š” active: false๋กœ ๋‘”๋‹ค โ€” broker ์—ฐ๊ฒฐ ๋Š๊ธด HA๊ฐ€ e-stop์„ active๋กœ ์ž ์žฌ์šธ ์ˆ˜ ์—†๊ฒŒ(false-positive). HW e-stop์€ ๋ณ„๋„ ํšŒ๋กœ๋กœ ๋ณดํ˜ธ.

Python ์˜ˆ์‹œ (mcp-robot-adapter)

import paho.mqtt.client as mqtt

client = mqtt.Client(client_id="mcp-robot-adapter")

# LWT ๋“ฑ๋ก (connect ์ „)
will_payload = json.dumps({
    "specversion": "1.0",
    "type": "robot.state",
    "source": "/services/mcp-robot-adapter",
    "time": datetime.utcnow().isoformat() + "Z",
    "data": {
        "robot_id": "spot-01",
        "connection": "offline",
        "timestamp": datetime.utcnow().isoformat() + "Z",
    },
})
client.will_set(
    topic="robot/spot-01/state",
    payload=will_payload,
    qos=1,
    retain=True,
)
client.connect("mosquitto", 1883)

Graceful shutdown

์ •์ƒ ์ข…๋ฃŒ ์‹œ์—๋„ ๋™์ผ payload๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ publish ํ›„ disconnect โ†’ broker๊ฐ€ LWT ๋ฐœ๋™ ์•ˆ ํ•จ.

def shutdown_handler(*_):
    client.publish("robot/spot-01/state", will_payload, qos=1, retain=True)
    client.disconnect()

Retain ๋ฉ”์‹œ์ง€ ์ˆ˜๋™ ์ •๋ฆฌ

์šด์˜ ์ค‘ ์˜๋„์น˜ ์•Š์€ retain ๋ฉ”์‹œ์ง€๊ฐ€ ๋‚จ์•˜์„ ๋•Œ:

docker exec mosquitto mosquitto_pub -h localhost -t "robot/spot-01/state" -r -n

(-r -n: retain + null payload โ†’ broker๊ฐ€ retain ๋ฉ”์‹œ์ง€ ์‚ญ์ œ)


5. ๋ฉฑ๋“ฑ์„ฑ๊ณผ ์ˆœ์„œ

  • ๋ฉฑ๋“ฑ์„ฑ: ๋™์ผ id(envelope) ์žฌ์ˆ˜์‹  ์‹œ ๊ตฌ๋…์ž๋Š” ์žฌ์ฒ˜๋ฆฌ ๊ธˆ์ง€. orchestrator๋Š” in-memory LRU(Least Recently Used, ์ตœ๊ทผ ๋ฏธ์‚ฌ์šฉ ์ œ๊ฑฐ ์บ์‹œ ์ •์ฑ…)(1024 entries) ์‚ฌ์šฉ ๊ถŒ์žฅ.
  • ์ˆœ์„œ: MQTT ์ž์ฒด๋Š” ํ† ํ”ฝ-ํด๋ผ์ด์–ธํŠธ ๋‹จ์œ„ ์ˆœ์„œ๋งŒ ๋ณด์žฅ. ์—ฌ๋Ÿฌ ํ† ํ”ฝ ๊ฐ„ ์ˆœ์„œ๊ฐ€ ์˜๋ฏธ ์žˆ์„ ๋•Œ(์˜ˆ: vision/zone/enter โ†’ tablet/+/intent)๋Š” traceparent์™€ time์œผ๋กœ ์‚ฌํ›„ ์ •๋ ฌ.
  • clock skew: ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ NTP ๋™๊ธฐํ™” ํ•„์ˆ˜.

6. ํ† ํ”ฝ ์™€์ผ๋“œ์นด๋“œ ์‚ฌ์šฉ

์„œ๋น„์Šค๊ฐ€ ๊ตฌ๋…ํ•  ๋•Œ ๋‹ค์Œ ๊ถŒ์žฅ:

# bridge
mqtt.subscribe("frigate/events", qos=1)
mqtt.subscribe("session/end",    qos=1)  # SCRUB: ์ž„๋ฒ ๋”ฉ ์ฐธ์กฐ ํ•ด์ œ (bridge๋Š” session/start ๋ฐœํ–‰์ž)

# orchestrator
mqtt.subscribe("vision/zone/+",               qos=1)
mqtt.subscribe("tablet/+/intent",             qos=1)
mqtt.subscribe("context/+/+/update",          qos=1)  # context/user/{id}/update (4-segment)
mqtt.subscribe("context/environmental/update", qos=1)  # ๋ณ„๋„ ๊ตฌ๋… ํ•„์š” (3-segment, ์œ„ ํŒจํ„ด ๋ฏธ๋งค์นญ)
mqtt.subscribe("robot/+/ack",                 qos=1)
mqtt.subscribe("safety/+",                    qos=2)

# mcp-robot-adapter
mqtt.subscribe("robot/+/command", qos=1)
mqtt.subscribe("safety/estop",    qos=2)

7. ์Šคํ‚ค๋งˆ ์ง„ํ™” ์ •์ฑ…

  • Backward ํ˜ธํ™˜ ์›์น™: ์ƒˆ ํ•„๋“œ๋Š” optional๋กœ ์ถ”๊ฐ€. ๊ธฐ์กด ํ•„๋“œ ์‚ญ์ œยทํƒ€์ž… ๋ณ€๊ฒฝ ๊ธˆ์ง€.
  • Breaking change ์‹œ type ๋ฒ„์ „ํ™”: vision.zone.enter.v2 ์ฒ˜๋Ÿผ type ๋งˆ์ง€๋ง‰์— .v2. v1ยทv2 ๋ณ‘ํ–‰ ๋ฐœํ–‰ โ†’ ๋ชจ๋“  ๊ตฌ๋…์ž ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ v1 ํ๊ธฐ.
  • ์Šคํ‚ค๋งˆ ์ •์˜๋Š” packages/topics/์— ๋‹จ์ผ ์†Œ์Šค๋กœ ๋‘๊ณ  codegen.

8. ๋””๋ฒ„๊น… ํŒ

  • ๋ชจ๋“  ํ† ํ”ฝ ๋ผ์ด๋ธŒ ํ…Œ์ผ:
    docker exec -it mosquitto mosquitto_sub -h localhost -t '#' -v
    
  • ํŠน์ • trace_id ์ถ”์ :
    mosquitto_sub -h localhost -t '#' -v | grep '0af7651916cd43dd8448eb211c80319c'
    
  • Retained ๋ฉ”์‹œ์ง€ ํ™•์ธ:
    mosquitto_sub -h localhost -t 'robot/+/state' -v --retained-only
    
  • ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ: mosquitto_pub์„ ๋ฃจํ”„์—์„œ ํ˜ธ์ถœ, MQTT broker๋Š” ๋‹จ์ผ ๋…ธ๋“œ์—์„œ๋„ ์ˆ˜๋งŒ msg/s ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ.