100行代码实现串口与UDP透传,轻松实现WiFi数据传输

100行代码实现串口与UDP透传/wifi数传

UDP与串口透传,或者说UDP与串口互转,本质是把串口接收到的字符数组/整型数组通过UDP发送出去,把UDP接收到的字符数组/整型数组通过串口发送出去。
不管叫字符数组还是整型数组,对应内存存储以及传输的二进制或者十六进制都是一样的。

UDP通信

创建socket

#include <sys/socket.h>
#include <arpa/inet.h>

    // 1、使用socket()函数获取一个socket文件描述符
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
		printf("socket open err.");
		return -1;
    }

绑定本地端口

绑定后,程序则是通过此端口进行UDP的发送和UDP的监听接收。

    // 2、绑定本地的相关信息,如果不绑定,则系统会随机分配一个端口号
    struct sockaddr_in local_addr = {0};
    local_addr.sin_family = AF_INET;                                //使用IPv4地址
    local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    local_addr.sin_port = htons(12300);                             //端口
    bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));//将套接字和IP、端口绑定

远端IP端口

UDP要发送的目的地IP和端口,这里也就是QGC所在电脑的局域网IP和UDP监听端口

    // 2. 准备接收方的地址和端口,'192.168.0.107'表示目的ip地址,8266表示目的端口号 
    struct sockaddr_in sock_addr = {0};	
    sock_addr.sin_family = AF_INET;                         // 设置地址族为IPv4
    sock_addr.sin_port = htons(8266);						// 设置地址的端口号信息
    sock_addr.sin_addr.s_addr = inet_addr("192.168.0.107");	// 设置IP地址

非阻塞通信

socket通信默认是阻塞的,UDP接收如果是阻塞,没有收到的消息的话程序会卡住不运行下去,所以需要设置为非阻塞的,主要是对UDP接收而言。
通过fcntl函数设置socket为非阻塞

#include <fcntl.h>
    fcntl(sockfd, F_SETFL, O_NONBLOCK);//非阻塞

UDP接收

注意recvfrom函数第四个参数得是MSG_DONTWAIT,MSG_DONTWAIT的意思是设置非阻塞操作。

      struct sockaddr_in recv_addr;
      socklen_t addrlen = sizeof(recv_addr);
      uint8_t udp_recvbuf[1024] = {0};
      int udp_recv_ret = recvfrom(sockfd, (char *)udp_recvbuf, 1024, MSG_DONTWAIT,(struct sockaddr*)&recv_addr,&addrlen);  //1024表示本次接收的最大字节数

UDP发送

        int udp_send_ret = sendto(sockfd, (char *)serial_recvbuf, serial_recv_ret, 0, (struct sockaddr*)&qgc_addr, sizeof(qgc_addr));

串口通信

串口通信我们可以使用这个串口库,https://github.com/wjwwood/serial ,这个串口库用的人很多,而且Github上星数也很高,既支持ubuntu也支持windows。
部署也非常简单方便,在Ubuntu上部署命令如下

git clone https://github.com/wjwwood/serial
cd serial
make

如果想安装也可以继续运行

make install

想卸载可以运行

make uninstall

设置串口,波特率

serial::Serial my_serial("/dev/ttyUSB0", 115200, serial::Timeout::simpleTimeout(50));

这是完整的构造函数,除了可以设置串口,波特率外,还可以设置数据位,校验方式,停止位,流控制,这里数据位,校验方式,停止位,流控制我们都保持默认。
serial/include/serial/serial.h

  Serial (const std::string &port = "",
          uint32_t baudrate = 9600,
          Timeout timeout = Timeout(),
          bytesize_t bytesize = eightbits,
          parity_t parity = parity_none,
          stopbits_t stopbits = stopbits_one,
          flowcontrol_t flowcontrol = flowcontrol_none);

