分类 IoT 网关 下的文章

此次移植工作以中国移动 ML302 模组为基础,亿琪软件 YiDTU 设备为原型,开发基于 Modbus Master 应用;
传统模式下,DTU 都是以透传为主要工作,所有的业务需求都是从服务器端发出,而 YiDTU 是作为边缘计算的角色,在工业现场就已经把数据处理做完;

yigate-yidtu.png

准备工作

硬件设备

  • YiDTU 设备一台
    亿琪软件 YiDTU 设备内置 UART 转 RS485 模块,对外连接 RS485 双线设备,如:YiSensor 模拟量采集模块;对于开发而言,那就是主要针对 UART 串口操作;
  • Micro USB 线一根
    用于操作 YiDTU 设备:AT 串口操作,烧录操作;
  • RS485 设备若干
    可用 USB 转 RS485 转换器代替(需配合 modbus slave 软件使用);有支持 Modbus Slave 设备最好;准备了两个:模拟量 4-20mA 和 开关量各一个;
  • 9~30V 直流电源一个
    支持最大 2A 电流的直流电源一个,用于给 YiDTU 设备供电;
  • 将以上硬件设备连接好

软件 SDK

  • 从中国移动物联网公司获取 ML302 相关 SDK,;
  • 确保 SDK 编译正常;
  • 烧录基本应用成功;

移植过程

Modbus Master 库

  • https://github.com/jiekechoo/ModBus-Master.git 获取源代码,使用 STM32 环境编译成功,确保你的环境没问题;
  • 将 Modbus_Master 目录移植到 ML302 工程中 src/demo/modbus 下,根据需求删除不必要的组件功能;
  • ML302 环境下编译通过,确保没有错误出现;
  • Modbus 枚举值要重新定义
typedef enum
{
  // Modbus function codes for bit access
  ku8MBReadCoils = 0x01,          ///< Modbus function 0x01 Read Coils
  ku8MBReadDiscreteInputs = 0x02, ///< Modbus function 0x02 Read Discrete Inputs
  ku8MBWriteSingleCoil = 0x05,    ///< Modbus function 0x05 Write Single Coil
  ku8MBWriteMultipleCoils = 0x0F, ///< Modbus function 0x0F Write Multiple Coils

  // Modbus function codes for 16 bit access
  ku8MBReadHoldingRegisters = 0x03,      ///< Modbus function 0x03 Read Holding Registers
  ku8MBReadInputRegisters = 0x04,        ///< Modbus function 0x04 Read Input Registers
  ku8MBWriteSingleRegister = 0x06,       ///< Modbus function 0x06 Write Single Register
  ku8MBWriteMultipleRegisters = 0x10,    ///< Modbus function 0x10 Write Multiple Registers
  ku8MBMaskWriteRegister = 0x16,         ///< Modbus function 0x16 Mask Write Register
  ku8MBReadWriteMultipleRegisters = 0x17 ///< Modbus function 0x17 Read Write Multiple Registers
} modbusFunc;

串口接收支持

  • 串口接收任务中,将收到的内容复制到全局变量中,目的是把 Modbus Slave 回复的内容获取到,交给 Modbus 组件使用;
extern uint8_t u8ModbusADU[1024];
extern uint8_t u8ModbusADUSize;

// 接收处理函数中
{
            u8ModbusADUSize = uart_buf_len;
            memcpy(u8ModbusADU, uart_buf, u8ModbusADUSize);
}
  • 接收完成后,创建一个 信号量,通知 Modbus 组件来处理;
{
                uart_buf_len = 0;
                recv_count = 0;
                osSignalSet(OC_Main_TaskHandle, 0x0004);
}

Modbus 组件

  • 将 Modbus 组件中接收处理替换成信号量等待,超时重置接收缓冲区;
  // 串口收到数据信号量,2000ms 超时
  osEvent osevent = osSignalWait(0x0004, ku16MBResponseTimeout);
  if (osevent.status == osEventTimeout)
  {
    u8MBStatus = ku8MBResponseTimedOut;
    memset(u8ModbusADU, 0, sizeof(u8ModbusADU));
    u8ModbusADUSize = 0;
  }
  • Modbus 发送函数重写
uint8_t Modbus_Master_Write(uint8_t *buf, uint8_t length)
{
  //  if(HAL_UART_Transmit(&huart2 ,(uint8_t *)buf,length,0xff))
  if (cm_uart_send_no_cache(OPENCPU_MAIN_URAT, (uint8_t *)buf, length, 0xff) == 0)
  {
    return HAL_ERROR;
  }
  else
  {
    return HAL_OK;
  }
}
  • 如果有 CS 控制口 IO,需要自定义相关函数
#if 0
  // transmit request RS485接口是需要每次发送前改变接口的模式
  
  if (_preTransmission)
  {
    _preTransmission();
  }
#endif

  //串口发送数据
  Modbus_Master_Write(u8ModbusADU, u8ModbusADUSize);
  u8ModbusADUSize = 0;

