用AWTK和AWPLC快速开发自定义功能块

awplc 是 zlg 自主研发的 plc 系统(兼容 iec61131-3),本文以定时器为例介绍一下如何扩展自定义功能块,以及代码生成器的用法。    背景
awtk 全称 toolkit anywhere,是 zlg 开发的开源 gui 引擎,旨在为嵌入式系统、web、各种小程序、手机和 pc 打造的通用 gui 引擎,为用户提供一个功能强大、高效可靠、简单易用、可轻松做出炫酷效果的 gui 引擎。
awplc 是 zlg 自主研发的 plc 系统(兼容 iec61131-3),其中 awplc 的运行时库 (runtime) 基于 zlg tkc 开发,可以移植到到任何主流 rtos 和 嵌入式系统。awplc 的集成开发环境 (ide) 基于 awtk 开发,可以运行在 windows、macos 和 linux 系统之上。awplc 的主要目标之一是把 plc 中低代码开发方法引入到嵌入式软件,从而提高嵌入式软件的开发效率和可靠性。    简介
在前一篇文章中,我们介绍了自定义 awplc 功能块的基本方法,但是有些部分的内容并没有提到,比如:
1. 功能块的部分虚函数的实现。这些函数在不同功能块中的实现是不同的,所以要做成虚函数,但是在各个功能块中的实现又是相似的,不得不去写一遍。比如 get_prop 这个函数,它在 ztimer 中的实现如下: static ret_t aw_plc_fb_ztimer_get_prop(aw_plc_fb_t* fb, const char* name, value_t* v) {
  aw_plc_fb_ztimer_t* ztimer = aw_plc_fb_ztimer(fb);
if (tk_str_eq(name, aw_plc_fb_ztimer_prop_in)) {
    value_set_bool(v, ztimer->in);
    return ret_ok;
  }
if (tk_str_eq(name, aw_plc_fb_ztimer_prop_pt)) {
    value_set_uint64(v, ztimer->pt);
    return ret_ok;
  }
if (tk_str_eq(name, aw_plc_fb_ztimer_prop_q)) {
    value_set_bool(v, ztimer->q);
    return ret_ok;
  }
if (tk_str_eq(name, aw_plc_fb_ztimer_prop_et)) {
    value_set_uint64(v, ztimer->et);
    return ret_ok;
  }
if (tk_str_eq(name, aw_plc_fb_ztimer_prop_count)) {
    value_set_uint32(v, ztimer->count);
    return ret_ok;
  }
return ret_not_found;
}
* 这样的代码看起来很简单,但是恰恰容易出错,更容易让人厌倦,没有什么乐趣。
2. api 和 结构的注释。我们来看看 ztimer 的结构注释: /**
 * @class aw_plc_fb_ztimer_t
 * @parent aw_plc_fb_t
 * @annotation [fb]
 * 循环定时器。
 * 
 * > 当输入 in 为 true 时,开始计时,输出 q 为 false,et 开始记录过去的时间。
 * > 定时时间到时,count 增加 1, 输出 q 在本次循环为 true,et 重置为 0。
 * > 输入 in 为 false 时重置定时器。
 */
