Files
StandardSence/StandardScene.Core/Charge/CommunicationMonitorForm.cs
T
zhaowei.huang a0dc1e6cd0 refactor: 插件 UI 从 WinForms 迁移到 CycleGUI,并修复代码质量问题
将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。

同时修复代码审核中的问题:
- 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式)
- CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告
- DummyCar 移除已废弃的 rightClickAction()/SetPosition()
- CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch
- 重命名名不副实的 Mstsc()(现为打开网页)
- 统一弃元命名为 _
- TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引
- csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props

构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。
注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
2026-06-26 15:00:53 +08:00

431 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 static readonly Color SendRowColor = Color.FromArgb(232, 245, 233);
private static readonly Color ReceiveRowColor = Color.FromArgb(227, 242, 253);
private static readonly Color SelectedRowColor = Color.FromArgb(255, 249, 196);
private static readonly CommunicationMessageService MessageService = CommunicationMessageService.Instance;
private static Panel _panel;
private static bool _subscribed;
private static bool _paused;
private static int _selectedIpIndex;
private static string[] _ipOptions = { "全部" };
private static int _selectedRowIndex = -1;
private static string _parsedText = "";
private static string _statsText = "";
private static List<CommunicationMessage> _displayMessages = new List<CommunicationMessage>();
private static readonly Queue<CommunicationMessage> PendingMessages = new Queue<CommunicationMessage>();
private static readonly object PendingLock = new object();
private static DateTime _lastStatsRefresh = DateTime.MinValue;
private static bool _pendingStatsRefresh;
/// <summary>打开(或置前)通讯监控面板。兼容原 <c>new CommunicationMonitorForm().Show()</c> 调用方式。</summary>
public void Show() => Open();
/// <summary>打开(或置前)通讯监控面板。</summary>
public static void Open()
{
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 static void Subscribe()
{
if (_subscribed)
return;
MessageService.MessageAdded += OnMessageAdded;
_subscribed = true;
}
private static void Unsubscribe()
{
if (!_subscribed)
return;
MessageService.MessageAdded -= OnMessageAdded;
_subscribed = false;
lock (PendingLock)
PendingMessages.Clear();
}
private static void OnMessageAdded(object sender, CommunicationMessage message)
{
if (!_subscribed || message == null)
return;
lock (PendingLock)
PendingMessages.Enqueue(message);
_panel?.Repaint();
}
/// <summary>定时批量刷新 UI,避免每条报文都抢占渲染线程。</summary>
private static 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 static void InsertMessageAtTop(CommunicationMessage msg)
{
_displayMessages.Insert(0, msg);
while (_displayMessages.Count > MaxDisplayRows)
_displayMessages.RemoveAt(_displayMessages.Count - 1);
if (_selectedRowIndex >= 0)
_selectedRowIndex++;
}
private static 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 static 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 static 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 static string SelectedIpFilter()
{
if (_ipOptions == null || _ipOptions.Length == 0)
return "全部";
if (_selectedIpIndex < 0 || _selectedIpIndex >= _ipOptions.Length)
return "全部";
return _ipOptions[_selectedIpIndex];
}
private static void RequestStatisticsRefresh()
{
_pendingStatsRefresh = true;
}
/// <summary>统计信息低频刷新(500ms)。</summary>
private static void MaybeRefreshStatistics()
{
if (!_pendingStatsRefresh)
return;
if (DateTime.Now - _lastStatsRefresh < TimeSpan.FromMilliseconds(StatsRefreshMs))
return;
_pendingStatsRefresh = false;
_lastStatsRefresh = DateTime.Now;
UpdateStatistics(_displayMessages.Count);
}
private static 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 static string TruncateRawData(string rawData, int maxLen = 48)
{
if (string.IsNullOrEmpty(rawData))
return "";
return rawData.Length <= maxLen ? rawData : rawData.Substring(0, maxLen) + "…";
}
private static 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}";
}
}
}
}