# 5. 节点开发规范
本章导读
AWFlow框架本身只是基础架构,很明显,对于一个实际应用,仅仅只有timer节点、随机数节点、add节点、log节点是远远不够的,要使AWFlow成为真正的开发利器,必须积累大量的节点。为了规范节点的开发,保证节点开发过程井然有序,本章特别介绍了一些约定的规范,在节点开发过程中,应该严格遵守。当前,基于该规范,已经开发了数量可观的节点。
# 5.1 节点相关文件概览
一个AWFlow节点至少包含6类文件,详见下表。
序号 | 文件 | 文件名 | 备注 |
---|---|---|---|
1 | 节点描述文件 | {category}_{name}.json | |
2 | help文件 | help.html | |
3 | 代码生成器脚本 | gen.sh | |
4 | 节点C文件 | aw_node_***{category}_{name}.c*** | |
5 | AWFlow Designer的node安装文件 | {category}_{name}_gen_js | 由脚本自动生成的文件夹 |
6 | 编译脚本 | SConscript |
文件名可能与节点类型(filter、pump、sink、config)和节点功能相关,表中以 {category} 表示节点类型,***{name}***表示与功能相关的名称,在一个特定的节点中,***{category}和{name}*应保持一致。
节点相关的所有文件,统一存放到名为 {category}_{name} 的文件夹中,以timer节点为例,相关文件详见下图。
除上表所列的必须存在的文件之外,根据需要,可能还会包含其它文件:
(1) 其它C文件
在实现相对复杂的节点时,可能会根据需要将一些功能的实现放到单独的C文件中,但无论怎样,节点标准节点C文件(aw_node_{category}_{name}.c)必须存在。
(2) 测试代码
对于与硬件无关的节点,还需提供测试代码。
# 5.1.1 节点描述文件
节点描述文件是JSON文件,文件命名为:***{category}_{name}.json***,比如:pump_timer.json。该文件用于描述节点相关的所有信息,包括基本信息、属性描述信息、函数描述信息、国际化字符串和节点对环境的依赖等。节点描述文件范例(以pump_timer节点为例)如下所示。
{
"name":"timer",
"category":"pump",
"desc":"Timer",
"order" : 10,
"version":"0.1.0",
"icon" : "debug.png",
"help" : "@help.html",
"keywords":["timer", "pump"],
"author": {
"name": "xxx",
"email": "xxx@zlg.cn",
"url": "http://www.zlg.com"
},
"props": [
{
"name":"duration",
"displayName":"Duration(ms)",
"desc":"Duration(ms)",
"type":"uint32_t",
"min":"0",
"max":"10000000",
"defvalue":1000,
"persistent":true,
"configurable":true
}
],
"locale": {
"Timer": {
"en": "Timer",
"zh_cn": "定时器"
},
" Duration(ms)": {
"en": "Duration(ms)",
"zh_cn": "定时周期(毫秒)"
}
}
}
通常情况下,可以参考现有节点描述文件,编写新节点的描述文件。有关节点描述文件中各个属性值的含义,将在5.2节中详细介绍。
# 5.1.2 help文件
help文件一个html文件,命名一般为help.html,也可以命名为其他,但必须和节点描述文件的help属性一致。
help文件主要用于在设计器中显示节点的帮助信息,应详细描述节点的输入、输出和属性。一个简单的范例如下所示。
<h1>生成一个指定范围的随机整数。</h1>
<h2>输入</h2>
无要求。
<h2>输出</h2>
<ul>
<li>topic: 设置的topic </li>
<li>payload: 随机整数</li>
</ul>
其对应的页面(help.html文件可使用浏览器打开)详见下图。

