CMS
레거시 MFC 검사 앱을 PySide6로 현대화한 외주.
CMS는?
18650·21700 배터리 셀의 내부저항(ACIR)과 개방전압(OCV)을 자동으로 측정·판정하는 생산라인용 산업 HMI다. 미쓰비시 PLC(지그)가 셀을 눌러 계측기로 측정하면, 기준값 대비 OK/NG를 판정하는 검사 시퀀스를 한 트레이 256채널로 돌린다. PySide6 데스크톱 앱.
이건 내 포트폴리오에서 유일하게 실제로 배포되어 생산 현장에서 돌아간 유료 프로덕션이다. 개인 프로젝트 대부분이 미출시인 것과 달리, 미쓰비시 PLC·계측기와 실제로 통신하며 라인에서 검사를 수행한 산업 소프트웨어다.
국내 배터리 제조사에 납품한 클라이언트 용역이라, 회사명·계약·판정 규격·설비 주소 등은 전부 마스킹했다. 내부 화면과 실 스펙은 비공개.
왜 만들었나
이전 벤더가 남긴 낡은 MFC(C++) 검사 시스템을 대체·현대화하는 용역이었다. C++이라 직접 이어 쓰기 어렵고 벤더에 종속돼 있어서, PySide6로 전면 재작성했다. 레거시는 "무엇을 검사하고 어떻게 판정하는가"의 설계 도면으로만 참조하고, 통신·안전감시·상태머신·Mock 같은 골격은 새로 설계했다.
개발은 AI 보조(바이브코딩)로 진행했다. 1인이 이 규모(1차 납품 시점 약 15,000줄, 2차 DCIR까지 더해 현재 약 19,000줄)를 현장 납품까지 끌고 가려면 그게 현실적인 방식이었다.
전체 구조


