设计 jrxml 文件

使用 Jaspersoft Studio 图形化设计报表,或使用文本文件

集成 JasperReports 和 JasperReports Font Extension

pom.xml 中新增

<dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports</artifactId>
    <version>6.15.0</version>
</dependency>
<dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-fonts</artifactId>
    <version>6.15.0</version>
</dependency>

PDF 中文支持

文件列表

jasperreports_extension.properties

src/main/resources 下新建文件

net.sf.jasperreports.awt.ignore.missing.font=true
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.dejavu=fonts/fonts.xml

fonts.xml

src/main/resources/fonts 目录下新建

<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
    <fontFamily name="宋体">
        <normal><![CDATA[fonts/simsun.ttf]]></normal>
        <pdfEncoding><![CDATA[Identity-H]]></pdfEncoding>
        <exportFonts/>
    </fontFamily>
</fontFamilies>

simsun.ttf

从Windows电脑拷贝文件到 src/main/resources/fonts 下

修改 /usr/share/thingsboard/conf/thingsboard.yml, 将 false 改成 true;

# Enable/disable SSL support
enabled: "${SSL_ENABLED:true}"

新建 /usr/share/thingsboard/conf/keystore 目录,且此目录下,增加自己的证书 keystore.p12,并授权 thingsboard 用户和组,命令行输入

mkdir /usr/share/thingsboard/conf/keystore 

keytool -genkey -alias tomcat \
-keystore keystore.p12 \
-storetype PKCS12 \
-keyalg RSA \
-validity 730 \
-keysize 2048

chown -R thingsboard:thingsboard /usr/share/thingsboard/conf/keystore

重启 thingsboard 即可

systemctl restart thingsboard

Chrome 下,无法访问自己制作的证书网站,方法在此

关于 Chrome 无法打开自定义的网站证书网站方法

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

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