ZView 샘플 — Modbus IO 모니터링 화면 한 장으로 만들기

이번 ZView 샘플은 산업 현장에서 가장 흔한 셋업 — 상위 마스터가 ZPi 의 4 채널 출력을 비트 단위로 제어하고, ZPi 의 4 채널 입력 상태를 비트 단위로 회신받는 원격 IO 노드 — 의 모니터링 화면 한 장이다. 운영자가 “지금 마스터가 어떤 명령을 보내고 있고, 입력이 어디까지 들어오고 있는지” 한눈에 알 수 있어야 한다.

ZView 의 매력은 이 정도 화면은 단일 HTML 파일 한 장 — 200 줄 안쪽 — 으로 끝낼 수 있고, 배포 = 파일 한 번 업로드라는 점이다. 전용 HMI IDE / 위젯 라이브러리 / 빌드 / 사이닝 절차 하나도 안 거친다.

ZView 모니터 — 활성 상태

어떤 샘플인가

화면 하나에 다음을 동시에 담았다.

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]/D[10] 메모리 맵 — Holding 28672 / 28682 의 하위 4 비트

디버깅 컷 하나만 더 — 초기 실행 때 마스터가 D[0]=3840 (0x0F00) 을 보내고 있었다. 스크립트는 하위 4 비트만 보니까 OUT 이 안 따라왔다. 마스터 측 바이트 스왑 이 원인이었고 마스터 설정 한 번 바꾸자 0x000F 로 정상 진입했다. ZView 의 비트 매트릭스가 이런 상황을 한 번에 짚어준다 — “값은 들어오는데 잘못된 비트 자리에 박혀있다” 가 화면에서 바로 보이니까.

바이트 스왑 디버깅 컷 — 같은 0F 가 상위 바이트(BEFORE)에서 하위 바이트(AFTER)로 내려와야 출력이 산다

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 단계 알약. 활성 / 유휴 / 끊김을 색으로 구분.

ZView 모니터 — 연결 끊김 상태

이 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 샘플은 다른 용도로 가지치기하기 쉽게 짰다.

본질적으로 ZView 는 “JSON 폴링 + DOM 토글” 이 전부이고, 이 패턴은 산업 모니터링 화면 대부분을 커버한다. HTML 한 파일이 IDE 한 화면 분량 의 표현력을 갖는다는 게 ZView 의 코어 가치고, 이번 샘플은 그 가치를 가장 짧게 보여주는 한 장이라고 생각하면 된다.

회고

댓글

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

댓글을 불러오는 중…