feat: bit15(MODECHANGE) 팝업+Auto→Manual+Resume 구현 및 가접 레시피/문서 정리

- PLCController.Address.cs: bit15 입출력 정의 (PLC/PC_MODE_CHANGE 0x0F, 양극 _P 0x73)
- SeqAutoRunPTOP: ResumeMode 플래그, 정지 시 에코워드(PC_PAGE_NO/RECIPE_NO) 보존,
  CheckInitCondition Resume 핸드셰이크 스킵, CheckCycleStop bit15 OFF/해제
- UCMainAutoRun(VM/XAML): bit15 상승엣지 감지→팝업→Manual 전환, AutoRun Resume 버튼(bit15 ON시 표시)
- docs: bit15 resume 작업지시서/분석/예상시나리오, 가접 레시피 인터페이스(20~23), 작업일지, 산출물 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
노도연 2026-06-21 13:17:55 +09:00
parent d5fc47c549
commit ff4fc03d2e
15 changed files with 621 additions and 657 deletions

View File

@ -24,7 +24,9 @@ namespace ILSOS_PrismaticRD_PTOP.Device
// 260618: M70509 Recipe 변경 요청 (구 PLC_RMS_USED, RMS 미사용으로 폐기 후 재정의)
PLC_RECIPE_CHANGE_REQ = 0x09,
// 0x0A ~ 0x0F
// 0x0A ~ 0x0E
// 260621: M70515 bit15 모드전환 요청 (Recipe/Page 확인 팝업 → Auto→Manual 잠시 전환)
PLC_MODE_CHANGE = 0x0F,
PLC_WELD_READY_REQ = 0x10,
PLC_WELD_WAIT = 0x11,
PLC_WELD_COMPLETE_ACK = 0x12,
@ -41,7 +43,9 @@ namespace ILSOS_PrismaticRD_PTOP.Device
PLC_DOOR_INTERLOCK_P = 0x6A,
PLC_EMO_STATUS_P = 0x6B,
// 0x6D
// 0x6F ~ 0x73
// 0x6F ~ 0x72
// 260621: M70615 bit15 모드전환 요청 (양극, 음극 PLC_MODE_CHANGE 0x0F 미러)
PLC_MODE_CHANGE_P = 0x73,
PLC_WELD_P_READY_REQ = 0x74,
PLC_WELD_P_WAIT = 0x75,
PLC_WELD_P_COMP_ACK = 0x76,
@ -60,7 +64,9 @@ namespace ILSOS_PrismaticRD_PTOP.Device
PC_PAGE_CHANGE_COMPLETE = 0x08,
// 260618: M70009 Recipe 변경 완료 응답 (구 PC_EMPTY01, 미사용 → 재정의)
PC_RECIPE_CHANGE_COMPLETE = 0x09,
// 0x0A ~ 0x0F
// 0x0A ~ 0x0E
// 260621: M70015 bit15 모드전환 ACK (PC→PLC, #2 AutoRun 복귀 시 전달)
PC_MODE_CHANGE = 0x0F,
PC_WELD_READY_ACK = 0x10,
PC_WELD_BUSY = 0x11,
PC_WELD_COMPLETE = 0x12,
@ -74,6 +80,8 @@ namespace ILSOS_PrismaticRD_PTOP.Device
PC_LIGHT_ALARM_STATUS_P = 0x68,
PC_HEAVY_ALARM_STATUS_P = 0x69,
// 260621: M70115 bit15 모드전환 ACK (양극, 음극 PC_MODE_CHANGE 0x0F 미러, #2 복귀 시 전달)
PC_MODE_CHANGE_P = 0x73,
PC_WELD_P_READY_REQ = 0x74,
PC_WELD_P_BUSY = 0x75,
PC_WELD_P_COMP_ACK = 0x76,

View File

@ -111,6 +111,9 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
private static TimeSpan _WeldingSpanTime; //TeacingRun 시퀀스에서 용접 소요시간
private int _CurrentPageNo;
// 260621: bit15 Resume — 정지 시 상태보존 + 재개 시 핸드셰이크 스킵 (VM·시퀀스 공유, 인스턴스 1개라 static)
public static bool ResumeMode = false;
private short plcPageNo = 0;// [0] 페이지 1 =음극 A [1]페이지 2= 양극 P(C)
private List<short> plcPageNoList = new List<short>();
@ -169,6 +172,7 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
> sb.Final);
sb.Flows(sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_CHECK_PAGENO]); // 루프백: 매 사이클 Page Change bit(0x08) 대기부터 재확인
sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ]); // 260621: Resume 시 핸드셰이크 스킵
sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_COMPLETE] > sb.Final);
sb.OnStopped(OnStopped);
sb.OnForceError(OnForceError);
@ -280,6 +284,11 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
// return SEQ_COMPLETE;
//}
Thread.Sleep(5);
if (ResumeMode)
{
Log.I("[CheckInitCondition] ResumeMode → 페이지 핸드셰이크 스킵, 용접리스트 업로드로 이동");
return SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ;
}
Log.I($"[CheckInitCondition] 조건 확인 완료 → CheckPageNo 이동");
return SEQ_CHECK_PAGENO;
}
@ -825,6 +834,15 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
Log.I($"[CheckCycleStop] 사이클 중지 여부 확인 중");
Status.SetWorkingPosition(eWorkingPosition.CHECKING_CYCLE);
// 260621: Resume 사이클 1회 완료 → 출력 bit15 OFF + ResumeMode 해제 (이후 정상 루프 복귀)
if (ResumeMode)
{
_PLC.WriteBit((int)eBitOutput.PC_MODE_CHANGE, false);
_PLC.WriteBit((int)eBitOutput.PC_MODE_CHANGE_P, false);
ResumeMode = false;
Log.I("[CheckCycleStop] Resume 사이클 완료 → PC_MODE_CHANGE OFF, ResumeMode 해제");
}
Log.I($"[CheckCycleStop] 사이클 계속 진행 → CheckPageNo 이동");
return SEQ_CHECK_PAGENO;
}
@ -1098,8 +1116,12 @@ namespace ILSOS_PrismaticRD_PTOP.Sequences
// 260512 워드
_PLC.WriteDWord((int)eWordOutput.PC_ERROR_CODE, 0);
_PLC.WriteDWord((int)eWordOutput.PC_PAGE_NO, 0);
_PLC.WriteDWord((int)eWordOutput.PC_RECIPE_NO, 0); // 260618 레시피 No init
// 260621: Resume 정지면 에코 워드(Page/Recipe No) 보존 — PLC가 정합 확인에 쓰므로 0으로 안 지움
if (!ResumeMode)
{
_PLC.WriteDWord((int)eWordOutput.PC_PAGE_NO, 0);
_PLC.WriteDWord((int)eWordOutput.PC_RECIPE_NO, 0); // 260618 레시피 No init
}
Log.I("[OnSeqActionStop] PLC IO Output Word Init");

View File

@ -18,6 +18,7 @@
<view:BoolConverter x:Key="convAlarmStatusColor" TrueValue="Red" FalseValue="ForestGreen"/>
<view:BoolConverter x:Key="convSequenceStatus" TrueValue="Process Running" FalseValue="Process Stop" />
<view:BoolConverter x:Key="convAlarmStatus" TrueValue="Alarm Error" FalseValue="No Alarm" />
<BooleanToVisibilityConverter x:Key="convBoolToVis"/>
</UserControl.Resources>
<behavior:Interaction.Triggers>
<behavior:PropertyChangedTrigger Binding="{Binding IsVisible, ElementName=uc}">
@ -67,21 +68,27 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"></ColumnDefinition>
<ColumnDefinition Width="1*"></ColumnDefinition>
<ColumnDefinition Width="1*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.Column="0" MinHeight="30">
<!-- 260621: bit15(MODECHANGE) 살아있을 때만 표시 — 누르면 PC→PLC bit15 ON + Auto 이어하기(Resume) -->
<Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Margin="3" FontWeight="Bold"
Content="AutoRun Resume" Style="{StaticResource StButtonAccentPurple}"
Command="{Binding CMD_AutoRunResume}"
Visibility="{Binding IsModeChangeActive, Converter={StaticResource convBoolToVis}}"/>
<Border Grid.Row="0" Grid.Column="1" MinHeight="30">
<TextBlock Margin="10 0" FontSize="12" Foreground="#FFBBBBBB" FontWeight="Bold" Text="AutoRun Status" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</Border>
<Border Grid.Row="0" Grid.Column="1" BorderThickness="1" BorderBrush="Gray" Margin="2">
<Border Grid.Row="0" Grid.Column="2" BorderThickness="1" BorderBrush="Gray" Margin="2">
<TextBlock Margin="10 0" FontSize="12" Foreground="#FFBBBBBB" FontWeight="Bold" Text="{Binding IsSequenceRunning_AutoRun, Converter={StaticResource convSequenceStatus}}" Background="{Binding IsSequenceRunning_AutoRun, Converter={StaticResource convSequenceStatusColor}}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</Border>
<Border Grid.Row="1" Grid.Column="0" MinHeight="30">
<Border Grid.Row="1" Grid.Column="1" MinHeight="30">
<TextBlock Margin="10 0" FontSize="12" Foreground="#FFBBBBBB" FontWeight="Bold" Text="Alarm Status" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</Border>
<Border Grid.Row="1" Grid.Column="1" BorderThickness="1" BorderBrush="Gray" Margin="2">
<Border Grid.Row="1" Grid.Column="2" BorderThickness="1" BorderBrush="Gray" Margin="2">
<TextBlock Margin="10 0" FontSize="12" Foreground="#FFBBBBBB" FontWeight="Bold" Text="{Binding IsAlarm, Converter={StaticResource convAlarmStatus}}" Background="{Binding IsAlarm, Converter={StaticResource convAlarmStatusColor}}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</Border>
</Grid>

View File

