BrewUI 项目复盘:打造冲煮/酿造场景的数据可视化与实时监控工具
2026/9/20 10:06:45 网站建设 项目流程

“BrewUI”这个词一开始其实挺有歧义的:老 macOS 用户会想到 Homebrew,玩精酿的朋友会想到发酵罐控制器,而我在做这个小项目的时候,心里想的却是手冲咖啡和自动化冲煮的数据可视化。其实三个方向底层的逻辑是一样的——把“酿造/冲煮”过程中的变量记录清楚,再通过一个直观的界面让人看得明白、调得放心。BrewUI 就是我业余时间搭的一套面向小型冲煮/酿造场景的开源界面工具,主打本地运行、设备直连、配方管理和实时曲线展示。这篇文章不是产品发布会,就是一次完整的项目复盘,从我为什么做它,到怎么选型、怎么接线、怎么调参,再到我实际踩过的一堆坑,都会写到。如果你也想给自己的咖啡称、温控壶或者小型发酵设备做一套监控界面,或者单纯对“硬件数据怎么变成好看曲线”这件事感兴趣,这篇应该能给你一个还不错的参考。

1. BrewUI 想解决什么问题

1.1 冲煮和酿造里最容易被忽略的事:记录

先说一个我在实际玩手冲和家酿之后最深的感觉:冲一杯咖啡只有几十秒到几分钟,但决定味道的变量非常多。粉量、水量、水温、注水节奏、闷蒸时间,每一项都在微小地影响最终萃取率。初学者经常遇到“上次明明很好喝,这次怎么完全不对”的困惑,就是因为没有留下足够细致的记录。家酿啤酒更夸张,一发二发的温度曲线、糖度变化、干投时间点,少记录一天,后面出了问题就只能靠猜。

市面上不是没有记录工具。手机 App 有至少五六款能做到计时和简单称重联动,专业级的设备像 Acaia 的电子秤也自带记录功能。但说实话,它们各有各的毛病:有的数据导出格式很封闭,有的只能配合自家硬件,还有的就是纯粹的云端订阅制,离线状态下基本没法用。我做 BrewUI 的核心动机很简单——数据应该是自己的,界面也应该能按自己习惯调整,而不是被厂商绑定。

1.2 我想要的界面到底是什么样

冷静想了一下,我其实想要的不是再做一款“电子秤 App”,而是一套能脱离固定硬件逻辑的冲煮/酿造工作台。它需要满足几个条件:

  • 数据本地优先,所有记录都以标准格式(后面我选了 JSON 和 CSV)保存在自己电脑或树莓派上。
  • 支持接入常见传感器设备:蓝牙秤、温度探头、流量计,最好还能通过串口接 Arduino 或 ESP32。
  • 界面以实时曲线为核心,但也要有配方阶段标记和冲煮备注功能。
  • 能离线跑,不强迫注册账号,不强迫联网。

听起来不复杂,但真的动手做,还是会遇到很多“想当然”和“实际不是那么回事”的地方。后面我会逐个拆开讲。

2. 整体设计与技术选型

2.1 为什么选了 Web 技术栈而不是原生应用

一开始我认真考虑过用 Electron、用 Flutter、甚至用 Qt 来做这个界面。后来我做了个很现实的决定:先用纯 Web 技术把核心功能跑通,界面用浏览器打开即可,之后如果有需要再用 Electron 包一层壳。

原因很简单。第一,Web 技术栈对实时数据的展示和调试效率非常高,浏览器里的开发者工具直接能看 WebSocket 消息、调试样式,迭代速度比原生快很多。第二,我想让 BrewUI 能跑在树莓派这种低功耗设备上,如果 UI 本身就是浏览器访问的,那服务端可以是一个轻量 Python 进程,前端资源丢过去就行,完全不需要 X Server 或者桌面环境。第三,很多家酿和咖啡玩家手头都有旧笔记本或平板,浏览器访问的方式对他们来说最没有门槛。

所以 BrewUI 的架构很清晰:

传感器/数据源 → 数据采集脚本(Python) → WebSocket/HTTP → 浏览器前端(Vue or 原生 JS)

