wanshenmean
8 天以前 fd18eaba5e1c086a588509371f91310e7aafff9c
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs
@@ -1,108 +1,466 @@
using WIDESEAWCS_QuartzJob;
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
    {
        private const string ConstraintMachineName = "拘束机";
        private const string PinMachineName = "插拔钉机";
        /// <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)
        {
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: true);
            // 记录入库场景的调试日志,包含子设备和目标地址信息
            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)
        {
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: false);
            // 记录出库场景的调试日志,包含子设备和目标地址信息
            WriteDebug(conveyorLine, "出库下一地址", childDeviceCode, nextAddress);
            // 委托通用处理方法,出库对应下层(isUpper: false)
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, Layer.Lower);
        }
        private void HandleDeviceRequest(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode, bool isUpper)
        /// <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)
        {
            var devices = Storage.Devices;
            if (string.Equals(nextAddress, ConstraintMachineName, StringComparison.Ordinal))
            // 通过字典查找目标地址对应的设备类型,如果找不到则 deviceType 为 None
            if (!AddressToDeviceType.TryGetValue(nextAddress, out var deviceType) || deviceType == TargetDeviceType.None)
            {
                ConstraintMachine? constraint = devices.OfType<ConstraintMachine>().FirstOrDefault(d => d.DeviceName == ConstraintMachineName);
                if (constraint == null)
                {
                    return;
                }
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    getMaterialRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, bool>(ConstraintMachineDBName.MaterialRequestUpper)
                        : constraint.GetValue<ConstraintMachineDBName, bool>(ConstraintMachineDBName.MaterialRequestLower),
                    getOutputRequest: () => isUpper
                        ? constraint.GetValue<ConstraintMachineDBName, bool>(ConstraintMachineDBName.OutputRequestUpper)
                        : constraint.GetValue<ConstraintMachineDBName, bool>(ConstraintMachineDBName.OutputRequestLower),
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
                        {
                            constraint.SetValue(ConstraintMachineDBName.ConstraintTrayOutputReadyUpper, outputReq ? 1 : 0);
                        }
                        else
                        {
                            constraint.SetValue(ConstraintMachineDBName.ConstraintTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    });
                // 目标地址不在已知映射表中,直接返回(可能是其他类型设备)
                return;
            }
            else if (string.Equals(nextAddress, PinMachineName, StringComparison.Ordinal))
            {
                PinMachine? pinMachine = devices.OfType<PinMachine>().FirstOrDefault(d => d.DeviceName == PinMachineName);
                if (pinMachine == null)
                {
                    return;
                }
                ProcessDeviceRequest(
                    conveyorLine,
                    childDeviceCode,
                    getMaterialRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, bool>(PinMachineDBName.MaterialRequestUpper)
                        : pinMachine.GetValue<PinMachineDBName, bool>(PinMachineDBName.MaterialRequestLower),
                    getOutputRequest: () => isUpper
                        ? pinMachine.GetValue<PinMachineDBName, bool>(PinMachineDBName.OutputRequestUpper)
                        : pinMachine.GetValue<PinMachineDBName, bool>(PinMachineDBName.OutputRequestLower),
                    setOutputReady: outputReq =>
                    {
                        if (isUpper)
                        {
                            pinMachine.SetValue(PinMachineDBName.PlugPinTrayOutputReadyUpper, outputReq ? 1 : 0);
                        }
                        else
                        {
                            pinMachine.SetValue(PinMachineDBName.PlugPinTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    });
            // 根据识别出的设备类型分发到对应的处理方法
            switch (deviceType)
            {
                case TargetDeviceType.ConstraintMachine:
                    // 拘束机处理分支:获取拘束机实例并处理其上下层请求
                    HandleConstraintMachine(conveyorLine, childDeviceCode, layer);
                    break; // 处理完毕,跳出 switch
                case TargetDeviceType.PinMachine:
                    // 插拔钉机处理分支:获取插拔钉机实例并处理其上下层请求
                    HandlePinMachine(conveyorLine, childDeviceCode, layer);
                    break; // 处理完毕,跳出 switch
            }
        }
        private static void ProcessDeviceRequest(
        /// <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,
            Func<bool> getMaterialRequest,
            Func<bool> getOutputRequest,
            Action<bool> setOutputReady)
            bool materialRequest,
            bool outputRequest,
            Action<bool> setOutputReady,
            string deviceType)
        {
            bool materialReq = getMaterialRequest();
            bool outputReq = getOutputRequest();
            // 记录当前请求状态的调试日志,供排查问题使用
            _logger.LogDebug("ProcessDeviceRequest:{DeviceType},子设备: {ChildDeviceCode},物料请求: {MaterialReq},出料请求: {OutputReq}",
                deviceType, childDeviceCode, materialRequest, outputRequest);
            // 同步写入 Quartz 日志文件(双写可追溯,这里保留与原逻辑一致的行为)
            QuartzLogger.Debug($"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialRequest},出料请求: {outputRequest}", conveyorLine.DeviceCode);
            if (materialReq)
            // 分支判断:设备是需要物料还是需要出料
            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
            {
                setOutputReady(outputReq);
                // 设备不需要物料 -> 通知设备可以继续出料(无论当前是否有货要出,都要通知)
                // 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);
        }
    }
}