
TV-Lite小电视
简介
此项目基于ESP8266构建,采用0805等易于焊接的元器件构成,适用于教学
简介:此项目基于ESP8266构建,采用0805等易于焊接的元器件构成,适用于教学开源协议
:GPL 3.0
描述
项目简介🪄
本项目基于ESP8266构建,是一个用于放置在桌面的简单信息显示设备,采用0805等较大封装的元器件,易于焊接,方便教学
项目成本大致在20元,大额费用在于屏幕和主控,屏幕大致为10元、主控为4元
项目功能🕹
本项目是基于ESP8266的桌面小电视项目,用于显示表情、时钟、天气、B站个人信息,用户可以通过按键切换界面,在表情界面长按按键可以切换表情。
- ✅表情显示
- ✅时钟显示
- ✅每日天气
- ✅B站信息
项目参数🔮
- 本设计采用ESP8266主控,内置WIFI功能,可以完成网络请求;
- 本设计采用ST7789 0.96寸圆形LCD显示屏,可显示表情、时钟、天气、B站等相关信息;
- 选用AMS1117 LDO线性稳压器,负责将5V电压转换成3.3V,为主控提供电源;
- Type-C接口接有5.1K电阻,支持双Type-C线材电源输入;
开发文档
在线文档:
嘉立创EDA Wiki
离线文档:
请查看附件【TV-Lite小电视开发文档.pdf】
原理解析
篇幅有限,这里仅讲解部分关键电路和程序,详细说明请查看开发文档
硬件设计
本项目电路由以下部分组成,电源部分、ESP8266主控、外部接口。
电源电路:

供电接口:采用TYPE-C-6P接口,焊盘较大,易于焊接。在CC1和CC2引脚处加入5.1K下拉电阻,便于不同主机识别和配置,支持双Type-C线供电。在5V供电入口处加入二极管,借助其单向导通性,防止接入电池产生的反向电压损坏Type-C口的供电设备。
稳压器:采用AMS1117线性稳压器,将5V电压转换为3.3V电压,使其为ESP8266主控及LCD屏幕提供电力支持
主控电路:

主控:参考ESP8266数据手册,对IO0、IO2、EN使能、RST重置引脚上拉,对CS片选信号下拉,以确保ESP8266及SPI通信正常
外部接口电路:

