LGES-OC5-PtoP-Welder/docs/분석_PLC210002_기동오알람_260810.md
NohDoyeon241104 e0e5416975 fix: PLC 기동 시 210002 오알람 차단 — Livetimer 초기화 및 모니터 타이머 기동 위치 이동
PLCController.DoActivate() 가 PLC 연결을 열기 전에 _monitorPlcInterfaceMap 을
기동해, 연결 확립 전 LiveStatusUpdate() 가 돌면서
ALARM_DEVICE_PLC_DISCONNECT(210002) 오알람이 매 기동마다 발생했다.

원인
- Livetimer 가 초기화되지 않아 DateTime.MinValue 상태 → (Now - MinValue) 가
  항상 10000ms 를 초과해 워치독이 오판. 임계값 조정으로는 해결 불가.
- MicroTimer 첫 콜백이 주기(100ms) 뒤가 아니라 Start() 직후(약 1ms)라
  경합이 아니라 100% 재현되는 결정적 버그.

실측 (2026-08-10 16:12:25.904)
  Livetimer=0001-01-01, elapsed=63921975145904ms,
  isLive=False, IsActivated=False, IsConnected=False

조치
- _monitorPlcInterfaceMap.Start() 를 base.DoActivate() 성공 이후로 이동
- Livetimer = DateTime.Now 로 기준시각 확보 후 타이머 기동
- 두 조치는 각각 단독으로는 무효. 반드시 함께 적용해야 한다.
- 수정 전/후를 #region 2개(검색: 260810 기동 오알람)로 기록해 원복 가능

검증: 2026-08-10 현장 재기동 확인 — 기동 시 210002 미발생

미해결(별건): Livetimer 이중 용도, ReadBit .err 무시, ClearAlarm 부재,
SeqAlarmMonitor 의 무지연 루프 및 Laser/Chiller 즉시 알람

상세: docs/분석_PLC210002_기동오알람_260810.md
작업일지: docs/작업일지/2026_08_10.md
2026-08-10 16:37:27 +09:00

