四轴运动控制系统 --- 博客二:服务层 (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 | 串口通信 | 提供 SerialPort、Parity、StopBits 等 |