毕设拯救计划(一)基于FreeRTOS的智能家居(STM32+Onenet云)

系列文章目录

毕设拯救计划(一)基于FreeRTOS的智能家居(STM32+Onenet云)
毕设拯救计划(二)基于QT的智能家居(泰山派+Onenet云)


前言

  这期算是补档,笔者之前出过STM32做的智能家具,当时利用的是EMQX,但是现在好像不是很好用,然后现在更新成ONENET云,并增加了Free RTOS。这部分算是第二节的下位机部分,大家可以自行扩展。


一、设计思路

  这部分的效果展示网上的公开资料已经很多了,具体的功能代码之前也已经给出了来了课设拯救计划之基于MQTT云的智能家居,这里主要是说一些具体的规划和实现细节,至于什么外设怎么使用将不再赘述。

1.1 Free RTOS的控制思路

  具体的框架思路如下图所示,在各部分模块初始化之后,创建所有需要的任务,将不需要的外设提前挂起以免占用资源。这里要主要内存的大小和优先的设置。这里我的优先级为:语音控制/网络控制>屏幕刷新>RTC>温湿度>监视器/灯光/电机>测距,遵循的原则是控制部分优先级最高,然后按照使用频率设计优先级。
在这里插入图片描述
  这里的大部分移植都比较简单,只需要把链接中的相关部分打包成任务就可以了,比较麻烦的会单独说一下。

1.2 监视器思路

  这里是将监视器拆为三部分:测距任务、监视拍照任务及图像调度任务,这样的好处是仿真任务的处理时间过长导致占用时间过长。

// 图像捕获任务
void Monitor_task(void *pvParameters) {
	float length = 0;
	while(1)
	{
		camera_refresh();
		if(Monitor_Status == 1 && sd_ok){
			length = Get_Length();
			if(length < 50){
				LED0=0;	//点亮DS0,提示正在拍照
				camera_new_pathname(pname);//得到文件名		    
				if(bmp_encode(pname,(lcddev.width-240)/2,(lcddev.height-320)/2,240,320,0)){//拍照有误
					Show_Str(40,130,240,12,"read file error",12,0);		 
					printf("read file error\n\r");
				}
				else{
					Show_Str(40,130,240,12,"photo saved",12,0);
					printf("photo saved\n\r");
					Show_Str(40+42,150,240,12,pname,12,0);		
					message++;					
		 		}
			LED1=1;//关闭DS1
			LCD_Clear(BLACK);
			}				
		}
		else vTaskDelay(pdMS_TO_TICKS(100));  
	}
}

