# 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 发送报文]
```

