基于ESP32+ASRPRO的智能家居助手设计与实现

项目介绍

实物图

请添加图片描述

项目名称

  • 基于ESP32+ASRPRO的智能家居助手设计与实现

项目背景

随着科技的不断进步,物联网(IoT)技术正在迅速改变我们的生活和工作方式。为了利用这一趋势,我开发了"基于ESP32+ASRPRO的智能家居助手设计与实现",旨在通过连接设备和数据,实现更智能化、更方便的生活。

项目目标

"基于ESP32+ASRPRO的智能家居助手设计与实现"的主要目标是:

  • 实现设备远程监测与管理
  • 全天候数据监测
  • 增强用户设备控制体验

项目架构

设备层:
使用ESP32作为主控制器,ASRPRO作为语音识别设备,DHT11作为温湿度数据检测设备
网络层:

  • 设备与服务器通过MQTTWiFi连接
  • 设备与设备之间通过WiFi蓝牙(可选)连接

数据层:

  • 数据主要通过主控制器进行处理并上传阿里云服务器

应用层:
[描述最终用户如何与系统交互,如手机应用、网页界面等]

  • 使用1.8寸TFT屏幕显示数据以及交互信息
  • 使用阿里云物联网平台开发的程序可在手机、网页、电脑端进行设备控制及数据查看
  • 可通过阿里云服务器对数据进行监控、修改
  • 本地通过语音控制其他设备以及获取其他设备信息

主要功能

  • 数据监测: 通过板载DHT11传感器实时采集温湿度数据,并在显示屏显示。

  • 远程控制: 用户可以通过手机或电脑远程控制设备。

  • 语音控制: 通过ASRPRO模块用户可以在本地通过语音控制设备。

  • 警报和通知: 在检测到异常情况时,板载蜂鸣器会自动发送警报通知用户。

  • 功能拓展:分别预留ESP32、ASRPRO的IO*2并用2.0mm间距3Pin接口引出用于功能拓展

技术实现

通信协议:

采用的通信协议:

  • MQTT :设备与服务器间的数据传输
  • HTTP/HTTPS :实时网络时间获取
  • UDP:各设备之前的数据传输

硬件:

模块
  • 主控制器:ESP32
  • 语音识别模块:ASRPRO
  • 温湿度传感器:DHT11
  • 报警器:12095蜂鸣器
  • 数据显示器:1.8寸TFT屏幕
  • 状态指示器:LED灯若干
外壳设计

底壳
在这里插入图片描述

PCB设计

PCB设计使用嘉立创EDA完成

原理图在这里插入图片描述
PCB

在这里插入图片描述

3D模型预览

在这里插入图片描述

软件:

  • 开发语言和框架:基于Arduino IDE(ESP32)以及天问 BLOCK(ASRPRO)使用C/C++进行开发
  • 控制程序基于阿里云物联网平台进行开发
控制程序-控制界面

在这里插入图片描述

控制程序-数据监测界面

在这里插入图片描述

ESP32代码
  • 长代码警告!(后续会进行封装)
#include <WiFi.h>
#include <WiFiUdp.h>
#include <DHT.h>  //引入DHT库使用DHT11
#include <AliyunIoTSDK.h>// 引入阿里云 IoT SDK
//引入显示屏库
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include "TFT_Image.h"       // 包含图片数组头文件

#include <NTPClient.h> //获取网络时间
// 中国的NTP服务器地址,可以选择多个
const char* ntpServer = "cn.pool.ntp.org";
const long  gmtOffset_sec = 8 * 3600;  // 中国标准时间(UTC+8)
const int   daylightOffset_sec = 0;    // 无夏令时偏移
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, gmtOffset_sec, 60000);  // 更新时间间隔为60000ms
int currentHour =0;
int currentMinute = 0;
int currentSecond = 0; 
char timeArr[] = "TIME:23-38-30";    
#define TIME_COLOUR ST7735_YELLOW 

#define TFT_CS    14 // 请替换为您的CS引脚号
#define TFT_DC    27 // 请替换为您的DC引脚号
#define TFT_RST   5 // 请替换为您的RST引脚号
//#define TFT_SCK   18 // 请替换为您的SCK引脚号 
//#define TFT_MOSI  23 // 请替换为您的MOSI引脚号
//已经默认配置
//#define COMPUTER_PIN 0 //连接继电器的引脚
#define DHTPIN 25  
#define DHTTYPE DHT11   // 定义传感器类型
DHT dht(DHTPIN, DHTTYPE);

char HumiStr[6] = "78";
char TempStr[6] = "34.8";

// 图片尺寸
#define IMAGE_WIDTH  60
#define IMAGE_HEIGHT 60
extern const unsigned char gImage_humi[];
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);//创建显示屏对象并初始化

hw_timer_t * timer = NULL; // 创建一个定时器对象

static WiFiClient espClient;

// 设置 wifi 信息
#define WIFI_SSID "Redmi_DC21"
#define WIFI_PASSWD "88888888"
//UDP变量定义
WiFiUDP udp;
WiFiUDP discoveryUdp;
const int udpPort = 5088;
const int discoveryPort = 5089;  // 用于动态发现的UDP端口
#define MAX_CONNECT_COUNT 10 //预期的最大连接设备数
//char incomingPacket[255];//UDP接收数据数组
struct DeviceInfo{//存储其他设备name及ip 
  String name;
  IPAddress ip;
};
DeviceInfo devices[MAX_CONNECT_COUNT];
int deviceCount = 0; //已经连接设备数

#define SEND_DELAY_TIME 200 //单位ms