这里说明一下,我自己后来前端用的是 Vue 3 + ECharts,采集层是 Python 3.9 跑的,设备通信用了 BLE 和串口两个通道。

2.2 通讯方案:蓝牙和串口都不完美,但能互补

这是整个项目里最折腾的一层。手冲咖啡场景最常见的电子秤,比如 Acaia Pearl,走的是蓝牙 BLE;而很多温控壶、PT100 温度探头、流量计,走的是串口或者需要自己用单片机读取。家酿设备上,像 iSpindel 这类浮子式比重计,往往通过 WiFi 上报数据,而不是传统蓝牙。

BrewUI 的做法是提供统一的“设备驱动”抽象层。每一种设备实现同样的接口:连接、断开、读取数据帧、返回当前重量/温度/流量值。这样上层完全不需要关心底层是蓝牙还是串口。BLE 通信的低功耗特性很适合电子秤这种电池设备,但缺点是连接不稳定,Windows 上的 BLE 驱动偶尔会出现掉线;串口通信稳定可靠,但需要物理接线和 USB 转串口芯片,对新手不够友好。

实际我用得最多的组合是:Acaia Pearl 通过 BLE 接入重量数据,DS18B20 防水温度探头通过 ESP32 的串口上传温度。两个数据流各自独立,BrewUI 在时间轴上做对齐。

2.3 为什么选 ECharts 而不是其他图表库

实时曲线需要的不只是“把点画出来”,还有缩放、跨时间段浏览、多数据列叠加。我先后试过 Chart.js、Plotly.js 和 ECharts。Chart.js 轻量,但多 Y 轴和实时大数据量的表现一般;Plotly 的交互很丰富,但打包体积太大,在树莓派上加载明显偏慢;ECharts 在折线图和缩放交互上的体验最好,默认就支持 dataZoom、legend 切换、markArea 标记,而且我只需要按需引入折线图模块,体积完全可以接受。

不过 ECharts 有一个地方需要自己处理:当数据点非常密集的时候,如果每收到一条数据就 setOption 一次,页面会明显卡顿。我在 BrewUI 里做了一层缓冲区,每 200ms 批量提交一次新数据,实测在 100 个点/秒的推送频率下,浏览器 CPU 占用能稳定在 10% 以内。

3. 从零跑通 BrewUI:实操记录

3.1 基础环境准备

如果你想在自己机器上复现这套流程,建议先准备好这些东西:

  • 一台能跑 Python 3.9+ 的电脑(树莓派 3B+ 以上也行,但编译依赖可能要多等一会儿)。
  • 一个支持 BLE 的 USB 蓝牙适配器,或者笔记本自带蓝牙。
  • 至少一个数据源。如果手头没有蓝牙秤,可以用 ESP32 + HX711 称重模块模拟一个,成本大约 30 块;或者直接用手机传感器?不行,称重必须硬件。
  • Node.js 18+,用于前端构建。

我建议在虚拟环境里安装 Python 依赖,不要直接怼到系统环境里。BrewUI 的依赖其实很少:bleak(蓝牙通信)、pyserial(串口通信)、fastapi(提供 API 和静态文件服务)、uvicorn(服务进程)、websockets(实时推送前端消息)。

安装命令还是很常规的:

sudo apt update sudo apt install -y python3-venv python3-pip mkdir brewui && cd brewui python3 -m venv .venv source .venv/bin/activate pip install bleak pyserial fastapi uvicorn websockets

如果你用的是 ESP32 给 BrewUI 供数据,还需要烧录一个最简单的固件,让 ESP32 周期性把 ADC 读到的重量或温度通过串口发出来。格式越简单越好,我用的就是一行 CSV 文本:

T:25.3,W:42.5

Python 端按行解析,拆出温度和重量值,然后封装成统一的数据帧送进消息队列。

3.2 配置设备:蓝牙秤怎么连,串口怎么设置

