Skip to content

5. 节点开发规范

本章导读

AWFlow框架本身只是基础架构,很明显,对于一个实际应用,仅仅只有timer节点、随机数节点、add节点、log节点是远远不够的,要使AWFlow成为真正的开发利器,必须积累大量的节点。为了规范节点的开发,保证节点开发过程井然有序,本章特别介绍了一些约定的规范,在节点开发过程中,应该严格遵守。当前,基于该规范,已经开发了数量可观的节点。

5.1 节点相关文件概览

一个AWFlow节点至少包含6类文件,详见下表。

序号文件文件名备注
1节点描述文件{category}_{name}.json
2help文件help.html
3代码生成器脚本gen.sh
4节点C文件aw_node_{category}_{name}.c
5AWFlow Designer的node安装文件{category}_{name}_gen_js由脚本自动生成的文件夹
6编译脚本SConscript

文件名可能与节点类型(filter、pump、sink、config)和节点功能相关,表中以 {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节点为例)如下所示。

json
{
  "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文件主要用于在设计器中显示节点的帮助信息,应详细描述节点的输入、输出和属性。一个简单的范例如下所示。

html
<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安装文件。相应的两条命令为:

bash
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的编译脚本,代码生成器已经生成了一个默认的脚本,内容如下所示。

python
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代码。典型示例如下所示。

json
{
  "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相关缺省值
persistentbool是否需要持久化,指定该属性是否需要持久化
configurablebool是否可配置
readonlybool是否是只读属性
requiredbool是否是必须填写的属性
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 : 枚举值列表

数值型属性描述范例如下所示。

json
{
  "name":"foo_int32",
  "type":"int32_t",
  "desc":"foo int32",
  "min":0,
  "max":1255,
  "defvalue":320,
  "persistent":true,
  "configurable":true
},

字符串型属性描述范例如下所示。

json
{
  "name":"foo_string",
  "type":"string",
  "desc":"foo string",
  "min":0,
  "max":32,
  "defvalue":"string demo",
  "persistent":true,
  "configurable":true
},

整数枚举型属性描述范例如下所示。

json
{
  "name":"foo_int_enums",
  "type":"int_enums",
  "desc":"foo int_enums",
  "defvalue":0,
  "enums":["0:red", "1:green", "2:blue"],
  "persistent":true,
  "configurable":true
},

其中,枚举值定义为:

json
"enums":["0:red", "1:green", "2:blue"],

red、green、blue会以枚举的形式显示出来供用户选择,最终该属性的值为相应选项前面的整数,比如选择了green,则该属性的值即为1。

字符串枚举型属性描述范例如下所示。

json
{
  "name":"foo_string_enums",
  "type":"string_enums",
  "desc":"foo string_enums",
  "defvalue":"red",
  "enums":["red", "green", "blue"],
  "persistent":true,
  "configurable":true
}

其中,枚举值定义为:

json
"enums":["red", "green", "blue"],

red、green、blue会以枚举的形式显示出来供用户选择,最终该属性的值就为相应的选项值。比如选择了green,则该属性的值即为green。

一个节点可能具有多个属性,每个属性都需要有对应的属性描述,这些属性描述统一存放到名为props的数组中,以random number节点为例,其定义了min和max两个属性,对应的属性描述如下所示。

json
"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语言版本)一致,包含操作名、操作描述、具体执行的函数、执行函数的参数描述、返回值描述等。定义范例如下所示。

json
"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"的布尔值控制,例如:

json
"required":true

注意,在JSON文件中,并没有描述该函数对应的具体操作(exec),在自动生成对应的C程序时,会针对exec生成一个空函数,这个空函数还需要用户根据实际情况完成,空函数示意如下所示。

c
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

通常情况下,只会包含英文和简体中文两部分,范例如下所示。

json
"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
ioio设备对象
uartUART接口数量整数
canCAN接口数量整数
ethernet以太网接口数量整数
usbUSB接口数量整数
os操作系统列表

节点依赖的运行环境描述范例如下所示。

json
"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 属性,针对该属性的描述,可以统一定义,如下所示。

json
{
  "name": "config",
  "displayName": "Config",
  "type": "string",
  "desc": "Config",
  "min": 1,
  "max": 64,
  "defvalue": "xxx",
  "persistent": true,
  "configurable": true
}

为支持国际化,相关字符串的翻译也可以统一定义,如下所示。

json
"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这些都不应阻塞。