对于timeout参数,调用读取或写入以来的总时间超过了指定的毫秒数,则会发生超时,读取或者写入就会结束。有点像UDP里的非阻塞。在serial/include/serial/serial.h 里有非常详细的注释说明。
这个timeout参数实际也会影响到循环程序的执行时间,特别是串口读取函数。

  /*! Sets the timeout for reads and writes using the Timeout struct.
   *
   * There are two timeout conditions described here:
   *  * The inter byte timeout:
   *    * The inter_byte_timeout component of serial::Timeout defines the
   *      maximum amount of time, in milliseconds, between receiving bytes on
   *      the serial port that can pass before a timeout occurs.  Setting this
   *      to zero will prevent inter byte timeouts from occurring.
   *  * Total time timeout:
   *    * The constant and multiplier component of this timeout condition,
   *      for both read and write, are defined in serial::Timeout.  This
   *      timeout occurs if the total time since the read or write call was
   *      made exceeds the specified time in milliseconds.
   *    * The limit is defined by multiplying the multiplier component by the
   *      number of requested bytes and adding that product to the constant
   *      component.  In this way if you want a read call, for example, to
   *      timeout after exactly one second regardless of the number of bytes
   *      you asked for then set the read_timeout_constant component of
   *      serial::Timeout to 1000 and the read_timeout_multiplier to zero.
   *      This timeout condition can be used in conjunction with the inter
   *      byte timeout condition with out any problems, timeout will simply
   *      occur when one of the two timeout conditions is met.  This allows
   *      users to have maximum control over the trade-off between
   *      responsiveness and efficiency.
   *
   * Read and write functions will return in one of three cases.  When the
   * reading or writing is complete, when a timeout occurs, or when an
   * exception occurs.
   *
   * A timeout of 0 enables non-blocking mode.
   *
   * \param timeout A serial::Timeout struct containing the inter byte
   * timeout, and the read and write timeout constants and multipliers.
   *
   * \see serial::Timeout
   */
  void
  setTimeout (Timeout &timeout);

串口发送

会返回发送成功的字节数

        int serial_write_bytes = my_serial.write(buf, ret);

总共有三种发送函数可以以整型数组,vector或者字符串的形式给发送函数进行串口发送。这里我选择整型数组的方式。

  size_t write (const uint8_t *data, size_t size);
  size_t write (const std::vector<uint8_t> &data);
  size_t write (const std::string &data);

串口接收

会返回接收成功的字节数

      int serial_recv_ret =  my_serial.read(serial_recvbuf, 150);  //返回缓冲区接收到的字节数目

有三种串口接收函数,可以以整型数组,vector或者字符串的形式输出收到的字节。这里我选择整型数组的方式。

  size_t read (uint8_t *buffer, size_t size);
  size_t read (std::vector<uint8_t> &buffer, size_t size = 1);
  size_t read (std::string &buffer, size_t size = 1);

暂停函数,建议在循环的串口读取和发送之间加上一点时间间隔,会更为鲁棒。

#include <thread>
#include <chrono>
      std::this_thread::sleep_for(std::chrono::milliseconds(10));

UDP通信和串口通信间的数据传递

数组在UDP通信和串口通信之间进行传递时,需要注意下,UDP通信使用的是字符数组,而串口通信我选择用的是整型数组。
虽然类型不同,但是只需要创建一个数组即可,因为不管是符数组还是整型数组,对应内存存储的二进制或者十六进制都是一样的,实际传输的二进制都是一样的。
比如定义一个整型数组

uint8_t buf[100];

把串口接收到的字节存到这个整型数组里可以直接这么写,因为所用串口库的串口发送函数支持整型数组类型传入

      int serial_recv_ret =  my_serial.read(buf, 150); 

接下来要把存储了串口接收到字节的整型数组buf,通过UDP发送出去,需要调用sendto函数,但是sendto函数对应需要传入的是字符数组的首地址,不支持整型数组的,此时我们不需要再新建一个字符数组,只需要相应传入(char *)buf即可,这样udp就会发送整型数组buf里面对应的字节了。

        int udp_send_ret = sendto(sockfd, (char *)buf, serial_recv_ret, 0, (struct sockaddr*)&qgc_addr, sizeof(qgc_addr));

完整示例代码

下面放上完整的serial_2_udp.cpp的示例代码
其中serial_2_udp_node_port对应serial_2_udp.cpp绑定的本地端口,qgc_ip是QGC所在电脑的局域网IP,qgc_port为设置的QGC的UDP端口,serial_name为串口驱动名称,serial_baudrate为串口对应波特率,这些需要根据自己实际情况做对应更改。

