NohDoyeon241104 5be34e612e feat: 본 레이저 단일 발진기 전환 - IO 채널 재정의 및 enum 단일화
레이저 발진기 교체(임시 Core/Ring 2대 -> 본 레이저 Max MFMC-2000/2000M 1대)에
따른 IO 재정의. 현장 적용 후 정상 동작 확인.

- ePrismaticDIAddress / ePrismaticDOAddress 를 본 레이저 38핀 커넥터 매뉴얼 기준으로
  재정의. 상태 신호 8개 -> 4개. 도면(엑셀) 라벨 오기도 함께 정정
  (Laser_FIRE -> Laser_MAIN_PWR_STATE)
- PAIX_PTOP / SeqAutoRunPTOP 의 중복 enum 정의 제거.
  WeldingCommon 의 PAIX_PrismaticScanner 를 IO 채널 번호의 단일 소스로 통일
- 발진 게이트를 Core/Ring_Control 2발 -> Laser_REMOTE 1발로 변경
  (ExecuteWelding ON/OFF, 정지 시 해제)
- DO 8 충돌 제거: Auto 시작 시 PCLBADReadyStatus(=8) write 삭제.
  본 레이저 배선에서 DO 8 = Laser_AC_KEY 라 그대로 두면 Auto 시작이
  레이저 AC KEY 릴레이를 켠다. LBAD 는 미등록 디바이스라 무의미
- GetLaserReady() 를 Laser_READY 단독 판정으로 변경
- 가이드빔 DO 를 EX_Red_Control -> Laser_GUIDE_BEAM_CONTROL 로 전환
- IO Board 에 설정 화면(UCIOInterfaceView) 연결
- IsLaserReady 표시를 DO Laser_REMOTE 되읽기로 확정 (260826 협의, 현장 미검증)

원복: 각 파일의 "260824 이전 원본" region 을 살리고 아래 블록을 제거하면 된다.
2026-08-26 11:45:40 +09:00

