# TV-Lite小电视 文档:https://wiki.yang17.top/products/oshwhub/oshw-e944bc15efdd4257a15492dbae845900/ 项目来源:https://oshwhub.com/course-examples/esp8266-tvlite 分类:硬件项目 更新时间:2026-10-20T04:40:39.000Z 此项目基于ESP8266构建,采用0805等易于焊接的元器件构成,适用于教学 资料超详细 👇 项目介绍 硬件设计 软件设计 附件下载 硬件工程 项目简介🪄 本项目基于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", "

Connecting...

"); }); server.begin(); } 在程序中,ESP8266通过从FS文件系统读取HTML网页,然后将其挂载到自身的本地网络端口上,这样用户就可以通过连接热点进入192.168.4.1进入HTML配网网页中,用户提交的form会以POST方式返回给ESP8266。 为了方便用户在重启后避免重复操作,还需要将form信息以JSON格式保存到FS文件系统中,以便下次开机直接读取,从而避免重复繁琐配网操作。 随后进入WIFI连接WiFi.begin方法输入WIFI名称和密码即可。 网页HTML代码

TV-Lite控制中心

本项目基于ESP8266主控开发

设备名称: TV-Lite
内存大小: 4MB
控制台版本: V1.1
官网: 立创EDA
在项目目录下新建一个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文件系统中,这个固件需要单独刷入,如果未刷入会导致无法加载配网页面。