using LFP_Manager.DataStructure; using LFP_Manager.Function; using LFP_Manager.Utils; using SnmpSharpNet; using System; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; namespace LFP_Manager.Threads { // 콜백 델리게이트 public delegate void SetResult(string result, bool error); public delegate void DataUpdate(object sender, ref CsDeviceData.DeviceModuleData rData); class CsSnmpThread { #region VARIABLES (상태) private CsDeviceData.DeviceModuleData ModuleData; private CommConfig Config; // 스레드/토큰 기반 (Abort 제거) private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private Task _snmpTask; // 상태 보호용 락 private readonly object _stateLock = new object(); // 실행 상태 private bool snmpThreadHold = true; // true = 정지, false = 동작 private string targetIP = ""; private bool AutoSnmpTx = true; // OID 순회 private int OidIndex = 0; private int OidMax = 0; private string[] OidList = Array.Empty(); // 통신 타임아웃 카운터 private int CommTimeOut = 0; // 결과 메시지 (레거시 호환) private bool snmpDataRecv = false; private string snmpResult = ""; // 대기중 SET 명령 (race 방지 위해 락으로 보호) private int _pendingModeInt = 0; private int _pendingModeStr = 0; private UInt32 _pendingValue = 0; private string _pendingString = null; private long _lastUpdateTicks = 0; private const int UpdateMinIntervalMs = 120; // 이벤트 public event DataUpdate OnUpdate = null; public event SetResult OnSetResult = null; #endregion #region CONSTRUCTORS public CsSnmpThread(CommConfig aConfig, ref CsDeviceData.DeviceModuleData mData) { Config = aConfig; ModuleData = mData; OidList = CsSnmpConstData.SnmpOidInfo ?? Array.Empty(); OidMax = OidList.Length; OidIndex = 0; _snmpTask = Task.Factory.StartNew( () => snmpThreadFuncA(_cts.Token), _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } #endregion #region PUBLIC API /// /// 안전 종료 (Abort 제거) /// public void disposeThread() { Stop(); _cts.Cancel(); try { _snmpTask?.Wait(1500); } catch { /* ignore */ } } /// /// 스레드 실행 시작/재시작 /// public void Start(string IP, bool autoTx) { lock (_stateLock) { targetIP = IP ?? ""; AutoSnmpTx = autoTx; snmpThreadHold = false; } } /// /// 스레드 일시정지 /// public void Stop() { lock (_stateLock) { snmpThreadHold = true; } } /// /// true면 정지 상태 /// public bool GetStatus() { lock (_stateLock) { return snmpThreadHold; } } /// /// 레거시: 마지막 결과 문자열 반환 후 플래그 리셋 /// public string GetResult() { lock (_stateLock) { if (snmpDataRecv) { snmpDataRecv = false; return snmpResult; } } return ""; } public void SetAutoTx(bool autoTx) { lock (_stateLock) { AutoSnmpTx = autoTx; } } public string GetSnmpOidInfo(int index) { if (index < 0 || index >= OidMax) return null; return OidList[index]; } /// /// 정수형 SET 명령 예약 /// public void SetDataBySnmp(int mode, UInt32 value) { lock (_stateLock) { _pendingModeInt = mode; _pendingValue = value; } } /// /// 문자열 SET 명령 예약 /// public void SetDataBySnmp(int mode, string value) { lock (_stateLock) { _pendingModeStr = mode; _pendingString = value; } } #endregion #region COMM. THREAD private void snmpThreadFuncA(CancellationToken ct) { var wait = new ManualResetEventSlim(false); int backoffMs = 50; // 정상 루프 기본 지연 while (!ct.IsCancellationRequested) { bool hold; string ip; bool autoTx; lock (_stateLock) { hold = snmpThreadHold; ip = targetIP; autoTx = AutoSnmpTx; } if (hold || string.IsNullOrWhiteSpace(ip)) { // 대기 wait.Wait(TimeSpan.FromMilliseconds(500), ct); continue; } // Target 생성 및 재사용 범위 if (!TryParseIp(ip, out var addr)) { OnSetResultSafe($"Invalid IP address: {ip}", true); wait.Wait(TimeSpan.FromMilliseconds(1000), ct); continue; } var param = CreateAgentParams(); // 커뮤니티/버전 통일 using (var target = new UdpTarget(addr, 161, 1000, 1)) { var pdu = new Pdu(PduType.Get); while (!ct.IsCancellationRequested) { lock (_stateLock) { if (snmpThreadHold) break; } // 1) 대기중 SET 우선 처리 int modeInt, modeStr; UInt32 setVal; string setStr; lock (_stateLock) { modeInt = _pendingModeInt; modeStr = _pendingModeStr; setVal = _pendingValue; setStr = _pendingString; _pendingModeInt = 0; _pendingModeStr = 0; _pendingString = null; } if (modeInt != 0 || modeStr != 0) { int mode = (modeInt != 0) ? modeInt : modeStr; bool err = SetCmdbySnmp(target, mode, setVal, setStr); backoffMs = err ? Math.Min(backoffMs * 2, 2000) : 100; // 에러 시 백오프 continue; } // 2) GET 폴링 autoTx = AutoSnmpTx; if (!autoTx) { wait.Wait(TimeSpan.FromMilliseconds(200), ct); continue; } if (OidMax == 0) { OnSetResultSafe("SNMP OID list is empty.", true); wait.Wait(TimeSpan.FromMilliseconds(1000), ct); continue; } int idx = NextOidIndex(); string oid = GetSnmpOidInfo(idx); if (string.IsNullOrEmpty(oid)) { Thread.Sleep(100); continue; } pdu.VbList.Clear(); pdu.VbList.Add(oid); try { target.Timeout = 500; OnSetResultSafe(csLog.trx_msg_print( $"SEND {target.Address}: OID [{pdu.VbList[0].Oid}]", 0), false); var packet = (SnmpV1Packet)target.Request(pdu, param); if (packet == null) { HandleNoResponse(ref backoffMs); continue; } if (packet.Pdu.ErrorStatus != 0) { string oidTxt = (packet.Pdu.VbList.Count > 0) ? packet.Pdu.VbList[0].Oid.ToString() : "(no vb)"; OnSetResultSafe(csLog.trx_msg_print( $"Error in SNMP reply. Error {packet.Pdu.ErrorStatus} index {packet.Pdu.ErrorIndex} - {oidTxt}", 1), true); HandleCommFailStep(ref backoffMs); continue; } if (packet.Pdu.VbList.Count == 0) { OnSetResultSafe("SNMP reply has empty VbList.", true); HandleCommFailStep(ref backoffMs); continue; } var vb = packet.Pdu.VbList[0]; OnSetResultSafe(csLog.trx_msg_print( $"RECV {target.Address}: OID [{vb.Oid}]: [{vb.Value}]", 1), false); // 특정 트리만 반영 (1.3.6.1.2.1.15.*) → 필요에 맞게 수정하세요. if (TryParseShelfOid(vb, out uint leaf)) { if (vb.Value.Type == AsnType.OCTETSTRING) { try { csMakeDataFunction.SetSnmpData((int)leaf, vb.Value.ToString(), ref ModuleData); } catch (Exception ex) { OnSetResultSafe($"{NowPrefix()} {ex.Message}", true); } } CalcAvgTemperatureModule(ref ModuleData, (uint)ModuleData.tempQty); CheckShelfCommFail(ref ModuleData); var nowTicks = Environment.TickCount; if (nowTicks - _lastUpdateTicks >= UpdateMinIntervalMs) { OnUpdate?.Invoke(this, ref ModuleData); _lastUpdateTicks = nowTicks; } } CommTimeOut = 0; ModuleData.CommFail = false; backoffMs = 50; Thread.Sleep(backoffMs); } catch (Exception ex) { string curOid = pdu.VbList.Count > 0 ? pdu.VbList[0].Oid.ToString() : "(no oid)"; OnSetResultSafe(csLog.trx_msg_print( $"Exception in SNMP reply. OID {curOid}: {ex.Message}", 1), true); HandleCommFailStep(ref backoffMs); } } // inner while } // using target Thread.Sleep(500); } // outer while } #endregion #region SNMP HELPERS private AgentParameters CreateAgentParams() { // 커뮤니티/버전 통일: v1 + "public" (필요시 Ver2로 변경) // Ver2가 가능하면: new AgentParameters(SnmpVersion.Ver2, new OctetString("public")); return new AgentParameters(SnmpVersion.Ver1, new OctetString("admin")); } private bool SetCmdbySnmp(UdpTarget sTarget, int mode, UInt32 value, string svalue) { bool resultError = false; var SetPdu = new Pdu(PduType.Set); SetPdu.VbList.Clear(); // 모드별 OID 매핑 (기존 코드 유지) switch (mode) { case 1: // Reset Command SetPdu.VbList.Add(new Oid(".1.3.6.1.4.1.33.13.1.0"), new Integer32(Convert.ToInt32(value))); break; case 2: // Module Quantity Set SetPdu.VbList.Add(new Oid(".1.3.6.1.4.1.33.2.4.0"), new Integer32(Convert.ToInt32(value))); break; case 3: // Alarm Output Mode SetPdu.VbList.Add(new Oid(".1.3.6.1.4.1.33.2.5.0"), new Integer32(Convert.ToInt32(value))); break; case 4: // Manufacture Date SetPdu.VbList.Add(new Oid(".1.3.6.1.4.1.33.2.6.0"), new Integer32(Convert.ToInt32(value))); break; case 5: // BMS Serial Number SetPdu.VbList.Add(new Oid(".1.3.6.1.4.1.33.2.7.0"), new OctetString(svalue ?? "")); break; default: OnSetResultSafe($"Unsupported SET mode: {mode}", true); return true; } try { var aparam = CreateAgentParams(); var result = sTarget.Request(SetPdu, aparam) as SnmpV1Packet; if (result == null) { OnSetResultSafe($"{NowPrefix()} No response received from SNMP agent.", true); return true; } if (result.Pdu.ErrorStatus != 0) { string oidTxt = (result.Pdu.VbList.Count > 0) ? result.Pdu.VbList[0].Oid.ToString() : "(no vb)"; OnSetResultSafe($"Error in SNMP SET. Error {result.Pdu.ErrorStatus} index {result.Pdu.ErrorIndex} - {oidTxt}", true); return true; } var vbOk = result.Pdu.VbList.Count > 0 ? result.Pdu.VbList[0] : null; var val = vbOk != null ? vbOk.Value.ToString() : "(no vb)"; OnSetResultSafe($"SNMP SET OK. OID [{vbOk?.Oid}] -> [{val}]", false); return false; } catch (Exception ex) { OnSetResultSafe($"{NowPrefix()} Exception: {ex.Message}", true); resultError = true; } return resultError; } private static bool TryParseIp(string ip, out IPAddress addr) { if (IPAddress.TryParse(ip, out addr)) return true; try { var host = Dns.GetHostEntry(ip); addr = host?.AddressList?.FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); return addr != null; } catch { addr = null; return false; } } private int NextOidIndex() { // 간단한 라운드로빈 int next = Interlocked.Increment(ref OidIndex); if (OidMax == 0) return 0; next %= OidMax; if (next < 0) next += OidMax; return next; } private static bool TryParseShelfOid(Vb vb, out uint leaf) { leaf = 0; if (vb?.Oid == null) return false; // OID를 문자열로 받아 숫자 배열로 파싱 // 예: ".1.3.6.1.2.1.15.7.0" 또는 "1.3.6.1.2.1.15.7.0" string oidStr = vb.Oid.ToString(); if (string.IsNullOrWhiteSpace(oidStr)) return false; // 선행 '.' 제거 후 분해 var parts = oidStr.Trim('.').Split('.'); if (parts.Length < 8) return false; // 숫자 배열로 변환 (변환 실패 시 false) uint[] ids = new uint[parts.Length]; for (int i = 0; i < parts.Length; i++) { if (!uint.TryParse(parts[i], out ids[i])) return false; } // 예시 트리: 1.3.6.1.2.1.15.x.* if (ids[0] == 1 && ids[1] == 3 && ids[2] == 6 && ids[3] == 1 && ids[4] == 2 && ids[5] == 1 && ids[6] == 15) { leaf = ids[7]; return true; } return false; } private void HandleNoResponse(ref int backoffMs) { lock (_stateLock) { if (!snmpDataRecv) { snmpResult = $"{NowPrefix()} No response received from SNMP agent."; snmpDataRecv = true; OnSetResult?.Invoke(snmpResult, true); } } CommTimeOut++; if (CommTimeOut >= 10) { ModuleData.Reset(); ModuleData.CommFail = true; var nowTicks = Environment.TickCount; if (nowTicks - _lastUpdateTicks >= UpdateMinIntervalMs) { OnUpdate?.Invoke(this, ref ModuleData); _lastUpdateTicks = nowTicks; } CommTimeOut = 10; } Thread.Sleep(backoffMs = Math.Min(backoffMs * 2, 2000)); } private void HandleCommFailStep(ref int backoffMs) { CommTimeOut++; if (CommTimeOut >= 10) { ModuleData.Reset(); ModuleData.CommFail = true; var nowTicks = Environment.TickCount; if (nowTicks - _lastUpdateTicks >= UpdateMinIntervalMs) { OnUpdate?.Invoke(this, ref ModuleData); _lastUpdateTicks = nowTicks; } CommTimeOut = 10; } Thread.Sleep(backoffMs = Math.Min(backoffMs * 2, 2000)); } private void OnSetResultSafe(string msg, bool error) { try { OnSetResult?.Invoke(msg, error); } catch { /* swallow */ } } private static string NowPrefix() => DateTime.Now.ToString("[yyyy-MM-dd HH:mm:ss] "); #endregion #region DOMAIN HELPERS (기존 함수 보강/유지) private void CheckShelfCommFail(ref CsDeviceData.DeviceModuleData mData) { // 상태코드 0x0003 → ShelfCommFail=true (기존 가정 유지) if (mData.StatusData.batteryStatus == 0x0003) { mData.ShelfCommFail = true; } else { mData.ShelfCommFail = false; } } // 기존 코드 유지 (평균/최대/최소 계산) static void CalcAvgTemperatureModule(ref CsDeviceData.DeviceModuleData mData, UInt32 tSize) { short i, j; int sum; TMinMax min, max; min = new TMinMax(); max = new TMinMax(); sum = 0; max.value = 0; max.num = 0; min.value = 0; min.num = 0; j = 0; for (i = 0; i < tSize; i++) { if (j == 0) { max.value = mData.ValueData.CellTemperature[i]; max.num = i; min.value = mData.ValueData.CellTemperature[i]; min.num = i; } if (max.value < mData.ValueData.CellTemperature[i]) { max.value = mData.ValueData.CellTemperature[i]; max.num = i; } if (min.value > mData.ValueData.CellTemperature[i]) { min.value = mData.ValueData.CellTemperature[i]; min.num = i; } sum += mData.ValueData.CellTemperature[i]; j++; } if (j == 0) { mData.AvgData.avgTemp = 0; } else { mData.AvgData.avgTemp = (short)(sum / j); } mData.AvgData.maxTemp = max.value; mData.AvgData.maxTempNum = max.num; mData.AvgData.minTemp = min.value; mData.AvgData.minTempNum = min.num; } #endregion } }