Modbus数据采集工具项目总结

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

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. 职责清晰:每层只负责自己的功能
  2. 易于维护:修改某一层不影响其他层
  3. 便于测试:可以单独测试每一层
  4. 可扩展性:后续可以轻松添加新功能

🛠️ 实现步骤详解

第一步:数据模型设计

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"
}

可以在创建对象时直接设置属性值,简化代码。


📊 项目总结

核心技术点

  1. 面向对象编程

    • 类和对象
    • 封装、继承、多态
    • 属性和方法
  2. 数据类型和转换

    • 基本数据类型
    • 枚举
    • 类型转换
    • 位运算
  3. 集合和 LINQ

    • List
    • LINQ 查询
    • 委托和事件
  4. 文件 I/O

    • 文件读写
    • CSV 格式
    • 编码处理
  5. 网络通信

    • TCP 协议
    • 串口通信
    • Modbus 协议
  6. 多线程

    • 线程安全
    • 锁机制
    • UI 线程更新
  7. 异常处理

    • try-catch
    • 异常传播
    • 错误日志
  8. WinForms 开发

    • 控件使用
    • 事件处理
    • 数据绑定
    • 对话框

设计模式应用

  1. 分层架构

    • UI 层
    • 业务逻辑层
    • 数据访问层
    • 工具层
  2. 观察者模式

    • 事件和委托
    • 发布-订阅
  3. 单例模式

    • Logger 静态类
  4. 工厂模式

    • ModbusMaster 创建

最佳实践

  1. 代码规范

    • 命名规范
    • 注释完整
    • 代码格式化
  2. 错误处理

    • 全面的异常捕获
    • 友好的错误提示
    • 详细的日志记录
  3. 性能优化

    • 批量读取
    • 资源释放
    • 避免重复操作
  4. 可维护性

    • 模块化设计
    • 职责分离
    • 代码复用

学习收获

通过这个项目,你学到了:

  1. 完整的软件开发流程

    • 需求分析
    • 架构设计
    • 编码实现
    • 测试调试
  2. 工业通信协议

    • Modbus 协议原理
    • TCP 和 RTU 区别
    • 寄存器操作
  3. C# 高级特性

    • LINQ
    • 委托和事件
    • 异步编程
    • 泛型
  4. 软件架构

    • 分层架构
    • 设计模式
    • 依赖注入
  5. 调试技巧

    • 日志记录
    • 异常追踪
    • 性能分析

🎯 后续改进方向

  1. 功能增强

    • 支持更多 Modbus 功能码
    • 添加数据图表显示
    • 实现数据历史记录
  2. 性能优化

    • 批量读取优化
    • 异步采集
    • 数据缓存
  3. 用户体验

    • 配置文件保存
    • 主题切换
    • 快捷键支持
  4. 测试完善

    • 单元测试
    • 集成测试
    • 性能测试

📚 参考资料


结语

这个 Modbus 数据采集工具项目涵盖了 C# 开发的许多核心概念和最佳实践。通过从零开始构建这个项目,你不仅学会了 Modbus 协议的使用,还掌握了软件架构设计、数据模型设计、异常处理、多线程编程等重要技能。

记住,软件开发是一个持续学习和改进的过程。这个项目可以作为一个起点,你可以根据自己的需求和兴趣继续扩展和完善它。

祝你在编程学习的道路上越走越远!🚀

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值