@ -43,6 +43,7 @@ namespace ILSOS_PrismaticRD_PTOP.Views
public ICommand CMD_RunSequenceAutoRun { get; }
public ICommand CMD_StopSequenceAutoRun { get; }
public ICommand CMD_Reset { get; } // Totoal Product, Today Product
public ICommand CMD_AutoRunResume { get; } // 260621: bit15 모드전환 후 Auto 이어하기(Resume)
private readonly IRecipeManager _RecipeManager;
private readonly ISettingEntry _SettingEntry;
@ -73,6 +74,14 @@ namespace ILSOS_PrismaticRD_PTOP.Views
public int CurrentPageNum { get => _CurrentPageNum; set => SetProperty(ref _CurrentPageNum, value); }
private int _CurrentPageNum;
// 260621: bit15(MODECHANGE) 팝업 — 상승엣지 래치 + 재진입 가드
private bool _prevBit15 = false;
private bool _isPopupOpen = false;
// 260621: bit15 살아있을 때만 [AutoRun Resume] 버튼 표시
public bool IsModeChangeActive { get => _IsModeChangeActive; set => SetProperty(ref _IsModeChangeActive, value); }
private bool _IsModeChangeActive;
public WeldingData CurrentWeldingData { get => Status.GetProcessingWeldingData(); }
private readonly DispatcherTimer _MonitorTimer;
@ -103,6 +112,7 @@ namespace ILSOS_PrismaticRD_PTOP.Views
CMD_RunSequenceAutoRun = new DelegateCommand(OnAutoRunStartRequest); // 260511 이곳 시퀸스
CMD_StopSequenceAutoRun = new DelegateCommand(OnStopSequenceAutoRun);
CMD_Reset = new DelegateCommand(PerformCmdReset);
CMD_AutoRunResume = new DelegateCommand(OnAutoRunResume);
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
@ -192,6 +202,66 @@ namespace ILSOS_PrismaticRD_PTOP.Views
InitializeTodayCount();
NowDateTime = DateTime.Now;
// 260621: PLC bit15(MODECHANGE) 상승엣지 → Recipe/Page 확인 팝업 → Auto→Manual 잠시 전환
if (_Plc == null || _Plc.IsSimul) return;
// 음극/양극 어느 쪽이 줘도 트리거 (AutoRun이 두 극을 함께 쓰는 구조와 동일)
bool bit15 = _Plc.ReadBit(eBitInput.PLC_MODE_CHANGE).data
|| _Plc.ReadBit(eBitInput.PLC_MODE_CHANGE_P).data;
IsModeChangeActive = bit15; // bit15 살아있을 때만 [AutoRun Resume] 버튼 표시
if (bit15 && !_prevBit15 && !_isPopupOpen)
{
short r = _Plc.ReadWord(eWordInput.PLC_RECIPE_NO).data;
short p = _Plc.ReadWord(eWordInput.PLC_PAGE_NO).data;
ShowRecipePageConfirmAndSwitchManual(r, p);
}
_prevBit15 = bit15;
}
// 260621: bit15 모드전환 — 정지(상태보존) → 안내 팝업 → Auto→Manual (작업자 가변 용접 수동 진행)
private void ShowRecipePageConfirmAndSwitchManual(short recipeNo, short pageNo)
{
_isPopupOpen = true;
try
{
// 정지 시 에코 워드(PC_RECIPE_NO/PC_PAGE_NO)·활성 레시피 보존 (OnSeqActionStop이 워드 0 안 씀)
SeqAutoRunPTOP.ResumeMode = true;
if (_SeqAutoRunPTOP.RunState == SequenceRunState.Running)
_SeqAutoRunPTOP.Stop();
// 정지 후 안내 팝업 (OK-only 모달, [확인]까지 블로킹). FindStrToken 우회 위해 MessageDialog 직접 호출.
string msg = $"Recipe No : {recipeNo}\nPage No : {pageNo}\n\n용접 위치 확인해주세요.";
Aurora.Api.Control.MessageDialog.Show(Application.Current.MainWindow, "용접 위치 확인", msg);
// [확인] 후 Auto → Manual (레시피는 이미 활성 → 재활성/롤백 안 함). 작업자가 가변 용접 직접 진행.
Status.SetStatus(eEqpStatus.Manual);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, false);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS_P, false);
Log.I($"[Bit15] ModeChange → 정지(보존)·Manual 전환 (Recipe:{recipeNo}, Page:{pageNo})");
}
finally { _isPopupOpen = false; }
}
// 260621: [AutoRun Resume] — PC→PLC bit15 ON + Auto 이어하기 (ResumeMode 유지 → 핸드셰이크 스킵)
private void OnAutoRunResume(object obj = null)
{
if (_Plc == null) return;
if (!SeqAutoRunPTOP.ResumeMode)
{
MessageUtil.ShowWarningToast("Resume 상태가 아닙니다.");
return;
}
// PC→PLC bit15(MODECHANGE) ON — resume run 신호 (한 사이클 완료 후 시퀀스가 OFF/해제)
_Plc.WriteBit(eBitOutput.PC_MODE_CHANGE, true);
_Plc.WriteBit(eBitOutput.PC_MODE_CHANGE_P, true);
// Auto 복귀 (ResumeMode=true → CheckInitCondition에서 핸드셰이크 스킵하고 업로드→용접)
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, true);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS_P, true);
Status.GetProcessingWeldingData().AutoRunStartTime = DateTime.Now;
RunSequenceAutoRun();
Log.I("[Bit15] AutoRun Resume → PC_MODE_CHANGE ON + Auto 이어하기(핸드셰이크 스킵)");
}
// PLC 비트에 의한 AutoRun 실행의 경우, PLC 인터페이스 이후 AutoRun 필요
@ -199,6 +269,7 @@ namespace ILSOS_PrismaticRD_PTOP.Views
{
if (!ApproveAutorunByUserOrAuto()) { return; }
SeqAutoRunPTOP.ResumeMode = false; // 일반 시작은 항상 핸드셰이크 포함 (resume 아님)
base.OnAutoRunStartRequest();
#region 팝업 띄우는 창과 함께 Bit false

View File

@ -0,0 +1,79 @@
# 제품 1개 용접 = Recipe n × Page m 사이클 — 예상 시나리오 (예시값)
- 작성일: 2026-06-21
- 근거: `SeqAutoRunPTOP.cs`(핸드셰이크/루프), bit15 Resume 구현, Recipe Number 시트(가접 20~23)
- 상태: **분석/예시** (소스 영향 없음). 페이지 수·bit15 정책은 **예시값** — 실제는 PLC/레시피 확정.
> 핵심: 시퀀스는 **무한 루프**(`CheckCycleStop`→`CheckPageNo`)로 계속 돈다. 매 루프 1회 = **(Recipe, Page) 1조합 = 페이지 1개 용접.**
> PLC가 매 사이클 `PAGE_CHANGE_REQ` + Recipe/Page 워드를 주고, 그 조합을 모두 돌면 **제품 1개 완료** → 다음 제품 반복.
---
## 1. 제품 1개 구성 (예시값)
U-Frame 1개 = 가접(Pre) + 본접, 전면/후면. (페이지 수는 예시)
| Recipe | 의미 | 페이지(예시) | bit15(가변 티칭) |
|---|---|---|---|
| 20 | Pre 가변 전면 가접 | 2 (P1,P2) | **O** (가변 → 작업자 티칭) |
| 21 | 전면 본접 | 3 (P1~P3) | X (정상 Auto) |
| 22 | Pre 가변 후면 가접 | 2 (P1,P2) | **O** |
| 23 | 후면 본접 | 3 (P1~P3) | X |
→ **제품 1개 = 2+3+2+3 = 10 페이지-사이클** (= 시퀀스 10바퀴). bit15 Resume은 가변 페이지(20·22)에서 발생.
---
## 2. 사이클 진행표 (제품 1개, 예시)
| # | PLC가 주는 RecipeNo/PageNo | bit15 | PC 동작(노드 경로) |
|---|---|---|---|
| 1 | 20 / 1 | O | 핸드셰이크 → **bit15 정지·팝업 → Manual 티칭 → Resume(스킵→용접)** |
| 2 | 20 / 2 | O | 핸드셰이크 → bit15 정지·팝업 → Manual 티칭 → Resume 용접 |
| 3 | 21 / 1 | X | CheckPageNo→RecipeNoComp(20→21 전환)→PageNoComp→업로드→용접 |
| 4 | 21 / 2 | X | (동일 레시피) PageNoComp→업로드→용접 |
| 5 | 21 / 3 | X | 〃 |
| 6 | 22 / 1 | O | 레시피 전환(21→22) 후 bit15 정지·팝업 → Manual 티칭 → Resume 용접 |
| 7 | 22 / 2 | O | bit15 정지·팝업 → Manual 티칭 → Resume 용접 |
| 8 | 23 / 1 | X | 레시피 전환(22→23) → 업로드→용접 |
| 9 | 23 / 2 | X | 〃 |
| 10 | 23 / 3 | X | 〃 → **제품 1개 완료** |
| → | (다음 제품) 20 / 1 … | … | 루프백(`CheckCycleStop`→`CheckPageNo`) 계속 반복 |
> 레시피 전환은 `CheckRecipeNoComp`이 RecipeNo 변화 감지 시에만 ActivateRecipe(같으면 스킵). 페이지만 바뀌면 레시피 유지.
---
## 3. 정상 페이지 1사이클 (bit15 X) 신호 흐름
```
PLC: PAGE_CHANGE_REQ ↑ + RecipeNo/PageNo 워드
PC : CheckPageNo(REQ 감지) → CheckRecipeNoComp(필요시 ActivateRecipe + PC_RECIPE_NO 에코)
→ CheckPageNoComp(PageNo 3회검증 + PC_PAGE_NO 에코 + PC_PAGE_CHANGE_COMPLETE)
→ 업로드 → WaitWeldingStart → ExecuteWelding(용접) → CheckCycleStop → (루프) CheckPageNo
```
## 4. 가변 페이지 1사이클 (bit15 O) 신호 흐름
```
… 위 핸드셰이크로 Recipe/Page 적용·에코 완료 …
PLC: PLC_MODE_CHANGE(bit15) ↑
PC : OnMonitoring 상승엣지 → ResumeMode=true → Stop(에코워드 보존) → 팝업 "용접 위치 확인"
→ [확인] → Manual 전환(PC_AUTO_STATUS↓)
작업자: 가변 용접 위치 직접 티칭 + 수동 RunWelding
PC : [AutoRun Resume] → PC_MODE_CHANGE(bit15)↑ + PC_AUTO_STATUS↑ + Auto 재개
→ CheckInitCondition(ResumeMode) → 핸드셰이크 스킵 → 업로드(_CurrentPageNo)→용접
(※ 재용접분은 PLC가 레이저 차단)
→ CheckCycleStop: PC_MODE_CHANGE↓ + ResumeMode=false → 정상 루프 복귀
PLC: PC_MODE_CHANGE 수신 후 PLC_MODE_CHANGE↓
```
---
## 5. 정리 / 변수
- **제품 1개 완료 단위** = 그 제품에 필요한 모든 (Recipe×Page) 조합 = 예시 10사이클. 끝나면 다음 제품으로 무한 반복.
- **페이지 수는 가변**(`Layers.Count()`) — 레시피별 1~5 등 자유. 위 2/3/2/3은 예시.
- **bit15 발생 정책 미확정**: 가변 페이지 "매 페이지마다" vs "가변 레시피당 1회" — 위 표는 매 가변 페이지 가정. PLC와 확정 필요.
- **레시피 전환 race**: 전환 사이클은 `ActivateRecipe`→`OnActiveReloaded`(동기 Layers 재로드)로 stale 방지(기구현).
- 타이밍차트(임시): `UnTrackedLocalFiles/hmi_mockups/bit15_timing_chart.png`.

View File

@ -0,0 +1,79 @@
# 작업일지 — 2026-06-21
## 가접 → 레시피 추가 방식 확정 (시퀀스 무수정)
- 가접을 시퀀스로 추가하지 않고, **레시피 번호만 추가**해서 프로그램상 처리하기로 함.
- 20번 PRE가접 전면용접 / 21번 U-Frame 전면 용접 / 22번 PRE가접 후면용접 / 23번 U-Frame 후면 용접
- 어차피 현재 시퀀스 그대로 가도 되는 이유: **레시피 활성화하고 페이지별로 용접하는 행위는 똑같으니까.**
- 코드 검증 결과: 레시피 조회가 `.Id` 1:1(하드코딩 없음), 페이지 검증도 `Layers.Count()` 가변 → **시퀀스 노드 수정 불필요. 레시피 데이터만 추가하면 됨.**
## Recipe/Page 핸드셰이크 = 이미 구현·커밋 완료 확인 (커밋 25edd79)
- PLC가 **전환마다 RECIPE_REQ는 안 주고, 양쪽에서 PAGE_REQ만** 주기로 함.
- 그래서 동작은: **Page change 신호만 체크 → 체크되면 RecipeNum/PageNum WORD값 읽기 → RecipeNum 체크해서 활성화 → 페이지넘버 받고 → 페이지 신호 ACK 송신.**
- 확인 결과 이 설계가 이미 `SeqAutoRunPTOP.cs`에 들어가 커밋(`25edd79 feat: Page Change bit로 Recipe+Page 전환 통합`)되어 있음.
- `CheckPageNo`: `PLC_PAGE_CHANGE_REQ`만 대기 (RECIPE_REQ 미감시)
- `CheckRecipeNoComp`: `PLC_RECIPE_NO` 읽기 → 0/미등록/현재활성과 동일이면 전환 스킵, 다를 때만 `ActivateRecipe` + `Dispatcher.Invoke(RecipeHelper.OnActiveReloaded())`로 Model.Layers 동기 재로드(레시피축 stale/race 방지) + `PC_RECIPE_NO` 에코
- `CheckPageNoComp`: `PLC_PAGE_NO` 3회 검증 → `Layers.Count()` 범위 검증 → `PC_PAGE_NO` 에코 + `PC_PAGE_CHANGE_COMPLETE`(ACK)
- 레시피 COMP 비트는 미사용(워드 에코만), 구 데드코드(CheckRecipeNo peek 노드) 정리됨
- 남은 건 검증: 빌드 0오류 / 동일레시피 스킵·레시피변경 시 Layers 교체 후 용접 / 페이지수 다른 레시피 전환 / 핸드셰이크 PLC 정상 종결 / 실제 Laser 발진 정합.
## 기동 시 디바이스 연결 vs 알람체크 타이밍 분석 (소스 미수정, 분석단계)
- 증상: 맨 처음 프로그램 켰을 때 연결은 어느 정도 바로 붙는데, **알람체크가 너무 빨리 돌아서** 연결 확립 전에 알람이 뜸. 연결된 다음에 알람체크가 되면 될 것 같음.
- `EquipmentSetup.cs`에 자동 연결 시도 부분(`OnStartUp` → `ActivateDevices`) 있음. 등록된 Device들을 Activate한 뒤 AlarmManager(알람모니터)가 돌아야 하는데, 이 타이밍을 조작할 수 있는지 분석.
### 분석 결론
- **순서 자체는 이미 "활성화 먼저 → 알람모니터"로 맞음.** `OnStartUp`은 `await ActivateDevices()`(모든 `device.Activate()` 완료 대기) 후에 `SeqAlarmMonitor.Start()` 호출(EquipmentSetup.cs:92→105).
- **진짜 갭은 "Activate ≠ Connected"**: `Activate()`는 연결 개시까지만 보장. 알람 체크 코드가 `IsActivated`와 `GetConnected()`를 따로 봄(프레임워크가 활성화/연결 구분) → 첫 루프가 연결 확립 전 `GetConnected()=false`를 읽어 오알람.
- **비대칭**: PLC는 `PLC_LIVE_STATUS` 2000ms 타임아웃 폴링이라 grace 있음. **Laser/OCT/Chiller/LBAD는 `!GetConnected()` 즉시 알람(grace 없음)** → 기동 직후 오알람은 주로 이쪽.
### 타이밍 조작 레버 (우선순위)
1. **(권장) `SeqAlarmMonitor.StartNode`에 "연결 확립 대기" 게이트 추가** — 시퀀스 진입 1회 실행 지점에서 활성 디바이스 `GetConnected()`/`IsActivated`(PLC는 live bit) true까지 타임아웃 폴링 후 루프 진입. 연결 상태 기준이라 견고.
2. 비어 있는 `WaitTime` 노드(현재 no-op) 활용 — 손쉽지만 고정시간이라 추측값.
3. `OnStartUp`에서 연결 확인까지 await 후 `Start()` — 단 `Activate()`가 연결완료까지 동기인지 확인 필요.
4. `OnPostDeviceActivated` 훅 — "활성화"지 "연결완료"가 아니라 게이트로는 부적절.
### 적용 전 확인 포인트
- `device.Activate()`가 연결완료까지 동기인가, 개시만인가 (디바이스별: Laser OPCUA/IO, PLC MXComponent, Chiller 등).
## 10:19 — 가접 레시피 추가된 PLC 인터페이스 문서 확인
대상: `docs/PtoP 유니셀 용접기 PLC 인터페이스/유니셀 PtoP 단동기 Laser Interface_260621_가접용접추가.xlsx` (시트: M70K 비트 / D70K 워드 / Recipe Number)
### Recipe Number 시트 — 가접 레시피 추가 구조
- 11: 양극/음극 탭 비드 용접 (page 1~2, 기존)
- **20: Pre 가변 전면 용접 (page 1~5)** ← 신규, PRE(가접) 전면
- **21: 전면 용접 (page 1~5)** ← 신규, 본접 전면(U-Frame 전면)
- **22: Pre 가변 후면 용접 (page 1~5)** ← 신규, PRE(가접) 후면
- **23: 후면 용접 (page 1~5)** ← 신규, 본접 후면(U-Frame 후면)
- 31: 상면 용접 (page 1~2, 기존) / 32: 하면 용접 (page 1~2, 기존)
- → 가접을 시퀀스 아닌 **레시피 번호(20~23)** 로 추가, PRE(가접)→본접을 레시피 쌍으로 분리(전면 20→21, 후면 22→23).
### 신호 인터페이스 — 그대로(가접 추가로 신호 신설 없음), 커밋 25edd79 코드와 1:1 일치
- PAGE 핸드셰이크만 사용: `M70508`(-)/`M70608`(+) PLC→PC Page_CHG Request, `M70008`/`M70108` PC→PLC Page_CHG Complete(ACK)
- 레시피·페이지는 워드: `D70502` PLC_RecipeNo, `D70501` PLC_PageNo / 에코 `D70002` PC_RecipeNo, `D70001` PC_PageNo
- `M70509/M70009` Recipe_CHG Req/Comp 비트는 표에 있으나 현장 결정대로 게이트 미사용 → RecipeNo는 PAGE_REQ 시점 워드(D70502)로 읽음.
### 확인 필요
- 셀 표기 "Pre **가변** 전면/후면 용접" → 의도는 PRE **가접**(tack)일 텐데 오타인지 확인 필요.
- 20~23 페이지 수 미확정(표상 1~5). 코드는 `Layers.Count()` 가변이라 2↔5페이지 혼재 자동 대응.
- 음극(-)/양극(+) 비대칭: -측은 Page+Recipe CHG 둘 다, +측은 Page_CHG만(기존 단동기 구조, 가접이 바꾸지 않음).
## bit15(MODECHANGE) 기능 구현 — 팝업 + Auto→Manual + Resume (빌드 0오류)
작업지시서 2종(`bit15_resume_재진입_문제분석…`, `2026_06_21_bit15_resume_구현.md`) 기준 보완 구현. 보완항목 사용자 확정: 1=작업지시서, 2=(나)작업자 수동 RunWelding, 3=작업지시서, 4=작업지시서, 5=현행유지.
- **bit 정의**(PLCController.Address.cs): 입력 `PLC_MODE_CHANGE=0x0F(M70515)`/`PLC_MODE_CHANGE_P=0x73(M70615)`, 출력 `PC_MODE_CHANGE=0x0F(M70015)`/`PC_MODE_CHANGE_P=0x73(M70115)`.
- **#1 정지 보존**: `SeqAutoRunPTOP.OnSeqActionStop`에서 `ResumeMode`면 `PC_PAGE_NO/PC_RECIPE_NO=0` 쓰기 스킵(에코 보존).
- **#2(나)**: 확인 핸들러 = `ResumeMode=true`→Stop(보존)→팝업(`MessageDialog.Show` OK-only)→Manual 전환. 레시피 이미 활성이라 `ActivateRecipe/OnActiveReloaded` 제거. 가변 용접은 작업자가 Manual에서 직접.
- **#3 핸드셰이크 스킵**: `CheckInitCondition`에서 `ResumeMode`면 `SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ`로 분기(+Builder Flow 엣지).
- **#4 출력 bit15 수명**: `OnAutoRunReStart`에서 `PC_MODE_CHANGE ON`+Auto 재개, `CheckCycleStop`에서 한 사이클 후 OFF+`ResumeMode=false`.
- **#5**: 감지는 `UCMainAutoRunViewModel.OnMonitoring`(화면 타이머) 현행 유지.
- ReStart는 "자동운전 시작?" 확인창 없이 즉시 재개(전용 경로)로 변경.
- 재용접 중복: option2 채택, **PLC측이 레이저 차단**하기로 함.
- ReStart 버튼 = AutoRun 화면 Equipment Status 그룹 좌측, `IsModeChangeActive`(bit15 ON) 때만 표시.
### PLC 협의 대기 (기록)
- **출력 PC_MODE_CHANGE(bit15) false 처리 위치**: 현재 `CheckCycleStop`에서 OFF. **대안** — 시퀀스 1바퀴 후 다시 PageChange 신호를 보고 있을 때(다음 `PAGE_CHANGE_REQ`가 true로 들어오는 코드블록 안)에서 false 처리. PLC가 출력 bit15 클리어를 인식하는 타이밍과 맞춰야 하므로 **PLC측과 합의 후 위치 확정.**
- bit15 PLC 주소(M70515/M70615/M70015/M70115) 실제 맵 대조 필요.

View File

@ -0,0 +1,131 @@
# 작업지시서 — PLC bit15 Recipe/Page 확인 팝업 + Auto→Manual 전환 (#1)
- 작성일: 2026-06-21
- 대상: `Machines/ILSOS_PrismaticRD_PTOP/Views/Main/UCMainAutoRunViewModel.cs` (메인)
`Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs` (bit 정의)
- 재사용: `AuroraComponents/AuroraUIComponent/Dialog/MessageUtil.cs` → `CreateAndShowMessageDialog(title, message)`
- 관련: 가접 레시피 20~23 추가, `25edd79` Recipe/Page 핸드셰이크
- 현재 상태: **소스 미수정** (분석 완료). 본 지시서대로 구현 착수.
> 목적: PLC가 bit 15를 ON 하면 PC가 Recipe/Page 워드를 읽어 "용접 위치 확인" 팝업(OK-only)을
> 띄우고, 작업자가 [확인]을 누르면 Auto→Manual로 잠시 전환(해당 레시피/페이지로 이동, 가변 티칭 후
> RunWelding)한다. **UC 신규 제작 없이** 기존 `MessageUtil.CreateAndShowMessageDialog` 재사용.
> (#2 AutoRun 버튼 → 재Auto + PC→PLC bit15 전달은 별도 지시서)
---
## 0. 착수 전 확정 사항 (코딩 전 합의 필요)
- [ ] **C-1. bit 15 주소**: `eBitInput` 0x0A~0x0F 가 비어 있음. bit15(0x0F)을 신규 정의
(예 `PLC_RECIPE_PAGE_POPUP_REQ = 0x0F`). PLC측 M70 주소(M70515 등) 확정.
- [ ] **C-2. 확인 후 동작 세부**: [확인] → Auto 정지 + Manual 전환 + 해당 Recipe/Page로 이동까지.
RunWelding 은 작업자가 수동으로 실행(자동 호출 안 함) — 맞는지 확정.
- [ ] **C-3. FindStrToken 거동**: `CreateAndShowMessageDialog`는 title/message를 `FindStrToken`에
통과시킴. 값 보간된 message 가 그대로 표시되는지 런타임 1회 확인. (깨지면 토큰 미적용 경로 검토)
---
## 1. 작업 범위 (Scope)
### 1-1. 변경 대상
- `PLCController.Address.cs` `eBitInput`: bit15(0x0F) 신규 enum 추가 (C-1).
- `UCMainAutoRunViewModel.cs`:
- 필드: `private bool _prevBit15 = false;` (상승엣지 래치), `private bool _isPopupOpen = false;` (재진입 가드)
- `OnMonitoring`: bit15 감지 → 팝업 → 확인 후 모드 전환 로직 추가
- 헬퍼: `ShowRecipePageConfirmAndSwitchManual(short recipeNo, short pageNo)`
### 1-2. 재사용 (신규 X)
- `MessageUtil.CreateAndShowMessageDialog(string title, string message)` — OK-only 모달.
- 기존 `_Plc.ReadWord(eWordInput.PLC_RECIPE_NO / PLC_PAGE_NO)`.
- 모드 전환은 기존 `OnStopSequenceAutoRun` 패턴 참고하되 **RollbackRecipe 는 호출 안 함**.
---
## 2. 구현 절차 (Step)
### Step 1. bit15 정의 (C-1 확정 후)
- `eBitInput`에 `PLC_RECIPE_PAGE_POPUP_REQ = 0x0F` 추가. (한글 주석 UTF-8 유지)
### Step 2. OnMonitoring 에 트리거 추가 (상승엣지 + 재진입 가드)
```csharp
private void OnMonitoring(object sender, EventArgs e)
{
// (기존 날짜/시간 갱신 로직 유지)
if (_Plc == null || _Plc.IsSimul) return;
bool bit15 = _Plc.ReadBit(eBitInput.PLC_RECIPE_PAGE_POPUP_REQ).data;
if (bit15 && !_prevBit15 && !_isPopupOpen) // OFF→ON 1회만, 팝업 중복 차단
{
short r = _Plc.ReadWord(eWordInput.PLC_RECIPE_NO).data;
short p = _Plc.ReadWord(eWordInput.PLC_PAGE_NO).data;
ShowRecipePageConfirmAndSwitchManual(r, p);
}
_prevBit15 = bit15;
}
```
- ★ 모달이 떠 있는 동안에도 DispatcherTimer 가 재틱될 수 있으므로 `_isPopupOpen` 가드 필수.
### Step 3. 팝업 + 확인 후 모드 전환
```csharp
private void ShowRecipePageConfirmAndSwitchManual(short recipeNo, short pageNo)
{
_isPopupOpen = true;
try
{
string msg = $"Recipe No : {recipeNo}\nPage No : {pageNo}\n\n용접 위치 확인해주세요.";
MessageUtil.CreateAndShowMessageDialog("용접 위치 확인", msg); // ← [확인] 까지 블로킹
// ▼ 확인 후 실행: Auto → Manual 전환 (롤백 X)
if (_SeqAutoRunPTOP.RunState == SequenceRunState.Running)
_SeqAutoRunPTOP.Stop();
Status.SetStatus(eEqpStatus.Manual);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, false);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS_P, false);
// 해당 Recipe/Page 로 이동 (작업자 가변 티칭용). 롤백/Correction 변경은 하지 않음.
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == recipeNo);
if (recipe != null)
{
_RecipeManager.ActivateRecipe(recipe.Id, "Bit15 Recipe Confirm");
RecipeHelper.OnActiveReloaded(); // Model.Layers 동기 반영
// TODO(C-2): pageNo 페이지로 캔버스 이동 (UI 페이지 선택)
}
// RunWelding 은 작업자가 수동 실행 (C-2 확정대로 자동 호출 안 함)
}
finally { _isPopupOpen = false; }
}
```
- 주의: `OnStopSequenceAutoRun`과 달리 `RecipeHelper.RollbackRecipe()` / `SelectCorrectionTable(1)` **호출 금지**
(그 레시피/페이지에서 티칭해야 하므로).
### Step 4. (선택) 페이지 이동
- `pageNo` 페이지로 캔버스/콤보 선택 이동 — 기존 페이지 선택 경로 확인 후 적용 (C-2).
---
## 3. 검증 (Test)
- [ ] 빌드 0오류 (MSBuild Debug|x64).
- [ ] bit15 OFF→ON 1회에 팝업 **1번만** 표시 (재진입 가드 동작), ON 유지 중 재표시 없음.
- [ ] 팝업 message 에 Recipe No / Page No 값이 정상 표시 (FindStrToken 거동 확인, C-3).
- [ ] [확인] 후 Auto 정지 + Manual 전환 + 해당 Recipe activate(Layers 반영) 확인.
- [ ] 롤백/Correction 변경이 일어나지 않음 확인.
- [ ] 미등록 RecipeNo 수신 시 전환 스킵(현 동작 유지) 확인.
---
## 4. 리스크 / 주의
- **재진입(중복 팝업)**: 100ms DispatcherTimer 가 모달 nested loop 중 재틱 가능 → `_isPopupOpen` + 상승엣지 래치로 차단.
- **FindStrToken**: 값 보간 message 가 토큰 조회로 깨질 가능성(C-3). 런타임 확인 필수.
- **모드 전환 부작용**: `OnStopSequenceAutoRun` 의 롤백/Correction 로직을 그대로 복사하지 말 것.
- **Simul/미연결**: `_Plc == null || IsSimul` 가드로 조기 반환.
---
## 5. 완료 기준 (DoD)
- [ ] bit15 정의 + OnMonitoring 트리거 + 확인 후 Manual 전환, 빌드 0오류.
- [ ] 팝업 1회 표시 / 확인 후 모드·레시피 전환 런타임 확인.
- [ ] 작업일지 기록 + (승인 후) 커밋/푸시.
- [ ] #2(AutoRun 버튼 + PC→PLC bit15) 는 별도 지시서로 진행.

View File

@ -0,0 +1,118 @@
# 작업지시서 — bit15 기반 AutoRun "이어하기(Resume)" 구현
- 작성일: 2026-06-21
- 대상: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs` 외 (아래 1장)
- 근거 분석: `docs/분석/2026/06/21/bit15_resume_재진입_문제분석_작업PC전달.md`
- 현재 상태: **소스 미수정**. bit15 미구현. 본 지시서대로 구현.
> 시나리오: PLC가 `PAGE_CHANGE_REQ(0x08)` 처리·`PAGE_CHANGE_COMPLETE` 응답 **후** 입력 bit15 ON → 장비 정지
> + 팝업 표시. 정지 중 작업자 수동작업 → "이어하기" 버튼이 AutoRun 시작 + 출력 bit15 ON → 페이지
> 핸드셰이크 건너뛰고 **중단된 그 페이지를 업로드→용접부터 이어감**.
---
## 0. 확정 사항 (질의 응답 반영)
- **C-1. 비트 번호**: 입력 `0x0F`=M70515(PLC→PC, 정지요청), 출력 `0x0F`=M70015(PC→PC, 이어하기 신호). 둘 다 현재 `0x0A~0x0F` 예약공백(PLCController.Address.cs:27, 63).
- **C-2. 정지 감지**: **기존 상시 모니터에서 입력 bit15 폴링** → AutoRun 정지 + 팝업. (알람 정지 패턴과 동일, AlarmManager_PTOP.cs:19 참고)
- **C-3. 팝업**: 정보성 모달(확인 버튼), side effect 없음.
- **C-4. 출력 bit15 수명**: 이어하기 시작 시 ON → **한 사이클(용접완료) 후 OFF**.
---
## 1. 작업 범위 (변경 대상)
- `…/Device/PLC/PLCController.Address.cs` — 입력/출력 bit15 enum 추가
- `…/Sequences/SeqAutoRunPTOP.cs` — ResumeMode 플래그, OnSeqActionStop 보존 분기, CheckInitCondition resume 분기, 출력 bit15 OFF, Flow
- `…/Views/Main/UCMainAutoRunViewModel.cs` (+ View) — RollbackRecipe 스킵 분기, "이어하기" 버튼/커맨드
- 상시 모니터 시퀀스(우선 `SeqStatusMonitor` 검토) — bit15 폴링 + 정지 트리거 + 팝업
- 팝업: 기존 모달 유틸(`MessageUtil` 등) 재사용
> 플래그 명칭은 제안값(작업 PC 확정): 비트 `PLC_RESUME_STOP_REQ`/`PC_RESUME_RUN`, 상태 `ResumeMode`.
---
## 2. 구현 절차 (Step)
### Step 1. 비트 정의 추가
- `eBitInput`에 `PLC_RESUME_STOP_REQ = 0x0F` (M70515) 추가.
- `eBitOutput`에 `PC_RESUME_RUN = 0x0F` (M70015) 추가.
### Step 2. 정지 감지 (모니터 폴링)
- 상시 모니터에서 `ReadBit(PLC_RESUME_STOP_REQ)` 폴링.
- ON(엣지) 감지 시:
1. `ResumeMode = true` (Step 3 플래그) **먼저 set**
2. AutoRun이 Running이면 `SeqAutoRunPTOP.Stop()` 호출
3. 팝업 표시(Step 4) — `PLC_RECIPE_NO`/`PLC_PAGE_NO` 읽어 전달
- **엣지 처리**: bit15를 레벨로만 보면 정지/팝업 반복 → OFF→ON 엣지 1회만 반응(직전값 비교).
### Step 3. ResumeMode 플래그 + 정지 시 상태 보존
- `SeqAutoRunPTOP`에 `public bool ResumeMode { get; set; }` 추가(모니터·ViewModel·Sequence 공유).
- `OnSeqActionStop`(SeqAutoRunPTOP.cs:1078): `if (ResumeMode)`면
- `PC_RECIPE_NO=0`(:1102) · `PC_PAGE_NO=0`(:1101) **쓰기 스킵** (에코 보존)
- 나머지(용접비트/게이트 OFF/`StopExecution`+`JumpAbs`)는 **그대로** (안전)
- `OnStopSequenceAutoRun`(UCMainAutoRunViewModel.cs:328): `if (ResumeMode)`면 `RollbackRecipe()`(:345) **스킵**. (워드 보존과 반드시 짝 — 안 하면 워드≠활성레시피)
- 일반/알람/ForceError 정지는 `ResumeMode=false`라 기존대로 전체 리셋.
### Step 4. 팝업 (정보성 모달)
- 내용(고정):
```
Recipe Number : [PLC_Recipe_No]
Page Number : [PLC_Page_No]
용접 위치를 확인해주십시오.
```
- 값: `ReadWord(eWordInput.PLC_RECIPE_NO).data`, `ReadWord(eWordInput.PLC_PAGE_NO).data`.
- UI 스레드(`Dispatcher`)에서 표시, 확인 버튼만. side effect 없음.
### Step 5. "이어하기" 버튼
- AutoRun 화면에 신규 버튼/커맨드 추가.
- 동작: `if (!ResumeMode) { 경고 후 return; }` → `Status.SetStatus(Auto)` → `_SeqAutoRunPTOP.Start(param, OnCompleteAutoRunPTOP)`(UCMainAutoRunViewModel.cs:244 패턴) → `WriteBit(PC_RESUME_RUN, true)` → `PC_AUTO_STATUS(_P)` ON.
- 기존 정상 AutoRun 버튼은 시작 시 `ResumeMode=false`로 두어 처음부터 동작(영향 없음).
### Step 6. resume 재진입 분기 (핸드셰이크 스킵)
- `CheckInitCondition`(SeqAutoRunPTOP.cs:270, Decision):
- `if (ResumeMode) return SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ;` (페이지 핸드셰이크 스킵, 유지된 `_CurrentPageNo`로 업로드→용접)
- else `return SEQ_CHECK_PAGENO;` (기존, :284)
- `Sequence_Enter`의 `Status.GetProcessingWeldingData().Clear()`(:230)가 resume에 문제 없는지 확인(용접리스트 재업로드라 무방 예상).
### Step 7. 출력 bit15 OFF (사이클 완료 후)
- 이어붙인 페이지 용접 완료 직후 `WriteBit(PC_RESUME_RUN, false)` + `ResumeMode=false`.
- 위치: `ExecuteWelding` 완료부(SeqAutoRunPTOP.cs:819 직전) 또는 `CheckCycleStop` 진입 시. (`CheckCycleStop`→`CheckPageNo` 루프(:829)라 이후는 정상 운전 복귀)
- **현재 구현**: `CheckCycleStop` 진입 시 `if(ResumeMode)` 블록에서 `PC_MODE_CHANGE(_P) false` + `ResumeMode=false`.
- **[대안 — PLC 협의 필요]** 출력 bit15 false 처리를, 시퀀스 1바퀴 후 **다시 PageChange 신호를 보고 있을 때(다음 PAGE_CHANGE_REQ가 true로 들어오는 그 코드블록 안)** 에서 수행하는 방안. 즉 `CheckPageNo`/`CheckPageNoComp`의 REQ 수신 시점에 PC_MODE_CHANGE를 내려주는 식. → PLC가 출력 bit15를 언제 클리어로 인식하는지(에코/ACK 타이밍)와 맞춰야 하므로 **PLC측과 합의 후 위치 확정.**
### Step 8. Builder/Flow + 빌드
- CheckInitCondition→Upload 엣지 추가: `sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ]);`
- `Draw("SeqAutoRunPTOP.pdf")` 갱신 확인.
- MSBuild Debug|x64 **0 오류**.
---
## 3. 검증 (Test)
- [ ] 빌드 0 오류.
- [ ] 입력 bit15 ON → 정지 + 팝업(Recipe/Page 값 정확).
- [ ] 정지 후 `PC_RECIPE_NO`/`PC_PAGE_NO`가 **0으로 안 바뀜**, `RollbackRecipe` **미실행**(활성 레시피 유지).
- [ ] "이어하기" 버튼 → 핸드셰이크 스킵, 유지된 페이지로 업로드→용접 이어짐. 출력 bit15 ON→완료 후 OFF.
- [ ] 정상 AutoRun 버튼은 기존대로 처음부터(핸드셰이크 포함) 동작.
- [ ] **알람/일반 Stop은 기존대로 전체 리셋**(워드 0 + RollbackRecipe) — resume 보존 미적용(플래그 분리 확인).
- [ ] 페이지 용접 완료 후 라인 정합(실제 Laser 발진).
---
## 4. 리스크 / 주의
- **플래그 공유 동기화**(모니터↔ViewModel↔Sequence): 잘못되면 일반/알람 정지에도 보존이 적용됨 → 분리 검증 필수.
- **mid-weld 중 bit15**: 스캐너 리스트 중도취소(StopExecution+JumpAbs)로 라인단위 복구 불가 → **페이지 단위 재시작만** 가능. 시나리오상 page change 직후에만 온다는 전제.
- **팝업 모달 교착**: 시퀀스/모니터 스레드에서 직접 모달 금지 → `Dispatcher`로 UI 스레드 표시.
- **출력 bit15 OFF 누락** 시 PLC가 계속 resume로 인식 → Step 7 누락 주의.
- **팝업값 vs 적용값**: 정지 시점 값과 resume 시점 값이 다를 수 있음(그사이 PLC 변경). 필요 시 정지 때 읽은 값을 래치해 resume에 사용.
---
## 5. 완료 기준 (DoD)
- [ ] bit15 입력 정지+팝업, 출력 bit15 ON→완료 OFF, 모두 런타임 확인.
- [ ] resume 정지 시 레시피 에코·활성 레시피 보존, 일반/알람 정지는 기존 전체 리셋(분리) 확인.
- [ ] 이어하기 시 핸드셰이크 스킵하고 정합된 레시피×페이지로 이어 용접.
- [ ] 빌드 0 오류 + 작업일지 기록 + 커밋/푸시.

View File

@ -0,0 +1,97 @@
# bit15 Resume 재진입 문제 분석 — 작업 PC 전달용
- 작성일: 2026-06-21
- 대상 시퀀스: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPTOP.cs`
- 목적: PLC↔PC **bit15 기반 "이어하기(Resume)"** 시나리오 구현 시, AutoRun 재진입에서 깨질 지점과 권고 구현 방향 전달.
- 상태: **분석만** (소스 미수정). bit15는 현재 **미구현**.
---
## 1. 시나리오 (신호 타이밍)
1. PLC가 `PAGE_CHANGE_REQ(0x08)` + word(recipe/page) 송신.
2. PC가 recipe/page 적용·에코 후 `PC_PAGE_CHANGE_COMPLETE(0x08)` 송신.
3. **그다음** PLC가 입력 **bit15** ON → 장비 Stop.
4. 정지 중 작업자 수동 작업.
5. 신규 "이어하기" 버튼: AutoRun 시작 + PC→PLC **출력 bit15** ON → 중단 지점부터 이어서 용접.
> 핵심: bit15(stop)는 **페이지 체인지가 이미 끝난 뒤** 온다 → 정지 시점에 recipe/page는 **이미 적용·에코 완료** 상태.
---
## 2. 현 구조 핵심 사실 (확정)
- AutoRun 시작 = `_SeqAutoRunPTOP.Start(param, …)` (UCMainAutoRunViewModel.cs:244). Aurora `Start()`는 **Init 노드(맨 위)부터** 실행 → `Enter → CheckInitCondition → CheckPageNo`. **Resume/Pause 없음.**
- `CheckCycleStop`이 항상 `CheckPageNo`로 되돌아가는 무한 루프(SeqAutoRunPTOP.cs:829) → **정상 종료 노드 `Complete` 미도달.** 모든 세션은 Stop으로만 끝남.
- `SeqAutoRunPTOP` 인스턴스는 셋업 때 **1회만 생성**(EquipmentSetup.cs:203) → `_CurrentPageNo / plcPageNo / plcRecipeNo` 등 **C# 필드는 Stop 후에도 유지** (resume에 유리).
- bit15(=0x0F)는 입력/출력 맵 모두 `0x0A~0x0F` **예약 공백**(PLCController.Address.cs:27, 63) → 현재 미사용, 신규 정의 충돌 없음. (decimal 15=0x0F vs hex 0x15 여부만 PLC와 확정)
---
## 3. 재진입 시 문제점
### 3-1. Stop이 "이어가는 데 필요한 상태"를 전부 지움
`OnStopSequenceAutoRun`(UCMainAutoRunViewModel.cs:336) → `Stop()` → `OnStopped` → `OnSeqActionStop`(SeqAutoRunPTOP.cs:1078~):
- `PC_PAGE_NO=0`(:1101), `PC_RECIPE_NO=0`(:1102), `PC_ERROR_CODE=0`(:1100)
- 모든 용접 비트 false, `PC_PAGE_CHANGE_COMPLETE` false, `PC_AUTO_STATUS(_P)` false
- 레이저 게이트 OFF, **`_Scanner.StopExecution()` + `JumpAbs(0,0)`**(:1123-1124)
### 3-2. ★ 정지 시 레시피 롤백 — resume에 치명적
`OnStopSequenceAutoRun`이 `RecipeHelper.RollbackRecipe()`(:345). 내부 `Dispatcher.InvokeAsync(() => ReloadRecipe(SelectedRecipe.name))`(RecipeHelper.cs:104).
→ **실제 활성 레시피를 원래 레시피로 되돌림 + 비동기**. 정지 중 화면/Layers가 다른 레시피로 바뀜 → resume 기준이 어긋남.
### 3-3. PC_RECIPE_NO 에코 비대칭
- `PC_RECIPE_NO`는 **PC→PLC 출력 워드**(`eWordOutput`, D70002). PLC가 이 값을 읽어 레시피 정합을 확인 → **0이면 안 됨.**
- 정상 연속 운전 중엔 0 안 됨(마지막 에코값 유지). **0이 되는 건 Stop 때**(:1102).
- 재진입(같은 레시피)이면 `CheckRecipeNoComp`가 "읽은 레시피 == 현재 활성"이면 에코 없이 조기 return(:316-321) → **0인 채 남음.**
### 3-4. 페이지 요청을 "레벨"로 판정 (엣지 아님)
`CheckPageNo`는 `ReadBit(PLC_PAGE_CHANGE_REQ).data`를 레벨로만 봄(:378). 재진입 시 PLC가 0x08 유지면 즉시 통과(중복 처리), 안 주면 무한 대기(데드락).
### 3-5. 복구 단위 = "라인 단위 불가, 잘해야 페이지 단위"
`OnSeqActionStop`이 `StopExecution + JumpAbs(0,0)`로 스캐너 리스트 중도 취소(:1123) → 용접 중간 라인 복구 불가.
---
## 4. 결정 사항 (이번 시나리오)
- bit15 stop 시점엔 recipe/page **이미 적용 완료** → **추가 change 없음.**
- 팝업은 적용된 recipe/page를 **읽어 보여주는 read-only**.
- resume 재진입은 **페이지 핸드셰이크 스킵**, 용접리스트 업로드→용접부터 이어감.
---
## 5. 작업 PC 권고 구현 방향
1. **bit15 정지 구분 플래그 도입.** `OnSeqActionStop`은 일반/알람/ForceError 정지 공용이므로, "bit15 resume 정지일 때만" 아래 보존이 적용되게 분기. (알람 정지엔 미적용)
2. **bit15 정지 시 워드 보존:** `OnSeqActionStop`의 `PC_RECIPE_NO=0`(:1102) · `PC_PAGE_NO=0`(:1101) **쓰기 스킵.**
3. **`RollbackRecipe()` 스킵**(UCMainAutoRunViewModel.cs:345). 워드 보존과 **반드시 짝** — 안 하면 워드 ≠ 실제 활성 레시피.
4. **팝업(read-only):** 정지 시 recipe/page 읽어 작업자에게 표시. 실제 change 동작 없음.
5. **resume 재진입 분기:** `Start()`가 Init부터 타므로, bit15 플래그가 켜져 있으면 `CheckInitCondition`(:270, Decision)에서 `CheckPageNo`(:284) 대신 **`SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ`(:133)로 분기**해 핸드셰이크를 건너뛰고 업로드→용접부터. 페이지 컨텍스트는 유지된 `_CurrentPageNo` 사용.
---
## 6. 미해결 / 주의
- **mid-weld 복구 불가:** resume은 "중단 페이지 재시작" 단위. 용접 도중 멈춘 라인부터는 불가(3-5).
- **팝업값 ≠ 적용값 가능성:** 팝업은 정지 시점 값, 실제 진행은 resume 시점. 그사이 PLC가 또 바꾸면 불일치 → 정지 때 읽은 값을 **래치**해 resume에 쓰거나 "resume 시 재확인 후 적용"으로 정할 것.
- **bit15 번호 확정:** decimal 15(0x0F) vs hex 0x15 — 둘 다 현재 미사용. PLC 담당과 확정.
- **`Start()`=Init부터** 전제: 코드 구조상 확실하나 프레임워크 바이너리라 100% 확정은 아님. 필요 시 별도 확인.
---
## 7. 파일·라인 레퍼런스
| 항목 | 위치 |
|---|---|
| AutoRun 시작(Start) | UCMainAutoRunViewModel.cs:244 |
| Stop 버튼 + RollbackRecipe | UCMainAutoRunViewModel.cs:336-337, 345 |
| OnSeqActionStop(전체 리셋) | SeqAutoRunPTOP.cs:1078~ (워드 :1100-1102, 스캐너 :1123-1124) |
| CheckRecipeNoComp(에코 스킵/쓰기) | SeqAutoRunPTOP.cs:316-321, 350 |
| CheckPageNo(레벨 판정) | SeqAutoRunPTOP.cs:378 |
| CheckPageNoComp(에코/COMPLETE) | SeqAutoRunPTOP.cs:484, 491 |
| CheckCycleStop(루프백) | SeqAutoRunPTOP.cs:829 |
| Builder/Flow | SeqAutoRunPTOP.cs:122-179 |
| 인스턴스 1회 생성 | EquipmentSetup.cs:203 |
| RollbackRecipe 본체 | RecipeHelper.cs:104 |
| 비트/워드 맵 | PLCController.Address.cs (0x08 :23, 0x0F공백 :27/63, PC_RECIPE_NO :154) |

