PLCController.DoActivate() 가 PLC 연결을 열기 전에 _monitorPlcInterfaceMap 을 기동해, 연결 확립 전 LiveStatusUpdate() 가 돌면서 ALARM_DEVICE_PLC_DISCONNECT(210002) 오알람이 매 기동마다 발생했다. 원인 - Livetimer 가 초기화되지 않아 DateTime.MinValue 상태 → (Now - MinValue) 가 항상 10000ms 를 초과해 워치독이 오판. 임계값 조정으로는 해결 불가. - MicroTimer 첫 콜백이 주기(100ms) 뒤가 아니라 Start() 직후(약 1ms)라 경합이 아니라 100% 재현되는 결정적 버그. 실측 (2026-08-10 16:12:25.904) Livetimer=0001-01-01, elapsed=63921975145904ms, isLive=False, IsActivated=False, IsConnected=False 조치 - _monitorPlcInterfaceMap.Start() 를 base.DoActivate() 성공 이후로 이동 - Livetimer = DateTime.Now 로 기준시각 확보 후 타이머 기동 - 두 조치는 각각 단독으로는 무효. 반드시 함께 적용해야 한다. - 수정 전/후를 #region 2개(검색: 260810 기동 오알람)로 기록해 원복 가능 검증: 2026-08-10 현장 재기동 확인 — 기동 시 210002 미발생 미해결(별건): Livetimer 이중 용도, ReadBit .err 무시, ClearAlarm 부재, SeqAlarmMonitor 의 무지연 루프 및 Laser/Chiller 즉시 알람 상세: docs/분석_PLC210002_기동오알람_260810.md 작업일지: docs/작업일지/2026_08_10.md
343 lines
13 KiB
C#
343 lines
13 KiB
C#
using Aurora.Api;
|
|
using Aurora;
|
|
using AuroraDeviceCommon.Utils;
|
|
using AuroraPLC;
|
|
using Hermes;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using AuroraMelsecNet;
|
|
using AuroraMXComponent;
|
|
using System.Reactive.Linq;
|
|
using System.Runtime.Remoting.Contexts;
|
|
using WeldingCommon.Utils;
|
|
using System.Threading;
|
|
using WeldingCommon.Alarm;
|
|
|
|
namespace WeldingCommon.Devices
|
|
{
|
|
// 메모리 맵핑을 abstract로 강제화하는 것 다시 추가(5:06) 다시제거(08:30)
|
|
public abstract partial class PLCController : PLCBase
|
|
{
|
|
// Log 사용 방법
|
|
static Logger Log = new Logger("PLCController");
|
|
|
|
//static Logger EventLog = new Logger(eMsg_TYPE.PLC.ToString(), "Event");
|
|
|
|
//MonitorPlcInterfaceMap 스레드는 타이머로 구성
|
|
public MicroTimer _monitorPlcInterfaceMap = new MicroTimer();
|
|
|
|
DateTime Livetimer;
|
|
bool beforeLiveStatus = false;
|
|
bool updateLiveWait = false;
|
|
|
|
#region 생성자
|
|
IAuroraEnvironment _AuroraEnv;
|
|
public PLCController(string deviceId, ePLCNetType plcNetType, bool isSimul, PLCMMapFileType fileType, IAuroraEnvironment aurora)
|
|
: base(deviceId, isSimul)
|
|
{
|
|
_AuroraEnv = aurora;
|
|
|
|
// PLC Network 생성
|
|
// PLCNet 의 생명주기 관리
|
|
NetworkType = plcNetType;
|
|
try {
|
|
if (plcNetType == ePLCNetType.MelsecNet) { // MelsecNet
|
|
#warning MXComponent 사용 시 AuroraMelsecNet Reference 삭제 및 new MelsecNet 코드 주석처리
|
|
PLCNet = new MelsecNet();
|
|
} else if (plcNetType == ePLCNetType.MXComponent) { // MxComponent
|
|
#warning MelsecNet 사용 시 AuroraMXComponent Reference 삭제 및 new MXComponent 코드 주석처리
|
|
PLCNet = new MXComponent(); // 주석 해제 필요
|
|
} else { // 통신 프로토콜 추가시, 여기에 추가...
|
|
// 에러
|
|
Log.E("Unsupported network type");
|
|
}
|
|
|
|
// 메모리맵 파싱 관련
|
|
mmapFileType = fileType;
|
|
|
|
//생성자에서 메모리 맵핑이 완료되야됨.
|
|
LoadMemoryMapFromFiles();
|
|
|
|
// PLC Motions 의 시뮬레이션은 추후 재검토 필요
|
|
|
|
if (PLCNet is null) {
|
|
Log.E("PLC Network (MXComponent or MelsecNet) is not existed");
|
|
}
|
|
|
|
#region PLC 모션축 세팅
|
|
Observable.Merge(
|
|
aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_DEVICE_ACTIVATED),
|
|
aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_DEVICE_DEACTIVATED))
|
|
.Select(msg => msg.Param as string)
|
|
.Where(x => x == deviceId)
|
|
.Subscribe(x => OnDeviceActiveChanged()
|
|
);
|
|
#endregion
|
|
} catch (Exception ex) {
|
|
Log.FuncE(ex.Message);
|
|
PLCNet = null;
|
|
}
|
|
|
|
// Event Publisher 확인(이전 PLC에 있는 타이머 부분 추가)
|
|
_monitorPlcInterfaceMap.OnTimeout += MonitorPlcInterfaceMap;
|
|
}
|
|
|
|
public void MonitorPlcInterfaceMap()
|
|
{
|
|
// Live Bit Control
|
|
LiveStatusUpdate();
|
|
|
|
var isAutoRequest = ReadBit(eCommonBitInput.PLC_AUTO_REQ).data;
|
|
var isManualRequest = ReadBit(eCommonBitInput.PLC_MANUAL_REQ).data;
|
|
|
|
if (isAutoRequest && isManualRequest) {
|
|
Log.I("PLC 에서 Auto Run Start 와 Manual 전환 신호가 동시에 들어왔습니다. 신호를 무시합니다.");
|
|
return;
|
|
}
|
|
|
|
#region PLC 신호중에 Command 신호 들어온 것에 대한 처리. rising edge 인 경우에만 반응하도록 코딩할 것
|
|
|
|
if (ChangeChecker.IsChangedBoolMonitoring("MainSeqStart", isAutoRequest, MonitoringValue: true)) {
|
|
EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_AUTO_RUN_START_REQUEST));
|
|
}
|
|
|
|
//260512 잠시 주석처리
|
|
if (ChangeChecker.IsChangedBoolMonitoring("MainSeqStop", isManualRequest, MonitoringValue: true))
|
|
{
|
|
EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_AUTO_RUN_STOP_REQUEST));
|
|
}
|
|
|
|
var isRecipeUpdate = ReadBit(eCommonBitInput.PLC_RMS_PARAMETER_READ_REQUEST).data;
|
|
if (ChangeChecker.IsChangedBoolMonitoring("RmsParameterReadReq", isRecipeUpdate, MonitoringValue: true)) {
|
|
EventPublisher.Publish(new EventMessage("PLC", CommonEventIds.EVENT_RMS_PARAMETER_READ_REQUEST));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private void LiveStatusUpdate()
|
|
{
|
|
if (IsSimul)
|
|
return;
|
|
|
|
// Live 신호 깜빡이기
|
|
// PLC 상태 변경 후 특정 시간동안 대기 하다가 값 넘겨주기
|
|
if (updateLiveWait) {
|
|
if (DateTime.Now.Subtract(Livetimer).TotalMilliseconds > 1000) {
|
|
updateLiveWait = false;
|
|
WriteBit(eCommonBitOutput.PC_LIVE_STATUS, beforeLiveStatus);
|
|
}
|
|
} else {
|
|
bool isLive = ReadBit(eCommonBitInput.PLC_LIVE_STATUS).data;
|
|
// PLC 상태가 변한경우
|
|
if (isLive != beforeLiveStatus) {
|
|
Livetimer = DateTime.Now;
|
|
beforeLiveStatus = isLive;
|
|
updateLiveWait = true;
|
|
}
|
|
// 10초간 PLC 상태가 변하지 않는 경우 TimeoOut 10000
|
|
//260512 주석 처리 잠시
|
|
if (DateTime.Now.Subtract(Livetimer).TotalMilliseconds > 10000)
|
|
{
|
|
AlarmManager.Alarm(AlarmCode.ALARM_DEVICE_PLC_DISCONNECT);
|
|
//Log.E($"PLC HeartBeat Signal Is Not Working");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RegisterMotion()
|
|
{
|
|
#region PLC 모션축 세팅
|
|
if (HasMotionAxes && _plcMotorList.Count == 0) {
|
|
// 모션축 등록.... (수정필요)
|
|
//_plcMotorList = new List<PLCMotorBase>();
|
|
//var motorList = DeviceIds.GetMotorDeviceIDs();
|
|
//foreach (var item in motorList) {
|
|
// _plcMotorList.Add(_AuroraEnv.DeviceManager[item] as PLCMotorBase);
|
|
//}
|
|
|
|
//// 별칭 설정 (인덱스는 ACS 랑 차후 검토 필요함...) (Inkjet 은 ACS 축은 없음)
|
|
//stageX = _plcMotorList[(int)eAxis.Stage_X - (int)eAxis.Stage_X];
|
|
//headY = _plcMotorList[(int)eAxis.Head_Y - (int)eAxis.Stage_X];
|
|
//headZ = _plcMotorList[(int)eAxis.Head_Z - (int)eAxis.Stage_X];
|
|
//stageT = _plcMotorList[(int)eAxis.Stage_T - (int)eAxis.Stage_X];
|
|
|
|
//// 메모리맵 Config 를 Readonly 로 세팅함..각 모터 override 활성화에서 처리
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
#region 활성화/비활성화
|
|
protected override int DoActivate(IDeviceConfig config)
|
|
{
|
|
//==============================================
|
|
// 로그 조건 세팅
|
|
//IsLogModePLCDetail = SolderingUtil.GetLogSetting().LOG_MODE_PLC_DETAIL;
|
|
IsLogModePLCDetail = false;
|
|
|
|
// MicroTimer 설정
|
|
_monitorPlcInterfaceMap.UpdateRateUS = 100 * 1000;
|
|
|
|
#region 260810 기동 오알람(210002) 수정 — 모니터 타이머 기동 위치 [수정 전]
|
|
// [수정 전] 여기서 타이머를 기동하면 base.DoActivate() 로 PLC 연결이 열리기도 전에
|
|
// LiveStatusUpdate() 가 돌아 ALARM_DEVICE_PLC_DISCONNECT(210002) 오알람 발생.
|
|
//_monitorPlcInterfaceMap.Start();
|
|
//
|
|
// [수정 후] 아래 "[수정 후]" region 으로 이동 (base.DoActivate() 성공 + Livetimer 확보 뒤)
|
|
#endregion
|
|
|
|
var res = base.DoActivate(config);
|
|
|
|
//Activate할 때, 한번 더 주소 맵핑 필요 (맵핑이 생성자에서 통신 세팅보다 느림)
|
|
LoadMemoryMapFromFiles();
|
|
|
|
if (res != E1.OK) { // PLC 통신 실패
|
|
return res;
|
|
}
|
|
beforeLiveStatus = false;
|
|
updateLiveWait = false;
|
|
|
|
#region 260810 기동 오알람(210002) 수정 — Livetimer 초기화 + 타이머 기동 [수정 후]
|
|
// 원인 : Livetimer 가 초기화되지 않아 DateTime.MinValue 인 상태로 LiveStatusUpdate() 가 돌면
|
|
// (Now - MinValue) 가 항상 10000ms 를 초과해 210002 가 발생한다.
|
|
// MicroTimer 의 첫 콜백은 주기(100ms) 뒤가 아니라 Start() 직후(약 1ms)라 100% 재현.
|
|
// 임계값 10000 을 키우는 방식으로는 해결되지 않는다(MinValue 앞에서는 모두 초과).
|
|
// 실측 : 2026-08-10 16:12:25.904 로그
|
|
// Livetimer=0001-01-01, elapsed=63921975145904ms, IsActivated=False, IsConnected=False
|
|
// 조치 : 기준시각을 확보한 뒤 모니터 타이머를 기동한다.
|
|
// 원복 : 이 region 을 삭제하고, 위 "[수정 전]" region 의 Start() 주석을 해제한다.
|
|
// 상세 : docs/분석_PLC210002_기동오알람_260810.md
|
|
Livetimer = DateTime.Now;
|
|
_monitorPlcInterfaceMap.Start();
|
|
#endregion
|
|
|
|
return res;
|
|
}
|
|
|
|
protected override int DoDeactivate()
|
|
{
|
|
var res = base.DoDeactivate();
|
|
|
|
_monitorPlcInterfaceMap.Stop();
|
|
|
|
// 비동기 통신이 종료되었는지 확인하는 로직으로 교체
|
|
if (!WaitUntilUpdateTerminateMonitor(1000, out int elapsedMSForUpdateTerminate))
|
|
{ // 업데이트 스레드 종료가 아직 안됨
|
|
Log.E("Data update is not teminated yet");
|
|
return E2.CANCELLED;
|
|
}
|
|
|
|
if (res != E1.OK) {
|
|
Log.E("Deactivate is failed");
|
|
return res;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
public bool WaitUntilUpdateTerminateMonitor(int timeOutMS, out int elapsedMS)
|
|
{
|
|
elapsedMS = timeOutMS;
|
|
// convert to Max count
|
|
var delayMS = 10;
|
|
int maxCount = timeOutMS / delayMS;
|
|
int i;
|
|
for (i = 0; i < maxCount; i++)
|
|
{
|
|
if (_monitorPlcInterfaceMap.IsRunning)
|
|
{ // 대기
|
|
// 짧은 주기로 검사하자 !!
|
|
Thread.Sleep(delayMS);
|
|
continue;
|
|
}
|
|
else
|
|
{ // Update thread is terminated
|
|
elapsedMS = i * delayMS;
|
|
return true;
|
|
}
|
|
}
|
|
// 실패
|
|
return false;
|
|
}
|
|
|
|
private void OnDeviceActiveChanged()
|
|
{
|
|
if (IsActivated) { // PLC 연결 성공
|
|
// PLCMotion 스레드 관련
|
|
// 시뮬 모드이고, PLCMotorList 가 비어있지 않으면 모션 스레드 생성
|
|
if (IsSimul && _plcMotorList.Count > 0) {
|
|
StartPLCMotionThread(); // 스레드 생성 및 시작
|
|
}
|
|
|
|
|
|
// Mover 활성화(수정필요)
|
|
//MachineMover.GetInstanceMachineMover()?.Activate();
|
|
} else {
|
|
// Mover 비활성화
|
|
//MachineMover.GetInstanceMachineMover()?.Deactivate();
|
|
|
|
StopPLCMotionThread();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 재연결
|
|
public override int ReconnectNetwork()
|
|
{
|
|
var res = base.ReconnectNetwork();
|
|
return res;
|
|
}
|
|
#endregion
|
|
|
|
#region 메모리맵 파일 파싱
|
|
public void LoadMemoryMapFromFiles()
|
|
{
|
|
/*
|
|
if (!ParsePLCAlarmSettingAddress(AlarmSettingPath, ref _stAlarmList, ref _stSettingList, out ePLCErrCode retCode)) {
|
|
if (retCode == ePLCErrCode.MemMapPaser_SETTINGALARM_LOAD_FAIL) {
|
|
_alarmService.SetAlarm(AlarmCode.ALARM_PLC_EXCEL_SETTINGALARM_LOAD_FAIL);
|
|
}
|
|
Log.E("SettingAlarm MMap parsing is failed");
|
|
}
|
|
*/
|
|
|
|
// PLC 메모리 주소를 매핑함
|
|
InitPLCAddress();
|
|
|
|
// 정상여부 체크...
|
|
if (BitInputs.Count == 0) {
|
|
Log.W("No. of Bit Input MMap is zero");
|
|
}
|
|
if (BitOutputs.Count == 0) {
|
|
Log.W("No. of Bit Output MMap is zero");
|
|
}
|
|
if (WordInputs.Count == 0) {
|
|
Log.W("No. of Word Input MMap is zero");
|
|
}
|
|
if (WordOutputs.Count == 0) {
|
|
Log.W("No. of Word Output MMap is zero");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
// 추후에 폴더 생성하고 옮기기
|
|
public enum eAxis
|
|
{
|
|
None = -1,
|
|
|
|
//====================================
|
|
// Motor Example
|
|
Stage_X = 0,
|
|
Head_Y,
|
|
Head_Z,
|
|
Stage_T,
|
|
|
|
AXIS_MAX_NUM
|
|
};
|
|
}
|
|
}
|
|
#endregion |