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

S6F11이 안 오는데 ack는 전부 0이다 — S2F33/S2F35/S2F37 순서를 byte로 확인하기

정의하지 않은 RPTID를 link하면 설비마다 답이 다르다. S2F33/S2F35/S2F37 순서와 LRACK 5, DRACK 3, ERACK 0을 두 설비의 실제 byte로 확인한다.

SECS/GEMMESSCADA문제 해결체크리스트

MES 쪽 코드는 다 짰다. Report를 정의하고, CEID에 붙이고, enable까지 보냈다. 세 message 모두 응답이 왔고 세 응답 모두 마지막 바이트가 00이었다. 그런데 S6F11이 한 건도 안 온다. 설비는 없고, 붙일 수 있는 건 시뮬레이터뿐이다.

여기서 host 개발자가 제일 많이 하는 착각은 저 세 message가 독립적이라는 것이다. 아니다. S2F35는 이미 정의된 RPTID를 참조하고, S2F37은 이미 link된 CEID를 참조한다. 순서를 뒤집으면 참조 대상이 없는 message가 되고, 그때 무슨 답이 오는지는 — 이게 핵심인데 — 설비 구현마다 다르다.

그래서 설비 두 대에 같은 순서로 같은 frame을 밀어 봤다. 한 대는 뭘 보내든 00을 돌려주고, 다른 한 대는 LRACK 5로 거절한다. 같은 host 코드, 같은 byte, 다른 답이다.

Host가 정의하지 않은 RPTID 700을 S2F35로 link해 LRACK 5를 받고, S2F33으로 정의한 뒤 다시 link하고 enable해서 S6F11을 받는 교환 Host설비 S2F35 — CEID 10001, RPTID 700 S2F36 — LRACK 5 S2F33 — RPTID 700 = VID 1, 2 S2F34 — DRACK 0 S2F35 — CEID 10001, RPTID 700 S2F36 — LRACK 0 S2F37 — CEED TRUE, CEID 10001 S2F38 — ERACK 0 S6F11 — RPTID 700 S6F12 정의 안 된RPTID

세 message가 참조로 엮여 있다

Define, link, enable이 무엇을 하는지와 tool restart 후 설정이 날아가는 이유는 event report link 구조를 다룬 글에 이미 정리해 뒀다. 그 글은 host가 보낸 byte를 보여 주고 응답은 못 받은 상태로 끝난다 — 당시 listener에 SECS-II 응답기가 없었다. 이 글은 그 응답 쪽이다. DRACK, LRACK, ERACK에 실제로 무슨 값이 실려 오는지.

참조 관계만 다시 짚으면 이렇다.

  • S2F33은 RPTID 하나에 VID 목록을 묶는다. VID는 설비가 갖고 있어야 한다.
  • S2F35는 CEID 하나에 RPTID 목록을 묶는다. RPTID는 이미 S2F33으로 정의돼 있어야 한다.
  • S2F37은 CEED(TRUE/FALSE)와 CEID 목록을 보낸다. 여기엔 RPTID가 안 나온다.

세 번째 항목이 함정이다. S2F37은 report를 전혀 모른다. CEID만 켜고 끈다. 그래서 link가 없는 CEID를 enable해도 설비가 거절할 이유가 없고, 실제로 거절하지 않는다. ERACK 0을 받은 host는 설정이 끝났다고 믿고, 설비는 붙일 report가 없어서 아무것도 안 보낸다. "ERACK 0 받았는데 S6F11이 안 온다"의 절반은 이 경우다.

관대한 설비: 순서를 뒤집어도 전부 0

먼저 SECS/GEM 시뮬레이터의 EQ1 passive listener(127.0.0.1:5501, SessionID 11, fault 없음)다. Select와 S1F13으로 COMMUNICATING까지 간 뒤 — SELECTED와 COMMUNICATING은 다른 상태다 — 일부러 순서를 뒤집었다. 정의한 적 없는 RPTID 700을 CEID 1101에 붙인다.

