文章目录
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上发一次传感器的数据。