// 图片调度任务
void PhotoManangerTask(void *pvParameters) {
	u8 res;
	DIR picdir;	 		// 图片目录
	FILINFO picfileinfo; // 文件信息
	u8 *fn;   			// 长文件名
	u16 totpicnum; 		// 图片文件总数
	u16 curindex;		// 图片当前索引
	u8 key;				// 键值
	u8 pause = 0;		// 暂停标记
	u8 t;
	u16 temp;
	u16 *picindextbl;	// 图片索引表 
	
	// 打开图片文件夹
	while(f_opendir(&picdir, "0:/PHOTO")) {	    
		vTaskDelay(pdMS_TO_TICKS(100)); 
		Show_Str(30, 170, 240, 16, "PHOTO文件夹错误!", 16, 0);
		vTaskDelay(pdMS_TO_TICKS(100)); 
		LCD_Fill(30, 170, 240, 186, WHITE); // 清除显示	     
	}  

	// 获取图片总数
	totpicnum = pic_get_tnum("0:/PHOTO");

	// 为长文件名、路径名、索引表分配内存
	picfileinfo.lfsize = _MAX_LFN * 2 + 1;						// 长文件名最大长度
	picfileinfo.lfname = mymalloc(SRAMIN, picfileinfo.lfsize);	// 为长文件缓存区分配内存
	pname = mymalloc(SRAMIN, picfileinfo.lfsize);				// 为带路径的文件名分配内存
	picindextbl = mymalloc(SRAMIN, 2 * totpicnum);				// 申请2 * totpicnum个字节的内存, 用于存放图片索引

	// 记录图片索引
	res = f_opendir(&picdir, "0:/PHOTO");
	if (res == FR_OK) {
		curindex = 0; // 当前索引为0
		while (1) { // 全部查询一遍
			temp = picdir.index; // 记录当前index
			res = f_readdir(&picdir, &picfileinfo); // 读取目录下的一个文件
			if (res != FR_OK || picfileinfo.fname[0] == 0) break; // 错误了/到末尾了,退出

			fn = (u8*)(*picfileinfo.lfname ? picfileinfo.lfname : picfileinfo.fname);
			res = f_typetell(fn);
			if ((res & 0XF0) == 0X50) { // 判断是否为图片文件
				picindextbl[curindex] = temp; // 记录索引
				curindex++;
			}
		}
	}

	// 显示图片
	Show_Str(30, 170, 240, 16, "开始显示...", 16, 0); 
	vTaskDelay(pdMS_TO_TICKS(1500));
	piclib_init(); // 初始化画图

	// 开始图片显示循环
	curindex = 0; // 从0开始显示
	res = f_opendir(&picdir, "0:/PHOTO");
	while (res == FR_OK) {	
		dir_sdi(&picdir, picindextbl[curindex]); // 改变当前目录索引
		res = f_readdir(&picdir, &picfileinfo); // 读取目录下的一个文件
		if (res != FR_OK || picfileinfo.fname[0] == 0) break; // 错误了/到末尾了,退出

		fn = (u8*)(*picfileinfo.lfname ? picfileinfo.lfname : picfileinfo.fname);
		strcpy((char*)pname, "0:/PHOTO/"); // 复制路径
		strcat((char*)pname, (const char*)fn); // 将文件名接在后面

		// 打印路径进行调试
		printf("Loading image: %s\n", pname);
		LCD_Clear(WHITE);
		res = ai_load_picfile(pname, 0, 0, lcddev.width, lcddev.height, 1); // 显示图片
		if (res != 0) printf("Error displaying image: %s\n", pname);
		
		Show_Str(2, 2, 240, 16, pname, 16, 1); // 显示图片名字

		// 按键逻辑
		t = 0;
		while (1) {
			key = KEY_Scan(0); // 扫描按键
			if (t > 250) key = 1; // 模拟按下KEY0
			if ((t % 20) == 0) LED0 = !LED0; // LED0闪烁, 提示程序正在运行

			if (key == KEY1_PRES) { // 上一张
				if (curindex) curindex--;
				else curindex = totpicnum - 1;
				break;
			} else if (key == KEY0_PRES) { // 下一张
				curindex++;		   	
				if (curindex >= totpicnum) curindex = 0; // 到末尾, 从头开始
				break;
			} else if (key == WKUP_PRES) { // 暂停
				pause = !pause;
				LED1 = !pause; 	// 暂停时LED1亮
			}

			if (pause == 0) t++;
			vTaskDelay(pdMS_TO_TICKS(100));  
		}
	}
		// 释放内存
		myfree(SRAMIN, picfileinfo.lfname);			    
		myfree(SRAMIN, pname);			    
		myfree(SRAMIN, picindextbl);			
}

1.3 时钟刷新任务

  这里部分也是由三部分组成:屏幕刷新任务、RTC时钟及闹钟(这里用的是软件定时器的方法,电机部分也是这个原理),大家可以回顾一下相关部分内容。

二、 联网功能

  这里简单提一下,笔者主要是拿STM32作为下位机使用,实际上可以直接使用TCP进行双机互通(你的上位机是电脑、单片机、Arm平台都可以,而且工作量会小很多),感兴趣的可以看一下毕设拯救计划(二)基于QT的智能家居(泰山派+Onenet云)。具体的STM32的实现TCP的思路可以参考STM32 esp8266 TCP,这里讲了一下怎么移植,大家可以学习一下,个人感觉这里没必要深究,会用就可以了,而且确实很省事情!!

2.1 新版ONENET的实现思路

  其实这部分也是很简单的,大家具体流程可以参考智能家居,我们这里利用的esp8266进行联网功能。主要是分为三部分:联网、订阅、发布,唯一值得注意的是上传的数据包结构,如果结构出错会直接断开连接。这部分的代码我这边是直接给出了,其实算是很固定的用法了,大家可以按照上面的视频自己再写写熟练一下。
