站内搜索
发作品签到
专业版

TV-Lite小电视

工程标签

1.4w
0
0
36

简介

此项目基于ESP8266构建,采用0805等易于焊接的元器件构成,适用于教学

简介:此项目基于ESP8266构建,采用0805等易于焊接的元器件构成,适用于教学
复刻成本:20

开源协议

GPL 3.0

创建时间:2024-09-19 11:10:31更新时间:2025-11-19 09:35:34

描述

项目简介🪄

本项目基于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主控、外部接口。

电源电路:

image.png
供电接口:采用TYPE-C-6P接口,焊盘较大,易于焊接。在CC1和CC2引脚处加入5.1K下拉电阻,便于不同主机识别和配置,支持双Type-C线供电。在5V供电入口处加入二极管,借助其单向导通性,防止接入电池产生的反向电压损坏Type-C口的供电设备。

稳压器:采用AMS1117线性稳压器,将5V电压转换为3.3V电压,使其为ESP8266主控及LCD屏幕提供电力支持

主控电路:

image.png

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

外部接口电路:

image.png
屏幕:本电路空间有限且为方便复刻,屏幕采用的是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-&gt;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 &amp;&amp; millis() - startAttemptTime &lt; 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) &amp;&amp; (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 &amp;&amp; !buttonPressed) {  // 检测按键按下且按键未被记录为按下状态
        buttonPressed = true;
        pressStartTime = currentTime;  // 记录按下时的时间
    }
    // 检测是否为长按
    if (buttonPressed &amp;&amp; (currentTime - pressStartTime) &gt; LONG_PRESS_DELAY) {
        isLongPress = true;
    }
    // 检测按键松开
    if (digitalRead(buttonPin) == HIGH &amp;&amp; buttonPressed) {
        if (isLongPress) {  // 如果是长按
            if (currentPage == 0) {  // 只有在第一页时才切换表情
                emojiState = (emojiState + 1) % 8;  // 切换表情
                mylcd.fillScreen(TFT_BLACK);  // 清屏
            }
        } else {  // 如果是短按
            if (currentTime - lastDebounceTime &gt; 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 &gt; 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密钥填入配置地址即可。
  • 初次开机时钟界面可能不同步,需等等片刻即可。

组装流程

组装流程相对简单,一块主板,一根连接线,一个屏幕,一个外壳即可。
首先,将连接线的一端和屏幕相连

d46f6044a7c4292927147653b2aadaa.jpg
其次,将线穿过外壳,并使用胶水固定好屏幕
fdd8e17c3f157fe3916fb1a4105e3cd.jpg
最后把线塞入外壳内,主板卡入限位槽,再根据底盖开孔,盖上底盖即可。

2ded181d0596e9b925cb709736e36d1.jpg

实物图

图1:表情页面
image.pngimage.png
image.pngimage.png
图2:时钟页图3:天气页
image.pngimage.png
图4:BiliBili页
image.png

附件说明

【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

3D模型

序号文件名称下载次数
1
TV-Lite.stl
9
2
TV-Lite_BottomV2.STL
5
3
TV-Lite_KEY.stl
4

附件

序号文件名称下载次数
1
演示视频.mp4
449
2
TV-Lite项目开发文档.pdf
175
3
TV_Lite-V1.3项目资源包.zip
420
克隆工程
添加到专辑
0
0
分享
侵权投诉
知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

评论

全部评论(1
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航