TX S2F35 W   00 00 00 24 00 0B 82 23 00 00 00 00 04 03
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 04 4D 01 01 B1 04 00 00 02 BC
RX S2F36     00 00 00 0D 00 0B 02 24 00 00 00 00 04 03  21 01 00

Body는 <L [2] <U4 1> <L [1] <L [2] <U4 1101> <L [1] <U4 700>>>>>다. 00 00 04 4D가 CEID 1101, 00 00 02 BC가 RPTID 700. RPTID 700은 이 세션에서 한 번도 정의된 적이 없다.

답은 21 01 00. Binary item 1바이트, 값 0. LRACK 0, 받아들였다는 뜻이다. 이어서 enable도 통과한다.

TX S2F37 W   00 00 00 17 00 0B 82 25 00 00 00 00 04 04  01 02 25 01 01 01 01 B1 04 00 00 04 4D
RX S2F38     00 00 00 0D 00 0B 02 26 00 00 00 00 04 04  21 01 00

25 01 01이 CEED TRUE다. ERACK 0. 그리고 같은 S2F33을 두 번 연달아 보내도 두 번 다 DRACK 0이 온다.

TX S2F33 W   00 00 00 2A 00 0B 82 21 00 00 00 00 04 06
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 02 BC 01 02 B1 04 00 00 00 64 B1 04 00 00 00 65
RX S2F34     00 00 00 0D 00 0B 02 22 00 00 00 00 04 06  21 01 00

이 listener는 S2F33/S2F35/S2F37에 조건 없이 21 01 00을 돌려준다. 설비 흉내가 아니라 ack 형식만 맞춰 주는 것이고, 그게 host 개발자에게는 최악의 상대다. 통합 시험을 여기서만 돌리면 순서 버그가 통째로 안 보인다. 그러다 실제 설비에서 처음 터진다.

엄격한 설비: LRACK 5, DRACK 3, LRACK 4

두 번째 상대는 같은 저장소의 secs-gem-equipment 0.1.0 패키지다. 127.0.0.1:5599에 listener로 띄웠고 SessionID는 0이다. CEID는 10001(START)과 10002(STOP), VID는 1(CONTROL_STATE), 2(PROCESS_STATE), 3(PROCESS_EVENT) 세 개만 있다. 같은 순서 뒤집기부터.

TX S2F35 W   00 00 00 24 00 00 82 23 00 00 00 00 04 03
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 27 11 01 01 B1 04 00 00 02 BC
RX S2F36     00 00 00 0D 00 00 02 24 00 00 00 00 04 03  21 01 05

00 00 27 11이 CEID 10001, 00 00 02 BC가 다시 RPTID 700이다. 답이 21 01 05LRACK 5. 이 구현에서 5는 "link에 등장한 RPTID 중 정의되지 않은 것이 있다"는 뜻이다. 같은 host frame, 아까와 다른 답.

그다음이 이 글에서 제일 중요한 한 줄이다. link가 실패한 CEID를 그대로 enable하면,

TX S2F37 W   00 00 00 17 00 00 82 25 00 00 00 00 04 04  01 02 25 01 01 01 01 B1 04 00 00 27 11
RX S2F38     00 00 00 0D 00 00 02 26 00 00 00 00 04 04  21 01 00

ERACK 0이다. 엄격한 쪽 설비도 여기서는 거절하지 않는다. S2F37에 RPTID가 없으니 거절할 근거가 없기 때문이다. Host log에는 "enable OK"가 남고 S6F11은 영원히 안 온다.

이제 순서대로. RPTID 700을 VID 1, 2로 정의한다.

TX S2F33 W   00 00 00 2A 00 00 82 21 00 00 00 00 04 05
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 02 BC 01 02 B1 04 00 00 00 01 B1 04 00 00 00 02
RX S2F34     00 00 00 0D 00 00 02 22 00 00 00 00 04 05  21 01 00

