← 전체 글
SECS/GEM/약 19분 읽기/ 조회

HSMS Select에 응답이 없을 때, 세 가지 원인을 실제 캡처로 가르는 법

Select 타임아웃 한 줄 뒤에는 진짜 무응답, SystemBytes 불일치, SessionID 불일치가 섞여 있다. 2026-09-17에 소켓에서 직접 잡은 패킷과 SEMI E37의 T5·T6·T7로 셋을 구분한다.

SECS/GEMMES문제 해결프로젝트 노트체크리스트

Host 로그에 남는 건 대개 한 줄이다.

[HSMS] EQ ETCH-01 10.20.4.11:5000 select timeout, retrying (3)

이 한 줄로 장비 업체에 메일을 보내면 답은 보통 "저희 쪽은 응답 보냈습니다"이고, 실제로 그 말이 맞을 때가 있다. TCP는 붙어 있고, 장비는 Select.rsp를 소켓에 썼고, 그 프레임은 host의 NIC까지 도착했다. 그런데 host stack이 그걸 버렸다. 버린 이유는 로그에 안 남는다. 남는 건 "timeout"뿐이다.

셋을 가르는 건 응답 프레임이 있었는지, 있었다면 어느 필드가 틀렸는지다. 아래 다이어그램이 그 순서고, 이 글의 나머지는 그 세 갈래를 실제로 소켓에서 잡은 hex다.

select timeout 한 줄을 가르는 순서: inbound 프레임이 아예 없으면 무응답, 있으면 SystemBytes와 SessionID를 보낸 값과 대조해 원인 2와 원인 3으로 나뉜다 로그 한 줄: select timeout inbound 프레임이 있었나 없음 원인 1 — 진짜 무응답 T6 만료, 연결 종료 있음 보낸 값과 대조 bytes 6-9 원인 2 — SystemBytes 불일치 00 00 11 03 → 00 00 11 04 bytes 0-1 원인 3 — SessionID 불일치 00 0B → 00 0C

아래 hex는 전부 실제로 소켓에 오간 것이다. SECS/GEM 시뮬레이터의 EQ1을 passive listener(127.0.0.1:5501)로 띄우고, 바깥에서 Python 소켓 클라이언트로 두드렸다. 원인마다 fault knob을 하나씩 켜고 TCP 연결을 새로 맺었으며, 장비 쪽 로그에 RX가 찍힌 프레임만 실었다. 시각은 클라이언트에서 잰 값이고, 원본 export는 content/demos/hsms-select-no-answer-three-failures.json에 그대로 넣어 뒀다. 캡처 날짜는 2026-09-17.

정상 Select은 이렇게 생겼다

Select.req와 Select.rsp에는 body가 없다. 4바이트 길이 뒤에 SEMI E37의 10바이트 message header가 전부고, 그래서 control message의 길이 필드는 항상 10이다.

fault 없음, {} 상태에서 잡은 왕복이다.

06:03:00.213 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 11 01
06:03:00.225 RX Select.rsp   00 00 00 0A 00 0B 00 00 00 02 00 00 11 01   11.9 ms
00 00 00 0A   Length = 10 (header만, body 없음)
00 0B         bytes 0-1  SessionID = 11
00            byte 2     control message라서 0
00            byte 3     Select.rsp에서는 Select Status. 0 = accepted
00            byte 4     PType = 0 (SECS-II)
01 / 02       byte 5     SType = 1 Select.req / 2 Select.rsp
00 00 11 01   bytes 6-9  SystemBytes = 0x00001101, 그대로 echo

E37은 header 바이트를 0부터 센다. SessionID가 bytes 0–1, SystemBytes가 bytes 6–9다. 이 번호를 1부터 세면 아래 체크리스트에서 엉뚱한 바이트를 보게 된다.

11.9 ms는 listener를 띄우고 처음 붙은 연결이라 그렇고, 같은 캡처 뒤쪽에서 같은 왕복을 다시 하니 2.9 ms였다. 어느 쪽이든 밀리초 단위다.

