在工控上位机领域,C# + WPF 仍是性价比很高的组合:WPF 的数据绑定、命令、模板和样式,适合做状态监控、参数配置、报警追溯;C# 的异步、依赖注入和生态,适合封装通信与业务逻辑。本文以石墨岛搬移系统为例,给出可落地的架构与核心代码。
推荐分层:View(XAML)→ ViewModel(状态与命令)→ Service(业务状态机)→ Communication(Modbus TCP/OPC UA)→ PLC。安全联锁必须独立于上位机,急停、光栅、限位直接接入安全 PLC 或安全继电器。上位机只做调度、监控和追溯。
项目结构:
BoatMove.Host/
├── Views/MainWindow.xaml
├── ViewModels/MainViewModel.cs
├── Services/BoatMoveService.cs
├── Communication/IPlcService.cs
├── Communication/ModbusPlcService.cs
├── Infrastructure/RelayCommand.cs
└── App.xaml.cs先抽象接口,便于替换 Modbus、S7 或 OPC UA:
public interface IPlcService
{
Task<bool> ReadCoilAsync(ushort addr);
Task WriteCoilAsync(ushort addr, bool value);
Task<ushort> ReadRegisterAsync(ushort addr);
Task WriteRegisterAsync(ushort addr, ushort value);
Task<bool> ReadInputAsync(ushort addr);
}基于 NModbus 的实现:
using NModbus;
using System.Net.Sockets;
public class ModbusPlcService : IPlcService, IDisposable
{
private TcpClient? _tcp;
private IModbusMaster? _master;
public async Task ConnectAsync(string ip, int port)
{
_tcp = new TcpClient();
await _tcp.ConnectAsync(ip, port);
_master = new ModbusFactory().CreateMaster(_tcp);
}
public Task<bool> ReadCoilAsync(ushort addr)
=> Task.Run(() => _master!.ReadCoils(1, addr, 1)[0]);
public Task WriteCoilAsync(ushort addr, bool value)
=> Task.Run(() => _master!.WriteSingleCoil(1, addr, value));
public Task<ushort> ReadRegisterAsync(ushort addr)
=> Task.Run(() => _master!.ReadHoldingRegisters(1, addr, 1)[0]);
public Task WriteRegisterAsync(ushort addr, ushort value)
=> Task.Run(() => _master!.WriteSingleRegister(1, addr, value));
public Task<bool> ReadInputAsync(ushort addr)
=> Task.Run(() => _master!.ReadInputs(1, addr, 1)[0]);
public void Dispose() => _tcp?.Dispose();
}状态机建议单线程驱动或加锁,避免并发写 PLC:
public enum BoatState { Idle, Homing, Ready, Picking, Moving, Placing, Error }
public class BoatMoveService
{
private readonly IPlcService _plc;
public BoatState State { get; private set; } = BoatState.Idle;
public event Action<BoatState>? StateChanged;
public BoatMoveService(IPlcService plc) => _plc = plc;
public async Task StepAsync()
{
if (!await SafetyOkAsync()) { SetState(BoatState.Error); return; }
switch (State)
{
case BoatState.Idle:
if (await _plc.ReadCoilAsync(0)) SetState(BoatState.Homing);
break;
case BoatState.Homing:
await _plc.WriteCoilAsync(10, true);
if (await _plc.ReadCoilAsync(11)) SetState(BoatState.Ready);
break;
case BoatState.Ready:
if (await _plc.ReadCoilAsync(1)) SetState(BoatState.Picking);
break;
case BoatState.Picking:
await _plc.WriteCoilAsync(20, true);
if (await _plc.ReadCoilAsync(21)) SetState(BoatState.Moving);
break;
case BoatState.Moving:
await _plc.WriteRegisterAsync(30, 1000);
if (await _plc.ReadCoilAsync(31)) SetState(BoatState.Placing);
break;
case BoatState.Placing:
await _plc.WriteCoilAsync(40, true);
if (await _plc.ReadCoilAsync(41)) SetState(BoatState.Idle);
break;
}
}
private async Task<bool> SafetyOkAsync()
=> await _plc.ReadInputAsync(0)
&& await _plc.ReadInputAsync(1)
&& await _plc.ReadInputAsync(2);
private void SetState(BoatState s)
{
State = s;
StateChanged?.Invoke(s);
}
}public class MainViewModel : INotifyPropertyChanged
{
private readonly BoatMoveService _move;
public string StateText => _move.State.ToString();
public ICommand StartCommand { get; }
public MainViewModel(BoatMoveService move)
{
_move = move;
_move.StateChanged += _ =>
Application.Current.Dispatcher.Invoke(() =>
OnPropertyChanged(nameof(StateText)));
StartCommand = new RelayCommand(async _ =>
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(100));
while (await timer.WaitForNextTickAsync())
await _move.StepAsync();
});
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new(name));
}RelayCommand 可自行实现 ICommand,核心是 Execute 调用委托,CanExecute 控制按钮可用性。
<Window x:Class="BoatMove.Host.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="20">
<TextBlock Text="{Binding StateText}" FontSize="24"/>
<Button Content="启动搬移" Command="{Binding StartCommand}"
Height="40" Margin="0,10"/>
<ListBox ItemsSource="{Binding Logs}" Height="200"/>
</StackPanel>
</Window>Dispatcher,状态机在后台线程运行。appsettings.json,支持多舟型切换。总结:C# + WPF 做搬移上位机,关键在通信稳定、状态机清晰、安全联锁独立、界面只做展示与命令。先保证可观测与可追溯,再优化节拍与自动化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。