第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类的"数据成员"部分 -
找到关键成员 :
cppQStringList _processedExpressions; // 处理后的表达式列表 QList<ModbusRegister> _registerList; // 解析出的寄存器列表 QRegularExpression _findRegRegex; // 查找寄存器表达式的正则 QRegularExpression _regParseRegex; // 解析单个寄存器的正则
-
-
查看正则表达式定义
- 打开 :
expressionregex.h(如果存在)或在代码中搜索cMatchRegister、cParseReg - 理解正则模式 (笔记中提到):
- 寄存器表达式形如:
{40001[@1][:f32b]}或{h0[@1][:f32b]} - 中括号内为可选项:连接编号
[@N]和数据类型[:type]
- 寄存器表达式形如:
- 关键学习点:理解这种语法设计为什么便于用户使用(同时支持十进制地址和助记符地址)
- 打开 :
-
深入解析算法
-
找到 :
ExpressionParser::processExpression函数 -
逐步分析算法流程 :
cpp1. 使用_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);
-
-
测试不同表达式格式
-
创建测试用例 :
cppQStringList 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类部分,理解它的三个关键容器:cppQList<ModbusRegister> _registerList; // 寄存器列表 QList<quint16> _registerIndexList; // 寄存器索引列表(可能已弃用或笔记有误) QList<QMuParser> _expressionParserList; // 表达式解析器列表 -
注意 :根据实际代码,
_registerIndexList可能不存在。以实际代码为准。
-
-
分析数据处理流程
- 找到 :
GraphDataHandler::processActiveRegisters函数 - 理解调用时机 :何时会调用这个函数?
- 图形激活状态变化时
- 图形表达式修改时
- 连接设置变化时
- 跟踪流程 :
- 从
GraphDataModel获取激活图形的表达式 - 创建
ExpressionParser实例,解析表达式 - 获取解析后的寄存器列表和表达式列表
- 用处理后的表达式初始化
QMuParser对象
- 从
- 找到 :
-
深入QMuParser计算核心
-
打开 :
qmuparser.h和qmuparser.cpp -
对照笔记 :仔细阅读
QMuParser类部分 -
理解静态数据成员 :
cppstatic QList<Result<double>> _registerValues; // 所有解析器共享的寄存器值 -
关键问题 :为什么寄存器值要设计为静态成员?
答案:所有表达式计算都需要访问相同的寄存器值,静态成员避免了重复传递数据。
-
-
分析计算回调机制
-
找到 :
QMuParser构造函数和mu::ParserRegister::setRegisterCallback -
理解回调链 :
cpp1. 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(); } }
-
-
跟踪实时计算流程
-
找到 :
GraphDataHandler::handleRegisterData函数 -
分析执行步骤 :
cpp1. 接收来自RegisterValueHandler的原始寄存器值 2. 调用QMuParser::setRegistersData更新静态寄存器值 3. 遍历_expressionParserList中的每个QMuParser 4. 调用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设置断点,查看不同表达式的计算过程
- 修改表达式 :在软件中设置不同的数学表达式
综合调试任务
-
设置完整的断点链
cpp// 从接收到数据到计算出结果 GraphDataHandler::handleRegisterData QMuParser::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),学习数据如何存储、组织,以及模型如何与视图交互。这是连接数据处理和图形显示的关键桥梁。
建议行动:晚上可以尝试修改一个简单表达式,观察软件行为变化,巩固今天所学。