Host 코드는 다 짰다. S1F13을 보내고 S6F11을 받아 파싱하고 DB에 넣는 데까지 돌아간다. 문제는 붙일 설비가 없다는 것이고, vendor FAT은 3주 뒤다.
FAT 당일에 처음 붙여 본다는 건, vendor 엔지니어 네 명이 뒤에 서 있는 자리에서 host driver를 디버깅한다는 뜻이다. 거기서 나오는 실패는 대단한 게 아니다. COMMACK이 0이 아니거나, S2F33이 통째로 거절되거나, 설비가 online으로 안 넘어간다. 전부 지금 사무실에서 확인할 수 있는 것들이다.
명령 하나면 된다.
npx secs-gem-host connect <host>:<port> --report
붙일 대상은 SECS/GEM 시뮬레이터의 HSMS passive listener다. 로컬에 시뮬레이터를 띄웠으면 그쪽 IP와 port를 그대로 넣으면 된다. --report를 붙이면 SEMI E30 startup을 10단계로 쪼개 돌리고, 단계마다 PASS/FAIL 표를 찍고, FAIL이 하나라도 있으면 exit code 1로 끝난다. CI에 그대로 걸 수 있다는 뜻이기도 하다.
실제로 찍히는 표
아래는 2026-09-09에 secs-gem-host 0.2.0으로 시뮬레이터 listener(127.0.0.1:5501)를 친 출력 그대로다. 합성이 아니다. 표의 모든 줄에 설비 쪽 RX packet이 하나씩 대응하고, 그 바이트는 다음 절에 있다.
Step Sent Expect Got Ack Time(ms) Result Note
------------------------------------------------------------------------
1 S1F13 S1F14 S1F14 0 1/45000 PASS VX-9000 Plasma Etcher/SECSGEM-1.4.1
2 S1F17 S1F18 S1F18 0 1/45000 PASS
3 S1F3 S1F4 S1F4 - 1/45000 PASS 4 values
4 S1F11 S1F12 S1F12 - 2/45000 PASS 4 SVIDs
5 S2F29 S2F30 S2F30 - 0/45000 PASS 2 ECs
6 S2F33 S2F34 S2F34 0 3/45000 PASS
7 S2F35 S2F36 S2F36 0 1/45000 PASS
8 S2F37 S2F38 S2F38 0 1/45000 PASS
9 S5F3 S5F4 S5F4 0 5/45000 PASS
10 S2F31 S2F32 S2F32 0 1/45000 PASS
------------------------------------------------------------------------
E30 startup: 10/10 passed
flag: model VIDs not in equipment SVID list: 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009, 10010, 10011
1단계 Note에 찍힌 VX-9000 Plasma Etcher/SECSGEM-1.4.1은 설비가 S1F14에 실어 보낸 MDLN과 SOFTREV다. Tool은 COMMACK만 보는 게 아니라 그 두 자리가 실제로 채워졌는지까지 확인한다.
Ack 열의 -는 실패가 아니다. S1F4, S1F12, S2F30에는 애초에 1바이트 ack code가 없다. 그 단계의 판정 기준은 "body가 list인가", "namelist가 비어 있지 않은가"다.
Time(ms) 열의 분모 45000은 T3 reply timeout이다. E37 timer 표가 T3에 typical 45초를 적어 두었고 — 의무 기본값이 아니라 typical이다 — 이 tool이 그걸 자기 기본값으로 삼는다. --t3으로 바꾼다. 여기서 1 ms가 나온 건 loopback이라 그렇다. 실제 설비에서 이 숫자가 초 단위로 올라가면 그것 자체가 보고할 값이다. Timer 쪽이 궁금하면 T3·T5·T6·T7·T8이 각각 무엇을 지키는지를 보면 된다.
표 뒤에 있는 바이트
위 실행은 socket을 탔으니 설비 쪽에도 같은 기록이 남는다. 아래는 그 실행에 대한 시뮬레이터의
/api/export/pcap 출력 그대로다. packet 27개, 양쪽 다. host가 보낸 frame은 전부 설비 쪽 RX
기록으로 남아 있다 — message 객체를 예쁘게 찍은 게 아니라 wire bytes라는 근거가 그것이다. raw
export는 content/demos/prove-e30-startup-handshake-in-one-command.json에 커밋해 뒀다.
Capture용으로 flag 하나만 더 붙였다. --device-id 11, wire의 SessionID를 시뮬레이터 값에 맞추려고
그랬다. 그냥 돌려도 된다. 이 listener는 그 자리를 검사하지 않는다.
10단계가 시작되기 전, Select
H→E 00 00 00 0A 00 0B 00 00 00 01 00 00 00 01
E→H 00 00 00 0A 00 0B 00 00 00 02 00 00 00 01
Length 00 00 00 0A, SessionID 00 0B(11), Header Byte 2-3 00 00, PType 00, SType는 나갈 때
01 돌아올 때 02, SystemBytes 00 00 00 01이 그대로 돌아온다. Select.rsp에서 Byte 3은 Select
Status고 00이 수락이다(SEMI E37). 이 줄이 있기 전에는 1단계 자체가 없다.
1단계 — S1F13 → S1F14, COMMACK 0
H→E 00 00 00 1C 00 0B 81 0D 00 00 00 00 00 02
01 02 41 07 47 45 4D 48 4F 53 54 41 05 30 2E 31 2E 30
E→H 00 00 00 37 00 0B 01 0E 00 00 00 00 00 02
01 02 21 01 00 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
Byte 2 81은 stream 1에 W-bit가 선 것, Byte 3 0D는 function 13. Body는
L[2]{A "GEMHOST", A "0.1.0"} — host 자기 MDLN과 SOFTREV다. 답은
L[2]{B 0, L[2]{A "VX-9000 Plasma Etcher", A "SECSGEM-1.4.1"}}이고, 21 01 00이 COMMACK 0이다.
저 문자열 두 개가 표 Note 열에 찍힌 것과 바이트 단위로 같다.
2단계 — S1F17 → S1F18, ONLACK 0
H→E 00 00 00 0A 00 0B 81 11 00 00 00 00 00 03
E→H 00 00 00 0D 00 0B 01 12 00 00 00 00 00 03 21 01 00
요청은 body가 아예 없는 14바이트 header다. Byte 3 11이 17. 답에는 body 3바이트, 21 01 00이
붙어 오는데 S1F14와 달리 list가 아니라 맨 binary item이다. 모든 응답을 L[2]로 벗기는 parser는
PASS로 찍힌 이 단계에서 깨진다.
3단계 — 길이 0짜리 list로 보낸 S1F3
H→E 00 00 00 0C 00 0B 81 03 00 00 00 00 00 04 01 00
Body 전체가 01 00, 길이 0인 L item이다. SEMI E5는 이걸 "SV 전부"로 읽고, 실제로 돌아온 S1F4에
값 4개가 들어 있다(A "Idle", 8바이트 실수 둘, A "ETCH_BASE"). 표의 4 values가 이거다.
6단계 — 아무도 안 보는 delete-all, 그다음 DRACK 0
H→E 00 00 00 14 00 0B 82 21 00 00 00 00 00 07 01 02 B1 04 00 00 00 01 01 00
E→H 00 00 00 0D 00 0B 02 22 00 00 00 00 00 07 21 01 00
L[2]{U4 1, L[0]} — DATAID 1과 빈 report list, 설비의 report 정의를 전부 지우는 message다. B1은
format code 44(U4)에 length byte 1개, 01 00이 빈 list. DRACK 0으로 답이 왔고 이 transaction은
표에 아예 안 나온다.
정의 본체는 SystemBytes 00 00 00 08에 136바이트 S2F33으로 뒤따라 나간다. 싣고 간 VID는
00 00 27 11부터 00 00 27 1B, 10001~10011이다. 답:
E→H 00 00 00 0D 00 0B 02 22 00 00 00 00 00 08 21 01 00
또 DRACK 0. 4단계에서 이 설비가 한 번도 내놓은 적 없는 VID 11개에 대해서다. 이 한 바이트가 다음 절의 주제다.
9단계 — ALED와 길이 0짜리 ALID list
H→E 00 00 00 11 00 0B 85 03 00 00 00 00 00 0B 01 02 21 01 80 B1 00
L[2]{B 0x80, U4[]}. 21 01 80은 최상위 bit를 세운 ALED다. E5 번호로 bit 8, enable bit.
B1 00은 길이 0인 U4 item, 즉 ALID가 하나도 없다는 뜻이고 E5는 그걸 "alarm 전부"로 읽는다.
B1 04 00 00 00 00이었으면 ALID 0이고, 전혀 다른 요청이 된다.
10단계 — 16자리 timestamp
H→E 00 00 00 1C 00 0B 82 1F 00 00 00 00 00 0C
41 10 32 30 32 36 30 39 30 39 30 36 30 33 31 39 39 31
41 10은 길이 16인 ASCII item, 내용은 2026090906031991 — YYYYMMDDhhmmsscc다. E5의 TIME에는
12자리 형식도 있다. 둘 중 하나만 받는 설비는 실제로 있는 실패 모드인데, 이 실행이 확인한 건 16자리
쪽뿐이다.
정리
H→E 00 00 00 0A 00 0B 00 00 00 09 00 00 00 0D
SType 9, Separate.req, 응답 없음. Communication state는 NOT COMMUNICATING으로 떨어지고 socket이 닫힌다. Control state는 안 움직인다 — 2단계에서 올려놓은 ON-LINE REMOTE 그대로다. 같은 명령을 연달아 두 번 돌리는 게 같은 시험 두 번이 아닌 이유가 이거다.
Select가 거절되면 표 자체가 없다
시뮬레이터의 selectRefuse fault를 3으로 두고 똑같은 명령을 돌렸다.
H→E 00 00 00 0A 00 0B 00 00 00 01 00 00 00 01
E→H 00 00 00 0A 00 0B 00 03 00 02 00 00 00 01
connect failed: Select.rsp status 3
$ echo $?
1
정상 실행에서 00이던 Byte 3이 03이다. SEMI E37은 3을 Connect Exhaust로 이름 붙인다. 나머지는
멀쩡하다 — 길이도 SessionID도 같고 SystemBytes도 그대로 돌아왔다. exit code는 여전히 1이라 CI는
실패로 잡는다. 다만 표는 한 줄도 안 찍힌다. 10개 row는 session이 SELECTED에 한 번은 들어간
뒤에야 생긴다. 표를 파싱하는 코드가 뒤에 있다면 빈 경우를 처리해 둬야 한다. Select Status 값이 각각
뭘 뜻하는지는 수락된 Select와 거절된 Select에 있다.
10단계가 각각 증명하는 것
| 단계 | PASS가 증명하는 것 | FAIL이면 현장에서 대개 |
|---|---|---|
| 1. S1F13 → S1F14 | COMMACK 0이 왔고 MDLN·SOFTREV 두 자리가 채워져 있다. 여기부터 SECS-II 대화가 성립한다 | COMMACK이 0이 아니면 설비가 아직 host를 받을 상태가 아니다. 응답 자체가 없으면 SECS 문제가 아니라 HSMS 문제 |
| 2. S1F17 → S1F18 | 설비가 remote control을 받아들였다 | 설비 panel이 LOCAL이거나 operator가 online을 안 눌렀다. FAT에서 제일 김빠지는 실패 |
| 3. S1F3 → S1F4 | 값을 실제로 읽어 온다. 빈 SVID list는 SEMI E5에서 "전부"로 읽힌다 | body가 list가 아니면 이 설비는 S1F3을 구현하지 않았거나 S9F5로 답한다 |
| 4. S1F11 → S1F12 | SVID namelist가 비어 있지 않다. 6~8단계의 판단 근거가 여기서 나온다 | 목록을 안 내놓는 설비다. 이후 report 정의를 interface manual만 믿고 해야 한다 |
| 5. S2F29 → S2F30 | Equipment constant 목록을 읽을 수 있다 | EC를 host에서 관리할 계획이었다면 여기서 이미 틀어진 것 |
| 6. S2F33 → S2F34 | DRACK 0. Report 정의가 설비에 실제로 들어갔다 | DRACK 4 — 요청한 VID 중 존재하지 않는 게 있다. 하나만 틀려도 message 전체가 거절된다 |
| 7. S2F35 → S2F36 | LRACK 0. RPTID가 CEID에 붙었다 | SEMI E5에서 LRACK 4는 CEID가 없다, 5는 RPTID가 없다, 3은 그 CEID의 link가 이미 정의돼 있다 — 지우지 않고 덧붙였다는 뜻 |
| 8. S2F37 → S2F38 | ERACK 0. Event가 켜졌다. 빈 CEID list에 CEED TRUE면 전부 enable | ERACK 1, 지정한 CEID가 없다. 이 단계를 빼먹으면 정의는 다 됐는데 S6F11이 한 건도 안 온다 |
| 9. S5F3 → S5F4 | ACKC5 0. Alarm 보고가 켜졌다. ALED 최상위 bit(0x80)를 세우고 길이 0짜리 ALID item을 넣으면 모든 alarm이다(ALID 0이 아니다) | Alarm만 조용한 host는 대개 이 message를 아예 안 보낸 host다 |
| 10. S2F31 → S2F32 | TIACK 0. 설비가 host 시각을 받았다. Tool은 YYYYMMDDhhmmsscc 16자리로 보낸다 | 설비가 시각 설정을 거부한다. Event timestamp를 host 수신 시각으로 대체할지 지금 정해야 한다 |
2단계에는 예외가 하나 있다. Tool은 ONLACK 0뿐 아니라 2도 PASS로 친다. 이미 online인 설비를 다시 online 시켰다고 실패로 볼 이유가 없어서다. SEMI E5는 ONLACK 2를 "설비가 이미 on-line"으로 정의하지만, 이 실행에서 2는 한 번도 안 나왔다. 시뮬레이터는 S1F17에 늘 21 01 00으로만 답한다. 즉 tool의 2 분기는 여기서 실행된 적 없는 코드다.
6단계에는 표에 안 보이는 동작이 하나 더 있다. 정의를 보내기 전에 DATAID와 빈 report list만 담은 S2F33을 한 번 먼저 보낸다. 설비에 남아 있는 report 정의와 event link를 전부 지우는 message다. 이전 통합 업체가 남긴 설정 위에 덧붙이는 사고를 막아 준다. 이 delete-all의 ack는 참고로만 적히고 판정에는 안 쓴다. DRACK·LRACK·ERACK 코드 표 전체는 collection event를 define·link·enable하는 순서 쪽에 있다.
10/10인데 flag가 붙는 이유
표 아래 한 줄이 이 실행에서 제일 값어치 있는 출력이다.
flag: model VIDs not in equipment SVID list: 10001, 10002, ... 10011
6단계 직전에 tool은 4단계에서 받아 둔 S1F12 namelist와 자기 model의 VID를 대조한다. 시뮬레이터가 내놓은 SVID는 4개, tool의 기본 model이 report에 쓰는 VID는 10001~10011. 하나도 안 겹친다.
그런데 DRACK은 0으로 돌아왔다. 시뮬레이터가 모르는 VID를 그냥 받아 준 것이다. 점수는 10/10이고 flag만 남는다.
실제 설비는 여기서 DRACK 4를 준다. 그리고 그게 startup에서 사람 시간을 제일 많이 먹는 실패다. 잘못된 VID 하나 때문에 S2F33 전체가 거절되는데, DRACK을 확인하지 않는 host driver는 "report setup 완료"를 찍고 넘어간다. 설정은 host에만 있고 설비에는 없는 상태로 몇 주가 간다.
그러니 이 flag는 무시하는 게 아니라 처리하는 것이다. 설비 interface manual의 SVID·CEID·RPTID를 JSON 하나에 적고 --model로 넘긴다.
npx secs-gem-host connect <host>:<port> --report --model ./vx9000.json
이렇게 하면 4단계가 읽어 온 실제 SVID 목록과 내가 쓰겠다고 적어 둔 VID가 같은 실행 안에서 대조된다. FAT 전에 이 flag를 없애 두는 쪽이, FAT 당일에 DRACK 4를 처음 보는 것보다 싸다.
이 실행이 증명하지 못하는 것
선을 그어 두는 편이 낫다.
- 설비가 맞다는 증명이 아니다. 시뮬레이터가 모르는 VID에 DRACK 0을 준 게 그 증거다. 이 표가 증명하는 건 host driver가 10개 transaction을 규격대로 만들고, 응답을 파싱하고, ack code를 실제로 확인한다는 데까지다.
- Timing은 재현이 안 된다. 1 ms는 loopback 숫자다. T3 45초짜리 설비에 붙었을 때 MES의 DB write가 그 안에 끝나는지는 별개 문제다.
- Recovery 중 RPTID 재매핑, spooling, S6F11의 실제 값 — 전부 설비만 할 수 있는 일이다.
- SELECTED에 들어가기 전에 실패한 건 startup 결과가 아니다. 위의 Select 거절 실행은 한 줄만 찍고 row는 하나도 안 남겼다. Select가 되고 나서 설비가 조용해지는 건 다른 문제다 — selected인데 communicating이 아닌 상태부터 봐야 한다.
3주 동안 할 순서
- 시뮬레이터 상대로 그냥 한 번 돌린다. 10/10이 안 나오면 그건 지금 host driver 버그다.
- 설비 interface manual에서 SVID·CEID·RPTID를 뽑아 model JSON을 만든다.
--model로 다시 돌려 flag가 사라지는지 본다.- 그 명령을 CI에 건다. exit code가 이미 1/0으로 나오니 따로 짤 게 없다.
- FAT 당일에는
<host>:<port>만 실제 설비로 바꿔 같은 명령을 돌린다. 표 한 장이 그날 회의록이 된다.
Tool은 npm에 secs-gem-host로 올라가 있고, 소스는 github.com/jungyoseok/secs-gem-host, MIT다. secs-gem-host mcp로 띄우면 같은 엔진이 MCP server가 되므로, AI agent에게 "이 설비에 붙어서 startup 돌려 보고 실패한 단계를 설명해"를 그대로 시킬 수 있다.
붙을 상대가 필요하면 SECS/GEM 시뮬레이터가 지금 떠 있다.
npx secs-gem-host connect <host>:<port> --report