# 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节点为例,相关文件详见下图。

图5.1 timer节点文件组织示意图
图5.1 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文件可使用浏览器打开)详见下图。

图5.2 节点帮助信息所示页面
图5.2 节点帮助信息所示页面

这只是一个简单的示例,其说明了该节点的功能是生成一个指定范围的随机整数,随机数存储在名为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,相关文件详见下图。

图5.3 脚本自动生成的文件
图5.3 脚本自动生成的文件

{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中,这类输入节点只有输出,没有输入,示意图详见下图。

图5.4 pump_serial_in节点
图5.4 pump_serial_in节点

# 2. 输出节点

输出节点负责将AWFlow系统中的数据输出到系统外部,是sink类型节点。输出节点的名称必须以out结尾,比如:sink_serial_out。

在AWFlow中,这类输出节点只有输入,没有输出,示意图详见下图。

图5.5 sink_serial_out节点
图5.5 sink_serial_out节点

# 3. config节点(可选)

配置节点可用于对通信参数进行配置,是config类型节点。例如,对于串行通信(serial),可以使用config节点配置串口号、波特率数据位、校验位、停止位、流控等参数。

在AWFlow中,这类配置节点没有输入和输出,示意图详见下图。

图5.6 config_serial节点
图5.6 config_serial节点

配置节点配置了一个通信接口,相应的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这些都不应阻塞。