如何编写简易物联网串口网关

1、前言

【第一周】:现在是2019年12月,现大三。希望借这个平台和正在阅读文章的你一起学习和提升。本着分享和记录的目的,我将不定期更新文章,如果文章中有不足之处,还请各位多多指教,如果需要源代码,欢迎在评论区留言邮箱。

本文为《搭建物联网基础通信框架系列教程》之《编写物联网串口网关》。
预备知识:Linux串口的配置和读写,参考连接

2、运行环境和硬件

树莓派3B作为网关,当然您也可以用其它嵌入式开发板甚至是Linux虚拟机替代。
底层硬件采用STM32F103,或者其它带有串口外设的MCU。
加密库采用OpenSSL,加密方式为RSA,AES。
开发语言为C++。

3、实现功能

与树莓派连接的节点作为数据汇集节点,用USB串口与树莓派连接,树莓派网关将数据进行加密,打包成我们自定义的数据包,发送到我们自己搭建的云平台。

4、编写树莓派端

(1)网关类的设计

class CGateway
{
private:
 RSA* m_rsa;
 CTcpClientSocket* m_pTcpClientSocket;//socket连接对象
 AES_KEY m_key_en;
 AES_KEY m_key_de;
 unsigned char m_ivec[16];
 CMyThread_ListenUsart* m_pTdListenUsart;//串口监听线程
 CSerial* m_pSerial;
private:
 void _recvCmdPkt(void* pData, int len);//收到数据指令数据包
 void _recvCipherDisbtPkt(void* pData, int len);//收到会话密钥分发包
 void _recvAuthCheckPkt(void* pData, int len);
 bool _initRsaKey(const char* path);
 bool _initAesKey(en_pkt_CipherDisbt& en_cipherDisbt);
 void _sendAuthCheckPkt();
public:
 void run();//网关启动入口
 void postSensorData(pkt_SensorData& sensorData);//上报传感器数据
public:
 CGateway(CSerial* pSerial, CSockAddr& serverAddr);
 ~CGateway();
};

这里的socket我进行了简单的封装,方便后边的使用。

(2)串口的读写

创建新的线程

1、创建新的线程
因为我们需要监听stm32是否有数据发送到网关,同时又要监听是否有用户的控制命令下发到stm32,因此我们需要需要再启动一个线程用于监听是否有数据发送到串口,而我们的主线程/进程则负责监听socket是否有数据可读。
当然,这个线程我们只是先设计出来,并不会马上创建它。
-----------串口监听线程的设计-----------

class CGateway;
class CMyThread_ListenUsart :
 public CBaseThread
{
private :
 CGateway* m_pGateway;
 CSerial* m_pSerial;
protected:
 void run();
public:
 CMyThread_ListenUsart(CSerial* pSerial, CGateway* pGeteway);
 ~CMyThread_ListenUsart();
};

-----------串口监听线程的执行函数-----------

void CMyThread_ListenUsart::run()
{
 pkt_SensorData sensorData;
 int errCount = 0;
 char buf[1024];
 char* pStart = NULL;
 int n = 0;
 bzero(buf, sizeof(buf));
 pthread_detach(pthread_self());
 while (true) {
  n = m_pSerial->readData(buf, 1);
  if (n <= 0) {
   cerr << "Usart connection error " << endl;
   cerr << "线程退出" << endl;
   pthread_exit(NULL);
  }
  if (buf[0] == '#') {
   bzero(&sensorData, sizeof(sensorData));
   int count = 0;
   pStart = (char*)&sensorData;
   while (count < PKT_LEN_SensorData) {
    n = m_pSerial->readData(pStart+ count, PKT_LEN_SensorData-count);
    if (n < 0) {
     cerr << "Usart connection error " << endl;
     cerr << "线程退出" << endl;
     pthread_exit(NULL);
    }
    count += n;
   }
   m_pGateway->postSensorData(sensorData);
  }
 }
}

pkt_SensorData 是自定义的传感器数据包,字符’#‘作为数据帧的起始信号

串口的配置

2、串口的配置
-----------串口类的设计-----------