BLE 设备连起来之前,一定要先搞清楚它的数据服务 UUID。这不是随口说的。我用 Python 读取 Acaia 数据时,就需要知道它的 weight characteristic UUID,这个信息不一定写在官网文档里,很多时候要扒社区或者直接用 LightBlue 这类工具去扫。这里要提醒一句:不同固件版本的 Acaia 对数据格式的处理可能有差异,像 Pearl 2021 和 Pearl S 的行为就不完全一样。

BrewUI 里我抽象了一个比较通用的“扫描-过滤-连接-订阅”流程:

from bleak import BleakClient async def connect_scale(address): client = BleakClient(address) await client.connect() # 假设这个 characteristic 是秤体上报数据的通道 await client.start_notify(WEIGHT_CHAR_UUID, handle_weight_notify) return client

拿到原始字节之后,很多秤的协议是把重量值编码成特定格式。为了兼容不同设备,我在驱动层做了解析函数注册机制,每一种设备型号可以单独注册一个解码函数。这样即使换了秤,界面层代码完全不用动。

串口设备相对简单,但要确认波特率。DS18B20 通过 ESP32 发数据,我用的 115200 波特率,串口路径在 Linux 下常见为/dev/ttyUSB0/dev/ttyACM0,窗口下则是COM3之类的名字。BrewUI 的配置页里可以直接填串口名和波特率,连接状态实时显示。

3.3 新建一个冲煮配方

记录数据之前,BrewUI 要求先建立“配方”或者叫“冲煮方案”。说到底这是一个阶段序列,比如手冲咖啡可以拆成:

  1. 闷蒸:时间 30s,注水量 50g
  2. 第一段注水:直到总水量 150g
  3. 第二段注水:直到总水量 250g
  4. 滴滤完成:等待流速停止

每个阶段有开始条件、目标量、备注字段。BrewUI 的前端提供一个很简单的表单:阶段名称、目标水量、时长、可选的温度要求。保存后配方会存成本地 JSON,下次可以直接加载。

这里我做的关键选择是:用“目标水量”而不是“注水速率”来划分阶段。原因是绝大多数家用电子秤只能精确反馈重量,而注水速率其实可以通过重量数据求导算出来,没必要让用户手动输入一个很难稳定的值。BrewUI 在曲线图上会自动显示“瞬时注水速度”,这个值我会在后续章节讲具体算法。

家酿啤酒的配方逻辑稍微复杂一点,因为还会涉及温度阶梯(糖化、煮沸、发酵温度),以及暂停条件。我做了两种配方类型:coffeebrew,共用一个阶段描述结构,只是brew类型额外带温度设定和保持时间字段。

3.4 启动服务并打开实时界面

一切配置好之后,启动 BrewUI 非常直接:

python server.py --config ./config.yaml --port 8080

然后在浏览器打开http://localhost:8080,就能看到冲煮工作台。左侧是设备连接状态和配方阶段列表,中间是实时曲线,右侧是操作日志和备注输入框。如果你是在本机跑,连接地址就是 localhost;如果想用平板在同一局域网访问,记得启动参数里监听0.0.0.0

实测下来,从启动到看到第一帧数据,大约 2 秒。接口响应延迟主要取决于蓝牙设备的通知频率。比如 Acaia 在实时重量模式下每秒推 1 到 10 次不等,我的前端按 200ms 缓冲渲染,曲线看起来非常顺滑。

4. 几个关键细节的实现

4.1 时间轴对齐:多设备数据怎么算齐

这是最容易被新手跳过,但实际影响非常大的一点。蓝牙秤、ESP32 串口、其他传感器,它们的数据到达时间不是严格同步的。如果每个设备各自按“收到数据的时刻”打时间戳,那么后续做注水速度计算时会出现明显的毛刺,因为重量数据下一秒的差分可能被插进了一个延迟很久的旧点。

我的方案很土但很有效:统一使用后端进程的单调时钟作为主时间轴,每条数据进入消息队列时立即打上“服务器接收时间”的时间戳,而不是设备上报的时间。设备上报的时间只在需要分析数据延迟时做参考。这样就不用处理跨设备时钟同步的问题,在局域网单机场景下足够准。

