今年夏天出门吃了个饭回来发现空调遥控器被猫踩进了床底翻出来的时候液晶屏已经裂了。当时我正处在刷题焦虑期每天雷打不动要上牛客做一道每日一题打卡两件事本来八竿子打不着。结果那个周末我突发奇想干脆做一个能把空调和刷题打卡联动起来的项目。于是就有了这个“空调遥控【牛客tracker 每日一题】”。这个项目说白了就是一个软硬结合的小玩具加生产力工具。硬件端是一个基于 ESP32 的红外遥控小盒子用来接管空调软件端是一个牛客 tracker负责记录每天在牛客网上的刷题进度中间用一套规则把它们联动起来到点该刷题了空调自动把房间调到舒适温度刷完打卡后自动切回节能模式。它适合三类人正在准备校招、想养成刷题习惯的同学对智能家居 DIY 感兴趣的嵌入式爱好者以及像我一样总是丢遥控器、又想给学习过程加点仪式感的人。1. 项目缘起从“找遥控器”到“让环境配合你学习”1.1 刷题党的真实痛点牛客网的每日一题算是我刷题计划里的固定项目。但人的意志力是有限的冷冰冰的待办列表真的很难坚持。我观察过自己夏天太热的时候不想开电脑冬天太冷的时候不想从被窝里出来而这些时候往往正是下班后或早上的刷题黄金时间。与其跟自己较劲不如想想怎么降低“开始行动”的阻力。我发现一个规律物理环境的舒适度对学习状态的影响非常大。如果空调在我该刷题的时间段提前把房间温度调好书桌灯光也按预设亮起来哪怕只是温度合适这一点就足以让我坐下打开编辑器。这才是把“每天刷题”坚持下去的关键而不是靠意志力硬撑。这个想法直接催生了项目的第一部分一个能自动调度、也能随时手动控制的空调遥控模块。1.2 为什么选软硬结合而不是纯软件纯软件方案我之前试过很多番茄钟、打卡 App、习惯养成类工具都能用但都少了一个东西物理世界的反馈。软件提醒只是屏幕上的一条通知划掉就没了。硬件不一样当你看到那个小盒子上的 LED 亮起来听到红外发射管发出“滴”的一声你会真切地感觉到“学习时段开始了”。这种仪式感对长期习惯养成非常重要。另外从技术角度讲纯软件方案很难解决“环境准备”这件事。App 没法替你把空调打开智能插座只能切断电源没法调温度和模式。要做就做一个真正能接管遥控器的设备。所以我选型时放弃了手机自带红外、成品智能插座直接走上 ESP32 加红外发射管的 DIY 路线顺便把牛客 tracker 也做进了同一套系统里。1.3 项目整体架构一览先给一个总览方便后面展开。整个系统分四层层级组成作用硬件层ESP32 DevKitC、红外发射/接收管、DHT11 温湿度传感器、PIR 人体传感器发射红外指令、感知环境温湿度和是否有人通信层WiFi MQTTHTTP API 兜底ESP32 与服务端之间交换指令和状态服务层牛客 tracker 服务、规则引擎、SQLite 数据库抓取每日一题、记录打卡、计算连续天数、执行联动规则展现层Web 管理页面 手机推送通知手动控制空调、查看刷题进度、接收提醒四层之间唯一的强耦合点是那个规则引擎它读取 tracker 的打卡状态再决定是否给 ESP32 下发空调指令。这样即使牛客 tracker 挂了空调遥控模块还能独立工作反过来也成立。2. 硬件方案与红外遥控原理自己动手抓红外码2.1 硬件选型ESP32 是起步价拿到这个项目后我第一件事是摸清楚自己的需求要能收发红外信号、要能连 WiFi、价格不能太贵、后续最好还能接传感器。对照下来手机自带红外方案直接排除因为现在很多手机已经砍掉红外发射头了ESP8266 虽然便宜但 IO 少而且我要跑的树莓派式的本地逻辑加上 MQTT内存有点吃紧最后选了 ESP32 DevKitC理由很实诚价格二十多块WiFi 和蓝牙都有IO 充足跑 MicroPython 或者 Arduino 都行以后想接更多传感器也不用换板子。红外部分需要三样东西一个发射用的 940nm 红外 LED一个驱动三极管 S8050一个接收用的 TSOP38238 红外接收头。整套硬件成本不到 50 块钱。建议买模块而不是散件比如带三极管驱动电路的红外发射模块能省掉不少焊接排错的功夫新手尤其推荐。2.2 红外遥控原理为什么空调遥控器能“隔空”按键这里稍微讲点原理搞懂了后面抓码才不会一头雾水。红外遥控的本质是在 38kHz 的载波上调制一串脉冲序列接收端通过检测“有没有这个频率的载波”来还原 0 和 1。你可以把它理解成发报机载波是发报员的“嘀嗒”节奏而脉冲的长短组合才是电报内容。电视遥控器大多走 NEC、RC5 这类标准协议帧结构固定抓码时解析出地址码和命令码就行简单省事。但空调遥控器是另一类动物它要通过一帧红外数据把温度、模式、风速、摆风方向、睡眠模式等一堆参数全部塞进去所以很多空调厂商会自己定义一套帧格式数据长度很长而且各个品牌还不一样。这意味着不能指望通用的解析库最稳妥的做法是直接把整帧的原始脉冲宽度记录保存下来发射时原样回放。2.3 抓码流程让 ESP32 先当一次“录音机”抓码听起来玄乎其实就是让 ESP32 先当一次红外“录音机”把你按下的每个按键的波形录下来。我用的红外接收头是 TSOP38238接到 ESP32 的一个 GPIO 上使用 IRremote 库的 receive 示例程序逐帧打印收到的原始脉冲数据。具体操作是这样的先把接收头接好注意信号脚别接错数据脚接到 ESP32 的 GPIO。通电后打开串口监视器把遥控器对着接收头按一个键观察是否打印出 raw 数据。一个按键一个按键地抓每抓到一个按键的数据立刻用文本文件命名保存比如cool_26.txt、heat_22.txt、swing_on.txt。这一步偷懒的话后面会非常痛苦因为空调遥控器有四五十个按键抓完一圈回来根本分不清哪段数据是哪个键。全部抓完后用 sendRaw 验证把保存下来的 raw 数据通过红外发射管发给空调看空调是否有响应。建议一次只验证一个按键并做好记录别一口气发 20 条指令不然你根本不知道哪些成功哪些失败。抓码过程中有个细节要提醒遥控器上的按键大多有“连发”逻辑按住不放会重复发送同一帧数据。抓码时一定要单点轻按松开手再等串口打印完毕否则会抓到一串重复帧保存下来的数据会带很多冗余发射时空调可能只响应第一条后面的就乱套了。2.4 一个很容易踩的坑发射角度与功率第一次把抓好的数据通过 sendRaw 发出去我以为能行结果空调纹丝不动。排查了一圈发现是两个问题叠加。第一是驱动电流不够。ESP32 的 GPIO 只能输出 3.3V 和很小的电流直接驱动红外 LED发射功率非常弱稍远一点或者角度偏一点就收不到。解决办法是加一个 S8050 三极管做开关放大把红外 LED 串在 5V 和集电极之间GPIO 控制基极。改完之后从原来的“贴着空调才能控制”变成“三五米内随便按都灵”。第二是发射角度。空调室内机的红外接收窗一般在面板右下方或右下角朝向斜下方。我的小盒子放在书桌上正对空调正面下部结果信号被桌面物品遮挡。后来把发射管朝斜上方 45 度固定稳定控制距离达到五米以上。实测下来发射角度比功率更影响成功率这一点很反直觉但真的重要。3. 牛客 tracker 的数据链路每日一题是怎么进来的3.1 每日一题的数据获取方式牛客网的每日一题简单说就是平台每天固定推给你的一道算法题经常是剑指 Offer、面试必刷题单里的经典题也有按知识点轮转的每日推荐。我要做的 tracker 首先得拿到这条数据今天这道题是什么、题目难不难、链接在哪。做数据获取的时候我没有写爬虫原因有两个一是牛客的页面结构会不定期调整爬虫一旦依赖某个固定的 DOM 结构改版就废了二是我不想把精力花在反爬对抗上毕竟核心目的是记录自己的学习进度。我用的方案是每天早上定时请求每日一题的目标页面解析出标题、题号、难度和链接然后存进数据库。解析逻辑封装成可配置的规则比如题目标题的 CSS 选择器、链接前缀这些抽到配置文件里页面结构变了改配置就行不用改代码。这里补充一句抓取公开页面的题目信息仅用于个人学习记录不涉及用户数据也不做批量采集是合理的个人自动化场景。3.2 tracker 的数据结构设计Tracker 的核心是两张表和一份统计状态数据库直接用 SQLite。单人使用完全够不需要为这个项目上 MySQL。第一张表daily_questions记录每日一题本身的元数据字段类型说明idINTEGER 主键自增question_dateTEXT题目所属日期titleTEXT题目标题difficultyTEXT难度等级linkTEXT题目链接tagsTEXT知识点标签如数组、DP第二张表practice_logs记录每一天的实际打卡行为字段类型说明idINTEGER 主键自增log_dateTEXT打卡日期question_idINTEGER对应每日一题 IDstatusTEXTdone / review / skippedduration_minINTEGER实际用时submit_countINTEGER提交次数noteTEXT思路笔记另外单独维护一份streaks状态表记录连续打卡天数。为什么单独算因为连续打卡是最有激励性的指标如果在打卡查询时每次都全表扫描再算连续天数数据多了之后会慢。这个量级的东西虽然不至于卡顿但单独维护可以让代码逻辑更清晰随时都能快速展示“你今天已经连续打卡几天了”这是 tracker 的灵魂指标。3.3 打卡判定逻辑怎么才算“完成”一道题打卡逻辑是整个 tracker 设计里最容易做废的地方。一开始我打算“只要用户在牛客上提交过这道题就算完成”但仔细一想不对提交过不代表做对了做对了也不代表真懂了。反过来如果判定太严格又会挫伤积极性毕竟刷题这件事最重要的是持续。最后我定的规则是三选一三个条件满足任意一个就记为完成在牛客上提交并显示了通过这个通过检查提交记录里的状态字段来识别写一篇思路笔记哪怕只有五十个字描述了这道题的解题思路和卡点手动标记“看题解搞懂了”这一条是兜底留给那些看了半小时还没思路、最后通过看题解弄明白的题。这三条规则解决了一个关键的信任问题tracker 记录的是“我真的解决了这道题”而不是“我打开过这道题的页面”。尤其是第三条把“做完”和“搞懂”做了区分避免为了维持连续打卡而自欺欺人地划掉任务。3.4 提醒链路从服务端到手机的通知数据有了打卡判定也有了接下来是闭环里最容易被忽视的一环提醒。每天早九点服务端推送一条通知到手机内容包括今日题目标题、难度和链接晚上九点如果还没打卡再补一条温和的提醒内容大致是提醒我今天还欠一道题并附上进度。推送通道我用了两款国内服务都比较成熟稳定一个是 Server酱通过微信服务号推送到微信另一个是企业微信机器人推送到企业微信群或个人。两者都只需要一个 Webhook 地址HTTP 请求就能发消息简单可靠。手机端不需要装额外的 App微信里就能收到这对提醒链路很重要——如果为了收提醒还要专门装一个应用我大概率会在第三天就把通知权限关掉。提醒内容里顺便带上 streak 信息“你已经连续打卡 12 天今天还差一道题”。这个数据比任何空泛的加油打气都管用因为连续天数断掉是真的会心痛。4. 联动逻辑与自动化空调、提醒、打卡是怎么串起来的4.1 为什么需要一套规则引擎项目做到这一步最核心也最好玩的部分来了把空调和 tracker 联动起来。一开始我粗暴地写了一堆 if-else类似“如果时间等于九点就发开空调指令”结果上线第三天就发现问题了——周末我不按工作日的节奏走有时候出差屋里根本没我有时候外面才二十多度开制冷反而傻。写死的条件一多代码就变成一团乱麻任何一条规则改了都要重新部署。所以我把联动逻辑重构成一套简单的规则引擎用 JSON 描述触发条件和动作。这是一次非常值得的重构规则和代码解耦之后调整行为只需要改配置文件连传感器上报的数据都能参与规则判断。下面是一条规则的例子{ rules: [ { name: 工作日晨间刷题预热, enabled: true, trigger: { type: schedule, time: 09:00, days: [mon, tue, wed, thu, fri] }, conditions: [ {sensor: tcpresence, op: eq, value: home} ], actions: [ {device: ac, cmd: cool, temp: 25}, {notify: daily_question_reminder} ] } ] }规则引擎的架构很简单一个定时器负责扫描所有启用的规则对每条规则先判断触发条件再判断附加条件比如人在不在家条件全部满足就执行动作列表。动作类型目前支持三种红外指令、通知推送、状态切换。4.2 状态机把“学习时段”当成一种状态有了规则引擎下一步是把离散的触发器升级成状态机。因为“到点就开、到点就关”带来的另一个问题是空调开多久、什么时候关需要更多的上下文才能决策。我设计了四个状态状态含义进入动作超时动作IDLE空闲什么都不做无无STUDY_PREP学习预热期空调制冷设 25℃、推送今日题目15 分钟后进入 STUDYINGSTUDYING刷题中保持环境温度、每 45 分钟提醒一次休息打卡成功后进入 COOLDOWNCOOLDOWN学习结束休息空调调回 28℃ 节能模式30 分钟后回到 IDLE状态机的价值在于行为不是由孤立的“时间点”驱动而是由“人的状态流转”驱动。比如你下午两点就完成了打卡状态直接从 STUDYING 跳到 COOLDOWN空调自动切到节能模式不会傻乎乎地等晚上九点才关。反过来如果今天一直没打卡状态会停在 STUDYING晚上九点的提醒会再次尝试把人拉回来。状态机的实现不复杂一个枚举加一个定时器就够了但设计完成后整个系统的行为逻辑立刻清晰了很多调试的时候也能很直观地看到当前处于哪个状态、为什么执行这个动作。4.3 手动优先原则任何自动化都必须保留手动控制的最高优先级这条是我从智能家居圈子里学到的设计原则也踩过实实在在的坑。规则引擎只负责提供“建议动作”真正的执行前会过一道手动覆盖检查。具体逻辑是如果用户在 Web 页面或物理按键上最近两小时内手动操作过空调那么任何自动指令都会被抑制。比如你刚手动把空调关了结果规则引擎判定“现在应该开空调”如果不加抑制逻辑它马上又会打开你会觉得这个系统像是个不听人话的傻东西。物理按键也很重要我在 ESP32 小盒子上留了一个按钮长按三秒直接开关空调不依赖任何服务端逻辑。这样即使手机没电、服务端挂了、WiFi 断了还能像用传统遥控器一样控制空调。手动优先原则保证了这套系统不会成为新的“遥控器受害者”。5. 实测踩坑红外抓不到、断网失联、误触发5.1 红外码抓得到却发不出去这是整个项目里排查最久的一个问题值得单独写出来。用 IRremote 的 receive 例程抓电视遥控器很顺利几秒钟就能抓到标准 NEC 格式的帧。但抓空调遥控器时串口打印出来的 raw 数据长达几百个脉冲看起来就很复杂。我把这些数据存成文本用 sendRaw 发给空调结果空调一点反应都没有。排查过程是这样的先怀疑发射功率加了 S8050 三极管驱动电路还是没有反应。再用逻辑分析仪同时抓原始遥控器和 ESP32 发射管引脚上的波形一对比发现两者明显不一样不是脉冲序列的内容不同而是载波频率和占空比有偏差。原始遥控器的载波是 38kHzESP32 默认发射时载波频率和占空比跟它不一致。空调接收端对载波频率很敏感载波对不上后面的数据全部白搭。换成 IRremoteESP8266 库的 sendRaw 函数明确指定 frequency 参数为 38kHz并加上 repeat 次数参数问题终于解决。这里有个关键经验标准电视遥控器兼容性好很多容错空间但空调这类私有协议的设备对时序极其敏感抓码后一定要验证载波频率和占空比。如果没有逻辑分析仪也可以先试 38kHz不行再试 36.7kHz 和 40kHz大概率能覆盖大多数空调。5.2 断网之后的降级策略ESP32 的所有指令都来自服务器一旦路由器重启或者网络波动设备就“失联”了空调遥控功能直接瘫痪。我第一次遇到这个问题是在一个加班晚上回到家发现空调开不了排查半天才发现是光猫和路由器都死机了。降级策略的设计思路是把关键能力下沉到设备本地。ESP32 里保存一份红外码库的全量副本即使服务器不可达ESP32 仍能按本地定时任务执行最基本的开关机和温度调节。本地任务的时间基准用 NTP 在校网正常时校准断网期间靠 ESP32 的内部 RTC 兜底恢复联网后自动再同步一次。这样设计之后云服务只负责“增强”而不负责“核心”。即使 tracker 服务下线、数据库挂了、Web 页面打不开空调仍然可以按照上周的作息正常运行。这个解耦设计在可靠性上特别值得也是我这次项目中收获最大的一条经验。5.3 误触发的根源时间规则太粗糙第一次上线用的规则很简单工作日每天 21:00 提醒打卡并打开空调制冷 26℃。结果有一天我临时出差人根本不在家系统依然在晚上九点准时打开了空调空转了三小时。回家看到电费账单的那一刻我就知道必须加“人在检测”。我在 ESP32 上接了一个 PIR 人体传感器能探测两三米内有没有人活动再配合手机 App 层上报的短期定位数据共同判断“家里是否有人”。规则引擎执行开空调动作前会检查这个条件人不在家就直接跳过。更重要的一个兜底是“逃生通道”任何自动开启指令发出后如果推送的确认消息在 15 分钟内没有被确认服务端会自动补发一条关闭指令。简单说就是所有自动化都有一个“不确认就回滚”的机制。这条兜底让我对这套系统彻底放心了——即使我的判断逻辑有 bug最坏情况下也只会白开 15 分钟空调而不是一晚上。6. 这个项目还能怎么玩扩展方向与个人体会6.1 扩展方向从空调到整个房间红外遥控的玩法远不止空调一个设备。同一个红外码库框架加上电视机、机顶盒、风扇、投影仪的码就能把整个房间的电器都纳入这套系统。我已经把电视的开关和音量控制加了进去实际体验比想象中顺手。另一个自然的扩展是闭环温控。目前空调是“按时开、按规则调”但如果接上 DHT11 温湿度传感器系统就能感知“当前房间已经 31℃了该开空调了”而不只是“现在九点了该开空调了”。温度降到 27℃ 后再自动把设定温度调高 1℃。这种基于真实环境状态而非纯时间的闭环控制才是智能家居该有的样子。6.2 学习数据可视化Tracker 攒下来的数据不能只躺在数据库里。我做了个很简单的 dashboard每打卡完一道题就自动给题目打标签——数组、字符串、动态规划、树、贪心等等。一周结束后用 group by 统计各知识点的做题数量一眼就能看出自己的短板在哪里。这个不需要复杂技术一个 SQL 查询加一个轻量前端页面就够了但对复习规划特别有用。数据可视化还带来一个额外的收获当你能直观看到自己在动态规划上连续几次都没思路时会更加愿意主动去补这块的专项训练而不是一直刷自己擅长的简单题。6.3 我的真实使用感受整套系统跑下来两个月给我最大的体会是环境助推真的能降低开始行动的门槛。每天早上坐回书桌前空调已经把温度调好手机里推送着今日题目那种“被系统温柔地推着走”的感觉对拖延症相当有效。连续打卡天数最长记录已经到 26 天这比任何打卡 App 的成就系统都让我更有动力。但我也时刻提醒自己工具是服务于人的不是反过来控制人的。手动优先原则、逃生通道、断网兜底这些设计比任何花哨的功能都重要。一个聪明的自动化系统应该在你想被鼓励的时候鼓励你在你需要安静的时候闭嘴。最后分享一个小技巧如果暂时不想折腾硬件可以先用手机自带的红外功能配合牛客的打卡提醒把整个流程先在软件层面跑通再考虑要不要加硬件。这套东西真正有价值的核心不是红外遥控本身也不是 tracker 这个工具而是“把你想坚持的事情变成环境自动配合的事情”。想明白这一点用什么技术栈反而不那么重要了。
