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

GD32 兆易创新单片机,国产芯,必须支持!完美兼容 ARM 系列其他产品,价格美丽,性能卓越,支持到位。
freeModbus,开源且免费的 Modbus slave 实现,支持各种平台,包括嵌入式单片机系统。

1. 准备 GD32 系列单片机设备,本文采用 GD32F130F8;

你需要一块带有 GD32 单片机的板子,可以说开发板或成品设备,电路正常,带有串口 USART,最好具备 RS485 电路;用官方的 GD32F1x0_Firmware_Library_V3.0.0 example 新建项目,编译成功,且下载到设备,能够运行跑马灯,串口能打印正常。

  • 串口输出正常,波特率正常,比如 USART0
  • 串口中断输入正常,收到外部串口输入能及时处理
  • 定时器TIMER正常,需要 us 微秒级控制
  • GPIO输出正常,能够控制高低电平

2、准备 freeModbus V1.6版本,V1.5 也可以;

作为开发者移植 Modbus,相关原理最好还是要熟悉一下:https://www.modbus.org/, 不管怎么样,通过你自己的方式熟读一下 Modbus 协议内容。
github下载 freeModbus, 给作者 star,fork 到自己的项目;

  • modbus 目录结构
    原封不动,搬进你的工程中
├── ascii // ASCII模式文件,本例程未用到
│   ├── mbascii.c
│   └── mbascii.h
├── functions // 功能函数模块,很重要,不需要修改
│   ├── mbfunccoils.c
│   ├── mbfuncdiag.c
│   ├── mbfuncdisc.c
│   ├── mbfuncholding.c
│   ├── mbfuncinput.c
│   ├── mbfuncother.c
│   └── mbutils.c
├── include  // 头文件,很重要,不需要修改
│   ├── mb.h
│   ├── mbconfig.h
│   ├── mbframe.h
│   ├── mbfunc.h
│   ├── mbport.h
│   ├── mbproto.h
│   └── mbutils.h
├── mb.c // 主文件,不需要修改
├── rtu // RTU 模块文件,很重要,本例程需要熟读内容
│   ├── mbcrc.c
│   ├── mbcrc.h
│   ├── mbrtu.c
│   └── mbrtu.h
└── tcp // TCP 模块,本例程未用到
    ├── mbtcp.c
    └── mbtcp.h
  • 移植要求
    主要是针对 port 目录下的移植
├── demo.c // 主函数移植内容
└── port // 移植目录
    ├── port.h // 头文件
    ├── portevent.c // 移植事件文件,本例程不修改
    ├── portserial.c // 移植串口处理,很重要,需要熟读
    └── porttimer.c // 移植定时器处理,很重要,需要熟读

3、移植过程;

main.c

使用上一步中的 demo/BARE/port 下所有文件导入 GD32 工程中,且加入所有的 .h 文件到编译目录;将 demo.c 中的主循环内容移植到你的工程主循环中,波特率,串口号,等等参数,自行根据需要修改。

    eMBErrorCode    eStatus;
    eStatus = eMBInit( MB_RTU, 0x03, 0, 9600, MB_PAR_NONE );
    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );
    for( ;; )
    {
        ( void )eMBPoll(  );
    }

port.c

在工程中新建一个 port.c , 把 modbus 回调函数填入,这里只是主要内容,其他默认回调也必须存在

    /* ----------------------- Defines ------------------------------------------*/
    #define REG_HOLDING_START ( 0x0000 )
    #define REG_HOLDING_NREGS ( 4 )
    
    /* ----------------------- Static variables ---------------------------------*/
    static USHORT   usRegHoldingStart = REG_HOLDING_START;
    static USHORT   usRegHoldingBuf[REG_HOLDING_NREGS] = { 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD};

    eMBErrorCode
    eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
    {
        /* error state */
        eMBErrorCode    eStatus = MB_ENOERR;
        /* offset */
        int16_t iRegIndex;
        
        /* test if the reg is in the range */
        if (((int16_t)usAddress-1 >= REG_HOLDING_START) 
            && (usAddress-1 + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS))
        {
            /* compute the reg's offset */
            iRegIndex = (int16_t)(usAddress-1 - REG_HOLDING_START);
            switch (eMode)
            {
                case MB_REG_READ:
                    while (usNRegs > 0)
                    {
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] >> 8 );
                        *pucRegBuffer++ = (uint8_t)( usRegHoldingBuf[iRegIndex] & 0xff);
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                case MB_REG_WRITE:
                    while (usNRegs > 0)
                    {
                        usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
                        usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
                        iRegIndex ++;
                        usNRegs --;
                    }
                    break;
                    
            }
        }
        else{
            eStatus = MB_ENOREG;
        }
        
        return eStatus;
    }

增加全局中断函数使能/失能调用,这里用 CMSIS 通用配置

void
EnterCriticalSection( void )
{
  __disable_irq();
}

void
ExitCriticalSection( void )
{
  __enable_irq();
}

portserial.c

串口使能/失能配置【很重要】

