| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Net.Sockets;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Threading.Tasks;
- using TeamAAS_VP.Core.RFID;
- namespace TeamAAS_VP.Core.PLCs
- {
- /// <summary>
- /// 三菱 PLC MC 协议客户端(3E 帧 / 二进制 / TCP)
- /// 适用于 Q/L/iQ-R/FX5 等系列以太网通讯
- /// </summary>
- public class MitsubishiPLC : IDisposable
- {
- // 3E 帧副头部 + 访问路径固定 9 字节,其后才是「数据长度」字段指定的内容
- private const int McHeaderLength = 9;
- private TcpClient _client;
- private NetworkStream _stream;
- private readonly object _sync = new object();
- public string IpAddress { get; }
- public int Port { get; }
- public byte NetworkNo { get; set; } = 0x00;
- public byte PcNo { get; set; } = 0xFF;
- public ushort IoModuleNo { get; set; } = 0x03FF;
- public byte StationNo { get; set; } = 0x00;
- public int TimeoutMs { get; set; } = 3000;
- public event Action<string> OnLog;
- public event Action ConnectionLost;
- public bool IsConnected => _client != null && _client.Connected;
- public MitsubishiPLC(string ipAddress, int port = 5000)
- {
- IpAddress = ipAddress;
- Port = port;
- }
- public bool Connect()
- {
- try
- {
- Disconnect();
- _client = new TcpClient();
- _client.NoDelay = true;
- _client.ReceiveTimeout = TimeoutMs;
- _client.SendTimeout = TimeoutMs;
- _client.Connect(IpAddress, Port);
- _stream = _client.GetStream();
- _stream.ReadTimeout = TimeoutMs;
- _stream.WriteTimeout = TimeoutMs;
- OnLog?.Invoke("已连接三菱 PLC (" + IpAddress + ":" + Port + ")");
- return true;
- }
- catch (Exception ex)
- {
- OnLog?.Invoke("连接 PLC 失败: " + ex.Message);
- Disconnect();
- return false;
- }
- }
- public void Disconnect()
- {
- try
- {
- if (_stream != null)
- _stream.Close();
- if (_client != null)
- _client.Close();
- }
- catch
- {
- // ignore cleanup errors
- }
- finally
- {
- _stream = null;
- _client = null;
- }
- }
- /// <summary>
- /// 读 Ready 位(PLC→PC 请求读卡)。未配置则抛异常。
- /// </summary>
- public bool ReadReadyBit(PlcRfidMapping mapping)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- if (!IsDeviceEnabled(mapping.ReadyBitDevice))
- throw new InvalidOperationException("Ready 位未配置(不能为 0)");
- lock (_sync)
- return ReadBit(mapping.ReadyBitDevice);
- }
- /// <summary>
- /// 读 PLC 指定的读写头端口号。
- /// </summary>
- public int ReadRequestPort(PlcRfidMapping mapping)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- if (!IsDeviceEnabled(mapping.PortDevice))
- throw new InvalidOperationException("端口寄存器未配置(不能为 0)");
- lock (_sync)
- {
- short[] words = ReadWords(mapping.PortDevice, 1);
- return words[0];
- }
- }
- /// <summary>
- /// 读卡成功:写数据到起始地址,置 OK=1、NG=0。
- /// </summary>
- public void WriteReadSuccess(PlcRfidMapping mapping, RFIDTagData tag)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- if (tag == null || tag.Data == null || tag.Data.Length == 0)
- throw new ArgumentException("标签数据无效");
- if (tag.Data.Length > mapping.MaxDataBytes)
- throw new ArgumentException("RFID 数据长度 " + tag.Data.Length + " 超过上限 " + mapping.MaxDataBytes);
- if (!IsDeviceEnabled(mapping.DataStartDevice))
- throw new ArgumentException("数据起始寄存器未配置");
- lock (_sync)
- {
- if (IsDeviceEnabled(mapping.QualityDevice))
- WriteDevice(mapping.QualityDevice, tag.Quality);
- short[] words = BytesToWords(tag.Data);
- WriteDeviceBlock(mapping.DataStartDevice, words);
- if (IsDeviceEnabled(mapping.OkBitDevice))
- WriteBit(mapping.OkBitDevice, true);
- if (IsDeviceEnabled(mapping.NgBitDevice))
- WriteBit(mapping.NgBitDevice, false);
- WriteReceiveBit(mapping, true);
- OnLog?.Invoke("读卡 OK → 已写数据 端口=" + tag.Port
- + " 字节=" + tag.Data.Length
- + " 起始=" + mapping.DataStartDevice
- + (IsDeviceEnabled(mapping.ReceiveBitDevice) ? " Receive=1" : ""));
- }
- }
- /// <summary>
- /// 读卡失败:置 OK=0、NG=1。
- /// </summary>
- public void WriteReadFailure(PlcRfidMapping mapping)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- lock (_sync)
- {
- if (IsDeviceEnabled(mapping.OkBitDevice))
- WriteBit(mapping.OkBitDevice, false);
- if (IsDeviceEnabled(mapping.NgBitDevice))
- WriteBit(mapping.NgBitDevice, true);
- WriteReceiveBit(mapping, true);
- OnLog?.Invoke("读卡 NG"
- + (IsDeviceEnabled(mapping.ReceiveBitDevice) ? " Receive=1" : ""));
- }
- }
- /// <summary>
- /// 置 Receive=1,通知 PLC 本次读卡交互已完成。
- /// </summary>
- public void WriteReceiveBit(PlcRfidMapping mapping, bool value)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- if (!IsDeviceEnabled(mapping.ReceiveBitDevice))
- return;
- lock (_sync)
- WriteBit(mapping.ReceiveBitDevice, value);
- }
- /// <summary>
- /// Ready 关闭后仅清除 Receive(OK/NG 保持供 PLC 读取)。
- /// </summary>
- public void ClearReceiveBit(PlcRfidMapping mapping)
- {
- if (mapping == null)
- throw new ArgumentNullException(nameof(mapping));
- lock (_sync)
- {
- if (IsDeviceEnabled(mapping.ReceiveBitDevice))
- WriteBit(mapping.ReceiveBitDevice, false);
- }
- }
- /// <summary>
- /// SFIS 过站/上传完成:写 OK/NG + Receive=1(OK/NG 不清除,仅清 Receive)。
- /// </summary>
- public void WriteSfisResult(PlcSfisFeedback feedback, bool isPassStation, bool success)
- {
- if (feedback == null)
- throw new ArgumentNullException(nameof(feedback));
- string okDev = isPassStation ? feedback.PassOkBitDevice : feedback.UploadOkBitDevice;
- string ngDev = isPassStation ? feedback.PassNgBitDevice : feedback.UploadNgBitDevice;
- string recvDev = isPassStation ? feedback.PassReceiveBitDevice : feedback.UploadReceiveBitDevice;
- string label = isPassStation ? "过站" : "上传";
- lock (_sync)
- {
- if (IsDeviceEnabled(okDev))
- WriteBit(okDev, success);
- if (IsDeviceEnabled(ngDev))
- WriteBit(ngDev, !success);
- if (IsDeviceEnabled(recvDev))
- WriteBit(recvDev, true);
- OnLog?.Invoke(label + (success ? " OK" : " NG")
- + (IsDeviceEnabled(recvDev) ? " Receive=1" : string.Empty));
- }
- }
- /// <summary>SFIS 信号关闭后仅清除对应 Receive。</summary>
- public void ClearSfisReceiveBit(PlcSfisFeedback feedback, bool isPassStation)
- {
- if (feedback == null)
- throw new ArgumentNullException(nameof(feedback));
- string recvDev = isPassStation ? feedback.PassReceiveBitDevice : feedback.UploadReceiveBitDevice;
- if (!IsDeviceEnabled(recvDev))
- return;
- lock (_sync)
- WriteBit(recvDev, false);
- }
- private bool _heartbeatBitOn;
- private short _heartbeatWord;
- /// <summary>连接后重置心跳内部计数/翻转状态。</summary>
- public void ResetHeartbeatState()
- {
- lock (_sync)
- {
- _heartbeatBitOn = false;
- _heartbeatWord = 0;
- }
- }
- /// <summary>
- /// 根据地址类型发送一次心跳:M 等位元件翻转 bool,D 等字元件递增数值。
- /// </summary>
- public void PulseHeartbeat(string device)
- {
- if (!IsDeviceEnabled(device))
- return;
- PlcHeartbeatKind kind = ResolveHeartbeatKind(device);
- lock (_sync)
- {
- if (kind == PlcHeartbeatKind.Bit)
- {
- _heartbeatBitOn = !_heartbeatBitOn;
- WriteBit(device, _heartbeatBitOn);
- }
- else
- {
- _heartbeatWord++;
- if (_heartbeatWord <= 0)
- _heartbeatWord = 1;
- WriteDevice(device, _heartbeatWord);
- }
- }
- }
- /// <summary>解析心跳模式;地址填 0 返回 Disabled。</summary>
- public static PlcHeartbeatKind ResolveHeartbeatKind(string device)
- {
- if (!IsDeviceEnabled(device))
- return PlcHeartbeatKind.Disabled;
- string type = GetDeviceTypePrefix(device);
- if (IsBitDeviceType(type))
- return PlcHeartbeatKind.Bit;
- if (IsWordDeviceType(type))
- return PlcHeartbeatKind.Word;
- throw new ArgumentException("心跳地址类型不支持(请使用 M 位或 D 字): " + device, nameof(device));
- }
- public static string GetDeviceTypePrefix(string device)
- {
- if (string.IsNullOrWhiteSpace(device))
- throw new ArgumentException("软元件名称不能为空", nameof(device));
- var match = Regex.Match(device.Trim(), @"^([A-Za-z]+)(\d+)$");
- if (!match.Success)
- throw new ArgumentException("无法解析软元件地址: " + device, nameof(device));
- return match.Groups[1].Value.ToUpperInvariant();
- }
- private static bool IsBitDeviceType(string type)
- {
- switch (type)
- {
- case "M":
- case "L":
- case "F":
- case "V":
- case "S":
- case "X":
- case "Y":
- case "B":
- case "SM":
- return true;
- default:
- return false;
- }
- }
- private static bool IsWordDeviceType(string type)
- {
- switch (type)
- {
- case "D":
- case "W":
- case "SD":
- case "R":
- case "ZR":
- return true;
- default:
- return false;
- }
- }
- /// <summary>
- /// 软元件是否启用:空、空白或 "0" 表示不使用。
- /// </summary>
- public static bool IsDeviceEnabled(string device)
- {
- if (string.IsNullOrWhiteSpace(device))
- return false;
- return device.Trim() != "0";
- }
- /// <summary>
- /// 连接后自检:写/读一个字,确认 MC 帧交互正常。
- /// </summary>
- public bool TestCommunication(string device = "D110", short testValue = 0x55AA)
- {
- if (!IsDeviceEnabled(device))
- {
- OnLog?.Invoke("PLC 通讯测试跳过:测试地址未配置");
- return false;
- }
- lock (_sync)
- {
- try
- {
- short[] before = null;
- try
- {
- before = ReadWords(device, 1);
- }
- catch
- {
- before = null;
- }
- WriteDevice(device, testValue);
- short[] after = ReadWords(device, 1);
- bool ok = after != null && after.Length > 0 && after[0] == testValue;
- if (before != null && before.Length > 0)
- {
- try { WriteDevice(device, before[0]); }
- catch { /* ignore */ }
- }
- OnLog?.Invoke(ok
- ? "PLC 通讯测试成功 (" + device + ")"
- : "PLC 通讯测试失败: 写入 " + testValue + " 读回 "
- + (after != null && after.Length > 0 ? after[0].ToString() : "空"));
- return ok;
- }
- catch (Exception ex)
- {
- OnLog?.Invoke("PLC 通讯测试异常: " + ex.Message);
- return false;
- }
- }
- }
- /// <summary>
- /// 连接后自检:对位软元件执行 读→写反→读→恢复,并输出 MC 报文与字读交叉验证。
- /// </summary>
- public bool TestBitCommunication(string device)
- {
- if (!IsDeviceEnabled(device))
- {
- OnLog?.Invoke("PLC 位通讯测试跳过:地址未配置");
- return false;
- }
- lock (_sync)
- {
- try
- {
- DeviceAddress addr = ParseDevice(device);
- OnLog?.Invoke("位测试 [" + device + "] 解析地址=" + addr.Address
- + " 软元件=0x" + addr.Code.ToString("X2"));
- byte rawOriginal = ReadBitRaw(addr);
- bool original = IsMcBitOn(rawOriginal);
- WordBitSnapshot snapOriginal = ReadWordBitSnapshot(addr);
- OnLog?.Invoke(" 步骤1 位读: 0x" + rawOriginal.ToString("X2")
- + " => " + original + " | 字读 M" + snapOriginal.WordStart
- + " bit" + snapOriginal.BitIndex + "=" + snapOriginal.BitOn
- + " (字=0x" + snapOriginal.WordHex + ")");
- bool target = !original;
- byte[] writeReq = BuildRequestBody(
- command: 0x1401,
- subCommand: 0x0001,
- deviceCode: addr.Code,
- address: addr.Address,
- points: 1,
- writeData: new[] { ToMcBitValue(target) });
- OnLog?.Invoke(" 步骤2 位写 " + target + " 完整帧: "
- + FormatHex(BuildFrame(writeReq)));
- WriteBits(addr.Code, addr.Address, new[] { target });
- byte rawFlipped = ReadBitRaw(addr);
- bool flipped = IsMcBitOn(rawFlipped);
- WordBitSnapshot snapFlipped = ReadWordBitSnapshot(addr);
- OnLog?.Invoke(" 步骤3 位读: 0x" + rawFlipped.ToString("X2")
- + " => " + flipped + " | 字读 M" + snapFlipped.WordStart
- + " bit" + snapFlipped.BitIndex + "=" + snapFlipped.BitOn
- + " (字=0x" + snapFlipped.WordHex + ")");
- WriteBits(addr.Code, addr.Address, new[] { original });
- byte rawRestored = ReadBitRaw(addr);
- bool restored = IsMcBitOn(rawRestored);
- OnLog?.Invoke(" 步骤4 恢复写 " + original + " 位读: 0x"
- + rawRestored.ToString("X2") + " => " + restored);
- bool ok = flipped != original && restored == original;
- if (!ok)
- OnLog?.Invoke(" 诊断: " + DiagnoseBitTestFailure(
- original, rawOriginal, target, rawFlipped, snapFlipped, restored, rawRestored));
- OnLog?.Invoke(ok
- ? "PLC 位通讯测试成功 (" + device + ")"
- : "PLC 位通讯测试失败 (" + device + ")");
- return ok;
- }
- catch (Exception ex)
- {
- OnLog?.Invoke("PLC 位通讯测试异常 (" + device + "): " + ex.Message);
- return false;
- }
- }
- }
- private struct WordBitSnapshot
- {
- public int WordStart;
- public int BitIndex;
- public bool BitOn;
- public string WordHex;
- }
- private WordBitSnapshot ReadWordBitSnapshot(DeviceAddress addr)
- {
- int wordStart = (addr.Address / 16) * 16;
- int bitIndex = addr.Address % 16;
- short[] words = ReadWords(addr.Code, wordStart, 1);
- int word = words[0] & 0xFFFF;
- return new WordBitSnapshot
- {
- WordStart = wordStart,
- BitIndex = bitIndex,
- BitOn = (word & (1 << bitIndex)) != 0,
- WordHex = word.ToString("X4")
- };
- }
- private static string DiagnoseBitTestFailure(
- bool original, byte rawOriginal,
- bool target, byte rawFlipped, WordBitSnapshot snapFlipped,
- bool restored, byte rawRestored)
- {
- if (target && (rawFlipped == 0x10 || rawFlipped == 0x01) && snapFlipped.BitOn)
- return "位读写实际成功,请检查测试判定逻辑";
- if (target && rawOriginal == 0x00 && rawFlipped == 0x00 && !snapFlipped.BitOn)
- return "位写后位读/字读均为 OFF — 可能原因: (1) PLC 梯形图强制清 M101 "
- + "(2) 位写报文未被 PLC 接受 (3) 未允许 RUN 中写入;请用 Hsl 对同一地址 Write/Read 对照";
- if (target && !snapFlipped.BitOn && (rawFlipped == 0x10 || rawFlipped == 0x01))
- return "位读为 ON 但字读为 OFF — 位/字地址映射或读路径异常";
- if (target && snapFlipped.BitOn && rawFlipped == 0x00)
- return "字读为 ON 但位读为 OFF — 位读响应解析可能有问题";
- if (restored != original)
- return "恢复失败,PLC 可能在持续改写该位";
- return "未知,请对照步骤2完整帧与 HslCommunication 抓包";
- }
- private static string FormatHex(byte[] data)
- {
- return BitConverter.ToString(data).Replace("-", " ");
- }
- public short[] ReadWords(string device, int count)
- {
- DeviceAddress addr = ParseDevice(device);
- return ReadWords(addr.Code, addr.Address, count);
- }
- public void WriteWords(string device, short[] values)
- {
- DeviceAddress addr = ParseDevice(device);
- WriteWords(addr.Code, addr.Address, values);
- }
- public void WriteDevice(string device, short value)
- {
- WriteWords(device, new[] { value });
- }
- public void WriteDeviceBlock(string startDevice, short[] values)
- {
- WriteWords(startDevice, values);
- }
- /// <summary>从字软元件读取指定字节数(低字节在前)。</summary>
- public byte[] ReadDeviceBytes(string device, int byteCount)
- {
- if (byteCount <= 0)
- return new byte[0];
- int wordCount = (byteCount + 1) / 2;
- short[] words = ReadWords(device, wordCount);
- byte[] all = WordsToBytes(words);
- if (all.Length <= byteCount)
- return all;
- var trimmed = new byte[byteCount];
- Array.Copy(all, trimmed, byteCount);
- return trimmed;
- }
- public void WriteBit(string device, bool value)
- {
- if (!IsDeviceEnabled(device))
- return;
- DeviceAddress addr = ParseDevice(device);
- WriteBits(addr.Code, addr.Address, new[] { value });
- }
- public bool ReadBit(string device)
- {
- if (!IsDeviceEnabled(device))
- throw new ArgumentException("位地址未启用: " + device);
- DeviceAddress addr = ParseDevice(device);
- return ReadBits(addr.Code, addr.Address, 1)[0];
- }
- public bool[] ReadBits(byte deviceCode, int address, int count)
- {
- if (count <= 0)
- throw new ArgumentOutOfRangeException(nameof(count));
- byte[] requestData = BuildRequestBody(
- command: 0x0401,
- subCommand: 0x0001,
- deviceCode: deviceCode,
- address: address,
- points: count);
- byte[] response = SendRequest(requestData);
- if (response.Length < 2 + count)
- throw new InvalidOperationException("PLC 位读响应数据长度不足");
- var result = new bool[count];
- for (int i = 0; i < count; i++)
- result[i] = IsMcBitOn(response[2 + i]);
- return result;
- }
- /// <summary>MC 协议位读响应:00H=OFF;01H 或 10H 均表示 ON。</summary>
- private static bool IsMcBitOn(byte value)
- {
- return value == 0x01 || value == 0x10;
- }
- /// <summary>MC 协议位写请求(1401/0001):00H=OFF,10H=ON。</summary>
- private static byte ToMcBitValue(bool value)
- {
- return value ? (byte)0x10 : (byte)0x00;
- }
- private byte ReadBitRaw(DeviceAddress addr)
- {
- byte[] requestData = BuildRequestBody(
- command: 0x0401,
- subCommand: 0x0001,
- deviceCode: addr.Code,
- address: addr.Address,
- points: 1);
- byte[] response = SendRequest(requestData);
- if (response.Length < 3)
- throw new InvalidOperationException("PLC 位读响应数据长度不足");
- return response[2];
- }
- public short[] ReadWords(byte deviceCode, int address, int count)
- {
- if (count <= 0)
- throw new ArgumentOutOfRangeException(nameof(count));
- byte[] requestData = BuildRequestBody(
- command: 0x0401,
- subCommand: 0x0000,
- deviceCode: deviceCode,
- address: address,
- points: count);
- byte[] response = SendRequest(requestData);
- return ParseWordReadResponse(response, count);
- }
- public void WriteWords(byte deviceCode, int address, short[] values)
- {
- if (values == null || values.Length == 0)
- throw new ArgumentException("写入数据不能为空", nameof(values));
- byte[] requestData = BuildRequestBody(
- command: 0x1401,
- subCommand: 0x0000,
- deviceCode: deviceCode,
- address: address,
- points: values.Length,
- writeData: WordsToBytes(values));
- SendRequest(requestData);
- }
- public void WriteBits(byte deviceCode, int address, bool[] values)
- {
- if (values == null || values.Length == 0)
- throw new ArgumentException("写入数据不能为空", nameof(values));
- // 二进制位写入:每点 1 字节,00H=OFF / 10H=ON
- var data = new byte[values.Length];
- for (int i = 0; i < values.Length; i++)
- data[i] = ToMcBitValue(values[i]);
- byte[] requestData = BuildRequestBody(
- command: 0x1401,
- subCommand: 0x0001,
- deviceCode: deviceCode,
- address: address,
- points: values.Length,
- writeData: data);
- SendRequest(requestData);
- }
- /// <summary>
- /// 请求数据:监视定时器 + 指令 + 子指令 + 起始软元件(3) + 软元件代码(1) + 点数 + [写数据]
- /// 注意:二进制 3E 帧中「地址在前、软元件代码在后」。
- /// </summary>
- private static byte[] BuildRequestBody(ushort command, ushort subCommand, byte deviceCode, int address, int points, byte[] writeData = null)
- {
- using (var ms = new MemoryStream())
- using (var writer = new BinaryWriter(ms))
- {
- writer.Write((ushort)0x0010); // 监视定时器:0x10 * 250ms = 4s
- writer.Write(command);
- writer.Write(subCommand);
- // 起始软元件编号:3 字节,低字节在前
- writer.Write((byte)(address & 0xFF));
- writer.Write((byte)((address >> 8) & 0xFF));
- writer.Write((byte)((address >> 16) & 0xFF));
- // 软元件代码
- writer.Write(deviceCode);
- writer.Write((ushort)points);
- if (writeData != null && writeData.Length > 0)
- writer.Write(writeData);
- return ms.ToArray();
- }
- }
- private byte[] BuildFrame(byte[] requestData)
- {
- using (var ms = new MemoryStream())
- using (var writer = new BinaryWriter(ms))
- {
- writer.Write((ushort)0x0050); // 请求副头部
- writer.Write(NetworkNo);
- writer.Write(PcNo);
- writer.Write(IoModuleNo); // 小端 0x03FF -> FF 03
- writer.Write(StationNo);
- writer.Write((ushort)requestData.Length);
- writer.Write(requestData);
- return ms.ToArray();
- }
- }
- private byte[] SendRequest(byte[] requestData)
- {
- if (!IsConnected || _stream == null)
- throw new InvalidOperationException("PLC 未连接");
- try
- {
- lock (_sync)
- {
- byte[] frame = BuildFrame(requestData);
- _stream.Write(frame, 0, frame.Length);
- _stream.Flush();
- // 响应头固定 9 字节(不是 11!多读会吃掉结束码,导致后续读超时)
- byte[] header = ReadExact(McHeaderLength);
- if (header[0] != 0xD0 || header[1] != 0x00)
- {
- throw new InvalidOperationException(
- "PLC 响应副头部无效: " + BitConverter.ToString(header));
- }
- int dataLength = header[7] | (header[8] << 8);
- if (dataLength < 2)
- throw new InvalidOperationException("PLC 响应数据长度无效: " + dataLength);
- byte[] body = ReadExact(dataLength);
- ushort endCode = (ushort)(body[0] | (body[1] << 8));
- if (endCode != 0)
- throw new InvalidOperationException("PLC 返回错误码: 0x" + endCode.ToString("X4")
- + " (" + DescribeEndCode(endCode) + ")");
- return body;
- }
- }
- catch (Exception ex) when (IsTransportFailure(ex))
- {
- OnLog?.Invoke("PLC 通讯中断: " + ex.Message);
- NotifyConnectionLost();
- throw;
- }
- }
- private void NotifyConnectionLost()
- {
- if (_client == null)
- return;
- Disconnect();
- ConnectionLost?.Invoke();
- }
- private static bool IsTransportFailure(Exception ex)
- {
- if (ex is IOException || ex is SocketException || ex is ObjectDisposedException)
- return true;
- if (ex is InvalidOperationException && ex.Message.Contains("连接已断开"))
- return true;
- return ex.InnerException != null && IsTransportFailure(ex.InnerException);
- }
- private static string DescribeEndCode(ushort endCode)
- {
- switch (endCode)
- {
- case 0xC050: return "不允许在 RUN 中写入,请在 PLC 以太网参数中勾选允许 RUN 中写入";
- case 0xC051: return "请求数据长度错误";
- case 0xC056: return "指令/子指令不支持";
- case 0xC059: return "指令格式错误(请检查软元件代码/地址顺序)";
- case 0xC05C: return "请求内容错误";
- case 0xC061: return "请求数据长度与实际不符";
- default: return "详见三菱 MC 协议手册结束代码";
- }
- }
- private short[] ParseWordReadResponse(byte[] body, int count)
- {
- if (body.Length < 2 + count * 2)
- throw new InvalidOperationException("PLC 读响应数据长度不足");
- var result = new short[count];
- int offset = 2;
- for (int i = 0; i < count; i++)
- {
- result[i] = (short)(body[offset] | (body[offset + 1] << 8));
- offset += 2;
- }
- return result;
- }
- private byte[] ReadExact(int length)
- {
- var buffer = new byte[length];
- int read = 0;
- while (read < length)
- {
- int n = _stream.Read(buffer, read, length - read);
- if (n <= 0)
- throw new IOException("PLC 连接已断开");
- read += n;
- }
- return buffer;
- }
- private static short[] BytesToWords(byte[] data)
- {
- int wordCount = (data.Length + 1) / 2;
- var words = new short[wordCount];
- for (int i = 0; i < wordCount; i++)
- {
- int idx = i * 2;
- byte low = data[idx];
- byte high = idx + 1 < data.Length ? data[idx + 1] : (byte)0;
- words[i] = (short)(low | (high << 8));
- }
- return words;
- }
- private static byte[] WordsToBytes(short[] words)
- {
- var data = new byte[words.Length * 2];
- for (int i = 0; i < words.Length; i++)
- {
- data[i * 2] = (byte)(words[i] & 0xFF);
- data[i * 2 + 1] = (byte)((words[i] >> 8) & 0xFF);
- }
- return data;
- }
- private struct DeviceAddress
- {
- public byte Code;
- public int Address;
- }
- private static bool TryParseDevice(string device, out DeviceAddress result)
- {
- result = default(DeviceAddress);
- try
- {
- result = ParseDevice(device);
- return true;
- }
- catch
- {
- return false;
- }
- }
- private static DeviceAddress ParseDevice(string device)
- {
- if (string.IsNullOrWhiteSpace(device))
- throw new ArgumentException("软元件名称不能为空", nameof(device));
- var match = Regex.Match(device.Trim(), @"^([A-Za-z]+)(\d+)$");
- if (!match.Success)
- throw new ArgumentException("无法解析软元件地址: " + device, nameof(device));
- string type = match.Groups[1].Value.ToUpperInvariant();
- string number = match.Groups[2].Value;
- // M/L/F/V/S/SM/SD/D/R/ZR:十进制;X/Y:GX Works 八进制;B/W/SB/SW:十六进制
- int address;
- if (type == "X" || type == "Y")
- address = Convert.ToInt32(number, 8);
- else if (type == "B" || type == "W" || type == "SB" || type == "SW")
- address = Convert.ToInt32(number, 16);
- else
- address = int.Parse(number);
- byte code;
- if (!DeviceCodes.TryGetValue(type, out code))
- throw new ArgumentException("不支持的软元件类型: " + type, nameof(device));
- return new DeviceAddress { Code = code, Address = address };
- }
- private static readonly Dictionary<string, byte> DeviceCodes = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase)
- {
- { "SM", 0x91 },
- { "SD", 0xA9 },
- { "M", 0x90 },
- { "L", 0x92 },
- { "F", 0x93 },
- { "V", 0x94 },
- { "S", 0x98 },
- { "X", 0x9C },
- { "Y", 0x9D },
- { "B", 0xA0 },
- { "D", 0xA8 },
- { "W", 0xB4 },
- { "R", 0xAF },
- { "ZR", 0xB0 }
- };
- public void Dispose()
- {
- Disconnect();
- GC.SuppressFinalize(this);
- }
- }
- public enum PlcHeartbeatKind
- {
- Disabled,
- Bit,
- Word
- }
- /// <summary>
- /// RFID 与 PLC 握手寄存器映射。
- /// 流程:PLC 置 Ready → PC 读端口号并读卡 → 写数据 → 写 OK/NG → 写 Receive
- /// → 等 Ready 关闭 → PC 仅清 Receive。
- /// 地址填 "0" 表示不使用该软元件。
- /// </summary>
- public class PlcRfidMapping
- {
- /// <summary>RFID 原始数据起始寄存器(PC 写入)</summary>
- public string DataStartDevice { get; set; } = "D110";
- /// <summary>端口号寄存器(PLC 写入,PC 读取)</summary>
- public string PortDevice { get; set; } = "D101";
- /// <summary>数据质量(PC 写入,可选)</summary>
- public string QualityDevice { get; set; } = "D102";
- /// <summary>Ready 位(PLC→PC 请求读卡)</summary>
- public string ReadyBitDevice { get; set; } = "M100";
- /// <summary>读 OK 位(PC→PLC)</summary>
- public string OkBitDevice { get; set; } = "0";
- /// <summary>读 NG 位(PC→PLC)</summary>
- public string NgBitDevice { get; set; } = "0";
- /// <summary>Receive 位(PC→PLC,读卡完成后置 1)</summary>
- public string ReceiveBitDevice { get; set; } = "0";
- /// <summary>最大转发字节数</summary>
- public int MaxDataBytes { get; set; } = 32;
- /// <summary>单次读卡超时(毫秒)</summary>
- public int ReadTimeoutMs { get; set; } = 2000;
- }
- /// <summary>
- /// SFIS 过站/上传 PLC 回写映射(PC→PLC)。
- /// 流程:信号上升沿触发 SFIS → 写 OK/NG + Receive → 信号关闭后仅清 Receive。
- /// </summary>
- public class PlcSfisFeedback
- {
- public string PassOkBitDevice { get; set; } = "M106";
- public string PassNgBitDevice { get; set; } = "M107";
- public string PassReceiveBitDevice { get; set; } = "M108";
- public string UploadOkBitDevice { get; set; } = "M109";
- public string UploadNgBitDevice { get; set; } = "M110";
- public string UploadReceiveBitDevice { get; set; } = "M111";
- }
- }
|