View File

@ -1,74 +0,0 @@
# 현장 Update 파일 — 변경 내역
> 어제(2026-06-17) 수정한 소스 파일들입니다. 현장 소스에 반영(머지)할 때 아래 **변경 부분만** 적용하면 됩니다.
> 이 폴더의 `.cs` 파일은 변경이 모두 반영된 **최신 전체 파일**이며, 참고/덮어쓰기용으로 함께 넣어두었습니다.
---
## 1. SequenceSetting.cs
- **원본 위치:** `WeldingComponents/WeldingCommon/Settings/SequenceSetting.cs`
- **네임스페이스/클래스:** `WeldingCommon.Settings.SequenceSetting`
- **커밋:** `9543238` — feat: SequenceSetting에 Axis Position Interlock 파라미터 추가
- **변경 요약:** 전축 위치 안전 인터락용 파라미터 4개 추가. 수직/수평 **기준값 + 허용오차(deg)** 단일 값 입력 속성.
추후 시퀀스에서 전축 위치 체크 후 안전 인터락 적용 예정.
### 변경 부분 (추가)
`GroupRamingUse` 속성 바로 아래에 추가:
```csharp
[DisplayName("Group Raming Use")]
public bool GroupRamingUse { get; set; } = false;
+
+ //260617 - 수직, 수평 + 허용오차 단일 값 입력하는 곳(시퀸스 돌 때 같이 입력해야할 것.)
+ [Category("Axis Position Interlock")]
+ [DisplayName("수직 기준값 [deg]")]
+ public double VerticalSector { get; set; } = 0.0;
+
+ [DisplayName("수직 허용오차 [deg]")]
+ public double VerticalTolerance { get; set; } = 0.0;
+
+ [DisplayName("수평 기준값 [deg]")]
+ public double HorizontalSector { get; set; } = 0.0;
+
+ [DisplayName("수평 허용오차 [deg]")]
+ public double HorizontalTolerance { get; set; } = 0.0;
```
---
## 2. UCScannerObjectTeachingControlBoxViewModel.cs
- **원본 위치:** `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs`
- **네임스페이스:** `WeldingCommon.Views`
- **커밋:** `9aa1b2a` — fix: Laser Ready 판정을 Core·Ring Control Status(DI12 & DI24) 동시 조건으로 변경
- **변경 요약:** `OnMonitoring`의 `IsLaserReady` 판정 조건 변경.
기존 `DI(9)` (CoreLaser_Laser_Ready 단독) → `DI(12) && DI(24)` (CoreLaser_Control_Status **AND** RingLaser_Control_Status) 동시 조건.
버튼이 켜는 Control(DO9/13)의 피드백을 Core·Ring 둘 다 확인하도록 변경.
### 변경 부분 (`OnMonitoring` 메서드 내부)
```csharp
try
{
- if (_IODevice.ReadDI(9))
+ // 변경 후
+ if (_IODevice.ReadDI(12) && _IODevice.ReadDI(24))
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
```
---
## 적용 체크리스트
- [ ] `SequenceSetting.cs` — Axis Position Interlock 파라미터 4개 추가 반영
- [ ] `UCScannerObjectTeachingControlBoxViewModel.cs` — Laser Ready 판정 `DI(12) && DI(24)`로 변경 반영
- [ ] 빌드 확인

