diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Alarm/AlarmManager_PTOP.cs b/Machines/ILSOS_PrismaticRD_PTOP/Alarm/AlarmManager_PTOP.cs index 2012851..b949c17 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Alarm/AlarmManager_PTOP.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Alarm/AlarmManager_PTOP.cs @@ -12,6 +12,10 @@ namespace ILSOS_PrismaticRD_PTOP.Alarm { public class AlarmManager_PTOP : AlarmManager { + // [로그계측 2026-09-16] 기반 클래스의 AlarmLog 는 private 이라 파생 클래스에서 쓸 수 없어 별도로 둔다. + // (원복: 아래 region 과 함께 이 필드도 삭제) + private static readonly Logger AlarmLogPTOP = new Logger("ALARM"); + public AlarmManager_PTOP(IAuroraEnvironment aurora) : base(aurora) { @@ -23,6 +27,19 @@ namespace ILSOS_PrismaticRD_PTOP.Alarm // High Alarm 은 Sequence 정지 if (alarm.Level == AlarmLevel.H) { + #region [로그계측 2026-09-16] 정지 대상 ID 불일치 경고 (원복: 이 region 삭제) + // 아래 StopSequence 의 대상은 SeqAutoRun(구 Prismatic)인데, 실제 운전 중인 것은 + // SeqAutoRunPTOP 이다 (EquipmentSetup.cs:218 vs :225 / UCMainAutoRunViewModel.cs:101, 365). + // → H 알람이 떠도 자동운전은 멈추지 않는다. + // 식별자 1개 교체로 해결되지만, 지금까지 안 멈추던 것이 멈추기 시작하면 오정지로 라인이 설 수 + // 있고 IO 알람이 무력화(ISSUE-260828-03)되어 실제 발생 H 알람 목록이 확인되지 않았다. + // 그래서 동작은 그대로 두고, 어떤 H 알람이 실제로 뜨는지 목록만 먼저 확보한다. (ISSUE-260828-02) + AlarmLogPTOP.W($"[로그계측][중알람] H 알람 발생 - Code:{code}, Name:{alarm.Name} → " + + $"StopSequence 대상이 '{SequenceIds.SeqAutoRun}' 이라 " + + $"실제 운전 중인 '{SequenceIds.SeqAutoRunPTOP}' 는 정지하지 않는다 " + + $"(ISSUE-260828-02 / 현재 동작 무변경)"); + #endregion + StopSequence(SequenceIds.SeqAutoRun); } } diff --git a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs index c11a130..78c61eb 100644 --- a/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs +++ b/Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs @@ -145,6 +145,18 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences const int RESYNC_TIMEOUT_SEC = 10; #endregion + #region [로그계측 2026-09-16] 자동운전 관측용 필드 + // 원복: 이 파일과 Alarm/AlarmManager_PTOP.cs 에서 "[로그계측 2026-09-16]" 표시가 붙은 것을 전부 삭제하면 된다 + // (이 region 포함, 현재 두 파일 합쳐 10곳 — region 5개 + 단일 라인 5개). + // 자동 전환 사전점검(Doc/인수인계/260916_자동운전_전환_사전점검.md)의 관측 항목을 남기기 위한 값들이다. + // 전부 기록 전용이며 분기·리턴·비트쓰기 어디에도 관여하지 않는다. + private static long _CycleCount = 0; // Enter 이후 누적 사이클(=페이지 용접) 수 + private TimeSpan _LastWeldSpan; // 직전 ExecuteList 소요(실제 용접 구간) + private double _LastGateOnMs; // 직전 Laser Gate ON→OFF 유지 시간 + private DateTime _GateOnTime; // Gate ON 시각(위 계산용) + private bool _ReqAlreadyOnAtEntry; // CheckPageNo 진입 시 REQ 가 이미 ON 이었는가 + #endregion + private eListNo _WeldingScannerListNo; private List _SelectedObjectList; @@ -324,7 +336,25 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences InitSequenceVariable(context); #endregion - // 장비 체크 + #region [로그계측 2026-09-16] 활성 레시피 페이지별 객체 수 1회 스캔 (원복: 이 region 삭제) + // 객체가 0개인 레이어를 PLC 가 지정하면 SetUploadWeldingList_SeqParams 가 시퀀스를 중단한다. + // 그 사실을 운전이 멈춘 뒤가 아니라 시작 시점에 알 수 있도록 페이지별 객체 수를 한 번만 남긴다. + // 읽기 전용이며, 조회가 실패해도 계측만 건너뛰고 운전은 그대로 진행한다. + _CycleCount = 0; + try + { + string pageSummary = Application.Current.Dispatcher.Invoke(() => + string.Join(", ", _ScannerCanvasUserCtrl.Model.Layers + .Select((layer, idx) => $"P{idx + 1}:{layer.Objects.Count()}"))); + Log.I($"[로그계측][Enter] 활성 레시피 페이지별 객체 수 — {pageSummary} (0 인 페이지는 그 페이지 지시 시 운전이 멈춘다)"); + } + catch (Exception ex) + { + Log.W($"[로그계측][Enter] 페이지별 객체 수 조회 실패({ex.GetType().Name}: {ex.Message}) — 계측만 생략, 운전에는 영향 없음"); + } + #endregion + + // 장비 체크 InitDev(context); _PLC.WriteBit(eBitOutput.PC_AUTO_STATUS, true); // 음극 (양극 P 미사용) @@ -691,6 +721,23 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences Log.I($"[CheckPageNo] PLC_PAGE_CHANGE_REQ 신호 대기 중..."); Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL); + #region [로그계측 2026-09-16] REQ 진입 시점 상태 (원복: 이 region 삭제) + // 이 노드는 REQ 가 ON 이면 통과하는 레벨 판정이다(아래 :726). 상승엣지를 보지 않으므로, + // CheckCycleStop 루프백 시점에 PLC 가 아직 REQ 를 내리지 않았다면 대기 없이 즉시 통과한다. + // 그 경우 같은 페이지를 다시 확정·용접할 수 있어, 엣지 판정 전환이 필요한지 가리기 위해 + // 진입 시점 값을 남긴다. 판정은 하지 않고 기록만 한다. + _ReqAlreadyOnAtEntry = false; + if (_PLC != null) + { + var reqAtEntry = _PLC.ReadBit(eBitInput.PLC_PAGE_CHANGE_REQ); + _ReqAlreadyOnAtEntry = reqAtEntry.err == Hermes.E1.OK && reqAtEntry.data; + if (_ReqAlreadyOnAtEntry) + Log.W($"[로그계측][CheckPageNo] 진입 시점에 REQ 가 이미 ON — 대기 없이 통과 (직전 사이클 #{_CycleCount})"); + else + Log.I($"[로그계측][CheckPageNo] 진입 시점 REQ OFF — 정상 대기 진입 (직전 사이클 #{_CycleCount})"); + } + #endregion + // 디버그 끝내고 삭제 //MessageUtil.ShowToast("CheckPageNo -> Wait..."); //Thread.Sleep(1000); @@ -1301,9 +1348,30 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences //Log.I($"[ExecuteWelding] Laser Gate ON (Core+Ring)"); #endregion + #region [로그계측 2026-09-16] 자동 출사 직전 Door 상태 (원복: 이 region 삭제) + // ★ 기록 전용이다. 차단하지 않는다. + // 자동 경로에는 Door 인터락이 없다(수동 경로에만 존재 — SeqComponentTeachingRun:285/624, + // UCScannerLaserControlViewModel:295/387). 그렇다고 여기서 바로 차단을 넣으면, + // Door 비트 극성이 실장비 미검증(ISSUE-260902-01 체크리스트 7번)이라 극성이 반대일 경우 + // 자동운전이 전면 차단된다. 먼저 판정값만 쌓아 극성을 확인한 뒤 차단 여부를 결정한다. + // IsDoorOpen 은 80ms TTL 캐시(AlarmManager.cs:304)라 사이클당 1회 호출은 IO 부담이 없고, + // caller 키가 분리돼 있어 수동 경로의 상태 전이 로그와 섞이지 않는다. + try + { + bool doorOpen = AlarmManager.IsDoorOpen("AutoRunExecuteWelding"); + Log.I($"[로그계측][DoorCond] 자동 출사 직전 판정 — DoorOpen:{doorOpen} / {AlarmManager.GetDoorStateText()} " + + $"(Recipe:{plcRecipeNo}, Page:{plcPageNo}, 사이클 #{_CycleCount + 1}) ※ 기록 전용 — 차단하지 않음"); + } + catch (Exception ex) + { + Log.W($"[로그계측][DoorCond] Door 상태 조회 실패({ex.GetType().Name}: {ex.Message}) — 계측만 생략, 출사에는 영향 없음"); + } + #endregion + // 260824 본 레이저 단일 발진기 — 제어권 요청 1발 (구 Core/Ring_Control 자리) _IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.Laser_REMOTE), true); Log.I($"[ExecuteWelding] Laser Gate ON (REMOTE)"); + _GateOnTime = DateTime.Now; // [로그계측 2026-09-16] Gate 유지시간 계산용 (원복: 이 줄 삭제) Thread.Sleep(laserEmissionGateDelayMs); // 260518 매뉴얼 기준 ≥10ms, 여유분 포함 =15ms 적용함. Log.I($"[ExecuteWelding] Gate ON Delay {laserEmissionGateDelayMs}ms 완료 → ExecuteList 시작"); @@ -1327,6 +1395,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences // 260824 본 레이저 단일 발진기 — 제어권 해제 1발 _IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.Laser_REMOTE), false); Log.I($"[ExecuteWelding] Laser Gate OFF (REMOTE)"); + _LastGateOnMs = DateTime.Now.Subtract(_GateOnTime).TotalMilliseconds; // [로그계측 2026-09-16] (원복: 이 줄 삭제) #endregion // 260626 Busy bit 위치 변경 - Laser On Display bit @@ -1335,6 +1404,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences Thread.Sleep(50); _WeldingSpanTime = DateTime.Now.Subtract(weldingTime); + _LastWeldSpan = _WeldingSpanTime; // [로그계측 2026-09-16] 이후 _WeldingSpanTime 은 다른 구간 측정에 재사용된다 (원복: 이 줄 삭제) Status.GetProcessingWeldingData().WeldingEndTime = DateTime.Now; @@ -1381,6 +1451,29 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences Log.I($"[CheckCycleStop] 사이클 중지 여부 확인 중"); Status.SetWorkingPosition(eWorkingPosition.CHECKING_CYCLE); + #region [로그계측 2026-09-16] 사이클 요약 1줄 (원복: 이 region 삭제) + // 노드별로 흩어진 값을 한 줄로 모은다. 현장 로그 대조 시간을 줄이기 위한 것이며 동작에 관여하지 않는다. + // PAGE_CMP: CheckPageNoComp 가 매 사이클 ON 으로 쓰지만(:1076) 이를 끄는 RestoreDeviceStatus 는 + // 아래 무조건 루프백 때문에 도달하지 않는다. 실제로 계속 ON 으로 latch 되는지 확인용. + // Resume : 아래 ResumeMode 해제 블록보다 앞에 두어야 해제 전 값이 찍힌다. + _CycleCount++; + try + { + int objCount = _SelectedObjectList == null ? -1 : _SelectedObjectList.Count(); + var pageCmp = _PLC.ReadOutBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE); + string pageCmpText = pageCmp.err == Hermes.E1.OK ? (pageCmp.data ? "ON" : "OFF") : "E"; + + Log.I($"[로그계측][사이클요약] #{_CycleCount} Recipe:{plcRecipeNo} Page:{plcPageNo}(Idx:{_CurrentPageNo}) " + + $"객체:{objCount} 용접:{_LastWeldSpan.TotalMilliseconds:F0}ms GateON:{_LastGateOnMs:F0}ms " + + $"PAGE_CMP:{pageCmpText} REQ진입시ON:{(_ReqAlreadyOnAtEntry ? "Y" : "N")} " + + $"Resume:{(ResumeMode ? "1" : "0")}"); + } + catch (Exception ex) + { + Log.W($"[로그계측][사이클요약] 요약 로그 실패({ex.GetType().Name}: {ex.Message}) — 계측만 생략, 운전에는 영향 없음"); + } + #endregion + // 260621: Resume 사이클 1회 완료 → 출력 bit15 OFF + ResumeMode 해제 (이후 정상 루프 복귀) if (ResumeMode) {