采用 Seeed Studio 硬件以及 Seeed_GFX 库,构建电子纸面板,显示剩余的假期天数
扫描二维码
随时随地手机看文章
简介
电子纸显示屏,又称 EPD,具有一项使其独具特色的特性:只有在图像刷新时才会消耗电力。这一特性使它们非常适合用于展示变化缓慢或完全不变的信息。如今,它们被广泛应用于信息标识、物联网面板和电子价格标签中,既有黑白版本也有彩色版本。
利用这一特殊特性,在这个项目中,我将向您展示如何构建一个智能面板,该面板能够显示距离下一个公共假日或非工作日还有多少天。乍一看,这似乎是一个简单的想法,但随着我们继续推进,您会发现它融合了多种有趣的技术,每种技术都有其自身的小难题。
它是如何运作的
该面板的主要组成部分是一个 5.83 英寸的单色电子纸显示屏,由基于 XIAO ESP32-S3 Plus 模块的 EE04 板控制,所有这些产品均来自赛德工作室。
如前所述,该面板的主要目的是显示距离下一个公共假期还剩多少天。要实现这一目标,系统需要完成几个中间步骤:
•了解当前日期:确定“今天”是哪一天。
•获取公共假日列表:获取本年度的公共假日信息。
•找出最近的那个:计算每个节假日距当前日期的天数差,然后选择天数差最小的那个。
•在电子显示屏上展示结果:将图形与文字相结合,以一种吸引人的方式呈现信息。
让我们仔细看看每一个步骤。
ESP32-S3 内置了一个内部实时时钟(RTC),能够记录当前的日期和时间。然而,EE04 板子没有配备备用电池,因此当电源被切断时,这些信息就会丢失。为了解决这个问题,我们将利用 ESP32-S3 的连接功能,并在每次面板开机时从互联网上的 NTP 服务器获取正确的时间。
一旦从 NTP 服务器获取到当前时间,我们就会利用该时间来更新 ESP32-S3 的内部实时时钟。
这些节假日数据可以被直接固化到脚本中,作为固定的数据表,但那样的话这个项目就会局限于一个国家,或者最多只能针对一小部分国家。在本项目中,我们会更进一步,使其具有国际性。为此,我们将使用纳格.日期(Nager.date)这一免费服务,它通过一个 API 提供了超过一百个国家的公共节假日列表,以 JSON 格式呈现。这些数据是通过 HTTPS 的 GET 请求获取的。
在下载完假期日期后,我们将通过将这些日期与以纪元格式表示的当前日期进行比较,来确定其中哪一个是最接近的日期。
最后,我们将使用 Seeed_GFX 库将结果显示在电子纸屏幕上,该库针对这种低功耗显示屏进行了优化。
如果之前描述中使用的某些术语听起来比较陌生,别担心。在接下来的几个部分中,我会逐一进行详细解释。
NTP(网络时间协议)
网络时间协议是一种通信协议,它能让设备连接到所谓的“NTP 服务器”。这些服务器遍布全球,能够提供极其精确的时间信息,通常精度可达毫秒级别。
NTP 服务器采用的是协调世界时(即全球统一时间)这一标准时间标准。然而,每个国家(在某些情况下甚至同一国家的不同地区)所使用的本地时间都可能与协调世界时有所不同。
这种情况之所以会发生,是因为地球被划分为多个时区,每个时区都是相对于协调世界时(UTC)设定的偏移时间。例如,在阿根廷,我们使用的是 UTC-3 时区,这意味着我们的当地时间比协调世界时晚三个小时。如果协调世界时是上午 11 点,那么这里的当地时间就是上午 8 点。
同步时钟
到目前为止,一切看起来都还不错——但我们在“下一个假期”小组讨论中究竟该如何运用它呢?
好问题。要在 Arduino 中从 NTP 服务器获取当前时间,我们可以使用一个非常有用的库——“time”,其中包含一个名为“configTime()”的函数,它负责整个过程。
“configTime()”函数的使用方式如下:
gmtOffset_sec:您所在地时间与协调世界时之间的时差,以秒为单位表示。该值通过将时区乘以 3600 得到。
日光节约时差(秒数):这是针对夏令时的额外调整,同样以秒为单位来表示。
ntpServer:您想要查询的 NTP 服务器。
当然,在尝试连接到 NTP 服务器之前,主板必须先连接到 Wi-Fi 网络。我假设您已经知道如何进行操作,所以这里就不详细说明了。
以下示例会连接到 Wi-Fi 网络,并从一个 NTP 服务器获取当前时间:
阅读时钟
ESP32 的内部实时时钟可以通过调用 getLocalTime() 函数来读取,该函数将一个 tm 结构体作为参数传递:
这种结构具有以下格式:
请注意以下几个方面:
年份字段(tm_year)存储的是自 1900 年以来的年数。因此,其值并非 2026,而是 126。
月份字段(tm_mon)的起始值为 0(即 1 月 = 0)。
对于一周中的每一天(tm_wday),周日的值为 0 。
通过我们目前所讲的内容,我们现在可以使用与 NTP 服务器所使用的原子钟精度相同的精度来设置 ESP32 的内部实时时钟。
现在,由于我们的目标是将那个实时时钟中存储的日期与公共假日日期进行比较,所以我们需要一种简单且可靠的日期比较方法。这就是“纪元格式”发挥作用的地方。接下来您将会看到,它使得这个过程变得非常简便。
“EPOCH 格式”
我们如何比较两个日期以确定,比如哪一个是先发生的,哪一个是后来发生的呢?
如果一侧是 2026 年 2 月 10 日,另一侧是 2025 年 11 月 8 日,我们需要一种方法来简单地确定它们的先后顺序。
实现这一操作的一种非常简单的方法是将两个日期都转换为“纪元”格式,然后将它们作为数字进行比较。
“纪元时间”于 20 世纪 70 年代作为 Unix 操作系统的一部分被引入。其理念十分精妙:将日期表示为自一个固定参照点以来所经过的秒数,该参照点被定义为 1970 年 1 月 1 日 00:00:00(协调世界时)。
计算这个数字并非易事,因为这需要考虑到每个月的天数、每天的小时数、每小时的秒数,以及还要考虑闰年的情况。
例如,使用像 epochconverter.com 这样的纪元计算器,您会得到以下这些数值:
2026 年 2 月 10 日 00:00 → 1770681600
2025 年 11 月 8 日 00:00 → 176256000
比较这两个数值,我们可以看出 1762560000 小于 1770681600,因此它所对应的日期要更早一些。
转换为纪元时间
在 Arduino 中,时间库包含了 mktime() 函数,该函数可将日期转换为纪元格式。
`mktime()` 函数要求日期信息存储在 `tm` 结构体中,就像我们之前用于读取 ESP32 实时时钟时所使用的结构体一样。
以下示例将之前的一个日期转换为“纪元”格式,并打印出结果:
在这个示例中,我将与时间相关的字段设为 0,以使日期与午夜时间相匹配。我还特意将月份设为 2 - 1,以此提醒您,1 月对应的是 0。对于年份,我使用了 2026 - 1900,这样您就能记住这个字段存储的是自 1900 年以来的年数。
运行代码后,串口监视器显示了如下结果,这与我们之前通过“纪元转换器”网站计算出的结果完全一致。
HTTPS 请求
我们之前使用的 NTP 协议是一种用于访问互联网服务器的方式,其作用是获取时间及日期信息。
还有许多其他协议用于交换不同类型的数据。其中最常见的一种是 HTTP,它使我们能够访问网站、从网站获取数据,甚至还能将信息发送回去。
HTTP 协议
HTTP 于 20 世纪 90 年代初与网络一同诞生,它规定了用户与服务器之间数据传输的方式。它遵循客户端-服务器模式,其中每一方都有明确的角色:服务器存储信息,而客户端请求信息。
每次我们打开网页时,实际上都是在使用 HTTP 协议。浏览器充当客户端,向服务器发送请求。如果一切顺利,服务器会根据请求返回所需的数据——通常是一份包含各种元素的页面——这就是所谓的响应。如果出现问题或者信息不存在,服务器就会返回错误消息,比如大家熟知的 404 未找到这类消息。
要使这种通信得以进行,客户端必须知晓网址,即服务器在互联网上的地址,并且还必须告知服务器它想要执行何种操作。为此,HTTP 定义了多种方法,但其中最常见的有两种:GET 和 POST。
GET:客户端告知服务器其希望接收信息。
POST:客户端告知服务器其希望发送信息。
虽然 GET 方法经常被使用,但只要我们需要向服务器发送数据, POST 方法就是更恰当的选择。
加强安全措施
HTTP 单独使用时是不安全的。如果第三方拦截了客户端与服务器之间的通信,那么传输的数据很容易被读取。想象一下用信用卡进行购物——任何攻击者都可能窃取这些敏感信息。
出于这个原因,原始的 HTTP 协议得到了扩展,从而产生了 HTTPS。正如您可能猜到的那样,额外的“S”代表“安全”。HTTPS 就是 HTTP 加上了一层额外的安全防护层,它提供了两个关键特性:
加密:与仅以明文形式传输数据的 HTTP 不同,HTTPS 会对信息进行加密处理,这样一来,即便数据被截获,也无法被利用。
认证:服务器会出示一份数字证书,以证明其身份。这能确保客户端与的是真正的网站,而非冒名顶替者。
如今,大多数服务器都使用 HTTPS 而非普通的 HTTP 协议。实际上,很多网址都会以“http://”开头,但一旦访问该网站,它会自动将客户端重定向至其安全的“https://”版本。
使用 Arduino 访问服务器
现在让我们来看看如何使用 Arduino 语言从 ESP32 访问使用 HTTPS 进行信息交换的服务器。为此,我们有两个非常有用的库可供使用:
WiFiClientSecure:通过 HTTPS 建立与服务器的连接。它负责处理加密和证书事宜。
HTTP客户端:实现了诸如 GET 和 POST 等 HTTP 方法。它既可以与 HTTP 协议配合使用,也可以与 HTTPS 协议配合使用。
让我们来看下面这个示例,它会建立一个 HTTPS 连接并执行一个 GET 请求。为了简化流程,并且因为我们已经知道要连接的服务器,所以我们将不使用证书。
如果一切运行正常且没有出现任何问题,您应该会在“串行监视器”中看到类似这样的内容:
第一条消息表示已成功建立 Wi-Fi 连接。接下来会显示 HTTP 状态码,此处为 200。这与您在浏览器中有时会看到的代码类型相同,比如 404(未找到)或 403(禁止访问)。状态码 200 表示一切运行正常。
接下来,该示意图展示了服务器作为响应返回的字节数(即“数据长度”),最后它还会打印出该响应的实际内容(即“数据”)。
仔细观察该回复所采用的格式。用 {} 符号括起来的部分是众所周知的 JSON 格式,它在物联网领域被广泛使用。我们稍后会更详细地探讨这一格式。
回到这段代码,让我们来剖析一下它是如何运作的。
首先,声明了“url”变量。该变量包含了我们将向其发送请求的网站的地址。在本示例中,我选择了 httpbin.org,这是一个常用于测试的网站。
接下来,该示意图会连接到无线网络。请记得使用您自己的网络名称和密码。
随后,客户端对象由 WiFiClientSecure 类创建而成。此对象随后将用于与服务器建立 HTTPS 连接。下一行内容非常重要:
“setInsecure()”这一方法指示客户端对象不要验证服务器的数字证书。这并不意味着连接不再进行加密:数据仍然以安全的方式传输,但客户端不会验证服务器的身份。
为了完成此声明阶段,会从 HTTPClient 类中创建一个 http 对象,该对象将用于向服务器发送请求。
现在是采取行动的时候了。接下来这一段代码会准备 HTTPS 请求,并检查其是否能正确初始化,然后再将数据发送给服务器。如果出现任何问题——比如没有网络连接或者 URL 无效——它就会打印出错误信息,等待几秒钟以避免立即重试,然后终止程序。
如果此初始化过程没有出现错误,那么请求最终就会被发送至服务器。http 对象的 GET() 方法会发送该请求,并返回 HTTP 响应状态码,该状态码会被存储在 httpCode 变量中。
正如我们之前所见,这个状态码向我们表明了请求的结果。如果一切顺利,其值将会是 200。如果出现任何问题,它将会返回一个不同的值。
接下来,该代码会进行精确的验证:确认请求是否成功,并且是否返回了 200 状态码(此处使用了常量 HTTP_CODE_OK 来替代,其值与之相同)。
如果出现错误,会打印出响应的前 200 个字符(因为响应可能很长)——然后使用 end() 方法关闭连接,程序也随之停止。
如果没有错误,就会使用 http.getString() 方法读取服务器响应,然后关闭连接,并显示响应内容及其长度。
必须明确区分状态码和响应体本身。前者告诉我们请求是成功还是失败;后者则包含了服务器返回的实际信息。
响应的格式取决于服务器。在这种情况下,它以 JSON 格式进行回复,所以接下来的几段内容我会解释一下这个格式的具体含义。
JSON(JavaScript Object Notation)
JSON 表示“JavaScript 对象表示法”。这是一种用于通过纯文本交换信息的标准格式,它具有轻量级、便于人类阅读以及便于程序处理的特点。
它在互联网中被广泛用于连接应用程序和网络服务,在物联网项目中也十分常见,因为在这些项目中,设备和服务器需要以简单且高效的方式交换数据。
对象与数组
以 JSON 格式编写的文本或文件由一个或多个对象组成。一个对象包含属性,这些属性是以冒号(:)分隔的键值对。
例如,以下就是一个包含单个属性(名称)的对象:
以下是一个包含三个属性(姓名、年龄和身高)的另一个对象:
如您所见,每个属性都由逗号隔开,而整个对象则被括号包围。
我们如何将信息以一种方式存储在 JSON 文件中,以便供多人使用呢?答案是:通过使用对象数组来实现。
这个 JSON 结构是一个数组,用方括号 [ ] 包围着,其中包含两个对象。每个对象都有三个属性。
更一般地说,我们可以说一个 JSON 文件是由对象或者对象数组组成的。
ArduinoJSON
在 Arduino 中有多个用于处理 JSON 文件并操作其内容的库。其中最受欢迎的一个是 ArduinoJSON。
ArduinoJSON 的突出特点是其轻量、高效,并且特别适合资源有限的微控制器,比如 ESP32。除此之外,它还允许您:
读取(解析)从互联网或内存中获取的 JSON 数据
轻松访问对象和数组
创建 JSON 格式的数据以发送至服务器
既能处理小型文件,也能处理更复杂的结构
优化内存使用,这在微控制器中至关重要
由于这些特性,它已成为物联网项目中的实际标准,而在这类项目中基于 JSON 的数据交换极为常见。
您可以在 Arduino IDE 的库管理器中轻松安装最新版本的 ArduinoJSON。
序列化
序列化是指将内部数据或程序变量转换为 JSON 文本格式的过程。
借助 ArduinoJSON,这一操作可以非常轻松地完成。请看下面的示例:
在上述代码中,第一步是引入 ArduinoJson.h 库。然后,通过 JsonDocument 类创建了一个 doc 对象。这个对象将用于存储后续将转换为 JSON 格式的值。
值是以一种非常简单的方式被加载到对象中的,方法是通过引用每个键来实现的,如下所示:
最后,声明了一个字符串变量来存储 JSON 文本,并将文档对象的内容序列化到该变量中。
可以通过将字符串打印到串口监视器中来查看结果:
该字符串已经是 JSON 格式,因此可以发送到互联网上的服务器、保存到 SD 卡中,或者按照您的项目需求以任何其他方式使用。
另一种方法是直接将 JSON 数据序列化为串行监视器中的内容,而无需将其存储在变量中。其示例如下:
这在“串行监视器”中产生的结果完全相同:
如果您希望将 JSON 数据以更易于阅读的格式呈现出来,可以使用 serializeJsonPretty() 方法:
这会产生如下所示的输出,该输出更易于阅读:
“JsonDocument doc”这一指令会创建一个对象,该对象用作后续在序列化过程中将值转换为 JSON 格式时的存储容器。此对象会占用内存资源。
这种声明被称为动态分配,它使得库能够自动为该对象管理内存分配。在拥有大量内存的微控制器(如 ESP32-S3)上,这通常不会成为问题,但在内存较为有限的板子上,这可能会成为一个问题。
如果所使用的微控制器的内存空间非常有限,那么通常使用静态分配方式并结合使用 StaticJsonDocument 会更为合适。例如:
在这个示例中,为文档对象预留了 128 字节的空间。这使您能够更精确地控制内存使用情况,但同时也需要对实际所需的字节数做出准确的估计。
反序列化
这是相反的过程:它会将一个以 JSON 格式编写的文本文件转换为内部变量。
在这个示例中,我们还从 JsonDocument 类中创建了一个 doc 对象。出于测试目的,我们将 payload 变量设置为一个以 JSON 格式表示的文本字符串。在实际项目中,这个 payload 通常会来自服务器。
“deserializeJson()”函数会将该文本转换为内部变量,并返回一个错误代码。
这个错误代码是来自 ArduinoJson 库的一个特殊对象,为了以可读的方式打印它,我们使用了 c_str() 方法。
如果未出现任何错误,该代码将获取与“name”和“age”这两个键相关联的值,然后将这些值输出到串行监视器中。
当您运行此示例时,串行监视器会显示:
在我们的下一个假期中,我们将向服务器发送一个请求,要求获取所有公共假期的列表,该列表将以 JSON 格式返回。我们的任务就是对这个 JSON 数据进行反序列化,并提取我们需要的信息——比如每个假期的日期——以便确定哪个假期离当前日期最近。
该代码
现在让我们来看看这个项目的代码以及其运作方式。其大致思路可以用下面的流程图来表示:
这就是该面板的完整代码。它的确比较长,但请不要担心。下面我们将逐部分对其进行详细讲解。
初始化
在开头的几行中,和往常一样,我们使用了 #include 指令来添加所有所需的库。同时还包含了文件 bitmap.h,其中包含了面板的背景图像。
为了控制电子纸显示器,我们将使用 Seeed_GFX 库,因为该库针对 EE04 板以及我们将要使用的显示屏进行了优化。
接下来,创建电子纸对象以控制显示,并定义连接 Wi-Fi 所需的凭证,即网络名称和密码。请记得将“yourSSID”替换为您的网络名称,将“yourPassword”替换为其密码。
然后,会定义与您的位置相关的两个数值,您需要根据您所居住的国家来更改这些数值。
时区就是您的时区,您可以在这个网站上找到它。国家部分包含纳格.日期服务所使用的国家代码,用于提供该特定国家的公共假日列表。可用国家及其对应的代码列表可以在这个页面中找到。
就我而言,时区设置为 -3,国家代码是 AR(阿根廷)。
初始化过程通过定义端点来结束。端点是指用于向网站发送请求的特定 URL。它是服务器内某个资源或服务的确切地址:不仅包括网站本身,还包括我们的应用程序向其发送数据或请求信息的精确位置。
根据纳格尔文档的描述,该端点的格式如下:
因此,它被定义为一个固定的字符串,该字符串包含年份(2026),然后之前几行中定义的国家代码会被与之连接起来。
函数定义
接下来,该代码定义了四个函数:
stringDateToEpoch:将日期字符串(例如“2026-2-16”)转换为 epoch 格式。
去除重音:将带有重音的元音替换为无重音的元音,并且将“ñ”替换为“n”,因为这些字符并非所使用的用于在屏幕上显示文本的字体所定义的字符。
sleepSeconds:使 ESP32 以深度睡眠模式运行指定的秒数。
连接WiFi:按照设定超时时间连接到无线网络。
在我们使用它们的过程中,我们会更仔细地研究一下这些功能各自的作用。
无线网络连接
在“setup()”函数内部,接下来要进行的操作是连接到 Wi-Fi 网络。这通过调用之前定义的“connectWiFi”函数来完成,该函数带有 30 秒的超时时间。如果连接成功,就会显示消息“WiFi 连接成功”。但如果在超时时间结束前连接未建立,ESP32 会进入低功耗模式 5 分钟(300 秒),以便能够重新启动并再次尝试连接。
同样的策略随后还会在其他可能导致错误情况的情形中被采用。
结束该代码块的返回语句实际上永远不会被执行,但为了完整性还是将其包含在内了。
环境参数设置(初始化)
接下来的步骤是初始化电子纸显示屏,清除屏幕上可能残留的任何内容,选择“Yellowtail_32”字体,显示“下一个公共假日还有几天”的字样,并加载背景图片。
背景图片是一个 648×480 像素的单色位图(与显示屏的尺寸相同),是通过人工智能生成的,然后经过编辑以调整一些细节。
要从代码中加载它,首先必须将其转换为一个.h 文件,该文件中包含了每个像素的相关信息。
对此有多种工具可供选择,但其中最完备且使用起来最简便的是一款由 Seeed Studio 开发的名为 SenseCraft HMI 的工具,而且它是免费的。
这就是我所采用的配置:
当您点击“生成标题”按钮时,我们需要包含的文件就会生成。该工具会始终在我们于配置中选择的文件名前加上字母“e”。
最后,我们需要在 Arduino IDE 中将此文件添加到我们的项目中:
该图像通过“drawBitmap”函数加载,其中指定了起始坐标(0,0)、包含图像定义的数组的名称(holidayBack)、图像尺寸(648 x 480)以及像素颜色(TFT_BLACK)。
请记住,要使 Seeed_GFX 能够与显示屏正常配合使用,必须先使用其在线配置工具生成 driver.h 文件。
RTC同步
在该代码的这一部分中,执行了与实时时钟以及日期/时间信息相关的多项任务。
第一步是使用一个 NTP 服务器作为参考来设置 ESP32 的内部实时时钟。这可以通过“configTime”指令来完成,正如我们之前所看到的那样。
接下来,会从实时时钟中读取日期和时间,并将其存储在“时间信息”结构中。如果在此过程中出现错误(就像之前那样),ESP32 将进入深度睡眠状态 5 分钟,然后重新启动。
如果一切运行正常,日期和时间会显示在串行监视器上作为检查内容,然后这些信息会在变量“epochNow”中转换为纪元格式,以便于后续处理。
接下来,该代码会计算从当前时刻到次日午夜还剩余多少秒。
这样做是为了确保在完成检查假期列表并显示结果的整个过程之后,ESP32 能够进入低功耗模式,直到那一刻。这样一来,面板每 24 小时会在午夜运行几秒钟来更新信息,其余时间则处于“休眠”状态,从而将功耗降至最低。
由于电子纸的双稳态特性,这些信息将一直保留在屏幕上显示着。
这种计算方法如下:
它始于两个完全相同的“tm”结构,这两个结构都包含日期和时间信息。tomorrow 初始时包含与 timeinfo 相同的数据,即当前的日期和时间。
然后“明天”的设定被修改了。小时、分钟和秒的数值都被设为 0,而日期则增加一天。这样一来,“明天”就变成了下一个午夜了。
然后,明天会被转换为“纪元”格式(在“epochTomorrow”中进行转换,此时“timeinfo”已转换为“epochNow”),接着计算两者之间(即“现在”与明天午夜之间)的秒数差。这就是之后将用于配置定时器的值,该定时器将“唤醒”ESP32 并将其从深度睡眠模式中唤醒。
最后,变量“epochToday”是根据“timeinfo”创建的,但其小时、分钟和秒的值都被设为 0,这样时间的计数就从当前日期的午夜开始,而不是从设备开启的那一刻开始。
这样做是为了在计算与节假日日期的差异时,当前这一天会被算作完整的一天,而如果节假日在次日发生,那么差异就会是一天。
与服务器的连接
在此,与服务器的 HTTPS 连接已建立,一切准备就绪,可以向纳格服务器发出 GET 请求了。
如果过程中没有出现错误,那么在流程结束时,变量“payload”中将包含服务器的响应内容,该内容即是我们之前指定的国家当年所有公共假日的相关信息。此信息采用的是 JSON 格式。
寻找下一个假期
我们差不多就要完成了。
在这一部分,我们会对服务器的响应进行分析,以确定接下来最临近的公共假期是哪一天。
正如我之前所提到的,该响应是以 JSON 格式呈现的,其内容大致如下所示。我仅展示了我国在 2026 年的部分节假日信息。
首先要注意的是,这段文本以方括号([ ])开头和结尾,这意味着我们正在处理一系列对象。每个对象代表一个公共假日,并包含若干个属性。最后一个属性“types”本身也是一个数组。
每个属性的含义在纳格尔文档中有详细说明。对于本项目而言,我们只关注前三个属性:
日期 — 该节日的日期
本地名称 — 以本地语言表述的节日名称
英文名称 — 以英语表述的节日名称
日期对于确定下一个假期至关重要。用当地语言表述的名称似乎是最适合在屏幕上显示的选项,但也有其不足之处。最后,如果我们要制作该面板的国际版本,那么英文名称则非常有用。
在探索此文件的内容之前,我们首先需要做的是对其进行反序列化,即将其从文本形式转换为内部变量。具体操作如下:
解序列化后,变量“doc”包含了所有的节假日信息。
现在是时候逐一来研究这些节日了。正如我们之前所看到的,在 C/C++ 中,这个数组的索引方式与其他数组一样。
例如,doc[0] 包含的是第一个对象,即:
对象内部的每个属性都可以通过其键进行引用,因此 doc[0]["date"] 的返回值为:
要遍历整个数组,我们可以使用如下的 for 循环:
“size”是一个方法,它会返回文档中包含的对象数量。在循环内部,会从每个对象中获取“date”和“name”这两个值,并将其显示在串行监视器上。
在同一个 for 循环中,会计算 epochDif 值,即假期日期与当前日期之间以“纪元”格式的差值。如果这个差值小于 0,这意味着假期已经过去,所以它不再具有意义。但如果差值大于或等于 0,则意味着假期仍在未来,或者实际上就是今天。
无论哪种情况,都会通过将 epochDif 中的秒数除以 86400(即一天的秒数)来计算两个日期之间的天数差,然后获取该节日的名称。接着,将天数以大数字的形式打印在 EPD 上,而节日名称则以较小的字体显示。
这里有一个小问题。在 Seeed_GFX 库中定义的字体并非国际通用字体,因此它们不包含带有重音符号或其他语言特定符号的字母。如果我们打印 localName 中包含的文本,那些字符将不会显示出来。
这个问题可以通过不同的方法来解决。你可以自行设计一种包含所需所有字符的字体,使用英文名称代替,或者在打印之前使用开头定义的“removeAccents”函数去除字符中的那些符号。
还有一个小问题:节日名称可能太长了。为了简化处理,我决定只打印前 38 个字符,这些字符是我所选字体下能完整显示在一行内的内容。理想的做法是编写一个函数,能够将文本分两行或多行显示出来,且不出现单词的断句,但我将把这个改进作为一项挑战留给您去完成。
在查找下一个即将到来的假期之前,先创建一个布尔变量“found”,并将其初始值设为“false”。如果找到了下一个假期,在更新屏幕后,该变量会被设置为“true”,然后程序将退出该 for 循环。
接下来,会检查“found”变量的值。如果其仍为“假”,则表示尚未找到即将到来的假期。这种情况可能发生在当年即将结束时,而日历中已没有剩余的假期了。在这种特殊情况下,程序会显示——剩余的天数以及文字“无更多假期!”
最后一步是为定时器编写程序,让 ESP32 在明天午夜时唤醒,然后进入睡眠状态。
该面板完成后的部分图片:
结论
我们已经完成了“下一个假期”智能面板项目。这是一个简单、实用且友好的构建方案,为我们提供了探索物联网项目中与网络服务器交互所使用的一些常见技术的绝佳机会:处理日期和时间、通过 HTTP 和 HTTPS 访问服务器、发出 GET 请求以及处理 JSON 数据。
所有这些知识在各种各样的项目中都将非常有用,而在这些项目中,您很可能会运用其中的一种或多种技术。
我们还学习了如何将电子纸显示屏与 Seeed Studio 的 EE04 控制板配合使用,如何使用不同的字体显示文本,以及如何通过 Seeed_GFX 库来展示位图图像,整个项目都采用了这种库。
本文编译自hackster.io





