将 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 警告(与基线一致,无新增)。
435 lines
16 KiB
C#
435 lines
16 KiB
C#
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"/>,批量刷新 UI(500ms 节流),最多显示 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}";
|
||
}
|
||
}
|
||
}
|
||
}
|