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";
}
}