一、项目介绍
本项目利用ESP32-S2 Audio V2.2开发板制作一个本地气象台,能够联网更新实时天气与时间,同时融入远程遥控的功能,通过任意红外遥控器控制网页上的按钮。
开发板运行实物图:
远程遥控网页:
二、设计思路
此次我完成了2个项目,分别是项目6和项目8,我们先看下题目要求:
项目6 制作一个本地气象台/温度计
- 利用OLED显示
- 显示当前本地的时间、温度和气象信息
项目8 远程遥控
- 利用板上的红外接收器,用遥控器控制网页界面上的按钮
题目看起来很简短,我们逐个分析一下。
关于项目6气象台方面:
由于开发板没有焊接温湿度传感器,但我们制作气象台的话需要想办法获取这些信息,因为ESP32-S2-Mini-1模组半载PCB天线,能够连接网络,所以气象数据和温湿度数据的获取可以通过心知天气API获取,随后我们把得到的数据进行解析,再通过OLED屏幕显示出来。
关于项目8远程遥控方面:
开发板上有红外接收器,我们通过Arduino库里的IRremote.h可以轻松驱动,并且把接受的数据解码为16进制。涉及网页的部分需要学习基本的HTML语法以及部分的JavaScript和CSS,这里我使用网页是从网上搜集的,原本CSS和HTML分开的,我对他进行了整合,并把全部代码放进了ESP32的flash内,我们可以根据接收到的不同红外型号,对网页请求返回不同的界面,从而达到开关切换的结果。
三、ESP32-Audio V2.2开发板硬件介绍
1.主控芯片模组
本平台使用了乐鑫公司的ESP32-S2-Mini-1模块,ESP32-S2-MINI-1是一颗通用型Wi-Fi MCU模组,功能强大,具有丰富的外设接口,可用于可穿戴电子设备、智能家居等场景。
ESP32-S2-MINI-1采用PCB板载天线,模组配置了4MB SPI flash,采用的是 ESP32-S2FN4 芯片。该芯片搭载了Xtensa® 32 位LX7 单核处理器,工作频率高达 240 MHz。
2.模组内部构成
3.核心功能介绍:
- 基于ESP32-S2 WiFi核心模块
- 128*64 OLED显示,SPI接口,显示信息、参数、波形
- 4个按键,用于参数控制、菜单选择
- 1路Mic音频输入 - 模拟电路,通过电位计可以调节增益0-40dB调节范围,并有带通滤波器
- 1路耳机插座音频输入 - 模拟电路,通过电位计可以调节增益 0-40dB调节范围,并有带通滤波器
- 2路音频输出,并有功率放大,可以驱动喇叭和耳机插座
- 一个FM接收模块,ESP32通过I2C接口对其进行参数设置,调节FM电台以及设置音量大小
- 一个模拟开关切换来自ESP32产生的音频还是FM输出的音频,模块开关的输出送到喇叭或耳机输出
4.开发板实物图
5.系统框图
四、开发环境的部署
我使用的Arduino IDE 1.8.19作为编译工具,VSCode作为代码编辑器。
Arduino IDE下载地址:https://www.arduino.cc/en/software
VSCode下载地址:https://code.visualstudio.com/
Arduino开发ESP32的方法可参考espressif官方文档:https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html
五、程序流程图与代码解析
首先对设备进行联网,若是连接局域网的话,路由器会分配给设备一个内网IP。
//连接网络
void linkNetwork()
{
Serial.println();
Serial.println();
Serial.print("正在连接WIFI");
u8g2.clearBuffer();
u8g2.setCursor(0, 15);
u8g2.print("联网中");
u8g2.sendBuffer();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.print(".");
u8g2.print(".");
u8g2.sendBuffer();
}
Serial.println("成功");
Serial.print("IP:");
Serial.println(WiFi.localIP());
u8g2.print(" OK");
u8g2.sendBuffer();
//OLED显示IP信息
u8g2.setCursor(0, 31);
u8g2.print(WiFi.localIP());
u8g2.sendBuffer();
}
随后与网络校准时间,并把当前时间在屏幕上。
//校准时间
void calibrateTime()
{
//东八区 8 * 3600,使用夏令时
configTime(8 * 3600, 0, "2.cn.pool.ntp.org", "time.nist.gov","3.cn.pool.ntp.org");
Serial.print(F("校时中"));
u8g2.setCursor(0, 47);
u8g2.print("校时中");
u8g2.sendBuffer();
time_t nowSecs = time(nullptr);
while (nowSecs < 8 * 3600 * 2) {
delay(1000);
Serial.print(F("."));
u8g2.print(".");
u8g2.sendBuffer();
yield();
nowSecs = time(nullptr);
}
if (!getLocalTime(&timeinfo))
{
Serial.println("获取时间失败");
return;
}
Serial.println("成功");
u8g2.print(" OK");
u8g2.sendBuffer();
//串口输出时间信息
Serial.print("当前日期:");
Serial.println(&timeinfo, "%F"); // 格式化输出
Serial.print("当前时间:");
Serial.println(&timeinfo, "%T"); // 格式化输出
//OLED输出时间信息
u8g2.setCursor(0, 63);
u8g2.print(&timeinfo, "%T");
u8g2.sendBuffer();
delay(500);
u8g2.clear();
}
利用心知天气的API获得温度与气象数据,并把显示在屏幕上。
//获取天气信息
void getWeather()
{
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort))
{
Serial.println("连接断开");
return;
}
client.print(String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
Serial.println("正在获取天气数据");
u8g2.setCursor(0, 15);
u8g2.print("获取天气数据");
u8g2.sendBuffer();
while(!client.available())
{
u8g2.print(".");
u8g2.sendBuffer();
delay(1000);
}
String answer;
while(client.available())
{
String line = client.readStringUntil('\r');
answer += line;
}
//断开连接
client.stop();
u8g2.print(" OK");
u8g2.sendBuffer();
//解析json
String jsonAnswer;
int jsonIndex;
for (int i = 0; i < answer.length(); i++)
{
if (answer[i] == '{')
{
jsonIndex = i;
break;
}
}
jsonAnswer = answer.substring(jsonIndex);
Serial.println();
Serial.print("返回JSON:");
Serial.println(jsonAnswer);
//访问下列链接即可得到天气的数据
//https://api.seniverse.com/v3/weather/now.json?key=SjO80gPUTg9VAFgHr&location=nanjing&language=zh-Hans&unit=c
//返回数据为下列格式
//{"results":[{"location":{"id":"WTSQQYHVQ973","name":"南京","country":"CN","path":"南京,南京,江苏,中国",
//"timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"阴","code":"9","temperature":"1"},
//"last_update":"2022-02-17T21:24:05+08:00"}]}
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 210;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, jsonAnswer);
JsonObject results_0 = doc["results"][0];
JsonObject results_0_location = results_0["location"];
const char* results_0_location_id = results_0_location["id"];
const char* results_0_location_name = results_0_location["name"];
const char* results_0_location_country = results_0_location["country"];
const char* results_0_location_path = results_0_location["path"];
const char* results_0_location_timezone = results_0_location["timezone"];
const char* results_0_location_timezone_offset = results_0_location["timezone_offset"];
JsonObject results_0_now = results_0["now"];
const char* results_0_now_text = results_0_now["text"];
const char* results_0_now_code = results_0_now["code"];
const char* results_0_now_temperature = results_0_now["temperature"];
const char* results_0_last_update = results_0["last_update"];
roll_str = (char *) malloc(100);
sprintf(roll_str, "今日%s天气%s 当前体感温度为%s摄氏度", results_0_location_name, results_0_now_text, results_0_now_temperature);
//串口输出天气信息
Serial.print("城市:");
Serial.println(results_0_location_name);
Serial.print("气温:");
Serial.println(results_0_now_temperature);
Serial.print("天气:");
Serial.println(results_0_now_text);
Serial.println("初始化完成");
//OLED输出天气信息
u8g2.setCursor(0, 31);
u8g2.print(results_0_location_name);
u8g2.print(" ");
u8g2.print(results_0_now_temperature);
u8g2.print("度 ");
u8g2.print(results_0_now_text);
u8g2.setCursor(0, 47);
u8g2.print("初始化完成");
u8g2.setCursor(0, 63);
u8g2.print("3 秒后进入系统");
u8g2.sendBuffer();
delay(1000);
u8g2.setCursor(0, 63);
u8g2.print("2 秒后进入系统");
u8g2.sendBuffer();
delay(1000);
u8g2.clearBuffer(); // 清除内部缓冲区
u8g2.setCursor(0, 63);
u8g2.print("1 秒后进入系统");
u8g2.sendBuffer();
delay(1000);
u8g2.clear();
}
关于气象数据的显示,由于长度太长并且我使用的是中文,屏幕一行128个像素点显示不完全,我们可以通过下列方法滚动显示,就像常见的广告点阵屏那样从左向右滚动。
//显示滚动字幕 方法2
void showRollStrInterrupt(char str[])
{
if(roll_str_move_dir == 0)
{
u8g2.setCursor(30-roll_str_move_x, 63);
u8g2.print(str);
if(roll_str_move_x == 220)
{
roll_str_move_dir = 1;
}
else
{
roll_str_move_x += 2 ;
}
}
else if(roll_str_move_dir == 1)
{
u8g2.setCursor(30-roll_str_move_x, 63);
u8g2.print(str);
if(roll_str_move_x == 0)
{
roll_str_move_dir = 0;
}
else
{
roll_str_move_x -= 4;
}
}
}
红外解码以及对网页的处理如下,通过不同数据改变请求的网页。
//红外读取解码
void irRead()
{
if (IrReceiver.decode(&ir_decode_results)) //解码
{
Serial.print("红外解码:0x");
Serial.println(ir_decode_results.value, HEX); //以16进制输出红外解码值
if((ir_decode_results.value == 0x36480002) || (ir_decode_results.value == 0xD410BC59)) //空调遥控 0x36480002 手机遥控 0xD410BC59
{
switch_state = 0;
u8g2.setFont(u8g2_font_weather); //设定字体
u8g2.setCursor(95, 43); //选定坐标
u8g2.print("OFF"); //OLED显示OFF
Serial.println("状态:OFF"); //串口输出OFF
server.close();
server.on("/", handleRoot);
server.begin();
}
else if((ir_decode_results.value == 0x36480000) || (ir_decode_results.value == 0x8F0C8B3A)) //空调遥控 0x36480000 手机遥控 0x8F0C8B3A
{
switch_state = 1;
u8g2.setFont(u8g2_font_weather); //设定字体
u8g2.setCursor(95, 43); //选定坐标
u8g2.print("ON"); //OLED显示ON
Serial.println("状态:ON"); //串口输出ON
server.close();
server.on("/", handleRoot);
server.begin();
}
IR.resume(); //继续接收下一个值
}
}
六、难点及注意要点
1.难点
- U8G2屏幕刷新和字符重叠问题,若字符重叠可以尝试写入屏幕数据后清除缓存。
- 网页遥控部分需要HTML、CSS和JavaScript基础。
2.注意要点
- 由于本次开发使用Arduino,在Arduino IDE中添加ESP32开发板需要较好的网络环境,一直刷不出来的话可以尝试使用手机热点。
- 由于U8G2自带的中文库不全,我自己建立了16像素高度的2500常用汉字的中文字库,建立方法可以参考这篇博客:https://blog.csdn.net/weixin_44395581/article/details/108608141
- 若设备管理器无法检测到开发板串口,可尝试安装CH340的驱动CH341SER.EXE。
七、未来计划
- 学习IDF开发环境,编译速度会比Arduino快很多,优化开发效率。
- 把板子所有功能用上,比如音频输入、运放、扬声器、FM模块这些。