Modbus 插件开发示例

南向驱动开发主要包含以下几个部分,最底层的是协议层开发,最外层的是驱动层开发。

模块文件说明
协议层开发modbus.c modbus.h插件对接的设备协议报文的组包与解包
协议栈解析modbus_stack.c modbus_stack.h主要用于协议栈的解析
点位处理modbus_point.c modbus_point.hNeuron 中 tag 类型转换为插件所需要的更具体的类型
驱动层开发modbus_tcp.c modbus_req.c modbus_req.h插件主题框架的实现
插件设置文件modbus-tcp.json插件设置文件的定义

常量说明

头文件 <define.h> 中定义了 Neuron 中主要的常量。

常量说明
NEU_TAG_NAME_LEN点位名称最大长度,32
NEU_TAG_ADDRESS_LEN点位地址的最大长度,64
NEU_TAG_DESCRIPTION_LEN点位描述字符串最大长度,128
NEU_GROUP_NAME_LEN组名字最大长度,32
NEU_GROUP_INTERVAL_LIMIT组的时间间隔下限,100
NEU_NODE_NAME_LEN节点名称最大长度,32
NEU_PLUGIN_NAME_LEN插件名称最大长度,32
NEU_PLUGIN_LIBRARY_LEN插件动态库文件名称最大长度,32
NEU_PLUGIN_DESCRIPTION_LEN插件描述字符串最大长度,512
NEU_DRIVER_TAG_CACHE_EXPIRE_TIME驱动点位缓存过期时间,30
NEU_APP_SUBSCRIBE_MSG_SIZE北向订阅消息的大小,4
NEU_TAG_FLOAG_PRECISION_MAX浮点类型精度的最大值,17
  1. typedef enum neu_plugin_kind {
  2. NEU_PLUGIN_KIND_STATIC = 0,
  3. NEU_PLUGIN_KIND_SYSTEM = 1,
  4. NEU_PLUGIN_KIND_CUSTOM = 2,
  5. } neu_plugin_kind_e;

插件类型:

  • NEU_PLUGIN_KIND_STATIC,Neuron 内嵌插件,不需要动态库文件;
  • NEU_PLUGIN_KIND_SYSTEM,系统插件,需要以动态库文件方式存储,可由 Neuron 加载;
  • NEU_PLUGIN_KIND_CUSTOM,自定义插件,需要以动态库文件方式存储,可由 Neuron 加载;
  1. typedef enum {
  2. NEU_NA_TYPE_DRIVER = 1,
  3. NEU_NA_TYPE_APP = 2,
  4. } neu_adapter_type_e,
  5. neu_node_type_e;

节点类型,NEU_NA_TYPE_DRIVER 代表南向节点,NEU_NA_TYPE_APP 代表北向节点。

  1. typedef enum {
  2. NEU_NODE_LINK_STATE_DISCONNECTED = 0,
  3. NEU_NODE_LINK_STATE_CONNECTED = 1,
  4. } neu_node_link_state_e;

节点连接状态,NEU_NODE_LINK_STATE_DISCONNECTED 代表断开连接,NEU_NODE_LINK_STATE_CONNECTED 代表已连接。

  1. typedef enum {
  2. NEU_NODE_RUNNING_STATE_IDLE = 0,
  3. NEU_NODE_RUNNING_STATE_INIT = 1,
  4. NEU_NODE_RUNNING_STATE_READY = 2,
  5. NEU_NODE_RUNNING_STATE_RUNNING = 3,
  6. NEU_NODE_RUNNING_STATE_STOPPED = 4,
  7. } neu_node_running_state_e;

节点运行状态:

  • NEU_NODE_RUNNING_STATE_IDLE,驱动配置中,进入配置中;
  • NEU_NODE_RUNNING_STATE_INIT,节点创建成功后,进入初始化;
  • NEU_NODE_RUNNING_STATE_READY,驱动配置完成后,进入准备好;
  • NEU_NODE_RUNNING_STATE_RUNNING,打开节点工作状态,进入运行中;
  • NEU_NODE_RUNNING_STATE_STOPPED,关闭节点工作状态,进入停止;

