【Qt开源项目】— ModbusScope-day 5

第5天:数据处理层深入攻略(ExpressionParser与GraphDataHandler)

目标:用5-6小时掌握表达式解析、寄存器替换、数学计算的核心流程,理解原始数据如何转化为最终曲线数值。


上午(3小时):ExpressionParser解析引擎

学习目标

理解如何将用户输入的表达式 {40001}+{40002*2} 转换为可计算的数学表达式。

详细攻略

  1. 从场景出发:理解表达式格式

    • 示例表达式{40001[@1][:f32b]} + {40002[@2][:32b]} * 2
    • 打开软件验证
      • 启动ModbusScope,添加一个寄存器
      • 在表达式编辑框中输入类似格式,观察软件如何接受这种语法
    • 对照笔记 :查看ExpressionParser类的_cRegisterFunctionTemplate,理解目标是将{...}替换为r(索引)
  2. 分析ExpressionParser类结构

    • 打开expressionparser.hexpressionparser.cpp

    • 对照笔记 :仔细阅读ExpressionParser类的"数据成员"部分

    • 找到关键成员

      cpp 复制代码
      QStringList _processedExpressions;  // 处理后的表达式列表
      QList<ModbusRegister> _registerList; // 解析出的寄存器列表
      QRegularExpression _findRegRegex;    // 查找寄存器表达式的正则
      QRegularExpression _regParseRegex;   // 解析单个寄存器的正则
  3. 查看正则表达式定义

    • 打开expressionregex.h(如果存在)或在代码中搜索cMatchRegistercParseReg
    • 理解正则模式 (笔记中提到):
      • 寄存器表达式形如:{40001[@1][:f32b]}{h0[@1][:f32b]}
      • 中括号内为可选项:连接编号[@N]和数据类型[:type]
    • 关键学习点:理解这种语法设计为什么便于用户使用(同时支持十进制地址和助记符地址)
  4. 深入解析算法

    • 找到ExpressionParser::processExpression函数

    • 逐步分析算法流程

      cpp 复制代码
      1. 使用_findRegRegex查找所有{...}模式
      2. 对每个匹配,使用_regParseRegex进一步解析
      3. 解析结果:地址、连接ID、数据类型
      4. 创建ModbusRegister临时对象
      5. 检查是否已存在于_registerList,不存在则添加
      6. 获取寄存器在列表中的索引
      7. 用r(索引)替换原表达式中的{...}
    • 动手实验 :在代码中添加调试输出,观察解析过程

      cpp 复制代码
      // 在processExpression函数中添加
      qDebug() << "原始表达式:" << graphExpr;
      qDebug() << "匹配到的寄存器:" << match.captured();
      qDebug() << "解析后的寄存器对象:" << modbusRegister;
      qDebug() << "替换为:" << QString("r(%1)").arg(regIdx);
  5. 测试不同表达式格式

    • 创建测试用例

      cpp 复制代码
      QStringList testExpressions = {
          "{40001}",                    // 简单地址
          "{40001} + {40002}",          // 两个寄存器相加
          "{h0[@1][:f32b]}",           // 助记符地址,连接1,浮点数
          "{30001[@2][:s16b] * 0.1}",  // 带乘法的表达式
          "sin({40001}) + {40002}"      // 使用数学函数
      };
    • 编写简单测试程序 (可选):创建一个小程序,测试ExpressionParser的解析结果

  6. 理解寄存器索引映射

    • 关键概念 :相同的寄存器(相同地址、相同连接、相同类型)只会出现在_registerList中一次
    • 思考:为什么需要这样设计?(避免重复读取同一个寄存器)
    • 验证 :表达式{40001} + {40001}中的两个{40001}会被映射到同一个索引

上午学习成果

  • ✅ 理解用户表达式的语法规则和设计原理
  • ✅ 掌握ExpressionParser{...}格式替换为r(索引)的完整流程
  • ✅ 理解寄存器去重机制和索引映射原理
  • ✅ 能手动解析简单表达式并确定寄存器索引
  • 检验 :表达式{40001[@1]} + {40002[@1]} * {40001[@1]}会被解析成几个不同的寄存器?替换后的表达式是什么?

下午(2-3小时):GraphDataHandler与QMuParser计算引擎

学习目标

掌握表达式如何从字符串变为实际数值的计算过程,理解数据流如何衔接。

详细攻略

  1. 理解GraphDataHandler的桥梁作用

    • 打开graphdatahandler.hgraphdatahandler.cpp

    • 对照笔记 :阅读GraphDataHandler类部分,理解它的三个关键容器:

      cpp 复制代码
      QList<ModbusRegister> _registerList;     // 寄存器列表
      QList<quint16> _registerIndexList;       // 寄存器索引列表(可能已弃用或笔记有误)
      QList<QMuParser> _expressionParserList;  // 表达式解析器列表
    • 注意 :根据实际代码,_registerIndexList可能不存在。以实际代码为准。

  2. 分析数据处理流程

    • 找到GraphDataHandler::processActiveRegisters函数
    • 理解调用时机 :何时会调用这个函数?
      • 图形激活状态变化时
      • 图形表达式修改时
      • 连接设置变化时
    • 跟踪流程
      1. GraphDataModel获取激活图形的表达式
      2. 创建ExpressionParser实例,解析表达式
      3. 获取解析后的寄存器列表和表达式列表
      4. 用处理后的表达式初始化QMuParser对象
  3. 深入QMuParser计算核心

    • 打开qmuparser.hqmuparser.cpp

    • 对照笔记 :仔细阅读QMuParser类部分

    • 理解静态数据成员

      cpp 复制代码
      static QList<Result<double>> _registerValues;  // 所有解析器共享的寄存器值
    • 关键问题 :为什么寄存器值要设计为静态成员?
      答案:所有表达式计算都需要访问相同的寄存器值,静态成员避免了重复传递数据。

  4. 分析计算回调机制

    • 找到QMuParser构造函数和mu::ParserRegister::setRegisterCallback

    • 理解回调链

      cpp 复制代码
      1. QMuParser构造函数设置回调函数为registerValue
      2. registerValue通过索引从静态_registerValues获取值
      3. mu::ParserRegister在计算表达式时调用此回调
    • 查看回调函数

      cpp 复制代码
      // 伪代码示意
      void registerValue(int idx, double* val, bool* ok) {
          if (idx >= 0 && idx < _registerValues.size()) {
              *val = _registerValues[idx].value();
              *ok = _registerValues[idx].isValid();
          }
      }
  5. 跟踪实时计算流程

    • 找到GraphDataHandler::handleRegisterData函数

    • 分析执行步骤

      cpp 复制代码
      1. 接收来自RegisterValueHandler的原始寄存器值
      2. 调用QMuParser::setRegistersData更新静态寄存器值
      3. 遍历_expressionParserList中的每个QMuParser
      4. 调用evaluate()计算表达式结果
      5. 收集所有结果,发出graphDataReady信号
    • 调试技巧:在此函数设置断点,观察每次数据到达时的计算过程

  6. 理解mu::ParserRegister的扩展功能

    • 查看muparserregister.hmuparserregister.cpp
    • 理解设计mu::ParserRegister继承自mu::ParserBase
    • 关键方法SetExpr设置表达式,Eval计算表达式
    • 扩展能力:除了基本数学运算,还支持哪些函数?(sin, cos, log等)
  7. 动手实验:观察表达式计算

    • 修改表达式 :在软件中设置不同的数学表达式
      • 简单加法:{40001} + {40002}
      • 带函数:sin({40001} * 3.14159 / 180)
      • 条件运算:{40001} > 100 ? {40001} : 0
    • 观察计算 :在QMuParser::evaluate设置断点,查看不同表达式的计算过程