在这里插入图片描述
 这部分是联网功能,主要是针对8266来写的,代码如下:

// esp8266.c
//==========================================================
//	函数名称:	ESP8266_Clear
//
//	函数功能:	清空缓存
//
//	入口参数:	无
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void ESP8266_Clear(void)
{

	memset(esp8266_buf, 0, sizeof(esp8266_buf));
	esp8266_cnt = 0;

}

//==========================================================
//	函数名称:	ESP8266_WaitRecive
//
//	函数功能:	等待接收完成
//
//	入口参数:	无
//
//	返回参数:	REV_OK-接收完成		REV_WAIT-接收超时未完成
//
//	说明:		循环调用检测是否接收完成
//==========================================================
_Bool ESP8266_WaitRecive(void)
{

	if(esp8266_cnt == 0) 							//如果接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数
		return REV_WAIT;
		
	if(esp8266_cnt == esp8266_cntPre)				//如果上一次的值和这次相同,则说明接收完毕
	{
		esp8266_cnt = 0;							//清0接收计数
			
		return REV_OK;								//返回接收完成标志
	}
		
	esp8266_cntPre = esp8266_cnt;					//置为相同
	
	return REV_WAIT;								//返回接收未完成标志

}

//==========================================================
//	函数名称:	ESP8266_SendCmd
//
//	函数功能:	发送命令
//
//	入口参数:	cmd:命令
//				res:需要检查的返回指令
//
//	返回参数:	0-成功	1-失败
//
//	说明:		
//==========================================================
_Bool ESP8266_SendCmd(char *cmd, char *res)
{
	
	unsigned char timeOut = 255;

	Usart_SendString(USART3, (unsigned char *)cmd, strlen((const char *)cmd));
	
	while(timeOut--)
	{
		if(ESP8266_WaitRecive() == REV_OK)							//如果收到数据
		{
			if(strstr((const char *)esp8266_buf, res) != NULL)		//如果检索到关键词
			{
				ESP8266_Clear();									//清空缓存
				
				return 0;
			}
		}
		
		delay_ms(10);
	}
	
	return 1;

}

//==========================================================
//	函数名称:	ESP8266_SendData
//
//	函数功能:	发送数据
//
//	入口参数:	data:数据
//				len:长度
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void ESP8266_SendData(unsigned char *data, unsigned short len)
{

	char cmdBuf[32];
	
	ESP8266_Clear();								//清空接收缓存
	sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len);		//发送命令
	if(!ESP8266_SendCmd(cmdBuf, ">"))				//收到‘>’时可以发送数据
	{
		Usart_SendString(USART3, data, len);		//发送设备连接请求数据
	}

}

//==========================================================
//	函数名称:	ESP8266_GetIPD
//
//	函数功能:	获取平台返回的数据
//
//	入口参数:	等待的时间(乘以10ms)
//
//	返回参数:	平台返回的原始数据
//
//	说明:		不同网络设备返回的格式不同,需要去调试
//				如ESP8266的返回格式为	"+IPD,x:yyy"	x代表数据长度,yyy是数据内容
//==========================================================
unsigned char *ESP8266_GetIPD(unsigned short timeOut)
{

	char *ptrIPD = NULL;
	
	do
	{
		if(ESP8266_WaitRecive() == REV_OK)								//如果接收完成
		{
			ptrIPD = strstr((char *)esp8266_buf, "IPD,");				//搜索“IPD”头
			if(ptrIPD == NULL)											//如果没找到,可能是IPD头的延迟,还是需要等待一会,但不会超过设定的时间
			{
				//UsartPrintf(USART_DEBUG, "\"IPD\" not found\r\n");
			}
			else
			{
				ptrIPD = strchr(ptrIPD, ':');							//找到':'
				if(ptrIPD != NULL)
				{
					ptrIPD++;
					return (unsigned char *)(ptrIPD);
				}
				else
					return NULL;
				
			}
		}
		
		delay_ms(5);													//延时等待
	} while(timeOut--);
	
	return NULL;														//超时还未找到,返回空指针

}

