NohDoyeon241104 c054439c19 feat: Recipe 작업 화면 상단에 PLC 지시 Recipe/Page 표시
bit15(M70515) 팝업은 상승엣지 1회라 닫으면 값이 사라져, 현장에서 작업자가
레시피/페이지를 잊는 문제가 있었다. 팝업과 같은 기준·같은 워드를 화면 상단
전폭 띠로 ON 구간 내내 표시한다(Collapsed 시 높이 0이라 평소 화면 영향 없음).

공용(WeldingCommon)은 Status만 읽고 PTOP을 참조하지 않는다 —
값을 넣지 않는 타 설비는 분기문 없이 자동 미표시.
2026-08-21 00:06:30 +09:00

519 lines
23 KiB
C#

using Aurora;
using Aurora.Api;
using Aurora.Api.Manager;
using Aurora.View;
using AuroraLaserScanner;
using CoreUnit.Scanner.UserControl.ScannerCanvas;
using ILSOS_PrismaticRD_PTOP.Device;
using ILSOS_PrismaticRD_PTOP.Recipes;
using ILSOS_PrismaticRD_PTOP.Sequences;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using VisionCommon;
using WeldingCommon;
using WeldingCommon.Alarm;
using WeldingCommon.Data;
using WeldingCommon.Devices;
using WeldingCommon.Popup;
using WeldingCommon.Sequences;
using WeldingCommon.Sequences.Main;
using WeldingCommon.Settings;
using WeldingCommon.Utils;
using WeldingCommon.Views;
using static ILSOS_PrismaticRD_PTOP.Device.PLCControllerPTOP;
using static WeldingCommon.Sequences.Main.SeqExecutePlcBitInterface_InitParam;
using static WeldingCommon.Views.UCCommonAutoRunViewModel;
using ILSOS_PrismaticRD_PTOP.Sequences;
namespace ILSOS_PrismaticRD_PTOP.Views
{
public class UCMainAutoRunViewModel : UCCommonAutoRunViewModel
{
private readonly Logger Log = new Logger("UCMainAutoRunViewModel");
public ICommand CMD_VisibleChanged { get; }
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;
private readonly IAlarmService _AlarmService;
private SystemParameter _SystemParameter;
private readonly Scanner _Scanner;
private readonly PLCControllerPTOP _Plc;
private ISequence _SeqAutoRun; // TestRun으로 우선 확인 후
private ISequence _SeqPlcInterface;
private ISequence _SeqQTest;
// 260605 현장 SeqAutoRunPTOP
private ISequence _SeqAutoRunPTOP;
public bool IsSequenceRunning_AutoRun { get => _IsSequenceRunning_AutoRun; set => SetProperty(ref _IsSequenceRunning_AutoRun, value); }
private bool _IsSequenceRunning_AutoRun;
public bool IsAlarm { get => _IsAlarm; set => SetProperty(ref _IsAlarm, value); }
private bool _IsAlarm;
private DateTime _NowDateTime;
public DateTime NowDateTime { get => _NowDateTime; set => SetProperty(ref _NowDateTime, value); }
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;
public UCMainAutoRunViewModel(IAuroraEnvironment aurora, string sequenceId) : base(aurora, sequenceId)
{
_RecipeManager = _Aurora.RecipeManager;
_SettingEntry = _Aurora.SettingEntry;
_AlarmService = _Aurora.AlarmService;
_SystemParameter = _Aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
_Scanner = _Aurora.DeviceManager[CommonDeviceIds.Scanner] as Scanner;
_Plc = _Aurora.DeviceManager[CommonDeviceIds.PLCDevice] as PLCControllerPTOP;
// MainViewModel에서 RunStateChanged를 설정해준다면, ManualView 상태에서도 해당 이벤트가 호출될지.. 호출될 듯 하긴 함.
_SeqAutoRun = _Aurora.SequenceEntry[SequenceIds.SeqAutoRun];
_SeqAutoRunPTOP = _Aurora.SequenceEntry[SequenceIds.SeqAutoRunPTOP]; // 260605
// Autorun과 Status 변경(Manual, Auto 변경) 시 사용하는 시퀸스ID가 다르도록 설정. 같을 경우, 문제 발생.
_SeqPlcInterface = _Aurora.SequenceEntry[SequenceIds.SeqExecutePlcBitInterfaceForStatusChange];
// Q Test
_SeqQTest = _Aurora.SequenceEntry[SequenceIds.SeqEventQTest];
InitVariable();
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
// 260813: 자동시작 요청 출처를 넘긴다 — OnAutoRunStartRequest 는 버튼/PLC 이벤트 공용 진입점이라
// 진입부 로그가 출처를 단정하면 안 된다(기존 문구가 버튼 클릭도 "AUTO_REQ"로 찍었다).
CMD_RunSequenceAutoRun = new DelegateCommand(o => OnAutoRunStartRequest("Button")); // 260511 이곳 시퀸스
CMD_StopSequenceAutoRun = new DelegateCommand(OnStopSequenceAutoRun); // 260813: 정지 시 에코 워드 0 초기화 재적용(조건부) — ResumeMode/M70515 구간만 보존, SeqAutoRunPTOP.OnSeqActionStop 260813 region
CMD_Reset = new DelegateCommand(PerformCmdReset);
CMD_AutoRunResume = new DelegateCommand(OnAutoRunResume);
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
//이벤트로 AUTORUN 스타트 요청에 대한 메소드 실행 오면 그때 동작함. 시퀸스 돔
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_AUTO_RUN_START_REQUEST)
.ObserveOnDispatcher()
.Subscribe(x => OnAutoRunStartRequest("PLC_AUTO_REQ")); // 260813: 출처 명시(발행처는 PLCController.MonitorPlcInterfaceMap 뿐)
// Stop 요청 시 정지 로직 실행
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_AUTO_RUN_STOP_REQUEST)
.ObserveOnDispatcher()
.Subscribe(x => OnStopSequenceAutoRun());
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_EQUIPMENT_STATUS_CHANGE)
.Subscribe(x => IsSequenceRunning_AutoRun = Status.EquipmentStatus == eEqpStatus.Auto);
Observable.Merge(
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_ALARM_SET_ALARM),
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_ALARM_RESET_ALARM),
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_ALARM_RESET_ALL_ALARM))
.Subscribe(x => IsAlarm = AlarmManager.HasAlarmLevelHigh());
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_CHANGE_RECIPE_INFO)
.ObserveOnDispatcher()
.Select(param => (RecipeInfo)param.Param)
.Subscribe(OnUpdateCurrentActivatedRecipe);
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM)
.ObserveOnDispatcher()
.Subscribe(x => OnAutoRunUpdatePageNum(x));
Status.SetWorkingPosition(eWorkingPosition.NONE);
}
private void InitVariable()
{
NowDateTime = DateTime.Now;
if (NowDateTime.Day != _SystemParameter.LastProductTime.Day)
InitializeTodayCount();
CurrentWeldingData.TodayProduct = _SystemParameter.TodayProductCount;
CurrentWeldingData.TotalProduct = _SystemParameter.TotalProductCount;
CurrentWeldingData.AutoRunStartTime = default(DateTime);
CurrentWeldingData.WeldingEndTime = _SystemParameter.LastProductTime;
CurrentWeldingData.WeldingSpanTimeMilliseconds = "0";
CurrentWeldingData.LotId = "Default_Lot_Id";
CurrentWeldingData.CellId = "Default_Cell_Id";
}
private void OnUpdateCurrentActivatedRecipe(RecipeInfo info)
{
// Activate Layer 불러오기
RecipeHelper.OnActiveReloaded();
// Page UI 업데이트
RecipeHelper.UpdateAutorunRecipe();
}
private void OnAutoRunUpdatePageNum(EventMessage x)
{
if (x.Param is int pageIdx)
{
CurrentPageNum = pageIdx + 1;
}
}
private void VisibleChanged(object obj)
{
bool visible = (bool)obj;
if (visible)
{
_MonitorTimer.Start();
}
else
{
_MonitorTimer.Stop();
}
}
private void OnMonitoring(object sender, EventArgs e)
{
// 켜져있는 상태에서는 24시 넘어갈 때만 초기화
if (NowDateTime.Day != DateTime.Now.Day)
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_WELD_READY_REQ).data;
IsModeChangeActive = bit15; // bit15 살아있을 때만 [AutoRun Resume] 버튼 표시
#region 260820 bit15 구간 Recipe/Page 를 Recipe 작업 화면에도 표시 (원복: 이 region 삭제)
// 팝업은 상승엣지 1회라 [확인] 을 누르면 값이 사라진다. 현장 소음 속에서 작업자가
// 몇 번 레시피 몇 페이지였는지 잊는다는 요청으로, Recipe 작업 화면 상단에도 같은 값을 띄운다.
// · 트리거: 팝업과 같은 bit15. 단 팝업은 엣지 1회, 표시는 ON 구간 내내 레벨 유지.
// · 값: 팝업이 읽는 두 워드 그대로(PLC 원본 입력). PC 에코(PC_*)가 아니다.
// 공용(WeldingCommon)은 Status 만 읽고 PTOP 을 참조하지 않는다 — 타 설비는 값이 없어 자동 미표시.
// 분석: docs/분석/2026/08/20/RecipePage_현장표시_레이아웃_분석.md
if (bit15)
{
Status.PlcGuideRecipeNo = _Plc.ReadWord(eWordInput.PLC_RECIPE_NO).data;
Status.PlcGuidePageNo = _Plc.ReadWord(eWordInput.PLC_PAGE_NO).data;
}
else if (Status.PlcGuideRecipeNo != null)
{
Status.PlcGuideRecipeNo = null;
Status.PlcGuidePageNo = null;
}
#endregion
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 지정 레시피를 활성화 + 티칭/캔버스 로드 → Manual에서 해당 Recipe/Page로 이동·수동용접 가능.
// (미등록/0이면 현재 활성 레시피로 폴백. OnMonitoring=UI 스레드라 OnActiveReloaded 직접 호출 OK)
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == recipeNo)
?? _RecipeManager.ActivatedRecipe();
if (recipe != null)
{
_RecipeManager.ActivateRecipe(recipe.Id, "Bit15 Manual Weld");
RecipeHelper.OnActiveReloaded();
Log.I($"[Bit15] Recipe 활성화+로드 (Id:{recipe.Id}) → Manual 이동 가능");
}
else
{
Log.W($"[Bit15] 활성화할 Recipe 없음 (PLC RecipeNo:{recipeNo}, 활성 레시피도 null)");
}
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(Weld_01 ReadyACK) ON — resume run 신호. 1초 펄스 후 자동 OFF(아래 타이머).
_Plc.WriteBit(eBitOutput.PC_WELD_READY_ACK, true);
// 1초 후 PC bit15 OFF (one-shot DispatcherTimer)
var bit15OffTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
bit15OffTimer.Tick += (s, ev) =>
{
bit15OffTimer.Stop();
_Plc.WriteBit(eBitOutput.PC_WELD_READY_ACK, false);
Log.I("[Bit15] PC_WELD_READY_ACK OFF (1초 펄스)");
};
bit15OffTimer.Start();
// Auto 재개 (옵션A: 정상 핸드셰이크 — bit15 후 PLC가 주는 새 Page Change를 정상 처리)
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, true);
Status.GetProcessingWeldingData().AutoRunStartTime = DateTime.Now;
RunSequenceAutoRun();
Log.I("[Bit15] AutoRun Resume → PC_WELD_READY_ACK ON + Auto 재개(정상 핸드셰이크)");
}
// PLC 비트에 의한 AutoRun 실행의 경우, PLC 인터페이스 이후 AutoRun 필요
public override async void OnAutoRunStartRequest(object obj = null)
{
// PLC bit15(MODECHANGE)가 ON인 동안에는 PLC_AUTO_REQ 자동시작 신호를 무시.
// (bit15 Manual 전환 중 AUTO_REQ rising edge가 AutoRun을 띄워 Manual을 덮어쓰는 현상 차단.
// 이어하기는 전용 [AutoRun Resume] 버튼(OnAutoRunResume) 경로라 이 가드와 무관.)
if (_Plc != null && !_Plc.IsSimul && _Plc.ReadBit(eBitInput.PLC_WELD_READY_REQ).data)
{
// 260813: 출처를 같이 남긴다. 이 메서드는 [운전 시작] 버튼과 PLC 이벤트가 공용으로 쓰는
// 진입점이라, 여기서 "AUTO_REQ"로 단정하면 버튼 클릭도 PLC 신호로 오독된다.
Log.I($"[Bit15] bit15 ON 중 → 자동시작 요청 무시 (요청출처:{obj ?? "unknown"}, Manual 유지)");
return;
}
if (!ApproveAutorunByUserOrAuto()) { return; }
SeqAutoRunPTOP.ResumeMode = false; // 일반 시작은 항상 핸드셰이크 포함 (resume 아님)
base.OnAutoRunStartRequest();
#region 팝업 띄우는 창과 함께 Bit false
bool IsStart = await TimerMessageBox.ShowAsync(Application.Current.MainWindow, "운전 시작", "자동 운전을 시작하시겠습니까?", true, 5000);
if (!IsStart)
{
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, false);
return;
}
#endregion
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, true); // 음극 (양극 P 미사용)
// PLC에선 Auto Req 받으면 바로 살려주도록
Status.GetProcessingWeldingData().AutoRunStartTime = DateTime.Now;
RunSequenceAutoRun();
}
private void RunSequenceAutoRun()
{
// 자동으로 Activated Recipe로 변경
OnUpdateCurrentActivatedRecipe(null);
//var param = new SeqAutoRunPrismatic_InitParam()
//{
// VisionRecipeNo = 0
//};
var param = new SeqTeachingRun_InitParam()
{
IsObjectSelectAll = true,
};
Status.SetStatus(eEqpStatus.Auto);
//_SeqAutoRun.Start(param, OnCompleteAutoRun);
// 260605
_SeqAutoRunPTOP.Start(param, OnCompleteAutoRunPTOP);
}
private void OnCompleteAutoRun(SequenceActionResult res)
{
if (res != null)
{
if (res.Param is SeqAutoRunPrismatic_OutParam result && result.Success == true)
{
MessageUtil.ShowSuccessToast(string.Format("Success SeqAutoRunPrismatic"));
return;
}
}
MessageUtil.ShowWarningToast(string.Format("Fail SeqAutoRunPrismatic"));
}
#region 260605
private void OnCompleteAutoRunPTOP(SequenceActionResult res)
{
if (res != null)
{
// OutParam 타입을 SeqAutoRunPTOP_OutParam으로 수정
if (res.Param is SeqAutoRunPTOP.SeqAutoRunPTOP_OutParam result && result.Success == true)
{
MessageUtil.ShowSuccessToast(string.Format("Success SeqAutoRunPTOP"));
return;
}
}
MessageUtil.ShowWarningToast(string.Format("Fail SeqAutoRunPTOP"));
}
#endregion
private bool ApproveAutorunByUserOrAuto()
{
// [테스트용] 알람/인터락 체크 일시 주석 처리
/*
bool boolian = AlarmManager.HasAlarmLevelHigh();
if (boolian)
{
Log.E($"Clear Heavy Alarms For Welding!");
MessageUtil.ShowWarningToast(string.Format("Clear Heavy Alarms For Welding!"));
return false;
}
boolian = AlarmManager.IsPlcDoorOpen();
if (boolian)
{
Log.E($"PLC Door Opened!! Can't Weld Now.");
MessageUtil.ShowWarningToast(string.Format("Close Door ! Clear PLC DoorInterlock!"));
return false;
}
boolian = AlarmManager.IsPlcEmoAlarm();
if (boolian)
{
Log.E($"PLC EMO Pushed!! Can't Weld Now.");
MessageUtil.ShowWarningToast(string.Format("Clear PLC EMO Status!"));
return false;
}
if (VisionRecipeManager.GetFovInfos().Count == 0)
{
MessageUtil.ShowWarningToast(string.Format("Please check the Vision Recipe of the currently active Recipe."));
return false;
}
*/
if (_SeqAutoRunPTOP.RunState == SequenceRunState.Running)
{
MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "SeqAutoRunPTOP"), Log);
return false;
}
return true;
}
public override void OnAutoRunStopRequest()
{
base.OnAutoRunStopRequest();
//// Prewelding은 자동 운전 정지 와도 무시
//StopSequenceAutoRun();
}
private void OnStopSequenceAutoRun(object obj = null)
{
//if (_SeqAutoRun.RunState == SequenceRunState.Running)
// _SeqAutoRun.Stop();
if (_SeqAutoRunPTOP.RunState == SequenceRunState.Running)
_SeqAutoRunPTOP.Stop();
Status.SetStatus(eEqpStatus.Manual);
_Plc.WriteBit(eBitOutput.PC_AUTO_STATUS, false);
//Recipe를 기존에 선택된 Recipe로 Rollback 해 줌
RecipeHelper.RollbackRecipe();
//AutoRun 정지 시 CorrectionFile을 Vision으로 변경
_Scanner.SelectCorrectionTable(1);
Log.I("Stop AutoRun Requested and Cleaned up.");
}
private void PerformCmdReset(object commandParameter)
{
// SystemParameter를 저장하기 전에는 반드시 업데이트를 해주어야 한다.
_SystemParameter = _Aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
switch ((string)commandParameter)
{
case "TotalProduct":
{
CurrentWeldingData.TotalProduct = 0;
_SystemParameter.TotalProductCount = CurrentWeldingData.TotalProduct;
Status.GetProcessingWeldingData().TotalProduct = 0;
_SettingEntry.Save(CommonSettingIds.SystemParameter, _SystemParameter, "UCMainAutoRunViewModel");
}
break;
case "TodayProduct":
{
CurrentWeldingData.TodayProduct = 0;
_SystemParameter.TodayProductCount = CurrentWeldingData.TodayProduct;
Status.GetProcessingWeldingData().TodayProduct = 0;
_SettingEntry.Save(CommonSettingIds.SystemParameter, _SystemParameter, "UCMainAutoRunViewModel");
}
break;
default:
return;
}
}
private void InitializeTodayCount()
{
// SystemParameter를 저장하기 전에는 반드시 업데이트를 해주어야 한다.
// SystemParameter에 저장된 값 불러오기
_SystemParameter = _Aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
CurrentWeldingData.TodayProduct = 0;
_SystemParameter.TodayProductCount = CurrentWeldingData.TodayProduct;
_SettingEntry.Save(CommonSettingIds.SystemParameter, _SystemParameter, "UCMainAutoRunViewModel");
}
}
enum InputType
{
keyboardInput,
ClickInput
}
}