Compare commits
2 Commits
7c974628d7
...
bd6a7cd21f
| Author | SHA1 | Date | |
|---|---|---|---|
| bd6a7cd21f | |||
| 3b8e64b9be |
229
Doc/인수인계/260904_레이저출사표시_및_출사조건로그.md
Normal file
229
Doc/인수인계/260904_레이저출사표시_및_출사조건로그.md
Normal 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 주석 정정 | 노도연 |
|
||||
@ -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).
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 으로 빼서 진행되도록 함.
|
||||
|
||||
@ -153,7 +153,21 @@ namespace WeldingCommon.Views
|
||||
Log.I($"Activate Model Name : {modelname}");
|
||||
|
||||
var activeRecipe = _RecipeManager.ActivatedRecipe();
|
||||
RecipeHelper.SaveRecipe(activeRecipe.Name);
|
||||
|
||||
// [레시피 덮어쓰기 이슈] 오토/메뉴얼 전환 시 저장 제거
|
||||
// 활성 전환 직후 메모리는 아직 이전 레시피 내용이다(LASA 는 레시피를 메모리에 1개만 유지).
|
||||
// 그 상태에서 무조건 저장하면 새 활성 레시피 파일에 이전 내용이 덮어써진다(교차오염).
|
||||
// ※ 완전 제거가 아니라 신원이 일치할 때만 저장하도록 좁혔다.
|
||||
// OnRecipeEntryChanged 의 Copy/Rename 분기가 비어 있어(:118-121)
|
||||
// 이 저장이 새 레시피 폴더의 TeachingEditor 파일 생성을 겸했을 가능성이 있기 때문.
|
||||
// 스킵 로그는 현장 발생 빈도 확인용이다. 정상 운영(활성=선택)에서는 저장이 그대로 수행된다.
|
||||
var selectedNameOnActivate = RecipeHelper.SelectedRecipe.name;
|
||||
if (string.Equals(activeRecipe.Name, selectedNameOnActivate, StringComparison.OrdinalIgnoreCase)) {
|
||||
RecipeHelper.SaveRecipe(activeRecipe.Name);
|
||||
} else {
|
||||
Log.W($"[레시피 덮어쓰기 차단] OnActivatedRecipe 자동저장 스킵. " +
|
||||
$"활성=[{activeRecipe.Name}] 선택=[{selectedNameOnActivate}]");
|
||||
}
|
||||
|
||||
// Aurora에서 제공하는 이벤트(SYS_RECIPE_ACTIVATED)를 체인으로 사용하면 동작(Aurora가 저장 후 LASA가 저장하는)에 대한 우선순위가 보장이 안되기 때문에
|
||||
_Aurora.EventPublisher.Publish(new EventMessage("OnActivatedRecipe", CommonEventIds.EVENT_CHANGE_RECIPE_INFO, activeRecipe));
|
||||
@ -190,6 +204,25 @@ namespace WeldingCommon.Views
|
||||
MessageUtil.ShowErrorToast("Auto 상태에서 레시피 변경 불가", Log);
|
||||
return false;
|
||||
}
|
||||
|
||||
// [레시피 덮어쓰기 이슈] 활성 ≠ 선택이면 저장 차단
|
||||
// 저장 대상은 선택 레시피인데 화면 메모리는 활성 레시피 내용이라,
|
||||
// 그대로 저장하면 엉뚱한 번호 파일에 다른 레시피 내용이 덮어써진다.
|
||||
// 운영 원칙(활성 레시피 = 선택 레시피)이 지켜지면 이 분기는 발화하지 않는다.
|
||||
var activeOnSave = _RecipeManager.ActivatedRecipe();
|
||||
var selectedOnSave = RecipeHelper.SelectedRecipe.name;
|
||||
if (activeOnSave != null && !string.IsNullOrWhiteSpace(selectedOnSave) &&
|
||||
!string.Equals(activeOnSave.Name, selectedOnSave, StringComparison.OrdinalIgnoreCase)) {
|
||||
Log.E($"[레시피 덮어쓰기 차단] PreSaved 저장 차단. " +
|
||||
$"저장대상=[{recipeName}] 선택=[{selectedOnSave}] 활성=[{activeOnSave.Name}]");
|
||||
MessageUtil.CreateAndShowMessageDialog("레시피 저장 차단",
|
||||
$"활성 레시피와 선택된 레시피가 다릅니다.\n\n" +
|
||||
$" 선택 레시피 : {selectedOnSave}\n" +
|
||||
$" 활성 레시피 : {activeOnSave.Name}\n\n" +
|
||||
$"이대로 저장하면 다른 레시피 내용이 덮어써집니다.\n" +
|
||||
$"레시피 목록에서 저장할 레시피를 다시 선택한 뒤 저장하십시오.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (Exception ex) {
|
||||
@ -205,6 +238,23 @@ namespace WeldingCommon.Views
|
||||
return;
|
||||
}
|
||||
|
||||
// [레시피 덮어쓰기 이슈] 저장 직전 2중 방어.
|
||||
// PreSaved 가 먼저 막으므로 정상적으로는 도달하지 않는다.
|
||||
// 도달했다면 Aurora 가 PreSaved=false 를 무시했다는 뜻이라 이미 이상 상황이다.
|
||||
// ※ 조용히 스킵하면 사용자가 "저장됐다"고 오인하므로 반드시 알린다.
|
||||
var activeOnSaved = _RecipeManager.ActivatedRecipe();
|
||||
if (activeOnSaved != null &&
|
||||
!string.Equals(activeOnSaved.Name, RecipeHelper.SelectedRecipe.name, StringComparison.OrdinalIgnoreCase)) {
|
||||
Log.E($"[레시피 덮어쓰기 차단] OnSaved 저장 스킵. " +
|
||||
$"저장대상=[{recipeName}] 선택=[{RecipeHelper.SelectedRecipe.name}] 활성=[{activeOnSaved.Name}]");
|
||||
MessageUtil.CreateAndShowMessageDialog("레시피 저장 차단",
|
||||
$"활성 레시피와 선택된 레시피가 달라 저장하지 않았습니다.\n\n" +
|
||||
$" 선택 레시피 : {RecipeHelper.SelectedRecipe.name}\n" +
|
||||
$" 활성 레시피 : {activeOnSaved.Name}\n\n" +
|
||||
$"저장되지 않았습니다. 레시피 목록에서 저장할 레시피를 다시 선택한 뒤 저장하십시오.");
|
||||
return;
|
||||
}
|
||||
|
||||
RecipeHelper.SaveRecipe(RecipeHelper.SelectedRecipe.name);
|
||||
var ChangedRecipe = RecipeHelper.getRecipeInfo(RecipeHelper.SelectedRecipe.name);
|
||||
var diffs = new ObservableCollection<PropertyDiff>();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user