//==========================================================
//	函数名称:	ESP8266_Init
//
//	函数功能:	初始化ESP8266
//
//	入口参数:	无
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void ESP8266_Init(void)
{
	ESP8266_Clear();
    while(ESP8266_SendCmd("+++", ""));               
    while(ESP8266_SendCmd("AT+RESTORE\r\n", "OK"));   
    while(ESP8266_SendCmd("AT\r\n", "OK"));           
    ESP8266_SendCmd("AT+RST\r\n", "");                
    delay_ms(500);
    ESP8266_SendCmd("AT+CIPCLOSE\r\n", "");           
    delay_ms(500);
    while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"));   
    while(ESP8266_SendCmd("AT+CIPMUX=0\r\n", "OK"));   
    while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "WIFI GOT IP"));
}

  接下来就是对Onenet云操作的一些函数了:

// onenet.c
//==========================================================
//	函数名称:	OneNet_DevLink
//
//	函数功能:	与onenet创建连接
//
//	入口参数:	无
//
//	返回参数:	1-成功	0-失败
//
//	说明:		与onenet平台建立连接
//==========================================================
_Bool OneNet_DevLink(void)
{
	
	MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0};					//协议包

	unsigned char *dataPtr;
	
	char authorization_buf[160];
	
	_Bool status = 1;
	
	OneNET_Authorization("2018-10-31", PROID, 1956499200, ACCESS_KEY, DEVICE_NAME,
								authorization_buf, sizeof(authorization_buf), 0);
	
	UsartPrintf(USART_DEBUG, "OneNET_DevLink\r\n"
							"NAME: %s,	PROID: %s,	KEY:%s\r\n"
                        , DEVICE_NAME, PROID, authorization_buf);
	
	if(MQTT_PacketConnect(PROID, authorization_buf, DEVICE_NAME, 256, 1, MQTT_QOS_LEVEL0, NULL, NULL, 0, &mqttPacket) == 0)
	{
		ESP8266_SendData(mqttPacket._data, mqttPacket._len);			//上传平台
		
		dataPtr = ESP8266_GetIPD(250);									//等待平台响应
		if(dataPtr != NULL)
		{
			if(MQTT_UnPacketRecv(dataPtr) == MQTT_PKT_CONNACK)
			{
				switch(MQTT_UnPacketConnectAck(dataPtr))
				{
					case 0:UsartPrintf(USART_DEBUG, "Tips:	连接成功\r\n");status = 0;break;
					
					case 1:UsartPrintf(USART_DEBUG, "WARN:	连接失败:协议错误\r\n");break;
					case 2:UsartPrintf(USART_DEBUG, "WARN:	连接失败:非法的clientid\r\n");break;
					case 3:UsartPrintf(USART_DEBUG, "WARN:	连接失败:服务器失败\r\n");break;
					case 4:UsartPrintf(USART_DEBUG, "WARN:	连接失败:用户名或密码错误\r\n");break;
					case 5:UsartPrintf(USART_DEBUG, "WARN:	连接失败:非法链接(比如token非法)\r\n");break;
					
					default:UsartPrintf(USART_DEBUG, "ERR:	连接失败:未知错误\r\n");break;
				}
			}
		}
		
		MQTT_DeleteBuffer(&mqttPacket);								//删包
	}
	else
		UsartPrintf(USART_DEBUG, "WARN:	MQTT_PacketConnect Failed\r\n");
	
	return status;
	
}
extern u8 temp,humi,message;
unsigned char OneNet_FillBuf(char *buf)
{
	
	char text[256];
	
	memset(text, 0, sizeof(text));
	
	strcpy(buf, "{\"id\":\"1\",\"params\":{");
	
	memset(text, 0, sizeof(text));
	sprintf(text, "\"tem\":{\"value\":%d},", temp);
	strcat(buf, text);
	
	memset(text, 0, sizeof(text));
	sprintf(text, "\"hmi\":{\"value\":%d},", humi);
	strcat(buf, text);
	
	memset(text, 0, sizeof(text));
	sprintf(text, "\"message\":{\"value\":%d},", message);
	strcat(buf, text);

	memset(text, 0, sizeof(text));
	sprintf(text, "\"monitor\":{\"value\":%s},", Monitor_info.Monitor_Status ? "true" : "false");
	strcat(buf, text);
//	printf("Buf after monitor: %s\n", buf); // 打印拼接后的buf
	 
	memset(text, 0, sizeof(text));
	sprintf(text, "\"led\":{\"value\":%s}", led_info.Led_Status ? "true" : "false");
	strcat(buf, text);
//	printf("Buf after led: %s\n", buf); // 打印拼接后的buf

	strcat(buf, "}}");
	
	return strlen(buf);
}
//==========================================================
//	函数名称:	OneNet_SendData
//
//	函数功能:	上传数据到平台
//
//	入口参数:	type:发送数据的格式
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void OneNet_SendData(void)
{
	
	MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0};												//协议包
	
	char buf[256];
	
	short body_len = 0, i = 0;
	