屏幕:本电路空间有限且为方便复刻,屏幕采用的是0.96寸 ST7789 LCD模块,该模块自带有屏幕驱动电路,仅需接口接入即可。在此根据该屏幕模块的接口线序配置好了对应接口的线序,直接插入即可使用。
串口:串口部分为方便下载,单独引出了IO0及GND接口作为跳帽插入接口,当插入跳帽时,IO0被拉低,进入下载模式。反之被主控部分电路拉高,进入工作模式。
电池:电池部分,引出了5V电路,因布局有限,对于希望使用电池供电的用户,可以购买充放一体模块并接入。
按键:按键部分使用的是IO2引脚,按键按下时拉低,空闲时被拉高。
软件代码
完整的项目文件已经放在了附件,可直接烧录的程序二进制及文件系统二进制文件也已经放在了附件,可自行下载。
时间有限,程序中仍有部分代码冗余和低级代码逻辑,有很多可以优化资源占用的部分,可进行修改
配网代码
void handleWiFiConfig() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if(SPIFFS.exists("/index.html")) {
fs::File file = SPIFFS.open("/index.html", "r");//从FS文件系统读取
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");
});
server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request) {//web服务接收
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();
// 保存WiFi信息到文件
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>");
});
server.begin();
}
在程序中,ESP8266通过从FS文件系统读取HTML网页,然后将其挂载到自身的本地网络端口上,这样用户就可以通过连接热点进入192.168.4.1进入HTML配网网页中,用户提交的form会以POST方式返回给ESP8266。
为了方便用户在重启后避免重复操作,还需要将form信息以JSON格式保存到FS文件系统中,以便下次开机直接读取,从而避免重复繁琐配网操作。
随后进入WIFI连接WiFi.begin方法输入WIFI名称和密码即可。
网页HTML代码
<div>
<h1>TV-Lite控制中心</h1>
<p>本项目基于ESP8266主控开发</p>
<input type="text" name="ssid" />
<input type="password" name="pass" />
<input type="uid" name="uid" />
<input type="api" name="api" />
<input type="city" name="city" />
<input type="submit" style="height:50px;width:320px" value="保存" />
<a href="https://lceda.cn/">
<table style="height:200px">
<tr>
<th>设备名称:</th>
<td>TV-Lite</td>
</tr>
<tr>
<th>内存大小:</th>
<td>4MB</td>
</tr>
<tr>
<th>控制台版本:</th>
<td>V1.1</td>
</tr>
<tr>
<th>官网:</th>
<td> <a href="https://lceda.cn/">立创EDA</a></td>
</tr>
</table>
</a>
</div>
在项目目录下新建一个data文件夹,新建index.html,写入配网网页的html代码即可。
在这个代码中,创建了一个form表格,并在表格内创建了分别为WIFI名称、密码、bilibili用户id、心知天气API和城市天气小写的输入框,用户在点击submit后,通过post方法把信息安全传入程序,程序通过输入框的name读取信息。
FLASH读写存储代码
void loadWiFiConfig() {
if (SPIFFS.begin()) {
fs::File file = SPIFFS.open(ssidFile, "r");
if (file) {
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, file);
if (!error) {
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;
WiFi.begin(ssid.c_str(), pass.c_str());
// 尝试连接WiFi,最多等待10秒
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 5000) {
delay(500);
}
// 如果连接失败,打印状态
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi connection failed, starting captive portal...");
startCaptivePortal();
} else {
Serial.println("WiFi connected");
timeClient.begin();
}
}
file.close();
}
}
}
在前面提到如何通过WIFI配网并以JSON的格式保存到FS文件系统,那么在这里读取配置信息就相对容易了,直接读取JSON格式的数据,然后启动WIFI连接的步骤即可
屏幕接线设置
#define USER_SETUP_INFO "User_Setup"
#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display
#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
#define TFT_WIDTH 240 // ST7789 240 x 240 and 240 x 320
#define TFT_HEIGHT 198
#define TFT_BL 4 // LED back-light control pin
#define TFT_BACKLIGHT_ON HIGH // Level to turn ON back-light (HIGH or LOW)
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 15 // Chip select control pin
#define TFT_DC 5 // Data Command control pin
#define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST
#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
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
#define USE_HSPI_PORT
屏幕部分为方便使用,采用的是TFT-eSPI库,User_Setup.h配置如上,但由于我们使用的是异形屏,还需修改一些驱动层面的方法
屏幕显示定义
在ST7789_Defines.h中添加198x240的屏幕显示定义
//0.96 circle TFT support
#if (TFT_HEIGHT == 198) && (TFT_WIDTH == 240)
#ifndef CGRAM_OFFSET
#define CGRAM_OFFSET
#endif
#endif
屏幕旋转定义
在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;
}
按键代码
void IRAM_ATTR handleButtonPress() {
unsigned long currentTime = millis();
if (digitalRead(buttonPin) == LOW && !buttonPressed) { // 检测按键按下且按键未被记录为按下状态
buttonPressed = true;
pressStartTime = currentTime; // 记录按下时的时间
}
// 检测是否为长按
if (buttonPressed && (currentTime - pressStartTime) > LONG_PRESS_DELAY) {
isLongPress = true;
}
// 检测按键松开
if (digitalRead(buttonPin) == HIGH && buttonPressed) {
if (isLongPress) { // 如果是长按
if (currentPage == 0) { // 只有在第一页时才切换表情
emojiState = (emojiState + 1) % 8; // 切换表情
mylcd.fillScreen(TFT_BLACK); // 清屏
}
} else { // 如果是短按
if (currentTime - lastDebounceTime > debounceDelay) {
currentPage = (currentPage + 1) % 4; // 短按切换页面
lastDebounceTime = currentTime;
mylcd.fillScreen(TFT_BLACK); // 清屏
}
}
// 重置按键状态
buttonPressed = false;
isLongPress = false;
}
mylcd.fillScreen(TFT_BLACK); // 清屏
}
在按键中,为了确保按键按下能立即做出反应,引入了中断操作,这样当按键按下时程序会立即执行翻页操作,而无需等待当前页面的程序执行完再翻页,从而快速响应。
因为切换表情的逻辑是在表情页面长按,还需要通过定时器来监测按键按下的时长,从而判断是长按操作,还是短按操作。并通过if判断是在表情页面长按,而不是其他页面。
最后,还需要加上全屏黑色来刷新屏幕,确保在下一页之前清除掉上一页的内容。
页面功能代码
void bilibili() {
if (initbilibili==false) {
mylcd.fillScreen(TFT_BLACK);
if (WiFi.status() == WL_CONNECTED) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
if (http.begin(client,bilibiliAPI+useruid)) {
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
Serial.println("JSON Response:");
Serial.println(payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
String following2 = doc["data"]["following"];
// Accessing data key
String follower2 = doc["data"]["follower"];
following=following2;
follower=follower2;
initbilibili=true;
Serial.print("Data received: ");
Serial.println(following);
Serial.println(follower);
} else {
Serial.printf("HTTP GET request failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
} else {
Serial.println("Failed to connect to server");
}
mylcd.fillScreen(TFT_BLACK);
}
mylcd.pushImage(mylcd.width() / 2-(99/2), 10, 99, 99,bilibiliimg);
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);
}
}
这里选取的是BiliBili页面的功能代码,首先先执行一次清屏,确保上一页的内容已经擦除,然后再检查网络状态,网络连接成功时启动HTTPClient,向在线服务器发送GET请求并接收返回的JSON格式值,解析并存储起来。然后关闭HTTP请求,执行屏幕显示代码。
在屏幕显示中因为使用了图片,所以先使用mylcd.pushImage将头文件引入的image.cpp中的bilibiliimg图片输出到屏幕中心偏上位置,使用mylcd.width()获取屏幕宽度÷2再减去图片宽度÷2。同理依次输出图片和文字,然后再使用mylcd.drawString输出String类型的文字即可(当然使用其他的也可以,具体参考之前存储的值格式)。
最后,再来解释下为什么开头使用了initbilibili==false,这是为了这个页面的页面程序仅执行一次,这样可以避免屏幕闪烁,也可以让程序快速响应,相当于将动态页面转成了静态页面。
界面逻辑代码
switch (currentPage) {
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;
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;
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;
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;
}
页面的逻辑代码就相对简单了,使用switch语句来判断页号即可。
另外,为了确保用户在未联网时不会出现异常,加入了WIFI状态判断,确保在线功能未联网时提示用户配网。
最后还需要在每一页的最后加入initxxx = false;确保能在下一次进入页面执行页面功能的初始化。
面板设计:
市面上共有2种0.96寸圆形LCD显示屏,一种是带玻璃盖板的,一种是不带玻璃盖板的,对于喜欢自定义的用户,特地提供了适配的个性化面板模板,用户可以通过立创面板打板定制。
3D外壳设计:
本项目适配了对应的圆形FDM打印外壳,外壳设计有固定槽位,可以很方便的将电路板固定。盖子的固定主要依靠层纹的摩擦,外壳已在FDM类型的3D打印机完成验证。
注意事项
- 请确保供电电压为5V,切勿使用其他电压值
- 设备配置地址为:192.168.4.1,请先连接配网WIFI,再进入该地址。
- 天气API采用的是心知天气API,该API为免费API,请在官网注册获取API密钥填入配置地址即可。
- 初次开机时钟界面可能不同步,需等等片刻即可。
组装流程
组装流程相对简单,一块主板,一根连接线,一个屏幕,一个外壳即可。
首先,将连接线的一端和屏幕相连

