Taskbus - 基于Qt的跨平台多进程合作框架(二)模块开发举例

我们继续前文,介绍taskBus (GitHub)的模块开发例子。

4. 开发指南

我们将以fftw为例子,介绍如何从0开始创建一个taskBus模块。

4.1 设计功能与撰写描述文件

创建一个模块的第一步,是设计功能,并确定接口、属性,撰写JSON文件。撰写JSON文件最快的方法是从例子修改,并保存为与可执行文件同名的json文件。使用UTF-8编码书写亚洲字符有利于加载速度。只要有JSON文件,即使模块没有完全实现,也可以在平台上看到效果。我们要设计的FFT模块,应该具备以下的结构:

FFTW

它具备时戳、信号两个输入接口(Subject);具备时戳、复数形式、幅度谱三个输出接口(Subject)。此外,可以支持用户配置FFT的点数、输入信号的波段数、样点数据类型三个静态参数。这些静态参数在用户单击图标时,可以在“属性”一栏修改,类似下图:

FFT_Para
具备上述特性的功能描述文件如下:

{
    "transform_fft":{
        "name":"libfftw",
        "parameters":{
		     "sptype":{
                "type":"enum",
                "tooltip":"sample point format",
                "default":0,
				"range":{
                    "0":"16 bit Intel",
                    "1":"16 bit Moto",
                    "2":"int8",
                    "3":"uint8"
				}
            },
             "channels":{
                "type":"int",
                "tooltip":"Channels",
                "default":1
            },		    
			"fftsize":{
                "type":"int",
                "tooltip":"fft size",
                "default":1024
            }

        },
        "input_subjects":
        {
            "signal":{
                "type":"byte",
                "tooltip":"signal"
            },
			"tmstamp_in":{
                "type":"unsigned long long",
                "tooltip":"tmstamp_in"
            }
        },
        "output_subjects":{           
            "FFT":{
                "type":"vector",
                "tooltip":"FFT in dB"
            },
            "Spec":{
                "type":"vector",
                "tooltip":"Spec in Complex"
            },
            "tmstamp_out":{
                "type":"unsigned long long",
                "tooltip":"tmstamp_out"
            }

        }
    }
}

一旦具备了上述JSON文件,只要搭配一个空白的可执行文件(文件名相同,扩展名不同),即可在平台载入并编辑——当然无法运行。

4.2 利用工具代码加快开发进度

如果您使用C#等特性丰富的语言,对命令行解析等操作会比较方便。对C++,平台提供了可直接加速开发速度的源代码,使用这些源码,会显著提高开发效率,简化代码量。

4.2.1 命令行解释

实现模块功能的第一步,是获取命令行参数。您可以采用Helloworld的代码实现命令行解析,但那样做显得比较复杂。使用提供的类,命令行解析、提取变得非常简单。无论是Qt、C++还是MFC,均可以采取类似的措施。

#include "cmdlineparser.h"
#include "tb_interface.h"
using namespace TASKBUS;
int main(int argc , char * argv[])
{
	init_client();
	cmdlineParser args(argc,argv);
	if (args.contains("information"))
		putjson();
	else if (args.contains("function","tranform_fft"))
	{
    	const int instance		= args.toInt("instance",0);
        const int isource		= args.toInt("signal",0);
        const int FFT		  	= args.toInt("FFT",0);
        const int Spec 			= args.toInt("Spec",0);
        const int itmstamp_in  = args.toInt("tmstamp_in",0);
        const int itmstamp_out = args.toInt("tmstamp_out",0);
        const int sptype	=	args.toInt("sptype",0);
        const int channels	=	args.toInt("channels",1);
		const int fftsize	=  args.toInt("fftsize",1024);
		//...work
	}
	else
		fprintf(stderr,"Error:Function does not exits.");
    return 0;
}

对Qt、MFC、C#的例子,可以参考范例代码。

扫描二维码关注公众号,回复: 8951768 查看本文章

4.2.2 数据收发

taskBus的数据吞吐格式是固定的。其严格遵循下面的顺序:

package record type length(bytes) meaning
1 header unsigned char [4] 4 必须为0x3C,0x5A,0x7E,0x69,以便用WinHex等软件调试
1 subject_id int 4 0: Control commands, >0:user subjects, <0 reserved
1 path_id int 4 >=0:user subjects, <0 reserved
1 data_len int 4 >0, following data length in bytes
1 data unsigned char data_len >0, data in bytes
2 header unsigned char [4] 4 0x3C,0x5A,0x7E,0x69
2 subject_id int 4 0: Control commands, >0:user subjects, <0 reserved
2 path_id int 4 >=0:user subjects, <0 reserved
2 data_len int 4 >0, following data length in bytes
2 data unsigned char data_len >0, data in bytes
3…

