본문 바로가기

(개인Project)_개발/PLC-PC 연결

[Program][Phase4] 프로토콜 팩토리 패턴으로 PLC 통신 코드를 어떻게 정리했나

반응형

Phase 3.5가 끝났을 때 남은 한 가지 아쉬움

Phase 3.5를 마무리했을 때, PLCLink는 캔버스 HMI까지 갖춘 꽤 완성된 도구가 됐습니다.

그런데 현장에 나가면 이런 상황이 생깁니다.

"저 쪽 설비는 Omron인데요."

"이 라인은 Modbus TCP로만 연결돼요."

"우리 공장은 OPC-UA 서버 깔려 있어요."

PLCLink는 Mitsubishi와 Keyence만 연결됐어요.

SLMP 프로토콜만 지원했거든요.

특정 현장에서는 쓸 수 없는 툴이었습니다.

Phase 4의 목표는 하나였어요.

어떤 PLC든, 어떤 프로토콜이든 연결되게.


설계 원칙 : 프로토콜과 메이커를 분리한다

가장 먼저 한 결정이 이겁니다.

기존 코드는 "Keyence면 이렇게, Mitsubishi면 저렇게" 방식으로 메이커가 통신 방식을 결정했어요.

새 프로토콜을 추가하려면 메이커 로직 전체를 손봐야 했습니다.

Phase 4에서 이 둘을 완전히 분리했습니다.

protocol → 어떻게 통신하는가
maker    → 주소를 어떻게 표기하는가

예를 들어 이런 조합이 가능합니다.

maker=keyence,    protocol=slmp       → 기존 방식
maker=keyence,    protocol=modbus_tcp → Keyence PLC를 Modbus로 연결
maker=mitsubishi, protocol=fins       → 미쓰비시를 FINS로? (이건 안 되지만 구조상 분리됨)
maker=omron,      protocol=fins       → Omron CJ2M + FINS/TCP

이 구조를 프로토콜 팩토리로 구현했습니다.

def make_client(protocol, host, port, timeout, maker):
    if protocol == "slmp":
        return PLCClient(host, port, timeout, maker)
    elif protocol == "modbus_tcp":
        return ModbusClient(host, port, timeout, maker)
    elif protocol == "opcua":
        return OPCUAClient(host, port, timeout, maker)
    elif protocol == "fins":
        return FINSClient(host, port, timeout, maker)

새 프로토콜이 생기면 클라이언트 하나만 추가하면 됩니다.

기존 코드를 건드리지 않아요.


프로토콜 4가지 : 각각 어떻게 다른가

SLMP (기존)

Mitsubishi와 Keyence 전용입니다.

바이너리 프레임을 직접 조립해서 소켓으로 보냅니다.

Phase 1부터 써온 방식이에요.

특징: 메이커 고유 프로토콜이라 공개 라이브러리가 없고, 3E Frame 구조를 직접 구현해야 합니다.

 

Modbus TCP (신규)

산업 현장에서 가장 보편적인 표준 프로토콜입니다.

PLC 브랜드 상관없이 Modbus 옵션이 있으면 쓸 수 있어요.

pymodbus 라이브러리를 사용했습니다.

함수 코드(FC)로 읽기 종류를 구분합니다.

FC01: Coil (출력 비트) 읽기
FC02: Discrete Input (입력 비트) 읽기
FC03: Holding Register (워드) 읽기
FC04: Input Register (읽기 전용 워드) 읽기

처음에는 "비트면 FC02, 워드면 FC03 쓰면 되겠다"고 생각했는데, 이게 SLMP와 다르게 동작하는 부분이 있어서 한 번 막혔습니다.

(자세한 내용은 보족 P에서)

 

OPC-UA (신규)

스마트 팩토리, MES 연동을 위한 현대적 표준 프로토콜입니다.

세션 기반으로 동작하고, PLC 데이터가 "노드"라는 개념으로 관리됩니다.

asyncua 라이브러리를 사용했습니다.

노드를 찾는 방법이 두 가지 있는데, 실제 서버와 연결하니까 예상대로 안 됐어요.

(자세한 내용은 보족 R에서)

 

FINS/TCP (신규)

Omron PLC 전용 프로토콜입니다.

SLMP처럼 바이너리 프레임을 직접 조립합니다.

