2026-06-14 11:19:15 +08:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Net ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
2026-06-26 15:00:53 +08:00
using StandardScene.Utils ;
2026-06-14 11:19:15 +08:00
using CommonUsage ;
using LessokajiWeaverUtilities.Utilities ;
using Newtonsoft.Json ;
2026-08-26 11:18:40 +08:00
using Simple3 ;
using Simple3.RCS ;
using Simple3.RCS.CarTypes ;
2026-06-14 11:19:15 +08:00
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 )
{
2026-08-26 22:49:07 +08:00
FairyUiHelper . Alert ( "提示" , "充电进程已启动,不可重复启动" );
2026-06-14 11:19:15 +08:00
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 );
// 跨程序集解析:充电桩具体类型可能位于卫星插件 dll( StandardScene.Devices.Charge),
// 不能再用 Type.GetType(简单名,仅当前程序集)。改用内核同款全域类型发现。
string chargeTypeFullName = "StandardScene.ChargeStationType." + chargeTypeString ;
2026-08-26 11:18:40 +08:00
Type type = Simple3 . Utils . UiTypeDiscovery . AllTypes ()
2026-06-14 11:19:15 +08:00
. 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 = "关闭充电进程")]
2026-08-26 22:49:07 +08:00
public override void Stop ()
2026-06-14 11:19:15 +08:00
{
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
}
}