Files
StandardSence/StandardScene.Core/ExtendDevice/Door/DoorManager.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

417 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CycleGUI;
using StandardScene.Utils;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 门控制器配置管理(CycleGUI 版,替代原 WinForms <c>DoorManager</c> 窗体)。
/// </summary>
public class DoorManager
{
private const string DataFileName = "DoorConfig.json";
private string DataFilePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DataFileName);
private readonly object SaveLock = new object();
private Panel _panel;
private List<DoorControllerModel> _controllers = new List<DoorControllerModel>();
private int _selectedCtrlIdx = -1;
private int _selectedDoorIdx = -1;
private string _status = "";
private string _ctrlIp = "";
private string _ctrlPort = "502";
private string _ctrlIndex = "";
private int _typeIdx;
private string[] _typeNames = Array.Empty<string>();
private string _doorIndex = "";
private string _doorCtrlAddr = "";
private string _doorOpenAddr = "";
private bool _doorNoControl;
public static void OpenViewer() => Open();
public static void Open() => (_instance ??= new DoorManager()).OpenCore();
private static DoorManager _instance;
private void OpenCore()
{
if (_panel != null)
{
try { _panel.BringToFront(); return; }
catch { _panel = null; }
}
_typeNames = DiscoverTypes();
LoadData();
_selectedCtrlIdx = -1;
_selectedDoorIdx = -1;
ClearControllerFields();
ClearDoorFields();
var panel = GUI.DeclarePanel()
.ShowTitle("门控制器管理")
.SetDefaultDocking(Panel.Docking.None)
.InitSize(1100, 720)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => { if (_panel == panel) _panel = null; });
panel.Define(pb =>
{
if (pb.Closing())
{
SaveData();
panel.Exit();
_panel = null;
return;
}
pb.SeparatorText("门控制器");
pb.Table("door-ctrl-list", new[] { "控制器编码", "IP", "端口", "类型", "Ctrl操作" },
_controllers.Count, (row, i) =>
{
var c = _controllers[i];
row.Label($"{c.Index}");
row.Label(c.Ip);
row.Label($"{c.Port}");
row.Label(c.Type);
if (row.ButtonGroup(new[] { "选控制器" }, new[] { "选择该控制器" }) == 0)
{
_selectedCtrlIdx = i;
_selectedDoorIdx = -1;
LoadControllerFields(c);
ClearDoorFields();
}
}, height: 8, enableSearch: true);
pb.SeparatorText("控制器编辑");
var (ip, _) = pb.TextInput("1. IP", _ctrlIp, alwaysReturnString: true);
_ctrlIp = ip;
pb.SameLine(12);
var (port, _) = pb.TextInput("2. 端口", _ctrlPort, alwaysReturnString: true);
_ctrlPort = port;
var (idx, _) = pb.TextInput("3. 控制器编码", _ctrlIndex, alwaysReturnString: true);
_ctrlIndex = idx;
if (_typeNames.Length > 0)
pb.DropdownBox("4. 类型", _typeNames, ref _typeIdx);
if (pb.Button("添加控制器", distinct: "door-add-ctrl")) AddController();
pb.SameLine(8);
if (pb.Button("保存控制器", distinct: "door-save-ctrl")) SaveController();
pb.SameLine(8);
if (pb.Button("删除控制器", distinct: "door-del-ctrl")) ConfirmDeleteController();
var doors = GetSelectedController()?.Doors ?? new List<DoorModel>();
pb.SeparatorText($"门列表(控制器 #{GetSelectedController()?.Index ?? 0},共 {doors.Count} 扇)");
pb.Table("door-list", new[] { "门编码", "控制地址", "开到位地址", "无控制", "门操作" },
doors.Count, (row, i) =>
{
var d = doors[i];
row.Label($"{d.Index}");
row.Label($"{d.ControlAddress}");
row.Label($"{d.OpenStatusAddress}");
row.Label(d.NoControl ? "是" : "否");
if (row.ButtonGroup(new[] { "选门" }, new[] { "选择该门" }) == 0)
{
_selectedDoorIdx = i;
LoadDoorFields(d);
}
}, height: 6, enableSearch: true);
pb.SeparatorText("门编辑");
var (dIdx, _) = pb.TextInput("5. 门编码", _doorIndex, alwaysReturnString: true);
_doorIndex = dIdx;
var (cAddr, _) = pb.TextInput("6. 控制地址", _doorCtrlAddr, alwaysReturnString: true);
_doorCtrlAddr = cAddr;
var (oAddr, _) = pb.TextInput("7. 开到位地址", _doorOpenAddr, alwaysReturnString: true);
_doorOpenAddr = oAddr;
pb.CheckBox("8. 无控制", ref _doorNoControl);
if (pb.Button("添加门", distinct: "door-add")) AddDoor();
pb.SameLine(8);
if (pb.Button("保存门", distinct: "door-save")) SaveDoor();
pb.SameLine(8);
if (pb.Button("删除门", distinct: "door-del")) ConfirmDeleteDoor();
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
});
}
private DoorControllerModel GetSelectedController() =>
_selectedCtrlIdx >= 0 && _selectedCtrlIdx < _controllers.Count ? _controllers[_selectedCtrlIdx] : null;
private DoorModel GetSelectedDoor()
{
var ctrl = GetSelectedController();
return ctrl != null && _selectedDoorIdx >= 0 && _selectedDoorIdx < ctrl.Doors.Count
? ctrl.Doors[_selectedDoorIdx] : null;
}
private string[] DiscoverTypes()
{
try
{
var names = SimpleLite.Utils.UiTypeDiscovery.AllTypes()
.Where(t => t.IsClass && !t.IsAbstract
&& t.Namespace == typeof(BasicDoorController).Namespace
&& t.IsSubclassOf(typeof(BasicDoorController))
&& t.IsDefined(typeof(DoorTypeAttribute), false))
.Select(t => t.GetCustomAttribute<DoorTypeAttribute>()?.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderBy(n => n)
.ToArray();
return names.Length > 0 ? names : new[] { "ModbusDoorController" };
}
catch
{
return new[] { "ModbusDoorController" };
}
}
private void LoadData()
{
try
{
if (!File.Exists(DataFilePath)) { _controllers = new List<DoorControllerModel>(); return; }
var json = File.ReadAllText(DataFilePath, Encoding.UTF8);
_controllers = string.IsNullOrWhiteSpace(json)
? new List<DoorControllerModel>()
: json.JsonTo<List<DoorControllerModel>>() ?? new List<DoorControllerModel>();
}
catch (Exception ex)
{
_controllers = new List<DoorControllerModel>();
_status = $"加载失败: {ex.Message}";
}
}
private void SaveData()
{
var json = _controllers.ToJson();
var path = DataFilePath;
Task.Run(() =>
{
try
{
lock (SaveLock)
File.WriteAllText(path, json, Encoding.UTF8);
}
catch (Exception ex)
{
_status = $"保存失败: {ex.Message}";
_panel?.Repaint();
}
});
}
private void LoadControllerFields(DoorControllerModel c)
{
_ctrlIp = c.Ip;
_ctrlPort = c.Port.ToString();
_ctrlIndex = c.Index.ToString();
_typeIdx = Math.Max(0, Array.IndexOf(_typeNames, c.Type));
if (_typeIdx < 0) _typeIdx = 0;
}
private void ClearControllerFields()
{
_ctrlIp = "";
_ctrlPort = "502";
_ctrlIndex = "";
_typeIdx = 0;
}
private void LoadDoorFields(DoorModel d)
{
_doorIndex = d.Index.ToString();
_doorCtrlAddr = d.ControlAddress.ToString();
_doorOpenAddr = d.OpenStatusAddress.ToString();
_doorNoControl = d.NoControl;
}
private void ClearDoorFields()
{
_doorIndex = "";
_doorCtrlAddr = "";
_doorOpenAddr = "";
_doorNoControl = false;
}
private void AddController()
{
if (!TryParseControllerInput(out var index, out var ip, out var port, out var type, false, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
if (_controllers.Any(c => c.Index == index)) { CycleUiHelper.Alert("错误", $"编码 {index} 已存在"); return; }
if (_controllers.Any(c => c.Ip == ip)) { CycleUiHelper.Alert("错误", $"IP {ip} 已存在"); return; }
var c = new DoorControllerModel { Index = index, Ip = ip, Port = port, Type = type };
_controllers.Add(c);
_selectedCtrlIdx = _controllers.Count - 1;
SaveData();
_status = "已添加门控制器";
_panel?.Repaint();
}
private void SaveController()
{
var cur = GetSelectedController();
if (cur == null) { CycleUiHelper.Alert("提示", "请先选择要保存的门控制器"); return; }
if (!TryParseControllerInput(out var index, out var ip, out var port, out var type, true, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
if (_controllers.Any(c => c.Index == index && c != cur)) { CycleUiHelper.Alert("错误", $"编码 {index} 已存在"); return; }
if (_controllers.Any(c => c.Ip == ip && c != cur)) { CycleUiHelper.Alert("错误", $"IP {ip} 已存在"); return; }
cur.Index = index;
cur.Ip = ip;
cur.Port = port;
cur.Type = type;
SaveData();
_status = "门控制器已保存";
CycleUiHelper.Alert("提示", "保存成功");
_panel?.Repaint();
}
private void ConfirmDeleteController()
{
var cur = GetSelectedController();
if (cur == null) { CycleUiHelper.Alert("提示", "请选择要删除的门控制器"); return; }
CycleUiHelper.ConfirmThen($"删除编码为 {cur.Index} 的门控制器?", () =>
{
_controllers.Remove(cur);
_selectedCtrlIdx = -1;
_selectedDoorIdx = -1;
ClearControllerFields();
ClearDoorFields();
SaveData();
_status = "已删除门控制器";
_panel?.Repaint();
});
}
private void AddDoor()
{
var ctrl = GetSelectedController();
if (ctrl == null) { CycleUiHelper.Alert("提示", "请先选择门控制器"); return; }
if (!TryParseDoorInput(out var index, out var cAddr, out var oAddr, false, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
if (ctrl.Doors.Any(d => d.Index == index)) { CycleUiHelper.Alert("错误", $"门编码 {index} 已存在"); return; }
var door = new DoorModel
{
Index = index,
ControlAddress = cAddr,
OpenStatusAddress = oAddr,
NoControl = _doorNoControl
};
ctrl.Doors.Add(door);
_selectedDoorIdx = ctrl.Doors.Count - 1;
SaveData();
_status = "已添加门";
_panel?.Repaint();
}
private void SaveDoor()
{
var ctrl = GetSelectedController();
var door = GetSelectedDoor();
if (ctrl == null || door == null) { CycleUiHelper.Alert("提示", "请先选择要保存的门"); return; }
if (!TryParseDoorInput(out var index, out var cAddr, out var oAddr, true, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
if (ctrl.Doors.Any(d => d.Index == index && d != door)) { CycleUiHelper.Alert("错误", $"门编码 {index} 已存在"); return; }
door.Index = index;
door.ControlAddress = cAddr;
door.OpenStatusAddress = oAddr;
door.NoControl = _doorNoControl;
SaveData();
_status = "门已保存";
CycleUiHelper.Alert("提示", "保存成功");
_panel?.Repaint();
}
private void ConfirmDeleteDoor()
{
var ctrl = GetSelectedController();
var door = GetSelectedDoor();
if (ctrl == null || door == null) { CycleUiHelper.Alert("提示", "请选择要删除的门"); return; }
CycleUiHelper.ConfirmThen($"删除编码为 {door.Index} 的门?", () =>
{
ctrl.Doors.Remove(door);
_selectedDoorIdx = -1;
ClearDoorFields();
SaveData();
_status = "已删除门";
_panel?.Repaint();
});
}
private bool TryParseControllerInput(out int index, out string ip, out int port, out string type, bool requireSelection, out string err)
{
index = 0; ip = ""; port = 502; type = ""; err = "";
ip = string.IsNullOrWhiteSpace(_ctrlIp) ? "192.168.1.100" : _ctrlIp.Trim();
if (!IsValidIp(ip)) { err = "无效的 IP 地址"; return false; }
if (!int.TryParse(_ctrlPort.Trim(), out port)) { err = "端口必须是数字"; return false; }
if (!int.TryParse(_ctrlIndex.Trim(), out index))
{
index = _controllers.Count > 0 ? _controllers.Max(c => c.Index) + 1 : 1;
if (!string.IsNullOrWhiteSpace(_ctrlIndex.Trim())) { err = "编码必须是数字"; return false; }
}
type = _typeNames.Length > 0 ? _typeNames[_typeIdx] : "ModbusDoorController";
if (requireSelection && GetSelectedController() == null) { err = "请先选择门控制器"; return false; }
return true;
}
private bool TryParseDoorInput(out int index, out ushort cAddr, out ushort oAddr, bool requireSelection, out string err)
{
index = 0; cAddr = 0; oAddr = 0; err = "";
var ctrl = GetSelectedController();
if (requireSelection && (ctrl == null || GetSelectedDoor() == null)) { err = "请先选择门"; return false; }
if (!int.TryParse(_doorIndex.Trim(), out index))
{
index = ctrl?.Doors.Count > 0 ? ctrl.Doors.Max(d => d.Index) + 1 : 1;
if (!string.IsNullOrWhiteSpace(_doorIndex.Trim())) { err = "门编码必须是数字"; return false; }
}
if (!ushort.TryParse(string.IsNullOrWhiteSpace(_doorCtrlAddr) ? "0" : _doorCtrlAddr.Trim(), out cAddr))
{ err = "控制地址必须是 0-65535"; return false; }
if (!ushort.TryParse(string.IsNullOrWhiteSpace(_doorOpenAddr) ? "0" : _doorOpenAddr.Trim(), out oAddr))
{ err = "开到位地址必须是 0-65535"; return false; }
return true;
}
private bool IsValidIp(string ip)
{
if (string.IsNullOrWhiteSpace(ip)) return false;
var pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
return Regex.IsMatch(ip, pattern) && IPAddress.TryParse(ip, out var addr)
&& addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork;
}
}
}