//	UsartPrintf(USART_DEBUG, "Tips:	OneNet_SendData-MQTT\r\n");
	
	memset(buf, 0, sizeof(buf));
	
	body_len = OneNet_FillBuf(buf);																	//获取当前需要发送的数据流的总长度
	
	if(body_len)
	{
		if(MQTT_PacketSaveData(PROID, DEVICE_NAME, body_len, NULL, &mqttPacket) == 0)				//封包
		{
			for(; i < body_len; i++)
				mqttPacket._data[mqttPacket._len++] = buf[i];
			
			ESP8266_SendData(mqttPacket._data, mqttPacket._len);									//上传数据到平台
//			UsartPrintf(USART_DEBUG, "Send %d Bytes\r\n", mqttPacket._len);
			
			MQTT_DeleteBuffer(&mqttPacket);															//删包
		}
		else
			UsartPrintf(USART_DEBUG, "WARN:	EDP_NewBuffer Failed\r\n");
	}
	
}

//==========================================================
//	函数名称:	OneNET_Publish
//
//	函数功能:	发布消息
//
//	入口参数:	topic:发布的主题
//				msg:消息内容
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void OneNET_Publish(const char *topic, const char *msg)
{

	MQTT_PACKET_STRUCTURE mqtt_packet = {NULL, 0, 0, 0};						//协议包
	
	UsartPrintf(USART_DEBUG, "Publish Topic: %s, Msg: %s\r\n", topic, msg);
	
	if(MQTT_PacketPublish(MQTT_PUBLISH_ID, topic, msg, strlen(msg), MQTT_QOS_LEVEL0, 0, 1, &mqtt_packet) == 0)
	{
		ESP8266_SendData(mqtt_packet._data, mqtt_packet._len);					//向平台发送订阅请求
		
		MQTT_DeleteBuffer(&mqtt_packet);										//删包
	}

}

//==========================================================
//	函数名称:	OneNET_Subscribe
//
//	函数功能:	订阅
//
//	入口参数:	无
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void OneNET_Subscribe(void)
{
	
	MQTT_PACKET_STRUCTURE mqtt_packet = {NULL, 0, 0, 0};						//协议包
	
	char topic_buf[56];
	const char *topic = topic_buf;
	
	snprintf(topic_buf, sizeof(topic_buf), "$sys/%s/%s/thing/property/set", PROID, DEVICE_NAME);
	
	UsartPrintf(USART_DEBUG, "Subscribe Topic: %s\r\n", topic_buf);
	
	if(MQTT_PacketSubscribe(MQTT_SUBSCRIBE_ID, MQTT_QOS_LEVEL0, &topic, 1, &mqtt_packet) == 0)
	{
		ESP8266_SendData(mqtt_packet._data, mqtt_packet._len);					//向平台发送订阅请求
		
		MQTT_DeleteBuffer(&mqtt_packet);										//删包
	}

}

