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

ECID 하나를 물었는데 값이 두 개 온다 — S2F29와 S2F13으로 equipment constant 읽기

S2F29로 ECID 목록을 받고 S2F13으로 값을 읽는다. 요청과 응답이 위치로만 묶이는 구조와 없는 ECID, S2F0이 오는 두 경우를 실제 byte로 확인한다.

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

MES 화면에 target pressure가 65로 떠 있다. 설비 화면에는 7.5다. Host 코드는 S2F13으로 ECID 301 하나만 물었고, 돌아온 S2F14의 첫 번째 값을 꺼내 썼다. 그 첫 번째 값은 ECID 300의 것이었다.

이게 equipment constant 읽기에서 제일 조용하게 터지는 버그다. S2F14 body에는 ECID가 실려 있지 않다. 값만 온다. 어느 값이 어느 constant인지는 host가 보낸 요청의 순서로만 알 수 있고, 설비가 그 순서를 지켰는지 확인할 수단이 message 안에 없다. 설비 한 대 없이 코드를 짜는 입장이면 이 구조를 byte로 한 번은 봐야 한다.

아래 frame은 전부 SECS/GEM 시뮬레이터의 EQ1 passive listener(127.0.0.1:5501, SessionID 11, fault 없음)에 socket을 붙여서 실제로 주고받은 것이다. Host 쪽은 SECS-II item을 직접 인코딩·디코딩하는 Python socket client다.

Host가 S2F29로 ECID 목록을 받고 S2F13으로 ECID 300 하나를 요청했지만 S2F14에 값 두 개가 돌아온 교환 Host설비 S2F29 — L,0 S2F30 — ECID, ECNAME, ECMIN, ECMAX, ECDEF, UNITS S2F13 — L,1 ECID 300 S2F14 — ECV 65, ECV 7.5 요청 1응답 2

빈 list는 "전부 달라"는 뜻이다

S2F13 body는 ECID의 list다. 항목이 0개면 SEMI E5는 이것을 **"가진 equipment constant를 전부 보내라"**로 읽는다. 그래서 host가 실수로 빈 배열을 넘기면 에러가 아니라 전체 덤프가 온다. 여기서 시작한다.

TX S2F13 W   00 00 00 0C 00 0B 82 0D 00 00 00 00 05 04  01 00
Byte의미
00 00 00 0CLength12 — header 10 + body 2
00 0BSessionID11
82Header byte 2W-bit + Stream 2
0DHeader byte 3Function 13
00 00 00 00 05 04PType, SType, SystemBytes0, 0, 0x0504
01 00BodyL,0 — 빈 list

01이 list item header다. 상위 6 bit가 format code 0(L), 하위 2 bit가 뒤따르는 length byte 수 1개. 그래서 00이 원소 0개를 뜻한다. List에서 length는 byte가 아니라 원소 개수를 센다 — item header byte를 쪼개 본 글에 이 부분만 따로 정리해 뒀다.

답은 이렇게 온다.

RX S2F14     00 00 00 53 00 0B 02 0E 00 00 00 00 05 04
             01 02
               01 02 41 17 45 43 33 30 30 5F 54 61 72 67 65 74 54 65 6D 70 65 72 61 74 75 72 65
                     81 08 40 50 40 00 00 00 00 00
               01 02 41 14 45 43 33 30 31 5F 54 61 72 67 65 74 50 72 65 73 73 75 72 65
                     81 08 40 1E 00 00 00 00 00 00

02 0E는 W-bit 없는 Stream 2 Function 14, SystemBytes는 요청과 같은 00 00 05 04다. 41 17은 ASCII item 23 byte — EC300_TargetTemperature. 81 08은 F8 item 8 byte이고, 40 50 40 00 00 00 00 00은 IEEE 754 배정밀도로 65다. 두 번째 항목은 EC301_TargetPressure, 40 1E 00 00 00 00 00 00 = 7.5.

여기서 이 시뮬레이터는 E5 모양을 따르지 않는다. E5의 S2F14는 ECV만 순서대로 담은 list인데, 이 구현은 L,2 {A ECNAME, F8 ECV} 쌍을 돌려준다. Host 파서 입장에서는 오히려 편한 답이라 위험하다 — 이름이 붙어 오는 데 익숙해진 코드는 ECV만 오는 진짜 설비 앞에서 첫 줄부터 깨진다. 파서를 여기에 맞추지 마라.

값만 와도 해석되게 만드는 건 S2F29다

그래서 순서는 S2F13이 먼저가 아니다. S2F29(Equipment Constant Namelist Request)를 빈 list로 보내면 설비가 자기가 가진 constant 전체를 설명해 준다.