但光这样还不够,ECharts 做实时横向滚动时,如果前端每收到一条新数据都推进一次时间窗口,用户几乎没法仔细看曲线上的阶段标记。我实现了一个“暂停跟随”开关,点击曲线后自动停止横轴自动滚动,再点一次恢复。这个小功能在场测时被朋友夸过很多次,说是看注水手法时最实用的一键。

4.2 瞬时注水速度:别直接用差分

我知道很多人会在做类似工具时直接对重量求一阶导:d_weight / d_time。如果你只是看个大致趋势,这没问题;但如果你在实时曲线上画这个值,会发现曲线像心电图一样狂跳。原因是电子秤本身有量化误差,再加上连续数据包的间隔不均匀,差分放大噪声的效果非常吓人。

BrewUI 里用的是滑动窗口线性回归估算速度:取最近 1 秒内所有数据点,做最小二乘拟合,斜率就是瞬时速度。这等价于一个低通滤波器,但实现起来更直观,也更容易解释给朋友听。核心代码如下:

def estimate_rate(ts_list, weight_list, window=1.0): # 只取当前时刻往前 window 秒内的点 while ts_list and ts_list[0] < ts_list[-1] - window: ts_list.pop(0) weight_list.pop(0) n = len(ts_list) if n < 3: return 0.0 avg_t = sum(ts_list) / n avg_w = sum(weight_list) / n denom = sum((t - avg_t) ** 2 for t in ts_list) if denom == 0: return 0.0 slope = sum((t - avg_t) * (w - avg_w) for t, w in zip(ts_list, weight_list)) / denom return slope

这里面的关键不是算法本身多高级,而是别用一个固定差分窗口,因为设备上报频率会变化。用时间窗口而不是点数窗口,能保证在任何上报频率下,滤波效果是一致的。

4.3 配方版本管理:比想象中重要

前面说了配方是 JSON 文件,但如果不同时候冲同一种豆子,参数稍微改了 0.5g 粉或者水温,配方文件旧版本就被覆盖了,那后面回顾时就少了一条重要对照信息。所以 BrewUI 在保存配方时做了一次“自动快照”:每次保存都会生成带时间戳的新版本,旧版本不会删除。界面里可以对比两个版本之间的差异,甚至可以直接把旧版本重新导入为当前配方。

这个需求一开始我没意识到,直到有一天我想复盘一个月前某支豆子的冲煮记录,发现配方已经被改成新的了,当时那个曲线对应的参数完全对不上。后来我才加了这个功能。虽然实现很简单:每次保存前把当前内容复制为recipe_[id]_[timestamp].json,但带来的体验提升很大。

4.4 曲线之外的记录:手写备注和分析

BrewUI 里有一个很“土”但很重要的功能:自由备注框。每次冲煮结束后,可以在界面输入风味感受、粉层状态、今天用的滤杯型号这些零碎信息。这些备注会连同传感器曲线一起保存进当次记录里。你可能会说,这不就是记笔记吗?确实,但把笔记和曲线存在同一个数据结构里,后续做对比分析时会非常方便。

我用一个简单的形如下面的数据文件来存储每次冲煮记录:

{ "id": "20250120-01", "recipe_version": "recipe_003_20250120.json", "start_time": "2025-01-20T09:30:00", "devices": ["acaia_pearl", "esp32_ds18b20"], "phases": ["bloom", "first_pour", "second_pour"], "samples": [ {"t": 0.0, "weight": 0.0, "temp": 92.1}, {"t": 0.1, "weight": 0.0, "temp": 92.0} ], "notes": "研磨度在C40上比上次调细两格,酸质明亮,body略薄" }

这个文件是一个完整的时间序列记录,既包含了原始传感器数据,也包含了配方和备注。以后如果要写分析脚本做数据分析,直接读这个 JSON 就够了,不用再回放当时的界面。

5. 实测中遇到的典型问题与排查经验

5.1 BLE 频繁断连,尤其是 Windows 上

