基于ESP32-S3的WIFI天气时钟基础版
项目组成开发环境Arduino开发平台硬件模块ESP32-S3-N16R8此模块自带wifi模块具有8MB大PSRAM16MB Flash能够跑LVGL大屏GUIOV2640摄像头图像缓存离线语音模型TinyML本地AI推理也能存放网页固件大模型文件频繁OTA也不容易空间不足支持ArduinoESP-IDF双开发框架有低功耗特点因为内置ULP-RISC-V协处理器深度睡眠可实现电池供电4引脚OLED此模块的分辨率为128x64运用的是I2C总线通信协议其I2C设备地址为0x3C最常见/0x3D具有SDA数据线SCL时钟线VDD必须是3.3VGND组成的驱动芯片是SSD1306能实现文字图形的显示显示为单色白色/蓝色自发光无需背光功耗低接线极简软件部分WiFi部分ESP32-S3自带WiFi模块此模块分为热点AP模式和客户端STA模式热点AP模式是指ESP32-S3自己开个网用PC端或者手机端进行连接客户端STA模式指的是ESP32-S3自己作为终端来连接路由器我们这次用的是STA模式WiFi相关函数WiFi.begin(ssid, password) //连接WiFi网络第一个参数为名称第二个为密码 WiFi.disconnect() //断开当前的WiFi连接 WiFi.status() //返回当前WiFi连接的状态用于判断是否连接成功 WL_CONNECTED //连接到了WiFi WL_DISCONNECTED //未连接到WiFi WL_IDLE_STATUS //WiFi处于空闲状态 WL_NO_SSID_AVAIL //未找到指定的WiFi网络 WiFi.localIP() //获取当前的IP地址 WiFi.macAddress() //返回ESP32的mac地址 WiFi.scanNetworks() //扫描周围可用WiFi返回一个整数表示扫到的网络数量 WiFi.SSID() / WiFi.RSSI() //能获取每个网络的详细信息 WiFi.SSID(networkIndex) //该函数返回指定索引的扫描到的WiFi网络的SSIDOLED部分此OLED分辨率是128x64,而一个中文汉字一般占16x16个点阵英文一般占8x8个点阵关键函数#include Adafruit_SSD1306.h //SSD1306 OLED驱动库 #include Adafruit_GFX.h //Adafruit通用图形库画线文字圆形 Adafruit_SSD1306 display(width, high, wire, OLED_RESET) //创建OLED对象最后一个参数为复位引脚默认为-1 display.begin(SSD1306_SWITCHCAPVCC, 0x3C) //初始化OLED默认地址为0x3C第一个参数为使用SSD1306芯片内部电荷泵产生负压 display.clearDisplay() //清除屏幕缓存 display.setTextColor(SSD1306_WHITE) //设置文字为白色 display.setCursor(0,0) //设置初始坐标(0,0)为OLED屏幕的左上角 display.display() //刷新屏幕 display.setTextSize(2) //设置数字/英文发达两倍不能设置中文NTP网络校时概念NTP实则是一种协议它的作用是从互联网时间服务器中获取UTC标准时间协调世界时的如何获取我们通过访问NTP服务器传输层中的UDP协议访问123端口从而获取标准世界时但是服务器返回的是NTP时间戳NTP时间戳NTP时间戳就是从1900-01-01 00:00:00 开始到现在所用的毫秒数Unix时间戳Unix时间戳就是从1970-01-01 00:00:00 开始到现在所用的毫秒数RTC内部时钟RTC时钟指的就是ESP32内部硬件时钟三者的关系其中RTC时钟只认Unix时间戳不认NTP时间戳只有Unix时间戳才能使RTC时钟同步到当前UTC标准时间中国位于东八区因为时区的影响我们要比其快8小时因此要加8小时还要考虑夏令时因为中国无夏令时因此直接是0即可因此我们首先需要将NTP时间戳转化为Unix时间戳configTime()这个函数统一完成了转换Unix时间戳转化公式Unix时间戳 NTP时间戳 - 2208988800NTP与Unix之间固定的时间差关键函数struct tm timeinfo //因为在C中固定将时间写入这个结构体当中 //tm功能 //1.时间日期默认写到这个结构体当中 //2.注意在写入年份的时候这个结构体的年份值固定-1900 //3.注意月份是0-110为1月因此我们在输出的时候我们要加1 // getLocalTime(timeinfo) //判断是否将时间写如到此结构体当中 configTime(时区差夏令时服务器地址) //configTime()函数功能 //1.通过传输层UDP协议直接访问NTP的123端口的服务器 //2.自动由NTP时间戳转换为Unix时间戳 //3.得到Unix时间戳后直接转换成当天时间然后写入内部时钟RTC以后通过RTC走时 //HTTP部分HTTP请求这里我们简单了解即可它最简单是由请求行请求头请求体组成请求行一般包括请求方法请求URL和HTTP协议版本请求方法有GETPOSTPUTDELETE等请求头包含与请求相关的信息列入浏览器类型请求时间等请求体包含请求所需要的数据关键函数#include HTTPClient.h //http库函数 HTTPClient http //创建http对象 http.begin(url) //指定发送请求的url http.GET() //发送GET请求返回HTTP状态码 http.responseBody() //获取响应的正文 http.getString() //获取响应正文作为字符串 http.getStream() //获取响应正文作为流对象 http.end() //关闭连接并释放资源API概念API指的是应用程序编程接口它定义了程序之间进行通信的方式和规范说白了就是程序与程序之间的通信方式我们在使用他人提供的API接口的时候我们要遵循他人的规则要求使用一般使用HTTP或者HTTPS协议发送请求来访问API然后再通过HTTP或者HTTPS协议来返回所需要的响应数据JSON概念json是一种轻量级的数据交换格式常用于Web应用程序之间的数据传输它是一种文本格式而我们访问API所得到的响应数据就是运用的这种数据格式传输格式JSON数据由键值对组成其中键是字符串值可以是字符串数字布尔值数字对象等数据类型{ name:张三, age:29, is student:flase }关键函数#include ArduinoJson.h //引入JSON库函数 DynamicJsonDocument doc(1024) //创建doc对象用来存储和处理JSON数据并指定文档的大小 deserializeJson(doc, json) //函数将Json数据解析到对象doc当中 //docDynamicJsonDocument对象用于存储解析后的Json数据 //json包含Json数据的字符串或字符数组也就是此项目中的响应数据 int value doc[key].asint(); //通过使用astype()方法从解析后的Json文档中获取值 //key是Json对象的键 //astype()是将值转换为指定的类型比如int,float,String等项目实现项目流程图项目代码实现WiFi连接核心代码//添加WiFi库 #include WiFi.h //WiFi账号密码参数设置 const char *ssid ; //WiFi的名称 const char *password ; //WiFi的密码 void setup() { // 串口初始化这里方便我们后面调试 Serial.begin(9600); //开始连接WiFi WiFi.begin(ssid, password); Serial.print(WiFi连接中); //判断是否连接成功 while(WiFi.status() ! WL_CONNECTED || WiFi.localIP() IPAddress(0,0,0,0)){ delay(500); Serial.print(.); } Serial.println(\nWiFi连接成功); Serial.print(IP地址为); Serial.println(WiFi.localIP()); }这段代码中最核心部分就是WiFi.begin(ssid, password)和是否连接成功实现的原理就是我们写上ssid和password以后开始连接然后根据WiF连接的状态来判断是否连接成功在判断IP地址是否是(0,0,0,0)这里如果我们不做这种判断很有可能打印出的地址就是这个这是因为我们在WiFi连接成功以后路由器还没来的及给分配地址因此就可能是(0,0,0,0)OLED核心代码//添加I2C库 #include Wire.h //添加OLED库 #include Adafruit_SSD1306.h //OLED配置 //配置OLED的分辨率 //OLED的宽 #define SCREEN_WIDTH 128 //OLED的高 #define SCREEN_HEIGHT 64 //不用OLED的复位引脚固定为-1 #define OLED_RESET -1 //定义SDA,SCL引脚 #define I2C_SDA 4 #define I2C_SCL 5 //创建OLED对象 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); void setup() { //初始化I2C总线配置SDA,SCL引脚 Wire.begin(I2C_SDA, I2C_SCL); //初始化OLEDI2C地址是0x3C if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)){ Serial.println(SSD1306分配失败请检查I2C连接和地址); for(;;); //如果屏幕没有找到程序停在这里 } display.clearDisplay(); //清空屏幕缓存 display.setTextColor(SSD1306_WHITE); //设置文字白色白底黑屏不需要反转; }OLED需要创建OLED对象初始化I2C总线配置SDA,SCL引脚然后初始化OLED其中第一个参数是启用内部电荷泵升压供电第二个参数是OLED设备地址由厂商定0x3C或0x3DHTTP及JSON核心代码//添加HTTP库 #include HTTPClient.h //添加Json库 #include ArduinoJson.h //HTTP部分 //设置api所用的url以及参数 String url ; String key ; String location ; String lang ; void setup() { //创建HTTPClient对象 HTTPClient http; //发送请求请求api接口 http.begin(url ?key key location location language lang); //查看http状态码 int code http.GET(); Serial.printf(状态码为%d,code); //接受响应所发来的数据 String response http.getString(); //释放资源 http.end(); //创建JSON对象,来处理和存储JSON数据 DynamicJsonDocument doc(1024); //使用函数将JSON数据解析到doc当中 deserializeJson(doc, response); //打印出想要打印的数据 city doc[results][0][location][name].asString(); sky doc[results][0][now][text].asString(); temp doc[results][0][now][temperature].asint(); humi doc[results][0][now][humidity].asint(); Serial.printf(城市%s 天气%s 温度%d 湿度%d,city,sky,temp,humi); }此核心代码我们需要注意的是访问API请求的url和一些参数需要根据一些网站的不同而定以上的变量只适合我用的那个网站然后根据返回来的JSON数据格式来打印想要打印的数据NTP//NTP参数设置 //服务器地址 const char *ntpServer ntp.aliyun.com; //东八区时区8小时 const long China_time 8 * 3600; //夏令时中国不实行固定为0 const int Summer_time 0; void setup() { //NTP初始化进行配置 //configTime()函数功能 //1.通过传输层UDP协议直接访问NTP的123端口的服务器 //2.自动由NTP时间戳转换为Unix时间戳 //3.得到Unix时间戳后直接转换成当天时间然后写入内部时钟RTC以后通过RTC走时 // configTime(China_time, Summer_time, ntpServer); //等待校时 Serial.print(\nNTP校时中...); //设置结构体从而判断是否写入到此结构体 //tm功能 //1.时间日期默认写到这个结构体当中 //2.注意在写入年份的时候这个结构体的年份值固定-1900 //3.注意月份是0-110为1月因此我们在输出的时候我们要加1 // struct tm timeinfo; //判断是否写入当中 while(!getLocalTime(timeinfo)){ delay(500); Serial.println(.); } //校时成功 Serial.println(\nNTP校时成功); //打印时间日期 Serial.printf(%04d-%02d-%02d %02d:%02d:%02d\r\n, timeinfo.tm_year 1900, timeinfo.tm_mon 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); }需要配置一些NTP参数服务器地址东八区夏令时这里面需要注意的是tm这个结构体因为同步的时间要写到这个结构体当中具体的代码中的说明已经解释了一些问题定时以及OLED核心代码//方便OLED打印 char buf[32]; //方便OLED打印天气 char sky_value[64]; char sky_value1[64]; //用millis()函数做判断实现定时需要三个宏定义来实现定时刷新屏幕定时获取天气数据定时获取NTP校时 //定时1s刷新屏幕 #define SCREEN_UPDATE 1000 //定时半小时获取一次天气数据 #define SKY_UPDATE 1800000 //定时1s获取NTP #define NTP_UPDATE 1000 //定义三个变量用于更新标记时间 unsigned long lastScreen 0; unsigned long lastSky 0; unsigned long lastNtp 0; void loop() { //打印天气数据 //打印城市 display.setCursor(0,9); snprintf(sky_value, sizeof(sky_value), city:%s,city.c_str()); display.println(sky_value); memset(sky_value, 0, sizeof(sky_value)); //获取当前运行毫秒 unsigned long now millis(); //用millis来进行定时NTP if(now - lastNtp NTP_UPDATE) { //更新标记时间 lastNtp now; printLocalTime(); //打印时间 } }这里面要说名一下snprintf这个函数等功能因为Adafruit_SSD1306.h当中没有包含display.printf()这个函数导致我们不能直接的显示一些复合的数据而这个函数就是解决这个问题第一个参数是存放数据的数组名第二个参数是数组的大小第三个参数是想要显示的任何字符当然不能超过数组的字节大小第四个参数则是存放数据的变量然后再通过display.println()打印到OLED中再由display.display()进行屏幕刷新其中memset()这个函数就是将数组中的数据清空的防止溢出出现问题首先我们是通过millis()这个函数进行的定时首先不用定时器是因为这个millis()函数更适合新手而且也不会堵塞任何程序它是在底层以毫秒的形式进行开机自启而我们使用此函数的逻辑就是利用其开机自启的毫秒数-上一次更新时间的毫秒数然后与自己所设置的时间进行对比如果正好或大于那么就进入执行所要执行的函数把存在问题因为这只是天气时钟的基础版还存在很多的问题需要解决比如首先初始化OLED屏幕方便后面排查问题解放了串口WiFi重新连接问题增加HTTP状态码判断问题OLED显示的数据更美观更整体等等问题都需要后期进行优化