Files
StandardSence/StandardScene.Core/Charge/StandardChargeMission.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

786 lines
32 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.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using StandardScene.Utils;
using CommonUsage;
using LessokajiWeaverUtilities.Utilities;
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore;
using SimpleCore.Library;
using SimpleCore.PropType;
using StandardScene.Chained;
using StandardScene.ChargeStationType;
using StandardScene.Model;
namespace StandardScene.Charge
{
/// <summary>
/// 标准充电进程状态
/// </summary>
public class StandardChargeMissionStatus : AbstractChargeMissionStatus
{
/// <summary>
/// 是否屏蔽充电桩交互(true=屏蔽,false=允许)
/// </summary>
public bool ShieldInterLock = false;
}
/// <summary>
/// 标准充电进程
/// 负责充电桩的初始化、通讯管理和充电业务逻辑处理
/// </summary>
[MissionType(Name = "充电进程", editor = typeof(StandardChargeMission))]
[I18N.DocumentTranslation(Name = "Charge Mission", locale = "en")]
public class StandardChargeMission : AbstractChargeLogiceMission
{
#region 字段和属性
/// <summary>
/// 充电站字典 Key=站点ID, Value=充电站对象
/// </summary>
[JsonIgnore]
public Dictionary<int, AbstractChargeStation> ChargeStations;
/// <summary>
/// 进程状态
/// </summary>
public override MissionStatus status { get; set; } = new StandardChargeMissionStatus();
/// <summary>
/// 进程是否已启动
/// </summary>
[JsonIgnore]
public bool myStarted = false;
/// <summary>
/// 时间同步服务是否已启动
/// </summary>
[JsonIgnore]
public bool tsStarted = false;
/// <summary>
/// 充电处理线程
/// </summary>
[JsonIgnore]
private Thread ChargeThread;
/// <summary>
/// 当前正在充电的车辆 Key=车辆ID, Value=充电次数
/// </summary>
[JsonIgnore]
public Dictionary<string, int> inChargeCar = new();
/// <summary>
/// 上一次充电的车辆记录
/// </summary>
[JsonIgnore]
public Dictionary<string, int> lastinChargeCar = new();
/// <summary>
/// 充电开始时间 Key=车辆ID, Value=开始时间
/// </summary>
[JsonIgnore]
public Dictionary<string, DateTime> BeginTime = new();
/// <summary>
/// 充电条件状态
/// </summary>
[JsonIgnore]
public Dictionary<string, bool> Condition = new();
/// <summary>
/// 上一次充电条件状态
/// </summary>
[JsonIgnore]
public Dictionary<string, bool> lastCondition = new();
/// <summary>
/// 充电超时时间(小时)
/// </summary>
[JsonIgnore]
public double outtimeOfCharge = 0.5;
/// <summary>
/// UDP通讯服务
/// </summary>
[JsonIgnore]
public ChargeUdpService UdpService;
#endregion
#region 辅助方法
/// <summary>
/// 获取最低电量车辆的SOC值
/// 用于充电策略判断,找到系统中电量最低的空闲车辆
/// </summary>
/// <param name="car">当前车辆</param>
/// <param name="allChargeSite">所有充电站点列表</param>
/// <returns>最低电量值</returns>
public override float LowerCarSoc(AbstractCar car, List<Site> allChargeSite)
{
try
{
// 查找符合条件的最低电量车辆:
// 1. 车辆在有效站点上(GetLastSite != -1
// 2. 车辆有坐标信息(haveCoordination
// 3. 车辆未被占用(!occupied
// 4. 车辆未在充电(!charging
// 5. 车辆不在充电站点上
// 6. 车辆在线
var lowCar = SimpleLib.GetAllCars()
.OfType<Car>()
.ToList()
.FindAll(p =>
p.GetLastSite() != -1 &&
p.haveCoordination &&
!p.tags.Contains("occupied") &&
!p.tags.Contains("charging") &&
!allChargeSite.Contains(SimpleLib.GetSite(p.GetLastSite())) &&
IsOnlineCar((Car)p))
.OrderBy(p => Commons.CarValue(p, "Soc"))
.FirstOrDefault();
// 如果没有找到符合条件的车辆,返回当前车辆的电量
if (lowCar == null)
return (float)Commons.CarValue((Car)car, "Soc");
return (float)Commons.CarValue(lowCar, "Soc");
}
catch (Exception e)
{
Diagnosis.Post($"获取最低电量车辆失败: {e.Message}", "error");
return (float)0;
}
}
/// <summary>
/// 获取车辆的充电类型
/// 根据车辆字段判断是FRLD还是MuXing类型
/// </summary>
/// <param name="car">车辆对象</param>
/// <returns>充电类型字符串</returns>
public override string GetChargeType(AbstractCar car)
{
if (car is null)
{
return "";
}
// 优先判断FRLD类型
if (car.fields.ContainsKey("FRLD"))
return "FRLD";
// 其次判断MuXing类型
if (car.fields.ContainsKey("MuXing"))
return "MuXing";
// 默认返回MuXing
return "MuXing";
}
/// <summary>
/// 判断车辆是否在线
/// 通过车辆是否在有效站点上来判断
/// </summary>
/// <param name="car">车辆对象</param>
/// <returns>true=在线, false=离线</returns>
public override bool IsOnlineCar(Car car)
{
return car.GetLastSite() != -1;
}
/// <summary>
/// 车辆到达站点时的处理
/// 记录车辆到达充电站点的日志
/// </summary>
/// <param name="car">车辆对象</param>
/// <param name="site">站点对象</param>
public override void ArriveAction(Car car, Site site)
{
// 判断是否为充电站点
if (site.fields.TryGetValue("Charge", out var strStationId))
{
// 记录到达日志(模拟车辆除外)
if (!car.name.Contains("模拟"))
{
Diagnosis.Post($"{car.name}({car.id})到达充电站点{site.id}");
}
}
else
{
Diagnosis.Post($"arrived, site{site.id} is not charge site");
}
}
/// <summary>
/// 车辆离开站点时的处理
/// 记录车辆离开充电站点的日志
/// </summary>
/// <param name="car">车辆对象</param>
/// <param name="site">站点对象</param>
public override void LeaveAction(Car car, Site site)
{
// 判断是否为充电站点
if (site.fields.TryGetValue("Charge", out var strStationId))
{
// 记录离开日志(模拟车辆除外)
if (!car.name.Contains("模拟"))
{
Diagnosis.Post($"{car.name}({car.id})离开充电站点{site.id}");
}
}
else
{
Diagnosis.Post($"left, site{site.id} is not charge site");
}
}
/// <summary>
/// 站点筛选器
/// 判断站点是否为充电站点
/// </summary>
/// <param name="siteId">站点ID</param>
/// <returns>true=充电站点, false=非充电站点</returns>
public override bool SiteFilter(int siteId)
{
// 如果未屏蔽充电桩交互,返回false
if (((StandardChargeMissionStatus)status).ShieldInterLock)
{
return false;
}
// 检查站点字段中是否包含"Charge"关键字
var site = SimpleLib.GetSite(siteId);
return site.fields.Keys.ToList().Any(p => p.Contains("Charge"));
}
#endregion
#region 核心业务方法
/// <summary>
/// 启动充电进程
/// 初始化充电站、创建通讯连接、启动充电业务循环
/// </summary>
[MethodMember(Name = "启动进程", Description = "开始处理充电维护进程")]
public override void Execute()
{
// 防止重复启动
if (myStarted)
{
CycleUiHelper.Alert("提示", "充电进程已启动,不可重复启动");
return;
}
status.status = "已启动";
myStarted = true;
int iteration = 0;
ChargeStations = new Dictionary<int, AbstractChargeStation>();
// 创建充电处理线程
ChargeThread = new Thread(() =>
{
ChargeStations = new Dictionary<int, AbstractChargeStation>();
while (true)
{
try
{
if (status.status.Contains("已停止"))
{
break;
}
var shieldInterLock = ((StandardChargeMissionStatus)status).ShieldInterLock;
// ==================== 步骤1: 初始化充电站 ====================
// 从充电桩管理配置中获取所有充电桩配置
var allStationConfigs = ChargeStationHelper.GetAllStationConfigs();
// 遍历所有充电桩配置,创建充电站实例
foreach (var stationConfig in allStationConfigs)
{
// 1.1 检查充电桩是否启用
if (!stationConfig.Enabled)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] is disabled, skip");
continue;
}
// 1.2 验证站点ID
if (!stationConfig.SiteId.HasValue || stationConfig.SiteId.Value <= 0)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] has invalid SiteId, skip");
continue;
}
int siteId = stationConfig.SiteId.Value;
// 1.4 验证IP地址格式
if (string.IsNullOrWhiteSpace(stationConfig.IpAddress) ||
!IPAddress.TryParse(stationConfig.IpAddress, out var ipAddress))
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] IP[{stationConfig.IpAddress}] is invalid");
continue;
}
// 1.5 验证端口范围
int port = stationConfig.Port;
if (port <= 0 || port > 65535)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] Port[{port}] is invalid");
continue;
}
// 1.3 检查站点是否已存在,以及IP/端口是否变更
if (ChargeStations.ContainsKey(siteId))
{
var existingStation = ChargeStations[siteId];
// 检查IP或端口是否变更
if (existingStation.Ip != ipAddress.ToString() ||
existingStation.Port != port)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] IP/Port changed from [{existingStation.Ip}:{existingStation.Port}] to [{ipAddress}:{port}], recreating connection...");
// 先关闭旧连接
try
{
existingStation.CloseCommunication();
}
catch (Exception ex)
{
Diagnosis.Log($"WARN:ChargeStation[{stationConfig.StationId}] failed to close old connection: {ex.Message}");
}
// 更新IP和端口
existingStation.Ip = ipAddress.ToString();
existingStation.Port = port;
// 重新创建通讯连接
try
{
existingStation.CreateCommunication(ipAddress, port);
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] connection recreated successfully");
}
catch (Exception ex)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] failed to recreate connection: {ex.Message}");
}
}
// 站点已存在且IP/端口未变更,跳过
continue;
}
// 1.6 根据充电桩类型创建对应的充电站对象
string chargeTypeString = GetChargeTypeString(stationConfig.Type);
// 跨程序集解析:充电桩具体类型可能位于卫星插件 dllStandardScene.Devices.Charge),
// 不能再用 Type.GetType(简单名,仅当前程序集)。改用内核同款全域类型发现。
string chargeTypeFullName = "StandardScene.ChargeStationType." + chargeTypeString;
Type type = SimpleLite.Utils.UiTypeDiscovery.AllTypes()
.FirstOrDefault(t => t.FullName == chargeTypeFullName);
if (type == null)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] Type[{chargeTypeString}] not found");
continue;
}
// 1.7 创建充电站实例并配置基本信息
object chargeStation = Activator.CreateInstance(type);
((AbstractChargeStation)chargeStation).SiteId = siteId;
((AbstractChargeStation)chargeStation).Ip = ipAddress.ToString();
((AbstractChargeStation)chargeStation).Port = port;
// 1.8 设置通讯类型(从配置读取,默认TCP)
if (stationConfig.Type == ChargeStationType.FRLDShort)
{
((AbstractChargeStation)chargeStation).CommunicationType = "UDP";
}
else
{
((AbstractChargeStation)chargeStation).CommunicationType = stationConfig.CommunicationType;
}
//((AbstractChargeStation)chargeStation).CommunicationType =
// string.IsNullOrWhiteSpace(stationConfig.CommunicationType)
// ? "UDP"
// : stationConfig.CommunicationType.ToUpper();
// 1.9 创建通讯连接
((AbstractChargeStation)chargeStation).CreateCommunication(ipAddress, port);
// 1.10 添加到充电站字典
ChargeStations.Add(siteId, (AbstractChargeStation)chargeStation);
Diagnosis.Log($"ChargeStation ADD: StationId[{stationConfig.StationId}] SiteId[{siteId}] IP[{ipAddress}:{port}] Type[{chargeTypeString}] Comm[{((AbstractChargeStation)chargeStation).CommunicationType}]");
}
// ==================== 步骤2: 初始化UDP服务 ====================
// 如果有任意充电桩使用UDP通讯,则创建UDP服务
if (ChargeStations.Any(c => c.Value.CommunicationType == "UDP"))
{
UdpService ??= new ChargeUdpService();
}
status.status = $"已启动-循环{iteration++}";
// ==================== 步骤3: 充电业务处理循环 ====================
foreach (var item in ChargeStations.Keys.ToArray()) //移除配置
{
var chargeStationSetting = ChargeStationHelper.GetStationBySiteId(item);
if (chargeStationSetting == null)
{
try
{
ChargeStations[item].CloseCommunication();
}
catch (Exception ex)
{
Diagnosis.Log($"WARN:ChargeStationHelper [{item}] failed to close old connection: {ex.Message}");
}
ChargeStations.Remove(item);
}
}
//S站点存在配置里没有的,需要移除配置
var sites = SimpleLib.GetAllSites().Where(s => s.fields.ContainsKey("Charge"));
foreach (var item in sites)
{
if (!ChargeStations.Keys.Contains(item.id))
{
//移除站点的配置
item.fields.Remove("Charge");
item.fields.Remove("setVoltage");
item.fields.Remove("setElectricCurrent");
item.fields.Remove("group");
Diagnosis.Post($"Charge {item.name}-{item.id} 未在充电管理配置移除参数");
item.name = "NoName";
//并关闭对应的连接
}
}
// 遍历所有已添加的充电站,处理充电业务逻辑
foreach (var chargeStationEntry in ChargeStations)
{
var openCharge = 0;
int siteId = chargeStationEntry.Key;
var chargeStation = chargeStationEntry.Value;
var site = SimpleLib.GetSite(siteId);
if (site == null)
{
Console.WriteLine($"从地图中未获取到站点的信息 siteId {siteId}");
continue;
}
var chargeStationSetting = ChargeStationHelper.GetStationBySiteId(siteId);
if (!chargeStationSetting.Enabled)
{
continue;
}
//绑定group 添加charge
if (site != null)
{
site.name = chargeStationSetting.Name;
site.fields["Charge"] = "True";
site.fields["setVoltage"] = chargeStationSetting.SetVoltage.ToString("0.0");
site.fields["setElectricCurrent"] = chargeStationSetting.SetElectricCurrent.ToString("0.0");
if (chargeStationSetting.Enabled)
{
site.fields["group"] = chargeStationSetting.GroupCarType.ToString();
}
else
{
site.fields["group"] = "禁停";
}
}
// 3.1 设置站点访问权限(默认允许进入和离开)
if (chargeStationSetting.ChargeMethod == ChargeMethodType.Side)
{
SetAllowEnter(siteId, chargeStationSetting.ShieldSiteMechanismStatus || chargeStationSetting.MechanismStatus == MechanismStatus.Retracted);
SetAllowExit(siteId, chargeStationSetting.ShieldSiteMechanismStatus || chargeStationSetting.MechanismStatus == MechanismStatus.Retracted);
SetAcknowledgeLeave(siteId, true);
}
else
{
SetAllowEnter(siteId, true);
SetAllowExit(siteId, true);
SetAcknowledgeLeave(siteId, true);
}
// 3.2 查找当前在充电站点的车辆
// 条件:车辆在站点上 或 正在获取站点锁 或 持有站点锁
var car = SimpleLib
.GetAllCars()
.FirstOrDefault(c =>
c.GetLastSite() == siteId ||
c.status.aquiringLock == siteId ||
c.status.holdingLocks.Contains(siteId)
);
// 3.3 如果找到车辆,处理充电逻辑
if (car != null)
{
// 3.3.1 检查车辆状态是否正常
if (Commons.GetVehicleStatus((Car)car) != VehicleStatus.Normal&&car.fields.ContainsKey("SkipStatus"))
{
continue;
}
// 3.3.2 判断车辆是否正在充电
var charging = car.tags.Contains("charging");
// 3.3.3 检查充电条件
// 条件:未被占用 && 未获取其他锁 && 正在充电标记
if (!car.tags.Contains("occupied")
&& car.status.holdingLocks.Length == 1 && car.status.pendingLocks.Length == 0 &&
charging)
{
openCharge = 1; // 允许充电
//// 3.3.4 安全检查:验证叉齿是否升起
//var actualLiftPillar = 1; // 默认已升起
//if (car.status.enums.TryGetValue("actualLiftPillar", out var liftPillar))
//{
// actualLiftPillar = Convert.ToInt32(liftPillar);
//}
//// 如果叉齿未升起,禁止充电
//if (actualLiftPillar != 1)
//{
// openCharge = 0;
// Console.WriteLine($"{car.id} 小车不满足充电的安全条件 叉齿未抬升");
// // TODO: 触发报警,通知人员处理
//}
}
}
// 3.3.5 发送充电指令(如果未屏蔽充电桩交互)
if (!shieldInterLock)
{
Diagnosis.Log($"向充电站[{siteId}]发送充电指令, 车辆[{car?.id}], 指令[{openCharge}]","Charge",true);
chargeStation.SendToChargeStation(openCharge, (Car)car, site);
}
}
}
catch (Exception ex)
{
Diagnosis.Post(
$"StandardChargeMission Error: {ExceptionFormatter.FormatEx(ex)}",
"error"
);
}
// 等待500ms后进行下一次循环
Thread.Sleep(500);
}
})
{
Name = "StandardChargeMission",
IsBackground = true
};
// 启动充电处理线程
ChargeThread.Start();
// ==================== 步骤4: 启动站点禁用状态上传任务 ====================
// 定期将禁用站点信息上传到迷毂系统
Task.Factory.StartNew(() =>
{
HttpPostData httpPostData = new HttpPostData();
while (true)
{
try
{
if (status.status.Contains("已停止"))
{
break;
}
// 4.1 获取所有标记为"unavailable"的站点
var sites = SimpleLib
.GetAllSites()
.Where(site => site.tags.Contains("unavailable"))
.ToList();
// 4.2 收集所有需要禁用的站点ID
HashSet<int> disabledSites = new HashSet<int>();
foreach (var site in sites)
{
// 添加当前站点
disabledSites.Add(site.id);
// 4.3 检查并添加关联的必须释放站点(mustFree)
if (site.fields.ContainsKey("mustFree") &&
site.mustFree != null &&
site.mustFree.Length > 0)
{
for (int i = 0; i < site.mustFree.Length; i++)
{
disabledSites.Add(site.mustFree[i]);
}
}
}
// 4.4 上传禁用站点列表到迷毂系统
Diagnosis.Log($"向迷毂提供禁用站点,共 {disabledSites.Count} 个", "siteIsEnable", true);
httpPostData.UploadListNode(disabledSites);
// 等待500ms后进行下一次上传
Thread.Sleep(500);
}
catch (Exception e)
{
Diagnosis.Post($"上传禁用站点失败: {ExceptionFormatter.FormatEx(e)}", "禁用站点");
}
}
}, TaskCreationOptions.LongRunning);
// 调用基类Execute方法
base.Execute();
}
/// <summary>
/// 关闭充电进程
/// 停止所有相关线程
/// </summary>
[MethodMember(Name = "关闭进程", Description = "关闭充电进程")]
public void Stop()
{
try
{
started = false;
myStarted = false;
// 先置停止状态,循环体检测到“已停止”后会自行 break
status.status = "已停止";
// 协作式停止:等待工作线程在下一次循环检测标志后退出(不再使用 .NET8 已不支持的 Thread.Abort
myThread?.Join(2000);
ChargeThread?.Join(2000);
Diagnosis.Log("充电进程已停止");
foreach (var item in ChargeStations.Values)
{
item.CloseCommunication();
Diagnosis.Post($"充电进程已停止,关闭充电通讯连接{item.Ip}-{item.Port}");
}
}
catch (Exception ex)
{
Diagnosis.Post($"充电进程已停止 {ex.ToString()}");
}
}
/// <summary>
/// 切换充电桩交互屏蔽状态
/// true=屏蔽交互,false=允许交互
/// </summary>
[MethodMember(Name = "切换充电桩交互状态", Description = "屏蔽/允许充电桩交互")]
public void ShieldInterLock()
{
var currentStatus = ((StandardChargeMissionStatus)status).ShieldInterLock;
((StandardChargeMissionStatus)status).ShieldInterLock = !currentStatus;
string statusText = ((StandardChargeMissionStatus)status).ShieldInterLock ? "已屏蔽" : "已允许";
Console.WriteLine($"充电桩交互状态: {statusText}");
Diagnosis.Log($"充电桩交互状态切换为: {statusText}");
}
/// <summary>
/// 打开充电桩管理界面
/// 用于配置和监控充电桩
/// </summary>
[MethodMember(Name = "打开充电桩管理界面", Description = "打开充电桩管理界面")]
public void OpenManagementWindow()
{
ChargeStationHelper.OpenManagementWindow();
}
#endregion
#region 辅助工具方法
/// <summary>
/// 将充电桩类型枚举转换为类型字符串
/// 用于通过反射创建对应的充电站对象
/// </summary>
/// <param name="type">充电桩类型枚举</param>
/// <returns>充电站类名</returns>
private string GetChargeTypeString(ChargeStationType type)
{
switch (type)
{
case ChargeStationType.FRLDTall:
return "FLChargeStation";
case ChargeStationType.FRLDShort:
return "PCBChargeStation";
case ChargeStationType.MuXing:
return "MuXingChargeStation";
default:
// 默认使用FRLD矮款充电桩
return "PCBChargeStation";
}
}
/// <summary>
/// 给车辆下发充电任务
/// 为测试或调试用途,手动给车辆添加shouldCharge标签
/// </summary>
[MethodMember(Name = "小车下发充电任务", Description = "给小车下发充电任务")]
public void addtagshuldcharge()
{
// 查找第一个在有效站点上的车辆
var car = (Car)SimpleLib.GetAllCars()
.ToList()
.Find(c => c.GetLastSite() != -1);
if (car != null)
{
// 添加shouldCharge标签
Commons.AddOrUpdateTag(car.tags, "shouldCharge", "true");
Diagnosis.Log($"已为车辆 {car.id} 添加充电任务标签");
}
else
{
Diagnosis.Log("未找到在有效站点上的车辆");
}
}
#endregion
}
}