/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
    if (xRxEnable)
    {
      gd_eval_ledoff(LED3); // 输入模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 打开输入中断
    }
    else
    {
      gd_eval_ledon(LED3); // 输出模式,设置 RS485 芯片 RE 口低电平
      usart_interrupt_disable(EVAL_COM1, USART_INT_RBNEIE); // 关闭输入中断
    }
    
    if(xTxEnable)
    {
      usart_interrupt_enable(EVAL_COM1, USART_INT_TCIE); // 打开输出完成中断
    }
    else
    {
      usart_interrupt_disable(EVAL_COM1, USART_INT_TCIE); // 关闭输出完成中断
    }
}

串口初始化配置

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
    /* USART interrupt configuration */
    nvic_irq_enable(USART0_IRQn, 0, 2);  // 串口中断号设置
     
    gd_eval_COMinit(EVAL_COM1); // 初始化串口
 
    usart_interrupt_enable(EVAL_COM1, USART_INT_RBNEIE); // 初始化后好就打开串口输入中断
    gd_eval_ledoff(LED3); // 485 芯片 RE 口低电平输入模式
  
    return TRUE;
}

串口输出/输入单字节操作【很重要,也很简单】

BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
    /* Put a byte in the UARTs transmit buffer. This function is called
     * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
     * called. */
    usart_data_transmit(EVAL_COM1, ucByte);
    return TRUE;
}

BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
    /* Return the byte in the UARTs receive buffer. This function is called
     * by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
     */
   *pucByte = usart_data_receive(EVAL_COM1); 
    return TRUE;
}

串口中断回调函数【非常重要,非常重要,非常重要】

void USART0_IRQHandler(void)
{
    if(RESET != usart_interrupt_flag_get(EVAL_COM1, USART_STAT_RBNE, USART_INT_RBNEIE)){
      /* receive data */
      prvvUARTRxISR();
    }
    
    if(RESET != usart_flag_get(EVAL_COM1, USART_STAT_TC)){
      /* transmit data */
      prvvUARTTxReadyISR();
    }
}

其他内容不需要修改。

porttimer.c

此文件主要是定时器设置,关于这个定时器,很多文章有写到,最关键是 50us 这个要相对准确

定时器初始化,说多了都是泪,自己体会,照抄下面的代码即可,注意使用哪个定时器TIMER1,MCU 主频要换算成 50us

BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(RCU_TIMER1);

    timer_deinit(TIMER1);

    /* TIMER1 configuration */
    timer_initpara.timer_prescaler         = 3599;  // 72MHz, 注意,这样就是50us
    timer_initpara.timer_alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.timer_counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.timer_period            = usTim1Timerout50us;
    timer_initpara.timer_clockdivision     = TIMER_CKDIV_DIV1;
    timer_initpara.timer_repetitioncounter = 0;
    timer_init(TIMER1,&timer_initpara);
    
    /* TIMER0 channel control update interrupt enable */
    timer_interrupt_enable(TIMER1,TIMER_INT_UP);
    
    /* TIMER1 counter enable */
    timer_enable(TIMER1);
  
    // NVIC CONFIG
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
    nvic_irq_enable(TIMER1_IRQn, 1, 1);
    
    return TRUE;
}

定时器使能/失能配置【很简单,但是很重要,尤其是计数器要清零】

inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
  
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP);
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_enable(TIMER1);  
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
  
    timer_counter_value_config(TIMER1, 0); // 清零
    /* TIMER1 counter enable */
    timer_disable(TIMER1);
}

定时器中断回调函数【很重要,但是很简单】

void TIMER1_IRQHandler(void)
{
    prvvTIMERExpiredISR();
    timer_interrupt_flag_clear(TIMER1,TIMER_INT_UP); // 清除定时器中断标志位
}

定时器其他照旧即可

portevent.c

没什么修改的

port.h

需要特别注意,修改下面两行

#define ENTER_CRITICAL_SECTION( )   EnterCriticalSection()
#define EXIT_CRITICAL_SECTION( )    ExitCriticalSection()

4、结果验证;

本例程只实现了 RTU 功能号 03 [保持寄存器读取],其他功能请自行扩展!
modbus-pull.PNG

有任何问题,请留言,我们共同讨论。

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 扩展更多的功能,完成更多的工作。通过简单的配置,或少量的代码,就可以实现数据路由和中转。

Kapua Getting Start

https://www.eclipse.org/kapua/getting-started.php

Kapua docker 应用启动后,数据都在docker容器中,一旦重启容器,数据将丢失。

经过分析,需要将两部分的数据持久化到宿主主机磁盘上。

分两步:
1、启动docker compose实例,将sql容器中的数据拷贝出来,ca58ae61b875 是sql 容器的id;

docker cp ca58ae61b875:/var/opt/h2/data/kapuadb.mv.db .

2、修改docker-compose.xml,在特定位置增加下面volumes, 在映射宿主主机文件系统到docker 容器;

  db:
    volumes:
      - ./sql:/var/opt/h2/data
es:
    volumes:
      - ./es:/usr/share/elasticsearch/data

至此,Kapua 测试环境就可以当做准生产环境了。