Files
StandardSence/StandardScene.Core/Chained/TransportMission.cs
T
2026-06-14 11:19:15 +08:00

600 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.RCS.Signal;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.Library;
using SimpleCore.PropType;
using StandardScene.Model;
using StandardScene;
using StandardScene.Utils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32.SafeHandles;
namespace StandardScene.Chained
{
/// <summary>
/// 任务类型枚举
/// </summary>
public enum TaskType
{
移动 = 1, // 移动任务(当前未使用)
搬运 = 2, // 搬运任务
装车 = 3, // 装车任务
卸车 = 4, // 卸车任务
}
/// <summary>
/// 运输任务类
/// 继承自AbstractDelivery,实现具体的运输任务功能
/// </summary>
/// public class TransportDelivery : AbstractChainedDeliveryMission.AbstractDelivery
public class TransportDelivery : ChainedDeliveryMission.Delivery
{
/// <summary>任务类型(枚举值)</summary>
public TaskType Type;
/// <summary>任务类型(字符串形式)</summary>
public string TaskType;
/// <summary>使用的小车名称</summary>
public string UsingCarName = "/";
/// <summary>物料信息</summary>
public string Material = "";
/// <summary>占车站点</summary>
public string HoldCarSite = "";
/// <summary>阶段任务代码(用于标记阶段任务)</summary>
public string MGTaskCode = "";
/// <summary>任务ID字符串(用于标记阶段任务,可包含多个任务ID,用逗号分隔)</summary>
public string TaskIdsString = "";
/// <summary>
/// 构造函数
/// 注册任务生命周期回调函数
/// </summary>
public TransportDelivery()
{
// 注册任务开始回调
OnStart += d => { TransportOnStart((TransportDelivery)d); };
// 注册取货完成回调
DoneFetch += d => { TransportDoneFetch((TransportDelivery)d); };
// 注册放货完成回调
DonePut += d => { TransportDonePut((TransportDelivery)d); };
// 注册任务完成回调
DoneMission += d => { TransportDoneMission((TransportDelivery)d); };
}
/// <summary>
/// 任务开始时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportOnStart(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-onStart");
}
/// <summary>
/// 取货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneFetch(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneFetch");
}
/// <summary>
/// 放货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDonePut(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-donePut");
}
/// <summary>
/// 任务完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneMission(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneMission");
}
}
/// <summary>
/// 搬运任务进程类
/// 继承自AbstractChainedDeliveryMission,实现具体的搬运任务调度
/// </summary>
[MissionType(Name = "搬运任务进程", editor = typeof(TransportMission))]
internal class TransportMission : ChainedDeliveryMission
{
/// <summary>是否启用就近任务策略("1"=启用,"0"=禁用)</summary>
private static readonly string NearestTask = "1";
/// <summary>任务查看器窗口对象</summary>
private DeliveryViewer dv;
public TransportMission()
{
// 确保回调静态类已完成注册
TransportDeliveryCallbacks.EnsureInitialized();
// 根据配置的 MissionCallbackURL 设置回调地址(若有)
var p = Param;
if (p != null && !string.IsNullOrWhiteSpace(p.MissionCallbackURL))
{
if (Commons.IsValidHttpUrl(p.MissionCallbackURL))
{
_lastCallbackUrl = p.MissionCallbackURL;
TransportDeliveryCallbacks.ConfigureCallbackUrl(_lastCallbackUrl);
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {p.MissionCallbackURL}", "TransportMission");
}
}
}
public class TransportMissionParam
{
/// <summary>
/// 递送任务状态回调地址。
/// </summary>
public string MissionCallbackURL;
}
public TransportMissionParam Param => StringDictConvert<TransportMissionParam>.Convert(fields);
private readonly bool _onDisplay = true;
private string _lastCallbackUrl;
protected override Delivery CreateDeliveryFromSnapshot(DeliveryStateSnapshot snap)
{
var usingCar = SimpleLib.GetCar(snap.UsingCarId);
if (snap.UsingCarId != -1 && usingCar == null)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] failed:usingCar[{snap.UsingCarId}] not found");
return null;
}
if (usingCar != null && usingCar is not Car)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] skipped:usingCar[{snap.UsingCarId}] is not Car type");
return null;
}
var d = new TransportDelivery
{
Id = snap.Id,
TaskId = snap.TaskId ?? string.Empty,
Src = snap.Src,
Dst = snap.Dst,
SkipFetch = snap.SkipFetch,
SkipPut = snap.SkipPut,
Putting = snap.Putting,
CreateTime = snap.CreateTime,
StartTime = snap.StartTime,
FinishTime = snap.FinishTime,
CarType = snap.CarType,
UsingCar = (Car)usingCar
};
if (usingCar != null && string.Equals(snap.Status, "putting", StringComparison.OrdinalIgnoreCase) && !CheckCarLoaded(usingCar))
{
d.Error = true;
}
// 恢复回调配置与事件绑定
d.ReportOnStarted = snap.ReportOnStarted;
d.ReportOnFetched = snap.ReportOnFetched;
d.ReportOnPut = snap.ReportOnPut;
d.ReportOnFinished = snap.ReportOnFinished;
d.ReportOnFailed = snap.ReportOnFailed;
d.ReportOnTerminated = snap.ReportOnTerminated;
d.OnStartCallbackKeys = snap.OnStartCallbackKeys ?? new List<string>();
d.DoneFetchCallbackKeys = snap.DoneFetchCallbackKeys ?? new List<string>();
d.DonePutCallbackKeys = snap.DonePutCallbackKeys ?? new List<string>();
d.DoneMissionCallbackKeys = snap.DoneMissionCallbackKeys ?? new List<string>();
d.FailedCallbackKeys = snap.FailedCallbackKeys ?? new List<string>();
d.OnTerminatedCallbackKeys = snap.OnTerminatedCallbackKeys ?? new List<string>();
// 统一通过 Attacher 挂载所有回调
DeliveryCallbackAttacher.AttachAll(d);
return d;
}
protected override bool CheckCarLoaded(AbstractCar car)
{
if(car is DummyCar)
return true;
var carLoaded = Commons.GetCarStatus((Car)car, "Loaded");
if (!string.IsNullOrEmpty(carLoaded) && bool.TryParse(carLoaded, out var result))
return result;
return false;
}
/// <summary>
/// 执行方法(启动任务调度进程)
/// 启动基类的任务调度循环,并启动状态显示线程
/// </summary>
public override void Execute()
{
base.Execute(); // 调用基类Execute方法,启动任务调度循环
//HideSimpleConsole(); // 隐藏控制台窗口
// 启动状态显示线程,实时显示小车状态和任务执行情况
new Thread(() =>
{
while (true)
{
Thread.Sleep(100); // 每100ms更新一次显示
var painter = SimpleMonitor.getPainter("TransportMissionPainter");
painter.clear();
if (!_onDisplay) continue; // 如果未启用显示,跳过
// 检查并更新任务状态回调 URL(如果配置发生变化)
var p = Param;
var currentUrl = p?.MissionCallbackURL;
if (!string.IsNullOrWhiteSpace(currentUrl) && currentUrl != _lastCallbackUrl)
{
// 使用 Commons 中的统一 URL 校验逻辑
if (Commons.IsValidHttpUrl(currentUrl))
{
TransportDeliveryCallbacks.ConfigureCallbackUrl(currentUrl);
_lastCallbackUrl = currentUrl;
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {currentUrl}", "TransportMission");
}
}
var onMissionCnt = 0; // 正在执行任务的小车数量
var carStr = $"{string.Join("\n", SimpleLib.GetAllCars().Select(cc =>
{
var status = "";
if (cc.tags.TryGetValue("occupied", out var occupiedStr))
{
onMissionCnt++;
status = occupiedStr;
if (cc.tags.TryGetValue("idle", out var _))
{
Diagnosis.Post($"strange idle tag, {cc.name}({cc.id})");
cc.tags.Remove("idle");
}
}
else if (cc.tags.TryGetValue("idle", out var idleStr)) status = DateTime.TryParse(idleStr, out var idleTime) ? $"idle:{idleTime}" : $"idle:{idleStr}";
return $"{cc.name}({cc.id})\t{status}";
}))}";
//var waitingMissions = GetDeliveries()
// .Where(dd => dd.GetStatus() == DeliveryStatus.Waiting).ToList();
painter.drawTextFixed(
$"小车:\t开动率:{onMissionCnt}/{SimpleLib.GetAllCars().Length}\n{carStr}\n\n",
new SolidBrush(Color.Black),
VirtualPainter.DrawPosition.LeftTop, Color.AliceBlue);
painter = null;
}
}).Start();
}
/// <summary>
/// 变更任务优先级(重写基类方法)
/// 根据配置决定是否执行就近任务策略
/// </summary>
public override void ChangePriority()
{
try
{
if (NearestTask == "1") // NearestTask 等于1 执行就近原则
{
// 就近任务执行(当前被注释,未启用)
// NearestTaskExecute();
}
else
{
// 如果未启用就近原则,移除所有小车的就近选车标记
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
Commons.DeleteTag(car.tags, "changePriority");
}
}
}
catch (Exception ex)
{
Console.WriteLine("同一工序就近执行就近任务执行:" + ex.ToString());
}
}
/// <summary>
/// 一键初始化所有小车
/// 将状态为"正常"或"上线"且未初始化的小车进行重置
/// </summary>
[MethodMember(Name = "一键初始化")]
public void ResetAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车未初始化(GetLastSite返回-1),执行重置
if (car.GetLastSite() == -1)
((Car)car).Reset();
}
}
/// <summary>
/// 一键上线所有小车
/// 将状态为"正常"或"上线"且已初始化的小车标记为在线状态
/// </summary>
[MethodMember(Name = "一键上线")]
public void OnlineAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车已初始化,标记为在线
if (car.GetLastSite() != -1)
{
Commons.AddOrUpdateTag(car.tags, "Online", "true");
}
}
}
/// <summary>
/// 查看任务列表界面
/// 打开任务查看器窗口,显示所有任务的状态
/// </summary>
[MethodMember(Name = "查看任务", Description = "显示界面")]
public void Print()
{
dv = new DeliveryViewer();
dv.Show();
}
/// <summary>
/// 手动创建单取货任务
/// 通过UI交互选择小车和取货点,创建仅取货任务(跳过取货步骤,小车从当前位置移动到取货点)
/// </summary>
[MethodMember(Name = "单取货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueueFetch()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
// 创建运输任务:从小车当前位置到选择的取货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的取货点
SkipFetch = true, // 跳过取货步骤(因为小车已经在起始点)
UsingCar = car, // 指定使用的小车
PutPlanInfo = new() { { "action", "fetch" } }, // 路径动作设置为fetch
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {car.GetLastSite()} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
//写一个冒泡排序的算法,按照距离排序
[MethodMember(Name = "切换后台显示")]
public void SwitchBackgroundDisplay()
{
ShowSimpleConsole();
}
/// <summary>
/// 手动创建单放货任务
/// 通过UI交互选择小车和放货点,创建仅放货任务(小车从当前位置移动到放货点并放货)
/// </summary>
[MethodMember(Name = "单放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueuePut()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择放货点(注释写的是"取货位"但实际是放货点)
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建运输任务:从小车当前位置到选择的放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的放货点
UsingCar = car, // 指定使用的小车
SkipFetch = true, // 跳过取货步骤
PutPlanInfo = new() { { "action", "put" } }, // 路径动作设置为put
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {srcSite.id} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建完整的取放货任务
/// 通过UI交互选择取货点和放货点,创建完整的搬运任务(取货->放货)
/// </summary>
[MethodMember(Name = "取放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueue()
{
try
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
G.pushStatus("选择放货位");
// 等待用户在UI中选择放货点
var pt2 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var dstSite = SimpleLib.GetSite(pt2.site);
// 创建完整的运输任务:从取货点到放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSite.id, // 取货点
Dst = dstSite.id, // 放货点
TaskId = $"ManualTask-{DateTime.Now}" // 生成任务ID
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {dstSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建移动任务(去某地)
/// 通过UI交互选择目标站点,创建移动任务(起点和终点相同,只移动不放货)
/// </summary>
[MethodMember(Name = "去某地", Description = "去某地")]
public async void ManualEnqueueGo()
{
try
{
G.pushStatus("选择前往的站点");
// 等待用户在UI中选择目标站点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建移动任务:起点和终点相同(只移动到目标点,不执行取放货操作)
var d = new TransportDelivery
{
CarType = "Car",
Src = pt1.site, // 起点(实际不会取货)
Dst = pt1.site, // 终点(实际不会放货)
TaskId = $"ManualTask-{DateTime.Now}",
SrcFindLoop = false, // 起点不查找回路
DstFindLoop = false // 终点不查找回路
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {srcSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 供按钮盒进程反射调用:ButtonMission.ExecuteButtonActionInternal 反射调用本方法后,
/// 由 HandleMethodResult 解析返回值,驱动 OnActionExecuted 成功/失败反馈。
/// 返回 true 表示已入队;false 表示站点无效。若 Enqueue 抛错,由按钮进程捕获后同样视为失败。
/// 按钮配置示例:TriggerMission=TransportMissionTriggerMethod=EnqueueTransportByButtonTriggerMethodParams=取货站点ID,放货站点ID
/// </summary>
/// <param name="srcSiteId">取货站点 ID</param>
/// <param name="dstSiteId">放货站点 ID</param>
public bool EnqueueTransportByButton(int srcSiteId, int dstSiteId)
{
if (SimpleLib.GetSite(srcSiteId) == null || SimpleLib.GetSite(dstSiteId) == null)
{
Diagnosis.Log($"按钮排队搬运失败:站点不存在 src={srcSiteId}, dst={dstSiteId}", "TransportMission", true);
return false;
}
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSiteId,
Dst = dstSiteId,
TaskId = $"Button-{DateTime.Now:yyyyMMddHHmmssfff}"
};
Enqueue(d);
Diagnosis.Post($"按钮排队搬运 {d.Id}: {srcSiteId} -> {dstSiteId}");
return true;
}
// ========== 控制台窗口显示控制 ==========
/// <summary>Windows API:获取控制台窗口句柄</summary>
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
/// <summary>Windows API:显示/隐藏窗口</summary>
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
/// <summary>控制台显示状态标志</summary>
private static bool ShowConsole = false;
/// <summary>
/// 切换控制台窗口显示/隐藏
/// </summary>
public static void ShowSimpleConsole()
{
ShowConsole = !ShowConsole; // 切换显示状态
var handle = GetConsoleWindow();
int n = ShowConsole ? 0 : 5; // 0=显示,5=隐藏
Console.WriteLine(n);
ShowWindow(handle, n);
}
/// <summary>
/// 隐藏控制台窗口
/// </summary>
public static void HideSimpleConsole()
{
var handle = GetConsoleWindow();
ShowWindow(handle, 0); // 0表示隐藏窗口
}
}
}