공개 Python 라이브러리가 없어서 Omron 공식 스펙 기반으로 처음부터 구현했어요.

TCP 연결 후 20바이트 핸드쉐이크를 먼저 하고, 그다음에 메모리 읽기 명령을 보냅니다.

구조체 포맷 하나가 틀려서 파싱이 전부 어긋나는 버그가 있었어요.

(자세한 내용은 보족 Q에서)


SetupFlow UX 재설계 : 통신 방식을 먼저 선택하게

프로토콜이 4가지가 됐는데, 기존 SetupFlow는 "메이커 선택"이 첫 화면이었습니다.

그런데 Keyence PLC라고 해도 SLMP로 연결할 건지, Modbus로 연결할 건지를 먼저 정해야 합니다.

"Keyence 선택"이 통신 방식까지 결정해버리면 혼란이 생겨요.

기존 흐름:
메이커 선택 (Keyence/Mitsubishi/Modbus 혼재) → IP/포트 → 진단

변경된 흐름:
통신 방식 선택 (SLMP/Modbus TCP/OPC-UA/FINS) → 장비 선택 → IP/포트 → 진단

사용자가 "어떻게 연결할 것인가"를 먼저 고르면, 그에 맞는 장비 목록이 표시됩니다.

포트 기본값도 자동으로 바뀌어요.

SLMP    선택 → 포트 기본값 5007
Modbus  선택 → 포트 기본값 502
OPC-UA  선택 → 포트 기본값 4840
FINS    선택 → 포트 기본값 9600

이 변경으로 "Modbus TCP 선택 → Omron CJ2M 선택 → 포트 502 자동 입력"처럼 흐름이 자연스러워졌습니다.


비트 읽기 : 프로토콜마다 방식이 달랐다

SLMP에서 비트를 읽을 때는 이런 최적화를 써왔습니다.

비트 신호 16개를 한 번의 요청으로 읽으려면, 워드(16비트) 1개를 읽고 프론트엔드에서 각 비트를 추출하면 됩니다.

요청 1번에 16개 비트를 처리할 수 있어요.

그런데 Modbus는 달랐습니다.

Modbus는 비트(Coil, DI)를 직접 읽는 전용 함수 코드(FC01, FC02)가 있습니다.

워드로 읽어서 분해하는 게 아니라 비트 주소로 바로 요청해야 해요.

이걸 프론트엔드에서 분기 처리했습니다.

// SLMP: 비트를 워드로 읽어서 프론트에서 분해 (요청 1개 = 16비트)
if (pt.type === "bit" && isSlmp) {
    return { ...pt, count: Math.ceil(count / 16), type: "word" }
}
// Modbus/FINS/OPC-UA: 비트 그대로 전달 → 백엔드가 FC01/FC02 사용
return { ...pt, count, type: pt.type }

프로토콜을 추가할 때 이런 동작 차이를 파악하지 않으면 Modbus Coil 읽기가 아무 값도 안 오거나 0만 나오는 증상이 생깁니다.


타이밍 차트 : 비트 신호를 계단파로 보면 달라진다

Data List에는 원래 Recharts LineChart로 트렌드 차트가 있었습니다.

숫자 값이 시간에 따라 어떻게 변하는지 보여주는 기능이에요.

그런데 비트 신호(ON/OFF)를 선 그래프로 그리면 이렇게 됩니다.

1
│   /\   /\
│  /  \ /  \
0 /    V    \
──────────────

언제 켜지고 언제 꺼졌는지는 보이는데, "이 신호가 얼마나 켜져 있었나"를 직관적으로 파악하기 어렵습니다.

타이밍 차트는 비트 신호를 계단파(stepAfter)로 그립니다.

1
│▓▓▓▓│   │▓▓▓│
│    │   │   │
0    │▓▓▓│   │▓▓
──────────────────

ON 구간이 막대 형태로 보입니다.

여러 신호를 독립 레인으로 쌓으면

"실린더 A가 전진하고, 그다음 실린더 B가 전진하고, 동시에 센서 C가 들어온다"는

타이밍을 한눈에 볼 수 있어요.

// 비트 신호 (계단파)
<Area dataKey={eid} type="stepAfter"
  stroke={color} fill={`${color}30`}
  strokeWidth={1.5} dot={false} />

// 워드 신호 (선 그래프)
<Line dataKey={eid} type="monotone"
  stroke={color} strokeWidth={1.5} dot={false} />

