四轴运动控制系统 — 博客二:服务层 (Services)

四轴运动控制系统 --- 博客二:服务层 (Services)

本篇覆盖 Services 目录下的三个核心文件:ModbusServiceBase(抽象基类)、ModbusRtuService(串口实现)、ModbusTcpService(网络实现)。


一、类继承关系图

复制代码
ModbusServiceBase  (abstract)   ← 抽象基类:通用连接状态 + 读/写方法
    │
    ├── ModbusRtuService         ← 串口 RTU:SerialPort + ModbusSerialMaster
    │
    └── ModbusTcpService         ← TCP/IP:TcpClient + ModbusIpMaster

设计思路

  • 基类封装所有服务共同的东西 :连接状态、5 个 Modbus 读写方法、IDisposable
  • 子类只实现各自不同的东西:如何连接、如何断开
  • 这样 ViewModel 不用关心底层是 RTU 还是 TCP,统一调用 _service.ReadHoldingRegisters(...)

二、ModbusServiceBase.cs --- 抽象基类

csharp 复制代码
// NModbus4 库的核心接口 IModbusMaster
// 定义 ReadHoldingRegisters / WriteSingleRegister 等方法
using Modbus.Device;

// CommConfig 在 Models 层,基类引用它来读取超时、从站 ID 等
using UpperMachine.Models;

namespace UpperMachine.Services
{
    /// <summary>
    /// Modbus 通信服务的抽象基类
    /// 定义所有子类共用的连接状态 + 读/写方法
    /// 实现 IDisposable 确保资源释放
    /// </summary>
    public abstract class ModbusServiceBase : IDisposable
    {
        // ============== 受保护字段 ==============

        /// <summary>
        /// NModbus4 的通信主站接口
        /// TCP 子类赋值为 ModbusIpMaster
        /// RTU 子类赋值为 ModbusSerialMaster
        /// 两者都实现了 IModbusMaster,所以基类用这个类型统一
        /// </summary>
        protected IModbusMaster? Master;

        // ============== 公开属性 ==============

        /// <summary>当前是否已成功连接,子类可用 protected set 修改</summary>
        public bool IsConnected { get; protected set; }

        /// <summary>
        /// 通信配置(公开字段)
        /// 子类和外部都可以直接读写里面的 IpAddress / PortName / Timeout 等
        /// </summary>
        public CommConfig Config = new CommConfig();

        // ============== 抽象方法(子类必须实现) ==============

        /// <summary>子类实现各自的连接逻辑,返回是否成功</summary>
        public abstract bool Connect();

        /// <summary>子类实现各自的断开逻辑</summary>
        public abstract void Disconnect();

        // ============== 私有辅助方法 ==============

        /// <summary>
        /// 检查连接是否有效
        /// Master != null 确保对象存在
        /// IsConnected 确保连接状态为真
        /// 两者缺一不可(断开后 Master 被置为 null)
        /// </summary>
        private bool _connected() => Master != null && IsConnected;

        // ============== 公共读/写方法 ==============

        /// <summary>
        /// 读取保持寄存器(Holding Registers)
        /// 常用功能码 0x03,可读可写
        /// </summary>
        /// <param name="startAddress">起始地址(如 0)</param>
        /// <param name="count">读取数量</param>
        /// <returns>ushort 数组,每个元素 16 位</returns>
        public ushort[] ReadHoldingRegisters(ushort startAddress, ushort count)
        {
            // 未连接则直接抛出异常
            if (!_connected()) throw new InvalidOperationException("未连接");

            // Master! 中的 ! 是 null 包容运算符
            // _connected() 已确保 Master != null,用 ! 消除编译器 CS8602 警告
            return Master!.ReadHoldingRegisters(Config.SlaveId, startAddress, count);
        }

        /// <summary>
        /// 读取输入寄存器(Input Registers)
        /// 功能码 0x04,只读(从 PLC 读取传感器等数据)
        /// </summary>
        public ushort[] ReadInputRegisters(ushort startAddress, ushort count)
        {
            if (!_connected()) throw new InvalidOperationException("未连接");
            return Master!.ReadInputRegisters(Config.SlaveId, startAddress, count);
        }

        /// <summary>
        /// 读取线圈状态(Coils)
        /// 功能码 0x01,读取 DO/开关量输出
        /// </summary>
        public bool[] ReadCoils(ushort startAddress, ushort count)
        {
            if (!_connected()) throw new InvalidOperationException("未连接");
            return Master!.ReadCoils(Config.SlaveId, startAddress, count);
        }

        /// <summary>
        /// 写入单个寄存器
        /// 功能码 0x06,Modbus 标准写寄存器
        /// </summary>
        public void WriteSingleRegister(ushort address, ushort value)
        {
            if (!_connected()) throw new InvalidOperationException("未连接");
            try
            {
                Master!.WriteSingleRegister(Config.SlaveId, address, value);
            }
            catch (Exception e)
            {
                // 将原始异常包装为更友好的消息
                throw new Exception($"写入失败: {e.Message}");
            }
        }