// 设置产品和设备的信息,从阿里云设备信息里查看
#define PRODUCT_KEY "xxxxxxxxxxx"
#define DEVICE_NAME "xxxxxxxxx"
#define DEVICE_SECRET "xxxxxxxxxxxxxxxxxxxxx"
#define REGION_ID "cn-shanghai"
//定义使用的颜色
#define BACKGROUND_COLOUR ST7735_BLACK //背景颜色
#define RECT1_COLOUR ST7735_RED 
#define TEXT1_COLOUR ST7735_GREEN
#define LINE1_COLOUR ST7735_RED
#define TEXT2_COLOUR ST7735_BLUE
#define RECT2_COLOUR ST7735_RED
#define TEXT3_COLOUR ST7735_GREEN
#define TEXT4_COLOUR ST7735_BLUE
#define DATA_COLOUR ST7735_WHITE
#define DATANAME_COLOUR ST7735_WHITE

#define SHOWDELAY_TIME 500 //字幕滚动速度
#define MAX_TEXT_COUNT 30 //输入文本最大字符数
#define MAX_SHOW_COUNT 12 //最大同时显示字符数
#define MAX_SHOW_STRING 6 //最大同时刷新字符串数量

#define SHOW_MODE_SEND 1 //向外发送的消息
#define SHOW_MODE_RECIVED 2//从外部收到的消息
#define SHOW_MODE_MYSELF 3 //内部消息

int textShowX[MAX_SHOW_STRING] = {0};
int textShowY[MAX_SHOW_STRING] = {0};
int showCount[MAX_SHOW_STRING] = {0};//实际显示数量
int maxCount[MAX_SHOW_STRING] = {0};//需要显示数量
char textShowString[MAX_SHOW_STRING][MAX_SHOW_COUNT + 1] = {"null"} ;//实际显示的字符串
char textMaxString[MAX_SHOW_STRING][MAX_TEXT_COUNT] = {"null"};//需要显示的字符串
int showFlag[MAX_SHOW_STRING] = {0};//显示刷新标志位

float humidity = 0;//温湿度
float temperature = 0;

bool ComputerControl = 1;//电脑控制

bool FanControl = 0;//风扇控制
int FanPwm = 50;//风扇调速
int FanRunTimeHour = 3;//风扇定时-时
int FanRunTimeMin = 30;//定时-分

bool Led1Control = 0;//灯光控制
int LedLuminance = 50;//亮度控制

int LightValue = 100;//光照检测
int SmokeDetection = 100;//烟雾检测

bool AmbientLightControl = 0;//氛围灯控制
int AmbientLightRed = 100;//氛围灯颜色控制
int AmbientLightGreen = 100;
int AmbientLightBlue = 100;
int AmbientLightLuminance = 50;//氛围灯亮度
int AmbientLightCount = 30;

// 定时器相关变量
unsigned long previousMillis = 0;
const unsigned long interval = 100; // 每100毫秒

unsigned long previousMillisUpdate = 0;
const unsigned long updateInterval = 1000; // 每1秒

unsigned long previousMillisData = 0;
const unsigned long dataInterval = 10000; // 每10秒

void displayImage(int16_t x, int16_t y, const unsigned char *image, uint16_t width, uint16_t height) {
  // 遍历图片数组,逐像素显示图片
  for (uint16_t row = 0; row < height; row++) {
    for (uint16_t col = 0; col < width; col++) {
      uint32_t pixelColor = pgm_read_byte(image + (row * width + col));
      tft.drawPixel(x + col, y + row, pixelColor);
    }
  }
}

