using Microsoft.Extensions.Logging;
|
using WIDESEAWCS_Core.LogHelper;
|
using WIDESEAWCS_QuartzJob;
|
|
namespace WIDESEAWCS_Tasks
|
{
|
/// <summary>
|
/// 输送线目标地址选择器 - 处理拘束机/插拔钉机的上下层请求
|
/// </summary>
|
/// <remarks>
|
/// 核心职责:
|
/// 1. 处理入库场景的目标地址选择
|
/// 2. 处理出库场景的目标地址选择
|
/// 3. 判断拘束机和插拔钉机的物料请求状态
|
/// 4. 协调输送线与上下层设备之间的物料流转
|
///
|
/// 拘束机和插拔钉机都有上下两层结构,
|
/// 每层都有独立的物料请求和出料信号,需要分别处理。
|
/// </remarks>
|
public class ConveyorLineTargetAddressSelector
|
{
|
/// <summary>
|
/// 设备层级(上层/下层)
|
/// </summary>
|
/// <remarks>
|
/// 用于区分拘束机和插拔钉机的上层工位和下层工位。
|
/// 入库任务对应上层(MaterialRequestUpper),出库任务对应下层(MaterialRequestLower)。
|
/// </remarks>
|
private enum Layer
|
{
|
/// <summary>
|
/// 上层工位
|
/// </summary>
|
Upper,
|
|
/// <summary>
|
/// 下层工位
|
/// </summary>
|
Lower
|
}
|
|
/// <summary>
|
/// 目标设备类型枚举
|
/// </summary>
|
/// <remarks>
|
/// 用于根据目标地址编码识别需要对接的设备类型。
|
/// </remarks>
|
private enum TargetDeviceType
|
{
|
/// <summary>
|
/// 无有效设备(地址不在已知范围内)
|
/// </summary>
|
None,
|
|
/// <summary>
|
/// 拘束机
|
/// </summary>
|
/// <remarks>
|
/// 负责固定/约束电池托盘的设备,有上下两层。
|
/// </remarks>
|
ConstraintMachine,
|
|
/// <summary>
|
/// 插拔钉机
|
/// </summary>
|
/// <remarks>
|
/// 负责插针和拔针操作的设备,有上下两层。
|
/// </remarks>
|
PinMachine
|
}
|
|
/// <summary>
|
/// 拘束机对应的点位编码集合
|
/// </summary>
|
/// <remarks>
|
/// 当目标地址在这些编码中时,表示需要与拘束机交互。
|
/// 使用 HashSet 保证 O(1) 的 Contains 查找性能。
|
/// </remarks>
|
private static readonly HashSet<string> ConstraintMachineCodes = new HashSet<string> { "10180", "20090" };
|
|
/// <summary>
|
/// 插拔钉机对应的点位编码集合
|
/// </summary>
|
/// <remarks>
|
/// 当目标地址在这些编码中时,表示需要与插拔钉机交互。
|
/// 使用 HashSet 保证 O(1) 的 Contains 查找性能。
|
/// </remarks>
|
private static readonly HashSet<string> PinMachineCodes = new HashSet<string> { "10190", "20100" };
|
|
/// <summary>
|
/// 目标地址到设备类型的映射
|
/// </summary>
|
/// <remarks>
|
/// 通过单一字典实现 O(1) 查找,替代原先分别用两个 List + Contains + if/else if 的写法。
|
/// Key: 目标地址编码,Value: 对应的设备类型。
|
/// </remarks>
|
private static readonly Dictionary<string, TargetDeviceType> AddressToDeviceType = new Dictionary<string, TargetDeviceType>
|
{
|
// 拘束机的两个点位编码都映射到 ConstraintMachine 类型
|
{ "10180", TargetDeviceType.ConstraintMachine },
|
{ "20090", TargetDeviceType.ConstraintMachine },
|
// 插拔钉机的两个点位编码都映射到 PinMachine 类型
|
{ "10190", TargetDeviceType.PinMachine },
|
{ "20100", TargetDeviceType.PinMachine }
|
};
|
|
/// <summary>
|
/// 日志记录器
|
/// </summary>
|
/// <remarks>
|
/// 通过 Microsoft.Extensions.Logging 接口注入,用于结构化日志输出。
|
/// </remarks>
|
private readonly ILogger _logger;
|
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
/// <param name="logger">日志记录器,由依赖注入容器自动注入</param>
|
public ConveyorLineTargetAddressSelector(ILogger logger)
|
{
|
_logger = logger; // 保存日志记录器实例,供后续方法使用
|
}
|
|
/// <summary>
|
/// 处理入库场景的下一地址请求
|
/// </summary>
|
/// <remarks>
|
/// 入库任务到达某个位置时调用此方法,判断目标设备是否需要物料。
|
/// 入库对应上层工位(Layer.Upper),因为物料从上层进入仓库。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于写入目标地址和 ACK 信号</param>
|
/// <param name="nextAddress">下一地址/目标设备编码,用于识别目标设备类型</param>
|
/// <param name="childDeviceCode">当前子设备编码,用于精确定位写入哪个子设备</param>
|
public void HandleInboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
|
{
|
// 记录入库场景的调试日志,包含子设备和目标地址信息
|
WriteDebug(conveyorLine, "入库下一地址", childDeviceCode, nextAddress);
|
// 委托通用处理方法,入库对应上层(isUpper: true)
|
HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, Layer.Upper);
|
}
|
|
/// <summary>
|
/// 处理出库场景的下一地址请求
|
/// </summary>
|
/// <remarks>
|
/// 出库任务到达某个位置时调用此方法,判断目标设备是否需要出料。
|
/// 出库对应下层工位(Layer.Lower),因为物料从下层离开仓库。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于写入目标地址和 ACK 信号</param>
|
/// <param name="nextAddress">下一地址/目标设备编码,用于识别目标设备类型</param>
|
/// <param name="childDeviceCode">当前子设备编码,用于精确定位写入哪个子设备</param>
|
public void HandleOutboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
|
{
|
// 记录出库场景的调试日志,包含子设备和目标地址信息
|
WriteDebug(conveyorLine, "出库下一地址", childDeviceCode, nextAddress);
|
// 委托通用处理方法,出库对应下层(isUpper: false)
|
HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, Layer.Lower);
|
}
|
|
/// <summary>
|
/// 根据目标地址类型分发到对应设备处理
|
/// </summary>
|
/// <remarks>
|
/// 通过 AddressToDeviceType 字典将目标地址映射到设备类型,
|
/// 然后分发到对应的专用处理方法(HandleConstraintMachine / HandlePinMachine)。
|
/// 如果目标地址不在已知映射表中,直接返回,不做任何处理。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,传递到具体设备处理方法</param>
|
/// <param name="nextAddress">目标设备编码,通过字典查找识别设备类型</param>
|
/// <param name="childDeviceCode">子设备编码,传递到具体设备处理方法</param>
|
/// <param name="layer">设备层级(上层或下层),决定读取哪组请求标志</param>
|
private void HandleDeviceRequest(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode, Layer layer)
|
{
|
// 通过字典查找目标地址对应的设备类型,如果找不到则 deviceType 为 None
|
if (!AddressToDeviceType.TryGetValue(nextAddress, out var deviceType) || deviceType == TargetDeviceType.None)
|
{
|
// 目标地址不在已知映射表中,直接返回(可能是其他类型设备)
|
return;
|
}
|
|
// 根据识别出的设备类型分发到对应的处理方法
|
switch (deviceType)
|
{
|
case TargetDeviceType.ConstraintMachine:
|
// 拘束机处理分支:获取拘束机实例并处理其上下层请求
|
HandleConstraintMachine(conveyorLine, childDeviceCode, layer);
|
break; // 处理完毕,跳出 switch
|
|
case TargetDeviceType.PinMachine:
|
// 插拔钉机处理分支:获取插拔钉机实例并处理其上下层请求
|
HandlePinMachine(conveyorLine, childDeviceCode, layer);
|
break; // 处理完毕,跳出 switch
|
}
|
}
|
|
/// <summary>
|
/// 处理拘束机的物料/出料请求
|
/// </summary>
|
/// <remarks>
|
/// 查找拘束机设备,获取当前层级的物料请求和出料请求状态,
|
/// 然后调用通用处理逻辑 ProcessDeviceRequest。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于通知目标地址和 ACK</param>
|
/// <param name="childDeviceCode">子设备编码,用于精确定位</param>
|
/// <param name="layer">设备层级,决定读取上层还是下层的请求标志</param>
|
private void HandleConstraintMachine(CommonConveyorLine conveyorLine, string childDeviceCode, Layer layer)
|
{
|
// 从全局设备列表中查找名为"拘束机"的拘束机设备实例
|
var constraint = FindDevice<ConstraintMachine>("拘束机");
|
if (constraint == null)
|
{
|
// 未找到拘束机设备,已在 FindDevice 中记录日志,此处直接返回
|
return;
|
}
|
|
// 获取当前层级(上层/下层)的物料请求标志,非零表示设备需要物料
|
bool materialRequest = GetConstraintFlag(constraint, layer, isMaterial: true);
|
// 获取当前层级(上层/下层)的出料请求标志,非零表示设备有货要出
|
bool outputRequest = GetConstraintFlag(constraint, layer, isMaterial: false);
|
|
// 构造设置输出就绪标志的委托(根据层级写入 Upper 或 Lower 对应的寄存器)
|
Action<bool> setOutputReady = outputReady =>
|
SetConstraintOutputReady(constraint, layer, outputReady);
|
|
// 调用通用请求处理逻辑,传入设备类型描述用于日志记录
|
ProcessDeviceRequest(conveyorLine, childDeviceCode, materialRequest, outputRequest, setOutputReady, "拘束机");
|
}
|
|
/// <summary>
|
/// 处理插拔钉机的物料/出料请求
|
/// </summary>
|
/// <remarks>
|
/// 查找插拔钉机设备,获取当前层级的物料请求和出料请求状态,
|
/// 然后调用通用处理逻辑 ProcessDeviceRequest。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于通知目标地址和 ACK</param>
|
/// <param name="childDeviceCode">子设备编码,用于精确定位</param>
|
/// <param name="layer">设备层级,决定读取上层还是下层的请求标志</param>
|
private void HandlePinMachine(CommonConveyorLine conveyorLine, string childDeviceCode, Layer layer)
|
{
|
// 从全局设备列表中查找名为"插拔钉机"的插拔钉机设备实例
|
var pinMachine = FindDevice<PinMachine>("插拔钉机");
|
if (pinMachine == null)
|
{
|
// 未找到插拔钉机设备,已在 FindDevice 中记录日志,此处直接返回
|
return;
|
}
|
|
// 获取当前层级(上层/下层)的物料请求标志,非零表示设备需要物料
|
bool materialRequest = GetPinMachineFlag(pinMachine, layer, isMaterial: true);
|
// 获取当前层级(上层/下层)的出料请求标志,非零表示设备有货要出
|
bool outputRequest = GetPinMachineFlag(pinMachine, layer, isMaterial: false);
|
|
// 构造设置输出就绪标志的委托(根据层级写入 Upper 或 Lower 对应的寄存器)
|
Action<bool> setOutputReady = outputReady =>
|
SetPinMachineOutputReady(pinMachine, layer, outputReady);
|
|
// 调用通用请求处理逻辑,传入设备类型描述用于日志记录
|
ProcessDeviceRequest(conveyorLine, childDeviceCode, materialRequest, outputRequest, setOutputReady, "插拔钉机");
|
}
|
|
/// <summary>
|
/// 查找指定名称的设备
|
/// </summary>
|
/// <remarks>
|
/// 从全局设备列表 Storage.Devices 中通过设备类型和名称查找设备实例。
|
/// 这是一个泛型方法,可以适用于 ConstraintMachine、PinMachine 等多种设备类型。
|
/// </remarks>
|
/// <typeparam name="T">设备类型,必须是引用类型(如 ConstraintMachine、PinMachine)</typeparam>
|
/// <param name="deviceName">设备名称,用于精确匹配设备的 DeviceName 属性</param>
|
/// <returns>找到的设备实例,未找到则返回 null</returns>
|
private T? FindDevice<T>(string deviceName) where T : class
|
{
|
// OfType<T>() 筛选出指定类型的设备,FirstOrDefault 按名称精确匹配
|
var device = Storage.Devices.OfType<T>().FirstOrDefault(d => GetDeviceName(d) == deviceName);
|
if (device == null)
|
{
|
// 设备未找到时记录调试日志,方便排查配置问题
|
_logger.LogDebug("FindDevice:未找到 {DeviceName}", deviceName);
|
}
|
return device; // 可能为 null,由调用方负责 null 检查
|
}
|
|
/// <summary>
|
/// 通过多态获取任意设备的名称
|
/// </summary>
|
/// <remarks>
|
/// 使用 switch 表达式根据设备的具体类型获取其 DeviceName 属性,
|
/// 避免为每种设备类型编写独立的反射或属性访问代码。
|
/// 当前支持 ConstraintMachine 和 PinMachine 两种类型。
|
/// </remarks>
|
/// <typeparam name="T">设备类型</typeparam>
|
/// <param name="device">设备实例,非空</param>
|
/// <returns>设备的名称字符串,如果类型不匹配则返回空字符串</returns>
|
private static string GetDeviceName<T>(T device) where T : class
|
{
|
// 模式匹配:根据设备的具体运行时类型返回对应的 DeviceName
|
return device switch
|
{
|
ConstraintMachine cm => cm.DeviceName, // 拘束机返回其设备名称
|
PinMachine pm => pm.DeviceName, // 插拔钉机返回其设备名称
|
_ => string.Empty // 未知类型返回空字符串(理论上不会走到这里)
|
};
|
}
|
|
/// <summary>
|
/// 获取拘束机的请求标志(物料请求或出料请求)
|
/// </summary>
|
/// <remarks>
|
/// 根据 isMaterial 参数决定读取物料请求还是出料请求寄存器,
|
/// 再根据 layer 参数决定读取上层(Upper)还是下层(Lower)寄存器。
|
/// 返回值表示请求是否有效(非零为有效)。
|
/// </remarks>
|
/// <param name="constraint">拘束机设备实例,用于读取 PLC 寄存器值</param>
|
/// <param name="layer">设备层级,决定读取 Upper 还是 Lower 寄存器</param>
|
/// <param name="isMaterial">true=读取物料请求标志,false=读取出料请求标志</param>
|
/// <returns>请求标志是否有效(true=有请求,false=无请求)</returns>
|
private bool GetConstraintFlag(ConstraintMachine constraint, Layer layer, bool isMaterial)
|
{
|
// 根据 isMaterial 选择对应的寄存器名称对(物料请求或出料请求)
|
var (materialKey, outputKey) = isMaterial
|
? (ConstraintMachineDBName.MaterialRequestUpper, ConstraintMachineDBName.MaterialRequestLower) // 物料请求
|
: (ConstraintMachineDBName.OutputRequestUpper, ConstraintMachineDBName.OutputRequestLower); // 出料请求
|
|
// 根据 layer 选择具体使用 Upper 还是 Lower 版本的寄存器
|
var key = layer == Layer.Upper ? materialKey : outputKey;
|
// 读取寄存器值,非零表示有请求,返回布尔值
|
return constraint.GetValue<ConstraintMachineDBName, short>(key) != 0;
|
}
|
|
/// <summary>
|
/// 获取插拔钉机的请求标志(物料请求或出料请求)
|
/// </summary>
|
/// <remarks>
|
/// 与 GetConstraintFlag 逻辑相同,但操作对象是插拔钉机(PinMachine)。
|
/// 根据 isMaterial 参数决定读取物料请求还是出料请求寄存器,
|
/// 再根据 layer 参数决定读取上层(Upper)还是下层(Lower)寄存器。
|
/// </remarks>
|
/// <param name="pinMachine">插拔钉机设备实例,用于读取 PLC 寄存器值</param>
|
/// <param name="layer">设备层级,决定读取 Upper 还是 Lower 寄存器</param>
|
/// <param name="isMaterial">true=读取物料请求标志,false=读取出料请求标志</param>
|
/// <returns>请求标志是否有效(true=有请求,false=无请求)</returns>
|
private bool GetPinMachineFlag(PinMachine pinMachine, Layer layer, bool isMaterial)
|
{
|
// 根据 isMaterial 选择对应的寄存器名称对(物料请求或出料请求)
|
var (materialKey, outputKey) = isMaterial
|
? (PinMachineDBName.MaterialRequestUpper, PinMachineDBName.MaterialRequestLower) // 物料请求
|
: (PinMachineDBName.OutputRequestUpper, PinMachineDBName.OutputRequestLower); // 出料请求
|
|
// 根据 layer 选择具体使用 Upper 还是 Lower 版本的寄存器
|
var key = layer == Layer.Upper ? materialKey : outputKey;
|
// 读取寄存器值,非零表示有请求,返回布尔值
|
return pinMachine.GetValue<PinMachineDBName, short>(key) != 0;
|
}
|
|
/// <summary>
|
/// 设置拘束机的输出就绪标志
|
/// </summary>
|
/// <remarks>
|
/// 向拘束机的 PLC 寄存器写入输出就绪状态,告知拘束机可以继续出料。
|
/// 根据 layer 参数决定写入上层(ConstraintTrayOutputReadyUpper)还是下层(ConstraintTrayOutputReadyLower)寄存器。
|
/// </remarks>
|
/// <param name="constraint">拘束机设备实例,用于写入 PLC 寄存器</param>
|
/// <param name="layer">设备层级,决定写入 Upper 还是 Lower 寄存器</param>
|
/// <param name="outputReady">输出就绪状态,true=可以出料,false=不能出料</param>
|
private void SetConstraintOutputReady(ConstraintMachine constraint, Layer layer, bool outputReady)
|
{
|
// 根据 layer 选择对应的输出就绪寄存器(Upper 或 Lower)
|
var key = layer == Layer.Upper
|
? ConstraintMachineDBName.ConstraintTrayOutputReadyUpper // 上层输出就绪寄存器
|
: ConstraintMachineDBName.ConstraintTrayOutputReadyLower; // 下层输出就绪寄存器
|
// 向 PLC 写入值,outputReady 为 true 时写入 1,否则写入 0
|
constraint.SetValue(key, outputReady ? true : false);
|
}
|
|
/// <summary>
|
/// 设置插拔钉机的输出就绪标志
|
/// </summary>
|
/// <remarks>
|
/// 向插拔钉机的 PLC 寄存器写入输出就绪状态,告知插拔钉机可以继续出料。
|
/// 根据 layer 参数决定写入上层(PlugPinTrayOutputReadyUpper)还是下层(PlugPinTrayOutputReadyLower)寄存器。
|
/// </remarks>
|
/// <param name="pinMachine">插拔钉机设备实例,用于写入 PLC 寄存器</param>
|
/// <param name="layer">设备层级,决定写入 Upper 还是 Lower 寄存器</param>
|
/// <param name="outputReady">输出就绪状态,true=可以出料,false=不能出料</param>
|
private void SetPinMachineOutputReady(PinMachine pinMachine, Layer layer, bool outputReady)
|
{
|
// 根据 layer 选择对应的输出就绪寄存器(Upper 或 Lower)
|
var key = layer == Layer.Upper
|
? PinMachineDBName.PlugPinTrayOutputReadyUpper // 上层输出就绪寄存器
|
: PinMachineDBName.PlugPinTrayOutputReadyLower; // 下层输出就绪寄存器
|
// 向 PLC 写入值,outputReady 为 true 时写入 1,否则写入 0
|
pinMachine.SetValue(key, outputReady ? true : false);
|
}
|
|
/// <summary>
|
/// 处理设备请求的核心逻辑
|
/// </summary>
|
/// <remarks>
|
/// 根据物料请求和出料请求的状态决定行为:
|
/// - 如果设备需要物料(materialRequest=true),设置输送线的目标地址为 1(有料进来)并回复 ACK 确认信号
|
/// - 如果设备不需要物料(materialRequest=false),则通知设备可以继续出料(setOutputReady)
|
///
|
/// 这是拘束机和插拔钉机共用的处理逻辑,设备特有的部分已在此方法外封装。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于写入目标地址(Target)和 ACK 信号</param>
|
/// <param name="childDeviceCode">子设备编码,用于精确定位写入哪个子设备</param>
|
/// <param name="materialRequest">物料请求标志,true=设备当前需要补充物料</param>
|
/// <param name="outputRequest">出料请求标志,true=设备当前有物料需要输出(配合 materialRequest=false 时使用)</param>
|
/// <param name="setOutputReady">设置设备输出就绪标志的委托,根据层级写入 Upper 或 Lower 寄存器</param>
|
/// <param name="deviceType">设备类型描述字符串,用于日志输出</param>
|
private void ProcessDeviceRequest(
|
CommonConveyorLine conveyorLine,
|
string childDeviceCode,
|
bool materialRequest,
|
bool outputRequest,
|
Action<bool> setOutputReady,
|
string deviceType)
|
{
|
// 记录当前请求状态的调试日志,供排查问题使用
|
_logger.LogDebug("ProcessDeviceRequest:{DeviceType},子设备: {ChildDeviceCode},物料请求: {MaterialReq},出料请求: {OutputReq}",
|
deviceType, childDeviceCode, materialRequest, outputRequest);
|
// 同步写入 Quartz 日志文件(双写可追溯,这里保留与原逻辑一致的行为)
|
QuartzLogger.Debug($"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialRequest},出料请求: {outputRequest}", conveyorLine.DeviceCode);
|
|
// 分支判断:设备是需要物料还是需要出料
|
if (materialRequest)
|
{
|
// 设备需要物料 -> 通知输送线有料过来
|
// 1. 设置目标地址为 1,表示"有料进入"
|
conveyorLine.SetValue(ConveyorLineDBNameNew.Target, 1, childDeviceCode);
|
// 2. 回复 ACK 确认信号,告知设备已收到请求
|
conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
|
// 3. 记录信息日志,表明已完成目标地址设置和 ACK 回复
|
_logger.LogInformation("ProcessDeviceRequest:{DeviceType} 需要物料,已设置目标地址和ACK", deviceType);
|
QuartzLogger.Info($"ProcessDeviceRequest:{deviceType} 需要物料,已设置目标地址和ACK", conveyorLine.DeviceCode);
|
}
|
else
|
{
|
// 设备不需要物料 -> 通知设备可以继续出料(无论当前是否有货要出,都要通知)
|
// outputRequest 表示设备当前是否确实有货,如果没有货则 outputReady=false,设备收到后等待
|
setOutputReady(outputRequest);
|
}
|
}
|
|
/// <summary>
|
/// 写入调试日志(同时输出到两个日志系统)
|
/// </summary>
|
/// <remarks>
|
/// 统一入口点日志格式,同时向 Microsoft.Extensions.Logging 和 QuartzLogger 写入,
|
/// 保证日志既能在控制台查看也能在文件中追溯。
|
/// </remarks>
|
/// <param name="conveyorLine">输送线设备对象,用于获取设备编码写入 QuartzLogger</param>
|
/// <param name="scenario">场景描述,如"入库下一地址"或"出库下一地址"</param>
|
/// <param name="childDeviceCode">子设备编码</param>
|
/// <param name="nextAddress">目标设备编码</param>
|
private void WriteDebug(CommonConveyorLine conveyorLine, string scenario, string childDeviceCode, string nextAddress)
|
{
|
// 写入结构化日志(可被 Serilog 等日志框架捕获)
|
_logger.LogDebug("Handle{Scenario}:子设备: {ChildDeviceCode},目标地址: {NextAddress}",
|
scenario, childDeviceCode, nextAddress);
|
// 写入 Quartz 专用日志文件(供定时任务轨迹追踪)
|
QuartzLogger.Debug($"Handle{scenario}:子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
|
}
|
}
|
}
|