首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C# + WPF 打造石墨岛搬移上位机:MVVM、Modbus TCP 与状态机落地

C# + WPF 打造石墨岛搬移上位机:MVVM、Modbus TCP 与状态机落地

原创
作者头像
用户12687280
修改于 2026-09-23 16:06:51
修改于 2026-09-23 16:06:51
1390
举报

在工控上位机领域,C# + WPF 仍是性价比很高的组合:WPF 的数据绑定、命令、模板和样式,适合做状态监控、参数配置、报警追溯;C# 的异步、依赖注入和生态,适合封装通信与业务逻辑。本文以石墨岛搬移系统为例,给出可落地的架构与核心代码。

一、架构设计

推荐分层:View(XAML)→ ViewModel(状态与命令)→ Service(业务状态机)→ Communication(Modbus TCP/OPC UA)→ PLC。安全联锁必须独立于上位机,急停、光栅、限位直接接入安全 PLC 或安全继电器。上位机只做调度、监控和追溯。

项目结构:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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 的实现:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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);
    }
}

四、ViewModel 与命令

代码语言:javascript
复制
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 控制按钮可用性。

五、XAML 绑定

代码语言:javascript
复制
<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>

六、工程要点

  1. 所有 PLC 读写加超时、重试、断线重连;写后读校验关键线圈。
  2. UI 更新必须回 Dispatcher,状态机在后台线程运行。
  3. 安全联锁独立于上位机,软件只做二次判断。
  4. 日志记录舟号、批次、状态迁移、报警码、操作员,推荐 Serilog。
  5. 配方、位置偏移、通信参数放 appsettings.json,支持多舟型切换。
  6. 与 MES 对接使用幂等设计,避免重复上报。

总结:C# + WPF 做搬移上位机,关键在通信稳定、状态机清晰、安全联锁独立、界面只做展示与命令。先保证可观测与可追溯,再优化节拍与自动化。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、架构设计
  • 二、通信层封装
  • 三、搬移状态机
  • 四、ViewModel 与命令
  • 五、XAML 绑定
  • 六、工程要点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档