본문 바로가기
Program/PLC

[LS PLC / C#] XGB/XGT FEnet Socket 통신 (3) - Data Read Write

by 냠만 2024. 3. 6.

[XGT Socket Read Write]

지난번에 헤더까지 작성하고 LS PLC를 언급하는 걸 잊어먹고 있었습니다.
다시 이어서 작성하는 LS XGT Socket 통신입니다.
Read Write 헤더까지는 알아봤고 이제 데이터를 쓰고 읽고를 해봅시다.

필자는 집이나 회사에 LS PLC가 없어서 자세한 내용은 캡처하지 못하니 소스를 첨부드리겠습니다.
필요하신 분은 메일 적어주시면 보내드리겠습니다.


[XGT Socket Read]

데이터 읽는 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public static bool XGT_DB_Read_Word(string STR_ADDR, int ADDR_CNT, ref int[] Value)//연속블록바이트읽기
{
STR_ADDR = "%DB" + (Convert.ToInt32(STR_ADDR) * 2).ToString(); // 어드레스 영역대 문자열 변경 필요
int addrCnt = ADDR_CNT;
 
byte[] sendData = new byte[41];
byte[] recvData = new byte[32 + 2 * addrCnt]; //ACK 신호시 헤더(20)+응답(10)+블록수(2)+데이터(2)*n
string retnData = null;
string retnValue = null;
StringBuilder str = new StringBuilder();
 
if (!PingTest(_IP))
{
var connectStateService = ConnectStateService.Instance.GetStateItem("XGT");
connectStateService.IsConnected = false;
return false;
}
 
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(_IP), 2004);
try
{
socket.SendTimeout = 1000;
socket.ReceiveTimeout = 1000;
socket.Connect((EndPoint)ipEndPoint);
}
catch
{
return false;
}
 
if (socket.Connected)
{
//---헤더
sendData[0] = Convert.ToByte('L');//0x4C
sendData[1] = Convert.ToByte('S');//0x53
sendData[2] = Convert.ToByte('I');//0x49
sendData[3] = Convert.ToByte('S');//0x53
sendData[4] = Convert.ToByte('-');//0x2D
sendData[5] = Convert.ToByte('X');//0x58
sendData[6] = Convert.ToByte('G');//0x47
sendData[7] = Convert.ToByte('T');//0x54
sendData[8] = 0x00;//예약(0x0000)
sendData[9] = 0x00;
sendData[10] = 0x00;//PLC_INFO(0x0000)
sendData[11] = 0x00;
sendData[12] = 0x00;//CPU_INFO(0xB0)(로라반PLC CPU Type)
sendData[13] = 0x33;//소스프레임(0x33)
sendData[14] = 0x00;//INVOKE_ID(0x0000)
sendData[15] = 0x00;
sendData[16] = 0x15;//LENGTH(0x0014) Application Instruction의 바이트수(20 byte) <-- 반드시수정(명령~전송종료 바이트수)
sendData[17] = 0x00;
sendData[18] = 0x00;//FENET_POSITION(0x00)
sendData[19] = 0x14;//예약영역(0x00)헤더바이트수
//--명령
sendData[20] = 0x54;//명령(0x54)
sendData[21] = 0x00;
sendData[22] = 0x14;//연속
sendData[23] = 0x00;
sendData[24] = 0x00;//예약(0x0000)
sendData[25] = 0x00;
sendData[26] = 0x01;//읽을변수개수(0x0001)
sendData[27] = 0x00;
sendData[28] = 0x9;//변수길이(0x0008):(%DW00016)
sendData[29] = 0x00;
byte[] plcAddr = Encoding.UTF8.GetBytes(STR_ADDR);//PLC읽기시작주소
sendData[30] = plcAddr[0];//(%DB00016)
sendData[31] = plcAddr[1];
sendData[32] = plcAddr[2];
sendData[33] = plcAddr[3];
sendData[34] = plcAddr[4];
sendData[35] = plcAddr[5];
sendData[36] = plcAddr[6];
sendData[37] = plcAddr[7];
sendData[38] = plcAddr[8];
string cntString = string.Format("{0:X4}", 2 * addrCnt);
sendData[39] = Convert.ToByte(cntString.Substring(2, 2), 16);//Hexa String(cntBytes)하위바이트
sendData[40] = Convert.ToByte(cntString.Substring(0, 2), 16);//Hexa String(cntBytes)상위바이트
 
try
{
// Keep-Alive 옵션을 설정하기 위한 TcpKeepAlive 객체 생성
var keepAliveOption = new TcpKeepAlive
{
OnOff = 1, // Keep-Alive 활성화
KeepAliveTime = 2000, // Keep-Alive 시작 시간 (밀리초 단위)
KeepAliveInterval = 100 // Keep-Alive 간격 (밀리초 단위)
};
socket.IOControl(IOControlCode.KeepAliveValues, keepAliveOption.GetBytes(), null);
 
socket.Send(sendData, 0, sendData.Length, SocketFlags.None);
if (socket.Receive(recvData, recvData.Length, SocketFlags.None) > 0)
retnData = BitConverter.ToString(recvData);
}
catch (Exception e)
{
retnData = null;
socket.Close();
}
 
 
if (recvData == null) return false;
// ACK확인
if (!(recvData[20] == 0x55 && recvData[26] == 0x00 && recvData[27] == 0x00)) //ACK,NAK확인
{
retnData = null;
}
 
if (recvData == null) return false;
 
if (retnData != null) // 데이터 읽어오기
{
for (int i = 0; i < addrCnt; ++i)
{
retnValue = Convert.ToInt16((string.Format("{0:X2}", recvData[32 + 2 * i + 1]) + string.Format("{0:X2}", recvData[32 + 2 * i])), 16).ToString();
str.Append(retnValue + ";");
Value[i] = Convert.ToInt32(retnValue);
}
}
else
{
retnData = null;
}
}
socket.Close();
 
if (retnData == null)
return false;
else
return true;
}
cs


