PLC通讯开发实战
大约 18 分钟约 5284 字
PLC通讯开发实战
简介
PLC(可编程逻辑控制器)是工业自动化的核心设备,与 PLC 通信是工业软件开发的基础能力。不同品牌的 PLC 使用不同的通信协议,掌握主流 PLC 的通讯方式对于工业上位机开发至关重要。本文将介绍如何使用 .NET/C# 与西门子、三菱、欧姆龙等主流 PLC 进行数据交互,并集成到 WPF 实时监控界面中。
特点
常见 PLC 通讯协议
| 协议名称 | 适用品牌 | 传输层 | .NET 库 | 特点 |
|---|---|---|---|---|
| S7 Protocol | 西门子 | TCP (Port 102) | S7.Net Plus | 最常用,支持 S7-200/300/400/1200/1500 |
| MC Protocol (SLMP) | 三菱 | TCP/UDP | HslCommunication | 支持Q系列、FX系列 |
| Fins Protocol | 欧姆龙 | TCP/UDP (Port 9600) | HslCommunication | 支持CP/CJ/CS系列 |
| Modbus TCP | 通用 | TCP (Port 502) | NModbus | 通用性强,多品牌支持 |
| MC Protocol (Binary) | 三菱 | TCP | MCProtocol | 二进制帧格式 |
| EtherNet/IP (CIP) | AB (罗克韦尔) | TCP | libplctag | 支持ControlLogix/CompactLogix |
西门子 S7 通讯
安装 NuGet 包
Install-Package S7netplusS7 连接基础
/// <summary>
/// 西门子 PLC 通讯管理器
/// 支持 S7-200 Smart、S7-300、S7-400、S7-1200、S7-1500
/// </summary>
public class SiemensPlcService : IDisposable
{
private Plc _plc;
private readonly SiemensPlcConfig _config;
private readonly ILogger<SiemensPlcService> _logger;
private readonly object _lock = new();
public bool IsConnected => _plc?.IsConnected ?? false;
public SiemensPlcService(SiemensPlcConfig config, ILogger<SiemensPlcService> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 建立 PLC 连接
/// </summary>
public async Task<bool> ConnectAsync()
{
try
{
// CpuType 根据实际 PLC 型号选择
// S7-200 Smart: CpuType.S71200
// S7-300: CpuType.S7300
// S7-1200: CpuType.S71200
// S7-1500: CpuType.S71500
_plc = new Plc(
_config.CpuType,
_config.IpAddress,
_config.Rack,
_config.Slot
);
await _plc.OpenAsync();
_logger.LogInformation("PLC连接成功:{Ip}", _config.IpAddress);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "PLC连接失败:{Ip}", _config.IpAddress);
return false;
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
if (_plc?.IsConnected == true)
{
_plc.Close();
}
}
public void Dispose()
{
Disconnect();
_plc?.Dispose();
}
}
/// <summary>
/// 西门子 PLC 配置
/// </summary>
public class SiemensPlcConfig
{
public CpuType CpuType { get; set; } = CpuType.S71200;
public string IpAddress { get; set; } = "192.168.1.100";
public short Rack { get; set; } = 0;
public short Slot { get; set; } = 1; // S7-1200/1500 默认Slot=1
}读写 DB 块数据
/// <summary>
/// S7 数据读写操作
/// 西门子 PLC 中数据存储区域:
/// DB 块(数据块)— 用户自定义数据区,最常用
/// I 区(输入)— 数字量/模拟量输入映像
/// Q 区(输出)— 数字量/模拟量输出映像
/// M 区(标记)— 内部标记存储区
/// T 区(定时器)— 定时器
/// C 区(计数器)— 计数器
/// </summary>
public class SiemensDataReader
{
private readonly Plc _plc;
private readonly ILogger _logger;
public SiemensDataReader(Plc plc, ILogger logger)
{
_plc = plc;
_logger = logger;
}
// ===== 读取单个变量 =====
/// <summary>读取DB块中的Bool值</summary>
public async Task<bool> ReadBoolAsync(int dbNumber, int offset, byte bitIndex)
{
// 地址示例:DB1.DBX0.0 表示 DB1 块,偏移0,第0位
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.Bit, 1, (byte)bitIndex);
return (bool)result;
}
/// <summary>读取DB块中的Byte</summary>
public async Task<byte> ReadByteAsync(int dbNumber, int offset)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.Byte, 1);
return (byte)result;
}
/// <summary>读取DB块中的Int16(西门子INT)</summary>
public async Task<short> ReadInt16Async(int dbNumber, int offset)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.Int, 1);
return (short)result;
}
/// <summary>读取DB块中的UInt16(西门子WORD)</summary>
public async Task<ushort> ReadUInt16Async(int dbNumber, int offset)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.Word, 1);
return (ushort)result;
}
/// <summary>读取DB块中的Int32(西门子DINT)</summary>
public async Task<int> ReadInt32Async(int dbNumber, int offset)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.DInt, 1);
return (int)result;
}
/// <summary>读取DB块中的Real(32位浮点数)</summary>
public async Task<float> ReadRealAsync(int dbNumber, int offset)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.Real, 1);
return (float)result;
}
/// <summary>读取DB块中的字符串</summary>
public async Task<string> ReadStringAsync(int dbNumber, int offset, int maxLength = 254)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, offset, VarType.String, maxLength);
return result?.ToString() ?? string.Empty;
}
// ===== 写入操作 =====
/// <summary>写入Bool值</summary>
public async Task WriteBoolAsync(int dbNumber, int offset, byte bitIndex, bool value)
{
await _plc.WriteAsync(DataType.DataBlock, dbNumber, offset, value, (byte)bitIndex);
}
/// <summary>写入Int16值</summary>
public async Task WriteInt16Async(int dbNumber, int offset, short value)
{
await _plc.WriteAsync(DataType.DataBlock, dbNumber, offset, value);
}
/// <summary>写入Real值</summary>
public async Task WriteRealAsync(int dbNumber, int offset, float value)
{
await _plc.WriteAsync(DataType.DataBlock, dbNumber, offset, value);
}
/// <summary>写入字符串</summary>
public async Task WriteStringAsync(int dbNumber, int offset, string value)
{
await _plc.WriteAsync(DataType.DataBlock, dbNumber, offset, value);
}
}批量读取数据
/// <summary>
/// 批量读取多个地址的数据
/// 使用 ReadMultipleVars 提高通信效率
/// </summary>
public async Task<Dictionary<string, object>> ReadMultipleVarsAsync(List<DataAddress> addresses)
{
var results = new Dictionary<string, object>();
try
{
// 构建批量读取地址列表
var items = addresses.Select(a => new DataItem
{
DataType = DataType.DataBlock,
DB = a.DbNumber,
StartByteAdr = a.Offset,
Count = a.Length,
VarType = a.VarType
}).ToList();
// 批量读取
await _plc.ReadMultipleVarsAsync(items);
// 解析结果
for (int i = 0; i < addresses.Count; i++)
{
results[addresses[i].Name] = items[i].Value;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "批量读取失败");
}
return results;
}
/// <summary>
/// 数据地址定义
/// </summary>
public class DataAddress
{
public string Name { get; set; }
public int DbNumber { get; set; }
public int Offset { get; set; }
public VarType VarType { get; set; }
public int Length { get; set; } = 1;
}读取完整 DB 块结构
/// <summary>
/// 读取 DB 块中的结构化数据
/// 例如 PLC 中定义了 UDT(用户自定义类型)
/// </summary>
public class PlcDataBlock
{
private readonly Plc _plc;
public PlcDataBlock(Plc plc)
{
_plc = plc;
}
/// <summary>
/// 读取整个 DB 块的原始字节数据
/// </summary>
public async Task<byte[]> ReadRawBytesAsync(int dbNumber, int startOffset, int length)
{
var result = await _plc.ReadAsync(DataType.DataBlock, dbNumber, startOffset, VarType.Byte, (ushort)length);
return (byte[])result;
}
/// <summary>
/// 写入原始字节数据到 DB 块
/// </summary>
public async Task WriteRawBytesAsync(int dbNumber, int startOffset, byte[] data)
{
await _plc.WriteBytesAsync(DataType.DataBlock, dbNumber, startOffset, data);
}
/// <summary>
/// 从字节数组中解析结构体
/// 注意:西门子 PLC 使用大端序(Big-Endian)
/// </summary>
public static T ParseStruct<T>(byte[] bytes, int offset = 0) where T : struct
{
// S7.Net Plus 内部已处理字节序问题
var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset, typeof(T));
}
finally
{
handle.Free();
}
}
}三菱 PLC 通讯
MC 协议通讯
/// <summary>
/// 三菱 PLC 通讯示例 — 使用 HslCommunication 库
/// Install-Package HslCommunication
/// 支持 MC 协议(SLMP)通信
/// 适用型号:Q系列、QnA系列、FX系列(需以太网模块)
/// </summary>
public class MitsubishiPlcService : IDisposable
{
private MelsecMcNet _melsecNet;
private readonly MitsubishiPlcConfig _config;
private readonly ILogger<MitsubishiPlcService> _logger;
public bool IsConnected { get; private set; }
public MitsubishiPlcService(MitsubishiPlcConfig config, ILogger<MitsubishiPlcService> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 连接三菱 PLC
/// </summary>
public OperateResult Connect()
{
_melsecNet = new MelsecMcNet(_config.IpAddress, _config.Port);
// 网络号和站号(通常默认即可)
_melsecNet.NetworkNumber = _config.NetworkNumber; // 0x00
_melsecNet.NetworkStationNumber = _config.StationNumber; // 0x00
// 超时设置
_melsecNet.ConnectTimeout = 5000;
var result = _melsecNet.ConnectServer();
IsConnected = result.IsSuccess;
if (result.IsSuccess)
{
_logger.LogInformation("三菱PLC连接成功:{Ip}:{Port}", _config.IpAddress, _config.Port);
}
else
{
_logger.LogError("三菱PLC连接失败:{Message}", result.Message);
}
return result;
}
/// <summary>读取单个Bool(Y0、M0、X0等)</summary>
public OperateResult<bool> ReadBool(string address)
{
return _melsecNet.ReadBool(address);
}
/// <summary>写入单个Bool</summary>
public OperateResult WriteBool(string address, bool value)
{
return _melsecNet.Write(address, value);
}
/// <summary>读取16位整数</summary>
public OperateResult<short> ReadInt16(string address)
{
return _melsecNet.ReadInt16(address);
}
/// <summary>读取32位浮点数</summary>
public OperateResult<float> ReadFloat(string address)
{
return _melsecNet.ReadFloat(address);
}
/// <summary>写入32位浮点数</summary>
public OperateResult WriteFloat(string address, float value)
{
return _melsecNet.Write(address, value);
}
/// <summary>批量读取</summary>
public OperateResult<byte[]> ReadBatch(string address, ushort length)
{
return _melsecNet.Read(address, length);
}
/// <summary>
/// 批量读取多个地址(支持不同区域的混合读取)
/// </summary>
public OperateResult<bool[]> ReadBoolBatch(string address, ushort length)
{
return _melsecNet.ReadBool(address, length);
}
public void Dispose()
{
if (IsConnected)
{
_melsecNet.ConnectClose();
}
_melsecNet?.Dispose();
}
}
public class MitsubishiPlcConfig
{
public string IpAddress { get; set; } = "192.168.1.10";
public int Port { get; set; } = 6000; // MC协议默认端口
public byte NetworkNumber { get; set; } = 0x00;
public byte StationNumber { get; set; } = 0x00;
}三菱地址格式说明
| 区域 | 地址格式 | 说明 | 示例 |
|---|---|---|---|
| 输入继电器 | X | 外部输入 | X0、X10、X1F |
| 输出继电器 | Y | 外部输出 | Y0、Y10、Y1F |
| 内部继电器 | M | 内部标志 | M0、M100 |
| 锁存继电器 | L | 停电保持 | L0、L100 |
| 数据寄存器 | D | 16位数据 | D0、D100 |
| 文件寄存器 | R | 文件数据 | R0、R100 |
| 定时器 | T | 定时器 | T0、T100 |
| 计数器 | C | 计数器 | C0、C100 |
欧姆龙 Fins 协议通讯
/// <summary>
/// 欧姆龙 PLC 通讯示例 — Fins 协议
/// Install-Package HslCommunication
/// 适用型号:CP系列、CJ系列、CS系列、NX系列
/// </summary>
public class OmronPlcService : IDisposable
{
private OmronFinsNet _finsNet;
private readonly OmronPlcConfig _config;
private readonly ILogger<OmronPlcService> _logger;
public bool IsConnected { get; private set; }
public OmronPlcService(OmronPlcConfig config, ILogger<OmronPlcService> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 连接欧姆龙 PLC
/// </summary>
public OperateResult Connect()
{
_finsNet = new OmronFinsNet(_config.IpAddress, _config.Port);
// 设置 SA1(本机 Fins 节点号)和 DA1(目标 PLC 节点号)
_finsNet.SA1 = _config.LocalNode; // 本机节点号
_finsNet.DA1 = _config.RemoteNode; // PLC 节点号
_finsNet.DA2 = _config.RemoteUnit; // PLC 单元号
var result = _finsNet.ConnectServer();
IsConnected = result.IsSuccess;
if (result.IsSuccess)
{
_logger.LogInformation("欧姆龙PLC连接成功:{Ip}:{Port}", _config.IpAddress, _config.Port);
}
else
{
_logger.LogError("欧姆龙PLC连接失败:{Message}", result.Message);
}
return result;
}
/// <summary>读取Bool(CIO区域)</summary>
public OperateResult<bool> ReadBool(string address)
{
return _finsNet.ReadBool(address);
}
/// <summary>读取16位整数</summary>
public OperateResult<short> ReadInt16(string address)
{
return _finsNet.ReadInt16(address);
}
/// <summary>读取32位浮点数</summary>
public OperateResult<float> ReadFloat(string address)
{
return _finsNet.ReadFloat(address);
}
/// <summary>写入Bool</summary>
public OperateResult WriteBool(string address, bool value)
{
return _finsNet.Write(address, value);
}
/// <summary>写入16位整数</summary>
public OperateResult WriteInt16(string address, short value)
{
return _finsNet.Write(address, value);
}
/// <summary>写入32位浮点数</summary>
public OperateResult WriteFloat(string address, float value)
{
return _finsNet.Write(address, value);
}
/// <summary>批量读取字数据</summary>
public OperateResult<byte[]> ReadBatch(string address, ushort length)
{
return _finsNet.Read(address, length);
}
public void Dispose()
{
if (IsConnected)
{
_finsNet.ConnectClose();
}
_finsNet?.Dispose();
}
}
public class OmronPlcConfig
{
public string IpAddress { get; set; } = "192.168.1.20";
public int Port { get; set; } = 9600; // Fins 默认端口
public byte LocalNode { get; set; } = 0x00;
public byte RemoteNode { get; set; } = 0x01;
public byte RemoteUnit { get; set; } = 0x00;
}欧姆龙地址格式说明
| 区域 | 地址前缀 | 说明 | 示例 |
|---|---|---|---|
| CIO | CIO 或无前缀 | 核心 I/O 区域 | CIO0.00、100 |
| Work | W | 工作区域 | W0.00、W100 |
| Holding | H | 保持继电器 | H0.00、H100 |
| Data Memory | D | 数据存储器 | D0、D100 |
| Timer | T | 定时器 | T0、T100 |
| Counter | C | 计数器 | C0、C100 |
通用通讯管理框架
统一接口抽象
/// <summary>
/// PLC 通讯统一接口 — 抽象不同品牌 PLC 的差异
/// </summary>
public interface IPlcCommunication : IDisposable
{
bool IsConnected { get; }
Task<bool> ConnectAsync();
void Disconnect();
Task<PlcReadResult<bool>> ReadBoolAsync(string address);
Task<PlcReadResult<short>> ReadInt16Async(string address);
Task<PlcReadResult<float>> ReadFloatAsync(string address);
Task<PlcReadResult<string>> ReadStringAsync(string address, int length);
Task<bool> WriteBoolAsync(string address, bool value);
Task<bool> WriteInt16Async(string address, short value);
Task<bool> WriteFloatAsync(string address, float value);
Task<bool> WriteStringAsync(string address, string value);
}
/// <summary>
/// PLC 读取结果
/// </summary>
public class PlcReadResult<T>
{
public bool Success { get; set; }
public T Value { get; set; }
public string ErrorMessage { get; set; }
public static PlcReadResult<T> Ok(T value) => new() { Success = true, Value = value };
public static PlcReadResult<T> Fail(string message) => new() { Success = false, ErrorMessage = message };
}自动重连机制
/// <summary>
/// PLC 通讯自动重连管理器
/// 断线自动重连 + 心跳检测
/// </summary>
public class PlcConnectionManager : IDisposable
{
private readonly IPlcCommunication _plc;
private readonly ILogger _logger;
private CancellationTokenSource _cts;
private readonly int _heartbeatInterval = 3000; // 心跳间隔(毫秒)
private readonly int _reconnectDelay = 5000; // 重连延迟
private int _failCount;
private const int MaxFailCount = 3;
public event EventHandler<bool> ConnectionStateChanged;
public event EventHandler<Exception> CommunicationError;
public PlcConnectionManager(IPlcCommunication plc, ILogger logger)
{
_plc = plc;
_logger = logger;
}
/// <summary>
/// 启动通讯管理和心跳检测
/// </summary>
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
while (!_cts.Token.IsCancellationRequested)
{
try
{
// 如果未连接,尝试连接
if (!_plc.IsConnected)
{
_logger.LogInformation("PLC未连接,尝试建立连接...");
var connected = await _plc.ConnectAsync();
ConnectionStateChanged?.Invoke(this, connected);
if (!connected)
{
_logger.LogWarning("PLC连接失败,{Delay}ms后重试", _reconnectDelay);
await Task.Delay(_reconnectDelay, _cts.Token);
continue;
}
}
// 心跳检测 — 读取一个已知地址的值
var result = await _plc.ReadInt16Async("Heartbeat");
if (result.Success)
{
_failCount = 0;
}
else
{
_failCount++;
_logger.LogWarning("心跳检测失败({Count}/{Max})", _failCount, MaxFailCount);
if (_failCount >= MaxFailCount)
{
_logger.LogWarning("连续心跳失败,断开连接准备重连");
_plc.Disconnect();
ConnectionStateChanged?.Invoke(this, false);
_failCount = 0;
}
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "通讯管理器异常");
CommunicationError?.Invoke(this, ex);
}
await Task.Delay(_heartbeatInterval, _cts.Token);
}
}
/// <summary>停止通讯管理</summary>
public void Stop()
{
_cts?.Cancel();
_plc?.Disconnect();
}
public void Dispose()
{
Stop();
_plc?.Dispose();
}
}数据解析工具
PLC 数据类型转换
/// <summary>
/// PLC 数据解析工具
/// 不同品牌 PLC 的字节序和数据编码可能不同
/// </summary>
public static class PlcDataParser
{
/// <summary>
/// 字节数组 → 16位整数(大端序)
/// 西门子 PLC 使用大端序
/// </summary>
public static short ToInt16BigEndian(byte[] bytes, int offset = 0)
{
if (BitConverter.IsLittleEndian)
{
return (short)((bytes[offset] << 8) | bytes[offset + 1]);
}
return BitConverter.ToInt16(bytes, offset);
}
/// <summary>
/// 字节数组 → 32位浮点数(大端序)
/// </summary>
public static float ToFloatBigEndian(byte[] bytes, int offset = 0)
{
var data = new byte[4];
if (BitConverter.IsLittleEndian)
{
data[0] = bytes[offset + 3];
data[1] = bytes[offset + 2];
data[2] = bytes[offset + 1];
data[3] = bytes[offset];
}
else
{
Array.Copy(bytes, offset, data, 0, 4);
}
return BitConverter.ToSingle(data, 0);
}
/// <summary>
/// 字节数组 → 16位整数(小端序)
/// 三菱、欧姆龙 PLC 通常使用小端序
/// </summary>
public static short ToInt16LittleEndian(byte[] bytes, int offset = 0)
{
return BitConverter.ToInt16(bytes, offset);
}
/// <summary>
/// 字节数组 → 32位浮点数(小端序)
/// </summary>
public static float ToFloatLittleEndian(byte[] bytes, int offset = 0)
{
return BitConverter.ToSingle(bytes, offset);
}
/// <summary>
/// BCD 码 → 整数
/// 部分设备使用 BCD 编码
/// </summary>
public static int BcdToInt(byte bcd)
{
return ((bcd >> 4) & 0x0F) * 10 + (bcd & 0x0F);
}
/// <summary>
/// 整数 → BCD 码
/// </summary>
public static byte IntToBcd(int value)
{
return (byte)(((value / 10) << 4) | (value % 10));
}
/// <summary>
/// 将字节数组解析为十六进制字符串(调试用)
/// </summary>
public static string ToHexString(byte[] bytes, int offset = 0, int length = -1)
{
if (length < 0) length = bytes.Length - offset;
return string.Join(" ", bytes.Skip(offset).Take(length).Select(b => b.ToString("X2")));
}
}WPF 实时监控仪表盘
ViewModel 实现
/// <summary>
/// PLC 实时监控 ViewModel
/// 支持多 PLC 同时监控
/// </summary>
public class PlcMonitorViewModel : ObservableObject, IDisposable
{
private readonly Dictionary<string, IPlcCommunication> _plcConnections = new();
private readonly ILogger _logger;
private CancellationTokenSource _cts;
// 连接状态
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private string _connectionStatus = "未连接";
public string ConnectionStatus
{
get => _connectionStatus;
set => SetProperty(ref _connectionStatus, value);
}
// 监控数据点
public ObservableCollection<MonitorPoint> MonitorPoints { get; } = new();
// 告警记录
public ObservableCollection<AlarmRecord> Alarms { get; } = new();
// 命令
public IAsyncRelayCommand ConnectCommand { get; }
public IAsyncRelayCommand DisconnectCommand { get; }
public IAsyncRelayCommand StartMonitorCommand { get; }
public IAsyncRelayCommand StopMonitorCommand { get; }
public PlcMonitorViewModel(IPlcCommunication plc, ILogger logger)
{
_plcConnections["PLC1"] = plc;
_logger = logger;
ConnectCommand = new AsyncRelayCommand(ConnectAsync, () => !IsConnected);
DisconnectCommand = new AsyncRelayCommand(DisconnectAsync, () => IsConnected);
StartMonitorCommand = new AsyncRelayCommand(StartMonitorAsync, () => IsConnected);
StopMonitorCommand = new AsyncRelayCommand(StopMonitorAsync);
InitializeMonitorPoints();
}
/// <summary>
/// 初始化监控数据点配置
/// </summary>
private void InitializeMonitorPoints()
{
MonitorPoints.Add(new MonitorPoint
{
Name = "主轴转速",
Address = "D100",
Unit = "RPM",
DataType = PlcDataType.Int16,
WarningLow = 100,
AlarmLow = 50,
AlarmHigh = 3000,
WarningHigh = 2500
});
MonitorPoints.Add(new MonitorPoint
{
Name = "切削温度",
Address = "D102",
Unit = "℃",
DataType = PlcDataType.Float,
WarningHigh = 80,
AlarmHigh = 100
});
MonitorPoints.Add(new MonitorPoint
{
Name = "液压压力",
Address = "D104",
Unit = "MPa",
DataType = PlcDataType.Float,
WarningLow = 5.0,
AlarmLow = 3.0,
WarningHigh = 20.0,
AlarmHigh = 25.0
});
MonitorPoints.Add(new MonitorPoint
{
Name = "运行状态",
Address = "M0",
Unit = "",
DataType = PlcDataType.Bool
});
}
/// <summary>
/// 周期性轮询所有监控点
/// </summary>
private async Task StartMonitorAsync()
{
_cts = new CancellationTokenSource();
try
{
while (!_cts.Token.IsCancellationRequested)
{
var plc = _plcConnections["PLC1"];
foreach (var point in MonitorPoints)
{
try
{
switch (point.DataType)
{
case PlcDataType.Bool:
var boolResult = await plc.ReadBoolAsync(point.Address);
if (boolResult.Success)
{
point.BoolValue = boolResult.Value;
point.DisplayValue = boolResult.Value ? "ON" : "OFF";
}
break;
case PlcDataType.Int16:
var intResult = await plc.ReadInt16Async(point.Address);
if (intResult.Success)
{
point.NumericValue = intResult.Value;
point.DisplayValue = $"{intResult.Value} {point.Unit}";
CheckAlarm(point);
}
break;
case PlcDataType.Float:
var floatResult = await plc.ReadFloatAsync(point.Address);
if (floatResult.Success)
{
point.NumericValue = floatResult.Value;
point.DisplayValue = $"{floatResult.Value:F1} {point.Unit}";
CheckAlarm(point);
}
break;
}
point.LastUpdateTime = DateTime.Now;
point.IsQualityGood = true;
}
catch (Exception ex)
{
point.IsQualityGood = false;
_logger.LogWarning(ex, "读取 {Name}({Address}) 失败", point.Name, point.Address);
}
}
await Task.Delay(500, _cts.Token); // 500ms 轮询周期
}
}
catch (OperationCanceledException) { }
}
/// <summary>
/// 检查是否触发告警
/// </summary>
private void CheckAlarm(MonitorPoint point)
{
var value = point.NumericValue;
if (point.AlarmHigh.HasValue && value > point.AlarmHigh.Value)
{
AddAlarm(point, AlarmLevel.Critical, $"{point.Name} 超高限:{value}{point.Unit} > {point.AlarmHigh}{point.Unit}");
}
else if (point.WarningHigh.HasValue && value > point.WarningHigh.Value)
{
AddAlarm(point, AlarmLevel.Warning, $"{point.Name} 超高警告:{value}{point.Unit} > {point.WarningHigh}{point.Unit}");
}
if (point.AlarmLow.HasValue && value < point.AlarmLow.Value)
{
AddAlarm(point, AlarmLevel.Critical, $"{point.Name} 超低限:{value}{point.Unit} < {point.AlarmLow}{point.Unit}");
}
else if (point.WarningLow.HasValue && value < point.WarningLow.Value)
{
AddAlarm(point, AlarmLevel.Warning, $"{point.Name} 超低警告:{value}{point.Unit} < {point.WarningLow}{point.Unit}");
}
}
private void AddAlarm(MonitorPoint point, AlarmLevel level, string message)
{
Alarms.Insert(0, new AlarmRecord
{
Timestamp = DateTime.Now,
Level = level,
PointName = point.Name,
Message = message
});
// 保留最近200条告警
while (Alarms.Count > 200) Alarms.RemoveAt(Alarms.Count - 1);
}
private async Task ConnectAsync()
{
var plc = _plcConnections["PLC1"];
IsConnected = await plc.ConnectAsync();
ConnectionStatus = IsConnected ? "已连接" : "连接失败";
}
private async Task DisconnectAsync()
{
_cts?.Cancel();
var plc = _plcConnections["PLC1"];
plc.Disconnect();
IsConnected = false;
ConnectionStatus = "已断开";
}
private async Task StopMonitorAsync()
{
_cts?.Cancel();
}
public void Dispose()
{
_cts?.Cancel();
foreach (var plc in _plcConnections.Values)
{
plc.Dispose();
}
}
}
public enum PlcDataType { Bool, Int16, Float, String }
public enum AlarmLevel { Info, Warning, Critical }
public class MonitorPoint : ObservableObject
{
public string Name { get; set; }
public string Address { get; set; }
public string Unit { get; set; }
public PlcDataType DataType { get; set; }
public double? WarningLow { get; set; }
public double? AlarmLow { get; set; }
public double? WarningHigh { get; set; }
public double? AlarmHigh { get; set; }
private string _displayValue;
public string DisplayValue { get => _displayValue; set => SetProperty(ref _displayValue, value); }
private double _numericValue;
public double NumericValue { get => _numericValue; set => SetProperty(ref _numericValue, value); }
private bool _boolValue;
public bool BoolValue { get => _boolValue; set => SetProperty(ref _boolValue, value); }
private DateTime _lastUpdateTime;
public DateTime LastUpdateTime { get => _lastUpdateTime; set => SetProperty(ref _lastUpdateTime, value); }
private bool _isQualityGood = true;
public bool IsQualityGood { get => _isQualityGood; set => SetProperty(ref _isQualityGood, value); }
}
public class AlarmRecord
{
public DateTime Timestamp { get; set; }
public AlarmLevel Level { get; set; }
public string PointName { get; set; }
public string Message { get; set; }
}WPF 监控界面
<!-- PLC 实时监控仪表盘 -->
<Window x:Class="PlcMonitor.DashboardWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="PLC实时监控仪表盘" Width="1200" Height="800">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="连接PLC" Command="{Binding ConnectCommand}" Margin="0,0,5,0" Padding="15,5"/>
<Button Content="断开" Command="{Binding DisconnectCommand}" Margin="0,0,5,0" Padding="15,5"/>
<Button Content="开始监控" Command="{Binding StartMonitorCommand}" Margin="0,0,5,0" Padding="15,5"/>
<Button Content="停止监控" Command="{Binding StopMonitorCommand}" Margin="0,0,5,0" Padding="15,5"/>
<Ellipse Width="12" Height="12" Margin="15,0,5,0" VerticalAlignment="Center"
Fill="{Binding IsConnected, Converter={StaticResource BoolToGreenRedConverter}}"/>
<TextBlock Text="{Binding ConnectionStatus}" VerticalAlignment="Center" FontWeight="Bold"/>
</StackPanel>
<!-- 监控数据面板 -->
<ItemsControl Grid.Row="1" ItemsSource="{Binding MonitorPoints}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="5" Padding="15" CornerRadius="8"
Background="{Binding IsQualityGood, Converter={StaticResource QualityToBgConverter}}">
<StackPanel>
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock Text="{Binding DisplayValue}" FontSize="24" Foreground="#1565C0"/>
<TextBlock Text="{Binding LastUpdateTime, StringFormat='更新: {0:HH:mm:ss}'}"
FontSize="11" Foreground="Gray" Margin="0,5,0,0"/>
<TextBlock Text="{Binding Address}" FontSize="11" Foreground="Gray"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 告警列表 -->
<DataGrid Grid.Row="2" ItemsSource="{Binding Alarms}" AutoGenerateColumns="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="时间" Binding="{Binding Timestamp, StringFormat={}{0:HH:mm:ss}}" Width="80"/>
<DataGridTextColumn Header="级别" Binding="{Binding Level}" Width="60"/>
<DataGridTextColumn Header="监控点" Binding="{Binding PointName}" Width="100"/>
<DataGridTextColumn Header="告警信息" Binding="{Binding Message}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>优点
缺点
总结
PLC 通讯开发是工业软件的核心能力。掌握西门子 S7 协议(S7.Net Plus)、三菱 MC 协议、欧姆龙 Fins 协议的通讯方式,理解不同 PLC 的地址格式和数据编码差异(特别是大端序/小端序),并通过统一的接口抽象实现品牌无关的通讯框架。结合 WPF 的数据绑定和 MVVM 模式,可以高效构建实时监控仪表盘,配合自动重连和告警机制保障系统稳定运行。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- 设备接入类主题通常同时涉及协议、线程、实时刷新和异常恢复。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 明确采集周期、重连策略、数据缓存和状态同步方式。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 只验证通信成功,不验证断线、抖动和异常包。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐设备模拟、离线回放、现场诊断和配置中心能力。
适用场景
- 当你准备把《PLC通讯开发实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《PLC通讯开发实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《PLC通讯开发实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《PLC通讯开发实战》最大的收益和代价分别是什么?