我在 Windows 笔记本上测试时,Acaia Pearl 几乎每两分钟就掉线一次。排查思路是这样的:先排除设备省电休眠问题,把秤的自动关机时间调到最长;然后在 Python 端加了断线自动重连逻辑;最后发现问题主要出在 Windows 蓝牙协议栈和 bleak 的兼容性上。同样的代码切到 Ubuntu + CSR 蓝牙适配器,连续跑一个小时都很稳。

如果实在要在 Windows 上跑,建议开一个虚拟机跑 Ubuntu,或者直接上 Windows 的 WSL2。我后面基本就用树莓派当控制端,稳定又省心。

5.2 HX711 称重模块的噪声和漂移

我第二套测试方案是用 ESP32 + HX711 + 小型称重传感器自己搭一个蓝牙秤。HX711 如果直接用默认增益,读出数据零飘很大,甚至放一个晚上数值会偏移十几克。这个不是 BrewUI 能解决的问题,但影响了数据质量,所以我在采集层软件里加了一个“去皮校准”流程:启动后取 30 次读数的中位数作为零点,然后每隔 5 分钟用滑动平均值慢速修正零点漂移。这个技巧对日常手冲完全够用,毕竟你不会在冲煮过程中长时间不去碰秤。

筛选用中位数而不是平均值,是因为 HX711 的偶发毛刺是离群值,中位数对离群值的鲁棒性更好。这是一个小细节,但我认为很多自己搭秤的人会踩到。

5.3 前端实时曲线在长时间记录时内存越来越大

家酿啤酒一发可能持续一周,如果每 5 秒记录一个温度点,后期曲线数据会是十几万个点。ECharts 直接渲染全部点,哪怕能画出来,拖动缩放也会卡。我做了两级降采样:第一级,存储层保留原始数据文件;第二级,前端展示时只加载当前视角范围内的点,并且在窗口内超过 2000 点时,用 LTTB(Largest Triangle Three Buckets,最大三角形三桶)算法抽稀。ECharts 内置的sampling: 'lttb'可以直接用,实际效果是曲线视觉形状几乎不变,但渲染点数降一个数量级。

这个优化对咖啡这种短时间冲煮不是必须的,但如果你像我一样把 BrewUI 挂在发酵罐上监控一周曲线,就会知道这功能有多救命。

5.4 时间戳的坑:不要用time.time()做跨天累计

有一段时间我发现记录的曲线在跨午夜时会莫名其妙多出一段水平线,后来定位到是我在计算相对时间时用了绝对时间戳,而某些采样点打的是本地时区时间,跨天时冬令时或夏令时变化导致时间差跳变。解决办法是统一在数据入口就把绝对时间转换成单调递增的秒计数器。Python 的time.monotonic()很合适,因为它只用来测间隔,不受系统时间调整影响。最终保存到 JSON 的时候,我会同时存绝对 ISO 时间和相对时间,这样既方便回放,也不怕时区问题。

这个坑花了我一整个晚上排查,写出来希望能帮你少走点弯路。

6. 工具选型与人机交互细节

6.1 为什么用 FastAPI 不用 Flask

选 FastAPI 其实没有太复杂的理由。主要是 WebSocket 支持太顺手了,FastAPI 的websocket接口天然支持异步,配合uvicorn跑实时推送非常干净。Flask 要用额外插件,虽然也能用,但异步流的代码没有 FastAPI 直观。BrewUI 的数据推送需要高频率更新前端图表,异步 WebSocket 的体验明显更好。

如果你非要用 Flask,也不是不行,但请你做好把大量回调嵌套在一起的准备。我自己的经验是:当一个项目里出现了“在 Web 接口里读传感器数据,再通过 WebSocket 推给前端”这种混合场景时,FastAPI 的 Type Hints 和自动文档功能是实打实的效率提升。

6.2 操作界面的“信息密度”陷阱

做监控界面最忌讳的就是把什么数字都堆上去。我第一版界面放了重量、温度、流速、注水总量、当前阶段、上一次注水量、剩余水量、当前时间,还有曲线。结果冲咖啡的时候根本来不及看,页面太满反而抓不住重点。

