using Aurora; using Aurora.Api; using Aurora.Api.Manager; using Aurora.Util.HislogPlus; using Aurora.View; using AuroraCellTracker; using AuroraCellTracker.DaekhonCoporation; using AuroraIO; using AuroraLaser; using AuroraLaserScanner; using AuroraLBAD.Service; using CoreUnit.Common; using CoreUnit.Control.UserControl.Canvas; using CoreUnit.Scanner.UserControl.ScannerCanvas; using DB; using Hermes; using ILSOS_CCW_H52A.Data; using ILSOS_CCW_H52A.Device; using ILSOS_CCW_H52A.Models; using ILSOS_CCW_H52A.RMS; using ILSOS_CCW_H52A.Sequences.ProcessObjects; using Microsoft.Win32; using OpenCvSharp; using OpenCvSharp.Aruco; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reactive.Subjects; using System.Reflection; using System.Threading; using System.Windows; using System.Windows.Media.Animation; using WeldingCommon; using WeldingCommon.Alarm; using WeldingCommon.Devices; using WeldingCommon.Recipes; using WeldingCommon.Sequences; using WeldingCommon.Settings; using WeldingCommon.Utils; using static AuroraLBAD.Service.StreamLightLBAD; using static ILSOS_CCW_H52A.Device.IOController_CCW_H52A; using static ILSOS_CCW_H52A.Device.PLCControllerCCWH52A; using static ILSOS_CCW_H52A.Device.ScannerListCCWH52A; namespace ILSOS_CCW_H52A.Sequences { class SeqAutoRunCCWH52A { static readonly Logger Log = new Logger("SeqAutoRunCCWH52A"); const string SEQ_ENTER = "[SeqAutoRunCCWH52A] SEQ_ENTER"; // inputParam 으로 받자. const string SEQ_CHECK_INIT_CONDITION = "[SeqAutoRunCCWH52A] Check Init Condition"; // Check Init Condition const string SEQ_FORK_ENCODER_MONITOR = "[SeqAutoRunCCWH52A] Fork Encoder Monitor"; const string SEQ_ENCODER_MONITOR = "[SeqAutoRunCCWH52A] Check Encoder Monitor"; const string SEQ_JOIN_ENCODER_MONITOR = "[SeqAutoRunCCWH52A] Join Encoder Monitor"; // TODO: [장비구현필요] 실제 공정 추가 const string SEQ_CHECK_LOAD_COMPLETE = "[SeqAutoRunCCWH52A] Check Load Complete"; public const string SEQ_CHECK_SCANNER_LIST_COMPLETE = "[SeqAutoRunCCWH52A] Check Scanner List Complete"; const string SEQ_READY_SCANNER_LIST = "[SeqAutoRunCCWH52A] Ready ScannerList"; //const string SEQ_SET_PARAMS_RECEIVE_CELL_INFO_SEQ = "[SeqAutoRunCCWH52A] Set Sequence Param (ReceiveCellInfo Seq)"; //const string SEQ_DO_ID_RECEIVE_CELL_INFO_SEQ = "[SeqAutoRunCCWH52A] Process (ReceiveCellInfo Seq)"; //const string SEQ_GET_PARAMS_RECEIVE_CELL_INFO_SEQ = "[SeqAutoRunCCWH52A] Get Sequence Result (ReceiveCellInfo Seq)"; const string SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunCCWH52A] Set Sequence Param (UploadWeldingList Seq)"; const string SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunCCWH52A] Process (UploadWeldingList Seq)"; const string SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ = "[SeqAutoRunCCWH52A] Get Sequence Result (UploadWeldingList Seq)"; const string SEQ_EXECUTE_WELDING = "[SeqAutoRunCCWH52A] Execute Welding"; const string SEQ_REQUEST_UNLOAD_CELL = "[SeqAutoRunCCWH52A] Request Unload Cell"; //const string SEQ_WAIT_LBAD_JUDGE = "[SeqAutoRunCCWH52A] Wait LBAD Judge"; const string SEQ_REPORT_CELL_RESULT = "[SeqAutoRunCCWH52A] Report Cell Result"; //const string SEQ_SET_PARAMS_REPORT_CELL_RESULT_SEQ = "[SeqAutoRunCCWH52A] Set Sequence Param (ReportCellResult Seq)"; //const string SEQ_DO_ID_REPORT_CELL_RESULT_SEQ = "[SeqAutoRunCCWH52A] Process (ReportCellResult Seq)"; //const string SEQ_GET_RESULT_REPORT_CELL_RESULT_SEQ = "[SeqAutoRunCCWH52A] Get Sequence Result (ReportCellResult Seq)"; //const string SEQ_REPORT_CELLINFO_TO_PLC = "[SeqAutoRunCCWH52A] ReportCellInfoToPLC"; const string SEQ_CHECK_CYCLE_STOP = "[SeqAutoRunCCWH52A] Check Cycle Stop"; const string SEQ_CHECK_CAN_WELDED = "[SeqAutoRunCCWH52A] Check Can Welded"; const string SEQ_SAVE_RESULT_DATA = "[SeqAutoRunCCWH52A] Save Result Data"; const string SEQ_WRITE_COMPLETION_PLC_BIT = "[SeqAutoRunCCWH52A] Write Completion PLC Bit"; const string SEQ_INIT_CONDITION_ERROR = "[SeqAutoRunCCWH52A] Init Condition Error"; const string SEQ_COMPLETE = "[SeqAutoRunCCWH52A] SEQ_COMPLETE"; // 완료 static class ContextKey { public const string KEY_SEQUENCE_RESULT = "KEY_SEQUENCE_RESULT_SeqAutoRunCCWH52A"; public const string KEY_SEQUENCE_START_TIME = "KEY_SEQUENCE_START_TIME_SeqAutoRunCCWH52A"; public const string KEY_SEQUENCE_ERROR_CODE = "KEY_SEQUENCE_ERROR_CODE_SeqAutoRunCCWH52A"; } const int PLC_TIMEOUT = 60; // sec, Timeout은 시퀀스 정지가 아니라 타임아웃 시간마다 로그만 남기도록 한다 private Scanner _Scanner; private LaserDevice _Laser; private PLCControllerCCWH52A _PLC; private IODevice _IO; private StreamLightLBAD _StreamLightLBAD; private static CellTracker _CellTracker; private SystemParameter _SystemParameter; private ScannerCanvasUserCtrl _ScannerCanvasUserCtrl; //스캐너 동작관련 객체 선언 private int _RecipeNo; private int _ModelNo; private IRecipeManager _RecipeManager; private eListNo _WeldingScannerListNo; private List _SelectedDrawObjectList; private SeqAutoRunCCWH52A_InitParam _InitParam; private double _LaserPowerByLBAD; private bool _ModulationMode; private DateTime _AutoRunStartTime; // 연속 지그 간 용접 간격 계산 전용 (보고 데이터 아님 — 인터벌 로그용) private DateTime _prevWeldEndTime; private int _EncoderOverflowCount = 0; private double _RotaryEncoderOffset = 0; private int _WeldingJigNo = -1; private int _WeldingCount = 0; private int _RotaryJigCount = 16; // 전전 셀/전 셀/대기 셀/완료 셀 파이프라인 스케줄러 (16개 JigInfo 풀, GC-free) private CellInfoScheduler _CellScheduler; private double _WeldingStartAngle; private bool isTheFirstListExecute; private bool UsePlcShotThemAll { get; set; } = false; private bool UsePlcPositionCompensation { get; set; } = false; private string RecipeFileName { get; set; } = ""; private enum InitErrorList { SUCCESS = 0, LBAD_CLIENT_ACTIVATE_FAILED, LBAD_SUB_ACTIVATE_FAILED, LASER_INIT_ERROR, PLC_AUTORUN_BIT_OFF, MOTF_OFF, PLC_ALARM, } public SequenceBuilder Builder() { var sb = new SequenceBuilder(1000, true); sb.AddAction(SEQ_ENTER, Sequence_Enter); sb.AddDecision(SEQ_CHECK_INIT_CONDITION, CheckInitCondition); //분기 sb.AddFork(SEQ_FORK_ENCODER_MONITOR); sb.AddAction(SEQ_ENCODER_MONITOR, EncoderMonitor); sb.AddJoin(SEQ_JOIN_ENCODER_MONITOR); sb.AddAction(SEQ_CHECK_LOAD_COMPLETE, CheckLoadComplete); sb.AddAction(SEQ_CHECK_SCANNER_LIST_COMPLETE, CheckScannerListComplete); sb.AddDecision(SEQ_READY_SCANNER_LIST, ReadyScannerList); #warning 아래 서브시퀀스가 CellTracker에서 받은 데이터를 WeldingData에 넣기만함, ProcessManager에서 Cell정보를 직접 Callback으로 받도록 수정함 //sb.AddAction(SEQ_SET_PARAMS_RECEIVE_CELL_INFO_SEQ, SetReceiveCellInfo_SeqParams); // Cell 정보를 받아와 입력한다. //sb.AddSubsequence(SEQ_DO_ID_RECEIVE_CELL_INFO_SEQ, SequenceManager.GetSeqBuilder_SeqReceiveCellInfo()); // cell 정보를 받아오는 Sub Seq 실행 //sb.AddAction(SEQ_GET_PARAMS_RECEIVE_CELL_INFO_SEQ, GetReceiveCellInfo_SeqResult); // Cell 정보를 가져온다 sb.AddAction(SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ, SetUploadWeldingList_SeqParams); sb.AddSubsequence(SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ, SequenceManager.GetSeqBuilder_SeqUploadWeldingList()); sb.AddAction(SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ, GetUploadWeldingList_SeqResult); sb.AddAction(SEQ_EXECUTE_WELDING, ExecuteWelding); sb.AddAction(SEQ_REQUEST_UNLOAD_CELL, RequestUnloadCell); sb.AddDecision(SEQ_CHECK_CAN_WELDED, CheckCanWelded); sb.AddAction(SEQ_REPORT_CELL_RESULT, ReportCellResult); #warning ReportCellResult함수로 아래 시퀀스 대체 //sb.AddAction(SEQ_SET_PARAMS_REPORT_CELL_RESULT_SEQ, SetReportCellResult_SeqParams); //sb.AddSubsequence(SEQ_DO_ID_REPORT_CELL_RESULT_SEQ, SequenceManager.GetSeqBuilder_SeqReportCellResult()); //sb.AddAction(SEQ_GET_RESULT_REPORT_CELL_RESULT_SEQ, GetReportCellResult_SeqResult); 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_INIT_CONDITION_ERROR, InitConditionError); sb.AddAction(SEQ_COMPLETE, Complete); sb.Flows(sb.Init > sb[SEQ_ENTER] > sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_CHECK_LOAD_COMPLETE] > sb[SEQ_CHECK_SCANNER_LIST_COMPLETE] > sb[SEQ_READY_SCANNER_LIST] > sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_EXECUTE_WELDING] > sb[SEQ_REQUEST_UNLOAD_CELL] > sb[SEQ_CHECK_CAN_WELDED] > sb[SEQ_REPORT_CELL_RESULT] //> sb[SEQ_SET_PARAMS_REPORT_CELL_RESULT_SEQ] > sb[SEQ_DO_ID_REPORT_CELL_RESULT_SEQ] > sb[SEQ_GET_RESULT_REPORT_CELL_RESULT_SEQ] > sb[SEQ_SAVE_RESULT_DATA] > sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_WRITE_COMPLETION_PLC_BIT] > sb[SEQ_JOIN_ENCODER_MONITOR] > sb[SEQ_COMPLETE] > sb.Final); sb.Flows(sb[SEQ_READY_SCANNER_LIST] > sb[SEQ_EXECUTE_WELDING]); sb.Flows(sb[SEQ_CHECK_CAN_WELDED] > sb[SEQ_CHECK_CYCLE_STOP]); // 용접을 하지 않은 Cell은 상위보고 하지 않는다. sb.Flows(sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_CHECK_LOAD_COMPLETE]); sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_INIT_CONDITION_ERROR] > sb.Final); sb.Flows(sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_ENCODER_MONITOR] > sb[SEQ_JOIN_ENCODER_MONITOR] > sb[SEQ_COMPLETE] > sb.Final); #region //sb.Flows(sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_ENCODER_MONITOR]); ////sb.Flows(sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_CHECK_LOAD_COMPLETE]); //sb.Flows(sb[SEQ_JOIN_ENCODER_MONITOR] > sb[SEQ_COMPLETE]); /// //// 1. 시퀀스 시작 ~ 포크 //sb.Flows(sb.Init > sb[SEQ_ENTER] > sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_FORK_ENCODER_MONITOR]); //// 2. Fork 이후 메인 루트 //sb.Flows(sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_CHECK_LOAD_COMPLETE]); //sb.Flows(sb[SEQ_CHECK_LOAD_COMPLETE] > sb[SEQ_CHECK_SCANNER_LIST_COMPLETE]); //sb.Flows(sb[SEQ_CHECK_SCANNER_LIST_COMPLETE] > sb[SEQ_READY_SCANNER_LIST]); //sb.Flows(sb[SEQ_READY_SCANNER_LIST] > sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ]); //sb.Flows(sb[SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ]); //sb.Flows(sb[SEQ_DO_ID_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ]); //sb.Flows(sb[SEQ_GET_RESULT_UPLOAD_WELDING_LIST_SEQ] > sb[SEQ_EXECUTE_WELDING]); //sb.Flows(sb[SEQ_EXECUTE_WELDING] > sb[SEQ_REQUEST_UNLOAD_CELL]); //sb.Flows(sb[SEQ_REQUEST_UNLOAD_CELL] > sb[SEQ_CHECK_CYCLE_STOP]); //sb.Flows(sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_SAVE_RESULT_DATA]); //sb.Flows(sb[SEQ_SAVE_RESULT_DATA] > sb[SEQ_WRITE_COMPLETION_PLC_BIT]); //sb.Flows(sb[SEQ_WRITE_COMPLETION_PLC_BIT] > sb[SEQ_JOIN_ENCODER_MONITOR]); //// 3. Fork 이후 병렬 브랜치 //sb.Flows(sb[SEQ_FORK_ENCODER_MONITOR] > sb[SEQ_ENCODER_MONITOR]); //sb.Flows(sb[SEQ_ENCODER_MONITOR] > sb[SEQ_JOIN_ENCODER_MONITOR]); //// 4. Join 이후 Complete //sb.Flows(sb[SEQ_JOIN_ENCODER_MONITOR] > sb[SEQ_COMPLETE]); //// 5. 에러 분기/종료 //sb.Flows(sb[SEQ_CHECK_INIT_CONDITION] > sb[SEQ_INIT_CONDITION_ERROR] > sb.Final); //// 6. 루프, 분기(선택적) //sb.Flows(sb[SEQ_READY_SCANNER_LIST] > sb[SEQ_EXECUTE_WELDING]); //sb.Flows(sb[SEQ_CHECK_CYCLE_STOP] > sb[SEQ_CHECK_LOAD_COMPLETE]); #endregion sb.OnStopped(OnStopped); sb.OnForceError(OnForceError); sb.Draw("WeldingSeqAutoRunH52A.pdf"); return sb; } private SequenceActionResult Sequence_Enter(SequenceContext context, string id, SequenceActionResult param) { Log.I("H52A Welding Sequence Start"); Status.SetWorkingPosition(eWorkingPosition.CHECKING_CONDITION); _InitParam = param.Param as SeqAutoRunCCWH52A_InitParam; _RecipeManager = Application.Current.AuroraEnv().RecipeManager; _SystemParameter = context.GetSetting(CommonSettingIds.SystemParameter); InitDev(context); if (!CheckDeviceReady(context)) { //Check Device Log.E("The devices are not active."); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_DeviceNotReady); } if (InitSequenceVariable(context) == false) { return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_InitVariableError); } Thread.Sleep(1); // 부하 방지 return SequenceAction.Ok(); } private bool InitSequenceVariable(SequenceContext context) { _AutoRunStartTime = DateTime.Now; var ActivateRecipeName = _RecipeManager.ActivatedRecipe(); Log.I($"Current Activate Recipe, ID : {ActivateRecipeName.Id}, Name : {ActivateRecipeName.Name}"); var mainPage = WeldingRecipeManager.GetWeldingPage(); _ScannerCanvasUserCtrl = mainPage.ScannerCanvasCtrlAutoPage; _WeldingScannerListNo = eListNo.LIST1; _RecipeNo = ActivateRecipeName.Id; var currentModelName = context.Recipes.ActivatedModelName(); var modelItem = context.Models.ModelItem(currentModelName, CommonModelIds.EquipmentSpecificModel) as EquipmentSpecificModel_CCW_H52A; _ModelNo = modelItem.Model_No; return true; } private void InitDev(SequenceContext context) { //Device Init _Scanner = context.GetLaserScanner(); _Laser = context.GetLaser(); if (_InitParam.UseStream) { _StreamLightLBAD = new StreamLightLBAD(); _StreamLightLBAD._Client = context.GetStreamLightClient(); _StreamLightLBAD._Sub = context.GetStreamLightSub(); ProcessManager.Instance.RegisterLbadStream(_StreamLightLBAD); } _CellTracker = context.GetCellTracker(); _PLC = context.Devices[DeviceIds.PLCDevice] as PLCControllerCCWH52A; _IO = context.GetIOUnit(); _CellScheduler = ProcessManager.Instance.cellScheduler; } private bool CheckDeviceReady(SequenceContext context) { List deviceListToCheck = new List() { CommonDeviceIds.Scanner, CommonDeviceIds.Laser, CommonDeviceIds.PLCDevice, CommonDeviceIds.IODevice }; if (_InitParam.UseStream) { deviceListToCheck.Add(CommonServiceIds.StreamLightLBADClient); deviceListToCheck.Add(CommonServiceIds.StreamLightLBADSub); } bool isAllDeviceActivated = true; foreach (string item in deviceListToCheck) { if (!context.Devices[item].IsActivated) { isAllDeviceActivated = isAllDeviceActivated && false; } } return isAllDeviceActivated; } private bool CheckCondition(SequenceContext context) { int InitializeCode = InitializeDeviceStatus(context); if (InitializeCode == (int)InitErrorList.SUCCESS) return true; else context.PutValue(ContextKey.KEY_SEQUENCE_ERROR_CODE, InitializeCode); return false; } private int InitializeDeviceStatus(SequenceContext context) { // PLC Interface AutoStatus Start // Won : PC의 AutoStatus 가 True 되는 순간 Rotary가 돌기 시작한다. // Rotary 돌기 시작 한 후 Encoder 값을 읽으면 안 되므로 정지상태에서 읽은 후 AutoStatus true 하도록 // InitAutoRunBit 시점 변경 //InitAutoRunBit(); // LBAD Initialize // LBAD 는 server 이기 때문에, 만약 끊어졌으면 다시 접속이 필요함 if (_SystemParameter.BypassModeLBAD == false) { if (!_StreamLightLBAD._Client.IsActivated) { int ret = _StreamLightLBAD._Client.Activate(); Thread.Sleep(500); if (ret != E1.OK) { Log.E($"StreamLightClient activate failed"); return (int)InitErrorList.LBAD_CLIENT_ACTIVATE_FAILED; } } if (_StreamLightLBAD._Sub.IsActivated == false) { int ret = _StreamLightLBAD._Sub.Activate(); Thread.Sleep(500); if (ret != E1.OK) { Log.E($"StreamLightSub activate failed"); return (int)InitErrorList.LBAD_SUB_ACTIVATE_FAILED; } } _StreamLightLBAD.Initialize(); if (_StreamLightLBAD.SendReadyStatus(eRequestData.Ready_On) == false) { Log.I(string.Format($"[LBAD Interface]CheckReadyStatus null")); //return false; } } // Laser Initialize _Laser.PilotLaserOff(); _Laser.LaserOn(); Thread.Sleep(500); if (!_Laser.GetLaserReady()) { Log.E("ERROR Initialize Laser"); return (int)InitErrorList.LASER_INIT_ERROR; } // Scanner Initialize if (_Scanner.GetModulationMode()) { _ModulationMode = true; } else { _ModulationMode = false; } ///// AutoRun Bit가 OFF인 경우 if (_PLC.ReadBit((int)eBitInput.PLC_AUTO_STATUS).data == false) { // AutoRun Request 상태 체크 if (_PLC.ReadBit((int)eBitInput.PLC_AUTO_REQ).data == false) { Log.E(string.Format("ERROR Initialize PLC, PLC Alarm ON")); // AutoRun Request와 AutoRun Bit 모두 OFF인 경우, PLC 측에서 Alarm 발생함. return (int)InitErrorList.PLC_ALARM; } else { Log.E(string.Format("ERROR Initialize PLC, Bit Off PLC_AUTO_STATUS")); // AutoRun Request가 ON인데 AutoRun Bit 모두 OFF인 경우, PLC에서 AutoRun Bit를 켜지 않음. return (int)InitErrorList.PLC_AUTORUN_BIT_OFF; } } // Marking On The Flying Off if (_Scanner.Config.UseMarkingOnTheFly == false) { Log.E("ERROR Config Setting : Scanner Marking on the fly (OFF)"); return (int)InitErrorList.MOTF_OFF; } // MOTF Set Scanner List _Scanner.SelectCorrectionTable(2); Log.I(string.Format("Normal, Select Laser Correction File")); // Aurora 내부에서 찾아야할 듯 함 //UsePlcShotThemAll = Dao.SystemRecipe.PlcShotThemAll; //UsePlcPositionCompensation = Dao.SystemRecipe.PlcPositionCompensationUse; string[] strArr = Dao.SystemRecipe.WorkRecipeFile.Split('\\'); RecipeFileName = strArr[strArr.Length - 1]; // Rotary는 시작 트리거 미사용 _Scanner.SetControlMode(0); UtilUnit.logUnit.SetLog(LogUnit.eLogType.DEBUG, MethodBase.GetCurrentMethod().Name, "[AutoRun] 외부 시작 트리거 DISABLE"); #region MOTF Absolute Encoder 기준으로 초기화 // Set Rotate Center는 Ctrl명령, 처음 한번만 실행 _Scanner.SetRotateCenter(_Scanner.Config.MarkingOnTheFlyRotateCenterX, _Scanner.Config.MarkingOnTheFlyRotateCenterY); _Scanner.InitFly2D(); // X, Y 엔코더 리셋 _EncoderOverflowCount = 0; // 현재 정지상태의 Rotary 엔코더 값을 읽어와서 저장 _RotaryEncoderOffset = GetRotaryCurrentPosition(); // m_abwRotaryEncoderOffset의 범위는 0 이상 360 미만 InitRotaryOnTheFlyWeldingCount(_RotaryEncoderOffset, _RotaryJigCount, out _WeldingCount, out _WeldingJigNo); Log.I(string.Format($"ScannerUnit.InitRotaryOnTheFlyWeldingCount : m_abwRotaryEncoderOffset({_RotaryEncoderOffset}), weldingCount({_WeldingCount}), weldingJigNo({_WeldingJigNo}), rotaryJigCount({_RotaryJigCount})")); // JigInfo 풀 스케줄러 초기화 (전전/전/대기/완료 셀 파이프라인) // ── 재시작 전 미완료 보고 보존 ─────────────────────────────────────────────── // Complete가 있는 상태로 정지가 들어오면 Reset() 후 슬롯이 Clear()되어 보고 불가. // SavePendingComplete()로 _pendingSlot에 복사한 뒤 Reset()을 호출한다. _CellScheduler.SavePendingComplete(); // 큐 잔류 셀 보존 — DrainCellInfoQueue 전에 선행 (Prefetch2 다음 지그 기준) // Prefetch2.JigNo가 확정값이므로 다음 지그 = (JigNo % 16) + 1 로 신뢰성 있게 계산 if (_CellScheduler.Prefetch2 != null && _CellScheduler.Prefetch2.JigNo > 0) { int nextJigNo = (_CellScheduler.Prefetch2.JigNo % ProcessManager.RotaryJigCount) + 1; if (ProcessManager.Instance.ReceivedCellInfoQueue.TryDequeue(out var qCell)) { _CellScheduler.SaveQueueEntry(qCell.LotId, qCell.CellId, nextJigNo); Log.I($"[QueueSave] Jig{nextJigNo} 큐 잔류 셀 보존 >> CellId({qCell.CellId})"); } } if (_CellScheduler.HasPendingPrefetch) Log.I("[PendingRestore] 미용접 셀 보존됨 >> 재시작 후 JigNo 매칭 시 복원 예정"); _CellScheduler.Reset(); ProcessManager.Instance.DrainCellInfoQueue(); // 정지 전 미저장 APD 잔존 데이터 클리어 — 재시작 시 중복 누적 방지 Status.GetProcessingWeldingData().APD.Clear(); // ───────────────────────────────────────────────────────────────────────────── isTheFirstListExecute = true; // 제일 첫 List는 ExecuteList 필요. 이후는 AutoChange _PLC.WriteBit(eBitOutput.PC_AUTO_STATUS, true); Log.I(string.Format($"[PLCInterface]SET PC_AUTO_STATUS True")); #endregion return (int)InitErrorList.SUCCESS; } private double GetRotaryCurrentPosition() { try { //double finalValue = Convert.ToDouble(_plc.GetLongAIValue("PLC_CURRENT_POS_1") / 100000f); double finalValue = Convert.ToDouble(_PLC.ReadDWord((int)eWordInput.PLC_CURRENT_POS_1).data / 100000f); finalValue += _Scanner.Config.RotaryEncoderOffset; if (360.0 <= finalValue) { while (360.0 <= finalValue) { finalValue -= 360.0; } } else if (finalValue < 0) { while (finalValue < 0) { finalValue += 360.0; } } return finalValue; } catch { return -9999.9999; } } /// /// 현재 Rotary의 회전 각도에서, 현재 용접 카운트와 지그 번호를 계산. 추가로 Overflow 카운트도 초기화 /// /// Rotary 회전 Offset. 운전 시작 시 Rotary가 지그 1번 정중심에서부터 시작했을 경우가 0. 범위는 0이상 /// Rotary 내 지그 갯수임. 장비 및 T/T에 따라서, 360도 당 지그의 개수는 다를 수 있으므로. /// Rotary Welding Count. 30도 구간 하나가 지나갈 때마다 +1 /// 현재 '용접 중' 구간에 들어가 있는 지그 번호 public void InitRotaryOnTheFlyWeldingCount(double rotaryAngleOffset, int rotaryJigCount, out int weldingCount, out int weldingJigNo) { // 현재 로터리 엔코더 값을 기준으로 현재 카운트, 지그 번호를 확인 double anglePerOneJig = 360 / (double)rotaryJigCount; //double로 바꿔서 계산해야 22가 아닌 22.5로 anglePerOneJig가 계산됨 25.08.02 weldingCount = (int)((rotaryAngleOffset + anglePerOneJig / 2) / anglePerOneJig); // _WeldingCount = 0, 1, 2, ... weldingJigNo = (int)(weldingCount % rotaryJigCount) + 1; // _WeldingJigNo = 1, 2, ... , 12 Log.I($"Angle Per One Jig : {anglePerOneJig}"); } private string CheckInitCondition(SequenceContext context, string id, ICollection branches, SequenceActionResult param) { 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_INIT_CONDITION_ERROR; } Status.EquipmentMode = eEquipmentMode.AUTO; var startTime = DateTime.Now; context.PutValue(ContextKey.KEY_SEQUENCE_START_TIME, startTime); Thread.Sleep(1); // 부하 방지 Log.D("ReceiveCellInfo Sequence CheckInitCondition"); return SEQ_FORK_ENCODER_MONITOR; //return SEQ_CHECK_LOAD_COMPLETE; } // 지그 상태 확인 private SequenceActionResult CheckLoadComplete(SequenceContext context, string id, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.LOADING_CELL); // MOTF WaitScannerLoadListEnable // List가 실행 중이라면(ex : 엔코더 대기 상태 등), 반환값은 0, LoadList 할 수 없는 상태(List 실행 중이거나, 엔코더 대기 상태) // LoadList가 0보다 크면 다음 시퀀스 진행해야 함 WaitContinue: var ret = context.RetryUntilSuccess(() => _Scanner.LoadList(_WeldingScannerListNo) > 0, int.MaxValue, 10, true); if (ret == E2.TIMEOUT) { Log.E("TIMEOUT_Scanner_LoadList : 600시간 Run"); goto WaitContinue; } Log.I($"LoadList function success : List {_WeldingScannerListNo}"); Thread.Sleep(1); // 부하 방지 return SequenceAction.Ok(); } private SequenceActionResult CheckScannerListComplete(SequenceContext context, string id, SequenceActionResult param) { // Prefetch1 = 2회 전 업로드된 지그 = 현재 물리적으로 레이저 통과 중 = 완료 체크 대상. // Shift()는 이 메서드 이후 ReadyScannerList에서 실행되므로, 이 시점의 Prefetch1이 // 원본 코드의 InputScannerList[(listNo-1)]과 동일 대상이다. // 예열 구간: iter 1~2(Pf1=null) → 통과. iter 3~: Pf1=Jig1 → 실제 처리. var jigComplete = _CellScheduler.Prefetch1; if (jigComplete == null) { Log.I($"[Warmup] ScannerList {_WeldingCount}: pipeline not ready, skip complete check"); return SequenceAction.Ok(); } if (!CheckInsideWeldPosition(jigComplete.StartAngle)) { // IsCanExist=false(공 지그)는 리스트 실행은 하지만 용접이 없으므로 // 엔코더 범위를 벗어나도 에러가 아님 if (jigComplete.IsCanExist) { Log.I("ERR_SCANNER_LIST_PROCESSING_TIMEOVER"); return SequenceAction.Error(); } return SequenceAction.Ok(); } else { if (jigComplete.IsCanExist) { #region 용접 시간, 엔코더 측정/기록 double elaspedTime = _Scanner.GetTime(); if (jigComplete.WeldComplete) { jigComplete.WeldEndTime = DateTime.Now; jigComplete.WeldingSpanTime = TimeSpan.FromSeconds(elaspedTime); TimeSpan weldInterval = jigComplete.WeldEndTime - _prevWeldEndTime; _prevWeldEndTime = jigComplete.WeldEndTime; // 보고 대상 지정 불필요 — Shift()가 Prefetch1 → Complete로 자동 전진 Log.I(string.Format("JigNo : {0}, Weld start : {1}, Weld End : {2}, WeldTime : {3}, interval : {4:F3}", jigComplete.JigNo, jigComplete.WeldStartTime.ToString("HH:mm:ss.fff"), jigComplete.WeldEndTime.ToString("HH:mm:ss.fff"), jigComplete.WeldingSpanTime.TotalMilliseconds, weldInterval.TotalSeconds)); if (_PLC.ReadBit((int)eBitInput.PLC_USE_WELD_JIG_1 + jigComplete.JigNo - 1).data) { _PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE_1 + jigComplete.JigNo - 1, true); Log.I(string.Format($"PC_WELD_COMPLETE_{jigComplete.JigNo} -> true)")); } if (_SystemParameter.LastResetTime < DateTime.Now.Date) { _SystemParameter.LastResetTime = DateTime.Now.Date; Log.I($"Today Count Reset. {_SystemParameter.TodayProductCount} -> 0"); _SystemParameter.TodayProductCount = 0; } if (_SystemParameter.TodayProductCount < uint.MaxValue) { _SystemParameter.TodayProductCount++; } if (_SystemParameter.TotalProductCount < uint.MaxValue) { _SystemParameter.TotalProductCount++; } Status.GetProcessingWeldingData().TotalProduct = _SystemParameter.TotalProductCount; Status.GetProcessingWeldingData().TodayProduct = _SystemParameter.TodayProductCount; Status.GetProcessingWeldingData().AutoRunStartTime = _AutoRunStartTime; Status.GetProcessingWeldingData().RecipeNo = _RecipeNo; Status.GetProcessingWeldingData().LaserPowerByLBAD = _LaserPowerByLBAD.ToString(); Status.GetProcessingWeldingData().LotId = jigComplete.LotId; Status.GetProcessingWeldingData().CellId = jigComplete.CellId; Status.GetProcessingWeldingData().WeldingStartTime = jigComplete.WeldStartTime; Status.GetProcessingWeldingData().WeldingEndTime = jigComplete.WeldEndTime; Status.GetProcessingWeldingData().WeldingSpanTimeMilliseconds = jigComplete.WeldingSpanTime.TotalMilliseconds.ToString(); Status.GetProcessingWeldingData().JigNo = jigComplete.JigNo.ToString(); SaveCellAPD(); } UtilUnit.logUnit.SetLog(LogUnit.eLogType.PRODUCT, MethodBase.GetCurrentMethod().Name, "UPDATE_WELDING_TIME;" + jigComplete.WeldingSpanTime.TotalMilliseconds); UtilUnit.logUnit.SetLog(LogUnit.eLogType.PRODUCT, MethodBase.GetCurrentMethod().Name, "UPDATE_PRODUCT_COUNT;" + jigComplete.WeldEndTime.ToString("yyyy-MM-dd HH:mm:ss")); double encoder0_0, encoder1_0, encoder0_1, encoder1_1; _Scanner.ReadEncoder(out encoder0_0, out encoder1_0, out encoder0_1, out encoder1_1, true); double startEncoderCount = encoder0_0; double endEncoderCount = encoder0_1; double angularVelocity = (endEncoderCount - startEncoderCount) / elaspedTime; // 단위는 [degree/sec] Log.I(string.Format("Encoder start : {0:F4}, end : {1:F4}, vel : {2:F2}", startEncoderCount, endEncoderCount, angularVelocity)); #endregion #region 스캐너 리스트 Record 파일 기록 if (_Scanner.Config.UseRecordSignal) { String RecordFileName = _RecipeManager.ActivatedRecipe().Name + "_Jig#" + jigComplete.JigNo.ToString(); string DataFilePath = ProjectUtil.GetDataLogSetting().GetFilePath_ScannerRecordLog(RecordFileName, true); _Scanner.SaveRecord(DataFilePath); } #endregion } } return SequenceAction.Ok(); } /// /// startAngle startAngle+45 /// │←── OK 구간 ────►│ /// ────┼─────────────────┼──────────────► 로터리 각도 /// 리스트 여기까지 /// 시작 와야 완료 체크 가능 /// 판정 로직: /// currentAngle < startAngle → Too early (아직 리스트 실행 중, 완료 체크 너무 이름) /// startAngle ≤ current < start+45 → OK (정상 완료 구간) /// currentAngle ≥ startAngle + 45 → Too late (너무 늦음, 타임오버 에러) /// /// 현재 지그의 시작 각도 /// 지나갔으면 True public bool CheckInsideWeldPosition(double startAngle) { double encoderX, currentAngle; _Scanner.GetEncoder(out encoderX, out currentAngle, true); // Encoder 반전되는 순간이며, 이 때만 인터락 무시 if (0 < startAngle && currentAngle < 0) return false; if (startAngle <= currentAngle && currentAngle < startAngle + 45) { Log.I(string.Format("Current encoder check OK, startAngle : {0}, currentAngle : {1})", startAngle, currentAngle)); return true; } if (currentAngle < startAngle) Log.W(string.Format("Current encoder check NG(Too early), startAngle : {0}, currentAngle : {1})", startAngle, currentAngle)); else Log.W(string.Format("Current encoder check NG(Too late), startAngle : {0}, currentAngle : {1})", startAngle, currentAngle)); return false; } private string ReadyScannerList(SequenceContext context, string id, ICollection branches, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.UPLOADING_WELD_LIST); //_SelectedDrawObjectList = _ScannerCanvasUserCtrl.Model.Layers[0].Objects; #region List 입력 시점의 엔코더 카운트 로그 기록 double encoderX, encoderY; _Scanner.GetEncoder(out encoderX, out encoderY, true); UtilUnit.logUnit.SetLog(LogUnit.eLogType.SYSTEM, MethodBase.GetCurrentMethod().Name, string.Format("Encoder X, Y, Offset, Y-X+Offset : {0}, {1}, {2}, {3}", encoderX.ToString("F5"), encoderY.ToString("F5"), _RotaryEncoderOffset.ToString("F5"), (encoderY - encoderX + _RotaryEncoderOffset).ToString("F5"))); #endregion // 스캐너 위치 초기화 _Scanner.Fly_Return(_Scanner.Config.MarkingOntheFlyParkingPositionX, _Scanner.Config.MarkingOntheFlyParkingPositionY); #region List 입력(엔코더 조건 만족 대기) _WeldingCount++; double compensateEncoderReset = 0.015; // 엔코더 리셋 보상 각도 설정(70도/sec 기준 약 0.015도) WaitForEncoderNextRotarySection(_WeldingCount, _RotaryJigCount, _RotaryEncoderOffset, compensateEncoderReset, out _WeldingStartAngle); Log.I(string.Format("ScannerUnit.WaitForEncoderNextRotarySection : m_weldingCount({0}),m_abwRotaryEncoderOffset({1:F3}), compensateEncoderReset({2:F3}), overflowCount({3}), weldingStartAngle({4:F3})", _WeldingCount, _RotaryEncoderOffset, compensateEncoderReset, _EncoderOverflowCount, _WeldingStartAngle)); #endregion // 3개임으로 3으로 작성 #region List 입력(용접 좌표 입력) if (_WeldingJigNo == _RotaryJigCount) _WeldingJigNo = 1; else _WeldingJigNo++; bool canExist; // 지그 용접 수행 여부 확인 if (UsePlcShotThemAll) { canExist = true; Log.I("Settings - PLC : Shot Them All Mode"); } else canExist = GetRotaryCanExist(_WeldingJigNo); //Log.I("Can Exist : " + canExist.ToString() + ", JigNo " + _WeldingJigNo + " ,CountNo " + _WeldingCount + ", StartAngle(Y) : " + _WeldingStartAngle); // canExist 여부와 무관하게 매 지그마다 파이프라인 전진 // canExist=false 지그도 Shift()해야 Prefetch1 타이밍이 올바르게 유지됨. _CellScheduler.Shift(_WeldingJigNo); _CellScheduler.Prefetch2.JigNo = _WeldingJigNo; _CellScheduler.Prefetch2.ListNo = _WeldingScannerListNo; _CellScheduler.Prefetch2.WriteTime = DateTime.Now; _CellScheduler.Prefetch2.StartAngle = _WeldingStartAngle; _CellScheduler.Prefetch2.WeldingCount = _WeldingCount; _CellScheduler.Prefetch2.IsCanExist = canExist; #warning OC2 ACW 기준 >> Cell Tracker는 어느 시점에 데이터는 주는지 모르겠지만, '전전전 셀' 쯤에서 이미 건네받음! 다른 프로젝트에서 확인 필요함 // 복원 우선: 정지 전 업로드됐으나 로터리 감속·정지로 용접 미완된 셀 정보 복원. // canExist=false 지그도 TryRestoreCell을 호출하여 슬롯 소비(폐기) — 이후 오염 방지. (string LotId, string CellId) recvInfo = (string.Empty, string.Empty); if (_CellScheduler.TryRestoreCell(_WeldingJigNo, out var rLot, out var rCell)) { if (canExist) { recvInfo.LotId = rLot; recvInfo.CellId = rCell; Log.I($"[PendingRestore] Jig{_WeldingJigNo} 셀 복원 >> CellId({rCell})"); } else { Log.W($"[PendingRestore] Jig{_WeldingJigNo} canExist=false, 복원 데이터 폐기 >> CellId({rCell})"); } } else if (ProcessManager.Instance.ReceivedCellInfoQueue.TryDequeue(out var cell)) { if (canExist) { recvInfo.LotId = cell.LotId; recvInfo.CellId = cell.CellId; } else { // 앞공정 NG 셀: CellTracker는 투입 시 이미 스캔했으므로 큐에서 소비 후 폐기. // 소비하지 않으면 다음 정상 셀이 이 NG 데이터를 가져가 정합성 붕괴. Log.W($"[NG Skip] Jig{_WeldingJigNo} canExist=false, 큐 데이터 폐기 >> CellId({cell.CellId})"); } } else { if (canExist) Log.E($"[QueueEmpty] Jig{_WeldingJigNo} canExist=true 이지만 큐가 비어있음"); _CellTracker.getDummyData(out string lotId, out string cellId); recvInfo.LotId = lotId; recvInfo.CellId = cellId; } _CellScheduler.Prefetch2.LotId = recvInfo.LotId; _CellScheduler.Prefetch2.CellId = recvInfo.CellId; Status.GetProcessingWeldingData().WorkLotId = recvInfo.LotId; Status.GetProcessingWeldingData().WorkCellId = recvInfo.CellId; Status.GetProcessingWeldingData().WorkJigNo = _WeldingJigNo.ToString(); if (_CellScheduler.Prefetch2 != null) Log.I($"Prefetch2 >> CellId({_CellScheduler.Prefetch2.CellId}) [CanExist({_CellScheduler.Prefetch2.IsCanExist})] [JigNo({_CellScheduler.Prefetch2.JigNo})] " + $"[ListNo({_CellScheduler.Prefetch2.ListNo})] [WeldingCount({_CellScheduler.Prefetch2.WeldingCount})]" + $"[StartAngle(Y)({_CellScheduler.Prefetch2.StartAngle})]"); if (_CellScheduler.Prefetch1 != null) Log.I($"Prefetch1 >> CellId({_CellScheduler.Prefetch1.CellId}) [CanExist({_CellScheduler.Prefetch1.IsCanExist})] [JigNo({_CellScheduler.Prefetch1.JigNo})] " + $"[ListNo({_CellScheduler.Prefetch1.ListNo})] [WeldingCount({_CellScheduler.Prefetch1.WeldingCount})]" + $"[StartAngle(Y)({_CellScheduler.Prefetch1.StartAngle})]"); if (canExist) { #region 용접할 Objects 선택 if (_WeldingJigNo - 1 < _ScannerCanvasUserCtrl.Model.Layers.Length) { _SelectedDrawObjectList = _ScannerCanvasUserCtrl.Model.Layers[_WeldingJigNo - 1].Objects; Log.I(string.Format("Page 불러오기 : [{0}])", _WeldingJigNo - 1)); } else { _SelectedDrawObjectList = _ScannerCanvasUserCtrl.Model.Layers[0].Objects; Log.I(string.Format("Page 불러오기 : [0])")); } #endregion return SEQ_SET_PARAMS_UPLOAD_WELDING_LIST_SEQ; } else { return SEQ_EXECUTE_WELDING; } #endregion } private ScannerParam GetSelectedDrawObjectData(IDrawObject DrawObj) { if (DrawObj is Line lineItem) return WeldingRecipeManager.GetScannerParam(lineItem.ScannerParamNo); else if (DrawObj is Arc arcItem) return WeldingRecipeManager.GetScannerParam(arcItem.ScannerParamNo); else if (DrawObj is Group groupItem) return WeldingRecipeManager.GetScannerParam(groupItem.ScannerParamNo); return null; } public void WaitForEncoderNextRotarySection(int weldingCount, int rotaryJigCount, double rotaryAngleOffset, double encoderResetOffset, out double sectionStartAngle) { // 한 바퀴당 지그 숫자에 따른 변경 double encoderAllowedMinDegree = Int32.MinValue / _Scanner.Config.MarkingOnTheFlyEncoderResolutionRotate * 360.0; double encoderAllowedMaxDegree = Int32.MaxValue / _Scanner.Config.MarkingOnTheFlyEncoderResolutionRotate * 360.0; double anglePerOneJig = (double)(360 / (double)rotaryJigCount); // 아래는 지그 당 각도가 30도라는 가정하의 수식임. // 1.1. 시작 각도 = (현재 지그 번호 * 30도) -15도 - 초기화 엔코더 값 - 엔코더 리셋 보상값 // rotaryAngleOffset = x가 0도 이상 15도 미만일 때, weldingCount = 1부터 시작, sectionStartAngle = 15 - x에서 30씩 증가 // rotaryAngleOffset = x가 15도 이상 45도 미만일 때, weldingCount = 2부터 시작, sectionStartAngle = 30 - x에서 30씩 증가 // rotaryAngleOffset = x가 45도 이상 75도 미만일 때, weldingCount = 3부터 시작, sectionStartAngle = 30 - x에서 30씩 증가 // rotaryAngleOffset = x가 315도 이상 345도 미만일 때, weldingCount = 12부터 시작, sectionStartAngle = 30 - x에서 30씩 증가 // rotaryAngleOffset = x가 345도 이상 360도 미만일 때, weldingCount = 13부터 시작, sectionStartAngle = 30 - x에서 30씩 증가 sectionStartAngle = weldingCount * anglePerOneJig - (anglePerOneJig / 2) - rotaryAngleOffset - encoderResetOffset - _EncoderOverflowCount * (encoderAllowedMaxDegree - encoderAllowedMinDegree); // 1.2. 계산된 시작각도가 이미 지나버렸으면 알람 발생 필요 // 1.3. Overflow가 나는 숫자라면, 보상할 것. if (sectionStartAngle > encoderAllowedMaxDegree) { sectionStartAngle -= encoderAllowedMaxDegree - encoderAllowedMinDegree; _EncoderOverflowCount++; } double sectionEndAngle = (encoderAllowedMaxDegree < sectionStartAngle + anglePerOneJig) ? encoderAllowedMaxDegree : sectionStartAngle + anglePerOneJig; _Scanner.WaitForEncoderRotateRange(encoderAllowedMinDegree, encoderAllowedMaxDegree, sectionStartAngle, sectionEndAngle); } public bool GetRotaryCanExist(int jigNo) { if (jigNo <= _RotaryJigCount) return _PLC.ReadBit((int)eBitInput.PLC_USE_WELD_JIG_1 + (jigNo - 1)).data; else return false; } private SequenceActionResult SetUploadWeldingList_SeqParams(SequenceContext context, string id, SequenceActionResult param) { //현재 시퀀스에서 LoadList를 해주기 때문에 Sub 시퀀스에 IncludeListStartEnd를 false로 넘겨줘야 함 SeqUploadWeldingList_InitParam seqParams = new SeqUploadWeldingList_InitParam() { NeedSetLaserTiming = _ModulationMode, IncludeListStartEnd = false, CellMeasureCenterOffsetX = 0, CellMeasureCenterOffsetY = 0, SelectedDrawObjectList = _SelectedDrawObjectList, WeldingScannerListNo = _WeldingScannerListNo, Rotation_Degree = 11.25, }; return SequenceAction.Ok(seqParams); } private SequenceActionResult GetUploadWeldingList_SeqResult(SequenceContext context, string id, SequenceActionResult param) { var resultOutParam = param.Param as SeqUploadWeldingList_OutParam; if (resultOutParam.Success == false) { //AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_UploadWeldingList_ResultFailed); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_UploadWeldingList_ResultFailed); //return null; } ScannerParam laserParam = null; laserParam = GetSelectedDrawObjectData(_SelectedDrawObjectList[0]); _CellScheduler.Prefetch2.InnerCellInfo.RecipeNo = _RecipeNo; _CellScheduler.Prefetch2.InnerCellInfo.LaserPowerCore = laserParam.LaserPower1.ToString(); _CellScheduler.Prefetch2.InnerCellInfo.LaserPowerRing = laserParam.LaserPower2.ToString(); _CellScheduler.Prefetch2.InnerCellInfo.MarkSpeed = laserParam.MarkSpeed.ToString(); _CellScheduler.Prefetch2.InnerCellInfo.FocalLength = (_PLC.ReadDWord((int)eWordInput.PLC_WELDING_FOCAL_LENGTH).data / 1000f).ToString(); // 업로드 성공 확인 후 뷰/EDC용 Status 갱신 (ReadyScannerList에서 중복 갱신 제거) Status.GetProcessingWeldingData().LaserPowerCore = laserParam.LaserPower1.ToString(); Status.GetProcessingWeldingData().LaserPowerRing = laserParam.LaserPower2.ToString(); Status.GetProcessingWeldingData().MarkSpeed = laserParam.MarkSpeed.ToString(); Status.GetProcessingWeldingData().FocalLength = _CellScheduler.Prefetch2.InnerCellInfo.FocalLength; Log.FuncI("CellTracker Load"); return SequenceAction.Ok(); } //private SequenceActionResult SetReceiveCellInfo_SeqParams(SequenceContext context, string id, SequenceActionResult param) //{ // SeqReceiveCellInfo_InitParam seqParams = new SeqReceiveCellInfo_InitParam() { // }; // return SequenceAction.Ok(seqParams); //} //private SequenceActionResult GetReceiveCellInfo_SeqResult(SequenceContext context, string id, SequenceActionResult param) //{ // var resultOutParam = param.Param as SeqReceiveCellInfo_OutParam; // if (resultOutParam.Success == false) { // return null; // } // return SequenceAction.Ok(); //} private SequenceActionResult ExecuteWelding(SequenceContext context, string id, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.EXECUTING_WELD); // MOTF Welding // List 닫기 _Scanner.SetEndList(); #region List 실행(처음엔 ExecuteList, 다음부터 AutoChangeList, 이후 대상 리스트 교체 (1 <-> 2)) if (isTheFirstListExecute) { isTheFirstListExecute = false; _Scanner.ExecuteList(_WeldingScannerListNo); Log.I(string.Format($"START Welding : ExecuteList_{_WeldingScannerListNo}")); } else { _Scanner.AutoChange(); Log.I(string.Format($"Next upload list : uploadList_{_WeldingScannerListNo}")); } _WeldingScannerListNo = (_WeldingScannerListNo != eListNo.LIST1) ? eListNo.LIST1 : eListNo.LIST2; #endregion return SequenceAction.Ok(); } private SequenceActionResult RequestUnloadCell(SequenceContext context, string id, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.UNLOADING_CELL); return SequenceAction.Ok(); } private SequenceActionResult EncoderMonitor(SequenceContext context, string id, SequenceActionResult param) { double send_time_offset = 0.1; while (!context.Stopped) { double encoderX, currentAngle; _Scanner.GetEncoder(out encoderX, out currentAngle, true); double CurrentPosPLC = GetRotaryCurrentPosition(); bool WeldingInPosition = encoderX > _Scanner.Config.MarkingRangeMinRotate + 11.25 - send_time_offset - 0.1 && encoderX < _Scanner.Config.MarkingRangeMinRotate + 11.25 - send_time_offset + 0.1; // ── JigScheduler: 포크 내에서 참조 캡처 (Shift와의 경합 방지) ────── // Prefetch1: 2회 전 업로드된 지그 = 현재 물리적으로 실행 중인 지그. // 원본 코드의 InputScannerList[(listNo-1)]과 동일 대상. // Shift()는 메인 시퀀스에서만 호출되므로 루프 시작 시 캡처해도 안전. var executeTarget = _CellScheduler.Prefetch1; if (WeldingInPosition) { if (executeTarget != null && executeTarget.IsCanExist && executeTarget.JigNo > 0) { Log.I($"Fork Before Min Rotate Encoder X : ({encoderX}), Encoder Y : ({currentAngle}), PLC Rotary : ({CurrentPosPLC}), List {executeTarget.ListNo}, JigNo : ({executeTarget.JigNo})"); if (!_SystemParameter.BypassModeLBAD) { if (!executeTarget.CheckSendCellinfo) { if (currentAngle > executeTarget.StartAngle) { executeTarget.CheckSendCellinfo = _StreamLightLBAD.SendCellInfo(executeTarget.LotId, executeTarget.CellId, _RecipeNo, _ModelNo, executeTarget.JigNo); if (!executeTarget.CheckSendCellinfo) Log.I($"[LBAD Interface]SendCellInfo null, LotID [{executeTarget.LotId}], CellID [{executeTarget.CellId}], JigNo [{executeTarget.JigNo}]"); else Log.I($"[StreamLight]SendCellInfo LotID [{executeTarget.LotId}], CellID [{executeTarget.CellId}], JigNo [{executeTarget.JigNo}]"); } } } } } var LaserIO = _IO._iOUnit.ReadDiBit(Convert.ToInt32(eADIAddress.LaserOn)); var IsLaserOn = ChangeChecker.IsChangedBoolMonitoring("LaserOn", LaserIO, true); if (IsLaserOn) { if (executeTarget != null && executeTarget.IsCanExist && executeTarget.WeldStartTime == DateTime.MinValue) { executeTarget.WeldStartTime = DateTime.Now; executeTarget.WeldComplete = true; Log.I($"Laser On, Welding Complete. JigNo : ({executeTarget.JigNo})"); } } // List의 시작/종료 로그 — MOTF에서 LIST1 -> LIST2 전환 시 종료+시작 각각 기록 _Scanner.GetListPointer(out eListNo curList, out uint pos); var isBusy = _Scanner.GetBusyStatus(); eListNo effectiveList = (!isBusy && (curList == eListNo.LIST1 || curList == eListNo.LIST2)) ? eListNo.NONE : curList; if (ChangeChecker.IsChangedInt("ListState", (int)effectiveList, out int prevListRaw)) { var prevList = (eListNo)prevListRaw; if (prevList != eListNo.NONE) Log.I($"[ListEnd] List {prevList} 종료 >> LaserOn({LaserIO}) EncoderY({currentAngle:F3}) JigNo({executeTarget?.JigNo})"); if (effectiveList != eListNo.NONE) Log.I($"[ListStart] List {effectiveList} 시작 >> LaserOn({LaserIO}) EncoderY({currentAngle:F3}) JigNo({executeTarget?.JigNo})"); } for (int i = 0; i < _RotaryJigCount; i++) { if (_PLC.ReadBit((int)eBitInput.PLC_USE_WELD_JIG_1 + i).data == false) { _PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE_1 + i, false); } } _PLC.WriteDWord((int)eWordOutput.PC_CURRENT_POS, (int)((currentAngle + _RotaryEncoderOffset) % 360 * 100_000f)); Thread.Sleep(1); // 부하 방지 } return SequenceAction.Ok(); } private SequenceActionResult ReportCellResult(SequenceContext context, string id, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.REPORTING_RESULT); string strDate = DateTime.Now.Date.ToString("yyyyMMdd"); string strTime = DateTime.Now.ToString("HHmmss"); // 셀 식별 정보 + LBAD AVG는 스케줄러 슬롯에서 직접 읽음 (ReportSlot 또는 pendingSlot) // ReportSlot 단계에서는 지그 1회전(~320ms) 경과 → LBAD MES Report(~100ms) 보장 수신 var report = _CellScheduler.ActiveReportSlot; bool lbadReceived = !double.IsNaN(report.LbadAvg); string lbadAvg = lbadReceived ? report.LbadAvg.ToString("F3") : "0"; if (!lbadReceived) Log.W($"[LBAD] ReportSlot LbadAvg 미수신 >> CellId({report.CellId})"); // 레시피·장비 계측값은 Status에서 읽음 (TodayProduct/TotalProduct 포함) var wd = Status.GetProcessingWeldingData(); string reportDataLocalPath = _CellTracker._CellTrackerConfig.reportDataLocalPath; string equipName = _CellTracker._CellTrackerConfig.EquipmentName; string directory = string.Format(reportDataLocalPath + "{0}", strDate); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); string strPath = string.Format(reportDataLocalPath + "{0}\\{1}_{2}_{3}.csv", strDate, strDate, "H52A_" + equipName, strTime.Substring(0, 2)); // ECS 전송용 strData 빌드 string strData = string.Format( "Date={0},Time={1},LotID={2},CellID={3}," + equipName + "DATA001={4}," + equipName + "DATA002={5}," + equipName + "DATA003={6}," + equipName + "DATA004={7}," + equipName + "DATA005={8}," + equipName + "DATA006={9}," + equipName + "DATA007={10}," + equipName + "DATA008={11}," + equipName + "DATA009={12}", strDate, strTime, report.LotId, report.CellId, report.InnerCellInfo.RecipeNo, report.InnerCellInfo.LaserPowerCore, report.InnerCellInfo.MarkSpeed, report.InnerCellInfo.FocalLength, report.JigNo, report.WeldingSpanTime.TotalMilliseconds.ToString(), lbadAvg, wd.TodayProduct, wd.TotalProduct); _CellTracker.SendEcsData(report.CellId, strPath, strData + ""); Log.I($"Report to CellTracker >> [LotID({report.LotId})] [CellID({report.CellId})] [JigNo({report.JigNo})] [LbadAvg({lbadAvg})]"); Log.I($"Cell Data : {strData}"); // 로컬 CSV 저장 string strItem = "//Date, Time, LotID, CellID, RecipeNo, LaserPower, MarkSpeed, FocalLength, HeadNo, WeldingTime, LaserPowerByLBAD, TodayCount, TotalCount"; string strCsvData = string.Format("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}", strDate, strTime, report.LotId, report.CellId, report.InnerCellInfo.RecipeNo, report.InnerCellInfo.LaserPowerCore, report.InnerCellInfo.MarkSpeed, report.InnerCellInfo.FocalLength, report.JigNo, report.WeldingSpanTime.TotalMilliseconds.ToString(), lbadAvg, wd.TodayProduct, wd.TotalProduct); WeldingCommon.Utils.ExcelManagementUnit.AppendStringInCSV(strPath, strItem, strCsvData); // 보고 완료: Complete.ReportComplete=true 설정 + pendingSlot 클리어 _CellScheduler.CompleteReport(); return SequenceAction.Ok(); } //private SequenceActionResult SetReportCellResult_SeqParams(SequenceContext context, string id, SequenceActionResult param) //{ // Status.SetWorkingPosition(eWorkingPosition.REPORTING_RESULT); // SeqReportCellResult_InitParam seqParams = new SeqReportCellResult_InitParam() { // EDC_Interface = new EDC(), // }; // return SequenceAction.Ok(seqParams); //} //private SequenceActionResult GetReportCellResult_SeqResult(SequenceContext context, string id, SequenceActionResult param) //{ // var resultOutParam = param.Param as SeqReportCellResult_OutParam; // if (resultOutParam.Success == false) { // return null; // } // return SequenceAction.Ok(); //} private static string CheckCycleStop(SequenceContext context, string id, ICollection branches, SequenceActionResult param) { Status.SetWorkingPosition(eWorkingPosition.CHECKING_CYCLE); // TODO: 확인 필요, CybleStop 에 대한 내용, cycle stop 여부 가져오기 //bCycleStop = GetCycleStopRequest(); // TODO: 한번만 돌도록 설정함, PLC 로 받으면 좋음. //Jin : cycleStop 명령을 용접 프로그램에서 할 경우가 현재는 없음.. // CheckCycleStop 노드와 Complete 노드 사이 다른 노드가 필요하다면 해당 노드를 CheckCycleStop 노드 위로 올리자 bool cycleStop = false; if (cycleStop) { return SEQ_SAVE_RESULT_DATA; } else { return SEQ_CHECK_LOAD_COMPLETE; } } private string CheckCanWelded(SequenceContext context, string id, ICollection branches, SequenceActionResult param) { // ── 재시작 전 미완료 보고 우선 처리 ───────────────────────────────────────── // SavePendingComplete()로 보존된 슬롯이 있으면 정상 파이프라인(Complete)보다 우선. if (_CellScheduler.HasPendingComplete) { var p = _CellScheduler.PendingComplete; Log.I($"[PendingReport] 재시작 전 미완료 보고: LotId={p.LotId}, CellId={p.CellId}, JigNo={p.JigNo}"); return SEQ_REPORT_CELL_RESULT; } // ───────────────────────────────────────────────────────────────────────────── // 정상 보고 경로: // - Complete(Shift로 자동 승격)에 용접 완료된 셀이 있고 아직 보고 미완인 경우 // - 예열 구간(Complete=null 또는 WeldComplete=false)은 자연스럽게 통과 if (_CellScheduler.HasReport) return SEQ_REPORT_CELL_RESULT; return SEQ_CHECK_CYCLE_STOP; } private void SaveCellAPD() { if (Status.GetProcessingWeldingData().APD.Count > 0) { string directoryPath = "D:\\ILSOSConfig\\APD_History"; string fileName = DateTime.Now.ToString("HH") + "_CellAPD"; string fullPath = Path.Combine(directoryPath, Status.GetProcessingWeldingData().APD.First().Date.ToString("yyyy-MM-dd"), fileName); Status.GetProcessingWeldingData().APD.CreateFiles(fullPath); } } private SequenceActionResult SaveResultData(SequenceContext context, string id, SequenceActionResult param) { context.PutValue(ContextKey.KEY_SEQUENCE_RESULT, true); return SequenceAction.Ok(); } private SequenceActionResult WriteCompletionPLCBit(SequenceContext context, string id, SequenceActionResult param) { // 완료 비트 write 하는 Node 추가 필요. format 유지, ReceiveCellInfo 없음 Log.D("AutoRun Sequence WriteCompletionPLCBit"); return SequenceAction.Ok(); } private SequenceActionResult InitConditionError(SequenceContext context, string id, SequenceActionResult param) { context.TryGetValue(ContextKey.KEY_SEQUENCE_ERROR_CODE, out int InitializeCode); switch (InitializeCode) { case (int)InitErrorList.PLC_AUTORUN_BIT_OFF: Log.D("AutoRun Sequence InitConditionError : PLC_AUTORUN_OFF"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition_PLC_AUTORUN_OFF); case (int)InitErrorList.LBAD_SUB_ACTIVATE_FAILED: Log.D("AutoRun Sequence InitConditionError : LBAD_SUB_ERR"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition_LBAD_SUB_ERR); case (int)InitErrorList.LBAD_CLIENT_ACTIVATE_FAILED: Log.D("AutoRun Sequence InitConditionError : LBAD_CLIENT_ERR"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition_LBAD_CLIENT_ERR); case (int)InitErrorList.LASER_INIT_ERROR: Log.D("AutoRun Sequence InitConditionError : LASER_ERR"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition_LASER_ERR); case (int)InitErrorList.MOTF_OFF: Log.D("AutoRun Sequence InitConditionError : MOTF_OFF"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition_MOTF_OFF); case (int)InitErrorList.PLC_ALARM: Log.D("AutoRun Sequence InitConditionError : PLC_ALARM"); return null; default: Log.D("AutoRun Sequence InitConditionError"); return SequenceAction.ForceStopByAlarm(AlarmCode.ALARM_SEQUENCE_AutoRun_CheckInitCondition); } } private SequenceActionResult Complete(SequenceContext context, string id, SequenceActionResult param) { RestoreDeviceStatus(); context.Settings.Save(CommonSettingIds.SystemParameter, _SystemParameter, "SeqAutoRunCCWH52A"); context.TryGetValue(ContextKey.KEY_SEQUENCE_RESULT, out bool seqResult); SeqAutoRunCCWH52A_OutParam result = new SeqAutoRunCCWH52A_OutParam() { Success = seqResult }; if (seqResult == false) { //AlarmManager.Alarm(AlarmCode.ALARM_SEQUENCE_AutoRun_ResultFailed); } Log.D("AutoRun Sequence Complete"); Status.SetWorkingPosition(eWorkingPosition.DONE); return SequenceAction.Ok(result); } private bool RestoreDeviceStatus() { //PLC All BIT OFF for (int i = 0; i < _RotaryJigCount; i++) { _PLC.WriteBit((int)eBitOutput.PC_WELD_COMPLETE_1 + i, false); } _PLC.WriteBit((int)eBitOutput.PC_AUTO_STATUS, false); //_PLC.WriteBit((int)eBitOutput.PC_RECIPE_PARAM_UPLOAD_COMPLETE, false); Log.I(string.Format("[PLCInterface]PC All Bit Off")); //LBAD Ready Off //_LBAD.SetReadybyAuto(false); if (_SystemParameter.BypassModeLBAD == false) { if (_StreamLightLBAD.SendReadyStatus(eRequestData.Ready_Off) == false) { Log.I(string.Format($"[LBAD Interface]CheckReadyStatus null")); } } //Laser Off _Laser.LaserOff(); //List 실행 종료 _Scanner.StopExecution(); //Anlog 출력 0으로 변경, efunctiontype을 command로 변경해주면 list가 아니라 직접 변경됨 _Scanner.WriteDA((uint)eAnalogPortOut.DA1, 0, eFunctionType.COMMAND); _Scanner.WriteDA((uint)eAnalogPortOut.DA2, 0, eFunctionType.COMMAND); //Scanner 원점 복귀 _Scanner.JumpAbs(0, 0, eFunctionType.COMMAND); //Vision Correctiong 파일 변경 _Scanner.SelectCorrectionTable(1); //Recipe를 기존에 Manual에서 작업하던 화면으로 rollback WeldingRecipeManager.RollbackRecipe(); return true; } private void OnForceError(SequenceContext context, string arg2) { context.Settings.Save(CommonSettingIds.SystemParameter, _SystemParameter, "SeqAutoRunCCWH52A"); OnSeqActionStop(); Log.I("SequenceAction Ok : OnForceError"); } private void OnStopped(SequenceContext context) { context.Settings.Save(CommonSettingIds.SystemParameter, _SystemParameter, "SeqAutoRunCCWH52A"); OnSeqActionStop(); Log.I("SequenceAction Ok : OnStopped"); } private void OnSeqActionStop() { Status.EquipmentMode = eEquipmentMode.MANUAL; foreach (var info in ProcessManager.Instance.ReceivedCellInfoQueue.Select((value, index) => (value, index))) { Log.I($"[{info.index}]Queue >> CellId({info.value.CellId}) LotId({info.value.LotId})"); } // 정지 시점 파이프라인 상태 로그 (재시작 후 복원 여부 사전 확인용) if (_CellScheduler.Prefetch1 != null && !string.IsNullOrEmpty(_CellScheduler.Prefetch1.CellId)) Log.I($"Stoped Seq >> Prefetch1[Jig{_CellScheduler.Prefetch1.JigNo}] CellId={_CellScheduler.Prefetch1.CellId}, WeldComplete={_CellScheduler.Prefetch1.WeldComplete}"); if (_CellScheduler.Prefetch2 != null && !string.IsNullOrEmpty(_CellScheduler.Prefetch2.CellId)) Log.I($"Stoped Seq >> Prefetch2[Jig{_CellScheduler.Prefetch2.JigNo}] CellId={_CellScheduler.Prefetch2.CellId}, WeldComplete={_CellScheduler.Prefetch2.WeldComplete}"); //PLC All BIT OFF for (int i = 0; i < _RotaryJigCount; i++) _PLC.WriteBit(eBitOutput.PC_WELD_COMPLETE_1 + i, false); _PLC.WriteBit(eBitOutput.PC_AUTO_STATUS, false); Log.I("[PLCInterface]PC All Bit Off"); //LBAD Ready Off //_LBAD.SetReadybyAuto(false); if (_SystemParameter.BypassModeLBAD == false) { if (_StreamLightLBAD.SendReadyStatus(eRequestData.Ready_Off) == false) { Log.I(string.Format($"[LBAD Interface]CheckReadyStatus null")); } } //Laser Off _Laser.LaserOff(); //List 실행 종료 _Scanner.StopExecution(); //Anlog 출력 0으로 변경, efunctiontype을 command로 변경해주면 list가 아니라 직접 변경됨 _Scanner.WriteDA((uint)eAnalogPortOut.DA1, 0, eFunctionType.COMMAND); _Scanner.WriteDA((uint)eAnalogPortOut.DA2, 0, eFunctionType.COMMAND); //Scanner 원점 복귀 _Scanner.JumpAbs(0, 0, eFunctionType.COMMAND); //Vision Correctiong 파일 변경 _Scanner.SelectCorrectionTable(1); //Recipe를 기존에 Manual에서 작업하던 화면으로 rollback WeldingRecipeManager.RollbackRecipe(); } } // For CSV 저장용 public class CenterPositionData : NotifyPropertyChangedBase { private double _CenterX; public double CenterX { get => _CenterX; set => SetProperty(ref _CenterX, value); } private double _CenterY; public double CenterY { get => _CenterY; set => SetProperty(ref _CenterY, value); } } public class SeqAutoRunCCWH52A_InitParam : InitParam { public ScoutSeqParams ScoutSeqParams { get; set; } public bool UseStream { get; set; } } public class SeqAutoRunCCWH52A_OutParam : OutParam { } }