The 30-second ZView demo
User (Claude Desktop chat):
"Build a pump control HMI on going-zpi-XXX. Start/stop plus a pressure gauge."
Claude:
1. plc_get_rules() — reads the running Python script
2. plc_info() — checks the board spec (4DI/4DO/500 D-registers)
3. Generates HTML (dark theme, large buttons, SVG pressure gauge)
4. zview_deploy(name="pump", html="...", activate=true)
User: opens http://going-zpi-XXX.local:5000/zview/ in a browser
enters the operator password → full-screen pump control HMI
User: "The buttons are too small."
Claude:
1. zview_get_active() — fetches the current HTML
2. Resizes the buttons and redeploys
Browser: ← SSE auto-reload fires, the page refreshes itself
That is ZView. You build an industrial HMI by talking to the AI in plain language, and the result drives real GPIO immediately.
The default operator and admin passwords are listed in the manual shipped with the board. Change them before putting the board into service.

Why AI-generated HMI was needed
Existing industrial HMI products (Crimson, Wonderware, Inductive, the Node-RED dashboard) share three problems:
- They are expensive, from hundreds of thousands to millions of KRW.
- The learning curve is steep: a dedicated IDE and its own widget library.
- The output is closed, tied to one vendor runtime.
Meanwhile, an LLM now writes HTML, CSS and JavaScript competently in one pass. An industrial HMI is fundamentally “press a button to toggle an output, poll the state and display it,” repeated. That is exactly the shape of problem an LLM is good at.
The open question was how the AI talks to the PLC at all. That bridge is MCP, the Model Context Protocol.
ZPi, MCP and ZView architecture
[Claude Desktop / Code]
│
│ MCP (stdio JSON-RPC)
▼
[ZPi.Controller.Mcp.exe] ← self-contained .NET 9 exe installed on the PC
│
│ HTTP (X-MCP-Token auth)
▼
[Raspberry Pi - ZpiController service]
│
├─ /api/zview/{name}/deploy (store HTML + activate + SSE push)
├─ /api/data/{i}/{value} (write a D[] register)
├─ /api/output/{i}/{state} (write GPIO directly)
└─ /zview/ (serve the active ZView HTML statically)
│
▼
[Operator browser — logged in with the operator password]
Full-screen HMI with live SSE auto-reload
Six MCP tools (zview_deploy, zview_list, zview_get_active, zview_activate, zview_revert, zview_delete) are exposed to Claude, which picks the right calls from a plain-language request.
The most interesting part — Claude reads the rule script first
An output flicker bug showed up in the first version. Pressing a ZView button turned the output on and then straight back off.
The cause: the user already had a Python script in the rule editor, and it was overwriting the output on every 50ms tick.
# the user script
def Tick():
Out[0] = D[0] != 0 # ← if D[0] is 0, Out[0] is forced to 0
Out[1] = D[1] != 0
...
The ZView button called /api/output/0/true and the GPIO did turn on, but 50ms later the next tick ran Out[0] = D[0] != 0, D[0] was still 0, so Out[0] = False. The output flickered.
The fix: ZView does not write Out[i] directly. It writes the D[i] that the script reads. The script then reflects that onto the output on its own, and because the physical input signals use the same D slots, the two control paths coexist naturally.
To make that permanent, we pushed the workflow into the MCP tool description itself:
[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(...)
This is the core idea. An AI tool description is LLM context, so the description itself becomes the behavioural guide. The result is that “build me a relay control UI” is enough for Claude to:
- recognise the script pattern,
- choose the write API that will not conflict,
- generate matching HTML,
- deploy it.
All of it automatic.

Two roles — admin vs operator
Operator role separation became the question as soon as ZView existed. PLC configuration (rules, Modbus, system) belongs to the administrator; the ZView full-screen view and output toggles should be open to the operator as well.
So we extended the single-password system into two roles.
| Authentication | Paths it can reach |
|---|---|
MCP token (X-MCP-Token header) | Everything (equivalent to admin) |
| admin cookie | Everything |
| operator cookie | Only /zview/*, /api/io, /api/info, /api/output/*, /api/data/*, /api/memory/*, /api/auth/* |
| No authentication | 401 → login |
If an operator accidentally calls an administrative API such as /api/rules, the answer is 403, not 401. That is deliberate: “you are authenticated but not authorised” is a far easier message to debug.
The login page is still a single password field. On submit the server tries admin first, then operator, and returns the matching role in the response. The client branches on that role: admin goes to /app/, operator goes to /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 auto-reload — the secret behind the UX
SSE auto-reload is what saves the operator from pressing F5 every time Claude redeploys a ZView. Server-Sent Events (SSE) solve it.
// 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 holds an EventSource stream open, and any deploy, activation or rollback calls PublishReload(), which pushes a reload event to every connected browser.
And here is the clever bit: when the server serves the ZView HTML, it injects the SSE client code automatically, right before </body>:
// 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 never has to write the SSE code. It produces clean HTML and the server slots the live-update mechanism in. The principle: the server owns the infrastructure behind a screen the AI generated.
Automatic version backup and rollback
ZView version backup runs on every zview_deploy call, archiving the previous version as a timestamped file under zview-apps/<name>/.versions/. The last five are kept. If Claude produces something wrong, a single zview_revert(name) restores the version before it.
From the operator point of view, the risk of the AI suddenly breaking a screen is zero. However badly it goes, one click walks it back.
Demo video (in preparation)
Things to add when this post is next updated:
- A 30-second clip: Claude chat, ZView deploy, then the browser refreshing on its own.
- A demonstration of changing the rule script so the same ZView is regenerated to toggle a different D index.
Reflections — what the MCP tool description pattern taught us
MCP server tool design turned out to be an interesting case of MCP becoming the hands of the AI and manipulating a system directly. It is not just tool calling: the tool description becomes the behavioural guide for the LLM, and the server automatically fills in the infrastructure around what the AI produced.
The pattern applies well beyond ZPi.
- Internal back-office screens generated by AI.
- Data dashboards deployed instantly from a plain-language request.
- Operator UIs for IoT devices, edited on the spot in the field.
From the builder side, it was a good school for learning what AI-friendly infrastructure actually means.
Put the workflow inside the MCP tool description and leave the infrastructure behind an AI-generated screen to the server — that is the core of ZView.
What comes next in the series
- Part 3 — the one-double-click MCP installer, and the story behind the BAT file that registers itself with both Claude Desktop and Claude Code.
- The full source will be published separately at a later date.
Contact
- Email: [email protected]
- Instagram: https://www.instagram.com/going.sen/
- Website: https://intosen.com/kr/consult/
Comments
Enter a nickname to leave a comment, or sign in with Google or GitHub.