데이터를 읽을때는 바이트단위로 읽고 순서대로 가져와도 되는데, 쓸 때는 바이트단위를 뒤집어줘야 워드단위로 정상적으로 데이터가 들어갑니다.

PLC에서 데이터를 받아들이는 방식은 결국 비트단위의 이진수를 자리수별로 읽어와 10진수 16진수로 변경하여 사용되게 됩니다.

 


[XGT Socket Write]

데이터를 쓰는 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
private static string XGT_DB_Write_Word(string ip_address, string STR_ADDR, string STR_DATA)
{
STR_ADDR = "%DB" + (Convert.ToInt32(STR_ADDR) * 2).ToString(); // D영역의 B(Byte)형식 - 워드라서 두배
int addrCnt = ADDR_CNT;//Write Count
 
//데이터 Hex String 변환
string hexData = null;
for (int i = 0; i &lt;= addrCnt; ++i)
{
hexData += string.Format("{0:X4}", int.Parse(TMP_SET_DATA[i]));
}
 
//변환된 Hex String을 Byte[]에 넣기
byte[] PLC_SET_DATA = new byte[2 * addrCnt];
for (int n = 0; n &lt; PLC_SET_DATA.Length; ++n)
{
PLC_SET_DATA[n] = Convert.ToByte(hexData.Substring(n * 2, 2), 16);
}
 
byte[] sendData = new byte[40];
byte[] plcData = new byte[2 * addrCnt];//데이터(2)*n
byte[] recvData = new byte[30]; //ACK 신호시 헤더(20)+응답(8)+블록수(2)
string retnData = null;
TcpClient client = new TcpClient();
NetworkStream stream;
try
{
client.Connect(ip_address, 2004);
stream = client.GetStream();
}
catch
{
return retnData;
}
//--Application Header(20바이트)
sendData[0] = Convert.ToByte('L');
sendData[1] = Convert.ToByte('S');
sendData[2] = Convert.ToByte('I');
sendData[3] = Convert.ToByte('S');
sendData[4] = Convert.ToByte('-');
sendData[5] = Convert.ToByte('X');
sendData[6] = Convert.ToByte('G');
sendData[7] = Convert.ToByte('T');
sendData[8] = 0x00;//예약영역(0x0000) 상하위바이트위치교환(항목 및 데이터값 전체 적용됨)
sendData[9] = 0x00;
sendData[10] = 0x00;//PLC_INFO(0x0000) PC-&gt;PLC전송시(0x0000)
sendData[11] = 0x00;
sendData[12] = 0x00;//CPU_INFO(0x00)
sendData[13] = 0x33;//소스프레임(0x33) PC-&gt;PLC(0x33), PLC-&gt;PC(0x11)
sendData[14] = 0x00;//INVOKE_ID(0x0000) 프레임순서지정
sendData[15] = 0x00;
string hexString = string.Format("{0:X4}", 20 + 2 * addrCnt);
sendData[16] = Convert.ToByte(hexString.Substring(2, 2), 16);//Hexa String(hexString)하위바이트
sendData[17] = Convert.ToByte(hexString.Substring(0, 2), 16);//Hexa String(hexString)상위바이트
sendData[18] = 0x00;//FENET_POSITION(0x00)
sendData[19] = 0x14;//예약영역(0x00), BCC:App Header Byte수(0x14)
sendData[20] = 0x58;//바이트연속쓰기명령(0x0058)
sendData[21] = 0x00;
sendData[22] = 0x14;//데이터타입(0x0000):연속(블록)
sendData[23] = 0x00;
sendData[24] = 0x00;//예약영역(0x0000)
sendData[25] = 0x00;
sendData[26] = 0x01;//변수개수(0x0001):PLC주소개수(연속:0x0001-쓰기시작위치)
sendData[27] = 0x00;
sendData[28] = 0x08;//변수길이(0x0007):PLC주소구성 바이트수
sendData[29] = 0x00;
byte[] plcAddr = Encoding.UTF8.GetBytes(STR_ADDR);
sendData[30] = plcAddr[0];//(%DB00016) PLC 블록시작주소
sendData[31] = plcAddr[1];
sendData[32] = plcAddr[2];
sendData[33] = plcAddr[3];
sendData[34] = plcAddr[4];
sendData[35] = plcAddr[5];
sendData[36] = plcAddr[6];
sendData[37] = plcAddr[7];
string cntString = string.Format("{0:X4}", 2 * addrCnt);
sendData[38] = Convert.ToByte(cntString.Substring(2, 2), 16);//Hexa String(cntBytes)하위바이트
sendData[39] = Convert.ToByte(cntString.Substring(0, 2), 16);//Hexa String(cntBytes)상위바이트
//데이터 상하위 바이트 위치 변경
byte[] PLC_SET_DATA2 = new byte[PLC_SET_DATA.Length];
for (int k = 0; k &lt; addrCnt; ++k)
{
PLC_SET_DATA2[k * 2] = PLC_SET_DATA[k * 2 + 1];//데이터상위바이트 하위바이트 변경
PLC_SET_DATA2[k * 2 + 1] = PLC_SET_DATA[k * 2];
}
//PLC전송프레임, 데이터프레임 바이트 배열 병합(명령+데이터)
byte[] sendData2 = new byte[sendData.Length + PLC_SET_DATA2.Length];//전체바이트정의
System.Buffer.BlockCopy(sendData, 0, sendData2, 0, sendData.Length);
System.Buffer.BlockCopy(PLC_SET_DATA2, 0, sendData2, sendData.Length, PLC_SET_DATA2.Length);
//전송
try
{
if (client.Connected)
{
// Send the message to the connected TcpServer.
stream.Write(sendData2, 0, sendData2.Length);
// Read the first batch of the TcpServer response bytes.
int bytes = stream.Read(recvData, 0, recvData.Length);
// ACK확인
if (recvData[20] == 0x59 &amp;&amp; recvData[26] == 0x00 &amp;&amp; recvData[27] == 0x00) //ACK,NAK확인
{
retnData = BitConverter.ToString(recvData);
}
else
{
retnData = null;
}
// ACK확인 종료
}
else
{
retnData = null;
}
//TCP Connected 종료
}
catch
{
retnData = null;
}
// Close everything.
stream.Close();
client.Close();
return retnData;
}
 
 
private static string XGT_DB_Write_C(string ip_address, int ADDR_CNT2, string STR_ADDR, string STR_DATA)
{
string[] TMP_SET_DATA = STR_DATA.Split(';');
 
int addrCnt = 99;
 
//데이터 Hex String 변환
string hexData = null;
for (int i = 0; i <= addrCnt; ++i)
{
hexData += string.Format("{0:X4}", int.Parse(TMP_SET_DATA[i]));
}
 
//변환된 Hex String을 Byte[]에 넣기
byte[] PLC_SET_DATA = new byte[2 * addrCnt];
for (int n = 0; n < PLC_SET_DATA.Length; ++n)
{
PLC_SET_DATA[n] = Convert.ToByte(hexData.Substring(n * 2, 2), 16);
}
 
byte[] sendData = new byte[40];
byte[] plcData = new byte[2 * addrCnt];//데이터(2)*n
byte[] recvData = new byte[30]; //ACK 신호시 헤더(20)+응답(8)+블록수(2)
string retnData = null;
 
TcpClient client = new TcpClient();
NetworkStream stream;
try
{
client.Connect(ip_address, 2004);
stream = client.GetStream();
}
catch
{
return retnData;
}
 
//--Application Header(20바이트)
sendData[0] = Convert.ToByte('L');
sendData[1] = Convert.ToByte('S');
sendData[2] = Convert.ToByte('I');
sendData[3] = Convert.ToByte('S');
sendData[4] = Convert.ToByte('-');
sendData[5] = Convert.ToByte('X');
sendData[6] = Convert.ToByte('G');
sendData[7] = Convert.ToByte('T');
sendData[8] = 0x00;//예약영역(0x0000) 상하위바이트위치교환(항목 및 데이터값 전체 적용됨)
sendData[9] = 0x00;
sendData[10] = 0x00;//PLC_INFO(0x0000) PC->PLC전송시(0x0000)
sendData[11] = 0x00;
sendData[12] = 0x00;//CPU_INFO(0x00)
sendData[13] = 0x33;//소스프레임(0x33) PC->PLC(0x33), PLC->PC(0x11)
sendData[14] = 0x00;//INVOKE_ID(0x0000) 프레임순서지정
sendData[15] = 0x00;
string hexString = string.Format("{0:X4}", 20 + 2 * addrCnt);
sendData[16] = Convert.ToByte(hexString.Substring(2, 2), 16);//Hexa String(hexString)하위바이트
sendData[17] = Convert.ToByte(hexString.Substring(0, 2), 16);//Hexa String(hexString)상위바이트
sendData[18] = 0x00;//FENET_POSITION(0x00)
sendData[19] = 0x14;//예약영역(0x00), BCC:App Header Byte수(0x14)
sendData[20] = 0x58;//바이트연속쓰기명령(0x0058)
sendData[21] = 0x00;
sendData[22] = 0x14;//데이터타입(0x0000):연속(블록)
sendData[23] = 0x00;
sendData[24] = 0x00;//예약영역(0x0000)
sendData[25] = 0x00;
sendData[26] = 0x01;//변수개수(0x0001):PLC주소개수(연속:0x0001-쓰기시작위치)
sendData[27] = 0x00;
sendData[28] = 0x08;//변수길이(0x0007):PLC주소구성 바이트수
sendData[29] = 0x00;
byte[] plcAddr = Encoding.UTF8.GetBytes(STR_ADDR);
sendData[30] = plcAddr[0];//(%DB00016) PLC 블록시작주소
sendData[31] = plcAddr[1];
sendData[32] = plcAddr[2];
sendData[33] = plcAddr[3];
sendData[34] = plcAddr[4];
sendData[35] = plcAddr[5];
sendData[36] = plcAddr[6];
sendData[37] = plcAddr[7];
string cntString = string.Format("{0:X4}", 2 * addrCnt);
sendData[38] = Convert.ToByte(cntString.Substring(2, 2), 16);//Hexa String(cntBytes)하위바이트
sendData[39] = Convert.ToByte(cntString.Substring(0, 2), 16);//Hexa String(cntBytes)상위바이트
 
//데이터 상하위 바이트 위치 변경
byte[] PLC_SET_DATA2 = new byte[PLC_SET_DATA.Length];
for (int k = 0; k < addrCnt; ++k)
{
PLC_SET_DATA2[k * 2] = PLC_SET_DATA[k * 2 + 1];//데이터상위바이트 하위바이트 변경
PLC_SET_DATA2[k * 2 + 1] = PLC_SET_DATA[k * 2];
}
 
//PLC전송프레임, 데이터프레임 바이트 배열 병합(명령+데이터)
byte[] sendData2 = new byte[sendData.Length + PLC_SET_DATA2.Length];//전체바이트정의
System.Buffer.BlockCopy(sendData, 0, sendData2, 0, sendData.Length);
System.Buffer.BlockCopy(PLC_SET_DATA2, 0, sendData2, sendData.Length, PLC_SET_DATA2.Length);
 
//전송
try
{
if (client.Connected)
{
// Send the message to the connected TcpServer.
stream.Write(sendData2, 0, sendData2.Length);
// Read the first batch of the TcpServer response bytes.
int bytes = stream.Read(recvData, 0, recvData.Length);
 
// ACK확인
if (recvData[20] == 0x59 && recvData[26] == 0x00 && recvData[27] == 0x00) //ACK,NAK확인
{
retnData = BitConverter.ToString(recvData);
}
else
{
retnData = null;
}
// ACK확인 종료
}
else
{
retnData = null;
}
//TCP Connected 종료
}
catch
{
retnData = null;
}
 
// Close everything.
stream.Close();
client.Close();
 
return retnData;
 
}
 
