一、项目结构
├── PLCControlDemo/
│ ├── Models/
│ │ └── PlcVariable.cs # PLC变量模型
│ ├── Services/
│ │ └── AdsCommunicationService.cs # ADS通信服务
│ ├── Views/
│ │ ├── MainWindow.xaml # 主界面
│ │ └── DashboardView.xaml # 仪表盘视图
│ └── App.xaml.cs # 应用入口
二、核心代码实现
1. PLC变量模型(Models/PlcVariable.cs)
public class PlcVariable : INotifyPropertyChanged
{
private object _value;
public string Name {
get; }
public string Description {
get; }
public object Value
{
get => _value;
set
{
_value = value;
OnPropertyChanged(nameof(Value));
}
}
public PlcVariable(string name, string description)
{
Name = name;
Description = description;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
2. ADS通信服务(Services/AdsCommunicationService.cs)
using TwinCAT.Ads;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class AdsCommunicationService : IDisposable
{
private readonly TcAdsClient _adsClient;
private readonly string _amsNetId = "192.168.0.1.1.1"; // PLC的AMS Net ID
private readonly int _port = 851; // 默认ADS端口
public event EventHandler<AdsEventArgs> ConnectionStateChanged;
public AdsCommunicationService()
{
_adsClient = new TcAdsClient();
}
public async Task ConnectAsync()
{
try
{
await _adsClient.ConnectAsync(_amsNetId, _port);
ConnectionStateChanged?.Invoke(this, new AdsEventArgs(ConnectionState.Connected));
}
catch (AdsException ex)
{
ConnectionStateChanged?.Invoke(this, new AdsEventArgs(ConnectionState.Failed, ex.Message));
throw;
}
}
public async Task<T> ReadVariableAsync<T>(string variableName) where T : struct
{
var handle = await _adsClient.CreateVariableHandleAsync(variableName);
var value = await _adsClient.ReadAnyAsync(handle, typeof(T));
_adsClient.DeleteVariableHandle(handle);
return (T)value;
}
public async Task WriteVariableAsync<T>(string variableName, T value) where T : struct
{
var handle = await _adsClient.CreateVariableHandleAsync(variableName);
await _adsClient.WriteAnyAsync(handle, value);
_adsClient.DeleteVariableHandle(handle);
}
public void Disconnect()
{
_adsClient?.Disconnect();
}
public void Dispose()
{
_adsClient?.Dispose();
}
}
public enum ConnectionState {
Disconnected, Connected, Failed }
public class AdsEventArgs : EventArgs
{
public ConnectionState State {
get; }
public string ErrorMessage {
get; }
public AdsEventArgs(ConnectionState state)
{
State = state;
}
public AdsEventArgs(ConnectionState state, string message)
{
State = state;
ErrorMessage = message;
}
}
3. 主界面逻辑(Views/MainWindow.xaml.cs)
using System.Windows;
using System.Windows.Controls;
namespace PLCControlDemo.Views
{
public partial class MainWindow : Window
{
private readonly AdsCommunicationService _plcService = new AdsCommunicationService();
private readonly Dictionary<string, PlcVariable> _variables = new Dictionary<string, PlcVariable>();
public MainWindow()
{
InitializeComponent();
InitializeVariables();
DataContext = this;
_plcService.ConnectionStateChanged += OnConnectionStateChanged;
}
private void InitializeVariables()
{
// 定义PLC变量映射
_variables.Add("MAIN.Temperature", new PlcVariable("Temperature", "温度值"));
_variables.Add("MAIN.StartCommand", new PlcVariable("StartCommand", "启动指令"));
}
private async void ConnectButton_Click(object sender, RoutedEventArgs e)
{
try
{
await _plcService.ConnectAsync();
UpdateStatus("已连接");
}
catch (Exception ex)
{
UpdateStatus($"连接失败: {ex.Message}");
}
}
private async void ReadButton_Click(object sender, RoutedEventArgs e)
{
try
{
var temp = await _plcService.ReadVariableAsync<double>("MAIN.Temperature");
TemperatureText.Text = temp.ToString("0.00");
}
catch (Exception ex)
{
MessageBox.Show($"读取失败: {ex.Message}");
}
}
private async void WriteButton_Click(object sender, RoutedEventArgs e)
{
try
{
await _plcService.WriteVariableAsync("MAIN.StartCommand", true);
StartCommandText.Text = "已启动";
}
catch (Exception ex)
{
MessageBox.Show($"写入失败: {ex.Message}");
}
}
private void OnConnectionStateChanged(object sender, AdsEventArgs e)
{
Dispatcher.Invoke(() =>
{
ConnectionStatus.Background = e.State switch
{
ConnectionState.Connected => System.Windows.Media.Brushes.Green,
ConnectionState.Failed => System.Windows.Media.Brushes.Red,
_ => System.Windows.Media.Brushes.Gray
};
ConnectionStatusText.Text = e.State.ToString() +
(string.IsNullOrEmpty(e.ErrorMessage) ? "" : $"\n错误: {e.ErrorMessage}");
});
}
private void UpdateStatus(string message)
{
StatusText.Text = message;
}
protected override void OnClosed(EventArgs e)
{
_plcService.Disconnect();
base.OnClosed(e);
}
}
}
4. 主界面XAML(Views/MainWindow.xaml)
<Window x:Class="PLCControlDemo.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="倍福PLC控制面板" Height="450" Width="800">
<Grid>
<!-- 状态栏 -->
<StatusBar VerticalAlignment="Bottom">
<StatusBarItem Content="{Binding ConnectionStatusText}"
Background="{Binding ConnectionStatus.Background}"/>
<StatusBarItem Content="{Binding StatusText}"/>
</StatusBar>
<!-- 控制面板 -->
<StackPanel VerticalAlignment="Top" Margin="20">
<Button Content="连接PLC" Click="ConnectButton_Click" Margin="5"/>
<Button Content="读取温度" Click="ReadButton_Click" Margin="5"/>
<Button Content="启动设备" Click="WriteButton_Click" Margin="5"/>
<!-- 实时数据显示 -->
<TextBlock Text="温度值:" FontWeight="Bold" Margin="5"/>
<TextBox x:Name="TemperatureText" Width="200" Margin="5" IsReadOnly="True"/>
<TextBlock Text="启动指令:" FontWeight="Bold" Margin="5"/>
<TextBox x:Name="StartCommandText" Width="200" Margin="5" IsReadOnly="True"/>
</StackPanel>
</Grid>
</Window>
三、关键功能实现
1. 异步通信优化
// 使用异步方法避免界面冻结
public async Task<double> GetTemperatureAsync()
{
return await _plcService.ReadVariableAsync<double>("MAIN.Temperature");
}
// 批量读取示例
public async Task<PlcDataBatch> GetBatchDataAsync()
{
var data = new PlcDataBatch();
data.Temperature = await GetTemperatureAsync();
data.Pressure = await _plcService.ReadVariableAsync<double>("MAIN.Pressure");
data.Position = await _plcService.ReadVariableAsync<int>("MAIN.Position");
return data;
}
2. 报警监控系统
// 报警规则定义
public class AlarmRule
{
public string VariableName {
get; }
public double HighLimit {
get; }
public double LowLimit {
get; }
public AlarmRule(string varName, double high, double low)
{
VariableName = varName;
HighLimit = high;
LowLimit = low;
}
public bool CheckAlarm(double value)
{
return value > HighLimit || value < LowLimit;
}
}
// 报警处理服务
public class AlarmService
{
private readonly AdsCommunicationService _plcService;
private readonly List<AlarmRule> _rules = new List<AlarmRule>();
public AlarmService(AdsCommunicationService plcService)
{
_plcService = plcService;
InitializeRules();
}
private void InitializeRules()
{
_rules.Add(new AlarmRule("MAIN.Temperature", 80.0, 10.0));
_rules.Add(new AlarmRule("MAIN.Pressure", 200.0, 50.0));
}
public async Task CheckAlarmsAsync()
{
foreach (var rule in _rules)
{
var value = await _plcService.ReadVariableAsync<double>(rule.VariableName);
if (rule.CheckAlarm(value))
{
ShowAlarm($"[{rule.VariableName}] 超出范围: {value}");
}
}
}
private void ShowAlarm(string message)
{
Application.Current.Dispatcher.Invoke(() =>
{
AlarmLog.AppendText($"{DateTime.Now}: {message}\n");
System.Media.SoundPlayer alarmSound = new System.Media.SoundPlayer("alarm.wav");
alarmSound.Play();
});
}
}
参考代码 基于C# 编写的倍福plc 应用上位机源程序 www.youwenfan.com/contentalg/113111.html
四、扩展功能实现
1. 历史数据记录
public class DataLogger
{
private readonly string _logFilePath = "plc_data_log.csv";
private readonly List<PlcDataPoint> _dataBuffer = new List<PlcDataPoint>();
public void LogData(PlcDataPoint data)
{
_dataBuffer.Add(data);
if (_dataBuffer.Count >= 100) // 每100条写入文件
{
File.WriteAllLines(_logFilePath, _dataBuffer.Select(d =>
$"{d.Timestamp:yyyy-MM-dd HH:mm:ss},{d.Temperature},{d.Pressure}"));
_dataBuffer.Clear();
}
}
}
public class PlcDataPoint
{
public DateTime Timestamp {
get; } = DateTime.Now;
public double Temperature {
get; set; }
public double Pressure {
get; set; }
}
2. 数据可视化仪表盘
<!-- DashboardView.xaml -->
<UserControl x:Class="PLCControlDemo.Views.DashboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<!-- 温度仪表盘 -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="温度监控" FontSize="16" Margin="10"/>
<Viewbox Grid.Row="1" Stretch="Uniform">
<Grid>
<Ellipse Width="200" Height="200" Fill="LightGray"/>
<RadialGauge
Value="{Binding Temperature}"
Minimum="0"
Maximum="100"
NeedleBrush="Red"
TickBrush="Black"
ScaleWidth="20"/>
</Grid>
</Viewbox>
</Grid>
</UserControl>
五、部署与调试
1. 环境配置
开发环境:Visual Studio 2022 + .NET 6.0
依赖库:
<!-- PLC通信 --> <PackageReference Include="TwinCAT.Ads" Version="6.0.1000.15" /> <!-- 数据可视化 --> <PackageReference Include="MahApps.Metro" Version="2.4.3" />
2. 调试技巧
Wireshark抓包分析:
- 过滤条件:
tcp.port == 851 - 验证ADS协议报文结构是否符合规范
- 过滤条件:
异常捕获增强:
public async Task SafeReadAsync<T>(string varName, out T value) { try { value = await ReadVariableAsync<T>(varName); } catch (AdsException ex) when (ex.ErrorCode == 0x800A0002) { value = default; ShowError($"变量不存在: {varName}"); } catch (TimeoutException) { value = default; ShowError("通信超时,请检查网络连接"); } }
六、性能优化
连接池管理:
private static readonly ObjectPool<TcAdsClient> _clientPool = new ObjectPool<TcAdsClient>(() => new TcAdsClient(), 3); // 维护3个连接实例批量数据传输:
public async Task<byte[]> ReadBulkDataAsync(string[] variables) { using (var ms = new MemoryStream()) { foreach (var var in variables) { var handle = await _adsClient.CreateVariableHandleAsync(var); var data = await _adsClient.ReadAnyAsync(handle, typeof(byte[])); ms.Write(data, 0, data.Length); _adsClient.DeleteVariableHandle(handle); } return ms.ToArray(); } }数据压缩:
public byte[] CompressData(byte[] rawData) { using (var memoryStream = new MemoryStream()) { using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) { gzipStream.Write(rawData, 0, rawData.Length); } return memoryStream.ToArray(); } }
七、测试用例
1. 基本功能测试
| 测试项 | 输入值 | 预期结果 |
|---|---|---|
| 温度读取 | 无 | 显示实时温度值 |
| 启动指令写入 | true | PLC启动状态变为运行 |
| 报警触发 | 温度>80℃ | 弹出警告并记录日志 |
2. 压力测试
// 模拟高频数据采集
public async Task StressTestAsync()
{
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
await _plcService.ReadVariableAsync<double>("MAIN.Temperature");
}
stopwatch.Stop();
Assert.IsTrue(stopwatch.ElapsedMilliseconds < 500); // 1000次读取应在500ms内完成
}
八、扩展应用场景
多PLC协同控制:
public class PlcClusterManager { private readonly List<AdsCommunicationService> _plcs = new List<AdsCommunicationService>(); public void AddPlc(string amsNetId) { var plc = new AdsCommunicationService(); plc.ConnectAsync(amsNetId).Wait(); _plcs.Add(plc); } }OPC UA集成:
public class OpcUaGateway { private readonly Opc.Ua.Client