第一步,协议层开发

这部分主要实现的是插件对接的设备协议的组包与解包的实现,还包括一些结构体定义以及相关函数的实现,其中 modbus.h 定义 modbus 协议基本报文,以及对该报文的组包和解包函数,modbus.c 实现组包与解包的函数。

Modbus 协议通过 TCP 方式传输时,读写请求报文格式如下列表格所示。

报文格式数据长度说明函数实现
报文头6 个字节应用报文头,包含传输标识、协议标识和字节长度组包函数 modbus_header_wrap 和解包函数 modbus_header_unwrap
地址码1 个字节设备地址,也是站点号组包函数 modbus_code_wrap 和解包函数 modbus_code_unwrap
功能码1 个字节通知执行哪种操作,不同的功能码对应不同的操作组包函数 modbus_code_wrap 和解包函数 modbus_code_unwrap
寄存器起始地址2 个字节指定读取的寄存器开始地址,高字节在前,低字节在后组包函数 modbus_address_wrap 和解包函数 modbus_address_unwrap
寄存器数量2 个字节指定读取的寄存器的数量,高字节在前,低字节在后组包函数 modbus_address_wrap 和解包函数 modbus_address_unwrap
数据域N 个字节在写指令中用到,需要写入寄存器的值写入寄存器数值的组包函数 modbus_data_wrap 和解包函数 modbus_data_unwrap

枚举说明

枚举说明
modbus_functionModbus 不同的功能码
modbus_area不同寄存器类型

modbus_code 代码解析

modbus_header,modbus_code,modbus_address,modbus_data 代码实现相似,以 modbus_header 为例代码分析。

  1. // Pack modbus_header into protocol_pack_buf
  2. void modbus_header_wrap(neu_protocol_pack_buf_t *buf, uint16_t seq)
  3. {
  4. // Check if there is enough space in protocol_pack_buf to store the structure of modbus_header
  5. assert(neu_protocol_pack_buf_unused_size(buf) >=
  6. sizeof(struct modbus_header));
  7. // Allocate memory for modbus_header in protocol_pack_buf and return the starting address of modbus_header
  8. struct modbus_header *header =
  9. (struct modbus_header *) neu_protocol_pack_buf(
  10. buf, sizeof(struct modbus_header));
  11. // Assign value to header
  12. header->seq = htons(seq);
  13. header->protocol = 0x0;
  14. header->len = htons(neu_protocol_pack_buf_used_size(buf) -
  15. sizeof(struct modbus_header));
  16. }
  17. // Parse modbus_header from received protocol_unpack_buf
  18. int modbus_header_unwrap(neu_protocol_unpack_buf_t *buf,
  19. struct modbus_header * out_header)
  20. {
  21. // Get the start address of modbus_header from protocol_unpack_buf
  22. struct modbus_header *header =
  23. (struct modbus_header *) neu_protocol_unpack_buf(
  24. buf, sizeof(struct modbus_header));
  25. // When the data size in protocol_unpack_buf is less than the size of modbus_header, return 0
  26. if (header == NULL ||
  27. ntohs(header->len) > neu_protocol_unpack_buf_unused_size(buf)) {
  28. return 0;
  29. }
  30. // When the parsed packet does not conform to the protocol specification, return -1
  31. if (header->protocol != 0x0) {
  32. return -1;
  33. }
  34. *out_header = *header;
  35. out_header->len = ntohs(out_header->len);
  36. out_header->seq = ntohs(out_header->seq);
  37. // When parsing mmmodbus_header is successful, return the parsed data size
  38. return sizeof(struct modbus_header);
  39. }

第二步,协议栈解析

这部分主要用于协议栈的解析,主要包含协议栈解析的初始化和资源的释放,生成读写请求报文,接收到的读写响应报文的解析及数据处理。