View File

@ -1,85 +0,0 @@
using Aurora.Api;
using PropertyTools.DataAnnotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WeldingCommon.Settings
{
// SequenceSetting 이 변경되면, 재부팅 필요함.
public class SequenceSetting : ISettingItem
{
[Category("Monitoring Sequence")]
public bool PlcMonitor { get; set; } = true; // PLC_REMOVE
public bool AlarmMonitor { get; set; } = true;
//public bool DeviceMonitor { get; set; } = true;
//public bool MESMonitor { get; set; } = true;
[Category("Alarm Monitoring Device")]
public bool PlcActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool PlcActivatedLive { get; set; } = true;
public bool ScannerActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool ScannerActivatedWithIO { get; set; } = false;
public bool LaserActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool LaserActivatedWithIO { get; set; } = false;
public bool LbadActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool LbadActivatedWithIO { get; set; } = false;
public bool OctActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool OctActivatedWithIO { get; set; } = false;
public bool ChillerActivated { get; set; } = true; // Check 되어야 아래 항목 동작
public bool ChillerActivatedWithIO { get; set; } = false;
public bool LeakSensorActivatedWithIO { get; set; } = false;
[Category("WeldingSequence")]
public bool UseCellReport { get; set; } = false;
public bool UseImageAcquire { get; set; } = false;
public int ImageAcquireDelayTime { get; set; } = 1000;
public bool UseWeldResultImage { get; set; } = false;
public bool UseCellOffset { get; set; } = false;
public bool UseSubPatternOnFailure { get; set; } = false;
public bool UseExistOffset { get; set; } = false;
public double CellOffsetLimit { get; set; } = 0.3;
public double FirstLimitScore { get; set; } = 85.0;
public double SecondLimitScore { get; set; } = 85.0;
public bool UsePageChangeMode { get; set; } = false;
public int LastPageNo { get; set; } = 1;
[DisplayName("NG(Vision 검출 or Limit Over)인 경우 미용접")]
public bool UseNgCellSkip { get; set; } = false;
[DisplayName("비전 보상 시 사용할 조명값")]
public int CompensationLightValue { get; set; } = 255;
[DisplayName("기본 조명값")]
public int LightValue { get; set; } = 255;
public string VisionFolderName { get; set; } = @"D:\ILSOSConfig\_vision\ModelFinder";
public string WeldResultFolderName { get; set; } = @"D:\ILSOSConfig\_vision\WeldResult";
[DisplayName("B-Line Hook Marking Use")]
public bool BLineHookMarkingUse { get; set; } = false;
[DisplayName("Group Raming Use")]
public bool GroupRamingUse { get; set; } = false;
//260617 - 수직, 수평 + 허용오차 단일 값 입력하는 곳(시퀸스 돌 때 같이 입력해야할 것.)
[Category("Axis Position Interlock")]
[DisplayName("수직 기준값 [deg]")]
public double VerticalSector { get; set; } = 0.0;
[DisplayName("수직 허용오차 [deg]")]
public double VerticalTolerance { get; set; } = 0.0;
[DisplayName("수평 기준값 [deg]")]
public double HorizontalSector { get; set; } = 0.0;
[DisplayName("수평 허용오차 [deg]")]
public double HorizontalTolerance { get; set; } = 0.0;
}
}

