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

本项目的交流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


#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


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

       

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瓦力农场

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值