cs

 


데이터를 쓰는 부분에서는 상 하위 바이트 위치를 변경해주는 작업이 필수로 필요합니다.
하여 반전된 데이터를 병합하여 스트림으로 작성하게됩니다.

 

[Ping Test]

커넥션을 수시로 붙였다 끊었다 하기 때문에 Ping 테스트 이후 커넥션이 있다면 예외처리를 하는 것이 좋습니다.

핑은 System.Net.NetworkInformation dll에서 제공하는 클래스로 동기 소켓 토큰을 사용하기 때문에 유용하게 사용할 수 있습니다.

핑테스트는 아래 소스로 진행하시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static bool PingTest(string IPAddress)
{
// Ping's the local machine.
Ping pingSender = new Ping();
//int cTimeout = 1000;
if (pingSender.Send(IPAddress, 500).Status == IPStatus.Success)
{
return true;
}
else
{
return false;
}
}
cs

 

 

[마치며]

위 방식으로 데이터를 작성하면 한번씩 읽고 쓰는 건 문제없이 동작할 것입니다. 다만 읽고 쓰는데 커넥션을 수시로 변경하기 때문에 데이터 로스가 발생할 수 있고, Ack를 정상적으로 처리하지 않는다면 읽어오는데 느려지는 현상이 발생할 수 있습니다.
가급적 비동기로 소켓을 구현하고 데이터는 필요할때만 작성하고 읽어오는 게 좋습니다.
통신을 뚫었다고하여 I/O를 안 쓰게 되면 로스가 발생할 수 있다는 점입니다.