#include <iostream>
#include "serial/serial.h"
#include <fcntl.h>
#include <yaml-cpp/yaml.h>
#include <chrono>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <iomanip> 
#include <thread>

int main() {

    int serial_2_udp_node_port = 12344;
    std::string qgc_ip = "192.168.1.102";
    int qgc_port = 12345;
    std::string serial_name = "/dev/ttyACM0";
    int serial_baudrate = 115200;
    int serial_timeout = 10;


    try {

    // 1、使用socket()函数获取一个socket文件描述符
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
		printf("socket open err.");
		return -1;
    }
 
    // 2、绑定本地的相关信息,如果不绑定,则系统会随机分配一个端口号
    struct sockaddr_in local_addr = {0};
    local_addr.sin_family = AF_INET;                                //使用IPv4地址
    //local_addr.sin_addr.s_addr = inet_addr("192.168.0.107");        //本机IP地址
    local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //local_addr.sin_port = htons(12300);                             //端口
    local_addr.sin_port = htons(serial_2_udp_node_port);                             //端口
    bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));//将套接字和IP、端口绑定

    fcntl(sockfd, F_SETFL, O_NONBLOCK);//非阻塞

    // 3. 准备接收方的地址和端口,'192.168.0.107'表示目的ip地址,8266表示目的端口号 
    struct sockaddr_in qgc_addr = {0};	
    qgc_addr.sin_family = AF_INET;                         // 设置地址族为IPv4
    //sock_addr.sin_port = htons(8266);						// 设置地址的端口号信息
    qgc_addr.sin_port = htons(qgc_port);
    //sock_addr.sin_addr.s_addr = inet_addr("192.168.0.107");	// 设置IP地址

    //将字符串格式的 IP 地址转换为用于网络通信的二进制格式
    if (inet_pton(AF_INET, qgc_ip.c_str(), &qgc_addr.sin_addr) <= 0) {
            std::cerr << "Invalid address\n";
            return 1;
    }



    // 创建串口对象
    serial::Serial my_serial(serial_name, serial_baudrate, serial::Timeout::simpleTimeout(serial_timeout));

    if (my_serial.isOpen()) {
        std::cout << "串口已打开" << std::endl;
    } else {
        std::cout << "串口打开失败" << std::endl;
        return 1;
    }

    std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now();
    uint32_t serial_send_total_bytes = 0; //存放串口一分钟内发送的字节数
    uint32_t serial_recv_total_bytes = 0; //存放串口一分钟内接收的字节数
    uint32_t udp_send_total_bytes = 0; //存放udp一分钟内发送的字节数
    uint32_t udp_recv_total_bytes = 0; //存放udp一分钟内接收的字节数


    while(1)
    {
      // 获取程序开始执行的时间点
      auto start = std::chrono::high_resolution_clock::now();

      //用来存储所收到的UDP消息的发送方的地址,包括IP和端口
      struct sockaddr_in recv_addr;
      socklen_t addrlen = sizeof(recv_addr);

      uint8_t udp_recvbuf[1024] = {0};
      //udp接收
      int udp_recv_ret = recvfrom(sockfd, (char *)udp_recvbuf, 1024, MSG_DONTWAIT,(struct sockaddr*)&recv_addr,&addrlen);  //1024表示本次接收的最大字节数
      //std::cout << "接收到的字符数组是:" << std::hex << recvbuf << std::endl; //std::hex表示十六进制显示
      for (int i = 0; i < udp_recv_ret; i++) {
        //printf("%02X ", udp_recvbuf[i]);
      }

      if(udp_recv_ret > 1) 
      {
        udp_recv_total_bytes = udp_recv_total_bytes + udp_recv_ret;
        int serial_write_bytes = my_serial.write(udp_recvbuf, udp_recv_ret); //串口发送
        serial_send_total_bytes  = serial_send_total_bytes  + serial_write_bytes;
      }


      // 暂停 10 毫秒 
      std::this_thread::sleep_for(std::chrono::milliseconds(10));

      uint8_t serial_recvbuf[1024] = {0};
      // 串口接收数据
      int serial_recv_ret =  my_serial.read(serial_recvbuf, 150);  //返回缓冲区接收到的字节数目

      if(serial_recv_ret > 1)
      {
        for (int i = 0; i < serial_recv_ret; i++) {
          //printf("%02X ", serial_recvbuf[i]);
        }
        serial_recv_total_bytes = serial_recv_total_bytes + serial_recv_ret;
        //udp发送
        int udp_send_ret = sendto(sockfd, (char *)serial_recvbuf, serial_recv_ret, 0, (struct sockaddr*)&qgc_addr, sizeof(qgc_addr));

        udp_send_total_bytes = udp_send_total_bytes + udp_send_ret;
      }
      
      // 暂停 10 毫秒 
      std::this_thread::sleep_for(std::chrono::milliseconds(10));

      // 获取程序执行结束的时间点
      auto end = std::chrono::high_resolution_clock::now();
  
      // 计算程序执行时间
      std::chrono::duration<double> duration = end - start;
      double seconds = duration.count();


      // 检查是否已过去一分钟
      auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - start_time).count();
      if (elapsed_seconds >= 60) {
      
            // 获取当前时间
            auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
            std::cout << "Time: " << std::put_time(std::localtime(&now), "%Y-%m-%d %X") << "   \n";
            printf("=====一分钟udp接收字节数:%d\n", udp_recv_total_bytes);
            printf("=====一分钟串口发送字节数:%d\n", serial_send_total_bytes);
            printf("=====一分钟串口接收字节数:%d\n", serial_recv_total_bytes);
            printf("=====一分钟udp发送字节数:%d\n", udp_send_total_bytes);
            std::cout << "程序单次循环执行时间: " << seconds << " 秒" << std::endl;
            // 重置计数器和起始时间
            serial_send_total_bytes = 0;
            serial_recv_total_bytes = 0; 
            udp_send_total_bytes = 0; 
            udp_recv_total_bytes = 0; 
            start_time = std::chrono::steady_clock::now();
      }
      
     }

     // 关闭串口
     my_serial.close();
     std::cout << "串口已关闭" << std::endl;
    }
    catch (serial::IOException& e) {
        std::cerr << "串口通信异常:" << e.what() << std::endl;
        return 1;
    }

    return 0;
}