비트 레인은 48px, 워드 레인은 72px 높이로 독립 표시합니다.

DB의 dl_history에서 읽어서 시간 범위 선택이 가능해요.


OK/NG 패턴 분석 : 불량 원인을 데이터로 찾는다

트리거 스냅샷을 OK/NG로 분류해두면, 각 채널 값을 비교해서 "어떤 채널이 OK와 NG 사이에 차이가 있는가"를 계산할 수 있습니다.

엔지니어가 "이 쪽 신호가 불량이랑 관련 있을 것 같다"는 감으로 찾던 것을 데이터로 확인하는 기능이에요.

유의도 계산 방식입니다.

def significance(ok_vals, ng_vals):
    delta  = abs(ok_평균 - ng_평균)
    spread = max(ok_표준편차, ng_표준편차)
    base   = max(|ok_평균|, |ng_평균|, 1)

    # 평균 차이가 표준편차의 2배 이상 + 5% 이상이면 HIGH
    if delta > spread * 2 and delta / base > 0.05:
        return "HIGH"
    elif delta > spread or delta / base > 0.02:
        return "MED"
    return "LOW"

단, 이 결과는 상관관계입니다.

인과관계가 아니에요.

"이 채널이 HIGH로 나왔다"는 건 OK/NG 사이에 값 차이가 크다는 것이지, 이게 불량의 원인이라는 뜻은 아닙니다.

최소 OK 3개, NG 3개 이상이 있어야 통계적으로 의미가 있고, 최종 판단은 엔지니어가 맥락과 함께 해야 해요.

데이터가 "여기 좀 봐" 하고 가리키는 것이고, 결론은 사람이 내립니다.


데이터 보존 정책 : 무한 누적되면 DB가 버티지 못한다

24시간 내내 폴링하면 데이터가 쌓이는 속도가 생각보다 빠릅니다.

1초 폴링에 채널 10개면 분당 600건, 30일이면 약 2600만 건입니다.

SQLite가 작은 규모에서 잘 동작하지만 이 정도 쌓이면 쿼리 속도가 느려지고 파일 크기도 커집니다.

DataCleaner를 추가했습니다.

서버 시작 30초 후, 그리고 24시간 간격으로 자동으로 오래된 데이터를 삭제합니다.

dl_history        → 30일 초과 삭제
trigger_snapshots → 90일 초과 삭제
alarm_history     → 180일 초과 삭제 (해제된 것만)

보존 기간은 SystemPage에서 변경 가능합니다.

"30일은 너무 짧다"는 현장이 있으면 늘릴 수 있어요.


버전 표시 자동화 : 배포할 때마다 수동으로 바꾸지 않으려고

헤더에 버전 번호를 하드코딩해두면 배포할 때마다 파일을 열어서 바꿔야 합니다.

한 번 잊으면 구버전으로 운영하게 됩니다.

서버 시작 시 git log에서 마지막 커밋 날짜를 읽어서 버전으로 쓰도록 바꿨어요.

date_str = subprocess.check_output(
    ["git", "log", "-1", "--format=%cd", "--date=format:%Y-%m-%d"]
).decode().strip()
# → "2026-05-21"

커밋하면 자동으로 버전이 올라갑니다.

git이 없거나 실패하면 오늘 날짜로 폴백해요.


Phase 4를 마무리하며

Phase 4에서 PLCLink는 Mitsubishi/Keyence 전용 툴에서 어떤 PLC든 연결할 수 있는 범용 PLC 모니터링 툴로 바뀌었습니다.

프로토콜 팩토리 구조로 확장성을 확보했고, 타이밍 차트와 OK/NG 분석으로 "데이터를 보는 것"에서 "데이터로 판단하는 것"으로 기능이 한 단계 올라갔어요.

아직 원형 게이지, 슬라이더 위젯, 멀티 상태 램프 등 캔버스 HMI에 추가하려던 것들이 남아있습니다.

이건 Phase 5 이후에 채워나갈 예정이에요.

Phase 5는 이 모든 것을 Python 설치 없이, 더블클릭 한 번으로 쓸 수 있는 EXE 파일로 패키징하는 작업입니다.

 

코드는 GitHub에서 확인할 수 있습니다.
github.com/Gweongooing/PLCLink

반응형