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
This commit is contained in:
노도연 2026-08-10 16:37:27 +09:00
parent b7437a00f7
commit e0e5416975
3 changed files with 328 additions and 1 deletions

View File

@ -182,7 +182,13 @@ namespace WeldingCommon.Devices
// MicroTimer 설정
_monitorPlcInterfaceMap.UpdateRateUS = 100 * 1000;
_monitorPlcInterfaceMap.Start();
#region 260810 기동 오알람(210002) 수정 — 모니터 타이머 기동 위치 [수정 전]
// [수정 전] 여기서 타이머를 기동하면 base.DoActivate() 로 PLC 연결이 열리기도 전에
// LiveStatusUpdate() 가 돌아 ALARM_DEVICE_PLC_DISCONNECT(210002) 오알람 발생.
//_monitorPlcInterfaceMap.Start();
//
// [수정 후] 아래 "[수정 후]" region 으로 이동 (base.DoActivate() 성공 + Livetimer 확보 뒤)
#endregion
var res = base.DoActivate(config);
@ -195,6 +201,20 @@ namespace WeldingCommon.Devices
beforeLiveStatus = false;
updateLiveWait = false;
#region 260810 기동 오알람(210002) 수정 — Livetimer 초기화 + 타이머 기동 [수정 후]
// 원인 : Livetimer 가 초기화되지 않아 DateTime.MinValue 인 상태로 LiveStatusUpdate() 가 돌면
// (Now - MinValue) 가 항상 10000ms 를 초과해 210002 가 발생한다.
// MicroTimer 의 첫 콜백은 주기(100ms) 뒤가 아니라 Start() 직후(약 1ms)라 100% 재현.
// 임계값 10000 을 키우는 방식으로는 해결되지 않는다(MinValue 앞에서는 모두 초과).
// 실측 : 2026-08-10 16:12:25.904 로그
// Livetimer=0001-01-01, elapsed=63921975145904ms, IsActivated=False, IsConnected=False
// 조치 : 기준시각을 확보한 뒤 모니터 타이머를 기동한다.
// 원복 : 이 region 을 삭제하고, 위 "[수정 전]" region 의 Start() 주석을 해제한다.
// 상세 : docs/분석_PLC210002_기동오알람_260810.md
Livetimer = DateTime.Now;
_monitorPlcInterfaceMap.Start();
#endregion
return res;
}

View File

@ -0,0 +1,217 @@
# 기동 시 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.`) — 본 건과 무관, 별도 확인 필요

View File