typedef struct _aw_plc_fb_ztimer_t {
  aw_plc_fb_t fb;
/** 
   * @property {bool_t} in
   * @annotation [in]
   * 为 true 开始计时,为 false 时重置定时器。
   */
  bool_t in : 1;
/** 
   * @property {iec_time_t} pt
   * @annotation [in]
   * 预设时间 (ms)。
   */
  iec_time_t pt;
/** 
   * @property {bool_t} q
   * @annotation [default,out]
   * 定时时间是否到(仅在时间到的当次循环为 true)。
   */
  bool_t q : 1;
/** 
   * @property {iec_time_t} et
   * @annotation [out]
   * 过去时间 (ms)。
   */
  iec_time_t et;
/** 
   * @property {uint32_t} count
   * @annotation [out]
   * 定时器时间到的次数。
   */
  uint32_t count;
/** 
   * @property {bool_t} prev_in
   * @annotation [private]
   * 前一次的输入。
   */
  bool_t prev_in : 1;
/** 
   * @property {uint8_t} state
   * @annotation [private]
   * 状态。
   */
  uint8_t state;
/** 
   * @property {iec_time_t} current_time
   * @annotation [private]
   * 当前时间 (ms)。
   */
  iec_time_t current_time;
/** 
   * @property {iec_time_t} start_time
   * @annotation [private]
   * 开始时间 (ms)。
   */
  iec_time_t start_time;
} aw_plc_fb_ztimer_t;
* 上面的代码看起来很美观,读起来很舒服,但是写起来却是有些费劲。3. ide 需要功能块的描述信息,以方便把它呈现到界面上。比如 ztimer 的描述信息如下: {
 type: fb_zlg_misc.ztimer,
 real_type: ztimer,
 helpurl: https://developer.zlg.cn,
 style: fb,
 desc: 循环定时器。 > 当输入 in 为 true 时,开始计时,输出 q 为 false,et 开始记录过去的时间。
> 定时时间到时,count 增加 1, 输出 q 在本次循环为 true,et 重置为 0。 > 输入 in 为 false 时重置定时器。,
 ins: [
  {
   name: in,
   desc: 为 true 开始计时,为 false 时重置定时器。,
   min_connections: 1,
   max_connections: 1,
   data_type: bool
  },  
  {
   name: pt,
   desc: 预设时间 (ms)。,
   min_connections: 1,
   max_connections: 1,
   data_type: time
  }
 ],
 outs: [
  {
   name: q,
   desc: 定时时间是否到(仅在时间到的当次循环为 true)。,
   data_type: bool
  },  
  {
   name: et,
   desc: 过去时间 (ms)。,
   data_type: time
  },
  {
   name: count,
   desc: 定时器时间到的次数。,
   data_type: dword
  }
 ]
}
* 这个 json 文件中的内容,和前面结构的注释很相似,除了呈现的格式不同,同时还加了一些新内容。4. ide 需要的文档。功能块需要提供一个 markdown 文档,这个文档会被转换成 html,在用户查看帮助时显示给用户。ztimer 的文档内容如下: # ztimer
## 功能
循环定时器。
> 当输入 in 为 true 时,开始计时,输出 q 为 false,et 开始记录过去的时间。
> 定时时间到时,count 增加 1, 输出 q 在本次循环为 true,et 重置为 0。 
> 输入 in 为 false 时重置定时器。
## 输入
* in **bool** 为 true 开始计时,为 false 时重置定时器。
 * pt **time** 预设时间 (ms)。
## 输出
* q **bool** 定时时间是否到(仅在时间到的当次循环为 true)。
 * et **time** 过去时间 (ms)。
 * count **dword** 定时器时间到的次数。 * 这个文档的内容和前面结构的注释,除了形式不同,内容是差不多的。很抱歉贴了这么代码,希望您并没仔细去读它们。不要被这些代码吓到,它们都是自动生成的。如果手工去写这些代码,一天能写一个功能块就不错了,不但辛苦而且容易出错。这些工作必须自动完成!所以 awplc 中提供了一个代码生成器,实测这个代码生成器让工作效率提高 10 倍,幸福指数提高 10 倍。 在进入正题前,我们先聊一下代码生成器的基本知识。
代码生成器基本知识
* 编写能编写代码的代码。-- 《程序员修炼之道》
代码生成器是一个普通程序,它能够生成另外的目标代码。可以不要代码生成器,直接编写目标代码吗?通常情况下是可以的,但是这违背了优秀程序员的第一美德-懒惰。因为懒惰,所以能让计算机做的事,优秀程序员是不会自己去做的。
这里所说的目标代码,也并非一定是严格意义上的代码,也可能是另外一些数据。当然,有时候要严格区分数据和代码,本身就是一件困难的事情。不过,这不是我们要说的重点,重点是通过代码生成器提升我们的工作效率。* 一个人的数据就是另外一个人的代码。-- 《编程珠玑 ii》1. 代码生成器的分类要说分类,就要先说分类的标准,在不同的分类标准和分类依据下,分出的类别迥异。《程序员修炼之道》里提到的一个分类标准具有极强的实用意义,这里我们重点介绍一下。它根据生成的目标代码是否需要二次修改来分类,将代码生成器分为两类: 被动代码生成器 目标代码生成之后,需要进行修改和完善,然后独立发展和维护,与代码生成器再与关系。比如 ide 的 wizard 就是此例。前面提到的自定义控件生成器,代码生成之后,你需要在上面添加需要的功能。如果过了一段时间,你想为控件添加一个新的属性,可能会遇到一点麻烦,要么手工添加;要么重新生成代码,然后把之前修改的代码重新加上,无论哪种方式都不是愉快的方式。被动代码生成器虽然有它的缺陷,但是仍然可以给我们带来很大帮助。
主动代码生成器 目标代码生成之后,不需要进行修改和完善,每次都重新生成,如果需要修改,修改元数据和代码生成器。比如编译器就是此例。前面提到的 mvvm 的 viewmodel 和 awflow 应用代码生成也属于此类。如果可以,优先使用主动代码生成器。
2. 基本形式 这是代码生成器的基本形式:代码生成器读取元数据,生成目标代码。元数据是描述数据的数据,这里是描述目标代码的数据,也就是控制目标代码的参数。一般情况下,目标代码整体结构由代码生成器决定,而变化的部分由元数据决定。
代码生成器本身一个很有意思的话题,有机会可以专门来聊聊,本文就不扯远了。  
 awplc 中的代码生成器
