
현장에서 가장 많이 들은 말
PLCLink를 현장 엔지니어들에게 소개하면 첫 번째로 나오는 반응이 있었습니다.
"Python 설치해야 해요?"
그리고 두 번째.
"pip install은 어떻게 해요?"
설치 방법을 설명하면 대부분 "그냥 다른 방법 쓸게요"가 됩니다.
현장 엔지니어 입장에서 Python 환경 설치는 진입장벽입니다.
HMI 소프트웨어는 설치 파일 하나 실행하면 끝인데,
PLCLink는 Python부터 시작해서 pip install -r requirements.txt까지 해야 했으니까요.
Phase 5의 목표는 하나였습니다.
더블클릭 한 번이면 바로 실행되게.
설계 원칙 : 아무것도 설치하지 않아도 된다

PyInstaller를 쓰면 Python 런타임과 모든 패키지를 EXE 안에 번들로 묶을 수 있습니다.
배포 파일을 받은 사람은 Python도, pip도 없어도 됩니다.

최종 배포 구조입니다.
dist\PLCLink\
├── PLCLink.exe ← 더블클릭만 하면 됨
├── _internal\ ← Python 런타임 (절대 수정 금지)
│ ├── python313.dll
│ ├── asyncua\
│ ├── pymodbus\
│ └── (모든 패키지)
├── config\ ← 포트 설정 (수정 가능)
│ └── settings.yaml
└── data\ ← DB + 로그 (자동 생성)
├── plclink.db
└── plclink.log
전체 크기는 약 95MB입니다.
Python 3.13 런타임 + FastAPI + uvicorn + asyncua + pymodbus + tkinter + pystray가 전부 들어있어요.
배포 방법은 dist\PLCLink\ 폴더 전체를 복사하는 것이 전부입니다.
앱 구조 : 세 개의 스레드

PLCLink EXE는 세 가지가 동시에 동작합니다.
main()
├── tkinter.mainloop() [메인 스레드 — 상태 창]
├── threading.Thread(_run_server) [daemon — uvicorn + FastAPI]
└── threading.Thread(_run_tray) [daemon — pystray 트레이 아이콘]
tkinter는 반드시 메인 스레드에서 실행해야 합니다.
Windows에서 tkinter를 비메인 스레드에서 실행하면 크래시가 납니다.
이 제약 때문에 uvicorn과 pystray를 daemon 스레드로 분리했어요.
각 스레드의 역할입니다.
- 메인 스레드(tkinter): PC 화면에 보이는 상태 창. 서버 준비 완료, PLC 연결 상태 등 로그를 표시합니다.
- uvicorn 스레드: FastAPI 서버를 8765 포트로 실행. 브라우저에서 접속하는 실제 앱 서버예요.
- pystray 스레드: 시스템 트레이 아이콘. 우클릭 메뉴로 상태 창 표시/숨김, 브라우저 열기, 종료를 제공합니다.
상태 창 : 현장 운영자를 위한 한국어 상태 표시
상태 창은 tkinter로 만든 단순한 텍스트 로그 창입니다.
하지만 uvicorn 로그를 그대로 보여주면 안 됩니다.
INFO: 127.0.0.1:52341 - "GET /api/io/read HTTP/1.1" 200 OK
INFO: 127.0.0.1:52342 - "GET /api/diagnostics/check-port HTTP/1.1" 200 OK
INFO: 127.0.0.1:52343 - "GET /api/io/read HTTP/1.1" 200 OK
1초마다 폴링하는 앱이라 이런 로그가 쉴 새 없이 올라옵니다.
실제로 중요한 이벤트(PLC 연결 실패, 알람 발생 등)가 이 사이에 묻혀버려요.

그래서 로그를 세 계층으로 분리했습니다.
상태 창 (현장 운영자) → 번역된 이벤트만
"PLC 연결 성공", "알람 발생" 같은 한국어 메시지
HTTP 접근 로그 전부 숨김
SystemPage (브라우저) → PLCLink 앱 이벤트만
/api/io/read, /api/diagnostics/check-port 200 → 억제
실제 이벤트만 표시
data/plclink.log → 모든 레벨 기록
RotatingFileHandler(5MB x 3)
개발/디버깅용
uvicorn 내부 메시지도 한국어로 번역해서 표시합니다.
_KOR_EVENTS = {
"Application startup complete": "서버 준비 완료 - 브라우저로 접속 가능",
"Uvicorn running": "포트 바인딩 완료",
"PLCLink 서버 시작": "PLCLink 서버 시작",
"PLC 통신 실패": "PLC 통신 실패",
}
런처 설정과 브라우저 연동

