esp8266时钟+天气+提醒(六)代码篇三
本篇中实现了提醒界面以及界面的瞬时切换,此外讲述了
- 字体缺失
- 延时函数与定时函数
- 忙等
等原理和方法,希望能够有所帮助。
八、提醒界面
1. 数据显示
上一篇中我们已经成功地在串口监视器中看到了数据,接下来我们要使其显示在界面上。
首先编写一个显示的函数:
void display_reminder(JsonArray array) {
u8g2.setFont(u8g2_font_unifont_t_chinese2);
u8g2.clearBuffer();
int height = 14;
for (JsonObject data_item : array) {
int data_item_id = data_item["id"]; // 提取id
const char* data_item_time = data_item["time"]; // 提取time
const char* data_item_content = data_item["content"]; // 提取content
// 在这里处理提取的数据,例如打印输出
Serial.print("ID: ");
Serial.print(data_item_id);
Serial.print(", Time: ");
Serial.print(data_item_time);
Serial.print(", Content: ");
Serial.println(data_item_content);
u8g2.setCursor(0, height);
u8g2.print(data_item_time);
u8g2.setCursor(50, height);
u8g2.print(data_item_content);
height+=16;
}
u8g2.sendBuffer();
}
这个函数接受了一个参数array,可以看到它的类型是JsonArray,是一个json数组。为什么是数组呢,回看我们返回的数据格式(第七章第四节中),返回的json对象里只有一个叫做data的键,它的值是数组,它里面装着我们需要的信息,所以直接处理这个数组就可以了。
所以,这个函数实际调用的方式是:
display_reminder(doc["data"].as<JsonArray>());
说回到display_reminder函数,在这个函数中大体实现了这样一个流程:设置字体、清屏、提取数组数据并显示。
接收到的数据至多只有四条,我们的显示屏也恰能显示四条。每一条height都递增16,因此刚好容纳。
接下来我们把这个函数放到reminder函数中使用:
void reminder() {
String reqRes = "/";
String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" + "Host: " + reminder_host + "\r\n" + "Connection: close\r\n\r\n";
Serial.println("");
Serial.print("Connecting to ");
Serial.print(host);
WiFiClient client;
// 尝试连接服务器
if (client.connect(reminder_host, 80)) {
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
Serial.println(httpRequest);
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
const size_t capacity = JSON_ARRAY_SIZE(4) + JSON_OBJECT_SIZE(1) + 4 * JSON_OBJECT_SIZE(3) + 230;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, client);
display_reminder(doc["data"].as<JsonArray>());
delay(5000);
}
}
2. 字体缺失
在测试时,我们发现了一个意外的问题:有些汉字不能正确显示(主要是什么都不显示,被直接略过)。这是因为尽管我们的支持中文字符,但并不是所有的中文字符都包含在每个字体中。
这个其实是比较棘手的一个问题,因为我们并不能确定从服务器发给我们的数据里都会有什么字,这很难预判。
解决方案包括:
- 使用更高级的字库
- 字体取模
方案一其实在专栏第二篇中有所涉及:
u8g2.setFont(u8g2_font_wqy16_t_gb2312);
使用龙泉驿点阵宋体16x16 点阵字库,这是更高级的字库,可以显示大部分中文字体。
方案二则是存在常用且确定要用的字无法显示时的解决方案。
3. 字体取模
字体取模是指将字体的图形转换为一系列的点阵数据(模)的过程。在这个过程中,每个字母或符号被分解成小的像素点,然后这些点阵数据被存储起来以便于后续的显示。这种方式特别适用于液晶显示(LCD)、LED显示屏或其他点阵显示设备,因为这些设备通过控制特定像素的亮暗来显示字符和图形。
字体取模能够显示文本和符号,通过预先生成和存储字符的点阵数据,可以有效地减少实时图形处理的需求,降低系统资源的消耗,使得在资源有限的嵌入式系统和小型设备上显示文本成为可能,使用这一方案的最大优势就在于节省资源。
字体取模工具可以自行上网搜素,注意设置字体的大小,因为这直接影响最终显示的效果,刚才我们所使用的都是16x16的,中文字体一般都是等边的。
生成的点阵数据需要按照特定的格式(如二进制、十六进制等)进行存储,以便于在目标显示设备上正确显示。下面是一个例子:
const unsigned char xing[] U8X8_PROGMEM = {
0x00, 0x00, 0xF8, 0x0F, 0x08, 0x08, 0xF8, 0x0F, 0x08, 0x08, 0xF8, 0x0F, 0x80, 0x00, 0x88, 0x00,
0xF8, 0x1F, 0x84, 0x00, 0x82, 0x00, 0xF8, 0x0F, 0x80, 0x00, 0x80, 0x00, 0xFE, 0x3F, 0x00, 0x00
}; /*星*/
上面将汉字“星”的点阵储存在一个常量里,而在显示时这样使用:
u8g2.drawXBM(80, 48, 16, 16, xing);
4. 按钮设置
接下来,我们将天气功能和提醒功能组合起来,首先设置两个中断:
// 天气中断
void IRAM_ATTR InterruptWeather() {
mode = 1;
}
// 提醒中断
void IRAM_ATTR InterruptReminder() {
mode = 2;
}
然后在setup函数中中断绑定:
attachInterrupt(digitalPinToInterrupt(D4), InterruptWeather, FALLING);
attachInterrupt(digitalPinToInterrupt(D3), InterruptReminder, FALLING);
提醒按钮连接D3引脚,阅读引脚说明表可知,D3引脚也是上拉输入的。因此我们选择下降沿触发,同时按钮另一端接地(注意消抖)。
这样一来,我们就可以通过按钮切换界面(非最终版):
void loop() {
if (mode == 0) {
clock_display(prevDisplay);
} else if (mode == 1) {
mode = 0;
TandW();
} else if (mode == 2){
mode = 0;
reminder();
}
}
但是,先前所说的瞬时切换界面仍未实现,接下来我们要解决这个问题。
九、延时与等待
1. 延时函数与定时函数
1.1 delay函数
delay函数用于使当前执行的程序暂停指定的毫秒数。在这段时间内,CPU将停止执行当前线程的后续指令,并在等待期间处理其他后台任务,如Wi-Fi操作和TCP/IP通信,这就是为什么获取天气和提醒的时候我们使用delay却没有影响后续联网操作。
delay(5000);
参数(单位)是毫秒,上面的效果就是等待5秒。
值得注意的是,delay是阻塞的,这使得响应性无法保证。简单来说,中断与delay的结合仍有极大的局限性。
那如何实现我们想要的效果呢,还有其他的方法。
1.2 millis函数
millis函数严格来讲并非延时函数,而是定时函数,它返回自程序开始执行以来经过的时间,单位为毫秒。这个函数非常有用,尤其是在需要非阻塞延时或计时的场景下。与delay函数不同,它不会阻塞程序的执行。
使用方式通常如下:
unsigned long currentMillis = millis();
currentMillis是一个无符号长整型,记录从程序开始以来经过的时间。
但无符号长整形也有上限,它总有溢出的时候。它会在大约 49.7 天后溢出(回绕到 0)。
这个函数能实现下面的效果,例如修改获取天气的函数(非最终版):
//----------用于获取实时天气的函数(0)----------
void TandW() {
// 获取当前毫秒数
unsigned long currentMillis = millis();
Serial.println(currentMillis);
Serial.println(previousMillis);
if (previousMillis >= 5000 && currentMillis - previousMillis <= 15000) {
// 用原有数据显示,不再发送请求
display_0(results_0_now_temperature_int_old, results_0_now_text_str_old);
delay(1000);
return ;
}
previousMillis = currentMillis;
String reqRes = "/v3/weather/now.json?key=" + reqUserKey + +"&location=" + reqLocation + "&language=en&unit=" + reqUnit;
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes, 0);
//延迟,需要低于20次/分钟
delay(5000);
}
它最大的特点在于可以避免重复发请求,因为重复的请求一方面带来了资源开销(对服务端和客户端都是),另一方面可以避免请求限制,例如心知天气的API就要求每分钟不得超过20次。
在函数执行的时候获取当前的毫秒数,如果毫秒数大于5000且当前毫秒数currentMillis与上次毫秒数previousMillis之差小于等于15000时,就直接显示原有数据,不再发送请求。
而如果上面的条件不满足,就更新previousMillis,再发请求、显示新数据。
现在再分析一下这个判断条件:
如果毫秒数小于5000,那就说明从未发起过请求,储存的天气数据应当是默认的0,所以应该发请求。
而如果这个差值大于15000,说明时间间隔足够大,可能信息有更新,所以也应该发请求。
2. 忙等
2.1 忙等
借助millis函数,我们可以避免无意义的重复请求,但这不能实现我们瞬时的界面切换,而忙等则是一种常见的解决方案。
对单片机了解的朋友们应该对这个比较熟悉:忙等待(Busy Waiting),也称为自旋锁定(Spinlocking),是一种编程技术,其中一个进程或线程在等待某个条件成立时重复检查该条件,而不进行休眠或让出CPU控制权。这种方法通常用于同步或在等待硬件操作完成时保持程序的控制流。
既然如此,忙等就具有以下特点:
- 高CPU占用:忙等过程中并没有休眠,CPU在持续运作。
- 响应速度快:也正因为没有休眠,忙等可以立即响应状态的变化,特别是中断操作。
- 实现简单:忙等不涉及操作系统层面,而像中断这样的任务调度都是在操作系统层面实现的,尽管单片机可能不具备操作系统。
接下来用代码演示忙等的效果。
2.2 实现
unsigned long i = 4294967295;
while(i>0){
i--;
}
上面是计数忙等,首先预设一个比较大的数,然后在while循环中使其递减,实现一个等待的效果。
这种操作在stm32系列中比较常见,除此之外还有一种忙等:
bool flag = false;
while(!flag){
}
这种称为标志忙等, 预设的是一个布尔值,它作为一个标志位、一个循环检测的边界条件。
相当简单,无需多言,一看就懂。
此外定时器延时不在此列,就不一一道来了。
忙等的最大缺陷就是功耗问题。
2.3 中断设计
前面说到,delay函数是阻塞的,与中断难以配合,而忙等响应速度快,可以立即响应中断,那么把它们结合起来,会有怎样的效果呢?
观察2.2中的计数忙等,在while循环中我们除了i--,没有任何多余的操作,但是我们可以主动在其中添加内容,例如delay函数(非最终版):
int i = 50;
while (i > 0) {
delay(100);
i--;
}
这段代码实现了循环50次,每次延时100毫秒,整体来看近似于延时了5000毫秒,即5秒。
之所以说是近似,是因为delay(5000)和上面的50次delay(100)还是有所区别的,毕竟while条件判断和递减操作都是需要时间的,尽管它们极其短暂。
现在我们假设程序运行到了某一次delay(100),这时这个延时操作还未结束,假设延时了40毫秒,还剩60毫秒,突然中断触发了。
于是任务调度,先“暂停延时”,去执行中断函数,就是将mode赋一个值,赋值完毕后恢复现场,再继续延时,去走完剩下的60毫秒,然后i--,然后进入下一次循环。
而我们的目的是界面瞬时切换,已知:
- mode=0时,下一屏是时钟界面
- mode=1时,下一屏是天气界面
- mode=2时,下一屏是提醒界面
对于时钟界面时间刷新,其实就是一屏一屏连续的时钟界面,每一屏的分秒都是不同的,一直保持前进。
可见mode其实反映的是下一屏应当是什么界面。
那么就有解决方案了,注意到loop函数中:
void loop() {
if (mode == 0) {
clock_display(prevDisplay);
} else if (mode == 1) {
mode = 0;
TandW();
} else if (mode == 2) {
mode = 0;
reminder();
}
}
如果mode不为0,那么第一件事就是将其置为0,然后进行天气或者提醒操作,于是我们可以更改刚才的延时函数:
int i = 50;
while (mode == 0 && i > 0) {
delay(100);
i--;
}
如果mode不为0,说明下一屏不是时钟界面,是需要立即跳转的,所以while循环结束,后续的延时不再执行,就可以实现立即跳转(尽管可能会有几十毫秒的延迟,但这是可以接受的)。
于是,我们改进了我们的天气函数TandW:
//----------用于获取实时天气的函数(0)----------
void TandW() {
// 获取当前毫秒数
unsigned long currentMillis = millis();
if (previousMillis >= 5000 && currentMillis - previousMillis <= 15000) {
display_0(results_0_now_temperature_int_old, results_0_now_text_str_old);
} else {
previousMillis = currentMillis;
String reqRes = "/v3/weather/now.json?key=" + reqUserKey + +"&location=" + reqLocation + "&language=en&unit=" + reqUnit;
// 向心知天气服务器服务器请求信息并对信息进行解析
httpRequest(reqRes, 0);
}
//延迟,需要低于20次/分钟
int i = 50;
while (mode == 0 && i > 0) {
delay(100);
i--;
}
}
这样就实现了我们的目标:当我们按下D4引脚的按钮时,切换到天气界面,当我们按下D3引脚的按钮时,会立即跳转到提醒界面,用户体验大大改善。与此同时,假如反复按下按钮导致界面反复切换,也不会频繁发出请求,避免资源开销和请求受限。
未完待续
接下来讲述外壳的制作。