218 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 기동 시 PLC_DISCONNECT(210002) 오알람 분석 — 2026-08-10
## 1. 증상
프로그램 첫 시작 시, PLC 연결이 열리기도 전에 `ALARM_DEVICE_PLC_DISCONNECT`(210002)가 뜬다.
연결은 곧 정상적으로 붙지만 알람은 화면에 남는다. 시뮬 모드에서는 발생하지 않는다.
---
## 2. 자동 연결 경로
| 순서 | 위치 | 내용 |
|---|---|---|
| 1 | `Machines/ILSOS_PrismaticRD_PTOP/EquipmentSetup.cs:91` | `AutomaticallyConnectDevices` 확인 |
| 2 | `EquipmentSetup.cs:92` | `await ActivateDevices()` |
| 3 | `WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs:135-158` | 디바이스 순차 `Activate()` — 실패 시 로그만 |
| 4 | `EquipmentSetup.cs:105-107` | 모니터 시퀀스 3종 Start |
`await`이 걸려 있어 **시퀀스 시작 순서 자체는 정상**이다(연결 시도 완료 후 모니터 시작).
`CommonEquipmentSetup.cs:149`의 `OnPostDeviceActivated`는 `Activate()` 실패해도 호출되지만,
`PLCBase.APIs.cs:334-341`이 `E2.NOT_CONNECTED`로 조용히 반환하므로 여기서 알람은 발생하지 않는다.
---
## 3. 210002를 올리는 지점 — 3곳, 그중 2곳만 살아있다
| | 위치 | 계층 | 상태 |
|---|---|---|---|
| A | `WeldingCommon/Devices/PLC/PLCController.Config.cs:146` | 디바이스 (100ms 타이머) | **활성** |
| B | `Machines/.../Sequences/Monitoring/SeqAlarmMonitor.cs:182` | 시퀀스 | **활성** |
| — | `WeldingCommon/Sequences/Monitoring/AlarmMonitoring.cs:169` | 시퀀스 (공통) | **죽은 코드** |
`AlarmMonitoring.Builder()`를 부르는 곳은 `SequenceManager.cs:83`의 주석 한 줄뿐이다
(`CommonSequenceIds.AlarmMonitor` 전체 검색 → `:51`·`:52`·`:83` 전부 주석). 등록 0건이므로
주석을 풀어도 동작하지 않는다. B는 이 클래스를 PTOP가 복사해 간 살아있는 사본이다.
### 두 사본이 갈라진 흔적 (같은 `260512` 주석, 정반대 방향)
| | AlarmMonitoring (죽음) | SeqAlarmMonitor (살아있음) |
|---|---|---|
| `TIMEOUT_PLC_LIVE_CHECK` | `200000` (`:52`) | `2000` (`:54`) |
| 알람 호출 | 주석 (`:169`) | 활성 (`:182`) |
| 루프 지연 | `Thread.Sleep(100)` (`:142`) | **없음** — 빈 `return Ok()` (`:145-148`) |
### A와 B는 판정 기준이 다른데 알람 코드가 같다
| | A (`PLCController`) | B (`SeqAlarmMonitor`) |
|---|---|---|
| 판정 | **엣지** — live 비트가 바뀌어야 정상 | **레벨** — 2초 안에 `true`면 정상 |
| 임계 | 10000ms | 2000ms |
| 코드 | `AlarmCode.ALARM_DEVICE_PLC_DISCONNECT` | `AlarmCode_PTOP....`(상속) |
`AlarmCode_PTOP : AlarmCode`(`AlarmCode_PTOP.cs:10`)이고 210002는 재정의되지 않았으므로 **동일 코드**다.
→ 화면에 210002가 떠도 **어느 워치독이 올렸는지 구분할 수단이 없다.** (§6의 진단 계측 사유)
---
## 4. A의 결함 분석 (`PLCController.Config.cs:122-150`)
```csharp
33 DateTime Livetimer; // 초기화 없음 = DateTime.MinValue
124 if (IsSimul) return; // 시뮬 미발생
129 if (updateLiveWait) { // [에코 대기]
130 if (Now - Livetimer > 1000) { updateLiveWait = false;
132 WriteBit(PC_LIVE_STATUS, beforeLiveStatus); }
134 } else { // [감시]
135 bool isLive = ReadBit(PLC_LIVE_STATUS).data;
137 if (isLive != beforeLiveStatus) {
138 Livetimer = Now; beforeLiveStatus = isLive; updateLiveWait = true; }
144 if (Now - Livetimer > 10000)
146 AlarmManager.Alarm(ALARM_DEVICE_PLC_DISCONNECT);
```
**① 임계값 조정(10000 → 20000)은 원리적으로 무효.**
`Livetimer` 대입 지점은 `:138` 하나뿐이고 **토글 감지 시에만** 실행된다. 기동 직후에는 토글이 없어
`Livetimer = DateTime.MinValue` → `Now - MinValue ≈ 2026년 ≈ 6.4×10¹⁰ ms`.
임계값을 얼마로 올리든 비교 결과는 항상 `true`. **임계값 문제가 아니라 초기화 누락이다.**
**② 리셋이 타이머보다 뒤에 있다.**
```
185 _monitorPlcInterfaceMap.Start(); ← 타이머 시작(100ms 주기)
187 base.DoActivate(config); ← 실제 PLC Open + 버퍼 생성 + 4회 Read + ManualDataUpdate
190 LoadMemoryMapFromFiles(); ← CSV 재파싱
195 beforeLiveStatus = false; ← 리셋은 여기 (Livetimer는 아예 빠져 있음)
```
`:187~:190` 구간이 100ms를 넘으면 그 사이 tick이 발생한다. 그 구간에서는
`PLCBase.APIs.cs:44-51`에 따라 `ReadBit`이 `(E2.NOT_READY, false)`를 반환하므로
`isLive`가 항상 false → `:137` 토글 감지 영구 실패 → `:146`이 **확정적으로 실행**된다.
([추론] Aurora `Device.Activate()`가 `IsActivated`를 `DoActivate` 반환 뒤에 세운다는 전제. 바이너리라 미확인)
**③ `Livetimer` 한 변수가 두 의미를 겸한다.** `:130`은 에코 지연 1초 타이머, `:144`는 워치독 기준시각.
`updateLiveWait == true`인 1초 동안 워치독 판정이 스킵된다.
**④ `ReadBit`의 `.err`를 버린다.** `:135`가 `.data`만 취해
"PLC가 하트비트를 안 준다"와 "PC가 아직 못 읽는다"가 둘 다 `false`로 뭉개진다.
**⑤ 자동 해제가 없다.** `AlarmManager.cs:79-104`의 `Alarm()`은 중복 억제만 하고
짝이 되는 `ClearAlarm` 호출이 없어, 통신 복구 후에도 210002가 남는다.
---
## 5. 수정 방향 (**2026-08-10 적용 완료** — §7 참조)
**타이머 이동만으로도, `Livetimer` 초기화만으로도 부족하다.** ①과 ②가 독립된 결함이라
한쪽만 고치면 다른 쪽이 남는다. 두 줄이 같은 블록에 있어야 한다.
```csharp
protected override int DoActivate(IDeviceConfig config)
{
IsLogModePLCDetail = false;
_monitorPlcInterfaceMap.UpdateRateUS = 100 * 1000; // 설정만 남기고 Start() 제거
var res = base.DoActivate(config);
LoadMemoryMapFromFiles();
if (res != E1.OK) return res; // 실패 시 타이머 안 돎
beforeLiveStatus = false;
updateLiveWait = false;
Livetimer = DateTime.Now; // ← 추가: 기준시각 확보
_monitorPlcInterfaceMap.Start(); // ← 이동: 기준 확보 뒤 기동
return res;
}
```
`beforeLiveStatus = false` 기준값은 그대로 둬도 된다 — 실제 비트가 true면 첫 tick에서 토글로 잡혀
`Livetimer`가 갱신되고, false면 초기화된 기준으로 10초 유예가 걸려 양쪽 다 안전하다.
부수 효과: `Activate` 실패 시 타이머가 계속 도는 문제, [추론] `LoadMemoryMapFromFiles()` 중 타이머가
같은 맵을 읽는 겹침이 함께 사라진다.
**이 수정으로 해결되지 않는 것:** ③·④·⑤, 그리고 **B(`SeqAlarmMonitor.cs:182`)의 2초 레벨 워치독**.
B가 같이 올리고 있다면 A만 고쳐서는 증상이 없어지지 않는다 → 그래서 §6을 먼저 한다.
---
## 6. 진단 계측 (2026-08-10 적용, 임시)
목적: 기동 시 210002가 **A에서 오는지 B에서 오는지 확정**한다.
| 파일 | 위치 | 내용 |
|---|---|---|
| `WeldingCommon/Devices/PLC/PLCController.Config.cs` | 필드부 / `LiveStatusUpdate` 알람 직전 | `[진단210002][A:PLCController.LiveStatusUpdate]` |
| `Machines/.../Sequences/Monitoring/SeqAlarmMonitor.cs` | 필드부 / `CheckAlarmPLC` 알람 직전 | `[진단210002][B:SeqAlarmMonitor.CheckAlarmPLC]` |
- 두 곳 모두 **최초 1회만** 기록한다(`_diagLoggedPlcDisconnect` / `_DiagLoggedPlcDisconnect` 플래그).
100ms·무지연 루프라 플래그가 없으면 로그가 폭주한다.
- 알람 동작 자체는 손대지 않았다 — 발생 경로만 식별한다.
### 계측 결과 — 2026-08-10 16:12 현장 로그 (원인 확정)
```
16:12:25.903 MicroTimer: timer interval: 100 [ms] ← 타이머 기동
16:12:25.904 E PLCController: [210002][A:PLCController.LiveStatusUpdate]
Livetimer=0001-01-01 00:00:00.000,
elapsed=63921975145904ms,
isLive=False, beforeLiveStatus=False,
IsActivated=False, IsConnected=False
```
| 항목 | 값 | 확정된 사실 |
|---|---|---|
| `Livetimer` | `0001-01-01` | `DateTime.MinValue` — **§4-① 미초기화 확정** |
| `elapsed` | 63,921,975,145,904 ms (≈2026년) | 임계값 조정 무효 확정 |
| `IsActivated`·`IsConnected` | 둘 다 `False` | **§4-② 연결 확립 전 발생 확정.** `IsActivated`가 `DoActivate` 반환 뒤에 세팅된다는 `[추론]`도 함께 검증됨 |
| `isLive` = `beforeLiveStatus` | 둘 다 `False` | 토글 미감지 → `:138` 미실행 → `Livetimer` 영구 `MinValue` |
| `[B:...]` 로그 | 미출현 | 시퀀스 계층(`SeqAlarmMonitor`)은 이 건과 무관 |
**추가 확정 — 타이머 첫 콜백은 즉시 발생한다.** 기동 로그 `.903` → 알람 `.904`, 즉 약 1ms.
`MicroTimer`는 첫 tick을 주기(100ms) 뒤가 아니라 `Start()` 직후에 쏜다. 따라서 §4-②에 달았던
"`base.DoActivate()`가 100ms를 넘으면"이라는 조건은 불필요하다 — **`Start()` 직후 무조건 1회 발생하는
결정적 버그**이며 경합이 아니다.
이로써 §5의 두 조치가 각각 단독으로는 무효임이 확정된다:
- `Livetimer` 초기화만(`:195`) → 타이머는 `:185`에서 이미 즉시 발화 → 알람
- `Start()` 이동만 → 이동 지점에서 즉시 발화, `Livetimer`는 여전히 `MinValue` → 알람
> **로그 검색 시 주의.** 로그 파일에서 한글이 깨진다(`[진단210002]` → `[ 210002]`, `위치:` → `ġ:`).
> `진단210002` 로는 안 잡히므로 **`210002][`** 로 검색할 것.
### 원복
**2026-08-10 원복 완료.** 두 파일의 `#region [진단계측 2026-08-10]` 블록 4개를 삭제했고
`.cs` 전체에서 잔존 0건을 확인했다. (검색에 걸리는 `[진단계측 2026-08-04]` 는 Recipe Save 건으로 별개다.)
---
## 7. 적용 내역 (2026-08-10)
**대상: `WeldingComponents/WeldingCommon/Devices/PLC/PLCController.Config.cs` 1개 파일,
`DoActivate()` 1개 메서드.** `SeqAlarmMonitor.cs` 등 다른 파일은 미변경.
수정 전/후를 `#region` 2개로 기록해 두었다 (검색: `260810 기동 오알람`).
| region | 위치 | 내용 |
|---|---|---|
| `... [수정 전]` | `:185-191` | 원래 `_monitorPlcInterfaceMap.Start();` 를 **주석으로 보존** |
| `... [수정 후]` | `:204-216` | `Livetimer = DateTime.Now;` + `_monitorPlcInterfaceMap.Start();` |
**원복 절차:** `[수정 후]` region 을 삭제하고 `[수정 전]` region 의 `Start()` 주석을 해제한다.
### 검증 결과 — **2026-08-10 현장 확인 완료**
빌드 후 재기동 → **기동 시 210002 미발생 확인.** 수정 유효.
### 이번 수정으로 바뀌지 않은 것
`10000` 임계값, 알람 발생 로직, §4-③(`Livetimer` 이중 용도)·④(`.err` 무시)·⑤(`ClearAlarm` 부재)는 그대로다.
확정된 원인(§4-①②)만 차단하는 최소 수정이다.
## 8. 잔여 과제
- §4-③④⑤ — `LiveStatusUpdate` 재작성이 필요해 범위가 커짐. 별건으로 판단
- `SeqAlarmMonitor.cs` — `WaitTime` 무지연 루프(`:145-148`), Laser/Chiller 즉시 알람(grace 없음),
`:190-193` `_Laser` null 시 NRE 위험, `InitDev` 1회 실행. 이번 건과 무관하나 미해결
- `AlarmMonitoring.cs` — 죽은 코드. 정리 대상
- `Light` 디바이스 `SerialPort.Open()` 실패
(`16:12:25.875 E EquipmentSetup: Light connect fail.`) — 본 건과 무관, 별도 확인 필요