init commit

This commit is contained in:
zhaowei.huang
2026-06-14 11:19:15 +08:00
parent e79a3815a5
commit c8e540d272
174 changed files with 60830 additions and 39 deletions
Binary file not shown.
+987
View File
@@ -0,0 +1,987 @@
# 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` / `Battery`
- `CommStatus``CommunicationStatus`):UI 中展示用的通讯状态(通常由 UI Ping 计算)
- `ChargeCommandStatus``ChargeCommandStatus`):最近一次“启动/停止充电指令”的状态
- `MechanismStatus`:机构伸缩状态
- `HasAlarm``AlarmLevel``AlarmMessage`:报警相关
- 实时数值
- `LastSendTime``LastReceiveTime`
- `RealTimeVoltage``RealTimeCurrent`
- `BatteryLevel``CurrentVehicle`
### 2.2 枚举含义(`ChargeStation.cs`
主要枚举:
- `ChargeStationStatus`:空闲/充电中/报警中/AGV电池已接入
- `CommunicationStatus`:未知/正常/延迟/超时/断开/错误
- `ChargeCommandStatus`:停止/启动
- `MechanismStatus`:伸出/缩回/运动中
- `AlarmLevel`:无/低/中/高/严重
- `ChargeMethodType`:地充/尾充/侧充
### 2.3 报警配置 `AlarmConfig``Charge/AlarmConfig.cs`
报警配置用于 UI 管理与展示(`AlarmConfigManagementForm` 管理)。字段包括:
- `AlarmId``AlarmCode``AlarmContent`
- `Level`(报警级别)、`Enabled`
- `Remarks`
### 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` 是本模块最核心的“桥梁”:
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`
- 更新 `BatteryLevel``CurrentVehicle`
- `ChargeStationDataService.UpdateStation(station, out errorMessage)` 落库
### 4.3 接收报文路径(AddReceiveMessage -> 更新状态/机构/告警)
- 外部调用:`AddReceiveMessage(ipAddress, port, rawData, type, stationId?)`
- 内部流程:
- `ParseReceiveRawData(rawData, type)` 解析
- `UpdateStationFromReceiveData(station, parsedData)` 更新:
- `LastReceiveTime`
- `MechanismStatus``RealTimeVoltage``RealTimeCurrent`
- `Status``Idle/Charging/Fault/Battery`
- `HasAlarm``AlarmLevel``AlarmMessage`
- `ChargeStationDataService.UpdateStation(...)` 落库
### 4.4 协议类型 `type`
解析分支中常见类型示例:
- `FRLDShort`
- `FRLDTall`
不同类型会使用不同索引位置从报文字节数组中解析字段。
---
## 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 对象)
关键步骤(来自实现片段):
1. 遍历系统 `Site` 中带有 `fields["Charge"]` 的站点,构建 station 配置
2. 根据 `ChargeStationType` 使用反射创建 `AbstractChargeStation` 实例
3. 给站点对象赋值:
- `SiteId``Ip``Port`
- `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 == siteId``holdingLocks.Contains(siteId)`
2. 若找到车辆:
- 判断车辆状态:`Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal`
- 判断是否正在“充电标记”(`car.tags.Contains("charging")`
- 结合锁状态与 tag 状态计算 `openCharge`0/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`
- 电气/运行时信息来源:
- `ChargeCommandStatus``Status``MechanismStatus``HasAlarm/AlarmLevel/AlarmMessage``RealTimeVoltage/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`
- `setVoltage``setElectricCurrent`
- `group`:根据 `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`:打开 `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(...)`
-`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 更新
```mermaid
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 展示
```mermaid
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 通信监控界面订阅报文事件
```mermaid
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”相关的部分,覆盖:
1. `ChargeStation`:充电桩配置/运行时状态字段含义与 `JsonIgnore` 持久化边界
2. `ChargeStationDataService``Config/ChargeStations.json` 的读取/保存、增删改与状态更新
3. `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` 需为可解析的 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.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 == 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`
持久化边界提醒(结合 `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 >= 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` 的运行时字段(例如 `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` 仅对以下两种有明确字节位映射:
- `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()`
注意:
- `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` 的字节位映射同样只实现了两种:
- `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`
与报警相关的映射:
- `ParseReceiveRawData``HasAlarm = chargeStationStatus == 2`
- `ParseStationStatus``statusByte == 2` 映射为 `ChargeStationStatus.Fault`
当前实现里 `AlarmLevel` 的来源有一个明显限制:
- `ParseReceiveRawData``AlarmLevel = ParseAlarmLevel(bytes[20])` 被注释掉了
- 因此 `parsedData.AlarmLevel` 多半保持默认值(`AlarmLevel.None`),但只要 `HasAlarm == true``AlarmMessage` 仍会按默认 `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`
`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`(复用原连接)
- 若不存在:
- 使用 `GetChargeTypeString(stationConfig.Type)` 映射到具体站点类名:
- `FRLDTall` -> `FLChargeStation`
- `FRLDShort` -> `PCBChargeStation`
- `MuXing` -> `MuXingChargeStation`
- 默认回退 -> `PCBChargeStation`
- `Activator.CreateInstance(type)` 创建对象,设置:
- `SiteId / Ip / Port`
- `CommunicationType``FRLDShort` 强制 `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 / 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);`
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`,否则不发送
- 读取 `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")` 后写入 TCP `stream`
- `PCBChargeStation.SendToChargeStation``FRLDShort`
- 使用 `UdpClient` 发送
- `AddSendMessage(..., "FRLDShort", site?.name)``udpClient.SendAsync(msg, msg.Length, _endPoint)`
```mermaid
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 发送报文]
```
@@ -0,0 +1,414 @@
# 🔌 充电桩实时数据功能说明
## ✅ 已完成的修改
### 1. 数据模型更新(ChargeStation.cs
添加了实时电压和电流字段:
```csharp
/// <summary>
/// 实时电压 (V) - 当前充电时的实际电压
/// </summary>
[DisplayName("实时电压(V)")]
public double RealTimeVoltage { get; set; }
/// <summary>
/// 实时电流 (A) - 当前充电时的实际电流
/// </summary>
[DisplayName("实时电流(A)")]
public double RealTimeCurrent { get; set; }
```
### 2. 列表显示更新
#### ❌ 移除的列:
- **功率(W)** - 功率列已移除
#### ✅ 新增的列:
- **实时电压(V)** - 显示充电桩当前实际电压
- **实时电流(A)** - 显示充电桩当前实际电流
#### 列表结构(更新后):
```
┌────────┬──────┬────────┬──────────┬────┬────────┬────────┬──────────┬──────────┬────┬────┬──────┬────┐
│ 编号 │ 名称 │ 类型 │ IP地址 │端口│ 电压(V)│ 电流(A)│实时电压(V)│实时电流(A)│状态│启用│站点ID│备注│
├────────┼──────┼────────┼──────────┼────┼────────┼────────┼──────────┼──────────┼────┼────┼──────┼────┤
│CS12345 │1号桩 │标准 │192.168..│502 │220.0 │32.0 │215.5 │28.3 │充电│是 │1001 │... │
│CS12346 │2号桩 │快速 │192.168..│502 │380.0 │63.0 │0.0 │0.0 │空闲│是 │1002 │... │
└────────┴──────┴────────┴──────────┴────┴────────┴────────┴──────────┴──────────┴────┴────┴──────┴────┘
```
### 3. 界面更新
#### ❌ 移除的按钮:
- **新增按钮** - 已从界面移除
#### ✅ 保留的按钮(重新排列):
- **保存** - 位置调整到最左侧(20, 20),尺寸 100×50
- **删除** - 位置调整到中间(150, 20),尺寸 100×50
- **取消** - 位置调整到右侧(280, 20),尺寸 100×50
```
┌──────────────────────────────────┐
│ │
│ [ 保存 ] [ 删除 ] [ 取消 ]│
│ (蓝色) (红色) (默认) │
│ │
└──────────────────────────────────┘
```
### 4. 编辑区保留功能
**设置电压和电流功能完全保留**
```
┌─────────────────────────────────┐
│ 充电桩信息 │
├─────────────────────────────────┤
│ │
│ 名称: [1号充电桩 ] │
│ 类型: [标准充电桩 ▼] │
│ IP地址:[192.168.1.100 ] │
│ 端口: [502 ▲▼] │
│ │
│ 电压(V)[220.0 ▲▼] │ ← 额定电压(设置值)
│ 电流(A)[32.0 ▲▼] │ ← 额定电流(设置值)
│ │
│ 状态: [空闲 ▼] │
│ ☑ 启用充电桩 │
│ ... │
└─────────────────────────────────┘
```
---
## 📊 字段说明
### 电压和电流的区别
| 字段 | 类型 | 说明 | 用途 |
|------|------|------|------|
| **Voltage** | 额定电压 | 充电桩的设计电压(固定值) | 充电桩参数配置 |
| **Current** | 额定电流 | 充电桩的设计电流(固定值) | 充电桩参数配置 |
| **RealTimeVoltage** | 实时电压 | 当前实际工作电压(动态值) | 实时监控显示 |
| **RealTimeCurrent** | 实时电流 | 当前实际工作电流(动态值) | 实时监控显示 |
### 典型场景示例
#### 场景1:充电桩空闲时
```
额定电压:220.0V
额定电流:32.0A
实时电压:0.0V ← 未在充电,实时值为0
实时电流:0.0A ← 未在充电,实时值为0
状态:空闲
```
#### 场景2:充电桩充电中
```
额定电压:220.0V
额定电流:32.0A
实时电压:215.5V ← 实际充电电压
实时电流:28.3A ← 实际充电电流
状态:充电中
实时功率:6098.65W (215.5V × 28.3A)
```
#### 场景3:充电桩故障
```
额定电压:220.0V
额定电流:32.0A
实时电压:180.2V ← 电压异常偏低
实时电流:5.1A ← 电流异常偏低
状态:故障
告警:电压低于额定值20%
```
---
## 💻 代码实现
### 1. 创建充电桩时初始化
```csharp
var station = new ChargeStation
{
Name = "1号充电桩",
Type = ChargeStationType.Standard,
IpAddress = "192.168.1.100",
Port = 502,
// 额定参数(固定)
Voltage = 220.0,
Current = 32.0,
// 实时参数(初始为0
RealTimeVoltage = 0.0,
RealTimeCurrent = 0.0,
Status = ChargeStationStatus.Idle
};
```
### 2. 更新实时数据(模拟PLC数据)
```csharp
/// <summary>
/// 更新充电桩实时数据
/// </summary>
public void UpdateRealTimeData(string stationId, double voltage, double current)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
station.RealTimeVoltage = voltage;
station.RealTimeCurrent = current;
dataService.UpdateStation(station, out string errorMsg);
// 检查异常
CheckVoltageCurrentAbnormal(station);
}
}
/// <summary>
/// 检查电压电流是否异常
/// </summary>
private void CheckVoltageCurrentAbnormal(ChargeStation station)
{
// 充电中才检查
if (station.Status == ChargeStationStatus.Charging)
{
// 电压偏差超过20%
double voltageDiff = Math.Abs(station.RealTimeVoltage - station.Voltage) / station.Voltage;
if (voltageDiff > 0.2)
{
Diagnosis.Log($"充电桩 {station.Name} 电压异常: " +
$"额定{station.Voltage}V, 实时{station.RealTimeVoltage}V",
"ChargeStation", true);
}
// 电流偏差超过20%
double currentDiff = Math.Abs(station.RealTimeCurrent - station.Current) / station.Current;
if (currentDiff > 0.2)
{
Diagnosis.Log($"充电桩 {station.Name} 电流异常: " +
$"额定{station.Current}A, 实时{station.RealTimeCurrent}A",
"ChargeStation", true);
}
}
}
```
### 3. 充电开始时设置实时数据
```csharp
/// <summary>
/// 开始充电
/// </summary>
public void StartCharging(string stationId, int carId)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
// 更新状态
station.Status = ChargeStationStatus.Charging;
// 初始化实时数据(初始值约为额定值的90%)
station.RealTimeVoltage = station.Voltage * 0.9;
station.RealTimeCurrent = station.Current * 0.9;
dataService.UpdateStation(station, out _);
Diagnosis.Log($"车辆 {carId} 开始充电: " +
$"充电桩 {station.Name}, " +
$"实时电压 {station.RealTimeVoltage:F1}V, " +
$"实时电流 {station.RealTimeCurrent:F1}A",
"ChargeStation", true);
}
}
```
### 4. 充电结束时清零实时数据
```csharp
/// <summary>
/// 停止充电
/// </summary>
public void StopCharging(string stationId, int carId)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
// 更新状态
station.Status = ChargeStationStatus.Idle;
// 清零实时数据
station.RealTimeVoltage = 0.0;
station.RealTimeCurrent = 0.0;
dataService.UpdateStation(station, out _);
Diagnosis.Log($"车辆 {carId} 充电完成: 充电桩 {station.Name}",
"ChargeStation", true);
}
}
```
### 5. 从PLC读取实时数据
```csharp
/// <summary>
/// 从PLC读取充电桩实时数据
/// </summary>
public void ReadRealTimeDataFromPLC()
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations()
.Where(s => s.Status == ChargeStationStatus.Charging)
.ToList();
foreach (var station in stations)
{
try
{
// 从PLC读取实时电压和电流
// 这里需要根据实际PLC通信协议实现
double voltage = ReadVoltageFromPLC(station.IpAddress, station.Port);
double current = ReadCurrentFromPLC(station.IpAddress, station.Port);
// 更新实时数据
station.RealTimeVoltage = voltage;
station.RealTimeCurrent = current;
dataService.UpdateStation(station, out _);
}
catch (Exception ex)
{
Diagnosis.Log($"读取充电桩 {station.Name} 实时数据失败: {ex.Message}",
"ChargeStation", true);
}
}
}
// 这些方法需要根据实际PLC协议实现
private double ReadVoltageFromPLC(string ip, int port)
{
// TODO: 实现PLC通信读取电压
return 0.0;
}
private double ReadCurrentFromPLC(string ip, int port)
{
// TODO: 实现PLC通信读取电流
return 0.0;
}
```
---
## 🔄 数据更新流程
### 完整充电流程
```
1. 车辆到达充电站
└─> 分配空闲充电桩
└─> 状态: Idle → Reserved
2. 开始充电
└─> 状态: Reserved → Charging
└─> 设置实时数据初始值
├─> RealTimeVoltage = Voltage * 0.9
└─> RealTimeCurrent = Current * 0.9
3. 充电中(定时更新)
└─> 每3-5秒从PLC读取实时数据
├─> 更新 RealTimeVoltage
├─> 更新 RealTimeCurrent
└─> 检查异常并告警
4. 充电完成
└─> 状态: Charging → Idle
└─> 清零实时数据
├─> RealTimeVoltage = 0.0
└─> RealTimeCurrent = 0.0
```
---
## 📋 JSON数据格式
保存到文件的数据包含实时字段:
```json
{
"StationId": "CS20240115123456",
"Name": "1号充电桩",
"Type": 0,
"IpAddress": "192.168.1.100",
"Port": 502,
"Voltage": 220.0,
"Current": 32.0,
"RealTimeVoltage": 215.5,
"RealTimeCurrent": 28.3,
"Status": 1,
"Enabled": true,
"SiteId": 1001,
"Remarks": "南区1号充电桩",
"CreatedTime": "2024-01-15T12:34:56",
"ModifiedTime": "2024-01-15T14:20:30"
}
```
---
## ✅ 使用检查清单
- [x] 数据模型添加实时电压和电流字段
- [x] 列表移除功率列
- [x] 列表添加实时电压和实时电流列
- [x] 界面移除新增按钮
- [x] 按钮重新排列
- [x] 保留设置电压和电流功能
- [x] 列表正确显示实时数据
- [x] 无编译错误
---
## 📝 总结
### ✅ 完成的功能
1. **数据模型** - 添加实时电压和电流字段
2. **列表显示** - 移除功率列,添加实时数据列
3. **界面优化** - 移除新增按钮,重新排列其他按钮
4. **功能保留** - 设置电压和电流功能完全保留
### 💡 后续集成建议
1. **与PLC通信集成**
- 实现从PLC读取实时电压和电流
- 定时更新实时数据(建议3-5秒)
2. **异常监控**
- 实时监控电压电流偏差
- 超过阈值时触发告警
3. **数据统计**
- 记录充电过程的电压电流曲线
- 分析充电效率和异常情况
4. **可视化展示**
- 实时数据图表显示
- 历史数据趋势分析
**现在您可以在充电桩管理界面中查看实时电压和电流数据了!** 🎉
@@ -0,0 +1,309 @@
# 充电桩报文自动更新功能说明
## 功能概述
系统现已支持发送和接收充电桩UDP报文,**分别解析后合并更新**充电桩管理列表中的实时数据。
## 工作流程
```
发送方向: 应用程序 → 发送报文 → CommunicationMessageService(存储+解析发送数据) → 更新设定值
接收方向: 充电桩设备 → UDP报文(40001端口) → ChargeUdpService → CommunicationMessageService(存储+解析接收数据) → 更新实时数据
合并结果: 发送数据 + 接收数据 → 数据服务 → 管理界面自动刷新
```
## 核心组件
### 1. CommunicationMessageService(通讯报文服务)
**文件位置**: `Charge/CommunicationMessageService.cs`
**主要功能**:
- 存储所有发送和接收的报文(最多保留100条)
- **分开解析发送和接收的报文数据**
- 根据IP地址匹配对应的充电桩并更新数据
- 提供报文查询和筛选功能
**发送报文解析的数据**:
- ✅ 充电指令(启动/停止)
- ✅ 设定电压(V
- ✅ 设定电流(A
- ✅ 发送时间
**接收报文解析的数据**:
- ✅ 通讯状态(正常/错误)
- ✅ 充电指令状态(启动/停止)
- ✅ 机构状态(伸出/缩回/伸出中/缩回中/故障)
- ✅ 实时电压(V
- ✅ 实时电流(A
- ✅ 电量百分比(%
- ✅ 报警状态和级别
- ✅ 充电桩状态(空闲/充电中/故障/离线)
- ✅ 当前充电车辆编号
- ✅ 接收时间
### 2. ChargeUdpServiceUDP监听服务)
**文件位置**: `Charge/ChargeUdpService.cs`
**监听端口**: 40001
**工作流程**:
1. 接收UDP报文
2. 调用 `CommunicationMessageService.AddReceiveMessage()` 记录报文
3. `CommunicationMessageService` 自动解析接收报文并更新充电桩数据
4. 保持原有任务处理逻辑的兼容性
### 3. 发送报文处理
**发送位置**:
- `ChargeStationType/MuXingChargeStation.cs`
- `ChargeStationType/FLChargeStation.cs`
- `ChargeStationType/PCBChargeStation.cs`
**工作流程**:
1. 发送UDP报文到充电桩
2. 调用 `CommunicationMessageService.AddSendMessage()` 记录报文
3. `CommunicationMessageService` 自动解析发送报文并更新充电桩设定值
### 4. ChargeStationDataService(数据服务)
**新增方法**: `GetStationByIp(string ipAddress)`
**功能**: 根据IP地址快速查找对应的充电桩记录
### 5. ChargeStationManagementForm(管理界面)
**新增功能**: 自动刷新
**刷新间隔**: 2秒
**特点**:
- 自动更新列表显示
- 不影响用户的编辑操作
- 窗体关闭时自动停止刷新
## 报文格式说明
### 当前支持的报文格式
报文采用逗号分隔的字节数组格式,例如:
```
1,2,3,4,5,...,28,29,30
```
### 发送报文字节位置定义(示例)
| 字节位置 | 数据内容 | 说明 |
|---------|---------|------|
| 5 | 充电指令 | 0=停止, 1=启动 |
| 6-7 | 设定电压 | 高低字节,单位0.1V |
| 8-9 | 设定电流 | 高低字节,单位0.1A |
### 接收报文字节位置定义(示例)
| 字节位置 | 数据内容 | 说明 |
|---------|---------|------|
| 10 | 充电指令状态 | 0=停止, 1=启动 |
| 11 | 机构状态 | 0=未知, 1=伸出, 2=缩回, 3=伸出中, 4=缩回中, 5=故障 |
| 12-13 | 实时电压 | 高低字节,单位0.1V |
| 14-15 | 实时电流 | 高低字节,单位0.1A |
| 16 | 电量百分比 | 0-100 |
| 20 | 报警级别 | 0=无, 1-2=低, 3-5=中, 6-8=高, 9+=严重 |
| 25 | 充电桩状态 | 0=空闲, 1=充电中, 2=故障, 3=离线 |
| 26-29 | 车辆编号 | 4字节整数 |
| 28 | 通讯状态 | 1=正常, 其他=错误 |
**⚠️ 注意**: 以上字节位置为示例,需要根据实际通讯协议进行调整。
## 如何调整报文解析规则
打开 `CommunicationMessageService.cs` 文件,分别修改发送和接收报文的解析方法:
### 调整发送报文解析
修改 `ParseSendRawData` 方法中的字节位置:
```csharp
private ParsedSendData ParseSendRawData(string rawData)
{
// ... 字节数组转换代码 ...
var parsed = new ParsedSendData
{
// 根据实际协议修改字节位置
ChargeCommand = bytes.Length > 5 ? bytes[5] : (byte)0,
SetVoltage = bytes.Length > 7 ? (bytes[6] << 8 | bytes[7]) / 10.0 : 0,
SetCurrent = bytes.Length > 9 ? (bytes[8] << 8 | bytes[9]) / 10.0 : 0,
SendTime = DateTime.Now
};
return parsed;
}
```
### 调整接收报文解析
修改 `ParseReceiveRawData` 方法中的字节位置:
```csharp
private ParsedReceiveData ParseReceiveRawData(string rawData)
{
// ... 字节数组转换代码 ...
var parsed = new ParsedReceiveData
{
// 根据实际协议修改字节位置
CommStatus = bytes[28] == 1 ? CommunicationStatus.Normal : CommunicationStatus.Error,
ChargeCommandStatus = bytes[10] == 1 ? ChargeCommandStatus.Started : ChargeCommandStatus.Stopped,
// ... 其他字段 ...
ReceiveTime = DateTime.Now
};
return parsed;
}
```
## 使用示例
### 1. 启动UDP监听
```csharp
// 在程序启动时创建UDP服务
var udpService = new ChargeUdpService();
```
### 2. 添加充电桩
在充电桩管理界面中添加充电桩,确保IP地址与实际设备一致:
```
充电桩编号: 1
名称: 1号充电桩
IP地址: 192.168.1.101 ← 必须与设备IP一致
端口: 502
```
### 3. 自动更新
**发送报文时**
1. 应用程序发送充电指令到充电桩
2. 系统记录发送报文
3. 解析发送报文数据(设定电压、电流等)
4. 根据IP地址匹配充电桩
5. 更新充电桩的设定值
**接收报文时**
1. 系统自动接收UDP报文(端口40001)
2. 记录接收报文
3. 解析接收报文数据(实时状态、电压、电流等)
4. 根据IP地址匹配充电桩
5. 更新充电桩的实时数据
**界面显示**
- 管理界面每2秒自动刷新显示
- 同时显示设定值(来自发送报文)和实时值(来自接收报文)
## 调试信息
系统会在日志中输出以下信息:
```
[UDP返回报文信息] ChargeStation ADD:[1,2,3,4,...]
[ChargeStation] 更新充电桩成功: [1] 1号充电桩 (192.168.1.101:502) - Charging
```
**查看报文记录**
- 打开通讯监控界面可以查看所有发送和接收的报文
- 报文按时间倒序排列(最新的在最前面)
- 最多保留100条报文记录
## 常见问题
### Q1: 报文接收了但数据没更新?
**检查项**:
1. 充电桩的IP地址是否在管理列表中
2. 报文格式是否正确(至少30字节)
3. 查看日志中是否有解析错误信息
### Q2: 如何修改刷新间隔?
`ChargeStationManagementForm.cs``InitializeAutoRefresh` 方法中修改:
```csharp
autoRefreshTimer.Interval = 2000; // 改为你需要的毫秒数
```
### Q3: 如何关闭自动刷新?
```csharp
// 在InitializeAutoRefresh方法中注释掉这行
// autoRefreshTimer.Start();
```
## 扩展功能
### 添加新的解析字段
**发送报文新字段**
1.`ParsedSendData` 类中添加新属性
2.`ParseSendRawData` 方法中解析新字段
3.`UpdateStationFromSendData` 方法中更新到充电桩对象
**接收报文新字段**
1.`ParsedReceiveData` 类中添加新属性
2.`ParseReceiveRawData` 方法中解析新字段
3.`UpdateStationFromReceiveData` 方法中更新到充电桩对象
### 支持其他通讯协议
`CommunicationMessageService.cs` 中可以根据端口号或其他特征判断协议类型:
```csharp
public void AddReceiveMessage(string ipAddress, int port, string rawData, string stationId = null)
{
// ... 添加报文记录 ...
// 根据端口判断协议类型
if (port == 40001)
{
ParseReceiveDataAndUpdateStation(ipAddress, rawData); // UDP协议
}
else if (port == 502)
{
ParseModbusReceiveAndUpdate(ipAddress, rawData); // Modbus协议
}
}
public void AddSendMessage(string ipAddress, int port, string rawData, string stationId = null)
{
// ... 添加报文记录 ...
// 根据端口判断协议类型
if (port == 40001)
{
ParseSendDataAndUpdateStation(ipAddress, rawData); // UDP协议
}
else if (port == 502)
{
ParseModbusSendAndUpdate(ipAddress, rawData); // Modbus协议
}
}
```
## 技术特点
**实时性**: UDP报文接收后立即解析更新
**自动化**: 无需手动刷新,数据自动同步
**分离解析**: 发送和接收报文分开解析,数据更准确
**合并更新**: 自动合并发送和接收数据到充电桩管理界面
**可扩展**: 支持自定义报文格式和解析规则
**兼容性**: 保留原有任务处理逻辑
**稳定性**: 异常处理完善,不影响系统运行
## 版本历史
- **v1.1** (2026-01-18): 发送和接收报文分开解析,合并更新到充电桩管理界面
- **v1.0** (2026-01-18): 初始版本,支持UDP报文自动解析和更新
@@ -0,0 +1,265 @@
# 充电策略配置说明
## 功能概述
充电策略配置界面用于管理和配置充电系统的各项参数,包括 SOC 阈值、时间参数、任务参数和开关参数。配置保存在 JSON 文件中,系统启动时自动加载。
## 打开配置界面
在**充电桩管理界面**点击 **"策略配置"** 按钮(绿色按钮)即可打开配置界面。
## 配置参数说明
### 1. SOC 参数(电量百分比)
| 参数名称 | 默认值 | 说明 | 取值范围 |
|---------|-------|------|---------|
| 必充电量 | 20% | 低于此电量必须充电 | 0-100% |
| 空闲充电电量 | 90% | 车辆空闲时开始充电的电量阈值 | 0-100% |
| 任务可用电量 | 60% | 可以执行任务的最低电量 | 0-100% |
| 满电电量 | 90% | 充电目标电量 | 0-100% |
| 允许中断电量 | 45% | 允许中断充电任务的最低电量 | 0-100% |
**逻辑关系**
- 必充电量 < 空闲充电电量
- 任务可用电量 > 必充电量
- 满电电量 ≥ 空闲充电电量
- 允许中断电量 > 必充电量
### 2. 时间参数
| 参数名称 | 默认值 | 说明 | 单位 |
|---------|-------|------|-----|
| 空闲充电时间 | 30 | 车辆空闲多久后开始充电 | 秒 (sec) |
| 空闲时间 | 5 | 判断车辆空闲的时间阈值 | 秒 (sec) |
| 必充时间 | 60 | 必须充电的持续时间 | 秒 (sec) |
| 补电时间 | 5 | 补电操作的持续时间 | 分钟 (min) |
### 3. 任务参数
| 参数名称 | 默认值 | 说明 |
|---------|-------|------|
| 允许空闲车充电的最小任务数 | 0 | 当任务数量大于此值时,允许空闲车辆充电 |
### 4. 开关参数
| 参数名称 | 默认值 | 说明 |
|---------|-------|------|
| 允许中断充电任务 | 否 | 是否允许中断正在进行的充电任务 |
| 优先使用低电量车辆充电 | 是 | 优先选择电量较低的车辆进行充电 |
| 启用充电错误检测 | 否 | 是否启用充电过程中的错误检测 |
| 使用充电站点筛选 | 否 | 是否根据站点筛选充电桩 |
## 操作说明
### 保存配置
1. 修改所需参数
2. 点击 **"保存"** 按钮
3. 系统会验证参数的有效性
4. 验证通过后保存到配置文件
配置文件位置:`Config/ChargeStrategyConfig.json`
### 应用配置
点击 **"应用"** 按钮可以保存配置但不关闭窗口,方便继续调整参数。
### 恢复默认配置
1. 点击 **"恢复默认"** 按钮
2. 确认恢复操作
3. 所有参数恢复为默认值
4. **注意**:恢复后需要点击"保存"才会生效
### 取消修改
点击 **"取消"** 按钮关闭窗口,不保存任何修改。
## 配置文件格式
配置以 JSON 格式保存,示例:
```json
{
"MustChargeSoc": 20.0,
"IdleChargeSoc": 90.0,
"TaskAvailableSoc": 60.0,
"FullChargeSoc": 90.0,
"AllowInterruptSoc": 45.0,
"IdleChargeSeconds": 30.0,
"IdleSeconds": 5.0,
"MustChargeSeconds": 60.0,
"TopUpMinutes": 5.0,
"MinAllowFreeCarToChargeTaskCnt": 0,
"AllowInterruptTask": false,
"UseLowerSocForCharge": true,
"EnableErrorChargeDetection": false,
"UseChargeSiteFilter": false
}
```
## 参数验证规则
系统会在保存时自动验证配置的有效性:
### SOC 参数验证
- ✅ 所有 SOC 值必须在 0-100 之间
- ✅ 必充电量 < 空闲充电电量
- ✅ 任务可用电量 > 必充电量
- ✅ 满电电量 ≥ 空闲充电电量
- ✅ 允许中断电量 > 必充电量
### 时间参数验证
- ✅ 所有时间值必须 ≥ 0
### 任务参数验证
- ✅ 最小任务数必须 ≥ 0
## 使用场景示例
### 场景 1:紧急任务模式
适用于任务紧急,需要快速周转车辆的情况。
```
必充电量: 15%
空闲充电电量: 80%
任务可用电量: 50%
满电电量: 85%
允许中断充电任务: 是
```
### 场景 2:节能模式
适用于任务不紧急,优先保证电池寿命的情况。
```
必充电量: 25%
空闲充电电量: 95%
任务可用电量: 70%
满电电量: 95%
允许中断充电任务: 否
```
### 场景 3:平衡模式(默认)
平衡任务效率和电池寿命。
```
必充电量: 20%
空闲充电电量: 90%
任务可用电量: 60%
满电电量: 90%
允许中断充电任务: 否
```
## 配置生效时机
- **立即生效**:保存配置后立即生效
- **自动加载**:系统启动时自动加载配置
- **实时更新**:充电逻辑会实时读取最新配置
## 常见问题
### Q1: 修改配置后没有生效?
**检查项**
1. 确认已点击"保存"按钮
2. 检查状态栏是否显示"配置保存成功"
3. 查看配置文件是否已更新
### Q2: 配置文件丢失怎么办?
系统会自动创建默认配置文件,无需担心。
### Q3: 如何备份配置?
配置文件位于 `Config/ChargeStrategyConfig.json`,直接复制此文件即可备份。
### Q4: 参数验证失败怎么办?
根据错误提示调整参数,确保满足所有验证规则。
### Q5: 可以手动编辑配置文件吗?
可以,但建议使用配置界面,因为界面会自动验证参数有效性。
## 技术细节
### 配置服务(单例模式)
```csharp
var configService = ChargeStrategyConfigService.Instance;
var config = configService.LoadConfig();
configService.SaveConfig(config);
```
### 配置模型
```csharp
public class ChargeStrategyConfig
{
// SOC 参数
public double MustChargeSoc { get; set; }
public double IdleChargeSoc { get; set; }
// ... 其他参数
// 验证方法
public bool Validate(out string errorMessage);
}
```
### 配置文件路径
- **Windows**: `应用程序目录\Config\ChargeStrategyConfig.json`
- **自动创建**: 首次运行时自动创建配置目录和默认配置文件
## 界面布局
```
┌─────────────────────────────────────────────────────────┐
│ 充电策略配置 │
├─────────────────────────────────────────────────────────┤
│ ┌─ SOC 参数 ─────────────────────────────────────────┐ │
│ │ 必充电量: [20.0] % 空闲充电电量: [90.0] % │ │
│ │ 任务可用电量: [60.0] % 满电电量: [90.0] % │ │
│ │ 允许中断电量: [45.0] % │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 时间参数 ─────────────────────────────────────────┐ │
│ │ 空闲充电时间: [30.0] 秒 空闲时间: [5.0] 秒 │ │
│ │ 必充时间: [60.0] 秒 补电时间: [5.0] 分钟 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 任务参数 ─────────────────────────────────────────┐ │
│ │ 允许空闲车充电的最小任务数: [0] │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 开关参数 ─────────────────────────────────────────┐ │
│ │ ☐ 允许中断充电任务 ☑ 优先使用低电量车辆充电 │ │
│ │ ☐ 启用充电错误检测 ☐ 使用充电站点筛选 │ │
│ └───────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 就绪... [恢复默认] [保存] [应用] [取消] │
└─────────────────────────────────────────────────────────┘
```
## 注意事项
⚠️ **参数调整建议**
1. 不建议频繁修改配置
2. 修改前建议备份当前配置
3. 修改后观察系统运行情况
4. 根据实际情况逐步调整参数
⚠️ **安全提示**
1. 必充电量不宜设置过低(建议 ≥ 15%)
2. 满电电量不宜设置过高(建议 ≤ 95%)
3. 允许中断任务需谨慎开启
## 版本历史
- **v1.0** (2026-01-25): 初始版本,支持所有充电策略参数配置
---
**提示**:配置界面提供了完整的参数验证和默认值恢复功能,建议通过界面进行配置管理。
@@ -0,0 +1,527 @@
# 🔌 充电桩管理系统 - 完整文件清单
## 📁 已创建的文件
### 核心文件(必需)
| 文件名 | 类型 | 说明 | 行数 |
|--------|------|------|------|
| **ChargeStation.cs** | 数据模型 | 充电桩实体类,包含所有属性和验证逻辑 | ~150 |
| **ChargeStationDataService.cs** | 数据服务 | 单例模式数据管理类,负责增删改查和持久化 | ~300 |
| **ChargeStationManagementForm.cs** | UI主类 | 管理窗口的业务逻辑和事件处理 | ~350 |
| **ChargeStationManagementForm.Designer.cs** | UI设计 | 窗口控件的初始化和布局代码 | ~550 |
### 辅助文件(可选但推荐)
| 文件名 | 类型 | 说明 | 行数 |
|--------|------|------|------|
| **ChargeStationHelper.cs** | 工具类 | 提供静态辅助方法,简化调用 | ~350 |
| **ChargeStationManagementExample.cs** | 示例代码 | 10个使用示例,包含完整的调用代码 | ~400 |
### 文档文件
| 文件名 | 类型 | 说明 |
|--------|------|------|
| **README_ChargeStationManagement.md** | 完整文档 | 详细的功能说明、API文档和使用指南 |
| **QUICKSTART.md** | 快速开始 | 5分钟快速上手指南,包含集成步骤 |
| **INTERFACE_LAYOUT.txt** | 界面说明 | ASCII艺术格式的界面布局和操作说明 |
| **完整文件清单.md** | 本文件 | 所有文件的清单和使用说明 |
---
## 🎯 文件功能详解
### 1. ChargeStation.cs - 充电桩数据模型
**功能**
- 定义充电桩的所有属性(编号、名称、IP、端口、电压、电流等)
- 提供数据验证方法 `IsValid()`
- 自动计算功率 `Power`
- 自动生成唯一编号 `GenerateStationId()`
**关键属性**
```csharp
public string StationId { get; set; } // 唯一编号
public string Name { get; set; } // 名称
public string IpAddress { get; set; } // IP地址
public int Port { get; set; } // 端口
public double Voltage { get; set; } // 电压(V)
public double Current { get; set; } // 电流(A)
public ChargeStationStatus Status { get; set; } // 状态
public bool Enabled { get; set; } // 是否启用
public int? SiteId { get; set; } // 站点ID
public double Power => Voltage * Current; // 功率(W)
```
**状态枚举**
```csharp
public enum ChargeStationStatus {
Idle = 0, // 空闲
Charging = 1, // 充电中
Fault = 2, // 故障
Offline = 3, // 离线
Maintenance = 4, // 维护中
Reserved = 5 // 预约中
}
```
---
### 2. ChargeStationDataService.cs - 数据服务
**功能**
- 单例模式,全局唯一实例
- 数据持久化(JSON格式)
- 线程安全(使用锁机制)
- CRUD操作(增删改查)
**核心方法**
```csharp
// 单例获取
ChargeStationDataService.Instance
// 查询
List<ChargeStation> GetAllStations()
ChargeStation GetStationById(string stationId)
List<ChargeStation> GetIdleStations()
int GetChargingCount()
// 添加
bool AddStation(ChargeStation station, out string errorMessage)
// 更新
bool UpdateStation(ChargeStation station, out string errorMessage)
bool UpdateStationStatus(string stationId, ChargeStationStatus status)
// 删除
bool DeleteStation(string stationId, out string errorMessage)
// 刷新
void Reload()
```
**数据存储位置**
```
项目根目录/Data/ChargeStations.json
```
---
### 3. ChargeStationManagementForm.cs - 管理窗口
**功能**
- 充电桩列表显示(DataGridView
- 实时搜索
- 添加/编辑/删除充电桩
- 数据导出(JSON/CSV
- 统计信息显示
- 状态颜色标识
**主要方法**
```csharp
private void LoadStations() // 加载数据到列表
private void UpdateStatistics() // 更新统计信息
private void btnAdd_Click() // 新增按钮
private void btnSave_Click() // 保存按钮
private void btnDelete_Click() // 删除按钮
private void btnRefresh_Click() // 刷新按钮
private void btnExport_Click() // 导出按钮
private void dgvStations_CellDoubleClick() // 双击编辑
private void txtSearch_TextChanged() // 搜索
```
**界面布局**
- 左侧:充电桩列表 + 搜索 + 统计
- 右侧:编辑区 + 操作按钮
- 尺寸:1200×700(可调整,最小1000×600
---
### 4. ChargeStationManagementForm.Designer.cs - UI设计文件
**功能**
- 自动生成的设计器代码
- 包含所有控件的初始化
- 不建议手动修改
**主要控件**
```csharp
SplitContainer splitContainer // 分割容器
DataGridView dgvStations // 数据表格
TextBox txtSearch // 搜索框
TextBox txtName, txtIpAddress // 文本框
NumericUpDown numPort, numVoltage // 数字输入框
ComboBox cmbStatus // 下拉框
CheckBox chkEnabled // 复选框
Button btnAdd, btnSave, btnDelete // 按钮
Label lblStatistics, lblPower // 标签
```
---
### 5. ChargeStationHelper.cs - 辅助工具类
**功能**
- 提供静态辅助方法
- 简化常用操作
- 封装复杂逻辑
**常用方法**
```csharp
// 打开管理窗口
ChargeStationHelper.OpenManagementWindow()
// 获取充电桩
ChargeStation station = ChargeStationHelper.GetStationBySiteId(1001)
ChargeStation station = ChargeStationHelper.GetStationByIp("192.168.1.100")
// 检查可用性
bool available = ChargeStationHelper.IsSiteHasAvailableChargeStation(1001)
// 充电控制
bool success = ChargeStationHelper.StartCharging("CS123456", carId)
bool success = ChargeStationHelper.StopCharging("CS123456", carId)
// 故障标记
bool success = ChargeStationHelper.MarkAsFault("CS123456", "通信超时")
// 获取状态摘要
string summary = ChargeStationHelper.GetStatusSummary()
// 输出: "总数:10 | 空闲:6 | 充电中:3 | 故障:1 | 离线:0"
// 查找最近的空闲充电桩
ChargeStation station = ChargeStationHelper.FindNearestIdleStation(currentSiteId)
// 快速创建充电桩
bool success = ChargeStationHelper.QuickAddStation("1号充电桩", "192.168.1.100", 1001)
// 显示选择对话框
ChargeStation selected = ChargeStationHelper.ShowStationSelectionDialog(ChargeStationStatus.Idle)
// 批量更新在线状态
int updatedCount = ChargeStationHelper.UpdateOnlineStatus(timeout: 3000)
```
---
### 6. ChargeStationManagementExample.cs - 示例代码
**功能**
- 提供10个完整的使用示例
- 每个方法都带有 `[MethodMember]` 属性,可在系统中直接调用
**示例列表**
| 方法名 | 说明 |
|--------|------|
| `OpenManagementForm()` | 打开充电桩管理窗口 |
| `InitializeTestData()` | 初始化4个测试充电桩 |
| `ShowIdleStations()` | 显示所有空闲充电桩 |
| `ShowChargeStationStatistics()` | 显示统计信息 |
| `AssignChargeStationToCar()` | 为车辆分配充电桩 |
| `StartCharging()` | 开始充电 |
| `StopCharging()` | 结束充电 |
| `CheckChargeStationOnlineStatus()` | 检查在线状态 |
| `ExportChargeStationData()` | 导出数据 |
| `ClearAllChargeStationData()` | 清空所有数据 |
---
## 📖 使用指南
### 方式1:快速开始(推荐新手)
1. **阅读快速开始文档**
```
打开: QUICKSTART.md
```
2. **在主窗口添加菜单项**
```csharp
var menuItem = new ToolStripMenuItem("充电桩管理");
menuItem.Click += (s, e) => {
ChargeStationHelper.OpenManagementWindow();
};
```
3. **初始化测试数据**
```csharp
ChargeStationManagementExample.InitializeTestData();
```
4. **打开管理窗口测试**
- 点击菜单项
- 查看测试数据
- 尝试添加/编辑/删除
### 方式2:集成到现有代码(推荐高级用户)
1. **阅读完整文档**
```
打开: README_ChargeStationManagement.md
```
2. **在充电任务中集成**
```csharp
// 在 AbstractChargeMission.cs 中
using StandardScene.Charge;
// 选择充电站点时
var dataService = ChargeStationDataService.Instance;
var idleStations = dataService.GetIdleStations();
// 到达充电站时
ChargeStationHelper.StartCharging(stationId, carId);
// 离开充电站时
ChargeStationHelper.StopCharging(stationId, carId);
```
3. **添加实时监控**
```csharp
// 在主窗口添加定时器
private Timer statusTimer = new Timer { Interval = 3000 };
statusTimer.Tick += (s, e) => {
lblStatus.Text = ChargeStationHelper.GetStatusSummary();
};
statusTimer.Start();
```
### 方式3:参考示例代码(推荐学习)
1. **查看示例代码**
```
打开: ChargeStationManagementExample.cs
```
2. **运行示例方法**
```csharp
// 直接调用示例方法
ChargeStationManagementExample.ShowIdleStations();
ChargeStationManagementExample.ShowChargeStationStatistics();
```
3. **根据需求修改**
- 复制示例代码
- 根据实际需求调整
- 集成到项目中
---
## 🔧 配置说明
### 数据文件配置
**位置**
```
项目根目录/Data/ChargeStations.json
```
**格式**
```json
[
{
"StationId": "CS20240115123456",
"Name": "1号充电桩",
"IpAddress": "192.168.1.100",
"Port": 502,
"Voltage": 220.0,
"Current": 32.0,
"Status": 0,
"Enabled": true,
"SiteId": 1001,
"Remarks": "南区1号充电桩",
"CreatedTime": "2024-01-15T12:34:56",
"ModifiedTime": "2024-01-15T14:20:30"
}
]
```
### 权限要求
- `Data` 文件夹需要**读写权限**
- 如果保存失败,检查文件夹权限
### 性能配置
- 数据量 < 100个充电桩:无需优化
- 数据量 > 100个充电桩:考虑分页显示
- 搜索性能:实时搜索,无需优化
---
## 🎨 界面定制
### 修改窗口大小
在 `ChargeStationManagementForm.Designer.cs` 中:
```csharp
this.Size = new Size(1400, 800); // 修改为你需要的尺寸
```
### 修改按钮颜色
```csharp
btnAdd.BackColor = Color.LightGreen;
btnSave.BackColor = Color.LightBlue;
btnDelete.BackColor = Color.LightCoral;
```
### 修改字体
```csharp
this.Font = new Font("微软雅黑", 10F);
```
---
## 🐛 故障排除
### 问题1:窗口打不开
**原因**:命名空间引用错误
**解决**
```csharp
using StandardScene.Charge;
```
### 问题2:数据保存失败
**原因**:文件夹权限不足
**解决**
1. 右键 `Data` 文件夹
2. 属性 → 安全
3. 确保当前用户有"写入"权限
### 问题3:找不到数据
**原因**:首次运行未初始化
**解决**
```csharp
ChargeStationManagementExample.InitializeTestData();
```
### 问题4:编译错误
**原因**:缺少依赖项
**解决**
- 确保安装 `Newtonsoft.Json` NuGet 包
- 检查项目引用
---
## 📊 系统要求
### 软件要求
- .NET Framework 4.5 或更高版本
- Windows Forms
- Newtonsoft.JsonNuGet
### 硬件要求
- 内存:数据量小,几乎无影响
- 磁盘:每个充电桩约 1KB 数据
- CPUUI操作,几乎无影响
### 兼容性
- Windows 7/8/10/11
- 与现有 AGV 系统完全兼容
- 不影响现有功能
---
## 🚀 下一步计划
### 已完成功能 ✅
- [x] 充电桩数据模型
- [x] 数据持久化(JSON
- [x] 可视化管理界面
- [x] 增删改查功能
- [x] 搜索和过滤
- [x] 数据导出
- [x] 辅助工具类
- [x] 完整文档和示例
### 可扩展功能 💡
- [ ] 充电桩实时监控(通信状态)
- [ ] 充电历史记录
- [ ] 充电曲线图表
- [ ] 充电计费管理
- [ ] 充电桩分组管理
- [ ] 权限控制(不同用户不同权限)
- [ ] 远程控制(启动/停止充电)
- [ ] 告警推送(故障/离线)
- [ ] 数据分析(充电效率统计)
- [ ] 与现有 PLC 系统集成
---
## 📞 技术支持
### 文档位置
- **完整文档**: `README_ChargeStationManagement.md`
- **快速开始**: `QUICKSTART.md`
- **界面说明**: `INTERFACE_LAYOUT.txt`
- **本文件**: `完整文件清单.md`
### 示例代码
- **工具类**: `ChargeStationHelper.cs`
- **示例代码**: `ChargeStationManagementExample.cs`
### 日志调试
```csharp
// 查看充电桩相关日志
// 日志标签: "ChargeStation"
```
---
## ✅ 检查清单
在部署到生产环境前,请确认:
- [ ] 所有文件都已添加到项目
- [ ] `Newtonsoft.Json` NuGet 包已安装
- [ ] `Data` 文件夹有读写权限
- [ ] 已在主窗口添加菜单项或按钮
- [ ] 已初始化测试数据并测试
- [ ] 管理窗口可以正常打开
- [ ] 增删改查功能正常
- [ ] 数据保存和加载正常
- [ ] 搜索功能正常
- [ ] 导出功能正常
- [ ] 已阅读完整文档
- [ ] 已测试与现有系统的集成
---
## 📝 版本信息
**当前版本**: v1.0.0
**发布日期**: 2024-01-15
**开发者**: MDCS System
**许可**: 内部使用
---
## 🎉 恭喜!
您已经获得了一个完整的充电桩管理系统!
**快速开始**
1. 打开 `QUICKSTART.md`
2. 按照步骤操作
3. 5分钟内即可开始使用
**需要帮助?**
- 查看文档
- 运行示例代码
- 检查日志输出
祝您使用愉快! 🚀
@@ -0,0 +1,320 @@
# 充电桩报文分离解析使用示例
## 概述
系统现在支持**发送报文**和**接收报文**分开解析,然后自动合并更新到充电桩管理界面。
## 数据流向图
```
┌─────────────────┐
│ 应用程序 │
└────────┬────────┘
│ 发送充电指令
┌─────────────────────────────────────────────┐
│ CommunicationMessageService │
│ ┌─────────────────────────────────────┐ │
│ │ AddSendMessage() │ │
│ │ ├─ 记录发送报文 │ │
│ │ └─ ParseSendDataAndUpdateStation() │ │
│ │ ├─ 解析设定电压 │ │
│ │ ├─ 解析设定电流 │ │
│ │ └─ 更新充电桩设定值 │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
┌────────────────┐
│ ChargeStation │ ◄─── 设定值已更新
│ SetVoltage │
│ SetCurrent │
└────────┬───────┘
│ 同时...
┌─────────────────▼───────────────────────────┐
│ ChargeUdpService (监听端口40001) │
│ ├─ 接收充电桩返回的UDP报文 │
│ └─ 调用 AddReceiveMessage() │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ CommunicationMessageService │
│ ┌─────────────────────────────────────┐ │
│ │ AddReceiveMessage() │ │
│ │ ├─ 记录接收报文 │ │
│ │ └─ ParseReceiveDataAndUpdateStation()│ │
│ │ ├─ 解析实时电压 │ │
│ │ ├─ 解析实时电流 │ │
│ │ ├─ 解析充电状态 │ │
│ │ ├─ 解析机构状态 │ │
│ │ └─ 更新充电桩实时数据 │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
┌────────────────┐
│ ChargeStation │ ◄─── 实时值已更新
│ RealTimeVoltage│
│ RealTimeCurrent│
│ Status │
│ MechanismStatus│
└────────┬───────┘
┌─────────────────────────────┐
│ ChargeStationManagementForm │
│ (每2秒自动刷新) │
│ 显示: │
│ - 设定电压 vs 实时电压 │
│ - 设定电流 vs 实时电流 │
│ - 充电状态 │
│ - 机构状态 │
└─────────────────────────────┘
```
## 代码示例
### 1. 发送充电指令(自动解析发送报文)
```csharp
// 在充电桩类中发送充电指令
public void StartCharging(double voltage, double current)
{
// 构造发送报文
byte[] sendData = new byte[10];
sendData[5] = 1; // 充电指令:启动
sendData[6] = (byte)((int)(voltage * 10) >> 8); // 设定电压高字节
sendData[7] = (byte)((int)(voltage * 10) & 0xFF); // 设定电压低字节
sendData[8] = (byte)((int)(current * 10) >> 8); // 设定电流高字节
sendData[9] = (byte)((int)(current * 10) & 0xFF); // 设定电流低字节
// 发送UDP报文
udpClient.Send(sendData, sendData.Length, endPoint);
// 记录发送报文(自动解析并更新设定值)
var messageService = CommunicationMessageService.Instance;
messageService.AddSendMessage(
ipAddress: endPoint.Address.ToString(),
port: endPoint.Port,
rawData: string.Join(",", sendData),
stationId: this.StationId
);
// ✅ 此时充电桩的 SetVoltage 和 SetCurrent 已自动更新
}
```
### 2. 接收充电桩反馈(自动解析接收报文)
```csharp
// 在 ChargeUdpService 中接收报文
private static async void ListenerProcess()
{
var messageService = CommunicationMessageService.Instance;
using (UdpClient udpListener = new UdpClient(40001))
{
while (true)
{
var result = await udpListener.ReceiveAsync();
var remoteEndPoint = result.RemoteEndPoint;
var message = result.Buffer;
// 记录接收报文(自动解析并更新实时数据)
messageService.AddReceiveMessage(
ipAddress: remoteEndPoint.Address.ToString(),
port: 40001,
rawData: string.Join(",", message)
);
// ✅ 此时充电桩的实时数据已自动更新:
// - RealTimeVoltage(实时电压)
// - RealTimeCurrent(实时电流)
// - Status(充电状态)
// - MechanismStatus(机构状态)
// - BatteryLevel(电量百分比)
// - 等等...
}
}
}
```
### 3. 在充电桩管理界面查看合并后的数据
```csharp
// 在 ChargeStationManagementForm 中显示数据
private void LoadStations()
{
var stations = ChargeStationDataService.Instance.GetAllStations();
foreach (var station in stations)
{
// 显示设定值(来自发送报文解析)
Console.WriteLine($"设定电压: {station.SetVoltage}V");
Console.WriteLine($"设定电流: {station.SetElectricCurrent}A");
// 显示实时值(来自接收报文解析)
Console.WriteLine($"实时电压: {station.RealTimeVoltage}V");
Console.WriteLine($"实时电流: {station.RealTimeCurrent}A");
Console.WriteLine($"充电状态: {GetStatusText(station.Status)}");
Console.WriteLine($"机构状态: {GetMechanismStatusText(station.MechanismStatus)}");
Console.WriteLine($"电量: {station.BatteryLevel}%");
// 显示通讯时间
Console.WriteLine($"最后发送: {station.LastSendTime}");
Console.WriteLine($"最后接收: {station.LastReceiveTime}");
}
}
```
## 解析流程详解
### 发送报文解析流程
```
AddSendMessage()
ParseSendDataAndUpdateStation()
ParseSendRawData() ← 解析发送报文
├─ ChargeCommand (字节5)
├─ SetVoltage (字节6-7)
└─ SetCurrent (字节8-9)
UpdateStationFromSendData() ← 更新设定值
├─ station.SetVoltage = parsedData.SetVoltage
├─ station.SetElectricCurrent = parsedData.SetCurrent
├─ station.LastSendTime = parsedData.SendTime
└─ station.ChargeCommandStatus = ...
```
### 接收报文解析流程
```
AddReceiveMessage()
ParseReceiveDataAndUpdateStation()
ParseReceiveRawData() ← 解析接收报文
├─ CommStatus (字节28)
├─ ChargeCommandStatus (字节10)
├─ MechanismStatus (字节11)
├─ RealTimeVoltage (字节12-13)
├─ RealTimeCurrent (字节14-15)
├─ BatteryLevel (字节16)
├─ HasAlarm (字节20)
├─ Status (字节25)
└─ CurrentVehicleId (字节26-29)
UpdateStationFromReceiveData() ← 更新实时值
├─ station.RealTimeVoltage = parsedData.RealTimeVoltage
├─ station.RealTimeCurrent = parsedData.RealTimeCurrent
├─ station.Status = parsedData.Status
├─ station.MechanismStatus = parsedData.MechanismStatus
├─ station.BatteryLevel = parsedData.BatteryLevel
├─ station.LastReceiveTime = parsedData.ReceiveTime
└─ ...
```
## 数据对比示例
| 数据项 | 来源 | 更新时机 | 用途 |
|-------|------|---------|------|
| SetVoltage | 发送报文 | 发送充电指令时 | 显示设定的目标电压 |
| RealTimeVoltage | 接收报文 | 接收充电桩反馈时 | 显示当前实际电压 |
| SetElectricCurrent | 发送报文 | 发送充电指令时 | 显示设定的目标电流 |
| RealTimeCurrent | 接收报文 | 接收充电桩反馈时 | 显示当前实际电流 |
| ChargeCommandStatus | 发送+接收 | 发送指令时更新,接收反馈时确认 | 显示充电指令执行状态 |
| Status | 接收报文 | 接收充电桩反馈时 | 显示充电桩当前状态 |
| MechanismStatus | 接收报文 | 接收充电桩反馈时 | 显示机构伸缩状态 |
| BatteryLevel | 接收报文 | 接收充电桩反馈时 | 显示电池电量百分比 |
| LastSendTime | 发送报文 | 发送充电指令时 | 显示最后发送时间 |
| LastReceiveTime | 接收报文 | 接收充电桩反馈时 | 显示最后接收时间 |
## 优势
**数据分离**:发送和接收数据各自独立,不会互相覆盖
**完整记录**:同时保留设定值和实时值,便于对比分析
**自动合并**:系统自动将两种数据合并到同一个充电桩对象
**实时更新**:界面每2秒自动刷新,显示最新数据
**易于调试**:可以清楚看到发送的指令和接收的反馈
## 调试技巧
### 1. 查看报文记录
```csharp
var messageService = CommunicationMessageService.Instance;
// 查看所有报文
var allMessages = messageService.GetAllMessages();
// 查看某个IP的报文
var ipMessages = messageService.GetMessagesByIp("192.168.1.101");
// 查看某个充电桩的报文
var stationMessages = messageService.GetMessagesByStationId("1");
foreach (var msg in allMessages)
{
Console.WriteLine($"[{msg.Direction}] {msg.IpAddress}:{msg.Port}");
Console.WriteLine($"时间: {msg.Timestamp}");
Console.WriteLine($"数据: {msg.RawData}");
Console.WriteLine("---");
}
```
### 2. 对比设定值与实时值
```csharp
var station = ChargeStationDataService.Instance.GetStationByIp("192.168.1.101");
if (station != null)
{
// 电压对比
double voltageDiff = Math.Abs(station.SetVoltage - station.RealTimeVoltage);
Console.WriteLine($"电压偏差: {voltageDiff}V");
// 电流对比
double currentDiff = Math.Abs(station.SetElectricCurrent - station.RealTimeCurrent);
Console.WriteLine($"电流偏差: {currentDiff}A");
// 通讯延迟
if (station.LastSendTime != null && station.LastReceiveTime != null)
{
var delay = station.LastReceiveTime.Value - station.LastSendTime.Value;
Console.WriteLine($"通讯延迟: {delay.TotalMilliseconds}ms");
}
}
```
### 3. 监控解析错误
如果数据没有更新,检查以下几点:
1. **IP地址是否匹配**:确保充电桩的IP地址在管理列表中
2. **报文长度是否足够**:发送报文至少10字节,接收报文至少30字节
3. **字节位置是否正确**:根据实际协议调整字节位置
4. **数据类型是否正确**:检查高低字节顺序、单位换算等
## 注意事项
⚠️ **字节位置**:示例中的字节位置仅供参考,请根据实际通讯协议调整
⚠️ **报文格式**:确保发送和接收的报文格式与解析规则一致
⚠️ **异常处理**:解析失败不会影响报文记录,但数据不会更新
⚠️ **线程安全**`CommunicationMessageService` 使用单例模式,内部已做线程同步
## 总结
通过分离解析发送和接收报文,系统可以:
- 准确记录每次发送的指令参数
- 准确获取充电桩的实时反馈
- 自动合并两种数据到充电桩管理界面
- 便于对比分析和故障诊断
这种设计使得充电桩管理更加精确和可靠!
@@ -0,0 +1,26 @@
UDP测试数据
[2026/01/16-16:11:55.679] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F6 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 00 F6 EE
[2026/01/16-16:11:56.260] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F7 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 80 F5 EE
[2026/01/16-16:11:56.835] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F8 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 80 EE EE
[2026/01/16-16:11:57.394] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F9 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 00 ED EE
[2026/01/16-16:14:49.300] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 28 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 38 EE
[2026/01/16-16:14:49.843] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 29 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 84 3B EE
[2026/01/16-16:14:50.410] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 2A 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 44 3D EE
[2026/01/16-16:14:50.943] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 2B 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 3E EE
[2026/01/19-17:35:58.294] >IsSafe:False ChargeIP: 192.168.100.74: BB 74 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:35:58.885] >IsSafe:False ChargeIP: 192.168.100.74: BB 75 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:35:59.465] >IsSafe:False ChargeIP: 192.168.100.74: BB 76 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:36:00.020] >IsSafe:False ChargeIP: 192.168.100.74: BB 77 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
TCP 发送指令
BB 00 42 48 00 00 42 5C 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EE // 启动
BB 00 42 48 00 00 42 5C 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EE // 停止
TCP返回指令
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 EE //充电
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 EE //缩回
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 EE //充电
@@ -0,0 +1,254 @@
# 脚本生成特性汇总表
> 本文档汇总了工程中所有的 `TemplateTrackCoderSettings`、`TemplateSiteCoderSettings` 和 `ProgramTrackCoderSettings` 特性配置。
---
## 关键参数说明
| 参数 | 说明 |
|------|------|
| **priority** | 优先级,数值越大则脚本生成顺序越靠前 |
| **useVerb** | 判断条件,为 true 时生成 templateString |
| **blockVerb** | 如果为 true,且 useVerb 通过,则低于此 priority 的脚本不再继续判断 |
| **templateString** | 生成的脚本模板,支持 `${变量}` 语法 |
| **siteFields** | 站点字段类型定义 |
| **trackFields** | 路径字段类型定义 |
| **planFields** | 计划字段类型定义 |
---
## 执行流程说明
1.**priority 从大到小** 依次检查每个 CoderSettings
2. 如果 **useVerb** 条件为 true,则生成 **templateString** 脚本
3. 如果 **blockVerb** 为 true 且 useVerb 通过,则 **阻止低优先级脚本继续执行**
4. ProgramTrackCoderSettings 使用自定义的 `ITrackCoder` 程序来生成脚本
---
## 一、TemplateTrackCoderSettings(路径脚本)
### 1. MultiWheelLifterCar.cs - 多舵轮顶升车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 30 | `track.SleepTime!=0` | false | `agv.Wait();agv.Sleep(${track.SleepTime});agv.Wait();` | 路径上休眠 |
| 30 | `track.TrayTarget!=0` | false | `agv.Wait();agv.TrayControl(${track.TrayTarget});agv.Wait();` | 托盘控制 |
| 27 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceDistance(${track.StopDistance},${track.SlowDistance}); });` | 更改避障距离 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `dst.ChangeAvoidanceParam==true` | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth},${dst.CarCenterX},${dst.CarCenterY}); });` | 切换避障尺寸 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(${track.BiasAlarmThresh},${track.DthAlarmThresh}); });` | 更改跟踪误差阈值 |
| 19 | `dst.tag>0 && src.tag>0` | true | `agv.QrGo(${src.x},${src.y},${src.id},${src.tag},${dst.x},${dst.y},${dst.id},${dst.tag},${track.id},...);` | 二维码导航 |
---
### 2. Kiva.cs - Kiva车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 30 | `dst.tag>0 && src.tag>0` | true | `agv.QrGo(...);` | 二维码导航 |
| 22 | `src.Shelf && plan.curSeg==1` | true | `agv.LeaveShelf(...);` | 离开货架 |
| 20 | `plan.action=='fetch' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Fetch(...);` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Put(...);` | 放货 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceDistance(...); });` | 更改避障距离 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth}); });` | 切换避障尺寸 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(...); });` | 更改跟踪误差阈值 |
| 17 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 10 | `track.CalibrateWheelEncoder && track.ReverseDst != dst.id` | true | `agv.CalibrateWheelEncoder(...);` | 标定轮里程计 |
---
### 3. Forklift.cs - 叉车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 22 | `src.Shelf` | true | `agv.LeaveShelf(...);agv.Wait();` | 离开货架 |
| 20 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `dst.CarLength != -1 && dst.CarWidth != -1` | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth}); });` | 切换避障尺寸 |
| 20 | `plan.action=='fetch' && dst.Shelf` | true | `agv.Wait();agv.Fetch(...);agv.Wait();` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf` | true | `agv.Wait();agv.Put(...);agv.Wait();` | 放货 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(...); });` | 更改跟踪误差阈值 |
---
### 4. MultiWheelForkLifter.cs - 多舵轮叉车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 20 | `plan.action=='fetch' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Wait();agv.Fetch(...);agv.Wait();` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Wait();agv.Put(...);agv.Wait();` | 放货 |
---
## 二、TemplateSiteCoderSettings(站点脚本)
### 1. Kiva.cs
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 25 | `plan.action=='fetch' && plan.segN == 1 && dst.Shelf` | true | `agv.FetchInPlace(...);agv.Wait();` | 原地取货 |
### 2. Forklift.cs
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 25 | `plan.action=='fetch' && plan.segN==1 && dst.Shelf` | true | `agv.FetchInPlace(...);agv.Wait();` | 原地取货 |
### 3. ArmCar.cs - 机械臂车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 5 | `plan.action=='pickFull' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PickFull("${dst.name}");agv.Wait();` | 满盘取货 |
| 5 | `plan.action=='pickEmpty' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PickEmpty("${dst.name}");agv.Wait();` | 空盘取货 |
| 5 | `plan.action=='putFull' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PutFull("${dst.name}");agv.Wait();` | 满盘放货 |
| 5 | `plan.action=='putEmpty' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PutEmpty("${dst.name}");agv.Wait();` | 空盘放货 |
| 5 | `plan.action=='MoveArm' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.MoveArm(${dst.MoveDirection});agv.Wait();` | 移动机械臂 |
---
## 三、ProgramTrackCoderSettings(程序化路径脚本)
| 车型 | Priority | Program | 说明 |
|------|----------|---------|------|
| MultiWheelLifterCar | 19 | `MagTrackCoder` | 磁导航路径编码 |
| Kiva | 19 | `AllCarMagTrackCoder` | 磁导航路径编码 |
| Kiva | 5 | `KivaCarTrackCoder` | Kiva旋转控制 |
---
## 四、字段类型定义
### BasicCarFields
```csharp
public float MagSlowSpeed = 0;
public float MagFullSpeed = 0;
```
### BasicSiteFields
```csharp
public bool Shelf = false;
public float CarLength = -1;
public float CarWidth = -1;
public float CarCenterX = 0;
public float CarCenterY = 0;
public int tag = -1;
public int TagValue = -1;// 磁导航,二维码值,或者rfid值
```
### BasicTrackFields
```csharp
public int IOArea = -1;
public int LidarArea = -2;
public float BiasAlarmThresh = -1;
public float DthAlarmThresh = -1;
public float Speed = 0.2f;
public bool Reverse = false;
public int ReverseDst = -1;
public bool SwitchBarrier = false;
public bool CalibrateWheelEncoder = false;
public float CarDirectionBias = 0;
public bool EnableCarAbsoluteDirection = false;
public float CarAbsoluteDirection = 0;
public float SlowDistance = -1;
public float StopDistance = -1;
```
### BasicPlanFields
```csharp
public string action = "/";
public float CarLength = -1;
public float CarWidth = -1;
```
---
## 五、扩展字段类型
### MultiWheelLifterTrackFields (继承 BasicTrackFields)
```csharp
public int SleepTime = 0;
public float TrayTarget = 0;
```
### MultiWheelLifterSiteFields (继承 BasicSiteFields)
```csharp
public bool ChangeAvoidanceParam = false;
```
### KivaSiteFields (继承 BasicSiteFields)
```csharp
public int AngleTarget = 0;
public bool Turn = false;
public float FetchSpeed = 0;
public int FetchLidarArea = -2;
public int FetchIOArea = -1;
public bool FetchReverse = false;
public float FetchBlindMoveDist = 0;
public float FetchLiftDownTarget = -1;
public float FetchLiftUpTarget = -1;
public bool FetchUseQr = false;
public int FetchQrMode = -1;
public bool FetchIsUpQr = false;
public bool FetchUseDetector = false;
public int FetchDetector = 0;
public float FetchDetectWidth = -1;
public float FetchDetectDepth = -1;
public bool FetchLeaveSrcEarly = false;
public float FetchShieldObstacleDist = -1;
// ... 以及 Put 和 LeaveShelf 相关字段
```
### KivaTrackFields (继承 BasicTrackFields)
```csharp
public int ManeuverDir = 0;
public int ForwardDst = 0;
public int ForwardObChooseDst = -2;
public float BlindMoveDist = 0;
public bool UseDetector = false;
public int DetectorMode = -1;
public float DetectWidth = -1;
public float DetectDepth = -1;
public bool LeaveSrcEarly = false;
public float ShieldObstacleDist = -1;
```
### KivaPlanFields (继承 BasicPlanFields)
```csharp
public bool reverse = false;
public int level = 0;
```
---
## 六、ITrackCoder 接口
程序化脚本生成器需要实现 `ITrackCoder` 接口:
```csharp
public interface ITrackCoder
{
/// <summary>
/// 生成脚本代码
/// </summary>
/// <param name="plan">路径计划</param>
/// <param name="track">当前路径段</param>
/// <param name="src">起点</param>
/// <param name="dst">终点</param>
/// <param name="i">段索引</param>
/// <returns>是否成功生成</returns>
bool Code(SegmentPlan plan, Track track, Site src, Site dst, int i);
/// <summary>
/// 是否阻止后续脚本生成
/// </summary>
bool toBlock();
}
```
---
*文档生成时间: 2024年*
Binary file not shown.
@@ -0,0 +1,944 @@
# DoorController 开发手册
## 目录
- [概述](#概述)
- [基础架构](#基础架构)
- [开发步骤](#开发步骤)
- [实现示例](#实现示例)
- [特性说明](#特性说明)
- [线程安全](#线程安全)
- [最佳实践](#最佳实践)
- [注意事项](#注意事项)
- [附录](#附录)
---
## 概述
`DoorController` 是门控制系统的核心组件,采用抽象基类设计,支持扩展不同类型的门控制器实现。本手册指导开发者如何创建自定义的门控制器。
### 核心概念
- **BasicDoorController**:门控制器抽象基类,定义通用接口和属性
- **DoorTypeAttribute**:类型特性,用于标记控制器类型,支持动态实例化
- **DoorState**:门状态枚举(Closed/Open/Unknown
- **DoorControllerState**:控制器状态枚举(Offline/Online/Connecting/Error
### 设计原则
1. **抽象化**:所有通信细节封装在具体实现类中
2. **线程安全**:状态读取和控制写入分离,通信操作在内部线程完成
3. **可扩展性**:通过 `DoorTypeAttribute` 实现类型自动识别和动态加载
4. **热更新支持**:配置变更无需重启任务
---
## 基础架构
### 1. 类继承关系
```
BasicDoorController (抽象基类)
└── ModbusDoorController (Modbus TCP 实现)
└── [其他实现...]
```
### 2. BasicDoorController 核心属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `Index` | int | 控制器索引(唯一标识) |
| `Ip` | string | IP地址 |
| `Port` | int | 端口号 |
| `State` | DoorControllerState | 控制器状态 |
| `IsOnline` | bool | 是否在线(只读) |
| `DoorStates` | Dictionary<int, DoorState> | 门状态字典 |
| `DoorControlTargets` | Dictionary<int, bool> | 门目标控制状态字典 |
| `DoorConfigs` | Dictionary<int, DoorModel> | 门配置信息字典 |
| `LastUpdateTime` | DateTime | 最后更新时间 |
| `ErrorMessage` | string | 错误信息 |
### 3. 核心方法
#### 3.1 必须实现的方法(抽象方法)
```csharp
/// <summary>
/// 读取门状态(开到位信号)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <returns>true=打开,false=关闭</returns>
public abstract bool ReadDoorState(int doorIndex);
/// <summary>
/// 写入门控制信号(开关控制)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <param name="open">true=打开,false=关闭</param>
public abstract void WriteDoorControl(int doorIndex, bool open);
```
#### 3.2 可重写的方法(虚方法)
```csharp
/// <summary>
/// 连接门控制器
/// </summary>
public virtual void Connect() { }
/// <summary>
/// 断开连接
/// </summary>
public virtual void Disconnect() { }
/// <summary>
/// 更新控制器状态
/// </summary>
public virtual void UpdateState(DoorControllerState newState, string errorMessage = "") { }
/// <summary>
/// 设置门的目标控制状态
/// </summary>
public virtual void SetDoorControlTarget(int doorIndex, bool open) { }
```
---
## 开发步骤
### 步骤1:创建控制器类
创建新类并继承 `BasicDoorController`
```csharp
using StandardScene.ExtendDevice.Door;
namespace StandardScene.ExtendDevice.Door
{
public class MyDoorController : BasicDoorController
{
// 实现抽象方法
}
}
```
### 步骤2:添加 DoorTypeAttribute
使用 `DoorTypeAttribute` 标记控制器类型:
```csharp
[DoorType("MyDoorController")]
public class MyDoorController : BasicDoorController
{
// ...
}
```
**注意**`DoorTypeAttribute``Name` 参数将显示在配置界面的类型下拉框中,并用于配置文件中的类型标识。
### 步骤3:实现抽象方法
实现 `ReadDoorState``WriteDoorControl` 方法:
```csharp
public override bool ReadDoorState(int doorIndex)
{
// 读取门状态逻辑
// 返回 true=打开,false=关闭
}
public override void WriteDoorControl(int doorIndex, bool open)
{
// 写入门控制信号逻辑
}
```
### 步骤4:重写 Connect/Disconnect 方法
如果需要初始化连接、启动后台任务等,重写 `Connect``Disconnect` 方法:
```csharp
public override void Connect()
{
base.Connect(); // 调用基类方法更新状态
// 初始化连接
// 启动后台任务
// 读取初始状态
}
public override void Disconnect()
{
// 停止后台任务
// 关闭连接
base.Disconnect(); // 调用基类方法更新状态
}
```
### 步骤5:实现线程安全的控制逻辑
如果需要定时读取状态或根据 `DoorControlTargets` 下发控制指令,实现后台任务:
```csharp
private CancellationTokenSource _cancellationTokenSource;
private Task _readTask;
public override void Connect()
{
base.Connect();
// 启动定时读取任务
_cancellationTokenSource = new CancellationTokenSource();
_readTask = Task.Run(() => ReadDoorStatesLoop(_cancellationTokenSource.Token));
}
private void ReadDoorStatesLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// 读取所有门的状态
ReadAllDoorStates();
// 根据 DoorControlTargets 下发控制指令
ApplyDoorControlTargets();
Thread.Sleep(ReadInterval);
}
}
```
---
## 实现示例
### 示例1ModbusDoorController(完整实现)
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StandardScene.Utils;
using SimpleCore.Library;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// Modbus 门控制器实现
/// </summary>
[DoorType("ModbusDoorController")]
public class ModbusDoorController : BasicDoorController
{
private ModbusRtu _modbusClient;
private readonly object _syncLock = new object();
private bool _isStarted = false;
private readonly Dictionary<int, bool> _lastSentControl = new Dictionary<int, bool>();
private CancellationTokenSource _cancellationTokenSource;
private Task _readTask;
// 可配置参数
public int ReadInterval { get; set; } = 1000; // 读取间隔(毫秒)
public int ReconnectInterval { get; set; } = 3000; // 重连间隔(毫秒)
public byte SlaveAddress { get; set; } = 1; // Modbus 从站地址
/// <summary>
/// 线程安全的设置门控制目标
/// </summary>
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock)
{
base.SetDoorControlTarget(doorIndex, open);
}
}
/// <summary>
/// 连接门控制器
/// </summary>
public override void Connect()
{
lock (_syncLock)
{
if (_isStarted) return;
try
{
UpdateState(DoorControllerState.Connecting);
// 初始化门状态
var doorIndices = DoorConfigs.Keys.OrderBy(k => k).ToList();
InitializeDoors(doorIndices);
// 初始化最近一次已下发的控制状态
_lastSentControl.Clear();
foreach (var index in doorIndices)
{
_lastSentControl[index] = false;
if (!DoorControlTargets.ContainsKey(index))
{
DoorControlTargets[index] = false;
}
}
// 连接 Modbus TCP
try
{
_modbusClient = new ModbusRtu();
_modbusClient.StartTcpRtu(Ip, Port);
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Connecting);
Diagnosis.Log($"ModbusDoorController[{Index}] 初次连接失败: {ex.Message}", "ModbusDoorController", true);
}
// 启动定时读取任务
_cancellationTokenSource = new CancellationTokenSource();
_readTask = Task.Run(() => ReadDoorStatesLoop(_cancellationTokenSource.Token));
_isStarted = true;
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"初始化失败: {ex.Message}");
_isStarted = false;
}
}
}
/// <summary>
/// 断开连接
/// </summary>
public override void Disconnect()
{
lock (_syncLock)
{
if (!_isStarted) return;
try
{
_cancellationTokenSource?.Cancel();
_readTask?.Wait(1000);
_modbusClient?.Close();
_modbusClient = null;
_isStarted = false;
UpdateState(DoorControllerState.Offline);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"断开连接失败: {ex.Message}");
}
}
}
/// <summary>
/// 定时读取门状态循环
/// </summary>
private void ReadDoorStatesLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (!_isStarted) break;
// 检查连接状态
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
if (_modbusClient == null || !isConnected)
{
UpdateState(DoorControllerState.Connecting);
TryReconnect();
isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
if (!isConnected)
{
Thread.Sleep(ReadInterval);
continue;
}
}
// 读取所有门的状态
ReadAllDoorStates();
// 根据目标控制状态下发控制指令
ApplyDoorControlTargets();
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 读取状态失败: {ex.Message}", "ModbusDoorController", true);
UpdateState(DoorControllerState.Error, $"读取状态失败: {ex.Message}");
TryReconnect();
}
Thread.Sleep(ReadInterval);
}
}
/// <summary>
/// 读取所有门的状态
/// </summary>
private void ReadAllDoorStates()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
try
{
var state = ReadDoorState(doorConfig.Index);
UpdateDoorState(doorConfig.Index, state ? DoorState.Open : DoorState.Closed);
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 读取门{doorConfig.Index}状态失败: {ex.Message}", "ModbusDoorController", true);
}
}
}
}
/// <summary>
/// 根据 DoorControlTargets 下发控制指令
/// </summary>
private void ApplyDoorControlTargets()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
var doorIndex = doorConfig.Index;
// 获取目标控制状态
bool target = false;
DoorControlTargets.TryGetValue(doorIndex, out target);
// 获取上一次已下发的状态
bool last;
var hasLast = _lastSentControl.TryGetValue(doorIndex, out last);
// 如果没有记录或状态发生变化,则下发控制
if (!hasLast || last != target)
{
try
{
WriteDoorControl(doorIndex, target);
_lastSentControl[doorIndex] = target;
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 下发门{doorIndex}控制指令失败: {ex.Message}", "ModbusDoorController", true);
}
}
}
}
}
/// <summary>
/// 读取门状态(开到位信号)
/// </summary>
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
throw new InvalidOperationException("Modbus连接未建立");
}
// 读取开到位信号(离散输入)
var data = _modbusClient.ReadDiscreteInputs_02(SlaveAddress, doorConfig.OpenStatusAddress, 1);
return data != null && data.Length > 0 && data[0];
}
}
/// <summary>
/// 写入门控制信号(开关控制)
/// </summary>
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
TryReconnect();
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
throw new InvalidOperationException("Modbus连接未建立");
}
}
// 写入开关控制信号(线圈)
_modbusClient.WriteMultipleCoils_15(SlaveAddress, doorConfig.ControlAddress, new[] { open });
}
}
private void TryReconnect()
{
// 重连逻辑...
}
}
}
```
### 示例2:简单门控制器(最小实现)
如果不需要定时读取或后台任务,可以实现最简单的版本:
```csharp
using System;
using StandardScene.ExtendDevice.Door;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 简单门控制器实现(同步模式)
/// </summary>
[DoorType("SimpleDoorController")]
public class SimpleDoorController : BasicDoorController
{
private SimpleDoorClient _client;
public override void Connect()
{
base.Connect();
_client = new SimpleDoorClient(Ip, Port);
UpdateState(DoorControllerState.Online);
}
public override void Disconnect()
{
_client?.Close();
_client = null;
base.Disconnect();
}
public override bool ReadDoorState(int doorIndex)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_client == null || !_client.IsConnected)
{
throw new InvalidOperationException("连接未建立");
}
// 读取门状态
return _client.ReadDoorStatus(doorConfig.OpenStatusAddress);
}
public override void WriteDoorControl(int doorIndex, bool open)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_client == null || !_client.IsConnected)
{
throw new InvalidOperationException("连接未建立");
}
// 写入控制信号
_client.WriteDoorControl(doorConfig.ControlAddress, open);
// 同步更新门状态(可选)
var state = _client.ReadDoorStatus(doorConfig.OpenStatusAddress);
UpdateDoorState(doorIndex, state ? DoorState.Open : DoorState.Closed);
}
/// <summary>
/// 重写 SetDoorControlTarget 以立即执行控制
/// </summary>
public override void SetDoorControlTarget(int doorIndex, bool open)
{
base.SetDoorControlTarget(doorIndex, open);
// 立即执行控制(同步模式)
try
{
WriteDoorControl(doorIndex, open);
}
catch (Exception ex)
{
Diagnosis.Log($"SimpleDoorController[{Index}] 控制门{doorIndex}失败: {ex.Message}", "SimpleDoorController", true);
}
}
}
}
```
---
## 特性说明
### DoorTypeAttribute
`DoorTypeAttribute` 用于标记门控制器类型,支持动态实例化。
**定义**
```csharp
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class DoorTypeAttribute : Attribute
{
public string Name { get; }
public DoorTypeAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
```
**使用**
```csharp
[DoorType("MyDoorController")]
public class MyDoorController : BasicDoorController
{
// ...
}
```
**作用**
1. **类型标识**`Name` 参数作为类型的唯一标识,用于配置文件中指定类型
2. **UI显示**:配置界面会自动识别并显示所有带此特性的控制器类型
3. **动态实例化**`DoorMission` 根据 `Name` 动态查找并创建实例
**注意**
- `Name` 必须唯一
- 建议使用类名作为 `Name`
- `Name` 会显示在配置界面的类型下拉框中
---
## 线程安全
### 1. 设计原则
门控制器采用**读写分离**的线程安全设计:
- **读取**`DoorMission` 通过 `controller.DoorStates` 直接访问门状态(受锁保护)
- **写入**`DoorMission` 通过 `controller.SetDoorControlTarget()` 设置目标状态,实际通信由门控制器内部线程完成
### 2. 线程安全要求
#### 2.1 SetDoorControlTarget 方法
如果多个线程可能同时调用 `SetDoorControlTarget`,必须加锁保护:
```csharp
private readonly object _syncLock = new object();
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock)
{
base.SetDoorControlTarget(doorIndex, open);
}
}
```
#### 2.2 ReadDoorState 和 WriteDoorControl 方法
如果这些方法会被多个线程调用,必须加锁保护:
```csharp
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock)
{
// 读取逻辑
}
}
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock)
{
// 写入逻辑
}
}
```
#### 2.3 后台任务访问共享资源
如果后台任务会访问 `DoorStates``DoorControlTargets` 等共享资源,必须加锁:
```csharp
private void ReadAllDoorStates()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
var state = ReadDoorState(doorConfig.Index);
UpdateDoorState(doorConfig.Index, state ? DoorState.Open : DoorState.Closed);
}
}
}
private void ApplyDoorControlTargets()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
// 访问 DoorControlTargets
// 调用 WriteDoorControl
}
}
}
```
### 3. 推荐实现模式
推荐使用单一锁对象保护所有共享资源:
```csharp
public class MyDoorController : BasicDoorController
{
private readonly object _syncLock = new object();
// 所有访问共享资源的方法都使用同一个锁
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock) { /* ... */ }
}
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock) { /* ... */ }
}
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock) { /* ... */ }
}
private void ReadAllDoorStates()
{
lock (_syncLock) { /* ... */ }
}
private void ApplyDoorControlTargets()
{
lock (_syncLock) { /* ... */ }
}
}
```
---
## 最佳实践
### 1. 错误处理
- **连接错误**:设置状态为 `Connecting``Error`,并记录错误信息
- **读取错误**:记录日志,但不抛出异常,返回默认值或保持当前状态
- **写入错误**:记录日志,尝试重连,但不影响其他门的操作
**示例**
```csharp
public override bool ReadDoorState(int doorIndex)
{
try
{
// 读取逻辑
}
catch (Exception ex)
{
Diagnosis.Log($"读取门{doorIndex}状态失败: {ex.Message}", "MyDoorController", true);
return false; // 返回默认值
}
}
```
### 2. 状态管理
- **及时更新状态**:在连接、断开、错误时及时调用 `UpdateState()`
- **更新最后更新时间**:在状态变化时更新 `LastUpdateTime`
- **错误信息**:在错误时记录详细的错误信息到 `ErrorMessage`
**示例**
```csharp
try
{
_client.Connect();
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"连接失败: {ex.Message}");
}
```
### 3. 资源释放
- **实现 Disconnect**:确保正确关闭连接和释放资源
- **实现析构函数**:作为最后的安全网,确保资源释放
**示例**
```csharp
public override void Disconnect()
{
try
{
_cancellationTokenSource?.Cancel();
_readTask?.Wait(1000);
_client?.Close();
_client = null;
UpdateState(DoorControllerState.Offline);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"断开连接失败: {ex.Message}");
}
}
~MyDoorController()
{
Disconnect();
}
```
### 4. 配置验证
`Connect()` 中验证配置的完整性:
```csharp
public override void Connect()
{
if (string.IsNullOrWhiteSpace(Ip))
{
UpdateState(DoorControllerState.Error, "IP地址未配置");
return;
}
if (Port <= 0 || Port > 65535)
{
UpdateState(DoorControllerState.Error, "端口号无效");
return;
}
if (DoorConfigs.Count == 0)
{
UpdateState(DoorControllerState.Error, "未配置门");
return;
}
// 连接逻辑...
}
```
---
## 注意事项
### 1. DoorTypeAttribute 命名
- `Name` 必须与配置文件中使用的类型名称一致
- 建议使用类名作为 `Name`
- 避免使用特殊字符
### 2. 异常处理
- **不要抛出未处理的异常**:所有异常都应该被捕获并记录
- **不要阻塞线程**:长时间操作应该在后台线程中执行
- **提供错误信息**:通过 `ErrorMessage` 属性提供详细的错误信息
### 3. 性能考虑
- **避免频繁的连接/断开**:保持连接持久化
- **批量读取**:如果可能,批量读取多个门的状态
- **控制读取频率**:根据实际需求设置合理的读取间隔
### 4. 兼容性
- **向后兼容**:新版本应该兼容旧版本的配置格式
- **版本标识**:如果需要,可以在实现中添加版本检查
---
## 附录
### A. DoorModel 说明
```csharp
public class DoorModel
{
/// <summary>
/// 门索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// 开关控制信号地址
/// </summary>
public ushort ControlAddress { get; set; }
/// <summary>
/// 开到位信号地址
/// </summary>
public ushort OpenStatusAddress { get; set; }
}
```
### B. DoorState 枚举
```csharp
public enum DoorState
{
Closed = 0, // 关闭
Open = 1, // 打开
Unknown = 2 // 未知状态
}
```
### C. DoorControllerState 枚举
```csharp
public enum DoorControllerState
{
Offline = 0, // 离线
Online = 1, // 在线
Connecting = 2, // 连接中
Error = 3 // 错误
}
```
### D. 开发检查清单
- [ ] 继承 `BasicDoorController`
- [ ] 添加 `DoorTypeAttribute` 特性
- [ ] 实现 `ReadDoorState` 方法
- [ ] 实现 `WriteDoorControl` 方法
- [ ] 重写 `Connect` 方法(如需要)
- [ ] 重写 `Disconnect` 方法(如需要)
- [ ] 实现线程安全(如需要)
- [ ] 添加错误处理
- [ ] 添加资源释放逻辑
- [ ] 添加诊断日志
- [ ] 测试连接/断开
- [ ] 测试读取状态
- [ ] 测试控制写入
- [ ] 测试配置热更新
---
**文档更新时间**2025-01-09
@@ -0,0 +1,634 @@
# DoorMission 使用手册
## 目录
- [概述](#概述)
- [基本功能](#基本功能)
- [启动与停止](#启动与停止)
- [配置管理](#配置管理)
- [门控逻辑](#门控逻辑)
- [站点配置](#站点配置)
- [UI界面](#ui界面)
- [参数说明](#参数说明)
- [故障排查](#故障排查)
- [附录](#附录)
---
## 概述
`DoorMission` 是一个门控进程类,用于管理多个门控制器及其关联的门。它提供了以下核心功能:
- 多门控制器管理(支持不同类型)
- 自动门控逻辑(根据小车位置自动开关门)
- 手动控制功能(支持临时手动控制,优先级高于自动控制)
- 车辆占用管理(跟踪和清空门区域的车辆占用)
- 配置文件动态监控(支持热更新)
- 线程安全的门状态读写
- 可视化的配置和监控界面
### 架构特点
- **抽象化设计**:门控制器通过抽象基类 `BasicDoorController` 实现,支持扩展不同类型的控制器
- **线程安全**:门状态读取和控制写入分离,所有通信操作在门控制器内部线程完成
- **事件驱动**:与交通控制系统集成,响应小车进入/离开站点事件
- **热更新支持**:配置文件每10秒自动检查更新,无需重启任务
- **控制仲裁机制**:手动控制优先级高于自动控制,手动控制过期后自动恢复自动模式
---
## 基本功能
### 1. 门控制器管理
#### 1.1 门控制器类型
门控制器通过 `DoorTypeAttribute` 标记类型,系统会自动识别并创建实例。当前支持:
- **ModbusDoorController**:基于 Modbus TCP 的门控制器
#### 1.2 门控制器配置
每个门控制器包含以下配置:
- **Index**:控制器索引(唯一标识)
- **Ip**IP地址
- **Port**:端口号(默认502
- **Type**:控制器类型(通过 `DoorTypeAttribute.Name` 指定)
- **Doors**:门列表
#### 1.3 门配置
每个门包含以下配置:
- **Index**:门索引(在控制器内唯一)
- **ControlAddress**:开关控制信号地址(Modbus 线圈地址)
- **OpenStatusAddress**:开到位信号地址(Modbus 离散输入地址)
### 2. 自动门控逻辑
#### 2.1 门开启条件
门会在以下情况自动开启:
1. **小车即将进入区域**:当小车到达站点且站点的 `EnterDoor` 字段匹配时,门会在锁定前开启
2. **小车在区域内**:当小车已进入并锁定站点时,门保持开启状态
#### 2.2 门关闭条件
门会在以下情况自动关闭:
- 小车离开区域后,门自动关闭
#### 2.3 门标识符格式
站点配置中的门标识符格式为:**控制器索引.门索引**
**示例**
```
"EnterDoor": "1.2" // 表示控制器索引1,门索引2
"LeaveDoor": "2.3" // 表示控制器索引2,门索引3
```
### 3. 配置文件动态监控
系统每10秒自动检查 `DoorConfig.json` 文件,并根据配置变化:
- **添加**:新增的门控制器会自动创建并连接
- **删除**:已移除的门控制器会自动断开并移除
- **修改**:已修改的门控制器会自动更新(IP、端口、类型或门配置变化)
---
## 启动与停止
### 1. 启动任务
```csharp
var doorMission = new DoorMission();
doorMission.Execute(); // 执行"启动进程"
```
**启动流程**
1. 设置数据文件路径(`DoorConfig.json`
2. 订阅交通控制事件(`BeforeLock``AfterLeave``OnLockAcquired`
3. 立即加载一次配置(避免监控界面在首次轮询前无数据)
4. 启动配置监控任务(每10秒检查一次)
5. 启动门控逻辑监控任务(每500毫秒检查一次)
### 2. 停止任务
```csharp
doorMission.Stop(); // 执行"停止进程"
```
**停止流程**
1. 取消事件订阅
2. 取消所有后台任务
3. 断开所有门控制器连接
4. 等待任务完成(最多等待5秒)
### 3. 公共方法
#### 3.1 读取门状态
```csharp
bool isOpen = doorMission.GetDoorState(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**`true`=打开,`false`=关闭
#### 3.2 设置门控制目标(自动模式)
```csharp
doorMission.SetDoorControlTarget(controllerIndex, doorIndex, open);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- `open``true`=打开,`false`=关闭
**注意**:此方法仅设置目标控制状态,实际通信由门控制器内部线程完成,确保线程安全。此方法会立即生效,但可能被手动控制覆盖。
#### 3.3 设置手动控制目标
```csharp
bool success = doorMission.SetManualDoorControl(controllerIndex, doorIndex, open, holdSeconds);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- `open``true`=打开,`false`=关闭
- `holdSeconds`:手动保持秒数(可选,默认10秒)
- **返回值**`true`=成功,`false`=失败(当有车辆占用且尝试关闭时返回false)
**功能说明**
- 手动控制优先级高于自动控制
- 手动控制会在指定时间后自动过期,恢复自动模式
- **安全保护**:当门区域内有车辆占用时,禁止手动关闭门
- 手动控制过期后,系统自动恢复自动控制逻辑
#### 3.4 清除手动控制
```csharp
doorMission.ClearManualDoorControl(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
**功能说明**:立即清除手动控制请求,恢复自动控制模式。
#### 3.5 清空车辆占用
```csharp
doorMission.ClearCarsInArea(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
**功能说明**:清空指定门的车辆占用记录。清空后,如果门处于打开状态且没有其他小车需要进入,门会自动关闭。
#### 3.6 获取门控制状态
```csharp
var status = doorMission.GetDoorControlStatus(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**`DoorControlStatus` 对象,包含:
- `Target`:当前目标状态(true=打开,false=关闭)
- `Source`:控制来源(`ControlSource.Auto``ControlSource.Manual`
- `ManualRemainingSeconds`:手动控制剩余秒数(仅当Source=Manual时有效)
- `CarsInArea`:车辆占用列表
#### 3.7 获取车辆占用情况
```csharp
var cars = doorMission.GetCarsInArea(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**:车辆ID列表(`IReadOnlyList<int>`
---
## 配置管理
### 1. 配置文件格式
配置文件 `DoorConfig.json` 位于程序根目录,格式如下:
```json
[
{
"Index": 1,
"Ip": "192.168.1.100",
"Port": 502,
"Type": "ModbusDoorController",
"Doors": [
{
"Index": 1,
"ControlAddress": 0,
"OpenStatusAddress": 0
},
{
"Index": 2,
"ControlAddress": 1,
"OpenStatusAddress": 1
}
]
},
{
"Index": 2,
"Ip": "192.168.1.101",
"Port": 502,
"Type": "ModbusDoorController",
"Doors": [
{
"Index": 1,
"ControlAddress": 0,
"OpenStatusAddress": 0
}
]
}
]
```
### 2. 配置界面
通过调用 `DoorMission.OpenViewer()` 打开门控制器管理界面,可以:
- 添加、删除、修改门控制器
- 为每个门控制器添加、删除、修改门配置
- 保存配置到 `DoorConfig.json`
**打开配置界面**
```csharp
DoorMission.OpenViewer();
```
---
## 门控逻辑
### 1. 事件响应流程
#### 1.1 BeforeLock 事件
当小车即将锁定站点时触发:
1. 检查站点的 `EnterDoor` 字段
2. 检查小车当前站点的 `PreEnterDoor` 字段是否匹配
3. 如果匹配,设置 `_needOpen[(controllerIndex, doorIndex)] = true`
4. 返回门的当前状态(如果门已打开则允许锁定)
#### 1.2 OnLockAcquired 事件
当小车成功锁定站点时触发:
1. 检查站点的 `EnterDoor` 字段
2. 检查小车当前站点的 `PreEnterDoor` 字段是否匹配
3. 如果匹配,将小车ID添加到 `carsInAreas[(controllerIndex, doorIndex)]`
4. 设置 `_needOpen[(controllerIndex, doorIndex)] = false`
#### 1.3 AfterLeave 事件
当小车离开站点时触发:
1. 检查站点的 `LeaveDoor` 字段
2. 检查小车当前站点的 `RearLeaveDoor` 字段是否匹配
3. 如果匹配,从 `carsInAreas[(controllerIndex, doorIndex)]` 中移除小车ID
### 2. 门控状态监控与仲裁
`MonitorDoorLogicAsync` 任务每500毫秒执行一次,检查每个门的控制逻辑并进行仲裁:
```csharp
// 自动目标:有车或需要打开
var needOpen = _needOpen.TryGetValue(key, out var open) && open;
var hasCarsInArea = carsInAreas.TryGetValue(key, out var cars) && cars.Count > 0;
var autoTarget = needOpen || hasCarsInArea;
// 手动请求仲裁:优先级 Manual > Auto,手动过期后自动恢复
bool finalTarget = autoTarget;
if (_manualRequests.TryGetValue(key, out var manual))
{
if (manual.ExpireAt <= DateTime.Now)
{
_manualRequests.Remove(key); // 手动控制过期,移除
}
else
{
finalTarget = manual.Target; // 手动控制有效,使用手动目标
}
}
// 设置门的目标控制状态
controller.SetDoorControlTarget(doorIndex, finalTarget);
```
**逻辑说明**
- **自动目标计算**
- 如果 `_needOpen[key] = true`,门需要打开(小车即将进入)
- 如果 `carsInAreas[key]` 中有小车,门需要保持打开(小车在区域内)
- 其他情况,自动目标为关闭
- **控制仲裁**
- 手动控制优先级高于自动控制
- 如果存在有效的手动控制请求(未过期),使用手动目标
- 手动控制过期后,自动移除并恢复自动控制
- 最终目标写入门控制器的 `DoorControlTargets` 字段
---
## 站点配置
### 1. 站点字段说明
站点需要配置以下字段以实现门控功能:
| 字段名 | 类型 | 说明 | 示例 |
|--------|------|------|------|
| `EnterDoor` | string | 进站门标识符(格式:控制器索引.门索引) | `"1.2"` |
| `LeaveDoor` | string | 离站门标识符(格式:控制器索引.门索引) | `"2.3"` |
### 2. 小车站点字段说明
小车当前站点需要配置以下字段:
| 字段名 | 类型 | 说明 | 示例 |
|--------|------|------|------|
| `PreEnterDoor` | string | 前方进站门标识符(与目标站点的 `EnterDoor` 匹配) | `"1.2"` |
| `RearLeaveDoor` | string | 后方离站门标识符(与目标站点的 `LeaveDoor` 匹配) | `"2.3"` |
### 3. 配置示例
**站点配置**
```json
{
"id": 100,
"name": "站点A",
"fields": {
"EnterDoor": "1.2",
"LeaveDoor": "2.3"
}
}
```
**小车站点配置**
```json
{
"id": 50,
"name": "小车当前位置",
"fields": {
"PreEnterDoor": "1.2",
"RearLeaveDoor": "2.3"
}
}
```
**工作流程**
1. 小车从站点50驶向站点100
2. 到达站点100时,触发 `BeforeLock` 事件
3. 系统检查站点100的 `EnterDoor``"1.2"`)是否与站点50的 `PreEnterDoor``"1.2"`)匹配
4. 如果匹配,设置控制器1的门2为打开状态
5. 门打开后,小车锁定站点100
6. 触发 `OnLockAcquired` 事件,门保持打开状态
7. 小车离开站点100时,触发 `AfterLeave` 事件
8. 系统检查站点100的 `LeaveDoor``"2.3"`)是否与站点50的 `RearLeaveDoor``"2.3"`)匹配
9. 如果匹配,从区域内小车列表中移除该小车
10. 如果没有其他小车在区域内,门自动关闭
---
## UI界面
### 1. 配置管理界面
**打开方式**
```csharp
DoorMission.OpenViewer();
```
**功能**
- 门控制器列表管理(添加、删除、修改)
- 门列表管理(为每个控制器添加、删除、修改门)
- 类型选择(自动识别所有带 `DoorTypeAttribute` 的控制器类型)
- 配置保存到 `DoorConfig.json`
### 2. 监控界面
**打开方式**
```csharp
DoorMission.OpenMonitor();
```
**功能**
- 实时显示所有门的状态
- 显示控制器索引、门索引、当前状态、控制目标、车辆占用
- 显示控制来源和手动控制剩余时间
- 显示控制地址和开到位信号地址
- 手动控制门开关(打开/关闭,带安全保护)
- 清空车辆占用记录
- 自动刷新(默认1秒刷新间隔)
**显示信息**
| 列名 | 说明 |
|------|------|
| 控制器编码 | 门控制器索引 |
| 门编码 | 门索引 |
| 当前状态 | 门的当前状态(开/关,带颜色标识:绿色=打开,红色=关闭) |
| 控制目标 | 门的目标控制状态(开/关,带颜色标识:绿色=开,红色=关) |
| 车辆占用 | 门区域内的小车ID列表(多个ID用逗号分隔,无车辆显示"无" |
| 控制来源 | 当前控制来源(自动/手动) |
| 手动剩余(s) | 手动控制剩余秒数(仅当控制来源=手动时显示,自动时显示"-" |
| 控制地址 | Modbus 线圈地址 |
| 开到位地址 | Modbus 离散输入地址 |
**手动控制功能**
- **打开按钮**:设置手动打开控制,默认保持10秒
- **关闭按钮**:设置手动关闭控制,默认保持10秒
- **安全保护**:当门区域内有车辆占用时,关闭按钮自动禁用,无法执行关闭操作
- 必须先清空车辆占用,才能手动关闭门
- **清空占用按钮**:清空选中门的车辆占用记录
- 清空后,如果门处于打开状态且没有其他小车需要进入,门会自动关闭
- 清空占用后,可以执行手动关闭操作
**控制优先级说明**
- 手动控制优先级高于自动控制
- 手动控制会在指定时间(默认10秒)后自动过期,恢复自动模式
- 可以通过"清空占用"按钮清空车辆占用,然后手动关闭门
---
## 参数说明
### 1. 监控间隔
| 参数 | 默认值 | 说明 |
|------|--------|------|
| 配置监控间隔 | 10秒 | 检查配置文件的间隔 |
| 门控逻辑监控间隔 | 500毫秒 | 检查门控逻辑的间隔(已优化) |
| 监控界面刷新间隔 | 1秒 | 监控界面自动刷新间隔 |
| 手动控制默认保持时间 | 10秒 | 手动控制请求的默认过期时间 |
### 2. 线程安全说明
- **门状态读取**:通过 `controller.DoorStates` 字典访问,所有读写操作受锁保护
- **门控制写入**:通过 `controller.SetDoorControlTarget()` 设置目标状态,实际通信由门控制器内部线程完成
- **配置同步**:配置变更时使用锁保护,确保线程安全
- **控制仲裁**:手动控制请求和自动控制逻辑的仲裁在同一锁内完成,确保线程安全
- **车辆占用管理**:车辆占用的增删改查操作均受锁保护
### 3. 控制仲裁机制
系统采用**控制仲裁机制**来协调手动控制和自动控制:
- **优先级**:手动控制 > 自动控制
- **手动控制过期**:手动控制请求会在指定时间(默认10秒)后自动过期,过期后恢复自动控制
- **安全保护**:当门区域内有车辆占用时,禁止手动关闭门,确保安全
- **仲裁流程**
1. 计算自动目标(基于车辆占用和需要打开标志)
2. 检查是否存在有效的手动控制请求
3. 如果手动控制未过期,使用手动目标;否则使用自动目标
4. 将最终目标写入门控制器的 `DoorControlTargets` 字段
---
## 故障排查
### 问题1:门控制器无法连接
**排查步骤**
1. 检查配置文件中的 IP 和端口是否正确
2. 检查网络连接是否正常
3. 查看诊断日志中的错误信息
4. 确认门控制器硬件是否在线
**诊断命令**
```csharp
var controllers = doorMission.GetDoorControllers();
foreach (var controller in controllers)
{
Console.WriteLine($"控制器{controller.Index}: IP={controller.Ip}, Port={controller.Port}, State={controller.State}, Error={controller.ErrorMessage}");
}
```
### 问题2:门不自动开启
**排查步骤**
1. 确认 `DoorMission` 任务已启动
2. 检查站点的 `EnterDoor` 字段是否配置正确
3. 检查小车站点的 `PreEnterDoor` 字段是否与目标站点的 `EnterDoor` 匹配
4. 查看门控制器的连接状态是否为 `Online`
5. 检查门的状态是否正确读取
**诊断命令**
```csharp
// 检查门状态
bool isOpen = doorMission.GetDoorState(1, 2);
Console.WriteLine($"控制器1门2的状态: {(isOpen ? "" : "")}");
// 检查门控制器状态
var controllers = doorMission.GetDoorControllers();
var controller = controllers.FirstOrDefault(c => c.Index == 1);
if (controller != null)
{
Console.WriteLine($"控制器状态: {controller.State}");
Console.WriteLine($"是否在线: {controller.IsOnline}");
Console.WriteLine($"错误信息: {controller.ErrorMessage}");
}
```
### 问题3:配置文件更新后不生效
**排查步骤**
1. 确认配置文件格式正确(JSON格式)
2. 检查配置文件是否保存成功
3. 等待最多10秒,系统会自动检测更新
4. 查看诊断日志中的配置同步信息
### 问题4:监控界面无数据
**排查步骤**
1. 确认 `DoorMission` 任务已启动
2. 检查是否有配置的门控制器
3. 查看门控制器是否成功连接
4. 检查监控界面的刷新间隔设置
### 问题5:手动关闭按钮无法点击
**原因**
- 门区域内有车辆占用,系统安全保护机制禁止手动关闭
**解决方法**
1. 先点击"清空占用"按钮,清空车辆占用记录
2. 清空后,关闭按钮会自动启用
3. 然后可以执行手动关闭操作
### 问题6:手动控制不生效
**排查步骤**
1. 检查手动控制是否已过期(默认10秒)
2. 查看"控制来源"列,确认是否为"手动"
3. 查看"手动剩余(s)"列,确认剩余时间
4. 如果已过期,手动控制会自动恢复为自动模式
5. 可以通过监控界面重新设置手动控制
---
## 附录
### A. 门标识符解析
门标识符格式:`控制器索引.门索引`
**解析规则**
- 必须包含一个点号(`.`
- 点号前后必须为整数
- 解析失败时返回 `null`
**示例**
- `"1.2"``(controllerIndex: 1, doorIndex: 2)`
- `"10.5"``(controllerIndex: 10, doorIndex: 5)`
- `"1"``null` ❌(缺少点号)
- `"1.2.3"``null` ❌(多个点号)
- `"a.2"``null` ❌(非数字)
### B. 诊断日志说明
系统会在以下情况记录诊断日志:
- 门控制器连接成功/失败
- 门控制器配置变更
- 门状态读取失败
- 门控制写入失败
- 配置加载失败
**日志位置**:系统诊断日志
### C. 类结构关系图
```
DoorMission (门控进程)
├── BasicDoorController (抽象基类)
│ │
│ └── ModbusDoorController (Modbus 实现)
├── DoorManager (配置界面)
├── DoorMonitor (监控界面)
└── DoorModel (配置模型)
```
### D. 控制来源枚举
```csharp
public enum ControlSource
{
Auto = 0, // 自动控制
Manual = 1 // 手动控制
}
```
### E. 门控制状态结构
```csharp
public class DoorControlStatus
{
public bool Target { get; set; } // 当前目标状态(true=打开,false=关闭)
public ControlSource Source { get; set; } // 控制来源(Auto/Manual
public double? ManualRemainingSeconds { get; set; } // 手动控制剩余秒数(仅当Source=Manual时有效)
public IReadOnlyList<int> CarsInArea { get; set; } // 车辆占用列表
}
```
### F. 版本历史
| 版本 | 日期 | 主要更新 |
|------|------|----------|
| 1.0 | 2025-01 | 初始版本,支持 Modbus 门控制器 |
| 1.1 | 2025-01 | 新增门控仲裁机制,支持手动控制与自动控制协调 |
| 1.2 | 2025-01 | 新增车辆占用管理功能,监控界面显示车辆占用情况 |
| 1.3 | 2025-01 | 优化门控逻辑监控间隔至500ms,新增安全保护机制(占用时禁止手动关闭) |
---
**文档更新时间**2025-01-21