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. 운영 전 반드시 변경하세요.

로그인 페이지 — 비밀번호 하나로 admin/operator 자동 판별

왜 만들었나

기존 산업 HMI 들 (Crimson, Wonderware, Inductive, Node-RED 대시보드)은:

반면 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 는:

  1. 스크립트 패턴 인식
  2. 충돌 안 나는 API 선택
  3. 그에 맞는 HTML 생성
  4. 배포

전 과정 자동.

룰 에디터 — 스크립트가 D[]/M[] 을 출력에 반영하는 구조

두 역할 인증 — 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. 어떻게 잘못 되어도 한 클릭 뒤로.

시연 영상 (준비 중)

다음 글 업데이트 때 추가할 것:

회고

이 기능은 “MCP 가 AI 의 손이 되어 시스템을 직접 조작한다” 의 흥미로운 사례다. 단순한 도구 호출이 아니라, 도구 description 이 LLM 의 행동 가이드 가 되고, AI 가 만든 산출물의 인프라를 서버가 자동으로 보완 하는 패턴.

이 패턴은 ZPi 가 아니어도 적용 가능하다:

만드는 입장에선 “AI 친화 인프라” 라는 게 무엇인지 배우는 좋은 학교 같았다.

다음

댓글

로그인 없이 이름만 적고 남기실 수 있어요. 남긴 댓글은 바로 게시됩니다.

댓글을 불러오는 중…