host가 확인해야 하는 건 딱 두 가지다. SessionID가 내가 보낸 것과 같은가, 그리고 SystemBytes가 보낸 값 그대로 돌아왔는가. E37에서 transaction을 짝지어 주는 값이 SystemBytes다. 이 둘 중 하나라도 어긋나면 제대로 만든 host stack은 그 프레임을 자기 transaction의 응답으로 인정하지 않는다. 인정하지 않는다는 건 곧 버린다는 뜻이고, 버렸다는 사실을 로그로 남기는 stack은 드물다.

여기서 Select이 성공해도 GEM 쪽은 아직 아무것도 시작되지 않았다. Select은 E37 transaction이고, E30의 통신 확립(S1F13/S1F14)은 세션이 SELECTED가 된 다음 일이다. 그 구분은 SELECTED와 COMMUNICATING은 다르다 쪽에 따로 적어 뒀다.

내 캡처의 Select.req는 장비에 설정된 SessionID 11을 그대로 쓴다. E37이 Select.req/Select.rsp의 SessionID에 FF FF를 요구하는지는 이 캡처로 확인할 수 없다 — 미검증으로 둔다. 확인된 건 Linktest 쪽으로, 거기서는 E37이 세션과 무관한 FF FF를 쓴다(Linktest는 받는데 Select은 안 끝나는 장비). Select.req에 넣을 번호는 인터페이스 규격서에 적힌 값을 쓰는 게 맞다.

원인 1 — 진짜로 아무것도 안 온다

silentAfterSelect fault를 켜고 잡았다. 클라이언트는 12초를 기다렸다.

06:03:00.442 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 11 02
             RX              (없음 — 12,011.7 ms 대기 후 클라이언트가 포기)
00 00 00 0A / 00 0B / 00 / 00 / 00 / 01 / 00 00 11 02
Length 10, SessionID 11, byte 2 = 0, byte 3 = 0, PType 0, SType 1, SystemBytes 0x00001102

장비 쪽 로그에는 RX가 찍혀 있다. 프레임은 받았고, 처리하지 않았을 뿐이다.

2026-09-17T06:03:00.445Z  RX     00 00 00 0A 00 0B 00 00 00 01 00 00 11 02
2026-09-17T06:03:00.445Z  FAULT  Select.req ignored (silentAfterSelect)

12초 동안 listener는 소켓을 닫지도 않았다. E37 기본값이면 T6(5 s)도 T7(10 s)도 이미 지난 시간이다. 이 시뮬레이터는 T6도 T7도 돌리지 않는다는 뜻이고, 시뮬레이터가 표준의 대체물이 아니라는 증거로 읽어야 한다. 현장 장비라면 이 시점에 연결이 끊겨 있어야 정상이다. 12,011.7 ms는 내 클라이언트의 인내심이지 T6나 T7의 값이 아니다.

현장에서 이 모습이 나오는 경우는 대체로 셋이다. 장비 통신 프로세스가 떠 있지만 초기화가 안 끝났거나, 앞선 세션이 제대로 안 닫혀서 장비가 이미 SELECTED 상태라고 믿고 있거나, 방화벽이 TCP handshake는 통과시키고 그 뒤 payload 방향을 떨어뜨리거나. 세 번째는 의외로 흔하다. connect는 되는데 그다음부터 아무것도 안 돌아오면 장비보다 방화벽을 먼저 본다.

그리고 뒤의 두 경우에서는 침묵 자체가 규격 위반이다. E37은 거절한 Select도 Select.rsp로 답하게 되어 있고, 거절 이유는 byte 3의 non-zero Select Status로 실린다. 아무 답도 없는 건 장비 구현 문제이거나 프레임이 중간에서 죽은 것이다. 이 문장을 vendor 티켓에 넣으면 답장의 내용이 바뀐다.

host 쪽 대응은 재시도 간격이지 재시도 횟수가 아니다. E37의 T5(Connect Separation Timeout, 기본 10 s)가 이걸 위해 있다. 끊고 곧바로 다시 붙는 루프를 돌면 장비 쪽 소켓 자원만 갉아먹고, 초기화 중이던 장비는 더 늦게 올라온다. Select은 control transaction이므로 host가 기다리는 타이머는 T6(Control Transaction Timeout, 기본 5 s, 범위 1–240 s)다. T6가 만료되면 연결을 내리고 T5만큼 쉬었다가 다시 붙는다. T7(NOT SELECTED Timeout, 기본 10 s, 범위 1–240 s)은 그 뒤의 backstop이고, 실제로 T7을 돌리는 쪽은 보통 Select.req를 기다리는 passive 장비다. 타이머별 만료 동작은 어느 HSMS 타이머가 터진 것인가에 정리해 뒀다.