函数名称说明
modbus_stack_create主要用于协议栈解析的初始化。
modbus_stack_destroy主要用于释放协议栈解析资源。
modbus_stack_recv主要用于向协议栈解析器中传递从 tcp/udp/serial port 中接收到的数据,并对数据进行处理分析。
modbus_stack_read主要用于通过协议解析器,生成要读取点位的协议报文。
modbus_stack_write主要用于通过协议解析器,生成要写入点位值的协议报文。

第三步,点位处理

这部分主要用于将 Neuron 中设置的 tag 信息转换为所需要的更具体的信息。

  • Neuron 中点位配置的地址可以进一步解析成设备地址 Slave ID,寄存器类型和寄存器起始地址,并能校验该地址是否符合插件规范。
  • 不同的寄存器支持的数据类型不同,这里可以根据解析出来的寄存器类型及 Neuron 中配置的属性进行判断配置的地址是否支持该属性。
  • Modbus 支持批量读取数据,Neuron 可以把用户配置的点位进行聚合分类,根据分类的结果,可实现批量读取数据的功能。

第四步,驱动层开发

modbus_req.c 和 modbus_req.h 文件定义 modbus_tcp.c 文件中使用的具体函数实现及结构体。modbus_tcp.c 文件主要是插件的接口函数,由 neuron 中的 plugin.h 定义,plugin_intf_funs 说明如下。

open

调用 driver_open 函数,基于 plugin 创建 node 时 neuron 第一个调用的函数,创建插件自己定义的结构体 struct neu_plugin。该结构体在 modbus_req.h 中定义,需要注意的是结构体中的第一个成员一定是 neu_plugin_common_t common,其他成员可根据驱动的具体实现增加。

  1. static neu_plugin_t *driver_open(void)
  2. {
  3. neu_plugin_t *plugin = calloc(1, sizeof(neu_plugin_t));
  4. neu_plugin_common_init(&plugin->common);
  5. return plugin;
  6. }

close

调用 driver_close 函数,删除 node 时,neuron 调用的最后一个函数,用于释放由 open 创建的 neu_plugin_t。

  1. static int driver_close(neu_plugin_t *plugin)
  2. {
  3. free(plugin);
  4. return 0;
  5. }

init

调用 driver_init 函数,在创建 node 时,neuron 调用完 open 后,紧接着调用的函数。此函数主要做插件内需要初始化的一些资源,modbus 插件中主要初始化 modbus 协议栈解析。其中的回调函数都在 modbus_req 文件中实现。

  1. static int driver_init(neu_plugin_t *plugin)
  2. {
  3. plog_info(plugin, "node: modbus init");
  4. return 0;
  5. }

uninit

调用 driver_uninit 函数,删除 node 时,neuron 首先调用的函数,此函数主要释放一些在 init 中申请以及初始化的资源。

  1. static int driver_uninit(neu_plugin_t *plugin)
  2. {
  3. plog_info(plugin, "node: modbus uninit");
  4. return 0;
  5. }

start

调用 driver_start 函数,用户在 neuron node 页面,点击开始时,neuron 会调用此函数,start 不做任何处理,只返回 0,通知插件开始运行,以及开始连接设备等,连接状态的处理放在 tcp 连接的异步回调函数中处理。

start

stop

调用 driver_stop 函数,用户在 neuron node 页面,点击停止时,neuron 会调用此函数,stop 通知插件停止运行,关闭与设备之间的连接,并且 driver.group_timer 将不会再触发。

stop

setting

调用 driver_config 函数,用户在 neuron node 设置页面进行设置时使用,node 设置的参数将通过 json 方式呈现,neuron 将通过此函数通知插件进行设置。driver_config 函数首先会解析并保存配置信息,然后建立 tcp 连接。modbus_conn_connected 和 modbus_conn_disconnected 两个回调函数会分别在 tcp 建立和关闭连接时被调用,相应的这两个函数会更新插件的连接状态。

config

  1. static int driver_config(neu_plugin_t *plugin, const char *config)
  2. {
  3. plog_info(plugin, "config: %s", config);
  4. return 0;
  5. }

request

调用 driver_request 函数,此函数在南向 driver 中暂未使用到。