这只是一个简单的示例,其说明了该节点的功能是生成一个指定范围的随机整数,随机数存储在名为payload的属性中。
# 5.1.3 代码生成器脚本
有了节点描述文件和help文件,就可以调用gen_node.js和gen_node_red.js工具生成C语言文件和node安装文件。相应的两条命令为:
node ../../../../tools/gen/gen_node.js {category}_{name}.json
node ../../../../tools/gen/gen_node_red.js {category}_{name}.json
命令后紧跟节点描述文件的名称,这两条命令需存放在名为 gen.sh 的脚本中。运行该脚本后,将自动生成两个目录:{category}_{name}_gen_c 和 {category}_{name}_gen_js,例如pump_timer_gen_c 和pump_timer_gen_js,相关文件详见下图。
{category}_{name}_gen_c目录下存放了C代码和编译脚本,可以将这些文件拷贝到节点根目录中,在此基础上修改完善。
{category}_{name}_gen_js主要包含了node安装文件,利用这些文件可以方便的将节点加入到AWFlow Designer中。
# 5.1.4 C语言文件
C语言文件是节点功能的实现部分,包括.c和.h文件。通常先由代码生成器脚本(gen.sh)自动生成框架代码,再在相应的事件处理中添加/修改。有关C文件编写的更多规范,在5.3节会进一步介绍。
# 5.1.5 编译脚本
整个AWFlow采用scons构建,必须提供scons的编译脚本,代码生成器已经生成了一个默认的脚本,内容如下所示。
import os
BIN_DIR=os.environ['BIN_DIR'];
env=DefaultEnvironment().Clone()
env.SharedLibrary(os.path.join(BIN_DIR, 'nodes', 'pump_timer'), Glob('*.c'));
即默认会将该节点相关的全部C代码编译成库,存放在BIN_DIR中。可根据实际需要修改脚本的内容。
# 5.1.6 用于AWFlow Designer的节点安装文件
相关文件是由代码生成器脚本(gen.sh)自动生成的,只要节点描述文件和help文件符合规范,则这部分文件无需作任何改动。但需注意:节点描述文件或help.html文件更新后,必须运行脚本以同步更新节点安装文件。
# 5.2 节点描述文件详解
节点描述文件中包含了节点相关的所有信息:基本信息、属性描述信息、函数描述信息、国际化字符串和节点对环境的依赖等。
# 5.2.1 基本信息
基本信息包括名称、节点类型、功能描述等,除此之外,还有一些与AWFlow Designer相关的辅助信息,详见下表。
分类 | 关键字 | 值 |
---|---|---|
基础信息 | name | 节点的名称(由英文字母、数字和下划线组成的有效 C 语言ID) |
基础信息 | category | 类别。取值为pump、filter、config或sink,决定实际类继承自哪个基类 |
基础信息 | desc | 节点的功能描述 |
基础信息 | version | 版本号。包括主版本、次版本和修正号,三者用英文的点 (.) 分隔 |
辅助信息 | displayName | 显示名称。主要用于在设计器中显示,提高流图的可读性。 |
辅助信息 | icon | 图标文件(在设计器中的图标,可选) |
辅助信息 | help | 帮助信息。一般取值为@help.html,表示从 help.html 中读取帮助信息 |
辅助信息 | order | 节点在设计器的节点列表中的前后顺序,小则靠前,大则靠后 |
辅助信息 | keywords | 关键字,主要用于在设计器中查找搜索 |
辅助信息 | author | 作者信息,有疑问时方便与作者联系 |
除name和category外,其它都是可选信息(主要用于AWFlow Designer辅助设计)。name和category会决定生成的C代码。典型示例如下所示。
{
"name":"random_number",
"category":"filter",
"desc":"generate a random number",
"order" : 10,
"version":"0.1.0",
"icon" : "debug.png",
"help" : "@help.html",
"keywords":["random_number", "pump"],
"author": {
"name": "xxx",
"email": "xxx@zlg.cn",
"url": "http://www.zlg.com"
}
}
完整的节点描述文件还需要包含属性描述和函数描述。
# 5.2.2 属性描述信息
属性描述信息通过JSON文件中一个关键字为"props"的键值对表示,其值为一个数组,数组中的每个值描述了一个属性,一个属性描述中需要包含的内容详见下表。
名称 | 类型 | 简介 |
---|---|---|
name | 字符串 | 属性名称,必须是由英文、数字和下划线组成的有效C语言ID |
displayNmae | 字符串 | 显示名称,主要用于设计器中显示,提高可读性 |
type | 字符串 | 数据类型,可能的取值详见注意,不同类型可能会扩展不同的成员,比如int8_t类型的属性,还扩展了min和max等属性。针对特定类型,扩展的属性也需要包含在属性描述中。各类型完整的扩展属性详见下表 |
desc | 字符串 | 说明信息 |
defvalue | 与type相关 | 缺省值 |
persistent | bool | 是否需要持久化,指定该属性是否需要持久化 |
configurable | bool | 是否可配置 |
readonly | bool | 是否是只读属性 |
required | bool | 是否是必须填写的属性 |
format | 字符串 | 数据格式,进一步描述数据的格式 |
group | 字符串 | 指定属性所属的分组,是可选属性。设计器会考虑将分组相同的属性放到一起。比如串口设置相关的参数,可以使用同一个分组(serial) |
visible | 字符串表达式 | 可见性。对于一些特殊属性,在设置界面是否可见,可能依赖于另外一个属性。比如通信方式有 tcp 和串口两种。当选择 tcp 的时候,显示主机和端口两个参数,而当选择串口的时候,显示设备文件、波特率和其它一些参数。 visible 的取值是一个字符串表达式,通过$可以引用其它属性。表达式可以任意组合,只要把其中变量替换成属性值之后,能用 JS 的 eval 进行计算得到一个布尔值即可。例如: $a > 100 && $a < 500 || $b >0 || $c == "abc" 注意:$符号左侧和变量名右侧,应保留一个空格 |
注意,不同类型可能会扩展不同的成员,比如int8_t类型的属性,还扩展了min和max等属性。针对特定类型,扩展的属性也需要包含在属性描述中。各类型完整的扩展属性详见下表。
类型 | 简介 | 扩展成员 |
---|---|---|
int8_t | 有符号8位整数 | min : 最小值 max : 最大值 |
uint8_t | 无符号8位整数 | min : 最小值 max : 最大值 |
int16_t | 有符号16位整数 | min : 最小值 max : 最大值 |
uint16_t | 无符号16位整数 | min : 最小值 max : 最大值 |
int32_t | 有符号32位整数 | min : 最小值 max : 最大值 |
uint32_t | 无符号32位整数 | min : 最小值 max : 最大值 |
int64_t | 有符号64位整数 | min : 最小值 max : 最大值 |
uint64_t | 无符号64位整数 | min : 最小值 max : 最大值 |
float | 单精度浮点数 | min : 最小值 max : 最大值 |
double | 双精度浮点数 | min : 最小值 max : 最大值 |
string | 字符串 | min : 最小长度 max : 最大长度 |
int_enums | 整数枚举 | enums : 枚举值列表 |
string_enums | 字符串枚举 | enums : 枚举值列表 |
数值型属性描述范例如下所示。
{
"name":"foo_int32",
"type":"int32_t",
"desc":"foo int32",
"min":0,
"max":1255,
"defvalue":320,
"persistent":true,
"configurable":true
},
字符串型属性描述范例如下所示。
{
"name":"foo_string",
"type":"string",
"desc":"foo string",
"min":0,
"max":32,
"defvalue":"string demo",
"persistent":true,
"configurable":true
},
整数枚举型属性描述范例如下所示。
{
"name":"foo_int_enums",
"type":"int_enums",
"desc":"foo int_enums",
"defvalue":0,
"enums":["0:red", "1:green", "2:blue"],
"persistent":true,
"configurable":true
},
其中,枚举值定义为:
"enums":["0:red", "1:green", "2:blue"],
red、green、blue会以枚举的形式显示出来供用户选择,最终该属性的值为相应选项前面的整数,比如选择了green,则该属性的值即为1。
字符串枚举型属性描述范例如下所示。
{
"name":"foo_string_enums",
"type":"string_enums",
"desc":"foo string_enums",
"defvalue":"red",
"enums":["red", "green", "blue"],
"persistent":true,
"configurable":true
}
其中,枚举值定义为:
"enums":["red", "green", "blue"],
red、green、blue会以枚举的形式显示出来供用户选择,最终该属性的值就为相应的选项值。比如选择了green,则该属性的值即为green。
一个节点可能具有多个属性,每个属性都需要有对应的属性描述,这些属性描述统一存放到名为props的数组中,以random number节点为例,其定义了min和max两个属性,对应的属性描述如下所示。
"props": [
{
"name":"min",
"displayName":"Min Value",
"type":"int32_t",
"desc":"min value of the random number",
"min":0,
"max":1000,
"defvalue":0,
"persistent":true,
"configurable":true
},
{
"name":"max",
"displayName":"Max Value",
"type":"int32_t",
"desc":"max value of the random number",
"min":0,
"max":1000,
"defvalue":500,
"persistent":true,
"configurable":true
}
]
# 5.2.3 函数描述信息
函数描述信息通过JSON文件中一个关键字为"functions"的键值对表示,其值为一个数组,数组中的每个值描述了一个函数,一个函数描述中需要包含的内容详见下表。
名称 | 类型 | 简要说明 |
---|---|---|
name | 字符串 | 函数名 |
desc | 字符串 | 函数说明信息 |
ret | 对象(一个属性描述) | 函数返回值信息,值为一个典型的"属性描述",但不需要包含name、defvalue、persistent 和 configurable。 |
args | 数组 | 函数参数信息。每个数组成员都是一个"属性描述",以描述一个参数,但不需要包含persistent 和 configurable。 |
其与3.2.5节中介绍的函数描述(C语言版本)一致,包含操作名、操作描述、具体执行的函数、执行函数的参数描述、返回值描述等。定义范例如下所示。
"functions": [
{
"name":"foo",
"desc":"foo function",
"ret": {
"type":"float",
"desc":"foo float",
"min":0,
"max":128
},
"args":[
{
"name":"arg1",
"type":"string",
"desc":"foo string",
"min":0,
"max":32,
"defvalue":"string demo"
},
{
"name":"arg2",
"type":"string_enums",
"desc":"foo string_enums",
"defvalue":"red",
"enums":["red", "green", "blue"]
}
]
}
]
"functions"的值是一个数组,每个数组成员都是一个独立的函数描述。上述代码中只描述了一个名为foo的函数。
"ret"对应的值表示返回值描述,表示了返回值的具体情况,其相关成员与属性描述是基本相同的,唯一的区别是返回值描述中无需再使用"configurable"或"persistent"控制对应的标志,这些标志对返回值描述是多余的。程序中描述了该操作的返回值为float类型,且值在0~ 128 之间。
"args"对应的值是一个数组,每个数组成员都描述了单个参数,单个参数的相关成员与属性描述是基本相同的,唯一不同的是flags标志的设置,对于参数描述来讲,目前只有一个标志可用:ARG_DESC_FLAGS_REQUIRED。在json文件中描述时,可以使用名为"required"的布尔值控制,例如:
"required":true
注意,在JSON文件中,并没有描述该函数对应的具体操作(exec),在自动生成对应的C程序时,会针对exec生成一个空函数,这个空函数还需要用户根据实际情况完成,空函数示意如下所示。
static ret_t aw_node_pump_random_number_foo(void* obj, value_t* ret, tk_object_t* args) {
/*TODO*/
return RET_NOT_IMPL;
}
# 5.2.4 国际化(多语言支持)
为了支持多语言,需要设计器上显示的内容(比如节点名称、属性名称等)可以根据系统语言自动切换。这就要求在实现节点时,提供各个字符串的翻译文本。
要翻译的字符串放到 locale 子键下,每个字符串下对应各种语言的翻译。不同语言和国家,用语言和国家标准缩写(https://www.science.co.il/language/Locale-codes.php)表示,一些典型的标准缩写详见下表。
语言和国家 | 文件名 |
---|---|
简体中文(Chinese -- China) | zh-cn |
美国英语(English - United States) | en-us |
日语 | ja |
通常情况下,只会包含英文和简体中文两部分,范例如下所示。
"locale": {
"Generate a random number": {
"en": "Generate a random number",
"zh_cn": "产生一个随机数。"
},
"Min Value": {
"en": "Min Value",
"zh_cn": "最小值"
},
"Min value of the random number": {
"en": "Min value of the random number",
"zh_cn": "随机数范围的最小值"
},
"Max Value": {
"en": "Max Value",
"zh_cn": "最大值"
},
"Max value of the random number": {
"en": "Max value of the random number",
"zh_cn": "随机数范围的最大值"
},
"Topic": {
"en": "Topic",
"zh_cn": "主题"
}
}
为了便于匹配,约定语言和国家统一使用小写字母表示。在实际查找时,优先使用语言和国家(地区)进行匹配,比如系统当前语言为美国英语,则优先用"en_us"去匹配,若匹配失败,则尝试仅使用语言去匹配,比如"en",如果均匹配失败,则表示找不到翻译的字符串,将不再翻译,使用原字符串。
通常情况下,帮助信息都存放在help.html文件中,如果帮助信息要国际化,就要提供不同语言版本的帮助信息。不同语言的帮助信息存放在不同的html文件中,文件命名为help_***{语言和国家标准缩写}***.html,比如:help_zh_cn.html,help_en.html,help_en_us.html。
设计器优先根据语言和国家(地区)进行匹配,比如系统当前语言为美国英语,则优先寻找 help_en_us.html 文件作为帮助信息,若该文件不存在,则根据当前的语言进行匹配,寻找help_en.html。若依然找不到相应的文件,则不再翻译,使用默认的help.html文件。
# 5.2.5 节点依赖的运行环境
有些节点依赖于特定的硬件资源(比如串口、CAN),所以使用AWFlow Designer软件时,首先要选择目标板,之后AWFlow Designer才根据目标板的硬件资源,列出目标板的可用节点供设计者使用。
在节点描述文件中,使用 dependencies子键表示节点依赖的运行环境,特别地,若没有dependencies或dependencies的值为空,则视为不依赖于特定的运行环境,换言之,在任何环境下都可以使用(一般是"纯软"节点,不依赖任何硬件)。
dependencies属性中的每个值都表示了对某个特定资源的要求,常见的资源详见下表。
名称 | 含义 | 值类型 |
---|---|---|
mcu | 控制器 | 对象 |
frequency | 主频 | 字符串,单位kHz、MHz、GHz |
ram | 内存 | 字符串,单位K、M、G |
flash | 闪存 | 字符串,单位K、M、G |
io | io设备 | 对象 |
uart | UART接口数量 | 整数 |
can | CAN接口数量 | 整数 |
ethernet | 以太网接口数量 | 整数 |
usb | USB接口数量 | 整数 |
os | 操作系统 | 列表 |
节点依赖的运行环境描述范例如下所示。
"dependencies": {
"mcu": {
"frequency": "200M",
},
"ram": "20K",
"flash": "50K",
"io": {
"uart": 2,
"can": 1,
},
"os": [
"AWorks",
"Linux"
]
}
# 5.3 C代码编写规范
# 5.3.1 代码规范
应遵循AWTK代码规范。
# 5.3.2 编程原则
除了需要遵守基础的代码规范以外,还应遵守以下几点编程原则:
# 1. 尽量依赖于抽象,而不依赖于具体实现
访问接口必须用动态接口,所谓动态接口是指运行时可切换不同的实现,一般用结构体放函数指针的方式实现。在AWFlow中,节点类、节点仓库、Loop循环提供的接口都是这样的接口。
# 2. 尽量使用TKC中的函数
当需要使用到一些特定功能时,比如文件、线程、内存分配等,均不允许个特定操作系统的函数(避免依赖于特定操作系统),必须使用操作系统提供的函数时,必须先设计好一个访问接口,然后至少实现一种操作系统的实现,节点只允许调用访问接口。
# 5.4 节点设计原则
# 5.4.1 兼容性
新版节点必须兼容旧版节点,如果做不到兼容,就开发新的节点。
# 5.4.2 pump节点设计
pump节点是数据流的"源头",其必须可以作为触发源,能够主动产生数据/事件以驱动数据流工作,而不能只看数据流向。
以random_number节点为例,其核心功能是产生一个随机数,从数据的角度看,其好像是数据流的起点。 但是,它并是一个"触发源",它不能"主动"产生随机数,因此,其只能作为filter节点,接受处理别的节点产生的事件。
读文件操作也非常类似,其核心功能是读取文件中的数据,从数据的角度看,其好像也产生了数据,是数据流的起点,但是,它同样不能"主动"读取文件数据,因此,其只能作为filter节点。
简言之,决定一个节点是不是pump节点的关键在于:该节点是否可以主动产生数据/事件,而与数据的出处(节点内部产生、从系统外部读取等)并没有直接关系。
# 5.4.3 与系统外通讯的节点设计
与系统外通讯的节点通常用2个或者3个节点实现:输入节点、输出节点和可选的配置节点。
# 1. 输入节点
输入节点负责从系统外部读取数据,输入到AWFlow系统之中,是pump类型节点。输入节点的名称必须以in结尾,比如:pump_serial_in。
在AWFlow中,这类输入节点只有输出,没有输入,示意图详见下图。
# 2. 输出节点
输出节点负责将AWFlow系统中的数据输出到系统外部,是sink类型节点。输出节点的名称必须以out结尾,比如:sink_serial_out。
在AWFlow中,这类输出节点只有输入,没有输出,示意图详见下图。
# 3. config节点(可选)
配置节点可用于对通信参数进行配置,是config类型节点。例如,对于串行通信(serial),可以使用config节点配置串口号、波特率数据位、校验位、停止位、流控等参数。
在AWFlow中,这类配置节点没有输入和输出,示意图详见下图。
配置节点配置了一个通信接口,相应的in和out节点需要使用该通信接口,以便输入、输出数据。为了便于in和out节点与config节点关联,特定义如下规范:
a) config节点的名称必须唯一,且具有标识性;
b) in和out节点中都有一个名为config的属性,该属性即用于指定关联的配置节点。
由于in 和 out 节点都具有功能类似的 config 属性,针对该属性的描述,可以统一定义,如下所示。
{
"name": "config",
"displayName": "Config",
"type": "string",
"desc": "Config",
"min": 1,
"max": 64,
"defvalue": "xxx",
"persistent": true,
"configurable": true
}
为支持国际化,相关字符串的翻译也可以统一定义,如下所示。
"Config": {
"en": "Config",
"zh_cn": "关联配置"
},
对于一些无需配置参数的场合,可以不要config节点。典型地,对于有连接的情况,比如tcp等,不用config节点,可以在in节点中将句柄,连接等上下文数据保存在message中,方便out节点处理。
# 5.4.4 节点功能实现设计
# 1. 多线程设计
节点创建的线程内不应直接调用aw_node_pump_input_and_emit接口来开启一次数据流,而应该使用aw_loop_add_timer或者aw_loop_add_idle,在定时或空闲回调内调用aw_node_pump_input_and_emit,防止多线程操作的异常情况。
# 2. 避免阻塞
节点数据流处理接口,原则上应不执行任何阻塞操作;如pump的input,filter节点的transform,sink节点的on_data,以及事件源处理函数、idle、timer这些都不应阻塞。
← 4. 代码生成器