//串口类配置出错信息的宏定义
#define ERROR_CFG_BaudeRate   0x01
#define ERROR_CFG_DataBits    0x02
#define ERROR_CFG_Partity    0x04
#define ERROR_CFG_StopBits    0x08
#define ERROR_CFG_FlowCtrl_hw  0x10
#define ERROR_CFG_FlowCtrl_sw  0x20
class CSerial {
public:
 CSerial(const char *pCfgFilePath);
 CSerial(const char* devPath,int baudRate,int dataBits = 8,int stopBits = 1,int parity = 0,bool bFlowCtrl_hw = false, bool bFlowCtrl_sw = false);
 ~CSerial();
private:
 CSerial();
private:
 unsigned int m_uiError;
 termios m_Termios;
 int m_fd_Dev; //串口设备文件描述符
 int m_iBaudRate; //波特率
 int m_iDataBits; //数据位设置
 int m_iStopBits; //停止位(Linux只支持1或2停止位)
 int m_iVMIN; //读取字符的最小数量
 int m_iVTIME; //读取第一个字符的等待时间  n分之一秒
 int m_iParity; //校验设置 0:不校验 1:奇校验 2:偶校验
 bool m_bFlowCtrl_hw; //硬件流控
 bool m_bFlowCtrl_sw; //软件流控
 char m_devPath[32];
private:
 bool _initFromFile(const char* pCfgFilePath);
 bool _isLocked(int fd);
 bool _unLockFile(int fd);
 bool _lockFile(int fd);
public:
 bool setBaudeRate(int baudeRate);
 bool setFlowCtrl_hw(bool bFlowCtrl_hw);
 bool setFlowCtrl_sw(bool bFlowCtrl_sw);
 bool setDataBits(int dataBits);
 bool setParity(int parity);
 bool setStopBits(int stopBits);
 bool setVMIN(int vmin);
 bool setVTIME(int vtime);
 bool enableConfig();
public:
 int getBaudeRate();
 bool getFlowCtrl_hw();
 bool getFlowCtrl_sw();
 int getDataBits();
 int getParity();
 int getStopBits();
 int getVMIN();
 int getVTIME();
 termios getTermios();
public:
 ssize_t writeData(const void *pBuf, size_t n);
 ssize_t readData(void *pBuf, size_t n);
protected:
};

具体的配置参照Linux系统库中结构体中的变量配置即可,参考连接

struct termios
  {
    tcflag_t c_iflag;  /* input mode flags */
    tcflag_t c_oflag;  /* output mode flags */
    tcflag_t c_cflag;  /* control mode flags */
    tcflag_t c_lflag;  /* local mode flags */
    cc_t c_line;   /* line discipline */
    cc_t c_cc[NCCS];  /* control characters */
    speed_t c_ispeed;  /* input speed */
    speed_t c_ospeed;  /* output speed */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
  };

(3)socket通信

这部分我们交给主线程/进程去做
-----------网关类的run()函数-----------

void CGateway::run()
{
 char buf[4096];
 int size = 0, n = 0;
 pkt_Head head;
 bzero(buf, sizeof(buf));
 bzero(&head, PKT_LEN_Head);
 //=============TCP循环解包========================
 while (true) {
  if ((n = m_pTcpClientSocket->readData(&buf[size], 1024)) < 0) {
   perror("recv error");
   exit(-1);
  }
  size += n;
  if (atol(head.length)==0 && size > PKT_LEN_Head) {
   memcpy(&head,buf, PKT_LEN_Head);
   memcpy(buf, &buf[PKT_LEN_Head], size - PKT_LEN_Head);
   size -= PKT_LEN_Head;
  }
  if (atol(head.length) > 0 && size >= atol(head.length)) {
   switch (atoi(head.type))
   {
   case PKT_TYPE_CIPHER_DISBT:
    _recvCipherDisbtPkt(buf, atoi(head.length));
    break;
   case PKT_TYPE_CMD:
    _recvCmdPkt(buf, atoi(head.length));
    break;
   default:
    break;
   }
   memcpy(buf, &buf[atoi(head.length)], size - atoi(head.length));
   size -= atoi(head.length);
   bzero(&head, PKT_LEN_Head);
  }
 }
}

我们不断去获取socket中的数据,在这里我并没有去设置socket的非阻塞属性,因此如果socket没有数据,程序将阻塞在readData函数。思路就是:如果数据缓冲区的数据长度>=数据包头的长度,我们就开始接收数据包体,将数据包包体完整接收后,我们根据数据包头的数据包类型字段,将不同数据包交给对应的处理函数进行处理。

(4)数据的加密

网络安全的基本操作是用【非对称加密】加密【对称加密】,然后用对称加密进行会话。在这我们也采用这样的逻辑。