driver.validate_tag

调用 driver_validate_tag 函数,在向 node 中添加 tag 或更新 tag 时,neuron 会把 tag 相关参数使用此函数通知到插件,插件根据各自实现检查此 tag 参数是否符合插件要求,该函数返回 0,代表成功。

  1. static int driver_validate_tag(neu_plugin_t *plugin, neu_datatag_t *tag)
  2. {
  3. plog_info(plugin, "validate tag: %s", tag->name);
  4. return 0;
  5. }

driver.group_timer

调用 driver_group_timer 函数,在 node 中添加 group,将 node 状态置为 running 后,此函数将以 group 的 interval 参数定时调用,主要用于与设备的交互,读取设备数据。

neu_plugin_group_t 结构体参数说明。

参数说明
group_name触发 timer 的 group 名称
tagsneu_datatag_t 类型的数组
user_data用户自定义的信息
group_free当删除此 group 时,释放用户自定义信息使用的回调函数
  1. static int driver_group_timer(neu_plugin_t *plugin, neu_plugin_group_t *group)
  2. {
  3. (void) plugin;
  4. (void) group;
  5. plog_info(plugin, "timer....");
  6. return 0;
  7. }

driver.write_tag

调用 driver_write 函数,当使用 write api 时,neuron 将调用此函数,通知插件,向点位 tag 写入特定的值。

  1. static int driver_write(neu_plugin_t *plugin, void *req, neu_datatag_t *tag,
  2. neu_value_u value)
  3. {
  4. (void) plugin;
  5. (void) req;
  6. (void) tag;
  7. (void) value;
  8. return 0;
  9. }

第五步,插件设置文件

modbus-tcp.json 文件配置插件设置,其中 tag_regex 和 timeout 是必填项,其他参数可根据插件配置自行添加,参数说明如下表所示。

参数说明
tag_regex针对驱动支持不同的数据类型,对地址配置的正则
description该字段的详细说明
attribute该字段的属性,只有两种可选和必选,即 required 和 optional
type该字段的类型,目前常用的是 int 和 string 两种类型
default填写的默认值
valid该字段可填写的范围
  1. {
  2. "tag_regex": [
  3. {
  4. "type": 3,
  5. "regex": "[1-9]+![3-4][0-9]+(#B|#L|)$"
  6. },
  7. {
  8. "type": 4,
  9. "regex": "[1-9]+![3-4][0-9]+(#B|#L|)$"
  10. },
  11. {
  12. "type": 5,
  13. "regex": "[1-9]+![3-4][0-9]+(#BB|#BL|#LL|#LB|)$"
  14. },
  15. {
  16. "type": 6,
  17. "regex": "[1-9]+![3-4][0-9]+(#BB|#BL|#LL|#LB|)$"
  18. },
  19. {
  20. "type": 9,
  21. "regex": "[1-9]+![3-4][0-9]+(#BB|#BL|#LL|#LB|)$"
  22. },
  23. {
  24. "type": 11,
  25. "regex": "[1-9]+!([0-1][0-9]+|[3-4][0-9]+.([0-9]|[0-1][0-5]))$"
  26. }
  27. ],
  28. "host": {
  29. "name": "host",
  30. "description": "local ip in server mode, remote device ip in client mode",
  31. "attribute": "required",
  32. "type": "string",
  33. "valid": {
  34. "regex": "/^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$/",
  35. "length": 30
  36. }
  37. },
  38. "port": {
  39. "name": "port",
  40. "description": "local port in server mode, remote device port in client mode",
  41. "attribute": "required",
  42. "type": "int",
  43. "default": 502,
  44. "valid": {
  45. "min": 1,
  46. "max": 65535
  47. }
  48. },
  49. "timeout": {
  50. "name": "timeout",
  51. "description": "recv msg timeout(ms)",
  52. "attribute": "required",
  53. "type": "int",
  54. "default": 3000,
  55. "valid": {
  56. "min": 1000,
  57. "max": 65535
  58. }
  59. }
  60. }