바이트가 아예 안 오는 게 아니라 프레임 중간에서 끊기는 경우라면 문제의 타이머는 T6가 아니라 T8(Network Intercharacter Timeout, 기본 5 s)이다. 길이 prefix와 프레임 재조립은 4바이트 length prefix가 결정하는 것 쪽 얘기다.

원인 2 — 응답은 왔는데 SystemBytes가 다르다

wrongSystemBytes fault. 이 knob은 요청의 SystemBytes에 1을 더해 돌려준다.

06:03:12.663 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 11 03
06:03:12.665 RX Select.rsp   00 00 00 0A 00 0B 00 00 00 02 00 00 11 04   1.9 ms
                                                        ^^^^^^^^^^^
00 00 00 0A   Length 10
00 0B         bytes 0-1  SessionID 11 — 보낸 값과 같다
00            byte 2     0
00            byte 3     Select Status 0 = accepted
00            byte 4     PType 0
02            byte 5     SType 2 Select.rsp
00 00 11 04   bytes 6-9  SystemBytes — 보낸 건 0x00001103, 돌아온 건 0x00001104

형식상 흠잡을 데 없는 Select.rsp다. SType 2, SessionID 일치, Select Status 0. 그런데 짝이 안 맞는다. 장비 로그에도 TX ... Select.rsp로 정상 송신이 찍힌다.

이게 앞의 경우와 결정적으로 다른 지점은 시간이다. 응답은 1.9 ms 만에 왔다. 원인 1은 T6가 만료될 때까지 host가 기다린다. 응용 로그에는 둘 다 "select timeout"으로 똑같이 찍히지만, 소켓 레벨에서는 하나는 즉시 도착한 프레임이고 하나는 침묵이다. 이 차이가 진단의 전부다.

실무에서 SystemBytes가 어긋나는 이유는 장비 stack이 응답을 만들 때 요청 값을 echo하지 않고 자기 카운터를 증가시켜 쓰기 때문인 경우가 많다. 오래된 펌웨어, 또는 여러 host 세션을 하나의 전역 카운터로 돌리는 구현에서 나온다. 시뮬레이터의 이 fault가 하는 일도 정확히 +1이다. 장비 업체 입장에서는 "응답을 보냈다"가 사실이라서, 캡처를 붙이지 않으면 대화가 진행되지 않는다.

원인 3 — SessionID가 다르다

wrongSessionId fault. 이 knob은 장비의 SessionID에 1을 더해 응답한다.

06:03:12.873 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 11 04
06:03:12.875 RX Select.rsp   00 00 00 0A 00 0C 00 00 00 02 00 00 11 04   1.7 ms
                                          ^^^^^
00 00 00 0A   Length 10
00 0C         bytes 0-1  SessionID 12 — 보낸 건 11
00            byte 2     0
00            byte 3     Select Status 0 = accepted
00            byte 4     PType 0
02            byte 5     SType 2 Select.rsp
00 00 11 04   bytes 6-9  SystemBytes 0x00001104, 정확히 echo

SystemBytes는 맞고 SessionID만 어긋난 경우다. 원인은 대개 단순하다. 장비에 여러 device가 정의돼 있고 설정 화면에서 host가 고른 번호와 장비가 응답에 쓰는 번호가 다르다. 라인 증설하면서 EQ 두 대의 설정을 복사해 쓰다 한쪽만 바꾼 경우도 있다.

여기는 host stack마다 동작이 갈린다. SessionID까지 엄격하게 검사하고 버리는 구현이 있고, control message에서는 SType과 SystemBytes만 보고 통과시키는 구현도 있다. 후자면 Select은 성공하는데 그다음 data message에서 문제가 터진다 — 장비가 계속 12로 보내니 host가 자기 EQ 매핑을 못 찾고, "unknown device" 계열 에러가 뜬다. 어느 쪽이든 최초 증상 한 줄만 보고는 판단할 수 없다. SystemBytes 쪽으로 좁혀졌다면 그 케이스만 따로 다룬 글에 정상 세션과 어긋난 세션을 나란히 놓아뒀다.

