四轴运动控制系统 — 博客一:数据模型层 (Models)

四轴运动控制系统 --- 博客一:数据模型层 (Models)

本篇涵盖 Models 目录下的三个核心数据类:CommConfig(通信配置)、AxisStatus 枚举(轴状态)、AxisData(轴数据容器)、AxisParam(轴参数)。


一、项目分层概览

复制代码
UpperMachine/
├── Models/              ← 数据模型(本篇重点)
│   ├── CommConfig.cs    ← 通信配置(TCP/RTU 参数)
│   ├── AxisData.cs      ← 轴状态 + 数据容器
│   └── AxisParam.cs     ← 轴 12 个实时参数(带通知)
├── Services/            ← 服务层(下一篇)
│   ├── ModbusServiceBase.cs
│   ├── ModbusRtuService.cs
│   └── ModbusTcpService.cs
├── ViewModels/          ← 业务逻辑层(第三篇)
│   ├── MainViewModel.cs
│   └── RelayCommand.cs
├── View/                ← 页面
│   └── HomePage.xaml
├── MainWindow.xaml      ← 主窗口
└── App.xaml             ← 全局资源

核心思路:数据模型只存数据 + 属性变更通知,不涉及任何 UI 或通信逻辑。


二、CommConfig.cs --- 通信配置模型

csharp 复制代码
// 引入 System.IO.Ports 命名空间,提供 Parity(校验位)和 StopBits(停止位)枚举
using System.IO.Ports;

namespace UpperMachine.Models
{
    /// <summary>
    /// 通信配置类
    /// 存储 TCP 和 RTU 两种连接方式的所有参数。
    /// 不包含通信逻辑,仅作为数据传输对象(DTO)。
    /// </summary>
    public class CommConfig
    {
        // ========== TCP 参数 ==========
        /// <summary>目标 PLC 的 IP 地址,默认值 192.168.1.100</summary>
        public string IpAddress { get; set; } = "192.168.1.100";

        /// <summary>TCP 端口号,Modbus TCP 标准端口为 502</summary>
        public int TcpPort { get; set; } = 502;

        // ========== RTU(串口)参数 ==========
        /// <summary>串口号,如 "COM1"、"COM3"</summary>
        public string PortName { get; set; } = "COM1";

        /// <summary>波特率,常用值 9600 / 19200 / 38400 / 115200</summary>
        public int BaudRate { get; set; } = 9600;

        /// <summary>数据位,通常为 8</summary>
        public int DataBits { get; set; } = 8;

        /// <summary>
        /// 校验位(枚举类型)
        /// None = 无校验, Odd = 奇校验, Even = 偶校验
        /// </summary>
        public Parity Parity { get; set; } = Parity.None;

        /// <summary>
        /// 停止位(枚举类型)
        /// One = 1位, Two = 2位, None = 无
        /// </summary>
        public StopBits StopBits { get; set; } = StopBits.One;

        // ========== 通用参数(TCP 和 RTU 共用) ==========
        /// <summary>Modbus 从站 ID,范围 1~247</summary>
        public byte SlaveId { get; set; } = 1;

        /// <summary>
        /// 通信超时时间(毫秒)
        /// TCP:连接 + 读写超时
        /// RTU:串口读写超时
        /// </summary>
        public int Timeout { get; set; } = 1000;

        /// <summary>"TCP" 或 "RTU",决定使用哪种连接方式</summary>
        public string ConnectionType { get; set; } = "TCP";
    }
}

CommConfig 设计要点

要点 说明
枚举类型 ParityStopBits 直接使用 System.IO.Ports 命名空间下的枚举,传给 SerialPort 构造函数时无需额外转换
默认值 每个属性都有合理的默认值,确保未配置时也能编译运行
纯数据 没有方法、没有逻辑,只存数据 ------ 符合单一职责原则

三、AxisStatus 枚举 --- 轴状态

csharp 复制代码
namespace UpperMachine.Models
{
    /// <summary>
    /// 轴运行状态枚举
    /// 用于 UI 底部状态栏和 HomePage 卡片显示
    /// </summary>
    public enum AxisStatus
    {
        StandBy,    // 待机(默认值,枚举第一个成员值为 0)
        Running,    // 运行中
        Stopped,    // 已停止
        Error       // 错误/报警
    }
}

为什么用枚举而不是字符串? 枚举编译时类型安全,XAML 绑定自动调用 .ToString() 显示文字,比硬编码字符串更可靠。


四、AxisData.cs --- 轴数据容器

csharp 复制代码
using UpperMachine.Models;

