物联网实战--平台篇之(十三)物模型设备端

本项目的交流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里的功能就行了,这样就可以把开发者从繁杂的底层通讯中解放出来,专注于产品本身的功能实现,尽可能优化,提升用户体验,这才是最为关键的。这也是我开发这个平台的根本原因所在。

相关推荐
中科岩创3 小时前
榆能横山煤电厂及周边建筑物爆破振动和位移自动化监测
物联网
mahuifa11 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai
冷眼看人间恩怨11 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
光路科技14 小时前
八大网络安全策略:如何防范物联网(IoT)设备带来的安全风险
物联网·安全·web安全
云空16 小时前
《QT 5.14.1 搭建 opencv 环境全攻略》
开发语言·qt·opencv
小老鼠不吃猫17 小时前
力学笃行(二)Qt 示例程序运行
开发语言·qt
晓纪同学18 小时前
QT创建一个模板槽和信号刷新UI
开发语言·qt·ui
委员19 小时前
基于NodeMCU的物联网空调控制系统设计
单片机·mcu·物联网·智能家居
爱码小白20 小时前
PyQt5 学习方法之悟道
开发语言·qt·学习方法
逝灮21 小时前
【蓝桥杯——物联网设计与开发】拓展模块3 - 温度传感器模块
驱动开发·stm32·单片机·嵌入式硬件·物联网·蓝桥杯·温度传感器