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
}