本项目的交流QQ群:701889554
物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html
物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_12631333.html
物联网实战--平台篇https://blog.csdn.net/ypp240124016/category_12653350.html
嵌入式文件 https://download.csdn.net/download/ypp240124016/89409505
APP文件 https://download.csdn.net/download/ypp240124016/89409506
一、设备演示
熟悉的画面,做了点改进。
净化器演示
二、模型要素
整个平台相当于搭了个窝,目的是为了下蛋,而产品就是我们要下的蛋;对于开发人员来讲,如何定义产品是个很关键的问题,在电信、移动等这些云平台上,他们一般是用一个profile解析插件作为设备模型的,本质上就是json文件,里面描述了产品设备的属性和功能,这种方式的缺点是性能损耗较大,对于功能比较复杂的产品定义和调试都是件很麻烦的事情,需要北向平台人员做配合,沟通成本很高。
而我们这里对设备模型的定义直接用源代码定义,清晰简洁,产品开发者不需要过多了解平台的底层协议和后台机制,定义好产品本身的数据和类型即可;对于展示界面,美工设计好后对照着样式用前端代码很容易就能实现了。
对于设备来讲,首先最重要的要素就是设备SN了,它是设备的身份标识,在这里我们的定义是一个4字节的数据,高2字节代表型号,低2字节代码地址码,这个SN是设备生产的时候保存在设备内部的。例如,在演示视频里,我们把之前的净化器进行了完善,并把A101这个型号定义为净化器产品,所以在本系统里,净化器产品的序列号范围是A1010001~A101FFFF,有6万多个,一般场景够用了。
接下来是要定义产品的属性,比如温湿度、PM2.5和开关状态等这些都算是属性,属性的主要约束内容是范围和精度,比如温度,范围是-40.0~120.0 ℃,精度是0.1℃,这个一般是传感器的物理特性决定的。
有些设备不仅有属性值,还能进行控制,我们称之为功能定义,比如净化器的开关和风扇速度设置,都属于产品功能。
有了这些属性和功能之后,还要想办法对它们进行有序地读取和操作,一般我们会定义不同的命令类型去执行不同的操作,比如下图是净化器产品的命令定义,至于每个命令后跟着哪些数据,具体根据产品自己定义即可。,
总的来讲,我们对设备模型的开发主要就是围绕着SN、属性、功能和命令类型这四个内容展开的;当然,还有一个是通讯密码,因为我们采用的是一型多密原则,所以每个型号的设备都定义了多组密码随机使用。那么,下面我们就以之前的净化器项目为例,做具体的讲解。
三、通讯协议
这里的通讯协议是指建立在MQTT之上的应用层通用协议,类似于modbus协议,我这边已经实现了具体内容,开发者只需要学会配置和调用即可,如果感兴趣可以对照着协议(链接文章第三节内容)和驱动代码看看。物联网实战--平台篇之(二)基础搭建_qt数据库驱动 封装其它数据库-CSDN博客
drv_server.h
cpp
#ifndef __DRV_SERVER_H
#define __DRV_SERVER_H
#include "drv_common.h"
#ifndef SERVER_PACK_SIZE
#define SERVER_PACK_SIZE 256 //数据包最大值
#endif
#ifndef SERVER_PROTOCOL_VER
#define SERVER_PROTOCOL_VER 1 //协议版本
#endif
#ifndef SERVER_PASSWD_CNTS
#define SERVER_PASSWD_CNTS 5 //密码组数
#endif
#ifndef SERVER_EEPROM_ADDR
#define SERVER_EEPROM_ADDR (0x0050)//存储地址
#endif
#ifndef CONFIG_EEPROM_READ
#define CONFIG_EEPROM_READ EEPROM_Read //配置读取函数
#endif
#ifndef CONFIG_EEPROM_WRITE
#define CONFIG_EEPROM_WRITE EEPROM_Write //配置保存函数
#endif
#define APP_ID_MIN (u32)(123000)
typedef enum
{
ENCRYPT_MODE_DISABLE=0,
ENCRYPT_MODE_TEA,
ENCRYPT_MODE_AES,
}encryptMode;
typedef enum
{
SERVER_CMD_UP_DATA=100,
SERVER_CMD_DOWN_DATA=200,
REG_CMD_REPORT_APP_ID=210,
REG_CMD_SET_APP_ID,
UPDATE_CMD_IAP=220,//远程升级总命令
UPDATE_CMD_INTO_BOOT,//使设备进入升级状态
UPDATE_CMD_KEEP,//保持连接
SERVER_CMD_LORA=230,//LORA总命令
}ServerCmdType;
typedef struct
{
u8 head[2];
u8 version;
u8 encrypt_index;//密码索引
u8 crc_h;
u8 crc_l;
u8 data_len_h;
u8 data_len_l;
u8 app_id[4];
u8 gw_sn[4];
}ServerHeadStruct;
typedef struct
{
u32 app_id;//应用ID
u32 gw_sn;//设备SN
u16 reserved;
u16 crcValue;
}ServerSaveStruct;
typedef struct
{
u8 passwd_table[SERVER_PASSWD_CNTS][16];//密码表,要跟用户端的模型对应
int (*fun_send)(u8 *buff, u16 len);
u16 (*fun_server_cmd_parse)(u8 cmd_type, u8 *in_buff, u16 in_len);
u16 (*fun_slave_cmd_parse)(u32 node_sn, u8 *in_buff, u16 in_len);
}ServerWorkStruct;
void drv_server_read(void);
void drv_server_write(void);
void drv_server_init(void);
u16 drv_server_send_msg(u8 cmd_type, u8 *in_buff, u16 in_len);
void drv_server_send_slave_msg(u32 node_sn, u8 *in_buff, u16 in_len);
int drv_server_send_level(u8 *buff, u16 len);
void drv_server_send_register(int (*fun_send)(u8 *buff, u16 len));
u16 drv_server_recv_parse(u8 *buff, u16 len, int (*fun_send)(u8 *buff, u16 len));
u16 drv_server_cmd_parse(u8 cmd_type, u8 *in_buff, u16 in_len);
void drv_server_cmd_parse_register(u16 (*fun_server_cmd_parse)(u8 cmd_type, u8 *in_buff, u16 in_len));
u16 drv_slave_cmd_parse(u32 node_sn, u8 *in_buff, u16 in_len);
void drv_slave_cmd_parse_register(u16 (*fun_slave_cmd_parse)(u32 node_sn, u8 *in_buff, u16 in_len));
void drv_server_set_app_id(u32 app_id);
u32 drv_server_get_app_id(void);
void drv_server_set_gw_sn(u32 gw_sn);
u32 drv_server_get_gw_sn(void);
void drv_server_add_passwd(u8 index, u8 *passwd);
#endif
drv_server.c
cpp
#include "drv_server.h"
#include "drv_encrypt.h"
ServerWorkStruct g_sServerWork={0};
ServerSaveStruct g_sServerSave={0};
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_read(void)
{
CONFIG_EEPROM_READ(SERVER_EEPROM_ADDR, (u8 *)&g_sServerSave, sizeof(g_sServerSave));
// printf_hex("read=", (u8 *)&g_sServerSave, sizeof(g_sServerSave));
if(g_sServerSave.crcValue!=drv_crc16((u8*)&g_sServerSave, sizeof(g_sServerSave)-2))
{
g_sServerSave.app_id=APP_ID_MIN;
g_sServerSave.gw_sn=M2M_DEV_TYPE<<16;
drv_server_write();
printf("server read new!\n");
}
// printf("read app_id=%u, gw_sn=0x%08X\n", g_sServerSave.app_id, g_sServerSave.gw_sn);
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_write(void)
{
g_sServerSave.crcValue=drv_crc16((u8*)&g_sServerSave, sizeof(g_sServerSave)-2);
CONFIG_EEPROM_WRITE(SERVER_EEPROM_ADDR, (u8 *)&g_sServerSave, sizeof(g_sServerSave));
// printf_hex("write=", (u8 *)&g_sServerSave, sizeof(g_sServerSave));
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_init(void)
{
drv_server_read();
}
/*
================================================================================
描述 :组合发送报文
输入 :
输出 : 组合后的数据长度
================================================================================
*/
u16 drv_server_send_msg(u8 cmd_type, u8 *in_buff, u16 in_len)
{
static u8 pack_num=0;
static u8 data_buff[SERVER_PACK_SIZE]={0}, make_buff[SERVER_PACK_SIZE];
static u8 to_server_pwd[16]={0};//密码
static u8 encrypt_mode=ENCRYPT_MODE_TEA;//加密模式--对于设备端来讲是固定的
static u32 gw_sn=0, app_id=0;
u16 data_len=0,union_len,remain_len,make_len,crcValue;
ServerHeadStruct *pHead=(ServerHeadStruct *)make_buff;
u8 *pData=&make_buff[16];//加密区起始地址
int out_len;
if(in_len+32>SERVER_PACK_SIZE)
{
printf("in len too long!\n");
return 0;
}
if(gw_sn==0)
{
gw_sn=g_sServerSave.gw_sn;
}
if(app_id==0)
{
app_id=g_sServerSave.app_id;
}
pack_num++;
memset(data_buff, 0, SERVER_PACK_SIZE);
data_len=0;
union_len=in_len+4;//数据单元长度
data_buff[data_len++]=union_len>>8;//从此处开始加密
data_buff[data_len++]=union_len;
data_buff[data_len++]=pack_num;
data_buff[data_len++]=cmd_type;
memcpy(&data_buff[data_len], in_buff, in_len);
data_len+=in_len;
crcValue=drv_crc16(data_buff, union_len);//数据单元校验
data_buff[data_len++]=crcValue>>8;
data_buff[data_len++]=crcValue;
remain_len=data_len%8;
if(remain_len>0)
data_len+=(8-remain_len);//8字节对齐,便于TEA加密
pHead->encrypt_index=drv_get_sec_counter()%SERVER_PASSWD_CNTS;//根据时间随机获取密码
memcpy(to_server_pwd, g_sServerWork.passwd_table[pHead->encrypt_index], 16);//根据索引复制密码
switch(encrypt_mode)
{
case ENCRYPT_MODE_DISABLE:
{
memcpy(pData, data_buff, data_len);
out_len=data_len;
break;
}
case ENCRYPT_MODE_TEA:
{
out_len=tea_encrypt_buff(data_buff, data_len, (u32*)to_server_pwd);
if(out_len==data_len)
{
memcpy(pData, data_buff, data_len);
}
else
{
printf("server tea error!\n");
return 0;
}
break;
}
#ifdef USE_AES //是否启用AES算法
case ENCRYPT_MODE_AES:
{
out_len=aes_encrypt_buff(data_buff, data_len, pData, SERVER_PACK_SIZE-16, to_server_pwd);//aes加密
if(out_len<16)
{
printf("server aes error!\n");
return 0;
}
break;
}
#endif
default:
return 0;
}
data_len=out_len+8;//加上app_id和gw_sn的长度
crcValue=drv_crc16(&make_buff[8], data_len);//总校验
pHead->head[0]=0xAA;
pHead->head[1]=0x55;
pHead->version=SERVER_PROTOCOL_VER;
pHead->crc_h=crcValue>>8;
pHead->crc_l=crcValue;
pHead->data_len_h=data_len>>8;
pHead->data_len_l=data_len;
pHead->app_id[0]=app_id>>24;
pHead->app_id[1]=app_id>>16;
pHead->app_id[2]=app_id>>8;
pHead->app_id[3]=app_id;
pHead->gw_sn[0]=gw_sn>>24;
pHead->gw_sn[1]=gw_sn>>16;
pHead->gw_sn[2]=gw_sn>>8;
pHead->gw_sn[3]=gw_sn;
make_len=data_len+8;
drv_server_send_level(make_buff, make_len);//发送
return make_len;
}
/*
================================================================================
描述 : 转发从机消息
输入 :
输出 :
================================================================================
*/
void drv_server_send_slave_msg(u32 node_sn, u8 *in_buff, u16 in_len)
{
u8 make_buff[100]={0};
u16 make_len=0;
if(in_len+20>sizeof(make_buff))
{
return;
}
make_buff[make_len++]=node_sn>>24;
make_buff[make_len++]=node_sn>>16;
make_buff[make_len++]=node_sn>>8;
make_buff[make_len++]=node_sn;
u8 *pUnion=&make_buff[make_len];
make_len+=2;//单元长度
memcpy(&make_buff[make_len], in_buff, in_len);
make_len+=in_len;
u16 union_len=make_len-4;//单元长度
pUnion[0]=union_len>>8;
pUnion[1]=union_len;
u16 crcValue=drv_crc16(pUnion, union_len);
make_buff[make_len++]=crcValue>>8;
make_buff[make_len++]=crcValue;
// printf_hex("slave msg=", make_buff, make_len);
drv_server_send_msg(SERVER_CMD_UP_DATA, make_buff, make_len); //转发
}
/*
================================================================================
描述 : 服务端数据接收解析
输入 :
输出 :
================================================================================
*/
u16 drv_server_recv_parse(u8 *buff, u16 len, int (*fun_send)(u8 *buff, u16 len))
{
static u8 head[2]={0xAA, 0x55}, out_buff[800]={0}, recv_pack_num=109;
u8 to_server_pwd[16]={0}, encrypt_mode=0;//加密模式
u8 *pBuff=buff, *pData=NULL, pack_num, cmd_type;
u16 data_len=0, out_len=0, union_len=0, crcValue;
static u32 local_app_id=0, local_gw_sn=0;
u32 recv_gw_sn, recv_app_id;
if(local_gw_sn==0)
{
local_gw_sn=g_sServerSave.gw_sn;
}
if(local_app_id==0)
{
local_app_id=g_sServerSave.app_id;
}
// printf_hex("drv_server_recv_parse: ", buff, len);
if( (pBuff=memstr(buff, len, head, 2))!=NULL )
{
ServerHeadStruct *pHead = (ServerHeadStruct *)pBuff;
data_len=pHead->data_len_h<<8|pHead->data_len_l;
crcValue=pHead->crc_h<<8|pHead->crc_l;
recv_app_id=pHead->app_id[0]<<24|pHead->app_id[1]<<16|pHead->app_id[2]<<8|pHead->app_id[3];
recv_gw_sn=pHead->gw_sn[0]<<24|pHead->gw_sn[1]<<16|pHead->gw_sn[2]<<8|pHead->gw_sn[3];
if(recv_app_id==0)
{
return 0;
}
if(recv_gw_sn!=local_gw_sn)
{
printf("drv_server_parse_recv error: recv_gw_sn(%u)!=local_gw_sn(%u)!", recv_gw_sn, local_gw_sn);
return 0;
}
if(data_len<12 || data_len+20>sizeof(out_buff))
{
printf("drv_server_parse_recv error: data_len<12 || data_len+20>sizeof(out_buff)\n");
return 0;
}
if(pHead->encrypt_index>=SERVER_PASSWD_CNTS)
{
return 0;
}
memcpy(to_server_pwd, g_sServerWork.passwd_table[pHead->encrypt_index], 16);//根据索引复制密码
encrypt_mode=ENCRYPT_MODE_TEA;
pData=pBuff+8;
if(crcValue==drv_crc16(pData, data_len))
{
pData+=8;//app_id和gw_sn不加密
data_len-=8;
//解密
switch(encrypt_mode)
{
case ENCRYPT_MODE_DISABLE:
{
memcpy(out_buff, pData, data_len);
out_len=data_len;
break;
}
case ENCRYPT_MODE_TEA:
{
out_len=tea_decrypt_buff(pData, data_len, (u32*)to_server_pwd);
if(out_len==data_len)
{
memcpy(out_buff , pData, data_len);
}
else
{
printf("server tea error!\n");
return 0;
}
break;
}
#ifdef USE_AES //是否启用AES算法
case ENCRYPT_MODE_AES:
{
out_len=aes_decrypt_buff(pData, data_len, out_buff, sizeof(out_buff)-16, to_server_pwd);//aes
if(out_len<16)
{
printf("server aes error!\n");
return 0;
}
break;
}
#endif
default:
return 0;
}
}
else
{
printf("drv_server_parse_recv crc error 000!\n");
return 0;
}
pData=out_buff;
union_len=pData[0]<<8|pData[1];
pData+=2;
pack_num=pData[0];
pData+=1;
cmd_type=pData[0];
pData+=1;
// printf("union_len=%d, pack_num=%d, cmd_type=%d\n", union_len, pack_num, cmd_type);
if(recv_pack_num==pack_num)//过滤相同的包序号
{
printf("recv_pack_num==pack_num\n");
return 0;
}
recv_pack_num=pack_num;//更新包序号
if(union_len<4 || union_len>sizeof(out_buff))
{
printf("drv_server_parse_recv error: union_len<4 || union_len>sizeof(out_buff)\n");
return 0;
}
crcValue=out_buff[union_len]<<8|out_buff[union_len+1];
if(crcValue==drv_crc16(out_buff, union_len))//解密后校验
{
union_len-=4;
// printf("cmd_type=%d\n", cmd_type);
switch(cmd_type)
{
case SERVER_CMD_DOWN_DATA://数据转发
{
u32 recv_node_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];//目标节点序列号
drv_slave_cmd_parse(recv_node_sn, pData, union_len);//从机解析
break;
}
case REG_CMD_SET_APP_ID://设置APP ID
{
u32 new_app_id=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];//新的应用ID
pData+=4;
if(new_app_id>=APP_ID_MIN)
{
drv_server_set_app_id(new_app_id);
delay_os(100);
drv_system_reset();//复位系统
}
break;
}
default://其余命令交给应用层处理
drv_server_cmd_parse(cmd_type, pData, union_len);
}
return 1;
}
else
{
printf("drv_server_parse_recv crc error 111!\n");
return 0;
}
}
return 0;
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
int drv_server_send_level(u8 *buff, u16 len)
{
if(g_sServerWork.fun_send != NULL)
{
return g_sServerWork.fun_send(buff, len);
}
return 0;
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_send_register(int (*fun_send)(u8 *buff, u16 len))
{
g_sServerWork.fun_send=fun_send;
}
/*
================================================================================
描述 : 服务端解析
输入 :
输出 :
================================================================================
*/
u16 drv_server_cmd_parse(u8 cmd_type, u8 *in_buff, u16 in_len)
{
u16 make_len=0;
if(g_sServerWork.fun_server_cmd_parse != NULL)
{
make_len=g_sServerWork.fun_server_cmd_parse(cmd_type, in_buff, in_len);
}
return make_len;
}
/*
================================================================================
描述 : 服务端解析函数注册
输入 :
输出 :
================================================================================
*/
void drv_server_cmd_parse_register(u16 (*fun_server_cmd_parse)(u8 cmd_type, u8 *in_buff, u16 in_len))
{
g_sServerWork.fun_server_cmd_parse=fun_server_cmd_parse;
}
/*
================================================================================
描述 : 从机端解析
输入 :
输出 :
================================================================================
*/
u16 drv_slave_cmd_parse(u32 node_sn, u8 *in_buff, u16 in_len)
{
u16 make_len=0;
if(g_sServerWork.fun_slave_cmd_parse != NULL)
{
make_len=g_sServerWork.fun_slave_cmd_parse(node_sn, in_buff, in_len);
}
return make_len;
}
/*
================================================================================
描述 : 从机端解析函数注册
输入 :
输出 :
================================================================================
*/
void drv_slave_cmd_parse_register(u16 (*fun_slave_cmd_parse)(u32 node_sn, u8 *in_buff, u16 in_len))
{
g_sServerWork.fun_slave_cmd_parse=fun_slave_cmd_parse;
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_set_app_id(u32 app_id)
{
g_sServerSave.app_id=app_id;
drv_server_write();
printf("set app_id=%u\n", app_id);
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
u32 drv_server_get_app_id(void)
{
return g_sServerSave.app_id;
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_set_gw_sn(u32 gw_sn)
{
g_sServerSave.gw_sn=gw_sn;
drv_server_write();
printf("set gw_sn=%08X\n", gw_sn);
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
u32 drv_server_get_gw_sn(void)
{
return g_sServerSave.gw_sn;
}
/*
================================================================================
描述 :
输入 :
输出 :
================================================================================
*/
void drv_server_add_passwd(u8 index, u8 *passwd)
{
if(index<SERVER_PASSWD_CNTS)
{
memcpy(g_sServerWork.passwd_table[index], passwd, 16);
}
}
四、密码表配置
在这个与平台服务器对接的通讯文件里,不仅实现了发送加密和接收解密的功能,还保存了app_id和设备SN,即gw_sn (gw是网关GateWay的缩写,意味着是跟服务器直接网络对接的角色)。还有一个是密码表,这个表实际使用时最好动态混淆填充,这样才能提升密码获取难度,这里我是直接定义的,二进制文件打开是可以直接检索到的,并不安全,暂时图个方便。密码表是QT那边随机生成复制过来的。
五、收发函数注册
drv_server.c文件里的收发函数都是在应用层注册的,因为我们用了mqtt,所以这个过程在app_mqtt.c文件内完成的,具体如下所示,整个mqtt的配置跟原来差不多,就是订阅话题做了些改动。
收发函数:
六、设备定义
从设备端开始,首先定义产品型号值是A101,这个值是自己分配定义的,你要定义成A102也行,核心就是不同产品类型值不能重复就行了,在规划上也要有长远的打算,不要乱定义、浪费数字资源。
净化器的属性包含了温度、湿度、PM2.5、风速等级和开关状态,如下图所示,至于数据为什么要乘以10在加1000这种操作,我在之前的文章里有详细解释了,可以回头看看。物联网实战--入门篇之(八)嵌入式-空气净化器-CSDN博客
上图是解析下发的指令,即功能定义,简单讲就是根据不同命令类型执行不同的操作了。
对于净化器本身的功能,跟原来入门篇是一样的,这里只是把发送和解析函数做了改动,接入现有的通讯协议系统。
对于产品开发者来讲,通讯层的驱动文件都是定义好的,无需改动,只要专心完成app_ap01.c里的功能就行了,这样就可以把开发者从繁杂的底层通讯中解放出来,专注于产品本身的功能实现,尽可能优化,提升用户体验,这才是最为关键的。这也是我开发这个平台的根本原因所在。