模板详解
用户可以从项目中的模板文件 template/online_template.cpp 开始应用本项目的框架编写在线程序。如果只是简单的开发,只需要基本的 ROOT 相关的知识,熟练使用 TH1 和 TH2 直方图,熟练使用 Fill 函数填充直方图就足够了。在获取方面,只需要知道每一个事件对应于一个通道,而一个通道又对应与探测器的一路即可。
下面将详细解释模板文件中的代码的含义。
主要流程
从 main() 函数开始了解主要脉络。
上面的 main() 函数的代码,可以主要分为 4 块。
- 4-21 行的 ROOT 相关的准备
- 24-36 行的用户输入响应
- 41-50 行的读取数据并处理
- 52-55 行程序结束清理
在线程序首先读入输入参数,确定需要处理的是哪个 run 哪个机箱的数据。然后做一些 ROOT 相关的准备,准备好在线需要的直方图。用户输入的响应主要是两部分,一是按照设定的帧率刷新直方图,二是对键盘鼠标输入的响应。核心的部分是从共享内存中读取数据,然后根据用户编写的算法进行实质的在线分析,并填到直方图中。这部分也是经常做数据分析的人所擅长的。最后是清理一下程序中的东西,并优雅地结束程序。
如果只是想要简单地开发基本能用的在线程序,实际上只需要关注第 3 部分数据处理相关的就足够了。下面将详细解释每一部分的工作流程。
ROOT 相关准备
为了方便,这里再一次展示 ROOT 准备的流程的代码。
第 2 行启用了多线程安全。这是因为在线程序用到了两个线程,第一个线程负责读取数据并填入直方图,第二个线程将更新后的直方图画出来。由于两个线程同时调用了直方图(实际上一个线程读取,一个线程写入,并不矛盾),可能存在数据冲突,所以需要启用多线程安全。
第 4 行创建了 ROOT 的 TApplication,简单理解就是程序运行时的弹出的 ROOT 窗口,实质是包含窗口在内的一系列运行环境。ROOT 的包装非常全面,所以只需要创建这个类就不需要再操心了。在一般的教程中,这个类常常配合 TApplication::Run 函数来进入循环并处理用户响应,但由于在线程序需要在另一个循环中读取在线数据,所以只能放弃 Run 函数并在后面手动处理用户响应。
第 6 行到 18 行就是常规的创建画布并画图。虽然一般来说,都是把直方图填充完了再画图,这里只需要画空的直方图即可。这个程序的逻辑是后续读取在线数据后再填入直方图并更新画布。
用户输入响应
用户输入的响应包含两个部分
-
对鼠标和键盘输入的响应。
-
持续更新图像
对鼠标和键盘输入的响应
鼠标和键盘的输入,目前只考虑了两种情形
- 点击窗口的关闭按钮终止在线程序
- 按 r 或者 F5 重置图像
- 按 s 或者 F12 保存截图
这两个功能都是依靠 HandleSignal()实现的。这个函数又依赖于 SignalHandler类,虽然听起来像套娃,但这都是有必要的。
上面的代码第 3-4 行使用智能指针创建了 SignalHandler类的实例,智能指针的好处就是不用自己管理内存,这个指针没用时会自动释放内存。SignalHandler 顾名思义,就是用来处理各种输入信号的,具体细节后面再看。
第 6-10 行,将窗口的关闭按钮和 SignalHandler的 Terminate() 函数关联起来,再用户点击关闭按钮同时,会调用Terminate()函数。该函数产生一个中断信号,类似于按 Ctrl+C。这是为了让管理共享内存的 iceoryx 停止工作,这个会在第三部分再次提到。
第 11-16 行,将 ROOT 自身的处理键盘和鼠标输入的函数 ProcessedEvent和 SignalHandler::Refresh 函数关联起来,也就是说每次用户点击鼠标或者敲键盘时,都会调用 Refresh 函数并给一些输入信息。Refresh 函数在发现输入信息是 F5 或者 r 时就会设置 should_refresh_ 为真,这里不直接重置图像是因为 SignalHandler 本身并不知道应该重置哪些图像,而想要让它知道就要传递更多的参数,这样就不够灵活了。而这里设置一个变量给外面的函数或者类判断是否需要重置,就可以在这个类外面完成重置的事情。所谓的外面,其实就是指的用户编写的在线程序。同理,在检测到 F12 或者 s 时就会设置 should_save_ 为真,告诉外面的程序是时候截图保存了。
另一方面,程序中使用的 Connect 函数是 ROOT 提供的,要求关联的类必须是继承自 RQ_Object的,所以必须设置 SignalHandler 这个类。而且这个类编译时还需要关联 LinkDef.h 头文件,所以编译时还需要用到一些技巧,详见后面的用cmake构建一节。
持续更新图像
持续更新,顾名思义,需要做两件事,持续和更新。为了保证持续性,需要一个无限循环,所以创建了一个新的线程。
这个线程调用的是 UpdateGui 函数。
这个函数主体是一个循环,终止条件是用户按下 Ctrl+C。前面提到了用户点击关闭按钮时,会触发SignalHandler::Terminate函数并调用 raise(SIGINT) 函数,这同样会产生一个中断信号,和按下 Ctrl+C 是一样。换句话说,这个循环的终止条件是用户按下 Ctrl+C 或者点击窗口右上角的关闭按钮。
循环中的 8-12 行是遍历画布中的所有 pad,并更新图像,常规的 ROOT 代码。
第 13-17 行是问一下 SignalHandler 有没有收到重置图像的指令,有的话就重置所有的图像。因为需要知道要重置哪些图像,所以需要传入参数const std::vector<TH1*> &histograms,这里需要传入所有的需要重置的图像的指针。所以可以在 main()函数代码中的 27-29 行看到需要构建一个直方图的数组。
第 18-22 行时保存截图,保存的截图的路径是在一开始的参数中设置的,而每个文件会按照机箱编号-run-时间的形式来组织。所以一方面需要 screenshot_name 来输入包含机箱和 run 信息的字符串,另一方面需要用 GetTime() 函数来获取当前时间并转化为字符串。
第 23 行是 ROOT 周期性的刷新,可以简单理解成刷新窗口。
第 24 行是休眠,根据设置的刷新率休眠一定时间。注意到这里以 us 为单位,所以刷新率不能高于 1000 Hz,当然,实际使用时,10 Hz 以下的刷新率就绰绰有余了。
第 26-28 是在结束这个函数之前,再保存一次截图。结束这个函数也意味着整个程序的结束,换句话说就是在程序关闭前,迅速保存一次截图,再关闭程序。
读取内存并处理数据
详见上手指南。
清理
在第二部分的用户输入响应中,我们创建了一个线程 gui_thread 来持续地更新图像并处理用户输入,所以在程序结束时,我们也要回收这个线程的资源,不然程序结束后显示 break。虽然这个 break 只会在程序结束后产生,理论上并不会造成任何影响,但是不够优雅。所以调用
来等待线程结束并回收。
另一方面还需要结束之前创建的 TApplication,所以调用
来终止 ROOT 的程序。
顺便一提,如果程序中有用到 new 或者 malloc 来申请内存,也应该在这一部分释放内存。
用 cmake 构建
这里简单解释模板 CMakeLists.txt中的代码的作用。当然,推荐还是系统学习 cmake,这里推荐 HSF 上的教程。
模板中一共包含 5 个命令
add_executable新增一个可执行文件,包含 2 个参数- 可执行文件的名字,即编译后的程序就叫
online_template - 源代码文件的名字
- 可执行文件的名字,即编译后的程序就叫
target_include_directories,给一个目标设置包含路径。所谓的目标即add_executable或者add_library之类的命令添加的东西,这里设置的目标是online_template,即上一个命令所声明的。包含路径就是源码中包含的头文件的所在目录,这里指定了两个目录,一个是项目的根目录${PROJECT_SOURCE_DIR},另一个是 ROOT 的头文件目录ROOT_INCLUDE_DIRtarget_link_libraries,给一个目标设置链接的库,这里目标设置为online_template。而链接的库是online_data_receiver是上手指南中提到OnlineDataReceiver类。另外两个链接的库是 ROOT 相关的,分别是 TH1、TH2、TGraph 相关的ROOT::Graf和 TCanvas 相关的ROOT::Gpadset_target_properties,给一个目标设置一些编译参数,这里主要是依赖的 iceoryx 库要求的,实际就设置了两个参数,一个是 iceoryx 要求的 C++ 标准,另一个是需要位置无关的代码root_generate_dictionary,这个是 ROOT 所需要的。前面提到了SignalHandler比较特殊,继承自RQ_Object,所以需要用这样的方式来链接动态库。online_template_dict是一个新的目标,本质是代码生成,SignalHandler继承自RQ_Object后被添加了一些新的代码,新的代码会编译出一个动态库,库的名字就是online_template_dict(这一段有一部分是我猜的)${PROJECT_SOURCE_DIR}/include/signal_handler.h告诉 cmakeSignalHandler在哪里声明和定义MODULE online_template告诉 cmake,把 ROOT 生成的online_template_dict链接到online_template上LINKDEF ${PROJECT_SOURCE_DIR}/include/linkdef.h告诉 cmake 对应的linkdef.h文件在哪,这一部分应该是和 cint 相关的,我不太了解
实际使用时,只需要简单修改 online_template 即可。