ZView 샘플 — Modbus IO 모니터링 화면 한 장으로 만들기
이번 ZView 샘플은 산업 현장에서 가장 흔한 셋업 — 상위 마스터가 ZPi 의 4 채널 출력을 비트 단위로 제어하고, ZPi 의 4 채널 입력 상태를 비트 단위로 회신받는 원격 IO 노드 — 의 모니터링 화면 한 장이다. 운영자가 “지금 마스터가 어떤 명령을 보내고 있고, 입력이 어디까지 들어오고 있는지” 한눈에 알 수 있어야 한다.
ZView 의 매력은 이 정도 화면은 단일 HTML 파일 한 장 — 200 줄 안쪽 — 으로 끝낼 수 있고, 배포 = 파일 한 번 업로드라는 점이다. 전용 HMI IDE / 위젯 라이브러리 / 빌드 / 사이닝 절차 하나도 안 거친다.
어떤 샘플인가
화면 하나에 다음을 동시에 담았다.
- OUT 4 채널 — 상위 마스터가 켠 출력. 큰 라이트로 한눈에.
- IN 4 채널 — 현장 입력 상태. 다른 색(주황)으로 OUT 과 구분.
- D[0] / D[10] 카드 — 마스터가 쓰는/읽는 16 비트 워드의 raw 값을 10진/16진/비트 매트릭스 세 가지로 표시. 비트 단위 디버깅이 한 화면에서 끝남.
- 통신 상태 알약 — 활성 / 유휴 / 끊김 3 상태를 색으로 구분.
- 서버 시계 — ZPi 본체 시간 (네트워크 불일치 디버깅용).
ZView 는 모니터링 전용으로 설계했다. 화면에서 OUT 을 직접 토글해봐야 50 ms 안에 마스터의 값으로 덮어쓰여서 의미가 없다 — 마스터가 진실의 출처이고 ZView 는 그 거울이다. 이 분리가 운영 책임을 명확히 한다.
전제 — Modbus 와 미러 스크립트
ZView 가 무엇을 보여줄지 정하려면 백엔드의 데이터 흐름을 먼저 못 박아야 한다. 이번 샘플의 백엔드는 두 부분이다.
(1) ZPi 의 RTU 슬레이브 설정 — 라즈베리파이 시리얼 포트 한 개를 통째로 Modbus RTU 슬레이브로 노출한다.
rtuSlave:
enabled: true
port: /dev/serial0
baud: 115200
parity: None
stopBits: 1
unitId: 1
저장 후 sudo systemctl restart zpi-controller 한 번. 마스터 측도 동일한 파라미터로
맞추고 RS-485 A/B/GND 연결만 확인하면 통신은 깔린다.
(2) 50 ms tick 마다 도는 IronPython 미러 스크립트 — D[0] 의 하위 4 비트를 그대로 OUT[0..3] 으로 풀고, IN[0..3] 을 D[10] 의 하위 4 비트로 패킹한다.
def Tick():
d0 = D[0]
Out[0] = bool(d0 & 0x0001)
Out[1] = bool(d0 & 0x0002)
Out[2] = bool(d0 & 0x0004)
Out[3] = bool(d0 & 0x0008)
d10 = 0
if In[0]: d10 |= 0x0001
if In[1]: d10 |= 0x0002
if In[2]: d10 |= 0x0004
if In[3]: d10 |= 0x0008
D[10] = d10
이 두 가지가 갖춰지면 ZView 는 그 위에 시각화만 얹는다. 메모리 맵은 아래처럼 단순하다.
디버깅 컷 하나만 더 — 초기 실행 때 마스터가
D[0]=3840(0x0F00) 을 보내고 있었다. 스크립트는 하위 4 비트만 보니까 OUT 이 안 따라왔다. 마스터 측 바이트 스왑 이 원인이었고 마스터 설정 한 번 바꾸자0x000F로 정상 진입했다. ZView 의 비트 매트릭스가 이런 상황을 한 번에 짚어준다 — “값은 들어오는데 잘못된 비트 자리에 박혀있다” 가 화면에서 바로 보이니까.
ZView 디자인 결정 5 가지
ZView 가 다른 HMI 와 다른 점은 HTML 단일 파일 이라는 것. 이 제약이 디자인 결정의 가짓수를 단순화한다.
1. 다크 테마 고정. 산업 현장의 형광등 / 야간 운영에서 가장 무난하다. 색상 변수는 본문 인라인 CSS 안에 박아놨다.
body { background: #0a0e1a; color: #e6ecf5; }
.light { background: #161c2c; border: 2px solid #232b40; }
.light.on { background: linear-gradient(180deg, #00c879 0%, #009a5e 100%);
box-shadow: 0 0 24px rgba(46, 230, 154, 0.45); }
.light.in.on { background: linear-gradient(180deg, #ff9e3a 0%, #d4781f 100%); }
OUT 은 녹색 글로우, IN 은 주황 글로우 — 출력과 입력을 색으로 즉시 구분 하는 게 운영자의 인지 부하를 가장 크게 줄인다.
2. 터치 친화적 큰 라이트. 최소 80 px 높이에 aspect-ratio: 1.4 / 1 로 잡았다.
손가락 끝으로 확인하는 동작이 자연스러워야 한다. user-scalable=no 와 -webkit-tap-highlight-color: transparent
로 터치 시 의도치 않은 확대 / 하이라이트 깜빡임을 막았다.
3. 비트 매트릭스 시각화. D[0]/D[10] 의 16 비트를 16 개 작은 셀로 한 줄에 펼쳐 보여준다. 바이트 스왑 / 비트 위치 오타 / 워드 엔디안 같은 문제가 화면에서 바로 보인다 — 이게 진짜 산업 현장에서 시간을 아껴주는 부분이다.
function setBits(cells, val) {
for (let i = 0; i < 16; i++) {
cells[i].classList.toggle('on', !!(val & (1 << i)));
}
}
4. 통신 상태 3 단계 알약. 활성 / 유휴 / 끊김을 색으로 구분.
- 활성 (녹색) — 최근 3 초 안에 D[0] 값이 한 번이라도 바뀌었다 = 마스터가 실제 쓰고 있다
- 유휴 (노랑) — 통신은 살아있는데 N 초 째 D[0] 변화가 없다 = 마스터 idle 상태
- 끊김 (빨강) —
/api/io가 응답 안 함 = ZPi 자체 또는 네트워크 문제
이 3 단계가 있어야 “통신은 되는데 마스터가 안 보내는 건지”, “통신 자체가 죽은 건지” 가 분리된다. 같은 회색 OUT 라이트만 보여주면 두 상태를 운영자가 구분할 수 없다.
5. 인터넷 없이 동작. 외부 CDN / 폰트 / 아이콘 의존성 0. ZPi 의 정적 파일 서빙 한 번이면 끝. 산업 현장에서 인터넷이 끊기는 건 사고가 아니라 일상이다.
핵심 코드 — 폴링 한 함수
ZView 의 동작 전체가 함수 하나로 압축된다. /api/io 를 200 ms 마다 GET 하고, 응답을 화면
세 영역(라이트 / 카드 값 / 비트 매트릭스) 에 그대로 반영한다.
async function poll() {
try {
const r = await fetch('/api/io', { cache: 'no-store' });
if (!r.ok) throw new Error('HTTP ' + r.status);
const s = await r.json();
// 1) 4 채널 라이트
for (let i = 0; i < 4; i++) {
outLights[i].classList.toggle('on', !!s.outputs[i]);
inLights[i].classList.toggle('on', !!s.inputs[i]);
}
// 2) D[0] / D[10] raw 값
const d0 = readDword(s, 0);
const d10 = readDword(s, 10);
$('d0dec').textContent = d0;
$('d0hex').textContent = hex4(d0);
$('d10dec').textContent = d10;
$('d10hex').textContent = hex4(d10);
// 3) 비트 매트릭스 32 셀 (각 16 비트)
setBits(d0BitCells, d0);
setBits(d10BitCells, d10);
// 4) 통신 상태 알약 (D[0] 변화 시각으로 판단)
if (d0 !== lastD0) { lastD0 = d0; lastChangeMs = Date.now(); }
updateCommStatus();
} catch (e) {
setCommStatus('연결 끊김', 'err');
}
}
setInterval(poll, 200);
readDword() 가 s.modbus 가 객체 / 배열 / 그 외 형태일 때를 모두 받아주는 한 줄 어댑터다.
ZPi 의 API 응답 형식이 버전에 따라 살짝 달라져도 ZView 가 깨지지 않는다.
배포 — zview_deploy 한 번
작성한 HTML 을 ZPi 의 MCP 도구 한 번으로 배포한다. ZView 는 자동으로 /zview/ 에 노출되고,
열려있는 운영자 브라우저는 SSE 자동 리로드 로 같이 새로고침된다.
zview_deploy(
name: "modbus-io",
html: "...", # 단일 자가 완결 HTML
activate: true
)
배포 후 메타데이터:
{
"ok": true,
"meta": {
"name": "modbus-io",
"sizeBytes": 10018,
"isActive": true
}
}
10 KB. 이게 IO 모니터 한 장의 전체 무게다. 비교하자면 Wonderware / Crimson 의 동등한 패널 하나가 수 MB.
터치 디스플레이에 띄우기
이번 셋업에는 Senvas Touch 디스플레이가 같이 붙어있다. 터치 측은 자기 기본 프로그램이
풀스크린으로 돌아가는 구조라, ZView 를 띄우려면 키오스크 모드 브라우저 한 개를 추가로
실행해서 http://<zpi-ip>:5000/zview/ 를 가리키면 된다. 이 부분의 자세한 셋업은 별도 글로
다룬다 — 이번 글의 초점은 ZView 자체의 디자인과 코드다.
다음 변형 — 같은 뼈대에서 늘리기
이 ZView 샘플은 다른 용도로 가지치기하기 쉽게 짰다.
- 펌프 모니터 — OUT 라이트 4 개 자리에 펌프 1 ~ 4 상태, IN 자리에 압력 센서 트립 상태. D[0] 비트 분배 / D[10] 패킹 패턴은 그대로.
- 컨베이어 라인 모니터 — 라이트 자리에 8 ~ 12 채널 상태 (그리드 늘리기), D 비트 매트릭스는 그대로 디버깅 패널로.
- 알람 보드 — 각 라이트에 알람 이름 / 시각 라벨 추가, D[0] 대신 별도
/api/data/{i}로 알람 확인 / 리셋 명령 전송.
본질적으로 ZView 는 “JSON 폴링 + DOM 토글” 이 전부이고, 이 패턴은 산업 모니터링 화면 대부분을 커버한다. HTML 한 파일이 IDE 한 화면 분량 의 표현력을 갖는다는 게 ZView 의 코어 가치고, 이번 샘플은 그 가치를 가장 짧게 보여주는 한 장이라고 생각하면 된다.
회고
- ZView 의 개발-배포 사이클이 한 호흡 이다. HTML 한 파일 작성 →
zview_deploy한 번 → 운영자 브라우저 자동 새로고침. 이 사이클이 짧기 때문에 디자인 결정을 빨리 실험해볼 수 있다. - 단일 파일 제약이 디자인을 단순화한다. 외부 라이브러리 / 디자인 시스템 / 빌드 도구를 못 쓰니까 자연스럽게 핵심만 남는다. 산업 모니터의 본질은 어차피 “값 → 픽셀” 이라 이 제약이 맞다.
- 비트 매트릭스 같은 작은 시각 요소 하나가 디버깅 시간을 결정적으로 줄인다. 워드 엔디안 스왑을 알람 / 로그 / 추적기 없이 화면에서 바로 짚어낼 수 있게 한 게 이번 샘플에서 가장 만족스러운 부분이다.
댓글
로그인 없이 이름만 적고 남기실 수 있어요. 남긴 댓글은 바로 게시됩니다.
댓글을 불러오는 중…