init commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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. ChargeUdpService(UDP监听服务)
|
||||
|
||||
**文件位置**: `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报文自动解析和更新
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.Json(NuGet)
|
||||
|
||||
### 硬件要求
|
||||
- 内存:数据量小,几乎无影响
|
||||
- 磁盘:每个充电桩约 1KB 数据
|
||||
- CPU:UI操作,几乎无影响
|
||||
|
||||
### 兼容性
|
||||
- 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现示例
|
||||
|
||||
### 示例1:ModbusDoorController(完整实现)
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user