
1. 项目概述与核心思路几年前我在一个创客社区里偶然看到了一个叫“TV-B-Gone”的老项目它的想法简单又带点恶作剧的趣味一个能发射几乎所有常见电视关机红外码的小设备让你能在公共场合“悄悄”关掉那些吵人的电视。这个由Mitch Altman发起的开源项目其硬件版本已经迭代了很多年。当我手头拿到一块Lilygo T-Watch 2020时我立刻意识到把这块搭载了ESP32、带触摸屏的智能手表开发板变成一个可穿戴的、更现代的TV-B-Gone会是一件非常酷的事。这不仅仅是复刻更是将一种经典的硬件黑客精神移植到当下更普及、功能更强的物联网平台上。这个项目的核心目标很明确利用Lilygo T-Watch 2020内置的硬件资源编写一个Arduino程序使其能够循环发射一系列预存的红外关机信号从而实现对兼容红外遥控的老式电视的“万能”关闭功能。它非常适合那些对嵌入式开发、物联网设备改装感兴趣的爱好者无论是想学习ESP32与Arduino的结合使用还是想深入了解红外通信协议亦或是单纯想制作一个有趣的极客玩具这个项目都能提供一条清晰的实践路径。你需要准备的硬件非常简单主要就是一块Lilygo T-Watch 2020 V3注意版本软件上则需要Arduino IDE和相应的开发板支持库。2. 硬件平台与开发环境搭建2.1 认识Lilygo T-Watch 2020 V3在开始敲代码之前我们必须先了解手中的“武器”。Lilygo T-Watch 2020 V3并不是一个消费级智能手表而是一个面向开发者的开源硬件平台。它的核心是一颗双核Xtensa® 32-bit LX6的ESP32芯片主频高达240MHz性能足以应对复杂的逻辑处理。更重要的是它集成了我们项目所需的关键外设红外发射管IR LED通常位于手表顶部或侧面。这是我们的“信号枪”负责将电信号转换为940nm左右的红外光脉冲。你需要确认你的手表版本是否焊接了此元件早期有些版本可能需要自行焊接。1.54英寸电容触摸屏采用ST7789V驱动IC。这为我们提供了图形化交互界面我们可以设计一个按钮来触发关机码发送而不是依赖物理按键。振动马达用于提供触觉反馈。我们可以在代码发送红外信号时让手表震动一下作为操作确认。电源管理内置AXP202电源管理芯片能高效管理电池充放电确保长时间运行。注意市面上有T-Watch 2020的多个变种如V1, V2, V3。本项目主要基于V3版本进行开发其引脚定义和库支持最为完善。如果你的版本不同可能在引脚配置或库函数上需要微调。2.2 Arduino IDE环境配置Arduino IDE以其简单易用著称是快速上手ESP32开发的绝佳选择。配置环境看似步骤繁多但一步步来非常清晰安装Arduino IDE从Arduino官网下载并安装最新稳定版1.8.x或2.0.x均可。添加ESP32开发板支持打开Arduino IDE进入文件 - 首选项。在“附加开发板管理器网址”中填入以下URL如果已有其他用逗号分隔https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json点击“好”保存。安装ESP32开发板包进入工具 - 开发板 - 开发板管理器。在搜索框中输入“esp32”。找到由“Espressif Systems”发布的“esp32”平台点击安装。这个过程会下载必要的编译工具链和核心库耗时较长请耐心等待。安装T-Watch专用库我们需要LilyGo-T-Watch库来驱动屏幕、触摸和振动等外设。最方便的方式是通过Arduino的库管理器安装。进入项目 - 加载库 - 管理库...。搜索“LilyGo T-Watch”通常能找到由“LilyGo”发布的库选择并安装。这个库会依赖安装其他库如LVGL图形库、TFT_eSPI等一并同意安装即可。选择开发板与端口用USB-C数据线连接T-Watch和电脑。手表需处于开机状态。在Arduino IDE中工具 - 开发板选择 “ESP32 Arduino” 下的 “TTGO T-Watch”。工具 - Upload Speed设置为 “921600” 以获得更快的上传速度。工具 - Port选择识别到的串口在Windows上是COMx在Mac/Linux上是/dev/cu.usbmodemxxx。实操心得环境配置中最常见的坑是端口选择错误或驱动问题。如果连接后无法识别端口可以尝试更换数据线必须使用支持数据传输的线。在设备管理器中检查是否有未知设备可能需要手动安装CP210x或CH340等USB转串口芯片的驱动这些驱动通常在你安装的ESP32包中已包含或可从芯片厂商官网下载。按住手表上的“复位”键再插入USB线有时能强制进入下载模式。3. 代码结构与核心逻辑解析拿到一个开源项目直接烧录固然快但理解其代码结构才能举一反三。本项目的代码主要由四个文件构成它们各司其职。3.1 核心文件职责分解TV-B-Gone.ino这是项目的主程序文件Arduino工程的入口。它包含了setup()和loop()两个核心函数负责硬件初始化、图形界面创建和主业务逻辑调度。WORLD_IR_CODES.h这是项目的“弹药库”。它以一个巨大的二维数组形式存储了数百个针对不同品牌、不同型号电视的原始红外关机码主要是北美NA和欧洲EU制式。每个码由一系列的开/关脉冲时长以微秒为单位构成。config.h与main.h这两个头文件是“配置中心”。config.h通常来自T-Watch的示例库定义了手表硬件相关的引脚映射、屏幕参数、背光控制等。main.h则来自TV-B-Gone的ESP32移植库定义了红外发射的引脚、频率通常为38kHz、以及一些全局变量和函数声明。3.2 红外信号发射原理与编码为什么一串数字能控制电视这需要理解红外遥控的基本原理。它并非直接发送“关机”这个指令而是发送一段特定的**脉冲位置调制PPM或脉冲宽度调制PWM**波形。载波为了抗干扰和提高发射效率红外信号会调制在一个固定频率通常是38kHz的载波上。你可以把它理解为用38kHz这个“哨音”来传递信息接收器只“听”这个频率的声音。编码协议常见的协议有NEC、RC-5、Sony SIRC等。每种协议都定义了自己的“语法”比如一个完整的信号以多长的引导码开始“0”和“1”分别用什么脉宽组合表示。数据在协议框架内包含设备地址码和命令码。例如“松下电视的地址是0x04关机命令是0x08”。本项目的工作方式WORLD_IR_CODES.h中存储的并不是原始的协议帧数据而是已经调制好的、最终的红外LED亮灭时间序列。代码的工作就是严格按照这个时间序列以极高的时间精度微秒级控制连接到红外LED的GPIO引脚输出高电平和低电平。输出高电平时红外LED以38kHz频率闪烁发光产生载波输出低电平时LED熄灭。电视的红外接收头接收到这段光脉冲序列后进行解调和解码如果匹配到自身的关机指令就执行关机动作。重要提示由于这些红外码采集于2009年左右它们主要对应当时及更早的电视型号。现代许多智能电视可能使用了不同的红外协议、蓝牙或Wi-Fi进行控制因此这个项目对新型电视很可能无效。这是其技术局限性也是其“复古”趣味的一部分。3.3 主程序逻辑流程剖析让我们深入TV-B-Gone.ino看它是如何运转起来的初始化阶段setup()函数初始化串口调试便于打印日志。调用watch.begin()初始化T-Watch硬件屏幕、触摸、电源、振动等。设置屏幕亮度加载LVGL图形库。关键一步将指定的GPIO引脚例如IR_PIN在main.h中定义为4设置为输出模式用于控制红外LED。创建一个全屏的按钮控件并为其绑定一个回调函数例如btn_event_cb。当用户在屏幕上点击这个按钮时就会触发这个函数。事件驱动与主循环loop()与 LVGL任务处理器loop()函数的核心通常是调用lv_task_handler()用于处理LVGL的界面刷新和触摸事件。Arduino的主循环在这里退居二线主要任务交给了图形系统。当用户点击屏幕按钮时LVGL会检测到触摸事件并调用我们预先绑定的btn_event_cb回调函数。红外发射触发回调函数内在回调函数中首先触发一次短震动给用户触觉反馈。然后调用核心的sendAllCodes()或类似函数。这个函数会遍历WORLD_IR_CODES.h中的某个数组例如NApowerCodes北美码表。对于数组中的每一组红外码它通过一个精密计时循环使用micros()函数获取微秒级时间按照码值交替控制IR_PIN输出高电平和低电平从而生成精确的红外信号波形。发送完一组码后会延迟一小段时间如100毫秒再发送下一组避免信号拥塞。区域代码选择在代码开头通常通过宏定义来选择发送北美NA还是欧洲EU的码集。你需要根据所在地区的电视主流制式来修改// 对于北美用户 #define NA 1 #define EU 0 // 对于欧洲用户 // #define NA 0 // #define EU 1实操心得调试红外发射时肉眼不可见。一个极其有用的技巧是用手机摄像头辅助调试。大多数手机摄像头的CMOS传感器对红外光敏感。在暗处用手机摄像头对准手表上的红外发射管当你触发发射时你应该能在手机屏幕上看到发射管发出白色的闪烁光点。这是快速验证硬件连接和代码是否成功驱动了红外LED的最简单方法。4. 完整项目实现与代码集成理解了原理现在我们将分散的代码整合成一个可编译、可上传的完整Arduino项目。4.1 创建工程与文件管理在Arduino IDE中点击文件 - 新建创建一个新的工程。点击文件 - 保存将其保存到一个独立的文件夹中例如TWatch_TV_B_Gone。在这个文件夹内你需要创建或放置四个关键文件TWatch_TV_B_Gone.ino(主文件可将下载的TV-B-Gone.ino内容复制过来)config.hmain.hWORLD_IR_CODES.h关键点.ino文件必须与文件夹同名。头文件.h放在同一目录下即可Arduino编译时会自动查找。4.2 核心代码实现与定制化修改以下是一个高度整合和注释后的主程序框架示例展示了如何将各个部分串联起来// TV-B-Gone.ino #include config.h // T-Watch硬件配置 #include main.h // TV-B-Gone全局配置和函数声明 #include WORLD_IR_CODES.h // 红外码库 // 定义区域1为启用0为禁用 #define NA 1 // 北美码 #define EU 0 // 欧洲码 TWatchClass *watch nullptr; // T-Watch对象 lv_obj_t *btn nullptr; // LVGL按钮对象 // 屏幕按钮事件回调函数 static void btn_event_cb(lv_obj_t *obj, lv_event_t event) { if (event LV_EVENT_CLICKED) { Serial.println(Button clicked, sending IR codes...); // 1. 触觉反馈振动1秒 watch-motor_shake(2, 50); // 参数可能因库版本而异需调整 // 2. 发送红外关机码 sendAllCodes(); // 3. 可选在屏幕上显示发送完成提示 lv_label_set_text(lv_obj_get_child(btn, NULL), Sent!); delay(1000); lv_label_set_text(lv_obj_get_child(btn, NULL), TV-B-Gone\nClick Me); } } void setup() { Serial.begin(115200); Serial.println(\n\nT-Watch 2020 TV-B-Gone Starting...); // 初始化手表硬件 watch TWatchClass::getWatch(); watch-begin(); watch-motor_shake(1, 20); // 开机短震 // 关闭蓝牙和Wi-Fi以省电本项目不需要 watch-bluetooth_off(); watch-wifi_off(); // 设置屏幕亮度并初始化LVGL watch-backlight_set_value(128); // 亮度值0-255 watch-lvgl_begin(); // 设置红外发射引脚 pinMode(IR_PIN, OUTPUT); digitalWrite(IR_PIN, LOW); // 初始状态为低关闭红外 // 创建用户界面 lv_obj_t *scr lv_scr_act(); lv_scr_load(scr); lv_obj_set_style_local_bg_color(scr, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); btn lv_btn_create(scr, NULL); lv_obj_set_size(btn, 200, 200); lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0); lv_obj_set_event_cb(btn, btn_event_cb); lv_obj_t *label lv_label_create(btn, NULL); lv_label_set_text(label, TV-B-Gone\nClick Me); lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0); Serial.println(Setup complete. Ready to zap TVs!); } void loop() { lv_task_handler(); // 处理LVGL界面任务 delay(5); // 短暂延迟释放CPU控制权 } // 发送所有红外码的核心函数 void sendAllCodes() { Serial.println(Starting to blast IR codes...); #if NA int numCodes sizeof(NApowerCodes) / sizeof(NApowerCodes[0]); Serial.printf(Sending %d NA codes.\n, numCodes); for (int i 0; i numCodes; i) { sendCode(NApowerCodes[i]); delay(INTERVAL_BETWEEN_CODES); // 发送间隔例如100ms } #endif #if EU int numCodes sizeof(EUpowerCodes) / sizeof(EUpowerCodes[0]); Serial.printf(Sending %d EU codes.\n, numCodes); for (int i 0; i numCodes; i) { sendCode(EUpowerCodes[i]); delay(INTERVAL_BETWEEN_CODES); } #endif Serial.println(All codes sent.); } // 发送单组红外码的函数 void sendCode(const uint16_t *code) { int index 0; uint32_t codeLength code[index]; // 第一个元素是码的长度 uint32_t freq code[index]; // 第二个元素是频率通常为38000 for (uint32_t i 0; i codeLength; i) { uint32_t duration code[index]; if (i 1) { // 奇数索引是“空间”低电平LED灭 digitalWrite(IR_PIN, LOW); } else { // 偶数索引是“脉冲”高电平LED以载波频率闪烁 // 这里需要生成38kHz载波。一个简单但占用CPU的方法是 uint32_t halfPeriod 1000000L / freq / 2; // 计算半周期微秒数 for (uint32_t j 0; j (duration / (halfPeriod * 2)); j) { digitalWrite(IR_PIN, HIGH); delayMicroseconds(halfPeriod); digitalWrite(IR_PIN, LOW); delayMicroseconds(halfPeriod); } } delayMicroseconds(duration); // 等待这个脉冲/空间的总时长 } digitalWrite(IR_PIN, LOW); // 发送完毕确保LED关闭 }关键修改点振动函数watch-motor_shake()的参数需要根据你实际安装的LilyGo-T-Watch库的API进行调整。查阅库的示例代码是最好方法。红外发射引脚确认IR_PIN在main.h或你的代码中的定义与T-Watch V3的实际连接引脚一致。常见的是GPIO4。载波生成上面的sendCode函数中的载波生成循环是一个简化的、阻塞式的实现。在发送长码时可能会影响系统响应。更高效的方法是使用ESP32的LEDCLED PWM控制器硬件外设来生成38kHz的PWM波然后只需控制其输出使能即可。这属于进阶优化。4.3 编译与上传确保所有文件已正确放置。在Arduino IDE中选择正确的开发板和端口。点击“验证”对勾图标编译代码。首次编译会较慢需要下载核心库。编译无误后点击“上传”右箭头图标。上传过程中手表屏幕可能会闪烁或变黑这是正常现象。上传成功后Arduino IDE会显示“上传完毕”。手表可能会自动重启。5. 测试、优化与问题排查5.1 功能测试与验证上传完成后手表屏幕应显示一个大的按钮。点击它你会感觉到手表震动一下。基础测试手机摄像头法在光线较暗的环境下打开手机的相机应用将手表顶部的红外发射管对准手机摄像头。点击手表屏幕按钮。你应该能在手机屏幕上清晰地看到红外发射管发出快速、明亮的白色闪烁光。这说明GPIO控制正常红外LED在工作。实际设备测试找一台老式的CRT电视或早期液晶电视2000-2010年代的产品成功率较高。将手表红外发射头对准电视的红外接收窗通常在正面下方或侧面距离在2-5米内中间无遮挡。点击发送按钮。如果电视在几秒到十几秒内关机恭喜你成功了多尝试几个不同品牌的老电视。5.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案编译错误1. 库未安装或版本不兼容。2. 头文件路径错误或内容缺失。3. 开发板未正确选择。1. 检查“管理库”中LilyGo-T-Watch和lvgl等库是否已安装。尝试更新到最新版。2. 确保四个源文件都在同一文件夹内且.ino文件与文件夹同名。检查#include语句的文件名是否正确。3. 确认开发板选择为“TTGO T-Watch” ESP32核心版本建议使用较稳定的1.0.6或2.0.x。上传失败1. 端口被占用或选择错误。2. 手表未进入下载模式。3. 驱动问题。1. 关闭其他可能占用串口的软件如串口监视器。重新拔插USB线重新选择端口。2. 尝试按住手表侧的“复位”RST按钮不放再按一下“电源”PWR按钮然后松开RST按钮。此时电脑应识别到新的下载端口。3. 安装正确的CP210x或CH340 USB转串口驱动。手机摄像头看不到红外光1. 红外LED损坏或未焊接。2. GPIO引脚定义错误。3. 代码未正确控制引脚。1. 检查硬件。用万用表二极管档测量红外LED是否正常。2. 核对原理图确认IR_PIN的宏定义如GPIO4是否与实物连接一致。3. 在sendCode函数开头添加Serial.println(“Sending code...”);通过串口监视器查看程序是否执行到发射部分。能看到红外光但电视无反应1. 电视太新不支持旧码库。2. 发射功率不足或距离太远。3. 载波频率偏差大。4. 码库区域错误。1. 这是预期之内项目主要针对老电视。2. 确保红外发射头正对电视接收窗距离拉近到1米内。可尝试将手表靠近电视不同角度。3. 38kHz载波是关键。简易循环生成的频率可能不准。考虑改用ESP32的LEDC硬件PWM生成精确的38kHz方波。4. 检查代码开头的#define NA 1和#define EU 0根据你的地理位置切换。点击按钮无振动/屏幕卡死1. 振动电机驱动代码错误。2. LVGL任务阻塞。1. 注释掉振动代码先测试其他功能。参考库示例中的正确振动函数用法。2.sendAllCodes()函数发送码时是阻塞的期间lv_task_handler()无法执行导致界面无响应。这是此简化设计的一个缺点。优化方向是使用FreeRTOS任务或将发送过程分段非阻塞化。功耗过高手表发热快1. 屏幕背光常亮。2. Wi-Fi/蓝牙未关闭。1. 在setup()中可适当调低背光值或添加代码在空闲时自动降低背光、进入睡眠。2. 确保watch-wifi_off();和watch-bluetooth_off();被调用。红外发射时电流较大属于正常现象。5.3 进阶优化与扩展思路一个基础功能实现后便是极客精神发挥的时刻硬件优化增强发射功率手表内置的红外LED功率有限。你可以外接一个NPN三极管如8050驱动一个更大功率的红外发射管并将它放在表带上这样发射距离和角度都能大幅提升。软件优化非阻塞式发送与UI响应当前的sendAllCodes()会阻塞主循环。你可以利用ESP32的双核特性创建一个独立的FreeRTOS任务来负责红外发送这样UI在发送时依然可以响应。或者使用状态机在loop()中分段发送每次只发送一个码的一部分用millis()进行非阻塞延时。功能扩展学习与自定义遥控让项目不止于“关机”。可以增加一个“学习模式”用手表接收其他遥控器的信号并存储下来。然后你可以创建多个按钮分别对应“开机”、“音量”、“频道-”等把它变成一个真正的万能学习型遥控手表。界面美化利用LVGL库设计更炫酷的界面比如模拟一个复古的电视关机动画或者添加发射进度条。这个基于Lilygo T-Watch 2020的TV-B-Gone项目从看到想法到亲手实现整个过程充满了硬件交互的乐趣。它不仅仅是一个恶作剧工具更是一个深入了解红外通信协议、ESP32编程和LVGUI设计的绝佳切入点。当你拿着自己改装的手表成功让一台老电视悄然熄灭时那种成就感是纯粹的。硬件项目的魅力就在于此代码和电路从虚拟变为现实并产生了物理世界的影响。