StandardScene - Charge 模块逻辑文档
本文档用于梳理 StandardScene/Charge/ 充电桩管理与充电业务的整体逻辑,重点包含:
- 充电桩配置(数据模型与持久化)
- 通信报文解析与
ChargeStation状态落库 StandardChargeMission的充电业务循环- 相关 WinForms 界面如何展示与交互
1. 目录/模块职责速览(Charge/ 内)
数据与配置层
ChargeStation.cs:充电桩数据模型(ChargeStation)及相关枚举(ChargeStationStatus、CommunicationStatus、ChargeCommandStatus等)ChargeStationDataService.cs:ChargeStation的持久化与查询/更新(JSON 文件存储)AlarmConfig.cs:报警配置模型(AlarmConfig)AlarmConfigDataService.cs:报警配置的持久化与查询/更新(JSON 文件存储)ChargeStrategyConfig.cs:充电策略配置模型ChargeStrategyConfigService.cs:充电策略配置的持久化(JSON 文件存储)
运行时与通信层
CommunicationMessageService.cs:通信报文“记录 + 解析 + 更新 ChargeStation”的核心服务ChargeUdpService.cs:UDP 监听入口,将 UDP 收到的数据转为CommunicationMessageService.AddReceiveMessage(...)StandardChargeMission.cs:主业务进程(初始化充电站实例、500ms 循环下发充电指令)
WinForms 界面层
ChargeStationManagementForm.cs:充电桩管理窗口(列表、增删改、跳转其它窗口)ChargeStrategyConfigForm.cs:充电策略参数配置窗口CommunicationMonitorForm.cs:通信报文监控窗口(订阅MessageAdded并刷新表格)AlarmConfigManagementForm.cs:报警配置管理窗口(增删改、筛选与搜索)
2. 数据模型(ChargeStation / AlarmConfig / 策略)
2.1 ChargeStation(Charge/ChargeStation.cs)
ChargeStation 是所有界面展示与通信落库的核心对象。与本模块强相关的字段包括:
-
身份与配置
StationId:充电桩编号(用于唯一标识,UI 校验 1-99)Name、Type(充电桩类型:FRLDTall/FRLDShort/MuXing)ChargeMethod(地充/尾充/侧充)IpAddress、Port:通信地址SetVoltage、SetElectricCurrent:设定值Enabled:是否启用GroupCarType、SiteId:与调度系统站点配置绑定
-
通信与状态(用于 UI 展示)
Status(ChargeStationStatus):Idle/Charging/Fault/BatteryCommStatus(CommunicationStatus):UI 中展示用的通讯状态(通常由 UI Ping 计算)ChargeCommandStatus(ChargeCommandStatus):最近一次“启动/停止充电指令”的状态MechanismStatus:机构伸缩状态HasAlarm、AlarmLevel、AlarmMessage:报警相关
-
实时数值
LastSendTime、LastReceiveTimeRealTimeVoltage、RealTimeCurrentBatteryLevel、CurrentVehicle
2.2 枚举含义(ChargeStation.cs)
主要枚举:
ChargeStationStatus:空闲/充电中/报警中/AGV电池已接入CommunicationStatus:未知/正常/延迟/超时/断开/错误ChargeCommandStatus:停止/启动MechanismStatus:伸出/缩回/运动中AlarmLevel:无/低/中/高/严重ChargeMethodType:地充/尾充/侧充
2.3 报警配置 AlarmConfig(Charge/AlarmConfig.cs)
报警配置用于 UI 管理与展示(AlarmConfigManagementForm 管理)。字段包括:
AlarmId、AlarmCode、AlarmContentLevel(报警级别)、EnabledRemarks
2.4 策略配置 ChargeStrategyConfig(Charge/ChargeStrategyConfig.cs)
策略配置包含 SOC 阈值、时间参数、以及开关项(例如 AllowInterruptTask、UseLowerSocForCharge 等),由 ChargeStrategyConfigForm 编辑、由 ChargeStrategyConfigService 持久化。
3. 持久化与服务层(DataService)
3.1 充电桩数据持久化:ChargeStationDataService
入口与关键能力(来自实现):
- 获取:
GetAllStations()、GetStationById(...)、GetStationByIp(ip,port) - 增加:
AddStation(...) - 更新:
UpdateStation(...)(可选isSave)、UpdateStationStatus(...) - 删除:
DeleteStation(...) - 刷新:
Reload()
落库逻辑特点:
- 通信解析后会调用
ChargeStationDataService.UpdateStation(station, out errorMessage),最终把ChargeStation新状态写回 JSON。
3.2 报警配置持久化:AlarmConfigDataService
入口与关键能力:
- 获取:
GetAllAlarmConfigs()、GetAlarmConfig(alarmId)、GetAlarmConfigByCode(...) - 增加:
AddAlarmConfig(...) - 更新:
UpdateAlarmConfig(...) - 删除:
DeleteAlarmConfig(...) - 刷新:
Reload()(实现中一般会重新从文件加载)
4. 通信报文解析与落库:CommunicationMessageService
Charge/CommunicationMessageService.cs 是本模块最核心的“桥梁”:
- 把发送/接收报文记录到内存队列(
LinkedList) - 根据报文原始 hex 字符串与协议类型
type解析出结构化数据 - 更新对应的
ChargeStation字段 - 调用
ChargeStationDataService.UpdateStation(...)落库到 JSON,并触发 UI 展示更新
4.1 报文记录与订阅
MessageAdded事件:当新报文加入时触发- UI 通信监控窗体(
CommunicationMonitorForm)订阅该事件,并在 UI 线程刷新表格
4.2 发送报文路径(AddSendMessage -> 更新 ChargeCommandStatus 等)
- 外部调用:
AddSendMessage(ipAddress, port, rawData, type, stationId?) - 内部流程:
ParseSendRawData(rawData, type)解析UpdateStationFromSendData(station, parsedData)更新:LastSendTime = SendTime- 根据
ChargeCommand(启动/停止)更新ChargeCommandStatus - 更新
BatteryLevel与CurrentVehicle
ChargeStationDataService.UpdateStation(station, out errorMessage)落库
4.3 接收报文路径(AddReceiveMessage -> 更新状态/机构/告警)
- 外部调用:
AddReceiveMessage(ipAddress, port, rawData, type, stationId?) - 内部流程:
ParseReceiveRawData(rawData, type)解析UpdateStationFromReceiveData(station, parsedData)更新:LastReceiveTimeMechanismStatus、RealTimeVoltage、RealTimeCurrentStatus(Idle/Charging/Fault/Battery)HasAlarm、AlarmLevel、AlarmMessage
ChargeStationDataService.UpdateStation(...)落库
4.4 协议类型 type
解析分支中常见类型示例:
FRLDShortFRLDTall
不同类型会使用不同索引位置从报文字节数组中解析字段。
5. 通信接入入口
5.1 UDP 接入:ChargeUdpService
Charge/ChargeUdpService.cs:
- 创建线程监听 UDP:
UdpClient(40001) - 循环接收并转发:
CommunicationMessageService.AddReceiveMessage(remoteIp, 40001, hexString, "FRLDShort")
- 同时会通过
SimpleProject.proj.Missions找到StandardChargeMission实例,并在chargeMission.ChargeStations中按 IP 找到对应站点 - 对特定站点类型(例如
PCBChargeStation)进一步更新站点字段(例如IsSafe、IndexReceive)
5.2 TCP 接入:以 FLChargeStation 为例(ChargeStationType)
以 ChargeStationType/FLChargeStation.cs 为例:
OnPlaintextReceived(...)在收到 TCP 明文后:- 提取报文字节(示例中
Take(35)) - 更新站点内的一些运行时字段(例如
IsSafe) - 调用
CommunicationMessageService.AddReceiveMessage(...),并把type传为对应协议类型(例如"FRLDTall")
- 提取报文字节(示例中
说明:具体 TCP 断连/重连机制由底层 TCP 客户端与对应站点实现决定;无论 TCP/UDP,最终都会汇聚到
CommunicationMessageService完成解析与落库。
6. 运行时充电业务循环:StandardChargeMission
Charge/StandardChargeMission.cs 负责把“调度系统中的车的状态 + 充电策略 + 站点配置”组合成周期性的充电指令下发。
6.1 初始化充电桩实例(创建 station 对象)
关键步骤(来自实现片段):
- 遍历系统
Site中带有fields["Charge"]的站点,构建 station 配置 - 根据
ChargeStationType使用反射创建AbstractChargeStation实例 - 给站点对象赋值:
SiteId、Ip、PortCommunicationType- 示例:
FRLDShort时设置为"UDP";否则使用配置中的CommunicationType(默认走 TCP)
- 示例:
- 调用
chargeStation.CreateCommunication(ipAddress, port)建立通信通道 - 把站点对象加入
ChargeStations字典:Dictionary<int, AbstractChargeStation>
如果存在任何 UDP 站点,会创建 UdpService ??= new ChargeUdpService()。
6.2 500ms 业务循环(选择车辆 -> 下发指令)
主循环(实现中包含 Thread.Sleep(500))逻辑大致如下:
- 对每个
chargeStationEntry(按站点遍历):- 通过
SimpleLib.GetAllCars()查找:- 车辆当前所在站点
c.GetLastSite() == siteId - 或车辆正在竞争锁/持有锁(
aquiringLock == siteId或holdingLocks.Contains(siteId))
- 车辆当前所在站点
- 通过
- 若找到车辆:
- 判断车辆状态:
Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal - 判断是否正在“充电标记”(
car.tags.Contains("charging")) - 结合锁状态与 tag 状态计算
openCharge(0/1)
- 判断车辆状态:
- 当未屏蔽交互(
shieldInterLock == false)时下发指令:chargeStation.SendToChargeStation(openCharge, (Car)car)
6.3 Stop/ShieldInterLock/管理界面入口
Stop():中止 mission 线程,并对每个 station 调用CloseCommunication()ShieldInterLock():切换“是否屏蔽充电桩交互”OpenManagementWindow():打开ChargeStationHelper.OpenManagementWindow()
7. WinForms 界面与交互细节
7.1 充电桩管理:ChargeStationManagementForm
文件:Charge/ChargeStationManagementForm.cs
核心展示数据来源
- 列表数据来源:
ChargeStationDataService.GetAllStations() - UI 侧通讯状态:
- 在
LoadStations()中对每个 station 执行Ping.Send(station.IpAddress, 1000) - Ping 成功则
station.CommStatus = CommunicationStatus.Normal,否则CommunicationStatus.Error
- 在
- 电气/运行时信息来源:
ChargeCommandStatus、Status、MechanismStatus、HasAlarm/AlarmLevel/AlarmMessage、RealTimeVoltage/Current等都来自CommunicationMessageService解析并落库后的ChargeStation字段
自动刷新
autoRefreshTimer.Interval = 3000AutoRefreshTimer_Tick:- 保存当前选中行的
StationId - 调用
LoadStations()重绘 - 恢复选中行
- 保存当前选中行的
关键编辑与保存逻辑(btnSave)
btnSave.Text == "修改":先切换为编辑模式SetEditMode(true)- 新增/保存时校验:
StationId不能为空且必须是 1-99 范围整数- 新增时禁止重复
StationId SiteId必须存在于调度系统站点集合(SimpleLib.GetSite((int)numSiteId.Value))
- 保存调用:
- 新增:
dataService.AddStation(...) - 更新:
dataService.UpdateStation(..., isSave:true)
- 新增:
- 同步到调度系统
Site.fields:setVoltage、setElectricCurrentgroup:根据Enabled设置为"禁用"或GroupCarType
删除逻辑(btnDelete)
- 调用
dataService.DeleteStation(stationId, out errorMessage) - 同步清理
Site.fields:- 移除
setVoltage、setElectricCurrent、Charge、group
- 移除
列表交互
dgvStations_CellDoubleClick:- 根据
StationId查找ChargeStation - 调用
LoadStationToFields(station) - 进入编辑模式
SetEditMode(true, true)
- 根据
其它窗口入口按钮
btnStrategyConfig_Click:打开ChargeStrategyConfigFormbtnCommMonitor_Click:打开CommunicationMonitorFormbtnAlarmConfig_Click:打开AlarmConfigManagementFormbtnExport_Click:导出 JSON 或 CSV(从GetAllStations()读取)
7.2 策略配置:ChargeStrategyConfigForm
文件:Charge/ChargeStrategyConfigForm.cs
- 初始化:
config = configService.LoadConfig() - 保存:把 UI 控件值写入
ChargeStrategyConfig后调用configService.SaveConfig(config) - 恢复默认:调用
ChargeStrategyConfig.CreateDefault()并重新加载到界面
7.3 通信监控:CommunicationMonitorForm
文件:Charge/CommunicationMonitorForm.cs
- 初始化:
messageService = CommunicationMessageService.Instance- 窗体加载完成后订阅:
messageService.MessageAdded += OnMessageAdded
- 新报文到达:
OnMessageAdded(...)- 若
InvokeRequired则BeginInvoke回 UI 线程 - 根据当前 IP 筛选条件刷新消息列表(调用
LoadMessages())
- 若
- 消息列表展示:
- 从
messageService.GetAllMessages()或GetMessagesByIp(ip)取出数据 - 根据
Direction(发送/接收)设置行颜色
- 从
- 统计信息:
lblStatistics.Text = $"显示: {displayCount} | 总数: ... | 发送: ... | 接收: ..."
7.4 报警配置管理:AlarmConfigManagementForm
文件:Charge/AlarmConfigManagementForm.cs
- 界面加载:
- 初始化级别下拉框与筛选下拉框
- 调用
LoadAlarmConfigs()
- 列表加载逻辑:
- 从
AlarmConfigDataService.GetAllAlarmConfigs()获取全量 - 按筛选条件(等级
cmbLevelFilter、搜索框txtSearch)过滤 - 填充
dgvAlarmConfigs并根据AlarmLevel设置行颜色
- 从
- 保存:
selectedAlarmConfig == null-> 新增dataService.AddAlarmConfig- 否则 -> 更新
dataService.UpdateAlarmConfig
- 删除:
dataService.DeleteAlarmConfig(selectedAlarmConfig.AlarmId, out ...)
- 双击列表:
dgvAlarmConfigs_CellDoubleClick读取AlarmId并加载到编辑区
7(代码一致性修订):UI 窗体导航与更新流
7.1 ChargeStationManagementForm(充电桩管理)
入口/导航
- 通过
ChargeStationHelper.OpenManagementWindow()(单例Show())或ChargeStationHelper.OpenManagementDialog()(ShowDialog())打开。 - 窗体内通过按钮打开:
btnStrategyConfig_Click->ChargeStrategyConfigForm.ShowDialog()btnCommMonitor_Click->CommunicationMonitorForm.Show()btnAlarmConfig_Click->AlarmConfigManagementForm.ShowDialog()
更新/刷新
- 列表自动刷新:
autoRefreshTimer.Interval = 3000,AutoRefreshTimer_Tick会保存当前选中StationId、重建dgvStations(LoadStations())、再恢复选中行。 - 关闭窗体:
OnFormClosing停止并释放autoRefreshTimer。 LoadStations()的状态刷新点:- 数据:
ChargeStationDataService.GetAllStations()+ 按cmbStatusFilter过滤。 - 通讯状态:逐个对站点执行
Ping.Send(station.IpAddress, 1000),成功/失败分别写入station.CommStatus,再刷新行颜色。
- 数据:
- 搜索/筛选:
txtSearch_TextChanged与cmbStatusFilter_SelectedIndexChanged都会触发ApplyFilters(),清空并重建dgvStations(包含行颜色规则)。 - 手动刷新:
btnRefresh_Click->dataService.Reload()->LoadStations()。
编辑与保存
- 双击列表:
dgvStations_CellDoubleClick->LoadStationToFields(station)->SetEditMode(false)(查看模式,btnSave.Text="修改")。 btnSave_Click两段式:btnSave.Text=="修改":仅切到编辑模式SetEditMode(true)。- 否则执行保存:校验
StationId(1-99)、新增时校验唯一性、校验SiteId存在,然后调用AddStation/UpdateStation(..., isSave:true)。 - 保存成功后同步调度系统
Site.fields:setVoltage、setElectricCurrent、group(启用写GroupCarType,禁用写"禁用"),再刷新列表并清空编辑区。
- 删除:
btnDelete_Click确认后DeleteStation,并同步清理Site.fields(setVoltage、setElectricCurrent、Charge、group)。
7.2 ChargeStrategyConfigForm(充电策略配置)
入口/导航
- 通常由管理窗体打开:
ChargeStationManagementForm的btnStrategyConfig_Click使用ShowDialog()。
更新/刷新
- 初始化:
configService = ChargeStrategyConfigService.Instance,构造时LoadConfig()把文件配置加载到界面控件。 - 保存/应用:
btnSave_Click与btnApply_Click都会先ValidateConfig()校验阈值关系,再把控件值写回config并调用configService.SaveConfig(config)。 - 恢复默认:
btnRestoreDefaults_Click确认后config = ChargeStrategyConfig.CreateDefault(),调用LoadConfig(true)刷新界面,但不自动保存(状态提示“未保存”)。 - 取消:
btnCancel_Click->Close()。
7.3 CommunicationMonitorForm(通信监控)
入口/导航
- 由管理窗体
btnCommMonitor_Click打开:Show()(非阻塞)。
更新/刷新(事件驱动)
- 构造中拿到
messageService = CommunicationMessageService.Instance;FormClosing退订MessageAdded。 CommunicationMonitorForm_Load:InitializeForm()+LoadMessages()后设置isFormLoaded=true- 再订阅
messageService.MessageAdded += OnMessageAdded
OnMessageAdded:InvokeRequired时BeginInvoke回 UI 线程- 新 IP 则刷新
cmbIpFilter(RefreshIpFilter()) - 若当前筛选匹配(“全部”或等于当前消息 IP)则调用
LoadMessages()重建消息列表
- 手动操作:
cmbIpFilter_SelectedIndexChanged->LoadMessages()btnRefresh_Click->RefreshIpFilter()+LoadMessages()btnClear_Click:确认 ->messageService.Clear()-> 刷新列表并清空txtParsedData
- 列表选择与解析展示:
dgvMessages_SelectionChanged根据所选行构造临时CommunicationMessage,再调用ParseMessage(),并将解析结果写入txtParsedData。
7.4 AlarmConfigManagementForm(报警配置管理)
入口/导航
- 由管理窗体
btnAlarmConfig_Click打开:ShowDialog()。
更新/刷新(加载 + 筛选/搜索)
- 构造:
dataService = AlarmConfigDataService.Instance,并订阅this.Load += AlarmConfigManagementForm_Load。 InitializeForm():- 初始化
cmbLevel与cmbLevelFilter - 调用
LoadAlarmConfigs()加载列表 - 调用
ClearEditFields()初始化编辑区(默认新增态)
- 初始化
LoadAlarmConfigs():- 数据源:
dataService.GetAllAlarmConfigs() - 过滤:
cmbLevelFilter(映射到AlarmLevel)与txtSearch(匹配AlarmId/AlarmCode/AlarmContent) - 填充
dgvAlarmConfigs并按AlarmLevel+Enabled设置行颜色/样式,同时更新统计与标题
- 数据源:
- 实时刷新:
txtSearch_TextChanged与cmbLevelFilter_SelectedIndexChanged都直接调用LoadAlarmConfigs();btnRefresh_Click会dataService.Reload()后重新加载。
编辑与保存
- 双击列表:
dgvAlarmConfigs_CellDoubleClick读取AlarmId->dataService.GetAlarmConfig(alarmId)->LoadAlarmConfigToFields()(编号不可编辑,切为编辑态)。 - 保存:
btnSave_Click校验numAlarmCode >= 0、txtAlarmContent非空;根据是否选中项决定AddAlarmConfig或UpdateAlarmConfig;成功后刷新列表并清空编辑区。 - 删除:
btnDelete_Click确认后DeleteAlarmConfig(selectedAlarmConfig.AlarmId),成功后刷新列表并清空编辑区。 - 取消/关闭:
btnCancel_Click清空编辑区,btnClose_Click关闭窗体。
8. 关键调用链(建议排查/理解用)
8.1 周期循环下发充电指令 -> 发送报文记录 -> ChargeCommandStatus 更新
flowchart TD
A[StandardChargeMission 500ms循环] --> B[chargeStation.SendToChargeStation(openCharge, car)]
B --> C[chargeStation 内部构造发送报文]
C --> D[CommunicationMessageService.AddSendMessage(...)]
D --> E[ParseSendRawData(type)]
E --> F[UpdateStationFromSendData]
F --> G[ChargeStationDataService.UpdateStation]
G --> H[ChargeStation 字段落库]
H --> I[ChargeStationManagementForm(3s刷新) 展示]
8.2 TCP/UDP 接收报文 -> 解析 -> ChargeStation 状态与告警更新 -> UI 展示
flowchart TD
A[TCP 收到明文 或 UDP 收到报文] --> B[CommunicationMessageService.AddReceiveMessage(...)]
B --> C[ParseReceiveRawData(type)]
C --> D[UpdateStationFromReceiveData]
D --> E[ChargeStationDataService.UpdateStation]
E --> F[ChargeStation 字段落库]
F --> G[ChargeStationManagementForm(3s刷新) 展示 Status/告警/电压电流]
8.3 通信监控界面订阅报文事件
flowchart TD
A[CommunicationMessageService.AddMessage/MessageAdded] --> B[CommunicationMonitorForm.OnMessageAdded]
B --> C[BeginInvoke 切到UI线程]
C --> D[LoadMessages 刷新 dgvMessages]
9. 常用调试点(建议)
- 通信解析落库:
- 看
CommunicationMessageService的UpdateStationFromSendData/ReceiveData更新了哪些字段
- 看
- UI 展示:
ChargeStationManagementForm.LoadStations()中的Ping.Send(...)会影响CommStatus展示
- 如果“列表里状态不变”:
- 优先确认报文是否真的进入
CommunicationMessageService.AddSendMessage/AddReceiveMessage - 再确认解析是否返回非 null(解析失败会直接
return null)
- 优先确认报文是否真的进入
StandardScene/Charge:充电桩数据模型与持久化(仅数据层)
本页聚焦 StandardScene/Charge/ 中与“数据模型 + DataService 持久化/更新 API”相关的部分,覆盖:
ChargeStation:充电桩配置/运行时状态字段含义与JsonIgnore持久化边界ChargeStationDataService:Config/ChargeStations.json的读取/保存、增删改与状态更新AlarmConfig与AlarmConfigDataService:Config/AlarmConfigs.json的读取/保存、增删改
1. 数据模型:ChargeStation
文件:Charge/ChargeStation.cs
1.1 配置/计算字段说明(按 JsonIgnore 区分)
ChargeStation 的下列字段用于“充电桩配置”,在 JSON 里会被序列化(即:未标注 JsonIgnore):
StationId:充电桩编号(唯一标识)Name:充电桩名称Type:充电桩类型(ChargeStationType)ChargeMethod:充电方式(ChargeMethodType)IpAddress:IP 地址Port:端口号CommunicationType:通讯类型(属性初始值为"TCP",但构造函数会覆盖为"UDP")SetVoltage:额定电压(V)SetElectricCurrent:额定电流(A)Enabled:是否启用GroupCarType:停靠车辆类型(ChargeStationCarType)SiteId:关联站点 ID(可选)ShieldSiteMechanismStatus:屏蔽机构状态交互Remarks:备注CreatedTime:创建时间ModifiedTime:最后修改时间Power:计算属性(SetVoltage * SetElectricCurrent),标注了[JsonIgnore],不会写入 JSON
1.2 运行时状态字段(不会被持久化到 JSON)
以下字段标注了 [JsonIgnore],因此不会写入 Config/ChargeStations.json(重启后这些运行时状态通常会丢失):
RealTimeVoltage、RealTimeCurrent:实时电压/电流LastSendTime、LastReceiveTime:最后发送/接收时间HasAlarm、AlarmMessage、AlarmLevel:报警标记/报警文本/报警级别CommStatus、LastCommunicationTime:通讯状态/最后通讯时间(注意:当前代码里通讯状态字段的更新路径不在本节展开)MechanismStatus:机构伸缩状态CurrentVehicle:当前充电车辆编号BatteryLevel:当前电量百分比ChargeCommandStatus:发送充电指令状态(停止/启动)Status:充电桩状态(空闲/充电中/报警中/AGV电池已接入)
1.3 校验:IsValid(out errorMessage)
ChargeStation.IsValid() 约束:
StationId、Name、IpAddress不能为空IpAddress需为可解析的 IPPort必须在1-65535SetVoltage必须在(0, 64]SetElectricCurrent必须在(0, 101]
2. 数据服务:ChargeStationDataService
文件:Charge/ChargeStationDataService.cs
2.1 单例与持久化文件
- 单例:
ChargeStationDataService.Instance - 内部数据:
private List<ChargeStation> chargeStations - JSON 文件路径:基于运行目录写入
AppDomain.CurrentDomain.BaseDirectory/Config/ChargeStations.json
- 构造函数会确保
Config/目录存在,并执行LoadData()
2.2 读取:LoadData()
行为:
- 若文件存在:读取文本并
JsonConvert.DeserializeObject<List<ChargeStation>>(json) - 若文件不存在:初始化为空列表(并不会自动生成默认样例)
- 异常:记录诊断日志并回退到空列表
2.3 保存:SaveData()
行为:
- 在锁
lockObj下序列化整个chargeStations列表 - 写入文件
Config/ChargeStations.json(Formatting.Indented) - 保存失败:返回
false并由调用方回滚内存状态(部分方法会回滚)
2.4 查询 API
List<ChargeStation> GetAllStations():返回列表副本(拷贝)ChargeStation GetStationById(string stationId):按StationId查找ChargeStation GetStationByIp(string ipAddress, int port):按IpAddress + Port查找List<ChargeStation> GetIdleStations():过滤Enabled && Status == Idleint GetChargingCount():统计Status == Chargingvoid Reload():重新执行LoadData()
2.5 新增:AddStation(ChargeStation station, out string errorMessage)
关键点:
station == null返回失败- 先执行
station.IsValid(out errorMessage) - 唯一性校验:
StationId不可重复IpAddress + Port组合不可重复
- 写入字段:
- 设置
CreatedTime/ModifiedTime为当前时间
- 设置
- 成功后:
SaveData();失败则将新增对象从内存移除
2.6 更新:UpdateStation(ChargeStation station, out string errorMessage, bool isSave = false)
该方法同时被用作“配置更新”与“运行时状态合并后再落盘”的入口之一(不同调用方会用不同的 isSave 值)。
核心流程:
- 校验:
station.IsValid(out errorMessage) - 找到原对象:
existingStation = chargeStations.FirstOrDefault(s => s.StationId == station.StationId) - 冲突校验:
IpAddress + Port不能被其它站点占用 - 时间处理:
- 保留
existingStation.CreatedTime - 更新
station.ModifiedTime = DateTime.Now
- 保留
- 赋值策略取决于
isSave:isSave == true:仅将“配置类字段”拷贝到existingStation(并令station = existingStation)isSave == false:不进行字段级拷贝,直接用传入的station替换列表里的对应项
- 之后无论
isSave为何都会执行SaveData()并落盘整个列表 - 保存失败:回滚为
existingStation
持久化边界提醒(结合 ChargeStation 的 JsonIgnore):
- 因为
Status / Alarm / 实时电压电流 等运行时字段都是JsonIgnore,即使UpdateStation被用于合并运行时字段,重启后这些运行时字段仍不会出现在 JSON 中 - 但
ModifiedTime(未JsonIgnore)会被写入,因此会出现“通信上报频繁导致 JSON 文件ModifiedTime刷新”的现象
2.7 删除:DeleteStation(string stationId, out string errorMessage)
- 按
stationId找到对象并移除 - 成功后保存;失败则将对象重新加入内存
- 代码中原本有“如果正在充电则禁止删除”的检查,但被注释掉了
2.8 状态更新(运行时):UpdateStationStatus(string stationId, ChargeStationStatus status)
- 修改内存对象的
Status与ModifiedTime - 然后
SaveData() - 由于
Status标注了JsonIgnore,因此重启后站点Status通常不会从 JSON 恢复(但ModifiedTime会更新)
3. 数据模型:AlarmConfig
文件:Charge/AlarmConfig.cs
3.1 字段含义(会被持久化)
AlarmConfig 没有 JsonIgnore,因此以下字段都能写入 Config/AlarmConfigs.json:
AlarmId:报警编号(构造函数自动生成,格式类似ALMyyyyMMddHHmmssxxx)AlarmCode:报警编码值(int)AlarmContent:报警内容描述(文本)Level:报警级别(AlarmLevel:None/Low/Medium/High/Critical)Enabled:是否启用Remarks:备注CreatedTime/ModifiedTime:创建与修改时间
3.2 校验:IsValid(out errorMessage)
AlarmId不能为空AlarmCode >= 0AlarmContent不能为空
4. 数据服务:AlarmConfigDataService
文件:Charge/AlarmConfigDataService.cs
4.1 单例与持久化文件
- 单例:
AlarmConfigDataService.Instance - 数据文件路径:
AppDomain.CurrentDomain.BaseDirectory/Config/AlarmConfigs.json - 构造函数调用
LoadData();若目录不存在则创建
4.2 读取:LoadData()
行为:
- 文件存在:读取并反序列化为
List<AlarmConfig>- 若反序列化结果为
null,回退为空列表
- 若反序列化结果为
- 文件不存在:初始化默认报警配置
InitializeDefaultAlarms(),随后SaveData() - 异常:记录
Debug.WriteLine,回退到空列表并初始化默认报警配置
默认报警包含(示例):
- 1001:电压过高
- 1002:电压过低
- 1003:电流过大
- 2001:温度异常
- 3001:通讯超时
- 3002:连接断开
4.3 保存:SaveData()
- 序列化整个
_alarmConfigs并写入AlarmConfigs.json - 保存失败会抛出异常(不只是返回
false)
4.4 查询 API
List<AlarmConfig> GetAllAlarmConfigs():返回列表副本AlarmConfig GetAlarmConfig(string alarmId):按AlarmId查找AlarmConfig GetAlarmConfigByCode(int alarmCode):按AlarmCode查找
4.5 新增:AddAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)
- 先执行
alarmConfig.IsValid(out errorMessage) - 校验
AlarmCode唯一性(不允许重复) - 添加到列表后
SaveData()
4.6 更新:UpdateAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)
- 校验:
IsValid - 查找目标:按
AlarmId找到索引;不存在则失败 - 冲突校验:
AlarmCode不能被其它报警配置占用 - 设置
alarmConfig.ModifiedTime = DateTime.Now - 替换列表项并
SaveData()
4.7 删除:DeleteAlarmConfig(string alarmId, out string errorMessage)
- 按
AlarmId找到并移除 - 然后
SaveData()
4.8 重新加载:Reload()
- 在锁下重新执行
LoadData()
5. 与“更新路径”的关系(为何运行时变化也会触发落盘)
虽然本页主要讲 DataService,但为了说明“哪些字段会/不会出现在 JSON 里”,需要点到调用关系:
- 通讯层(
Charge/CommunicationMessageService.cs)在解析发送/接收报文后,会:- 更新
ChargeStation的运行时字段(例如HasAlarm、Status、RealTimeVoltage/Current等) - 然后调用
ChargeStationDataService.UpdateStation(station, out errorMessage)(使用默认isSave=false)
- 更新
- UI 保存站点配置(
Charge/ChargeStationManagementForm.cs)在“保存/修改配置”时会调用:ChargeStationDataService.UpdateStation(station, out errorMessage, true)
- 报警配置的 UI 增删改(
Charge/AlarmConfigManagementForm.cs)直接调用:AddAlarmConfig / UpdateAlarmConfig / DeleteAlarmConfig
因此你会观察到:
ChargeStations.json中的“运行时字段”不会被写入(因为它们带JsonIgnore)- 但
ModifiedTime这类未忽略字段会被写入,所以文件仍会频繁变化
6. 通讯报文解析:CommunicationMessageService 如何更新 ChargeStation
本节重点解释 Charge/CommunicationMessageService.cs 中“报文解析 -> 更新充电桩运行时字段”的完整链路(并说明当前实现里哪些字段没有被真正落到 ChargeStation)。
6.1 入口与站点匹配规则
CommunicationMessageService 通过两个入口接收外部报文,并在内部完成“解析 + 更新 + 落盘(通过 DataService)”:
- 发送报文入口:
AddSendMessage(ipAddress, port, rawData, type, stationId) - 接收报文入口:
AddReceiveMessage(ipAddress, port, rawData, type, stationId)
两条链路在解析前都会做同样的站点匹配:
- 先拿到单例:
ChargeStationDataService.Instance - 通过
GetStationByIp(ipAddress, port)找到对应ChargeStation - 找不到站点直接返回(此时只会记录报文,不会更新该站点运行时字段)
解析成功后才会调用:
ChargeStationDataService.UpdateStation(station, out errorMessage)(该调用在当前代码里使用默认参数,最终会落盘整个ChargeStations.json;但由于运行时字段多为JsonIgnore,重启后这些运行时值不会恢复)
异常处理方面:
ParseSendDataAndUpdateStation/ParseReceiveDataAndUpdateStation都使用try/catch并“静默吞掉异常”,因此解析失败通常表现为:报文列表有记录,但充电桩字段没有变化。
6.2 发送报文解析与字段更新(UpdateStationFromSendData)
发送报文完整调用链如下:
AddSendMessage -> ParseSendDataAndUpdateStation
-> ParseSendRawData(rawData, type)
-> UpdateStationFromSendData(station, parsedData)
-> ChargeStationDataService.UpdateStation(...)
6.2.1 ParseSendRawData 输入格式与 type 支持
ParseSendRawData 的输入要求:
rawData以空格分隔字节 token(例如:"BB 01 42 ...")- 每个 token 会按十六进制解析:
byte.TryParse(token, NumberStyles.HexNumber, ...) - 发送报文最少 token 数:
parts.Length >= 10
当前实现里,type 仅对以下两种有明确字节位映射:
FRLDShortFRLDTall
其他 type(例如 MuXing)不会命中映射分支,此时解析出来的数值保持默认值,然后仍可能触发 UpdateStationFromSendData 的“默认覆盖”逻辑(见下文“已知限制”)。
6.2.2 从发送报文写入哪些 ChargeStation 字段
UpdateStationFromSendData 实际更新的字段如下(直接对应代码赋值):
station.LastSendTime = parsedData.SendTimestation.ChargeCommandStatusparsedData.ChargeCommand == 1->ChargeCommandStatus.StartedparsedData.ChargeCommand == 0->ChargeCommandStatus.Stopped
station.BatteryLevel = parsedData.BatteryLevelstation.CurrentVehicle = parsedData.CurrentVehicleId.ToString()
注意:
UpdateStationFromSendData里SetVoltage/SetElectricCurrent的赋值被注释掉了(即:发送报文不会更新ChargeStation.SetVoltage/ChargeStation.SetElectricCurrent的配置目标值)。
6.3 接收报文解析与字段更新(UpdateStationFromReceiveData)
接收报文完整调用链如下:
AddReceiveMessage -> ParseReceiveDataAndUpdateStation
-> ParseReceiveRawData(rawData, type)
-> UpdateStationFromReceiveData(station, parsedData)
-> ChargeStationDataService.UpdateStation(...)
6.3.1 ParseReceiveRawData 输入格式与 type 支持
ParseReceiveRawData 的输入要求:
rawData以空格分隔字节 token(rawData.Split(' '))- 接收报文最少 token 数:
parts.Length >= 30 - 每个 token 的解析使用的是
byte.TryParse(parts[i], out bytes[i])(没有显式NumberStyles.HexNumber)
因此当 rawData token 形如十六进制字节(例如 0A、FF)时,可能出现解析失败导致 parsedData == null(从而不会更新站点字段)的情况。
type 的字节位映射同样只实现了两种:
FRLDShortFRLDTall
6.3.2 从接收报文写入哪些 ChargeStation 字段
UpdateStationFromReceiveData 实际更新的字段如下:
station.LastReceiveTime = parsedData.ReceiveTimestation.MechanismStatus = parsedData.MechanismStatusstation.RealTimeVoltage = parsedData.RealTimeVoltagestation.RealTimeCurrent = parsedData.RealTimeCurrentstation.Status = parsedData.Statusstation.HasAlarm = parsedData.HasAlarmstation.AlarmLevel = parsedData.AlarmLevelstation.AlarmMessageparsedData.HasAlarm == true->报警级别: {GetAlarmLevelText(parsedData.AlarmLevel)}- 否则 ->
string.Empty
与报警相关的映射:
ParseReceiveRawData里HasAlarm = chargeStationStatus == 2ParseStationStatus将statusByte == 2映射为ChargeStationStatus.Fault
当前实现里 AlarmLevel 的来源有一个明显限制:
ParseReceiveRawData中AlarmLevel = ParseAlarmLevel(bytes[20])被注释掉了- 因此
parsedData.AlarmLevel多半保持默认值(AlarmLevel.None),但只要HasAlarm == true,AlarmMessage仍会按默认AlarmLevel生成文本
同时,ParsedReceiveData 中的以下字段虽然会解析出来,但 UpdateStationFromReceiveData 没有把它们写入 ChargeStation:
ParsedReceiveData.CommStatusParsedReceiveData.ChargeCommandStatusParsedReceiveData.ChargeIDParsedReceiveData.BatteryAH
6.4 已知限制/行为总结(影响“字段是否更新”)
-
解析失败只影响“字段更新”,不影响“报文记录与 UI 列表展示”
- 报文一定会先进入
_messages(并触发MessageAdded) - 但解析函数返回
null/ 站点找不到 / 异常时,字段更新不会发生
- 报文一定会先进入
-
站点匹配使用
IP + PortGetStationByIp(ipAddress, port)找不到对应ChargeStation时,不会更新该站点运行时字段
-
type只对FRLDShort/FRLDTall完成了映射- 发送侧对未知
type仍会返回默认ParsedSendData,从而可能覆盖ChargeCommandStatus/BatteryLevel/CurrentVehicle为默认值 - 接收侧未知
type也可能产生默认ParsedReceiveData,但前提是rawData.Split(' ')后仍满足parts.Length >= 30
- 发送侧对未知
-
接收侧 token 解析方式可能与输入十六进制格式不一致
ParseReceiveRawData未使用NumberStyles.HexNumber- 如果
rawDatatoken 是十六进制字节(如0A),可能导致parsedData == null,进而不更新实时字段
7. 运行时充电业务:StandardChargeMission
本节聚焦 Charge/StandardChargeMission.cs 中的“充电进程启动 + 500ms 充电业务循环”,并跟踪 SendToChargeStation(...) 的真实调用路径到具体充电桩实现类。
7.1 启动入口:Execute()
StandardChargeMission.Execute() 负责启动充电进程,核心流程:
- 设置进程状态:
status.status = "已启动" - 防重复启动:通过
myStarted判断,避免重复创建线程 - 初始化运行时字典:
ChargeStations = new Dictionary<int, AbstractChargeStation>() - 创建后台线程:
ChargeThread = new Thread(() => { ... })- 在线程内部完成“充电站初始化 + 500ms 业务循环”
- 启动辅助任务:定期上传带
unavailable标签的站点到迷毂系统(同样是Thread.Sleep(500)周期) - 最后调用
base.Execute(),让基类调度/联锁逻辑继续工作
7.2 初始化:后台线程 Step1(创建/重建 AbstractChargeStation)
在 ChargeThread 的 while (true) 内部,每一轮都会先执行“步骤1:初始化充电站”:
- 读取配置:
ChargeStationHelper.GetAllStationConfigs()- 底层来自
ChargeStationDataService.Instance.GetAllStations()
- 底层来自
- 遍历每个充电桩配置项,执行校验与创建:
Enabled == false:跳过- 校验
SiteId > 0、IpAddress可解析、Port在1-65535 - 若字典里已存在相同
siteId的站点:- 当 IP/Port 发生变化:
existingStation.CloseCommunication()后更新Ip/Port并重新CreateCommunication(...) - IP/Port 未变化:直接
continue(复用原连接)
- 当 IP/Port 发生变化:
- 若不存在:
- 使用
GetChargeTypeString(stationConfig.Type)映射到具体站点类名:FRLDTall->FLChargeStationFRLDShort->PCBChargeStationMuXing->MuXingChargeStation- 默认回退 ->
PCBChargeStation
Activator.CreateInstance(type)创建对象,设置:SiteId / Ip / PortCommunicationType:FRLDShort强制UDP,其它使用配置里的CommunicationType
- 调用
CreateCommunication(ipAddress, port)建立通信连接 - 放入字典:
ChargeStations.Add(siteId, stationInstance)
- 使用
同时,线程内部还会做 UDP 服务初始化:
- 若存在任意站点
CommunicationType == "UDP":UdpService ??= new ChargeUdpService();
7.3 500ms 业务循环:后台线程 Step3 + SendToChargeStation(...)
ChargeThread 的主循环结构(简化):
- 读取互锁开关:
var shieldInterLock = ((StandardChargeMissionStatus)status).ShieldInterLock - 更新/清理配置绑定:
- 若
ChargeStationHelper.GetStationBySiteId(siteId) == null:从ChargeStations移除该站点 - 对
SimpleLib.GetAllSites()中仍带fields["Charge"]但不在ChargeStations配置里的站点:- 移除
Charge / setVoltage / setElectricCurrent / group等字段
- 移除
- 若
- 遍历每个站点,执行“车辆搜索 -> openCharge 计算 -> 下发”:
- 取站点配置:
chargeStationSetting = ChargeStationHelper.GetStationBySiteId(siteId) - 若
!chargeStationSetting.Enabled:跳过 - 将站点配置绑定回
site.fields:site.fields["Charge"] = "True"site.fields["setVoltage"] = chargeStationSetting.SetVoltage.ToString("0.0")site.fields["setElectricCurrent"] = chargeStationSetting.SetElectricCurrent.ToString("0.0")
site.fields["group"]:启用时写GroupCarType,禁用时写"禁用"- 设置站点进入/离开权限:
ChargeMethodType.Side分支:SetAllowEnter / SetAllowExit与ShieldSiteMechanismStatus/MechanismStatus == Retracted联动- 非
Side:直接SetAllowEnter(true) / SetAllowExit(true) / SetAcknowledgeLeave(true)
- 查找与该站点相关的车辆(在站/获取锁/持有锁):
GetLastSite() == siteId或aquiringLock == siteId或holdingLocks.Contains(siteId)
- 计算
openCharge:- 默认
0 - 仅当车辆存在且
Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal - 并且满足充电条件:
charging标记存在- 未被占用:
!car.tags.Contains("occupied") - 锁状态匹配:
holdingLocks.Length == 1且pendingLocks.Length == 0
- 则
openCharge = 1
- 默认
- 互锁门控后下发指令:
- 若
!shieldInterLock:chargeStation.SendToChargeStation(openCharge, (Car)car);
- 若
- 取站点配置:
- 循环尾部固定节拍:
Thread.Sleep(500)
7.4 SendToChargeStation 调用链(下发路径)
��� 500ms 循环中,下发的调用路径是:
StandardChargeMission(ChargeThread 500ms loop)
-> AbstractChargeStation 子类 SendToChargeStation(int isCharge, Car car)
-> 子类内部组包 + 记录发送报文:CommunicationMessageService.Instance.AddSendMessage(...)
-> 通过 TCP/UDP 通道真正发送报文
各站点实现类的“发送端”关键点:
-
FLChargeStation.SendToChargeStation- 依赖
IsConnected && Client != null,否则不发送 - 读取
Car的Soc/Voltage/ElectricCurrent,并可覆盖site.fields["setVoltage"]/["setElectricCurrent"] AddSendMessage(..., "FRLDTall", site?.name)后Client.Send(msg)
- 依赖
-
MuXingChargeStation.SendToChargeStation- 计算
openChargePort = (isCharge == 1 ? 2 : 3)并组包(包含时间戳与 CRC) AddSendMessage(..., "MuXing")后写入 TCPstream
- 计算
-
PCBChargeStation.SendToChargeStation(FRLDShort)- 使用
UdpClient发送 AddSendMessage(..., "FRLDShort", site?.name)后udpClient.SendAsync(msg, msg.Length, _endPoint)
- 使用
flowchart TD
A[StandardChargeMission.Execute\n启动 ChargeThread] --> B[ChargeThread while(true)]
B --> C[Step1 初始化/重建 ChargeStations]
B --> D[Step3 遍历每个站点]
D --> E[计算 openCharge(0/1)]
E --> F{!ShieldInterLock}
F -->|false| Z[跳过下发]
F -->|true| G[chargeStation.SendToChargeStation(openCharge, car)]
G --> H[站点子类组包]
H --> I[CommunicationMessageService.AddSendMessage]
I --> J[TCP/UDP 发送报文]