refactor: 插件 UI 从 WinForms 迁移到 CycleGUI,并修复代码质量问题
将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。 同时修复代码审核中的问题: - 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式) - CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告 - DummyCar 移除已废弃的 rightClickAction()/SetPosition() - CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch - 重命名名不副实的 Mstsc()(现为打开网页) - 统一弃元命名为 _ - TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引 - csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props 构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。 注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
# AbstractLoopMission 抽象环线任务基类
|
||||
|
||||
## 概述
|
||||
|
||||
`AbstractLoopMission` 是环线任务的抽象基类,提供了完整的循环任务调度框架。它支持多种启动类型、任务类别、流量控制和优先级调度,是所有具体环线任务实现的基础。
|
||||
|
||||
## 核心特性
|
||||
|
||||
| 特性 | 说明 |
|
||||
|
||||
|------|------|
|
||||
| 多启动类型 | AutoLoop(自动循环)、Api、Plc、ButtonBox、Charge |
|
||||
| 多任务类别 | Loop(普通循环)、BranchPoint(分流点)、JoinPoint(汇合点) |
|
||||
| 流量控制 | 限制目标站点的最大车辆数 |
|
||||
| 优先级调度 | 高优先级任务优先处理 |
|
||||
| 配置热更新 | tasklist.json 文件变更自动刷新 |
|
||||
| 路径缓存 | 避免重复计算路径,提升性能 |
|
||||
| 条件触发 | 子类可重写事件回调,自定义触发条件 |
|
||||
|
||||
## 类图
|
||||
|
||||

