Linktest.req를 보냈는데 Linktest.rsp가 안 온다. T6가 터지고 호스트는 멀쩡한 소켓을 끊는다. 그런데 장비 쪽 로그를 열어 보면 프레임은 분명히 들어와 있다. 받긴 받았고, 답을 안 한 것이다.
HSMS 메시지는 4바이트 길이 접두사 뒤에 10바이트 헤더가 붙는다. SEMI E37은 이 헤더 바이트를 0부터 센다. 바이트 0-1이 SessionID, 바이트 2와 3이 Header Byte 2/3, 바이트 4가 PType, 바이트 5가 SType, 바이트 6-9가 SystemBytes다. PType은 뒤에 오는 메시지 본문을 어떤 규칙으로 읽을지 정하는 값이고, E37에서 0이 SECS-II 인코딩이다. 현장에서 0 말고 다른 값을 쓰는 걸 본 적은 없다. SType은 Select.req나 Linktest.req 같은 제어 메시지 종류를 고르고, 0이면 데이터 메시지다. 두 바이트는 서로 붙어 있다. 인코더가 SType을 오프셋 5가 아니라 4에 쓰면, Linktest.req의 5가 PType 자리로 들어가고 SType 자리에는 00이 남는다.
틀린 PType은 그대로 통과했다
아래는 전부 passive HSMS 리스너에 소켓을 직접 열어 호스트 역할로 찍은 캡처다. 장비 쪽이 모든 프레임을 RX로 기록했으니 실제로 선을 탄 바이트다.
Select를 마치고 S1F13으로 통신을 연 다음, 같은 소켓에 S1F1을 두 번 보냈다. 한 번은 PType 0, 한 번은 PType 3으로.
TX S1F1 PType=0 00 00 00 0A 00 0B 81 01 00 00 00 00 10 03
RX S1F2 00 00 00 32 00 0B 01 02 00 00 00 00 10 03
01 02 41 15 56 58 2D 39 30 30 30 20 50 6C 61 73 6D 61 20 45 74 63 68 65 72
41 0D 53 45 43 53 47 45 4D 2D 31 2E 34 2E 31
정상 거래다. 요청은 헤더만 14바이트, 바이트 2가 81이라 W-bit 1에 Stream 1, 바이트 3이 01로 Function 1. 응답은 L[2]에 MDLN VX-9000 Plasma Etcher와 SOFTREV SECSGEM-1.4.1이 들어 있다.
TX S1F1 PType=3 00 00 00 0A 00 0B 81 01 03 00 00 00 10 04
RX S1F2 00 00 00 32 00 0B 01 02 00 00 00 00 10 04
01 02 41 15 56 58 2D 39 30 30 30 20 50 6C 61 73 6D 61 20 45 74 63 68 65 72
41 0D 53 45 43 53 47 45 4D 2D 31 2E 34 2E 31
두 요청의 차이는 바이트 4의 00과 03, 그리고 SystemBytes 끝자리뿐이다. 응답 본문은 한 바이트도 다르지 않다. 리스너는 PType을 아예 읽지 않는다 — 소스를 열어 보면 SType만 분기하고 PType은 디코딩만 해 두고 쓰지 않는다. 그리고 자기가 보내는 응답에는 언제나 PType 0을 찍는다. 위 응답 두 개 모두 오프셋 4가 00인 게 그 증거다.
여기서 하고 싶은 말은 리스너가 틀렸다는 게 아니다. 테스트 상대가 검사하지 않는 필드는 테스트로 잡히지 않는다. 호스트 인코더가 바이트 4에 쓰레기를 넣고 있어도 시뮬레이터 상대로는 전 구간이 초록불이다. E37은 지원하지 않는 PType에 대해 Reject.req(SType 7)를 돌려주도록 정의하고 있는데, 그 reason code 번호까지는 이번 실행에서 확인하지 못했다. 장비 벤더 스택이 그 검사를 실제로 하는지도 확인한 바 없다. 확실한 건 하나다. 이 검사가 켜져 있는 장비를 만나는 날이 공장에서의 첫날이 된다.
한 칸 밀린 Linktest는 S0F0이 된다
같은 소켓에서, 다음 프레임은 SType 5를 오프셋 5가 아니라 4에 썼다.
TX 밀린 Linktest 00 00 00 0A 00 0B 00 00 05 00 00 00 10 05
RX (없음) 3초 대기, 아무것도 오지 않음
장비 로그에는 이 프레임이 RX로 남았다. ptype: 5, stype: 0으로 파싱됐다. 길이 접두사도 맞고 프레임도 깨지지 않았다. 받는 쪽은 규칙대로 처리했을 뿐이다. SType 0은 데이터 메시지라는 뜻이고, 바이트 2가 00이니 W-bit는 0, Stream도 0, 바이트 3도 00이라 Function 0. 즉 응답을 요구하지 않는 S0F0이다. 리스너 코드는 W-bit가 서 있을 때만 응답을 만든다. 답할 이유가 없으니 답하지 않았다.
바로 다음에 똑같은 Linktest를 제대로 써서 보냈다.
TX Linktest.req 00 00 00 0A 00 0B 00 00 00 05 00 00 10 06
RX Linktest.rsp 00 00 00 0A 00 0B 00 00 00 06 00 00 10 06
즉시 왔다. 두 프레임의 차이는 05가 오프셋 4에 있느냐 5에 있느냐, 그것뿐이다. 소켓도 세션도 같다.
이게 디버깅에서 왜 고약하냐면, 증상이 "장비가 Linktest에 응답하지 않는다"로 보이기 때문이다. 그 문장으로 시작하면 장비 설정, 방화벽, 벤더 쪽 T6 값을 뒤지게 된다. 실제 범인은 우리 쪽 헤더 빌더다. 그리고 Select.req도 Separate.req도 같은 방식으로 조용히 사라진다 — SType이 몇이든 밀리면 전부 W-bit 없는 S0F0이 된다.
캡처 전체
RX Select.req 00 00 00 0A 00 0B 00 00 00 01 00 00 10 01
TX Select.rsp 00 00 00 0A 00 0B 00 00 00 02 00 00 10 01
RX S1F13 W 00 00 00 0C 00 0B 81 0D 00 00 00 00 10 02 01 00
TX S1F14 00 00 00 37 00 0B 01 0E 00 00 00 00 10 02 01 02 21 01 00 ...
RX S1F1 PType=0 00 00 00 0A 00 0B 81 01 00 00 00 00 10 03
TX S1F2 00 00 00 32 00 0B 01 02 00 00 00 00 10 03 ...
RX S1F1 PType=3 00 00 00 0A 00 0B 81 01 03 00 00 00 10 04
TX S1F2 00 00 00 32 00 0B 01 02 00 00 00 00 10 04 ...
RX 밀린 Linktest 00 00 00 0A 00 0B 00 00 05 00 00 00 10 05
RX Linktest.req 00 00 00 0A 00 0B 00 00 00 05 00 00 10 06
TX Linktest.rsp 00 00 00 0A 00 0B 00 00 00 06 00 00 10 06
방향 표기는 장비 입장이다. RX가 호스트에서 올라온 것, TX가 장비가 내보낸 것. 밀린 Linktest 다음 줄에 TX가 없는 게 이 글의 전부다.
호스트 쪽에 넣을 것
인코더에서 PType을 변수로 두지 말고 상수 0으로 박아라. 바이트 4에 값이 들어갈 경로가 아예 없으면 밀릴 수도 없다.
디코더에서는 반대로 PType을 읽고 0이 아니면 버려라. 그냥 SECS-II로 파싱해 버리면 운 나쁘게 디코딩이 성공해서 없는 메시지를 하나 지어내게 된다.
그리고 헤더 빌더에 단위 테스트 한 줄. Linktest.req를 만들어서 바이트 열이 00 00 00 0A ss ss 00 00 00 05 ...인지 보는 것으로 충분하다. 오프셋을 고정값으로 비교하는 테스트는 이 버그가 들어오는 순간 깨진다. 통합 테스트는 안 깨진다 — 위에서 본 대로 상대가 PType을 안 보기 때문이다.
마지막으로, "응답 없음" 로그를 남길 때 보낸 바이트 열을 통째로 같이 남겨라. 이번 건은 그 hex 한 줄이면 3초 만에 끝난다.
리스너는 지금도 떠 있다. 위 hex를 그대로 소켓에 밀어 넣어 보면 같은 결과가 나온다: SECS/GEM 시뮬레이터.