using System.Net; using SnmpSharpNet; namespace ETTerms.PduCore; /// /// PDU controller for iPoMan II/III models via SNMP。 /// GUI 與 ETTerms.PduMcp 共用的唯一實作(v0.4.0 起取代兩份複製的版本)。 /// 診斷輸出走建構子注入的 log 委派:GUI 給 AppLogger、MCP server 給 stderr /// (stdio MCP 的 stdout 專供 JSON-RPC,不可污染)。 /// SNMP 走 UDP、非獨佔,多個行程可同時對同一台 PDU 操作。 /// public sealed class PduController : IDisposable { private readonly string _ip; private readonly Action? _logInfo; private readonly Action? _logWarn; private const string Community = "private"; private const int SnmpPort = 161; private const int Timeout = 3000; public string Ip => _ip; public PduController(string ip, Action? logInfo = null, Action? logWarn = null) { _ip = ip; _logInfo = logInfo; _logWarn = logWarn; } /// 讀 PDU 型號/名稱 OID;非空且含 "PDU" 代表連得上。 public string? GetModelName() => SnmpGet(".1.3.6.1.4.1.2468.1.4.2.1.1.4"); public bool CheckConnection() { var name = GetModelName(); _logInfo?.Invoke($"[PDU] CheckConnection {_ip}: name='{name ?? ""}'"); return !string.IsNullOrEmpty(name) && name.Contains("PDU"); } public bool SetPortOn(int port) => SnmpSet(PortControlOid(port), new Integer32(3)); public bool SetPortOff(int port) => SnmpSet(PortControlOid(port), new Integer32(4)); /// true = on, false = off, null = unknown/unreachable. public bool? GetPortState(int port) => ParseState(SnmpGet(PortStateOid(port))); public int? GetPortCurrent(int port) => int.TryParse(SnmpGet(PortCurrentOid(port)), out int v) ? v : null; public double? GetPortPowerWatts(int port) => int.TryParse(SnmpGet(PortPowerOid(port)), out int v) ? v / 10.0 : null; /// /// 一次讀回所有插座的狀態 / 電流(mA) / 功率(W)。 /// 以 3 個批次 SNMP GET(每個 PDU 含 個 varbind)取代 /// 逐 port 逐 OID 的 3×N 個請求 —— 12 port 從 36 個 UDP 來回縮成 3 個, /// 逾時時的最壞情況也從 36×Timeout 縮到 3×Timeout。 /// public (bool? State, int? CurrentMilliAmps, double? PowerWatts)[] GetAllPortsStatus(int portCount) { var ports = Enumerable.Range(1, portCount).ToArray(); var states = SnmpGetMany(ports.Select(PortStateOid).ToArray()); var currents = SnmpGetMany(ports.Select(PortCurrentOid).ToArray()); var powers = SnmpGetMany(ports.Select(PortPowerOid).ToArray()); var result = new (bool?, int?, double?)[portCount]; for (int i = 0; i < portCount; i++) { result[i] = ( ParseState(states[i]), int.TryParse(currents[i], out int c) ? c : null, int.TryParse(powers[i], out int p) ? p / 10.0 : null); } return result; } private static bool? ParseState(string? r) => r == "3" ? true : (r == "2" || r == "4") ? false : null; private static string PortControlOid(int port) => $".1.3.6.1.4.1.2468.1.4.2.1.3.2.4.1.2.{port}"; private static string PortStateOid(int port) => $".1.3.6.1.4.1.2468.1.4.2.1.3.2.3.1.2.{port}"; private static string PortCurrentOid(int port) => $".1.3.6.1.4.1.2468.1.4.2.1.3.2.3.1.3.{port}"; private static string PortPowerOid(int port) => $".1.3.6.1.4.1.2468.1.4.2.1.3.2.3.1.5.{port}"; private bool SnmpSet(string oid, AsnType value) { try { var param = new AgentParameters(new OctetString(Community)) { Version = SnmpVersion.Ver1 }; using var target = new UdpTarget((IPAddress)new IpAddress(_ip), SnmpPort, Timeout, 1); var pdu = new SnmpSharpNet.Pdu(PduType.Set); pdu.VbList.Add(new Oid(oid), value); var result = (SnmpV1Packet)target.Request(pdu, param); return result?.Pdu.ErrorStatus == 0; } catch (Exception ex) { _logWarn?.Invoke($"[PDU] SNMP SET {oid} exception: {ex.Message}"); return false; } } private string? SnmpGet(string oid) { var r = SnmpGetMany(new[] { oid }); return r[0]; } /// 一個 SNMP GET 帶多個 OID(varbind),回傳同序的值;失敗整批回 null。 private string?[] SnmpGetMany(string[] oids) { var result = new string?[oids.Length]; try { var param = new AgentParameters(new OctetString(Community)) { Version = SnmpVersion.Ver1 }; using var target = new UdpTarget((IPAddress)new IpAddress(_ip), SnmpPort, Timeout, 1); var pdu = new SnmpSharpNet.Pdu(PduType.Get); foreach (var oid in oids) pdu.VbList.Add(new Oid(oid)); var resp = (SnmpV1Packet)target.Request(pdu, param); if (resp == null) { _logWarn?.Invoke($"[PDU] SNMP GET ({oids.Length} oids): no response (timeout)"); return result; } if (resp.Pdu.ErrorStatus != 0) { _logWarn?.Invoke($"[PDU] SNMP GET ({oids.Length} oids): ErrorStatus={resp.Pdu.ErrorStatus}"); return result; } // SNMP GET 回應的 varbind 順序與請求一致,直接依 index 對回去。 int n = Math.Min(oids.Length, resp.Pdu.VbList.Count); for (int i = 0; i < n; i++) result[i] = resp.Pdu.VbList[i].Value.ToString(); } catch (Exception ex) { _logWarn?.Invoke($"[PDU] SNMP GET ({oids.Length} oids) exception: {ex.Message}"); } return result; } public void Dispose() { } }