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