안녕하세요.
저번시간에 이론으로 배운 이더넷 통신을 이번 시간에는 직접 코드를 통해 구현해 보고 LS산전 PLC의 프로토콜에 대해서 알아보는 시간을 가져보도록 하겠습니다.
이더넷 프로토콜
LS산전 홈페이지에 있는 이더넷 통신 설명서 중 일부를 발췌하여 작성하였습니다.
만약 직접 구현을 하거나 만드실때 제 코드를 따라서 해봐도 좋지만 PLC의 모델이 다르기 때문에 직접 설명서를 다운로드하여서 보시는 것을 추천드립니다. (무료입니다)
1. 프로토콜 개요 및 프레임 구조

여기서 눈여겨 봐야할 부분은 크게 4가지입니다.
- XGT 전용 통신은 TCP와 UDP 두 통신 방식으로 사용이 가능하다.
- XGT 전용 프로토콜의 개념과 기능
- XGT 전용 프로토콜의 TCP/IP 통신방식은 2004번 포트를 사용한다.
- 프레임 구조
2. XGT 전용 프로토콜의 헤더 구조

1번에서 봤던 프레임 구조안에 있는 헤더의 구조에 대한 부분입니다.
이 부분은 다 쓰이기 때문에 전부 다 잘 봐두셔야 하고 PLC와 슬롯의 위치에 따라 숫자가 바뀌기 때문에 잘 보고 작성하셔야 합니다.
3. 명령어

이 부분은 헤더 다음에 오는 command부분입니다.
명령어에는 크게 읽기 쓰기 두 가지로 나뉘며 그 안에서도 요구와 응답 2가지로 나뉩니다.
이 부분은 각자 필요한 기능들을 골라 사용하시면 됩니다.
단. 데이터 타입을 잘 보시고 작성하시기 바랍니다.
4. 헤더 및 데이터 구조

헤더, 명령어, 데이터 타입, 데이터 전부 다 보여주는 부분입니다.
위의 내용과 비슷하여 따로 설명할 부분은 없어 보이며 데이터 부분은 각자 사용하는 부분에 맞게 사용하시면 됩니다.
5. 변수 개별 읽기, 연속 읽기 요청 및 응답 프레임


자 드디어 예시가 나왔습니다.
아무리 제가 설명을 잘해도 한번 예시를 보시는 게 엄청 도움이 됩니다.
역시 그대로 따라 쓰시면 안 되고 본인의 PLC와 CPU, 명령어 등등 기호에 맞게 사용하셔야 합니다.
구현
자 그럼 파이썬을 켜서 한번 코드로 구현을 해 봅시다.
우리는 소켓통신 중 UDP가 아닌 TCP/IP를 통해 통신이 잘 되는지 응답을 확인을 해야 하기 때문에 TCP/IP 방식을 사용해서 구현을 할 것입니다.
응답과 요청, 소켓 통신을 할 때 클라이언트와 서버가 있겠죠?
그렇다면 PC와 PLC의 관계는 클라이언트와 서버일까요? 아니면 서버와 클라이언트일까요?
정답은 PC가 클라이언트 PLC가 서버가 됩니다.
그래서 PLC서버에 파이썬으로 요청을 해야 합니다.
우선 PLC 쪽에서 읽어올 데이터를 저장을 해 봅시다.

XG5000에서 LD(레더)로 짜셔도 되지만 저는 간단하게 변수 하나만 넣을 거라 ST로 짰습니다.
다른 것을 보실 필요 없고 D00000 := 9;라는 부분만 보시면 됩니다.
D영역 00000에 9라는 값을 집어넣어두고 PC(파이썬)에서 어떻게 요청을 보내야 PLC와 통신을 해서 저 값을 읽어올 수 있는지 또는 쓸 수 있는지 알아보겠습니다.
사실 소켓(TCP/IP) 통신에서 서버와 클라이언트의 역할 수행이 약간 다릅니다.