void messageShowUI(const int x,const int y,char text1[],char text2[],char text3[],char text4[]){

  const int RectX = x;//基准坐标 (矩形左上角)
  const int RectY = y;//基准坐标
  const int RectWight = 160;//宽160
  const int RectHight = 14;//高14

  const int Rect1x = RectX;//矩形框1x坐标
  const int Rect1y = RectY;//矩形框1y坐标
  const int Rect1w = RectWight - RectX;//矩形框1宽度
  const int Rect1h = RectHight;//矩形框1高度
//tft.drawRect(Rect1x,Rect1y,Rect1w,Rect1h,RECT1_COLOUR);//绘制矩形框1 外部包围

  const int Text1x = Rect1x +3;//Text1x坐标
  const int Text1y = Rect1y +3;//Text1y坐标
  const int Text1C = 4;//Text1字符数量
  char Text1String[Text1C + 1];//Text1字符数组
  strncpy(Text1String, text1,Text1C);//从text1拷贝Text1C个字符到Text1String
  Text1String[Text1C] = '\0'; // 添加字符串结束符
  const int Text1Size = 1;

  const int Line1x = Text1x + Text1C * 6 + 3;//字符大小为6*8
  const int Line1y = RectY;
  const int Line1h = RectHight;

  const int Text2x = Line1x + 3;//Text2x坐标
  const int Text2y = Text1y;//Text2y坐标
  const int Text2C = 6 ;//Text2字符数量
  const int Text2Size = 1;
  char Text2String[Text2C + 1];//Text2字符数组
  strncpy(Text2String, text2,Text2C);//从text2拷贝Text2C个字符到Text2String
  Text2String[Text2C] = '\0'; // 添加字符串结束符


  const int Rect2x = Text2x + Text2C * 6 + 1;//矩形框2x坐标
  const int Rect2y = RectY;//矩形框2y坐标
  const int Rect2w = 14;//矩形框2宽度
  const int Rect2h = 14;//矩形框2高度

  const int Text3x = Rect2x + 5;//Text3x坐标
  const int Text3y = Text1y;//Text3y坐标
  const int Text3C = 1;//Text3字符数量
  const int Text3Size = 1;
  char Text3String[Text3C + 1];//Text3字符数组
  strncpy(Text3String, text3,Text3C);//从text3拷贝Text3C个字符到Text3String
  Text3String[Text3C] = '\0'; // 添加字符串结束符

  const int Text4x = Rect2x + Rect2w + 3;//Text4x坐标
  const int Text4y = Text1y ;//Text4y坐标
  const int Text4Size = 1;
  const int Text4C = (159 - Text4x)/6 ;//剩余空间可显示字符数量(以0,0作为起点最多显示12个字符)字符大小6*8
  char Text4String[Text4C + 1];//Text4字符数组
  int Text4Length = 0;//传入需要显示的字符数量
  while (text4[Text4Length] != '\0') {//动态分配的数组不能使用sizeof来计算数组大小
    Text4Length++;
  }
  static bool flag = true;  
  if(flag){
    tft.initR(INITR_BLACKTAB); // 使用适当的初始化参数
    tft.setRotation(1); // 设置显示方向,1表示顺时针旋转90度
    tft.fillScreen(BACKGROUND_COLOUR);//设置背景颜色
    flag = false; //初始化完成改变标志位防止重复初始化
  }     
  tft.drawRect(Rect1x,Rect1y,Rect1w,Rect1h,RECT1_COLOUR);//绘制矩形框1:整体框架
  
  tft.setTextColor(TEXT1_COLOUR);//设置文字颜色
  tft.setTextSize(Text1Size);//设置字体大小
  tft.setCursor(Text1x,Text1y);//设置起始位置
  tft.print(Text1String);//显示Text1 From

  tft.drawFastVLine(Line1x,Line1y,Line1h,LINE1_COLOUR);//绘制Text1后竖线
  
  tft.setTextColor(TEXT2_COLOUR);//设置文字颜色
  tft.setTextSize(Text2Size);//设置字体大小
  tft.setCursor(Text2x,Text2y);//设置起始位置
  tft.fillRect(Text2x, Text2y, 6 * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
  tft.print(Text2String);//显示Text2 ASRPRO

  //tft.drawFastVLine(Line2x,Line2y,Line2h,LINE2_COLOUR);//绘制Text2后竖线

  tft.fillRect(Rect2x,Rect2y,Rect2w,Rect2h,RECT2_COLOUR);//绘制矩形框2:Text3包围框
  
  tft.setTextColor(TEXT3_COLOUR);//设置文字颜色
  tft.setTextSize(Text3Size);//设置字体大小
  tft.setCursor(Text3x,Text3y);//设置起始位置
  tft.print(Text3String);//显示Text3 R

  tft.setTextWrap(false);//关闭超出范围自动换行 实现滚动字幕显示
  if(Text4Length > Text4C){//需要显示的数据数量大于剩余空间可显示字符数量
    //传递参数
    for(int i = 0; i < MAX_SHOW_STRING ; i++){
      if(showFlag[i] == 0){
        showFlag[i] = 1;//标记已使用
        showCount[i] = Text4C;//可显示的字符数
        maxCount[i] = Text4Length;//需要显示的字符数
        textShowX[i] = Text4x;
        textShowY[i] = Text4y;
        strcpy(textMaxString[i],text4);//传递需要显示的字符串
        timerAlarmEnable(timer); // 启用定时器
        
        //直接静态显示
        tft.fillRect(Text4x, Text4y, Text4C * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
        strncpy(Text4String, text4,Text4C);//从text4拷贝Text4C个字符到Text4String
        Text4String[Text4C] = '\0'; // 添加字符串结束符
        tft.setTextColor(TEXT4_COLOUR);//设置文字颜色
        tft.setTextSize(Text4Size);//设置字体大小
        tft.setCursor(Text4x,Text4y);//设置起始位置
        tft.print(Text4String);//显示Text4 Recived data!
        return ;//找到可显示位置即退出循环
      }
    }
  }
  else{//直接静态显示
    tft.fillRect(Text4x, Text4y, Text4C * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
    strncpy(Text4String, text4,Text4C);//从text4拷贝Text4C个字符到Text4String
    Text4String[Text4C] = '\0'; // 添加字符串结束符
    tft.setTextColor(TEXT4_COLOUR);//设置文字颜色
    tft.setTextSize(Text4Size);//设置字体大小
    tft.setCursor(Text4x,Text4y);//设置起始位置
    tft.print(Text4String);//显示Text4 Recived data!
  }  
}
void showDataUI(int dataX,int dataY,int Icon,char dataArr[],char dataName[]){//起始点坐标x,y 显示图标 数据字符串 数据名称
  int IconX = dataX;
  int IconY = dataY;
  int IconW = 12;
  int IconH = 25;
  int IconN = Icon;//0代表温度
  if(IconN == 0) tft.fillRect(IconX,IconY,IconW,IconH,ST7735_GREEN);
  else tft.fillRect(IconX,IconY,IconW,IconH,ST7735_BLUE);//ICON绘制

  tft.fillRect(IconX + IconW + 5, IconY, 10 * 5, 16, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
  tft.setCursor(IconX + IconW + 5,IconY);//设置起始位置
  tft.setTextColor(DATA_COLOUR);//设置文字颜色
  tft.setTextSize(2);//设置字体大小
  tft.print(dataArr);//显示Text4 Recived data!
  if(dataX == 0) tft.print("C");
  else tft.print("%");

  //tft.fillRect(IconX + IconW + 1, IconY + 18, 10 * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
  tft.setCursor(IconX + IconW + 1,IconY + 18);//设置起始位置
  tft.setTextColor(DATANAME_COLOUR);//设置文字颜色
  tft.setTextSize(1);//设置字体大小
  tft.print(dataName);//显示Humidity
  
}
void showLongMessage(int Mode,String Message){
  static int LongMessageCount = 3;
  static int showCount = 0;
  int X = 0;
  int Y = 0;
  int MessageX = X;
  int MessageY = 100 - 1 - 13 * LongMessageCount + showCount % 3 * 13;
  int MessageW = 160 - X;
  int MessageH = 14;
  
  tft.drawRect(MessageX, MessageY, MessageW, MessageH, ST7735_RED);//绘制矩形框
  
  tft.fillRect(MessageX,MessageY, 14, 14, ST7735_RED);//绘制包围矩形框
  tft.setCursor(MessageX + 3,MessageY + 3);//设置起始位置
  tft.setTextColor(ST7735_GREEN);//设置文字颜色
  tft.setTextSize(1);//设置字体大小
  if(Mode == SHOW_MODE_RECIVED) tft.print("R");//显示R
  else if(Mode == SHOW_MODE_SEND) tft.print("S");//显示S
  else if(Mode == SHOW_MODE_MYSELF) tft.print("M");//显示M
  
  tft.fillRect(MessageX + 3 + 15,MessageY + 3, (160 - 3 - X - 14) / 6 * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
  tft.setCursor(MessageX + 3 + 15,MessageY + 3);//设置起始位置
  tft.setTextColor(ST7735_BLUE);//设置文字颜色
  tft.setTextSize(1);//设置字体大小
  tft.print(Message);//显示Message
  showCount += 1;
  
}


void ComputerControlCallBack(JsonVariant p){
  //电脑控制回调函数
    int ComputerControl = p["ComputerControl"];
    //digitalWrite(COMPUTER_PIN,LOW);
    delay(500);
    //digitalWrite(COMPUTER_PIN,HIGH);
}
void LedControlCallBack(JsonVariant p){
  //灯光控制回调函数
  int LedControl = p["LedControl"];
  Serial.println("LedControl:" + String(LedControl));
}
void DateSend(void){
// 发送一个数据到云平台,CurrentTemperature 是在设备产品中定义的物联网模型的 id
    AliyunIoTSDK::send("Temperature", temperature);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("Humility", humidity);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("ComputerControl", ComputerControl);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("FanControl", FanControl);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("FanPwm", FanPwm);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("FanRunTimeHour", FanRunTimeHour);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("FanRunTimeMin", FanRunTimeMin);
    delay(SEND_DELAY_TIME);  
    AliyunIoTSDK::send("LedControl", Led1Control);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("LedLuminance", LedLuminance);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("LightValue", LightValue);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("SmokeDetection", SmokeDetection);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("AmbientLightControl", AmbientLightControl);
    delay(SEND_DELAY_TIME);   
    AliyunIoTSDK::send("AmbientLightRed", AmbientLightRed);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("AmbientLightGreen", AmbientLightGreen);
    delay(SEND_DELAY_TIME);
    AliyunIoTSDK::send("AmbientLightBlue", AmbientLightBlue);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("AmbientLightCount", AmbientLightCount);
    delay(SEND_DELAY_TIME);    
    AliyunIoTSDK::send("AmbientLightLuminance", AmbientLightLuminance);
    delay(SEND_DELAY_TIME);    
}
void wifiInit(const char *ssid, const char *passphrase)
{
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, passphrase);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(1000);
        Serial.println("WiFi not Connect");
    }
    Serial.println("Connected to AP");
    
}

void serial2DataDispose(){//处理串口2接收的数据 
  if(Serial2.available() > 0) { // 如果串口2中有数据可用 数据格式From:Name-State 
    String receivedFromString2  = Serial2.readStringUntil(':');//读取一行字符串,直到换行符 ':'
    String receivedNameString2  = Serial2.readStringUntil('-'); // 读取一行字符串,直到符  '-'
    String receivedStateString2 = Serial2.readStringUntil('\n'); // 读取一行字符串,直到换行符 '\n'  
    
    char receivedFromArray2[receivedFromString2.length() + 1];
    receivedFromString2.toCharArray(receivedFromArray2, receivedFromString2.length() + 1);
    
    char receivedNameArray2[receivedNameString2.length() + 1];    
    receivedNameString2.toCharArray(receivedNameArray2, receivedNameString2.length() + 1);
    
    char receivedStateArray2[receivedStateString2.length() + 1];    
    receivedStateString2.toCharArray(receivedStateArray2, receivedStateString2.length() + 1);
    
    String receivedMessageString2 = String(receivedNameString2 + '-' + receivedStateString2);
    char receivedMessageArray2[receivedMessageString2.length() + 1];    
    receivedMessageString2.toCharArray(receivedMessageArray2, receivedMessageString2.length() + 1);
        
    if(receivedNameString2 == ""){//name为空 无格式字符串
      messageShowUI(0,113 - 13 * 1,"From","NULL","R",receivedFromArray2);
      showLongMessage(SHOW_MODE_RECIVED,String(receivedFromString2 + receivedNameString2));
    }     
    else {//定义格式字符串      
      messageShowUI(0,113 - 13 * 1,"From",receivedFromArray2,"R",receivedMessageArray2);
      showLongMessage(SHOW_MODE_RECIVED,String(receivedFromString2 + ':' + receivedMessageString2));         
      if(receivedNameArray2[0] == 'T' && receivedNameArray2[1] == 'i' && receivedNameArray2[2] == 'm'){//Time-Get
        if(receivedStateArray2[0] == 'G' && receivedStateArray2[1] == 'e' && receivedStateArray2[2] == 't'){          
          sprintf(timeArr, "Time:%02d-%02d-%02d", currentHour, currentMinute, currentSecond);// 使用sprintf格式化时间
          Serial2.printf(timeArr);//使用printf ASRPRO才能接收到数据
          messageShowUI(0,113,"Goal","ASRPRO","S",timeArr);//目标 名称 发送 数据 
          showLongMessage(SHOW_MODE_SEND,timeArr);   
        }
      }
      else if(receivedNameArray2[0] == 'L' && receivedNameArray2[1] == 'e' && receivedNameArray2[2] == 'd'){//Led-On
        if(receivedStateArray2[0] == 'O' && receivedStateArray2[1] == 'n' ){
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Led",3) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Led:On");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Led","S","Led-On");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Led-On"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }        
          messageShowUI(0,113,"Goal","Led","S","Led-On");//目标 名称 发送 数据 
          showLongMessage(SHOW_MODE_SEND,"Led-On");   
        }
        else if(receivedStateArray2[0] == 'O' && receivedStateArray2[1] == 'f' && receivedStateArray2[2] == 'f'){//Led-Off
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Led",3) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Led:Off");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Led","S","Led-Off");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Led-Off"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }  
          messageShowUI(0,113,"Goal","Led","S","Led-Off");//目标 名称 发送 数据 
          showLongMessage(SHOW_MODE_SEND,"Led-Off");          
        }         
      }
      else if(receivedNameArray2[0] == 'R' && receivedNameArray2[1] == 'e' && receivedNameArray2[2] == 'l'){//Relay-On
        if(receivedStateArray2[0] == 'O' && receivedStateArray2[1] == 'n' ){       
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Relay",5) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Relay:On");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Relay","S","Relay-On");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Relay-On"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }        
        }
        else if(receivedStateArray2[0] == 'O' && receivedStateArray2[1] == 'f' && receivedStateArray2[2] == 'f'){//Relay-Off         
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Relay",5) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Relay:Off");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Relay","S","Relay-Off");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Relay-Off"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }      
        }  
        else if(receivedStateArray2[0] == 'O' && receivedStateArray2[1] == 'p' && receivedStateArray2[2] == 'e'){//Relay-Open         
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Relay",5) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Relay:Open");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Relay","S","Relay-Open");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Relay-Open"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }      
        } 
        else if(receivedStateArray2[0] == 'C' && receivedStateArray2[1] == 'l' && receivedStateArray2[2] == 'o'){//Relay-Close         
          for(int i = 0; i < deviceCount;i++){
            if(strncmp(devices[i].name.c_str(),"Relay",5) == 0){
              udp.beginPacket(devices[i].ip, udpPort);
              udp.printf("Relay:Close");
              udp.endPacket();
              messageShowUI(0,113,"Goal","Relay","S","Relay-Close");//目标 名称 发送 数据 
              showLongMessage(SHOW_MODE_SEND,"Relay-Close"); 
              Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
              break;                
            }
            else if(i == deviceCount - 1){//当i == deviceCount - 1 时仍未找到即不存在该设备
              showLongMessage(SHOW_MODE_SEND,"No found device!"); 
            }
          }      
        }                         
      }      
    }
  }
}
void dht11DataGet(void){
  static bool initFlag = false;
  if(!initFlag)//未初始化  
  {
    delay(2000);  // 延迟2秒以便传感器稳定
    initFlag = true;
  }
  humidity = dht.readHumidity();        // 读取湿度
  temperature = dht.readTemperature();  // 读取温度(默认摄氏度)
  if (isnan(humidity) || isnan(temperature)) {
    showLongMessage(SHOW_MODE_MYSELF,"Failed to read from DHT sensor!");
    return;
  }
  sprintf(HumiStr,"%.0f",humidity);//"%.1f"一位小数的float类型
  sprintf(TempStr,"%.1f",temperature);
}
void showTimeUI(void){
  char timeString[] = "23:59:59";
  int timeX = 6;
  int timeY = 32;
  int timeSize = 3;
  static int showHour = 0;
  static int showMinute = 0;
  static int showSecond = 0;
  tft.setTextColor(TIME_COLOUR);//设置文字颜色
  tft.setTextSize(timeSize);//设置字体大小
  if(showHour != currentHour){
    showHour = currentHour;
    char showHourArr[] = "59";
    sprintf(showHourArr,"%02d",showHour); //格式化%02d      
    tft.fillRect(timeX,timeY,6*timeSize*2,8*timeSize,ST7735_BLACK);//刷新
    tft.setCursor(timeX,timeY);//设置显示位置   
    tft.print(showHourArr);//showHourArr 
  }
  tft.setCursor(timeX + 6 * timeSize * 2,timeY);//设置显示位置
  tft.print(":");//: 
  if(showMinute != currentMinute){
    showMinute = currentMinute;
    char showMinuteArr[] = "59";
    sprintf(showMinuteArr,"%02d",showMinute); //格式化%02d     
    tft.fillRect(timeX + 6 * timeSize * 3,timeY,6 * timeSize * 2,8 * timeSize,ST7735_BLACK);//刷新
    tft.setCursor(timeX + 6 * timeSize * 3,timeY);//设置显示位置   
    tft.print(showMinuteArr);//showMinuteArr 
  }
  tft.setCursor(timeX + 6 * timeSize * 5,timeY);//设置显示位置  
  tft.print(":");//: 
  if(showSecond != currentSecond){
    showSecond = currentSecond;
    char showSecondArr[] = "59";
    sprintf(showSecondArr,"%02d",showSecond);
    tft.fillRect(timeX + 6 * timeSize * 6,timeY,6 * timeSize * 2,8 * timeSize,ST7735_BLACK);//刷新
    tft.setCursor(timeX + 6 * timeSize * 6,timeY);//设置显示位置   
    tft.print(showSecondArr);//showSecondArr 
  }
  else{
    char showSecondArr[] = "59";
    sprintf(showSecondArr,"%02d",showSecond); 
    tft.setCursor(timeX + 6 * timeSize * 6,timeY);//设置显示位置   
    tft.print(showSecondArr);//showSecondArr       
  }
}
void processDiscoverPacket(){//处理5089端口的发现请求
  int packetSize = discoveryUdp.parsePacket();
  if(packetSize){
    char incomingPacket[255];
    int len = discoveryUdp.read(incomingPacket,255);//读取数据
    if(len > 0){
      incomingPacket[len] = 0;
    }
    //处理发送请求
    if (strcmp(incomingPacket, "DISCOVER") == 0) {
      discoveryUdp.beginPacket(discoveryUdp.remoteIP(), discoveryUdp.remotePort());
      discoveryUdp.printf("ESP32_IP:%s,PORT:%d", WiFi.localIP().toString().c_str(), udpPort);
      discoveryUdp.endPacket();
    }    
  }
}

