esp8266时钟+天气+提醒(六)代码篇三

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. 字体缺失

在测试时,我们发现了一个意外的问题:有些汉字不能正确显示(主要是什么都不显示,被直接略过)。这是因为尽管我们的支持中文字符,但并不是所有的中文字符都包含在每个字体中。

这个其实是比较棘手的一个问题,因为我们并不能确定从服务器发给我们的数据里都会有什么字,这很难预判。

解决方案包括:

  1. 使用更高级的字库
  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引脚的按钮时,会立即跳转到提醒界面,用户体验大大改善。与此同时,假如反复按下按钮导致界面反复切换,也不会频繁发出请求,避免资源开销和请求受限。

未完待续

接下来讲述外壳的制作。

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值