Files
StandardSence/StandardScene.Core/Docs/Charge/README.md
T
2026-06-14 11:19:15 +08:00

44 KiB
Raw Blame History

StandardScene - Charge 模块逻辑文档

本文档用于梳理 StandardScene/Charge/ 充电桩管理与充电业务的整体逻辑,重点包含:

  • 充电桩配置(数据模型与持久化)
  • 通信报文解析与 ChargeStation 状态落库
  • StandardChargeMission 的充电业务循环
  • 相关 WinForms 界面如何展示与交互

1. 目录/模块职责速览(Charge/ 内)

数据与配置层

  • ChargeStation.cs:充电桩数据模型(ChargeStation)及相关枚举(ChargeStationStatusCommunicationStatusChargeCommandStatus 等)
  • ChargeStationDataService.csChargeStation 的持久化与查询/更新(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 ChargeStationCharge/ChargeStation.cs

ChargeStation 是所有界面展示与通信落库的核心对象。与本模块强相关的字段包括:

  • 身份与配置

    • StationId:充电桩编号(用于唯一标识,UI 校验 1-99)
    • NameType(充电桩类型:FRLDTall / FRLDShort / MuXing
    • ChargeMethod(地充/尾充/侧充)
    • IpAddressPort:通信地址
    • SetVoltageSetElectricCurrent:设定值
    • Enabled:是否启用
    • GroupCarTypeSiteId:与调度系统站点配置绑定
  • 通信与状态(用于 UI 展示)

    • StatusChargeStationStatus):Idle / Charging / Fault / Battery
    • CommStatusCommunicationStatus):UI 中展示用的通讯状态(通常由 UI Ping 计算)
    • ChargeCommandStatusChargeCommandStatus):最近一次“启动/停止充电指令”的状态
    • MechanismStatus:机构伸缩状态
    • HasAlarmAlarmLevelAlarmMessage:报警相关
  • 实时数值

    • LastSendTimeLastReceiveTime
    • RealTimeVoltageRealTimeCurrent
    • BatteryLevelCurrentVehicle