void processIncomingPacket(){//处理5088端口接收的数据
  int packetSize = udp.parsePacket();
  if (packetSize) {
    char incomingPacket[255];
    int len = udp.read(incomingPacket, 255);
    if (len > 0) {
      incomingPacket[len] = 0;
    }
    //Serial.printf("Received packet of size %d from %s:%d\n", packetSize, udp.remoteIP().toString().c_str(), udp.remotePort());
    //Serial.printf("Packet contents: %s\n", incomingPacket);
    showLongMessage(SHOW_MODE_MYSELF,incomingPacket);

    // 处理设备消息
    String message = String(incomingPacket);
    String deviceName = message.substring(0, message.indexOf(':'));//数据格式deviceName:stateName 例如Relay:On
    String stateName = message.substring(message.indexOf(':') + 1 );
    IPAddress senderIP = udp.remoteIP();
    // 处理具体的设备信息
    if(strcmp(stateName.c_str(),"Link") == 0){
      udp.beginPacket(senderIP, udpPort);
      udp.printf("OK");
      udp.endPacket(); 
      showLongMessage(SHOW_MODE_MYSELF,"Send OK!");      
    }    
    bool deviceExists = false;
    for (int i = 0; i < deviceCount; i++) {
      if (devices[i].name == deviceName) {
        deviceExists = true;
        devices[i].ip = senderIP;  // 更新设备IP
        break;
      }
    }    
    if (!deviceExists && deviceCount < MAX_CONNECT_COUNT) {
      devices[deviceCount].name = deviceName;
      devices[deviceCount].ip = senderIP;
      deviceCount++;
    }
    Serial.println("Device List:");
    for (int i = 0; i < deviceCount; i++) {
      Serial.printf("Device %d: Name = %s, IP = %s\n", i + 1, devices[i].name.c_str(), devices[i].ip.toString().c_str());
    }


  }
}
void IRAM_ATTR onTimer() {
  // 在定时器中断服务程序中更新屏幕显示
  //static int timeCount = 0;
  //开启滚动显示
  static int i[MAX_SHOW_STRING] = {0};
  for(int j = 0; j < MAX_SHOW_STRING; j++){
    if(showFlag[j] == 1){//需要显示
      if(i[j] <= maxCount[j] - showCount[j]){//滚动字幕显示 
        tft.fillRect(textShowX[j], textShowY[j], showCount[j] * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域 
        strncpy(textShowString[j], textMaxString[j] + i[j],showCount[j]);//从textMaxString[j]的第i[j]个字符开始,拷贝showCount[j]个字符到textShowString[j]  
        textShowString[j][showCount[j]] = '\0'; // 添加字符串结束符
        tft.setTextColor(TEXT4_COLOUR);//设置文字颜色
        tft.setTextSize(1);//设置字体大小
        tft.setCursor(textShowX[j],textShowY[j]);//设置显示位置
        tft.print(textShowString[j]);//显示Text4 Recived data!
        i[j] += 1;
      }
      else{//滚动字幕显示结束 显示前showCount[j]个字符
        tft.fillRect(textShowX[j], textShowY[j], showCount[j] * 6, 8, BACKGROUND_COLOUR);//绘制矩形刷新部分区域
        strncpy(textShowString[j], textMaxString[j],showCount[j]);//将textMaxString[j]的前showCount[j]个字符,拷贝到textShowString[j] 
        textShowString[j][showCount[j]] = '\0'; // 添加字符串结束符
        tft.setTextColor(TEXT4_COLOUR);//设置文字颜色
        tft.setTextSize(1);//设置字体大小
        tft.setCursor(textShowX[j],textShowY[j]);//设置显示位置
        tft.print(textShowString[j]);//显示Text4 Recived data!
        i[j] = 0;
        showFlag[j] = 0;
        //timerEnd(timer); // 关闭定时器      
      }
    }
  }
}
void setup() { 
  Serial.begin(115200);//初始化串口0用于调试
  Serial2.begin(115200);//初始化串口2用于交换数据
  dht.begin();        // 初始化DHT传感器
  // 初始化 wifi
  wifiInit(WIFI_SSID, WIFI_PASSWD);
  // 启动两个UDP服务器
  udp.begin(udpPort);// udpPort
  discoveryUdp.begin(discoveryPort);// discoveryPort
  // 初始化 iot,需传入 wifi 的 client,和设备产品信息
  AliyunIoTSDK::begin(espClient, PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET, REGION_ID);
  // 初始化NTP客户端
  timeClient.begin();
  // 配置硬件定时器
  timer = timerBegin(0, 80, true); // 使用定时器0,分频器80,计数上升沿触发中断
  timerAttachInterrupt(timer, &onTimer, true); // 将中断服务程序与定时器关联
  timerAlarmWrite(timer, SHOWDELAY_TIME * 1000, true); // 设置定时器触发时间为SHOWDELAY_TIME*1000(单位:微秒)默认为500us*1000 = 0.5s
  //timerAlarmEnable(timer); // 启用定时器

  dht11DataGet();
  messageShowUI(0,113 - 13 * 1,"From","ASRPRO","R","Recived data:Hello World!");
  messageShowUI(0,113,"Goal","Fan","S","Send data:Hello World!");  
  showDataUI(0,0,0,TempStr, "Temperature");
  showDataUI(80,0,1,HumiStr,"Humidity");
  //showLongMessage(SHOW_MODE_MYSELF,"Hello World!");
}

void loop() {
  unsigned long currentMillis = millis();//获取已运行时间
  static char oldHumiStr[6] = "78";
  static char oldTempStr[6] = "34.8";  
  // 每100毫秒处理一次数据
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    serial2DataDispose();
    processIncomingPacket();//处理5088端口接收的数据
  } 
  // 每1秒更新一次时间
  if (currentMillis - previousMillisUpdate >= updateInterval) {
    previousMillisUpdate = currentMillis;
    timeClient.update();
    unsigned long epochTime = timeClient.getEpochTime();
    struct tm *ptm = gmtime((time_t *)&epochTime);
    currentHour = ptm->tm_hour;
    currentMinute = ptm->tm_min;
    currentSecond = ptm->tm_sec;
    showTimeUI();
    processDiscoverPacket();//处理5089端口的发现请求
  }
  // 每10秒更新一次温度和湿度数据
  if (currentMillis - previousMillisData >= dataInterval) {
    previousMillisData = currentMillis;
    dht11DataGet();
    if (strcmp(oldTempStr, TempStr) != 0) {
      showDataUI(0, 0, 0, TempStr, "Temperature");
      strcpy(oldTempStr, TempStr);
      AliyunIoTSDK::send("Temperature", temperature);//上传数据到服务器    
    }
    if (strcmp(oldHumiStr, HumiStr) != 0) {
      showDataUI(80, 0, 1, HumiStr, "Humidity");
      strcpy(oldHumiStr, HumiStr);
      AliyunIoTSDK::send("Humility", humidity);//上传数据到服务器    
    }
    AliyunIoTSDK::loop(); // 你提到的保持服务器的连接,每10秒执行一次
  }    
}

