작업자가 레시피 로드 버튼을 누르면 메시지 줄에 OK가 깜빡이는데 장비는 그대로다. 트레이스를 뜯어보면 Call 서비스는 Good을 돌려줬다. 그런데 레시피는 컨트롤러까지 가지도 않았다. 누가 거짓말을 한 게 아니다. HMI는 자기가 아는 사실, 즉 서버가 요청을 접수했다는 것만 정직하게 표시했다. 이 "접수됐다"와 "실행됐다" 사이의 간격이 설계에서 다뤄야 할 핵심이고, 태그 쓰기 대신 버튼을 method에 연결할 때 팀들이 처음 데는 지점이다.
method는 쓰기 태그 하나로 표현하기 애매한 동작에 맞는다. 레시피 다운로드 요청, 장비 메시지 확인, 캘리브레이션 단계 실행, 대기열 삭제, 캐리어 배출 같은 작업이다. Call 서비스(OPC UA Part 4, §5.11)는 서버에 입력 인자를 넘기고, 애플리케이션 코드를 돌리고, 출력 인자와 상태 코드를 돌려준다. StartRequest = 1을 쓰고 피드백 비트를 보는 것과는 분명히 다르다.
하지만 HMI가 method를 마법 버튼처럼 다루면 이 구조는 아무 값어치가 없다. method call은 명령 트랜잭션이다. 입력이 있고, 검증이 있고, 상태 코드가 있고, 그 뒤에 실제 동작이 있다. 그 구조가 실제로 필요할 때만 쓴다.
- 여러 입력값을 한 번에, 원자적으로 묶어 보내야 한다.
- 서버가 뭔가 받아들이기 전에 요청을 검증해야 한다.
- 거절 사유 코드, 메시지, 또는 부여된 job id를 돌려받아야 한다.
- 유지되는 setpoint가 아니라 한 번 실행하는 동작이다.
이 중 어느 것도 아니라면, 태그 쓰기와 피드백 비트가 시운전할 것도 적고 틀릴 것도 적다. 주소 공간에서 깔끔해 보인다는 이유만으로 method를 쓰지 마라.
작은 API처럼 인터페이스를 정한다 — 실제로 그렇기 때문에
믿을 만한 인터페이스는 호출자가 무엇을 넣고 서버가 무엇을 돌려주는지 명확하다. 인자 이름, 타입, 순서를 문서로 남겨라. 서버 업데이트가 인자 순서를 바꾸면 그 method를 부르는 모든 화면이 조용히 깨진다.
챙길 만한 입력 인자:
| 인자 | 예 | 현장 메모 |
|---|---|---|
| 장비 위치 | Line3.Filler.Infeed | 대상을 명시적으로 넘긴다. 서버 내부의 "현재 선택" 상태에 기대는 method는 지뢰다. |
| 명령 종류 | LoadRecipe, ClearFaultQueue | 정해진 목록. 화면 입력 필드의 자유 입력은 안 된다. |
| 레시피 또는 작업 ID | RCP-2041-A | 존재 여부 그리고 승인 버전을 실행 전에 확인한다. |
| 작업자 / 세션 ID | op_1452 | 감사 로그, 그리고 새벽 2시에 누가 뭘 했는지 찾을 때 쓴다. |
| 요청 ID | UUID, 순번 | 재시도를 idempotent하게 만드는 고리 — 아래 참고. |
| 기대 상태 | Stopped, Manual, NoLotLoaded | 오래된 화면에서 나간 명령을 서버가 거절하게 해준다. |
돌아올 때는 단순 boolean 하나로 끝내지 마라. 접수/거절 결과, 애플리케이션 사유 코드, 짧은 진단 문자열, 서버가 부여한 command/job id, 그리고 대략적인 실행 상태(Queued, Running, Completed, Failed)를 돌려준다. 진단 문장은 HMI 메시지 줄에 들어갈 만큼 짧게, 전체 로그는 서버에 남긴다.
Good은 통신 판정이지 공정 판정이 아니다
서두의 함정을 그대로 말하면 이렇다. Call 서비스의 Good은 서버가 형식에 맞는 요청을 받았고 유효한 출력 인자를 돌려줬다는 뜻이다. 장비가 그 일을 실제로 했는지에 대해서는 아무 말도 하지 않는다.
레시피 다운로드 method는 보통 수백 밀리초 만에 돌아온다 — 요청을 접수했다는 뜻이다. 그 다운로드는 몇 초 뒤에 실패할 수 있다. 컨트롤러가 파라미터를 거절하거나, 장비 모드가 바뀌거나, 대상 스테이션 통신이 끊긴 경우다. 그래서 피드백을 두 층으로 나누고 서로 분리해 둔다.
- 호출 결과 — 서버가 요청을 접수하고 유효한 출력을 돌려줬는가?
StatusCode와 애플리케이션 사유 코드다. - 실행 결과 — 장비가 정해진 시간 안에 명령된 동작을 끝냈는가?
실행 결과는 method 반환값이 아니라 일반 태그나 이벤트로 확인한다.
CommandActive,CommandDone,CommandFailedLastCommandId,LastCommandResultCode- 장비 상태, 모드, permissive 상태
작업자가 실제로 보는 건 두 번째 층이다. 서비스 상태는 초록인데 장비가 안 움직이면, 반환 코드가 뭐라 하든 운전실에서는 실패한 명령이다.
권한은 버튼이 아니라 서버에 있어야 한다
method는 내부에서 여러 쓰기를 연쇄하거나 시퀀스 전체를 시작할 수 있어서, 쓰기 태그만큼은 권한을 따져야 한다. OPC UA는 그 고리를 직접 준다. 모든 Method 노드에는 Executable과 UserExecutable 속성이 있고(Part 3, §5.7), 허용되지 않은 호출은 Good이 아니라 Bad_NotExecutable이나 Bad_UserAccessDenied로 돌아온다. HMI가 이걸 읽어서 보여주게 하라. 전부 "method failed"로 뭉개면 안 된다.
시운전 때 두 층을 모두 훑는다.
- OPC UA 신원 — anonymous, username/password, certificate, token.
- 서버 role 매핑 — 어떤 role이 이 method를 부를 수 있는가.
- 노드 속성 — 보이긴 하는데 runtime 사용자에게
UserExecutable = false인가? - HMI 권한 — 운전, 보전, 엔지니어, 관리자.
- 장비 모드 — 자동, 수동, 보전, local, remote.
- 공정 permissive — 도어, 인터록, 배치 진행 여부, 안전 속도.
버튼을 숨기는 건 접근 제어가 아니다. 테스트 클라이언트, 스크립트, 잊힌 엔지니어링 워크스테이션이 같은 노드에 닿을 수 있다면, 안 된다고 말하는 건 서버여야 한다.
호출이 도착했는지조차 네트워크는 거짓말한다고 가정하라
가장 고약한 경우는 거절이 아니라 타임아웃이다. HMI가 Call을 쐈는데 응답이 안 온다. 이제 서버가 받았는지 아무도 모른다. 작업자가 버튼을 다시 누르면 자재가 두 번 이송된다.
그래서 클라이언트 요청 ID가 제 몫을 한다. 부작용이 있는 명령이라면 서버의 중복 규칙을 미리 정한다.
- 같은 요청 ID, 같은 인자 → 이전 결과나 현재 명령 상태를 돌려준다. idempotent, 재시도 안전.
- 같은 요청 ID, 다른 인자 → 클라이언트 오류로 거절한다. 뭔가 꼬였으니 추측하지 않는다.
- 충돌 명령이 실행 중인데 새 요청 ID → busy / invalid state로 거절한다.
자재 이송, 라벨 출력, 캐리어 배출, 레시피 다운로드, lot start — 이런 동작에서 중복은 화면 메시지 하나가 아니라 생산 이력 한 줄이 두 번 찍히는 문제다. UI 성가심과, 고객에게 해명해야 하는 추적성 문제의 차이다.
전부 같은 타임아웃은 틀렸다
검증 조회와 시퀀스 시작은 기다리는 시간이 다르다. 같은 타임아웃을 주지 마라.
| 호출 종류 | HMI 서비스 타임아웃 | 이유 |
|---|---|---|
| 검증/조회 | 1~3초 | 빨리 끝나거나 눈에 보이게 실패해야 한다. |
| 명령 접수 | 3~10초 | 서버 검증 여유. 장비 전체 동작 시간은 아니다. |
| 레시피 다운로드 요청 | 10~30초 | 레시피 크기와 컨트롤러 인터페이스에 따라. |
| 장비 실행 완료 | 별도 watchdog | Call 타임아웃이 아니라 상태 태그로 본다. |
서비스 타임아웃이 너무 길면 HMI가 멈춘 듯 보인다. 너무 짧으면 명령은 뒤에서 조용히 진행 중인데 화면엔 실패가 뜨고 — 그게 앞 절의 중복 발사를 부른다. 타임아웃은 접수에 묶고, 실행은 상태 태그 watchdog에 맡겨라.
FAT, SAT에서 실제로 보게 될 실패
이 중 OPC UA 통신 문제는 거의 없다. 전부 인터페이스 약속이 깨진 것이다.
- method는
Good인데 HMI가 거절 코드가 담긴 출력 인자를 무시한다. - browse path가 바뀐 뒤 HMI가 다른 object instance의 method를 부른다.
- 서버 업데이트로 인자 순서가 밀렸는데 화면 스크립트는 그대로다.
- 숫자 인자의 단위가 틀렸거나 enum 값이 하나 어긋났다.
- 엔지니어 로그인은 되고 운전 runtime 로그인은
Bad_UserAccessDenied. - HMI가 타임아웃 후 재시도하며 명령을 두 번 접수한다.
- 장비가 local 모드인데 서버가 명령을 접수한다.
- 작업자에게 "레시피 미승인", "스테이션 미비움" 대신 "method failed"만 보인다.
생산에 넣기 전에는 화면이 아니라 제어 기능처럼 명령 경로를 시험한다. 정확한 노드, object context, 인자 이름과 타입을 확정하고; 모든 거절을 강제하고(잘못된 모드, permissive 부족, 잘못된 레시피, 권한 없음, 장비 busy); 호출 중 네트워크 케이블을 뽑고 HMI를 지켜보고; 같은 요청 ID를 재생해 중복 규칙이 지켜지는지 확인하고; 감사 로그에 사용자, 시간, 클라이언트, 인자, 결과가 남는지 보고; 그다음 서버 재시작, HMI 재시작, 인증서 갱신 뒤에 전부 다시 한다.
여기서 하나만 챙긴다면 두 층 피드백을 제대로 잡아라. 나머지는 뒷정리다. 접수와 실행을 별개의 사실로 보고하는 명령이라야 작업자가 믿을 수 있다.