Modbus 数据采集工具项目总结
📋 项目概述
项目目标
开发一个基于 WinForms 的 Modbus 数据采集工具,支持 Modbus TCP 和 RTU 两种通信方式,能够实时采集、显示和导出 Modbus 设备的寄存器数据。
功能需求
- ✅ 支持 Modbus TCP 和 RTU 两种连接方式
- ✅ 实时读取多个寄存器数据
- ✅ 数据可视化显示
- ✅ 数据导出为 CSV 格式
- ✅ 日志记录功能
- ✅ 友好的用户界面
🔍 项目分析
为什么需要这个工具?
在工业自动化领域,Modbus 是最常用的通信协议之一。开发人员经常需要:
- 监控设备状态
- 采集传感器数据
- 调试设备通信
- 分析历史数据
技术选型
- 开发语言:C#
- 界面框架:WinForms
- 通信库:NModbus4(开源的 Modbus 库)
- 目标框架:.NET 8.0 Windows
架构设计原则
采用分层架构,各层职责明确:
┌─────────────────────────────────────┐
│ UI Layer (Form1.cs) │ 用户界面和交互
├─────────────────────────────────────┤
│ Service Layer (DataService.cs) │ 业务逻辑和数据采集
├─────────────────────────────────────┤
│ Communication Layer │ Modbus 通信
│ (ModbusClient.cs) │
├─────────────────────────────────────┤
│ Model Layer │ 数据模型
│ (RegisterConfig.cs, │
│ RegisterData.cs) │
├─────────────────────────────────────┤
│ Utility Layer │ 工具类
│ (Logger.cs, CsvExporter.cs) │
└─────────────────────────────────────┘
分层架构的优点:
- 职责清晰:每层只负责自己的功能
- 易于维护:修改某一层不影响其他层
- 便于测试:可以单独测试每一层
- 可扩展性:后续可以轻松添加新功能
🛠️ 实现步骤详解
第一步:数据模型设计
1.1 创建数据类型枚举
知识点:枚举(Enum)
枚举是一种值类型,用于定义一组命名的常量。在 Modbus 中,不同的数据类型需要不同的处理方式。
public enum DataType
{
UInt16, // 无符号16位整数 (0-65535)
Int16, // 有符号16位整数 (-32768-32767)
UInt32, // 无符号32位整数 (占用2个寄存器)
Int32, // 有符号32位整数 (占用2个寄存器)
Float, // 单精度浮点数 (占用2个寄存器)
}
为什么要用枚举?
- 提高代码可读性
- 避免使用魔法数字
- 类型安全,编译时检查
1.2 创建寄存器配置类
知识点:类(Class)和属性(Property)
类是对象的模板,属性是类的特征。RegisterConfig 类描述了一个寄存器的配置信息。
public class RegisterConfig
{
/// <summary>
/// 寄存器地址
/// </summary>
public ushort Address { get; set; }
/// <summary>
/// 寄存器名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 数据长度(占用几个寄存器)
/// </summary>
public ushort WordCount { get; set; } = 1;
/// <summary>
/// 数据类型
/// </summary>
public DataType DataType { get; set; } = DataType.UInt16;
/// <summary>
/// 缩放因子
/// </summary>
public double Scale { get; set; } = 1.0;
/// <summary>
/// 偏移量
/// </summary>
public double Offset { get; set; } = 0.0;
/// <summary>
/// 小数位数
/// </summary>
public int DecimalPlaces { get; set; } = 0;
/// <summary>
/// 单位
/// </summary>
public string Unit { get; set; } = string.Empty;
}
知识点:自动属性
public ushort Address { get; set; }
这种写法称为自动属性,编译器会自动生成私有字段和 get/set 方法,简化代码。
知识点:默认值
public double Scale { get; set; } = 1.0;
在属性声明时可以直接设置默认值,这样在创建对象时,这些属性会自动初始化为默认值。
1.3 实现构造函数
知识点:构造函数(Constructor)
构造函数是在创建对象时自动调用的方法,用于初始化对象的状态。
public RegisterConfig(ushort address, string name, DataType dataType = DataType.UInt16)
{
Address = address;
Name = name;
DataType = dataType;
WordCount = GetWordCountFromDataType(dataType);
}
知识点:可选参数
DataType dataType = DataType.UInt16
这个参数有默认值,调用时可以不传这个参数:
var config1 = new RegisterConfig(0, "温度"); // 使用默认的 UInt16
var config2 = new RegisterConfig(0, "温度", DataType.Int32); // 指定类型
1.4 实现数据类型转换逻辑
知识点:Switch 表达式
这是 C# 8.0 引入的特性,比传统的 switch 语句更简洁。
private ushort GetWordCountFromDataType(DataType dataType)
{
return dataType switch
{
DataType.UInt16 => 1,
DataType.Int16 => 1,
DataType.UInt32 => 2,
DataType.Int32 => 2,
DataType.Float => 2,
};
}
知识点:位运算
在 Modbus 中,32位数据通常存储在两个连续的16位寄存器中,需要通过位运算组合。
case DataType.UInt32:
// 将两个16位寄存器组合成一个32位整数
// rawValues[0] 是高16位,rawValues[1] 是低16位
convertedValue = (uint)((rawValues[0] << 16) | rawValues[1]);
break;
位运算解释:
<< 16:左移16位,将高16位移到正确位置|:按位或运算,组合两个值
知识点:浮点数转换
浮点数在 Modbus 中以 IEEE 754 格式存储,需要特殊处理。
case DataType.Float:
uint intValue = (uint)((rawValues[0] << 16) | rawValues[1]);
byte[] bytes = BitConverter.GetBytes(intValue);
// 检查字节序
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
convertedValue = BitConverter.ToSingle(bytes, 0);
break;
知识点:字节序(Endianness)
- 大端序(Big Endian):高位字节在前,网络传输常用
- 小端序(Little Endian):低位字节在前,Intel x86 架构常用
BitConverter.IsLittleEndian:检查当前系统的字节序,如果不是大端序,需要反转字节数组。
1.5 实现数据验证
知识点:参数验证
在接收用户输入时,必须进行验证,防止无效数据。
public bool isValid()
{
if (string.IsNullOrWhiteSpace(Name)) return false;
if (Scale == 0) return false;
if (DecimalPlaces < 0) return false;
if (WordCount != 1 && WordCount != 2) return false;
return true;
}
public string GetValidationError()
{
if (string.IsNullOrWhiteSpace(Name))
return "名称不能为空";
if (Scale == 0)
return "缩放因子不能为0";
if (DecimalPlaces < 0)
return "小数位数不能为负数";
if (WordCount != 1 && WordCount != 2)
return "寄存器数量必须是1或2";
return string.Empty;
}
为什么要验证?
- 防止程序崩溃
- 提供友好的错误提示
- 保证数据质量
1.6 实现数据转换方法
知识点:类型转换和数据缩放
Modbus 设备返回的是原始值,需要转换为物理值。
public object ConvertValue(ushort[] rawValues)
{
// 1. 参数验证
if (rawValues == null)
throw new ArgumentException("寄存器数据不能为空");
if (rawValues.Length < WordCount)
throw new ArgumentException($"寄存器数据不足,需要 {WordCount} 个寄存器");
// 2. 根据数据类型转换原始值
object convertedValue;
switch (DataType)
{
case DataType.UInt16:
convertedValue = rawValues[0];
break;
case DataType.Int16:
convertedValue = (short)rawValues[0];
break;
// ... 其他类型
}
// 3. 应用缩放和偏移
if (convertedValue is short)
{
short value = (short)convertedValue;
value = (short)(value * Scale + Offset);
convertedValue = value;
}
// ... 其他类型
// 4. 返回转换后的值
return convertedValue;
}
缩放公式:
物理值 = (原始值 × 缩放因子) + 偏移量
示例:
- 原始值:250
- 缩放因子:0.1
- 偏移量:0
- 物理值:250 × 0.1 + 0 = 25.0(温度 25°C)
第二步:数据存储模型
2.1 创建数据存储类
知识点:时间戳和状态标记
RegisterData 类存储一次采集的数据,包含时间戳和有效性标记。
public class RegisterData
{
public RegisterConfig config { get; set; }
public ushort[] RawValues { get; set; }
public object ConvertedValue { get; set; }
public DateTime Timestamp { get; set; }
public bool IsValid { get; set; }
}
知识点:DateTime 结构体
DateTime 表示时间点,常用于记录事件发生时间。
this.Timestamp = DateTime.Now;
2.2 实现构造函数
public RegisterData(RegisterConfig config, ushort[] rawValues)
{
this.config = config;
this.RawValues = rawValues;
this.Timestamp = DateTime.Now;
this.ConvertedValue = config.ConvertValue(rawValues);
this.IsValid = true;
}
构造函数的作用:
- 初始化所有字段
- 自动进行数据转换
- 记录时间戳
- 标记数据为有效
2.3 实现格式化方法
知识点:字符串格式化
格式化方法将数值转换为指定格式的字符串。
public string GetFormattedValue()
{
if (!IsValid || ConvertedValue == null)
{
return "无效数据";
}
// 获取小数位数
int decimalPlaces = config.DecimalPlaces;
// 转换为 double 进行格式化
double value = Convert.ToDouble(ConvertedValue);
// 格式化为字符串,保留指定小数位
return value.ToString($"F{decimalPlaces}");
}
字符串插值和格式化:
$"F{decimalPlaces}"
这是字符串插值,F 表示固定点数格式,{decimalPlaces} 是变量值。
示例:
double value = 25.567;
int decimalPlaces = 2;
value.ToString($"F{decimalPlaces}"); // 返回 "25.57"
知识点:条件运算符(三元运算符)
string unitStr = string.IsNullOrEmpty(config.Unit) ? "" : $" {config.Unit}";
这是简化的 if-else 语句:
string unitStr;
if (string.IsNullOrEmpty(config.Unit))
unitStr = "";
else
unitStr = $" {config.Unit}";
第三步:日志工具实现
3.1 创建日志级别枚举
public enum LogLevel
{
Info,
Warning,
Error
}
3.2 实现日志类
知识点:静态类和静态方法
静态类不能被实例化,所有成员都是静态的。Logger 设计为静态类,方便全局调用。
public class Logger
{
private static readonly string LogFilePath = "modbus_log.txt";
private static readonly object _lock = new object();
public static void Log(LogLevel level, string message)
{
lock (_lock)
{
string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
File.AppendAllText(LogFilePath, logEntry + Environment.NewLine);
}
}
public static void Info(string message) => Log(LogLevel.Info, message);
public static void Warning(string message) => Log(LogLevel.Warning, message);
public static void Error(string message) => Log(LogLevel.Error, message);
}
知识点:线程安全
在多线程环境下,多个线程可能同时写入日志文件,导致数据混乱。使用 lock 关键字确保同一时间只有一个线程能执行代码块。
lock (_lock)
{
// 这段代码同一时间只能被一个线程执行
}
知识点:File 类
File 类提供了静态方法用于文件操作,这里使用 File.AppendAllText 追加文本到文件。
知识点:表达式体方法
public static void Info(string message) => Log(LogLevel.Info, message);
这是 C# 6.0 引入的语法,对于简单的方法,可以用 => 简化。
第四步:Modbus 通信客户端实现
4.1 定义连接类型
public enum ConnectionType
{
Tcp,
Rtu
}
4.2 创建 ModbusClient 类
知识点:依赖注入
ModbusClient 不直接创建 NModbus 的对象,而是通过外部传入,提高了可测试性和灵活性。
public class ModbusClient
{
private TcpClient? _tcpClient;
private SerialPort? _serialPort;
private ModbusIpMaster? _tcpMaster;
private ModbusSerialMaster? _rtuMaster;
private readonly object _lock = new object();
public bool IsConnected { get; private set; }
}
知识点:可空引用类型(Nullable Reference Types)
private TcpClient? _tcpClient;
? 表示这个引用类型可以为 null,需要在代码中检查 null。
4.3 实现 TCP 连接
知识点:TCP 通信
TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。
public bool ConnectTcp(string ipAddress, int port = 502)
{
try
{
lock (_lock)
{
// 如果已连接,先断开
if (IsConnected)
{
Disconnect();
}
// 创建 TCP 客户端
_tcpClient = new TcpClient();
// 设置超时时间
_tcpClient.ReceiveTimeout = 3000; // 3秒
_tcpClient.SendTimeout = 3000;
// 连接到服务器
_tcpClient.Connect(ipAddress, port);
// 创建 Modbus TCP 主站
_tcpMaster = ModbusIpMaster.CreateIp(_tcpClient);
_tcpMaster.Transport.ReadTimeout = 3000;
_tcpMaster.Transport.WriteTimeout = 3000;
IsConnected = true;
Logger.Info($"成功连接到 TCP 设备: {ipAddress}:{port}");
return true;
}
}
catch (Exception ex)
{
Logger.Error($"TCP 连接失败: {ex.Message}");
IsConnected = false;
return false;
}
}
知识点:异常处理
try
{
// 可能出错的代码
}
catch (Exception ex)
{
// 处理异常
}
异常处理用于捕获和处理运行时错误,防止程序崩溃。
知识点:超时设置
设置超时时间可以防止程序长时间等待无响应的设备。
4.4 实现 RTU 连接
知识点:串口通信
RTU 模式使用串口通信,需要配置串口参数。
public bool ConnectRtu(string portName, int baudRate = 9600)
{
try
{
lock (_lock)
{
if (IsConnected)
{
Disconnect();
}
// 创建串口
_serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
_serialPort.ReadTimeout = 3000;
_serialPort.WriteTimeout = 3000;
// 打开串口
_serialPort.Open();
// 创建 Modbus RTU 主站
_rtuMaster = ModbusSerialMaster.CreateRtu(_serialPort);
_rtuMaster.Transport.ReadTimeout = 3000;
_rtuMaster.Transport.WriteTimeout = 3000;
IsConnected = true;
Logger.Info($"成功连接到 RTU 设备: {portName}, 波特率: {baudRate}");
return true;
}
}
catch (Exception ex)
{
Logger.Error($"RTU 连接失败: {ex.Message}");
IsConnected = false;
return false;
}
}
串口参数说明:
portName:串口名称(COM1、COM2 等)baudRate:波特率(9600、19200 等)Parity.None:无奇偶校验8:8个数据位StopBits.One:1个停止位
4.5 实现断开连接
知识点:资源释放
使用完资源后必须释放,防止内存泄漏。
public void Disconnect()
{
lock (_lock)
{
try
{
// 关闭 TCP 连接
if (_tcpClient != null)
{
_tcpClient.Close();
_tcpClient.Dispose();
_tcpClient = null;
_tcpMaster = null;
}
// 关闭 RTU 连接
if (_serialPort != null)
{
_serialPort.Close();
_serialPort.Dispose();
_serialPort = null;
_rtuMaster = null;
}
IsConnected = false;
Logger.Info("已断开连接");
}
catch (Exception ex)
{
Logger.Error($"断开连接时出错: {ex.Message}");
}
}
}
知识点:Dispose 模式
Dispose() 方法用于释放非托管资源(如文件句柄、网络连接等)。
4.6 实现寄存器读取
知识点:Modbus 协议
Modbus 协议定义了主站和从站之间的通信规则。功能码 03 表示读取保持寄存器。
public ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count)
{
lock (_lock)
{
if (!IsConnected)
{
throw new InvalidOperationException("未连接到设备");
}
try
{
ushort[] values;
// 根据连接类型选择不同的 Master
if (_tcpMaster != null)
{
values = _tcpMaster.ReadHoldingRegisters(slaveId, startAddress, count);
}
else if (_rtuMaster != null)
{
values = _rtuMaster.ReadHoldingRegisters(slaveId, startAddress, count);
}
else
{
throw new InvalidOperationException("未找到有效的 Master");
}
Logger.Info($"读取寄存器成功: 从站={slaveId}, 地址={startAddress}, 数量={count}");
return values;
}
catch (Exception ex)
{
Logger.Error($"读取寄存器失败: {ex.Message}");
throw;
}
}
}
知识点:throw 和 throw ex
throw; // 保留原始堆栈跟踪
throw ex; // 重置堆栈跟踪
使用 throw 而不是 throw ex 可以保留原始的异常堆栈跟踪,便于调试。
第五步:数据采集服务实现
5.1 创建 DataService 类
知识点:事件(Event)
事件是一种特殊的委托,用于实现发布-订阅模式。
public class DataService
{
private readonly ModbusClient _modbusClient;
private readonly List<RegisterConfig> _configs;
private System.Timers.Timer? _collectionTimer;
private bool _isCollecting;
private byte _slaveId = 1;
// 定义事件
public event Action<List<RegisterData>>? DataUpdated;
}
知识点:事件的作用
- 解耦:发布者和订阅者不直接依赖
- 扩展性:可以有多个订阅者
- 异步通知:可以异步处理事件
5.2 实现配置管理
public void AddConfig(RegisterConfig config)
{
if (config == null)
throw new ArgumentNullException(nameof(config));
if (!config.isValid())
throw new ArgumentException($"配置无效: {config.GetValidationError()}");
// 检查是否已存在相同地址的配置
if (_configs.Any(c => c.Address == config.Address))
{
throw new ArgumentException($"地址 {config.Address} 的配置已存在");
}
_configs.Add(config);
Logger.Info($"添加寄存器配置: 地址={config.Address}, 名称={config.Name}");
}
知识点:LINQ(Language Integrated Query)
LINQ 提供了一种统一的方式来查询和操作数据。
_configs.Any(c => c.Address == config.Address)
Any() 方法检查集合中是否存在满足条件的元素。
5.3 实现定时采集
知识点:定时器(Timer)
定时器用于定期执行某个操作。
public void StartCollection(int intervalMs = 1000, byte slaveId = 1)
{
if (_isCollecting)
{
Logger.Warning("已经在采集中,无需重复启动");
return;
}
if (_configs.Count == 0)
{
Logger.Warning("没有配置任何寄存器,无法开始采集");
return;
}
_slaveId = slaveId;
_collectionTimer = new System.Timers.Timer(intervalMs);
_collectionTimer.Elapsed += CollectData;
_collectionTimer.AutoReset = true;
_collectionTimer.Start();
_isCollecting = true;
Logger.Info($"开始数据采集,间隔: {intervalMs}ms, 从站ID: {slaveId}");
}
知识点:事件订阅
_collectionTimer.Elapsed += CollectData;
+= 操作符用于订阅事件,-= 用于取消订阅。
知识点:AutoReset 属性
_collectionTimer.AutoReset = true;
true:定时器重复触发false:定时器只触发一次
5.4 实现数据采集方法
private void CollectData(object? sender, ElapsedEventArgs e)
{
try
{
if (!_modbusClient.IsConnected)
{
Logger.Warning("未连接到设备,跳过数据采集");
return;
}
var dataList = new List<RegisterData>();
// 逐个读取寄存器
foreach (var config in _configs)
{
try
{
// 读取寄存器
ushort[] rawValues = _modbusClient.ReadHoldingRegisters(
_slaveId,
config.Address,
config.WordCount
);
// 创建数据对象(会自动转换)
var data = new RegisterData(config, rawValues);
dataList.Add(data);
}
catch (Exception ex)
{
Logger.Error($"读取寄存器 {config.Address} 失败: {ex.Message}");
// 添加无效数据
var invalidData = new RegisterData(config, new ushort[config.WordCount]);
invalidData.IsValid = false;
dataList.Add(invalidData);
}
}
// 触发事件通知 UI
DataUpdated?.Invoke(dataList);
}
catch (Exception ex)
{
Logger.Error($"数据采集过程出错: {ex.Message}");
}
}
知识点:条件空运算符(?.)
DataUpdated?.Invoke(dataList);
如果 DataUpdated 不为 null,则调用 Invoke 方法;否则什么都不做。
知识点:foreach 循环
用于遍历集合中的每个元素。
5.5 停止采集
public void StopCollection()
{
if (!_isCollecting)
{
return;
}
_collectionTimer?.Stop();
_collectionTimer?.Dispose();
_collectionTimer = null;
_isCollecting = false;
Logger.Info("停止数据采集");
}
知识点:条件空运算符(?.)和空合并运算符(??)
_collectionTimer?.Stop();
等价于:
if (_collectionTimer != null)
_collectionTimer.Stop();
第六步:CSV 导出工具实现
6.1 创建 CsvExporter 类
public class CsvExporter
{
public static void ExportToCsv(string filePath, List<RegisterData> dataList)
{
try
{
using var writer = new StreamWriter(filePath, false, Encoding.UTF8);
// 写入表头
writer.WriteLine("时间,寄存器名称,地址,原始值,转换后值,单位");
// 写入数据
foreach (var data in dataList)
{
writer.WriteLine(BuildCsvLine(data));
}
Logger.Info($"数据导出成功: {filePath}");
}
catch (Exception ex)
{
Logger.Error($"导出 CSV 失败: {ex.Message}");
throw;
}
}
}
知识点:using 语句
using 语句确保在代码块结束后自动调用 Dispose() 方法,释放资源。
using var writer = new StreamWriter(filePath, false, Encoding.UTF8);
知识点:StreamWriter 类
StreamWriter 用于写入文本文件,这里使用 UTF-8 编码。
6.2 构建CSV行
private static string BuildCsvLine(RegisterData data)
{
string time = data.Timestamp.ToString("yyyy-MM-dd HH:mm:ss");
string name = data.config.Name;
string address = data.config.Address.ToString();
string rawValue = string.Join(",", data.RawValues);
string convertedValue = data.GetDisplayText();
string unit = data.config.Unit;
return $"{time},{name},{address},{rawValue},{convertedValue},{unit}";
}
知识点:字符串连接
string.Join(",", data.RawValues)
将数组元素用逗号连接成字符串。
知识点:字符串插值
$"{time},{name},{address},{rawValue},{convertedValue},{unit}"
将变量值直接嵌入到字符串中。
第七步:用户界面实现
7.1 初始化字段
private ModbusClient _modbusClient;
private DataService _dataService;
private List<RegisterConfig> _configs;
private List<RegisterData> _currentData;
private BindingSource _configBindingSource;
private BindingSource _dataBindingSource;
private void InitializeFields()
{
_modbusClient = new ModbusClient();
_dataService = new DataService(_modbusClient);
_configs = new List<RegisterConfig>();
_currentData = new List<RegisterData>();
_configBindingSource = new BindingSource();
_dataBindingSource = new BindingSource();
}
知识点:BindingSource 类
BindingSource 是 Windows Forms 数据绑定的桥梁,简化了数据绑定操作。
7.2 订阅数据更新事件
private void InitializeUI()
{
// 初始化数据类型下拉框
cmbDataType.DataSource = Enum.GetValues(typeof(DataType));
// 绑定数据源
_configBindingSource.DataSource = _configs;
dgvConfigs.DataSource = _configBindingSource;
_dataBindingSource.DataSource = _currentData;
dgvRealtimeData.DataSource = _dataBindingSource;
// 订阅数据更新事件
_dataService.DataUpdated += OnDataUpdated;
Logger.Info("应用程序启动");
}
知识点:数据绑定
将数据源绑定到控件,控件会自动显示数据的变化。
7.3 处理数据更新事件
知识点:线程安全更新 UI
Windows Forms 控件不是线程安全的,必须从创建控件的线程(UI 线程)更新 UI。
private void OnDataUpdated(List<RegisterData> dataList)
{
// 在 UI 线程中更新
if (InvokeRequired)
{
Invoke(new Action<List<RegisterData>>(OnDataUpdated), dataList);
return;
}
_currentData.Clear();
_currentData.AddRange(dataList);
_dataBindingSource.ResetBindings(false);
}
知识点:Invoke 方法
Invoke(new Action<List<RegisterData>>(OnDataUpdated), dataList);
Invoke 方法将在 UI 线程上执行指定的委托。
知识点:Action 委托
Action<T> 是系统定义的委托类型,表示一个无返回值的方法。
7.4 实现连接按钮事件
private void btnConnect_Click(object sender, EventArgs e)
{
try
{
if (rbTcp.Checked)
{
string ip = txtIpAddress.Text;
int port = int.Parse(txtPort.Text);
if (_modbusClient.ConnectTcp(ip, port))
{
Logger.Info($"成功连接到 TCP 设备: {ip}:{port}");
MessageBox.Show("连接成功!", "提示");
}
else
{
MessageBox.Show("连接失败!", "错误");
}
}
else
{
string portName = txtComPort.Text;
int baudRate = int.Parse(txtBaudRate.Text);
if (_modbusClient.ConnectRtu(portName, baudRate))
{
Logger.Info($"成功连接到 RTU 设备: {portName}");
MessageBox.Show("连接成功!", "提示");
}
else
{
MessageBox.Show("连接失败!", "错误");
}
}
}
catch (Exception ex)
{
Logger.Error($"连接失败: {ex.Message}");
MessageBox.Show($"连接失败: {ex.Message}", "错误");
}
}
知识点:事件处理程序
事件处理程序是响应事件的回调方法。
private void btnConnect_Click(object sender, EventArgs e)
sender:事件源(触发事件的对象)e:事件参数(包含事件相关的数据)
7.5 实现导出功能
private void btnExportCsv_Click(object sender, EventArgs e)
{
try
{
if (_currentData.Count == 0)
{
MessageBox.Show("没有数据可导出!", "提示");
return;
}
SaveFileDialog saveFileDialog = new SaveFileDialog
{
Filter = "CSV文件|*.csv",
Title = "导出数据为CSV",
FileName = $"modbus_data_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
};
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
CsvExporter.ExportToCsv(saveFileDialog.FileName, _currentData);
Logger.Info($"数据导出成功: {saveFileDialog.FileName}");
MessageBox.Show("导出成功!", "提示");
}
}
catch (Exception ex)
{
Logger.Error($"导出失败: {ex.Message}");
MessageBox.Show($"导出失败: {ex.Message}", "错误");
}
}
知识点:SaveFileDialog
SaveFileDialog 是 Windows Forms 提供的文件保存对话框。
知识点:对象初始化器
new SaveFileDialog
{
Filter = "CSV文件|*.csv",
Title = "导出数据为CSV",
FileName = $"modbus_data_{DateTime.Now:yyyyMMdd_HHmmss}.csv"
}
可以在创建对象时直接设置属性值,简化代码。
📊 项目总结
核心技术点
-
面向对象编程
- 类和对象
- 封装、继承、多态
- 属性和方法
-
数据类型和转换
- 基本数据类型
- 枚举
- 类型转换
- 位运算
-
集合和 LINQ
- List
- LINQ 查询
- 委托和事件
-
文件 I/O
- 文件读写
- CSV 格式
- 编码处理
-
网络通信
- TCP 协议
- 串口通信
- Modbus 协议
-
多线程
- 线程安全
- 锁机制
- UI 线程更新
-
异常处理
- try-catch
- 异常传播
- 错误日志
-
WinForms 开发
- 控件使用
- 事件处理
- 数据绑定
- 对话框
设计模式应用
-
分层架构
- UI 层
- 业务逻辑层
- 数据访问层
- 工具层
-
观察者模式
- 事件和委托
- 发布-订阅
-
单例模式
- Logger 静态类
-
工厂模式
- ModbusMaster 创建
最佳实践
-
代码规范
- 命名规范
- 注释完整
- 代码格式化
-
错误处理
- 全面的异常捕获
- 友好的错误提示
- 详细的日志记录
-
性能优化
- 批量读取
- 资源释放
- 避免重复操作
-
可维护性
- 模块化设计
- 职责分离
- 代码复用
学习收获
通过这个项目,你学到了:
-
完整的软件开发流程
- 需求分析
- 架构设计
- 编码实现
- 测试调试
-
工业通信协议
- Modbus 协议原理
- TCP 和 RTU 区别
- 寄存器操作
-
C# 高级特性
- LINQ
- 委托和事件
- 异步编程
- 泛型
-
软件架构
- 分层架构
- 设计模式
- 依赖注入
-
调试技巧
- 日志记录
- 异常追踪
- 性能分析
🎯 后续改进方向
-
功能增强
- 支持更多 Modbus 功能码
- 添加数据图表显示
- 实现数据历史记录
-
性能优化
- 批量读取优化
- 异步采集
- 数据缓存
-
用户体验
- 配置文件保存
- 主题切换
- 快捷键支持
-
测试完善
- 单元测试
- 集成测试
- 性能测试
📚 参考资料
结语
这个 Modbus 数据采集工具项目涵盖了 C# 开发的许多核心概念和最佳实践。通过从零开始构建这个项目,你不仅学会了 Modbus 协议的使用,还掌握了软件架构设计、数据模型设计、异常处理、多线程编程等重要技能。
记住,软件开发是一个持续学习和改进的过程。这个项目可以作为一个起点,你可以根据自己的需求和兴趣继续扩展和完善它。
祝你在编程学习的道路上越走越远!🚀

1095

被折叠的 条评论
为什么被折叠?