        /// <summary>
        /// 写入单个线圈
        /// 功能码 0x05
        /// </summary>
        public void WriteSingleCoil(ushort address, bool value)
        {
            if (!_connected()) throw new InvalidOperationException("未连接");
            try
            {
                Master!.WriteSingleCoil(Config.SlaveId, address, value);
            }
            catch (Exception e)
            {
                throw new Exception($"写入失败: {e.Message}");
            }
        }

        // ============== 资源释放 ==============

        /// <summary>
        /// 实现 IDisposable.Dispose()
        /// 供 using 块或 ViewModel 手动调用释放资源
        /// 内部调用 Disconnect() 关闭连接 + 释放 Master
        /// </summary>
        public void Dispose()
        {
            Disconnect();              // 调用子类的断开逻辑
            GC.SuppressFinalize(this); // 告诉 GC 不需要再调用析构函数
        }
    }
}

基类知识点总结

知识点 代码 说明
抽象类 public abstract class 不能直接 new,必须继承
抽象方法 abstract bool Connect() 子类必须 override,强迫实现
protected protected IModbusMaster? Master 只有子类能访问,外部看不到
空包容运算符 Master! 告诉编译器"我知道它不为 null"
IDisposable : IDisposable 支持 using 或手动 Dispose

三、ModbusRtuService.cs --- 串口 RTU 实现

csharp 复制代码
// 引入 NModbus4 的 ModbusSerialMaster(静态工厂类)
using Modbus.Device;

// 引入 System.IO.Ports 的 SerialPort 类
using System.IO.Ports;

namespace UpperMachine.Services
{
    /// <summary>
    /// Modbus RTU(串口)通信实现
    /// 通过 SerialPort 连接 PLC,使用 ModbusSerialMaster 通信
    /// </summary>
    internal class ModbusRtuService : ModbusServiceBase
    {
        /// <summary>底层的串口对象,生命周期由 Connect/Disconnect 管理</summary>
        private SerialPort? _serialPort;

        /// <summary>
        /// 建立 RTU 连接
        /// 1. 从 Config 读取串口参数
        /// 2. 创建 SerialPort 并打开
        /// 3. 通过 ModbusSerialMaster.CreateRtu 创建通信主站
        /// </summary>
        public override bool Connect()
        {
            try
            {
                // ---- 第 1 步:创建串口对象 ----
                // SerialPort 构造函数参数顺序:
                //   (端口名, 波特率, 校验位, 数据位, 停止位)
                _serialPort = new SerialPort(
                    Config.PortName,      // 如 "COM1"
                    Config.BaudRate,      // 如 9600
                    Config.Parity,        // Parity 枚举,如 Parity.None
                    Config.DataBits,      // 通常为 8
                    Config.StopBits       // StopBits 枚举,如 StopBits.One
                )
                {
                    // 设置读写超时,单位毫秒
                    // 从 Config.Timeout 读取,默认 1000ms
                    ReadTimeout = Config.Timeout,
                    WriteTimeout = Config.Timeout
                };

                // ---- 第 2 步:打开串口 ----
                // 如果端口不存在或已被占用,会抛出异常
                _serialPort.Open();

                // ---- 第 3 步:创建 Modbus 主站 ----
                // ModbusSerialMaster.CreateRtu 是 NModbus4 的静态工厂方法
                // 参数:已经打开的 SerialPort 对象
                // 返回值:IModbusSerialMaster(继承 IModbusMaster)
                Master = ModbusSerialMaster.CreateRtu(_serialPort);

                // ---- 第 4 步:更新连接状态 ----
                IsConnected = true;
                return true;
            }
            catch (Exception ex)
            {
                // 连接失败时打印错误信息到输出窗口
                Console.WriteLine($"RTU连接失败: {ex.Message}");

                // 确保状态为 false
                IsConnected = false;
                return false;
            }
        }

        /// <summary>
        /// 断开 RTU 连接
        /// 关闭串口 → 释放资源 → 置为 null
        /// 调用顺序很重要:先关串口,再释放 Master
        /// </summary>
        public override void Disconnect()
        {
            IsConnected = false;  // 先改状态,防止断开过程中被其他线程误用

            // ?. 是 null 条件运算符:如果 _serialPort 为 null 则不执行
            _serialPort?.Close();    // 关闭串口(释放硬件资源)
            _serialPort?.Dispose();  // 释放托管资源
            _serialPort = null;      // 解除引用,方便 GC 回收

            Master?.Dispose();       // 释放 Modbus 主站
            Master = null;
        }
    }
}

RTU 关键流程

复制代码
用户点击 Connect
    → MainViewModel.Connect() 创建 ModbusRtuService
    → 调用 service.Connect()
        → new SerialPort(COM1, 9600, None, 8, One)
        → serialPort.Open()
        → ModbusSerialMaster.CreateRtu(serialPort)
        → IsConnected = true
    → 返回 true
    → 界面灯变绿,显示 "Connected"

四、ModbusTcpService.cs --- TCP 实现

csharp 复制代码
// 引入 NModbus4 的 ModbusIpMaster(TCP 主站创建器)
using Modbus.Device;

// TCP 客户端
using System.Net.Sockets;