실장비를 다루는 소프트웨어
이 프로젝트의 무게는 화면이 아니라 실제 PLC·계측기·비상정지와 안전하게 대화하는 부분에 있다.
PLC 통신 + 32/64비트 브릿지
미쓰비시 PLC와 통신해 M/D 비트를 읽고 쓰며 검사 시퀀스를 제어한다. 안전 신호, 하트비트, 채널 릴레이 스위칭까지 여기서 오간다. 통신은 미쓰비시 MX Component COM 인터페이스를 win32com으로 잡고, 별도 QThread에서 명령 큐로 처리한다.
까다로운 지점: MX Component가 32비트 전용이라 64비트 PySide6 앱에서 바로 못 부른다. 그래서 64비트를 감지하면 32비트 파이썬 서브프로세스를 띄워 로컬 TCP로 JSON 명령을 주고받는 브릿지를 만들어 우회했다. 하트비트로 연결 끊김을 감지하고, 안전 신호는 500ms로 폴링한다.
채널 그리드 HMI (16×16, 마스킹)
안전 시스템, EMG 래치
비상정지(EMG)와 도어 열림을 500ms 폴링으로 감시하다가, 발생하면 즉시 사이클을 중단하고 ESC로도 안 닫히는 모달을 띄운다. 해제는 PLC에 에러리셋 신호를 배치로 쓰고 ACK 비트를 기다리는 핸드셰이크로, 3초 안에 응답이 없으면 강제 클리어한다.
가장 값진 교훈이 여기서 나왔다. 물리 EMG 버튼이 눌린 상태면 PLC가 안전 비트를 1로 유지해서, 소프트웨어 Reset으로는 절대 안 풀린다(폴링이 계속 재발화). "코드를 아무리 고쳐도 안 풀리면, 버튼을 물리적으로 복구했는지부터 물어라"는 규칙은 여기서 몸으로 배운 것이다.
테스트 사이클 FSM
한 트레이 256채널을 지그 DOWN 한 번 → 채널을 순차로 릴레이 전환하며 측정·판정 → 지그 UP 한 번으로 돈다. 레거시의 전역 플래그 조합을 11-state Enum FSM으로 승격했다. 전이는 PLC 비트 콜백·사용자 버튼·타이머로 일어난다.
디버깅에서 배운 것: FSM이 개별 쓰기 완료 신호의 엣지에 걸려 있어서, 배치 쓰기를 그냥 하면 트리거가 안 됐다. 배치를 주소별 개별 신호로 분해해야 체인이 돈다(Mock도 같은 규약을 재현). 그리고 256채널 뒤에 버리는 **더미 채널(257)**을 하나 더 돌려 PLC 레이턴시로 마지막 채널이 누락되는 걸 막았다.
측정 · 판정 · 캘리브레이션
Hioki 계측기로 ACIR과 OCV를 RS-232C로 측정한다(원시 저항 값을 mΩ로 환산, N회 평균). 판정은 채널별 오프셋을 적용한 뒤, 미장착(오버/언더레인지) 검출 → 범위 판정 → NG 심각도 순. 원시값은 무결하게 기록하고 오프셋은 판정 단계에서만 쓴다. 캘리브레이션 탭에서 채널별 오프셋을 잡는다.
실제 판정 기준값·오프셋은 클라이언트 고유 규격이라 비공개. 코드 기본값은 범용 Li-ion 새니티 범위(예: ACIR 10
50mΩ, OCV 3.04.2V)일 뿐이다.
Mock 엔진, 무장비 개발
실장비(PLC·계측기·스캐너)가 없어도 로직·FSM·UI를 검증할 수 있게, 실엔진과 완전히 같은 신호 인터페이스를 가진 Mock 엔진 3종을 만들었다(드롭인 교체). 타이머로 응답을 모사하고, 배치 쓰기도 주소별 개별 신호로 재현하며, 비상정지 같은 장애를 주입하는 API까지 뒀다. 덕분에 현장 장비 없이 개발·검증이 돌아갔다.
2차 개발, DCIR 측정 추가 (진행 중)
1차를 납품하고 나서 발주처가 다시 의뢰한 추가 개발이다. 1차가 ACIR·OCV였다면 2차는 DCIR(직류 내부저항)을 잰다. 이름이 비슷해서 같은 일로 보이지만 성격이 다르다.
| 측정 | 무엇을 보나 | 어떻게 얻나 | 단계 |
|---|---|---|---|
| OCV | 개방전압 | 무부하 상태에서 전압을 읽는다 | 1차 (납품 완료) |
| ACIR | 교류 내부저항 | 계측기가 교류 신호를 걸어 직접 잰다 | 1차 (납품 완료) |
| DCIR | 직류 내부저항 | 셀을 정전류로 방전시켜 전압 강하로 산출한다 | 2차 (진행 중) |
앞의 둘은 계측기에 물어보면 값이 나온다. DCIR은 셀을 실제로 방전시켜야 값이 생긴다. R = (V1 − V2) / I, V1은 부하를 걸기 전 전압, V2는 방전이 끝나는 시점의 전압이다. 그래서 신규 장비가 들어온다. 정전류를 뽑아내는 전자로드(Chroma 63000 계열)와, 전압을 정밀하게 읽을 6.5자리 DMM(Keysight 34465A)이다. PLC와 채널 경로는 1차 그대로 재사용한다.
최종 구성은 PLC 1대 + 계측기 2대 + 전자로드 1대의 하이브리드다. 계측기가 둘인 이유가 있다. DMM은 DC 전압계라 ACIR(교류 내부저항)을 못 잰다. 그래서 1차의 계측기를 빼고 갈아끼우는 게 아니라, 측정 종류에 따라 계측기가 갈린다. ACIR·OCV는 기존 계측기가, DCIR의 V1·V2는 새 DMM이 맡는다. 장비를 하나로 통일하는 대신 각자 잘하는 걸 시키는 쪽을 택했다.
| 역할 | 장비 | 담당 |
|---|---|---|
| 채널 선택 | PLC (미쓰비시) | 1차 그대로 |
| ACIR · OCV | 계측기 (Hioki, RS-232C) | 1차 그대로 |
| DCIR 전압 (V1·V2) | DMM (Keysight 34465A, USB) | 2차 신규 |
| 정전류 방전 | 전자로드 (Chroma 63000 계열, USB) | 2차 신규 |
절대 규칙이 하나 있다. 1차 납품분은 건드리지 않는다. 실장비 검증이 끝난 기준선이라, 고치는 대신 얹는다. 통신 검증도 본 앱이 아니라 고립된 미니 스크립트로 먼저 했다. PLC 자동화·MUX·FSM을 전부 빼고 전자로드 통신과 타이밍만 남겨서, 변수를 하나씩만 확인했다.
통신 테스트는 통과했고, 0.3V가 남았다
현장 통신 테스트에서 목표는 전부 통과했다. USB(USBTMC)로 자동 인식, SCPI로 모드·전류·부하 ON/OFF 제어, 측정값 읽기, 셀에 2A 실부하까지. 장비 전면 패널이 Remote·CC·I Set 2.000A로 바뀌는 걸로 양방향 통신을 눈으로 확인했다. 명령과 응답은 전부 원시 문자열로 로깅했다.
문제는 다른 데서 나왔다. 전자로드가 읽는 전압이 계측기보다 0.3V 넘게 낮았다. 어느 쪽이 틀렸는지부터 정해야 해서 제3의 측정기로 교차 확인했고, 계측기가 맞았다.
원인은 고장이 아니라 구조다. 전자로드는 자기 단자에서 전압을 읽는데, 전류가 흐르면 셀과 로드 사이의 배선·접점에서 전압이 깎인다(전류 × 선저항). 그래서 로드가 보는 값은 실제 셀 전압에서 배선 손실을 뺀 값이다. 계측기는 셀 단자 근처에서 4단자(켈빈)로 재기 때문에 그 손실이 빠진다. 전류가 클수록 차이는 커진다. DCIR 목표 전류가 20A대인 걸 생각하면, 2A에서 0.3V는 넘어갈 숫자가 아니었다.
전자로드 한 대가 전류도 걸고 전압도 읽으면 배선도 코드도 단순해진다. 실측이 그 편의를 기각했다. DCIR은 전압 차이로 저항을 만드는 계산이라, 전압에 섞인 배선 손실이 그대로 저항 오차가 된다.
그래서 전압 취득은 1차 때 만들어 둔 경로를 그대로 쓴다. PC가 PLC로 채널을 물리고, 4단자로 붙은 계측기가 V1·V2를 읽고, 전자로드는 전류만 담당한다. 역할을 쪼갠 1차 설계가 옳았다는 걸 실측으로 확인한 셈이고, 덕분에 재사용할 것이 하나 더 늘었다.
계측기는 6.5자리 DMM(Keysight 34465A)으로 간다. 전자로드와 똑같이 USB(USBTMC)라 드라이버 설치 없이 잡히고, 둘 다 같은 방식으로 붙으니 응답 문자열로 누가 로드고 누가 DMM인지 가른다. 전압은 MEAS:VOLT:DC? 한 방이면 자동 범위로 나온다.
그래서 채널 하나의 시퀀스는 이렇게 된다. PC가 PLC에 채널을 지정하고 → 계측기로 V1을 읽고 → 전자로드에 정전류 방전을 걸고 → PC가 타이머를 들고 있다가 정해둔 간격마다 전압을 받아 적고 → 방전이 끝나는 시점의 값을 V2로 잡아 R을 계산해 그리드에 올리고 → 부하를 끊고 다음 채널로 넘어간다. 타이머를 장비가 아니라 PC가 쥐고 있는 게 핵심이다.
측정값을 "한 번만 잡을지, 계속 받아 적을지"는 두 번 뒤집힌 항목이다. 처음엔 방전 끝에 한 번 읽었고, 다음엔 그 시점에서 여러 번 읽어 평균 내는 쪽으로 갔다가, 최종적으로는 방전하는 동안 정해둔 간격마다 계속 기록하는 시계열로 정해졌다. R에 쓰는 V2는 그중 마지막 값이다. 발주처가 원한 건 저항 한 숫자가 아니라 전압이 어떻게 떨어지는지 그 자체였다. 규격을 물어보지 않고 "합리적으로" 정해 뒀으면 세 번 다 틀렸을 자리다.
계획에서 코드로
통신 검증이 끝나고, 미니 스크립트에 있던 드라이버를 본 앱으로 승격했다. 측정 엔진, DCIR 탭, 캘리브레이션 탭, 판정, 결과 저장까지 약 2,300줄이 새로 붙었다.
여기서도 규칙은 하나였다. 1차 파일은 건드리지 않는다. 판정 로직도 1차 것을 고치는 대신 DCIR 전용 함수를 따로 뒀고, 결과 CSV도 1차 파일과 분리했다. 얹기만 하고 빼지 않는다.
엔진은 1차 계측기 엔진과 같은 구조를 그대로 복제했다. 별도 스레드에서 돌고 신호로만 화면과 이야기한다. 다른 점 하나는 안전 처리다. 부하를 거는 장비라서, 어느 경로로 빠져나가든 마지막에 반드시 부하를 끊도록 finally에 넣었다. 예외가 나도, 창을 닫아도, 연결을 끊어도 부하는 먼저 내려간다.
전자로드와 DMM이 둘 다 USB로 붙는 게 오히려 문제였다. 포트만 봐서는 누가 누군지 모른다. 그래서 장비에 이름을 물어보고 응답 문자열로 가른다. 이왕 물어본 김에 화면에도 제네릭한 "DMM"이 아니라 실제 모델명을 띄우게 했다. 현장에서 "지금 뭐가 붙어 있냐"를 눈으로 확인할 수 있는 게 낫다.
현장마다 OCV만 쓰는 곳, DCIR만 쓰는 곳, 둘 다 쓰는 곳이 갈린다. 앱 하나로 셋을 다 덮으려면 어딘가에서 갈라야 하는데, 코드에서 갈면 현장 수만큼 분기가 늘어난다.
설정 파일의 플래그 하나로 사이드바와 탭 노출을 가르는 쪽으로 갔다. 인스톨러가 설치 마법사에서 고른 구성을 그 플래그에 기록하고, 앱은 켜질 때 읽어서 필요한 탭만 만든다. 플래그가 없으면 전부 노출로 떨어지게 해서, 이미 나가 있는 배포분은 아무것도 달라지지 않는다.
덕분에 납품물은 계속 setup 파일 하나다. 설치할 때 체크박스만 다르게 고른다.
그 외에 재사용이 한 번 더 통했다. 1차의 채널 그리드를 DCIR에도 그대로 쓰되, 채널 수가 달라도 정사각에 가깝게 열을 자동으로 잡도록만 손봤다. 현장마다 트레이를 읽는 방향이 달라서, 채널을 가로로 채울지 세로로 채울지도 설정에서 고르게 했다. Mock도 그대로 따라왔다. 장비 없이 띄우면 DCIR도 가짜 장비에 붙어서 흐름을 끝까지 돌려볼 수 있다.
실장비 앞에서, R이 허수로 나왔다
장비를 실제로 물리고 첫 DCIR을 재던 날, 저항이 말이 안 되는 값으로 나왔다. 코드를 열기 전에 원시 로그부터 봤다. 계측기만 셀에 붙였을 때는 계측기가 3.27V를 읽는데 전류가 0이었고, 전자로드만 붙였을 때는 5A가 흐르는데 계측기 전압이 0이었다. 소프트웨어 문제가 아니었다. 전류를 흘리는 장비와 전압을 재는 장비를 셀에 동시에 물리지 못한 상태였던 것이다.
DCIR은 전류를 흘리는 그 순간의 전압 강하를 재는 측정이라, 둘 중 하나만 붙어 있으면 애초에 성립하지 않는다. 셀의 +/− 각 단자에 로드의 전류 리드와 계측기의 전압 리드를 함께 무는 4단자 배선이 필요했다. 앞에서 0.3V 차이를 보고 "전압은 계측기로 읽는다"고 정해 둔 것의 물리적 대가를 여기서 치른 셈이다.
통신 쪽은 전부 통과했다. 두 장비 모두 USB로 인식됐고, 모드·전류·부하 제어와 값 읽기가 다 정상이었고, 셀에 5A가 실제로 흘렀다. 남은 건 배선 하나였다. 코드를 고치러 가지 않고 로그로 물리 원인을 특정한 것이, 1차에서 EMG 래치로 배운 것과 정확히 같은 절차였다.
자동으로 도는 DCIR
1차의 검사 탭이 Auto와 Manual로 갈려 있듯, DCIR에도 자동 순회 탭을 붙였다. 설정한 채널 범위를 릴레이로 하나씩 옮겨 가며 측정하고, 결과를 그리드와 카운터에 쌓고, 끝나면 CSV로 떨군다. 상태머신은 1차 것을 재사용하지 않고 따로 뒀다. 1차는 지그를 눌렀다 떼는 것까지 포함하는 사이클이지만, 이쪽은 지그를 사람이 잡고 릴레이로 채널만 옮기는 훨씬 짧은 루프라서다.
여기서 하나 조심할 것이 있었다. 측정 엔진을 자동 탭과 수동 탭이 같이 쓰는데, 측정이 별도 스레드에서 비동기로 끝나기 때문에 결과 신호가 양쪽에 다 도착한다. 자기가 시킨 측정만 처리하도록 대기 표시를 두고 걸렀다. 1차에서 신호 체인 때문에 한참 헤맸던 경험이 그대로 쓰였다.
릴레이를 실제로 어떻게 물리는지가 그때는 확정 전이었다. 그래서 채널을 바꾸는 동작을 함수 하나에 몰아 뒀다. 확정되지 않은 것을 코드 여기저기에 흩뿌리지 않는 게 그 단계에서 할 수 있는 최선이었다.
얼마 뒤 발주처에서 방식이 내려왔고, 1차에서 쓰던 펄스 방식이 아니라 채널 번호를 PLC의 한 주소에 써 넣으면 넘어가는 명령 방식이었다. 짐작으로 채워 뒀던 것과 다른 답이 온 것이다. 고친 곳은 그 함수 한 줄이었다. 순회 로직도, 탭도, 측정 엔진도 그대로다. 모르는 것을 한곳에 가둬 두면 답이 왔을 때 치르는 값이 이만큼이라는 게, 이 프로젝트에서 두 번째로 확인된 셈이다.
채널을 넘기는 신호가 PLC로 나가니, 자동 순회의 전제도 PLC 연결이 됐다. 전자로드와 DMM 옆에 PLC 연결 표시등을 하나 더 붙였다. 현장에서 순회가 안 돌 때 "장비가 안 붙었나, 채널이 안 넘어가나"를 화면에서 먼저 가를 수 있어야 해서다. 같은 이유로 현장 절차서도 따로 썼다. 순서대로 따라 하는 법, 안 될 때 원시 로그 세 줄로 원인을 가르는 법, 그리고 자동 순회가 끝내 안 맞으면 수동 측정으로 채널을 하나씩 도는 플랜 B까지.
기술 스택
- UI/앱: Python · PySide6 · 자체 제작 산업 위젯(QPainter · QSS)
- PLC: 미쓰비시 MX Component COM(pywin32) + 32/64비트 TCP 브릿지
- 계측/스캐너: Hioki 계측기 · 바코드 스캐너 (RS-232C, pyserial)
- 2차(DCIR): 전자로드 Chroma 63000 계열 · DMM Keysight 34465A (USB · USBTMC · SCPI, pyvisa)
- 구조: 11-state FSM · QThread + Signal/Slot 명령 큐 · dataclass 도메인 모델 · Mock 엔진
- 환경: Windows + 32비트 Python + MX Component · 1차는 zip + run.bat 배포, 2차는 PyInstaller + Inno Setup 인스톨러(원클릭 빌드 스크립트까지 작성, 빌드는 윈도우에서)
왜 이 스택
- PySide6: 낡은 MFC(C++) 검사 시스템을 대체하는 용역이었다. C++은 이어 쓰기 어렵고 벤더에 종속돼 있어 Python으로 전면 재작성했다. 1인이 이 규모(1차 기준 약 15,000줄)를 현장 납품까지 끌고 가려면 Python의 생산성이 필요했고, 산업 HMI 위젯은 QPainter·QSS로 자체 제작했다.
- MX Component (PLC 통신): 미쓰비시 PLC의 표준 통신 인터페이스라, 지그·계측기를 제어하려면 이걸 통해야 한다.
- 32/64비트 브릿지: MX Component가 32비트 전용이라 64비트 PySide6 앱에서 바로 못 부른다. 그래서 32비트 파이썬 서브프로세스를 띄워 로컬 TCP로 우회했다. 우아하진 않지만 벤더 라이브러리 제약을 실무에서 넘는 방법이었다.
- pyserial: 계측기·스캐너가 RS-232C 시리얼이라 pyserial로 통신했다.
Trouble Shooting
이 프로젝트의 트러블은 대부분 실장비 앞에서 로그로 잡은 것들이다.
- 특정 채널부터 오버레인지가 떴다. Start 시 PLC 상태 초기화가 부족해 상태가 오염됐다. 앱 재시작으로도 복구가 안 되고 레거시 클리어로만 풀려서, Start 전에 관련 주소를 일괄 0으로 밀고 에러리셋을 거는 것으로 해결했다.
- 비상정지가 안 잡혔다. 추측 수정 대신 디버그 덤프로 "실제로 변하는 주소"를 현장에서 확정해 감시 대상에 반영했다.
- 에러리셋이 안 풀렸다. ACK 핸드셰이크와 3초 강제 타임아웃으로 방어했다.
- Qt UI가 자꾸 번복됐다. HTML 목업을 먼저 만들어 "이 수치 그대로 Qt에 옮긴다"를 규칙으로 못 박아 재발을 막았다.
- 레거시 이식 누락도 있었다. 신규 코드에 빠진 필드를 레거시와 대조해 찾아 채웠다.
- 2차 탭을 얹었더니 언어 토글이 그 탭에만 안 먹었다. 1차에서 만든 전역 재번역 배선이 새 탭까지 자동으로 따라오지 않아서, 화면을 추가할 때 재번역 함수도 같이 등록하도록 고쳤다.
- 시작 팝업 하나에 1차 폼과 2차 DCIR 설정을 같이 담았더니 내용이 창보다 커져서 입력칸이 겹쳤다. 창을 키우고 상단 폼 높이를 고정해 아래 Cell ID 그리드를 살렸는데, 이번엔 작은 노트북 화면에서 팝업이 화면보다 커져 위쪽이 잘렸다. 결국 고정을 걷어내고 최소 높이만 준 뒤 남는 세로는 그리드가 흡수하게 했다. 그러고도 한 번 더 눌렸다. 상단의 파일 목록이 자기 최소 크기로 위쪽을 부풀리고 있어서 아래 그리드가 얇게 깔렸다. 목록이 세로 공간을 요구하지 않도록 정책을 바꿔서, 위는 폼 높이만큼만 쓰고 남는 세로는 전부 그리드로 가게 했다. 한 화면에 두 시대의 폼을 합치면 화면 크기 가정이 충돌한다. 고정 값으로 막으면 반대편에서 터지고, 결국 "누가 남는 공간을 먹는가"를 정해야 끝난다.
- 앱을 켤 때마다 "저장 완료" 팝업이 떴다. 저장해 둔 언어를 복원하는 코드가 콤보 값을 바꾸면서 사용자가 직접 고른 것과 똑같은 핸들러를 태웠고, 그 핸들러가 설정 저장까지 하고 있었다. 복원할 때는 신호를 막고 언어만 적용하도록 갈랐다. 설정 복원이 조작용 핸들러를 그대로 타면 부팅 때 부작용이 나온다.
- DCIR 첫 실측에서 저항이 허수로 나왔다. 원인은 코드가 아니라 배선이었고, 원시 로그에서 "계측기만 붙으면 전류 0, 로드만 붙으면 전압 0"이 그대로 보였다. 전류와 전압을 셀에 동시에 물리는 4단자 배선이 필요했다.
미해결 잔여도 있다. 고차 채널의 간헐적 하드웨어 실패(물리 점검 필요) 같은 것들이다. "45/45 통과"는 레거시 대조 자체검증이지 클라이언트 최종 검수가 아니다.
Decision Log
C++ 레거시는 직접 복사할 수 없고 벤더에 종속돼 있었다. "무엇을·어떻게 판정하는가"만 참조하고 PySide6로 새로 썼다. "다 내가 만들었다"가 아니라 "이식 + 직접 설계"가 정확한 표현.
MX Component가 32비트 전용이라, 64비트 앱에서 32비트 파이썬 서브프로세스를 띄워 로컬 TCP로 우회했다. 초기엔 소켓 프로토콜을 고려했지만 최종 배포는 COM.
레거시엔 없던 것. EMG·도어 폴링과 ACK 핸드셰이크, 실시간 비트 모니터링을 새로 얹었다. 실장비를 다루는 소프트웨어의 핵심이 여기다.
무장비 개발·검증을 위해 실엔진과 같은 인터페이스의 Mock을 만들어 드롭인 교체하게 했다. 하드웨어 없이 로직을 돌릴 수 있는 게 1인 개발의 속도를 만들었다.
Qt UI를 머릿속으로 잡으면 계속 번복된다. HTML 목업을 먼저 확정하고 "이 수치 그대로 옮긴다"를 규칙으로 삼았다.
회고, 유일한 실전 프로덕션
개인 프로젝트들이 "혼자 어디까지 만들 수 있나"의 증거라면, CMS는 다르다. 실제 돈을 받고, 실제 설비와 통신하며, 실제 생산 라인에서 돌아간 유일한 프로덕션이다.
낡은 MFC C++를 PySide6로 이식하면서, EMG 안전 래치·신호 체인·PLC 상태 오염 같은 실장비의 함정을 추측이 아니라 로그로 잡아냈다. 이 경험들이 내 디버깅 규칙(하드웨어를 먼저 의심하라, 로그가 가리키는 곳을 봐라)의 실제 출처다. 그리고 AI 보조로 1인이 이 규모를 현장 납품까지 끌고 갔다.
"스펙트럼 넓은 사람"이라는 말의 가장 무거운 증거다. 다만 회사·금액·규격은 전부 가린 채로만 보여줄 수 있는, 그런 종류의 증거이기도 하다.
2차(DCIR 측정 추가)는 통신 검증을 마치고 앱 안에 구현까지 들어갔다. 수동 측정과 자동 순회, 캘리브레이션까지 코드로는 돌아간다. 장비 앞에서 통신과 실제 방전까지 확인했고, 실제 저항 값을 얻는 건 배선을 잡고 다시 서는 날로 남아 있다.