알람 연동 테스트는 전부 통과했다. 그런데 실제 장비를 붙이니 알람이 한 건도 MES로 올라오지 않는다. 호스트 로그를 열어 보면 이상한 게 없다. S5F1을 보내고 S5F2를 받은 기록이 줄줄이 있다.
거기가 문제다. S5F1은 호스트가 보내는 메시지가 아니다. 장비가 보내고 호스트가 받는 메시지다. 테스트에서 통과한 건 알람 수신 경로가 아니라 알람 발신 경로였고, 받는 쪽 코드는 한 번도 실행된 적이 없다.
아래 프레임은 시뮬레이터 passive listener에 소켓을 직접 열어서 주고받은 것이다. 호스트가 보낼 자격이 없는 메시지에는 ACK가 왔고, 호스트 몫인 메시지는 abort로 돌아왔다.
세션부터
TX Select.req 00 00 00 0A 00 0B 00 00 00 01 00 00 06 01
RX Select.rsp 00 00 00 0A 00 0B 00 00 00 02 00 00 06 01
SessionID 00 0B은 11. 헤더 바이트 3이 00이니 Select Status 0, 수락이다(E37). 그다음 S1F13을 먼저 보내야 한다. 이 listener는 COMMUNICATING이 되기 전에는 다른 스트림 전부에 SxF0을 돌려준다 — E30 통신 게이트 글에서 다룬 동작이다.
TX S1F13 W 00 00 00 1A 00 0B 81 0D 00 00 00 00 06 02 01 02 41 07 48 4F 53 54 4D 45 53 41 03 31 2E 30
RX S1F14 00 00 00 37 00 0B 01 0E 00 00 00 00 06 02 01 02 21 01 00 01 02 ...
21 01 00이 COMMFLAG 0. 여기서부터 데이터 메시지가 정상 처리된다.
stream 5: 호스트 몫은 S5F1이 아니다
문제의 프레임이다. 장비가 올려야 할 알람 보고를 호스트가 보냈다.
TX S5F1 W 00 00 00 2C 00 0B 85 01 00 00 00 00 06 03
01 03 21 01 80 B1 04 00 00 13 89 41 15 43 68 61 6D 62 65 72 ...
RX S5F2 00 00 00 0D 00 0B 05 02 00 00 00 00 06 03 21 01 00
바디는 L[3] { B[1] 80, U4 5001, A "Chamber pressure high" } — ALCD, ALID, ALTX다. ALCD 80은 8번 비트가 set이니 알람 발생 쪽이고, 이 비트 하나가 뭘 정하는지는 ALCD 한 비트 글에 따로 썼다.
돌아온 건 S5F2, 바디 21 01 00, ACKC5 0. 잘 받았다는 뜻이다. 방향이 거꾸로라는 지적은 한 글자도 없다.
호스트가 stream 5에서 실제로 보낼 메시지는 S5F3이다. 알람 하나를 켜고 끄는 요청이다.
TX S5F3 W 00 00 00 15 00 0B 85 03 00 00 00 00 06 04 01 02 21 01 00 B1 04 00 00 13 89
RX S5F4 00 00 00 0D 00 0B 05 04 00 00 00 00 06 04 21 01 00
역시 ACKC5 0. 두 응답의 바디는 바이트 단위로 같다. 정당한 요청과 방향이 뒤집힌 요청을 응답만 보고 구분할 방법이 없다는 뜻이다.
그러면서 호스트 몫인 다른 메시지는 아예 안 받는다.
TX S5F5 W 00 00 00 0C 00 0B 85 05 00 00 00 00 06 05 01 00
RX S5F0 00 00 00 0A 00 0B 05 00 00 00 00 00 06 05
헤더 바이트 2가 05(W-bit 0, Stream 5), 바이트 3이 00. Function 0, abort다. 바디 없는 14바이트 프레임에 SystemBytes만 그대로 붙어 온다. Function 0을 실패 신호로 읽는 법은 stream 9 글에 정리해 뒀다.
stream 10은 더 헷갈린다
터미널 서비스도 같은 함정이 있다. 여기선 더 선명하게 드러난다.
TX S10F1 W 00 00 00 21 00 0B 8A 01 00 00 00 00 06 06 01 02 B1 04 00 00 00 01 41 0D 4F 50 45 ...
RX S10F2 00 00 00 0D 00 0B 0A 02 00 00 00 00 06 06 21 01 00
S10F1은 장비 쪽 오퍼레이터가 호스트로 텍스트를 올리는 메시지다. 호스트가 보냈는데 S10F2, ACKC10 0이 왔다.
정작 호스트가 쓰고 싶은 쪽은 이거다. 장비 화면에 글자를 띄우는 메시지.
TX S10F3 W 00 00 00 22 00 0B 8A 03 00 00 00 00 06 07 01 02 B1 04 00 00 00 01 41 0E 4C 4F 54 ...
RX S10F0 00 00 00 0A 00 0B 0A 00 00 00 00 00 06 07
TX S10F5 W 00 00 00 1C 00 0B 8A 05 00 00 00 00 06 08 01 02 B1 04 00 00 00 01 01 01 41 06 4C 49 ...
RX S10F0 00 00 00 0A 00 0B 0A 00 00 00 00 00 06 08
둘 다 abort. 이 listener가 구현한 stream 10 메시지는 호스트가 보낼 일이 없는 그 하나뿐이다.
방향은 메시지 정의의 일부다
E5는 각 Function마다 어느 쪽이 보내는지를 같이 정해 둔다. Stream과 Function 번호만 맞으면 되는 게 아니다. 아래 표는 E5 정의를 옮긴 것이고 이번 캡처가 증명한 내용이 아니다 — 캡처가 증명한 건 이 listener가 각 프레임에 무엇으로 답했는지까지다.
| 메시지 | 보내는 쪽 | 이 listener의 응답 |
|---|---|---|
| S5F1 알람 보고 | 장비 | S5F2, ACKC5 0 |
| S5F3 알람 enable/disable | 호스트 | S5F4, ACKC5 0 |
| S5F5 알람 목록 요청 | 호스트 | S5F0 |
| S10F1 터미널 요청 | 장비 | S10F2, ACKC10 0 |
| S10F3 터미널 표시, 단일 블록 | 호스트 | S10F0 |
| S10F5 터미널 표시, 멀티 블록 | 호스트 | S10F0 |
E30은 알람 관리에서 S5F1을 장비가 올리도록 요구한다. 호스트 코드에서 S5F1을 send() 하는 줄이 보이면 그건 기능이 아니라 버그다.
이게 왜 통합 테스트를 통과하는가
관대한 장비 시뮬레이터는 방향을 보지 않는다. Stream/Function 조합만 보고 정해진 secondary를 돌려준다. 그래서 잘못된 방향으로 보낸 primary가 초록불을 받는다.
여기서 시간 제일 많이 버린다. 테스트는 다 통과했는데 현장에서 알람이 안 올라오니 장비 벤더를 부르고, 벤더 로그에는 S5F1을 보낸 적이 없으니 양쪽이 각자 맞는 말을 한다. 호스트가 S5F1을 보내고 있었다는 사실은 두 로그를 한 화면에 놓기 전까지 아무도 모른다.
드라이버 쪽에서 막는 방법은 간단하다. 송신 함수에 방향 화이트리스트를 둔다. 호스트가 보낼 수 있는 (Stream, Function) 목록을 표로 박아 놓고, 목록에 없으면 소켓에 쓰기 전에 예외를 던진다. GEM 범위면 표가 길지 않다.
확인하지 못한 것
표의 방향 지정은 E5의 메시지 정의를 따른 것이고, 특정 개정판의 절 번호와 맞춰 읽지는 않았다. 문서에 절을 인용해야 한다면 손에 있는 사본을 직접 확인하는 게 맞다. ACKC5와 ACKC10의 0 이외 코드값도 이번 캡처로는 하나도 확인하지 못했다 — 돌아온 값은 전부 0이었다.
그리고 이건 listener 하나의 동작이다. 방향을 검사해서 S9F5나 SxF0으로 거절하는 장비도 있다. 어느 쪽이든 호스트가 방향을 틀렸다는 사실은 달라지지 않고, 관대한 쪽에서만 테스트했다면 그 사실을 현장에서 처음 알게 된다.
위 프레임은 전부 공개된 SECS/GEM 시뮬레이터의 passive listener에 소켓을 열어 주고받은 것이다. 본인 드라이버로 S10F3을 한 번 쏴 보면 호스트 스택이 S10F0을 어떻게 기록하는지 바로 보인다. 그게 실제로 알아볼 값어치가 있는 부분이다.