显示部分代码可参考Arduino平台基于GFX库的滚动字幕消息显示UI设计(定时器实现刷新)
阿里云平台SDK相关代码参考使用ESP-01S与阿里云物联网平台服务器的连接与配置(温湿度检测)

ASRPRO代码
#include "asr.h"
extern "C"{ void * __dso_handle = 0 ;}
#include "setup.h"
#include "HardwareSerial.h"
#include "myLib/asr_event.h"

uint32_t snid;
void ASR_CODE();

//{speak:小蝶-清新女声,vol:4,speed:10,platform:haohaodada}
//{playid:10001,voice:欢迎使用智能家居助手,用小璐同学唤醒我。}
//{playid:10002,voice:我退下了,用小璐同学唤醒我}

/*默认循环函数
*/
void ASR_CODE(){
  String Serial1_Data = "TIME:23-45-30";
  String Data_Type = "0";
  String Clock = "0";
  String Min = "0";
  switch (snid) {
   case 2:
    Serial1.println("ASRPRO:Led-On");
    delay(10);
    break;
   case 3:
    Serial1.println("ASRPRO:Led-Off");
    delay(10);
    break;
   case 4:
    Serial1.println("ASRPRO:Led-Up");
    delay(10);
    break;
   case 5:
    Serial1.println("ASRPRO:Led-Down");
    delay(10);
    break;
   case 8:
    Serial1.println("ASRPRO:Computer-On");
    delay(10);
    break;
   case 9:
    Serial1.println("ASRPRO:Computer-Off");
    delay(10);
    break;
   case 10:
    Serial1.println("ASRPRO:Fan-On");
    delay(10);
    break;
   case 11:
    Serial1.println("ASRPRO:Fan-Off");
    delay(10);
    break;
   case 12:
    Serial1.println("ASRPRO:Fan-Up");
    delay(10);
    break;
   case 13:
    Serial1.println("ASRPRO:Fan-Down");
    delay(10);
    break;
   case 14:
    Serial1.println("ASRPRO:Led-Up");
    delay(10);
    break;
   case 15:
    Serial1.println("ASRPRO:Led-Down");
    delay(10);
    break;
   case 16:
    Serial1.println("ASRPRO:Led-Max");
    delay(10);
    break;
   case 17:
    Serial1.println("ASRPRO:Led-Min");
    delay(10);
    break;
   case 18:
    Serial1.println("ASRPRO:Switch-On");
    delay(10);
    //{playid:10500,voice:开关已打开}
    play_audio(10500);
    break;
   case 19:
    Serial1.println("ASRPRO:Switch-Off");
    delay(10);
    //{playid:10501,voice:开关已关闭}
    play_audio(10501);
    break;
   case 20:
    Serial1.println("ASRPRO:Fan-Max");
    delay(10);
    break;
   case 21:
    Serial1.println("ASRPRO:Fan-Min");
    delay(10);
    break;
   case 22:
    Serial1.println("ASRPRO:Time-Get");
    delay(500);
    if(Serial1.available() > 0){
      Serial1_Data = Serial1.readString();
      Data_Type = String(Serial1_Data).substring(1,4);
      if(String(Data_Type).indexOf("TIME",1)){
        Clock = String(Serial1_Data).substring(5,7);
        Min = String(Serial1_Data).substring(8,10);
        //{playid:10502,voice:现在时间}
        play_audio(10502);
        play_num((int64_t)((String(Clock).toInt()) * 100), 1);
        //{playid:10503,voice:点}
        play_audio(10503);
        play_num((int64_t)((String(Min).toInt()) * 100), 1);
        //{playid:10504,voice:分}
        play_audio(10504);
      }
    }
    break;
   case 23:
    Serial1.println("ASRPRO:Sweater-Get");
    delay(500);
    break;
   case 24:
    Serial1.println("ASRPRO:Relay-On");
    delay(100);
    break;
   case 25:
    Serial1.println("ASRPRO:Relay-Off");
    delay(100);
    break;
  }

}

