CheckPageNoComp 에코 수렴 루프에서 PC_PAGE_NO 를 쓴 직후 되읽어 비교하면 PLC 데이터 업데이트 주기가 반영되기 전 값을 읽어 수렴 판정이 흔들릴 수 있다. WriteWord 이후 되읽기 전에 200ms 대기를 넣는다. 대상: SeqAutoRunPTOP.cs CheckPageNoComp() "4. 되읽어 수렴 확인" 직전
1521 lines
77 KiB
C#
1521 lines
77 KiB
C#
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 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
|
||
|
||
|
||
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
|
||
|
||
public enum ePrismaticDOAddress
|
||
{
|
||
CoreLaser_Control = 9,
|
||
CoreLaser_ErrorClearing,
|
||
RingLaser_Control = 13,
|
||
RingLaser_ErrorClearing,
|
||
EX_Red_Control = 17,
|
||
}
|
||
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
|
||
|
||
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_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_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] 조건 확인 완료 → CheckPageNo 이동");
|
||
return SEQ_CHECK_PAGENO;
|
||
}
|
||
|
||
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++;
|
||
|
||
// ── 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
|
||
|
||
// 페이지 체크 - 페이지만 감시 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)
|
||
{
|
||
Log.I("[CheckPageNoComp] PLC_PAGE_NO 수렴 시작");
|
||
Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL);
|
||
|
||
int retry = 0;
|
||
|
||
while (!context.Stopped)
|
||
{
|
||
if (retry > 0) Thread.Sleep(ECHO_RETRY_MS);
|
||
retry++;
|
||
|
||
// ── 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;
|
||
|
||
// ── 수렴 후 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은 유지한다.
|
||
_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);
|
||
|
||
_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)");
|
||
|
||
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)
|
||
// ↓ 이 두 줄 삭제
|
||
_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
|
||
|
||
// 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
|
||
|
||
_IO._iOUnit.WriteDoBit(Convert.ToInt32(eADOAddress.PCLBADReadyStatus), true);
|
||
Log.I(string.Format($"[IOInterface]SET PCLBADReadyStatus true"));
|
||
|
||
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
|
||
|
||
|
||
|
||
Log.I("[PLCInterface] PC All Bit Off (Auto, ReadyAck, Busy, Complete)");
|
||
// LBAD IO All BIT OFF
|
||
try
|
||
{
|
||
_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.CoreLaser_Control), false);
|
||
_IO._iOUnit.WriteDoBit(Convert.ToInt32(ePrismaticDOAddress.RingLaser_Control), 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
|
||
{
|
||
|
||
}
|
||
}
|
||
}
|