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) { // 记录当前请求状态的调试日志,供排查问题使用 QuartzLogHelper.LogDebug(_logger, "ProcessDeviceRequest:{DeviceType},子设备: {ChildDeviceCode},物料请求: {MaterialReq},出料请求: {OutputReq}", $"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialRequest},出料请求: {outputRequest}", conveyorLine.DeviceCode, deviceType, childDeviceCode, materialRequest, outputRequest); // 分支判断:设备是需要物料还是需要出料 if (materialRequest) { // 设备需要物料 -> 通知输送线有料过来 // 1. 设置目标地址为 1,表示"有料进入" conveyorLine.SetValue(ConveyorLineDBNameNew.Target, 1, childDeviceCode); // 2. 回复 ACK 确认信号,告知设备已收到请求 conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode); // 3. 记录信息日志,表明已完成目标地址设置和 ACK 回复 QuartzLogHelper.LogInfo(_logger, "ProcessDeviceRequest:{DeviceType} 需要物料,已设置目标地址和ACK", $"ProcessDeviceRequest:{deviceType} 需要物料,已设置目标地址和ACK", conveyorLine.DeviceCode, deviceType); } else { // 设备不需要物料 -> 通知设备可以继续出料(无论当前是否有货要出,都要通知) // outputRequest 表示设备当前是否确实有货,如果没有货则 outputReady=false,设备收到后等待 setOutputReady(outputRequest); } } /// /// 写入调试日志(同时输出到两个日志系统) /// /// /// 统一入口点日志格式,同时向 Microsoft.Extensions.Logging 和 QuartzLogger 写入, /// 保证日志既能在控制台查看也能在文件中追溯。 /// /// 输送线设备对象,用于获取设备编码写入 QuartzLogger /// 场景描述,如"入库下一地址"或"出库下一地址" /// 子设备编码 /// 目标设备编码 private void WriteDebug(CommonConveyorLine conveyorLine, string scenario, string childDeviceCode, string nextAddress) { // 写入结构化日志(可被 Serilog 等日志框架捕获) QuartzLogHelper.LogDebug(_logger, "Handle{Scenario}:子设备: {ChildDeviceCode},目标地址: {NextAddress}", $"Handle{scenario}:子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode, scenario, childDeviceCode, nextAddress); } } }