SIG350RFIDClient.cs 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425
  1. using Sres.Net.EEIP;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Data.Entity.Core.Metadata.Edm;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using TeamAAS_VP.Models;
  10. namespace TeamAAS_VP.Core.RFID
  11. {
  12. /// <summary>
  13. /// SIG350 EtherNet/IP 通讯类 - 专为 RFID 应用设计
  14. /// 支持 SICK IO-Link RFID 读写头 (RFU63x, RFU62x 等)
  15. /// </summary>
  16. public class SIG350RFIDClient : IDisposable
  17. {
  18. #region 私有字段
  19. private readonly EEIPClient _client;
  20. private readonly string _ipAddress;
  21. private bool _isConnected;
  22. private bool _isIOModeActive;
  23. private CancellationTokenSource _ioCancelToken;
  24. private Task _ioReceiveTask;
  25. private readonly object _ioSync = new object();
  26. private O2TData _lastOutput = new O2TData();
  27. private T2OData _lastInput = new T2OData();
  28. private readonly HashSet<int> _enabledPorts = new HashSet<int> { 1 };
  29. private Rfh5xxReadOptions _readOptions = new Rfh5xxReadOptions();
  30. private volatile bool _disposed;
  31. private volatile bool _autoReconnectEnabled = true;
  32. private int _lastRpiMs = 10;
  33. private readonly object _reconnectSync = new object();
  34. private CancellationTokenSource _reconnectCts;
  35. private Task _reconnectTask;
  36. /// <summary>初始连接重试次数</summary>
  37. public int ConnectRetryCount { get; set; } = 10;
  38. /// <summary>初始连接重试间隔(毫秒)</summary>
  39. public int ConnectRetryDelayMs { get; set; } = 2000;
  40. /// <summary>启动 I/O 前回调(Management 用于配置端口/读模式,含 EIP 重连场景)。</summary>
  41. public Action BeforeStartRfidReading { get; set; }
  42. /// <summary>断线后自动重连基础间隔(毫秒),随失败次数递增,最大 30 秒</summary>
  43. public int ReconnectIntervalMs { get; set; } = 3000;
  44. // Assembly ID(EDS / 手册)
  45. private const ushort ASSEMBLY_CONFIG = 102; // 配置:端口 80 字节,完整含 DIO 为 130
  46. private const ushort ASSEMBLY_O2T = 100; // 输出 276 字节
  47. private const ushort ASSEMBLY_T2O = 101; // 输入 276 字节
  48. private const int PROCESS_DATA_PER_PORT = 32;
  49. private const int IO_ASSEMBLY_SIZE = 4 + 8 * (2 + PROCESS_DATA_PER_PORT); // 276
  50. private const int CONFIG_PORT_SIZE = 80; // 8 × 10
  51. private const int CONFIG_FULL_SIZE = 130; // 80 + layout + substitute + 8×6 DIO
  52. // IOL Status Bit7 = 过程数据有效 (PQ)
  53. private const byte IOL_STATUS_PQ_VALID = 0x80;
  54. private const byte IOL_STATUS_DEV_COM = 0x20;
  55. // RFH5xx HF ISO15693 PDI/PDO(手册 8.3.1)
  56. private const byte RFH5xxPdoStart = 0x01;
  57. private const byte RFH5xxTagPresent = 0x04;
  58. private const byte RFH5xxFault = 0x02;
  59. private const int RFH5xxCmdAutoRead = 1;
  60. private const int RFH5xxCmdRead = 3;
  61. private const int RFH5xxCmdReadUid = 5;
  62. private const int RFH5xxUidLength = 8;
  63. private const int RFH5xxMaxUserPayload = 28;
  64. // RFU61x UHF(保留兼容)
  65. private const byte RFU_TAG_PRESENT = 0x04;
  66. private const int RFU_LENGTH_INDEX = 3;
  67. private const int RFU_DATA_OFFSET = 4;
  68. private const int RFU_MAX_EPC_LEN = 28;
  69. BgEthernetIP Config;
  70. #endregion
  71. #region 事件
  72. public event EventHandler<RFIDTagDataEventArgs> OnTagDataReceived;
  73. public event Action<bool> OnConnectReceive;
  74. public event EventHandler<Exception> OnError;
  75. public event Action<string> OnLog;
  76. #endregion
  77. #region 构造函数
  78. public SIG350RFIDClient(BgEthernetIP config)
  79. {
  80. Config = config;
  81. _ipAddress = config.IP;
  82. _client = new EEIPClient();
  83. OnConnectReceive += HandleConnectionChanged;
  84. }
  85. #endregion
  86. #region 连接管理
  87. public bool Connect()
  88. {
  89. StopReconnectLoop();
  90. for (int i = 0; i < ConnectRetryCount; i++)
  91. {
  92. if (InternalConnect())
  93. {
  94. OnConnectReceive?.Invoke(true);
  95. return true;
  96. }
  97. if (i < ConnectRetryCount - 1)
  98. Thread.Sleep(ConnectRetryDelayMs);
  99. }
  100. OnConnectReceive?.Invoke(false);
  101. return false;
  102. }
  103. private bool InternalConnect()
  104. {
  105. try
  106. {
  107. if (_isConnected)
  108. return true;
  109. _client.RegisterSession(_ipAddress);
  110. _isConnected = true;
  111. OnLog?.Invoke($"成功连接到 SIG350 ({_ipAddress})");
  112. return true;
  113. }
  114. catch (Exception ex)
  115. {
  116. OnLog?.Invoke($"连接失败: {ex.Message}");
  117. OnError?.Invoke(this, ex);
  118. CleanupSessionOnly();
  119. return false;
  120. }
  121. }
  122. private void HandleConnectionChanged(bool connected)
  123. {
  124. if (connected || _disposed || !_autoReconnectEnabled)
  125. return;
  126. ScheduleReconnect();
  127. }
  128. private void ScheduleReconnect()
  129. {
  130. lock (_reconnectSync)
  131. {
  132. if (_disposed || !_autoReconnectEnabled)
  133. return;
  134. if (_reconnectTask != null && !_reconnectTask.IsCompleted)
  135. return;
  136. StopReconnectLoop();
  137. _reconnectCts = new CancellationTokenSource();
  138. var token = _reconnectCts.Token;
  139. _reconnectTask = Task.Run(() => ReconnectLoop(token), token);
  140. }
  141. }
  142. private void ReconnectLoop(CancellationToken token)
  143. {
  144. OnLog?.Invoke("EIP 连接断开,开始自动重连...");
  145. int attempt = 0;
  146. while (!token.IsCancellationRequested && !_disposed && _autoReconnectEnabled)
  147. {
  148. attempt++;
  149. try
  150. {
  151. if (TryRestoreSession())
  152. {
  153. OnLog?.Invoke($"EIP 重连成功(第 {attempt} 次)");
  154. OnConnectReceive?.Invoke(true);
  155. return;
  156. }
  157. OnLog?.Invoke($"EIP 重连失败(第 {attempt} 次),稍后重试");
  158. }
  159. catch (Exception ex)
  160. {
  161. OnLog?.Invoke($"EIP 重连异常: {ex.Message}");
  162. }
  163. int delay = Math.Min(ReconnectIntervalMs * Math.Min(attempt, 10), 30000);
  164. if (token.WaitHandle.WaitOne(delay))
  165. break;
  166. }
  167. }
  168. private bool TryRestoreSession()
  169. {
  170. CleanupSessionOnly();
  171. if (!InternalConnect())
  172. return false;
  173. if (_lastRpiMs <= 0)
  174. return true;
  175. try
  176. {
  177. StartRFIDReading(_lastRpiMs);
  178. return true;
  179. }
  180. catch (Exception ex)
  181. {
  182. OnLog?.Invoke($"EIP 重连后恢复 I/O 失败: {ex.Message}");
  183. CleanupSessionOnly();
  184. return false;
  185. }
  186. }
  187. private void StopReconnectLoop()
  188. {
  189. try
  190. {
  191. if (_reconnectCts != null)
  192. {
  193. _reconnectCts.Cancel();
  194. try
  195. {
  196. if (_reconnectTask != null)
  197. _reconnectTask.Wait(3000);
  198. }
  199. catch (AggregateException) { /* ignore */ }
  200. _reconnectCts.Dispose();
  201. }
  202. }
  203. catch { /* ignore */ }
  204. finally
  205. {
  206. _reconnectCts = null;
  207. _reconnectTask = null;
  208. }
  209. }
  210. public void Disconnect()
  211. {
  212. _autoReconnectEnabled = false;
  213. StopReconnectLoop();
  214. CleanupResources();
  215. }
  216. public bool IsConnected => _isConnected;
  217. public bool IsIOModeActive => _isIOModeActive;
  218. /// <summary>
  219. /// 当前启用监听的端口(1-8)。空集合表示不上报任何标签。
  220. /// </summary>
  221. public IReadOnlyCollection<int> EnabledPorts
  222. {
  223. get
  224. {
  225. lock (_ioSync)
  226. return _enabledPorts.OrderBy(p => p).ToList();
  227. }
  228. }
  229. /// <summary>
  230. /// 设置要读取的读写头端口(1-8)。
  231. /// </summary>
  232. public void SetEnabledPorts(IEnumerable<int> ports)
  233. {
  234. if (ports == null)
  235. throw new ArgumentNullException(nameof(ports));
  236. var next = new HashSet<int>();
  237. foreach (int p in ports)
  238. {
  239. if (p < 1 || p > 8)
  240. throw new ArgumentException($"端口号无效: {p},必须在1-8之间");
  241. next.Add(p);
  242. }
  243. lock (_ioSync)
  244. {
  245. _enabledPorts.Clear();
  246. foreach (int p in next)
  247. _enabledPorts.Add(p);
  248. }
  249. OnLog?.Invoke(next.Count == 0
  250. ? "未启用任何 RFID 端口"
  251. : $"已启用 RFID 端口: {string.Join(",", next.OrderBy(x => x))}");
  252. }
  253. private bool IsPortEnabled(int port)
  254. {
  255. lock (_ioSync)
  256. return _enabledPorts.Contains(port);
  257. }
  258. /// <summary>
  259. /// 配置 RFH5xx 读取内容:UID 或用户存储区(手册 8.3.1)。
  260. /// </summary>
  261. public void ConfigureReadOptions(Rfh5xxReadOptions options)
  262. {
  263. if (options == null)
  264. throw new ArgumentNullException(nameof(options));
  265. lock (_ioSync)
  266. _readOptions = options.Clone();
  267. }
  268. public Rfh5xxReadOptions GetReadOptions()
  269. {
  270. lock (_ioSync)
  271. return _readOptions.Clone();
  272. }
  273. #endregion
  274. #region 配置管理
  275. /// <summary>
  276. /// 配置 RFID 端口为 IO-Link 模式
  277. /// </summary>
  278. public void ConfigureRFIDPort(int port, PortMode mode = PortMode.IOLAutostart)
  279. {
  280. if (port < 1 || port > 8)
  281. throw new ArgumentException("端口号必须在1-8之间");
  282. try
  283. {
  284. var config = ReadConfiguration();
  285. ApplyPortMode(config, port, mode);
  286. WriteConfiguration(config);
  287. OnLog?.Invoke($"端口 {port} 已配置为 {mode}");
  288. }
  289. catch (Exception ex)
  290. {
  291. OnLog?.Invoke($"配置端口 {port} 失败: {ex.Message}");
  292. throw;
  293. }
  294. }
  295. /// <summary>
  296. /// 批量配置多个端口为 IO-Link 模式,并更新启用端口集合。
  297. /// </summary>
  298. public void ConfigureRFIDPorts(IEnumerable<int> ports, PortMode mode = PortMode.IOLAutostart)
  299. {
  300. if (ports == null)
  301. throw new ArgumentNullException(nameof(ports));
  302. var list = ports.Distinct().OrderBy(p => p).ToList();
  303. if (list.Count == 0)
  304. throw new ArgumentException("至少选择一个端口");
  305. foreach (int p in list)
  306. {
  307. if (p < 1 || p > 8)
  308. throw new ArgumentException($"端口号无效: {p},必须在1-8之间");
  309. }
  310. try
  311. {
  312. var config = ReadConfiguration();
  313. foreach (int port in list)
  314. ApplyPortMode(config, port, mode);
  315. WriteConfiguration(config);
  316. SetEnabledPorts(list);
  317. OnLog?.Invoke($"端口 [{string.Join(",", list)}] 已配置为 {mode}");
  318. }
  319. catch (Exception ex)
  320. {
  321. OnLog?.Invoke($"批量配置端口失败: {ex.Message}");
  322. throw;
  323. }
  324. }
  325. private static void ApplyPortMode(SIG350Configuration config, int port, PortMode mode)
  326. {
  327. config.PortMode[port - 1] = (byte)mode;
  328. config.IQBehavior[port - 1] = (byte)IQBehavior.NotSupported;
  329. config.ValidationBackup[port - 1] = 0;
  330. config.PortCycleTime[port - 1] = 0;
  331. }
  332. private SIG350Configuration ReadConfiguration()
  333. {
  334. byte[] data = ReadAssembly(ASSEMBLY_CONFIG);
  335. if (data == null || data.Length < CONFIG_PORT_SIZE)
  336. throw new InvalidOperationException($"读取配置失败,长度={data?.Length ?? 0},至少需要 {CONFIG_PORT_SIZE}");
  337. return ParseConfiguration(data);
  338. }
  339. private void WriteConfiguration(SIG350Configuration config)
  340. {
  341. if (config == null)
  342. throw new ArgumentNullException(nameof(config));
  343. // 按设备返回长度写回:>=130 用完整块,否则只写 EDS 规定的 80 字节端口配置
  344. int size = config.RawLength >= CONFIG_FULL_SIZE ? CONFIG_FULL_SIZE : CONFIG_PORT_SIZE;
  345. byte[] data = SerializeConfiguration(config, size);
  346. WriteAssembly(ASSEMBLY_CONFIG, data);
  347. }
  348. #endregion
  349. #region RFID 数据操作
  350. public void StartRFIDReading(int rpiMilliseconds = 10)
  351. {
  352. if (!_isConnected)
  353. throw new InvalidOperationException("未连接到SIG350");
  354. _lastRpiMs = Math.Max(1, rpiMilliseconds);
  355. if (_isIOModeActive)
  356. return;
  357. BeforeStartRfidReading?.Invoke();
  358. List<int> ports;
  359. lock (_ioSync)
  360. ports = _enabledPorts.OrderBy(p => p).ToList();
  361. if (ports.Count == 0)
  362. ports.Add(1);
  363. try
  364. {
  365. StartIOCommunication(rpiMilliseconds);
  366. EnablePortsOutput(ports);
  367. ApplyRfh5xxCommandForPorts(ports, oneShot: false);
  368. string modeText = _readOptions.Kind == RfidPayloadKind.Uid ? "Read UID" : "Auto-Read 用户区";
  369. OnLog?.Invoke($"RFID数据读取已启动 (端口: {string.Join(",", ports)}, RPI: {rpiMilliseconds}ms, 模式: {modeText})");
  370. }
  371. catch (Exception ex)
  372. {
  373. // ForwardOpen 已成功但后续失败时必须关掉,避免半开连接
  374. try { StopIOCommunication(); } catch { /* ignore */ }
  375. OnLog?.Invoke($"启动RFID读取失败: {ex.Message}");
  376. throw;
  377. }
  378. }
  379. public void StopRFIDReading()
  380. {
  381. try
  382. {
  383. if (_isIOModeActive)
  384. {
  385. try { DisableAllPortOutputs(); }
  386. catch { /* ignore */ }
  387. }
  388. }
  389. finally
  390. {
  391. StopIOCommunication();
  392. OnLog?.Invoke("RFID数据读取已停止");
  393. }
  394. }
  395. /// <summary>
  396. /// Autostart 模式下只需使能端口输出;不要向过程数据写入臆造命令字节,
  397. /// 以免覆盖 IO-Link 设备输出映像。
  398. /// </summary>
  399. private void EnablePortsOutput(IList<int> ports)
  400. {
  401. lock (_ioSync)
  402. {
  403. var output = CloneOutput(_lastOutput);
  404. for (int i = 0; i < 8; i++)
  405. {
  406. output.OutputEnable[i] = 0;
  407. Array.Clear(output.PortProcessData[i], 0, PROCESS_DATA_PER_PORT);
  408. }
  409. foreach (int port in ports)
  410. output.OutputEnable[port - 1] = 1;
  411. WriteOtIoData(SerializeO2TData(output));
  412. _lastOutput = output;
  413. }
  414. }
  415. private void DisableAllPortOutputs()
  416. {
  417. lock (_ioSync)
  418. {
  419. var output = CloneOutput(_lastOutput);
  420. for (int i = 0; i < 8; i++)
  421. {
  422. output.OutputEnable[i] = 0;
  423. Array.Clear(output.PortProcessData[i], 0, PROCESS_DATA_PER_PORT);
  424. }
  425. WriteOtIoData(SerializeO2TData(output));
  426. _lastOutput = output;
  427. }
  428. }
  429. /// <summary>
  430. /// 使能端口并向 RFH5xx 下发 PDO 命令(Read UID / Auto-Read)。
  431. /// </summary>
  432. private void ApplyRfh5xxCommandForPorts(IList<int> ports, bool oneShot)
  433. {
  434. if (ports == null || ports.Count == 0)
  435. return;
  436. lock (_ioSync)
  437. {
  438. var output = CloneOutput(_lastOutput);
  439. foreach (int port in ports)
  440. {
  441. output.OutputEnable[port - 1] = 1;
  442. WriteRfh5xxPdoToPort(output, port, _readOptions, start: true);
  443. }
  444. SendOutputData(output);
  445. _lastOutput = output;
  446. }
  447. }
  448. private void ApplyRfh5xxCommandForPort(int port, bool oneShot, bool start)
  449. {
  450. lock (_ioSync)
  451. {
  452. var output = CloneOutput(_lastOutput);
  453. output.OutputEnable[port - 1] = 1;
  454. WriteRfh5xxPdoToPort(output, port, _readOptions, start);
  455. SendOutputData(output);
  456. _lastOutput = output;
  457. }
  458. }
  459. /// <summary>RFH5xx PDO:Byte0 bit0=START, bit5~7=CMD;用户区 Byte1=块数, Byte3=起始块。</summary>
  460. /// <summary>
  461. /// RFH5xx 正确 PDO 命令格式:Byte0 = 命令码
  462. /// Read UID = 5,Auto-Read 用户区 = 1,单次读用户区 = 3
  463. /// </summary>
  464. private static void WriteRfh5xxPdoToPort(O2TData output, int port, Rfh5xxReadOptions options, bool start)
  465. {
  466. var pdo = new byte[32];
  467. if (!start)
  468. {
  469. // 停止:首字节写 0
  470. Array.Copy(pdo, 0, output.PortProcessData[port - 1], 0, 32);
  471. return;
  472. }
  473. // 命令码直接写入 Byte0
  474. int cmd = options.Kind == RfidPayloadKind.Uid
  475. ? RFH5xxCmdReadUid // 5
  476. : RFH5xxCmdAutoRead; // 1
  477. pdo[0] = (byte)cmd;
  478. // 用户区配置:Byte1=块数,Byte3=起始块地址
  479. if (options.Kind == RfidPayloadKind.UserMemory)
  480. {
  481. pdo[1] = (byte)(options.UserBlockCount & 0x1F);
  482. pdo[3] = options.UserBlockAddress;
  483. }
  484. Array.Copy(pdo, 0, output.PortProcessData[port - 1], 0, 32);
  485. }
  486. public RFIDTagData ReadRFIDTag(int port, int timeoutMs = 1000, Rfh5xxReadOptions options = null)
  487. {
  488. if (port < 1 || port > 8)
  489. throw new ArgumentException("端口号必须在1-8之间");
  490. Rfh5xxReadOptions opts = options ?? GetReadOptions();
  491. try
  492. {
  493. if (!_isIOModeActive)
  494. throw new InvalidOperationException("I/O通讯未启动,请先启动监听");
  495. // Read/ReadUID:翻转 START 触发一次(手册 8.3.1.2)
  496. ApplyRfh5xxCommandForPort(port, oneShot: opts.Kind == RfidPayloadKind.UserMemory, start: false);
  497. Thread.Sleep(50);
  498. ApplyRfh5xxCommandForPort(port, oneShot: opts.Kind == RfidPayloadKind.UserMemory, start: true);
  499. DateTime startTime = DateTime.Now;
  500. while ((DateTime.Now - startTime).TotalMilliseconds < timeoutMs)
  501. {
  502. var input = ReadInputData();
  503. var tag = TryExtractTag(input, port, opts);
  504. if (tag != null)
  505. return tag;
  506. Thread.Sleep(10);
  507. }
  508. return null;
  509. }
  510. catch (Exception ex)
  511. {
  512. OnLog?.Invoke($"读取RFID标签失败: {ex.Message}");
  513. throw;
  514. }
  515. finally
  516. {
  517. }
  518. }
  519. [Obsolete("RFH5xx 请使用 ReadRFIDTag;此方法保留兼容。")]
  520. public void SendRFIDCommand(int port, RFIDCommand command, byte[] data = null)
  521. {
  522. if (port < 1 || port > 8)
  523. throw new ArgumentException("端口号必须在1-8之间");
  524. try
  525. {
  526. lock (_ioSync)
  527. {
  528. var output = CloneOutput(_lastOutput);
  529. output.OutputEnable[port - 1] = 1;
  530. Array.Clear(output.PortProcessData[port - 1], 0, PROCESS_DATA_PER_PORT);
  531. if (data != null && data.Length > 0)
  532. {
  533. int copyLen = Math.Min(PROCESS_DATA_PER_PORT, data.Length);
  534. Array.Copy(data, 0, output.PortProcessData[port - 1], 0, copyLen);
  535. }
  536. // 过程数据第 0 字节作为简易触发(具体含义取决于读写头)
  537. if (command == RFIDCommand.StartRead)
  538. output.PortProcessData[port - 1][0] = 0x85;
  539. else if (command == RFIDCommand.StopRead)
  540. output.PortProcessData[port - 1][0] = 0x00;
  541. SendOutputData(output);
  542. _lastOutput = output;
  543. }
  544. OnLog?.Invoke($"RFID命令已发送到端口 {port}: {command}");
  545. }
  546. catch (Exception ex)
  547. {
  548. OnLog?.Invoke($"发送RFID命令失败: {ex.Message}");
  549. throw;
  550. }
  551. }
  552. public RFIDTagData ReadRFIDTag(int port, int timeoutMs = 1000)
  553. {
  554. return ReadRFIDTag(port, timeoutMs, null);
  555. }
  556. private RFIDTagData TryExtractTag(T2OData input, int port, Rfh5xxReadOptions options = null)
  557. {
  558. if (input == null || port < 1 || port > 8)
  559. return null;
  560. if (input.PortStatus == null || input.PortProcessData == null)
  561. return null;
  562. byte[] processData = input.PortProcessData[port - 1];
  563. if (processData == null)
  564. return null;
  565. byte status = input.PortStatus[port - 1];
  566. if ((status & IOL_STATUS_PQ_VALID) == 0)
  567. return null;
  568. byte[] tagData = ExtractRFIDData(processData, options ?? _readOptions);
  569. if (tagData == null || tagData.Length == 0)
  570. return null;
  571. return new RFIDTagData
  572. {
  573. Port = port,
  574. Data = tagData,
  575. Timestamp = DateTime.Now,
  576. Quality = status,
  577. PayloadKind = (options ?? _readOptions).Kind
  578. };
  579. }
  580. /// <summary>
  581. /// 从 IO-Link 32 字节 PDI 提取有效载荷(默认 RFH5xx,失败时尝试 RFU61x)。
  582. /// </summary>
  583. internal static byte[] ExtractRFIDData(byte[] processData, Rfh5xxReadOptions options = null)
  584. {
  585. if (processData == null || processData.Length == 0)
  586. return null;
  587. if (IsAllZero(processData))
  588. return null;
  589. if (options == null)
  590. options = new Rfh5xxReadOptions();
  591. byte[] rfh = ExtractRfh5xxPayload(processData, options);
  592. if (rfh != null && rfh.Length > 0)
  593. return rfh;
  594. return ExtractRfu61xPayload(processData);
  595. }
  596. /// <summary>RFH5xx:Tag=Byte0 bit2;UID=Byte4~11;用户区=Byte4 起。</summary>
  597. internal static byte[] ExtractRfh5xxPayload(byte[] pdi, Rfh5xxReadOptions options)
  598. {
  599. if (pdi == null || pdi.Length < 12 || options == null)
  600. return null;
  601. byte b0 = pdi[0];
  602. if ((b0 & RFH5xxTagPresent) == 0)
  603. return null;
  604. if ((b0 & RFH5xxFault) != 0)
  605. return null;
  606. if (options.Kind == RfidPayloadKind.Uid)
  607. {
  608. if (pdi.Length < 4 + RFH5xxUidLength)
  609. return null;
  610. byte[] uid = new byte[RFH5xxUidLength];
  611. Array.Copy(pdi, 4, uid, 0, RFH5xxUidLength);
  612. return IsAllZero(uid) ? null : uid;
  613. }
  614. int byteCount = options.UserBlockCount * options.UserBlockSizeBytes;
  615. if (byteCount <= 0)
  616. return null;
  617. byteCount = Math.Min(RFH5xxMaxUserPayload, Math.Min(byteCount, pdi.Length - 4));
  618. byte[] user = new byte[byteCount];
  619. Array.Copy(pdi, 4, user, 0, byteCount);
  620. return IsAllZero(user) ? null : user;
  621. }
  622. private static byte[] ExtractRfu61xPayload(byte[] processData)
  623. {
  624. if (processData.Length >= RFU_DATA_OFFSET + 1)
  625. {
  626. bool tagPresent = (processData[0] & RFU_TAG_PRESENT) != 0;
  627. int epcLen = processData[RFU_LENGTH_INDEX];
  628. if (tagPresent)
  629. {
  630. if (epcLen <= 0 || epcLen > RFU_MAX_EPC_LEN || epcLen > processData.Length - RFU_DATA_OFFSET)
  631. return null;
  632. if (IsAllZero(processData, RFU_DATA_OFFSET, epcLen))
  633. return null;
  634. byte[] epc = new byte[epcLen];
  635. Array.Copy(processData, RFU_DATA_OFFSET, epc, 0, epcLen);
  636. return epc;
  637. }
  638. // TagPresent=0 且 Length=0:RFU 空闲帧(无芯片),忽略状态/RSSI 噪声
  639. if (epcLen == 0)
  640. return null;
  641. }
  642. // 2) 其它设备:仅接受「首字节=长度 + 后面全是填充0」且长度合理(>=4)
  643. int dataLength = processData[0];
  644. if (dataLength >= 4 && dataLength < processData.Length)
  645. {
  646. int payloadEnd = 1 + dataLength;
  647. if (payloadEnd <= processData.Length
  648. && IsAllZero(processData, payloadEnd, processData.Length - payloadEnd)
  649. && !IsAllZero(processData, 1, dataLength))
  650. {
  651. byte[] result = new byte[dataLength];
  652. Array.Copy(processData, 1, result, 0, dataLength);
  653. return result;
  654. }
  655. }
  656. // 不再把“去尾零后的任意非零过程数据”当作标签
  657. return null;
  658. }
  659. private static bool IsAllZero(byte[] data)
  660. {
  661. return IsAllZero(data, 0, data.Length);
  662. }
  663. private static bool IsAllZero(byte[] data, int offset, int count)
  664. {
  665. if (data == null || count <= 0)
  666. return true;
  667. int end = Math.Min(data.Length, offset + count);
  668. for (int i = offset; i < end; i++)
  669. {
  670. if (data[i] != 0)
  671. return false;
  672. }
  673. return true;
  674. }
  675. #endregion
  676. #region I/O通讯 (隐式消息)
  677. private void StartIOCommunication(int rpiMilliseconds = 10)
  678. {
  679. if (!_isConnected)
  680. throw new InvalidOperationException("未连接到SIG350");
  681. if (_isIOModeActive)
  682. return;
  683. try
  684. {
  685. // RPI:界面单位是 ms,协议单位是 μs。至少 1ms,避免 Max(1000, ms) 把 10ms 误变成 1 秒。
  686. int rpiMs = Math.Max(1, rpiMilliseconds);
  687. uint rpiMicroseconds = (uint)rpiMs * 1000u;
  688. _client.O_T_InstanceID = (byte)ASSEMBLY_O2T;
  689. _client.T_O_InstanceID = (byte)ASSEMBLY_T2O;
  690. _client.O_T_Length = (ushort)IO_ASSEMBLY_SIZE;
  691. _client.T_O_Length = (ushort)IO_ASSEMBLY_SIZE;
  692. _client.O_T_RealTimeFormat = RealTimeFormat.Header32Bit;
  693. _client.T_O_RealTimeFormat = RealTimeFormat.Modeless;
  694. _client.O_T_OwnerRedundant = false;
  695. _client.T_O_OwnerRedundant = false;
  696. _client.O_T_Priority = Priority.Scheduled;
  697. _client.T_O_Priority = Priority.Scheduled;
  698. _client.O_T_VariableLength = false;
  699. _client.T_O_VariableLength = false;
  700. _client.O_T_ConnectionType = ConnectionType.Point_to_Point;
  701. _client.T_O_ConnectionType = ConnectionType.Point_to_Point;
  702. _client.RequestedPacketRate_O_T = rpiMicroseconds;
  703. _client.RequestedPacketRate_T_O = rpiMicroseconds;
  704. // 使用空配置实例,端口配置已通过显式 Assembly 102 写入
  705. _client.ConfigurationAssemblyInstanceID = 1;
  706. _client.ForwardOpen();
  707. // 初始化输出缓冲区(必须通过 setter 写回,getter 返回的是 Clone)
  708. lock (_ioSync)
  709. {
  710. _lastOutput = new O2TData();
  711. WriteOtIoData(SerializeO2TData(_lastOutput));
  712. }
  713. _isIOModeActive = true;
  714. _ioCancelToken = new CancellationTokenSource();
  715. _ioReceiveTask = Task.Run(() => IOReceiveLoop(_ioCancelToken.Token));
  716. OnLog?.Invoke($"I/O通讯已启动 (O2T/T2O={IO_ASSEMBLY_SIZE}字节, RPI={rpiMs}ms)");
  717. }
  718. catch (Exception ex)
  719. {
  720. _isIOModeActive = false;
  721. OnLog?.Invoke($"启动I/O通讯失败: {ex.Message}");
  722. throw;
  723. }
  724. }
  725. private void StopIOCommunication()
  726. {
  727. if (!_isIOModeActive)
  728. return;
  729. try
  730. {
  731. _ioCancelToken?.Cancel();
  732. try { _ioReceiveTask?.Wait(3000); }
  733. catch (AggregateException) { /* ignore cancel */ }
  734. try { _client.ForwardClose(); }
  735. catch (Exception ex) { OnLog?.Invoke($"ForwardClose: {ex.Message}"); }
  736. }
  737. catch (Exception ex)
  738. {
  739. OnLog?.Invoke($"停止I/O通讯异常: {ex.Message}");
  740. }
  741. finally
  742. {
  743. _isIOModeActive = false;
  744. _ioCancelToken?.Dispose();
  745. _ioCancelToken = null;
  746. _ioReceiveTask = null;
  747. }
  748. }
  749. private void IOReceiveLoop(CancellationToken token)
  750. {
  751. // 按端口记录上次上报指纹,标签离开后清掉,相同标签再次进入仍可上报
  752. var lastByPort = new string[8];
  753. while (!token.IsCancellationRequested && _isIOModeActive)
  754. {
  755. try
  756. {
  757. var input = TryReadInputData();
  758. if (input == null)
  759. {
  760. Thread.Sleep(10);
  761. continue;
  762. }
  763. try
  764. {
  765. // 显式读取一次输入Assembly,验证设备响应
  766. byte[] test = _client.AssemblyObject.getInstance(ASSEMBLY_T2O);
  767. if (test == null || test.Length == 0)
  768. throw new Exception("Assembly读取为空");
  769. }
  770. catch
  771. {
  772. OnLog?.Invoke("心跳检测失败,IO连接已断开");
  773. StopIOCommunication();
  774. CleanupSessionOnly();
  775. OnConnectReceive?.Invoke(false);
  776. break;
  777. }
  778. var changedTags = new List<RFIDTagData>();
  779. for (int port = 1; port <= 8; port++)
  780. {
  781. if (!IsPortEnabled(port))
  782. {
  783. lastByPort[port - 1] = null;
  784. continue;
  785. }
  786. var tag = TryExtractTag(input, port, _readOptions);
  787. if (tag == null)
  788. {
  789. lastByPort[port - 1] = null;
  790. continue;
  791. }
  792. string fingerprint = tag.ToHexString();
  793. if (fingerprint == lastByPort[port - 1])
  794. continue;
  795. lastByPort[port - 1] = fingerprint;
  796. changedTags.Add(tag);
  797. }
  798. if (changedTags.Count > 0)
  799. {
  800. var args = new RFIDTagDataEventArgs(input, changedTags);
  801. OnTagDataReceived?.Invoke(this, args);
  802. }
  803. Thread.Sleep(10);
  804. }
  805. catch (Exception ex)
  806. {
  807. if (token.IsCancellationRequested)
  808. break;
  809. OnLog?.Invoke($"接收I/O数据异常: {ex.Message}");
  810. OnError?.Invoke(this, ex);
  811. Thread.Sleep(100);
  812. }
  813. }
  814. }
  815. private void SendOutputData(O2TData outputData)
  816. {
  817. if (!_isIOModeActive)
  818. throw new InvalidOperationException("I/O通讯未启动");
  819. // 调用方若已持有 _ioSync,Monitor 可重入;此处统一走 setter 写回
  820. byte[] data = SerializeO2TData(outputData);
  821. lock (_ioSync)
  822. {
  823. WriteOtIoData(data);
  824. }
  825. }
  826. /// <summary>
  827. /// EEIP.NetStandard 的 O_T_IOData getter 返回 Clone,
  828. /// 直接改下标或 Array.Copy 到 getter 结果都不会写回内部缓冲区,必须走 setter。
  829. /// </summary>
  830. private void WriteOtIoData(byte[] data)
  831. {
  832. if (data == null)
  833. throw new ArgumentNullException(nameof(data));
  834. _client.O_T_IOData = data;
  835. }
  836. /// <summary>
  837. /// 尝试读取输入;缓冲区尚未就绪时返回 null,避免接收循环狂抛异常。
  838. /// </summary>
  839. private T2OData TryReadInputData()
  840. {
  841. if (!_isIOModeActive)
  842. return null;
  843. byte[] data = _client.T_O_IOData;
  844. if (data == null || data.Length < IO_ASSEMBLY_SIZE)
  845. return null;
  846. var parsed = ParseT2OData(data);
  847. lock (_ioSync)
  848. _lastInput = parsed;
  849. return parsed;
  850. }
  851. private T2OData ReadInputData()
  852. {
  853. if (!_isIOModeActive)
  854. throw new InvalidOperationException("I/O通讯未启动");
  855. var parsed = TryReadInputData();
  856. if (parsed == null)
  857. throw new InvalidOperationException("T2O 数据尚未就绪或长度不足");
  858. return parsed;
  859. }
  860. #endregion
  861. #region 数据序列化/反序列化
  862. private static T2OData ParseT2OData(byte[] data)
  863. {
  864. if (data == null || data.Length < IO_ASSEMBLY_SIZE)
  865. throw new ArgumentException($"输入数据无效,需要 {IO_ASSEMBLY_SIZE} 字节,实际 {data?.Length ?? 0}");
  866. var result = new T2OData();
  867. int offset = 0;
  868. result.DIStatus = data[offset++];
  869. offset++; // Reserved
  870. result.DIData = BitConverter.ToUInt16(data, offset);
  871. offset += 2;
  872. for (int port = 0; port < 8; port++)
  873. {
  874. result.PortStatus[port] = data[offset++];
  875. offset++; // Reserved
  876. Array.Copy(data, offset, result.PortProcessData[port], 0, PROCESS_DATA_PER_PORT);
  877. offset += PROCESS_DATA_PER_PORT;
  878. }
  879. return result;
  880. }
  881. private static byte[] SerializeO2TData(O2TData data)
  882. {
  883. if (data == null)
  884. throw new ArgumentNullException(nameof(data));
  885. // 手册: 4 + 8×(OutputEnable + Reserved + 32 PD) = 276
  886. byte[] result = new byte[IO_ASSEMBLY_SIZE];
  887. int offset = 0;
  888. result[offset++] = data.DOStatus;
  889. result[offset++] = 0; // Reserved
  890. byte[] doBytes = BitConverter.GetBytes(data.DOData);
  891. result[offset++] = doBytes[0];
  892. result[offset++] = doBytes[1];
  893. for (int port = 0; port < 8; port++)
  894. {
  895. result[offset++] = data.OutputEnable != null && port < data.OutputEnable.Length
  896. ? data.OutputEnable[port]
  897. : (byte)0;
  898. result[offset++] = 0; // Reserved
  899. if (data.PortProcessData != null
  900. && port < data.PortProcessData.Length
  901. && data.PortProcessData[port] != null)
  902. {
  903. int copyLen = Math.Min(PROCESS_DATA_PER_PORT, data.PortProcessData[port].Length);
  904. Array.Copy(data.PortProcessData[port], 0, result, offset, copyLen);
  905. }
  906. offset += PROCESS_DATA_PER_PORT;
  907. }
  908. return result;
  909. }
  910. private static SIG350Configuration ParseConfiguration(byte[] data)
  911. {
  912. if (data == null || data.Length < CONFIG_PORT_SIZE)
  913. throw new ArgumentException($"配置数据无效,至少需要 {CONFIG_PORT_SIZE} 字节");
  914. var config = new SIG350Configuration { RawLength = data.Length };
  915. int offset = 0;
  916. for (int port = 0; port < 8; port++)
  917. {
  918. config.PortMode[port] = data[offset++];
  919. config.ValidationBackup[port] = data[offset++];
  920. config.IQBehavior[port] = data[offset++];
  921. config.PortCycleTime[port] = data[offset++];
  922. config.VendorID[port] = BitConverter.ToUInt16(data, offset);
  923. offset += 2;
  924. // Device ID:手册为 UDINT,有效 24 bit
  925. config.DeviceID[port] = BitConverter.ToUInt32(data, offset) & 0x00FFFFFF;
  926. offset += 4;
  927. }
  928. // 完整配置 130 字节:layout + substitute + 8×6 DIO(中间无 Reserved)
  929. if (data.Length >= CONFIG_FULL_SIZE)
  930. {
  931. config.DIOProcessDataLayout = data[offset++];
  932. config.DOSubstituteMode = data[offset++];
  933. for (int port = 0; port < 8; port++)
  934. {
  935. config.DI_IQPinPolarity[port] = data[offset++];
  936. config.DI_CQPinPolarity[port] = data[offset++];
  937. config.DI_IQPinSignalFilter[port] = data[offset++];
  938. config.DI_CQPinSignalFilter[port] = data[offset++];
  939. config.DO_IQPinMode[port] = data[offset++];
  940. config.DO_CQPinMode[port] = data[offset++];
  941. }
  942. }
  943. return config;
  944. }
  945. private static byte[] SerializeConfiguration(SIG350Configuration config, int size)
  946. {
  947. if (size != CONFIG_PORT_SIZE && size != CONFIG_FULL_SIZE)
  948. size = size >= CONFIG_FULL_SIZE ? CONFIG_FULL_SIZE : CONFIG_PORT_SIZE;
  949. byte[] data = new byte[size];
  950. int offset = 0;
  951. for (int port = 0; port < 8; port++)
  952. {
  953. data[offset++] = config.PortMode[port];
  954. data[offset++] = config.ValidationBackup[port];
  955. data[offset++] = config.IQBehavior[port];
  956. data[offset++] = config.PortCycleTime[port];
  957. BitConverter.GetBytes(config.VendorID[port]).CopyTo(data, offset);
  958. offset += 2;
  959. BitConverter.GetBytes(config.DeviceID[port]).CopyTo(data, offset);
  960. offset += 4;
  961. }
  962. if (size >= CONFIG_FULL_SIZE)
  963. {
  964. data[offset++] = config.DIOProcessDataLayout;
  965. data[offset++] = config.DOSubstituteMode;
  966. for (int port = 0; port < 8; port++)
  967. {
  968. data[offset++] = config.DI_IQPinPolarity[port];
  969. data[offset++] = config.DI_CQPinPolarity[port];
  970. data[offset++] = config.DI_IQPinSignalFilter[port];
  971. data[offset++] = config.DI_CQPinSignalFilter[port];
  972. data[offset++] = config.DO_IQPinMode[port];
  973. data[offset++] = config.DO_CQPinMode[port];
  974. }
  975. }
  976. return data;
  977. }
  978. private byte[] ReadAssembly(ushort assemblyId)
  979. {
  980. byte[] data = _client.AssemblyObject.getInstance(assemblyId);
  981. OnLog?.Invoke($"读取 Assembly {assemblyId}: {data?.Length ?? 0} 字节");
  982. return data;
  983. }
  984. private void WriteAssembly(ushort assemblyId, byte[] data)
  985. {
  986. _client.AssemblyObject.setInstance(assemblyId, data);
  987. OnLog?.Invoke($"写入 Assembly {assemblyId}: {data?.Length ?? 0} 字节");
  988. }
  989. private static O2TData CloneOutput(O2TData src)
  990. {
  991. var dst = new O2TData
  992. {
  993. DOStatus = src.DOStatus,
  994. DOData = src.DOData
  995. };
  996. Array.Copy(src.OutputEnable, dst.OutputEnable, 8);
  997. for (int i = 0; i < 8; i++)
  998. Array.Copy(src.PortProcessData[i], dst.PortProcessData[i], PROCESS_DATA_PER_PORT);
  999. return dst;
  1000. }
  1001. #endregion
  1002. #region 资源清理
  1003. /// <summary>仅释放 EIP 会话,保留自动重连能力</summary>
  1004. private void CleanupSessionOnly()
  1005. {
  1006. try
  1007. {
  1008. if (_isIOModeActive)
  1009. StopIOCommunication();
  1010. if (_isConnected)
  1011. {
  1012. try { _client.UnRegisterSession(); }
  1013. catch (Exception ex) { OnLog?.Invoke($"UnRegisterSession: {ex.Message}"); }
  1014. _isConnected = false;
  1015. OnLog?.Invoke("EIP 会话已释放");
  1016. }
  1017. }
  1018. catch (Exception ex)
  1019. {
  1020. OnLog?.Invoke($"EIP 会话释放异常: {ex.Message}");
  1021. }
  1022. }
  1023. private void CleanupResources()
  1024. {
  1025. CleanupSessionOnly();
  1026. }
  1027. public void Dispose()
  1028. {
  1029. _disposed = true;
  1030. _autoReconnectEnabled = false;
  1031. StopReconnectLoop();
  1032. CleanupResources();
  1033. GC.SuppressFinalize(this);
  1034. }
  1035. #endregion
  1036. }
  1037. #region 枚举定义
  1038. public enum IOConnectionType
  1039. {
  1040. ExclusiveOwner,
  1041. InputOnly,
  1042. ListenOnly
  1043. }
  1044. public enum PortMode
  1045. {
  1046. Deactivated = 0,
  1047. IOLManual = 1,
  1048. IOLAutostart = 2,
  1049. DigitalInput = 3,
  1050. DigitalOutput = 4
  1051. }
  1052. public enum IQBehavior
  1053. {
  1054. NotSupported = 0,
  1055. DigitalInput = 1,
  1056. DigitalOutput = 2
  1057. }
  1058. public enum RfidPayloadKind
  1059. {
  1060. /// <summary>RFH5xx Read UID(PDI Byte4~11,8 字节)</summary>
  1061. Uid,
  1062. /// <summary>RFH5xx 用户存储区(PDI Byte4 起,块地址/块数在 PDO 配置)</summary>
  1063. UserMemory
  1064. }
  1065. /// <summary>RFH5xx 读取参数(SICK 操作说明 8.3.1)。</summary>
  1066. public class Rfh5xxReadOptions
  1067. {
  1068. public RfidPayloadKind Kind { get; set; } = RfidPayloadKind.Uid;
  1069. /// <summary>用户区起始块号(PDO Byte3)</summary>
  1070. public byte UserBlockAddress { get; set; }
  1071. /// <summary>读取块数量(PDO Byte1,1~31)</summary>
  1072. public int UserBlockCount { get; set; } = 4;
  1073. /// <summary>每块字节数:ICODE 多为 4,部分 IC 为 8</summary>
  1074. public int UserBlockSizeBytes { get; set; } = 4;
  1075. public Rfh5xxReadOptions Clone()
  1076. {
  1077. return new Rfh5xxReadOptions
  1078. {
  1079. Kind = Kind,
  1080. UserBlockAddress = UserBlockAddress,
  1081. UserBlockCount = UserBlockCount,
  1082. UserBlockSizeBytes = UserBlockSizeBytes
  1083. };
  1084. }
  1085. }
  1086. public enum RFIDCommand
  1087. {
  1088. StartRead,
  1089. StopRead,
  1090. Write,
  1091. Reset
  1092. }
  1093. #endregion
  1094. #region 数据结构
  1095. public class T2OData
  1096. {
  1097. public byte DIStatus { get; set; }
  1098. public ushort DIData { get; set; }
  1099. public byte[] PortStatus { get; set; } = new byte[8];
  1100. public byte[][] PortProcessData { get; set; } = new byte[8][];
  1101. public T2OData()
  1102. {
  1103. for (int i = 0; i < 8; i++)
  1104. PortProcessData[i] = new byte[32];
  1105. }
  1106. }
  1107. public class O2TData
  1108. {
  1109. public byte DOStatus { get; set; }
  1110. public ushort DOData { get; set; }
  1111. public byte[] OutputEnable { get; set; } = new byte[8];
  1112. public byte[][] PortProcessData { get; set; } = new byte[8][];
  1113. public O2TData()
  1114. {
  1115. for (int i = 0; i < 8; i++)
  1116. PortProcessData[i] = new byte[32];
  1117. }
  1118. }
  1119. public class SIG350Configuration
  1120. {
  1121. /// <summary>设备实际返回的配置长度(80 或 130)</summary>
  1122. public int RawLength { get; set; }
  1123. public byte[] PortMode { get; set; } = new byte[8];
  1124. public byte[] ValidationBackup { get; set; } = new byte[8];
  1125. public byte[] IQBehavior { get; set; } = new byte[8];
  1126. public byte[] PortCycleTime { get; set; } = new byte[8];
  1127. public ushort[] VendorID { get; set; } = new ushort[8];
  1128. public uint[] DeviceID { get; set; } = new uint[8];
  1129. public byte DIOProcessDataLayout { get; set; }
  1130. public byte DOSubstituteMode { get; set; }
  1131. public byte[] DI_CQPinPolarity { get; set; } = new byte[8];
  1132. public byte[] DI_CQPinSignalFilter { get; set; } = new byte[8];
  1133. public byte[] DI_IQPinPolarity { get; set; } = new byte[8];
  1134. public byte[] DI_IQPinSignalFilter { get; set; } = new byte[8];
  1135. public byte[] DO_IQPinMode { get; set; } = new byte[8];
  1136. public byte[] DO_CQPinMode { get; set; } = new byte[8];
  1137. }
  1138. public class RFIDTagData
  1139. {
  1140. public int Port { get; set; }
  1141. public byte[] Data { get; set; }
  1142. public DateTime Timestamp { get; set; }
  1143. public byte Quality { get; set; }
  1144. public RfidPayloadKind PayloadKind { get; set; } = RfidPayloadKind.Uid;
  1145. public string ToHexString()
  1146. {
  1147. if (Data == null) return string.Empty;
  1148. return BitConverter.ToString(Data).Replace("-", " ");
  1149. }
  1150. public string ToASCIIString()
  1151. {
  1152. if (Data == null) return string.Empty;
  1153. return Encoding.ASCII.GetString(Data);
  1154. }
  1155. public string ToUTF8String()
  1156. {
  1157. if (Data == null) return string.Empty;
  1158. return Encoding.UTF8.GetString(Data);
  1159. }
  1160. }
  1161. public class RFIDTagDataEventArgs : EventArgs
  1162. {
  1163. public T2OData Data { get; }
  1164. public List<RFIDTagData> Tags { get; } = new List<RFIDTagData>();
  1165. public RFIDTagDataEventArgs(T2OData data)
  1166. : this(data, null)
  1167. {
  1168. }
  1169. public RFIDTagDataEventArgs(T2OData data, IEnumerable<RFIDTagData> tags)
  1170. {
  1171. Data = data;
  1172. if (tags != null)
  1173. {
  1174. Tags.AddRange(tags);
  1175. return;
  1176. }
  1177. if (data == null || data.PortStatus == null || data.PortProcessData == null)
  1178. return;
  1179. for (int port = 0; port < 8; port++)
  1180. {
  1181. byte status = data.PortStatus[port];
  1182. // 与 TryExtractTag 一致:必须 PQ 有效
  1183. if ((status & 0x80) == 0)
  1184. continue;
  1185. byte[] processData = data.PortProcessData[port];
  1186. byte[] tagBytes = SIG350RFIDClient.ExtractRFIDData(processData, new Rfh5xxReadOptions());
  1187. if (tagBytes == null || tagBytes.Length == 0)
  1188. continue;
  1189. Tags.Add(new RFIDTagData
  1190. {
  1191. Port = port + 1,
  1192. Data = tagBytes,
  1193. Timestamp = DateTime.Now,
  1194. Quality = status
  1195. });
  1196. }
  1197. }
  1198. }
  1199. #endregion
  1200. }