명시적 거절은 이 셋이 아니다

byte 3이 0이 아니면 얘기가 다르다. selectRefuse: 1을 켜고 잡은 프레임이다.

06:03:13.084 TX Select.req   00 00 00 0A 00 0B 00 00 00 01 00 00 11 05
06:03:13.086 RX Select.rsp   00 00 00 0A 00 0B 00 01 00 02 00 00 11 05   2.0 ms
                                                ^^

byte 3이 01이고 나머지는 전부 정상이다. SessionID 11, SystemBytes echo, SType 2. E37은 낮은 Select Status 값에 이름을 붙여 놓았다 — 0 accepted, 1 Communication Already Active, 2 Connection Not Ready, 3 Connect Exhaust, 4 이상은 reserved. 이 표는 표준에서 인용한 것이고 캡처로 증명되는 건 아니다. 캡처가 보여 주는 건 장비가 byte 3에 넣은 값이 그대로 실려 온다는 사실뿐이다. 값별 해석은 정상 Select과 거절된 Select은 한 바이트만 다르다에 있다.

장비 쪽 상태 변화가 하나 더 있다. Select Status 1로 거절한 뒤 listener의 HSMS 상태는 NOT CONNECTED로 남았고, 받아들인 왕복에서는 SELECTED가 됐다. 소켓은 두 경우 다 열려 있었다. 열린 소켓은 세션이 아니다.

이 글이 다루는 건 거절이 아니라 host 눈에 아무 말도 없어 보이는 쪽이다. 거절은 적어도 로그에 이유를 남긴다.

장비 없이 이 셋을 가르는 법

  1. host 장비에서 직접 tcpdump를 뜬다. tcpdump -i any -X -s0 'tcp port 5000'. inbound 프레임이 아예 없으면 원인 1이다. 있으면 host stack이 버린 것이고, 그럼 hex를 읽는다.
  2. 길이 4바이트를 먼저 본다. 00 00 00 0A면 body 없는 control message다. 그 뒤 10바이트만 읽으면 된다.
  3. bytes 0–1(SessionID)과 bytes 6–9(SystemBytes)를 보낸 값과 비교한다. SessionID가 다르면 원인 3, SystemBytes가 다르면 원인 2. header 바이트 번호는 E37 기준으로 0부터다.
  4. byte 3을 본다. 0이 아니면 무응답이 아니라 거절이고, 그 숫자가 이유다.
  5. 응답까지의 시간을 본다. 캡처 없이 host 로그만 있을 때 쓸 수 있는 유일한 단서다. 요청 직후 수 ms 만에 무언가 왔다가 timeout이 났으면 침묵이 아니다. 여기 캡처에서 그 값은 1.7–2.9 ms였고, 무응답 쪽은 12초를 기다려도 아무것도 없었다.

그리고 host stack을 직접 만들고 있다면 — 대부분의 MES 프로젝트가 그렇다 — transaction matching 이전에 받은 프레임을 먼저 로그로 남겨야 한다. 매칭에 성공한 것만 로그하는 구조가 이 문제를 진단 불가능하게 만드는 진짜 원인이다. header 10바이트를 hex 그대로 한 줄 찍는 것으로 충분하다. 이 글의 네 경우가 그 한 줄에서 즉시 갈린다. byte 3은 E5의 data message에서 Function 번호가 들어가는 자리를, control message에서는 Select Status가 빌려 쓰는 것이라 특히 놓치기 쉽다.

캡처 하나 붙여서 "0x00001103을 보냈고 0x00001104를 받았다"고 쓰면 장비 업체와의 메일이 한 번에 끝난다. "응답이 없습니다"로는 세 번 왕복한다.

같은 실험을 직접 해 보려면 scadathings.com/secs-gem-simulator에서 passive listener를 띄우고 silentAfterSelect, wrongSystemBytes, wrongSessionId, selectRefuse를 하나씩 걸어 자기 host stack을 붙여 보면 된다. 위 hex는 전부 거기서 나왔다.