서버와 클라이언트 간의 흐름도입니다.
저희는 PLC가 알아서 해주기 때문에 PC(파이썬) 쪽만 신경 써서 만드시면 됩니다.
송신 코드 분석
간단하게 작성한 클라이언트(PC 파이썬) 코드입니다.
원래는 통신이 안될 때 에러를 대비하여 코드들을 더 작성을 해야 하지만 생략하고 간단한 부분만 작성하였습니다.
import socket
TCP_IP = '192.xxx.x.xxx' # PLC의 ip
TCP_PORT = 2004 # PLC의 포트번호 TCP는 2004 UDP는 2005
BUFFER_SIZE = 1024
message = (b'LSIS-XGT\n\n\n\n\xA0\x33\x00\x00\x12\x00\x02\x00\x54\x00\x02\x00\00\00\x01\x00\x08\x00%DW00000')
# 소켓 오픈
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT)) # 소켓 연결
s.send(message) # 메세지를 보낸다.
data = s.recv(BUFFER_SIZE) # 메세지를 받는다
s.close() # 소켓 닫기
print("received data: ", data.hex()) # 받은 메세지를 hex형태로 출력
주석이 달려 있지만 한 줄씩 알아봅시다.
1. import socket은 socket모듈을 사용하겠다고 선언하는 부분입니다.
2. TCP_IP = '192.xxx.x.xxx'이 부분은 PLC의 IP 주소를 입력해 주시면 됩니다.
(XG5000에서 이더넷검색을 통해 알 수 있습니다)
3. TCP_PORT = 2004 이 부분은 위의 설명서에도 나와있습니다. TCP는 2004 UDP는 2005 저희는 2004를 사용합니다.
4. BUFFER_SIZE = 1024 버퍼란 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역입니다. 그 메모리의 사이즈를 미리 할당해 놓은 겁니다.
5. message = (b'LSIS-XGT\n\n\n\n\xA0\x33\x00\x00\x12\x00\x02\x00\x54\x00\x02\x00\00\00\x01\x00\x08\x00% DW00000')
※ 이 부분이 오늘의 핵심 내용입니다. ※
아무도 이 부분에 대해서 자세하게 설명한 곳이 없어 제가 직접 이 코드 한 줄 때문에 며칠간 시행착오 끝에 알아냈습니다!!
시리얼 때는 모니터라도 있었지만 이더넷은...
그럼 한번 자세히 알아보겠습니다.
핵심내용
구조
|
항목
|
크기(byte)
|
내용
|
Company Header
|
Company ID
|
10
|
LSIS-XGT\n\n
|
PLC Info
|
2
|
\n\n
|
|
CPU Info
|
1
|
\xA0
|
|
Source of Frame
|
1
|
\x33
|
|
Invoke ID
|
2
|
\x00\x00
|
|
Length
|
2
|
\x12\x00
|
|
Position
|
1
|
\x02
|
|
Check Sum
|
1
|
\x00
|
|
Command
|
Command
|
2
|
\x54\x00
|
Data Type
|
Data Type
|
2
|
\x02\x00
|
Data
|
Reserved
|
2
|
\x00\x00
|
Block No.
|
2
|
\x01\x00
|
|
Variable Length
|
2
|
\x08\x00
|
|
Data Address
|
8
|
%DW00000
|
위에 나왔던 LS사의 설명서의 마지막 예시 부분을 보고 하나하나 쪼개서 작성을 했습니다.
제가 구성한 PLC환경과 같을 수 없기 때문에 반드시 설명서를 차근차근 읽어보시고 제가 분석한 대로 쪼개서 프래임을 구성한 뒤 합치시면 수월하실 겁니다.
시리얼 통신을 한번 해보시거나 제가 쓴 글에 비슷한 구조로 설명이 되어있기 때문에 한번 읽어보시는 게 도움이 되실 겁니다.
저와 다르게 쓰셔야 하는 부분만 설명을 드리겠습니다.
- CPU Info는 plc종류마다 달라서 설명서를 보고 참고해서 작성하시기 바랍니다.
- Length는 헤더다음의 바이트크기를 다 더하시면 됩니다. 저는 다 더하면 18이었는데 이것을 hex로 바꾸면 0012가 되어서 순서를 바꾸어 \x12\x00으로 적었습니다.
- Position은 슬롯의 번호라서 각자 이더넷 모듈을 꽂은 부분을 적으시면 됩니다.
- Data는 읽기라 \x54\x00을 사용했습니다.
- Data Type은 D영역은 워드형식이라 \x02\x00을 사용했습니다.
- Variable Length는 뒤에 Data Address의 크기가 8byte 이므로 \x08\x00
- Data Address는 제가 아까 XG5000에서 설정한 부분을 적은 것입니다.
6. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
소켓을 생성합니다.
socket.AF_INET : 주소종류를 IPv4로 지정해 줍니다.
socket.SOCK_STREAM : 통신종류를 TCP로 지정해 줍니다.
7. s.connect((TCP_IP, TCP_PORT))
지정한 IP와 PORT로 서버에 접속을 합니다.
8. s.send(message)
메시지를 전송합니다.
9. data = s.recv(BUFFER_SIZE)
메시지를 서버로부터 받습니다.
10. s.close()
소켓을 닫습니다.
11. print("received data: ", data.hex())
받은 메세지를 hex형태로 출력합니다.
hex 형태로 출력을 하는 이유
통신을 할 때 hex로 출력을 하지 않으면 이상한 문자가 나오게 됩니다.
한번 직접 hex를 빼고 해 보시기 바랍니다
수신(응답) 분석