使用工具代码,可以直接完成数据收发。主要推送函数有:

    void push_subject(
	    const unsigned int subject_id,
	    const unsigned int path_id,
	    const unsigned int data_length,
	    const unsigned char   *dataptr
	    );
    void push_subject(
	    const unsigned int subject_id,
	    const unsigned int path_id,
	    const char   *dataptr
	    );
    void push_subject(
	    const unsigned char   *allptr,
	    const unsigned int totalLength
	    );
    void push_subject(
	    const subject_package_header header,
	    const unsigned char   *dataptr
	    );

接收函数(阻塞)为:

   std::vector<unsigned char> pull_subject(  subject_package_header * header  );

**性能说明:**该函数会直接返回一个向量,含有一个完整数据包(不包括头部)的数据内容。由于默认使用C++14以上标准,返回 std::vector 并不会导致内存深度拷贝,而是触发右值引用与内存传递。

使用上述工具函数的收发代码片段如下:

		while (false==bfinished)
		{
			subject_package_header header;
			vector<unsigned char> packagedta = pull_subject(&header);
			if (is_valid_header(header)==false)
            	break;
			if ( is_control_subject(header))
			{				
				if (strstr(control_subject(header,packagedta).c_str(),"\"quit\":")!=nullptr)
					bfinished = true;
			}
			else if (header.subject_id==source)
			{
            	//...fft
                //output
				if (FFT>0)
					push_subject(FFT,header.path_id,fftsize*sizeof(double),(const unsigned char *)vec_fft_abs.data());
				if (Spec>0)
					push_subject(Spec,header.path_id,fftsize*sizeof(double),(const unsigned char *)out);
			}
		}

4.2.3 调试

为了一个简单的模块,从平台进程开始跟踪调试,需要在不同的语言、编译器开发的二进制机器代码中漫游,非常繁琐。幸运的是,taskBus提供了一套非常巧妙而简单的离线调试解决方案,使得您可以单独调试模块本身,并随时接入平台运行。

调试的核心思路是标准输入输出管道的重载。平台可以根据需要,把某个模块的输入输出数据、命令行启动参数等要素全部录制到磁盘上。而后,在调试平台时,只要指定录制文件夹,即可实现场景回放。

(1)录制

在主界面选中模块,单击“启动调试”按钮,会开启录制。

TurnOnDebug

一旦整个项目开始运行,记录数据会源源不断地记录在 debug 文件夹。子文件夹的名字为当前的操作系统进程ID,文件夹下有3个文件,分别对应stdin,stdout,stderr。注意,为了捕获所有异常,平台会一直独占日志,直到下一次调试开始运行。因此,为了结束录制,需要关闭平台或者再次启动工程。

Debug Folder

(2)回放与调试

回放与调试的关键技术是freopen的使用。这个特性能够在保持文件句柄不变的情况下,把实际来源重定向到文件。用户可以自己通过语句来实现,一旦下面这个语句被执行,数据吞吐的stdin将自动使用文件中录制的数据:

	std::string fm_stdin = strpath + "/stdin.dat";
    freopen(fm_stdin.c_str(),"rb",stdin);

对于cpp,平台已经准备好了方便的工具函数。一个支持切换调试模式、正常模式的代码只需要在main入口添加几行代码:

using namespace TASKBUS;
const int OFFLINEDEBUG = 0;
int main(int argc , char * argv[])
{
	init_client();
	cmdlineParser args;
	if (OFFLINEDEBUG==0)
		args.parser(argc,argv);
	else
	{
		auto ars = debug("debug/pid21580");
		args.parser(ars);
	}
    //...
}

在上面的代码中,将从debug/pid21580 读取先前录制的命令行参数、输入数据。这样,您就可以独立调试了。别忘了完成调试后,把调试开关OFFLINEDEBUG掷为0.

4.3 数据处理

在前文的例子里,数据的处理都是没有记忆的。对到来的数据,直接处理后输出,是最简单的情况。然而,很多场景下我们需要应对数据记忆与多路处理问题。对于一个数据流量很大或者处理耗时的操作,负荷控制也是要考虑的问题。-

4.3.1 数据缓存建议

推荐使用STL库。

  • 若需要缓存旧的数据,使用STL容器是非常合适的选择。
  • 对于不同的通路(path_id),缓存应该是独立的。此时,std::map<path_id, cache> 会提供非常好的操作性。
  • 使用嵌套 map, vector, list,能够实现非常复杂的动态数据结构。
  • 使用智能指针“shared_ptr",也是一个好的主意。

4.3.2 负荷控制

与Gnuradio的拉取方式不同,taskBus的生产者会主动推送数据。计算资源充裕时,设计者不需要考虑负荷控制的问题。但是在进行耗时的处理时,若没有措施保护,瓶颈模块可能会导致整个平台的内存开销不断增加。生产者不断写入stdout, 平台输出到消费者的stdin,看似没有问题。实际上,在windows下,平台写入的是操作系统的缓存。消费者若无法及时消费缓存,缓存会持续增长。