void hardware_init(){
  setPinFun(2,FORTH_FUNCTION);
  setPinFun(3,FORTH_FUNCTION);
  Serial1.begin(115200);
  vTaskDelete(NULL);
}

void setup()
{
  //{ID:0,keyword:"唤醒词",ASR:"小璐同学",ASRTO:"我在"}
  //{ID:1,keyword:"命令词",ASR:"退下",ASRTO:"好的"}
  //{ID:2,keyword:"命令词",ASR:"开灯",ASRTO:"好的,马上打开灯光"}
  //{ID:3,keyword:"命令词",ASR:"关灯",ASRTO:"好的,马上关闭灯光"}
  //{ID:4,keyword:"命令词",ASR:"灯亮一点",ASRTO:"好的,正在为您增加亮度"}
  //{ID:14,keyword:"命令词",ASR:"亮一点",ASRTO:"好的,正在为您增加亮度"}
  //{ID:5,keyword:"命令词",ASR:"灯暗一点",ASRTO:"好的,正在为您降低亮度"}
  //{ID:15,keyword:"命令词",ASR:"暗一点",ASRTO:"好的,正在为您降低亮度"}
  //{ID:6,keyword:"命令词",ASR:"当前温度",ASRTO:"当前温度二十七摄氏度"}
  //{ID:7,keyword:"命令词",ASR:"当前湿度",ASRTO:"当前湿度百分之七十"}
  //{ID:8,keyword:"命令词",ASR:"打开电脑",ASRTO:"好的,正在为您打开电脑"}
  //{ID:9,keyword:"命令词",ASR:"关闭电脑",ASRTO:"好的,正在为您关闭电脑"}
  //{ID:10,keyword:"命令词",ASR:"打开风扇",ASRTO:"好的,正在为您打开风扇"}
  //{ID:11,keyword:"命令词",ASR:"关闭风扇",ASRTO:"好的,正在为您关闭风扇"}
  //{ID:12,keyword:"命令词",ASR:"风扇快一点",ASRTO:"好的,正在增大风速"}
  //{ID:13,keyword:"命令词",ASR:"风扇慢一点",ASRTO:"好的,正在减小风速"}
  //{ID:16,keyword:"命令词",ASR:"亮度调到最大",ASRTO:"好的,正在调整为最大亮度"}
  //{ID:17,keyword:"命令词",ASR:"亮度调到最小",ASRTO:"好的,正在调整为最小亮度"}
  //{ID:18,keyword:"命令词",ASR:"打开开关",ASRTO:"好的,正在为您打开开关"}
  //{ID:19,keyword:"命令词",ASR:"关闭开关",ASRTO:"好的,正在为您关闭开关"}
  //{ID:20,keyword:"命令词",ASR:"风速调到最大",ASRTO:"好的,正在调整为最大风速"}
  //{ID:21,keyword:"命令词",ASR:"风速调到最小",ASRTO:"好的,正在调整为最小风速"}
  //{ID:22,keyword:"命令词",ASR:"几点了",ASRTO:"正在查询"}
  //{ID:23,keyword:"命令词",ASR:"今天天气怎么样",ASRTO:"正在查询"}
  //{ID:24,keyword:"命令词",ASR:"打开继电器",ASRTO:"好的,正在为您打开继电器"}
  //{ID:25,keyword:"命令词",ASR:"关闭继电器",ASRTO:"好的,正在为您关闭继电器"}

  //{playid:10084,voice:零}
  //{playid:10085,voice:一}
  //{playid:10086,voice:二}
  //{playid:10087,voice:三}
  //{playid:10088,voice:四}
  //{playid:10089,voice:五}
  //{playid:10090,voice:六}
  //{playid:10091,voice:七}
  //{playid:10092,voice:八}
  //{playid:10093,voice:九}
  //{playid:10094,voice:十}
  //{playid:10095,voice:百}
  //{playid:10096,voice:千}
  //{playid:10097,voice:万}
  //{playid:10098,voice:亿}
  //{playid:10099,voice:负}
  //{playid:10100,voice:点}
}

应用场景

智能家居:
配合其他设备实现家庭设备的智能化管理,如照明、温控等。

项目进展

已完成部分功能以及测试,包括但不限于:

  • 本地语音控制
  • 控制程序开发
  • 手机、电脑网页端控制
  • 阿里云物联网平台后台控制
  • 设备间通信及控制
  • 温湿度数据获取、显示以及上传
  • 网络实时时间获取及显示
  • 语音报时

后续进行完善及补充,后续将在立创开源社区进行软硬件开源,后续以开源社区更新为准,此博客不实时更新
点击前往立创开源社区查看:智能家居助手

演示视频

语音控制继电器以及LED灯演示

阿里云物联网平台应用演示

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值