四轴运动控制系统 --- 博客一:数据模型层 (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 设计要点
| 要点 | 说明 |
|---|---|
| 枚举类型 | Parity 和 StopBits 直接使用 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,其中 Data 是 AxisParam 类型。这样 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() 即可用 |