using Sres.Net.EEIP; using System; using System.Collections.Generic; using System.Data.Entity.Core.Metadata.Edm; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using TeamAAS_VP.Models; namespace TeamAAS_VP.Core.RFID { /// /// SIG350 EtherNet/IP 通讯类 - 专为 RFID 应用设计 /// 支持 SICK IO-Link RFID 读写头 (RFU63x, RFU62x 等) /// public class SIG350RFIDClient : IDisposable { #region 私有字段 private readonly EEIPClient _client; private readonly string _ipAddress; private bool _isConnected; private bool _isIOModeActive; private CancellationTokenSource _ioCancelToken; private Task _ioReceiveTask; private readonly object _ioSync = new object(); private O2TData _lastOutput = new O2TData(); private T2OData _lastInput = new T2OData(); private readonly HashSet _enabledPorts = new HashSet { 1 }; private Rfh5xxReadOptions _readOptions = new Rfh5xxReadOptions(); private volatile bool _disposed; private volatile bool _autoReconnectEnabled = true; private int _lastRpiMs = 10; private readonly object _reconnectSync = new object(); private CancellationTokenSource _reconnectCts; private Task _reconnectTask; /// 初始连接重试次数 public int ConnectRetryCount { get; set; } = 10; /// 初始连接重试间隔(毫秒) public int ConnectRetryDelayMs { get; set; } = 2000; /// 启动 I/O 前回调(Management 用于配置端口/读模式,含 EIP 重连场景)。 public Action BeforeStartRfidReading { get; set; } /// 断线后自动重连基础间隔(毫秒),随失败次数递增,最大 30 秒 public int ReconnectIntervalMs { get; set; } = 3000; // Assembly ID(EDS / 手册) private const ushort ASSEMBLY_CONFIG = 102; // 配置:端口 80 字节,完整含 DIO 为 130 private const ushort ASSEMBLY_O2T = 100; // 输出 276 字节 private const ushort ASSEMBLY_T2O = 101; // 输入 276 字节 private const int PROCESS_DATA_PER_PORT = 32; private const int IO_ASSEMBLY_SIZE = 4 + 8 * (2 + PROCESS_DATA_PER_PORT); // 276 private const int CONFIG_PORT_SIZE = 80; // 8 × 10 private const int CONFIG_FULL_SIZE = 130; // 80 + layout + substitute + 8×6 DIO // IOL Status Bit7 = 过程数据有效 (PQ) private const byte IOL_STATUS_PQ_VALID = 0x80; private const byte IOL_STATUS_DEV_COM = 0x20; // RFH5xx HF ISO15693 PDI/PDO(手册 8.3.1) private const byte RFH5xxPdoStart = 0x01; private const byte RFH5xxTagPresent = 0x04; private const byte RFH5xxFault = 0x02; private const int RFH5xxCmdAutoRead = 1; private const int RFH5xxCmdRead = 3; private const int RFH5xxCmdReadUid = 5; private const int RFH5xxUidLength = 8; private const int RFH5xxMaxUserPayload = 28; // RFU61x UHF(保留兼容) private const byte RFU_TAG_PRESENT = 0x04; private const int RFU_LENGTH_INDEX = 3; private const int RFU_DATA_OFFSET = 4; private const int RFU_MAX_EPC_LEN = 28; BgEthernetIP Config; #endregion #region 事件 public event EventHandler OnTagDataReceived; public event Action OnConnectReceive; public event EventHandler OnError; public event Action OnLog; #endregion #region 构造函数 public SIG350RFIDClient(BgEthernetIP config) { Config = config; _ipAddress = config.IP; _client = new EEIPClient(); OnConnectReceive += HandleConnectionChanged; } #endregion #region 连接管理 public bool Connect() { StopReconnectLoop(); for (int i = 0; i < ConnectRetryCount; i++) { if (InternalConnect()) { OnConnectReceive?.Invoke(true); return true; } if (i < ConnectRetryCount - 1) Thread.Sleep(ConnectRetryDelayMs); } OnConnectReceive?.Invoke(false); return false; } private bool InternalConnect() { try { if (_isConnected) return true; _client.RegisterSession(_ipAddress); _isConnected = true; OnLog?.Invoke($"成功连接到 SIG350 ({_ipAddress})"); return true; } catch (Exception ex) { OnLog?.Invoke($"连接失败: {ex.Message}"); OnError?.Invoke(this, ex); CleanupSessionOnly(); return false; } } private void HandleConnectionChanged(bool connected) { if (connected || _disposed || !_autoReconnectEnabled) return; ScheduleReconnect(); } private void ScheduleReconnect() { lock (_reconnectSync) { if (_disposed || !_autoReconnectEnabled) return; if (_reconnectTask != null && !_reconnectTask.IsCompleted) return; StopReconnectLoop(); _reconnectCts = new CancellationTokenSource(); var token = _reconnectCts.Token; _reconnectTask = Task.Run(() => ReconnectLoop(token), token); } } private void ReconnectLoop(CancellationToken token) { OnLog?.Invoke("EIP 连接断开,开始自动重连..."); int attempt = 0; while (!token.IsCancellationRequested && !_disposed && _autoReconnectEnabled) { attempt++; try { if (TryRestoreSession()) { OnLog?.Invoke($"EIP 重连成功(第 {attempt} 次)"); OnConnectReceive?.Invoke(true); return; } OnLog?.Invoke($"EIP 重连失败(第 {attempt} 次),稍后重试"); } catch (Exception ex) { OnLog?.Invoke($"EIP 重连异常: {ex.Message}"); } int delay = Math.Min(ReconnectIntervalMs * Math.Min(attempt, 10), 30000); if (token.WaitHandle.WaitOne(delay)) break; } } private bool TryRestoreSession() { CleanupSessionOnly(); if (!InternalConnect()) return false; if (_lastRpiMs <= 0) return true; try { StartRFIDReading(_lastRpiMs); return true; } catch (Exception ex) { OnLog?.Invoke($"EIP 重连后恢复 I/O 失败: {ex.Message}"); CleanupSessionOnly(); return false; } } private void StopReconnectLoop() { try { if (_reconnectCts != null) { _reconnectCts.Cancel(); try { if (_reconnectTask != null) _reconnectTask.Wait(3000); } catch (AggregateException) { /* ignore */ } _reconnectCts.Dispose(); } } catch { /* ignore */ } finally { _reconnectCts = null; _reconnectTask = null; } } public void Disconnect() { _autoReconnectEnabled = false; StopReconnectLoop(); CleanupResources(); } public bool IsConnected => _isConnected; public bool IsIOModeActive => _isIOModeActive; /// /// 当前启用监听的端口(1-8)。空集合表示不上报任何标签。 /// public IReadOnlyCollection EnabledPorts { get { lock (_ioSync) return _enabledPorts.OrderBy(p => p).ToList(); } } /// /// 设置要读取的读写头端口(1-8)。 /// public void SetEnabledPorts(IEnumerable ports) { if (ports == null) throw new ArgumentNullException(nameof(ports)); var next = new HashSet(); foreach (int p in ports) { if (p < 1 || p > 8) throw new ArgumentException($"端口号无效: {p},必须在1-8之间"); next.Add(p); } lock (_ioSync) { _enabledPorts.Clear(); foreach (int p in next) _enabledPorts.Add(p); } OnLog?.Invoke(next.Count == 0 ? "未启用任何 RFID 端口" : $"已启用 RFID 端口: {string.Join(",", next.OrderBy(x => x))}"); } private bool IsPortEnabled(int port) { lock (_ioSync) return _enabledPorts.Contains(port); } /// /// 配置 RFH5xx 读取内容:UID 或用户存储区(手册 8.3.1)。 /// public void ConfigureReadOptions(Rfh5xxReadOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); lock (_ioSync) _readOptions = options.Clone(); } public Rfh5xxReadOptions GetReadOptions() { lock (_ioSync) return _readOptions.Clone(); } #endregion #region 配置管理 /// /// 配置 RFID 端口为 IO-Link 模式 /// public void ConfigureRFIDPort(int port, PortMode mode = PortMode.IOLAutostart) { if (port < 1 || port > 8) throw new ArgumentException("端口号必须在1-8之间"); try { var config = ReadConfiguration(); ApplyPortMode(config, port, mode); WriteConfiguration(config); OnLog?.Invoke($"端口 {port} 已配置为 {mode}"); } catch (Exception ex) { OnLog?.Invoke($"配置端口 {port} 失败: {ex.Message}"); throw; } } /// /// 批量配置多个端口为 IO-Link 模式,并更新启用端口集合。 /// public void ConfigureRFIDPorts(IEnumerable ports, PortMode mode = PortMode.IOLAutostart) { if (ports == null) throw new ArgumentNullException(nameof(ports)); var list = ports.Distinct().OrderBy(p => p).ToList(); if (list.Count == 0) throw new ArgumentException("至少选择一个端口"); foreach (int p in list) { if (p < 1 || p > 8) throw new ArgumentException($"端口号无效: {p},必须在1-8之间"); } try { var config = ReadConfiguration(); foreach (int port in list) ApplyPortMode(config, port, mode); WriteConfiguration(config); SetEnabledPorts(list); OnLog?.Invoke($"端口 [{string.Join(",", list)}] 已配置为 {mode}"); } catch (Exception ex) { OnLog?.Invoke($"批量配置端口失败: {ex.Message}"); throw; } } private static void ApplyPortMode(SIG350Configuration config, int port, PortMode mode) { config.PortMode[port - 1] = (byte)mode; config.IQBehavior[port - 1] = (byte)IQBehavior.NotSupported; config.ValidationBackup[port - 1] = 0; config.PortCycleTime[port - 1] = 0; } private SIG350Configuration ReadConfiguration() { byte[] data = ReadAssembly(ASSEMBLY_CONFIG); if (data == null || data.Length < CONFIG_PORT_SIZE) throw new InvalidOperationException($"读取配置失败,长度={data?.Length ?? 0},至少需要 {CONFIG_PORT_SIZE}"); return ParseConfiguration(data); } private void WriteConfiguration(SIG350Configuration config) { if (config == null) throw new ArgumentNullException(nameof(config)); // 按设备返回长度写回:>=130 用完整块,否则只写 EDS 规定的 80 字节端口配置 int size = config.RawLength >= CONFIG_FULL_SIZE ? CONFIG_FULL_SIZE : CONFIG_PORT_SIZE; byte[] data = SerializeConfiguration(config, size); WriteAssembly(ASSEMBLY_CONFIG, data); } #endregion #region RFID 数据操作 public void StartRFIDReading(int rpiMilliseconds = 10) { if (!_isConnected) throw new InvalidOperationException("未连接到SIG350"); _lastRpiMs = Math.Max(1, rpiMilliseconds); if (_isIOModeActive) return; BeforeStartRfidReading?.Invoke(); List ports; lock (_ioSync) ports = _enabledPorts.OrderBy(p => p).ToList(); if (ports.Count == 0) ports.Add(1); try { StartIOCommunication(rpiMilliseconds); EnablePortsOutput(ports); ApplyRfh5xxCommandForPorts(ports, oneShot: false); string modeText = _readOptions.Kind == RfidPayloadKind.Uid ? "Read UID" : "Auto-Read 用户区"; OnLog?.Invoke($"RFID数据读取已启动 (端口: {string.Join(",", ports)}, RPI: {rpiMilliseconds}ms, 模式: {modeText})"); } catch (Exception ex) { // ForwardOpen 已成功但后续失败时必须关掉,避免半开连接 try { StopIOCommunication(); } catch { /* ignore */ } OnLog?.Invoke($"启动RFID读取失败: {ex.Message}"); throw; } } public void StopRFIDReading() { try { if (_isIOModeActive) { try { DisableAllPortOutputs(); } catch { /* ignore */ } } } finally { StopIOCommunication(); OnLog?.Invoke("RFID数据读取已停止"); } } /// /// Autostart 模式下只需使能端口输出;不要向过程数据写入臆造命令字节, /// 以免覆盖 IO-Link 设备输出映像。 /// private void EnablePortsOutput(IList ports) { lock (_ioSync) { var output = CloneOutput(_lastOutput); for (int i = 0; i < 8; i++) { output.OutputEnable[i] = 0; Array.Clear(output.PortProcessData[i], 0, PROCESS_DATA_PER_PORT); } foreach (int port in ports) output.OutputEnable[port - 1] = 1; WriteOtIoData(SerializeO2TData(output)); _lastOutput = output; } } private void DisableAllPortOutputs() { lock (_ioSync) { var output = CloneOutput(_lastOutput); for (int i = 0; i < 8; i++) { output.OutputEnable[i] = 0; Array.Clear(output.PortProcessData[i], 0, PROCESS_DATA_PER_PORT); } WriteOtIoData(SerializeO2TData(output)); _lastOutput = output; } } /// /// 使能端口并向 RFH5xx 下发 PDO 命令(Read UID / Auto-Read)。 /// private void ApplyRfh5xxCommandForPorts(IList ports, bool oneShot) { if (ports == null || ports.Count == 0) return; lock (_ioSync) { var output = CloneOutput(_lastOutput); foreach (int port in ports) { output.OutputEnable[port - 1] = 1; WriteRfh5xxPdoToPort(output, port, _readOptions, start: true); } SendOutputData(output); _lastOutput = output; } } private void ApplyRfh5xxCommandForPort(int port, bool oneShot, bool start) { lock (_ioSync) { var output = CloneOutput(_lastOutput); output.OutputEnable[port - 1] = 1; WriteRfh5xxPdoToPort(output, port, _readOptions, start); SendOutputData(output); _lastOutput = output; } } /// RFH5xx PDO:Byte0 bit0=START, bit5~7=CMD;用户区 Byte1=块数, Byte3=起始块。 /// /// RFH5xx 正确 PDO 命令格式:Byte0 = 命令码 /// Read UID = 5,Auto-Read 用户区 = 1,单次读用户区 = 3 /// private static void WriteRfh5xxPdoToPort(O2TData output, int port, Rfh5xxReadOptions options, bool start) { var pdo = new byte[32]; if (!start) { // 停止:首字节写 0 Array.Copy(pdo, 0, output.PortProcessData[port - 1], 0, 32); return; } // 命令码直接写入 Byte0 int cmd = options.Kind == RfidPayloadKind.Uid ? RFH5xxCmdReadUid // 5 : RFH5xxCmdAutoRead; // 1 pdo[0] = (byte)cmd; // 用户区配置:Byte1=块数,Byte3=起始块地址 if (options.Kind == RfidPayloadKind.UserMemory) { pdo[1] = (byte)(options.UserBlockCount & 0x1F); pdo[3] = options.UserBlockAddress; } Array.Copy(pdo, 0, output.PortProcessData[port - 1], 0, 32); } public RFIDTagData ReadRFIDTag(int port, int timeoutMs = 1000, Rfh5xxReadOptions options = null) { if (port < 1 || port > 8) throw new ArgumentException("端口号必须在1-8之间"); Rfh5xxReadOptions opts = options ?? GetReadOptions(); try { if (!_isIOModeActive) throw new InvalidOperationException("I/O通讯未启动,请先启动监听"); // Read/ReadUID:翻转 START 触发一次(手册 8.3.1.2) ApplyRfh5xxCommandForPort(port, oneShot: opts.Kind == RfidPayloadKind.UserMemory, start: false); Thread.Sleep(50); ApplyRfh5xxCommandForPort(port, oneShot: opts.Kind == RfidPayloadKind.UserMemory, start: true); DateTime startTime = DateTime.Now; while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs) { var input = ReadInputData(); var tag = TryExtractTag(input, port, opts); if (tag != null) return tag; Thread.Sleep(10); } return null; } catch (Exception ex) { OnLog?.Invoke($"读取RFID标签失败: {ex.Message}"); throw; } finally { } } [Obsolete("RFH5xx 请使用 ReadRFIDTag;此方法保留兼容。")] public void SendRFIDCommand(int port, RFIDCommand command, byte[] data = null) { if (port < 1 || port > 8) throw new ArgumentException("端口号必须在1-8之间"); try { lock (_ioSync) { var output = CloneOutput(_lastOutput); output.OutputEnable[port - 1] = 1; Array.Clear(output.PortProcessData[port - 1], 0, PROCESS_DATA_PER_PORT); if (data != null && data.Length > 0) { int copyLen = Math.Min(PROCESS_DATA_PER_PORT, data.Length); Array.Copy(data, 0, output.PortProcessData[port - 1], 0, copyLen); } // 过程数据第 0 字节作为简易触发(具体含义取决于读写头) if (command == RFIDCommand.StartRead) output.PortProcessData[port - 1][0] = 0x85; else if (command == RFIDCommand.StopRead) output.PortProcessData[port - 1][0] = 0x00; SendOutputData(output); _lastOutput = output; } OnLog?.Invoke($"RFID命令已发送到端口 {port}: {command}"); } catch (Exception ex) { OnLog?.Invoke($"发送RFID命令失败: {ex.Message}"); throw; } } public RFIDTagData ReadRFIDTag(int port, int timeoutMs = 1000) { return ReadRFIDTag(port, timeoutMs, null); } private RFIDTagData TryExtractTag(T2OData input, int port, Rfh5xxReadOptions options = null) { if (input == null || port < 1 || port > 8) return null; if (input.PortStatus == null || input.PortProcessData == null) return null; byte[] processData = input.PortProcessData[port - 1]; if (processData == null) return null; byte status = input.PortStatus[port - 1]; if ((status & IOL_STATUS_PQ_VALID) == 0) return null; byte[] tagData = ExtractRFIDData(processData, options ?? _readOptions); if (tagData == null || tagData.Length == 0) return null; return new RFIDTagData { Port = port, Data = tagData, Timestamp = DateTime.Now, Quality = status, PayloadKind = (options ?? _readOptions).Kind }; } /// /// 从 IO-Link 32 字节 PDI 提取有效载荷(默认 RFH5xx,失败时尝试 RFU61x)。 /// internal static byte[] ExtractRFIDData(byte[] processData, Rfh5xxReadOptions options = null) { if (processData == null || processData.Length == 0) return null; if (IsAllZero(processData)) return null; if (options == null) options = new Rfh5xxReadOptions(); byte[] rfh = ExtractRfh5xxPayload(processData, options); if (rfh != null && rfh.Length > 0) return rfh; return ExtractRfu61xPayload(processData); } /// RFH5xx:Tag=Byte0 bit2;UID=Byte4~11;用户区=Byte4 起。 internal static byte[] ExtractRfh5xxPayload(byte[] pdi, Rfh5xxReadOptions options) { if (pdi == null || pdi.Length < 12 || options == null) return null; byte b0 = pdi[0]; if ((b0 & RFH5xxTagPresent) == 0) return null; if ((b0 & RFH5xxFault) != 0) return null; if (options.Kind == RfidPayloadKind.Uid) { if (pdi.Length < 4 + RFH5xxUidLength) return null; byte[] uid = new byte[RFH5xxUidLength]; Array.Copy(pdi, 4, uid, 0, RFH5xxUidLength); return IsAllZero(uid) ? null : uid; } int byteCount = options.UserBlockCount * options.UserBlockSizeBytes; if (byteCount <= 0) return null; byteCount = Math.Min(RFH5xxMaxUserPayload, Math.Min(byteCount, pdi.Length - 4)); byte[] user = new byte[byteCount]; Array.Copy(pdi, 4, user, 0, byteCount); return IsAllZero(user) ? null : user; } private static byte[] ExtractRfu61xPayload(byte[] processData) { if (processData.Length >= RFU_DATA_OFFSET + 1) { bool tagPresent = (processData[0] & RFU_TAG_PRESENT) != 0; int epcLen = processData[RFU_LENGTH_INDEX]; if (tagPresent) { if (epcLen <= 0 || epcLen > RFU_MAX_EPC_LEN || epcLen > processData.Length - RFU_DATA_OFFSET) return null; if (IsAllZero(processData, RFU_DATA_OFFSET, epcLen)) return null; byte[] epc = new byte[epcLen]; Array.Copy(processData, RFU_DATA_OFFSET, epc, 0, epcLen); return epc; } // TagPresent=0 且 Length=0:RFU 空闲帧(无芯片),忽略状态/RSSI 噪声 if (epcLen == 0) return null; } // 2) 其它设备:仅接受「首字节=长度 + 后面全是填充0」且长度合理(>=4) int dataLength = processData[0]; if (dataLength >= 4 && dataLength < processData.Length) { int payloadEnd = 1 + dataLength; if (payloadEnd <= processData.Length && IsAllZero(processData, payloadEnd, processData.Length - payloadEnd) && !IsAllZero(processData, 1, dataLength)) { byte[] result = new byte[dataLength]; Array.Copy(processData, 1, result, 0, dataLength); return result; } } // 不再把“去尾零后的任意非零过程数据”当作标签 return null; } private static bool IsAllZero(byte[] data) { return IsAllZero(data, 0, data.Length); } private static bool IsAllZero(byte[] data, int offset, int count) { if (data == null || count <= 0) return true; int end = Math.Min(data.Length, offset + count); for (int i = offset; i < end; i++) { if (data[i] != 0) return false; } return true; } #endregion #region I/O通讯 (隐式消息) private void StartIOCommunication(int rpiMilliseconds = 10) { if (!_isConnected) throw new InvalidOperationException("未连接到SIG350"); if (_isIOModeActive) return; try { // RPI:界面单位是 ms,协议单位是 μs。至少 1ms,避免 Max(1000, ms) 把 10ms 误变成 1 秒。 int rpiMs = Math.Max(1, rpiMilliseconds); uint rpiMicroseconds = (uint)rpiMs * 1000u; _client.O_T_InstanceID = (byte)ASSEMBLY_O2T; _client.T_O_InstanceID = (byte)ASSEMBLY_T2O; _client.O_T_Length = (ushort)IO_ASSEMBLY_SIZE; _client.T_O_Length = (ushort)IO_ASSEMBLY_SIZE; _client.O_T_RealTimeFormat = RealTimeFormat.Header32Bit; _client.T_O_RealTimeFormat = RealTimeFormat.Modeless; _client.O_T_OwnerRedundant = false; _client.T_O_OwnerRedundant = false; _client.O_T_Priority = Priority.Scheduled; _client.T_O_Priority = Priority.Scheduled; _client.O_T_VariableLength = false; _client.T_O_VariableLength = false; _client.O_T_ConnectionType = ConnectionType.Point_to_Point; _client.T_O_ConnectionType = ConnectionType.Point_to_Point; _client.RequestedPacketRate_O_T = rpiMicroseconds; _client.RequestedPacketRate_T_O = rpiMicroseconds; // 使用空配置实例,端口配置已通过显式 Assembly 102 写入 _client.ConfigurationAssemblyInstanceID = 1; _client.ForwardOpen(); // 初始化输出缓冲区(必须通过 setter 写回,getter 返回的是 Clone) lock (_ioSync) { _lastOutput = new O2TData(); WriteOtIoData(SerializeO2TData(_lastOutput)); } _isIOModeActive = true; _ioCancelToken = new CancellationTokenSource(); _ioReceiveTask = Task.Run(() => IOReceiveLoop(_ioCancelToken.Token)); OnLog?.Invoke($"I/O通讯已启动 (O2T/T2O={IO_ASSEMBLY_SIZE}字节, RPI={rpiMs}ms)"); } catch (Exception ex) { _isIOModeActive = false; OnLog?.Invoke($"启动I/O通讯失败: {ex.Message}"); throw; } } private void StopIOCommunication() { if (!_isIOModeActive) return; try { _ioCancelToken?.Cancel(); try { _ioReceiveTask?.Wait(3000); } catch (AggregateException) { /* ignore cancel */ } try { _client.ForwardClose(); } catch (Exception ex) { OnLog?.Invoke($"ForwardClose: {ex.Message}"); } } catch (Exception ex) { OnLog?.Invoke($"停止I/O通讯异常: {ex.Message}"); } finally { _isIOModeActive = false; _ioCancelToken?.Dispose(); _ioCancelToken = null; _ioReceiveTask = null; } } private void IOReceiveLoop(CancellationToken token) { // 按端口记录上次上报指纹,标签离开后清掉,相同标签再次进入仍可上报 var lastByPort = new string[8]; while (!token.IsCancellationRequested && _isIOModeActive) { try { var input = TryReadInputData(); if (input == null) { Thread.Sleep(10); continue; } try { // 显式读取一次输入Assembly,验证设备响应 byte[] test = _client.AssemblyObject.getInstance(ASSEMBLY_T2O); if (test == null || test.Length == 0) throw new Exception("Assembly读取为空"); } catch { OnLog?.Invoke("心跳检测失败,IO连接已断开"); StopIOCommunication(); CleanupSessionOnly(); OnConnectReceive?.Invoke(false); break; } var changedTags = new List(); for (int port = 1; port <= 8; port++) { if (!IsPortEnabled(port)) { lastByPort[port - 1] = null; continue; } var tag = TryExtractTag(input, port, _readOptions); if (tag == null) { lastByPort[port - 1] = null; continue; } string fingerprint = tag.ToHexString(); if (fingerprint == lastByPort[port - 1]) continue; lastByPort[port - 1] = fingerprint; changedTags.Add(tag); } if (changedTags.Count > 0) { var args = new RFIDTagDataEventArgs(input, changedTags); OnTagDataReceived?.Invoke(this, args); } Thread.Sleep(10); } catch (Exception ex) { if (token.IsCancellationRequested) break; OnLog?.Invoke($"接收I/O数据异常: {ex.Message}"); OnError?.Invoke(this, ex); Thread.Sleep(100); } } } private void SendOutputData(O2TData outputData) { if (!_isIOModeActive) throw new InvalidOperationException("I/O通讯未启动"); // 调用方若已持有 _ioSync,Monitor 可重入;此处统一走 setter 写回 byte[] data = SerializeO2TData(outputData); lock (_ioSync) { WriteOtIoData(data); } } /// /// EEIP.NetStandard 的 O_T_IOData getter 返回 Clone, /// 直接改下标或 Array.Copy 到 getter 结果都不会写回内部缓冲区,必须走 setter。 /// private void WriteOtIoData(byte[] data) { if (data == null) throw new ArgumentNullException(nameof(data)); _client.O_T_IOData = data; } /// /// 尝试读取输入;缓冲区尚未就绪时返回 null,避免接收循环狂抛异常。 /// private T2OData TryReadInputData() { if (!_isIOModeActive) return null; byte[] data = _client.T_O_IOData; if (data == null || data.Length < IO_ASSEMBLY_SIZE) return null; var parsed = ParseT2OData(data); lock (_ioSync) _lastInput = parsed; return parsed; } private T2OData ReadInputData() { if (!_isIOModeActive) throw new InvalidOperationException("I/O通讯未启动"); var parsed = TryReadInputData(); if (parsed == null) throw new InvalidOperationException("T2O 数据尚未就绪或长度不足"); return parsed; } #endregion #region 数据序列化/反序列化 private static T2OData ParseT2OData(byte[] data) { if (data == null || data.Length < IO_ASSEMBLY_SIZE) throw new ArgumentException($"输入数据无效,需要 {IO_ASSEMBLY_SIZE} 字节,实际 {data?.Length ?? 0}"); var result = new T2OData(); int offset = 0; result.DIStatus = data[offset++]; offset++; // Reserved result.DIData = BitConverter.ToUInt16(data, offset); offset += 2; for (int port = 0; port < 8; port++) { result.PortStatus[port] = data[offset++]; offset++; // Reserved Array.Copy(data, offset, result.PortProcessData[port], 0, PROCESS_DATA_PER_PORT); offset += PROCESS_DATA_PER_PORT; } return result; } private static byte[] SerializeO2TData(O2TData data) { if (data == null) throw new ArgumentNullException(nameof(data)); // 手册: 4 + 8×(OutputEnable + Reserved + 32 PD) = 276 byte[] result = new byte[IO_ASSEMBLY_SIZE]; int offset = 0; result[offset++] = data.DOStatus; result[offset++] = 0; // Reserved byte[] doBytes = BitConverter.GetBytes(data.DOData); result[offset++] = doBytes[0]; result[offset++] = doBytes[1]; for (int port = 0; port < 8; port++) { result[offset++] = data.OutputEnable != null && port < data.OutputEnable.Length ? data.OutputEnable[port] : (byte)0; result[offset++] = 0; // Reserved if (data.PortProcessData != null && port < data.PortProcessData.Length && data.PortProcessData[port] != null) { int copyLen = Math.Min(PROCESS_DATA_PER_PORT, data.PortProcessData[port].Length); Array.Copy(data.PortProcessData[port], 0, result, offset, copyLen); } offset += PROCESS_DATA_PER_PORT; } return result; } private static SIG350Configuration ParseConfiguration(byte[] data) { if (data == null || data.Length < CONFIG_PORT_SIZE) throw new ArgumentException($"配置数据无效,至少需要 {CONFIG_PORT_SIZE} 字节"); var config = new SIG350Configuration { RawLength = data.Length }; int offset = 0; for (int port = 0; port < 8; port++) { config.PortMode[port] = data[offset++]; config.ValidationBackup[port] = data[offset++]; config.IQBehavior[port] = data[offset++]; config.PortCycleTime[port] = data[offset++]; config.VendorID[port] = BitConverter.ToUInt16(data, offset); offset += 2; // Device ID:手册为 UDINT,有效 24 bit config.DeviceID[port] = BitConverter.ToUInt32(data, offset) & 0x00FFFFFF; offset += 4; } // 完整配置 130 字节:layout + substitute + 8×6 DIO(中间无 Reserved) if (data.Length >= CONFIG_FULL_SIZE) { config.DIOProcessDataLayout = data[offset++]; config.DOSubstituteMode = data[offset++]; for (int port = 0; port < 8; port++) { config.DI_IQPinPolarity[port] = data[offset++]; config.DI_CQPinPolarity[port] = data[offset++]; config.DI_IQPinSignalFilter[port] = data[offset++]; config.DI_CQPinSignalFilter[port] = data[offset++]; config.DO_IQPinMode[port] = data[offset++]; config.DO_CQPinMode[port] = data[offset++]; } } return config; } private static byte[] SerializeConfiguration(SIG350Configuration config, int size) { if (size != CONFIG_PORT_SIZE && size != CONFIG_FULL_SIZE) size = size >= CONFIG_FULL_SIZE ? CONFIG_FULL_SIZE : CONFIG_PORT_SIZE; byte[] data = new byte[size]; int offset = 0; for (int port = 0; port < 8; port++) { data[offset++] = config.PortMode[port]; data[offset++] = config.ValidationBackup[port]; data[offset++] = config.IQBehavior[port]; data[offset++] = config.PortCycleTime[port]; BitConverter.GetBytes(config.VendorID[port]).CopyTo(data, offset); offset += 2; BitConverter.GetBytes(config.DeviceID[port]).CopyTo(data, offset); offset += 4; } if (size >= CONFIG_FULL_SIZE) { data[offset++] = config.DIOProcessDataLayout; data[offset++] = config.DOSubstituteMode; for (int port = 0; port < 8; port++) { data[offset++] = config.DI_IQPinPolarity[port]; data[offset++] = config.DI_CQPinPolarity[port]; data[offset++] = config.DI_IQPinSignalFilter[port]; data[offset++] = config.DI_CQPinSignalFilter[port]; data[offset++] = config.DO_IQPinMode[port]; data[offset++] = config.DO_CQPinMode[port]; } } return data; } private byte[] ReadAssembly(ushort assemblyId) { byte[] data = _client.AssemblyObject.getInstance(assemblyId); OnLog?.Invoke($"读取 Assembly {assemblyId}: {data?.Length ?? 0} 字节"); return data; } private void WriteAssembly(ushort assemblyId, byte[] data) { _client.AssemblyObject.setInstance(assemblyId, data); OnLog?.Invoke($"写入 Assembly {assemblyId}: {data?.Length ?? 0} 字节"); } private static O2TData CloneOutput(O2TData src) { var dst = new O2TData { DOStatus = src.DOStatus, DOData = src.DOData }; Array.Copy(src.OutputEnable, dst.OutputEnable, 8); for (int i = 0; i < 8; i++) Array.Copy(src.PortProcessData[i], dst.PortProcessData[i], PROCESS_DATA_PER_PORT); return dst; } #endregion #region 资源清理 /// 仅释放 EIP 会话,保留自动重连能力 private void CleanupSessionOnly() { try { if (_isIOModeActive) StopIOCommunication(); if (_isConnected) { try { _client.UnRegisterSession(); } catch (Exception ex) { OnLog?.Invoke($"UnRegisterSession: {ex.Message}"); } _isConnected = false; OnLog?.Invoke("EIP 会话已释放"); } } catch (Exception ex) { OnLog?.Invoke($"EIP 会话释放异常: {ex.Message}"); } } private void CleanupResources() { CleanupSessionOnly(); } public void Dispose() { _disposed = true; _autoReconnectEnabled = false; StopReconnectLoop(); CleanupResources(); GC.SuppressFinalize(this); } #endregion } #region 枚举定义 public enum IOConnectionType { ExclusiveOwner, InputOnly, ListenOnly } public enum PortMode { Deactivated = 0, IOLManual = 1, IOLAutostart = 2, DigitalInput = 3, DigitalOutput = 4 } public enum IQBehavior { NotSupported = 0, DigitalInput = 1, DigitalOutput = 2 } public enum RfidPayloadKind { /// RFH5xx Read UID(PDI Byte4~11,8 字节) Uid, /// RFH5xx 用户存储区(PDI Byte4 起,块地址/块数在 PDO 配置) UserMemory } /// RFH5xx 读取参数(SICK 操作说明 8.3.1)。 public class Rfh5xxReadOptions { public RfidPayloadKind Kind { get; set; } = RfidPayloadKind.Uid; /// 用户区起始块号(PDO Byte3) public byte UserBlockAddress { get; set; } /// 读取块数量(PDO Byte1,1~31) public int UserBlockCount { get; set; } = 4; /// 每块字节数:ICODE 多为 4,部分 IC 为 8 public int UserBlockSizeBytes { get; set; } = 4; public Rfh5xxReadOptions Clone() { return new Rfh5xxReadOptions { Kind = Kind, UserBlockAddress = UserBlockAddress, UserBlockCount = UserBlockCount, UserBlockSizeBytes = UserBlockSizeBytes }; } } public enum RFIDCommand { StartRead, StopRead, Write, Reset } #endregion #region 数据结构 public class T2OData { public byte DIStatus { get; set; } public ushort DIData { get; set; } public byte[] PortStatus { get; set; } = new byte[8]; public byte[][] PortProcessData { get; set; } = new byte[8][]; public T2OData() { for (int i = 0; i < 8; i++) PortProcessData[i] = new byte[32]; } } public class O2TData { public byte DOStatus { get; set; } public ushort DOData { get; set; } public byte[] OutputEnable { get; set; } = new byte[8]; public byte[][] PortProcessData { get; set; } = new byte[8][]; public O2TData() { for (int i = 0; i < 8; i++) PortProcessData[i] = new byte[32]; } } public class SIG350Configuration { /// 设备实际返回的配置长度(80 或 130) public int RawLength { get; set; } public byte[] PortMode { get; set; } = new byte[8]; public byte[] ValidationBackup { get; set; } = new byte[8]; public byte[] IQBehavior { get; set; } = new byte[8]; public byte[] PortCycleTime { get; set; } = new byte[8]; public ushort[] VendorID { get; set; } = new ushort[8]; public uint[] DeviceID { get; set; } = new uint[8]; public byte DIOProcessDataLayout { get; set; } public byte DOSubstituteMode { get; set; } public byte[] DI_CQPinPolarity { get; set; } = new byte[8]; public byte[] DI_CQPinSignalFilter { get; set; } = new byte[8]; public byte[] DI_IQPinPolarity { get; set; } = new byte[8]; public byte[] DI_IQPinSignalFilter { get; set; } = new byte[8]; public byte[] DO_IQPinMode { get; set; } = new byte[8]; public byte[] DO_CQPinMode { get; set; } = new byte[8]; } public class RFIDTagData { public int Port { get; set; } public byte[] Data { get; set; } public DateTime Timestamp { get; set; } public byte Quality { get; set; } public RfidPayloadKind PayloadKind { get; set; } = RfidPayloadKind.Uid; public string ToHexString() { if (Data == null) return string.Empty; return BitConverter.ToString(Data).Replace("-", " "); } public string ToASCIIString() { if (Data == null) return string.Empty; return Encoding.ASCII.GetString(Data); } public string ToUTF8String() { if (Data == null) return string.Empty; return Encoding.UTF8.GetString(Data); } } public class RFIDTagDataEventArgs : EventArgs { public T2OData Data { get; } public List Tags { get; } = new List(); public RFIDTagDataEventArgs(T2OData data) : this(data, null) { } public RFIDTagDataEventArgs(T2OData data, IEnumerable tags) { Data = data; if (tags != null) { Tags.AddRange(tags); return; } if (data == null || data.PortStatus == null || data.PortProcessData == null) return; for (int port = 0; port < 8; port++) { byte status = data.PortStatus[port]; // 与 TryExtractTag 一致:必须 PQ 有效 if ((status & 0x80) == 0) continue; byte[] processData = data.PortProcessData[port]; byte[] tagBytes = SIG350RFIDClient.ExtractRFIDData(processData, new Rfh5xxReadOptions()); if (tagBytes == null || tagBytes.Length == 0) continue; Tags.Add(new RFIDTagData { Port = port + 1, Data = tagBytes, Timestamp = DateTime.Now, Quality = status }); } } } #endregion }