出于灵活性考虑,taskBus把这个问题留给模块设计者。模块设计者可以通过至少两种简单的策略解决这个问题。

  • 方法1,通过设置信号源的速率,确保信号以小于处理极限的速率生成。
  • 方法2,消费者输出数据时戳,并向生产者回连,形成闭环。生产者检测回连时戳。若显著低于当前生产进度,则生产者调慢生产进度。

下面这个范例工程即采用了策略2. 技术细节参考范例工程 source_soundcard.

AntiBlocking

4.4 运行与发布

taskBus的运行时为绿色发布(copy-deployment)提供了遍历,开发者可参考下面的章节调整路径设置。

4.4.1 路径策略

taskBus 的项目在启动时,会强制把当前路径设置到 taskBusPlatform 可执行文件所在的路径。taskBusPlatform 会读取同一文件夹下的 default_mods.text 预先加载所有的已知模块。这个文件中,每一行存储一个模块的可执行文件位置,一个范例如下所示:

../../../examples/voice_spec.exe
modules/network_p2p.exe
modules/sink_file.exe
modules/example_helloworld.exe
modules/source_files.exe
modules/transform_fft.exe
modules/sink_SQL.exe
modules/source_soundcard.exe
modules/sink_plots.exe

taskBusPlatform 会尽量使用相对路径存储模块的位置,除非绝对路径的字符长度要小于相对路径。因此,对于一个需要发布的系统,可以把所有模块放在 taskBusPlatform 可执行文件所在的路径中,用文件夹 modules 或 subs 等子文件夹管理。如此操作后,拷贝到新的计算机上,无需设置即可运行啦。

4.4.2 子工程与嵌套

taskBus允许工程作为整体,被其他工程引用。所有悬空的引脚都会被分配临时ID,并暴露出来供外部工程链接。 以声卡的FFT为例,我们可以把声卡、FFT合成为一个“声音频谱”模块。

(1) 创建子工程

新建一个子工程,其结构如下:

New SubProject

  • 这个子项目包括两个模块,声卡模块与fft模块。
  • fft模块的输出管脚悬空,作为子项目的外部接口。
  • 如果有两个相同名称的悬空接口,会使用对应进程分配的ID做区分。

保存这个子项目为tbj文件,如“voice_spec.tbj”。

(2) 附加包装器

把平台自带的包装器“subtask_warpper.exe”拷贝到“voice_spec.tbj”相同的文件夹,命名为“voice_spec.exe”

  • voice_spec.exe 会自动读取 voice_spec.tbj,并告诉平台自己的接口。
  • 在Linux下,没有exe扩展名。
(3) 附加默认模块加载脚本

由于voice_spec需要自行加载模块,需要把 taskBusPlatform.exe 所在文件夹下的 default_mods.text 拷贝到“voice_spec.tbj”相同的文件夹,命名为“voice_spec.text”。

  • 由于平台的当前运行路径永远是 taskBusPlatform.exe 所在路径,因此,文件voice_spec.text中的相对路径仍旧与 default_mods.text 不变。用户不需要额外编辑。
  • 但仍旧需要检视文件 文件voice_spec.text, 以便清除无用的条目。
  • 尤其需要注意,文件voice_spec.text中不能包含voice_spec模块的入口行。这样会导致递归加载,启动上百个子进程拖慢系统

完成2、3两个步骤后,文件夹看起来是这样的:
SubProj Folder

文件voice_spec.text被修改为:

modules/source_soundcard.exe
modules/transform_fft.exe
(4)载入子项目模块

在平台中载入子项目模块"voice_spec.exe"后,可以把该模块作为整体拖入设计区:

SubProjEmb

  • 双击“voice_spec” 图标,会打开子项目。

4.5 多平台、分布式及扩展

taskBus 核心采用 Qt 开发,是跨平台的结构框架,可以在支持Qt的所有平台上运行。同时,通过引入相应的模块,taskBus 可以较为方便地实现分布式计算。在代码示例中,含有基于TCP的点对点传输模块、接受SQL脚本的数据库记录模块。

taskBus除了支持使用 C++, C#, VB等编译型语言,还可以通过包装器直接引用 python,matlab 等作为模块,非常灵活。

具体例子参考附带代码。

5 关于taskBus

版权保护与商业用途

taskBus 本身是开放标准的软件体系,遵循LGPL协议,但不规定模块的许可类型。
若开发者发布的是商用模块,要自行完成注册认证、激活等功能设计。
TaskBus 由 goldenhawking studio 开发,欢迎活跃开发者加入!

邮箱:[email protected]

发布了127 篇原创文章 · 获赞 330 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/goldenhawking/article/details/84192460
今日推荐