EXE를 실행하면 먼저 작은 런처 창이 뜹니다.
서버 포트를 설정하는 창이에요.
"런처에서 PLC IP도 설정할 수 있으면 어떨까?"라는 생각이 잠깐 들었는데, 하지 않았습니다.
이유가 있습니다.
런처(tkinter)와 브라우저 앱(React + SQLite)은 완전히 분리된 저장소를 씁니다.
런처에서 PLC IP를 설정해도 브라우저 앱이 이걸 모릅니다.
설정했는데 반영이 안 되면 사용자 혼란이 생겨요.
대신 이런 방식을 썼습니다.
- 런처에서 PLC 설정을 한 경험이 있으면
config/settings.yaml에 저장 - 브라우저에서
GET /api/setup/launcher-config호출 - SetupFlow 첫 화면에 배너 표시: "런처에서 설정한 IP가 있어요. 이 설정으로 바로 진행하시겠어요?"
PLC 설정은 브라우저에서만, 런처는 포트 설정만 합니다.
역할을 명확히 분리했어요.
Windows 방화벽 자동 등록

서버를 0.0.0.0:8765로 바인딩하면 이 PC에서는 localhost:8765로 접속됩니다.
그런데 다른 PC에서 IP:8765로 접속하면 타임아웃이 납니다.
Windows Firewall이 외부 인바운드를 막고 있기 때문이에요.
PLCLink는 현장에서 여러 PC에서 접속하는 도구라서 이게 반드시 해결돼야 했습니다.
첫 실행 시 관리자 권한으로 방화벽 규칙을 자동으로 추가하도록 했어요.
ctypes.windll.shell32.ShellExecuteW(
None, "runas", "cmd.exe",
f'/c netsh advfirewall firewall add rule '
f'name="PLCLink-{port}" dir=in action=allow protocol=TCP localport={port}',
None, 0
)
Windows UAC 팝업이 뜨고, 허용하면 방화벽 규칙이 추가됩니다.
이미 규칙이 있으면 건너뜁니다.
한 번만 하면 이후에는 자동으로 됩니다.
사이드바 UX 정리
기능이 늘어나면서 사이드바가 복잡해졌습니다.
정리가 필요했어요.
헤더에 이미 있는 것들이 있었습니다.
연결 상태, IP:포트, 설정 변경 버튼.
그런데 사이드바에도 통신 진단이 있었어요.
중복이었습니다.
정리 결과입니다.
정리 전 사이드바:
IO Monitor / Data List / Canvas / Trigger / Alarm / 통신 진단 / System
정리 후 사이드바:
IO Monitor / Data List / Canvas / Trigger / Alarm
─────────────────── (구분선)
System / Log
변경된 것:
- 통신 진단: 사이드바 제거 → 헤더 연결 상태 클릭 시 이동
- System/Log: 구분선 아래 분리 (운영 메뉴와 시각적 분리)
사이드바는 순수하게 현장 운영 도구만 남겼습니다.
통신 진단이나 시스템 설정 같은 관리 기능은 구분선으로 분리해서 시각적으로 다른 성격임을 보여줍니다.
PyInstaller 삽질 요약

EXE 패키징 과정에서 네 가지 문제가 있었습니다.
각각 보족에서 자세히 다루고, 여기서는 결론만 정리합니다.
| 문제 | 증상 | 해결 |
|---|---|---|
| console=False + isatty() | AttributeError: NoneType | uvicorn.Config에 log_config=None |
| Windows + 비메인 스레드 asyncio | ProactorEventLoop 불안정 | WindowsSelectorEventLoopPolicy |
| frozen 모드에서 import 실패 | from main import app 불가 | sys.path에 _MEIPASS 추가 |
| bat 파일 CRLF | 명령어 인식 안 됨 | LF → CRLF 명시적 변환 |
Phase 5를 마무리하며
Phase 5가 끝난 PLCLink는 현장 엔지니어가 Python을 전혀 모르더라도 쓸 수 있는 도구가 됐습니다.
배포 받은 엔지니어가 하는 일은 두 가지예요.
PLCLink.exe더블클릭- 브라우저로 접속
나머지는 다 자동이에요. 서버가 뜨고, 방화벽이 열리고, 버전이 표시됩니다.
"Python 설치해야 해요?"라는 질문이 이제 필요없게 됐습니다.
Phase 4+5로 PLCLink는 어떤 PLC든, Python 없이도 쓸 수 있는 도구가 됐어요.
원형 게이지, 슬라이더 위젯 같이 캔버스 HMI에 추가하지 못한 것들은 이후 업데이트로 채워갈 예정입니다.
'(개인Project)_개발 > PLC-PC 연결' 카테고리의 다른 글
| [Program][보족] FastAPI 서버를 백그라운드로 돌리면서 트레이 아이콘까지 만드는 방법 (0) | 2026.06.12 |
|---|---|
| [Program][보족] PyInstaller EXE가 콘솔 없이 에러 메시지도 없이 꺼진다 : 초기화 로그로 디버깅 (0) | 2026.06.11 |
| [Program][보족] Python OPC-UA 클라이언트 구현 삽질기 : browse path, NodeId, VariantType (0) | 2026.06.08 |
| [Program][보족] Python으로 PLC 프로토콜을 직접 짜야 할 때 : FINS 스펙 읽는 방법 (0) | 2026.06.07 |
| [Program][보족] pymodbus 버전별 API 변경 정리 : 3.6 이하에서 3.13으로 마이그레이션 (0) | 2026.06.06 |