后来我做了减法:主视觉区只保留重量曲线和对应的注水速度曲线,阶段标记用不同颜色的半透明条带叠加在曲线下方。所有次要数字收进右侧“详细数据”面板,需要时展开看。另外一个重要调整是,每当进入新的配方阶段,界面顶部会弹出一条接近全屏宽度的提示条,用大号字体显示“当前动作:轻柔注水至 250g”,这个提示比任何曲线上的标记都更醒目。

这里的关键思考是:冲煮者低头看界面的时间只有两三秒,所以界面必须做到“余光可读”,而不是“认真阅读”。

6.3 数据导出:别只给用户一份 PDF 或 Excel

很多设备厂商喜欢搞漂亮的 PDF 报告,但我真的厌烦那种只能看不能分析的格式。BrewUI 提供两个导出选项:完整 JSON 记录和 CSV 时间序列。CSV 可以直接拖进 Excel 或 Python 做进一步分析,JSON 则保证原始数据无损。导出操作需要一键完成,并且文件名带冲煮日期和配方名,比如20250120-yunnan-01.json

顺便说一下,BrewUI 还支持导出成一张 PNG 曲线图,以便发朋友圈或者在讨论组里交流。但这也是唯一为“分享”设计的输出格式,我不会让核心数据流程依赖 PNG,因为图片不可检索,不利于后续整理。

7. 一些值得继续做的扩展

7.1 多设备同时接入和录制回放

我下一步计划给 BrewUI 加一个“录制回放”功能。现在数据是存下来了,但回看时只能看静态图表。如果能把一次冲煮的整个状态按时间轴回放,包括当前阶段高亮和曲线增长过程,就可以非常直观地跟朋友复盘当时的注水节奏哪里出了问题。

回放功能在技术上是很有意思的,因为你要把存储的samples数组按原时间间隔重新灌进前端的数据流,就像是当初实时推送一样。前端完全不用改,只是把数据源从 WebSocket 换成本地模拟器。

7.2 建立自己的冲煮参数分析库

当记录积攒到几十条之后,BrewUI 可以加入一个简单的统计页:对比不同配方版本的平均萃取时间、相同水温下的总注水量、流速波动幅度。这些数据分析代码并不复杂,但一旦直观地把结果展示出来,你对自己的冲煮习惯会有一个非常清晰的认识。

比如我最近做的一次统计发现,我习惯在第二段注水时把流速压制得比较低,这会导致总冲煮时间偏长。以前没数据时完全没意识到这个问题,后来调整手法后,同一支豆子冲出来的风味确实更干净了。数据驱动的好处不在于“权威”,而在于让人看见自己看不见的习惯。

8. 最后几个我特别想说的经验教训

如果你看完上面这些内容,也想自己搭一套类似的记录系统,我最后分享几条我比较“肉疼”的经验。

第一,不要过度设计。BrewUI 第一版我用了 Docker、Redis、PostgreSQL,看起来非常工业级,但实际冲一杯咖啡根本不需要这些。后来我全砍掉,只用 SQLite 存元数据和 JSON 文件存原始轨迹,部署起来反而轻松很多。工具不是越重越好,能解决问题才算好。

第二,先用手动数据把前端曲线调好,再连真实传感器。我一开始就把蓝牙秤接上调试,结果完全分不清是前端显示问题还是秤的上报数据有问题。后来我先写了一个假的随机数据生成器,把曲线、阶段标记、状态提示全部调试好,再接硬件,排查问题瞬间清晰了很多。

第三,保存数据格式一定要稳定。我早期随意改 JSON 结构,导致很多老记录无法在新版本里正常读取。后来我强制在导出文件里带一个schema_version字段,哪怕以后格式大改,也能写迁移脚本把老数据升级过来。这个习惯适用于任何涉及数据记录的软件项目,而不只是 BrewUI。

我自己的使用频率是每周至少冲三四次咖啡,每次冲完都会打开 BrewUI 看一遍曲线和备注。它并不会告诉我哪一杯更好喝,但它能把“我觉得今天的冲煮手法更稳定”这句话,变成可回看的曲线、可对比的数据和可复用的配方。我以为,这就是记录工具最有意思的部分——不是代替你判断,而是帮你记住那些容易流失的细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询