// Task 用于异步连接 + 超时控制
using System.Threading.Tasks;

namespace UpperMachine.Services
{
    /// <summary>
    /// Modbus TCP 通信实现
    /// 通过 TcpClient 连接 PLC,使用 ModbusIpMaster 通信
    /// </summary>
    internal class ModbusTcpService : ModbusServiceBase
    {
        /// <summary>底层 TCP 客户端,生命周期由 Connect/Disconnect 管理</summary>
        private TcpClient? _tcpClient;

        /// <summary>
        /// 建立 TCP 连接
        /// 核心改进:使用 ConnectAsync + Wait 实现可配置超时,
        /// 而不是用阻塞的 new TcpClient(host, port) 默认 20s+ 超时
        /// </summary>
        public override bool Connect()
        {
            try
            {
                // ---- 第 1 步:创建 TcpClient(不连接) ----
                // 不在构造函数传参,因为那会立即开始阻塞连接
                _tcpClient = new TcpClient();

                // ---- 第 2 步:异步连接 + 超时控制 ----
                // ConnectAsync 返回 Task,不会阻塞线程
                var connectTask = _tcpClient.ConnectAsync(Config.IpAddress, Config.TcpPort);

                // Wait(timeout) 等待指定时间
                // 如果连接成功返回 true,超时返回 false
                if (!connectTask.Wait(TimeSpan.FromMilliseconds(Config.Timeout)))
                {
                    // 超时处理:关闭资源,返回失败
                    Console.WriteLine($"TCP连接超时 ({Config.Timeout}ms)");
                    _tcpClient.Close();
                    _tcpClient = null;
                    IsConnected = false;
                    return false;
                }

                // ---- 第 3 步:设置读写超时 ----
                _tcpClient.ReceiveTimeout = Config.Timeout;
                _tcpClient.SendTimeout = Config.Timeout;

                // ---- 第 4 步:创建 Modbus 主站 ----
                // ModbusIpMaster.CreateIp 接收已连接的 TcpClient
                Master = ModbusIpMaster.CreateIp(_tcpClient);

                IsConnected = true;
                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"TCP连接失败: {ex.Message}");
                IsConnected = false;

                // 确保资源清理
                _tcpClient?.Close();
                _tcpClient = null;
                return false;
            }
        }

        /// <summary>
        /// 断开 TCP 连接
        /// </summary>
        public override void Disconnect()
        {
            IsConnected = false;

            _tcpClient?.Close();    // 关闭 TCP 连接
            _tcpClient?.Dispose();  // 释放
            _tcpClient = null;

            Master?.Dispose();
            Master = null;
        }
    }
}

TCP 超时机制详解

csharp 复制代码
// 传统写法(阻塞,最长等 20~30 秒):
_tcpClient = new TcpClient("192.168.1.100", 502);   // ❌ UI 卡死

// 我们现在的写法(可配置超时,不阻塞 UI):
_tcpClient = new TcpClient();                         // 只创建,不连接
var task = _tcpClient.ConnectAsync(ip, port);         // 后台线程连接
task.Wait(TimeSpan.FromMilliseconds(timeout));         // 等指定时间
if (!task.IsCompleted) { /* 超时处理 */ }              // 超时了

为什么这样不会卡 UI? 因为 ViewModel 里 Connect()async void,通过 await Task.Run(() => service.Connect()) 把整个连接过程扔到线程池执行,UI 线程不会被阻塞。


五、服务层与数据模型层的交互

复制代码
            CommConfig                      ModbusServiceBase
         ┌──────────────┐              ┌────────────────────────┐
         │ IpAddress     │◄───────────│ Config (字段引用)        │
         │ TcpPort       │             │ IsConnected             │
         │ PortName      │             │ ReadHoldingRegisters()  │
         │ BaudRate      │             │ WriteSingleRegister()   │
         │ Parity        │             │ Dispose()               │
         │ StopBits      │             └────────┬───────────────┘
         │ SlaveId       │                      │
         │ Timeout       │              ┌───────┴───────┐
         └──────────────┘          ┌────┴────┐    ┌────┴────┐
                                   │ RTU     │    │ TCP     │
                                   │ Serial  │    │ TcpClnt │
                                   │ Master  │    │ Master  │
                                   └─────────┘    └─────────┘

ViewModel 通过 ModbusServiceBase 接口与服务层交互,不关心底层协议:

csharp 复制代码
// ViewModel 中
_service = new ModbusTcpService();     // 或者 ModbusRtuService
_service.Config.Timeout = 1000;        // 共同配置
_service.Connect();                     // 共同接口
ushort[] data = _service.ReadHoldingRegisters(0, 10);  // 共同方法

六、NuGet 包说明

xml 复制代码
<PackageReference Include="NModbus4" Version="2.1.0" />
<PackageReference Include="System.IO.Ports" Version="10.0.9" />
包名 用途 说明
NModbus4 Modbus 协议实现 提供 ModbusSerialMaster(RTU)和 ModbusIpMaster(TCP)
System.IO.Ports 串口通信 提供 SerialPortParityStopBits

下一篇博客三:ViewModel + XAML 前后端交互