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
- ๋ฐํ: Frigate
- ๊ตฌ๋
:
bridge - ํฌ๋งท: Frigate ์์ฒด (CloudEvents ๋ฏธ์ ์ฉ)
- ์ฐธ์กฐ: https://docs.frigate.video/integrations/mqtt#frigateevents
{
"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 publishrobot/+/commandโmcp-robot-adapterMQTT 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: falsepayload๋ฅผ 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 ์ฒ๋ฆฌ ๊ฐ๋ฅ.