第5天:数据处理层深入攻略(ExpressionParser与GraphDataHandler)
目标:用5-6小时掌握表达式解析、寄存器替换、数学计算的核心流程,理解原始数据如何转化为最终曲线数值。
上午(3小时):ExpressionParser解析引擎
学习目标
理解如何将用户输入的表达式{40001}+{40002*2}转换为可计算的数学表达式。
详细攻略
从场景出发:理解表达式格式
- 示例表达式:
{40001[@1][:f32b]} + {40002[@2][:32b]} * 2 - 打开软件验证:
- 启动ModbusScope,添加一个寄存器
- 在表达式编辑框中输入类似格式,观察软件如何接受这种语法
- 对照笔记:查看
ExpressionParser类的_cRegisterFunctionTemplate,理解目标是将{...}替换为r(索引)
- 示例表达式:
分析ExpressionParser类结构
- 打开:
expressionparser.h和expressionparser.cpp - 对照笔记:仔细阅读
ExpressionParser类的"数据成员"部分 - 找到关键成员:
QStringList _processedExpressions;// 处理后的表达式列表QList<ModbusRegister>_registerList;// 解析出的寄存器列表QRegularExpression _findRegRegex;// 查找寄存器表达式的正则QRegularExpression _regParseRegex;// 解析单个寄存器的正则
- 打开:
查看正则表达式定义
- 打开:
expressionregex.h(如果存在)或在代码中搜索cMatchRegister、cParseReg - 理解正则模式(笔记中提到):
- 寄存器表达式形如:
{40001[@1][:f32b]}或{h0[@1][:f32b]} - 中括号内为可选项:连接编号
[@N]和数据类型[:type]
- 寄存器表达式形如:
- 关键学习点:理解这种语法设计为什么便于用户使用(同时支持十进制地址和助记符地址)
- 打开:
深入解析算法
- 找到:
ExpressionParser::processExpression函数 - 逐步分析算法流程:
1.使用_findRegRegex查找所有{...}模式2.对每个匹配,使用_regParseRegex进一步解析3.解析结果:地址、连接ID、数据类型4.创建ModbusRegister临时对象5.检查是否已存在于_registerList,不存在则添加6.获取寄存器在列表中的索引7.用r(索引)替换原表达式中的{...} - 动手实验:在代码中添加调试输出,观察解析过程
// 在processExpression函数中添加qDebug()<<"原始表达式:"<<graphExpr;qDebug()<<"匹配到的寄存器:"<<match.captured();qDebug()<<"解析后的寄存器对象:"<<modbusRegister;qDebug()<<"替换为:"<<QString("r(%1)").arg(regIdx);
- 找到:
测试不同表达式格式
- 创建测试用例:
QStringList testExpressions={"{40001}",// 简单地址"{40001} + {40002}",// 两个寄存器相加"{h0[@1][:f32b]}",// 助记符地址,连接1,浮点数"{30001[@2][:s16b] * 0.1}",// 带乘法的表达式"sin({40001}) + {40002}"// 使用数学函数}; - 编写简单测试程序(可选):创建一个小程序,测试
ExpressionParser的解析结果
- 创建测试用例:
理解寄存器索引映射
- 关键概念:相同的寄存器(相同地址、相同连接、相同类型)只会出现在
_registerList中一次 - 思考:为什么需要这样设计?(避免重复读取同一个寄存器)
- 验证:表达式
{40001} + {40001}中的两个{40001}会被映射到同一个索引
- 关键概念:相同的寄存器(相同地址、相同连接、相同类型)只会出现在
上午学习成果
- ✅ 理解用户表达式的语法规则和设计原理
- ✅ 掌握
ExpressionParser将{...}格式替换为r(索引)的完整流程 - ✅ 理解寄存器去重机制和索引映射原理
- ✅ 能手动解析简单表达式并确定寄存器索引
- 检验:表达式
{40001[@1]} + {40002[@1]} * {40001[@1]}会被解析成几个不同的寄存器?替换后的表达式是什么?
下午(2-3小时):GraphDataHandler与QMuParser计算引擎
学习目标
掌握表达式如何从字符串变为实际数值的计算过程,理解数据流如何衔接。
详细攻略
理解GraphDataHandler的桥梁作用
- 打开:
graphdatahandler.h和graphdatahandler.cpp - 对照笔记:阅读
GraphDataHandler类部分,理解它的三个关键容器:QList<ModbusRegister>_registerList;// 寄存器列表QList<quint16>_registerIndexList;// 寄存器索引列表(可能已弃用或笔记有误)QList<QMuParser>_expressionParserList;// 表达式解析器列表 - 注意:根据实际代码,
_registerIndexList可能不存在。以实际代码为准。
- 打开:
分析数据处理流程
- 找到:
GraphDataHandler::processActiveRegisters函数 - 理解调用时机:何时会调用这个函数?
- 图形激活状态变化时
- 图形表达式修改时
- 连接设置变化时
- 跟踪流程:
- 从
GraphDataModel获取激活图形的表达式 - 创建
ExpressionParser实例,解析表达式 - 获取解析后的寄存器列表和表达式列表
- 用处理后的表达式初始化
QMuParser对象
- 从
- 找到:
深入QMuParser计算核心
- 打开:
qmuparser.h和qmuparser.cpp - 对照笔记:仔细阅读
QMuParser类部分 - 理解静态数据成员:
staticQList<Result<double>>_registerValues;// 所有解析器共享的寄存器值 - 关键问题:为什么寄存器值要设计为静态成员?
答案:所有表达式计算都需要访问相同的寄存器值,静态成员避免了重复传递数据。
- 打开:
分析计算回调机制
- 找到:
QMuParser构造函数和mu::ParserRegister::setRegisterCallback - 理解回调链:
1.QMuParser构造函数设置回调函数为registerValue2.registerValue通过索引从静态_registerValues获取值3.mu::ParserRegister在计算表达式时调用此回调 - 查看回调函数:
// 伪代码示意voidregisterValue(intidx,double*val,bool*ok){if(idx>=0&&idx<_registerValues.size()){*val=_registerValues[idx].value();*ok=_registerValues[idx].isValid();}}
- 找到:
跟踪实时计算流程
- 找到:
GraphDataHandler::handleRegisterData函数 - 分析执行步骤:
1.接收来自RegisterValueHandler的原始寄存器值2.调用QMuParser::setRegistersData更新静态寄存器值3.遍历_expressionParserList中的每个QMuParser4.调用evaluate()计算表达式结果5.收集所有结果,发出graphDataReady信号 - 调试技巧:在此函数设置断点,观察每次数据到达时的计算过程
- 找到:
理解mu::ParserRegister的扩展功能
- 查看:
muparserregister.h和muparserregister.cpp - 理解设计:
mu::ParserRegister继承自mu::ParserBase - 关键方法:
SetExpr设置表达式,Eval计算表达式 - 扩展能力:除了基本数学运算,还支持哪些函数?(sin, cos, log等)
- 查看:
动手实验:观察表达式计算
- 修改表达式:在软件中设置不同的数学表达式
- 简单加法:
{40001} + {40002} - 带函数:
sin({40001} * 3.14159 / 180) - 条件运算:
{40001} > 100 ? {40001} : 0
- 简单加法:
- 观察计算:在
QMuParser::evaluate设置断点,查看不同表达式的计算过程
- 修改表达式:在软件中设置不同的数学表达式
综合调试任务
设置完整的断点链
// 从接收到数据到计算出结果GraphDataHandler::handleRegisterDataQMuParser::setRegistersData(静态方法)QMuParser::evaluate mu::ParserRegister::Eval(第三方库)GraphDataHandler::graphDataReady(信号发射处)创建测试场景
- 配置2个寄存器:40001(值为10),40002(值为20)
- 设置表达式:
{40001} + {40002} * 2 - 预期结果:10 + 20*2 = 50
调试观察
- 逐步执行,观察寄存器值如何传递
- 查看
_registerValues静态成员的变化 - 观察回调函数
registerValue被调用的次数和参数
异常情况测试
- 寄存器值无效:模拟一个寄存器读取失败,观察表达式计算结果
- 语法错误表达式:输入
{40001} +(不完整表达式),观察错误处理 - 除零错误:表达式包含除法且除数为0的情况
数据处理层学习总结
核心概念掌握:
表达式解析双阶段:
- 阶段一:
ExpressionParser将用户友好语法转换为机器友好语法 - 阶段二:
QMuParser(基于muParser)执行数学计算
- 阶段一:
数据流清晰分离:
- 寄存器值管理:
GraphDataHandler负责接收和分发 - 表达式管理:每个激活图形对应一个
QMuParser实例 - 值共享机制:静态成员
_registerValues确保所有表达式使用相同数据
- 寄存器值管理:
扩展性设计:
- 语法易于扩展:通过正则表达式可支持新格式
- 计算能力强大:借助muParser库支持复杂数学运算
- 错误处理完善:无效寄存器值不会导致崩溃
典型问题解答:
Q: 如果表达式包含10个
{40001}引用,这个寄存器会被读取几次?
A: 只读取1次。ExpressionParser会去重,所有引用指向同一个寄存器索引。Q: 表达式计算是同步还是异步的?
A: 在handleRegisterData中是同步计算的,但这个过程很快,不会阻塞UI。Q: 如何添加自定义函数?
A: 可以扩展mu::ParserRegister,添加新的函数定义。
实际应用思考:
- 性能优化:表达式解析只在配置改变时进行,计算时直接使用预编译的解析器
- 错误恢复:单个寄存器读取失败不会影响其他寄存器的计算
- 灵活性:支持复杂的数学运算和条件判断,满足各种数据处理需求
今日完整成果
- ✅ 掌握从用户表达式到可执行代码的完整转换流程
- ✅ 理解静态寄存器值共享机制的设计原理
- ✅ 能解释回调函数如何将寄存器索引映射到实际数值
- ✅ 掌握表达式计算过程中的错误处理机制
- ✅ 能设计测试用例验证表达式解析和计算的正确性
明日预告:第6天将进入数据模型层(GraphDataModel),学习数据如何存储、组织,以及模型如何与视图交互。这是连接数据处理和图形显示的关键桥梁。
建议行动:晚上可以尝试修改一个简单表达式,观察软件行为变化,巩固今天所学。