综合调试任务

  1. 设置完整的断点链

    cpp 复制代码
    // 从接收到数据到计算出结果
    GraphDataHandler::handleRegisterData
    QMuParser::setRegistersData (静态方法)
    QMuParser::evaluate
    mu::ParserRegister::Eval (第三方库)
    GraphDataHandler::graphDataReady (信号发射处)
  2. 创建测试场景

    • 配置2个寄存器:40001(值为10),40002(值为20)
    • 设置表达式:{40001} + {40002} * 2
    • 预期结果:10 + 20*2 = 50
  3. 调试观察

    • 逐步执行,观察寄存器值如何传递
    • 查看_registerValues静态成员的变化
    • 观察回调函数registerValue被调用的次数和参数
  4. 异常情况测试

    • 寄存器值无效:模拟一个寄存器读取失败,观察表达式计算结果
    • 语法错误表达式 :输入{40001} + (不完整表达式),观察错误处理
    • 除零错误:表达式包含除法且除数为0的情况

数据处理层学习总结

核心概念掌握:

  1. 表达式解析双阶段

    • 阶段一:ExpressionParser将用户友好语法转换为机器友好语法
    • 阶段二:QMuParser(基于muParser)执行数学计算
  2. 数据流清晰分离

    • 寄存器值管理:GraphDataHandler负责接收和分发
    • 表达式管理:每个激活图形对应一个QMuParser实例
    • 值共享机制:静态成员_registerValues确保所有表达式使用相同数据
  3. 扩展性设计

    • 语法易于扩展:通过正则表达式可支持新格式
    • 计算能力强大:借助muParser库支持复杂数学运算
    • 错误处理完善:无效寄存器值不会导致崩溃

典型问题解答:

  • Q : 如果表达式包含10个{40001}引用,这个寄存器会被读取几次?
    A : 只读取1次。ExpressionParser会去重,所有引用指向同一个寄存器索引。

  • Q : 表达式计算是同步还是异步的?
    A : 在handleRegisterData中是同步计算的,但这个过程很快,不会阻塞UI。

  • Q : 如何添加自定义函数?
    A : 可以扩展mu::ParserRegister,添加新的函数定义。

实际应用思考:

  1. 性能优化:表达式解析只在配置改变时进行,计算时直接使用预编译的解析器
  2. 错误恢复:单个寄存器读取失败不会影响其他寄存器的计算
  3. 灵活性:支持复杂的数学运算和条件判断,满足各种数据处理需求

今日完整成果

  • ✅ 掌握从用户表达式到可执行代码的完整转换流程
  • ✅ 理解静态寄存器值共享机制的设计原理
  • ✅ 能解释回调函数如何将寄存器索引映射到实际数值
  • ✅ 掌握表达式计算过程中的错误处理机制
  • ✅ 能设计测试用例验证表达式解析和计算的正确性

明日预告 :第6天将进入数据模型层(GraphDataModel),学习数据如何存储、组织,以及模型如何与视图交互。这是连接数据处理和图形显示的关键桥梁。

建议行动:晚上可以尝试修改一个简单表达式,观察软件行为变化,巩固今天所学。

相关推荐
老秦包你会5 小时前
QT第五课------QT系统相关------线程
开发语言·qt
lkbhua莱克瓦245 小时前
IO练习——网络爬虫(爬取数据)
java·开发语言·爬虫·io流练习·java练习
net3m335 小时前
雅特力单片机用串口USART_INT_TDE中断比用USART_INT_TRAC的 发送效率要高
java·开发语言·算法
爱打代码的小林6 小时前
python基础(逻辑回归例题)
开发语言·python·逻辑回归
一过菜只因6 小时前
JavaWeb后端(spring--boot)
java·开发语言
五仁火烧6 小时前
安装rust开发环境
开发语言·后端·rust
Yue丶越6 小时前
【C语言】动态内存管理
c语言·开发语言
Edward111111116 小时前
普通java项目转为maven项目 J文件后缀.java变C文件
java·开发语言·maven
赵谨言6 小时前
基于OpenCV的图像梯度与边缘检测研究
大数据·开发语言·经验分享·python