WPF 数据采集网关系统设计与实现
一、系统概述
本系统是一个基于 WPF 的数据采集网关,支持主流 PLC(可编程逻辑控制器)的数据采集,并将采集到的数据汇总存储到数据库中。系统采用模块化设计,具有良好的扩展性和可维护性。
二、系统架构
1. 整体架构
+-------------------+
| WPF客户端 | <-- 用户界面
+-------------------+
|
v
+-------------------+
| 数据采集服务 | <-- 核心业务逻辑
+-------------------+
|
v
+-------------------+
| PLC通信模块 | <-- 支持多种PLC协议
+-------------------+
|
v
+-------------------+
| 数据库存储 | <-- 数据持久化
+-------------------+
2. 技术选型
前端界面:WPF(Windows Presentation Foundation)
PLC通信:OPC UA/DA、Modbus TCP/RTU、S7协议等
数据库:SQL Server/MySQL/PostgreSQL(根据需求选择)
通信框架:.NET Core/.NET Framework + 第三方PLC库(如libnodave、NModbus等)
三、核心模块设计
1. PLC通信模块
1.1 支持的PLC类型及协议
PLC品牌 | 支持协议 | 实现方式 |
---|---|---|
Siemens | S7(S7Comm) | 使用libnodave或S7.Net |
Siemens | OPC UA/DA | 使用OPC基金会库 |
Mitsubishi | MELSEC | 使用MelsecNet |
Omron | SYSMAC | 使用OmronPlc |
Allen-Bradley | ControlLogix | 使用RSLinx |
Modbus设备 | Modbus TCP/RTU | 使用NModbus |
1.2 接口设计
// IPlcCommunication.cs
public interface IPlcCommunication
{
bool Connect(string connectionString);
void Disconnect();
bool IsConnected { get; }
// 读取数据
Task<T> ReadAsync<T>(string address);
Task<IEnumerable<T>> ReadMultipleAsync<T>(IEnumerable<string> addresses);
// 写入数据
Task<bool> WriteAsync<T>(string address, T value);
}
1.3 具体实现示例(Siemens S7)
// SiemensS7Plc.cs
using Snap7;
using System;
using System.Threading.Tasks;
public class SiemensS7Plc : IPlcCommunication
{
private S7Client _client;
public bool Connect(string connectionString)
{
// 解析连接字符串,格式如:"IP=192.168.0.1;Rack=0;Slot=1"
var parameters = ParseConnectionString(connectionString);
_client = new S7Client();
int result = _client.ConnectTo(
parameters["IP"],
Convert.ToInt32(parameters["Rack"]),
Convert.ToInt32(parameters["Slot"]));
return result == 0; // 0表示成功
}
public async Task<bool> ReadBoolAsync(string address)
{
int dbNumber;
int byteOffset;
int bitOffset;
ParseAddress(address, out dbNumber, out byteOffset, out bitOffset);
int result = await Task.Run(() =>
_client.DBRead(dbNumber, byteOffset, 1, out byte[] buffer));
if (result != 0) return false;
return (buffer[0] & (1 << bitOffset)) != 0;
}
// 其他读写方法实现...
}
2. 数据采集服务
2.1 核心功能
定时采集PLC数据
数据缓存与处理
异常处理与重试机制
数据格式转换
2.2 实现示例
// PlcDataCollector.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class PlcDataCollector : IDisposable
{
private readonly List<IPlcCommunication> _plcs = new List<IPlcCommunication>();
private readonly ConcurrentDictionary<string, object> _dataCache = new ConcurrentDictionary<string, object>();
private readonly Timer _timer;
private readonly int _collectionIntervalMs = 1000; // 默认1秒
public event EventHandler<DataChangedEventArgs> DataChanged;
public PlcDataCollector(IEnumerable<IPlcCommunication> plcs, int collectionIntervalMs = 1000)
{
_plcs.AddRange(plcs);
_collectionIntervalMs = collectionIntervalMs;
_timer = new Timer(CollectData, null, 0, _collectionIntervalMs);
}
private void CollectData(object state)
{
try
{
var newData = new Dictionary<string, object>();
foreach (var plc in _plcs)
{
if (!plc.IsConnected) continue;
// 示例:读取多个地址的数据
var tags = new[] { "DB1.DBX0.0", "DB1.DBW2", "DB1.DBD4" };
foreach (var tag in tags)
{
try
{
// 根据数据类型调用不同的读取方法
if (tag.EndsWith("X")) // BOOL
{
var value = plc.ReadAsync<bool>(tag).Result;
newData[tag] = value;
}
else if (tag.EndsWith("W")) // INT
{
var value = plc.ReadAsync<int>(tag).Result;
newData[tag] = value;
}
else if (tag.EndsWith("D")) // REAL
{
var value = plc.ReadAsync<double>(tag).Result;
newData[tag] = value;
}
}
catch (Exception ex)
{
// 记录错误日志
LogError($"读取PLC数据失败: {tag}, 错误: {ex.Message}");
}
}
}
// 更新缓存并触发事件
foreach (var kvp in newData)
{
_dataCache[kvp.Key] = kvp.Value;
}
DataChanged?.Invoke(this, new DataChangedEventArgs(newData));
}
catch (Exception ex)
{
LogError($"数据采集失败: {ex.Message}");
}
}
public object GetData(string address)
{
return _dataCache.TryGetValue(address, out var value) ? value : null;
}
public void Dispose()
{
_timer?.Change(Timeout.Infinite, 0);
_timer?.Dispose();
foreach (var plc in _plcs)
{
plc.Disconnect();
}
}
// 日志记录方法...
}
3. 数据库存储模块
3.1 数据库设计
-- PLC数据表
CREATE TABLE PlcData (
Id INT PRIMARY KEY IDENTITY(1,1),
TagName NVARCHAR(100) NOT NULL,
DataType NVARCHAR(50) NOT NULL,
Value NVARCHAR(MAX) NOT NULL,
Timestamp DATETIME DEFAULT GETDATE(),
PlcId INT NOT NULL,
FOREIGN KEY (PlcId) REFERENCES Plcs(Id)
);
-- PLC设备表
CREATE TABLE Plcs (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL,
IpAddress NVARCHAR(50),
Port INT,
Protocol NVARCHAR(50),
Description NVARCHAR(200)
);
3.2 数据持久化实现
// DatabaseService.cs
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
public class DatabaseService : IDisposable
{
private readonly IDbConnection _db;
public DatabaseService(string connectionString)
{
_db = new SqlConnection(connectionString);
}
public async Task SavePlcDataAsync(IEnumerable<PlcDataModel> dataList)
{
using (var transaction = _db.BeginTransaction())
{
try
{
var sql = @"
INSERT INTO PlcData (TagName, DataType, Value, Timestamp, PlcId)
VALUES (@TagName, @DataType, @Value, @Timestamp, @PlcId)";
await _db.ExecuteAsync(sql, dataList, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
public async Task<IEnumerable<PlcDataModel>> GetRecentDataAsync(int maxRecords = 1000)
{
return await _db.QueryAsync<PlcDataModel>(
"SELECT TOP(@maxRecords) * FROM PlcData ORDER BY Timestamp DESC",
new { maxRecords });
}
public void Dispose()
{
_db?.Dispose();
}
}
public class PlcDataModel
{
public string TagName { get; set; }
public string DataType { get; set; }
public string Value { get; set; }
public DateTime Timestamp { get; set; }
public int PlcId { get; set; }
}
4. WPF 前端界面
4.1 主界面设计
<!-- MainWindow.xaml -->
<Window x:Class="PlcDataGateway.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PlcDataGateway"
mc:Ignorable="d"
Title="PLC数据采集网关" Height="600" Width="1000">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 工具栏 -->
<ToolBarTray Grid.Row="0">
<ToolBar>
<Button Content="连接PLC" Command="{Binding ConnectCommand}"/>
<Button Content="断开连接" Command="{Binding DisconnectCommand}"/>
<Separator/>
<Button Content="刷新数据" Command="{Binding RefreshDataCommand}"/>
<Separator/>
<ComboBox ItemsSource="{Binding PlcConnections}"
SelectedItem="{Binding SelectedPlcConnection}"
DisplayMemberPath="Name"/>
</ToolBar>
</ToolBarTray>
<!-- 数据显示区域 -->
<TabControl Grid.Row="1">
<TabItem Header="实时数据">
<DataGrid ItemsSource="{Binding RealTimeData}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="标签" Binding="{Binding TagName}"/>
<DataGridTextColumn Header="值" Binding="{Binding Value}"/>
<DataGridTextColumn Header="数据类型" Binding="{Binding DataType}"/>
<DataGridTextColumn Header="时间戳" Binding="{Binding Timestamp, StringFormat='yyyy-MM-dd HH:mm:ss.fff'}"/>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="历史数据">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<DataGrid Grid.Column="0" ItemsSource="{Binding HistoricalData}"
AutoGenerateColumns="False">
<!-- 列定义同上 -->
</DataGrid>
<StackPanel Grid.Column="1" VerticalAlignment="Top">
<DatePicker SelectedDate="{Binding HistoryStartDate}"/>
<DatePicker SelectedDate="{Binding HistoryEndDate}"/>
<Button Content="查询" Command="{Binding QueryHistoryCommand}"/>
</StackPanel>
</Grid>
</TabItem>
</TabControl>
<!-- 状态栏 -->
<StatusBar Grid.Row="2">
<StatusBarItem>
<TextBlock Text="{Binding StatusMessage}"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
4.2 ViewModel 实现
// MainViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;
public class MainViewModel : INotifyPropertyChanged
{
private readonly PlcDataCollector _collector;
private readonly DatabaseService _dbService;
private string _statusMessage = "就绪";
private ObservableCollection<RealTimeDataItem> _realTimeData;
private ObservableCollection<HistoricalDataItem> _historicalData;
private PlcConnection _selectedPlcConnection;
private DateTime? _historyStartDate;
private DateTime? _historyEndDate;
public event PropertyChangedEventHandler PropertyChanged;
public ICommand ConnectCommand { get; }
public ICommand DisconnectCommand { get; }
public ICommand RefreshDataCommand { get; }
public ICommand QueryHistoryCommand { get; }
public ObservableCollection<PlcConnection> PlcConnections { get; } =
new ObservableCollection<PlcConnection>();
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
OnPropertyChanged();
}
}
public ObservableCollection<RealTimeDataItem> RealTimeData
{
get => _realTimeData;
set
{
_realTimeData = value;
OnPropertyChanged();
}
}
public ObservableCollection<HistoricalDataItem> HistoricalData
{
get => _historicalData;
set
{
_historicalData = value;
OnPropertyChanged();
}
}
public PlcConnection SelectedPlcConnection
{
get => _selectedPlcConnection;
set
{
_selectedPlcConnection = value;
OnPropertyChanged();
}
}
public DateTime? HistoryStartDate
{
get => _historyStartDate;
set
{
_historyStartDate = value;
OnPropertyChanged();
}
}
public DateTime? HistoryEndDate
{
get => _historyEndDate;
set
{
_historyEndDate = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
// 初始化数据收集器
_collector = new PlcDataCollector(new[] { /* PLC实例 */ }, 1000);
_collector.DataChanged += OnDataChanged;
// 初始化数据库服务
_dbService = new DatabaseService("YourConnectionString");
// 初始化命令
ConnectCommand = new RelayCommand(ConnectPlc);
DisconnectCommand = new RelayCommand(DisconnectPlc, () => _collector.IsRunning);
RefreshDataCommand = new RelayCommand(RefreshData);
QueryHistoryCommand = new RelayCommand(QueryHistoryData);
// 加载PLC连接配置
LoadPlcConnections();
}
private void OnDataChanged(object sender, DataChangedEventArgs e)
{
// 更新实时数据显示
Task.Run(() =>
{
var newData = e.ChangedData.Select(d => new RealTimeDataItem
{
TagName = d.Key,
Value = d.Value.ToString(),
DataType = GetDataTypeName(d.Value.GetType()),
Timestamp = DateTime.Now
}).ToList();
Application.Current.Dispatcher.Invoke(() =>
{
if (RealTimeData == null)
{
RealTimeData = new ObservableCollection<RealTimeDataItem>();
}
foreach (var item in newData)
{
var existing = RealTimeData.FirstOrDefault(d => d.TagName == item.TagName);
if (existing != null)
{
existing.Value = item.Value;
existing.DataType = item.DataType;
existing.Timestamp = item.Timestamp;
}
else
{
RealTimeData.Add(item);
}
}
// 限制显示的数据量
if (RealTimeData.Count > 1000)
{
RealTimeData.RemoveRange(0, RealTimeData.Count - 1000);
}
});
});
}
private async void ConnectPlc()
{
try
{
StatusMessage = "正在连接PLC...";
// 连接选定的PLC
if (SelectedPlcConnection != null)
{
var plc = CreatePlcInstance(SelectedPlcConnection);
await plc.ConnectAsync();
// 添加到数据收集器
_collector.AddPlc(plc);
}
StatusMessage = "PLC连接成功";
}
catch (Exception ex)
{
StatusMessage = $"连接失败: {ex.Message}";
}
}
private async void DisconnectPlc()
{
try
{
StatusMessage = "正在断开PLC连接...";
if (SelectedPlcConnection != null)
{
var plc = _collector.Plcs.FirstOrDefault(p =>
p.ConnectionName == SelectedPlcConnection.Name);
if (plc != null)
{
_collector.RemovePlc(plc);
await plc.DisconnectAsync();
}
}
StatusMessage = "PLC已断开连接";
}
catch (Exception ex)
{
StatusMessage = $"断开连接失败: {ex.Message}";
}
}
private async void RefreshData()
{
try
{
StatusMessage = "正在刷新数据...";
// 触发数据收集器立即采集一次数据
_collector.ForceCollection();
StatusMessage = "数据刷新完成";
}
catch (Exception ex)
{
StatusMessage = $"刷新失败: {ex.Message}";
}
}
private async void QueryHistoryData()
{
try
{
StatusMessage = "正在查询历史数据...";
var startDate = HistoryStartDate ?? DateTime.Today.AddDays(-1);
var endDate = HistoryEndDate ?? DateTime.Now;
var historyData = await _dbService.GetHistoricalDataAsync(
SelectedPlcConnection?.Name,
startDate,
endDate);
Application.Current.Dispatcher.Invoke(() =>
{
HistoricalData = new ObservableCollection<HistoricalDataItem>(historyData);
});
StatusMessage = $"查询完成,共{historyData.Count}条记录";
}
catch (Exception ex)
{
StatusMessage = $"查询失败: {ex.Message}";
}
}
private void LoadPlcConnections()
{
// 从配置文件或数据库加载PLC连接配置
PlcConnections.Add(new PlcConnection
{
Name = "Siemens S7-1200",
IpAddress = "192.168.0.1",
Port = 102,
Protocol = "S7"
});
PlcConnections.Add(new PlcConnection
{
Name = "Modbus RTU Device",
IpAddress = "COM3",
BaudRate = 9600,
Protocol = "ModbusRTU"
});
// 更多连接配置...
}
private string GetDataTypeName(Type type)
{
if (type == typeof(bool)) return "BOOL";
if (type == typeof(int)) return "INT";
if (type == typeof(double)) return "REAL";
if (type == typeof(string)) return "STRING";
return type.Name;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// 数据模型类
public class RealTimeDataItem
{
public string TagName { get; set; }
public string Value { get; set; }
public string DataType { get; set; }
public DateTime Timestamp { get; set; }
}
public class HistoricalDataItem : RealTimeDataItem
{
public string PlcName { get; set; }
public int? BatchId { get; set; }
}
// PLC连接配置类
public class PlcConnection
{
public string Name { get; set; }
public string IpAddress { get; set; }
public int? Port { get; set; }
public string Protocol { get; set; }
public string ComPort { get; set; }
public int? BaudRate { get; set; }
// 其他连接参数...
}
五、关键技术实现细节
1. 多协议支持实现
// PlcFactory.cs
public static class PlcFactory
{
public static IPlcCommunication CreatePlcInstance(PlcConnection config)
{
switch (config.Protocol.ToUpper())
{
case "S7":
return new SiemensS7Plc(config.IpAddress, config.Port.Value);
case "MODBUS_RTU":
return new ModbusRtuPlc(config.ComPort, config.BaudRate.Value);
case "MODBUS_TCP":
return new ModbusTcpPlc(config.IpAddress, config.Port ?? 502);
// 更多协议...
default:
throw new NotSupportedException($"不支持的PLC协议: {config.Protocol}");
}
}
}
// SiemensS7Plc.cs (简化示例)
public class SiemensS7Plc : IPlcCommunication
{
private S7Client _client;
public SiemensS7Plc(string ipAddress, int rackSlot)
{
// 解析Rack和Slot
var parts = rackSlot.ToString().Split('/');
int rack = int.Parse(parts[0]);
int slot = int.Parse(parts[1]);
_client = new S7Client();
}
public bool Connect()
{
int result = _client.ConnectTo("192.168.0.1", 0, 1); // 示例参数
return result == 0;
}
// 其他方法实现...
}
2. 数据缓存与更新机制
// PlcDataCache.cs
public class PlcDataCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache =
new ConcurrentDictionary<string, CacheItem>();
private readonly TimeSpan _expirationTime = TimeSpan.FromMinutes(5);
public bool TryGetValue(string key, out object value)
{
if (_cache.TryGetValue(key, out var item) && !item.IsExpired)
{
value = item.Value;
return true;
}
value = null;
return false;
}
public void SetValue(string key, object value)
{
_cache[key] = new CacheItem(value, DateTime.Now.Add(_expirationTime));
}
public void RemoveExpiredItems()
{
var expiredKeys = _cache.Where(kvp => kvp.Value.IsExpired)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_cache.TryRemove(key, out _);
}
}
private class CacheItem
{
public object Value { get; }
public DateTime ExpirationTime { get; }
public CacheItem(object value, DateTime expirationTime)
{
Value = value;
ExpirationTime = expirationTime;
}
public bool IsExpired => DateTime.Now >= ExpirationTime;
}
}
3. 异常处理与重试机制
// RetryHelper.cs
public static class RetryHelper
{
public static async Task<T> ExecuteWithRetryAsync<T>(
Func<Task<T>> action,
int maxRetries = 3,
TimeSpan delay = default)
{
delay = delay == default ? TimeSpan.FromSeconds(1) : delay;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
return await action();
}
catch (Exception ex) when (attempt < maxRetries - 1)
{
// 记录错误日志
LogError($"尝试 {attempt + 1}/{maxRetries} 失败: {ex.Message}");
if (attempt < maxRetries - 1)
{
await Task.Delay(delay);
delay = delay.Add(TimeSpan.FromSeconds(1)); // 指数退避
}
}
}
throw new Exception($"操作失败,已达到最大重试次数 {maxRetries}");
}
}
// 在Plc通信中使用
public async Task<bool> ReadBoolWithRetryAsync(string address)
{
return await RetryHelper.ExecuteWithRetryAsync(async () =>
{
return await ReadBoolAsync(address);
}, maxRetries: 3, delay: TimeSpan.FromSeconds(1));
}
六、部署与运行
1. 部署方案
单机部署:
安装.NET运行时环境
配置数据库连接
直接运行WPF应用程序
网络部署:
配置PLC网络访问权限
设置数据库服务器访问
可能需要配置防火墙规则
容器化部署(可选):
使用Docker容器化WPF应用(需特殊处理)
或者将核心服务分离为后台服务,WPF仅作为客户端
2. 配置文件示例
<!-- App.config -->
<configuration>
<connectionStrings>
<add name="PlcGatewayDB"
connectionString="Server=.;Database=PlcGateway;User Id=user;Password=pass;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<appSettings>
<add key="DataCollectionInterval" value="1000"/>
<add key="MaxCacheSize" value="10000"/>
<add key="RetryAttempts" value="3"/>
</appSettings>
</configuration>
3. 日志配置
// 使用NLog配置日志
public static class LoggerConfig
{
public static void Configure()
{
var config = new NLog.Config.LoggingConfiguration();
// 控制台输出
var consoleTarget = new NLog.Targets.ConsoleTarget("console");
config.AddRule(LogLevel.Debug, LogLevel.Fatal, consoleTarget);
// 文件输出
var fileTarget = new NLog.Targets.FileTarget("file") {
FileName = "logs/plc_gateway.log",
ArchiveFileName = "logs/archives/plc_gateway.{#}.log",
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.DateAndSequence,
ArchiveAboveSize = 1024 * 1024 * 10 // 10MB
};
config.AddRule(LogLevel.Info, LogLevel.Fatal, fileTarget);
NLog.LogManager.Configuration = config;
}
}
七、性能优化建议
批量采集:
合并多个PLC读取请求为批量操作
减少网络往返次数
异步处理:
所有IO操作使用异步模式
避免UI线程阻塞
数据压缩:
对历史数据进行压缩存储
减少数据库空间占用
缓存策略:
实现多级缓存(内存+磁盘)
设置合理的过期时间
连接池:
对频繁访问的PLC设备维护连接池
减少连接建立开销
八、扩展功能建议
报警系统:
配置报警阈值
实现报警通知(邮件、短信、消息队列)
可视化监控:
添加实时趋势图
实现历史数据曲线展示
权限管理:
用户角色与权限控制
操作审计日志
API接口:
提供RESTful API
支持第三方系统集成
移动端支持:
开发配套的移动应用
实现远程监控功能
九、常见问题解决
PLC连接不稳定:
检查网络连接质量
增加重连机制
实现心跳检测
数据采集延迟:
优化采集间隔设置
使用高性能PLC通信库
考虑边缘计算设备
数据库写入瓶颈:
批量插入代替单条插入
使用存储过程提高效率
考虑NoSQL数据库作为补充
内存泄漏:
确保释放所有COM对象
使用内存分析工具检测泄漏点
定期重启服务
十、总结
本文详细介绍了基于WPF的PLC数据采集网关系统的设计与实现,涵盖了从架构设计到具体实现的各个方面。系统采用模块化设计,支持多种PLC协议,具备良好的扩展性和稳定性。通过合理的性能优化和功能扩展,可以满足工业自动化领域的数据采集需求。
实际开发中,建议:
先实现核心功能,再逐步扩展
编写全面的单元测试和集成测试
考虑使用依赖注入框架管理组件
实现完善的错误处理和恢复机制
定期进行性能测试和优化
随着工业物联网的发展,此类数据采集网关系统将发挥越来越重要的作用,为工业数字化转型提供坚实的基础支撑。
暂无评论内容