//==========================================================
//	函数名称:	OneNet_RevPro
//
//	函数功能:	平台返回数据检测
//
//	入口参数:	dataPtr:平台返回的数据
//
//	返回参数:	无
//
//	说明:		
//==========================================================
void OneNet_RevPro(unsigned char *cmd)
{
	
	char *req_payload = NULL;
	char *cmdid_topic = NULL;
	
	unsigned short topic_len = 0;
	unsigned short req_len = 0;
	
	unsigned char qos = 0;
	static unsigned short pkt_id = 0;
	
	unsigned char type = 0;
	
	short result = 0;
	
	cJSON *raw_json, *params_json, *led_json, *monitor_json;
	
	type = MQTT_UnPacketRecv(cmd);
	switch(type)
	{
		case MQTT_PKT_PUBLISH:																//接收的Publish消息
		
			result = MQTT_UnPacketPublish(cmd, &cmdid_topic, &topic_len, &req_payload, &req_len, &qos, &pkt_id);
			if(result == 0)
			{				
				UsartPrintf(USART_DEBUG, "topic: %s, topic_len: %d, payload: %s, payload_len: %d\r\n",
																	cmdid_topic, topic_len, req_payload, req_len);				
				raw_json = cJSON_Parse(req_payload);
				params_json = cJSON_GetObjectItem(raw_json,"params");
				led_json = cJSON_GetObjectItem(params_json,"led");
				if(led_json != NULL)
				{
					if(led_json->type == cJSON_True) control = 1;
					else if(led_json->type == cJSON_False) control = 2;
				}
				
				monitor_json = cJSON_GetObjectItem(params_json,"monitor");
				if(monitor_json !=NULL)
				{
					if(monitor_json->type == cJSON_True) control = 4;
					else if(monitor_json->type == cJSON_False) control = 3;
				}

				
				cJSON_Delete(raw_json);
				
			}
			
		case MQTT_PKT_PUBACK:														//发送Publish消息,平台回复的Ack
		
			if(MQTT_UnPacketPublishAck(cmd) == 0)
				UsartPrintf(USART_DEBUG, "Tips:	MQTT Publish Send OK\r\n");
			
		break;
		
		case MQTT_PKT_SUBACK:																//发送Subscribe消息的Ack
		
			if(MQTT_UnPacketSubscribe(cmd) == 0)
				UsartPrintf(USART_DEBUG, "Tips:	MQTT Subscribe OK\r\n");
			else
				UsartPrintf(USART_DEBUG, "Tips:	MQTT Subscribe Err\r\n");
		
		break;
		
		default:
			result = -1;
		break;
	}
	
	ESP8266_Clear();									//清空缓存
	
	if(result == -1)
		return;
	
	if(type == MQTT_PKT_CMD || type == MQTT_PKT_PUBLISH)
	{
		MQTT_FreeBuffer(cmdid_topic);
		MQTT_FreeBuffer(req_payload);
	}

}

2.2 获取当地时间及天气预报

  这部分内容就更简单了,主要是调用了一下相关的API,获取了包括时间戳、天气情况等进行预测。考虑这部分不会经常使用,我们这里是在软件初始化之后就立即获取,得到所需的数据后先存储下来,定时对RTC进行时间校准。而对于天气情况等信息则是由语音下达命令之后,再打印出来,以免重复调度占用资源。接下来是我所用的几个API接口:

//心知天气API
#define Xinzhi_TCP		"AT+CIPSTART=\"TCP\",\"api.seniverse.com\",80\r\n"
//拼多多API
#define Time_TCP		"AT+CIPSTART=\"TCP\",\"qapi.pinduoduo.com\",80\r\n"


//获取当天天气
#define Now_GET			"GET https://api.seniverse.com/v3/weather/now.json?key=[你申请的密钥]=shenyang&language=en&unit=c\r\n"
//获取天气预报
#define Forcast_GET		"GET https://api.seniverse.com/v3/weather/daily.json?key=[你申请的密钥]=shenyang&language=en&unit=c&start=0&days=3\r\n"
//获取拼多多时间戳
#define Time_GET		"GET https://api.pinduoduo.com/api/server/_stm\r\n"

  之后就可以得到相对于的数据包,大家按照需求进行解包即可。


免责声明:本次项目的部分代码也是参考了很多优秀作者开源的项目,再次感谢,如有侵权可联系笔者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值