1727 lines
88 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Aurora;
using Aurora.Api;
using Aurora.Api.Manager;
using Aurora.View;
using AuroraIO;
using AuroraLaserScanner;
using AuroraPLC;
using CoreUnit.Control.UserControl.Canvas;
using CoreUnit.Scanner.UserControl.ScannerCanvas;
// 260806: PLC 반환 err 판정은 Hermes.E1.OK 를 완전 한정명으로 쓴다.
// using Hermes; 를 새로 넣으면 이 파일이 이미 여는 다수 네임스페이스와 이름이 겹칠 수 있어 피했다.
using ILSOS_PrismaticRD_PTOP.Device;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using VisionCommon;
using WeldingCommon;
using WeldingCommon.Devices;
using WeldingCommon.Devices.Laser;
using WeldingCommon.Sequences;
using WeldingCommon.Utils;
using WeldingCommon.Alarm;
using ILSOS_PrismaticRD_PTOP.Alarm;
using ILSOS_PrismaticRD_PTOP.Settings;
using static AuroraIO.AJINEXTEK.AjinIO.IOModuleInfo;
using static ILSOS_PrismaticRD_PTOP.Device.PLCControllerPTOP;
using static WeldingCommon.Devices.PLCController;
using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner; // 260824 IO 채널 enum 단일화
using Application = System.Windows.Application;
using SystemParameter = WeldingCommon.Settings.SystemParameter;
namespace ILSOS_PrismaticRD_PTOP.Sequences
{
/*
*
*
[Sequence_Enter] 시퀀스 진입 - 레시피 초기화 및 장비 연결
[CheckInitCondition] 조건 확인 완료 → CheckPageNo 이동
[CheckPageNo] PLC_PAGE_CHANGE_REQ 신호 대기 중...
[CheckPageNo] PLC_PAGE_CHANGE_REQ ON 감지 → CheckPageNoComp 이동
[CheckPageNoComp] 페이지 확정 - PC_PAGE_NO: [1] 에코 완료
[SetUploadWeldingList] Group Count: [2], Total Line Count: [34]
[WeldingStartReadBit] PLC_WELD_TRIGGER_REQ ON 감지 → WaitWeldingStart 이동
[WaitWeldingStart] READY_ACK/BUSY ON, COMPLETE OFF 완료
[ExecuteWelding] Laser Gate ON (Core+Ring)
[ExecuteWelding] Gate ON Delay 15ms 완료 → ExecuteList 시작
[ExecuteWelding] WaitForListTaskCompleted 완료
[ExecuteWelding] Laser Gate OFF (Core+Ring)
[ExecuteWelding] 용접 완료 - TactTime: [285ms]
[CheckCycleStop] 사이클 계속 진행 → CheckPageNo 이동
*
*
*/
class SeqAutoRunPTOP
{
static readonly Logger Log = new Logger("SeqAutoRunPTOP");
static readonly IAuroraEnvironment aurora; // 완료
const string SEQ_ENTER = "[SeqAutoRunPTOP] SEQ_ENTER"; // inputParam 으로 받자.
const string SEQ_CHECK_INIT_CONDITION = "[SeqAutoRunPTOP] Check Init Condition"; // Check Init Condition
const string SEQ_CHECK_RECIPENO_COMPLETE = "[SeqAutoRunPTOP] CHECK RecipeNo Complete"; // 260618 Recipe Change
const string SEQ_CHECK_PAGENO = "[SeqAutoRunPTOP] CHECK PageNo";
const string SEQ_CHECK_PAGENO_COMPLETE = "[SeqAutoRunPTOP] CHECK PageNo Complete"; // 260513
#region 260813
const string SEQ_RESYNC_RECIPE_PAGE = "[SeqAutoRunPTOP] Resync Recipe/Page";
#endregion
const string SEQ_CHECK_WELDING_START_READ_BIT = "[SeqAutoRunPTOP] Check Welding Start ReadBit"; // 26 05 13
const string SEQ_WAIT_WELDING_START = "[SeqAutoRunPTOP] Wait Welding Start";
#region 티칭런에 있던 주요 노드 복사
const string SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunPTOP] Set Sequence Param (UploadWeldingList Seq)";
const string SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunPTOP] Process (UploadWeldingList Seq)";
const string SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunPTOP] Get Sequence Result (UploadWeldingList Seq)";
#endregion
const string SEQ_EXECUTE_WELDING = "[SeqAutoRunPTOP] Execute Welding";
const string SEQ_CHECK_CYCLE_STOP = "[SeqAutoRunPTOP] Check Cycle Stop";
const string SEQ_SAVE_RESULT_DATA = "[SeqAutoRunPTOP] Save Result Data";
const string SEQ_WRITE_COMPLETION_PLC_BIT = "[SeqAutoRunPTOP] Write Completion PLC Bit";
const string SEQ_COMPLETE = "[SeqAutoRunPTOP] SEQ_COMPLETE"; // 완료
#region 260622: M70509(Scout Enable) 기반 용접 전/후 Scout
// 용접 전: M70516(Weld Trigger) 감지 후 M70509 ON이면 scout(pre) → 0.1s → 용접
const string SEQ_PRE_SCOUT_CHECK = "[SeqAutoRunPTOP] Pre-Weld Scout Check"; // 분기: M70509 ON/OFF
const string SEQ_PRE_SCOUT_SET_PARAMS = "[SeqAutoRunPTOP] Pre-Weld Scout SetParams";
const string SEQ_PRE_SCOUT_RUN = "[SeqAutoRunPTOP] Pre-Weld Scout Run"; // SeqScoutNormal 서브시퀀스
const string SEQ_PRE_SCOUT_DONE = "[SeqAutoRunPTOP] Pre-Weld Scout Done"; // 0.1s 후 진행
// 용접 후: 발진 완료 후 완료비트 송신 전 M70509 ON이면 scout(post) → 0.1s → 완료비트
const string SEQ_POST_SCOUT_CHECK = "[SeqAutoRunPTOP] Post-Weld Scout Check";
const string SEQ_POST_SCOUT_SET_PARAMS = "[SeqAutoRunPTOP] Post-Weld Scout SetParams";
const string SEQ_POST_SCOUT_RUN = "[SeqAutoRunPTOP] Post-Weld Scout Run";
const string SEQ_POST_SCOUT_DONE = "[SeqAutoRunPTOP] Post-Weld Scout Done"; // 0.1s 후 진행
// 용접 완료 비트 송신(post-scout 분기 뒤로 분리): TriggerACK off / BUSY off / COMPLETE on
const string SEQ_WRITE_WELD_COMPLETE = "[SeqAutoRunPTOP] Write Weld Complete";
private const int SCOUT_PROCEED_DELAY_MS = 100; // scout 완료 후 진행 지연(0.1초)
#endregion
#region 260824 이전 원본 (로컬 복사본 — WeldingCommon 의 PAIX_PrismaticScanner 로 단일화)
// ↑ 상단 using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner 로 대체.
// 원복 시 아래 블록을 살리고 그 using static 을 제거한다.
//public enum ePrismaticDOAddress
//{
// CoreLaser_Control = 9,
// CoreLaser_ErrorClearing,
// RingLaser_Control = 13,
// RingLaser_ErrorClearing,
// EX_Red_Control = 17,
//}
#endregion
static class ContextKey
{
public const string KEY_SEQUENCE_RESULT = "KEY_SEQUENCE_RESULT_SeqAutoRunPTOP";
public const string KEY_SEQUENCE_START_TIME = "KEY_SEQUENCE_START_TIME_SeqAutoRunPTOP";
}
public IAuroraEnvironment AuroraEnv { get; set; }
// 장비 3개만 initDev
private Scanner _Scanner;
private PLCControllerPTOP _PLC;
//private PLCController _PLC;
private IODevice _IO;
private SystemParameter _SystemParameter;
private IRecipeManager _RecipeManager;
private ScannerCanvasUserCtrl _ScannerCanvasUserCtrl; //스캐너 동작관련 객체 선언
private SeqTeachingRun_InitParam _InitParam; // Enter에 기본으로 적용할 Param
//
const int PLC_TIMEOUT = 60; // sec, Timeout은 시퀀스 정지가 아니라 타임아웃 시간마다 로그만 남기도록 한다
#region 260806 신규 — 에코 수렴 루프 상수 (원복: 이 region 통째로 삭제)
const int ECHO_RETRY_MS = 200; // 수렴 재시도 간격. 정상 흐름은 1회 순회로 끝나므로 보정 경로에서만 쓰인다
const int ECHO_TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시
#endregion
#region 260813
private bool _InResyncNode = false;
private bool _ResyncAborted = false;
private DateTime _ResyncStartTime;
const int RESYNC_TIMEOUT_SEC = 10;
#endregion
private eListNo _WeldingScannerListNo;
private List<IDrawObject> _SelectedObjectList;
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)
#region 260806 구버전 보존 — Page 3회 수집용 필드 (원복: 아래 주석을 푼다)
// 수렴 루프로 대체돼 참조가 없어졌다. 삭제하지 않고 남긴다.
//private List<short> plcPageNoList = new List<short>();
#endregion
private short plcRecipeNo = 0; // 260618 PLC Recipe No (11/21/22/31/32)
int laserEmissionGateDelayMs = 15; // 260518
public SequenceBuilder Builder()
{
var sb = new SequenceBuilder(100000, true);
sb.AddAction(SEQ_ENTER, Sequence_Enter);
sb.AddDecision(SEQ_CHECK_INIT_CONDITION, CheckInitCondition); //분기
sb.AddAction(SEQ_RESYNC_RECIPE_PAGE, ResyncRecipePage);
sb.AddAction(SEQ_CHECK_PAGENO, CheckPageNo); // 단일 트리거: PLC_PAGE_CHANGE_REQ(0x08) 대기
sb.AddAction(SEQ_CHECK_RECIPENO_COMPLETE, CheckRecipeNoComp); // Recipe 먼저 확정/전환 (Page보다 상위)
sb.AddAction(SEQ_CHECK_PAGENO_COMPLETE, CheckPageNoComp); // 그다음 Page 확정
#region
sb.AddAction(SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ, SetUploadWeldingList_SeqParams);
sb.AddSubsequence(SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ, CommonSequenceManager.GetSeqBuilder_SeqUploadWeldingList());
sb.AddAction(SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ, GetUploadWeldingList_SeqResult);
#endregion
// 260513 plc
sb.AddAction(SEQ_CHECK_WELDING_START_READ_BIT, WeldingStartReadBit);
// 260622: 용접 전 Scout (M70509 ON 시)
sb.AddDecision(SEQ_PRE_SCOUT_CHECK, PreScoutCheck);
sb.AddAction(SEQ_PRE_SCOUT_SET_PARAMS, PreScoutSetParams);
sb.AddSubsequence(SEQ_PRE_SCOUT_RUN, SequenceManager.GetSeqBuilder_SeqScoutNormal());
sb.AddAction(SEQ_PRE_SCOUT_DONE, PreScoutDone);
sb.AddAction(SEQ_WAIT_WELDING_START, WaitWeldingStart);
sb.AddAction(SEQ_EXECUTE_WELDING, ExecuteWelding);
// 260622: 용접 후 Scout (M70509 ON 시) → 완료비트 송신
sb.AddDecision(SEQ_POST_SCOUT_CHECK, PostScoutCheck);
sb.AddAction(SEQ_POST_SCOUT_SET_PARAMS, PostScoutSetParams);
sb.AddSubsequence(SEQ_POST_SCOUT_RUN, SequenceManager.GetSeqBuilder_SeqScoutNormal());
sb.AddAction(SEQ_POST_SCOUT_DONE, PostScoutDone);
sb.AddAction(SEQ_WRITE_WELD_COMPLETE, WriteWeldComplete);
sb.AddDecision(SEQ_CHECK_CYCLE_STOP, CheckCycleStop); // 체크페이지
sb.AddAction(SEQ_SAVE_RESULT_DATA, SaveResultData); // 해당 공정의 정보를 기록 함.
sb.AddAction(SEQ_WRITE_COMPLETION_PLC_BIT, WriteCompletionPLCBit); // 정상적으로 종료 되면, complete bit write
sb.AddAction(SEQ_COMPLETE, Complete); // Enter로 올린다.
sb.Flows(sb.Init
> sb[SEQ_ENTER]
> sb[SEQ_CHECK_INIT_CONDITION]
> sb[SEQ_RESYNC_RECIPE_PAGE]
> sb[SEQ_CHECK_PAGENO] // 단일 트리거 PLC_PAGE_CHANGE_REQ(0x08) ON 대기
> sb[SEQ_CHECK_RECIPENO_COMPLETE] // Recipe 먼저 확정/전환
> sb[SEQ_CHECK_PAGENO_COMPLETE] // 그다음 Page 확정
#region TeachingRun 시퀸스의 중간 용접리스트 생성 구간
> sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ] // 1 파라미터 세팅 - TeachingRun 노드 참고
> sb[SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ] // 2 리스트 업로드 (서브시퀀스) - TeachingRun 노드 참고
> sb[SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ] // 3 업로드 결과 확인 - TeachingRun 노드 참고
#endregion
> sb[SEQ_CHECK_WELDING_START_READ_BIT] // while read bit (M70516 대기)
> sb[SEQ_PRE_SCOUT_CHECK] // 260622: M70509 ON → pre scout
> sb[SEQ_PRE_SCOUT_SET_PARAMS]
> sb[SEQ_PRE_SCOUT_RUN]
> sb[SEQ_PRE_SCOUT_DONE] // 0.1s 후 용접
> sb[SEQ_WAIT_WELDING_START] // 4 업로드 완료 PLC 완료 신호 및 BUSY 신호로 변경
> sb[SEQ_EXECUTE_WELDING] // 5 Scanner 실행 (실제 용접, 완료비트는 보류)
> sb[SEQ_POST_SCOUT_CHECK] // 260622: M70509 ON → post scout
> sb[SEQ_POST_SCOUT_SET_PARAMS]
> sb[SEQ_POST_SCOUT_RUN]
> sb[SEQ_POST_SCOUT_DONE] // 0.1s 후 완료비트
> sb[SEQ_WRITE_WELD_COMPLETE] // 용접 완료 비트 송신 → 다음 시퀀스
> sb[SEQ_CHECK_CYCLE_STOP]
> sb[SEQ_SAVE_RESULT_DATA]
> sb[SEQ_WRITE_COMPLETION_PLC_BIT] // PLC 비트 완료 신호 보내기
> sb[SEQ_COMPLETE] // 다시 enter로 올리기
> sb.Final);
// 260622: Scout 스킵 분기 (M70509 OFF → 기존과 동일하게 용접만)
sb.Flows(sb[SEQ_PRE_SCOUT_CHECK] > sb[SEQ_WAIT_WELDING_START]);
sb.Flows(sb[SEQ_POST_SCOUT_CHECK] > sb[SEQ_WRITE_WELD_COMPLETE]);
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);
sb.Draw("SeqAutoRunPTOP.pdf");
return sb;
}
private bool InitSequenceVariable(SequenceContext context)
{
Log.I($"[InitSequenceVariable] 캔버스 핸들 확보 + 용접대상 오브젝트 선택 시작");
var mainPage = CommonHelper.GetMainPage();
_ScannerCanvasUserCtrl = mainPage.ScannerCanvasCtrlTeaching;
_WeldingScannerListNo = eListNo.LIST1;
if (_InitParam.IsObjectSelectAll)
{
//_SelectedObjectList = _ScannerCanvasUserCtrl.Model.GetLayer(_CurrentPageNo).Objects;
_SelectedObjectList = _ScannerCanvasUserCtrl.Model.ActiveLayer.Objects;
}
else
{
_SelectedObjectList = _ScannerCanvasUserCtrl.Canvas.Model.SelectedObjects;
}
//Status.GetProcessingWeldingData().WeldingScannerListNo = _WeldingScannerListNo;
//Status.GetProcessingWeldingData().SelectedDrawObjectList = _SelectedObjectList;
Log.I($"[InitSequenceVariable] 변수 초기화 완료 (선택 오브젝트 {_SelectedObjectList.Count()}개)");
return true;
}
public SeqAutoRunPTOP()
{
_SystemParameter = Application.Current.AuroraEnv().SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter;
}
private SequenceActionResult Sequence_Enter(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[Sequence_Enter] 시퀀스 진입 - 레시피 초기화 및 장비 연결");
// 디버그 끝내고 삭제
MessageUtil.ShowToast("Sequence_Enter");
//Thread.Sleep(1000);
// 260618 RecipeManager 미리 확보(Recipe Change 노드 CheckRecipeNoComp에서 사용)
_RecipeManager = Application.Current.AuroraEnv().RecipeManager;
// 활성화된레시피ID
var activatedRecipeId = _RecipeManager.ActivatedRecipe().Id;
var recipeName = RecipeHelper.SearchRecipeItem(activatedRecipeId);
_InitParam = param.Param as SeqTeachingRun_InitParam; // 260511 - param NULL
Status.GetProcessingWeldingData().AutoRunStartTime = DateTime.Now;
Status.GetProcessingWeldingData().Clear();
Log.I($"[Sequence_Enter] 레시피 [{activatedRecipeId}], {recipeName.ToString()} 활성화, 장비 초기화 완료");
//레시피의 현재 변수 false인지 여부 확인할 것 .
#region [TEST_BYPASS] Alarm Check - Init Variable
/*
if (InitSequenceVariable(context) == false)
{
AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_TeachingRun_InitVariableError);
return null;
}
*/
InitSequenceVariable(context);
#endregion
// 장비 체크
InitDev(context);
_PLC.WriteBit(eBitOutput.PC_AUTO_STATUS, true); // 음극 (양극 P 미사용)
#region [TEST_BYPASS] Alarm Check - DeFvice Ready
/*
if (!CheckDeviceReady(context))
{ //Check Device
AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_AutoRun_DeviceNotReady);
Log.E("The devices are not active.");
return null;
}
*/
#endregion
Log.I($"[Sequence_Enter] 장비 초기화 완료");
Thread.Sleep(5); // 추후 제거
Log.I($"[Sequence_Enter] 완료 → CheckInitCondition 이동");
return SequenceAction.Ok();
}
private string CheckInitCondition(SequenceContext context, string id, ICollection<string> branches, SequenceActionResult param)
{
Log.I($"[CheckInitCondition] 초기 조건 확인 중");
Status.SetWorkingPosition(eWorkingPosition.CHECKING_CONDITION);
//if (!CheckCondition(context))
//{
// AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition);
// Log.E("Condition is not in state to start sequence.");
// context.PutValue(ContextKey.KEY_SEQUENCE_RESULT, false);
// return SEQ_COMPLETE;
//}
Thread.Sleep(5);
Log.I($"[CheckInitCondition] 조건 확인 완료 → ResyncRecipePage 이동");
return SEQ_RESYNC_RECIPE_PAGE;
}
private int _CenterBeamPower;
private int _RingBeamPower;
private int _CenterRingRatio;
#region 260806 구버전 보존 — CheckRecipeNoComp (원복: 아래 블록 주석을 풀고 뒤의 260806 신규 region을 제거)
/*
// CheckRecipeNoComp: 단일 트리거(PAGE_CHANGE_REQ 0x08) 통합 — Page보다 먼저 Recipe를 확정/전환.
// PLC_RECIPE_NO 1회 읽기 → 0/미등록/현재활성과 동일이면 전환·워드 재기록 생략(시퀀스 정지 없음).
// 실제로 다를 때만 ActivateRecipe + UI 스레드 동기 reload(Model.Layers 갱신 보장) + PC_RECIPE_NO 에코.
private SequenceActionResult CheckRecipeNoComp(SequenceContext context, string id, SequenceActionResult param)
{
// Recipe No 1회 읽기 (Page Change bit와 함께 래치되어 안정적이라고 가정 → 노이즈 검증 생략)
plcRecipeNo = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data;
Log.I($"[CheckRecipeNoComp] PLC_RECIPE_NO: [{plcRecipeNo}]");
// 0이면 레시피 전환 의사 없음 → 현 Recipe 유지하고 Page로
if (plcRecipeNo == 0)
{
Log.I("[CheckRecipeNoComp] PLC_RECIPE_NO=0 → 레시피 전환 생략, 현 Recipe 유지 → CheckPageNoComp 이동");
return SequenceAction.Ok();
}
// 등록 레시피 조회(.Id 1:1, 하드코딩 금지). 미등록이면 전환 생략(정지 안 함)
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo);
if (recipe == null)
{
Log.W($"[CheckRecipeNoComp] {plcRecipeNo}번 레시피 미등록 → 전환 생략, 현 Recipe 유지");
return SequenceAction.Ok();
}
// 이미 활성 레시피면 재활성화·Model reload는 생략하되, PC_RECIPE_NO 에코는 반드시 유지.
// (정지 시 OnSeqActionStop이 D70002=0으로 지우므로, 동일 레시피 재진입에서 에코를 건너뛰면 PLC가 0/이전값을 봄)
var preActivated = _RecipeManager.ActivatedRecipe();
if (preActivated != null && preActivated.Id == recipe.Id)
{
bool bRecipeCheck = true;
int count = 0;
while(bRecipeCheck)
{
plcRecipeNo = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data;
recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == plcRecipeNo);
// 널 가드: 재폴링 중 0/미등록이면 recipe==null → 전환하지 않고 계속 재확인
if (recipe != null && recipe.Id != preActivated.Id)
{
bRecipeCheck = false;
break;
}
Thread.Sleep(200);
if(count++ < 3) // 3번 체크하자
{
Log.I($"[CheckRecipeNoComp] RecipeCheckCount : {count} -> {recipe?.Id}");
continue;
}
// 끝까지 동일/무효 → 현 활성 레시피(preActivated) 번호로 에코 (null-safe)
_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, (short)preActivated.Id);
Log.I($"[CheckRecipeNoComp] 이미 활성 레시피({preActivated.Id}) → 재활성화 생략, PC_RECIPE_NO 에코({preActivated.Id}) 유지 → CheckPageNoComp 이동");
return SequenceAction.Ok();
}
}
// ── 실제 전환 ──
_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change");
Log.I($"[CheckRecipeNoComp] ActivateRecipe ({(preActivated == null ? -1 : preActivated.Id)} -> {recipe.Id})");
// ActivatedRecipe().Id 반영 대기 (비동기 race 주의)
var actStart = DateTime.Now;
while (!context.Stopped)
{
var cur = _RecipeManager.ActivatedRecipe();
if (cur != null && cur.Id == recipe.Id) break;
if (DateTime.Now.Subtract(actStart).TotalSeconds > PLC_TIMEOUT)
{
Log.W("[CheckRecipeNoComp] ActivateRecipe 반영 대기 TIMEOUT...");
actStart = DateTime.Now;
}
Thread.Sleep(10);
}
if (context.Stopped) return null;
// ★ Model.Layers를 새 레시피로 동기 갱신 (UI 스레드).
// ActivateRecipe의 이벤트 체인(ObserveOnDispatcher 2단)은 비동기라 Page 검증 전 Layers 갱신을 보장 못 함.
// RecipeHelper.OnActiveReloaded()가 ReloadRecipe를 동기 수행 → 반환 시점에 Layers가 새 레시피로 보장됨.
Application.Current.Dispatcher.Invoke(() => RecipeHelper.OnActiveReloaded());
Log.I("[CheckRecipeNoComp] Model.Layers 동기 reload 완료 (UI thread)");
// 에코 (실제 전환 시에만). PC_RECIPE_CHANGE_COMPLETE는 미사용(PLC가 감시 안 함).
_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, plcRecipeNo);
Log.I($"---> [CheckRecipeNoComp] Recipe 전환 완료 - Set PC_RECIPE_NO: [{plcRecipeNo}] → CheckPageNoComp 이동");
return SequenceAction.Ok();
}
*/
#endregion
#region 260806 신규 — CheckRecipeNoComp 에코 수렴형 (원복: 이 region 제거 후 위 구버전 주석 해제)
// 방침(2026-08-06 PLC 확정): PLC는 PC가 에코한 PC_RECIPE_NO 가 자기 값과 같아지는지를 상시 확인한다.
// 따라서 PC의 책임은 "정확히 읽고 → 레시피를 실제로 활성화하고 → 에코" 뿐이다.
// 어긋났을 때만 다시 읽어 고쳐 쓰고, 맞는 즉시 다음 노드로 넘어간다(대기는 다음 노드가 한다).
//
// 노드를 나갈 때 보장: PC_RECIPE_NO == PLC_RECIPE_NO 그리고 활성 레시피 Id == 그 번호
// ★ 워드만 맞추면 안 된다. 활성 레시피를 같이 갱신하지 않으면 PLC는 일치로 보고 통과시키는데
// PC는 옛 레시피로 용접한다. 이 경우는 PLC 게이트로도 막히지 않는다(에코가 맞으니 트리거가 나온다).
private SequenceActionResult CheckRecipeNoComp(SequenceContext context, string id, SequenceActionResult param)
{
Log.I("[CheckRecipeNoComp] PLC_RECIPE_NO 수렴 시작");
// 시뮬레이션: PLC 워드가 항상 0이라 아래 수렴 루프를 빠져나갈 수 없다.
// → 종전 동작(현 활성 레시피 유지 + 에코)으로 통과시킨다.
if (_PLC.IsSimul)
{
var simRecipe = _RecipeManager.ActivatedRecipe();
plcRecipeNo = (short)(simRecipe == null ? 0 : simRecipe.Id);
_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, plcRecipeNo);
Log.I($"[CheckRecipeNoComp] 시뮬레이션 → 수렴 생략, 현 활성 레시피({plcRecipeNo}) 에코 → CheckPageNoComp 이동");
return SequenceAction.Ok();
}
int retry = 0;
while (!context.Stopped)
{
if (retry > 0) Thread.Sleep(ECHO_RETRY_MS);
retry++;
if (AbortIfResyncTimedOut())
{
Log.W($"[CheckRecipeNoComp] 재동기 상한 {RESYNC_TIMEOUT_SEC}s 초과 — 수렴 미완으로 포기 " +
$"(재시도 {retry}회차, 직전 회차 에코가 남아 있을 수 있음)");
return SequenceAction.Ok();
}
// ── 1. PLC가 준 Recipe No 읽기 ──
var read = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO);
if (read.err != Hermes.E1.OK)
{
Log.E($"[CheckRecipeNoComp] PLC_RECIPE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차");
continue;
}
short readRecipeNo = read.data;
// 0은 정상 상태가 아니다. PLC는 워드를 항상 써놓는다(260804 확인).
// 미연결·미활성이면 ReadWord가 (NOT_READY/NOT_CONNECTED, 0)을 돌려주므로 여기서도 걸러진다.
if (readRecipeNo == 0)
{
if (retry == 1 || retry % ECHO_TOAST_EVERY == 0)
MessageUtil.ShowErrorToast("PLC Recipe No = 0\n모니터링 확인해주세요.");
Log.E($"[CheckRecipeNoComp] PLC_RECIPE_NO=0 (이상값) — 에코하지 않고 재시도 {retry}회차");
continue;
}
// 등록 레시피 조회(.Id 1:1, 하드코딩 금지). 미등록이면 에코하지 않는다 —
// PLC가 넘어가지 않으므로 잘못된 레시피로 용접이 나갈 수 없다.
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == readRecipeNo);
if (recipe == null)
{
if (retry == 1 || retry % ECHO_TOAST_EVERY == 0)
MessageUtil.ShowErrorToast($"미등록 레시피: {readRecipeNo}\n레시피 확인해주세요.");
Log.E($"[CheckRecipeNoComp] {readRecipeNo}번 레시피 미등록 — 에코하지 않고 재시도 {retry}회차");
continue;
}
// ── 2. 내부 상태 갱신: 실제 레시피 활성화 (에코와 같은 세트) ──
var preActivated = _RecipeManager.ActivatedRecipe();
if (preActivated == null || preActivated.Id != recipe.Id)
{
_RecipeManager.ActivateRecipe(recipe.Id, "PLC Recipe Change");
Log.I($"[CheckRecipeNoComp] ActivateRecipe ({(preActivated == null ? -1 : preActivated.Id)} -> {recipe.Id})");
if (!WaitRecipeActivated(context, recipe.Id))
{
if (context.Stopped) return null;
Log.W($"[CheckRecipeNoComp] ActivateRecipe({recipe.Id}) 반영 대기 TIMEOUT — 에코하지 않고 재시도 {retry}회차");
continue;
}
// ★ Model.Layers를 새 레시피로 동기 갱신 (UI 스레드).
// ActivateRecipe의 이벤트 체인(ObserveOnDispatcher 2단)은 비동기라
// Page 검증 전 Layers 갱신을 보장 못 한다.
Application.Current.Dispatcher.Invoke(() => RecipeHelper.OnActiveReloaded());
Log.I("[CheckRecipeNoComp] Model.Layers 동기 reload 완료 (UI thread)");
}
// ── 3. 에코 ──
int werr = _PLC.WriteWord(eWordOutput.PC_RECIPE_NO, readRecipeNo);
if (werr != Hermes.E1.OK)
{
Log.E($"[CheckRecipeNoComp] PC_RECIPE_NO 쓰기 실패 (err:{werr}) — 재시도 {retry}회차");
continue;
}
// ── 4. 되읽어 수렴 확인 (내가 쓴 값이 반영됐는가 + PLC가 여전히 같은 값인가) ──
short echoed = _PLC.ReadOutWord(eWordOutput.PC_RECIPE_NO).data;
short current = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data;
if (echoed == readRecipeNo && current == readRecipeNo)
{
plcRecipeNo = readRecipeNo;
Log.I($"---> [CheckRecipeNoComp] 수렴 완료 ({retry}회차) - PC_RECIPE_NO: [{plcRecipeNo}] → CheckPageNoComp 이동");
break;
}
Log.W($"[CheckRecipeNoComp] 미수렴 - 읽음:[{readRecipeNo}] 에코:[{echoed}] 현재PLC:[{current}] — 재시도 {retry}회차");
}
if (context.Stopped) return null;
return SequenceAction.Ok();
}
// ActivatedRecipe().Id 가 targetId 로 바뀔 때까지 대기. 타임아웃이면 false를 돌려주고 호출부가 재시도한다.
// (구버전 :408-419는 타임아웃 시 시작시각만 리셋해 빠져나가지 못했다)
private bool WaitRecipeActivated(SequenceContext context, int targetId)
{
var start = DateTime.Now;
while (!context.Stopped)
{
var cur = _RecipeManager.ActivatedRecipe();
if (cur != null && cur.Id == targetId) return true;
if (DateTime.Now.Subtract(start).TotalSeconds > PLC_TIMEOUT) return false;
Thread.Sleep(10);
}
return false;
}
#endregion
#region 260813
private SequenceActionResult ResyncRecipePage(SequenceContext context, string id, SequenceActionResult param)
{
if (ResumeMode)
{
Log.I("[ResyncRecipePage] ResumeMode=true → 재동기 생략 → CheckPageNo 이동");
return SequenceAction.Ok();
}
if (_PLC == null)
{
Log.E("[ResyncRecipePage] _PLC instance is null! 재동기 불가");
return null;
}
if (_PLC.IsSimul)
{
Log.I("[ResyncRecipePage] 시뮬레이션 → 재동기 생략 → CheckPageNo 이동");
return SequenceAction.Ok();
}
var recipeRead = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO);
var pageRead = _PLC.ReadWord(eWordInput.PLC_PAGE_NO);
bool recipeValid = recipeRead.err == Hermes.E1.OK
&& recipeRead.data != 0
&& _RecipeManager.Recipes().Any(x => x.Id == recipeRead.data);
if (pageRead.err != Hermes.E1.OK || pageRead.data < 1 || !recipeValid)
{
Log.W($"[ResyncRecipePage] PLC 워드가 재동기 조건 미충족 " +
$"(RecipeNo:{recipeRead.data}/err:{recipeRead.err}/유효:{recipeValid}, " +
$"PageNo:{pageRead.data}/err:{pageRead.err}) → 재동기 생략, CheckPageNo 의 REQ 대기로 진행");
return SequenceAction.Ok();
}
Log.I($"[ResyncRecipePage] 재기동 재동기 시작 — PLC 값(RecipeNo:{recipeRead.data}, " +
$"PageNo:{pageRead.data})으로 Recipe/Page 확정 후 에코");
try
{
Application.Current.Dispatcher.Invoke(() => RecipeHelper.OnActiveReloaded());
Log.I("[ResyncRecipePage] Model.Layers 선(先) reload 완료 (UI thread)");
}
catch (Exception ex)
{
Log.E($"[ResyncRecipePage] Model.Layers 선(先) reload 실패 ({ex.GetType().Name}: {ex.Message}) " +
$"→ 재동기 생략, CheckPageNo 의 REQ 대기로 진행");
MessageUtil.ShowErrorToast("레시피 로드 실패\n재동기를 건너뜁니다. 레시피를 확인해주세요.");
return SequenceAction.Ok();
}
_ResyncStartTime = DateTime.Now;
_ResyncAborted = false;
_InResyncNode = true;
try
{
if (CheckRecipeNoComp(context, id, param) == null) return null;
if (_ResyncAborted) return ResyncGiveUp("Recipe");
if (CheckPageNoComp(context, id, param) == null) return null;
if (_ResyncAborted) return ResyncGiveUp("Page");
}
finally
{
_InResyncNode = false;
}
Log.I($"---> [ResyncRecipePage] 재동기 완료 (RecipeNo:{plcRecipeNo}, PageNo:{plcPageNo}, " +
$"PC_Index:{_CurrentPageNo}) → CheckPageNo 이동");
return SequenceAction.Ok();
}
private bool AbortIfResyncTimedOut()
{
if (!_InResyncNode) return false;
if (DateTime.Now.Subtract(_ResyncStartTime).TotalSeconds <= RESYNC_TIMEOUT_SEC) return false;
_ResyncAborted = true;
return true;
}
private SequenceActionResult ResyncGiveUp(string stage)
{
Log.W($"[ResyncRecipePage] {stage} 수렴 실패(상한 {RESYNC_TIMEOUT_SEC}s 초과) → 재동기 포기, " +
$"CheckPageNo 의 REQ 대기로 진행 " +
$"(직전 회차에 쓴 에코/내부 상태는 남아 있을 수 있음 — RecipeNo:{plcRecipeNo}, PageNo:{plcPageNo})");
return SequenceAction.Ok();
}
#endregion
// 페이지 체크 - 페이지만 감시 AutoRunStatus는 실제 상위에서 동작 체크하고 시퀸스 동작한다.
private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[CheckPageNo] PLC_PAGE_CHANGE_REQ 신호 대기 중...");
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
// 디버그 끝내고 삭제
//MessageUtil.ShowToast("CheckPageNo -> Wait...");
//Thread.Sleep(1000);
// 1. PLC_PAGE_CHANGE_REQ (bit 8) ON 대기
var interfaceStartTime = DateTime.Now;
while (!context.Stopped)
{
//plc
if (_PLC == null)
{
Log.E("[CheckPageNo] _PLC instance is null! Handshake cannot proceed.");
return null;
}
if (_PLC.ReadBit(eBitInput.PLC_PAGE_CHANGE_REQ).data)
{
Log.I("---> [CheckPageNo] Check OK -> PLC_PAGE_CHANGE_REQ : ON");
// 임시 미리 한 번 더 읽기 260515
//plcPageNo = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data;
//Log.I($"---> [CheckPageNo] Check OK -> PLC_PAGE_CHANGE_REQ : {plcPageNo}");
// 디버그 끝내고 삭제
//MessageUtil.ShowToast("PLC_PAGE_CHANGE_REQ : ON");
//Thread.Sleep(1000);
break;
}
else
{
//Log.I("[CheckPageNo] Detected PLC_PAGE_CHANGE_REQ: f");
}
if (DateTime.Now.Subtract(interfaceStartTime).TotalSeconds > PLC_TIMEOUT)
{
Log.W("[CheckPageNo] Waiting for PLC_PAGE_CHANGE_REQ -> TIMEOUT...");
interfaceStartTime = DateTime.Now;
}
Thread.Sleep(10);
}
if (context.Stopped) return null;
Log.I($"[CheckPageNo] PLC_PAGE_CHANGE_REQ ON 감지 → CheckPageNoComp 이동");
return SequenceAction.Ok();
}
#region 260806 구버전 보존 — CheckPageNoComp (원복: 아래 블록 주석을 풀고 뒤의 260806 신규 region을 제거)
/*
private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[CheckPageNoComp] PLC 페이지 번호 읽기 시작 (3회 연속 검증)");
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
int checknum = 0;
int invalidPageWait = 0; // 260805: 범위 밖 PageNo로 대기한 회차 (중단하지 않고 대기)
const int TOAST_EVERY = 10; // 토스트 폭주 방지 — 첫 회차 + 이후 N회마다만 표시
plcPageNoList = new List<short>();
while (!context.Stopped)
{
//Thread.Sleep(1000);
// 2. PLC 데이터 읽기 및 인덱스 변환 (1,2 -> 0,1)
//plcPageNo = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data;
plcPageNoList.Add(_PLC.ReadWord(eWordInput.PLC_PAGE_NO).data);
plcPageNo = plcPageNoList[checknum];
checknum++;
_CurrentPageNo = plcPageNo - 1; // 1-based to 0-based // page index
// 디버그 끝내고 삭제
//MessageUtil.ShowToast($"ReadWord -> {plcPageNo}");
Thread.Sleep(200);
string cellId = _PLC.CellId();
string lotId = _PLC.LotId();
Status.GetProcessingWeldingData().CellId = cellId;
Status.GetProcessingWeldingData().LotId = lotId;
Log.I($"[CheckPageNoComp]페이지 확인 [{checknum}/3] - PLC_Page: [{plcPageNo}], PC_Index: [{_CurrentPageNo}], CellId: [{cellId}], LotId: [{lotId}]");
if (checknum > 2)
{
// 260618: 상한 하드코딩(>2) 제거 → 활성 Recipe의 페이지 수(레이어 수)로 가변 검증 (Recipe별 2~5페이지)
// B-1: Model.Layers 접근을 UI 스레드로 직렬화 (레시피 reload와의 race 방지)
int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count());
if (plcPageNo > recipePageCount || plcPageNo < 1)
{
// 260805 가드: 종전에는 여기서 return null 이었다. Aurora에서 Action이 null을 돌려주면
// 시퀀스가 중단되므로(sequence-builder 규칙: "null 중단"), 일시적인 오독 한 번에
// AutoRun이 죽고 작업자가 재시작해야 했다.
// → 중단하지 않고 이 노드에서 대기하며 재수집한다. 작업자가 토스트를 보고
// 모니터링에서 값을 확인·조치하면 다음 회차에 정상값을 읽어 그대로 이어간다.
// ※ recipePageCount는 매 회차 활성 Recipe의 Layers 수로 다시 구한다.
// Recipe마다 페이지 수가 다르고, 레시피 반영이 늦으면 이 값도 늦게 맞으므로
// 재시도 자체가 그 지연을 흡수한다.
// ※ 빠져나가는 경로는 while 조건의 context.Stopped — Stop 버튼은 정상 동작한다.
invalidPageWait++;
Log.E($"[CheckPageNoComp] Invalid PageNo received: {plcPageNo} " +
$"(활성 Recipe 페이지 수: {recipePageCount}) — 대기 {invalidPageWait}회차, 시퀀스 중단하지 않음");
if (invalidPageWait == 1 || invalidPageWait % TOAST_EVERY == 0)
MessageUtil.ShowErrorToast($"PLC Page No 이상: {plcPageNo}\n모니터링 확인해주세요.");
plcPageNoList.Clear();
checknum = 0;
Thread.Sleep(3000);
continue;
}
else
{
//for (int i = 0; i > plcPageNoList.Count; i++)
//{
Log.E($"[CheckPageNoComp] List {plcPageNoList[0]}/{plcPageNoList[1]}/{plcPageNoList[2]}");
//}
break;
}
}
}
Thread.Sleep(5);
// 3. 에코 및 완료 비트(Ack) ON
_PLC.WriteWord(eWordOutput.PC_PAGE_NO, plcPageNo);
Log.I($"---> [CheckPageNoComp] Set PC_PAGE_NO: {plcPageNo}");
//Thread.Sleep(1000);
//MessageUtil.ShowErrorToast($"WriteWord -> {plcPageNo}");
//Thread.Sleep(1000);
// 에코한 PC_PAGE_NO(plcPageNo)와 현재 PLC_PAGE_NO 비교 → 같으면 ACK ON, 다르면 OFF
short curPlcPageNo = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data;
bool pageMatched = (plcPageNo == curPlcPageNo);
_PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, pageMatched);
Log.I($"[CheckPageNoComp] PC_PAGE_NO({plcPageNo}) == PLC_PAGE_NO({curPlcPageNo}) ? → PC_PAGE_CHANGE_COMPLETE {pageMatched}");
//Thread.Sleep(5);
// UI 페이지 업데이트 알림
//if (Application.Current != null && Application.Current.AuroraEnv() != null)
{
Application.Current.AuroraEnv().EventPublisher.Publish(new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo));
}
Thread.Sleep(5); // 추후 제거
Log.I($"[CheckPageNoComp] 페이지 확정 - PC_PAGE_NO: [{plcPageNo}] 에코 완료 → WeldingList 업로드 이동");
return SequenceAction.Ok();
}
*/
#endregion
#region 260806 신규 — CheckPageNoComp 에코 수렴형 (원복: 이 region 제거 후 위 구버전 주석 해제)
// 노드를 나갈 때 보장: PC_PAGE_NO == PLC_PAGE_NO 그리고 _CurrentPageNo == PC_PAGE_NO - 1
// ★ 워드만 맞추고 _CurrentPageNo를 갱신하지 않으면 PLC는 일치로 보고 통과시키는데
// PC는 옛 페이지 레이어로 용접한다. 반드시 "재읽기 → _CurrentPageNo 갱신 → 에코"를 한 세트로 처리한다.
//
// 구버전과의 차이:
// - 3회 수집(Sleep 200×3) 제거 → 정상 흐름 지연 0. 값 안정성은 되읽기 일치로 확인한다.
// (구버전 로그는 "3회 연속 검증"이라 했으나 3개 값을 비교하는 코드가 없었다)
// - 범위 밖 PageNo: 종전 Sleep(3000) 무한 대기 → 200ms 재읽기. 에코하지 않으므로
// PLC가 넘어가지 않아 잘못된 페이지로 용접이 나갈 수 없다.
// - while 탈출 후 context.Stopped 가드 추가 (종전에는 정지 중에도 미검증 값을 에코했다)
private SequenceActionResult CheckPageNoComp(SequenceContext context, string id, SequenceActionResult param)
{
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
// 260818: Recipe/Page 최종 확인 (원복: 이 outer while 과 아래 "최종 확인" 블록을 제거하고
// 안쪽 블록의 들여쓰기만 되돌리면 260806 상태가 된다)
// 문제(260812 로그 15:17:29): CheckRecipeNoComp 가 Recipe 를 22로 확정하고 나온 뒤
// Page 수렴 중 PLC 가 Page 를 6→2로 바꿨다. Page 만 재수렴하고 넘어갔으나 PLC 는 그때
// Recipe 도 함께 바꿨을 수 있고, PC 는 옛 값(22)을 에코한 채였다. PLC 게이트는 두 워드를
// 모두 보므로 PLC_WELD_TRIGGER_REQ 가 끝내 오지 않고 타임아웃만 반복됐다.
// → Page 수렴이 끝나면 PLC 의 Recipe/Page 를 다시 읽어 PC 확정값과 둘 다 같은지 확인하고,
// 어긋났으면 Recipe 부터 다시 확정(CheckRecipeNoComp)한 뒤 Page 를 재수렴한다.
int joint = 0;
while (!context.Stopped)
{
joint++;
Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작");
int retry = 0;
while (!context.Stopped)
{
if (retry > 0) Thread.Sleep(ECHO_RETRY_MS);
retry++;
if (AbortIfResyncTimedOut())
{
Log.W($"[CheckPageNoComp] 재동기 상한 {RESYNC_TIMEOUT_SEC}s 초과 — 수렴 미완으로 포기 " +
$"(재시도 {retry}회차, 직전 회차 에코가 남아 있을 수 있음)");
return SequenceAction.Ok();
}
// ── 1. PLC가 준 Page No 읽기 ──
var read = _PLC.ReadWord(eWordInput.PLC_PAGE_NO);
if (read.err != Hermes.E1.OK)
{
Log.E($"[CheckPageNoComp] PLC_PAGE_NO 읽기 실패 (err:{read.err}) — 재시도 {retry}회차");
continue;
}
short p = read.data;
// 상한은 활성 Recipe의 페이지 수(레이어 수)로 가변 검증한다 (Recipe별 2~5페이지, 하드코딩 금지).
// Model.Layers 접근은 UI 스레드로 직렬화 (레시피 reload와의 race 방지).
int recipePageCount = Application.Current.Dispatcher.Invoke(() => _ScannerCanvasUserCtrl.Model.Layers.Count());
if (p < 1 || p > recipePageCount)
{
if (retry == 1 || retry % ECHO_TOAST_EVERY == 0)
MessageUtil.ShowErrorToast($"PLC Page No 이상: {p}\n모니터링 확인해주세요.");
Log.E($"[CheckPageNoComp] Invalid PageNo: {p} (활성 Recipe 페이지 수: {recipePageCount}) " +
$"— 에코하지 않고 재시도 {retry}회차");
continue;
}
// ── 2. 내부 상태 갱신 (에코와 같은 세트) ──
_CurrentPageNo = p - 1; // 1-based to 0-based // page index
// ── 3. 에코 ──
int werr = _PLC.WriteWord(eWordOutput.PC_PAGE_NO, p);
if (werr != Hermes.E1.OK)
{
Log.E($"[CheckPageNoComp] PC_PAGE_NO 쓰기 실패 (err:{werr}) — 재시도 {retry}회차");
continue;
}
// ── 4. 되읽어 수렴 확인 ──
Thread.Sleep(200);
short echoed = _PLC.ReadOutWord(eWordOutput.PC_PAGE_NO).data;
short current = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data;
if (echoed == p && current == p)
{
plcPageNo = p;
Log.I($"---> [CheckPageNoComp] 수렴 완료 ({retry}회차) - PC_PAGE_NO: [{plcPageNo}], PC_Index: [{_CurrentPageNo}]");
break;
}
Log.W($"[CheckPageNoComp] 미수렴 - 읽음:[{p}] 에코:[{echoed}] 현재PLC:[{current}] — 재시도 {retry}회차");
}
if (context.Stopped) return null;
// ── 5. 최종 확인: Recipe/Page 를 다시 읽어 둘 다 PC 확정값과 같아야 넘어간다 ──
// 시뮬레이션은 PLC 워드가 항상 0이라 최종 확인 대상이 아니다.
if (_PLC.IsSimul) break;
short curRecipe = _PLC.ReadWord(eWordInput.PLC_RECIPE_NO).data;
short curPage = _PLC.ReadWord(eWordInput.PLC_PAGE_NO).data;
if (curRecipe == plcRecipeNo && curPage == plcPageNo)
{
Log.I($"---> [CheckPageNoComp] Recipe/Page 최종 확인 완료 ({joint}회차) - " +
$"PC_RECIPE_NO: [{plcRecipeNo}], PC_PAGE_NO: [{plcPageNo}]");
break;
}
if (joint == 1 || joint % ECHO_TOAST_EVERY == 0)
MessageUtil.ShowErrorToast($"확정 후 Recipe/Page 변경 감지\n" +
$"Recipe {plcRecipeNo}→{curRecipe}, Page {plcPageNo}→{curPage}, 재동기화 중");
Log.W($"[CheckPageNoComp] 최종 확인 불일치 - Recipe PLC:[{curRecipe}]/에코:[{plcRecipeNo}], " +
$"Page PLC:[{curPage}]/에코:[{plcPageNo}] — Recipe 재확정 후 Page 재수렴 {joint}회차");
// Recipe 부터 다시 확정한다(ActivateRecipe + Model.Layers reload + PC_RECIPE_NO 재에코).
// 레시피가 바뀌면 Layers(페이지 수)도 바뀌므로 Page 는 위에서 다시 수렴한다.
if (CheckRecipeNoComp(context, id, param) == null) return null;
}
if (context.Stopped) return null;
// ── 수렴 후 1회만 수행 ──
string cellId = _PLC.CellId();
string lotId = _PLC.LotId();
Status.GetProcessingWeldingData().CellId = cellId;
Status.GetProcessingWeldingData().LotId = lotId;
Log.I($"[CheckPageNoComp] CellId: [{cellId}], LotId: [{lotId}]");
// PLC는 이 비트를 보지 않는다(260806 확인 — 게이트는 워드다). 기존 배선 호환을 위해 ON은 유지한다.
if (_InResyncNode)
{
Log.I("[CheckPageNoComp] 재동기 경로 → PC_PAGE_CHANGE_COMPLETE 쓰기 생략 (REQ 미수신 상태)");
}
else
{
_PLC.WriteBit(eBitOutput.PC_PAGE_CHANGE_COMPLETE, true);
}
// UI 페이지 업데이트 알림
Application.Current.AuroraEnv().EventPublisher.Publish(
new EventMessage("", CommonEventIds.EVENT_AUTO_RUN_UPDATE_PAGENUM, (int)_CurrentPageNo));
Log.I($"[CheckPageNoComp] 페이지 확정 - PC_PAGE_NO: [{plcPageNo}] 에코 완료 → WeldingList 업로드 이동");
return SequenceAction.Ok();
}
#endregion
private bool LaserReadyResponse()
{
Log.I($"[LaserReadyResponse] 레이저 Ready 응답 (현재 no-op)");
//if (ApplicationExt.IsSimulation)
return true;
}
private SequenceActionResult SetUploadWeldingList_SeqParams(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[SetUploadWeldingList_SeqParams] PageNo [{_CurrentPageNo}] 용접 리스트 레이어 선택 시작");
Status.SetWorkingPosition(eWorkingPosition.UPLOADING_WELD_LIST);
#region 페이지 번호에 따라 용접 리스트 레이어 선택 됨. 아래 데이터가 정확한지는 확인 필요
// 페이지 번호에 따라 용접 리스트(레이어) 선택
int totalLineCount = 0;
// B-1: Model.Layers 접근(레이어 선택)을 UI 스레드로 직렬화 (레시피 reload와의 race 방지)
Application.Current.Dispatcher.Invoke(() =>
{
int layerCount = _ScannerCanvasUserCtrl.Model.Layers.Count();
if (layerCount > _CurrentPageNo && _CurrentPageNo >= 0)
{
var layer = _ScannerCanvasUserCtrl.Model.Layers[_CurrentPageNo];
_SelectedObjectList = layer.Objects;
Log.I($"[SetUploadWeldingList] Selected PageNo [{_CurrentPageNo}] → Layer Name: [{layer.No}], Object Count: [{_SelectedObjectList.Count()}]");
}
else
{
Log.E($"[SetUploadWeldingList] Invalid PageNo [{_CurrentPageNo}] (Total Layers: {layerCount}) → Fallback to Active Layer");
_SelectedObjectList = _ScannerCanvasUserCtrl.Model.ActiveLayer.Objects;
Log.W($"[SetUploadWeldingList] Fallback Layer Name: [{_ScannerCanvasUserCtrl.Model.ActiveLayer.No}], Object Count: [{_SelectedObjectList.Count()}]");
}
});
if (_SelectedObjectList == null || _SelectedObjectList.Count() == 0)
{
Log.E("[SetUploadWeldingList] No welding objects found! Sequence will fail.");
return null;
}
// 공통 로그 - if/else 밖으로 이동
var groups = _SelectedObjectList.OfType<Group>().ToList();
totalLineCount = groups.Sum(g => g.SubObjects.OfType<Line>().Count());
Log.I($"[SetUploadWeldingList] Group Count: [{groups.Count}], Total Line Count: [{totalLineCount}]");
for (int i = 0; i < groups.Count; i++)
{
int lineCount = groups[i].SubObjects.OfType<Line>().Count();
Log.I($"[SetUploadWeldingList] Group[{i}] Line Count: [{lineCount}]");
}
#endregion
// 각도(D70510) 기반 Front/Down offset 선택 (기존 단일 CellCenterOffset 대체)
var thetaOffsetSetting = Application.Current.AuroraEnv().SettingEntry[SettingIds.LaserInterlock] as LaserInterlockSetting;
var (angleOffX, angleOffY) = LaserAngleOffsetSelector.Select(_PLC, _SystemParameter, thetaOffsetSetting, Log, "ExecuteWelding");
SeqUploadWeldingList_InitParam seqParams = new SeqUploadWeldingList_InitParam()
{
NeedSetLaserTiming = _InitParam.NeedSetLaserTiming,
IncludeListStartEnd = true,
CellMeasureCenterOffsetX = angleOffX + _InitParam.CellCenterOffsetX,
CellMeasureCenterOffsetY = angleOffY + _InitParam.CellCenterOffsetY,
SelectedDrawObjectList = _SelectedObjectList, // 이게 중요 포인트
WeldingScannerListNo = _WeldingScannerListNo,
};
//Thread.Sleep(1000); // 추후 제거
Log.I($"[SetUploadWeldingList_SeqParams] 용접 파라미터 세팅 완료 → UploadWeldingList 서브시퀀스 실행");
return SequenceAction.Ok(seqParams);
}
private SequenceActionResult GetUploadWeldingList_SeqResult(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[GetUploadWeldingList_SeqResult] 용접 리스트 업로드 결과 확인 중");
Status.SetWorkingPosition(eWorkingPosition.UPLOADING_WELD_LIST);
var resultOutParam = param.Param as SeqUploadWeldingList_OutParam;
if (resultOutParam.Success == false)
{
//Thread.Sleep(1000); // 추후 제거
Log.E($"[GetUploadWeldingList_SeqResult] 업로드 실패 → 시퀀스 중단");
return null;
}
//Thread.Sleep(1000); // 추후 제거
Log.I($"[GetUploadWeldingList_SeqResult] 업로드 성공 → WeldingStartReadBit 이동");
return SequenceAction.Ok();
}
// 260513 추가노드
private SequenceActionResult WeldingStartReadBit(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"---> [WeldingStartReadBit] PLC_WELD_TRIGGER_REQ 신호 대기 중... ");
// 1. PLC 용접 요청 대기
var interfaceStartTime = DateTime.Now;
while (!context.Stopped)
{
if (_PLC == null)
{
Log.E("[WeldingStartReadBit] _PLC instance is null!");
return null;
}
if (_PLC.ReadBit(eBitInput.PLC_WELD_TRIGGER_REQ).data)
{
Log.I($"[WeldingStartReadBit] Check OK -> PLC_WELD_TRIGGER_REQ : ON");
// 디버그 끝내고 삭제
MessageUtil.ShowToast("PLC_WELD_TRIGGER_REQ : ON");
//Thread.Sleep(1000);
break;
}
if (DateTime.Now.Subtract(interfaceStartTime).TotalSeconds > PLC_TIMEOUT)
{
Log.W($"[WeldingStartReadBit] PLC_WELD_TRIGGER_REQ -> TIMEOUT...");
interfaceStartTime = DateTime.Now;
}
Thread.Sleep(10);
}
Log.I($"---> [WeldingStartReadBit] PLC_WELD_TRIGGER_REQ ON 감지 → WaitWeldingStart 이동");
return SequenceAction.Ok();
}
private SequenceActionResult WaitWeldingStart(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[---> WaitWeldingStart] 용접 준비 신호 송신 - PageNo: [{plcPageNo}] CurrentPage: {_CurrentPageNo + 1})");
Status.SetWorkingPosition(eWorkingPosition.WAITING_WELD_START);
// 양극(P) 미사용 → 모든 페이지 음극 용접 비트로 응답 (PLC 트리거가 음극 PLC_WELD_TRIGGER_REQ라 정합)
_PLC.WriteBit((int)eBitOutput.PC_WELD_TRIGGER_ACK, true);
Thread.Sleep(200);
_PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE, false);
Thread.Sleep(5);
Log.I($"[WaitWeldingStart] Welding READY_ACK=ON / BUSY=ON / COMPLETE=OFF 송신 (PageNo:{plcPageNo})");
// 디버그 끝내고 삭제
//MessageUtil.ShowToast("WaitWeldingStart -> WriteBit True");
//Thread.Sleep(3000);
Log.I($"---> [WaitWeldingStart] READY_ACK/BUSY ON, COMPLETE OFF 완료 → ExecuteWelding 이동");
return SequenceAction.Ok();
}
// 이곳에서 SeqTeachingRun.cs 를 태운다. (레시피의 WeldingRun 호출 구간)
private SequenceActionResult ExecuteWelding(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[ExecuteWelding] 용접 실행 시작 - PageNo: [{plcPageNo}], ListNo: [{_WeldingScannerListNo}]"); // ← 여기
#region 코드 확인 필요
Status.SetWorkingPosition(eWorkingPosition.EXECUTING_WELD); // 현재 시퀸스 위치
// === Head Theta 인터락: 수평/수직(±tol)에서만 발진 허용 (체크박스 사용 시) ===
var thetaSetting = Application.Current.AuroraEnv().SettingEntry[SettingIds.LaserInterlock] as LaserInterlockSetting;
if (thetaSetting != null && thetaSetting.HeadThetaInterlockEnabled)
{
bool blocked = false;
if (_PLC == null || !_PLC.IsActivated)
{
Log.W("[ExecuteWelding] HeadTheta 인터락 - PLC 미연결/미활성 → 발진 차단");
blocked = true; // fail-closed
}
else
{
short thetaRaw = _PLC.ReadWord(eWordInput.HEAD_THETA_POSITION).data;
double scale = thetaSetting.HeadThetaPlcScale != 0 ? thetaSetting.HeadThetaPlcScale : 1; // 0 가드
double thetaDeg = thetaRaw / scale;
bool okFront = Math.Abs(thetaDeg - thetaSetting.HeadThetaFront) <= thetaSetting.HeadThetaTolerance;
bool okDown = Math.Abs(thetaDeg - thetaSetting.HeadThetaDown) <= thetaSetting.HeadThetaTolerance;
Log.I($"[ExecuteWelding] HeadTheta raw={thetaRaw}, deg={thetaDeg:F1} (Front={thetaSetting.HeadThetaFront}/Down={thetaSetting.HeadThetaDown}/±{thetaSetting.HeadThetaTolerance})");
// ① 안전 게이트(기존): 안전 구역(Front/Down 부근) 아니면 차단
if (!okFront && !okDown)
{
Log.W($"[ExecuteWelding] ① HeadTheta 인터락 → Front/Down ±{thetaSetting.HeadThetaTolerance}° 벗어남(deg={thetaDeg:F1}) → 발진 차단");
blocked = true;
}
else
{
// ② 레시피별 방향 차단(신규): 체크된 방향의 구역이면 차단
var rl = WeldingCommon.Settings.RecipeLaserInterlock.Load();
if ((rl.Front && okFront) || (rl.Down && okDown))
{
Log.W($"[ExecuteWelding] ② 레시피 차단 deg={thetaDeg:F1} Front?{rl.Front}/{okFront} Down?{rl.Down}/{okDown} → 발진 차단");
blocked = true;
}
}
}
if (blocked)
{
try { AlarmManager.Alarm(AlarmCode_PTOP.ALARM_LASER_HEAD_THETA_INTERLOCK); }
catch (Exception ex) { Log.E($"[ExecuteWelding] HeadTheta 알람 표시 실패(테이블 미등록?): {ex.Message}"); }
return null; // 발진 차단 + 시퀀스 중단
}
}
// 260626 Busy bit for Laser On Display
_PLC.WriteBit((int)eBitOutput.PC_WELD_BUSY, true);
_Scanner.SelectCorrectionTable(2);
#region 260824 이전 원본 (Core/Ring 2발)
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.CoreLaser_Control), true);
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.RingLaser_Control), true);
//Log.I($"[ExecuteWelding] Laser Gate ON (Core+Ring)");
#endregion
// 260824 본 레이저 단일 발진기 — 제어권 요청 1발 (구 Core/Ring_Control 자리)
_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.Laser_REMOTE), true);
Log.I($"[ExecuteWelding] Laser Gate ON (REMOTE)");
Thread.Sleep(laserEmissionGateDelayMs); // 260518 매뉴얼 기준 ≥10ms, 여유분 포함 =15ms 적용함.
Log.I($"[ExecuteWelding] Gate ON Delay {laserEmissionGateDelayMs}ms 완료 → ExecuteList 시작");
// ── SeqTeachingRun SubSequence 실행 ──────────────────────────────
DateTime weldingTime = DateTime.Now;
Status.GetProcessingWeldingData().WeldingStartTime = weldingTime;
_Scanner.ExecuteList(_WeldingScannerListNo); // 레이저 출사 막을 땐 주석
Log.I($"[ExecuteWelding] ExecuteList 시작 - ListNo: {_WeldingScannerListNo}");
_Scanner.WaitForListTaskCompleted(1000);//(_SystemParameter.WeldLinesTaskTimeOut);
Log.I($"[ExecuteWelding] WaitForListTaskCompleted 완료");
#region Gate OFF 위치 변경 - 용접 완료 즉시 OFF (TruPulse nano 매뉴얼 기준 ≥10ms)
// ↓ 이 두 줄 삭제
#region 260824 이전 원본 (Core/Ring 2발)
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.CoreLaser_Control), false);
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.RingLaser_Control), false);
//Log.I($"[ExecuteWelding] Laser Gate OFF (Core+Ring)");
#endregion
// 260824 본 레이저 단일 발진기 — 제어권 해제 1발
_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.Laser_REMOTE), false);
Log.I($"[ExecuteWelding] Laser Gate OFF (REMOTE)");
#endregion
// 260626 Busy bit 위치 변경 - Laser On Display bit
_PLC.WriteBit((int)eBitOutput.PC_WELD_BUSY, false);
Log.I($"[WriteWeldComplete] 용접완료 송신 - BUSY=false (PageNo:{plcPageNo})");
Thread.Sleep(50);
_WeldingSpanTime = DateTime.Now.Subtract(weldingTime);
Status.GetProcessingWeldingData().WeldingEndTime = DateTime.Now;
//if (_CurrentPageNo == 1)
//{
// Log.D(string.Format($"[ExecuteWelding] -->(Wedling TackTime) {_CurrentPageNo} Welding Complete Time : {_WeldingSpanTime.TotalMilliseconds}"));
//}
//else
//{
// Log.D(string.Format($"[ExecuteWelding] -->(Wedling TackTime) {_CurrentPageNo} Welding Complete Time : {_WeldingSpanTime.TotalMilliseconds}"));
//}
double dExecuteListTime = _Scanner.GetTime();
//Log.I(string.Format($"[ExecuteWelding] ExecuteList Time : {dExecuteListTime * 1000} msec"));
// File Format Record Test
weldingTime = DateTime.Now;
_WeldingSpanTime = DateTime.Now.Subtract(weldingTime);
Log.D(string.Format($"[ExecuteWelding] SaveScannerRecordFileForamt Time : {_WeldingSpanTime.TotalMilliseconds}"));
weldingTime = DateTime.Now;
_Scanner.JumpAbs(0, 0, eFunctionType.COMMAND);
//용접 완료 후 CorrectionFile Vision으로 원복
_Scanner.SelectCorrectionTable(1);
_WeldingSpanTime = DateTime.Now.Subtract(weldingTime);
Log.D($"[ExecuteWelding] JumpAbs + SelectCorrectionTable Time: {_WeldingSpanTime.TotalMilliseconds}ms");
Log.D(string.Format($"[ExecuteWelding] Change SelectCorrectionTable Time : {_WeldingSpanTime.TotalMilliseconds}"));
Thread.Sleep(500);
#endregion
// 260622: 완료비트(TriggerACK off/BUSY off/COMPLETE on) 송신은 post-scout(M70509) 분기 뒤로 분리 → WriteWeldComplete 노드
Log.I($"[ExecuteWelding] 발진 완료 (완료비트 송신 보류) → PostScoutCheck 이동");
return SequenceAction.Ok();
}
private string CheckCycleStop(SequenceContext context, string id, ICollection<string> branches, SequenceActionResult param)
{
Log.I($"[CheckCycleStop] 사이클 중지 여부 확인 중");
Status.SetWorkingPosition(eWorkingPosition.CHECKING_CYCLE);
// 260621: Resume 사이클 1회 완료 → 출력 bit15 OFF + ResumeMode 해제 (이후 정상 루프 복귀)
if (ResumeMode)
{
_PLC.WriteBit((int)eBitOutput.PC_WELD_READY_ACK, false);
ResumeMode = false;
Log.I("[CheckCycleStop] Resume 사이클 완료 → PC_WELD_READY_ACK OFF, ResumeMode 해제");
}
Log.I($"[CheckCycleStop] 사이클 계속 진행 → CheckPageNo 이동");
return SEQ_CHECK_PAGENO;
}
#region 260622: M70509(Scout Enable) 기반 용접 전/후 Scout
// 이미지 파일명 접미어: auto_{recipeNo}_{pageNo}_{pre|post} → 최종 {기존}_auto_{recipeNo}_{pageNo}_{phase}.PNG
private string ScoutSaveTag(string phase) => $"auto_{plcRecipeNo}_{plcPageNo}_{phase}";
private SeqScoutNormal_InitParam BuildScoutParams(string saveTag)
{
return new SeqScoutNormal_InitParam()
{
IsRealTimeScout = false,
ScoutSeqParams = new ScoutSeqParams(VisionRecipeManager.GetFovInfos()[0], eDisplayType.Machine_B),
PageIndex = _CurrentPageNo, // 현재 처리 중인 페이지(0-base)
SaveTag = saveTag,
};
}
// ── 용접 전 Scout (M70516 감지 후) ──────────────────────────
private string PreScoutCheck(SequenceContext context, string id, ICollection<string> branches, SequenceActionResult param)
{
bool scout = _PLC.ReadBit(eBitInput.PLC_SCOUT_ENABLE).data; // M70509
Log.I($"[PreScoutCheck] M70509(Scout Enable) = {scout}");
// M70509 ON → pre scout / OFF → 기존과 동일하게 바로 용접
return scout ? SEQ_PRE_SCOUT_SET_PARAMS : SEQ_WAIT_WELDING_START;
}
private SequenceActionResult PreScoutSetParams(SequenceContext context, string id, SequenceActionResult param)
{
Log.I("[PreScout] Scout 파라미터 구성(pre)");
return SequenceAction.Ok(BuildScoutParams(ScoutSaveTag("pre")));
}
private SequenceActionResult PreScoutDone(SequenceContext context, string id, SequenceActionResult param)
{
var result = param.Param as SeqScoutNormal_OutParam;
if (result == null || result.Success == false)
{
Log.E("[PreScout] Scout 실패 → 시퀀스 중단");
return null;
}
Thread.Sleep(SCOUT_PROCEED_DELAY_MS); // 0.1초 후 용접 진행
Log.I($"[PreScout] 완료 → {SCOUT_PROCEED_DELAY_MS}ms 후 WaitWeldingStart 이동");
return SequenceAction.Ok();
}
// ── 용접 후 Scout (완료비트 송신 전) ────────────────────────
private string PostScoutCheck(SequenceContext context, string id, ICollection<string> branches, SequenceActionResult param)
{
bool scout = _PLC.ReadBit(eBitInput.PLC_SCOUT_ENABLE).data; // M70509
Log.I($"[PostScoutCheck] M70509(Scout Enable) = {scout}");
// M70509 ON → post scout / OFF → 바로 완료비트 송신
return scout ? SEQ_POST_SCOUT_SET_PARAMS : SEQ_WRITE_WELD_COMPLETE;
}
private SequenceActionResult PostScoutSetParams(SequenceContext context, string id, SequenceActionResult param)
{
Log.I("[PostScout] Scout 파라미터 구성(post)");
return SequenceAction.Ok(BuildScoutParams(ScoutSaveTag("post")));
}
private SequenceActionResult PostScoutDone(SequenceContext context, string id, SequenceActionResult param)
{
var result = param.Param as SeqScoutNormal_OutParam;
if (result == null || result.Success == false)
{
Log.E("[PostScout] Scout 실패 → 시퀀스 중단");
return null;
}
Thread.Sleep(SCOUT_PROCEED_DELAY_MS); // 0.1초 후 완료비트 송신
Log.I($"[PostScout] 완료 → {SCOUT_PROCEED_DELAY_MS}ms 후 WriteWeldComplete 이동");
return SequenceAction.Ok();
}
// 용접 완료 비트 송신(post-scout 분기 뒤로 분리). 음극 비트로 응답(양극 P 미사용).
private SequenceActionResult WriteWeldComplete(SequenceContext context, string id, SequenceActionResult param)
{
_PLC.WriteBit((int)eBitOutput.PC_WELD_TRIGGER_ACK, false);
Log.I($"[WriteWeldComplete] 용접완료 송신 - READY_ACK=false (PageNo:{plcPageNo})");
Thread.Sleep(50);
_PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE, true);
Log.I($"[WriteWeldComplete] 용접완료 송신 - COMPLETE=true (PageNo:{plcPageNo})");
Thread.Sleep(5);
Status.SetWorkingPosition(eWorkingPosition.UNLOADING_CELL);
Log.I($"[WriteWeldComplete] 완료 → CheckCycleStop 이동");
return SequenceAction.Ok();
}
#endregion
private SequenceActionResult SaveResultData(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[SaveResultData] 용접 결과 데이터 저장 시작");
Status.SetWorkingPosition(eWorkingPosition.REPORTING_RESULT);
// 스캐너의 결과를 저장하는 동작 추가
SaveScannerRecordFileForamt();
// 결과 Data 를 업데이트 함.
context.PutValue(ContextKey.KEY_SEQUENCE_RESULT, true);
Status.GetProcessingWeldingData().IsVisionNG = false;
Log.I("[SaveResultData] Welding Process Result: SUCCESS");
// 결과 데이터 저장하는 시퀸스
//Thread.Sleep(1000); // 추후 제거
Log.I($"[SaveResultData] 결과 저장 완료 → WriteCompletionPLCBit 이동");
return SequenceAction.Ok();
}
// 실제 동작 _Scanner에서
private void SaveScannerRecordFileForamt()
{
Log.I($"[SaveScannerRecordFileForamt] 스캐너 파형 기록 저장 시작 (SaveRecord)");
// ── 용접 완료 후 파형 데이터 저장 ────────────────────────────
_Scanner.SaveRecord("AutoRunTDC");
Log.I("[SaveScannerRecordFileForamt] SaveRecord 완료");
_Scanner.JumpAbs(0, 0, eFunctionType.COMMAND);
_Scanner.SelectCorrectionTable(1);
Log.D("[SaveScannerRecordFileForamt] JumpAbs(0,0) + CorrectionTable(1) 복귀 완료");
//Thread.Sleep(1000); // 추후 제거
Log.I($"[SaveScannerRecordFileForamt] 스캐너 파형 기록 저장 완료");
}
// 이곳에서 bit 초기화 진행함. 260512
private SequenceActionResult WriteCompletionPLCBit(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[WriteCompletionPLCBit] PLC 완료 비트 초기화 시작");
Status.SetWorkingPosition(eWorkingPosition.REPORTING_RESULT);
// 완료 비트 write 하는 Node 추가 필요. format 유지, ReceiveCellInfo 없음
if (RestoreDeviceStatus())
{
// 초기화 완료
Log.D("AutoRun Sequence WriteCompletionPLCBit");
}
//Thread.Sleep(1000); // 추후 제거
Log.I($"[WriteCompletionPLCBit] PLC 비트 초기화 완료 → Complete 이동");
return SequenceAction.Ok();
}
private SequenceActionResult Complete(SequenceContext context, string id, SequenceActionResult param)
{
Log.I($"[Complete] 1사이클 완료 처리]");
context.TryGetValue(ContextKey.KEY_SEQUENCE_RESULT, out bool seqResult);
SeqAutoRunPTOP_OutParam result = new SeqAutoRunPTOP_OutParam() { Success = seqResult };
if (seqResult)
{
uint prevTotal = _SystemParameter.TotalProductCount;
uint prevToday = _SystemParameter.TodayProductCount;
_SystemParameter.TotalProductCount++;
_SystemParameter.TodayProductCount++;
_SystemParameter.LastProductTime = DateTime.Now;
//Application.Current.AuroraEnv().SettingEntry.Save(CommonSettingIds.SystemParameter, _SystemParameter);
Status.GetProcessingWeldingData().TotalProduct = _SystemParameter.TotalProductCount;
Status.GetProcessingWeldingData().TodayProduct = _SystemParameter.TodayProductCount;
Log.I($"[Complete] Product Count Updated - Total: {prevTotal} -> {_SystemParameter.TotalProductCount}, Today: {prevToday} -> {_SystemParameter.TodayProductCount}");
}
else
{
Status.GetProcessingWeldingData().IsVisionNG = true;
Log.W("[Complete] Sequence result was FAIL. Product count not incremented.");
}
Status.SetWorkingPosition(eWorkingPosition.DONE);
//Thread.Sleep(1000); // 추후 제거
Log.I($"[Complete] 카운트 업데이트 완료 → 다음 사이클 진입");
return SequenceAction.Ok(result);
}
private bool RestoreDeviceStatus()
{
Log.I($"[RestoreDeviceStatus] PLC 출력 비트 리셋 시작 (Page/Weld 음극)");
////PLC All BIT OFF
_PLC.WriteBit((int)eCommonBitOutput.PC_PAGE_CHANGE_COMPLETE, false); // 페이지 false
_PLC.WriteBit((int)eCommonBitOutput.PC_WELD_TRIGGER_ACK, false); // 용접 준비 false
_PLC.WriteBit((int)eCommonBitOutput.PC_WELD_BUSY, false); // 동작 중 false
_PLC.WriteBit((int)eCommonBitOutput.PC_WELD_COMPLETE, false); // 용접 완료 false
Log.I(string.Format("[PLCInterface]PC Anod Bit Off"));
// 양극(P) 미사용 — 양극 용접 비트 리셋 제거
//Thread.Sleep(1000); // 추후 제거
Log.I($"[RestoreDeviceStatus] PLC 출력 비트 리셋 완료");
return true;
}
// 장비 3개 가져오기 완료
private void InitDev(SequenceContext context)
{
Log.I($"[InitDev] 디바이스 핸들 확보 시작 (Scanner/PLC/IO)");
_Scanner = context.GetLaserScanner();
_PLC = context.GetPLC() as PLCControllerPTOP;
_IO = context.GetIOUnit();
Log.I($"[InitDev] 디바이스 핸들 확보 완료");
}
private bool CheckDeviceReady(SequenceContext context)
{
Log.I($"[CheckDeviceReady] 디바이스 활성 상태 확인 시작 (Scanner/Laser/PLC/IO)");
List<string> deviceListToCheck = new List<string>() { CommonDeviceIds.Scanner, CommonDeviceIds.Laser,
CommonDeviceIds.PLCDevice, CommonDeviceIds.IODevice };
bool isAllDeviceActivated = true;
foreach (string item in deviceListToCheck)
{
if (!context.Devices[item].IsActivated)
{
isAllDeviceActivated = isAllDeviceActivated && false;
}
}
Log.I($"[CheckDeviceReady] 디바이스 활성 상태 확인 완료 (AllActive={isAllDeviceActivated})");
return isAllDeviceActivated;
}
private bool CheckCondition(SequenceContext context)
{
Log.I($"[CheckCondition] 시작 조건 확인 (인터락 + 디바이스 초기상태)");
// 체크 신호 plc
if (CheckCondition_Interlock())
{
if (InitializeDeviceStatus(context))
{
Log.I($"[CheckCondition] 조건 충족 → 시작 가능");
return true;
}
}
Log.I($"[CheckCondition] 조건 불충족 → 시작 불가");
return false;
}
private bool CheckCondition_Interlock()
{
// PLC, Data, IO 등 Interlock checking
Log.I($"[CheckCondition_Interlock] 인터락 확인 (현재 항상 통과)");
return true;
}
private bool InitializeDeviceStatus(SequenceContext context)
{
Log.I($"[InitializeDeviceStatus] 디바이스 초기상태 설정 시작 (AutoRun 비트 + LBAD Ready)");
// PLC Interface AutoStatus Start
InitAutoRunBit();
// Laser Initialize
//_Laser.PilotLaserOff();
//_Laser.LaserOn();
//Thread.Sleep(100);
//if (!_Laser.GetLaserReady())
//{
// Log.E("ERROR Initialize Laser");
// return false;
//}
// Scanner Initialize
// Scanner 없음.
// OCT Initialize
// TODO: EnterSequenceModeOCT 와 함께 Initialize 하는 것을 재구현 필요함. 초반에 한번만 check 하고 돌아 갈수 있도록
// PLC Initialize
#region [TEST_BYPASS] Alarm Check - PLC_AUTO_STATUS
/*
if (_PLC.ReadBit((int)eBitInput.PLC_AUTO_STATUS).data == false)
{
Log.E(string.Format("ERROR Initialize PLC, Bit Off PLC_AUTO_STATUS"));
return false;
}
*/
#endregion
#region 260824 제거 — DO 채널 8 충돌 (eADOAddress.PCLBADReadyStatus = 8 = 본 레이저 Laser_AC_KEY)
// LBAD 는 현재 미등록 디바이스라 이 비트를 쓰는 의미가 없고,
// 본 레이저 배선에서 DO 8 은 Laser_AC_KEY(레이저 AC 전원 투입) 이다.
// 그대로 두면 Auto 시작이 레이저 AC KEY 릴레이를 켠다. → 제거.
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(eADOAddress.PCLBADReadyStatus), true);
//Log.I(string.Format($"[IOInterface]SET PCLBADReadyStatus true"));
#endregion
Log.I($"[InitializeDeviceStatus] 디바이스 초기상태 설정 완료");
return true;
}
private void InitAutoRunBit()
{
Log.I($"[InitAutoRunBit] AutoRun 시작 PLC 비트 초기화 시작 (음극)");
_PLC.WriteBit((int)eBitOutput.PC_WELD_TRIGGER_ACK, false);
Log.I(string.Format($"[PLCInterface]SET (-)PC_WELD_TRIGGER_ACK False"));
_PLC.WriteBit((int)eBitOutput.PC_WELD_BUSY, false);
Log.I(string.Format($"[PLCInterface]SET (-)PC_WELD_BUSY False"));
_PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE, false);
Log.I(string.Format($"[PLCInterface]SET (-)PC_WELD_COMPLETE False"));
_PLC.WriteBit((int)eBitOutput.PC_AUTO_STATUS, true);
Log.I(string.Format($"[PLCInterface]SET (-)PC_AUTO_STATUS True"));
// 양극(P) 미사용 — 양극 초기화 비트 제거
Log.I($"[InitAutoRunBit] AutoRun 시작 PLC 비트 초기화 완료");
}
private void OnForceError(SequenceContext arg1, string arg2)
{
Log.I($"[OnForceError] ForceError 발생 → 장비 정지 처리 시작");
OnSeqActionStop();
Log.I($"[OnForceError] ForceError 정지 처리 완료");
}
private void OnStopped(SequenceContext obj)
{
Log.I($"[OnStopped] 시퀀스 Stop 수신 → 장비 정지 처리 시작");
OnSeqActionStop();
Log.I($"[OnStopped] Stop 정지 처리 완료");
}
private void OnSeqActionStop()
{
Log.I($"[OnSeqActionStop] 시퀀스 강제 중지 - PLC/IO/Scanner 초기화 시작");
// PLC All BIT OFF
_PLC.WriteBit((int)eBitOutput.PC_AUTO_STATUS, false);
_PLC.WriteBit((int)eBitOutput.PC_WELD_TRIGGER_ACK, false);
_PLC.WriteBit((int)eBitOutput.PC_WELD_BUSY, false);
_PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE, false);
Log.I("[OnSeqActionStop] PLC 음극 Output All false (양극 P 미사용 — 양극 비트 제거)");
_PLC.WriteBit((int)eBitOutput.PC_PAGE_CHANGE_COMPLETE, false);
Log.I("[OnSeqActionStop] PLC IO Output PAGECHANGE Init");
// 260512 워드
// 260621: 에코 워드(PC_PAGE_NO/PC_RECIPE_NO)는 정지 시 0으로 지우지 않음 — PLC가 정합 확인에 쓰는
// 마지막 핸드셰이크 값을 보존(다음 사이클에 어차피 재에코).
#region 260805 정지 모드만 로그로 남긴다 (워드 0 초기화는 하지 않음)
// 0 초기화를 넣었다가 롤백했다. 지울 필요가 없는 이유 —
// ① 재진입 시 CheckRecipeNoComp / CheckPageNoComp 가 어차피 워드를 재에코한다.
// ② PLC가 보는 핸드셰이크 게이트는 워드가 아니라 PC_PAGE_CHANGE_COMPLETE 비트이고,
// 그 비트는 바로 위에서 이미 false로 내렸다 → 잔재 워드를 유효값으로 읽을 경로가 없다.
// ③ bit15 Resume(Manual 수동 출사 후 이어하기)은 직전 Recipe/Page 에코가 남아 있어야 한다.
// 아래 분기는 진단용이다. 워드를 쓰지 않으므로 PLC 동작에는 영향이 없다.
// 0 초기화가 필요하다고 판단되면 PLC 담당 확인 후 !ResumeMode 쪽에만 WriteWord를 넣는다.
// 로그 값은 캐시된 마지막 핸드셰이크 값이다 — 정지(특히 ForceError) 시점에 PLC를 다시 읽지 않는다.
#region 260806 정정 — 위 ②는 사실과 반대다 (원복: 이 region 통째로 삭제. 위 원문은 그대로 보존돼 있다)
// 2026-08-06 PLC측 시퀀스 확정 결과:
// · PLC가 보는 핸드셰이크 게이트는 **워드**(PC_PAGE_NO / PC_RECIPE_NO)다.
// PC 에코 넘버가 자기 값과 같아지는지를 상시 확인하고, 같으면 다음 스텝으로 진행한다.
// · PC_PAGE_CHANGE_COMPLETE(M70008)는 PLC가 **보지 않는다**. 따라서 위 ②의
// "비트를 내렸으니 잔재 워드를 읽을 경로가 없다"는 논리는 성립하지 않는다.
// 그럼에도 동작은 바꾸지 않는다 — 워드 0 초기화는 SeqStatusMonitor.UpdatePLC()가
// Auto OFF 시 이미 담당하고 있고(M70515 ON 구간은 예외로 보존), 여기서 중복으로 쓰면
// bit15 Resume이 의존하는 보존값을 깨뜨린다(위 ③).
// 분석: docs/분석/2026/08/06/RecipePage_에코보장_방침_분석.md
#endregion
if (!ResumeMode)
{
Log.I($"[OnSeqActionStop] 일반 정지 (ResumeMode=false) → 에코 워드 보존, 0 초기화 안 함 " +
$"(마지막 핸드셰이크 PageNo:{plcPageNo}, RecipeNo:{plcRecipeNo})");
}
else
{
Log.I($"[OnSeqActionStop] Resume 정지 (ResumeMode=true, bit15 Manual 전환) → 에코 워드 반드시 보존 " +
$"(마지막 핸드셰이크 PageNo:{plcPageNo}, RecipeNo:{plcRecipeNo})");
}
#endregion
#region 260820 260811 초기화 제거 (원복: 아래 3줄 주석을 풀면 260811 무조건판으로 돌아간다)
// PLC측 확정(2026-08-20): PLC 는 PC 에코 워드가 0이면 레이저 출사 신호를 내지 않는다.
// 그런데 bit15 가접 운전은 페이지마다 정지하므로, 여기서 0을 쓰면 페이지마다 0/0 창이 열린다.
// PC 는 다음 PLC_PAGE_CHANGE_REQ 를 받아야만 재에코하므로(CheckPageNo) 그 창 동안 PLC 는
// 유효값을 못 본다 — 2026-08-20 로그에서 15 사이클 전부 이 경로를 탔다.
// 0 초기화는 SeqStatusMonitor.UpdateStatus() 가 Auto OFF 시에만 담당한다(위 260806 주석과 동일 규칙).
// 여기서 중복으로 쓰면 bit15 Resume 이 의존하는 보존값도 함께 깨진다.
// ※ 남은 창: SeqStatusMonitor 는 Auto OFF 하강엣지에 0을 쓰고 끝이라, Auto 복귀 후 첫 REQ 까지
// 0/0 이 latch 된다(2026-08-20 10:24:47~10:43:17). 그 구간 재에코는 별건으로 검토한다.
//_PLC.WriteWord(eWordOutput.PC_RECIPE_NO, 0);
//_PLC.WriteWord(eWordOutput.PC_PAGE_NO, 0);
//Log.I($"[OnSeqActionStop] 에코 워드 0 초기화 (PC_RECIPE_NO/PC_PAGE_NO)");
#endregion
Log.I("[PLCInterface] PC All Bit Off (Auto, ReadyAck, Busy, Complete)");
// LBAD IO All BIT OFF
try
{
#region 260824 이전 원본 (Core/Ring 2발)
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.CoreLaser_Control), false);
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.RingLaser_Control), false);
#endregion
// 260824 본 레이저 단일 발진기 — 정지 시 제어권 해제 1발
_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.Laser_REMOTE), false);
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(eADOAddress.PCLBADWeldingTrigger), false);
//_IO._iOUnit.WriteDoBit(Convert.ToInt32(eADOAddress.PCLBADReadyStatus), false);
Log.I("[IOInterface] PC All Bit Off (Laser Control, LBAD Trigger, LBAD Ready)");
}
catch (Exception ex)
{
Log.E($"[IOInterface] Failed to reset IO Bits: {ex.Message}");
}
// List 실행 종료 및 원점 복귀
_Scanner.StopExecution();
_Scanner.JumpAbs(0, 0, eFunctionType.COMMAND);
Log.I("[Scanner] StopExecution and JumpAbs(0,0)");
// 용접 완료 후 CorrectionFile Vision으로 원복
_Scanner.SelectCorrectionTable(1);
Log.I($"[OnSeqActionStop] 모든 장비 초기화 완료");
}
public class SeqAutoRunPTOP_OutParam : OutParam
{
}
}
}