RSA的初始化

这一部分设计到RSA加密API的基本使用(其实就是几个函数),首先读取公钥配置文件,然后生成RSA密钥。

bool CGateway::_initRsaKey(const char * path)
{
 FILE *fp = NULL;
 if ((fp = fopen(path, "r")) == NULL) {
  perror("fopen error");
  return false;
 }
 /* 读取公钥PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函数 */
 if ((m_rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
  perror("PEM_read_RSAPublicKey error");
  return false;
 }
 else {
  cout << "RSA公钥初始化完成" << endl;
 }
 fclose(fp);
 return true;
}
AES的初始化

网络安全的基本操作是用【非对称加密】加密【对称加密】,然后用对称加密进行

bool CGateway::_initAesKey(en_pkt_CipherDisbt& en_cipherDisbt)
{
 pkt_CipherDisbt cipherDisbt;
 bzero(&cipherDisbt, sizeof(cipherDisbt));
 if (RSA_public_decrypt(RSA_size(m_rsa), (unsigned char *)en_cipherDisbt.en_CipherDisbt, \
  (unsigned char*)&cipherDisbt, m_rsa, RSA_PKCS1_PADDING) != sizeof(cipherDisbt)) {
  cout << "RSA解密会话密钥失败" << endl;
  return false;
 }
 if (AES_set_encrypt_key((const unsigned char*)cipherDisbt.aes_userKey, 16 * 8, &m_key_en) != 0) {
  cout << "更新会话加密密钥失败" << endl;
  return false;
 }
 else {
  cout << "更新会话加密密钥成功" << endl;
 }
 if (AES_set_decrypt_key((const unsigned char*)cipherDisbt.aes_userKey, 16 * 8, &m_key_de) != 0) {
  cout << "更新会话解密密钥失败" << endl;
  return false;
 }
 else {
  cout << "更新会话解密密钥成功" << endl;
 }
 memcpy(m_ivec, cipherDisbt.aes_ivec, 16);
 _sendAuthCheckPkt();
 return true;
}

pkt_CipherDisbt 定义如下

typedef struct {
 char tm[16];
 unsigned char aes_ivec[20];
 unsigned char aes_userKey[20];
}pkt_CipherDisbt;

aes_ivec:
在每一次的AES加密和解密中会用到,当然您也可以在收发双方约定好,每次加密解密只用UserKey,ivec设置为0。
aes_userKey:
用于生成AES加密密钥和解密密钥。

(5)从main函数开始

int main() {
 CGateway* pGateway;
 CSerial serial("/dev/ttyUSB0", 115200);//串口类
 CSockAddr serAddr("xxx.xxx.xxx.xxx", port);//tcp地址类
 pGateway = new CGateway(&serial, serAddr);//网关类
 pGateway->run();
 return 0;
}

创建串口对象
tcp地址类的初始化需要更改为您使用服务器的ip地址和端口
创建网关类对象,启动网关类。
-----------网关类的构造函数-----------

CGateway::CGateway(CSerial* pSerial, CSockAddr& serverAddr)
{
 m_pSerial = pSerial;
 m_pTcpClientSocket = new CTcpClientSocket();//tcp客户端类
 m_pTcpClientSocket->setServerAddr(&serverAddr);
 bzero(m_ivec, sizeof(m_ivec));
 /*connect*/
 if (m_pTcpClientSocket->connect_() != true) {
  m_pTcpClientSocket->closeSocket();
  delete m_pTcpClientSocket;
  exit(-1);
 }
 if (_initRsaKey("./public.pem") != true) {
  cout << "_initRsaKey failed" << endl;
  delete m_pTcpClientSocket;
  exit(-1);
 }
 m_pTdListenUsart = NULL;
}

当收到会话密钥分发数据包时,我们之前设计的串口线程类就可以开始工作了

void CGateway::_recvCipherDisbtPkt(void* pData, int len)
{
 en_pkt_CipherDisbt en_cipherDisbt;
 memcpy(&en_cipherDisbt, pData, PKT_LEN_en_CipherDisbt);
 if(_initAesKey(en_cipherDisbt) != true){
  exit(-1);
 }
 if (!m_pTdListenUsart) {//会话密钥分发成功后创建线程监听串口数据
  m_pTdListenUsart = new CMyThread_ListenUsart(m_pSerial, this);
  m_pTdListenUsart->start(NULL);
 }
}

树莓派端的关键代码我们就编写完了

5、编写STM32端

因为作为测试,在stm32端的代码我做很复杂的功能,只是一些基本的配置和操作。

(1)串口的配置

void Usart1_Init(u32 baudRate)
{
 
 USART_InitTypeDef USART_InitStruct; 
 GPIO_InitTypeDef GPIO_InitStruct;
 NVIC_InitTypeDef NVIC_InitStruct;
 
 //打开USART1和GPIOA的时钟
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);
 
 /******************配置数据发送管脚*********************/
 
 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;   //发送数据管脚
 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //设置管脚为复用推挽输出
 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //设置速率
 GPIO_Init(GPIOA,&GPIO_InitStruct);    //设置速率
  
 /******************配置数据接收管脚*********************/ 
  
 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;    //接收数据管脚 
 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //设置管脚为浮空输入 
 GPIO_Init(GPIOA,&GPIO_InitStruct);     //设置生效
  
 /**********************配置USART1************************/
 //设置波特率
 USART_InitStruct.USART_BaudRate = baudRate;
 //设置字长
 USART_InitStruct.USART_WordLength = USART_WordLength_8b;
 //设置为1个停止位
 USART_InitStruct.USART_StopBits = USART_StopBits_1;
 //设置为无硬件控制流
 USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
 //设置为无奇偶校验
 USART_InitStruct.USART_Parity = USART_Parity_No;
 //设置USART模式控制:同时使能接收和发送 
 USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
 //设置生效
 USART_Init(USART1,&USART_InitStruct);
 
 //使能USART1外设
 USART_Cmd(USART1,ENABLE); 
 //使能接收中断
 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
 //使能总线空闲中断
 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); 
  
 /******************配置中断控制器*********************/
 //设置中断源为USART1
 NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; 
 //设置组别为NVIC_PriorityGroup_1
 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
 //设置抢断优先级为1
 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
 //设置子优先级为1
 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
 //使能中断
 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
 //设置生效
 NVIC_Init(&NVIC_InitStruct);
}