可以把此serial_2_udp.cpp放到 https://github.com/wjwwood/serial 工程根目录下,同时再在serial工程的CMakeLists.txt里可以加上serial_2_udp.cpp的编译命令,如下所示,然后再在serial文件夹运行make命令编译,在serial/build/devel/lib/serial文件夹下可以找到生成的serial_2_udp可执行文件,并运行。

add_executable(serial_2_udp serial_2_udp.cpp)
add_dependencies(serial_2_udp ${PROJECT_NAME})
target_link_libraries(serial_2_udp ${PROJECT_NAME})

按照上面更改后的serial完整代码工程可见 马熙/serial2udp
此工程对应部署运行命令如下,注意需要先把对应串口和UDP IP端口配置成对应的再进行make编译

git clone https://gitee.com/maxibooksiyi/serial2udp
cd serial2udp
make
cd serial/build/devel/lib/serial
./serial_2_udp

serial_2_udp可执行文件运行时的终端打印如下图所示,每分钟会显示串口和UDP各自收发字节数

输入图片说明

基于serial_2_udp.cpp实现QGC基于udp连接px4飞控

要实现把PX4飞控的mavlink经过serial_2_udp转发到QGC地面站,首先把飞控设置为mavlink口的TELEM口经过USB转TTL或者micro usb口经过USB线接在ubuntu平台上,serial_2_udp.cpp里的串口serial_name和波特率serial_baudrate改为相应串口驱动名称和波特率,同时QGC端添加一个UDP通信并设置对应监听端口即可,不需要其他额外的设置,如果是按照上面所给的serial_2_udp.cpp里的udp发送端口,QGC里的UDP端口需要设置为12345,如下图所示。
 

输入图片说明

设置好QGC的UDP端口后,如下图所示点击连接,在启动serial_2_udp的情况下,即可以通过serial_2_udp转发来的mavlink消息连接上px4飞控,可以对飞控进行正常操控。
 

输入图片说明

 

输入图片说明

作者:诗筱涵

物联沃分享整理
物联沃-IOTWORD物联网 » 100行代码实现串口与UDP透传,轻松实现WiFi数据传输

发表回复