#if 0
// transmit request RS485接口是需要每次发送后改变接口的模式
      if (_postTransmission)
  {
    _postTransmission();
  }
#endif

移植过程比较简单,细节需要一定的嵌入式和 C 语言基础。

效果展示

在 YiDTU 业务逻辑中增加相应的功能模块,把数据传输到云平台,这样,就可以及时获取现场的模拟量数据。
PSX_20210119_151717.jpg

Google Protocol Buffers ,一个非常高效的数据序列化工具,ava/Python/C++/C#等都是完美支持,可惜了,官方不支持C语言,还好世界强大,有人做了C语言版本:https://github.com/protobuf-c/protobuf-c

关于protobuf,请自行google或百度查询,这里不再啰嗦,进入正题。

1. 第一步:安装Protocol Buffers

https://developers.google.com/protocol-buffers/
很简单,download下来,编译即可;make && make install

2. 第二步:安装protobuf-c

https://github.com/protobuf-c/protobuf-c
也是很简单,download下来,编译即可;make && make install

当你输入 protoc-c 出现以下信息,代表安装成功。

jiekechoo:mqtt_protoc jiekechoo$ protoc-c 
Usage: protoc-c [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
  -IPATH, --proto_path=PATH   Specify the directory in which to search for
                              imports.  May be specified multiple times;
                              directories will be searched in order.  If not
                              given, the current working directory is used.
                              If not found in any of the these directories,
                              the --descriptor_set_in descriptors will be
                              checked for required proto file.
  --version                   Show version info and exit.
  -h, --help                  Show this text and exit.
  --encode=MESSAGE_TYPE       Read a text-format message of the given type
                              from standard input and write it in binary
                              to standard output.  The message type must
                              be defined in PROTO_FILES or their imports.
  --decode=MESSAGE_TYPE       Read a binary message of the given type from
                              standard input and write it in text format
                              to standard output.  The message type must
                              be defined in PROTO_FILES or their imports.
  --decode_raw                Read an arbitrary protocol message from
                              standard input and write the raw tag/value
                              pairs in text format to standard output.  No
                              PROTO_FILES should be given when using this
                              flag.
  --descriptor_set_in=FILES   Specifies a delimited list of FILES
                              each containing a FileDescriptorSet (a
                              protocol buffer defined in descriptor.proto).
                              The FileDescriptor for each of the PROTO_FILES
                              provided will be loaded from these
                              FileDescriptorSets. If a FileDescriptor
                              appears multiple times, the first occurrence
                              will be used.
  -oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
    --descriptor_set_out=FILE defined in descriptor.proto) containing all of
                              the input files to FILE.
  --include_imports           When using --descriptor_set_out, also include
                              all dependencies of the input files in the
                              set, so that the set is self-contained.
  --include_source_info       When using --descriptor_set_out, do not strip
                              SourceCodeInfo from the FileDescriptorProto.
                              This results in vastly larger descriptors that
                              include information about the original
                              location of each decl in the source file as
                              well as surrounding comments.
  --dependency_out=FILE       Write a dependency output file in the format
                              expected by make. This writes the transitive
                              set of input file paths to FILE
  --error_format=FORMAT       Set the format in which to print errors.
                              FORMAT may be 'gcc' (the default) or 'msvs'
                              (Microsoft Visual Studio format).
  --print_free_field_numbers  Print the free field numbers of the messages
                              defined in the given proto files. Groups share
                              the same field number space with the parent 
                              message. Extension ranges are counted as 
                              occupied fields numbers.

  --c_out=OUT_DIR             Generate C/H files.
  @<filename>                 Read options and filenames from file. If a
                              relative file path is specified, the file
                              will be searched in the working directory.
                              The --proto_path option will not affect how
                              this argument file is searched. Content of
                              the file will be expanded in the position of
                              @<filename> as in the argument list. Note
                              that shell expansion is not applied to the
                              content of the file (i.e., you cannot use
                              quotes, wildcards, escapes, commands, etc.).
                              Each line corresponds to a single argument,
                              even if it contains spaces.

3. 测试protobuf

转换proto文件为 C 语言库

protoc-c --c_out=. kurapayload.proto

生成protoc文件

-rw-r--r--  1 jiekechoo  staff  14836  8 18 11:36 kurapayload.pb-c.c
-rw-r--r--  1 jiekechoo  staff   6427  8 18 11:36 kurapayload.pb-c.h
-rw-r--r--  1 jiekechoo  staff   1749  8 18 11:17 kurapayload.proto

完成protobuf集成工作

#include <stdio.h>
#include <stdlib.h>
#include "MQTTClient.h"
#include "kurapayload.pb-c.h"
 
void main()
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
int rc;

MQTTClient_create(&client, ADDRESS, CLIENTID,
    MQTTCLIENT_PERSISTENCE_NONE, NULL);
conn_opts.keepAliveInterval = 20;
conn_opts.cleansession = 1;
conn_opts.username = "username";
conn_opts.password = "password";

