我做了一个智能水杯秤,试着让喝水记录真正无感
用 ESP32、称重传感器和稳定段算法,自动识别办公室里的喝水、加水与拿杯动作。
「每天多喝水」是一件所有人都知道,却很难长期坚持的事情。
很多饮水 App 的问题不在提醒,而在记录:每喝一次水,都要拿起手机、打开 App、选择容量、再点确认。坚持几天后很容易放弃。
灵感来源
这个项目最初的触动,来自身边几个朋友陆续查出肾结石。医生的嘱咐很一致:必须长期、大量地喝水,才能降低复发风险。
但观察下来,真正卡住他们的不是「不知道要喝」,而是「喝了多少没记录、没反馈」,时间一长饮水量又悄悄回落。这让我想做一个不需要主动操作的记录设备——只是安静地记录数据和提醒,不改变原有习惯。它看起来只是工位上的一个杯垫,却能感知杯子重量变化,自动判断喝了多少、什么时候加了水,以及一天喝水的频率。
项目的基本想法
核心结构很简单:
ESP32 读取称重数据,通过 Wi-Fi 把重量样本推送到本地服务端,再由服务端识别出喝水和加水事件。但真正设计时马上遇到一个问题:
重量下降,并不一定意味着喝水。
用户可能只是碰了一下杯子,也可能端起来又原样放回,甚至把手机、钥匙临时放到杯垫上。如果只看前后重量差,误报会非常多。
因此,这个项目真正的核心不是称重,而是 如何理解重量变化。
不记录每一次波动,只记录稳定状态
称重传感器的数据一直在变。桌面震动、杯中水晃动、传感器漂移,都会带来几克甚至十几克的波动。
所以我没有直接使用瞬时重量,而是先判断「稳定段」。一个稳定值需要满足:
- 连续约 2 秒重量变化处于允许范围(空载判定放宽到约 0.8 秒,放回判定约 1.5 秒)
- 剔除明显异常值
- 多次采样后取平均或中位数
- 重量变化小于约 8 克时,不作为饮水事件上传
系统处理的不再是每一条原始数据,而是一串稳定状态:
这三个状态,比一条不断抖动的重量曲线更有意义。
如何识别一次喝水
一次典型的喝水过程:
- 杯子原本放在底座上
- 用户拿起杯子
- 杯垫重量变成 0
- 用户喝水
- 杯子重新放回
- 新重量小于拿起前的重量
因此喝水事件可以表示为:
当 A > B 时:
喝水量 = A - B
例如 520g → 0g → 455g,可判断本次约喝水 65g。水的密度接近 1g/ml,可近似记为 65ml。
更明确的历史条件是:
上一次稳定重量是 0,并且上上次稳定重量大于当前重量,则判断为喝水。
这正是项目中最关键的一条规则。
如何识别加水
加水与喝水相似,只是最终重量变大:
当 C > A 时,加水量 = C - A。例如 180g → 0g → 610g,可理解为拿起后加入了约 430g 水。
系统不需要用户点击「加水」,也不需要知道杯子标称容量,只需要观察一次完整的拿起和放回。
为什么必须经过 0
如果重量从 520g 直接变成 460g,并不能确认发生了喝水——可能是杯子被推到边缘受力异常,也可能是其他物品被拿走。
而经过稳定 0,意味着杯子曾经完整离开承重面板。系统才能把事件理解为:
因此 A → 0 → B 比单纯的 A → B 稳定得多。
每天的统计方式
早期还考虑过更简单的逻辑:每天 0 点新开一轮、累积重量减少量、小于 10g 不上报。实现成本低,但换杯、移动和临时放置物品仍容易干扰。
更稳妥的方案是以事件为核心:
{
"event_type": "drink",
"before_weight_g": 520,
"after_weight_g": 455,
"delta_g": 65,
"recorded_at_local": "2026-07-20T10:32:14+08:00"
}
服务端再按天聚合:当日总量、次数、每小时喝水曲线、事件明细和加水次数。
ESP32 的配网与运行
设备放在办公室工位上,不能依赖串口配网。首次启动时它会创建一个开放热点 Drinkwater-Scale-XXXX,手机直接连上后打开 192.168.4.1 完成配置:
- ESP32 创建开放配置热点(无需 Wi-Fi 密码)
- 手机连接热点,打开本地配置页
- 输入设备密码(OLED 上显示,防止局域网内他人误操作)
- 选择办公室 Wi-Fi 并输入密码
- 配置本地 Flask 服务地址和认证 Token
- 保存并重启
- 页面提示如何开始运行
配网完成后进入普通模式,自动连 Wi-Fi 并向本地服务端推送样本。Wi-Fi 还有一层智能回退:如果保存的 Wi-Fi 怎么都搜不到,设备会自动清掉旧凭据退回纯热点模式,换环境也不用插串口擦配置。
校准是强制两步:先空载去皮归零,再放上已知重物输入克数校准。没去皮直接校准会被拒绝。去皮偏移和校准系数都存进 NVS,掉电不丢。
用 ESP-IDF 开发学习成本更高,但对长期运行、网络管理和状态控制更合适。
设备本身要能被维护
一个长期运行的小设备,维护成本往往被低估。所以我在固件里多做了几件事:
- OLED 实时反馈:一块 0.96 寸 SSD1306,四行显示当前重量、稳定状态、IP 和推送状态;没连 Wi-Fi 时显示热点名和设备密码,不用查文档就能配网
- 内嵌网页控制台:设备自己跑一个单页应用,输入设备密码后可以去皮、校准、配网、配置推送地址,也能远程恢复出厂
- 串口命令:Type-C 连上电脑,115200 波特率输入
RESET/APMODE/STATUS/FLASH,分别恢复出厂、仅清 Wi-Fi、查状态、进刷机模式 - 网页 USB 刷机:浏览器配合 Web Serial,选串口就能一键烧录 bootloader + 分区表 + 应用三件套,不用装工具链
这些功能不是产品卖点,而是让设备坏了能修、换环境能配、固件能更新。对个人项目来说,可维护性比功能多更重要。
事件识别放在本地服务端,而不是 ESP32
一开始我倾向于让 ESP32 自己判断喝水事件,只把结果上传。但实际做下来,职责是这样拆分的:
- ESP32 负责 EMA 低通滤波、去皮、校准,以及一个轻量的稳定状态机(区分
UNSTABLE/ZERO/STABLE/LIFTED) - ESP32 不上传每一条原始采样,只在状态变化、稳定重量变化超过 8 克,或超过 15 秒心跳时推送一次
- 真正的稳定段判定和喝水/加水识别,放在本地 Flask 服务上完成
- Flask 把事件写入 SQLite,并提供当天喝水次数、总量、每小时曲线和事件列表
这样拆分的好处是:ESP32 的固件保持简单稳定,事件逻辑可以随时在服务端调整阈值,而不必重新刷固件;同时网络流量极低,断网时 ESP32 仍能继续采样,网络恢复后补传。
饮水事件本来也不需要上云。整套数据都留在本地,更符合一个长期摆在工位上的小工具的定位。
结构设计比想象中更重要
为了适合长期摆在工位上,我已经做了一套 3D 打印外壳,大致四层:

设计重点包括:杯子放在不同位置仍准确;承重面板不碰底壳;泼洒后不易进水;外壳防滑;传感器不受侧向力;拆装和校准不能太复杂。
外观上,我不想做成普通电子秤,更喜欢类似《健身环大冒险》通关站立台那种轻微仪式感,同时保持简洁。它不是医疗设备,也不是强制管理工具,而是桌面上的轻量反馈装置。
产品真正的价值是什么
表面上在记录喝水量,更有价值的部分是「无感」。用户不必改变原有习惯:
- 仍然用自己的杯子
- 仍然像平时一样拿起放下
- 不需要点按钮
- 不需要每次打开 App
- 不需要买带特殊结构的智能水杯
设备只负责观察重量变化,并把行为转换成记录。这类产品的关键,不是增加更多提醒,而是尽量减少用户需要完成的步骤。
仍然存在的问题
还需要继续处理边界情况:
- 更换杯子
- 杯子没有完全放回中央
- 杯子和其他物品同时放在底座上
- 端起杯子但没有喝水
- 用吸管时杯子始终不离开底座
- 水温变化引起微小重量变化
- 长时间运行后零点漂移
- 桌面震动导致稳定段判断失败
其中吸管场景尤其特殊。如果杯子始终不离开底座,需要另一种模式:在足够长的稳定时间内,检测到持续、缓慢且超过阈值的重量下降。
但第一版我仍更倾向只支持「拿起杯子喝水」——清晰的行为边界,比覆盖所有场景更重要。
写在最后
我喜欢这个项目,是因为它没有试图发明一种新的喝水方式,只是让原本已经发生的行为可以被可靠地记录。
一块称重传感器、一个 HX711 和一块 ESP32 并不复杂。真正需要设计的,是设备如何从一串不稳定的重量数字中,理解用户刚刚做了什么。
当硬件不再要求用户配合,而是安静地适应用户原有习惯时,它才开始真正变得有用。