按前面代码生成器的分类方式,awplc 里实现了一个主动代码生成器,实现成主动代码生成器是很重要的,awplc 还在快速迭代中,有些接口可能会变化,主动代码生成器保证,即使接口有变化,也只需要运行一些脚本,重新生成目标文件即可。 1. 基本架构
awplc 功能块代码生成器架构如下图所示。其中功能块描述文件就是前面所说的元数据,代码生成器用它生成前面介绍的各种代码和数据。 2. 功能块描述文件格式
描述文件用标准的 json 格式,其内容包括两个部分:
2.1 基本信息 基本信息包括:
name 功能块的名称。英文小写,必须是合格的 c 语言变量名; category 功能块所属的分类。各层级之间用/分隔,它决定了生成文件的位置; is_function_block true 表示功能块,false 表示函数; impl 具体实现的源文件; author 作者联系方式; version 版本号;; date 更新时间; desc 功能描述; properties 属性列表。具体定义如下。 示例:
  name: ztimer,
  category: zlg/misc,
  is_function_block: true,
  impl: input/zlg/misc/ztimer.c,
  author: li xianjing,
  desc: 循环定时器。 > 当输入 in 为 true 时,开始计时,输出 q 为 false,et 开始记录过去的时间>。 > 定时时间到时,count 增加 1, 输出 q 在本次循环为 true,et 重置为 0。 > 输入 in 为 false 时重
置定时器。,
2.2 属性描述对于每个属性,又包括下列信息: name 属性名; desc 属性描述; type 实际的数据类型; data_type(可选) 用于在 ide 中时类型检查,缺省为 type 对应的 iec 的数据类型,但是有时可用 any_int 和 any_num 等来放宽类型检查; annotation 用于额外的标识。目前主要用于指定输入输出等特性。 示例:     {
      name: count,
      desc: 定时器时间到的次数。,
      type: uint32_t,
      annotation: {
        out: true
      }    
},
2.3 使用方法
代码生成器用 nodejs 编写,需要安装 nodejs。具体用法如下:
node gen.js 描述文件名。
如:
node gen.js input/zlg/misc/ztimer.json
上面介绍了用 c 语言开发原生功能块的方法。当然,也可以用 iec 61131-3 中一些语言开发功能块,除此之外,awplc 还会支持用 awblock 开发功能块,在后续文章中,我们将一一介绍,敬请关注。awplc 目前还处于开发阶段的早期,写这个系列文章的目的,除了用来验证目前所做的工作外,还希望得到大家的指点和反馈。如果您有任何疑问和建议,请在评论区留言。


6N1电子管耳放的电源电路图
解析4680电池和21700电池
基于蒙特卡罗方法的理论
奥克斯空调多重优惠“花式”来袭 巅峰狂欢趴即将开幕
人脸识别系统现如今已发展到了什么程度?
用AWTK和AWPLC快速开发自定义功能块
Nio EC6的价格预计将在2020年成都车展上宣布
NVIDIA将通过5G技术传输云渲染VR/AR内容
所有家庭设备连接物联网生活将会如何改变你会期待吗
摇一摇手机就能变色的LED灯 获美国IDEA工业设计大奖
邮轮控制系统中电容器的作用什么?
基于CDMA 1X的无线通信监控方案设计
UDS在CAN和以太网上的实现方案
efuseIP进行spice建模的实现方案
法国石油巨头道达尔收购电池开发商的股份 并专注于开发针对高能量密度电池的先进材料
第三届无代码探索者大会将于7月6日盛大开幕
数字频率合成器的作用
配置开关频率的方法?何时需要调节开关频率的大小?
“歌尔方案”助力消费电子产业升级
全屋智能三国志