其次,将线穿过外壳,并使用胶水固定好屏幕

最后把线塞入外壳内,主板卡入限位槽,再根据底盖开孔,盖上底盖即可。

实物图
| 图1: | 表情页面 |
|---|---|
![]() | ![]() |
![]() | ![]() |
| 图2:时钟页 | 图3:天气页 |
![]() | ![]() |
| 图4:BiliBili页 | |
![]() |
附件说明
【firmware.bin】:程序固件
【spiffs.bin】:FS文件系统固件
【TV-Lite.SLDASM】:组合体文件
【TV-Lite.SLDPRT】:外壳源文件
【TV-Lite_BottomV2.SLDPRT】:底壳源文件
【TV-Lite_KEY.SLDPRT】:按键增长源文件
【TV-Lite.stl】:外壳3D文件
【TV-Lite_BottomV2.stl】:底壳3D文件
【TV-Lite_KEY.stl】:按键增长3D文件
注意事项
- 3D外壳建议选择FDM打印机打印
- OLED显示屏为0.96寸圆屏
- 按键建议购买2.5x2.5x10,可无需增长
更新说明
PCB
V1.1:初版
V1.2:优化布线
V1.3:重构布局布线,天线移至板框外,提升信号强度
3D
TV-Lite_BottomV2:适配PCBV1.3版型
BOM
器件重新标准化
文件
1.添加表情包位图
2.添加图像取模工具
FAQ
表情包从哪里来
表情来自OpenMojis剪切并转换成位图再按照教程转化成数组而来,这是一个开源免费的表情包 https://openmoji.org/如何配网
开机后连接名称为TV-Lite的WIFI热点,浏览器输入192.168.4.1即可进入配网页面(部分手机会弹出WIFI无网络是否继续连接,务必点击继续。也有部分手机会直接切换到流量,导致无法配网。)无法进入配网页面
配网页面的html保存至spiffs文件系统中,这个固件需要单独刷入,如果未刷入会导致无法加载配网页面。设计图
未生成预览图,请在编辑器重新保存一次BOM
暂无BOM
克隆工程知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。









评论