DRACK 0. 같은 message를 한 번 더 보내면,

RX S2F34     00 00 00 0D 00 00 02 22 00 00 00 00 04 06  21 01 03

DRACK 3. 이미 정의된 RPTID다. 없는 VID 99로 다른 report를 정의하면 다른 값이 온다.

TX S2F33 W   00 00 00 24 00 00 82 21 00 00 00 00 04 07
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 02 BD 01 01 B1 04 00 00 00 63
RX S2F34     00 00 00 0D 00 00 02 22 00 00 00 00 04 07  21 01 04

00 00 00 63 = VID 99, 이 설비에 없는 번호. DRACK 4. RPTID 701(02 BD)은 정의되지 않는다. 여기서 기억할 점: 거절은 message 단위다. report 다섯 개를 한 S2F33에 담고 그중 하나의 VID가 틀리면 다섯 개가 전부 안 생긴다.

정의를 마쳤으니 link가 통과한다.

RX S2F36     00 00 00 0D 00 00 02 24 00 00 00 00 04 08  21 01 00

LRACK 0. 같은 link를 다시 보내면 21 01 03LRACK 3, 이미 link된 CEID다. 존재하지 않는 CEID 9999(00 00 27 0F)로 link하면 21 01 04LRACK 4다.

값 하나에 한 줄로 정리하면 이렇다. 아래는 이 설비 코드가 부여한 의미이고, SEMI E5 본문의 DRACK/LRACK/ERACK 정의와 자구까지 대조하지는 않았다.

S2F34 DRACKS2F36 LRACK
0수락수락
2item 구조가 기대와 다름item 구조가 기대와 다름
3이미 정의된 RPTID이미 link된 CEID
4없는 VID없는 CEID
5정의 안 된 RPTID

S2F38 ERACK은 이 구현에서 0(수락)과 1(없는 CEID 또는 CEED가 BOOLEAN이 아님) 두 가지만 나온다.

순서대로 하면 S6F11은 이렇게 생겼다

Define → link → enable을 마치고 S1F17로 ON-LINE REMOTE로 올린 뒤 S2F41 START를 보냈다. HCACK 0이 오고 — HCACK 2와 control state 이야기는 따로 있다 — 곧바로 event가 올라온다.

RX S6F11 W   00 00 00 2A 00 00 86 0B 00 00 00 00 00 01
             01 03 B1 04 00 00 00 01
                   B1 04 00 00 27 11
                   01 01 01 02 B1 04 00 00 02 BC 01 02 A5 01 05 A5 01 03
TX S6F12     00 00 00 0D 00 00 06 0C 00 00 00 00 00 01  21 01 00

Body는 <L [3] <U4 1> <U4 10001> <L [1] <L [2] <U4 700> <L [2] <U1 5> <U1 3>>>>>다. DATAID 1, CEID 10001, 그리고 report 하나 — RPTID 700 안에 VID 1과 VID 2의 값이 정의할 때 준 순서 그대로 들어 있다. A5는 U1 item이고 값은 5(CONTROL_STATE = ON-LINE REMOTE)와 3(PROCESS_STATE = EXECUTING)이다. Report body에는 VID 번호가 안 실린다. 순서가 곧 계약이다.

Header에서 하나 더. 86 0B은 W-bit + Stream 6, Function 11이고 SystemBytes는 00 00 00 01이다. Host가 쓰던 00 00 04 xx 대역이 아니다. S6F11은 설비가 새로 여는 primary message라서 설비 자신의 SystemBytes를 쓴다. S6F12는 그 값을 그대로 되돌려 줘야 한다. Host의 transaction table에서 찾으려 들면 못 찾는다.

S2F33 한 번에 link가 통째로 사라진다