TX S2F29 W   00 00 00 0C 00 0B 82 1D 00 00 00 00 05 03  01 00
RX S2F30     00 00 00 8B 00 0B 02 1E 00 00 00 00 05 03
             01 02
               01 06 B1 04 00 00 01 2C
                     41 17 45 43 33 30 30 5F 54 61 72 67 65 74 54 65 6D 70 65 72 61 74 75 72 65
                     81 08 00 00 00 00 00 00 00 00
                     81 08 40 60 40 00 00 00 00 00
                     81 08 40 50 40 00 00 00 00 00
                     41 00
               01 06 B1 04 00 00 01 2D
                     41 14 45 43 33 30 31 5F 54 61 72 67 65 74 50 72 65 73 73 75 72 65
                     81 08 00 00 00 00 00 00 00 00
                     81 08 40 2E 00 00 00 00 00 00
                     81 08 40 1E 00 00 00 00 00 00
                     41 00

항목 하나가 L,6이고, E5가 정한 순서대로 ECID, ECNAME, ECMIN, ECMAX, ECDEF, UNITS다. B1 04 00 00 01 2C는 U4 item 4 byte, 값 300. ECMIN은 0, ECMAX는 40 60 40 00 앞머리 = 130, ECDEF는 65. 마지막 41 00길이 0짜리 ASCII item이다. UNITS가 빈 문자열이라는 뜻이고, 41 00을 "item이 없다"로 처리하면 그 자리에서 파서가 어긋난다.

ECMAX가 정확히 현재 값의 두 배, ECDEF가 현재 값과 같은 건 시뮬레이터가 모델에 min/max/default를 갖고 있지 않아서 만들어 낸 숫자다. 설비 데이터가 아니니 그 값 자체에 의미를 두지 마라. 의미가 있는 건 자리와 형식이다.

E5는 이 자리들의 타입을 느슨하게 열어 뒀다. ECID는 U1/U2/U4/U8, 대응하는 I 계열, 또는 A로 올 수 있고 ECMIN/ECMAX/ECDEF도 수치형·A·B·BOOLEAN이 모두 허용된다. ECNAME과 UNITS만 항상 A다. 그래서 ECID를 U4로 하드코딩한 host는 여기서는 잘 돌고, ECID를 문자열로 매기는 설비를 만나는 날 connect 단계에서 죽는다.

요청과 응답은 위치로만 묶여 있다

이제 ECID 하나만 물어보자. B1 04 00 00 01 2C = U4 300, list 원소 1개.

TX S2F13 W   00 00 00 12 00 0B 82 0D 00 00 00 00 05 05  01 01 B1 04 00 00 01 2C
RX S2F14     00 00 00 53 00 0B 02 0E 00 00 00 00 05 05  01 02 ... (앞의 전체 덤프와 동일)

Length가 00 00 00 53, 즉 83 byte로 빈 list를 보냈을 때와 한 byte도 다르지 않다. 원소 하나를 요청했는데 두 개가 왔다. 없는 ECID로도 해 봤다. 00 00 03 E7 = 999, 이 설비에 없는 번호다.

TX S2F13 W   00 00 00 12 00 0B 82 0D 00 00 00 00 05 06  01 01 B1 04 00 00 03 E7
RX S2F14     00 00 00 53 00 0B 02 0E 00 00 00 00 05 06  01 02 ... (역시 동일)

거절도, 빈 item도, S9F도 없다. 같은 83 byte다. 이 listener는 S2F13의 body를 아예 읽지 않고 가진 constant를 전부 돌려준다 — 이건 시뮬레이터의 구현이지 설비 일반의 동작이 아니다. 실제 설비가 없는 ECID에 어떻게 답하는지는 벤더마다 갈린다. E5가 이런 응답 list에서 쓰는 관례는 해당 자리에 길이 0인 item을 넣는 것인데, 그게 실제 tool에서 어떻게 나오는지는 확인하지 못했다.

Host 쪽 결론은 어느 쪽이든 똑같다. 응답 list의 길이를 요청 list의 길이와 비교해라. 하나 물었는데 두 개가 왔으면 그건 데이터가 아니라 에러다. 그리고 길이가 맞더라도 길이 0인 item은 "값 없음"이지 0이 아니다. 이 두 검사를 안 걸면 host는 ECID 301 자리에 ECID 300의 값을 넣고 아무 로그도 남기지 않는다. 첫 문단의 65가 그렇게 만들어진다.

S2F0이 답으로 오는 두 가지 경우

읽기는 S2F13, 쓰기는 S2F15다. 두 칸 차이라 오타가 나면 함수 번호는 그럴듯한 값이 되고, Stream 2 안에서 둘 다 유효한 함수라서 프레이밍 단계에서는 아무도 안 잡아 준다. 이 설비에 S2F15를 보내면 이렇게 온다.

