ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BrewUI 项目复盘:打造冲煮/酿造场景的数据可视化与实时监控工具

BrewUI 项目复盘:打造冲煮/酿造场景的数据可视化与实时监控工具 “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.5Python 端按行解析拆出温度和重量值然后封装成统一的数据帧送进消息队列。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 要求先建立“配方”或者叫“冲煮方案”。说到底这是一个阶段序列比如手冲咖啡可以拆成闷蒸时间 30s注水量 50g第一段注水直到总水量 150g第二段注水直到总水量 250g滴滤完成等待流速停止每个阶段有开始条件、目标量、备注字段。BrewUI 的前端提供一个很简单的表单阶段名称、目标水量、时长、可选的温度要求。保存后配方会存成本地 JSON下次可以直接加载。这里我做的关键选择是用“目标水量”而不是“注水速率”来划分阶段。原因是绝大多数家用电子秤只能精确反馈重量而注水速率其实可以通过重量数据求导算出来没必要让用户手动输入一个很难稳定的值。BrewUI 在曲线图上会自动显示“瞬时注水速度”这个值我会在后续章节讲具体算法。家酿啤酒的配方逻辑稍微复杂一点因为还会涉及温度阶梯糖化、煮沸、发酵温度以及暂停条件。我做了两种配方类型coffee和brew共用一个阶段描述结构只是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, window1.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 点时用 LTTBLargest 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 看一遍曲线和备注。它并不会告诉我哪一杯更好喝但它能把“我觉得今天的冲煮手法更稳定”这句话变成可回看的曲线、可对比的数据和可复用的配方。我以为这就是记录工具最有意思的部分——不是代替你判断而是帮你记住那些容易流失的细节。
返回列表