(2)ADC的配置

void lightADC_init(void)
{
 ADC_InitTypeDef ADC_InitStruct;
 GPIO_InitTypeDef GPIO_InitStruct;
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); 
 RCC_ADCCLKConfig(RCC_PCLK2_Div6);   
 ADC_DeInit(ADC1);   
 //配置ADC
 ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; 
 ADC_InitStruct.ADC_ScanConvMode = DISABLE; 
 ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; 
 ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; 
 ADC_InitStruct.ADC_NbrOfChannel = 1; 
 ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; 
 ADC_Init(ADC1,&ADC_InitStruct);
 
 ADC_Cmd(ADC1,ENABLE); 
 
 ADC_ResetCalibration(ADC1);
 while(ADC_GetResetCalibrationStatus(ADC1)); 
 ADC_StartCalibration(ADC1); 
 while(ADC_GetCalibrationStatus(ADC1)); 
 
 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOA,&GPIO_InitStruct);
}

(3)数据上发函数和收到控制命令

void task_recvCmd(unsigned char* p)
{
    if(p[0]==0xD0 && (u8)p[1]==0x00)
    {
        beep_set(0);
    }
    else if(p[0]==0xD0 && (u8)p[1]==0x01)
    {
        beep_set(1);
    }
}
void task_UpdateData(void)
{
    uint16_t temp,humi;
    pkt_SensorData sensorData;
    u8 *p = (u8*)&sensorData;
    int i = 0;
    memset(&sensorData,0,sizeof(sensorData));
    DHT11_Read_Data(&temp,&humi);
    sprintf(sensorData.humi,"%.2f",humi/100.0);
    sprintf(sensorData.light,"%.2f",light_getVal()/30.0);
    sprintf(sensorData.temp,"%.2f",temp/100.0);
    Usart_SendByte(USART1,'#');
    for(i = 0;i < sizeof(pkt_SensorData);++i)
    {
        Usart_SendByte(USART1,p[i]);
    }
}

(4)从main函数开始

int main()
{
    My_Init_SysTick();   
    Usart1_Init(115200);    
    DHT11_Init();  
    lightADC_init();
    beep_init();
    beep_set(0);
    while(1)  
    {
        Delay_ms(2000);
        task_UpdateData();
    }
    return 0;
}

首先对系统初始化,随后不间断每隔2s上发一次传感器的数据。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值