위의 복잡한 절차를 통해 코드를 실행시키면 PLC로부터 답장이 옵니다!!
자, 그럼 답장도 한번 분석을 해 봅시다.
구조
|
항목
|
크기(byte)
|
내용
|
Company Header
|
Company ID
|
10
|
4c5349532d5847540a0a
|
PLC Info
|
2
|
0301
|
|
CPU Info
|
1
|
a0
|
|
Source of Frame
|
1
|
11
|
|
Invoke ID
|
2
|
0000
|
|
Length
|
2
|
0e00
|
|
Position
|
1
|
02
|
|
Check Sum
|
1
|
34
|
|
Command
|
Command
|
2
|
5500
|
Data Type
|
Data Type
|
2
|
0200
|
Data
|
Reserved
|
2
|
0000
|
Error State
|
2
|
0000
|
|
Variable Length
|
2
|
0100
|
|
Data Count
|
2
|
0200
|
|
Data
|
2
|
0900
|
형식에 맞게 잘 날아온 것을 볼 수 있습니다.
그렇다면 우리가 알고 싶어 했던 D00000에 있는 값이 잘 왔는지 봅시다.
Data에 0900이라고 날아왔습니다.
※여기서 포인트※
TCP/IP내의 모든 프로토콜 계층은 빅 엔디안 방식을 따르기 때문에 항상 최상위 바이트부터 전송하고 받습니다. 하지만 대부분의 컴퓨터 (리틀 엔디안 프로세서)에서 변환을 하면서 순서가 바뀌게 된 겁니다. 그래서 0900으로 나오지만 사실 0009입니다.
0009를 10진수로 변환하면 9입니다.
XG5000에서 D00000에 저장한 값이 9였으니 원하던 값을 TCP/IP 통신으로 읽어왔습니다!!
처음에는 많이 헷갈리고 순서도 뒤죽박죽에 hex까지 변환을 해야 하니 복잡합니다. 하지만 직접 통신을 하면서 ascii표와 함께 참고하니까 충분히 분석할 만했습니다.
이번 시간에는 코드가 어렵다기보다 통신 프로토콜에 맞춰서 프레임을 작성하는 것과 hex 변환, 라디안에 따른 자리 바뀜 등등 난코스가 많았지만 차근차근해보시길 바랍니다.
감사합니다.