View File

@ -1,489 +0,0 @@
using Aurora;
using Aurora.Api;
using Aurora.View;
using AuroraIO;
using AuroraLaser;
using AuroraLaserScanner;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using VisionCommon;
using WeldingCommon;
using WeldingCommon.Devices.Laser;
using WeldingCommon.Models;
using WeldingCommon.Sequences;
using WeldingCommon.Settings;
using WeldingCommon.Utils;
using static AuroraLaser.Ekspla.NL202;
using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner;
namespace WeldingCommon.Views
{
public class UCScannerObjectTeachingControlBoxViewModel : NotifyPropertyChangedBase
{
private readonly Logger Log = new Logger("UCScannerObjectTeachingControlBoxViewModel");
public ICommand CMD_VisibleChanged { get; }
public ICommand CMD_RunScoutNormal { get; }
public ICommand CMD_StopScoutNormal { get; }
public ICommand CMD_RunScoutFlyingVision { get; }
public ICommand CMD_StopScoutFlyingVision { get; }
public ICommand CMD_RunTeachingRun { get; }
public ICommand CMD_StopTeachingRun { get; }
public ICommand CMD_LaserGuideBeam { get; }
public ICommand CMD_LaserReadyCheck { get; }
public ICommand CMD_ImportObjectFromDXF { get; }
private readonly IAuroraEnvironment _Aurora;
private SystemParameter _SystemParameter;
//private readonly EquipmentInfo _EquipmentInfo;
private readonly Scanner _Scanner;
private readonly Laser _Laser;
private readonly LaserConfig _LaserConfig;
private IODevice _IODevice;
private ISequence _SeqScoutNormal;
private bool _IsSequenceRunning_ScoutNormal;
public bool IsSequenceRunning_ScoutNormal { get => _IsSequenceRunning_ScoutNormal; set => SetProperty(ref _IsSequenceRunning_ScoutNormal, value); }
private ISequence _SeqScoutFlyingVision;
private bool _IsSequenceRunning_ScoutFlyingVision;
public bool IsSequenceRunning_ScoutFlyingVision { get => _IsSequenceRunning_ScoutFlyingVision; set => SetProperty(ref _IsSequenceRunning_ScoutFlyingVision, value); }
private ISequence _SeqTeachingRun;
private bool _IsSequenceRunning_TeachingRun;
public bool IsSequenceRunning_TeachingRun { get => _IsSequenceRunning_TeachingRun; set => SetProperty(ref _IsSequenceRunning_TeachingRun, value); }
private bool _IsSelectedScoutNormal;
public bool IsSelectedScoutNormal { get => _IsSelectedScoutNormal; set => SetProperty(ref _IsSelectedScoutNormal, value); }
private bool _IsGuidBeam;
public bool IsGuidBeam { get => _IsGuidBeam; set => SetProperty(ref _IsGuidBeam, value); }
private bool _IsLaserReady;
public bool IsLaserReady { get => _IsLaserReady; set => SetProperty(ref _IsLaserReady, value); }
// Object Select Type
private bool _IsObjectSelectAll;
public bool IsObjectSelectAll { get => _IsObjectSelectAll; set => SetProperty(ref _IsObjectSelectAll, value); }
private readonly DispatcherTimer _MonitorTimer;
private SequenceDialog _SequenceDialog;
private WinSequenceDiagram _WinSequenceDiagramScoutNormal;
private WinSequenceDiagram _WinSequenceDiagramScoutFlyingVision;
private WinSequenceDiagram _WinSequenceDiagramTeachingRun;
private WinSequenceDiagram _WinSequenceDiagramComponentTeachingRun;
public bool toggleIO = true;
public UCScannerObjectTeachingControlBoxViewModel(IAuroraEnvironment aurora)
{
_Aurora = aurora;
_SystemParameter = aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
_SeqScoutNormal = _Aurora.SequenceEntry[CommonSequenceIds.SeqScoutNormal];
_SeqScoutFlyingVision = _Aurora.SequenceEntry[CommonSequenceIds.SeqScoutFlyingVision];
_Scanner = _Aurora.DeviceManager[CommonDeviceIds.Scanner] as Scanner;
_Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as Laser;
_IODevice = _Aurora.DeviceManager[CommonDeviceIds.IODevice] as IODevice;
// Trumpf 레이저인 경우에는 ComponentTeachingRun 실행
_LaserConfig = _Aurora.DeviceManager.GetDeviceConfig(CommonDeviceIds.Laser) as LaserConfig;
_SeqTeachingRun = _Aurora.SequenceEntry[CommonSequenceIds.SeqComponentTeachingRun];
InitVariable();
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
CMD_RunScoutNormal = new DelegateCommand(PerformCmdRunScoutNormal);
CMD_StopScoutNormal = new DelegateCommand(PerformCmdStopScoutNormal);
CMD_RunScoutFlyingVision = new DelegateCommand(PerformCmdRunScoutFlyingVision);
CMD_StopScoutFlyingVision = new DelegateCommand(PerformCmdStopScoutFlyingVision);
CMD_RunTeachingRun = new DelegateCommand(PerformCmdRunTeachingRun);
CMD_StopTeachingRun = new DelegateCommand(PerformCmdStopTeachingRun);
CMD_LaserGuideBeam = new DelegateCommand(PerformCmdLaserGuideBeam);
CMD_LaserReadyCheck = new DelegateCommand(PerformCmdLaserReadyCheck);
CMD_ImportObjectFromDXF = new DelegateCommand(PerformCmdImportObjectFromDXF);
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_SETTING_CHANGED)
.ObserveOnDispatcher()
.Subscribe(OnSettingReloaded);
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
}
private void InitVariable()
{
if (_SystemParameter.ScoutType == eScoutType.Normal) {
IsSelectedScoutNormal = true;
} else {
IsSelectedScoutNormal = false;
}
IsSequenceRunning_ScoutNormal = false;
IsSequenceRunning_ScoutFlyingVision = false;
IsSequenceRunning_TeachingRun = false;
IsGuidBeam = false;
IsLaserReady = false;
IsObjectSelectAll = true;
}
private void VisibleChanged(object obj)
{
bool visible = (bool)obj;
if (visible) {
_MonitorTimer.Start();
} else {
_MonitorTimer.Stop();
}
}
private void OnSettingReloaded(object obj)
{
_SystemParameter = _Aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
if (_SystemParameter.ScoutType == eScoutType.Normal) {
IsSelectedScoutNormal = true;
} else {
IsSelectedScoutNormal = false;
}
}
private void OnMonitoring(object sender, EventArgs e)
{
// TODO: Laser Device 에 구현된 내용에 따르면, 일부 Laser Device 는 구현이 되어 있지 않은 상태. 항상 false
if (_Laser.GetPilotLaserStatus()) {
IsGuidBeam = true;
} else {
IsGuidBeam = false;
}
try
{
// 변경 후
if (_IODevice.ReadDI(12) && _IODevice.ReadDI(24))
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
catch (Exception ex)
{
Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}");
IsLaserReady = false;
}
// TODO: PRIBAT_L1, PRIBAT_L2 는 Blow 의 상태 check 가 들어 있음. 장비 한정이므로 이를 check 하는 것을 modelItem 이나 setting 으로 빼서 진행되도록 함.
}
private void PerformCmdRunScoutNormal(object obj)
{
var result = MessageUtil.ShowYesNoMessageDialog("Scanner Scout", "Start Scanner Scout Normal Sequence?");
if (result == false) {
return;
}
if (VisionRecipeManager.GetFovInfos().Count == 0) {
MessageUtil.ShowWarningToast(string.Format("Please check the Vision Recipe of the currently active Recipe."));
return;
}
if (_SeqScoutNormal.RunState == SequenceRunState.Running) {
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "SeqScoutNormal"), Log);
return;
}
var scoutSeqParams = new ScoutSeqParams(VisionRecipeManager.GetFovInfos()[0], eDisplayType.Machine_B);
var param = new SeqScoutNormal_InitParam() {
IsRealTimeScout = false,
ScoutSeqParams = scoutSeqParams,
};
PopupSequenceDiagram(ref _WinSequenceDiagramScoutNormal, CommonSequenceIds.SeqScoutNormal, param);
IsSequenceRunning_ScoutNormal = true;
_SeqScoutNormal.Start(param, OnCompleteScoutNormal);
}
private void OnCompleteScoutNormal(SequenceActionResult res)
{
if (res != null) {
if (res.Param is SeqScoutNormal_OutParam result && result.Success == true) {
IsSequenceRunning_ScoutNormal = false;
MessageUtil.ShowSuccessToast(string.Format("Success SeqScoutNormal"));
CloseSequenceDiagram(_WinSequenceDiagramScoutNormal);
return;
}
}
if (_Laser.GetLaserReady()) {
_Laser.LaserOff();
}
IsSequenceRunning_ScoutNormal = false;
MessageUtil.ShowWarningToast(string.Format("Fail SeqScoutNormal"));
}
private void PerformCmdStopScoutNormal(object obj)
{
if (_SeqScoutNormal.RunState == SequenceRunState.Idle) {
return;
}
IsSequenceRunning_ScoutNormal = false;
_SeqScoutNormal.Stop();
}
private void PerformCmdRunScoutFlyingVision(object obj)
{
var result = MessageUtil.ShowYesNoMessageDialog("Flying Vision", "Start Scanner Scout Flying Vision Sequence?");
if (result == false) {
return;
}
if (VisionRecipeManager.GetFovInfos().Count == 0) {
MessageUtil.ShowWarningToast(string.Format("Please check the Vision Recipe of the currently active Recipe."));
return;
}
if (_SeqScoutFlyingVision.RunState == SequenceRunState.Running) {
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "SeqScoutFlyingVision"), Log);
return;
}
var scoutSeqParams = new ScoutSeqParams(VisionRecipeManager.GetFovInfos()[0], eDisplayType.Machine_B);
var param = new SeqScoutFlyingVision_InitParam() {
IsRealTimeScout = false,
ScoutSeqParams = scoutSeqParams,
SequenceAutoRunCount = 0,
WeldingScannerListNo = eListNo.LIST2,
};
PopupSequenceDiagram(ref _WinSequenceDiagramScoutFlyingVision, CommonSequenceIds.SeqScoutFlyingVision, param);
IsSequenceRunning_ScoutFlyingVision = true;
_SeqScoutFlyingVision.Start(param, OnCompleteScoutFlyingVision);
}
private void OnCompleteScoutFlyingVision(SequenceActionResult res)
{
if (res != null) {
if (res.Param is SeqScoutFlyingVision_OutParam result && result.Success == true) {
IsSequenceRunning_ScoutFlyingVision = false;
MessageUtil.ShowSuccessToast(string.Format("Success SeqScoutFlyingVision"));
CloseSequenceDiagram(_WinSequenceDiagramScoutFlyingVision);
return;
}
}
if (_Laser.GetLaserReady()) {
_Laser.LaserOff();
}
IsSequenceRunning_ScoutFlyingVision = false;
MessageUtil.ShowWarningToast(string.Format("Fail SeqScoutFlyingVision"));
}
private void PerformCmdStopScoutFlyingVision(object obj)
{
if (_SeqScoutFlyingVision.RunState == SequenceRunState.Idle) {
return;
}
IsSequenceRunning_ScoutFlyingVision = false;
_SeqScoutFlyingVision.Stop();
}
private void PerformCmdRunTeachingRun(object obj)
{
var result = MessageUtil.ShowYesNoMessageDialog("Teaching Run", "Start TeachingRun Sequence?");
if (result == false) {
return;
}
if (IsLaserReady == false) {
MessageUtil.ShowWarningToast(string.Format("Please check Laser Ready."));
return;
}
// TODO: PRIBAT_L1, PRIBAT_L2 에서 Suction 과 CLAMP 상태 등에 따라 Laser 가 나가지 않도록 해야 함.
// TODO: 이 부분은, Delegate 를 사용해서, Interlock 을 각 장비별로 설정할 수 있도록 해주는게 좋겠음.
if (_SeqTeachingRun.RunState == SequenceRunState.Running) {
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "SeqTeachingRun"), Log);
return;
}
// TODO: NeedSetLaserTiming 는 트럼프 레이저 를 사용할 때, UploadWeldingList Sequence 에서 LaserTiming Set 을 해야 할때 사용함.
// TODO: SMW 와 LHS 는 트럼프 레이저를 사용하지 않으므로 false, PRIBAT_L1 과 PRIBAT_L2 할때 LaserType 을 읽어서 설정하는 것으로 추가하자
var currentModelInfo = EquipmentSpecificModelUtil.GetCurrentRecipeEquipmentSpecificModel();
if (currentModelInfo.equipmentSpecificModel == null) {
MessageUtil.ShowWarningToast(string.Format("선택된 model 이 없습니다.", "SeqTeachingRun"));
return;
}
var intervalTimeBlowOn = 0;
var intervalTimeBlowOff = 0;
if (currentModelInfo.equipmentSpecificModel.UseBlow) {
intervalTimeBlowOn = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOn;
intervalTimeBlowOff = currentModelInfo.equipmentSpecificModel.IntervalTimeBlowOff;
}
var param = new SeqTeachingRun_InitParam() {
IsObjectSelectAll = IsObjectSelectAll,
//UseStream = _EquipmentInfo.UseStream,
UseStream = currentModelInfo.equipmentSpecificModel.UseStream,
UseBlow = currentModelInfo.equipmentSpecificModel.UseBlow,
IntervalTimeBlowOn = intervalTimeBlowOn,
IntervalTimeBlowOff = intervalTimeBlowOff,
NeedSetLaserTiming = false,
};
//PopupSequenceDiagram(ref _WinSequenceDiagramTeachingRun, CommonSequenceIds.SeqComponentTeachingRun, param);
IsSequenceRunning_TeachingRun = true;
_SeqTeachingRun.Start(param, OnCompleteTeachingRun);
}
private void OnCompleteTeachingRun(SequenceActionResult res)
{
//if (res != null) {
// if (res.Param is SeqTeachingRun_OutParam result && result.Success == true) {
// IsSequenceRunning_TeachingRun = false;
// MessageUtil.ShowSuccessToast(string.Format("Success SeqTeachingRun"));
// CloseSequenceDiagram(_WinSequenceDiagramTeachingRun);
// return;
// }
//}
if (_Laser.GetLaserReady()) {
_Laser.LaserOff();
}
IsSequenceRunning_TeachingRun = false;
//MessageUtil.ShowWarningToast(string.Format("Fail SeqTeachingRun"));
}
private void PerformCmdStopTeachingRun(object obj)
{
IsSequenceRunning_TeachingRun = false;
if (_SeqTeachingRun.RunState == SequenceRunState.Idle) {
return;
}
_SeqTeachingRun.Stop();
}
private void PopupSequenceDiagram(ref WinSequenceDiagram winSequenceDiagram, string seqId, object param)
{
if (winSequenceDiagram == null) {
winSequenceDiagram = new WinSequenceDiagram(seqId);
winSequenceDiagram.Closing += ClosingEvent;
winSequenceDiagram.SetParam(new SeqDrawerParam() {
SequenceId = seqId,
DiagramType = eDiagramType.Tree,
LineType = eLineType.Straight,
IsShowSubSequence = false,
VisibleKeys = new List<string>()
});
winSequenceDiagram.Owner = Application.Current.MainWindow;
winSequenceDiagram.Width = 600;
winSequenceDiagram.SequenceParam = param;
winSequenceDiagram.Show();
} else {
winSequenceDiagram.SequenceParam = param;
winSequenceDiagram.WindowState = WindowState.Normal;
winSequenceDiagram.Show();
}
}
private void CloseSequenceDiagram(WinSequenceDiagram winSequenceDiagram)
{
Application.Current.Dispatcher.Invoke(() => {
if (winSequenceDiagram != null) {
winSequenceDiagram.Close();
}
});
}
private void ClosingEvent(object sender, CancelEventArgs e)
{
e.Cancel = true;
(sender as Window).Hide();
}
private void PerformCmdLaserGuideBeam(object obj)
{
if (!_Laser.GetPilotLaserSetBitStatus()) {
_Laser.PilotLaserOn();
} else {
_Laser.PilotLaserOff();
}
}
private void PerformCmdLaserReadyCheck(object obj)
{
// 추후 Write bit 파라미터화 해야함
if(toggleIO)
{
_IODevice._iOUnit.WriteDoBit(9, true);
_IODevice._iOUnit.WriteDoBit(13, true);
toggleIO = false;
}
else
{
_IODevice._iOUnit.WriteDoBit(9, false);
_IODevice._iOUnit.WriteDoBit(13, false);
toggleIO = true;
}
}
private void PerformCmdImportObjectFromDXF(object obj)
{
System.Windows.Forms.FileDialog dlg = new System.Windows.Forms.OpenFileDialog();
dlg.InitialDirectory = @"D:\share\jungwoo.jin\모바일글라스도면 외 (3) 파일_20241028";
dlg.DefaultExt = "dxf";
dlg.Filter = "DXF 도면 파일 (*.dxf)|*.dxf";
dlg.Title = "DXF 도면 열기";
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
CommonHelper.GetMainPage().DxfParser(dlg.FileName);
}
}
}
}