namespace UpperMachine.Models
{
    /// <summary>
    /// 单轴的数据容器
    /// 组合了状态(Status)和参数(Data),
    /// 供 MainViewModel 和 HomePage 绑定使用
    /// </summary>
    internal class AxisData
    {
        /// <summary>轴运行状态(StandBy / Running / Stopped / Error)</summary>
        public AxisStatus Status { get; set; }

        /// <summary>
        /// 轴实时参数(位置、速度、力矩等 12 个 float 值)
        /// 内嵌 AxisParam 对象,支持嵌套属性变更通知
        /// </summary>
        public AxisParam Data { get; set; } = new AxisParam();
    }
}

为什么用组合而不是继承?

AxisData 包含 Status + Data,其中 DataAxisParam 类型。这样 HomePage 绑定时写:

xml 复制代码
<TextBlock Text="{Binding Axis1Data.Status}" />         <!-- 显示状态 -->
<TextBlock Text="{Binding Axis1Data.Data.CurrentPos}" /><!-- 显示位置 -->

如果不嵌套,需要在 AxisData 里平铺 12 个 float 属性 + 1 个 Status,代码会膨胀 3 倍。


五、AxisParam.cs --- 轴参数(带通知)

csharp 复制代码
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace UpperMachine.Models
{
    /// <summary>
    /// 单轴 12 个运动参数
    /// 实现 INotifyPropertyChanged 以便属性变更时 UI 自动更新
    /// </summary>
    internal class AxisParam : INotifyPropertyChanged
    {
        // ========== 实时数据 ==========
        private float _currentPos;     // 当前位置
        private float _currentVel;     // 当前速度
        private float _currentTorque;  // 当前力矩

        // ========== 绝对运动参数 ==========
        private float _absoPos;        // 绝对定位目标位置
        private float _absoVel;        // 绝对运动速度
        private float _absoAccel;      // 绝对运动加速度
        private float _absoDecel;      // 绝对运动减速度

        // ========== 相对运动参数 ==========
        private float _relPos;         // 相对定位距离
        private float _relVel;         // 相对运动速度
        private float _relAccel;       // 相对运动加速度
        private float _relDecel;       // 相对运动减速度

        // 每个公开属性都 + 字段 + 通知
        public float CurrentPos  { get => _currentPos;  set { _currentPos = value;  OnPropertyChanged(); } }
        public float CurrentVel  { get => _currentVel;  set { _currentVel = value;  OnPropertyChanged(); } }
        public float CurrentTorque { get => _currentTorque; set { _currentTorque = value; OnPropertyChanged(); } }
        public float AbsoPos     { get => _absoPos;     set { _absoPos = value;     OnPropertyChanged(); } }
        public float AbsoVel     { get => _absoVel;     set { _absoVel = value;     OnPropertyChanged(); } }
        public float AbsoAccel   { get => _absoAccel;   set { _absoAccel = value;   OnPropertyChanged(); } }
        public float AbsoDecel   { get => _absoDecel;   set { _absoDecel = value;   OnPropertyChanged(); } }
        public float RelPos      { get => _relPos;      set { _relPos = value;      OnPropertyChanged(); } }
        public float RelVel      { get => _relVel;      set { _relVel = value;      OnPropertyChanged(); } }
        public float RelAccel    { get => _relAccel;    set { _relAccel = value;    OnPropertyChanged(); } }
        public float RelDecel    { get => _relDecel;    set { _relDecel = value;    OnPropertyChanged(); } }

        // INotifyPropertyChanged 接口必需的事件
        public event PropertyChangedEventHandler? PropertyChanged;

        /// <summary>
        /// 触发属性变更通知
        /// [CallerMemberName] 自动填充调用方属性名,无需手动传参
        /// </summary>
        protected void OnPropertyChanged([CallerMemberName] string? name = null)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

INotifyPropertyChanged 为什么必要?

WPF 绑定默认只读取一次属性值。没有 INotifyPropertyChanged,当 PLC 数据更新时,UI 不会刷新。有了它,每次 set 都会通知 UI 重新读取。

CallerMemberName 的妙用

csharp 复制代码
// 不用写属性名
public float CurrentPos { set { _currentPos = value; OnPropertyChanged(); } }
// 编译器自动展开为:
public float CurrentPos { set { _currentPos = value; OnPropertyChanged("CurrentPos"); } }

六、关键设计决策总结

决策 理由
CommConfig 单独一个类 与服务层解耦,方便未来从配置文件/数据库加载
AxisStatus 用枚举 类型安全,XAML 直接显示文字
AxisData 组合 + 嵌套 避免 12 个 float 平铺在 ViewModel 里
AxisParam 独立通知 嵌套属性变化时 UI 自动更新
默认值全部初始化 new AxisData() / new CommConfig() 即可用

下一篇博客二:服务层 --- Modbus 通信基类与 RTU/TCP 实现