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 { /// /// 三菱 PLC MC 协议客户端(3E 帧 / 二进制 / TCP) /// 适用于 Q/L/iQ-R/FX5 等系列以太网通讯 /// 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 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; } } /// /// 读 Ready 位(PLC→PC 请求读卡)。未配置则抛异常。 /// 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); } /// /// 读 PLC 指定的读写头端口号。 /// 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]; } } /// /// 读卡成功:写数据到起始地址,置 OK=1、NG=0。 /// 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" : "")); } } /// /// 读卡失败:置 OK=0、NG=1。 /// 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" : "")); } } /// /// 置 Receive=1,通知 PLC 本次读卡交互已完成。 /// 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); } /// /// Ready 关闭后仅清除 Receive(OK/NG 保持供 PLC 读取)。 /// public void ClearReceiveBit(PlcRfidMapping mapping) { if (mapping == null) throw new ArgumentNullException(nameof(mapping)); lock (_sync) { if (IsDeviceEnabled(mapping.ReceiveBitDevice)) WriteBit(mapping.ReceiveBitDevice, false); } } /// /// SFIS 过站/上传完成:写 OK/NG + Receive=1(OK/NG 不清除,仅清 Receive)。 /// 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)); } } /// SFIS 信号关闭后仅清除对应 Receive。 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; /// 连接后重置心跳内部计数/翻转状态。 public void ResetHeartbeatState() { lock (_sync) { _heartbeatBitOn = false; _heartbeatWord = 0; } } /// /// 根据地址类型发送一次心跳:M 等位元件翻转 bool,D 等字元件递增数值。 /// 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); } } } /// 解析心跳模式;地址填 0 返回 Disabled。 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; } } /// /// 软元件是否启用:空、空白或 "0" 表示不使用。 /// public static bool IsDeviceEnabled(string device) { if (string.IsNullOrWhiteSpace(device)) return false; return device.Trim() != "0"; } /// /// 连接后自检:写/读一个字,确认 MC 帧交互正常。 /// 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; } } } /// /// 连接后自检:对位软元件执行 读→写反→读→恢复,并输出 MC 报文与字读交叉验证。 /// 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); } /// 从字软元件读取指定字节数(低字节在前)。 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; } /// MC 协议位读响应:00H=OFF;01H 或 10H 均表示 ON。 private static bool IsMcBitOn(byte value) { return value == 0x01 || value == 0x10; } /// MC 协议位写请求(1401/0001):00H=OFF,10H=ON。 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); } /// /// 请求数据:监视定时器 + 指令 + 子指令 + 起始软元件(3) + 软元件代码(1) + 点数 + [写数据] /// 注意:二进制 3E 帧中「地址在前、软元件代码在后」。 /// 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 DeviceCodes = new Dictionary(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 } /// /// RFID 与 PLC 握手寄存器映射。 /// 流程:PLC 置 Ready → PC 读端口号并读卡 → 写数据 → 写 OK/NG → 写 Receive /// → 等 Ready 关闭 → PC 仅清 Receive。 /// 地址填 "0" 表示不使用该软元件。 /// public class PlcRfidMapping { /// RFID 原始数据起始寄存器(PC 写入) public string DataStartDevice { get; set; } = "D110"; /// 端口号寄存器(PLC 写入,PC 读取) public string PortDevice { get; set; } = "D101"; /// 数据质量(PC 写入,可选) public string QualityDevice { get; set; } = "D102"; /// Ready 位(PLC→PC 请求读卡) public string ReadyBitDevice { get; set; } = "M100"; /// 读 OK 位(PC→PLC) public string OkBitDevice { get; set; } = "0"; /// 读 NG 位(PC→PLC) public string NgBitDevice { get; set; } = "0"; /// Receive 位(PC→PLC,读卡完成后置 1) public string ReceiveBitDevice { get; set; } = "0"; /// 最大转发字节数 public int MaxDataBytes { get; set; } = 32; /// 单次读卡超时(毫秒) public int ReadTimeoutMs { get; set; } = 2000; } /// /// SFIS 过站/上传 PLC 回写映射(PC→PLC)。 /// 流程:信号上升沿触发 SFIS → 写 OK/NG + Receive → 信号关闭后仅清 Receive。 /// 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"; } }