LS XGI / XBC / XBM (FEnet) 드라이버
개요
LS ELECTRIC (구 LS산전) 의 XGI / XBC / XBM PLC 시리즈 와 FEnet (Fast Ethernet) 표준 프로토콜로 통신합니다.
| 항목 | 값 |
|---|---|
opc_type | LS |
| 구현 클래스 | plantpulse.driver.protocol.ls.LSDriver |
| 통신 라이브러리 | 자체 자바 구현 (plantpulse-plc-driver-ls.jar → FEnetClient) |
| read | ✅ |
| write | ❌ (자체 자바 드라이버에 write API 미노출) |
| 보안 | 없음 (사내망 전제) |
이전엔 .NET 으로 작성된 외부 네이티브 바이너리 (FENetClient.exe) 를 외부 프로세스로 띄워 쓰는
NativeProcessDriver 기반이었습니다. 2025 부터 자체 자바 구현
(plantpulse-plc-driver-ls.jar) 으로 전환 되어 socket 직접 통신을 합니다. 결과적으로 OS 의존 / 외부
프로세스 관리 부담이 사라졌습니다.
OPC 등록 폼
| 필드 | 의미 | 예시 |
|---|---|---|
opc_agent_ip | PLC IP | 192.168.0.80 |
opc_agent_port | FEnet 포트 | 2004 (XGT 기본) |
timecycle | 폴링 주기 (ms) | 1000 |
options.company-id | FEnet company ID (선택) | (보통 비움) |
options.use-checksum | BCC 체크섬 사용 | false (default) |
options.use-hex-bit-index | 비트 index 16진 사용 | false (default) |
options.connect-timeout | 연결 타임아웃 (ms) | 3000 |
options.read-timeout | read 타임아웃 (ms) | 3000 |
connect 단계에서 NetUtils.isReachable(ip) ping 검사를 먼저 수행합니다 → 도달 불가 시 즉시 실패.
태그 plc_address 형식
FEnet 표준 (정식 형식)
%<DeviceType><DataType><Index>
| 예시 | 의미 |
|---|---|
%DW00309 | Data 영역, Word, index 309 |
%DD00600 | Data 영역, Double Word (32-bit), index 600 |
%DL00800 | Data 영역, Long Word (64-bit), index 800 |
%MX02704 | Memory 영역, Bit, index 0x2704 (또는 10진 2704) |
%MW00100 | Memory 영역, Word, index 100 |
DeviceType 글자: D (Data), M (Memory), K (Keep), F (Flag), T (Timer), C (Counter), R 등 LS XGI 의 표준 디바이스 코드.
DataType 글자: X (Bit), B (Byte), W (Word, 16-bit), D (Double Word, 32-bit), L (Long
Word, 64-bit).
Edge short form (권장)
Edge 는 short form 을 받아 자동으로 FEnet 표준으로 변환합니다.
| Edge 입력 | data_type | format | 자동 변환 결과 |
|---|---|---|---|
D00309 | Integer | (empty) | %DW00309 |
D00600 | Integer | DW | %DD00600 |
D00800 | Long | (empty) | %DL00800 |
M02704 | Boolean | (empty) | %MX02704 |
D00100 | Float | REAL (또는 default) | %DD00100 |
D00200 | Double | LREAL | %DL00200 |
D00300 | String | STR[10] | %DW00300 부터 10 워드 read |
내부 구현 (LSDriver.toFEnet):
// raw plc_address (D00309) → %D<dataCh>00309
static String toFEnet(String rawAddr, char dataCh) {
if (rawAddr.charAt(0) == '%') return rawAddr; // 이미 표준이면 유지
return "%" + Character.toUpperCase(rawAddr.charAt(0)) + dataCh + rawAddr.substring(1);
}
dataCh 는 data_type 과 format 으로 결정 (다음 절 참고).
data_type / format 매핑 (상세)
LS 드라이버는 같은 short address 로도 format 만으로 메모리 폭을 명시 변경 할 수 있어, 한
디바이스 디바이스 코드를 16/32/64-bit 로 자유롭게 매핑할 수 있습니다.
Integer 계열 (data_type=Integer / Short / Int / Int16 …)
format | 의미 | 폭 | FEnet DataType | 호출 |
|---|---|---|---|---|
| (empty) | Word (signed) | 16-bit | %DW | client.readWord |
UI / UW / UWORD | UWord (unsigned) | 16-bit | %DW | client.readUWord |
DW / DWORD / DOUBLE_WORD | DWord (signed) | 32-bit | %DD | client.readDWord |
UL / UDW / DUW / UDWORD | UDWord (unsigned) | 32-bit | %DD | client.readUDWord |
LW / LWORD / LONG_WORD | LWord (signed) | 64-bit | %DL | client.readLWord |
ULW / ULWORD | ULWord (unsigned) | 64-bit | %DL | client.readULWord |
D00309 + data_type=Integer + format= (비움) 은 16-bit signed Word 입니다.
32-bit 가 필요하면 format=DW 로 명시. 이는 LS XGI 의 일반 컨벤션과 일치합니다.
Float / Double
data_type | format | 폭 | FEnet | 호출 |
|---|---|---|---|---|
Float / REAL | (empty) / REAL / FLOAT | 32-bit IEEE 754 | %DD | client.readFloat |
Double / LREAL | (empty) / LREAL / DOUBLE | 64-bit IEEE 754 | %DL | client.readDouble |
format=DW + data_type=Float 인 경우에도 32-bit float 으로 해석합니다 (isFloat(dt) 분기).
Boolean
data_type | format | 의미 |
|---|---|---|
Boolean / Bool / Bit | (empty) / X / BIT | %MX... 또는 %PX... 비트 단위 read |
short form 그대로: M02704 → %MX02704.
String
format | 의미 | 폭 |
|---|---|---|
STR | 1 워드 (2 글자) | 16-bit × 1 |
STR[N] | N 워드 (2N 글자) | 16-bit × N |
ASCII 인코딩 + low byte first (LS spec). 끝의 NUL 바이트는 trim 처리.
// readStringMulti: N 워드 연속 read, low byte 먼저
for (int i = 0; i < wordCount; i++) {
int w = client.readUWord("%" + dev + "W" + (startIdx + i));
bytes[b++] = (byte) (w & 0xFF);
bytes[b++] = (byte) ((w >> 8) & 0xFF);
}
BIN / BIN[idx]
워드 1개를 읽고 비트 idx 만 추출 (Modbus 와 같은 컨벤션).
format | 동작 |
|---|---|
BIN | 워드 read 후 bit 0 추출 |
BIN[5] | bit 5 추출 |
BIN[F] | hex F (=15) bit 추출 (한 글자는 16진 허용) |
흔한 에러 + 해결
| 메시지 / 증상 | 원인 | 해결 |
|---|---|---|
LS-PLC Ping failed : <ip> | ICMP 비도달 | 케이블/방화벽 확인. ICMP block 환경이면 차단 정책 검토 |
LS connect failed | FEnet 모듈 미가동 / 포트 mismatch | XG5000 에서 FEnet 모듈 IP/Port 확인 (보통 2004) |
| 값이 항상 0 | short form → 폭 mismatch (Word 인데 DWord 로 읽음) | format 비우거나 DW 로 정확히 명시 |
| 32-bit float 깨짐 | format 누락 → Word 로 읽힘 | data_type=Float + format=REAL 명시 |
| 문자열 글자가 깨짐 | LS 의 low-byte-first 미반영 | edge 가 알아서 처리. 외부에서 bytes 직접 해석하면 주의 |
| Bit 가 안 읽힘 | short form 이 D00100 으로 들어감 | M02704 (M 영역) 사용 또는 format=BIN[idx] |
curl 등록 예시
XGI / XBC / XBM 의 D, M 영역 혼합 예:
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_LS_XBM_0001",
"opc_type": "LS",
"opc_name": "XBM Line A",
"opc_agent_ip": "192.168.0.80",
"opc_agent_port": "2004",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"options": {
"connect-timeout": "3000",
"read-timeout": "3000",
"use-checksum": "false"
},
"tag_list": [
{
"tag_id": "OPC_LS_XBM_0001_TAG_00001",
"tag_name": "Counter",
"plc_address": "D00309",
"data_type": "Integer"
},
{
"tag_id": "OPC_LS_XBM_0001_TAG_00002",
"tag_name": "Production32",
"plc_address": "D00600",
"data_type": "Integer",
"format": "DW"
},
{
"tag_id": "OPC_LS_XBM_0001_TAG_00003",
"tag_name": "Pressure",
"plc_address": "D00100",
"data_type": "Float",
"format": "REAL"
},
{
"tag_id": "OPC_LS_XBM_0001_TAG_00004",
"tag_name": "Status",
"plc_address": "M02704",
"data_type": "Boolean"
},
{
"tag_id": "OPC_LS_XBM_0001_TAG_00005",
"tag_name": "BatchName",
"plc_address": "D00500",
"data_type": "String",
"format": "STR[10]"
},
{
"tag_id": "OPC_LS_XBM_0001_TAG_00006",
"tag_name": "Bit3OfStatus",
"plc_address": "D00400",
"data_type": "Integer",
"format": "BIN[3]"
}
]
}'
값 read:
curl -s http://<edge-host>/api/v1/tag/OPC_LS_XBM_0001_TAG_00003/value | jq
동작 메모
- Read 분기 우선순위 (
LSDriver.read()):data_type=String또는format이STR/STR[N]→readStringMultiformat이BIN또는BIN[idx]→ 워드 read 후 비트 추출format명시 분기 (UI,DW,UL,LW,REAL,LREAL등) →readByFormat- 폴백:
data_type단독 (readByDataType)
client호출은 synchronized 되어 있어 한 OPC 의 다중 태그 read 는 직렬. 다른 OPC 는 별 스레드.- write 는 미구현. Sparkplug NCMD 를 사용하려면 후속에서
FEnetClient.write*노출 필요.
예제 모음 (데이터타입별)
LS XGI / XBC / XBM 운영 현장에서 자주 쓰는 모든 조합을 한 표에 정리. plc_address 는 short form 기준이며 자동으로 %D... 표준 표기로 변환됩니다.
비트 / 불 (Boolean)
data_type | format | plc_address | 메모리 폭 | 의미 / 비고 |
|---|---|---|---|---|
Boolean | (empty) | M02704 | 1 bit | 메모리 영역 비트. 가장 흔한 형태 |
Boolean | (empty) | P0001F | 1 bit | LS XGI 16진 비트 인덱스 (F=15). use-hex-bit-index=true 일 때 |
Boolean | (empty) | K00010 | 1 bit | Keep relay 비트 |
Boolean | (empty) | T0110 | 1 bit | Timer 출력 비트 |
Boolean | BIN | D00309 | 16 bit → bit 0 | 워드 read 후 0 비트 추출 |
Boolean | BIN[3] | D00309 | 16 bit → bit 3 | bit 3 추출 |
Boolean | BIN[F] | D00309 | 16 bit → bit 15 | hex 인덱스 F=15 |
16-bit 정수 (Integer/Word)
data_type | format | plc_address | 메모리 폭 | 의미 |
|---|---|---|---|---|
Integer | (empty) | D00309 | 16 bit | signed Word (%DW00309) |
Integer | UI | D00300 | 16 bit | UWord (unsigned, client.readUWord) |
Integer | UWORD | D00300 | 16 bit | UI 와 동일 |
32-bit 정수 (DWord)
data_type | format | plc_address | 메모리 폭 | 의미 |
|---|---|---|---|---|
Integer | DW | D00600 | 32 bit | DWord signed (%DD00600, D00600/00601 결합) |
Integer | DWORD | D00600 | 32 bit | 위와 동일 |
Integer | DUW | D00690 | 32 bit | UDWord (unsigned 32) |
Integer | UDW | D00690 | 32 bit | UDW 와 동일 |
Integer | UL | D00690 | 32 bit | ULong = UDWord (alias) |
64-bit 정수 (LWord)
data_type | format | plc_address | 메모리 폭 | 의미 |
|---|---|---|---|---|
Long | (empty) | D00700 | 64 bit | LWord signed (%DL00700) |
Long | LW | D00700 | 64 bit | 명시 LWord |
Long | LWORD | D00700 | 64 bit | 동일 |
Long | ULW | D00750 | 64 bit | ULWord (unsigned 64) |
실수 (Float / Double)
data_type | format | plc_address | 메모리 폭 | 의미 |
|---|---|---|---|---|
Float | (empty) | D00450 | 32 bit | IEEE 754 single (%DD00450) |
Float | REAL | D00450 | 32 bit | 명시 |
Float | DW | D00450 | 32 bit | isFloat(dt) 분기로 float 처리 |
Double | LREAL | D00500 | 64 bit | IEEE 754 double (%DL00500) |
Double | (empty) | D00500 | 64 bit | LREAL 과 동일 |
문자열 (String)
data_type | format | plc_address | 메모리 폭 | 의미 |
|---|---|---|---|---|
String | STR | D00803 | 16 bit (1 word) | 2 chars (low byte first) |
String | STR[5] | D00800 | 80 bit (5 words) | 10 chars, NUL trim |
String | STR[10] | D00500 | 160 bit (10 words) | 20 chars |
String | STR[16] | D00100 | 32 word | 32 chars (32 byte) — XGI 권장 정렬 |
Formula 활용 (LS 현장 패턴)
| 용도 | data_type | fomula | 입력 → 출력 |
|---|---|---|---|
| 정수 raw → 소수 1자리 | Float | ${VALUE}*0.1 | 1638 → 163.8 |
| 정수 raw → 소수 2자리 | Float | ${VALUE}*0.01 | 12345 → 123.45 |
| signed 16 → unsigned 변환 | Integer | ${VALUE}+65536 | 음수 raw 보정 (16-bit 환산) |
| 영점 보정 (다른 태그) | Float | ${VALUE}-${TAG_ZERO} | 1024 - 24 = 1000 |
| 캘리브 (gain × x + offset) | Float | ${VALUE}*${TAG_GAIN}+${TAG_OFFSET} | 보정 계수 별도 태그 |
| Hz 변환 (rpm → Hz) | Float | ${VALUE}/60 | 1800 → 30.0 |
자세한 fomula 사용은 Formula 레퍼런스 참고.
curl 종합 예제 (모든 타입 한 번에)
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_LS_FULL",
"opc_type": "LS",
"opc_name": "LS XGI Full",
"opc_agent_ip": "192.168.0.80",
"opc_agent_port": "2004",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"options": { "use-hex-bit-index": "true" },
"tag_list": [
{"tag_id":"OPC_LS_FULL_T01", "tag_name":"BitM", "plc_address":"M02704", "data_type":"Boolean"},
{"tag_id":"OPC_LS_FULL_T02", "tag_name":"BitPHex", "plc_address":"P0001F", "data_type":"Boolean"},
{"tag_id":"OPC_LS_FULL_T03", "tag_name":"WordSigned", "plc_address":"D00309", "data_type":"Integer"},
{"tag_id":"OPC_LS_FULL_T04", "tag_name":"WordU", "plc_address":"D00300", "data_type":"Integer", "format":"UI"},
{"tag_id":"OPC_LS_FULL_T05", "tag_name":"DWordS", "plc_address":"D00600", "data_type":"Integer", "format":"DW"},
{"tag_id":"OPC_LS_FULL_T06", "tag_name":"DWordU", "plc_address":"D00690", "data_type":"Integer", "format":"DUW"},
{"tag_id":"OPC_LS_FULL_T07", "tag_name":"LWord", "plc_address":"D00700", "data_type":"Long"},
{"tag_id":"OPC_LS_FULL_T08", "tag_name":"Real", "plc_address":"D00450", "data_type":"Float", "format":"REAL"},
{"tag_id":"OPC_LS_FULL_T09", "tag_name":"LReal", "plc_address":"D00500", "data_type":"Double", "format":"LREAL"},
{"tag_id":"OPC_LS_FULL_T10", "tag_name":"Str1Word", "plc_address":"D00803", "data_type":"String", "format":"STR"},
{"tag_id":"OPC_LS_FULL_T11", "tag_name":"Str10Word", "plc_address":"D00500", "data_type":"String", "format":"STR[10]"},
{"tag_id":"OPC_LS_FULL_T12", "tag_name":"BitOfWord", "plc_address":"D00309", "data_type":"Boolean", "format":"BIN[3]"},
{"tag_id":"OPC_LS_FULL_T13", "tag_name":"PressScale", "plc_address":"D00100", "data_type":"Float", "format":"REAL", "fomula":"${VALUE}*0.1"}
]
}'