Files
ETTerms/src/ETTerms.PduCore/PduController.cs
T
etwenandClaude Fable 5 572e6589ec feat: v0.4.0 performance & stability overhaul
Terminal:
- cache font variants + single reusable brush in render hot path;
  resolve cell colors once per cell; dispose GDI resources
- scrollback: List+RemoveRange -> O(1) ring buffer
- follow new output only when already at bottom (no more yank-to-bottom
  while reading history)
- alt-screen mouse wheel -> arrow keys (vim/htop scroll)
- answer DSR (ESC[5n/6n) and DA (ESC[c) queries so TUIs no longer hang
- remove dead code in OnKeyPress

Encoding correctness (garbled CJK across chunk boundaries):
- stateful UTF-8 Decoder in TTLInterpreter.OnData, SerialBridgeServer
  rx forwarding, and SessionLogger.Write

Sessions:
- ShellChannel: free proc-thread attribute list, close hProcess,
  notify '[ETTerms] shell process exited' in the tab
- SshChannel: surface ErrorOccurred / ShellStream.Closed in the tab;
  Write no longer throws into the UI thread on a dead connection

PDU:
- new shared ETTerms.PduCore project replaces the two drifted copies of
  PduController (GUI + PduMcp); logging via injected delegates
- batched SNMP GET (GetAllPortsStatus): 12-port poll is 3 UDP
  round-trips instead of 36 (StatusView polling + pdu_status tool)

Scripting:
- cap TTL receive buffer at 1MB; skip re-scan in wait when buffer
  length unchanged
- merge ScriptRunner.RunAsync/RunGroupAsync; new TtlScript helper
  dedups script picking + group-command checks (3 copies -> 1)

Misc:
- ConnectionStore: parse LastUsedUtc with InvariantCulture/RoundtripKind
- version 0.4.0; About changelog; CLAUDE.md notes (intentional group
  barrier behavior, SSH.NET reflection resize caveat)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 09:17:19 +08:00

125 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using SnmpSharpNet;
namespace ETTerms.PduCore;
/// <summary>
/// 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 操作。
/// </summary>
public sealed class PduController : IDisposable
{
private readonly string _ip;
private readonly Action<string>? _logInfo;
private readonly Action<string>? _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<string>? logInfo = null, Action<string>? logWarn = null)
{
_ip = ip;
_logInfo = logInfo;
_logWarn = logWarn;
}
/// <summary>讀 PDU 型號/名稱 OID;非空且含 "PDU" 代表連得上。</summary>
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 ?? "<null>"}'");
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));
/// <summary>true = on, false = off, null = unknown/unreachable.</summary>
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;
/// <summary>
/// 一次讀回所有插座的狀態 / 電流(mA) / 功率(W)。
/// 以 3 個批次 SNMP GET(每個 PDU 含 <paramref name="portCount"/> 個 varbind)取代
/// 逐 port 逐 OID 的 3×N 個請求 —— 12 port 從 36 個 UDP 來回縮成 3 個,
/// 逾時時的最壞情況也從 36×Timeout 縮到 3×Timeout。
/// </summary>
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];
}
/// <summary>一個 SNMP GET 帶多個 OID(varbind),回傳同序的值;失敗整批回 null。</summary>
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() { }
}