桌上有台五美元包邮的Wi-Fi小方钟,原厂固件,能显示时间、日期、温度和几个愚人节级别的天气图标。本来它是买来当普通桌面显示器用的,但东西到手没两天,我就动了个念头:能不能让它把AI生成的天气摘要、待办提醒、每日一句这类内容直接怼到那块小屏幕上?而且前提是——不拆机、不刷机、不碰任何原厂固件的内部逻辑。就这么个看似苛刻的需求,最后把Claude和Codex这两套AI开发工具全拉了进来,从流量分析到脚本落地,折腾出一套可以照着复现的完整流程。
这个项目适合两类人参考:一是想给手头廉价Wi-Fi设备做"私有化控制"、但不想冒刷机变砖风险的玩家;二是想认真试试"AI辅助硬件开发"到底能省多少事的人。前者关注"原厂固件能挖出什么料",后者关注"Claude和Codex在真实项目里分工能多默契"。我尽量把两者都写透。
1. 五美元设备的选择逻辑:为什么坚持用原厂固件不刷机
1.1 这台"小方钟"到底是什么东西
先说硬件底子。这台小方钟本质上就是一个带Wi-Fi模块的桌面小闹钟,正面一块点阵像素屏,背面一个温湿度传感器开口,供电是USB 5V,内部还塞了一颗纽扣电池保证断电后时间不丢。出厂支持2.4G Wi-Fi,手机App扫码就能配网,App里能改时间格式、切换摄氏/华氏、看室内外温度、设闹钟。硬件用料非常朴素,整套BOM加起来和五美元这个价格是匹配的。
但它有一个非常关键的特点:局域网直连控制。App在同一个Wi-Fi下操作时,并不总是绕道厂商云端,很多时候会直接向设备局域网IP发HTTP请求。低价IoT设备为了省服务器带宽、降低交互延迟,普遍会做这个设计。这意味着设备本身暴露了一个"后门"——当然不是安全漏洞意义上的后门,而是厂商为了方便App调试和本地控制留下的接口。我后面做的所有事情,都是围绕这个接口展开的。
1.2 不刷固件的三个理由
第一,风险不值得。五美元设备用的芯片方案大概率是某个小容量Flash的Wi-Fi模组,市面上几乎没有开源固件可刷。就算有,刷坏了变砖,恢复需要拆机接串口,那比设备本身还贵,纯亏。
第二,原厂固件里本来就有可用的控制接口。很多玩智能家居的朋友一上来就想着刷Tasmota、刷ESPhome,那是因为大厂设备固件封闭、云端不可控。但这台小方钟的App做了本地直连,说明固件已经内置了局域网控制协议,我们只需要找到它、调用它,没必要替换它。
第三,目标决定路径。我要的不是永久改造这台设备,而是给它"外挂一个AI大脑"。原厂固件负责显示时钟、维护Wi-Fi连接,外挂的脚本负责喂内容。这个分工本身就是在利用原厂固件,而不是推翻它。
所以结论很清晰:能不改的地方尽量不改。后来整个流程跑通也证明,固件监管之外能做文章的地方比想象中多得多。
1.3 三条可能的接入路径
开始动手前,我理了一下理论上可以接入的方向,列成表格:
| 路径 | 原理 | 可行性 | 难度 |
|---|---|---|---|
| 局域网HTTP API | 设备在局域网内监听控制端口,App直接发请求 | 高 | 低 |
| 云端MQTT通道 | 手机App和设备通过厂商云服务器中转消息 | 中 | 高,需要搞到鉴权凭证 |
| 模拟App操作 | 用自动化工具控制手机App,再截屏识别屏幕内容 | 低 | 不高但体验差 |
最终我选了第一条路,因为它在技术层面最干净:不需要解密HTTPS、不需要伪造云端的鉴权token,直接在交换机层面观察明文流量就行。如果抓包后发现设备没有局域网API,那再考虑走云端通道。不过实测下来,低价设备往往比大厂设备"开放"得多。
2. 抓包流程:先搞清这台时钟在跟谁说话
2.1 搭一个可控的实验网
做流量分析的第一步不是打开抓包工具,而是先搭一个干净的实验网络。我翻出一个吃灰的旧路由器,设置了独立SSID,把电脑、手机、小方钟全部接到这个隔离网络里。这样做有两个好处:一是过滤掉办公网里乱七八糟的广播包和DNS请求,抓包文件小、信噪比高;二是手机App在同一子网下更容易触发"局域网直连"模式,而不是走云端。
电脑我用的是有线口接路由器LAN,同时开无线连同一个SSID的另一个网卡,这样抓包时能同时看到PC自己发的包和设备流经交换机的包。如果你的电脑只有一个网卡,也完全没问题,只要它和时钟在同一个二层网络,就能抓到设备与手机之间的流量。
2.2 抓包动作与App联动
然后开始抓包。我在终端里先看了一下列表确认小方钟的IP,它的MAC前缀能看出来自某个通用Wi-Fi模组厂商,IP是192.168.1.123。接着执行:
sudo tcpdump -i eth0 host 192.168.1.123 -w clock_trace.pcap这里用-w直接写文件而不是打印到终端,是因为我们要连续抓十几分钟,终端滚动输出毫无意义,反而可能因为缓冲区满丢包。抓包期间,我拿手机打开厂商App,做了一系列操作:切换摄氏/华氏、改时间格式、手动刷新天气、开关闹钟、调节屏幕亮度。每个操作之间停顿三到五秒,方便后面按时间切片分析。这一步看似机械,其实是整个项目信息量最大的一步——App的每一个操作,都会对应一组协议报文,而这组报文就是后续逆向的"标准答案"。
抓完之后Ctrl+C结束,得到一个几MB的pcap文件。如果你发现文件特别小,多半是过滤条件写错了,或者设备其实走上了云端路径,这时候需要在抓包期间让手机断网(关闭4G/5G)来强制设备走局域网。
2.3 从流量里识别协议形态
用图形化抓包工具打开pcap之后,我先在过滤栏输入ip.addr == 192.168.1.123看全量流量,再逐步叠加协议过滤。很快就发现两类明显特征:
第一类是MQTT流量,端口1883,设备启动后主动向某个公网IP发CONNECT报文,看起来是在连接厂商的云MQTT broker,订阅和发布的内容都是设备状态。这类流量直接放弃,因为它要鉴权且依赖公网,不适合本地控制。
第二类是HTTP流量,端口8080,来源指向手机App。仔细看请求路径,内容非常直白,基本可以断定就是控制接口。我用命令行工具把HTTP请求全部导出:
tshark -r clock_trace.pcap -Y "http" -T fields \ -e http.request.method -e http.request.uri -e http.request.body \ -e ip.src -e ip.dst导出结果里出现了一组API路径,我整理成表格方便后续引用:
| Method | Path | 说明 |
|---|---|---|
| GET | /api/state | 获取设备状态、当前温度、运行模式 |
| POST | /api/display | 设置显示文本 |
| POST | /api/display/effect | 切换显示效果(滚动/闪烁/静态) |
| POST | /api/brightness | 设置屏幕亮度 |
| GET | /api/alarm | 读取闹钟配置 |
让我印象最深的是/api/display,一个能直接往屏幕上推文字量的接口。到了这一步,项目的可行性已经确认大半。我随手在电脑上试了一下:
curl http://192.168.1.123:8080/api/state返回的JSON包含当前温度、Wi-Fi信号强度和固件版本。这说明接口不仅存在,而且没有做局域网侧的鉴权,任何人都能访问。这在大厂设备上很难想象,但在廉价硬件上非常普遍。
3. 让Claude当协议翻译官:把未知流量变成代码用的API文档
3.1 喂给Claude的材料,必须先做一步预处理
接口路径虽然清楚了,但每个API的参数含义、取值范围、返回字段还不完整。比如/api/display到底接受什么格式的JSON?scroll字段怎么取值?如果要推中文,编码是UTF-8还是GBK?这些信息在原厂文档里不存在,只能从流量和二进制层面去猜。
这时候Claude登场了。它不是直接吞pcap文件,而是需要一个好消化的文本形态。我做了两步预处理:先用tshark把HTTP会话导出成可读文本,再用脚本把MQTT payload里的十六进制转成可见字符串。处理后的文件长这样:
请求 192.168.1.123:8080 POST /api/display 载荷: {"text":"24.5C","scroll":true,"speed":3} 请求 192.168.1.123:8080 POST /api/brightness 载荷: {"level":128}为什么不做成JSON丢给Claude?因为对话式AI对"人类可读的流量日志"理解得最好,而原始JSON嵌套太深反而容易忽略关键字段。预处理这一步,本质是替AI把"噪声"先去掉。
3.2 第一轮问答如何定位核心接口
我给的提示词大概是这样的:
这是一台低价Wi-Fi时钟的局域网抓包日志文本。请帮我分析: 1. 识别所有HTTP API路径和请求方法,推测每个接口的作用; 2. 对每个API,整理出请求参数的取值范围和可能的默认值; 3. 特别关注POST /api/display,我需要一个能复用的调用来把文本显示到屏幕上; 4. 输出一份适合直接写Python脚本的接口清单,用表格呈现。Claude返回的核心结论是:/api/display接受{"text": "...", "scroll": true, "speed": 3},其中text会被当作待显示字符串,scroll控制是否滚动播放,speed控制滚动速度,取值可能是1到5。它还提醒我,从抓包日志看,短文本时App用的是scroll: false,只有超过屏幕宽度的文本才滚动。
这些猜测虽然都还停留在"假设"层面,但它们给后续验证指明了方向。最省时间的做法,是把Claude当成一个不用睡觉的协议分析师,让它把所有可能参数组合列出来,然后我按图索骥去实测。
3.3 Claude分析里最有价值的一种提示写法
用了几轮之后,我发现一个特别有用的套路:给Claude设定角色,让它站在"设备的固件开发工程师"角度去猜。提示词大致是:
假设你是这台Wi-Fi时钟的固件开发者,局域网API是你写的,但没有公开文档。现在请你根据这些抓包记录,推演一下/api/display内部可能如何处理text字段,特别关注编码和长度限制。这个角度的好处是,它会把问题拆成"开发者视角下必然要考虑的问题":屏幕像素宽度、字符集、缓冲区大小、滚动实现的边界条件。Claude基于这些约束给出的推断,往往比直接问"这个字段是什么意思"靠谱得多。比如它推测出点阵屏的缓冲区可能只有256字节,超过这个长度,要么截断要么设备直接不响应。后来实测证明,这个推断大体正确。
4. Codex接手:用生成代码的方式把协议跑通
4.1 先用curl手工点亮一个接口
Claude给了接口清单之后,我没有急着写脚本,而是先用curl做了一次最小验证。这一步很多人跳过,但实际很关键——它可以确认设备对某种格式的响应到底是什么,避免把协议层的错误当成代码问题。
curl -X POST http://192.168.1.123:8080/api/display \ -H "Content-Type: application/json" \ -d '{"text":"hello","scroll":true,"speed":3}'设备响应了{"code":0,"msg":"ok"},并且屏幕确实开始滚动显示"hello"。这一刻基本宣告整个方案成立:一个五美元的时钟,可以通过局域网HTTP API接收任意文本。接下来要做的,就是把这条路变成自动化。
4.2 让Codex写第一版驱动脚本
接下来轮到Codex。我的工作流是:把Claude整理好的API文档压缩成一段极简摘要,连同设备IP一起交给Codex,让它生成一个Python客户端脚本。摘要大致是这个样子:
设备IP 192.168.1.123:8080 - GET /api/state 返回状态JSON - POST /api/display 参数 {"text": string, "scroll": bool, "speed": int} - POST /api/brightness 参数 {"level": int, 0-255} 要求:用requests库,封装3个函数,带超时和异常处理,输出可直接调用的main示例。Codex生成的脚本比我预期的还要完整,它自动加了重试逻辑、超时机制和一版简单的命令行参数解析。我自己改进的部分是:把设备IP改成了环境变量读取,这样换网络不用改代码。最终的clock_client.py核心函数是这样的:
import requests BASE = "http://192.168.1.123:8080" def get_state(timeout: int = 3): r = requests.get(f"{BASE}/api/state", timeout=timeout) r.raise_for_status() return r.json() def show_text(text: str, scroll: bool = True, speed: int = 3, timeout: int = 5): payload = {"text": text, "scroll": scroll, "speed": speed} r = requests.post(f"{BASE}/api/display", json=payload, timeout=timeout) r.raise_for_status() return r.json() def set_brightness(level: int, timeout: int = 3): payload = {"level": max(0, min(255, level))} r = requests.post(f"{BASE}/api/brightness", json=payload, timeout=timeout) r.raise_for_status() return r.json()4.3 调试中踩到的最深的两个坑
脚本跑通之后,真正的折磨才开始。第一个坑是中文显示。我让Codex写了推送中文天气的脚本,结果屏幕上一片乱码,有些字直接变成竖条纹。抓包对比后发现,手机App发送中文时用的是GBK编码,而Python的requests默认用UTF-8。修复方法是先把文本编码成GBK再放进JSON:
def to_device_text(raw: str) -> str: # 原始字符串按UTF-8解码后,再编码为GBK字符串供固件解析 return raw.encode("utf-8").decode("utf-8")更直接的办法是构造payload时用data而不是json,手动设置Content-Type: application/json; charset=gbk,让requests按指定编码发送。这里要特别小心:不同批次出厂的廉价设备,固件可能来自不同方案商,有的用UTF-8,有的用GBK,还有的只认ASCII。判断标准只有一个——看App实际发出去的字节流,不要猜。
第二个坑是设备看门狗重启。我在测试时用循环脚本每两秒推一次文本,十几次之后设备直接重启了,屏幕灭掉,Wi-Fi断开。后来分析pcap发现,设备在处理显示请求时会频繁写入Flash保存当前显示模式,Flash擦写次数极快,触发了固件内置的看门狗保护。解决办法是控制推送频率,两个请求之间至少间隔30秒,并且尽量复用同一个滚动状态,不要每次都改scroll参数。
5. 从"能点亮"到"真能用":把AI内容接到时钟上
5.1 一个主循环搞定信息流
API驱动有了,下面就是内容管线的问题。我不想做成一个"往屏幕推字符串"的玩具,而是希望它作为桌面信息终端,自动轮换显示天气、待办、每日一句。于是写了一个主循环:
import time from datetime import datetime from clock_client import get_state, show_text def fetch_weather(): # 返回简短天气字符串,例如 "晴 24C 湿度45%" ... def fetch_todo(): # 从本地TXT文件读取待办,挑选最近一条 ... def fetch_quote(): # 从AI接口获取一句短文案 ... def format_for_clock(raw: str) -> str: # 截断到屏幕缓冲限制内,并处理滚动 max_len = 64 return raw[:max_len] while True: try: line = format_for_clock(fetch_weather()) show_text(line, scroll=True, speed=3) time.sleep(60) todo = fetch_todo() if todo: show_text(format_for_clock(todo), scroll=True, speed=3) time.sleep(60) except Exception: # 设备离线或网络抖动,静默等待后重试 time.sleep(30)核心思路是:每个数据源都是纯函数,format_for_clock是统一的"翻译层",所有内容最后都变成设备能吃的文本。这个架构不优雅,但非常实用,因为廉价设备只认文本,什么结构化数据、富文本统统不认识。
5.2 数据源选择与限流策略
数据源这块我没有用任何需要复杂鉴权的服务。天气用一个免密钥的公开接口,待办就是从本地markdown文件里解析今天没完成的事项,每日一句则是让AI按"不超过20个字、slogan风格"生成一批,存成文本文件,循环轮换。为什么不每次实时调用AI?因为廉价的display接口写完Flash就要歇一会儿,而且AI生成接口有成本有延迟,没必要为一个五美元屏幕花钱。
| 数据源 | 获取方式 | 更新频率 | 注意点 |
|---|---|---|---|
| 天气 | 免费HTTP接口 | 每10分钟 | 免费接口有次数限制,务必缓存 |
| 待办 | 本地文件解析 | 每次轮换 | 注意中文逗号分隔 |
| 每日一句 | AI生成文件 | 每日一次 | 提前生成,运行时只读文件 |
5.3 显示格式:时钟能吃的文本和你想的不一样
这个环节最容易翻车。点阵像素屏的分辨率极低,根本显示不了复杂字符。我实测下来,英文字母和数字效果最好,中文勉强能认出字形,但笔画多的字会糊成一团。屏幕滚动时,如果文本里有连续多个空格或者超长字符,还会出现闪烁和漏字。
解决方案也很土:推送前自己先把文本打磨成"时钟友好的格式"。比如天气只保留温度、天气词、湿度三个要素,用空格分隔;待办只保留事项名称,去掉时间和优先级标记;每日一句控制在20字以内,超过就截断。我还在format_for_clock里加了一个针对点阵屏的过滤函数,把一些GKB编码里容易显示混乱的生僻字替换成常用字。
接受现实就好:这台时钟的屏幕本来就不是设计来看长篇文本的,它最适合的是"每屏一句话、每句不超过屏幕滚动范围"。
6. 跑了一个多月的稳定性总结:容易翻车的七个细节
6.1 一张表说清楚风险和对策
整个系统连续跑了一个多月,设备没有重启过,但中间出现过各种奇奇怪怪的现象。我整理了一张风险表,送给准备复现的读者:
| 风险点 | 现象 | 对策 |
|---|---|---|
| 设备休眠 | 凌晨2点后API无响应 | 利用原厂闹钟功能定时唤醒 |
| 文本编码 | 中文显示乱码 | 对照App抓包,确认固件实际编码 |
| 推送频率 | 请求间隔过短导致设备重启 | 每次推送间隔不低于30秒 |
| 上游接口限流 | 天气数据更新失败 | 本地缓存上次成功结果 |
| 时区错乱 | 显示的时间与本地时间差8小时 | 检查设备NTP同步状态 |
| 断电重启 | 原厂固件恢复默认配置 | 脚本启动后先GET /api/state确认设备在线 |
| 滚动闪烁 | 长文本滚动时丢字符 | 限制单条文本长度,减少滚动速度档位 |
6.2 我最想提醒的三件事
第一,不要贪推送频率。这个项目里最大的隐形杀手就是高频写入。设备固件没有为频繁的显示请求做过优化,它的初衷是一天改几次,不是一秒改几次。把间隔拉到一分钟以上,系统立刻就稳定了。
第二,留意设备的系统时间。廉价时钟的RTC精度很一般,NTP同步也不是每次开机都能成功。如果你的内容里有"今天""明天"这类相对时间,必须从脚本侧计算好后直接传完整文本,千万不要指望设备自己算。
第三,日志要轻。由于Flash写入会磨损,脚本日志也不要直接写到设备端不存在的路径上。我在电脑端落盘,设备只做显示,不做任何持久化。一个月跑下来,这应该是环境最友好、设备负担最小的接入方式。
最后再说一句体外话。这个项目真正打动我的地方,不完全是五美元的时钟能显示AI内容这件事,而是一套只用原厂固件、不拆机、不刷机的开发流程,居然能被Claude和Codex两个AI工具压缩到几天内完成。放在过去,光是读懂未知设备的私有协议就够折腾一周。现在AI负责把脏活累活做掉八成,我只需要验证它给的假设、处理它想不到的边界条件。如果你手头也有一台吃灰的廉价Wi-Fi设备,不妨先抓个包看看它的局域网接口,说不定下一个"AI桌面信息终端"就是它。