diff --git a/Doc/인수인계/260904_레이저출사표시_및_출사조건로그.md b/Doc/인수인계/260904_레이저출사표시_및_출사조건로그.md new file mode 100644 index 0000000..a19f1d7 --- /dev/null +++ b/Doc/인수인계/260904_레이저출사표시_및_출사조건로그.md @@ -0,0 +1,229 @@ +# 메뉴얼 화면 레이저 출사중 표시 및 출사조건 로그 (2026-09-04) + +> **목적** : 메뉴얼 조작 중 레이저가 실제로 출사되고 있는지 화면에서 즉시 알 수 있게 하고, +> 출사가 안 될 때 무엇이 걸렸는지 로그로 추적할 수 있게 한다. +> +> - 대상 설비 : PtoP 각형화 단동 용접기 (유니셀) +> - 이슈번호 : `ISSUE-260904-01` +> - 관련 문서 : `Doc/인수인계/260903_Door인터락_현장검증_가이드.html` (260902 Door 인터락) + +**표기 규칙** (기존 인수인계 문서와 동일) + +| 표기 | 의미 | +|---|---| +| 🟩 **소스확인** | PC 소스/설정 파일에서 직접 대조해 확인한 사실 | +| 🟦 **현장기록** | 현장 로그·시험 기록에 남아 있는 값 | +| 🟨 **미확보** | 아직 확인되지 않음. 이어받는 사람이 채워야 할 항목 | + +--- + +## 1. 한 줄 요약 + +`Manual > Scanner Operation` 화면에 **레이저 출사중 램프**를 추가했다. +판정은 **DI `Laser_EMISSION_ON`(10)** 과 **DO `Laser_REMOTE`(9)** 두 신호를 함께 보고, +출사 조건 스냅샷은 화면이 아니라 **로그(`[LaserCond]`)** 로 남긴다. + +> **출사 차단 로직은 한 줄도 바꾸지 않았다.** 표시·기록 전용이다. +> 차단은 260902 에 넣은 Door 인터락이 여전히 유일하다. + +--- + +## 2. 배경 + +현장 요청 두 가지였다. + +1. 메뉴얼에서 레이저 출사 시 IO EMISSION 이 점등되니, 그걸 화면에도 표시해 달라. +2. 출사가 안 되면 무엇이 체크되지 않았는지(도어락 OPEN, 알람 미해제 등) 알 수 있으면 좋겠다. + +2번은 **화면에 체크리스트를 띄우지 않고 로그로만** 남기기로 확정했다(260904 협의). +현장에서 이미 돌고 있는 코드라 화면·동작 변경을 최소화하는 쪽을 택했다. + +--- + +## 3. 판정 신호 두 개 + +| 신호 | 의미 | 근거 | +|---|---|---| +| DI `Laser_EMISSION_ON` = **10** | **실제 발진 피드백** | 🟦 260904 현장 실측 — 이 램프가 들어올 때 실제로 레이저가 출사 중이었다 | +| DO `Laser_REMOTE` = **9** | External Start = **Emission 게이트** | 🟩 레이저 38핀 매뉴얼. 🟦 260826 현장 검증 완료 | + +### 3.1 코드 주석 정정 🟩 + +`LaserIOControl.cs` 의 DI 10 주석에 **"DO Laser_REMOTE(External Start) 의 에코"** 라고 적혀 있었으나 +**오기**다. 260904 현장 실측으로 실제 발진 피드백임이 확인되어 주석을 정정했다(동작 무관, 주석만). + +> 260826 에 레시피 화면 `Laser Ready` 표시를 DI 10 대신 DO 9 되읽기로 정한 판단은 **그대로 유효**하다. +> DI 10 은 출사 중에만 뜨는 신호라 애초에 Ready 램프로는 쓸 수 없다. + +### 3.2 왜 두 신호를 같이 보나 + +DI 가 현장에서 어떻게 거동할지 확신이 없는 상태라, **이미 검증되어 돌고 있는 DO 9 표시를 빼지 않고** +새 DI 를 함께 얹었다. 램프 안에 `DI:ON / DO:ON` 을 같이 찍으므로 두 신호를 각각 대조할 수 있다. + +--- + +## 4. 화면 표시 규칙 + +`Manual > Scanner Operation` 우측 **최상단** `Laser Emission` 그룹박스. + +> **왜 최상단인가 (260904 현장 확인 후 이동)** 🟦 +> 처음에는 패널 맨 아래(비어 있던 4번째 행)에 뒀는데 현장에서 **화면에 보이지 않았다.** +> 원인은 `UCScannerLaserControl.xaml` 의 컨버터가 `Collapsed` 가 아니라 **`Hidden`** 이라는 것이다. +> +> ```xml +> +> ``` +> +> `Marking On The Fly` 그룹이 이 컨버터를 쓰는데 PTOP 은 `IsUseMarkingOnTheFly = false` +> (`EquipmentSetup.cs:297` 에서 `false` 주입) 라 **안 보이면서도 높이는 그대로 점유**한다. +> 그 아래 행은 패널 밖으로 밀려난다. → 램프를 0행으로 올려 다른 행 상태와 무관하게 항상 보이도록 했다. + +| DI 10 | DO 9 | 표시 문구 | 색 | +|---|---|---|---| +| ON | — | `레이저 출사중` | 적색 `#C62828` | +| OFF | ON | `출사 대기 (Remote ON)` | 주황 `#EF6C00` | +| OFF | OFF | `OFF` | 회색 `#3E3E42` | +| \- | \- | `IO 미연결` / `IO 읽기 실패` | 어두운 회색 `#2A2A2E` | + +단순 OR 로 "출사중"만 쓰면 레시피 화면에서 `Laser Ready` 버튼만 켜 둔 상태에서도 출사중으로 +보이기 때문에 문구를 3단으로 나눴다. + +**갱신 주기 200ms.** 기존 100ms `DispatcherTimer`(`OnMonitoring`)에 얹었고 2틱마다 읽는다. +PAIX·Ajin 드라이버 모두 비트 1개를 읽을 때마다 DI(DO) 배열 전체를 조회하므로 매 틱 읽지 않는다. + +> ⚠️ **화면을 벗어나면 타이머가 멈춘다.** 단, 직접 출사(`No Use Spot Time`) 중에는 260902 조치로 +> 타이머가 유지된다. 롱텀 출사 중에는 화면을 벗어나지 말 것. + +--- + +## 5. 출사조건 로그 + +### 5.1 형식 + +``` +[LaserCond][ShotOn(Percent)] Door=CLOSE(PLC=False,IO=False) EMO=False Alarm=없음 + DI8_READY=T DI9_ALARM=정상 DO9_REMOTE=T NoLaser=False SpotTime=True Busy=False + Model=U-Frame SeqTeaching=Idle +``` + +| 필드 | 의미 | 비고 | +|---|---|---| +| `Door` | PLC 비트 OR IO DI 4점 | 괄호 안이 계통별 값 | +| `EMO` | PLC EMO 비트 | | +| `Alarm` | 현재 발생 알람 코드 | **표시 전용 — 출사를 막지 않는다** | +| `DI8_READY` | 레이저 Ready (전원 ON & No error) | 전원만 들어오면 상시 T | +| `DI9_ALARM` | 레이저 알람 | **Active-Low** — `정상`/`알람` 으로 변환해 기록 | +| `DO9_REMOTE` | Emission 게이트 | F 면 출사 자체가 불가 | +| `NoLaser` | `No Laser (전압만)` 토글 | **T 면 발진신호를 생략** — 출사 안 되는 흔한 원인 | +| `SpotTime` | `Use Spot Time` 토글 | 아래 5.3 참고 | +| `Busy` / `Model` / `SeqTeaching` | 스캐너·모델·시퀀스 상태 | | + +### 5.2 찍히는 시점 + +| 태그 | 시점 | +|---|---| +| `ShotOn(Percent)` / `ShotOn(Watt)` | Laser On 버튼을 누른 즉시 (**Door 차단 여부와 무관하게 먼저**) | +| `Heartbeat` | 직접 출사 중 10초 주기 (기존 Door 하트비트에 편승) | +| `ShotOff` | Laser Off 실행 후 | +| `DoorForceOff` | Door Open 감지로 강제 OFF 한 직후 | + +`DoorForceOff` 로그의 `DI8/DI9/DO9` 값으로 **강제 OFF 가 실제로 먹었는지**를 신호로 증명할 수 있다. + +### 5.3 알아둘 것 — 출사 경로가 두 개다 🟩 + +Laser On 버튼은 `Use Spot Time` 토글에 따라 완전히 다른 코드로 갈린다. + +| | Use Spot Time (ON) | **No Use Spot Time (OFF) = 직접 출사** | +|---|---|---| +| 방식 | `SeqTeachingRun` 시퀀스 경유 | `LaserSignalOn()` 직타 | +| 자동 OFF | 있음 | **없음. Shot Off 누를 때까지 계속 출사** | +| 사전 검사 | Scanner Busy / Laser Ready / 시퀀스 중복 / Model 선택 | **Door 게이트가 유일** | + +파워측정·파워캘·롱텀은 전부 오른쪽이다. 260902 사고가 여기서 났다. + +--- + +## 6. 아직 확인되지 않은 것 🟨 + +| 항목 | 내용 | +|---|---| +| 적색(`레이저 출사중`) 표시 | 🟨 **아직 미확인.** 회색·주황은 확인됨(아래 §7 참고) | +| Door 인터락 극성 | 260902 조치의 DI/PLC 비트 극성이 **실장비 미검증**. 이 로그의 `Door=...(PLC=,IO=)` 로 검증 가능 | +| `_Laser.GetLaserReady()` | `Laser.json` 이 `IsSimul:true` 라 현장에서 항상 true 로 알려져 있음. `Use Spot Time` 경로의 Ready 검사가 실질 무력일 가능성 | + +--- + +## 7. 현장 검증 절차 및 결과 + +### 검증 결과 (2026-09-04) 🟦 + +| 단계 | 결과 | +|---|---| +| `OFF`(회색) | ✅ 확인 | +| `출사 대기 (Remote ON)`(주황) | ✅ 확인 | +| `레이저 출사중`(적색) | 🟨 미확인 | +| Door 강제 OFF 연동 | 🟨 미확인 | + +> **주황 확인의 의미** — `Laser Ready`(DO 9) 만 ON 인 상태에서 램프가 주황(`DI:OFF / DO:ON`)이라는 것은 +> **DI 10 이 DO 9 의 에코가 아님**을 뜻한다. 에코였다면 그 즉시 적색으로 갔어야 한다. +> §3.1 의 주석 오기 판정과 3단 표시 설계가 현장에서 확증됐다. + +### 절차 + +1. `Manual > Scanner Operation` 진입 → 램프가 `OFF` / `IO 미연결` 중 무엇인지 확인 +2. 레시피 화면에서 `Laser Ready` 버튼 ON → 램프가 **`출사 대기 (Remote ON)`(주황)** 으로 바뀌는지 + - 이때 `DI:` 가 `OFF` 여야 정상. `ON` 이면 DI 10 은 게이트 에코이며 3단 판정을 재검토해야 한다 +3. `Laser On` → 램프가 **`레이저 출사중`(적색)** 으로 바뀌는지 +4. `Laser Off` → 램프 복귀 확인 +5. 출사 중 문을 열어 강제 OFF 동작 + 램프 복귀 + `[LaserCond][DoorForceOff]` 로그 확인 +6. 로그에서 `[LaserCond]` 로 검색해 위 4개 태그가 모두 남는지 확인 + +--- + +## 8. 변경 파일 + +| 파일 | 변경 | +|---|---| +| `Views/Manual/Scanner/UCScannerLaserControl.xaml` | `Laser Emission` 그룹박스 신설 (**0행 = 최상단**). 기존 그룹박스 3개는 `Grid.Row` 번호만 +1 | +| `Views/Manual/Scanner/UCScannerLaserControlViewModel.cs` | 램프 프로퍼티 3개, `RefreshEmissionIndicator()`, `LogLaserCondition()` 추가 및 호출 5곳 | +| `Views/Recipes/UCScannerObjectTeachingControlBox.xaml` | `Laser Control` 그룹박스에 4행 추가 — 출사중 박스 1개 | +| `Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs` | `EmissionText`/`EmissionColor` + `SetEmission()`, 기존 `OnMonitoring` 3개 분기에 반영 | +| `Devices/Laser/LaserIOControl.cs` | DI 10 주석 정정 (동작 무관) | + +전부 `WeldingComponents/WeldingCommon/` 하위다. + +### 8.1 레시피 화면은 2단이다 + +`Recipe > Scanner Object Teaching` 의 `Laser Control` 그룹박스, `Laser Ready` 버튼 아래. + +| DI 10 | 표시 | 색 | +|---|---|---| +| ON | `레이저 출사중` | 적색 `#C62828` | +| OFF | `출사 없음` | 회색 `#3E3E42` | +| \- | `IO 미연결` | 어두운 회색 `#2A2A2E` | + +메뉴얼 화면의 주황(`출사 대기`) 단계가 없는 이유는, 이 화면에는 바로 위에 **`Laser Ready Status` 박스가 +이미 DO 9 를 초록으로 보여주고 있어** 중복이기 때문이다. 여기서는 출사중 여부만 본다. +색 의미는 메뉴얼 화면과 동일하게 맞췄다 — 같은 신호가 화면마다 다른 색이면 위험하다. + +신규 파일 없음 → **`.csproj` 수정 불필요.** +전부 `[출사 표시 2026-09-04]` 마커가 붙어 있어 원복 시 검색으로 찾을 수 있다. + +--- + +## 9. 하지 않은 것 (의도적 범위 밖) + +- **알람 미해제 시 출사 차단** — 표시·기록만 한다. 현재 메뉴얼 출사 경로에는 알람 검사가 전혀 없으며, + 차단을 넣으면 상시 알람이 떠 있을 때 파워캘·롱텀 작업이 막힐 수 있어 협의 후 진행하기로 했다. +- **직접 출사 분기에 Laser Ready / Busy 게이트 추가** +- **EMO 차단 추가** +- `OnMonitoring` 의 기존 결함 — 스캐너 미활성 시 `_MonitorTimer.Stop()` 후 `return` 이 없어 + 바로 아래 `GetEncoder()` 를 호출한다. 이번 건과 무관해 손대지 않았다. + +--- + +## 변경 이력 + +| 일자 | 내용 | 작성 | +|---|---|---| +| 2026-09-04 | 최초 작성. 출사중 램프 + `[LaserCond]` 로그 추가, DI 10 주석 정정 | 노도연 | diff --git a/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs b/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs index efd1789..a22a850 100644 --- a/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs +++ b/WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs @@ -69,7 +69,12 @@ namespace WeldingCommon.Devices.Laser // if (!ReadDiBit(Laser_ALARM)) → 알람 발생 Laser_ALARM, - // (Pin23) LASER_EMISSION_ON — Emission ON=High. DO Laser_REMOTE(External Start) 의 에코. + // (Pin23) LASER_EMISSION_ON — Emission ON=High. 실제 발진 중에 High 가 된다. + // ★ 260904 현장 실측 정정 — 종전 주석의 "DO Laser_REMOTE(External Start) 의 에코" 는 오기다. + // 이 신호가 들어올 때 실제로 레이저가 출사 중이었음을 현장에서 확인했다. + // → 메뉴얼 화면 "레이저 출사중" 램프의 판정 근거로 쓴다(UCScannerLaserControlViewModel). + // ※ 260826 에 레시피 화면 Laser Ready 표시를 DO Laser_REMOTE 되읽기로 정한 판단은 그대로 유효하다. + // 출사 중에만 뜨는 신호라 애초에 Ready 램프로는 쓸 수 없다. Laser_EMISSION_ON, // (Pin24) MAIN_PWR_STATE — 레이저 전원 투입 상태(Key Switch ON=High). diff --git a/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml b/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml index 178eef2..7106ca9 100644 --- a/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml +++ b/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml @@ -25,12 +25,17 @@ + + - + @@ -129,7 +134,7 @@ - + @@ -218,7 +223,7 @@ - + @@ -298,5 +303,21 @@ + + + + + + + + + + diff --git a/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs b/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs index 30d7efb..492a5fd 100644 --- a/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs +++ b/WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs @@ -1,6 +1,7 @@ using Aurora; using Aurora.Api; using Aurora.View; +using AuroraIO; using AuroraLaser; using AuroraLaserScanner; using CoreUnit.Control.UserControl.Canvas; @@ -18,6 +19,7 @@ using WeldingCommon.Models; using WeldingCommon.Sequences; using WeldingCommon.Settings; using WeldingCommon.Utils; +using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner; // 260824 IO 채널 enum 단일 소스 namespace WeldingCommon.Views { @@ -120,6 +122,10 @@ namespace WeldingCommon.Views private bool _IsManualEmitting = false; private DateTime _LastEmitHeartbeat = DateTime.MinValue; + // [출사 표시 2026-09-04] 레이저 출사중 램프 / 출사 조건 로그용 IO 핸들 + private IODevice _IODevice; + private int _EmissionTick = 0; + private readonly DispatcherTimer _MonitorTimer; private ISequence _SeqTeachingRun; @@ -193,6 +199,10 @@ namespace WeldingCommon.Views // 아래 스캐너 읽기에서 예외가 나더라도 차단 로직은 먼저 돌아야 하므로 맨 앞에 둔다. MonitorDoorInterlock(); + // [출사 표시 2026-09-04] 램프 갱신도 아래 스캐너 읽기보다 먼저 한다. + // 스캐너에서 예외가 나더라도 출사 여부 표시는 살아 있어야 한다. + RefreshEmissionIndicator(); + if (_Scanner.IsActivated == false) { Log.D(string.Format("[OnMonitoring] Check Scanner device Connect. ScannerLaserControl view OnMonitoring Stop")); MessageUtil.ShowWarningToast(string.Format("Check Scanner device Connect.")); @@ -278,6 +288,9 @@ namespace WeldingCommon.Views private void PerformCmdLaserShotOnPercent(object obj) { + // [출사 표시 2026-09-04] 차단 여부와 무관하게 누른 시점의 조건을 먼저 남긴다. + LogLaserCondition("ShotOn(Percent)"); + // [Door Interlock 2026-09-02] 두 분기(시퀀스 / 직접 출사) 공통 게이트 if (!CheckDoorInterlockBeforeEmit("LaserShotOn(Percent)")) return; @@ -367,6 +380,9 @@ namespace WeldingCommon.Views } private void PerformCmdLaserShotOnWatt(object obj) { + // [출사 표시 2026-09-04] 차단 여부와 무관하게 누른 시점의 조건을 먼저 남긴다. + LogLaserCondition("ShotOn(Watt)"); + // [Door Interlock 2026-09-02] 두 분기(시퀀스 / 직접 출사) 공통 게이트 if (!CheckDoorInterlockBeforeEmit("LaserShotOn(Watt)")) return; @@ -483,6 +499,9 @@ namespace WeldingCommon.Views Log.I("[DoorInterlock] Shot Off 실행 → 직접 출사 종료, 출사 중 Door 감시 해제"); _IsManualEmitting = false; } + + // [출사 표시 2026-09-04] Off 직후 조건 스냅샷. DI9_ALARM / DO9_REMOTE 가 실제로 떨어졌는지 확인용. + LogLaserCondition("ShotOff"); } #region [Door Interlock 2026-09-02] 메뉴얼 출사 Door 인터락 (ISSUE-260902-01) @@ -532,6 +551,12 @@ namespace WeldingCommon.Views _IsManualEmitting = false; Log.E("[DoorInterlock] 레이저 강제 OFF 완료. 재출사하려면 문을 닫고 Shot On 을 다시 누르십시오."); + + // [출사 표시 2026-09-04] 강제 OFF 가 실제로 먹었는지 확인용. + // DI Laser_EMISSION_ON 이 실제 발진 피드백이므로(260904 현장 실측), + // 이 로그에서 출사가 끊겼는지를 신호로 증명할 수 있다. + LogLaserCondition("DoorForceOff"); + MessageUtil.ShowErrorToast("Door Open 감지 → 레이저를 강제 OFF 했습니다. (Laser Interlock)"); } @@ -555,6 +580,156 @@ namespace WeldingCommon.Views _LastEmitHeartbeat = DateTime.Now; Log.I($"[DoorInterlock] 직접 출사 중... Door={(doorOpen ? "OPEN" : "CLOSE")}, " + $"DA1={AnalogOut1Voltage:F3}V, DA2={AnalogOut2Voltage:F3}V, {AlarmManager.GetDoorStateText()}"); + + // [출사 표시 2026-09-04] 롱텀 구간에도 조건 스냅샷을 같은 주기로 남긴다. + LogLaserCondition("Heartbeat"); + } + } + #endregion + + #region [출사 표시 2026-09-04] 레이저 출사중 표시 + 출사 조건 로그 (요청 260904-01) + // 요청: 메뉴얼 화면에서 레이저가 실제로 출사 중인지 눈으로 알 수 있게 할 것. + // 출사가 안 될 때 무엇이 걸렸는지도 남길 것(화면 아닌 로그로 — 사용자 확정 260904). + // + // 램프 판정 신호 2개: + // DI Laser_EMISSION_ON (10) : 실제 발진 피드백. + // ★ 260904 현장 실측 — 이 신호가 들어올 때 실제로 레이저가 출사 중이었다. + // LaserIOControl.cs 의 "DO Laser_REMOTE 의 에코" 주석은 오기이며 함께 정정했다. + // DO Laser_REMOTE (9) : External Start = Emission 게이트. + // 260826 부터 레시피 화면 "Laser Ready" 표시가 쓰는 비트로, 현장에서 이미 검증되어 + // 돌고 있는 기존 표시 근거다. DI 가 어떻게 뜰지 확신이 없으므로 기존 신호를 빼지 않고 + // 둘을 함께 본다(사용자 지시). + // + // 문구는 3단. 단순 OR 로 "출사중" 이라고만 쓰면 Laser Ready 버튼만 켜 둔 상태에서도 + // 출사중으로 보이기 때문이다. + // DI ON → "레이저 출사중" (적색) + // DI OFF, DO ON → "출사 대기 (Remote ON)" (주황) + // 둘 다 OFF → "OFF" (회색) + // + // ※ 표시·로그 전용이다. 출사 차단 로직은 한 줄도 바꾸지 않았다. + // (차단은 260902 Door 인터락이 유일. 알람/EMO 는 표시·기록만 하고 막지 않는다.) + + private const string COLOR_EMISSION_ON = "#C62828"; // 적색 - 실제 출사중 + private const string COLOR_EMISSION_WAIT = "#EF6C00"; // 주황 - Remote ON(출사 대기) + private const string COLOR_EMISSION_OFF = "#3E3E42"; // 회색 - OFF + private const string COLOR_EMISSION_NA = "#2A2A2E"; // IO 미연결/읽기실패 + + // 100ms 타이머 × 2 = 200ms 주기. + // PAIX/Ajin 드라이버 모두 비트 1개를 읽을 때마다 DI(DO) 배열 전체를 조회하므로 + // (AlarmManager 가 Door 판정에 80ms 캐시를 둔 것과 같은 이유) 매 틱마다 읽지 않는다. + private const int EMISSION_REFRESH_TICKS = 2; + + private string _EmissionText = "IO 미연결"; + public string EmissionText { get => _EmissionText; set => SetProperty(ref _EmissionText, value); } + + private string _EmissionColor = COLOR_EMISSION_NA; + public string EmissionColor { get => _EmissionColor; set => SetProperty(ref _EmissionColor, value); } + + // 두 신호를 각각 확인할 수 있게 램프 안에 같이 찍는다. DI 거동 현장 대조용. + private string _EmissionSignalText = "DI:- / DO:-"; + public string EmissionSignalText { get => _EmissionSignalText; set => SetProperty(ref _EmissionSignalText, value); } + + /// IO Device 지연 조회. 화면 생성 시점에 Device 등록이 끝나지 않았을 수 있다. + private IODevice ResolveIODevice() + { + if (_IODevice == null) { + try { + _IODevice = _Aurora.DeviceManager[CommonDeviceIds.IODevice] as IODevice; + } catch (Exception ex) { + Log.E($"[LaserEmission] IODevice 조회 실패: {ex.Message}"); + } + } + return _IODevice; + } + + private void SetEmissionNotAvailable(string text) + { + EmissionText = text; + EmissionColor = COLOR_EMISSION_NA; + EmissionSignalText = "DI:- / DO:-"; + } + + /// 200ms 주기로 출사중 램프를 갱신한다. + private void RefreshEmissionIndicator() + { + if (++_EmissionTick < EMISSION_REFRESH_TICKS) return; + _EmissionTick = 0; + + bool di, doRemote; + try { + var io = ResolveIODevice(); + if (io == null || !io.IsConnected()) { + SetEmissionNotAvailable("IO 미연결"); + return; + } + + di = io.ReadDI((int)ePrismaticDIAddress.Laser_EMISSION_ON); + doRemote = io.ReadDO((int)ePrismaticDOAddress.Laser_REMOTE); + } catch (Exception ex) { + Log.E($"[LaserEmission] IO 읽기 실패: {ex.Message}"); + SetEmissionNotAvailable("IO 읽기 실패"); + return; + } + + EmissionSignalText = $"DI:{(di ? "ON" : "OFF")} / DO:{(doRemote ? "ON" : "OFF")}"; + + if (di) { + EmissionText = "레이저 출사중"; + EmissionColor = COLOR_EMISSION_ON; + } else if (doRemote) { + EmissionText = "출사 대기 (Remote ON)"; + EmissionColor = COLOR_EMISSION_WAIT; + } else { + EmissionText = "OFF"; + EmissionColor = COLOR_EMISSION_OFF; + } + } + + /// + /// 출사 조건 스냅샷을 로그 1줄로 남긴다. 출사가 안 될 때 무엇이 걸렸는지 추적용. + /// 판정만 하고 아무것도 차단하지 않는다. 예외가 나도 출사 동작에 영향이 없도록 전부 감싼다. + /// + private void LogLaserCondition(string tag) + { + try { + // Door 는 AlarmManager 의 80ms 캐시를 그대로 쓴다(추가 IO 읽기 없음). + bool doorOpen = AlarmManager.IsDoorOpen("ScannerLaserControl"); + bool doorPlc = AlarmManager.IsPlcDoorOpen(); + bool doorIo = AlarmManager.IsIoDoorOpen(); + bool emo = AlarmManager.IsPlcEmoAlarm(); + + string alarmText = "확인불가"; + try { + var codes = _Aurora.AlarmService?.GetCurrentAlarmCodes(); + alarmText = (codes == null || codes.Count == 0) + ? "없음" + : $"{codes.Count}건({string.Join(",", codes.Take(5))})"; + } catch (Exception ex) { + alarmText = $"조회실패({ex.Message})"; + } + + string diReady = "-", diAlarm = "-", doRemote = "-"; + var io = ResolveIODevice(); + if (io != null && io.IsConnected()) { + diReady = io.ReadDI((int)ePrismaticDIAddress.Laser_READY) ? "T" : "F"; + // DI Laser_ALARM 은 Active-Low — 정상 High, 알람 발생 시 Low + diAlarm = io.ReadDI((int)ePrismaticDIAddress.Laser_ALARM) ? "정상" : "알람"; + doRemote = io.ReadDO((int)ePrismaticDOAddress.Laser_REMOTE) ? "T" : "F"; + } + + bool busy = false; + try { busy = _Scanner != null && _Scanner.GetBusyStatus(); } catch { } + + string modelName = "-"; + try { modelName = EquipmentSpecificModelUtil.GetCurrentRecipeEquipmentSpecificModel().name; } catch { } + + Log.I($"[LaserCond][{tag}] Door={(doorOpen ? "OPEN" : "CLOSE")}(PLC={doorPlc},IO={doorIo}) " + + $"EMO={emo} Alarm={alarmText} " + + $"DI8_READY={diReady} DI9_ALARM={diAlarm} DO9_REMOTE={doRemote} " + + $"NoLaser={IsNoLaser} SpotTime={IsUseLaserAutoOffChecked} Busy={busy} " + + $"Model={modelName} SeqTeaching={_SeqTeachingRun?.RunState}"); + } catch (Exception ex) { + Log.E($"[LaserCond][{tag}] 조건 로그 실패: {ex.Message}"); } } #endregion diff --git a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml index 64bc148..6dd5571 100644 --- a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml +++ b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml @@ -115,6 +115,8 @@ + + @@ -141,6 +143,17 @@ + + + + + diff --git a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs index 4303bbe..51e0cc2 100644 --- a/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs +++ b/WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs @@ -271,6 +271,41 @@ namespace WeldingCommon.Views } } + #region [출사 표시 2026-09-04] 레이저 출사중 표시 (요청 260904-01) + // 메뉴얼 화면(UCScannerLaserControlViewModel)과 같은 신호·같은 색을 쓴다. + // 같은 신호가 화면마다 다른 색이면 위험하므로 의미를 일치시킨다. + // + // 판정: DI Laser_EMISSION_ON(10) = 실제 발진 피드백. + // ★ 260904 현장 실측 2건으로 확증 — ① 램프 점등 시 실제 출사 중이었음 + // ② Laser Ready(DO 9)만 ON 인 상태에서는 이 DI 가 OFF (에코가 아님) + // + // 이 화면은 바로 위 "Laser Ready Status" 박스가 이미 DO 9 를 보여주므로 + // 메뉴얼 화면의 주황(출사 대기) 단계는 중복이라 뺐다. 여기서는 출사중 여부만 본다. + private const string COLOR_EMISSION_ON = "#C62828"; // 적색 - 실제 출사중 + private const string COLOR_EMISSION_OFF = "#3E3E42"; // 회색 - 출사 없음 + private const string COLOR_EMISSION_NA = "#2A2A2E"; // IO 미연결/읽기실패 + + private string _EmissionText = "IO 미연결"; + public string EmissionText { get => _EmissionText; set => SetProperty(ref _EmissionText, value); } + + private string _EmissionColor = COLOR_EMISSION_NA; + public string EmissionColor { get => _EmissionColor; set => SetProperty(ref _EmissionColor, value); } + + private void SetEmission(bool? on) + { + if (on == null) { + EmissionText = "IO 미연결"; + EmissionColor = COLOR_EMISSION_NA; + } else if (on == true) { + EmissionText = "레이저 출사중"; + EmissionColor = COLOR_EMISSION_ON; + } else { + EmissionText = "출사 없음"; + EmissionColor = COLOR_EMISSION_OFF; + } + } + #endregion + private void OnMonitoring(object sender, EventArgs e) { // 260820: 아래 IO 블록은 미연결 시 return 하므로, PLC 지시 표시는 반드시 그 앞에서 갱신한다. @@ -293,6 +328,7 @@ namespace WeldingCommon.Views { IsGuidBeam = false; IsLaserReady = false; + SetEmission(null); // [출사 표시 2026-09-04] return; } @@ -315,12 +351,16 @@ namespace WeldingCommon.Views IsGuidBeam = _IODevice.ReadDO((int)ePrismaticDOAddress.Laser_GUIDE_BEAM_CONTROL); IsLaserReady = _IODevice.ReadDO((int)ePrismaticDOAddress.Laser_REMOTE); + // [출사 표시 2026-09-04] 실제 발진 피드백(DI 10). 위 두 줄과 달리 DO 가 아니라 DI 다. + SetEmission(_IODevice.ReadDI((int)ePrismaticDIAddress.Laser_EMISSION_ON)); + } catch (Exception ex) { Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}"); IsLaserReady = false; IsGuidBeam = false; + SetEmission(null); // [출사 표시 2026-09-04] } // TODO: PRIBAT_L1, PRIBAT_L2 는 Blow 의 상태 check 가 들어 있음. 장비 한정이므로 이를 check 하는 것을 modelItem 이나 setting 으로 빼서 진행되도록 함.