TX S2F15 W   00 00 00 1E 00 0B 82 0F 00 00 00 00 05 07
             01 01 01 02 B1 04 00 00 01 2C 81 08 40 51 80 00 00 00 00 00
RX S2F0      00 00 00 0A 00 0B 02 00 00 00 00 00 05 07

Body는 L,1 {L,2 {U4 300, F8 70}}, ECID 300을 70으로 바꾸라는 요청이다. 답은 body 없는 S2F0. Header byte 3이 00, function 0이다. E5에서 SxF0은 transaction abort이고, 여기서는 이 listener가 S2F15를 구현하지 않았다는 뜻이다. EAC가 실린 S2F16이 아니다 — 진짜 설비에서 쓰기 응답과 EAC 코드가 어떻게 생겼는지, 그리고 EAC 0이 왜 "적용됐다"는 뜻이 아닌지는 equipment constant 변경 관리 글에 따로 있다.

S2F0이 오는 경로가 하나 더 있다. 세션을 새로 열어 Select만 하고 바로 S2F13을 보내면,

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
TX S2F13 W     00 00 00 0C 00 0B 82 0D 00 00 00 00 06 02  01 00
RX S2F0        00 00 00 0A 00 0B 02 00 00 00 00 00 06 02

같은 S2F0이다. 이번엔 구현이 없어서가 아니라 이 listener가 S1F13/S1F14가 지나가기 전에는 primary message를 받아 주지 않기 때문이다. E30이 NOT COMMUNICATING이라고 부르는 구간이다. 그 구간에서 S9F 계열이 아니라 S2F0으로 답하는 건 이 시뮬레이터의 선택이고, 실제 설비가 어떻게 답하는지는 확인하지 못했다. S1F13/S1F14를 먼저 통과시키면 같은 byte가 정상적으로 답을 받는다 — E30 startup handshake를 한 번에 확인하는 글이 그 순서를 다룬다.

TX S1F13 W   00 00 00 0C 00 0B 81 0D 00 00 00 00 06 03  01 00
RX S1F14     00 00 00 37 00 0B 01 0E 00 00 00 00 06 03  01 02 21 01 00 ...
TX S2F13 W   00 00 00 0C 00 0B 82 0D 00 00 00 00 06 04  01 00
RX S2F14     00 00 00 53 00 0B 02 0E 00 00 00 00 06 04  01 02 ...

같은 응답 코드가 "그런 message 없음"과 "지금은 때가 아님" 둘 다를 덮는다는 게 host 쪽에는 나쁜 소식이다. S2F0을 받았을 때 재시도가 맞는지 코드 수정이 맞는지 message만 봐서는 못 고른다. 그래서 S2F0은 SystemBytes와 그 시점의 comm state를 같이 로그에 남겨야 구분이 된다.

Host 코드에 실제로 걸어 두는 것

  1. Namelist를 먼저 받고, 그 안에 없는 ECID는 S2F13에 아예 넣지 마라.
  2. 응답 list 길이 ≠ 요청 list 길이면 파싱을 중단하고 예외로 올려라. 이게 가장 싸고 가장 많이 잡는다.
  3. 길이 0인 item은 0이 아니라 "값 없음"이다. 41 00, 81 00 모두 해당된다.
  4. ECID와 ECV의 format code를 하드코딩하지 말고 namelist에서 읽은 것으로 검증해라. U4를 가정한 코드는 ASCII ECID 앞에서 죽는다.
  5. Namelist는 캐시하되 설비 software revision과 함께 캐시해라. S1F14가 돌려준 SECSGEM-1.4.1 같은 문자열이 바뀌면 캐시를 버려라. 펌웨어 업데이트로 ECMIN/ECMAX가 조용히 바뀌면 host의 range check가 설비가 받아 주는 값을 거절하거나, 더 나쁘게는 설비가 잘라 버릴 값을 통과시킨다.

캐시 이야기를 마지막에 두는 이유가 있다. 위의 검사 넷은 배포 첫날에 걸리고, 다섯 번째는 반년 뒤 PM 끝난 다음 날 아침에 걸린다. Namelist를 한 번 받아 설정 파일에 넣어 두고 다시는 안 읽는 host를 여러 번 봤고, 그 파일이 설비와 다르다는 걸 알게 되는 경로는 항상 scrap이었다.

위 frame은 전부 socket에 실제로 오간 것이다. 설비 쪽 RX log에 같은 byte가 남아 있고, 원본 export는 content/demos/s2f13-s2f14-reading-equipment-constants-host-side.json에 넣어 뒀다. 같은 byte를 직접 밀어 보려면 SECS/GEM 시뮬레이터에 socket을 붙이면 된다.