if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
{
    printf("Failed to connect, return code %d\n", rc);
    exit(-1);
}
    Kuradatatypes__KuraPayload payload = KURADATATYPES__KURA_PAYLOAD__INIT;
    Kuradatatypes__KuraPayload__KuraMetric **metrics;
    Kuradatatypes__KuraPayload__KuraPosition position = KURADATATYPES__KURA_PAYLOAD__KURA_POSITION__INIT;
 
position.latitude = 1.1;
position.longitude = 1.1;

payload.position = &position;

metrics = malloc(sizeof (Kuradatatypes__KuraPayload__KuraMetric) * 2);
int i = 0;
for(i=0;i<2;i++)
{
    metrics[i] = malloc (sizeof (Kuradatatypes__KuraPayload__KuraMetric));
    kuradatatypes__kura_payload__kura_metric__init(metrics[i]);

    metrics[i]->name = (char*)EdcBirthPayloadNames[i];
    printf("%s, ", metrics[i]->name);
    metrics[i]->type = KURADATATYPES__KURA_PAYLOAD__KURA_METRIC__VALUE_TYPE__STRING;        
    metrics[i]->string_value = (char*)EdcBirthPayloadValues[i];
    printf("%s\r\n", metrics[i]->string_value);
}
    payload.n_metric = 2;
    payload.metric = metrics;

    size_t len = kuradatatypes__kura_payload__get_packed_size(&payload);
    void* data = malloc(len); 
    kuradatatypes__kura_payload__pack(&payload, data);
    pubmsg.payload = data;
    pubmsg.payloadlen = kuradatatypes__kura_payload__get_packed_size(&payload);
    MQTTClient_publishMessage(client, BIRTH_TOPIC, &pubmsg, &token);

}

编译,如果没有出错就上成功。

gcc kurapayload.pb-c.c main.c -lprotobuf-c

4. 注意事项

  • 系统要能够使用 malloc 功能,好多地方要用到;
  • 结构体初始化 INIT 或 使用 init 函数;
  • 数据准备完毕,要 pack 一下,再 packed_size 计算长度,后面该怎么处理都行了;

最终,C 语言版本的 Kura 模拟器成功连接 Kapua。
1761599121932_.pic_hd.jpg

Eclipse Kura 作为边缘计算网关应用套件,功能非常强大,作为一个基础性框架,在上面跑一些用户应用,那是非常合适的。

一、启用 Kura 上面支持 MQTT broker
进入 Kura web console,开启Simple Artemis MQTT Broker,测试中暂定 usename 和 password 都为 mqtt。

WX20200707-174344.png

登录 Kura 设备后台,查看 TCP 1833 端口已经开启

root@raspberrypi:/home/pi# netstat -nlpt |grep 1883
tcp        0      0 0.0.0.0:1883            0.0.0.0:*               LISTEN      427/java            

二、模拟 MQTT 设备发送数据
使用 Chrome 浏览器插件 MQTTBox 登录我们的 MQTT server
WX20200707-174844@2x.png

三、安装 Kura 插件
使用 Eclipse marketplace 安装以下插件

  • Apache Camel MQTT endpoint
  • Apache Camel Groovy language support
  • Apache Camel GSON data format

四、创建新的 Component
创新 component
Integrating-with-Kura-and-Kapua-5.png

并输入以下内容

<routes xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="paho:humidity/sensor1/humidity?brokerUrl=tcp://localhost:1883&amp;clientId=route1&amp;userName=mqtt&amp;password=mqtt"/>
    <unmarshal><json library="Gson"></json></unmarshal>
    <transform><simple>${body["humidity"]}</simple></transform>
    <transform><groovy>["HUMIDITY": request.body/100, "ASSETNAME": "HrY", "SENSOR": "sensor1"]</groovy></transform>
    <to uri="seda:wiresOutput1"/>
  </route>
</routes>

WX20200707-175458.png

五、创建一个 Cloud publisher
WX20200707-175640.png

六、新建一个 Wire Graph
Camel Consumer - camel_comsumer
WX20200707-175756.png

Publisher - pub_camel1
WX20200707-175907.png

把两个相连起来
WX20200707-180007.png

七、发送数据,测试 Kapua 数据接收情况
发布/订阅 主题topic: humidity/sensor1/humidity
WX20200707-180254@2x.png

发送数据验证
WX20200707-180510@2x.png

Kapua 接收,查询结果
WX20200707-180442.png

Apache Camel是一个基于规则路由和中介引擎,提供企业集成模式的Java对象的实现,通过应用程序接口来配置路由和中介的规则。领域特定语言意味着Apache Camel支持你在的集成开发工具中使用平常的,类型安全的,可自动补全的Java代码来编写路由规则,而不需要大量的XML配置文件。 来自维基百科

网站: https://camel.apache.org

Kura_Camel_Integration.png

根据上图所示,Apache Camel 与 Apache Kura 集成,可以使得 Kura 扩展更多的功能,完成更多的工作。通过简单的配置,或少量的代码,就可以实现数据路由和中转。