【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),学习数据如何存储、组织,以及模型如何与视图交互。这是连接数据处理和图形显示的关键桥梁。

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

相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00614 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术14 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript