旧手表复用实战:用AI辅助开发Zepp OS番茄钟与待办清单
1. 为什么我要给一块旧手表单独写个番茄钟手里这块华米Amazfit GTR4大概是两年前入的当时图的是续航和轻便日常戴着看通知、记步数、测心率都挺顺手。但用久了就发现一个尴尬的事我真正需要的番茄工作法和待办清单这两个功能它自带的系统里要么没有要么做得极其简陋——自带的番茄钟只能计时不能跟任务绑定待办事项又藏在很深的菜单里抬手看一眼要划好几下。市面上主流的番茄类App基本都盯着手机端手表端要么是阉割版要么干脆没有。我试过几个方案一是用手机上的番茄App但每次开始专注都得掏手机反而增加了分心的机会二是用系统自带的倒计时但它跟任务系统是割裂的做完一个番茄不知道对应的是哪件事三是干脆换块新表但为了一个番茄钟换表实在不划算而且这块表的硬件完全够用——它有屏幕、有震动、有按键、能连手机缺的只是一个合适的软件。于是我就动了念头能不能自己给它写一个正好这段时间我在用AI辅助编程工具做一些小项目就想着拿这块旧手表练手。核心目标很明确一个能在手表上独立运行的番茄钟待办清单App支持任务创建、番茄计时、震动提醒、完成打卡最好还能跟手机端同步数据。关键词就三个旧设备复用、番茄工作法、AI辅助开发。这篇文章我会把整个从零到跑通的完整过程拆开讲包括我怎么选技术路线、怎么跟AI工具配合、踩了哪些坑、最后怎么把App装进手表里。如果你手里也有闲置的智能手表或者想试试用AI coding做点小工具这篇应该能给你不少参考。2. 先搞清楚这块表到底能跑什么2.1 华米GTR4的系统底子与开发入口动手之前必须先摸清楚设备的软件底细不然写出来的代码根本装不进去。Amazfit GTR4跑的是Zepp OS这是华米自家的一套轻量级手表操作系统跟Wear OS完全不是一回事。Zepp OS的应用生态相对封闭但它提供了官方的开发者工具链叫Zepp OS Developer Toolkit支持用JavaScript写应用逻辑界面则用一种叫UX的声明式框架来描述。这一点很关键——意味着我不需要去啃C或者汇编用前端那套思路就能上手。我查了一下GTR4的硬件参数屏幕是1.39英寸AMOLED分辨率454×454圆形表盘处理器是低功耗方案内存和存储都比较有限有物理按键支持震动马达支持蓝牙连接手机。这些信息决定了我的App设计边界界面必须适配圆形屏幕不能做太复杂的动画内存占用要克制交互尽量靠按键和滑动完成。另外Zepp OS的应用是跑在手表本地的但可以通过蓝牙跟手机上的Zepp App做数据通信这就为后续同步留下了口子。提示不同型号的Zepp OS设备API支持程度可能不一样。动手前一定要去官方开发者文档里确认你的具体型号支持哪些能力尤其是震动、按键事件、本地存储这几项。2.2 为什么选JavaScript而不是其他方案Zepp OS官方主推的就是JavaScript开发配套的还有一套叫Zepp OS Simulator的模拟器可以在电脑上先跑起来看效果不用每次都真机调试。我对比过几条路一是用官方工具链老老实实写二是找现成的开源番茄钟项目改三是干脆放弃手表端只做手机端。第三条直接排除因为我的核心诉求就是不掏手机。第二条的问题是现成的Zepp OS番茄钟项目很少而且大多年久失修改起来未必比从头写快。所以最终选了第一条用官方JS工具链从零写。选JS还有一个隐性好处——AI编程工具对JavaScript的支持是最成熟的。我用的AI coding助手在生成JS代码时准确率明显高于其他语言尤其是涉及界面布局和事件绑定时基本能一次成型。这大大降低了我的开发成本毕竟我不是专业的手表开发者很多Zepp OS特有的API我也是边查边用。2.3 功能范围的取舍先做减法一开始我列了一大堆想做的功能任务分类、优先级、标签、统计图表、云同步、多设备协同……但冷静下来想想手表屏幕就这么大交互方式就这几种功能堆太多反而没人用。最后我砍到只剩四个核心能力任务管理能新建、查看、完成、删除待办事项每条任务有标题和状态。番茄计时选中一条任务后启动25分钟倒计时结束时震动提醒。打卡记录每个完成的番茄自动记一笔关联到对应任务。本地持久化数据存在手表本地重启不丢。同步功能我放到了二期因为涉及手机端配合复杂度陡增先把单机跑通再说。这个取舍很关键——很多个人项目死在想做的太多先把最小可用版本跑起来后面才有迭代的动力。3. 用AI coding搭出第一版可运行骨架3.1 跟AI工具配合的正确姿势很多人用AI写代码的方式是丢一句帮我写个番茄钟App然后就等结果这样出来的东西基本没法直接用。我的做法是把任务拆到足够细每次只让AI解决一个具体问题。比如第一步我不是让它写整个App而是问Zepp OS里怎么创建一个带文本和按钮的页面给我一个最小示例。拿到示例后我自己跑通确认没问题再问下一步怎么在这个页面上绑定按钮点击事件并修改文本这样一步步推进每一段代码我都能理解、能验证。另一个心得是给AI提供足够的上下文。Zepp OS的API跟标准Web API有差异如果我不说明AI很容易按浏览器的思路写结果就是各种报错。所以我会在提问时附上官方文档里相关API的签名或者直接说这是Zepp OS环境不是浏览器环境请用Zepp OS的API。加上这句之后生成代码的可用率明显提升。3.2 项目目录结构与核心文件Zepp OS应用的标准结构大概是这样的my-pomodoro/ ├── app.json // 应用配置声明权限、页面路由等 ├── app.js // 应用入口生命周期管理 ├── page/ │ ├── home/ │ │ └── home.js // 主页任务列表 │ └── timer/ │ └── timer.js // 计时页番茄倒计时 └── utils/ └── storage.js // 本地存储封装app.json里要声明用到的权限比如震动、存储。页面路由也要在这里注册不然跳转会失败。我第一版就踩了个坑忘了在app.json里注册timer页面结果点击任务后跳转直接白屏排查了半天才发现是路由没声明。3.3 任务列表页的实现逻辑主页的核心是一个可滚动的任务列表。Zepp OS的UI框架里列表一般用SCROLL_LIST组件实现。每条任务渲染成一个列表项显示标题和状态图标。数据从本地存储读取用一个数组维护。关键代码逻辑大致是这样基于常见实践整理// 读取任务列表 const tasks storage.getTasks() || []; // 构建列表数据 const listData tasks.map((task, index) ({ title: task.title, status: task.done ? 已完成 : 进行中, index: index })); // 绑定到SCROLL_LIST const list document.getElementById(task-list); list.setProperty(hmUI.prop.DATA, listData);点击某条任务时把任务ID存到一个全局变量里然后跳转到计时页。这里要注意Zepp OS的页面跳转是hmApp.gotoPage参数传递不像Web那么方便我最后是用全局变量本地存储组合来传的。3.4 番茄计时页与震动提醒计时页的逻辑相对简单进来后读取当前任务ID启动一个25分钟的倒计时每秒更新一次界面上的剩余时间。倒计时结束调用震动API然后写一条打卡记录。let remaining 25 * 60; // 25分钟单位秒 const timer setInterval(() { remaining--; updateDisplay(remaining); if (remaining 0) { clearInterval(timer); vibrate(); // 震动提醒 saveRecord(); // 记录打卡 hmApp.goBack(); // 返回主页 } }, 1000);震动用的是hmSensor或者hmSetting里的相关接口具体哪个取决于系统版本。我实测下来GTR4的震动强度够用戴在手腕上能明显感觉到不会漏掉提醒。注意setInterval在手表息屏后可能会被系统挂起导致计时不准。这是个很隐蔽的坑后面我会专门讲怎么处理。4. 那些让我卡了半天的坑与排查过程4.1 息屏后计时停摆最要命的一个问题第一版跑起来后我兴冲冲地测试结果发现一个致命问题手表息屏之后番茄钟就不走了。亮屏一看时间还停在息屏那一刻。这意味着整个番茄钟根本没法用——谁专注的时候会一直亮着屏幕排查过程是这样的我先怀疑是setInterval的问题查了Zepp OS文档发现系统为了省电在应用进入后台或屏幕熄灭后会限制定时器执行。这不是bug是设计如此。解决方案有两个方向一是用系统提供的后台定时能力二是改用时间戳计算而不是依赖定时器累加。我最终选了第二种因为它更可靠。具体做法是启动番茄钟时记录一个开始时间戳每次界面刷新时用当前时间 - 开始时间算出已经过去多久而不是靠每秒减一。这样即使定时器被挂起只要界面重新激活算出来的剩余时间依然是准确的。震动提醒则配合系统的后台唤醒能力在预计结束时间点触发。const startTime Date.now(); const totalMs 25 * 60 * 1000; function getRemaining() { const elapsed Date.now() - startTime; return Math.max(0, totalMs - elapsed); }这个改动之后息屏再亮屏时间显示完全正确。这个坑我踩了差不多一个下午但想通之后觉得特别值——它让我理解了嵌入式设备上时间这件事不能想当然。4.2 本地存储的读写时机与数据丢失第二个坑是数据丢失。有几次我新建了任务重启手表后任务就没了。查下来是存储写入的时机不对——我原本是在页面销毁时才写存储但Zepp OS的页面销毁时机不一定可靠有时候应用被系统直接杀掉销毁回调根本没执行。改成每次数据变更后立即写入问题就解决了。虽然写入频率高了但任务数据量很小对性能没影响。存储用的是hmFS或者localStorage风格的接口具体看系统版本。我封装了一个storage.js统一处理序列化和反序列化避免到处写重复代码。// storage.js export function saveTasks(tasks) { localStorage.setItem(tasks, JSON.stringify(tasks)); } export function getTasks() { const raw localStorage.getItem(tasks); return raw ? JSON.parse(raw) : []; }4.3 圆形屏幕的布局适配GTR4是圆形表盘我第一版界面是按方形思路做的结果四个角的内容全被裁掉了。Zepp OS的UI框架虽然会自动处理一部分适配但边距和元素位置还是得自己调。我的做法是所有内容都限制在一个内切圆范围内关键信息放在屏幕中央区域边缘只放不重要的装饰。列表项的高度也调小了保证一屏能显示足够多的条目。另外圆形屏幕上的文字对齐要特别注意居中对齐比左对齐更稳妥因为左右边缘的可用宽度是变化的。4.4 真机调试与模拟器的差异模拟器上跑得好好的装到真机上却出现各种问题字体渲染不一样、震动没反应、按键事件不触发。这是因为模拟器毕竟不是真实硬件很多传感器和外设行为它模拟不了。我的经验是模拟器只用来验证逻辑和大致布局真机调试才是关键。Zepp OS提供了真机调试的桥接工具可以把应用直接推到手表上运行还能看日志。虽然每次部署要等一会儿但比在模拟器上自欺欺人强得多。尤其是震动、按键、息屏这些跟硬件强相关的功能必须真机验证。5. 让番茄钟真正好用的几个细节打磨5.1 按键交互减少抬手滑动手表上滑动操作其实挺累的尤其是运动或者手上有东西的时候。GTR4有物理按键我就把最常用的操作映射到了按键上短按开始/暂停计时长按结束当前番茄。这样即使不看屏幕盲操作也能控制计时体验提升非常明显。实现上要监听按键事件Zepp OS提供了onKey之类的回调。注意不同按键的键值不一样要查文档确认。我一开始把键值搞错了按半天没反应后来打印日志才找到正确的值。5.2 任务与番茄的绑定关系设计一开始我把任务和番茄记录分开存结果统计的时候对不上——不知道哪个番茄属于哪个任务。后来改成每条番茄记录里带上任务ID任务本身只存标题和状态统计时通过ID关联。这样数据结构更清晰也方便后续做每个任务花了几个番茄的统计。// 番茄记录结构 { taskId: task_001, startTime: 1699999999999, duration: 25 * 60 * 1000, completed: true }5.3 默认时长可配置25分钟是番茄工作法的经典时长但不是所有人都适用。我加了一个设置项允许在20到45分钟之间调整。设置存在本地启动计时时读取。这个功能实现起来很简单但实用性提升很大——我有个朋友习惯用45分钟一个周期没有这个配置他就用不了。5.4 完成打卡的正反馈番茄钟结束后的反馈很重要。我除了震动还在屏幕上显示一个简单的完成动画和第N个番茄的提示。别小看这个它给了用户一种积累感坚持用下去的动力会强很多。动画不用复杂一个缩放或者淡入淡出就够了手表性能有限太花哨反而卡。6. 从单机到同步数据打通思路6.1 手表与手机通信的基本原理Zepp OS应用可以通过蓝牙跟手机上的Zepp App通信官方提供了消息通道API。基本模式是手表端发送消息手机端有个配套的服务接收并处理。但这里有个限制——手机端需要有一个能接收消息的载体要么是Zepp App的小程序要么是配套的原生应用。纯手表端应用没法直接跟云端通信。我评估了一下做完整的双向同步工作量不小涉及手机端开发、消息协议设计、冲突处理等。所以二期我只做了单向的手表推送到手机把番茄记录同步过去手机端负责展示和备份。反向的任务下发暂时没做因为手表上直接新建任务已经够方便了。6.2 消息格式与容错通信消息我用JSON格式包含类型、时间戳、数据体三个字段。发送前先检查蓝牙连接状态没连接就暂存到本地队列等连接恢复后补发。这个队列机制很有必要不然手表离开手机范围时产生的记录就丢了。function sendRecord(record) { if (!isConnected()) { pendingQueue.push(record); return; } const msg { type: pomodoro_record, timestamp: Date.now(), data: record }; sendMessage(JSON.stringify(msg)); }容错方面接收端要做数据校验防止格式错误导致崩溃。我吃过这个亏——有一次发了个字段缺失的消息手机端直接报错排查了半天。7. 关于旧设备复用和AI辅助开发的一点个人体会这块GTR4现在成了我每天用得最多的设备之一不是因为它有多先进而是因为它上面跑着一个完全按我需求定制的工具。整个项目从起念到跑通大概花了两周多的业余时间其中真正写代码的时间可能只占一半剩下都在查文档、调试、踩坑。但这个过程让我对旧设备复用这件事有了新的认识很多被淘汰的硬件性能其实远没到不能用缺的只是匹配的软件。而AI辅助编程工具的出现恰好把写软件这件事的门槛降到了普通人能够到的程度。我用AI coding的过程中最大的感受是它擅长的是填空不擅长架构。你得先想清楚要做什么、怎么拆、用什么技术路线然后让AI帮你把每一块的具体实现写出来。如果你自己都没想明白指望AI给你一个完整方案结果往往是一堆跑不起来的代码。所以人的判断力依然是核心AI只是把执行效率放大了。另外一点是关于够用就好的心态。我见过太多个人项目因为追求完美而永远停留在开发中。这个番茄钟现在还有一堆可以优化的地方——界面不够精致、没有统计图表、同步只做了单向——但它已经能稳定地帮我完成每天的专注任务了。先让它跑起来再慢慢打磨这个顺序不能反。如果你也想拿手里的旧设备做点什么我的建议是从一个你每天都会用的小功能开始别贪大。手表、旧手机、旧平板甚至树莓派都是很好的练手平台。工具链现在都很成熟AI也能帮上大忙真正稀缺的是我想解决这个问题的那股劲。