|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置任务列表
|
||||
|
||||
在 任务进程方法显示界面中配置任务或者在`tasklist.json` 中配置任务
|
||||
[ { "Id": 1, "Name": "主线循环", "CurrentStationId": 100, "TargetStationId": 200, "StartType": "AutoLoop",
|
||||
"Kind": "Loop", "Priority": 10, "TrafficControl": 2, "IsViaPoint": true },
|
||||
{ "Id": 2, "Name": "分流点A", "CurrentStationId": 150, "TargetStationId": 201, "StartType": "AutoLoop", "Kind": "BranchPoint", "Priority": 8, "TrafficControl": 1 },
|
||||
{ "Id": 3, "Name": "汇合点B", "CurrentStationId": 180, "TargetStationId": 300, "StartType": "Plc", "Kind": "JoinPoint", "Priority": 5, "TrafficControl": 1 } ]
|
||||
|
||||
### 2. 创建子类
|
||||
|
||||
public class MyLoopMission : AbstractLoopMission { protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car) { // 业务逻辑判断 if (ShouldProcessTask(task, car)) { // 方式1:使用子类指定的目标站点 return ExternalTriggerResult.UseTarget(200);
|
||||
// 方式2:使用配置文件中的目标站点
|
||||
// return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
// 业务失败,不分配任务
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
|
||||
protected override ExternalTriggerResult OnPlcTrigger(int currentSiteId, LoopTask task, Car car)
|
||||
{
|
||||
// PLC 信号触发逻辑
|
||||
if (CheckPlcSignal(currentSiteId))
|
||||
{
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
## 任务类别说明
|
||||
|
||||
### Loop(普通循环)
|
||||
|
||||
车辆从当前站点移动到目标站点的简单任务。
|
||||
站点A ──────────────► 站点B
|
||||
|
||||
### BranchPoint(分流点)
|
||||
|
||||
一个站点可以分流到多个目标站点,按优先级和流量控制选择目标。
|
||||
┌──► 目标站点1 (优先级高)
|
||||
分流点 ────┼──► 目标站点2 └──► 目标站点3 (优先级低)
|
||||
|
||||
### JoinPoint(汇合点)
|
||||
|
||||
多个站点汇合到同一个目标站点,按优先级决定放行顺序。
|
||||
来源站点1 ──┐ 来源站点2 ──┼──► 汇合点 来源站点3 ──┘
|
||||
|
||||
## 流量控制
|
||||
|
||||
通过 `TrafficControl` 属性限制目标站点的最大车辆数:
|
||||
// 检查流量控制 if (!CheckTrafficControl(targetSiteId, task.TrafficControl)) { // 目标站点流量已满,等待 continue; }
|
||||
|
||||
- `TrafficControl = 0`:不限制
|
||||
- `TrafficControl = 1`:目标站点最多 1 辆车
|
||||
- `TrafficControl = N`:目标站点最多 N 辆车
|
||||
|
||||
统计范围包括:
|
||||
|
||||
1. 已在目标站点的车辆(`holdingLocks` 包含该站点)
|
||||
2. 正在前往目标站点的车辆(`pendingLocks` 最后一个为该站点)
|
||||
|
||||
### 3. 启动任务
|
||||
|
||||
var mission = new MyLoopMission();
|
||||
// 启动所有线程(策略同步 + 业务逻辑) mission.StartAll();
|
||||
// 或者分别启动 // mission.StartLoop(); // 启动策略同步线程 // mission.StartLogicLoop(); // 启动业务逻辑线程
|
||||
// 停止任务 // mission.StopAll();
|
||||
// 释放资源 // mission.Dispose();
|
||||
|
||||
## 启动类型说明
|
||||
|
||||
| 类型 | 枚举值 | 说明 | 子类接口 |
|
||||
|
||||
|------|--------|------|----------|
|
||||
| AutoLoop | `TaskStartType.AutoLoop` | 自动循环,车辆到站自动触发 | 无需重写 |
|
||||
| Api | `TaskStartType.Api` | API 外部调用触发 | `OnApiTrigger()` |
|
||||
| Plc | `TaskStartType.Plc` | PLC 信号触发 | `OnPlcTrigger()` |
|
||||
| ButtonBox | `TaskStartType.ButtonBox` | 按钮盒触发 | `OnButtonTrigger()` |
|
||||
| Charge | `TaskStartType.Charge` | 充电条件触发 | `OnChargeTrigger()` |
|
||||
|
||||
## 子类可重写事件回调
|
||||
|
||||
### 概述
|
||||
|
||||
`AbstractLoopMission` 提供了四个可重写的事件回调方法,子类可以通过重写这些方法实现自定义的触发条件逻辑。当车辆到达配置的当前站点时,系统会根据任务的 `StartType` 调用对应的回调方法。
|
||||
|
||||
### 回调方法签名
|
||||
|
||||
| 方法 | 触发条件 | 默认行为 |
|
||||
|------|----------|----------|
|
||||
| `OnApiTrigger(int currentSiteId, LoopTask task, Car car)` | StartType = Api | 返回 `Fail()` |
|
||||
| `OnPlcTrigger(int currentSiteId, LoopTask task, Car car)` | StartType = Plc | 返回 `Fail()` |
|
||||
| `OnButtonTrigger(int currentSiteId, LoopTask task, Car car)` | StartType = ButtonBox | 返回 `Fail()` |
|
||||
| `OnChargeTrigger(int currentSiteId, LoopTask task, Car car)` | StartType = Charge | 返回 `Fail()` |
|
||||
|
||||
### 回调参数说明
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `currentSiteId` | `int` | 车辆当前所在站点ID |
|
||||
| `task` | `LoopTask` | 匹配到的任务配置 |
|
||||
| `car` | `Car` | 到达站点的车辆对象 |
|
||||
|
||||
### 返回值说明
|
||||
|
||||
回调方法必须返回 `ExternalTriggerResult` 对象:
|
||||
|
||||
| 返回方式 | 说明 | 使用场景 |
|
||||
|----------|------|----------|
|
||||
| `ExternalTriggerResult.UseTarget(siteId)` | 成功,使用子类指定的目标站点 | 需要动态计算目标站点时 |
|
||||
| `ExternalTriggerResult.UseConfigTarget()` | 成功,使用配置文件中的目标站点 | 条件满足,使用预设目标时 |
|
||||
| `ExternalTriggerResult.Fail()` | 失败,不分配任务 | 条件不满足,等待下次检查时 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### 示例1:API 触发 - 检查外部系统状态
|
||||
|
||||
public class ApiLoopMission : AbstractLoopMission { protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car) { // 检查外部 API 是否允许发车 var apiResult = ExternalApiService.CheckCanDispatch(currentSiteId, car.id);
|
||||
if (apiResult.Success)
|
||||
{
|
||||
// API 返回指定目标
|
||||
if (apiResult.TargetSiteId.HasValue)
|
||||
{
|
||||
return ExternalTriggerResult.UseTarget(apiResult.TargetSiteId.Value);
|
||||
}
|
||||
// 使用配置目标
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
// 条件不满足,等待
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
#### 示例2:PLC 触发 - 检查 PLC 信号状态
|
||||
|
||||
public class PlcLoopMission : AbstractLoopMission { protected override ExternalTriggerResult OnPlcTrigger(int currentSiteId, LoopTask task, Car car) { // 读取 PLC 信号 bool plcSignal = PlcManager.ReadBool($"Station{currentSiteId}.AllowDispatch");
|
||||
if (plcSignal)
|
||||
{
|
||||
// 根据 PLC 数据决定目标
|
||||
int plcTarget = PlcManager.ReadInt($"Station{currentSiteId}.TargetStation");
|
||||
if (plcTarget > 0)
|
||||
{
|
||||
return ExternalTriggerResult.UseTarget(plcTarget);
|
||||
}
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
#### 示例3:按钮盒触发 - 检查按钮状态
|
||||
|
||||
public class ButtonBoxLoopMission : AbstractLoopMission { protected override ExternalTriggerResult OnButtonTrigger(int currentSiteId, LoopTask task, Car car) { // 检查按钮盒是否按下 var buttonBox = ButtonBoxManager.GetByStation(currentSiteId);
|
||||
if (buttonBox != null && buttonBox.IsPressed)
|
||||
{
|
||||
// 重置按钮状态
|
||||
buttonBox.Reset();
|
||||
// 根据按钮类型选择目标
|
||||
switch (buttonBox.PressedButton)
|
||||
{
|
||||
case ButtonType.Green:
|
||||
return ExternalTriggerResult.UseTarget(task.TargetStationId);
|
||||
case ButtonType.Yellow:
|
||||
return ExternalTriggerResult.UseTarget(GetAlternativeTarget(currentSiteId));
|
||||
default:
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
}
|
||||
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
#### 示例4:充电触发 - 检查电量条件
|
||||
|
||||
public class ChargeLoopMission : AbstractLoopMission { private const int LOW_BATTERY_THRESHOLD = 20;
|
||||
protected override ExternalTriggerResult OnChargeTrigger(int currentSiteId, LoopTask task, Car car)
|
||||
{
|
||||
// 获取车辆电量
|
||||
int batteryLevel = car.GetBatteryLevel();
|
||||
// 检查是否需要充电
|
||||
if (batteryLevel <= LOW_BATTERY_THRESHOLD)
|
||||
{
|
||||
// 查找最近的空闲充电站
|
||||
int chargeStation = FindNearestAvailableChargeStation(currentSiteId);
|
||||
if (chargeStation > 0)
|
||||
{
|
||||
Diagnosis.Post($"车辆 {car.name} 电量 {batteryLevel}%,前往充电站 {chargeStation}", "Charge", true);
|
||||
return ExternalTriggerResult.UseTarget(chargeStation);
|
||||
}
|
||||
}
|
||||
// 电量充足或无可用充电站,不触发充电任务
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
|
||||
private int FindNearestAvailableChargeStation(int currentSiteId)
|
||||
{
|
||||
// 实现查找最近充电站逻辑
|
||||
return ChargeStationManager.FindNearest(currentSiteId);
|
||||
}
|
||||
}
|
||||
|
||||
#### 示例5:组合条件触发
|
||||
|
||||
public class ComplexLoopMission : AbstractLoopMission { protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car) { // 组合多个条件判断
|
||||
// 条件1:检查车辆是否携带货物
|
||||
bool hasLoad = car.tags.ContainsKey("hasLoad") && car.tags["hasLoad"] == "true";
|
||||
// 条件2:检查目标站点是否可用
|
||||
var targetSite = SimpleLib.GetSite(task.TargetStationId);
|
||||
bool targetAvailable = targetSite != null && !targetSite.IsDisabled();
|
||||
// 条件3:检查时间窗口
|
||||
bool inTimeWindow = DateTime.Now.Hour >= 8 && DateTime.Now.Hour <= 20;
|
||||
// 条件4:检查优先级车辆
|
||||
bool isPriorityCar = car.tags.ContainsKey("priority");
|
||||
|
||||
// 组合判断
|
||||
if (hasLoad && targetAvailable && (inTimeWindow || isPriorityCar))
|
||||
{
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
}
|
||||
|
||||
// 记录不满足条件的原因
|
||||
if (!hasLoad) Diagnosis.Post($"车辆 {car.name} 未携带货物", "ApiTrigger", true);
|
||||
if (!targetAvailable) Diagnosis.Post($"目标站点 {task.TargetStationId} 不可用", "ApiTrigger", true);
|
||||
if (!inTimeWindow && !isPriorityCar) Diagnosis.Post($"当前不在工作时间窗口", "ApiTrigger", true);
|
||||
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
### 回调执行流程
|
||||
|
||||
车辆到达站点 │ ▼ 匹配任务配置 (CurrentStationId) │
|
||||
▼ 检查车辆可用性 (Commons.SelectCar) │
|
||||
▼ 根据 StartType 调用对应回调 │ ├
|
||||
─ AutoLoop ──► 自动处理,无需回调
|
||||
├─ Api ────────► OnApiTrigger()
|
||||
├─ Plc ────────► OnPlcTrigger()
|
||||
├─ ButtonBox ──► OnButtonTrigger() └─ Charge ─────► OnChargeTrigger()
|
||||
│ ▼ 检查返回结果 │ ├─ Fail() ──────────► 跳过,等待下次检查
|
||||
│ └─ Success ─────────► 确定目标站点 │
|
||||
├─ CustomTargetSiteId 有值 ──► 使用子类指定目标 └─ CustomTargetSiteId 为空 ──► 使用配置文件目标
|
||||
│ ▼ 流量控制检查 │ ├─ 通过 ──► AssignCarToTarget() └─ 不通过 ──► 等待
|
||||
|
||||
### 汇合点回调执行流程
|
||||
|
||||
按目标站点分组
|
||||
│
|
||||
▼
|
||||
遍历每个汇合点组
|
||||
│
|
||||
▼
|
||||
组内任务按优先级降序排列
|
||||
│
|
||||
▼
|
||||
遍历排序后的任务
|
||||
│
|
||||
├─► 流量控制检查 ──► 不通过 ──► 跳过
|
||||
│
|
||||
├─► 查找车辆 ──► 未找到 ──► 跳过
|
||||
│
|
||||
├─► 检查车辆可用 ──► 不可用 ──► 跳过
|
||||
│
|
||||
├─► 调用子类接口 ──► 返回失败 ──► 跳过
|
||||
│
|
||||
├─► 确定最终目标(子类指定 > 配置)
|
||||
│
|
||||
├─► 子类指定不同目标时再次检查流量
|
||||
│
|
||||
└─► 分配目标站点
|
||||
|
||||
### 注意事项
|
||||
|
||||
## 外部触发结果
|
||||
|
||||
`ExternalTriggerResult` 用于子类返回触发处理结果:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `UseTarget(siteId)` | 使用子类指定的目标站点 |
|
||||
| `UseConfigTarget()` | 使用配置文件中的目标站点 |
|
||||
| `Fail()` | 业务失败,不分配任务 |
|
||||
|
||||
protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car) {
|
||||
// 场景1:动态计算目标 int dynamicTarget = CalculateTarget(car); return ExternalTriggerResult.UseTarget(dynamicTarget);
|
||||
// 场景2:使用配置目标
|
||||
return ExternalTriggerResult.UseConfigTarget();
|
||||
|
||||
// 场景3:条件不满足
|
||||
return ExternalTriggerResult.Fail();
|
||||
}
|
||||
|
||||
1. **默认返回 Fail**:所有回调方法默认返回 `Fail()`,子类必须重写才能启用对应的触发功能。
|
||||
|
||||
2. **线程安全**:回调方法在业务逻辑线程中执行,访问共享资源时需注意线程安全。
|
||||
|
||||
3. **执行频率**:业务逻辑线程每 500ms 执行一次,回调方法应避免长时间阻塞。
|
||||
|
||||
4. **异常处理**:回调方法中的异常会被捕获并记录,不会影响其他任务的处理。
|
||||
|
||||
5. **流量控制**:即使回调返回成功,仍会进行流量控制检查,可能因流量已满而等待。
|
||||
|
||||
## 路径查找与任务匹配
|
||||
|
||||
### 路径查找
|
||||
|
||||
基于轨道连接的广度优先搜索,支持轨道方向:
|
||||
// 获取两站点之间的路径 var path = GetSitesBetween(100, 200); // 返回: [100, 150, 180, 200]
|
||||
// 检查路径上是否有车辆 bool hasCar = HasCarOnPath(100, 200, excludeCar);
|
||||
// 获取路径长度 int length = GetPathLength(100, 200);
|
||||
|
||||
|
||||
### 任务策略匹配
|
||||
|
||||
根据车辆当前位置匹配最优任务:
|
||||
// 查找车辆的最优任务 var match = FindBestTaskForCar(car);
|
||||
if (match != null) { Console.WriteLine($"任务ID: {match.TaskId}");
|
||||
Console.WriteLine($"目标站点: {match.TargetSiteId}");
|
||||
Console.WriteLine($"下一站点: {match.NextSiteId}");
|
||||
Console.WriteLine($"剩余距离: {match.DistanceToTarget}");
|
||||
Console.WriteLine($"进度: {match.ProgressPercent:F1}%"); }
|
||||
|
||||
匹配优先级:
|
||||
|
||||
1. 起点匹配优先
|
||||
2. 任务优先级高的优先
|
||||
3. 距离目标近的优先
|
||||
|
||||
> **注意**:如果站点是路径的终点,则不匹配该任务。
|
||||
|
||||
### TaskListChanged 事件
|
||||
|
||||
任务列表变更时触发:
|
||||
mission.TaskListChanged += (sender, e) =>
|
||||
{ switch (e.ChangeType) {
|
||||
case TaskChangeType.Added: Console.WriteLine($"添加任务: 索引={e.Index}"); break;
|
||||
case TaskChangeType.Updated: Console.WriteLine($"更新任务: 索引={e.Index}"); break;
|
||||
case TaskChangeType.Removed: Console.WriteLine($"移除任务: 索引={e.Index}"); break;
|
||||
case TaskChangeType.Replaced: Console.WriteLine("任务列表已替换"); break; } };
|
||||
|
||||
## 线程模型
|
||||
|
||||
| 线程 | 名称 | 间隔 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 策略同步线程 | `AbstractLoopMission_Strategy` | 1000ms | 同步配置文件,更新任务列表 |
|
||||
| 业务逻辑线程 | `AbstractLoopMission_Logic` | 500ms | 处理任务调度,分配车辆目标 |
|
||||
|
||||
// 分别控制线程 mission.StartLoop(); // 启动策略同步 mission.StartLogicLoop(); // 启动业务逻辑
|
||||
mission.StopLoop(); // 停止策略同步 mission.StopLogicLoop(); // 停止业务逻辑
|
||||
|
||||
## 常量配置
|
||||
|
||||
| 常量 | 值 | 说明 |
|
||||
|
||||
|------|------|------|
|
||||
|
||||
| `STRATEGY_SYNC_INTERVAL_MS` | 1000 | 策略同步间隔(毫秒) |
|
||||
| `LOGIC_LOOP_INTERVAL_MS` | 500 | 业务逻辑间隔(毫秒) |
|
||||
| `FILE_CHANGE_DEBOUNCE_MS` | 50 | 文件变更防抖延迟(毫秒) |
|
||||
| `ERROR_RECOVERY_DELAY_MS` | 3000 | 异常恢复等待时间(毫秒) |
|
||||
| `SCRIPT_ERROR_TRIGGER_DELAY_MS` | 3000 | 脚本异常检测延迟(毫秒) |
|
||||
|
||||
## 辅助方法
|
||||
|
||||
### 车辆查找
|
||||
|
||||
// 查找到达指定站点的车辆 Car car = FindCarArrivedAtSite(siteId);
|
||||
// 获取在站或前往站点的车辆 var cars = GetCarsAtOrHeadingToSite(siteId);
|
||||
// 统计车辆数量 int count = CountCarsAtOrHeadingToSite(siteId);
|
||||
// 检查车辆是否空闲 bool idle = IsCarIdle(car);
|
||||
|
||||
### 车辆分配
|
||||
|
||||
// 分配目标站点 AssignCarToTarget(car, targetSiteId);
|
||||
// 导航到目标站点 await GoSite(car, targetSite, action: "/", reverse: false);
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 脚本异常恢复
|
||||
|
||||
当车辆脚本状态为 `Error` 或 `Bad` 时,自动执行恢复流程:
|
||||
|
||||
1. 标记站点不可用
|
||||
2. 禁止车辆调度
|
||||
3. 重置车辆状态
|
||||
4. 清理车辆标签
|
||||
5. 执行车辆重置
|
||||
Reference in New Issue
Block a user