外观
2.1 环境搭建
本项目的构建环境为VsCode、PlatformIO完成,当然也可以使用JetBrains CLion 2024、PlatfromIO或Arduino完成,根据个人喜好选择即可。在本教程中主要使用VsCode作为编译器开发
2.1.1 VsCode安装
方法1:官网下载
VsCode是一个强大的主流代码编辑器,也是完全免费使用的,我们可以在VsCode官网下载安装即可 https://code.visualstudio.com/insiders/
方法2:应用商店下载
这种方法更为简便,您可以使用Windows自带微软商店搜索并下载
2.1.2 PlatfromIO框架安装
PlatfromIO是一个更强大更专业的开发框架,支持ARM、FPGA、RISC-V以及8位单片机的开发,是一个嵌入式多平台开发工具,可以支持Arduino、ESPIDF、STM32Cube等多个固件的开发,且自定义程度较高、支持开源库的快速安装使用,非常方便。 在VsCode点击左侧插件图标,搜索安装PlatfromIO框架插件即可,使用ArduinoIDE也可直接用ArduinoIDE完成开发,但ArduinoIDE功能有限,修改库函数和板级配置困难,所以不推荐使用
2.1.3 程序下载说明
ESP8266模组主要使用TTL串口下载方式,准备一个USB转TTL或者DAP-Link即可
(1)安装CH340驱动
鼠标右键以管理员身份进行安装; 如果出现安装失败,那可能是你电脑已经有了该驱动,你可以尝试先点击卸载,再次安装即可。
(2)硬件配置
在硬件设计中,我们特意为主板设计了下载模式跳线接口,使用跳帽或镊子短接后再上电,即可进入下载模式进行程序下载。然后再将主板上的TX连接到烧录器的RX,主板上的RX连接到烧录器的TX(RX、TX交叉连接) 如果成功进入下载模式,此时在VsCode左下角应该有端口选择按钮,点击选择能看到端口
2.2 流程设计
在开发之前,我们先要对整个程序的流程进行设计,确保知道我们的项目需要做什么
2.2.1 整体程序分析
对于本项目来说,我们应该实现表情显示与切换、时钟显示、天气信息显示和Bilibili信息显示并通过长按或短按按键完成交互,大致的程序流程如下 当然,这仅局限于本教程,你也可以为你的小电视开发更多好玩有意思的功能
2.3 创建项目
2.3.1 构建项目框架
首先,我们新建一个PlatformIO项目,修改好项目名称,版型选择NodeMCU中的NodeMCU 1.0,使用Arduino固件然后点击创建。(版型的选择是随意的,主要是选对正确的芯片类型,因为ESP8266有多种款式,我们使用的类型是ESP8266,80MHZ,4M)
2.3.2 项目结构介绍
(1)项目文件夹
在项目文件夹下共有5个文件夹,这些文件夹我们在后面介绍。我们先来讲解该目录下最为重要的platformio.ini文件,这个文件是用于存放我们需要调用的第三方库和配置板级配置的文件,在这个文件中可以配置分区、配置处理器频率、固件类型等等 platformio.ini 默认的文件内容如下,platform指的是我们使用的平台,board指的是我们使用的版型,framework指的是我们使用的固件类型,完整的文件配置信息可以查看官方的说明文档 https://docs.platformio.org/en/latest/projectconf/index.html
(2).pio文件夹
.pio文件夹是用于存放我们依赖的第三方库文件以及构建后的项目固件及构建文件的地方,默认文件如下 build 用于存放构建文件,生成的固件也会在这个文件夹中 libdeps 当引入第三方库时才会生成该目录,在该目录下可以修改第三方库的源代码
(3)src文件夹
其他文件夹就不再讲解了,主要是用于分类和自行导入第三方库用的。 src文件夹是生成初始程序的文件夹,在新建的项目中会在这里生成main.cpp,这个就是我们的主程序了
2.3.3 程序结构介绍
这就是一个基本的Arduino程序结构,在程序最上面导入头文件和全局变量。 setup() 在这个方法中我们主要存放需要初始化的代码,在该方法中的所有程序仅在开机时执行一次 loop() 在这个方法在我们主要存放主代码,在该方法中的所有程序会进入死循环,无限次重复执行
2.4 驱动库
2.4.1 屏幕
TFT_eSPI 是一个用于 Arduino 和 ESP8266/ESP32 的图形库,主要用于驱动 TFT LCD 显示屏。它是一个高性能的库,具有良好的灵活性和兼容性,适用于多种不同的显示屏和驱动芯片。 主要特点 1.高性能: 使用 SPI 通信,可以快速地绘制图形和文本,适合需要流畅更新的应用。2.灵活性: 支持多种 TFT 显示屏,用户可以根据需要进行配置。3.丰富的功能: 提供了绘制点、线、圆、矩形、填充图形、文本显示等多种基本图形绘制功能。 支持位图显示,可以将图像加载到显示屏上。4.颜色支持: 支持 16 位颜色模式,允许丰富的图形和图像显示。
(1)TFT-eSPI驱动库导入
TFT_eSPI 是一个用于彩色 TFT 屏幕显示库,它支持使用 SPI 协议的显示屏,可以通过该库在屏幕上绘制各种形状、文本、图像等。TFT_eSPI 库的配置文件在 User_Setup.h 中,可以指定屏幕型号、通信引脚等。 这类SPI彩屏主要使用TFT-eSPI库开发完成,要使用TFT-eSPI库,我们可以直接在platformio.ini中最下面添加
lib_deps =
bodmer/TFT_eSPI @ ^2.5.43
1
2
2
然后我们点击左侧PlatformIO的图标,选择Dependencies,点击Update更新依赖
现在platformio会自动拉取第三方库的git仓库到项目
(2)驱动适配
因为我们使用的屏幕是240*198的异形屏,所以我们必须要修改库函数。
配置SPI连接
1.打开.pio/libdeps/nodemcuv2/TFT_eSPI文件夹2.打开该目录下的User_Setup.h头文件3.将配置修改如下
#define USER_SETUP_INFO "User_Setup"
#define ST7789_DRIVER // 驱动芯片
#define TFT_RGB_ORDER TFT_BGR //显示序列
#define TFT_WIDTH 240 // 屏幕宽度
#define TFT_HEIGHT 198 //屏幕高度
#define TFT_BL 4 //背光引脚
#define TFT_BACKLIGHT_ON HIGH //背光启用
#define TFT_MOSI 13 //MOSI引脚配置
#define TFT_SCLK 14 //SCLK时钟引脚
#define TFT_CS 15 //片选信号
#define TFT_DC 5 //数据控制引脚
#define TFT_RST -1 //重置引脚
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SMOOTH_FONT
#define SPI_FREQUENCY 40000000 //SPI频率
#define SPI_READ_FREQUENCY 20000000 //SPI读取频率
#define SPI_TOUCH_FREQUENCY 2500000//SPI触控频率
#define USE_HSPI_PORT //启用硬件SPI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
屏幕尺寸适配
在ST7789_Defines.h中添加198x240的屏幕显示定义
#if (TFT_HEIGHT == 198) && (TFT_WIDTH == 240)
#ifndef CGRAM_OFFSET
#define CGRAM_OFFSET
#endif
#endif
1
2
3
4
5
2
3
4
5
屏幕旋转定义
在ST7789_Rotation.h中
case 2: 下添加
else if(_init_height == 198)
{
colstart = 0;
rowstart = 122;
}
case 3: 下添加
else if(_init_height == 198)
{
colstart = 122;
rowstart = 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
(3)屏幕代码初始化
在main.cpp中最顶部添加
#include <TFT_eSPI.h>
#include <SPI.h>
TFT_eSPI mylcd = TFT_eSPI();
1
2
3
2
3
在setup()中添加
mylcd.init();
mylcd.setRotation(0);
mylcd.fillScreen(TFT_BLACK);
1
2
3
2
3
2.4.2 NTP
NTPClient 是一个用于在 Arduino 和其他嵌入式设备上获取网络时间的库。它通过网络时间协议(NTP)从互联网的 NTP 服务器获取当前时间,适用于需要精确时间戳的项目,比如时钟、日志记录或定时器等。 主要特点
1.简单易用:
提供直观的 API,使得获取和管理时间变得容易。
2.支持时区:
可以设置时区偏移量,以获取本地时间。
3.定时更新:
支持定期更新时间,确保时钟保持准确。
(1)NTPClient库导入
lib_deps =
arduino-libraries/NTPClient @ ^3.2.1
1
2
2
然后我们重新加载
(2)NTP初始化
NTPClient 库是一个简单的 NTP 客户端库,可以从 NTP 服务器同步时间。在这个项目中,ESP8266 使用 NTPClient 获取当前时间,用于显示或其他时间相关的功能。 在main.cpp中最顶部添加
#include <NTPClient.h>
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com", 8 * 3600, 60000);
1
2
3
2
3
2.4.3 Json格式提取
ArduinoJson 是一个用于在 Arduino 和其他嵌入式平台上处理 JSON 数据的轻量级库。它允许开发者轻松地解析、生成和操作 JSON 数据,非常适合用于处理来自网络请求的响应、配置文件或其他结构化数据。 主要特点
1.轻量级:
设计针对内存受限的环境,内存占用小。
2.易于使用:
提供直观的 API,简单的语法使得 JSON 的创建和解析变得方便。
3.支持动态和静态内存分配:
可以根据需求选择动态分配内存或使用静态数组,适应不同的使用场景。
4.文档生成:
支持生成和解析 JSON 文档,便于处理复杂的数据结构。
Json格式的读取与写入我们使用ArduinoJson库完成
(1)ArduinoJson库导入
lib_deps =
bblanchon/ArduinoJson @ ^7.2.0
1
2
2
然后我们重新加载
(2)ArduinoJson初始化
在main.cpp中最顶部添加
#include <ArduinoJson.h>
1
2.4.4 ESPWeb本地服务器
ESPAsyncWebServer 是一个为 ESP8266 和 ESP32 平台设计的异步 Web 服务器库。与传统的 Web 服务器库相比,它允许在处理 HTTP 请求时非阻塞地执行其他操作,适合于需要同时处理多个连接的应用。 主要特点
1.异步处理:
允许同时处理多个客户端连接,而不会阻塞主程序流。
2.简单的 API:
提供易于使用的 API,可以快速设置路由和处理 HTTP 请求。
3.支持多种 HTTP 方法:
支持 GET、POST、PUT、DELETE 等多种 HTTP 方法。
4.处理静态文件:
能够直接提供存储在 SPIFFS 或 LittleFS 文件系统中的静态文件(如HTML、CSS、JavaScript 文件等)。
5.WebSocket 支持:
支持 WebSocket,允许与客户端进行实时双向通信。
为了实现本地网页配网,我们需要一个Web本地服务器
(1)ESPAsyncWebServer库导入
lib_deps =
esphome/ESPAsyncWebServer-esphome @ ^3.2.2
1
2
2
然后我们重新加载
(2)ESPAsyncWebServer初始化
在main.cpp中最顶部添加
#include <ESP8266HTTPClient.h>
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
1
2
3
2
3
2.4.5 FS文件系统
FS(文件系统)在微控制器和嵌入式开发中用于管理和存储文件。特别是在Arduino、ESP8266和ESP32等平台上,文件系统可以用来读取和写入数据,便于保存配置、网页文件和其他信息。 常见的文件系统
1.SPIFFS(SPI Flash File System):
设计用于闪存设备,适合于较小的存储需求。
支持文件读取、写入、删除等基本操作。
在ESP8266和ESP32上常用。
2.LittleFS:
提供更好的耐用性和性能,相比SPIFFS在特定场景下更为高效。
支持原子性写入和可变大小的文件,适合更复杂的应用。
(1)FS库导入
在main.cpp中最顶部添加
#include <FS.h>
1
2.4.6 WIFI网络
ESP8266WiFi 是一个专为 ESP8266 微控制器设计的库,用于处理 WiFi 连接。它提供了简单易用的 API,使得开发者可以方便地连接到 WiFi 网络、管理连接、发送和接收数据等。
(1)ESP8266WiFi库导入
在main.cpp中最顶部添加
#include <ESP8266WiFi.h>
1
2.5 图像库
2.5.1 分辨率
图像中我们尽可能统一分辨率 图标分辨率:99x99 表情分辨率:200x100
2.5.2 取模
取模的软件有很多,本文档中我们主要使用lcd-image-converter来完成图像和字库 附上截取完成的表情图片合集: https://drive.weixin.qq.com/s?k=AFIAGQdxAAY5NTKYhgAVIA5gYlAGw 项目创建 选择New Image创建一个图像取模项目 任意设定一个名称
图像导入 点击Image,选择Import,导入需要取模的图片
取模设置 点击Options,选择Conversion
点击Preset选择Color R5G6B5
选择Image,点击Block size选择16bit,再点击Show Preview即可查看到取模后的数值
右侧文本框内的就是我们需要的取模后的值了
2.5.2 存储
现在我们在src中新建一个image.cpp
const uint16_t 在此处自定义名称[] PROGMEM= {
//在此处粘贴取模后的数值
};
1
2
3
2
3
附上取模完成的完整image.cpp https://drive.weixin.qq.com/s?k=AFIAGQdxAAYkNsqADIAVIA5gYlAGw
2.5.3 导入
#include "image.cpp"
1
2.6 字库
文字的取模同样使用lcd-image-converter软件完成
2.6.1 取模
项目创建 选择New Font创建一个图像取模项目
任意设定一个名称 文字输入 在右侧输入文字,字体和风格可自定义,然后点击OK即可
由于现在字体的背景色是白色,所以我们还需要反相一下,点击Font-Inverse
点击Conversion
和图像一样,点击Image
右侧文本框的就是我们需要的文字的值了
2.6.2 存储
现在我们在src中新建一个font.cpp
const uint16_t 在此处自定义名称[] PROGMEM= {
//在此处粘贴字体的数值
};
1
2
3
2
3
附上取模完成的完整font.cpp https://drive.weixin.qq.com/s?k=AFIAGQdxAAYZ4AE8BTAVIA5gYlAGw
2.6.3 导入
#include "font.cpp"
1
2.7 程序开发
现在,你已经完成了所有准备工作,开始编写程序吧。 为方便理解,本项目教程的程序并没有进行性能和安全优化,如果有需求可以自行修改。
2.7.1 库的导入
这个项目旨在使用 ESP8266 芯片,通过 TFT_eSPI 库在屏幕上显示图像、字体以及其他信息,同时可以联网并使用 NTP 客户端进行时间同步,HTTPClient 用于访问网络数据,ArduinoJson 用于解析 JSON 数据,ESPAsyncWebServer 用于创建 Web 服务器。·#include <TFT_eSPI.h> // 引入 TFT_eSPI 库,用于控制 TFT 屏幕
#include <SPI.h> // 引入 SPI 库,为 TFT 显示屏提供通信支持
#include <ESP8266WiFi.h> // 引入 ESP8266WiFi 库,管理 ESP8266 的 WiFi 连接
#include <WiFiUdp.h> // 引入 WiFiUdp 库,用于 UDP 通信
#include <NTPClient.h> // 引入 NTPClient 库,通过 NTP 协议同步时间
#include <ESP8266HTTPClient.h> // 引入 ESP8266HTTPClient 库,用于 HTTP 请求和通信
#include <FS.h> // 引入 FS 库,用于文件系统操作
#include <ArduinoJson.h> // 引入 ArduinoJson 库,用于 JSON 解析和生成
#include <ESPAsyncWebServer.h> // 引入 ESPAsyncWebServer 库,创建异步 Web 服务器
#include "image.cpp" // 引入图像文件,定义和存储项目中的图像
#include "font.cpp" // 引入字体文件,用于在屏幕上显示自定义字体
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2.7.2 全局变量与定义
在本项目程序中,我们需要使用这些全局变量,具体用处可以看代码注释 创建 TFT 屏幕对象
TFT_eSPI mylcd = TFT_eSPI(); // 创建 TFT 屏幕对象
1
TFT_eSPI 是控制 TFT 屏幕的库,通过 mylcd 对象可以实现显示图像、绘制文字、清屏等操作。 按键和长按检测相关变量
#define LONG_PRESS_DELAY 1000 // 长按阈值为 1000 毫秒(1 秒)
unsigned long pressStartTime = 0; // 按下时的时间戳
bool isLongPress = false; // 标记是否为长按
bool buttonPressed = false; // 标记按键是否被按下
int emojiState = 0; // 当前表情状态
1
2
3
4
5
2
3
4
5
LONG_PRESS_DELAY 定义长按的时间阈值,超过这个时间就视为长按。pressStartTime 记录按下按键的时间戳,用于判断按键按下的时长。isLongPress 标识当前操作是否为长按。buttonPressed 用来判断按键是否被按下。emojiState 用于记录表情的状态,可以在按键事件中切换。 初始化标志变量
static bool initialized = false; // 时钟是否已初始化
static bool initweather = false; // 天气是否已初始化
static bool initbilibili = false; // B站信息是否已初始化
1
2
3
2
3
这些变量标识是否已经成功获取时钟、天气和 B 站信息,避免重复初始化。 WiFi 热点名称
const char* AP_NAME = "配网WIFI"; // WiFi热点名称
1
AP_NAME 是 ESP8266 创建的 WiFi 热点的名称,设备可以通过连接该热点进行配置。 按键去抖动相关变量
volatile unsigned long lastDebounceTime = 0; // 上次按键去抖动时间
const unsigned long debounceDelay = 200; // 去抖延迟时间(200 毫秒)
1
2
2
NTP 客户端和时间同步
WiFiUDP ntpUDP; // 创建 WiFi UDP 对象
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com", 8 * 3600, 60000); // 创建 NTP 客户端
1
2
2
ntpUDP 用于网络时间协议(NTP)同步时创建 UDP 通信。timeClient 是 NTP 客户端对象,连接阿里云的 NTP 服务器("ntp1.aliyun.com")同步时间,时区设置为 UTC+8。 定义按键和页面变量
const int buttonPin = 2; // 按键引脚
volatile int currentPage = 0; // 当前页面编号
unsigned long total_time = 0; // 累计时间变量
1
2
3
2
3
buttonPin 是连接按键的引脚号。currentPage 用于标记当前显示的页面,按键可以切换页面。total_time 用于累积时间,可以用于定时功能。 时间相关变量
unsigned char hours = 0, mins = 0, secs = 0, flag = 1; // 时间变量
unsigned int hours_x=119, hours_y=119, mins_x=119, mins_y=119, secs_x=119, secs_y=119; // 时钟指针坐标
1
2
2
hours, mins, secs 用于记录小时、分钟和秒。hours_x, hours_y, mins_x, mins_y, secs_x, secs_y 定义了表盘指针的坐标,用于在屏幕上显示时钟。 天气和 B 站 API 相关 URL
const char* weatherAPI = "http://api.seniverse.com/v3/weather/daily.json?key="; // 天气 API URL 前缀
const char* bilibiliAPI = "https://api.bilibili.com/x/relation/stat?vmid="; // B 站 API URL 前缀
1
2
2
weatherAPI 是天气 API 的 URL 前缀,可以根据位置等参数进行扩展。bilibiliAPI 是 B 站 API 的 URL 前缀,用于获取用户的关注数和粉丝数等信息。 创建异步 Web 服务器
AsyncWebServer server(80); // 创建异步 Web 服务器,监听端口 80
1
server 是一个异步 Web 服务器对象,监听端口号为 80,允许客户端访问 ESP8266 提供的 Web 页面来配置或查看信息。 天气和 B 站信息变量
String temperature = ""; // 温度
String humidity = ""; // 湿度
String weather = ""; // 天气状况
String following = ""; // B站关注数
String follower = ""; // B站粉丝数
String useruid = ""; // 用户 ID
String cityname = ""; // 城市名称
String weatherapi = ""; // 天气 API 密钥
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
这些字符串变量用于存储天气和 B 站信息,例如温度、湿度、天气、粉丝数量等,便于在屏幕或 Web 页面上显示。 WiFi 信息文件路径
const char* ssidFile = "/ssid.json"; // 保存 WiFi 信息的文件路径
1
ssidFile 用于存储 WiFi 配置信息(如 SSID 和密码),保存到 ESP8266 文件系统中,以便下次启动时读取。 中断记录变量
#define LONG_PRESS_DELAY 1000 // 定义长按的时间阈值
unsigned long pressStartTime = 0;
bool isLongPress = false;
bool buttonPressed = false;
1
2
3
4
2
3
4
表情状态记录变量
int emojiState = 0; // 表情状态
1
2.7.3 初始化
首先,我们先完成一些初始化设定
void setup() {
//在此写入初始化逻辑
}
1
2
3
2
3
初始化串口通讯
Serial.begin(9600);
1
启动串口通信并设置波特率为 9600,这使得可以通过串口监视器查看调试信息。 初始化 TFT 屏幕
mylcd.init();
mylcd.setRotation(0);
mylcd.fillScreen(TFT_BLACK);
1
2
3
2
3
mylcd.init():初始化屏幕,确保其准备好显示内容。mylcd.setRotation(0):设置屏幕的显示方向。mylcd.fillScreen(TFT_BLACK):清空屏幕,将整个屏幕填充为黑色背景。 显示初始图像
mylcd.pushImage(mylcd.width() / 2 - (170 / 2), mylcd.height() / 2 - (29 / 2), 170, 29, lceda);
1
将 lceda 图像显示在屏幕中央。 mylcd.width() / 2 - (170 / 2) 和 mylcd.height() / 2 - (29 / 2) 计算图像的居中位置。 配置按键引脚和中断
pinMode(buttonPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(buttonPin), handleButtonPress, CHANGE);
1
2
2
pinMode(buttonPin, INPUT_PULLUP):将按键引脚设置为输入模式,启用内部上拉电阻。attachInterrupt(digitalPinToInterrupt(buttonPin), handleButtonPress, CHANGE):配置中断,当 buttonPin 上的信号发生变化时(按下或释放),会触发 handleButtonPress() 函数。 加载 WiFi 配置
loadWiFiConfig();
1
调用 loadWiFiConfig() 函数,从文件系统中加载保存的 WiFi 配置信息。 检查 WiFi 连接状态
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Starting captive portal...");
startCaptivePortal();
} else {
Serial.println("WiFi connected");
timeClient.begin();
}
1
2
3
4
5
6
7
2
3
4
5
6
7
如果设备未连接到 WiFi,则启动配网模式,调用 startCaptivePortal() 进行配网。如果 WiFi 已连接,则启动 NTP 客户端以同步时间。 延迟等待
delay(1000);
1
delay(1000) 为初始化后的延迟,确保屏幕内容完全显示。
2.7.4 BiliBili
这里选取的是BiliBili页面的功能代码,首先先执行一次清屏,确保上一页的内容已经擦除,然后再检查网络状态,网络连接成功时启动HTTPClient,向在线服务器发送GET请求并接收返回的JSON格式值,解析并存储起来。然后关闭HTTP请求,执行屏幕显示代码。 在屏幕显示中因为使用了图片,所以先使用mylcd.pushImage将头文件引入的image.cpp中的bilibiliimg图片输出到屏幕中心偏上位置,使用mylcd.width()获取屏幕宽度÷2再减去图片宽度÷2。同理依次输出图片和文字,然后再使用mylcd.drawString输出String类型的文字即可(当然使用其他的也可以,具体参考之前存储的值格式)。 最后,再来解释下为什么开头使用了initbilibili==false,这是为了这个页面的页面程序仅执行一次,这样可以避免屏幕闪烁,也可以让程序快速响应,相当于将动态页面转成了静态页面。 创建方法
void bilibili() {
//在此写入逻辑代码
}
1
2
3
2
3
检查初始化状态
if (initbilibili == false) {
1
initbilibili 变量用于标识是否已经成功获取过 Bilibili 数据。初始化完成后,数据会存储在全局变量中,避免每次都重新获取数据。 清空屏幕
mylcd.fillScreen(TFT_BLACK); // 清空屏幕为黑色
1
每次进入 bilibili() 函数时,首先将屏幕清空,确保上一次显示的内容不会干扰当前的显示。 检查 WiFi 连接状态
if (WiFi.status() == WL_CONNECTED) {
1
函数会检查设备是否已经连接到 WiFi 网络,如果未连接,则跳过后续步骤,避免请求失败。 创建 WiFi 客户端并设置
WiFiClientSecure client; // 创建 WiFi 安全客户端
client.setInsecure(); // 设置为不验证 SSL 证书
1
2
2
WiFiClientSecure 创建了一个支持 HTTPS 的客户端。client.setInsecure() 用于跳过 SSL 证书验证,因为某些自签名证书会导致验证失败。 发送 HTTP 请求
HTTPClient http; // 创建 HTTP 客户端对象
if (http.begin(client, bilibiliAPI + useruid)) {
1
2
2
HTTPClient 对象用于处理 HTTP 请求和响应。http.begin() 用于发起 HTTP 请求,构造的 URL 为 bilibiliAPI + useruid,即 Bilibili API 的完整 URL。 处理 HTTP 响应
int httpCode = http.GET(); // 执行 GET 请求
if (httpCode > 0) {
String payload = http.getString(); // 获取响应内容
Serial.println("JSON Response:");
Serial.println(payload); // 打印服务器返回的 JSON 数据
1
2
3
4
5
6
2
3
4
5
6
http.GET() 执行 HTTP GET 请求,返回的 httpCode 用于检查请求是否成功。如果 httpCode > 0 表示请求成功,payload 变量接收并存储服务器返回的 JSON 数据,随后将其打印到串口监视器中。 解析 JSON 数据
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
String following2 = doc["data"]["following"];
String follower2 = doc["data"]["follower"];
1
2
3
4
2
3
4
DynamicJsonDocument 用于存储 JSON 数据,deserializeJson 解析服务器返回的 JSON 数据。following2 和 follower2 变量分别存储关注数和粉丝数。 更新全局变量和打印数据
following = following2;
follower = follower2;
initbilibili = true; // 设置B站信息已初始化
Serial.print("Data received: ");
Serial.println(following); // 打印关注数
Serial.println(follower); // 打印粉丝数
1
2
3
4
5
6
2
3
4
5
6
将解析出的数据存储在全局变量 following 和 follower 中,供后续显示。将 initbilibili 设为 true,表示数据已经获取并存储,防止重复请求。 错误判断
} else {
// 如果请求失败,打印错误信息
Serial.printf("HTTP GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end(); // 关闭HTTP连接
} else {
Serial.println("Failed to connect to server"); // 如果连接失败,打印错误信息
}
mylcd.fillScreen(TFT_BLACK); // 再次清空屏幕
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
显示 Bilibili 图标和数据信息
mylcd.pushImage(mylcd.width() / 2 - (99 / 2), 10, 99, 99, bilibiliimg); // 显示B站图标
mylcd.pushImage(50, 125, 25, 25, guan); // 显示“关”图标
mylcd.pushImage(80, 125, 25, 25, zhu); // 显示“注”图标
mylcd.pushImage(110, 125, 25, 25, maohao); // 显示冒号
mylcd.drawString(following, 140, 125, 4); // 显示关注数
mylcd.pushImage(30, 160, 25, 25, geng); // 显示“跟”图标
mylcd.pushImage(60, 160, 25, 25, sui); // 显示“随”图标
mylcd.pushImage(90, 160, 25, 25, maohao); // 显示冒号
mylcd.drawString(follower, 120, 160, 4); // 显示粉丝数
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2.7.5 天气
用于从网络 API 获取天气数据,并根据当前的天气状况在屏幕上显示相应的图标和数据(温度、湿度等)。 天气的程序和Bilibili的逻辑大致相似,都是先GET请求,再截取返回的Json值,然后做个简单的判断再显示即可 创建方法
void fetchWeather() {
//在此写入逻辑代码
}
1
2
3
2
3
检查天气数据是否已初始化
if(initweather == false) {
mylcd.fillScreen(TFT_BLACK);
1
2
2
通过检查 initweather 是否为 false 来确定是否需要获取天气数据。如果未初始化(即 initweather == false),则清空屏幕为黑色背景,准备显示天气数据。 检查 WiFi 连接状态
if (WiFi.status() == WL_CONNECTED) {
WiFiClient client;
HTTPClient http;
1
2
3
2
3
确认设备已连接 WiFi,才能发送请求获取天气数据。创建 WiFi 客户端 client 和 HTTP 客户端 http,以用于发送和接收数据。 构建并发送 HTTP 请求
if (http.begin(client, weatherAPI + weatherapi + "&location=" + cityname + "&language=zh-Hans&unit=c&start=0&days=1")) {
int httpCode = http.GET();
1
2
2
使用 http.begin() 函数创建 HTTP GET 请求,将 API 密钥、城市名称、语言设置等拼接到 URL 中。http.GET() 发送 GET 请求,返回 HTTP 响应代码 httpCode 以确认请求成功与否。 解析服务器响应并提取数据
if (httpCode > 0) {
String payload = http.getString();
Serial.println("JSON Response:");
Serial.println(payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
1
2
3
4
5
6
2
3
4
5
6
如果 httpCode > 0 表示请求成功,将服务器响应内容保存到 payload。 从 JSON 中获取温度、湿度和天气状况
String temperature2 = doc["results"][0]["daily"][0]["high"];
String humidity2 = doc["results"][0]["daily"][0]["humidity"];
String weathe2r = doc["results"][0]["daily"][0]["text_day"];
1
2
3
2
3
从 JSON 数据中提取每日最高温度、湿度和白天天气状况。 保存数据并更新初始化状态
temperature = temperature2;
humidity = humidity2;
weather = weathe2r;
initweather = true;
1
2
3
4
2
3
4
将数据保存到全局变量 temperature、humidity 和 weather 中,并将 initweather 标记为 true,表示天气数据已获取成功。 数据打印与错误处理
Serial.print("Data received: "); // 打印数据
Serial.println(temperature);
Serial.println( humidity);
Serial.println( weather);
} else { // 如果请求失败
Serial.printf("HTTP GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end(); // 结束HTTP客户端
} else {
Serial.println("Failed to connect to server");
}
mylcd.fillScreen(TFT_BLACK); // 清空屏幕
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
显示对应天气的图标
if(weather == "阴" || weather == "多云") {
mylcd.pushImage(mylcd.width() / 2 - (99 / 2), 10, 99, 99, cloud);
} else if(weather == "小雨" || weather == "大雨" || weather == "暴雨" || weather == "雨") {
mylcd.pushImage(mylcd.width() / 2 - (99 / 2), 10, 99, 99, rain);
} else if(weather == "晴") {
mylcd.pushImage(mylcd.width() / 2 - (99 / 2), 10, 99, 99, sun);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
根据 weather 状态,显示相应的天气图标。mylcd.pushImage() 函数会将对应的图标绘制在屏幕顶部中央。 显示温度和湿度
mylcd.pushImage(50, 125, 25, 25, wen);
mylcd.pushImage(80, 125, 25, 25, du);
mylcd.pushImage(110, 125, 25, 25, maohao);
mylcd.drawString(temperature + " C", 140, 125, 4);
mylcd.pushImage(50, 160, 25, 25, shi);
mylcd.pushImage(80, 160, 25, 25, du);
mylcd.pushImage(110, 160, 25, 25, maohao);
mylcd.drawString(humidity + " %", 140, 160, 4);
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2.7.6 Web服务器
用于处理 ESP8266 的 Web 服务器的配置,通过 HTTP 请求管理 WiFi 和其他参数的设置。它加载 HTML 页面显示给用户,并接收 WiFi 信息、B站 UID、城市和 API 的参数。 在程序中,ESP8266通过从FS文件系统读取HTML网页,然后将其挂载到自身的本地网络端口上,这样用户就可以通过连接热点进入192.168.4.1进入HTML配网网页中,用户提交的form会以POST方式返回给ESP8266。 为了方便用户在重启后避免重复操作,还需要将form信息以JSON格式保存到FS文件系统中,以便下次开机直接读取,从而避免重复繁琐配网操作。 随后进入WIFI连接WiFi.begin方法输入WIFI名称和密码即可。 创建方法
void handleWiFiConfig() {
//在此写入逻辑代码
}
1
2
3
2
3
处理根目录请求 (/)
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if (SPIFFS.exists("/index.html")) {
fs::File file = SPIFFS.open("/index.html", "r");
if (file) {
size_t fileSize = file.size();
String fileContent;
while (file.available()) {
fileContent += (char)file.read();
}
file.close();
request->send(200, "text/html", fileContent);
return;
}
}
request->send(404, "text/plain", "File Not Found");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
当用户访问根目录 / 时,服务器会检查 SPIFFS 文件系统中是否存在 index.html 文件。如果文件存在,读取其内容并返回给客户端作为响应。如果文件不存在,则返回 404 错误,表示文件未找到。 处理连接请求 (/connect)
server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request) {
String ssid = request->getParam("ssid", true)->value();
String pass = request->getParam("pass", true)->value();
String uid = request->getParam("uid", true)->value();
String city = request->getParam("city", true)->value();
String api = request->getParam("api", true)->value();
Serial.println(ssid);
Serial.println(pass);
DynamicJsonDocument doc(1024);
doc["ssid"] = ssid;
doc["pass"] = pass;
doc["uid"] = uid;
doc["city"] = city;
doc["api"] = api;
fs::File file = SPIFFS.open(ssidFile, "w");
if (file) {
serializeJson(doc, file);
file.close();
}
useruid = uid;
cityname = city;
weatherapi = api;
WiFi.begin(ssid.c_str(), pass.c_str());
request->send(200, "text/html", "<h1>Connecting...</h1>");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
当用户通过 HTTP POST 请求 /connect 提交表单时,ESP8266 会获取表单中 ssid、pass、uid、city、api 等参数。将这些参数打印到串口以便调试。创建一个 JSON 文档,将这些参数写入到 JSON 对象中,并存储在 SPIFFS 文件系统的 ssidFile 文件中。将 useruid、cityname 和 weatherapi 等全局变量更新为新接收到的值。调用 WiFi.begin() 开始连接到指定的 WiFi 网络。向用户返回一个 HTML 响应
Connecting...
,提示设备正在连接 WiFi。 启动 Web 服务器server.begin();
1
启动 Web 服务器以响应请求并进入运行状态。
2.7.7 WIFI自动连接
在前面提到如何通过WIFI配网并以JSON的格式保存到FS文件系统,那么在这里读取配置信息就相对容易了,直接读取JSON格式的数据,然后启动WIFI连接的步骤即可 主要功能是从 SPIFFS 文件系统中读取 WiFi 配置信息,尝试连接到 WiFi。如果连接失败,它会启动强制门户,以便用户可以输入新的 WiFi 信息。这个过程确保设备能够正常连接到网络,从而进行后续的数据获取和功能实现。 创建方法
void loadWiFiConfig() {
// 在此输入逻辑代码
}
1
2
3
2
3
初始化 SPIFFS
if (SPIFFS.begin()) {
1
初始化 SPIFFS 文件系统,以便可以读取存储在其中的文件。 打开 WiFi 配置文件
fs::File file = SPIFFS.open(ssidFile, "r");
if (file) {
1
2
2
尝试打开指定的 WiFi 配置文件 (ssidFile) 进行读取。 解析 JSON 数据
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, file);
if (!error) {
1
2
3
2
3
创建一个 JSON 文档,并尝试从文件中反序列化 JSON 数据。如果成功,继续执行。 读取配置信息
String ssid = doc["ssid"];
String pass = doc["pass"];
String uid = doc["uid"];
String city = doc["city"];
String api = doc["api"];
useruid = uid;
cityname = city;
weatherapi = api;
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
从 JSON 文档中提取 WiFi SSID、密码、B站 UID、城市名称和天气 API,并将其存储在全局变量中。 连接到 WiFi
WiFi.begin(ssid.c_str(), pass.c_str());
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000) {
delay(500);
}
1
2
3
4
5
2
3
4
5
使提取的 SSID 和密码开始连接 WiFi。使用一个 while 循环最多等待 5 秒钟,检查 WiFi 是否连接成功。每 500 毫秒检查一次。 连接失败的处理
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi connection failed, starting captive portal...");
startCaptivePortal(); // 启动强制门户
} else {
Serial.println("WiFi connected");
timeClient.begin();
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
如果连接失败,则打印错误信息并启动强制门户(用于输入 WiFi 配置信息)。如果连接成功,则打印成功信息并开始 NTP 客户端。 关闭文件
file.close();
}
}
1
2
3
2
3
关闭配置文件,释放资源。
2.7.8 时钟表盘
时钟的也较为简单,先使用TFT-eSPI的fillCircle绘制时钟界面的UI 绘制了一个模拟钟表的背景,包括外圈、时刻线、分钟刻度和中心点。通过这些操作,生成一个美观的钟表表盘供后续实时显示时间使用。 创建方法
void ClockInit() {
//在此写入逻辑代码
}
1
2
3
2
3
时钟初始化
int i=0;
unsigned int x0,y0,x1,y1;
1
2
2
清空屏幕
mylcd.fillScreen(TFT_BLACK);
1
清空整个屏幕,将背景设为黑色,为钟表绘制准备一个干净的屏幕。 绘制钟表外圈
mylcd.fillCircle(119, 119, 117, TFT_GREEN); // 外圈大圆
mylcd.fillCircle(119, 119, 107, TFT_BLACK); // 内圈小圆
1
2
2
使用两个圆圈绘制钟表的外框,设置外圆为绿色直径 117 的圆,再在其内绘制一个黑色小圆形成边框。 绘制刻度线
for(i=0; i<360; i+=30) {
x0 = cos((i-90)*0.0174532925)*114 + 120;
y0 = sin((i-90)*0.0174532925)*114 + 120;
x1 = cos((i-90)*0.0174532925)*98 + 120;
y1 = sin((i-90)*0.0174532925)*98 + 120;
mylcd.drawLine(x0, y0, x1, y1, TFT_GREEN);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
使用 for 循环每 30 度绘制一条长刻度线(对应每小时位置)。计算 x0, y0 和 x1, y1 的坐标,并调用 drawLine() 画线来表示时刻位置。 绘制分钟刻度点
for(i=0; i<360; i+=6) {
x0 = cos((i-90)*0.0174532925)*102 + 120;
y0 = sin((i-90)*0.0174532925)*102 + 120;
mylcd.drawPixel(x0, y0, TFT_WHITE); // 绘制分钟刻度点
if(i == 0 || i == 180)
mylcd.fillCircle(x0, y0, 2, TFT_BLUE);
if(i == 90 || i == 270)
mylcd.fillCircle(x0, y0, 2, TFT_BLUE);
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
使用 for 循环每 6 度绘制一个白色刻度点,表示分钟位置。特殊处理 0、90、180 和 270 度角的刻度点,以蓝色小圆标记 3、6、9 和 12 点位置。 绘制中心圆点
mylcd.fillCircle(119, 120, 4, TFT_BLUE);
1
在钟表中心绘制一个蓝色小圆点,以突出钟表中心。 初始化时间变量
total_time = millis() + 1000;
1
使用 total_time 来存储当前时间,控制一秒钟的时间间隔,以便后续定时刷新秒针位置。
2.7.9 时钟指针
负责绘制和更新时钟界面,显示当前时间并处理时、分、秒指针的绘制与刷新。它通过 NTP 客户端获取时间并每秒更新显示,为用户提供实时的时间信息。 创建方法
void showClockPage() {
//在此写入逻辑代码
}
1
2
3
2
3
初始化时钟
if (!initialized) {
ClockInit();
initialized = true;
}
1
2
3
4
2
3
4
检查时钟是否已初始化。如果没有,调用 ClockInit() 来绘制时钟的背景,并设置 initialized 为 true。 更新时间
timeClient.update(); // NTP更新
hours = timeClient.getHours();
mins = timeClient.getMinutes();
secs = timeClient.getSeconds();
1
2
3
4
2
3
4
从 NTP 客户端获取当前的小时、分钟和秒。 每秒更新时间
if (total_time < millis()) {
total_time += 1000; // 每次循环增加 1000ms
secs++; // 秒数加一
if (secs >= 60) {
secs = 0;
mins++; // 分钟数加一
if (mins > 59) {
mins = 0;
hours++; // 小时数加一
if (hours > 23) {
hours = 0; // 如果超过 23 点则归零
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
检查是否经过了一秒(total_time < millis())。如果是,增加秒数并处理进位(秒、分钟和小时)。 绘制时、分、秒指针
if ((secs == 0) || flag) {
flag = 0;
mylcd.drawLine(hours_x, hours_y, 119, 120, TFT_BLACK); // 清除旧的小时指针
hours_x = cos((hours * 30 + (mins * 6 + (secs * 6) * 0.01666667) * 0.0833333 - 90) * 0.0174532925) * 62 + 120;
hours_y = sin((hours * 30 + (mins * 6 + (secs * 6) * 0.01666667) * 0.0833333 - 90) * 0.0174532925) * 62 + 120;
mylcd.drawLine(mins_x, mins_y, 119, 120, TFT_BLACK); // 清除旧的分钟指针
mins_x = cos((mins * 6 + (secs * 6) * 0.01666667 - 90) * 0.0174532925) * 84 + 119;
mins_y = sin((mins * 6 + (secs * 6) * 0.01666667 - 90) * 0.0174532925) * 84 + 120;
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
如果秒数为零或者是第一次调用,清除旧的指针线并计算新的指针坐标。这里使用三角函数计算小时和分钟指针的终点坐标。 绘制秒针
mylcd.drawLine(secs_x, secs_y, 119, 120, TFT_BLACK); // 清除旧的秒针
secs_x = cos((secs * 6 - 90) * 0.0174532925) * 90 + 120;
secs_y = sin((secs * 6 - 90) * 0.0174532925) * 90 + 120;
1
2
3
2
3
清除旧的秒针,计算新秒针的坐标。 绘制当前时间指针
mylcd.drawLine(hours_x, hours_y, 119, 120, TFT_YELLOW); // 绘制小时指针
mylcd.drawLine(mins_x, mins_y, 119, 120, TFT_WHITE); // 绘制分钟指针
mylcd.drawLine(secs_x, secs_y, 119, 120, TFT_RED); // 绘制秒针
mylcd.fillCircle(119, 120, 4, TFT_RED); // 绘制中心圆点
}
1
2
3
4
5
2
3
4
5
绘制每个时间指针(小时、分钟、秒)以及中心点。使用不同的颜色区分。 显示标题
mylcd.drawString("JLC EDA TV Lite", mylcd.width() / 2 - 50, 160, 2);
1
在屏幕底部中心显示标题,增强用户界面友好性。
2.7.9 中断
通过中断处理按键输入,能够区分短按和长按操作,并根据当前页面状态执行相应的操作。短按切换页面,长按切换表情,使得用户界面更加动态和交互式。 在按键中,为了确保按键按下能立即做出反应,引入了中断操作,这样当按键按下时程序会立即执行翻页操作,而无需等待当前页面的程序执行完再翻页,从而快速响应。 因为切换表情的逻辑是在表情页面长按,还需要通过定时器来监测按键按下的时长,从而判断是长按操作,还是短按操作。并通过if判断是在表情页面长按,而不是其他页面。 最后,还需要加上全屏黑色来刷新屏幕,确保在下一页之前清除掉上一页的内容。 创建中断方法
void IRAM_ATTR handleButtonPress() {
//在此输入中断逻辑代码
}
1
2
3
2
3
获取当前时间
unsigned long currentTime = millis();
1
记录当前时间(自设备启动以来的毫秒数),用于后续的时间比较。 检测按键按下
if (digitalRead(buttonPin) == LOW && !buttonPressed) {
buttonPressed = true;
pressStartTime = currentTime; // 记录按下时的时间
}
1
2
3
4
2
3
4
检测到按键被按下(电平为低)且 buttonPressed 状态为 false 时,将 buttonPressed 设置为 true,并记录按下的时间。 检测长按
if (buttonPressed && (currentTime - pressStartTime) > LONG_PRESS_DELAY) {
isLongPress = true;
}
1
2
3
2
3
如果按键已经被按下,并且从按下开始经过的时间超过 LONG_PRESS_DELAY,则标记为长按。 检测按键松开
if (digitalRead(buttonPin) == HIGH && buttonPressed) {
1
当检测到按键松开(电平为高)且 buttonPressed 状态为 true 时,执行以下操作: 处理长按
if (isLongPress) {
if (currentPage == 0) { // 只有在第一页时才切换表情
emojiState = (emojiState + 1) % 8; // 切换表情
mylcd.fillScreen(TFT_BLACK); // 清屏
}
1
2
3
4
5
2
3
4
5
如果是长按,并且当前页面是第一页,则切换表情并清屏。 处理短按
} else { // 如果是短按
if (currentTime - lastDebounceTime > debounceDelay) {
currentPage = (currentPage + 1) % 4; // 短按切换页面
lastDebounceTime = currentTime;
mylcd.fillScreen(TFT_BLACK); // 清屏
}
}
1
2
3
4
5
6
7
2
3
4
5
6
7
如果是短按,并且距离上次按键操作的时间超过去抖动延迟 debounceDelay,则切换到下一个页面并清屏。 重置按键状态
buttonPressed = false;
isLongPress = false;
}
1
2
3
2
3
重置按键状态,以便准备处理下一次按键事件。 再次清屏
mylcd.fillScreen(TFT_BLACK); // 清屏
1
2.7.10 循环
根据 currentPage 的值处理不同页面的逻辑,通过 WiFi 状态决定显示内容。这种结构使得代码清晰易于管理,便于未来扩展新页面或功能。确保在每个页面中都进行了必要的 WiFi 状态检查,以提高用户体验。 页面的逻辑代码就相对简单了,使用switch语句来判断页号即可。 另外,为了确保用户在未联网时不会出现异常,加入了WIFI状态判断,确保在线功能未联网时提示用户配网。 最后还需要在每一页的最后加入initxxx = false;确保能在下一次进入页面执行页面功能的初始化。
void loop() {
//在此输入循环逻辑
}
1
2
3
2
3
页面切换
switch (currentPage) {
1
使用 switch 语句根据 currentPage 的值切换到不同的页面。
首页(case 0)
case 0:
initbilibili = false;
switch (emojiState) {
case 0:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,hi); // 表情1
break;
case 1:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,nice);// 表情2
break;
case 2:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,mid);// 表情3
break;
case 3:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,unhappy);
break;
case 4:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,error);
break;
case 5:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,star);
break;
case 6:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,love);
break;
case 7:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,what);
break;
case 8:
mylcd.pushImage(mylcd.width() / 2-(200/2), mylcd.height()/2-(100/2), 200, 100,dowhat);
break;
}
break;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
当 currentPage 为 0 时,显示不同的表情图像,根据 emojiState 的值选择不同的表情:hi、nice、mid、unhappy、error、star、love、what 和 dowhat。 每个表情通过 mylcd.pushImage() 在屏幕中央显示。
第二页(case 1)
case 1:
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Starting captive portal...");
mylcd.pushImage(mylcd.width() / 2-(99/2), 10, 99, 99,wifi);
mylcd.drawString("WIFI:TV-Lite", 50, 125, 4);
mylcd.drawString("IP:192.168.4.1", 40, 155, 4);
}else {
showClockPage(); // 显示时钟
}
break;
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
当 currentPage 为 1 时,如果 WiFi 连接失败,则显示 WiFi 信息和状态:显示 WiFi 名称和 IP 地址。 如果 WiFi 连接成功,则调用 showClockPage() 函数显示时钟。
第三页(case 2)
case 2: // 第三页
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Starting captive portal...");
mylcd.pushImage(mylcd.width() / 2-(99/2), 10, 99, 99,wifi);
mylcd.drawString("WIFI:TV-Lite", 50, 125, 4);
mylcd.drawString("IP:192.168.4.1", 40, 155, 4);
}else {
initialized = false;
fetchWeather(); // 获取天气
}
break;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
当 currentPage 为 2 时,同样检查 WiFi 状态。如果未连接,则显示 WiFi 信息;如果连接,则调用 fetchWeather() 函数获取天气数据。
第四页(case 3)
case 3: // 第四页
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Starting captive portal...");
mylcd.pushImage(mylcd.width() / 2-(99/2), 10, 99, 99,wifi);
mylcd.drawString("WIFI:TV-Lite", 50, 125, 4);
mylcd.drawString("IP:192.168.4.1", 40, 155, 4);
}else {
initweather = false;
bilibili(); // 显示 bilibili 页面
}
break;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
当 currentPage 为 3 时,检查 WiFi 状态。如果未连接,显示 WiFi 信息;如果连接,则调用 bilibili() 函数显示 bilibili 页面的内容。 延时
}
delay(100);
1
2
2
在每次循环结束时,延时 100 毫秒,控制页面更新的频率。
2.8 网页开发
2.8.1 环境配置
创建文件夹 在项目目录下我们新建一个data文件夹,用于存放要烧录到FS文件系统的文件 index.html文件 在data文件夹下新建一个index.html文件,用于存放我们的配网页面
2.8.2 编写HTML
用于 TV-Lite 控制中心的 HTML 页面,用户可以通过该页面输入 WiFi 信息、bilibili 用户 ID 和天气 API 密钥。下面是对页面结构和样式的详细解读 在项目目录下新建一个data文件夹,新建index.html,写入配网网页的html代码即可。 在这个代码中,创建了一个form表格,并在表格内创建了分别为WIFI名称、密码、bilibili用户id、心知天气API和城市天气小写的输入框,用户在点击submit后,通过post方法把信息安全传入程序,程序通过输入框的name读取信息。 这里我们用了一些简单的CSS样式来美化我们的界面
(1)HTML 结构
文档头部
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>TV-Lite</title>
1
2
3
4
5
6
2
3
4
5
6
文档声明为 HTML5,设置字符集为 UTF-8,并指定视口属性以适应不同设备。
样式
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
h1 {
text-align: center;
}
.button {
display: inline-block;
height: 30px;
width: 300px;
margin-top: 20px;
padding: 10px 20px;
background-color: darkgray;
color: #fff;
border: none;
border-radius: 20px; /* 添加圆角 */
text-decoration: none;
line-height: 2; /* 通过调整line-height的值来调整文字的垂直位置 */
text-align: center; /* 文字居中 */
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* 添加立体感 */
transition: all 0.3s ease; /* 添加过渡效果 */
}
.button:hover {
background-color: #86b7fe; /* 鼠标悬停时的背景颜色 */
transform: translateY(2px); /* 点击效果 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); /* 添加更多立体感 */
}
.search-box {
margin-top: 20px;
display: inline-block;
height: 30px;
width: 300px;
padding: 5px 10px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 20px;
text-align: center; /* 文字居中 */
}
.hidden {
display: none; /* 初始隐藏 */
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
包含基本的样式,设置页面的字体、颜色、按钮样式等。样式中包括:.container: 中心化和最大宽度限制。 .button: 定义按钮的外观,包括背景颜色、圆角、阴影和悬停效果。 .search-box: 设置输入框的样式。 .hidden: 用于隐藏元素。
主体内容
<body>
<form action='/connect' method='POST'>
<div class='container'>
<h1>TV-Lite控制中心</h1>
<p>本项目基于ESP8266主控开发</p>
<input type='text' name='ssid' placeholder='输入WIFI名称' class='search-box'>
<input type='password' name='pass' placeholder='输入WIFI密码' class='search-box'>
<input type='uid' name='uid' placeholder='输入bilibili用户ID' class='search-box'>
<input type='api' name='api' placeholder='输入心知天气API密钥' class='search-box'>
<input type='city' name='city' placeholder='输入城市拼音小写' class='search-box'>
<input type='submit' style="height: 50px;width: 320px" class='button' value="保存">
<a href="https://lceda.cn/">
<table class="container button" style="height: 200px">
...
</table>
</a>
</div>
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
包含一个表单,用户可以在此输入 WiFi、bilibili 用户 ID 和天气 API 密钥。提交按钮使用自定义样式,表单通过 POST 方法提交到 /connect 路径。下面的表格展示设备信息,包括设备名称、内存大小和控制台版本。
2.9 编译
2.9.1 编译主程序
点击底下一排中的勾,开始构建 如果显示SUCCESS则表示构建成功
2.9.2 编译FS文件系统
点击左侧PlatformIO的图标,选择Build Filesystem Image 一样,出现SUCCESS则表示构建成功
2.10 烧录
2.10.1 接线
在硬件设计中,我们特意为主板设计了下载模式跳线接口,使用跳帽或镊子短接后再上电,即可进入下载模式进行程序下载。然后再将主板上的TX连接到烧录器的RX,主板上的RX连接到烧录器的TX(RX、TX交叉连接) 如果成功进入下载模式,此时在VsCode左下角应该有端口选择按钮,点击选择或使用默认的自动识别都可以。
2.10.2 烧录主程序
方法1:使用官方程序
构建成功后,在.pio/build/nodemcuv2文件夹下,应该会有bin文件和elf文件,这都是固件,只是类型不同,您可以通过这些固件使用官方烧录器烧录
方法2:通过编译器
点击底部箭头,等待烧录完成即可
2.10.3 烧录文件系统
方法1:使用官方程序
文件系统的固件同样位于.pio/build/nodemcuv2文件夹下,先烧录完主程序后再烧录文件系统
方法2:通过编译器
点击左侧的Upload Filesystem Image,等待烧录完成即可