MitsubishiPLC.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Sockets;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using System.Threading.Tasks;
  9. using TeamAAS_VP.Core.RFID;
  10. namespace TeamAAS_VP.Core.PLCs
  11. {
  12. /// <summary>
  13. /// 三菱 PLC MC 协议客户端(3E 帧 / 二进制 / TCP)
  14. /// 适用于 Q/L/iQ-R/FX5 等系列以太网通讯
  15. /// </summary>
  16. public class MitsubishiPLC : IDisposable
  17. {
  18. // 3E 帧副头部 + 访问路径固定 9 字节,其后才是「数据长度」字段指定的内容
  19. private const int McHeaderLength = 9;
  20. private TcpClient _client;
  21. private NetworkStream _stream;
  22. private readonly object _sync = new object();
  23. public string IpAddress { get; }
  24. public int Port { get; }
  25. public byte NetworkNo { get; set; } = 0x00;
  26. public byte PcNo { get; set; } = 0xFF;
  27. public ushort IoModuleNo { get; set; } = 0x03FF;
  28. public byte StationNo { get; set; } = 0x00;
  29. public int TimeoutMs { get; set; } = 3000;
  30. public event Action<string> OnLog;
  31. public event Action ConnectionLost;
  32. public bool IsConnected => _client != null && _client.Connected;
  33. public MitsubishiPLC(string ipAddress, int port = 5000)
  34. {
  35. IpAddress = ipAddress;
  36. Port = port;
  37. }
  38. public bool Connect()
  39. {
  40. try
  41. {
  42. Disconnect();
  43. _client = new TcpClient();
  44. _client.NoDelay = true;
  45. _client.ReceiveTimeout = TimeoutMs;
  46. _client.SendTimeout = TimeoutMs;
  47. _client.Connect(IpAddress, Port);
  48. _stream = _client.GetStream();
  49. _stream.ReadTimeout = TimeoutMs;
  50. _stream.WriteTimeout = TimeoutMs;
  51. OnLog?.Invoke("已连接三菱 PLC (" + IpAddress + ":" + Port + ")");
  52. return true;
  53. }
  54. catch (Exception ex)
  55. {
  56. OnLog?.Invoke("连接 PLC 失败: " + ex.Message);
  57. Disconnect();
  58. return false;
  59. }
  60. }
  61. public void Disconnect()
  62. {
  63. try
  64. {
  65. if (_stream != null)
  66. _stream.Close();
  67. if (_client != null)
  68. _client.Close();
  69. }
  70. catch
  71. {
  72. // ignore cleanup errors
  73. }
  74. finally
  75. {
  76. _stream = null;
  77. _client = null;
  78. }
  79. }
  80. /// <summary>
  81. /// 读 Ready 位(PLC→PC 请求读卡)。未配置则抛异常。
  82. /// </summary>
  83. public bool ReadReadyBit(PlcRfidMapping mapping)
  84. {
  85. if (mapping == null)
  86. throw new ArgumentNullException(nameof(mapping));
  87. if (!IsDeviceEnabled(mapping.ReadyBitDevice))
  88. throw new InvalidOperationException("Ready 位未配置(不能为 0)");
  89. lock (_sync)
  90. return ReadBit(mapping.ReadyBitDevice);
  91. }
  92. /// <summary>
  93. /// 读 PLC 指定的读写头端口号。
  94. /// </summary>
  95. public int ReadRequestPort(PlcRfidMapping mapping)
  96. {
  97. if (mapping == null)
  98. throw new ArgumentNullException(nameof(mapping));
  99. if (!IsDeviceEnabled(mapping.PortDevice))
  100. throw new InvalidOperationException("端口寄存器未配置(不能为 0)");
  101. lock (_sync)
  102. {
  103. short[] words = ReadWords(mapping.PortDevice, 1);
  104. return words[0];
  105. }
  106. }
  107. /// <summary>
  108. /// 读卡成功:写数据到起始地址,置 OK=1、NG=0。
  109. /// </summary>
  110. public void WriteReadSuccess(PlcRfidMapping mapping, RFIDTagData tag)
  111. {
  112. if (mapping == null)
  113. throw new ArgumentNullException(nameof(mapping));
  114. if (tag == null || tag.Data == null || tag.Data.Length == 0)
  115. throw new ArgumentException("标签数据无效");
  116. if (tag.Data.Length > mapping.MaxDataBytes)
  117. throw new ArgumentException("RFID 数据长度 " + tag.Data.Length + " 超过上限 " + mapping.MaxDataBytes);
  118. if (!IsDeviceEnabled(mapping.DataStartDevice))
  119. throw new ArgumentException("数据起始寄存器未配置");
  120. lock (_sync)
  121. {
  122. if (IsDeviceEnabled(mapping.QualityDevice))
  123. WriteDevice(mapping.QualityDevice, tag.Quality);
  124. short[] words = BytesToWords(tag.Data);
  125. WriteDeviceBlock(mapping.DataStartDevice, words);
  126. if (IsDeviceEnabled(mapping.OkBitDevice))
  127. WriteBit(mapping.OkBitDevice, true);
  128. if (IsDeviceEnabled(mapping.NgBitDevice))
  129. WriteBit(mapping.NgBitDevice, false);
  130. WriteReceiveBit(mapping, true);
  131. OnLog?.Invoke("读卡 OK → 已写数据 端口=" + tag.Port
  132. + " 字节=" + tag.Data.Length
  133. + " 起始=" + mapping.DataStartDevice
  134. + (IsDeviceEnabled(mapping.ReceiveBitDevice) ? " Receive=1" : ""));
  135. }
  136. }
  137. /// <summary>
  138. /// 读卡失败:置 OK=0、NG=1。
  139. /// </summary>
  140. public void WriteReadFailure(PlcRfidMapping mapping)
  141. {
  142. if (mapping == null)
  143. throw new ArgumentNullException(nameof(mapping));
  144. lock (_sync)
  145. {
  146. if (IsDeviceEnabled(mapping.OkBitDevice))
  147. WriteBit(mapping.OkBitDevice, false);
  148. if (IsDeviceEnabled(mapping.NgBitDevice))
  149. WriteBit(mapping.NgBitDevice, true);
  150. WriteReceiveBit(mapping, true);
  151. OnLog?.Invoke("读卡 NG"
  152. + (IsDeviceEnabled(mapping.ReceiveBitDevice) ? " Receive=1" : ""));
  153. }
  154. }
  155. /// <summary>
  156. /// 置 Receive=1,通知 PLC 本次读卡交互已完成。
  157. /// </summary>
  158. public void WriteReceiveBit(PlcRfidMapping mapping, bool value)
  159. {
  160. if (mapping == null)
  161. throw new ArgumentNullException(nameof(mapping));
  162. if (!IsDeviceEnabled(mapping.ReceiveBitDevice))
  163. return;
  164. lock (_sync)
  165. WriteBit(mapping.ReceiveBitDevice, value);
  166. }
  167. /// <summary>
  168. /// Ready 关闭后仅清除 Receive(OK/NG 保持供 PLC 读取)。
  169. /// </summary>
  170. public void ClearReceiveBit(PlcRfidMapping mapping)
  171. {
  172. if (mapping == null)
  173. throw new ArgumentNullException(nameof(mapping));
  174. lock (_sync)
  175. {
  176. if (IsDeviceEnabled(mapping.ReceiveBitDevice))
  177. WriteBit(mapping.ReceiveBitDevice, false);
  178. }
  179. }
  180. /// <summary>
  181. /// SFIS 过站/上传完成:写 OK/NG + Receive=1(OK/NG 不清除,仅清 Receive)。
  182. /// </summary>
  183. public void WriteSfisResult(PlcSfisFeedback feedback, bool isPassStation, bool success)
  184. {
  185. if (feedback == null)
  186. throw new ArgumentNullException(nameof(feedback));
  187. string okDev = isPassStation ? feedback.PassOkBitDevice : feedback.UploadOkBitDevice;
  188. string ngDev = isPassStation ? feedback.PassNgBitDevice : feedback.UploadNgBitDevice;
  189. string recvDev = isPassStation ? feedback.PassReceiveBitDevice : feedback.UploadReceiveBitDevice;
  190. string label = isPassStation ? "过站" : "上传";
  191. lock (_sync)
  192. {
  193. if (IsDeviceEnabled(okDev))
  194. WriteBit(okDev, success);
  195. if (IsDeviceEnabled(ngDev))
  196. WriteBit(ngDev, !success);
  197. if (IsDeviceEnabled(recvDev))
  198. WriteBit(recvDev, true);
  199. OnLog?.Invoke(label + (success ? " OK" : " NG")
  200. + (IsDeviceEnabled(recvDev) ? " Receive=1" : string.Empty));
  201. }
  202. }
  203. /// <summary>SFIS 信号关闭后仅清除对应 Receive。</summary>
  204. public void ClearSfisReceiveBit(PlcSfisFeedback feedback, bool isPassStation)
  205. {
  206. if (feedback == null)
  207. throw new ArgumentNullException(nameof(feedback));
  208. string recvDev = isPassStation ? feedback.PassReceiveBitDevice : feedback.UploadReceiveBitDevice;
  209. if (!IsDeviceEnabled(recvDev))
  210. return;
  211. lock (_sync)
  212. WriteBit(recvDev, false);
  213. }
  214. private bool _heartbeatBitOn;
  215. private short _heartbeatWord;
  216. /// <summary>连接后重置心跳内部计数/翻转状态。</summary>
  217. public void ResetHeartbeatState()
  218. {
  219. lock (_sync)
  220. {
  221. _heartbeatBitOn = false;
  222. _heartbeatWord = 0;
  223. }
  224. }
  225. /// <summary>
  226. /// 根据地址类型发送一次心跳:M 等位元件翻转 bool,D 等字元件递增数值。
  227. /// </summary>
  228. public void PulseHeartbeat(string device)
  229. {
  230. if (!IsDeviceEnabled(device))
  231. return;
  232. PlcHeartbeatKind kind = ResolveHeartbeatKind(device);
  233. lock (_sync)
  234. {
  235. if (kind == PlcHeartbeatKind.Bit)
  236. {
  237. _heartbeatBitOn = !_heartbeatBitOn;
  238. WriteBit(device, _heartbeatBitOn);
  239. }
  240. else
  241. {
  242. _heartbeatWord++;
  243. if (_heartbeatWord <= 0)
  244. _heartbeatWord = 1;
  245. WriteDevice(device, _heartbeatWord);
  246. }
  247. }
  248. }
  249. /// <summary>解析心跳模式;地址填 0 返回 Disabled。</summary>
  250. public static PlcHeartbeatKind ResolveHeartbeatKind(string device)
  251. {
  252. if (!IsDeviceEnabled(device))
  253. return PlcHeartbeatKind.Disabled;
  254. string type = GetDeviceTypePrefix(device);
  255. if (IsBitDeviceType(type))
  256. return PlcHeartbeatKind.Bit;
  257. if (IsWordDeviceType(type))
  258. return PlcHeartbeatKind.Word;
  259. throw new ArgumentException("心跳地址类型不支持(请使用 M 位或 D 字): " + device, nameof(device));
  260. }
  261. public static string GetDeviceTypePrefix(string device)
  262. {
  263. if (string.IsNullOrWhiteSpace(device))
  264. throw new ArgumentException("软元件名称不能为空", nameof(device));
  265. var match = Regex.Match(device.Trim(), @"^([A-Za-z]+)(\d+)$");
  266. if (!match.Success)
  267. throw new ArgumentException("无法解析软元件地址: " + device, nameof(device));
  268. return match.Groups[1].Value.ToUpperInvariant();
  269. }
  270. private static bool IsBitDeviceType(string type)
  271. {
  272. switch (type)
  273. {
  274. case "M":
  275. case "L":
  276. case "F":
  277. case "V":
  278. case "S":
  279. case "X":
  280. case "Y":
  281. case "B":
  282. case "SM":
  283. return true;
  284. default:
  285. return false;
  286. }
  287. }
  288. private static bool IsWordDeviceType(string type)
  289. {
  290. switch (type)
  291. {
  292. case "D":
  293. case "W":
  294. case "SD":
  295. case "R":
  296. case "ZR":
  297. return true;
  298. default:
  299. return false;
  300. }
  301. }
  302. /// <summary>
  303. /// 软元件是否启用:空、空白或 "0" 表示不使用。
  304. /// </summary>
  305. public static bool IsDeviceEnabled(string device)
  306. {
  307. if (string.IsNullOrWhiteSpace(device))
  308. return false;
  309. return device.Trim() != "0";
  310. }
  311. /// <summary>
  312. /// 连接后自检:写/读一个字,确认 MC 帧交互正常。
  313. /// </summary>
  314. public bool TestCommunication(string device = "D110", short testValue = 0x55AA)
  315. {
  316. if (!IsDeviceEnabled(device))
  317. {
  318. OnLog?.Invoke("PLC 通讯测试跳过:测试地址未配置");
  319. return false;
  320. }
  321. lock (_sync)
  322. {
  323. try
  324. {
  325. short[] before = null;
  326. try
  327. {
  328. before = ReadWords(device, 1);
  329. }
  330. catch
  331. {
  332. before = null;
  333. }
  334. WriteDevice(device, testValue);
  335. short[] after = ReadWords(device, 1);
  336. bool ok = after != null && after.Length > 0 && after[0] == testValue;
  337. if (before != null && before.Length > 0)
  338. {
  339. try { WriteDevice(device, before[0]); }
  340. catch { /* ignore */ }
  341. }
  342. OnLog?.Invoke(ok
  343. ? "PLC 通讯测试成功 (" + device + ")"
  344. : "PLC 通讯测试失败: 写入 " + testValue + " 读回 "
  345. + (after != null && after.Length > 0 ? after[0].ToString() : "空"));
  346. return ok;
  347. }
  348. catch (Exception ex)
  349. {
  350. OnLog?.Invoke("PLC 通讯测试异常: " + ex.Message);
  351. return false;
  352. }
  353. }
  354. }
  355. /// <summary>
  356. /// 连接后自检:对位软元件执行 读→写反→读→恢复,并输出 MC 报文与字读交叉验证。
  357. /// </summary>
  358. public bool TestBitCommunication(string device)
  359. {
  360. if (!IsDeviceEnabled(device))
  361. {
  362. OnLog?.Invoke("PLC 位通讯测试跳过:地址未配置");
  363. return false;
  364. }
  365. lock (_sync)
  366. {
  367. try
  368. {
  369. DeviceAddress addr = ParseDevice(device);
  370. OnLog?.Invoke("位测试 [" + device + "] 解析地址=" + addr.Address
  371. + " 软元件=0x" + addr.Code.ToString("X2"));
  372. byte rawOriginal = ReadBitRaw(addr);
  373. bool original = IsMcBitOn(rawOriginal);
  374. WordBitSnapshot snapOriginal = ReadWordBitSnapshot(addr);
  375. OnLog?.Invoke(" 步骤1 位读: 0x" + rawOriginal.ToString("X2")
  376. + " => " + original + " | 字读 M" + snapOriginal.WordStart
  377. + " bit" + snapOriginal.BitIndex + "=" + snapOriginal.BitOn
  378. + " (字=0x" + snapOriginal.WordHex + ")");
  379. bool target = !original;
  380. byte[] writeReq = BuildRequestBody(
  381. command: 0x1401,
  382. subCommand: 0x0001,
  383. deviceCode: addr.Code,
  384. address: addr.Address,
  385. points: 1,
  386. writeData: new[] { ToMcBitValue(target) });
  387. OnLog?.Invoke(" 步骤2 位写 " + target + " 完整帧: "
  388. + FormatHex(BuildFrame(writeReq)));
  389. WriteBits(addr.Code, addr.Address, new[] { target });
  390. byte rawFlipped = ReadBitRaw(addr);
  391. bool flipped = IsMcBitOn(rawFlipped);
  392. WordBitSnapshot snapFlipped = ReadWordBitSnapshot(addr);
  393. OnLog?.Invoke(" 步骤3 位读: 0x" + rawFlipped.ToString("X2")
  394. + " => " + flipped + " | 字读 M" + snapFlipped.WordStart
  395. + " bit" + snapFlipped.BitIndex + "=" + snapFlipped.BitOn
  396. + " (字=0x" + snapFlipped.WordHex + ")");
  397. WriteBits(addr.Code, addr.Address, new[] { original });
  398. byte rawRestored = ReadBitRaw(addr);
  399. bool restored = IsMcBitOn(rawRestored);
  400. OnLog?.Invoke(" 步骤4 恢复写 " + original + " 位读: 0x"
  401. + rawRestored.ToString("X2") + " => " + restored);
  402. bool ok = flipped != original && restored == original;
  403. if (!ok)
  404. OnLog?.Invoke(" 诊断: " + DiagnoseBitTestFailure(
  405. original, rawOriginal, target, rawFlipped, snapFlipped, restored, rawRestored));
  406. OnLog?.Invoke(ok
  407. ? "PLC 位通讯测试成功 (" + device + ")"
  408. : "PLC 位通讯测试失败 (" + device + ")");
  409. return ok;
  410. }
  411. catch (Exception ex)
  412. {
  413. OnLog?.Invoke("PLC 位通讯测试异常 (" + device + "): " + ex.Message);
  414. return false;
  415. }
  416. }
  417. }
  418. private struct WordBitSnapshot
  419. {
  420. public int WordStart;
  421. public int BitIndex;
  422. public bool BitOn;
  423. public string WordHex;
  424. }
  425. private WordBitSnapshot ReadWordBitSnapshot(DeviceAddress addr)
  426. {
  427. int wordStart = (addr.Address / 16) * 16;
  428. int bitIndex = addr.Address % 16;
  429. short[] words = ReadWords(addr.Code, wordStart, 1);
  430. int word = words[0] & 0xFFFF;
  431. return new WordBitSnapshot
  432. {
  433. WordStart = wordStart,
  434. BitIndex = bitIndex,
  435. BitOn = (word & (1 << bitIndex)) != 0,
  436. WordHex = word.ToString("X4")
  437. };
  438. }
  439. private static string DiagnoseBitTestFailure(
  440. bool original, byte rawOriginal,
  441. bool target, byte rawFlipped, WordBitSnapshot snapFlipped,
  442. bool restored, byte rawRestored)
  443. {
  444. if (target && (rawFlipped == 0x10 || rawFlipped == 0x01) && snapFlipped.BitOn)
  445. return "位读写实际成功,请检查测试判定逻辑";
  446. if (target && rawOriginal == 0x00 && rawFlipped == 0x00 && !snapFlipped.BitOn)
  447. return "位写后位读/字读均为 OFF — 可能原因: (1) PLC 梯形图强制清 M101 "
  448. + "(2) 位写报文未被 PLC 接受 (3) 未允许 RUN 中写入;请用 Hsl 对同一地址 Write/Read 对照";
  449. if (target && !snapFlipped.BitOn && (rawFlipped == 0x10 || rawFlipped == 0x01))
  450. return "位读为 ON 但字读为 OFF — 位/字地址映射或读路径异常";
  451. if (target && snapFlipped.BitOn && rawFlipped == 0x00)
  452. return "字读为 ON 但位读为 OFF — 位读响应解析可能有问题";
  453. if (restored != original)
  454. return "恢复失败,PLC 可能在持续改写该位";
  455. return "未知,请对照步骤2完整帧与 HslCommunication 抓包";
  456. }
  457. private static string FormatHex(byte[] data)
  458. {
  459. return BitConverter.ToString(data).Replace("-", " ");
  460. }
  461. public short[] ReadWords(string device, int count)
  462. {
  463. DeviceAddress addr = ParseDevice(device);
  464. return ReadWords(addr.Code, addr.Address, count);
  465. }
  466. public void WriteWords(string device, short[] values)
  467. {
  468. DeviceAddress addr = ParseDevice(device);
  469. WriteWords(addr.Code, addr.Address, values);
  470. }
  471. public void WriteDevice(string device, short value)
  472. {
  473. WriteWords(device, new[] { value });
  474. }
  475. public void WriteDeviceBlock(string startDevice, short[] values)
  476. {
  477. WriteWords(startDevice, values);
  478. }
  479. /// <summary>从字软元件读取指定字节数(低字节在前)。</summary>
  480. public byte[] ReadDeviceBytes(string device, int byteCount)
  481. {
  482. if (byteCount <= 0)
  483. return new byte[0];
  484. int wordCount = (byteCount + 1) / 2;
  485. short[] words = ReadWords(device, wordCount);
  486. byte[] all = WordsToBytes(words);
  487. if (all.Length <= byteCount)
  488. return all;
  489. var trimmed = new byte[byteCount];
  490. Array.Copy(all, trimmed, byteCount);
  491. return trimmed;
  492. }
  493. public void WriteBit(string device, bool value)
  494. {
  495. if (!IsDeviceEnabled(device))
  496. return;
  497. DeviceAddress addr = ParseDevice(device);
  498. WriteBits(addr.Code, addr.Address, new[] { value });
  499. }
  500. public bool ReadBit(string device)
  501. {
  502. if (!IsDeviceEnabled(device))
  503. throw new ArgumentException("位地址未启用: " + device);
  504. DeviceAddress addr = ParseDevice(device);
  505. return ReadBits(addr.Code, addr.Address, 1)[0];
  506. }
  507. public bool[] ReadBits(byte deviceCode, int address, int count)
  508. {
  509. if (count <= 0)
  510. throw new ArgumentOutOfRangeException(nameof(count));
  511. byte[] requestData = BuildRequestBody(
  512. command: 0x0401,
  513. subCommand: 0x0001,
  514. deviceCode: deviceCode,
  515. address: address,
  516. points: count);
  517. byte[] response = SendRequest(requestData);
  518. if (response.Length < 2 + count)
  519. throw new InvalidOperationException("PLC 位读响应数据长度不足");
  520. var result = new bool[count];
  521. for (int i = 0; i < count; i++)
  522. result[i] = IsMcBitOn(response[2 + i]);
  523. return result;
  524. }
  525. /// <summary>MC 协议位读响应:00H=OFF;01H 或 10H 均表示 ON。</summary>
  526. private static bool IsMcBitOn(byte value)
  527. {
  528. return value == 0x01 || value == 0x10;
  529. }
  530. /// <summary>MC 协议位写请求(1401/0001):00H=OFF,10H=ON。</summary>
  531. private static byte ToMcBitValue(bool value)
  532. {
  533. return value ? (byte)0x10 : (byte)0x00;
  534. }
  535. private byte ReadBitRaw(DeviceAddress addr)
  536. {
  537. byte[] requestData = BuildRequestBody(
  538. command: 0x0401,
  539. subCommand: 0x0001,
  540. deviceCode: addr.Code,
  541. address: addr.Address,
  542. points: 1);
  543. byte[] response = SendRequest(requestData);
  544. if (response.Length < 3)
  545. throw new InvalidOperationException("PLC 位读响应数据长度不足");
  546. return response[2];
  547. }
  548. public short[] ReadWords(byte deviceCode, int address, int count)
  549. {
  550. if (count <= 0)
  551. throw new ArgumentOutOfRangeException(nameof(count));
  552. byte[] requestData = BuildRequestBody(
  553. command: 0x0401,
  554. subCommand: 0x0000,
  555. deviceCode: deviceCode,
  556. address: address,
  557. points: count);
  558. byte[] response = SendRequest(requestData);
  559. return ParseWordReadResponse(response, count);
  560. }
  561. public void WriteWords(byte deviceCode, int address, short[] values)
  562. {
  563. if (values == null || values.Length == 0)
  564. throw new ArgumentException("写入数据不能为空", nameof(values));
  565. byte[] requestData = BuildRequestBody(
  566. command: 0x1401,
  567. subCommand: 0x0000,
  568. deviceCode: deviceCode,
  569. address: address,
  570. points: values.Length,
  571. writeData: WordsToBytes(values));
  572. SendRequest(requestData);
  573. }
  574. public void WriteBits(byte deviceCode, int address, bool[] values)
  575. {
  576. if (values == null || values.Length == 0)
  577. throw new ArgumentException("写入数据不能为空", nameof(values));
  578. // 二进制位写入:每点 1 字节,00H=OFF / 10H=ON
  579. var data = new byte[values.Length];
  580. for (int i = 0; i < values.Length; i++)
  581. data[i] = ToMcBitValue(values[i]);
  582. byte[] requestData = BuildRequestBody(
  583. command: 0x1401,
  584. subCommand: 0x0001,
  585. deviceCode: deviceCode,
  586. address: address,
  587. points: values.Length,
  588. writeData: data);
  589. SendRequest(requestData);
  590. }
  591. /// <summary>
  592. /// 请求数据:监视定时器 + 指令 + 子指令 + 起始软元件(3) + 软元件代码(1) + 点数 + [写数据]
  593. /// 注意:二进制 3E 帧中「地址在前、软元件代码在后」。
  594. /// </summary>
  595. private static byte[] BuildRequestBody(ushort command, ushort subCommand, byte deviceCode, int address, int points, byte[] writeData = null)
  596. {
  597. using (var ms = new MemoryStream())
  598. using (var writer = new BinaryWriter(ms))
  599. {
  600. writer.Write((ushort)0x0010); // 监视定时器:0x10 * 250ms = 4s
  601. writer.Write(command);
  602. writer.Write(subCommand);
  603. // 起始软元件编号:3 字节,低字节在前
  604. writer.Write((byte)(address & 0xFF));
  605. writer.Write((byte)((address >> 8) & 0xFF));
  606. writer.Write((byte)((address >> 16) & 0xFF));
  607. // 软元件代码
  608. writer.Write(deviceCode);
  609. writer.Write((ushort)points);
  610. if (writeData != null && writeData.Length > 0)
  611. writer.Write(writeData);
  612. return ms.ToArray();
  613. }
  614. }
  615. private byte[] BuildFrame(byte[] requestData)
  616. {
  617. using (var ms = new MemoryStream())
  618. using (var writer = new BinaryWriter(ms))
  619. {
  620. writer.Write((ushort)0x0050); // 请求副头部
  621. writer.Write(NetworkNo);
  622. writer.Write(PcNo);
  623. writer.Write(IoModuleNo); // 小端 0x03FF -> FF 03
  624. writer.Write(StationNo);
  625. writer.Write((ushort)requestData.Length);
  626. writer.Write(requestData);
  627. return ms.ToArray();
  628. }
  629. }
  630. private byte[] SendRequest(byte[] requestData)
  631. {
  632. if (!IsConnected || _stream == null)
  633. throw new InvalidOperationException("PLC 未连接");
  634. try
  635. {
  636. lock (_sync)
  637. {
  638. byte[] frame = BuildFrame(requestData);
  639. _stream.Write(frame, 0, frame.Length);
  640. _stream.Flush();
  641. // 响应头固定 9 字节(不是 11!多读会吃掉结束码,导致后续读超时)
  642. byte[] header = ReadExact(McHeaderLength);
  643. if (header[0] != 0xD0 || header[1] != 0x00)
  644. {
  645. throw new InvalidOperationException(
  646. "PLC 响应副头部无效: " + BitConverter.ToString(header));
  647. }
  648. int dataLength = header[7] | (header[8] << 8);
  649. if (dataLength < 2)
  650. throw new InvalidOperationException("PLC 响应数据长度无效: " + dataLength);
  651. byte[] body = ReadExact(dataLength);
  652. ushort endCode = (ushort)(body[0] | (body[1] << 8));
  653. if (endCode != 0)
  654. throw new InvalidOperationException("PLC 返回错误码: 0x" + endCode.ToString("X4")
  655. + " (" + DescribeEndCode(endCode) + ")");
  656. return body;
  657. }
  658. }
  659. catch (Exception ex) when (IsTransportFailure(ex))
  660. {
  661. OnLog?.Invoke("PLC 通讯中断: " + ex.Message);
  662. NotifyConnectionLost();
  663. throw;
  664. }
  665. }
  666. private void NotifyConnectionLost()
  667. {
  668. if (_client == null)
  669. return;
  670. Disconnect();
  671. ConnectionLost?.Invoke();
  672. }
  673. private static bool IsTransportFailure(Exception ex)
  674. {
  675. if (ex is IOException || ex is SocketException || ex is ObjectDisposedException)
  676. return true;
  677. if (ex is InvalidOperationException && ex.Message.Contains("连接已断开"))
  678. return true;
  679. return ex.InnerException != null && IsTransportFailure(ex.InnerException);
  680. }
  681. private static string DescribeEndCode(ushort endCode)
  682. {
  683. switch (endCode)
  684. {
  685. case 0xC050: return "不允许在 RUN 中写入,请在 PLC 以太网参数中勾选允许 RUN 中写入";
  686. case 0xC051: return "请求数据长度错误";
  687. case 0xC056: return "指令/子指令不支持";
  688. case 0xC059: return "指令格式错误(请检查软元件代码/地址顺序)";
  689. case 0xC05C: return "请求内容错误";
  690. case 0xC061: return "请求数据长度与实际不符";
  691. default: return "详见三菱 MC 协议手册结束代码";
  692. }
  693. }
  694. private short[] ParseWordReadResponse(byte[] body, int count)
  695. {
  696. if (body.Length < 2 + count * 2)
  697. throw new InvalidOperationException("PLC 读响应数据长度不足");
  698. var result = new short[count];
  699. int offset = 2;
  700. for (int i = 0; i < count; i++)
  701. {
  702. result[i] = (short)(body[offset] | (body[offset + 1] << 8));
  703. offset += 2;
  704. }
  705. return result;
  706. }
  707. private byte[] ReadExact(int length)
  708. {
  709. var buffer = new byte[length];
  710. int read = 0;
  711. while (read < length)
  712. {
  713. int n = _stream.Read(buffer, read, length - read);
  714. if (n <= 0)
  715. throw new IOException("PLC 连接已断开");
  716. read += n;
  717. }
  718. return buffer;
  719. }
  720. private static short[] BytesToWords(byte[] data)
  721. {
  722. int wordCount = (data.Length + 1) / 2;
  723. var words = new short[wordCount];
  724. for (int i = 0; i < wordCount; i++)
  725. {
  726. int idx = i * 2;
  727. byte low = data[idx];
  728. byte high = idx + 1 < data.Length ? data[idx + 1] : (byte)0;
  729. words[i] = (short)(low | (high << 8));
  730. }
  731. return words;
  732. }
  733. private static byte[] WordsToBytes(short[] words)
  734. {
  735. var data = new byte[words.Length * 2];
  736. for (int i = 0; i < words.Length; i++)
  737. {
  738. data[i * 2] = (byte)(words[i] & 0xFF);
  739. data[i * 2 + 1] = (byte)((words[i] >> 8) & 0xFF);
  740. }
  741. return data;
  742. }
  743. private struct DeviceAddress
  744. {
  745. public byte Code;
  746. public int Address;
  747. }
  748. private static bool TryParseDevice(string device, out DeviceAddress result)
  749. {
  750. result = default(DeviceAddress);
  751. try
  752. {
  753. result = ParseDevice(device);
  754. return true;
  755. }
  756. catch
  757. {
  758. return false;
  759. }
  760. }
  761. private static DeviceAddress ParseDevice(string device)
  762. {
  763. if (string.IsNullOrWhiteSpace(device))
  764. throw new ArgumentException("软元件名称不能为空", nameof(device));
  765. var match = Regex.Match(device.Trim(), @"^([A-Za-z]+)(\d+)$");
  766. if (!match.Success)
  767. throw new ArgumentException("无法解析软元件地址: " + device, nameof(device));
  768. string type = match.Groups[1].Value.ToUpperInvariant();
  769. string number = match.Groups[2].Value;
  770. // M/L/F/V/S/SM/SD/D/R/ZR:十进制;X/Y:GX Works 八进制;B/W/SB/SW:十六进制
  771. int address;
  772. if (type == "X" || type == "Y")
  773. address = Convert.ToInt32(number, 8);
  774. else if (type == "B" || type == "W" || type == "SB" || type == "SW")
  775. address = Convert.ToInt32(number, 16);
  776. else
  777. address = int.Parse(number);
  778. byte code;
  779. if (!DeviceCodes.TryGetValue(type, out code))
  780. throw new ArgumentException("不支持的软元件类型: " + type, nameof(device));
  781. return new DeviceAddress { Code = code, Address = address };
  782. }
  783. private static readonly Dictionary<string, byte> DeviceCodes = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase)
  784. {
  785. { "SM", 0x91 },
  786. { "SD", 0xA9 },
  787. { "M", 0x90 },
  788. { "L", 0x92 },
  789. { "F", 0x93 },
  790. { "V", 0x94 },
  791. { "S", 0x98 },
  792. { "X", 0x9C },
  793. { "Y", 0x9D },
  794. { "B", 0xA0 },
  795. { "D", 0xA8 },
  796. { "W", 0xB4 },
  797. { "R", 0xAF },
  798. { "ZR", 0xB0 }
  799. };
  800. public void Dispose()
  801. {
  802. Disconnect();
  803. GC.SuppressFinalize(this);
  804. }
  805. }
  806. public enum PlcHeartbeatKind
  807. {
  808. Disabled,
  809. Bit,
  810. Word
  811. }
  812. /// <summary>
  813. /// RFID 与 PLC 握手寄存器映射。
  814. /// 流程:PLC 置 Ready → PC 读端口号并读卡 → 写数据 → 写 OK/NG → 写 Receive
  815. /// → 等 Ready 关闭 → PC 仅清 Receive。
  816. /// 地址填 "0" 表示不使用该软元件。
  817. /// </summary>
  818. public class PlcRfidMapping
  819. {
  820. /// <summary>RFID 原始数据起始寄存器(PC 写入)</summary>
  821. public string DataStartDevice { get; set; } = "D110";
  822. /// <summary>端口号寄存器(PLC 写入,PC 读取)</summary>
  823. public string PortDevice { get; set; } = "D101";
  824. /// <summary>数据质量(PC 写入,可选)</summary>
  825. public string QualityDevice { get; set; } = "D102";
  826. /// <summary>Ready 位(PLC→PC 请求读卡)</summary>
  827. public string ReadyBitDevice { get; set; } = "M100";
  828. /// <summary>读 OK 位(PC→PLC)</summary>
  829. public string OkBitDevice { get; set; } = "0";
  830. /// <summary>读 NG 位(PC→PLC)</summary>
  831. public string NgBitDevice { get; set; } = "0";
  832. /// <summary>Receive 位(PC→PLC,读卡完成后置 1)</summary>
  833. public string ReceiveBitDevice { get; set; } = "0";
  834. /// <summary>最大转发字节数</summary>
  835. public int MaxDataBytes { get; set; } = 32;
  836. /// <summary>单次读卡超时(毫秒)</summary>
  837. public int ReadTimeoutMs { get; set; } = 2000;
  838. }
  839. /// <summary>
  840. /// SFIS 过站/上传 PLC 回写映射(PC→PLC)。
  841. /// 流程:信号上升沿触发 SFIS → 写 OK/NG + Receive → 信号关闭后仅清 Receive。
  842. /// </summary>
  843. public class PlcSfisFeedback
  844. {
  845. public string PassOkBitDevice { get; set; } = "M106";
  846. public string PassNgBitDevice { get; set; } = "M107";
  847. public string PassReceiveBitDevice { get; set; } = "M108";
  848. public string UploadOkBitDevice { get; set; } = "M109";
  849. public string UploadNgBitDevice { get; set; } = "M110";
  850. public string UploadReceiveBitDevice { get; set; } = "M111";
  851. }
  852. }