S1F3에 SVID를 두 개 실어 보냈다. 돌아온 S1F4에는 값이 네 개 들어 있고, 그중 무엇이 내가 물어본 101인지 알려주는 필드는 응답 어디에도 없다. S1F4 body에는 SVID가 아예 들어가지 않기 때문이다.
그래서 S1F3은 host가 처음 보낼 message가 아니다. 설비가 가진 SVID 번호와 이름이 무엇인지 알아내는 통로는 S1F11/S1F12, Status Variable Namelist Request 하나뿐이다. 값을 읽기 전에 카탈로그부터 받아와야 응답의 값이 해석 가능해진다. 설비 없이 host 코드만 짜는 입장이면 이 두 message의 body 모양을 byte로 한 번은 봐야 한다.
아래 frame은 전부 SECS/GEM 시뮬레이터의 EQ1 passive listener(127.0.0.1:5501, SessionID 11, fault 설정 없음)에 Python socket client를 붙여서 실제로 주고받은 것이다. SECS-II item은 client가 직접 encode/decode 한다.
빈 리스트를 보내면 카탈로그 전체가 온다
S1F11의 body는 SVID 리스트다. 원소가 0개면 SEMI E5 stream 1에서 이건 "가진 status variable을 전부 설명해라"는 뜻이다. 설비에 처음 붙는 host라면 어차피 아는 SVID가 없으니 이 형태로 시작하게 된다.
TX S1F11 W 00 00 00 0C 00 0B 81 0B 00 00 00 00 41 03 01 00
| Bytes | Field | Value |
|---|---|---|
00 00 00 0C | Length | 12 — 헤더 10 + body 2 |
00 0B | SessionID | 11 |
81 | 헤더 바이트 2 | W-bit 1, Stream 1 |
0B | 헤더 바이트 3 | Function 11 |
00 00 | PType, SType | 0, 0 — 데이터 message |
00 00 41 03 | SystemBytes | 0x00004103 |
01 00 | Body | L,0 — 빈 리스트 |
돌아온 것:
RX S1F12 00 00 00 79 00 0B 01 0C 00 00 00 00 41 03
01 04
01 03 B1 04 00 00 00 64 41 12 53 56 31 30 30 5F 50 72 6F 63 65 73 73 53 74 61 74 65 41 00
01 03 B1 04 00 00 00 65 41 11 53 56 31 30 31 5F 54 65 6D 70 65 72 61 74 75 72 65 41 00
01 03 B1 04 00 00 00 66 41 0E 53 56 31 30 32 5F 50 72 65 73 73 75 72 65 41 00
01 03 B1 04 00 00 00 67 41 0C 53 56 31 30 33 5F 52 65 63 69 70 65 41 00
길이는 00 00 00 79, 121이다. 헤더 10에 body 111. 헤더 바이트 2가 01이라 W-bit는 꺼졌고 Stream은 1, 바이트 3 0C가 Function 12다. SystemBytes 41 03은 요청 값 그대로다.
body는 L,4, entry 네 개. entry 하나는 L,3이고 SEMI E5 stream 1이 정한 순서대로 SVID, SVNAME, UNITS가 들어간다. 첫 entry를 byte로 뜯으면 이렇다.
01 03— item header byte01의 상위 6비트가 format code 0(List), 하위 2비트가 "길이 바이트 1개". 이어지는03은 원소 3개다. 리스트의 길이는 byte가 아니라 원소를 센다.B1 04 00 00 00 64—B1은 상위 6비트가 44, 즉 U4. 길이 4바이트, 값 100. 이게 SVID다.41 12 53 56 31 30 30 …—41은 ASCII, 길이12는 18바이트.SV100_ProcessState. SVNAME이다.41 00— 길이 0짜리 ASCII item. UNITS가 빈 문자열이라는 뜻이다.
나머지 세 entry는 SVID 101 SV101_Temperature, 102 SV102_Pressure, 103 SV103_Recipe다. 00 00 00 65가 101, 66이 102, 67이 103.
UNITS가 넷 다 비어 있는 건 이 시뮬레이터의 모델에 단위가 없기 때문이다. 실제 설비는 degC나 Torr 같은 문자열을 여기 싣는다. host 쪽에서 중요한 건 41 00을 "item이 없다"로 처리하면 그 자리에서 파서가 한 칸씩 밀린다는 것. 값 0도 아니고 null도 아니고, 길이가 0인 item이 자리를 차지하고 있다.
모르는 SVID를 섞어도 이 설비는 같은 답을 준다
SVID 101 하나에, 이 설비에 없는 번호 9999를 붙여서 다시 물어본다. 00 00 27 0F가 9999다.
TX S1F11 W 00 00 00 18 00 0B 81 0B 00 00 00 00 41 04 01 02 B1 04 00 00 00 65 B1 04 00 00 27 0F
RX S1F12 00 00 00 79 00 0B 01 0C 00 00 00 00 41 04 01 04 … (앞의 응답과 byte 단위로 동일)
길이가 또 00 00 00 79, 121이다. 두 개를 물었는데 네 개가 왔고, 없는 번호를 거절하는 항목도 없다. S9 message도 오지 않는다. 이 listener는 S1F11의 body를 아예 읽지 않고 가진 변수를 전부 돌려준다 — 이건 시뮬레이터 구현이지 설비 일반에 대한 진술이 아니다. 모르는 SVID를 실제 설비가 어떻게 처리하는지는 벤더마다 다르고, E5가 이런 응답 리스트에서 쓰는 관례는 그 entry를 빼지 않고 남긴 채 SVNAME과 UNITS 자리에 길이 0짜리 item을 넣는 것인데 그게 실제 도구에서 어떻게 나오는지는 확인하지 못했다.
그래서 host 쪽 방어는 응답 내용이 아니라 길이 비교다. 요청한 리스트 길이와 응답 리스트 길이가 다르면 파싱을 멈추고 올린다. 두 개 물어보고 네 개 받은 걸 그냥 앞에서부터 잘라 쓰면 그때부터 온도 자리에 process state가 들어간다.
S1F3/S1F4를 나란히 놓으면 왜 카탈로그가 먼저인지 보인다
같은 SVID 리스트를 그대로 S1F3에 실어 보낸다. 요청 body는 방금 S1F11에 보낸 것과 한 byte도 다르지 않고, 헤더 바이트 3만 0B에서 03으로 바뀐다.
TX S1F3 W 00 00 00 18 00 0B 81 03 00 00 00 00 41 05 01 02 B1 04 00 00 00 65 B1 04 00 00 27 0F
RX S1F4 00 00 00 31 00 0B 01 04 00 00 00 00 41 05
01 04
41 04 49 64 6C 65
81 08 40 37 59 99 99 99 99 9A
81 08 3F F1 2B 02 0C 49 BA 5E
41 09 45 54 43 48 5F 42 41 53 45
길이 00 00 00 31은 49, body 39바이트다. 01 04로 원소 네 개. 41 04 49 64 6C 65는 ASCII Idle. 81은 상위 6비트가 32, F8이다. 길이 8바이트짜리 IEEE 754 배정도로 40 37 59 99 99 99 99 9A가 23.35, 3F F1 2B 02 0C 49 BA 5E가 1.073. 마지막 41 09 45 54 43 48 5F 42 41 53 45는 ETCH_BASE다.
여기 SVID는 한 개도 없다. 위 S1F12를 받아두지 않았다면 23.35가 온도인지 압력인지 message만 보고는 알 방법이 없다. S1F12가 SVID 101에 SV101_Temperature를 붙여줬기 때문에 두 번째 값이 온도라고 읽는 것이고, 그 연결은 순서 하나로만 유지된다.
빈 리스트로 물어도 답은 같다.
TX S1F3 W 00 00 00 0C 00 0B 81 03 00 00 00 00 41 06 01 00
RX S1F4 00 00 00 31 00 0B 01 04 00 00 00 00 41 06 01 04 … (`41 05` 응답과 byte 단위로 동일)
float byte까지 같은 건 두 요청 사이 4밀리초 동안 온도 값이 움직이지 않았기 때문이다. 이 listener가 S1F3의 body도 읽지 않는다는 건 위 41 05 교환에서 이미 드러났다. 캐시가 아니다.
S2F29는 옆 동네다
이름이 비슷해서 자주 섞이는 게 equipment constant 쪽 namelist다. 같은 소켓에서 S2F29를 던져보면 차이가 byte로 보인다.
TX S2F29 W 00 00 00 0C 00 0B 82 1D 00 00 00 00 41 07 01 00
RX S2F30 00 00 00 8B 00 0B 02 1E 00 00 00 00 41 07
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 … 41 00
요청 헤더 바이트 2가 82다. S1F11의 81과 하위 비트가 다르다 — Stream 2다. 그리고 entry가 01 06, 여섯 항목이다. ECID 300, ECNAME, 그리고 최소·최대·기본값 자리에 F8이 셋, 마지막이 UNITS다. S1F12의 entry는 01 03, 세 항목이었다.
이게 두 message를 가르는 기준으로 쓸 만하다. status variable은 설비가 지금 들고 있는 읽기 전용 값이라 범위나 기본값이라는 개념이 없고, equipment constant는 host가 바꿀 수 있는 설정값이라 min/max/default가 따라온다. 그래서 entry가 3항목이면 S1F12, 6항목이면 S2F30이다. S2F29/S2F13 쪽은 host에서 equipment constant 읽기에 따로 정리해뒀다.
host 코드에 넣을 것
- 접속 후 S1F13/S1F14가 끝나면 S1F11부터 보낸다. 카탈로그 없이 보낸 S1F3의 응답은 해석할 수 없는 숫자 뭉치다.
- 요청 리스트 길이와 응답 리스트 길이를 비교한다. 다르면 예외다. 이 설비처럼 요청을 무시하고 전부 돌려주는 구현이 실재한다.
- SVID의 format code를 U4로 박아두지 말 것. 위 capture는
B1, U4지만 E5는 SVID 자리를 정수형 여러 개와 ASCII까지 허용한다. 문자열로 SVID를 매기는 설비에 붙는 날 접속 단계에서 죽는다. - SVNAME을 키로 삼고 SVID를 값으로 캐시하되, S1F14가 돌려주는 software revision과 함께 저장한다.
SECSGEM-1.4.1문자열이 바뀌면 캐시를 버린다.
4번이 실제로 문제되는 순간은 펌웨어 업데이트 다음 날이다. SVID 번호가 재배치되면 host는 아무 에러도 못 보고, 잘못된 값을 정상값으로 계속 기록한다. S1F12를 한 번 읽어 config 파일에 적어두고 다시는 안 읽는 host를 몇 번 봤는데, 그게 틀렸다는 걸 알아낸 경로는 언제나 스크랩이었다.
위 frame은 전부 실제로 소켓을 타고 오갔고, 설비 쪽 RX 로그에 같은 byte가 남아 있다. raw export는 content/demos/s1f11-s1f12-svid-namelist-host-side.json에 있고 이 글의 실행은 SystemBytes 0x4101–0x4108 구간이다(파일에는 다른 실행들도 같이 들어 있다). 같은 바이트를 직접 던져보려면 SECS/GEM 시뮬레이터에 소켓을 붙이면 된다.