한 세션을 새로 열어 이번엔 CEID 10001과 10002 둘 다 RPTID 700에 붙이고 enable한 뒤, START로 S6F11 하나를 정상 수신했다. 그다음 report를 하나 더 추가할 생각으로 S2F33을 보냈다. 새 RPTID 701만 담아서.

TX S2F33 W   00 00 00 24 00 00 82 21 00 00 00 00 05 08
             01 02 B1 04 00 00 00 01
                   01 01 01 02 B1 04 00 00 02 BD 01 01 B1 04 00 00 00 03
RX S2F34     00 00 00 0D 00 00 02 22 00 00 00 00 05 08  21 01 00

DRACK 0. 성공이다. 이어서 STOP을 보냈다.

TX S2F41 W   00 00 00 14 00 00 82 29 00 00 00 00 05 09  01 02 41 04 53 54 4F 50 01 00
RX S2F42     00 00 00 11 00 00 02 2A 00 00 00 00 05 09  01 02 21 01 00 01 00

HCACK 0. 그리고 3초 동안 아무것도 안 왔다. CEID 10002에 대한 S6F11이 없다.

이 설비는 S2F33 하나를 report 정의 전체의 교체로 처리한다. 새 정의에 RPTID 700이 없으니 700이 사라지고, 700을 참조하던 link 두 개가 조용히 같이 지워진다. Ack는 0이고 경고도 없다. 어제까지 오던 report가 오늘 안 오는 전형적인 모양이다.

E5가 S2F33을 교체로 규정하는지 누적으로 규정하는지는 내가 표준 본문에서 확인하지 못했다. 확실한 건 위 capture에서 이 설비가 교체로 동작했다는 것이고, host 쪽 결론은 어느 쪽이든 같다. Report를 추가할 때 부분 message를 보내지 마라. 그 세션에서 원하는 정의 전체를 매번 다시 보내는 편이 안전하다.

Host 쪽 정리

  1. 순서는 S2F33 → S2F35 → S2F37. 앞 단계가 0이 아니면 다음 단계를 보내지 마라.
  2. ack의 마지막 바이트만 보지 말고 값 자체를 log에 남겨라. LRACK 5와 LRACK 4는 원인이 완전히 다르고, "링크 설정 실패" 한 줄로 뭉치면 둘 다 못 고친다.
  3. ERACK 0은 link가 있다는 증거가 아니다. 유일한 증거는 S6F11이 실제로 오는 것이다. Setup 직후 event 하나를 강제로 발생시켜 확인해라.
  4. 정의 전에 S1F11로 VID 목록을 받아 두고, 없는 VID는 아예 S2F33에 넣지 마라. DRACK 4는 message 전체를 날린다.
  5. S6F11의 SystemBytes는 설비 것이다. S6F12에 그대로 실어 보내고, host의 outstanding transaction과 대조하지 마라.
  6. Report body에는 VID 번호가 없다. S2F33에 보낸 순서를 host 쪽에도 저장해 두고, 그 mapping이 없으면 값을 해석하지 마라.
  7. Report를 바꿀 때는 전체를 다시 보낸다. 부분 추가가 되는 설비인지 아닌지는 벤더 문서가 아니라 capture로 확인해라.

위 frame은 전부 socket에 실제로 오간 것이다. 설비 쪽 RX log와 host 쪽 TX log 양쪽에 같은 byte가 남아 있고, 원본 export는 content/demos/s2f35-lrack-report-link-order.json에 넣어 뒀다. 직접 해 보려면 SECS/GEM 시뮬레이터에 socket을 붙이면 된다.

마지막으로 한 가지. 이 글의 ack 값은 두 구현의 것이지 표준의 것이 아니다. 진짜 설비는 세 번째 답을 갖고 있을 수 있고 — 침묵, SxF0, 또는 여기 없는 코드 — 그래서 host 코드는 "아는 값이 아니면 실패로 처리한다"를 기본으로 깔아야 한다. 모르는 ack를 0처럼 다루는 순간, 이 글의 모든 실패가 조용해진다.