feat: 레이저 출사중 표시 및 출사조건 로그 (ISSUE-260904-01)

메뉴얼/레시피 화면에 DI Laser_EMISSION_ON(10) 기반 출사중 램프 추가. 260904 현장 실측으로 DI 10 이 DO 9 의 에코가 아닌 실제 발진 피드백임을 확증하여 LaserIOControl 주석도 정정. 출사조건 스냅샷은 [LaserCond] 로그로 4개 시점(ShotOn/Heartbeat/ShotOff/DoorForceOff)에 기록. 표시·기록 전용이며 출사 차단 로직은 변경 없음.
This commit is contained in:
노도연 2026-09-04 14:13:13 +09:00
parent 7c974628d7
commit 3b8e64b9be
6 changed files with 487 additions and 4 deletions

View File

@ -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
> <view:BoolConverter x:Key="convTrueVisible" TrueValue="Visible" FalseValue="Hidden"/>
> ```
>
> `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 주석 정정 | 노도연 |

View File

@ -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).

View File

@ -25,12 +25,17 @@
</behavior:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<!-- [출사 표시 2026-09-04] 0행 = Laser Emission 램프.
맨 위에 둔 이유: 아래 Marking On The Fly 그룹이 convTrueVisible(FalseValue=Hidden)
을 쓰기 때문에 미사용 설비에서도 화면 높이를 그대로 점유한다.
램프를 그 아래에 두면 패널 밖으로 밀려나 보이지 않는다(260904 현장 확인). -->
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<GroupBox Grid.Row="0" Header="Manual Laser Power Control" Margin="0 3 3 0">
<GroupBox Grid.Row="1" Header="Manual Laser Power Control" Margin="0 3 3 0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
@ -129,7 +134,7 @@
</GroupBox>
<GroupBox Grid.Row="1" Header="Manual Laser Power Control" Margin="0 3 3 0" Visibility="Collapsed">
<GroupBox Grid.Row="2" Header="Manual Laser Power Control" Margin="0 3 3 0" Visibility="Collapsed">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
@ -218,7 +223,7 @@
<GroupBox Grid.Row="2" Header="Marking On The Fly" Margin="0 3 3 0" Visibility="{Binding IsUseMarkingOnTheFly, Converter={StaticResource convTrueVisible}}">
<GroupBox Grid.Row="3" Header="Marking On The Fly" Margin="0 3 3 0" Visibility="{Binding IsUseMarkingOnTheFly, Converter={StaticResource convTrueVisible}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
@ -298,5 +303,21 @@
</Grid>
</GroupBox>
<!-- [출사 표시 2026-09-04] 레이저 출사중 표시 (요청 260904-01)
DI Laser_EMISSION_ON(10) = 실제 발진 피드백(260904 현장 실측),
DO Laser_REMOTE(9) = Emission 게이트(260826 부터 쓰던 기존 표시 근거).
두 신호를 함께 보되 문구는 3단으로 나눈다. 표시 전용이며 출사를 차단하지 않는다. -->
<GroupBox Grid.Row="0" Header="Laser Emission" Margin="0 3 3 0" VerticalAlignment="Top">
<Border Margin="3" MinHeight="60" BorderBrush="Gray" BorderThickness="1"
Background="{Binding EmissionColor}">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{Binding EmissionText}" FontSize="20" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding EmissionSignalText}" FontSize="11"
Foreground="#FFDDDDDD" HorizontalAlignment="Center" Margin="0 2 0 0"/>
</StackPanel>
</Border>
</GroupBox>
</Grid>
</UserControl>

View File

@ -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); }
/// <summary>IO Device 지연 조회. 화면 생성 시점에 Device 등록이 끝나지 않았을 수 있다.</summary>
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:-";
}
/// <summary>200ms 주기로 출사중 램프를 갱신한다.</summary>
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;
}
}
/// <summary>
/// 출사 조건 스냅샷을 로그 1줄로 남긴다. 출사가 안 될 때 무엇이 걸렸는지 추적용.
/// 판정만 하고 아무것도 차단하지 않는다. 예외가 나도 출사 동작에 영향이 없도록 전부 감싼다.
/// </summary>
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

View File

@ -115,6 +115,8 @@
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<!-- [출사 표시 2026-09-04] 4행 = 레이저 출사중 박스 -->
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" MinHeight="30">
<TextBlock Margin="10 0" FontSize="12" Foreground="#FFBBBBBB" FontWeight="Bold" Text="Guid Beam Status" VerticalAlignment="Center" HorizontalAlignment="Right"/>
@ -141,6 +143,17 @@
<TextBlock Text="Laser Ready"/>
</StackPanel>
</Button>
<!-- [출사 표시 2026-09-04] 레이저 출사중 표시 (요청 260904-01)
DI Laser_EMISSION_ON(10) = 실제 발진 피드백. 위 Ready 박스(DO 9)와 다른 신호다.
메뉴얼 화면 램프와 같은 색 의미를 쓴다(출사중=적색). -->
<Border Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" MinHeight="32"
BorderThickness="1" BorderBrush="Gray" Margin="0 10 0 0"
Background="{Binding EmissionColor}">
<TextBlock Margin="10 0" FontSize="13" FontWeight="Bold" Foreground="White"
Text="{Binding EmissionText}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</Grid>
</GroupBox>

View File

@ -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 으로 빼서 진행되도록 함.