@ -0,0 +1,90 @@
# 작업일지 — 2026-08-10
## 1. PLC 기동 오알람 210002 — 원인 확정 및 해결
### 증상
프로그램 첫 기동 시 PLC 연결이 열리기도 전에 `ALARM_DEVICE_PLC_DISCONNECT`(210002) 발생.
연결은 곧 붙지만 알람은 남는다(자동 해제 없음). 시뮬 모드에서는 무발생.
### 210002를 올리는 경로가 2개였음
| | 위치 | 판정 | 임계 | 상태 |
|---|---|---|---|---|
| A | `PLCController.Config.cs:146` (디바이스, 100ms 타이머) | **엣지** — 비트가 바뀌어야 정상 | 10000ms | 활성 |
| B | `SeqAlarmMonitor.cs:182` (시퀀스) | **레벨** — 2초 안에 true면 정상 | 2000ms | 활성 |
| — | `AlarmMonitoring.cs:169` (공통 시퀀스) | — | — | **죽은 코드** |
- `AlarmCode_PTOP : AlarmCode`(`AlarmCode_PTOP.cs:10`) 상속이고 210002는 재정의 없음 →
**A와 B가 같은 코드**를 써서 화면만 봐서는 구분 불가.
- `AlarmMonitoring.Builder()` 호출처는 `SequenceManager.cs:83` 주석 한 줄뿐(등록 0건).
주석을 풀어도 동작하지 않는다.
### 근본 원인
`PLCController.Config.cs`
- `DateTime Livetimer`(`:33`)가 초기화되지 않아 `DateTime.MinValue`.
대입 지점은 토글 감지 시(`:138`) 한 곳뿐이라, 기동 직후에는 값이 들어가지 않는다.
→ `(Now - MinValue)`가 항상 10000ms를 초과 → 워치독 오판.
**임계값 10000을 20000으로 키우는 방식은 원리적으로 무효**(MinValue 앞에서는 어떤 값이든 초과).
- `_monitorPlcInterfaceMap.Start()`(`:185`)가 `base.DoActivate()`(`:187`)보다 **앞**에 있어
연결 확립 전에 타이머가 돌았다. `beforeLiveStatus` 리셋(`:195`)도 그보다 뒤.
### 진단 계측 (적용 → 결과 확보 → 원복 완료)
A/B 중 어느 쪽인지 확정하기 위해 두 곳에 임시 로그를 넣고 재기동. 현장 로그:
```
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` → 미초기화 확정.
- `IsActivated=False, IsConnected=False` → **연결 확립 전 발생 확정.**
사전에 `[추론]`으로 달아 뒀던 "Aurora가 `IsActivated`를 `DoActivate` 반환 뒤에 세팅한다"도 함께 검증됨.
- `[B:...]` 로그 미출현 → **A 단독**. 시퀀스 계층은 이번 건과 무관.
- 타이머 기동(`.903`)과 알람(`.904`) 간격이 1ms → `MicroTimer` 첫 콜백이 주기(100ms) 뒤가 아니라
`Start()` 직후에 발생. 경합이 아니라 **100% 재현되는 결정적 버그**였다.
계측 코드는 결과 확보 후 전량 원복(`.cs` 잔존 0건 확인).
> 로그 파일에서 한글이 깨진다(`[진단210002]` → `[ 210002]`, `위치:` → `ġ:`).
> 검색어는 `210002][` 를 쓸 것.
### 수정 (최소 수정)
`WeldingComponents/WeldingCommon/Devices/PLC/PLCController.Config.cs` 의
`DoActivate()` **1개 메서드만** 변경. `SeqAlarmMonitor.cs` 등 다른 파일 미변경.
- `_monitorPlcInterfaceMap.Start()` 를 `base.DoActivate()` 성공 이후로 이동
- `Livetimer = DateTime.Now` 로 기준시각 확보 후 타이머 기동
- **두 조치는 각각 단독으로는 무효** — 타이머만 옮기면 `Livetimer`가 여전히 `MinValue`,
초기화만 하면 타이머가 그 앞에서 이미 발화. 반드시 함께 적용해야 한다.
- 수정 전/후를 `#region` 2개로 기록해 원복 가능하게 함 (검색어: `260810 기동 오알람`)
부수 효과: `Activate` 실패 시 타이머가 계속 돌던 문제 해소.
[추론] `LoadMemoryMapFromFiles()` 중 타이머가 같은 맵을 읽던 겹침도 함께 사라진다.
### 검증
**현장 재기동 확인 — 기동 시 210002 미발생. 해결 완료.**
### 잔여 과제 (별건)
- `Livetimer` 가 에코 1초 타이머(`:130`)와 워치독 10초 기준시각(`:144`)을 겸용하는 구조
- `LiveStatusUpdate` 가 `ReadBit` 의 `.err` 를 버려 "PLC 미송신"과 "PC 미수신"이 구분 안 됨
- `AlarmManager.Alarm`(`AlarmManager.cs:79-104`)에 짝이 되는 `ClearAlarm` 이 없어
통신 복구 후에도 알람 잔존
- `SeqAlarmMonitor` — `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.`)
— 본 건과 무관, 미확인
### 산출
- `docs/분석_PLC210002_기동오알람_260810.md` (분석 상세)
- 커밋 대상: 위 분석 문서 + `PLCController.Config.cs`