Files
StandardSence/StandardScene.Core/Charge/CommunicationMonitorForm.cs
T
zhaowei.huang 54cda958db refactor: 插件窗体由 static 单例状态改为实例字段(对齐 TextViewer)
将 10 个 CycleGUI 窗体/管理器从进程级 static 单例状态改为实例字段 + 实例方法,
消除本地端/Web 端共享同一可变状态的隐患(对齐 TextViewer 正例)。

每个类保留一个 private static _instance 单实例持有者;静态入口 Open()/OpenViewer()
与实例 Show() 均转发到该实例,调用方零改动、单实例“置前”行为不变。

涉及:ChargeStationManagementForm、ChargeStrategyConfigForm、CommunicationMonitorForm、
AlarmConfigManagementForm、ButtonBoxManager、DoorManager、DoorMonitor、
LoopViewer、DeliveryViewer、TrafficInterlockViewer。

构建:dotnet build StandardScene.sln --no-incremental → 0 错误,30 警告(与基线一致,无新增)。
2026-06-26 15:31:02 +08:00

435 lines
16 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;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using CycleGUI;
using StandardScene.Utils;
namespace StandardScene.Charge
{
/// <summary>
/// 通讯监控面板(CycleGUI 版,替代原 WinForms 窗体)。
/// <list type="bullet">
/// <item>单实例:再次打开则把已有面板置前。</item>
/// <item>订阅 <see cref="CommunicationMessageService.MessageAdded"/>,批量刷新 UI500ms 节流),最多显示 100 行。</item>
/// <item>支持 IP 筛选、暂停/继续、清空(二次确认)、选中报文解析详情。</item>
/// </list>
/// 保留可实例化 + <see cref="Show"/> 以兼容既有调用。
/// </summary>
public class CommunicationMonitorForm
{
private const int MaxDisplayRows = 100;
private const int UiBatchSize = 20;
private const int StatsRefreshMs = 500;
private const string TableId = "comm-monitor-msgs";
private readonly Color SendRowColor = Color.FromArgb(232, 245, 233);
private readonly Color ReceiveRowColor = Color.FromArgb(227, 242, 253);
private readonly Color SelectedRowColor = Color.FromArgb(255, 249, 196);
private readonly CommunicationMessageService MessageService = CommunicationMessageService.Instance;
private Panel _panel;
private bool _subscribed;
private bool _paused;
private int _selectedIpIndex;
private string[] _ipOptions = { "全部" };
private int _selectedRowIndex = -1;
private string _parsedText = "";
private string _statsText = "";
private List<CommunicationMessage> _displayMessages = new List<CommunicationMessage>();
private readonly Queue<CommunicationMessage> PendingMessages = new Queue<CommunicationMessage>();
private readonly object PendingLock = new object();
private DateTime _lastStatsRefresh = DateTime.MinValue;
private bool _pendingStatsRefresh;
/// <summary>打开(或置前)通讯监控面板。兼容原 <c>new CommunicationMonitorForm().Show()</c> 调用方式。</summary>
public void Show() => Open();
/// <summary>打开(或置前)通讯监控面板。</summary>
public static void Open() => (_instance ??= new CommunicationMonitorForm()).OpenCore();
private static CommunicationMonitorForm _instance;
private void OpenCore()
{
if (_panel != null)
{
try
{
_panel.BringToFront();
return;
}
catch
{
_panel = null;
}
}
_paused = false;
_selectedRowIndex = -1;
_parsedText = "";
RefreshIpFilter();
ReloadFromService();
var panel = GUI.DeclarePanel()
.ShowTitle("通讯监控")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(1400, 800)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() =>
{
Unsubscribe();
_panel = null;
});
Subscribe();
panel.Define(pb =>
{
if (pb.Closing())
{
Unsubscribe();
panel.Exit();
_panel = null;
return;
}
FlushPendingBatch();
if (pb.DropdownBox("IP筛选", _ipOptions, ref _selectedIpIndex))
ReloadFromService();
pb.SameLine(16);
if (pb.Button(_paused ? "继续" : "暂停", distinct: "comm-pause"))
_paused = !_paused;
pb.SameLine(8);
if (pb.Button("刷新", distinct: "comm-refresh"))
{
RefreshIpFilter();
ReloadFromService();
RequestStatisticsRefresh();
}
pb.SameLine(8);
if (pb.Button("清空", distinct: "comm-clear"))
{
CycleUiHelper.ConfirmThen("确定要清空所有报文记录吗?", () =>
{
MessageService.Clear();
lock (PendingLock)
PendingMessages.Clear();
RefreshIpFilter();
_displayMessages.Clear();
_selectedRowIndex = -1;
_parsedText = "";
RequestStatisticsRefresh();
});
}
pb.SameLine(8);
if (pb.Button("关闭", distinct: "comm-close"))
{
Unsubscribe();
panel.Exit();
_panel = null;
return;
}
MaybeRefreshStatistics();
pb.Label(_statsText);
pb.Table(TableId,
new[] { "时间", "方向", "IP地址", "端口", "长度", "原始数据", "站点", "类型", "操作" },
_displayMessages.Count, (row, i) =>
{
var msg = _displayMessages[i];
row.SetColor(_selectedRowIndex == i
? SelectedRowColor
: msg.Direction == MessageDirection.Send ? SendRowColor : ReceiveRowColor);
row.Label($"{msg.Timestamp:HH:mm:ss.fff}");
row.Label(msg.Direction == MessageDirection.Send ? "发送" : "接收");
row.Label(msg.IpAddress ?? "");
row.Label($"{msg.Port}");
row.Label($"{msg.Length}");
row.Label(TruncateRawData(msg.RawData));
row.Label(string.IsNullOrEmpty(msg.StationId) ? "-" : msg.StationId);
row.Label(msg.Type ?? "");
if (row.ButtonGroup(new[] { "解析" }, new[] { "解析该报文" }) == 0)
{
_selectedRowIndex = i;
_parsedText = BuildParsedText(msg);
}
}, height: 20, enableSearch: true);
pb.SeparatorText("报文解析");
pb.SelectableText(null, _parsedText ?? "", copyButton: true);
pb.Panel.Repaint(repaintTimeMs: 500);
});
}
private void Subscribe()
{
if (_subscribed)
return;
MessageService.MessageAdded += OnMessageAdded;
_subscribed = true;
}
private void Unsubscribe()
{
if (!_subscribed)
return;
MessageService.MessageAdded -= OnMessageAdded;
_subscribed = false;
lock (PendingLock)
PendingMessages.Clear();
}
private void OnMessageAdded(object sender, CommunicationMessage message)
{
if (!_subscribed || message == null)
return;
lock (PendingLock)
PendingMessages.Enqueue(message);
_panel?.Repaint();
}
/// <summary>定时批量刷新 UI,避免每条报文都抢占渲染线程。</summary>
private void FlushPendingBatch()
{
if (_paused)
return;
List<CommunicationMessage> batch = null;
lock (PendingLock)
{
if (PendingMessages.Count == 0)
return;
int count = Math.Min(UiBatchSize, PendingMessages.Count);
batch = new List<CommunicationMessage>(count);
for (int i = 0; i < count; i++)
batch.Add(PendingMessages.Dequeue());
}
if (batch == null || batch.Count == 0)
return;
var filter = SelectedIpFilter();
bool displayChanged = false;
foreach (var message in batch)
{
EnsureIpInFilter(message.IpAddress);
if (string.IsNullOrEmpty(filter) || filter == "全部" || filter == message.IpAddress)
{
InsertMessageAtTop(message);
displayChanged = true;
}
}
if (displayChanged)
RequestStatisticsRefresh();
}
private void InsertMessageAtTop(CommunicationMessage msg)
{
_displayMessages.Insert(0, msg);
while (_displayMessages.Count > MaxDisplayRows)
_displayMessages.RemoveAt(_displayMessages.Count - 1);
if (_selectedRowIndex >= 0)
_selectedRowIndex++;
}
private void ReloadFromService()
{
try
{
var filter = SelectedIpFilter();
var messages = string.IsNullOrEmpty(filter) || filter == "全部"
? MessageService.GetAllMessages()
: MessageService.GetMessagesByIp(filter);
_displayMessages = messages.Take(MaxDisplayRows).ToList();
_selectedRowIndex = -1;
_parsedText = "";
_pendingStatsRefresh = false;
UpdateStatistics(_displayMessages.Count);
}
catch (Exception ex)
{
_statsText = $"加载报文失败: {ex.Message}";
}
}
private void RefreshIpFilter()
{
try
{
var selectedIp = SelectedIpFilter();
var options = new List<string> { "全部" };
var ipAddresses = MessageService.GetUniqueIpAddresses();
if (ipAddresses != null)
{
foreach (var ip in ipAddresses)
{
if (!string.IsNullOrEmpty(ip))
options.Add(ip);
}
}
_ipOptions = options.ToArray();
if (!string.IsNullOrEmpty(selectedIp))
{
var idx = Array.IndexOf(_ipOptions, selectedIp);
_selectedIpIndex = idx >= 0 ? idx : 0;
}
else
{
_selectedIpIndex = 0;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"刷新IP筛选失败: {ex.Message}");
}
}
private void EnsureIpInFilter(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
return;
if (_ipOptions.Contains(ipAddress))
return;
var list = _ipOptions.ToList();
list.Add(ipAddress);
_ipOptions = list.ToArray();
}
private string SelectedIpFilter()
{
if (_ipOptions == null || _ipOptions.Length == 0)
return "全部";
if (_selectedIpIndex < 0 || _selectedIpIndex >= _ipOptions.Length)
return "全部";
return _ipOptions[_selectedIpIndex];
}
private void RequestStatisticsRefresh()
{
_pendingStatsRefresh = true;
}
/// <summary>统计信息低频刷新(500ms)。</summary>
private void MaybeRefreshStatistics()
{
if (!_pendingStatsRefresh)
return;
if (DateTime.Now - _lastStatsRefresh < TimeSpan.FromMilliseconds(StatsRefreshMs))
return;
_pendingStatsRefresh = false;
_lastStatsRefresh = DateTime.Now;
UpdateStatistics(_displayMessages.Count);
}
private void UpdateStatistics(int displayCount)
{
try
{
var allMessages = MessageService.GetAllMessages();
if (allMessages == null)
{
_statsText = "统计信息加载失败";
return;
}
var sendCount = allMessages.Count(m => m.Direction == MessageDirection.Send);
var receiveCount = allMessages.Count(m => m.Direction == MessageDirection.Receive);
_statsText = $"显示: {displayCount} | 总数: {allMessages.Count} | 发送: {sendCount} | 接收: {receiveCount}";
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新统计信息失败: {ex.Message}");
_statsText = "统计信息加载失败";
}
}
private string TruncateRawData(string rawData, int maxLen = 48)
{
if (string.IsNullOrEmpty(rawData))
return "";
return rawData.Length <= maxLen ? rawData : rawData.Substring(0, maxLen) + "…";
}
private string BuildParsedText(CommunicationMessage message)
{
if (message == null)
return "";
try
{
var parsed = new StringBuilder();
parsed.AppendLine("=== 报文解析 ===");
parsed.AppendLine($"时间: {message.Timestamp:yyyy-MM-dd HH:mm:ss.fff}");
parsed.AppendLine($"方向: {(message.Direction == MessageDirection.Send ? "发送" : "接收")}");
parsed.AppendLine($"地址: {message.IpAddress}:{message.Port}");
parsed.AppendLine($"站点: {message.StationId ?? "未关联"}");
parsed.AppendLine($"长度: {message.Length} 字节");
parsed.AppendLine();
parsed.AppendLine("=== 原始数据 (HEX) ===");
parsed.AppendLine(message.RawData);
parsed.AppendLine();
parsed.AppendLine("=== 数据解析 ===");
if (message.Direction == MessageDirection.Send)
{
var sendData = MessageService.ParseSendRawData(message.RawData, message.Type);
parsed.AppendLine("示例解析:");
parsed.AppendLine($"充电指令:{sendData.ChargeCommand}");
parsed.AppendLine($"发送电压:{sendData.SetVoltage}");
parsed.AppendLine($"发送电流:{sendData.SetCurrent}");
parsed.AppendLine($"车辆ID{sendData.CurrentVehicleId}");
parsed.AppendLine($"车辆电量:{sendData.BatteryLevel}");
parsed.AppendLine($"车辆电压:{sendData.CarVoltage}");
parsed.AppendLine($"车辆电流:{sendData.CarCurrent}");
}
else
{
var recData = MessageService.ParseReceiveRawData(message.RawData, message.Type);
string mechanismStatus = (int)recData.MechanismStatus == 1 ? "伸出"
: (int)recData.MechanismStatus == 2 ? "缩回"
: (int)recData.MechanismStatus == 3 ? "运动中"
: recData.MechanismStatus.ToString();
parsed.AppendLine("示例解析:");
parsed.AppendLine($"机构状态:{mechanismStatus}");
parsed.AppendLine($"实时电压:{recData.RealTimeVoltage}");
parsed.AppendLine($"实时电流:{recData.RealTimeCurrent}");
parsed.AppendLine($"充电量: {recData.BatteryAH}");
parsed.AppendLine($"是否报警:{recData.HasAlarm}");
parsed.AppendLine($"充电状态:{recData.Status}");
}
return parsed.ToString();
}
catch (Exception ex)
{
return $"解析失败: {ex.Message}";
}
}
}
}