ZView: AI 가 자연어로 만드는 산업 HMI — Claude + MCP + PLC
30초 데모
사용자 (Claude Desktop 채팅창):
"going-zpi-XXX 에 펌프 제어 HMI 만들어줘. 시작/정지 + 압력 게이지"
Claude:
1. plc_get_rules() — 현재 Python 스크립트 읽음
2. plc_info() — 보드 사양 (4DI/4DO/500 D-register) 확인
3. HTML 생성 (다크 테마, 큰 버튼, 압력 게이지 SVG)
4. zview_deploy(name="pump", html="...", activate=true)
사용자: 브라우저에서 http://going-zpi-XXX.local:5000/zview/ 열림
operator 비번 입력 → 풀스크린 펌프 제어 HMI
사용자: "버튼이 작아"
Claude:
1. zview_get_active() — 현재 HTML 가져옴
2. 버튼 크기 조정 + 재배포
사용자 브라우저: ← SSE 자동 리로드 발동, 화면 자동 새로고침
이게 ZView 다. 산업 HMI 를 AI 와의 자연어 대화로 만들고, 만든 즉시 실제 GPIO 를 제어한다.
operator 기본 비밀번호는
going, admin 기본 비밀번호는admin. 운영 전 반드시 변경하세요.

왜 만들었나
기존 산업 HMI 들 (Crimson, Wonderware, Inductive, Node-RED 대시보드)은:
- 비싸다 (수십~수백만원)
- 학습곡선이 가파르다 (전용 IDE, 위젯 라이브러리)
- 결과물이 닫혀있다 (특정 벤더 런타임 의존)
반면 LLM 은 이제 HTML+CSS+JS 를 한 번에 잘 짠다. 산업 HMI 는 본질적으로 “버튼 누르면 출력 토글, 상태 폴링해서 표시” 의 반복이라 LLM 이 정말 잘 짤 수 있는 영역이다.
문제는 “AI 가 어떻게 PLC 와 대화하는가”. 그 다리가 MCP (Model Context Protocol) 다.
아키텍처
[Claude Desktop / Code]
│
│ MCP (stdio JSON-RPC)
▼
[ZPi.Controller.Mcp.exe] ← PC 에 설치된 self-contained .NET 9 exe
│
│ HTTP (X-MCP-Token 인증)
▼
[Raspberry Pi - ZpiController service]
│
├─ /api/zview/{name}/deploy (HTML 저장 + 활성화 + SSE 푸시)
├─ /api/data/{i}/{value} (D[] 레지스터 쓰기)
├─ /api/output/{i}/{state} (GPIO 직접 쓰기)
└─ /zview/ (활성 ZView HTML 정적 서빙)
│
▼
[운영자 브라우저 — operator 비번 로그인]
풀스크린 HMI 표시 + 라이브 SSE 자동 리로드
MCP 도구 6개 (zview_deploy, zview_list, zview_get_active,
zview_activate, zview_revert, zview_delete) 가 Claude 에 노출되고,
Claude 는 자연어 요청을 받아 적절히 호출한다.
가장 흥미로운 부분 — Claude 가 스크립트를 먼저 읽는다
처음 만들 때 어이없는 버그가 있었다. ZView 버튼을 누르면 출력이 켜졌다가 바로 꺼졌다.
원인: 사용자가 룰 에디터에 이미 Python 스크립트를 짜놓았는데, 그게 매 50ms tick 마다 출력을 덮어쓰고 있었다.
# 사용자 스크립트
def Tick():
Out[0] = D[0] != 0 # ← D[0] 가 0 이면 Out[0] 도 강제 0
Out[1] = D[1] != 0
...
ZView 버튼이 /api/output/0/true 를 호출해 GPIO 를 켜도, 50ms 후 다음
tick 에서 스크립트가 Out[0] = D[0] != 0 실행 → D[0] 은 여전히 0
→ Out[0] = False. 깜빡거림.
해결: ZView 가 Out[i] 를 직접 쓰지 말고, 스크립트가 읽는 D[i] 를
쓴다. 그러면 스크립트가 알아서 출력에 반영해주고, 같은 D 슬롯을
물리적 입력 신호도 쓰니까 두 컨트롤 경로가 자연스럽게 공존한다.
이걸 영구히 보장하기 위해 MCP 도구 description 자체에 워크플로를 강제 했다:
[McpServerTool, Description(
"Deploy a ZView. ...\n\n" +
"WORKFLOW — ALWAYS follow this order before writing HTML:\n" +
" STEP 1: Call plc_get_rules first. Read the running script.\n" +
" STEP 2: Identify the CONTROL VARIABLES. Common patterns:\n" +
" - Out[i] = D[i] != 0 → button writes /api/data/{i}/{0|1}\n" +
" - Out[i] = M[i] → button writes /api/memory/{i}/{true|false}\n" +
" - 스크립트 없음 → /api/output/{i}/{true|false} 직접\n" +
" STEP 3: Pick the right write API. NEVER write Out[i] directly\n" +
" if the script reflects another variable to it.\n" +
" ...")]
public static async Task<string> ZView_Deploy(...)
이게 핵심이다. “AI 도구 == LLM 컨텍스트” 라는 점을 활용해서, 도구 설명 자체를 LLM 의 행동 지침으로 쓰는 것. 그 결과 사용자가 “릴레이 제어 UI 만들어줘” 한 마디면 Claude 는:
- 스크립트 패턴 인식
- 충돌 안 나는 API 선택
- 그에 맞는 HTML 생성
- 배포
전 과정 자동.
![룰 에디터 — 스크립트가 D[]/M[] 을 출력에 반영하는 구조](/images/zpi/sc_editor.png)
두 역할 인증 — admin vs operator
ZView 를 만들었으니 누가 접근하느냐가 문제다. PLC 설정 (룰, 모드버스, 시스템) 은 관리자만, ZView 풀스크린 / 출력 토글은 운영자도.
기존 단일 비번 시스템을 두 역할로 확장:
| 인증 방식 | 통과 가능한 경로 |
|---|---|
MCP 토큰 (X-MCP-Token 헤더) | 전부 (admin 동급) |
| admin 쿠키 | 전부 |
| operator 쿠키 | /zview/*, /api/io, /api/info, /api/output/*, /api/data/*, /api/memory/*, /api/auth/* 만 |
| 무인증 | 401 → 로그인 |
운영자가 잘못해서 /api/rules 같은 관리 API 를 호출하면 403 (not 401).
의도적으로 “인증은 됐는데 권한이 없다” 메시지 — 디버깅이 편하다.
로그인 페이지는 그대로 비번 한 칸. 입력하면 서버가 admin → operator
순으로 시도하고 매칭된 role 을 응답에 담아 돌려준다. 클라이언트가
role 보고 admin → /app/, operator → /zview/ 로 분기.
// Software/Auth/AuthService.cs
public LoginResult Login(string password, string clientIp)
{
// ... lockout check
if (Verify(password, _data.Salt, _data.Hash))
return IssueToken(AuthRole.Admin, now);
if (Verify(password, _data.OpSalt, _data.OpHash))
return IssueToken(AuthRole.Operator, now);
// ... fail counter
}
SSE 자동 리로드 — UX의 비밀
Claude 가 ZView 를 재배포할 때마다 운영자가 F5 눌러야 한다면 끔찍하다. Server-Sent Events (SSE) 로 해결.
// Software/ZView/ZViewSseManager.cs
public void PublishReload()
{
var payload = $"event: reload\ndata: {Interlocked.Increment(...)}\n\n";
foreach (var sub in _subscribers.Values)
sub.Queue.TryEnqueue(payload, max: 4);
}
/api/zview/events 가 EventSource 스트림을 유지하고, 배포/활성화/롤백
시 PublishReload() 호출되면 모든 연결된 브라우저에 reload 이벤트
푸시된다.
그리고 — 여기가 영리한 부분 — 서버가 ZView HTML 을 서빙할 때
</body> 직전에 SSE 클라이언트 코드를 자동 주입:
// Software/Program.cs - ServeActiveZView
const string reloader =
"<script>(function(){try{var es=new EventSource('/api/zview/events');" +
"es.addEventListener('reload',function(){location.reload()});" +
"}catch(e){}})();</script>";
var idx = lower.LastIndexOf("</body>");
string patched = idx >= 0
? html.Substring(0, idx) + reloader + html.Substring(idx)
: html + reloader;
Claude 는 SSE 코드를 쓸 필요가 없다. 그냥 깨끗한 HTML 만 만들면 서버가 알아서 자동 갱신 기능을 끼워준다. AI 가 만드는 화면의 인프라는 서버가 책임진다는 관점.
버전 자동 백업 + 롤백
매 zview_deploy 호출 시 이전 버전이 zview-apps/<name>/.versions/
타임스탬프 파일로 자동 보관. 최근 5개 유지. Claude 가 잘못 만들면
zview_revert(name) 한 번으로 직전 버전 복구.
운영자 입장에선 “AI 가 갑자기 화면을 깨뜨리는 위험” 이 0. 어떻게 잘못 되어도 한 클릭 뒤로.
시연 영상 (준비 중)
다음 글 업데이트 때 추가할 것:
- Claude 채팅창 → ZView 배포 → 브라우저 자동 새로고침 30초 클립
- 룰 스크립트 바꿔서 같은 ZView 가 자동으로 다른 D 인덱스 토글하도록 재생성 시연
회고
이 기능은 “MCP 가 AI 의 손이 되어 시스템을 직접 조작한다” 의 흥미로운 사례다. 단순한 도구 호출이 아니라, 도구 description 이 LLM 의 행동 가이드 가 되고, AI 가 만든 산출물의 인프라를 서버가 자동으로 보완 하는 패턴.
이 패턴은 ZPi 가 아니어도 적용 가능하다:
- 사내 백오피스 화면을 AI 가 생성
- 데이터 대시보드를 자연어로 즉시 배포
- IoT 디바이스의 운영자 UI 를 현장에서 즉석 수정
만드는 입장에선 “AI 친화 인프라” 라는 게 무엇인지 배우는 좋은 학교 같았다.
다음
- 3편 — MCP exe 한 번 더블클릭 설치 — Claude Desktop + Code 양쪽에 자동 등록하는 BAT 의 비하인드
- 코드 전체는 추후 별도 공개 예정
댓글
로그인 없이 이름만 적고 남기실 수 있어요. 남긴 댓글은 바로 게시됩니다.
댓글을 불러오는 중…