2.2 枚举含义(ChargeStation.cs

主要枚举:

  • ChargeStationStatus:空闲/充电中/报警中/AGV电池已接入
  • CommunicationStatus:未知/正常/延迟/超时/断开/错误
  • ChargeCommandStatus:停止/启动
  • MechanismStatus:伸出/缩回/运动中
  • AlarmLevel:无/低/中/高/严重
  • ChargeMethodType:地充/尾充/侧充

2.3 报警配置 AlarmConfigCharge/AlarmConfig.cs

报警配置用于 UI 管理与展示(AlarmConfigManagementForm 管理)。字段包括:

  • AlarmIdAlarmCodeAlarmContent
  • Level(报警级别)、Enabled
  • Remarks

2.4 策略配置 ChargeStrategyConfigCharge/ChargeStrategyConfig.cs

策略配置包含 SOC 阈值、时间参数、以及开关项(例如 AllowInterruptTaskUseLowerSocForCharge 等),由 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 是本模块最核心的“桥梁”:

  1. 把发送/接收报文记录到内存队列(LinkedList
  2. 根据报文原始 hex 字符串与协议类型 type 解析出结构化数据
  3. 更新对应的 ChargeStation 字段
  4. 调用 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
      • 更新 BatteryLevelCurrentVehicle
    • ChargeStationDataService.UpdateStation(station, out errorMessage) 落库

4.3 接收报文路径(AddReceiveMessage -> 更新状态/机构/告警)

  • 外部调用:AddReceiveMessage(ipAddress, port, rawData, type, stationId?)
  • 内部流程:
    • ParseReceiveRawData(rawData, type) 解析
    • UpdateStationFromReceiveData(station, parsedData) 更新:
      • LastReceiveTime
      • MechanismStatusRealTimeVoltageRealTimeCurrent
      • StatusIdle/Charging/Fault/Battery
      • HasAlarmAlarmLevelAlarmMessage
    • ChargeStationDataService.UpdateStation(...) 落库

4.4 协议类型 type

解析分支中常见类型示例:

  • FRLDShort
  • FRLDTall

不同类型会使用不同索引位置从报文字节数组中解析字段。


5. 通信接入入口

5.1 UDP 接入:ChargeUdpService

Charge/ChargeUdpService.cs

  • 创建线程监听 UDPUdpClient(40001)
  • 循环接收并转发:
    • CommunicationMessageService.AddReceiveMessage(remoteIp, 40001, hexString, "FRLDShort")
  • 同时会通过 SimpleProject.proj.Missions 找到 StandardChargeMission 实例,并在 chargeMission.ChargeStations 中按 IP 找到对应站点
  • 对特定站点类型(例如 PCBChargeStation)进一步更新站点字段(例如 IsSafeIndexReceive

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 对象)

关键步骤(来自实现片段):

  1. 遍历系统 Site 中带有 fields["Charge"] 的站点,构建 station 配置
  2. 根据 ChargeStationType 使用反射创建 AbstractChargeStation 实例
  3. 给站点对象赋值:
    • SiteIdIpPort
    • CommunicationType
      • 示例:FRLDShort 时设置为 "UDP";否则使用配置中的 CommunicationType(默认走 TCP
  4. 调用 chargeStation.CreateCommunication(ipAddress, port) 建立通信通道
  5. 把站点对象加入 ChargeStations 字典:Dictionary<int, AbstractChargeStation>

如果存在任何 UDP 站点,会创建 UdpService ??= new ChargeUdpService()

6.2 500ms 业务循环(选择车辆 -> 下发指令)

主循环(实现中包含 Thread.Sleep(500))逻辑大致如下:

  1. 对每个 chargeStationEntry(按站点遍历):
    • 通过 SimpleLib.GetAllCars() 查找:
      • 车辆当前所在站点 c.GetLastSite() == siteId
      • 或车辆正在竞争锁/持有锁(aquiringLock == siteIdholdingLocks.Contains(siteId)
  2. 若找到车辆:
    • 判断车辆状态:Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal
    • 判断是否正在“充电标记”(car.tags.Contains("charging")
    • 结合锁状态与 tag 状态计算 openCharge0/1
  3. 当未屏蔽交互(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
  • 电气/运行时信息来源:
    • ChargeCommandStatusStatusMechanismStatusHasAlarm/AlarmLevel/AlarmMessageRealTimeVoltage/Current 等都来自 CommunicationMessageService 解析并落库后的 ChargeStation 字段

自动刷新

  • autoRefreshTimer.Interval = 3000
  • AutoRefreshTimer_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
    • setVoltagesetElectricCurrent
    • group:根据 Enabled 设置为 "禁用"GroupCarType

删除逻辑(btnDelete

  • 调用 dataService.DeleteStation(stationId, out errorMessage)
  • 同步清理 Site.fields
    • 移除 setVoltagesetElectricCurrentChargegroup

列表交互

  • dgvStations_CellDoubleClick
    • 根据 StationId 查找 ChargeStation
    • 调用 LoadStationToFields(station)
    • 进入编辑模式 SetEditMode(true, true)

其它窗口入口按钮

  • btnStrategyConfig_Click:打开 ChargeStrategyConfigForm
  • btnCommMonitor_Click:打开 CommunicationMonitorForm
  • btnAlarmConfig_Click:打开 AlarmConfigManagementForm
  • btnExport_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(...)
    • InvokeRequiredBeginInvoke 回 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 = 3000AutoRefreshTimer_Tick 会保存当前选中 StationId、重建 dgvStationsLoadStations())、再恢复选中行。
  • 关闭窗体:OnFormClosing 停止并释放 autoRefreshTimer
  • LoadStations() 的状态刷新点:
    • 数据:ChargeStationDataService.GetAllStations() + 按 cmbStatusFilter 过滤。
    • 通讯状态:逐个对站点执行 Ping.Send(station.IpAddress, 1000),成功/失败分别写入 station.CommStatus,再刷新行颜色。
  • 搜索/筛选:txtSearch_TextChangedcmbStatusFilter_SelectedIndexChanged 都会触发 ApplyFilters(),清空并重建 dgvStations(包含行颜色规则)。
  • 手动刷新:btnRefresh_Click -> dataService.Reload() -> LoadStations()

编辑与保存

  • 双击列表:dgvStations_CellDoubleClick -> LoadStationToFields(station) -> SetEditMode(false)(查看模式,btnSave.Text="修改")。
  • btnSave_Click 两段式:
    • btnSave.Text=="修改":仅切到编辑模式 SetEditMode(true)
    • 否则执行保存:校验 StationId1-99)、新增时校验唯一性、校验 SiteId 存在,然后调用 AddStation / UpdateStation(..., isSave:true)
    • 保存成功后同步调度系统 Site.fieldssetVoltagesetElectricCurrentgroup(启用写 GroupCarType,禁用写 "禁用"),再刷新列表并清空编辑区。
  • 删除:btnDelete_Click 确认后 DeleteStation,并同步清理 Site.fieldssetVoltagesetElectricCurrentChargegroup)。

7.2 ChargeStrategyConfigForm(充电策略配置)

入口/导航

  • 通常由管理窗体打开:ChargeStationManagementFormbtnStrategyConfig_Click 使用 ShowDialog()

更新/刷新

  • 初始化:configService = ChargeStrategyConfigService.Instance,构造时 LoadConfig() 把文件配置加载到界面控件。
  • 保存/应用:btnSave_ClickbtnApply_Click 都会先 ValidateConfig() 校验阈值关系,再把控件值写回 config 并调用 configService.SaveConfig(config)
  • 恢复默认:btnRestoreDefaults_Click 确认后 config = ChargeStrategyConfig.CreateDefault(),调用 LoadConfig(true) 刷新界面,但不自动保存(状态提示“未保存”)。
  • 取消:btnCancel_Click -> Close()

7.3 CommunicationMonitorForm(通信监控)

入口/导航

  • 由管理窗体 btnCommMonitor_Click 打开:Show()(非阻塞)。

更新/刷新(事件驱动)

  • 构造中拿到 messageService = CommunicationMessageService.InstanceFormClosing 退订 MessageAdded
  • CommunicationMonitorForm_Load
    • InitializeForm() + LoadMessages() 后设置 isFormLoaded=true
    • 再订阅 messageService.MessageAdded += OnMessageAdded
  • OnMessageAdded
    • InvokeRequiredBeginInvoke 回 UI 线程
    • 新 IP 则刷新 cmbIpFilterRefreshIpFilter()
    • 若当前筛选匹配(“全部”或等于当前消息 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()
    • 初始化 cmbLevelcmbLevelFilter
    • 调用 LoadAlarmConfigs() 加载列表
    • 调用 ClearEditFields() 初始化编辑区(默认新增态)
  • LoadAlarmConfigs()
    • 数据源:dataService.GetAllAlarmConfigs()
    • 过滤:cmbLevelFilter(映射到 AlarmLevel)与 txtSearch(匹配 AlarmId/AlarmCode/AlarmContent
    • 填充 dgvAlarmConfigs 并按 AlarmLevel + Enabled 设置行颜色/样式,同时更新统计与标题
  • 实时刷新:txtSearch_TextChangedcmbLevelFilter_SelectedIndexChanged 都直接调用 LoadAlarmConfigs()btnRefresh_ClickdataService.Reload() 后重新加载。

编辑与保存

  • 双击列表:dgvAlarmConfigs_CellDoubleClick 读取 AlarmId -> dataService.GetAlarmConfig(alarmId) -> LoadAlarmConfigToFields()(编号不可编辑,切为编辑态)。
  • 保存:btnSave_Click 校验 numAlarmCode >= 0txtAlarmContent 非空;根据是否选中项决定 AddAlarmConfigUpdateAlarmConfig;成功后刷新列表并清空编辑区。
  • 删除: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. 常用调试点(建议)

  • 通信解析落库:
    • CommunicationMessageServiceUpdateStationFromSendData/ReceiveData 更新了哪些字段
  • UI 展示:
    • ChargeStationManagementForm.LoadStations() 中的 Ping.Send(...) 会影响 CommStatus 展示
  • 如果“列表里状态不变”:
    • 优先确认报文是否真的进入 CommunicationMessageService.AddSendMessage/AddReceiveMessage
    • 再确认解析是否返回非 null(解析失败会直接 return null

StandardScene/Charge:充电桩数据模型与持久化(仅数据层)

本页聚焦 StandardScene/Charge/ 中与“数据模型 + DataService 持久化/更新 API”相关的部分,覆盖:

  1. ChargeStation:充电桩配置/运行时状态字段含义与 JsonIgnore 持久化边界
  2. ChargeStationDataServiceConfig/ChargeStations.json 的读取/保存、增删改与状态更新
  3. AlarmConfigAlarmConfigDataServiceConfig/AlarmConfigs.json 的读取/保存、增删改

1. 数据模型:ChargeStation

文件:Charge/ChargeStation.cs

1.1 配置/计算字段说明(按 JsonIgnore 区分)

ChargeStation 的下列字段用于“充电桩配置”,在 JSON 里会被序列化(即:未标注 JsonIgnore):

  • StationId:充电桩编号(唯一标识)
  • Name:充电桩名称
  • Type:充电桩类型(ChargeStationType
  • ChargeMethod:充电方式(ChargeMethodType
  • IpAddressIP 地址
  • 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(重启后这些运行时状态通常会丢失):

  • RealTimeVoltageRealTimeCurrent:实时电压/电流
  • LastSendTimeLastReceiveTime:最后发送/接收时间
  • HasAlarmAlarmMessageAlarmLevel:报警标记/报警文本/报警级别
  • CommStatusLastCommunicationTime:通讯状态/最后通讯时间(注意:当前代码里通讯状态字段的更新路径不在本节展开)
  • MechanismStatus:机构伸缩状态
  • CurrentVehicle:当前充电车辆编号
  • BatteryLevel:当前电量百分比
  • ChargeCommandStatus:发送充电指令状态(停止/启动)
  • Status:充电桩状态(空闲/充电中/报警中/AGV电池已接入)

1.3 校验:IsValid(out errorMessage)

ChargeStation.IsValid() 约束:

  • StationIdNameIpAddress 不能为空
  • IpAddress 需为可解析的 IP
  • Port 必须在 1-65535
  • SetVoltage 必须在 (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.jsonFormatting.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 == Idle
  • int GetChargingCount():统计 Status == Charging
  • void 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

持久化边界提醒(结合 ChargeStationJsonIgnore):

  • 因为 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)

  • 修改内存对象的 StatusModifiedTime
  • 然后 SaveData()
  • 由于 Status 标注了 JsonIgnore,因此重启后站点 Status 通常不会从 JSON 恢复(但 ModifiedTime 会更新)

3. 数据模型:AlarmConfig

文件:Charge/AlarmConfig.cs

3.1 字段含义(会被持久化)

AlarmConfig 没有 JsonIgnore,因此以下字段都能写入 Config/AlarmConfigs.json

  • AlarmId:报警编号(构造函数自动生成,格式类似 ALMyyyyMMddHHmmssxxx
  • AlarmCode:报警编码值(int
  • AlarmContent:报警内容描述(文本)
  • Level:报警级别(AlarmLevelNone/Low/Medium/High/Critical
  • Enabled:是否启用
  • Remarks:备注
  • CreatedTime / ModifiedTime:创建与修改时间

3.2 校验:IsValid(out errorMessage)

  • AlarmId 不能为空
  • AlarmCode >= 0
  • AlarmContent 不能为空

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 的运行时字段(例如 HasAlarmStatusRealTimeVoltage/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 仅对以下两种有明确字节位映射:

  • FRLDShort
  • FRLDTall

其他 type(例如 MuXing)不会命中映射分支,此时解析出来的数值保持默认值,然后仍可能触发 UpdateStationFromSendData 的“默认覆盖”逻辑(见下文“已知限制”)。

6.2.2 从发送报文写入哪些 ChargeStation 字段

UpdateStationFromSendData 实际更新的字段如下(直接对应代码赋值):

  • station.LastSendTime = parsedData.SendTime
  • station.ChargeCommandStatus
    • parsedData.ChargeCommand == 1 -> ChargeCommandStatus.Started
    • parsedData.ChargeCommand == 0 -> ChargeCommandStatus.Stopped
  • station.BatteryLevel = parsedData.BatteryLevel
  • station.CurrentVehicle = parsedData.CurrentVehicleId.ToString()

注意:

  • UpdateStationFromSendDataSetVoltage / 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 以空格分隔字节 tokenrawData.Split(' ')
  • 接收报文最少 token 数:parts.Length >= 30
  • 每个 token 的解析使用的是 byte.TryParse(parts[i], out bytes[i])(没有显式 NumberStyles.HexNumber

因此当 rawData token 形如十六进制字节(例如 0AFF)时,可能出现解析失败导致 parsedData == null(从而不会更新站点字段)的情况。

type 的字节位映射同样只实现了两种:

  • FRLDShort
  • FRLDTall

6.3.2 从接收报文写入哪些 ChargeStation 字段

UpdateStationFromReceiveData 实际更新的字段如下:

  • station.LastReceiveTime = parsedData.ReceiveTime
  • station.MechanismStatus = parsedData.MechanismStatus
  • station.RealTimeVoltage = parsedData.RealTimeVoltage
  • station.RealTimeCurrent = parsedData.RealTimeCurrent
  • station.Status = parsedData.Status
  • station.HasAlarm = parsedData.HasAlarm
  • station.AlarmLevel = parsedData.AlarmLevel
  • station.AlarmMessage
    • parsedData.HasAlarm == true -> 报警级别: {GetAlarmLevelText(parsedData.AlarmLevel)}
    • 否则 -> string.Empty

与报警相关的映射:

  • ParseReceiveRawDataHasAlarm = chargeStationStatus == 2
  • ParseStationStatusstatusByte == 2 映射为 ChargeStationStatus.Fault

当前实现里 AlarmLevel 的来源有一个明显限制:

  • ParseReceiveRawDataAlarmLevel = ParseAlarmLevel(bytes[20]) 被注释掉了
  • 因此 parsedData.AlarmLevel 多半保持默认值(AlarmLevel.None),但只要 HasAlarm == trueAlarmMessage 仍会按默认 AlarmLevel 生成文本

同时,ParsedReceiveData 中的以下字段虽然会解析出来,但 UpdateStationFromReceiveData 没有把它们写入 ChargeStation

  • ParsedReceiveData.CommStatus
  • ParsedReceiveData.ChargeCommandStatus
  • ParsedReceiveData.ChargeID
  • ParsedReceiveData.BatteryAH

6.4 已知限制/行为总结(影响“字段是否更新”)

  1. 解析失败只影响“字段更新”,不影响“报文记录与 UI 列表展示”

    • 报文一定会先进入 _messages(并触发 MessageAdded
    • 但解析函数返回 null / 站点找不到 / 异常时,字段更新不会发生
  2. 站点匹配使用 IP + Port

    • GetStationByIp(ipAddress, port) 找不到对应 ChargeStation 时,不会更新该站点运行时字段
  3. type 只对 FRLDShort / FRLDTall 完成了映射

    • 发送侧对未知 type 仍会返回默认 ParsedSendData,从而可能覆盖 ChargeCommandStatus / BatteryLevel / CurrentVehicle 为默认值
    • 接收侧未知 type 也可能产生默认 ParsedReceiveData,但前提是 rawData.Split(' ') 后仍满足 parts.Length >= 30
  4. 接收侧 token 解析方式可能与输入十六进制格式不一致

    • ParseReceiveRawData 未使用 NumberStyles.HexNumber
    • 如果 rawData token 是十六进制字节(如 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

ChargeThreadwhile (true) 内部,每一轮都会先执行“步骤1:初始化充电站”:

  • 读取配置:ChargeStationHelper.GetAllStationConfigs()
    • 底层来自 ChargeStationDataService.Instance.GetAllStations()
  • 遍历每个充电桩配置项,执行校验与创建:
    • Enabled == false:跳过
    • 校验 SiteId > 0IpAddress 可解析、Port1-65535
    • 若字典里已存在相同 siteId 的站点:
      • 当 IP/Port 发生变化:existingStation.CloseCommunication() 后更新 Ip/Port 并重新 CreateCommunication(...)
      • IP/Port 未变化:直接 continue(复用原连接)
    • 若不存在:
      • 使用 GetChargeTypeString(stationConfig.Type) 映射到具体站点类名:
        • FRLDTall -> FLChargeStation
        • FRLDShort -> PCBChargeStation
        • MuXing -> MuXingChargeStation
        • 默认回退 -> PCBChargeStation
      • Activator.CreateInstance(type) 创建对象,设置:
        • SiteId / Ip / Port
        • CommunicationTypeFRLDShort 强制 UDP,其它使用配置里的 CommunicationType
      • 调用 CreateCommunication(ipAddress, port) 建立通信连接
      • 放入字典:ChargeStations.Add(siteId, stationInstance)

同时,线程内部还会做 UDP 服务初始化:

  • 若存在任意站点 CommunicationType == "UDP"
    • UdpService ??= new ChargeUdpService();

7.3 500ms 业务循环:后台线程 Step3 + SendToChargeStation(...)

ChargeThread 的主循环结构(简化):

  1. 读取互锁开关:var shieldInterLock = ((StandardChargeMissionStatus)status).ShieldInterLock
  2. 更新/清理配置绑定:
    • ChargeStationHelper.GetStationBySiteId(siteId) == null:从 ChargeStations 移除该站点
    • SimpleLib.GetAllSites() 中仍带 fields["Charge"] 但不在 ChargeStations 配置里的站点:
      • 移除 Charge / setVoltage / setElectricCurrent / group 等字段
  3. 遍历每个站点,执行“车辆搜索 -> 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 / SetAllowExitShieldSiteMechanismStatus / MechanismStatus == Retracted 联动
      • Side:直接 SetAllowEnter(true) / SetAllowExit(true) / SetAcknowledgeLeave(true)
    • 查找与该站点相关的车辆(在站/获取锁/持有锁):
      • GetLastSite() == siteIdaquiringLock == siteIdholdingLocks.Contains(siteId)
    • 计算 openCharge
      • 默认 0
      • 仅当车辆存在且 Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal
      • 并且满足充电条件:
        • charging 标记存在
        • 未被占用:!car.tags.Contains("occupied")
        • 锁状态匹配:holdingLocks.Length == 1pendingLocks.Length == 0
      • openCharge = 1
    • 互锁门控后下发指令:
      • !shieldInterLock
        • chargeStation.SendToChargeStation(openCharge, (Car)car);
  4. 循环尾部固定节拍: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,否则不发送
    • 读取 CarSoc/Voltage/ElectricCurrent,并可覆盖 site.fields["setVoltage"]/["setElectricCurrent"]
    • AddSendMessage(..., "FRLDTall", site?.name)Client.Send(msg)
  • MuXingChargeStation.SendToChargeStation

    • 计算 openChargePort = (isCharge == 1 ? 2 : 3) 并组包(包含时间戳与 CRC
    • AddSendMessage(..., "MuXing") 后写入 TCP stream
  • PCBChargeStation.SendToChargeStationFRLDShort

    • 使用 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 发送报文]