Repository navigation
MQTT_SCHEMA FUTURE_CAR
이 문서는 Future Car 팀이 작성합니다. 위키
MQTT_SCHEMA-FUTURE_CAR(2026-07-19판)를main-2026실제 구현 기준으로 갱신한 것입니다. 초안이 아니라 현재 배포되어 도는 코드를 기술합니다 — 스키마와 코드가 어긋나면 코드가 정본입니다.
- 대상 구현:
zer01ne-future-car-server브랜치main-2026 - 배포: Mac Studio (Tailscale) · FastAPI + paho-mqtt
- 최종 수정일: 2026-08-28 (차량 마이크 STT 트리거 · task/start · basic 만 수행 · 8장 전체 흐름 서술)
plan · dashboard · me enter · exit
OC ───────────────────────────────────────▶ 미래차 ◀────────────── 태블릿
OC ◀────────────────────────────────────── 미래차 ──────────────▶ 태블릿
task/start · task/complete · ai/mode state · route · step · error · ai/response(질문) · ai/suggestions
▲
차량 마이크(STT) — 관람객 발화가 스텝을 이끈다
| 상대 | 미래차가 받는 것 | 미래차가 보내는 것 |
|---|---|---|
| OC |
plan(페르소나·장소 카탈로그) · dashboard(차 앞뒤 장소·태스크) · me(동행자 프롬프트) |
task/start · task/complete · ai/mode
|
| 태블릿 |
enter · exit
|
state · route · step · error · ai/response·ai/suggestions(채팅 질문과 답 후보 — 직접 발행)
|
| 관람객(차량 마이크) | 발화(STT → POST /ambient/utterance) — 스텝 진행 트리거
|
— |
2026-08-28 정책: 스텝 진행은 차량 화면에 연결한 마이크로만 한다. 미래차 모드(
enter~exit) 동안 태블릿은 마이크·타이핑·제안 칩을 잠근다(tablet-fe 작업).ai/response.transcript구독은 남아 있지만 정식 경로가 아니다.
즉 데이터는 OC 에서 받고, 조작은 태블릿에서 받고, 결과는 양쪽에 보낸다. OC 와의 상호작용이 적은 게 아니라 "받는 것 4, 보내는 것 2~3" 으로 오히려 OC 쪽이 더 많다. 태블릿과는 요청 2종·응답 4종이 전부다.
구현 스펙(담당자 전달용): OC · tablet-fe
- 0. 2026-07-19판 대비 변경점
- 1. 개요
- 2. 토픽 목록
- 3. REQUEST (
future-car/request) - 4. DASHBOARD (
orchestration/{sessionId}/dashboard) - 5. RESPONSE (
future-car/response) - 6. 비-MQTT 인바운드 — 렌더 완료 보고 · 관람객 발화
- 7. 상태머신 (phase)
- 8. 전체 흐름 — 탑승 직전부터 하차까지
- 9. tablet-fe 연동 가이드
- 10. QoS · retain · 기타 설정
- 11. 남은 합의 항목
먼저 읽어야 하는 부분입니다. 기존 문서를 알고 계신 분은 여기만 봐도 차이를 알 수 있습니다.
| # | 항목 | 2026-07-19판 | main-2026 (현재) |
|---|---|---|---|
| 1 | RESPONSE 타입 |
state · reaction 2종 |
state · step · error 3종. reaction 은 계약에 남아있으나 발행하지 않음
|
| 2 | 스텝 콘텐츠 전달 | 명시 없음 |
step 메시지가 주력. 미래차 화면이 재생할 연출 데이터(data)를 통째로 실어 보냄 |
| 3 | plan.stops |
스텝 수·장소를 결정 |
생성 입력으로 쓰지 않음. plan.persona 만 사용, 스텝 수는 4개 고정
|
| 4 | 여정 소재 | plan.stops[].place |
orchestration/{sid}/dashboard 토픽에서 차 위치의 이전/다음 장소·태스크를 추출 |
| 5 | start |
독립 트리거 (step1 진입) |
폐기. 진행은 advance 로 통일. 아웃바운드(NextAction)에서는 제거했고, 인바운드만 별칭으로 받아준다 |
| 6 | context_action |
미래차가 reaction 생성 |
no-op. 수신해도 아무 동작 없음(디스패치 안정성 위해 핸들러만 유지) |
| 7 |
phase=arrived 진입 |
미정 | 미래차 화면의 렌더 완료 보고로 진입 (아래 6장) |
| 8 | retained request
|
언급 없음 |
retain 재전송분은 무시. 서버 재시작 시 과거 세션 대량 재생성을 막기 위함 (아래 3장 |
| 9 |
state 필드 |
— |
prev_place · next_place 추가 (대기 화면이 "어디서 왔는지"를 표시) |
| 10 | 에러 통지 | 없음 |
error 타입 + 코드 4종 |
| 11 | 진행 트리거 | 태블릿 advance 버튼 |
관람객 발화 — 차량 마이크 STT → POST /ambient/utterance. advance 폐기 — 태블릿은 enter·exit 만 보낸다 |
| 12 | 스텝 생성 시점 | plan 수신 직후 4개 일괄 | 발화가 도착할 때 하나씩. 미리 만들지 않는다 |
| 13 | 태블릿 채팅 | 무관 | 미래차가 ai/response·ai/suggestions 에 질문과 답 후보를 직접 발행한다(3-2-1). 관람객의 답은 차량 마이크로 받는다 |
| 14 | RESPONSE 타입 | 3종 |
+ route (이번 구간의 출발/도착 stop_id) |
| 15 | state.next |
advance | exit
|
exit 하나. 마지막 스텝 도착 때만 실린다 |
| 16 | 구독 토픽 | request · dashboard | + ai/response · me |
| 17 | 차량 태스크 | 제목만 소재로 사용 |
type=basic 만 step 당 1개씩 AI 가 골라 수행. 스텝 발행 시 task/start, 렌더 완료 시 task/complete(서버는 완료분을 빼서 중복 방지). 그 외 타입(돋보기 모달형)은 태블릿에서 관람객이 완료 — 미래차는 손대지 않는다 |
| 18 | step4 질문 | 있음 | 없음. 마지막 step 뒤에는 질문을 띄우지 않는다 |
| 19 | ai/request |
지시문 동봉 |
쓰지 않는다. 질문은 ai/response 직접 발행(A′). passthrough 는 OC 가 직접 발행을 거절했을 때의 대안으로만 남김(env OC_CHAT_PUBLISH=request) |
| 항목 | 내용 |
|---|---|
| 브로커 |
MQTT_BROKER_URL (기본 tcp://127.0.0.1:1883) |
| 계정 | user future-car / MQTT_USERNAME·MQTT_PASSWORD 로 오버라이드 |
| client_id |
future-car (MQTT_CLIENT_ID) — |
| 페이로드 | 평문 JSON · snake_case · session_id 는 토픽 경로에만 (페이로드에 없음) |
| 분기 방식 | request/response 2토픽 + type 필드 |
| MQTT 접속 주체 | 미래차 FastAPI 서버 1개. 브라우저(전시 화면 /ambient)는 MQTT 미접속 → 서버가 WebSocket 으로 중계 |
| 스텝 수 |
4 고정 (STEP_COUNT = 4). plan.stops 개수와 무관 |
⚠️ client_id 충돌 주의. 같은client_id로 두 인스턴스가 붙으면 브로커가 먼저 붙은 쪽을 끊습니다(session taken over). 로컬 개발 서버를 띄울 땐 반드시MQTT_CLIENT_ID를 다르게 주세요. 증상은 "메시지가 조용히 사라짐"이라 원인을 찾기 어렵습니다.
| 구분 | Topic | 방향 | QoS | Retain | 설명 |
|---|---|---|---|---|---|
| REQUEST | orchestration/{sessionId}/future-car/request |
OC·태블릿 → 미래차 | 1 | OC 가 plan 을 retain 발행 중 |
type: plan · enter · exit (start·advance 폐기·별칭 · context_action no-op) |
| RESPONSE | orchestration/{sessionId}/future-car/response |
미래차 → 태블릿·OC | 1 |
state 만 retain=true |
type: state · route · step · error
|
| DASHBOARD | orchestration/{sessionId}/dashboard |
OC → 전체 | 1 | retain=true | 방문자의 모든 장소·태스크. 미래차는 구독만 |
| ME | orchestration/{sessionId}/me |
OC → 전체 | 1 | retain=true |
ai_companion.prompt — 스텝 생성 프롬프트 앞에 붙일 인격. 구독만
|
| AI RESPONSE | orchestration/{sessionId}/ai/response |
OC → 전체 + 미래차 → 태블릿 | 1 | false | OC 발행분: transcript = 관람객 발화(STT) → 미래차가 스텝 재료로. 미래차 발행분: message = 채팅에 띄울 질문(A′) |
| AI SUGGESTIONS | orchestration/{sessionId}/ai/suggestions |
미래차 → 태블릿 | 1 | false | 미래차 질문에 대한 답 후보 칩 2개 |
| AI MODE | orchestration/{sessionId}/ai/mode |
미래차 → OC | 1 | true |
enter 직후 {"mode":"car","ttl_sec":1800}, exit 직후 {"mode":"chat"}. OC 구현 완료(2026-08-24) — car 동안 type=voice 는 STT 만, signout 시 retained 삭제 |
| TASK START | orchestration/{sessionId}/task/start |
미래차 → OC | 1 | false |
{"task_id"} — 스텝 발행(주행 시작) 시 그 스텝의 basic 태스크를 진행 중으로. OC 가 dashboard 를 in_progress 로 재발행 → 태블릿 카드 "진행 중". TASK_START_ENABLED=false 로 끌 수 있다 |
| TASK COMPLETE | orchestration/{sessionId}/task/complete |
미래차 → OC | 1 | false |
{"task_id"} — 렌더 완료 시 같은 태스크를 완료로. 태블릿이 보내는 것과 동일 페이로드. OC 가 dashboard·point 재발행(응답 토픽 없음) |
구독은 전부 와일드카드다 — session_id 는 토픽에서 추출한다.
orchestration/+/future-car/request
orchestration/+/dashboard
orchestration/+/me
orchestration/+/ai/response
OC 가 plan 을 retain=true 로 발행합니다. 그러면 미래차 서버가 재시작해 재구독하는 순간 브로커가 과거 방문자들의 plan 을 전부 다시 보냅니다. 그대로 처리하면 세션마다 4스텝씩 LLM 재생성이 돌아, 재배포 1회에 36세션 × 4 ≈ 144회 호출이 발생합니다(실측).
그래서 retain 플래그가 선 REQUEST 메시지는 전부 버립니다.
이게 라이브 발행을 막지 않는 이유: MQTT 명세상 retain 플래그는 새 구독의 결과로 재전송될 때만 세워집니다. 이미 구독 중인 클라이언트에게 실시간으로 배달되는 메시지에는 서지 않습니다. 즉 OC 는 지금처럼 retain=true 로 계속 발행하면 되고, 정상 여정은 그대로 처리됩니다.
페르소나 생성 완료 시 1회 발행. 미래차는 이걸 받는 즉시 백그라운드로 4스텝을 순차 생성합니다.
{
"type": "plan",
"plan": {
"persona": { "id": 12, "name": "지호", "summary": "야근 잦은 개발자" },
"stops": []
}
}| 필드 | 타입 | 필수 | 미래차가 쓰는가 |
|---|---|---|---|
type |
string | Y | — |
plan.persona |
object | Y | ✅ 생성 입력. 구조는 자유(dict 통째로 LLM 에 전달) |
plan.stops[] |
array | Y (빈 배열 허용) | ❌ 쓰지 않음. 계약 호환을 위해 필드는 유지 |
-
스텝 수는
stops와 무관하게 항상 4개입니다. 전시 정책상 고정입니다. - 여정 소재(장소·태스크)는
stops가 아니라 dashboard 토픽에서 옵니다(4장). -
동일 plan 재수신은 무시합니다(값 비교). 진행 중인 여정이 리셋되지 않습니다. 단 이미
done인 세션은 동일 plan 이어도 새 여정으로 다시 시작합니다 — 그러지 않으면 그session_id가 영구히 잠깁니다. - 다른 plan 이 오면 전체 리셋 후 처음부터 재생성합니다.
미래차 공간 입장. → 대기 화면(step0)으로 전환. 발행 주체: 태블릿.
{ "type": "enter" }응답: state · phase=waiting (next 없음)
그리고 미래차가 곧바로 ai/response 에 고정 질문 "출발 할까요?" 와 ai/suggestions 답 후보 2개를 발행하고(3-2-1),
ai/mode {car} 를 retained 로 올린다. 질문은 태블릿 채팅에 뜨고, 관람객이 차에 대고 답하면 그 답이 step1 생성의 재료가 된다(6-2).
→ 진행 흐름은 7장·8장 참고.
결정(2026-08-23): 두 부분으로 나뉜다.
A′ — 미래차 문장은 미래차가 직접 쓴다 (적용됨, OC 작업 없음)
미래차는 enter 직후와 각 step 렌더 완료 후 orchestration/{sid}/ai/response 에
{ok:true, context_id, message:"질문"} 과 ai/suggestions {context_id, suggestions:[답 후보 2개]} 를 직접 발행한다.
태블릿은 이미 모르는 context_id 의 message 를 AI 말풍선으로, suggestions 를 칩으로 그린다 — 태블릿 변경 없음.
OC 는 이 토픽을 구독하지 않으므로 영향 없지만, 계약상 OC 가 발행자인 토픽이라 동의를 구한다.
거절 시 대안: ai/request 에 passthrough:true(LLM 없이 그대로) 구현 요청 — 미래차는 env 한 줄로 그 경로로 돌아간다.
B — 세션 모드 ai/mode (OC 구현 완료 2026-08-24, 미래차 발행 중)
enter 직후 {"mode":"car","ttl_sec":1800}, exit 직후 {"mode":"chat"} (retained).
car 동안 OC 는 관람객 음성 발화(type=voice)에 STT 는 하되 LLM 답변·후속 제안을 생략한다(transcript 만, message 없음). 안전장치: TTL 만료·exit·signout(retained 삭제) 세 겹.
type=text(칩 탭·타이핑)에는 걸리지 않는다 — 그래서 태블릿이 미래차 모드 동안 입력을 잠그는 것(9-2-1)이 1차 방어고, ai/mode 는 2차다.
2026-08-28 정책으로 관람객 발화는 차량 마이크로만 받으므로, 여정 중 태블릿 AI 와의 대화는 없다.
둘 다 쓰지 마세요. 스텝 진행은 버튼이 아니라 관람객 발화가 이끈다(7장).
-
아웃바운드에서는 완전히 제거됐다.
NextAction은exit하나뿐이다. - 인바운드는 받아준다 — 받으면 "발화 없이 강제 진행" 한다(현장 비상용·디버그용). 지워버리면 구 태블릿은 아무 응답도 못 받고 멈춘다(unknown type 은 에러조차 발행하지 않음).
- 받으면
/mqtt트래픽 로그에deprecated로 남는다. 이 기록이 더는 안 보이면 별칭을 제거해도 안전하다. - 미래차
/mqtt디버그 화면의 수동 발행에서advance는 강제 진행 버튼으로 여전히 쓸 수 있다.
여정 종료. → 작별 화면 고정.
{ "type": "exit" }응답: state · phase=done
이후 새 plan 이 오기 전까지 어떤 요청도 상태를 바꾸지 못하며, error · JOURNEY_ENDED 가 돌아옵니다.
{ "type": "context_action", "context_id": "c1", "action_kind": "open_meeting_doc" }ambient 는 사전 생성된 step/error 로만 응답하므로 수신해도 아무 동작이 없습니다(로그만 남김). reaction 도 발행하지 않습니다.
OC 가 방문자의 모든 장소(집/공원/마트/차 …)와 태스크를 retain 발행합니다. 미래차는 구독만 하며, 여기서 차량 부분만 골라 스텝 생성의 여정 소재로 씁니다.
{
"locations": [
{ "name": "집", "visit_order": 1, "tasks": [ { "title": "..." } ] },
{ "name": "차", "visit_order": 2, "tasks": [ { "title": "충전소 경유" } ] },
{ "name": "마트", "visit_order": 3, "tasks": [] }
]
}| 필드 | 용도 |
|---|---|
locations[].name |
차량 장소 판별 기준. 이름이 차 로 시작하면 차량으로 본다 |
locations[].visit_order |
정렬 키. 차 바로 앞/뒤 장소를 prev_place/next_place 로 뽑는다 |
locations[].tasks[] |
차에 걸린 태스크. id·title·type·description·status 를 추린다. description 은 planning 메타(planning_task_id: … 같은 key: value 줄)를 걷어낸 첫 문장만 쓴다. type=basic 만 미래차가 수행한다(태블릿에 조작 UI 가 없는 타입 — 그 외는 돋보기 모달에서 관람객이 완료). step 당 1개 — 어느 것을 할지는 AI 가 고른다. 서버는 아직 안 끝난 basic 태스크만(tasks_remaining) 매 턴 넘기고, AI 가 그중 하나를 task_key 로 보고하면 스텝 발행 때 task/start, 관람객이 연출을 본 뒤(렌더 완료) task/complete 를 발행하고 다음 턴 목록에서 뺀다. 4 step 이라 basic 이 1개면 나머지 step 은 태스크 없이 간다(몰아서 처리하지 않음). status 는 OC 재발행으로 in_progress→completed 로 돌아오며, completed·skip 만 "끝남" 으로 본다 |
-
이름이 1순위 판별 기준입니다.
id는 시드마다 정수/UUID 로 제각각이라 불안정합니다. - 운영 브로커의 retain 메시지 193건을 검증한 결과 차량 장소 이름은
차,차량,차량 - 이동세 변형이 관측됐습니다 → 접두사차로 판별합니다. -
device.name은 쓰지 않습니다.Physical AI Hub라는 device 가집과차양쪽에 등장한 사례가 있어 신뢰할 수 없습니다. - 접두사는 코드 배포 없이 바꿀 수 있게
DASHBOARD_VEHICLE_PREFIXES환경변수(콤마 구분)로 오버라이드됩니다.
- 차량 장소가 아예 없는 세션이 정상적으로 존재합니다 — OC 가 관람객마다 장소를 랜덤 배정하기 때문입니다. 이 경우 persona 만으로 생성하며 경고 로그를 남깁니다.
- plan 과 dashboard 는 별도 토픽이라 도착 순서가 보장되지 않습니다. 미래차는 생성 시점에 "그 순간 최신값"을 조회하는 방식으로 대응합니다.
- retain 재전송분은 기록 없이 버립니다 — 구독 직후 과거 세션 200건가량이 한꺼번에 밀려와 디버그 화면의 링버퍼를 덮어쓰기 때문입니다.
type 3종입니다. 전부 같은 토픽으로 나갑니다.
{
"type": "state",
"step": 2,
"phase": "arrived",
"contexts": [],
"next": "advance",
"prev_place": "회사",
"next_place": "집"
}| 필드 | 타입 | 설명 |
|---|---|---|
type |
string |
"state" 고정 |
step |
integer | 현재 스텝 (0~4) |
phase |
string |
idle | waiting | driving | arrived | done
|
next |
string | 없음 | 태블릿 주 버튼 판단. 값은 항상 하나다(아래 표 참고). 해당 없으면 필드 자체가 생략됨 |
prev_place |
string | 없음 | dashboard 기준 차 이전 장소. 대기 화면이 "어디서 왔는지" 표시 |
next_place |
string | 없음 | dashboard 기준 차 다음 장소. 마지막 스텝에서 다음 공간으로 인계 |
scene · stop · contexts
|
— |
plan.stops 를 안 쓰므로 비어서 나갑니다. 렌더에 쓰지 마세요 |
next 는 exit 하나뿐이고, 마지막 스텝(4) 도착 때만 실린다. 그 외에는 필드 자체가 없다.
진행 버튼이 사라졌으므로 태블릿이 관리할 버튼은 exit 하나다.
| 상황 | phase |
next |
|---|---|---|
| plan 수신 | idle |
(없음) |
| enter 직후 | waiting |
(없음) — 버튼 대신 채팅 질문이 뜬다 |
| step 1·2·3 렌더 완료 | arrived |
(없음) — 채팅 질문이 뜬다 |
| step 4 렌더 완료 | arrived |
exit |
| 재생 중(step 메시지) | driving |
(state 를 발행하지 않음) |
| exit 직후 | done |
(없음) |
직렬화 시
null필드는 아예 빠집니다(exclude_none).next가 없으면 "다음 동작 없음"입니다.
{
"type": "step",
"step": 1,
"phase": "driving",
"data": { "step": 1, "bgv": {...}, "bgm": {...}, "assets_timeline": [...] }
}| 필드 | 타입 | 설명 |
|---|---|---|
step |
integer | 스텝 번호 (1~4) |
phase |
string | 발행 시점 phase (보통 driving) |
data |
object |
미래차 화면 전용 연출 데이터(classic StepInfo 형식). 태블릿은 해석할 필요 없음 |
retain 하지 않는 이유:
step은 "지금 이걸 재생하라"는 1회성 명령입니다. retain 하면 나중에 접속·재접속한 구독자에게 브로커가 지난 step 을 자동 재전달해 엉뚱한 재생이 일어납니다.
step 직전에 발행된다. 태블릿이 "지금 어디서 어디로 가는 중인지"를 그린다.
{ "type": "route", "step": 2, "from_stop_id": "0199…aaa", "from_place": "home", "to_stop_id": "0199…bbb", "to_place": "mart" }| 필드 | 설명 |
|---|---|
step |
이 구간이 속한 스텝 (1~4) |
from_stop_id · to_stop_id
|
태블릿이 쓰는 기준. plan.stops[].id 중 하나 |
from_place · to_place
|
사람이 로그를 읽을 때의 보조값. home·park·mart·factory·charging_station·drone
|
- LLM 이 채우지 못한 값은 필드가 생략된다(
null아님).stop_id가 없으면place로 폴백하거나 이전 구간을 유지하라. - 여정은 장소 5개를 잇는 4구간이다. P0(최초 출발)·P4(최종 도착)는 dashboard(차 앞뒤 장소)에서, P1~P3 는 LLM이 고른다. 서버는 연속성을 교정하지 않는다 — 프롬프트가 책임진다.
{ "type": "error", "step": 2, "code": "STEP_NOT_READY", "message": "step 2 준비 안 됨" }| code | 언제 | 태블릿 대응 |
|---|---|---|
STEP_NOT_READY |
생성이 아직 안 끝났는데 진행 요청 | 잠시 후 재시도 가능 |
GENERATION_FAILED |
LLM 생성 실패 | 재시도해도 안 됨 — 운영자 개입. 요청 없이도 실패 즉시 푸시됨 |
PLAN_MISSING |
plan 없이 진행 요청 | OC 의 plan 발행 확인 |
JOURNEY_ENDED |
exit 이후 요청 / 마지막 스텝 초과 |
정상 종료 상태. 새 plan 대기 |
계약 모델은 남아있지만 ambient 파이프라인은 발행하지 않습니다. 태블릿은 구현하지 않아도 됩니다.
미래차 화면(/ambient)이 해당 스텝의 asset(대사·팝업·음성)을 전부 재생하면 서버에 알린다.
POST http://creator-mac-studio.tail69930a.ts.net:8100/ambient/step-rendered
{ "session_id": "...", "step": 2 }
MQTT 가 아니라 HTTP 인 이유: request 토픽은 태블릿·OC 의 명령 채널이고, 이건 화면 자신의 사실 보고다. 브로커가 죽어도 도달해야 하고, 태블릿 요청과 섞이면 트래픽 해석이 어려워진다.
이 보고를 받으면 서버가 세 가지를 한다:
-
phase=arrived로 올리고state발행 (step 4 면next=exit) - 그 스텝에서 AI 가 고른 차량 태스크를
task/complete로 발행 (step 당 1개, 몰아서 처리 없음. 시작은 스텝 발행 때task/start로 이미 알렸다) -
그 스텝의
question을ai/response에,answer_suggestions를ai/suggestions에 발행 → 태블릿 채팅에 다음 질문과 답 후보가 뜬다. step 4 는 발행하지 않는다 — 다음 step 이 없어 답이 와도 갈 곳이 없다 - 재생 중 미뤄둔 관람객 발화가 있으면 곧바로 다음 스텝 생성을 시작
⚠️ 예전에는 "버튼을 켜도 된다"는 신호였지만, 이제 이 보고가 안 오면 여정이 그 자리에서 멈춘다. 질문이 안 뜨니 관람객이 답할 수 없고, 답이 없으니 다음 스텝이 안 만들어진다.
서버가 걸러내는 것: 이미 다음 스텝으로 넘어간 뒤 도착한 지각 보고, 같은 스텝 중복 보고, exit 이후 보고 → 전부 무시.
차량 화면(/ambient)이 마이크로 받아 STT 한 관람객 발화를 보낸다. 화면은 관람객 차례(대기 화면, 스텝 렌더 완료 뒤 ~ 다음
step 수신 전, 서버 error 뒤)에만 마이크를 열고, 재생 중에는 닫는다(우리 TTS 를 받아 적는 자기수신 방지).
POST /ambient/utterance { "session_id": "...", "transcript": "커피 마시고 싶어요", "source": "car-stt" }
→ { "ok": true, "result": "collected" } (closed / empty 면 ok:false)
GET /ambient/utterance/{session_id} ← 지금까지 수집된 발화 목록
ai/response 수신과 같은 판정 경로를 탄다 — 수집 창(enter~exit)이 닫혀 있으면 똑같이 버려진다. 수집되면 곧바로
다음 스텝 생성이 시작되고, 화면은 서버가 수집했다는 응답을 받으면 그 창에서는 더 듣지 않는다(중복 트리거 방지).
/mqtt 나 curl 로 흉내낼 때는 source:"debug" 로 보내면 트래픽 로그에서 구분된다.
진행은 태블릿 버튼이 아니라 관람객 발화가 이끈다 — 차량 마이크 STT(6-2). 태블릿이 보내는 것은 enter·exit 둘뿐이고, 미래차 모드 동안 태블릿 입력은 잠긴다.
stateDiagram-v2
[*] --> idle: plan 수신
idle --> waiting: enter (+ 채팅에 "출발 할까요?")
waiting --> driving: 관람객 발화 → step1 생성·송출
driving --> arrived: 화면 렌더 완료 (+ 채팅에 그 스텝의 question)
arrived --> driving: 관람객 발화 → step N+1 생성·송출
arrived --> done: exit (step 4 도착 후)
done --> idle: 새 plan
-
스텝은 미리 만들지 않는다. 발화가 도착한 그 시점에 LLM 이 다음 스텝 하나를 만든다(수 초). 그동안 미래차 화면은 직전 스텝 화면이 그대로 유지되다가 새
step이 오면 이어 재생한다 — 대기 화면·스피너는 없다. -
재생 중(
driving)에 발화가 오면 버리지 않고 미룬다. 렌더 완료 시 곧바로 다음 생성이 시작된다. - 발화 수집 창은
enter이후 ~exit이전. 그 밖의 발화·채팅은 여정과 무관하므로 무시한다. -
ai/response.transcript구독은 남아 있다(태블릿 잠금 전 테스트·비상용). 미래차 자신이 발행한 질문은context_id로 걸러낸다. -
next가 실리는 곳은 step 4arrived의exit뿐이다.
이 절만 읽어도 한 여정이 어떻게 굴러가는지 알 수 있게 쓴다. 세부 스키마는 3~6장을 참조.
등장인물은 넷이다.
| 누구 | 역할 | 이 문서에서의 표기 |
|---|---|---|
| OC (orchestration-server) | 페르소나·장소·태스크의 정본. plan·dashboard·me 를 주고, 미래차가 보내는 task/start·task/complete·ai/mode 를 받는다 |
OC |
| 태블릿 (tablet-fe) | 관람객이 들고 다니는 화면. 차에 탈 때 enter, 내릴 때 exit 를 보낸다. 차 안에서는 입력이 잠긴다
|
태블릿 |
미래차 서버 (이 문서의 주인공, Mac Studio :8100) |
발화를 재료로 스텝을 LLM 생성해 화면에 밀어 넣고, 태스크 완료와 채팅 질문을 발행한다 | 서버 |
미래차 화면 (/ambient, 차 안 디스플레이 + 마이크) |
스텝을 재생하고, 재생이 끝나면 서버에 보고하고, 관람객 차례에 마이크를 열어 STT 한 발화를 서버로 보낸다 | 화면 |
관람객은 태블릿을 들고 차에 타고, 차 안에서는 차에 대고 말한다. 스텝은 그 말이 이끈다.
- 관람객이 태블릿에서 페르소나를 만들면 OC 가
plan·dashboard·me를 retained 로 발행한다. 서버는plan을 받는 순간 세션(session_id)을 만들고state phase=idle을 발행한다(5-1). - 서버는
dashboard에서 차 장소(name이 "차"로 시작)의 앞뒤 장소(P0/P4)와 태스크를 뽑아 둔다(4장). 태스크 중type=basic만 차가 수행할 후보다 — 태블릿에 조작 UI 가 없어 서버가 완료해야 하는 타입. 그 외(vehicle_ready·detailed·quote_briefing…)는 태블릿 돋보기 모달에서 관람객이 완료하므로 차는 건드리지 않는다. -
me.ai_companion.prompt는 스텝 생성 프롬프트 맨 앞에 붙는 인격(동행자)이다. - 화면은 WS(
/ws/futurecar)로 서버에 붙어 있다. 새 plan(state idle)이 오면 그 세션을 따라간다.
| # | 시점 · 트리거 | 서버가 하는 일 | 서버가 내보내는 것 | 화면 | 태블릿 | OC |
|---|---|---|---|---|---|---|
| ① |
탑승 — 태블릿이 "차로 이동"을 확정하며 enter 발행 |
발화 수집 창 열기, phase=waiting
|
state waiting · ai/mode {car, ttl 1800}(retained) · ai/response message="출발 할까요?" + ai/suggestions 칩 2개 |
대기 화면. 마이크 열림("듣고 있어요") | 채팅에 "출발 할까요?" 표시. 입력 잠금(미래차 모드) | car 모드 저장 — 이후 음성 발화는 STT 만 |
| ② | 관람객이 차에 말함 | 화면이 STT 한 문장을 POST /ambient/utterance 로 받음 → 창 안이면 수집 → 즉시 step 1 생성 시작 |
(없음 — 생성 중) | 마이크 정지("잠시만요"). 대기 화면 유지 | — | — |
| ③ | 스텝 생성 완료 (LLM 수 초) | 컨텍스트(페르소나·앞뒤 장소·누적 발화·남은 basic 태스크·동행자 프롬프트) → LLM JSON. route 확정(P0/P4 는 dashboard 우선, stop_id 는 plan 카탈로그 매핑), task_key 검증 |
route → step(연출 데이터) → task/start {task_id}(그 스텝에 basic 태스크를 골랐을 때) |
step 재생 시작. 마이크 닫힘(우리 TTS 자기수신 방지) |
구간(from→to) 표시. basic 카드 "진행 중" |
dashboard 를 in_progress 로 재발행. 이벤트 타이밍 판단(task_started 트리거) |
| ④ |
렌더 완료 — 화면이 POST /ambient/step-rendered
|
phase=arrived. step 4 면 next=exit. 그 스텝의 basic 태스크 task/complete. step 1~3 이면 그 스텝의 question 을 채팅에 |
state arrived · task/complete {task_id} · ai/response message=question + ai/suggestions
|
마지막 장면 유지. 마이크 열림(step 1~3) | 카드 "완료", 채팅에 다음 질문 |
dashboard completed·point 재발행. 이벤트 타이밍 판단(task_completed 트리거) |
| ⑤ | ②~④ 반복 (step 2 → 3 → 4) | 발화 하나 = 스텝 하나. 미리 만들지 않는다 | ||||
| ⑥ | step 4 렌더 완료 |
phase=arrived next=exit. 질문 없음. 태스크 완료만 |
state arrived step=4 next=exit · task/complete
|
엔딩 화면("체험이 모두 끝났습니다"). 마이크 안 열림 | 하차(차를 떠나 다음 장소로 이동) 확정 가능 | |
| ⑦ |
하차 — 태블릿이 exit 발행 |
창 닫기, phase=done
|
state done · ai/mode {chat}
|
작별 화면 | 입력 잠금 해제 | chat 모드 복귀 |
| — | 다음 관람객의 plan
|
새 세션 idle
|
state idle |
대기 |
읽는 법 세 가지:
- "다음으로"는 관람객의 말이다. 버튼이 아니다. 발화가 없으면 여정은 그 자리에 멈춘다(관람객이 답을 안 하는 상황은 전시 운영이 막는다).
- 렌더 완료 보고(④)가 여정의 심장이다. 이게 안 오면 태스크 완료도, 다음 질문도, 다음 스텝도 없다(6-1).
- 태스크는 스텝 하나에 하나. 시작은 스텝 발행 때(③), 완료는 렌더 완료 때(④). 그래서 태블릿에는 스텝 내내 "진행 중"이 보인다. 몰아서 처리하지 않는다.
① plan OC → 서버 orchestration/{sid}/future-car/request (retain=true)
dashboard · me OC → 서버 orchestration/{sid}/dashboard · /me (retain=true)
← state phase=idle orchestration/{sid}/future-car/response (retain=true)
② enter 태블릿 → 서버 orchestration/{sid}/future-car/request {"type":"enter"}
← state phase=waiting
→ ai/mode {"mode":"car","ttl_sec":1800} orchestration/{sid}/ai/mode (retain=true) [서버 → OC]
→ ai/response message="출발 할까요?" orchestration/{sid}/ai/response ★ 태블릿 채팅
→ ai/suggestions ["네, 출발해요!", …] orchestration/{sid}/ai/suggestions ★ 칩(표시만, 탭 잠금)
③ (관람객이 차량 마이크에 대고 말함)
⇢ POST /ambient/utterance {session_id, transcript:"네, 출발해요", source:"car-stt"} [화면 → 서버, HTTP]
⟳ step 1 생성 (LLM, 수 초)
← route step=1 · from/to stop_id · from/to place ★ 태블릿 구간 표시
← step step=1 · phase=driving · data={assets_timeline…} [화면 재생]
→ task/start {"task_id":…} orchestration/{sid}/task/start [서버 → OC] basic 만
… 화면이 step 1 재생 (마이크 닫힘) …
⇢ POST /ambient/step-rendered {session_id, step:1} [화면 → 서버, HTTP]
← state step=1 · phase=arrived
→ task/complete {"task_id":…} orchestration/{sid}/task/complete [서버 → OC]
→ ai/response message=<step1 question> + ai/suggestions ★ 태블릿 채팅
④ ③ 반복 (step 2, 3)
⑤ step 4 렌더 완료
← state step=4 · phase=arrived · next=exit (질문 없음) ★ 하차 가능
→ task/complete (step 4 에 basic 태스크를 골랐다면)
⑥ exit 태블릿 → 서버 {"type":"exit"}
← state phase=done
→ ai/mode {"mode":"chat"} [서버 → OC]
→ 서버가 OC/태블릿 토픽에 발행, ← 서버가 future-car/response 에 발행, ⇢ 화면이 서버에 HTTP.
arrived(N-1) driving(N) arrived(N)
───┬───────────┬───────┬────────────────────────────┬──────────┬───────────────▶
│ 발화 수집 │ 생성 │ step N 재생 │ 렌더 완료 │ 질문 · 발화 수집
│ (마이크 ON)│ (OFF) │ (마이크 OFF · task 진행 중) │ 보고 │ (마이크 ON)
└ utterance ┘ └ route → step → task/start ┘ └ state → task/complete → question
-
생성 중에는
state를 따로 내보내지 않는다. 화면은 직전 장면을 유지하고, 태블릿은route가 올 때까지 이전 구간을 보여준다. 생성은 보통 10~30초(구조화 출력 실패 시 자유 형식으로 1회 재시도, 120초 타임아웃). - 화면은 서버가 발화를 수집했다는 응답(
result:"collected")을 받으면 그 창에서는 더 듣지 않는다 — 같은 창에서 두 문장이 두 번 생성을 걸지 않게. 다음 창(다음 렌더 완료)에 다시 연다. - 서버 쪽 창은
enter~exit하나다. 그 안에서 스텝별 발화는step_at으로 태깅돼 누적되고, 매 생성에 지금까지의 발화 전부가 재료로 들어간다.
| 상황 | 동작 |
|---|---|
| 스텝 생성 실패(LLM 오류·파싱 실패) |
error {code:"GENERATION_FAILED"} 발행. 화면은 마이크를 다시 열고, 관람객이 다시 말하면 재시도한다. 태블릿은 JOURNEY_ENDED 이외의 에러만 표시 |
창 밖 발화(enter 전, exit 후) |
result:"closed" — 버린다. 화면은 계속 듣지만 enter 전엔 마이크를 열지 않는다 |
| 재생 중 발화 | 화면 마이크가 닫혀 있어 정상적으론 생기지 않는다. ai/response 경유(태블릿 잠금 전·비상)로 들어오면 버리지 않고 미뤄 두었다가 렌더 완료 직후 다음 생성에 쓴다 — 이때는 질문 없이 넘어간다 |
| 렌더 완료 보고 지각·중복 | 무시(ignored_stale·ignored_duplicate·ignored_done). 이미 다음 스텝으로 넘어갔거나 같은 스텝을 두 번 보고한 경우 |
태블릿이 enter 를 두 번 |
두 번째는 무시(태블릿도 phase 가드로 안 보낸다). exit 는 waiting·driving·arrived 어디서든 받는다 → 그 자리에서 done
|
advance·start 가 오면 |
폐기된 별칭 — 받으면 발화 없이 강제 생성한다(현장 비상·디버그용, /ambient dev 패널) |
| 서버 재시작 |
state·스텝 결과는 DB 에서 복원되지만 발화 누적·대화 히스토리는 복원되지 않는다(다음 스텝 품질 저하). dashboard·me·plan 은 retained 라 재구독으로 돌아온다 |
| OC 이벤트 팝업이 차 스텝 중에 뜸 | OC 는 task/start·task/complete 를 이벤트 발행 판단의 트리거로 쓴다. 팝업은 태블릿에만 뜨고 차는 모른다 — 차가 이벤트에 반응할지는 미정(11장) |
| 마이크 권한 거부·STT 오류 | 화면 하단 알약이 붉게 표시된다(not-allowed·network). 운영자는 /ambient dev 패널의 advance(강제 진행)로 여정을 이어갈 수 있다 |
tablet-fe
60d353c기준 1·2·3 은 구현돼 있다(future-car-store,TaskCompleteDialog의 enter/exit). 남은 것은 9-2-1 입력 잠금이다.
orchestration/{session_id}/future-car/response QoS 1
session_id 는 페르소나 생성 시 발급받은 값이다. 태블릿이 여러 대면 반드시 자기 세션으로 고정한다.
state 는 retain=true 라 늦게 붙어도 마지막 상태를 즉시 받는다. route·step·error 는 retain 이 없다.
채팅은 추가 구독이 필요 없다. 미래차가 orchestration/{sid}/ai/response 에 {ok, context_id, message} 로 질문을,
ai/suggestions 에 {context_id, suggestions[]} 로 답 후보를 직접 발행한다 — 태블릿이 이미 구독하는 토픽이고,
모르는 context_id 의 message 를 AI 말풍선으로, suggestions 를 칩으로 그리는 기존 로직이 그대로 동작한다.
| 받은 메시지 | 태블릿 |
|---|---|
state · phase=idle |
새 여정. 지난 구간·에러 비우기 |
state · phase=waiting |
탑승됨. 곧 채팅에 "출발 할까요?" 가 뜬다. 입력 잠금 시작(9-2-1) |
route |
구간 표시 갱신 (from_stop_id → to_stop_id, 없으면 place·이전 구간 유지) |
step · phase=driving |
미래차 재생 중 — data 는 무시, phase·step 만 취한다 |
state · phase=arrived (step 1~3) |
곧 채팅에 다음 질문이 뜬다. 관람객은 차에 대고 답한다 |
state · phase=arrived · next=exit |
하차 가능(canExit). 관람객이 차를 떠나 다음 장소로 이동을 확정하면 exit
|
state · phase=done |
여정 끝. 입력 잠금 해제 |
error |
5-3-1 표에 따라 처리 (JOURNEY_ENDED 는 표시하지 않음) |
advance 버튼은 없다. 다음 스텝은 관람객이 차에 말하면 자동으로 진행된다.
phase ∈ {waiting, driving, arrived} 인 동안 채팅 텍스트 입력·마이크·제안 칩 탭을 잠근다. 미래차가 띄운 질문과 칩은
표시만 한다(칩 disabled). 안내 문구 예: "차량 안에서는 차에 직접 말씀해 주세요". phase=done(또는 idle) 에서 푼다.
이유: 스텝 진행은 차량 마이크로만 한다. 태블릿에서 답하면 OC 의 AI 동반자가 따로 답을 해 차와 태블릿이 "따로 노는" 상황이 생기고
(OC ai/mode=car 는 음성만 막는다), 미래차도 그 답을 스텝 재료로 받아 버린다.
orchestration/{session_id}/future-car/request QoS 1 · retain=false
| 언제 | 페이로드 |
|---|---|
| 다음 목적지가 차량이고 이동을 확정할 때(탑승) | {"type":"enter"} |
| 지금 장소가 차량이고 다음 장소로 이동을 확정할 때(하차) | {"type":"exit"} |
이게 전부다. advance·start 는 폐기됐다(받아주긴 하지만 deprecated 로 기록되고 강제 진행된다). plan 은 OC 가 발행한다.
중복 발행 가드: enter 는 phase 가 미수신/idle 일 때만, exit 는 waiting·driving·arrived 에서만.
retain=false 로 발행한다. 태블릿 요청을 retain 하면 미래차 서버 재시작 시 브로커가 재전송하는데, 서버는 retained REQUEST 를 전부 버리므로 조용히 무시된다.
-
step.data를 해석하지 마라. 미래차 화면 전용 연출 데이터다. 태블릿이 볼 필드는type·step·phase·next와route의stop_id·place뿐이다. -
null필드는 생략되어 온다.next키의 존재 여부로 판단하라(next === null비교 금지).route의stop_id도 마찬가지. - 채팅 질문은 미래차가 띄운다. 태블릿이 "다음으로" 안내 문구를 따로 만들지 마라 — 이중으로 뜬다.
- 미래차 모드에서는 입력을 잠근다(9-2-1). 관람객이 답하지 않으면 진행되지 않는다 — 현장 스태프가 차에 말하도록 가이드한다(설계 전제).
-
차량 태스크 카드는 손대지 않는다.
type=basic태스크의in_progress→completed는 미래차가task/start·task/complete로 바꾸고 OC 가dashboard로 내려준다. 그 외 타입(돋보기 모달)은 지금처럼 관람객이 태블릿에서 완료한다.
| 주소 | 용도 |
|---|---|
http://creator-mac-studio.tail69930a.ts.net:8100/mqtt |
주고받은 MQTT 원문을 시간순으로 확인 + 수동 발행 (DB 이력 토글로 재시작 전 것도) |
POST …:8100/ambient/utterance {session_id, transcript, source:"debug"}
|
관람객 발화 흉내 — 차량 화면 없이 여정을 한 스텝씩 돌려볼 수 있다 |
GET …:8100/ambient/session/{session_id} |
세션 스냅샷(phase·step·완료 태스크·수집 발화) |
GET …:8100/logs?session_id=… |
서버 진행 로그(DB) |
http://creator-mac-studio.tail69930a.ts.net:8100/docs |
REST API (Swagger) |
전시망(Tailscale) 안에서만 열린다. 미래차 서버 주소가 바뀌면 이 표를 갱신할 것.
/mqtt 에서 enter 를 누르고 /ambient/utterance 로 발화를 넣으면 어떤 메시지가 어떤 순서로 나가는지 8-2 그대로 눈으로 확인할 수 있다.
| 항목 | 값 |
|---|---|
| QoS | 1 (모든 토픽) |
retain — REQUEST plan
|
OC 발행 true (미래차는 재전송분만 무시) |
| retain — REQUEST 태블릿 트리거 | false |
retain — RESPONSE state
|
true (신규 구독자 즉시 최신 상태) |
retain — RESPONSE route · step · error
|
false |
| retain — AI REQUEST (미래차 발행) | false |
| retain — DASHBOARD | OC 발행 true
|
| 페이로드 | 평문 JSON · snake_case · null 필드 생략 |
| 연결 옵션 | AutoReconnect (reconnect_delay 1~30초). 브로커가 죽어 있어도 서버 부팅을 막지 않음 |
| ACL |
orchestration/# 하위라 현 ACL(future-car 계정)로 통과 — 변경 불필요 |
- ✅
enter발행 주체 — 태블릿(2026-08-20). tablet-fe 구현 완료(TaskCompleteDialog). - ✅ OC 세션 모드
ai/mode— OC 구현 완료(2026-08-24).ai/response직접 발행은 OC 가 구독하지 않는 토픽이라 영향 없음(정식 동의는 받아 둘 것). - ⬜ tablet-fe: 미래차 모드 입력 잠금(9-2-1) — 2026-08-28 정책. 이게 없으면 태블릿 답변이 스텝 재료로 섞여 들어간다.
- ⬜ 이벤트(돌발 팝업)와 차의 관계 — 미정. OC 는
task/start·task/complete를 이벤트 발행 판단 트리거로 쓰므로 차 스텝 중 태블릿에 팝업이 뜰 수 있다. 차가 반응하려면orchestration/{sid}/event구독이 필요한데, 페이로드에 장소 참조가 없어 OC 에location_id추가(또는dashboard.locations[].event요약)를 요청해야 한다. - ⬜ 충전소 하드웨어 연동 —
route.to_place=charging_station인 스텝의 렌더 완료 = 충전소 도착. 어느 브로커·토픽·페이로드로 알릴지 담당 팀과 미정. - ⬜
exit이후 다음 공간 전환 신호 — 주체/토픽 미확정. 미래차는state.next_place로 "다음 장소가 어디인지"까지만 알려준다. - ⬜ 운영 규칙: 차가 완료해야 하는 차 태스크는 백오피스에서
type=basic으로 등록한다(타입이 곧 완료 주체 스위치). - ⬜ AsyncAPI 반영 — RESPONSE 4종(
state·route·step·error)을Orchestration MQTT API의future-car채널에 반영. - ℹ️
reaction폐기 여부 — 계약에만 남아있고 발행하지 않는다. - ℹ️ 인증 없음 —
/mqtt/publish·/ambient/utterance로 전시 중인 여정을 조작할 수 있다. Tailscale 망 안이라 우선순위는 낮으나, 외부 노출 시 대응 필요.
- Google ADK 검토 (프레임워크·모델 비교)
- 페르소나 + 상황 카드 모델 (2차 시나리오 구동 방식)
- Mac Studio 로컬 LLM + Claude Opus 하이브리드 라우팅 전략
- 개발 일정·할 일 점검표 (단계별 진행 점검)
- 로봇 Adapter 개발 온보딩 (HA·MQTT 계정 등록, 토픽 적용, 테스트 송수신, 상태 확인)
- LG ThinQ 가전 연동 (HA 통합 · REST 데이터 취득 · 제어)
- 장소 도착 자동화 (태스크/이벤트/장소 규칙 → SPOT·로봇팔·반려로봇·Hub 자동 명령)
- 엔딩 화면 데이터 출처 점검 (차량 3필드 · 세션 종료 엔딩 5화면)