自建发酵数据记录系统:传感器+本地数据库实现温度、pH、重量自动监测与报警

发布时间:2026/10/1 14:11:12
自建发酵数据记录系统:传感器+本地数据库实现温度、pH、重量自动监测与报警 去年在朋友的小酒厂盯了整整一个发酵季的Excel表格之后我终于受不了了。每天去罐边抄温度、记比重、算失重数据记在纸质本子上回头要复盘时还得一张张拍照翻聊天记录。发酵这件事本身已经够复杂了我不想让记录这件事再拖后腿。于是就有了Madeira这套系统的雏形——一个用低成本硬件自建的发酵数据记录平台可以长期放在发酵罐旁边自动采集温度、pH、重量最后生成曲线和异常报警让我不用再守着罐子看数据。Madeira这个名字是和一位酿酒师朋友敲定的。马德拉酒最出名的特点是陈年过程中反复受热和氧化风味反而变得复杂而有层次。我希望这套系统也能像马德拉酒一样把平时容易忽略的细节记录下来让它们在时间的沉淀里变成真正有用的历史数据。说白了这个项目做的是“给发酵过程记笔记”只是这个笔记是用传感器和数据库来写的。这套系统适合什么人我认为主要有三类。第一类是小型精酿工坊的酿酒师需要长期跟踪发酵过程但不想花几千块去买商业记录仪也不希望数据被锁在某个厂商的App里第二类是家庭酿造爱好者想用业余时间做出一套能跑、能看、能报警的自动化监测装置第三类是做康普茶、酸奶、奶酪这类发酵食品的手工食品爱好者原理完全一样换个传感器和量程就能复用。今天的文章我会把整个项目从思路、选型到实操、避坑的过程完整讲一遍。1. 项目整体设计与思路拆解1.1 核心需求与方案取舍做这个项目的第一步不是买传感器而是先想清楚到底要解决什么问题。发酵记录这件事拆开来看有四个核心需求实时性每隔一段时间就要能看到当前发酵状态不能等到发酵结束才拿到数据。历史性一个发酵周期可能持续7天、14天甚至30天数据要能完整回放重启、断电都不能丢。可报警当温度、pH偏离设定区间时要能及时通知我否则半夜出了问题只能第二天早上才发现。可导出数据格式要开放最好能导出CSV或直接查数据库方便后续做复盘和工艺调整。这四个需求单独看都不难聚在一起就关系到架构选择。我最初考虑过两种方案一种是完全用现成的商业物联网平台把数据传到云端手机上查看另一种是本地自建一套轻量级服务数据全部落在自己控制的存储里。最终我选了第二种。理由很直接酿酒数据的价值在于长期积累如果数据传到了第三方平台平台的接口调整、服务停运、收费变化都会让历史数据变成一堆无法读取的黑箱。而本地自建虽然前期要折腾一下但数据的管理权完全在自己手里哪怕三年后回头整理导出一份CSV就能还原整个发酵过程。1.2 整体架构与数据流确定了自建路线之后我把系统分成了三层采集层、核心层、展示层。采集层由主控板和传感器组成负责定时读取温度、pH、重量等数据然后通过局域网HTTP请求发送给核心层。核心层运行在一台低功耗小主机上负责接收数据、校验清洗、写入数据库同时执行报警判断。展示层是一个轻量级网页接口把数据库里的记录读取出来形成曲线和表格。这个架构最大的好处是松耦合。采集端只要按照固定的JSON格式把数据POST到接口后端不关心你用的是哪个型号的温度探头还是称重模块。以后想增加二氧化碳浓度传感器或者把pH测量改成自动进样只需要在采集端改动后端代码一个字符都不用变。这种“接口固定、内部自由”的设计在实际使用中帮我省了很多事。1.3 为什么不用现成的发酵记录仪市面上其实已经有不少现成的发酵记录设备比如一些带WiFi的温湿度记录仪配套的手机App也能看曲线。但它们普遍有几个问题数据导出往往要额外收费报警推送方式很单一大多数只能接一种类型的传感器。更有意思的是很多设备的云端服务需要厂商服务器正常运转才能用一旦厂商停止维护设备就变成了废铁。对酿酒来说我更在意数据的可追溯性和可组合性。发酵罐旁边不只有一个测点可能是罐体温度、环境温度、环境湿度、失重曲线同时存在。商业设备一台管一种传感器数据还散落在不同的App里。自己做这套系统只需要在总线上挂不同的模块所有数据最终汇入同一个数据库前后对比非常方便。2. 核心细节解析与实操要点2.1 传感器选型温度、pH、重量怎么搭配发酵监测最常用的三类传感器我根据自己的实测经验整理成了一张对比表传感器类型采集内容推荐模块/方案精度与误差注意事项温度探头液体或环境温度DS18B20防水数字探头±0.5℃稳定性好单总线可并联多个长线需上拉电阻pH传感器液体酸碱度Analog pH meter如DFRobot需要校准长期浸泡会漂移不要长期泡在发酵液里定期用标准液校准称重模块发酵罐重量HX711称重传感器承重板精度受电源纹波影响大必须做滑动平均滤波防止读数抖动温度探头是整个系统里最可靠的部分。DS18B20是数字传感器走单总线协议一条总线上可以挂多个探头每个探头有唯一地址通过编程就能区分。防水封装版本可以直接放进发酵液里但要注意长期浸泡后探头表面会结一层生物膜影响导热测出的温度会比实际偏低。我的经验是每两周把探头拿出来用清水冲洗一下。pH传感器是最需要小心的。它本质上是模拟传感器输出信号会随时间和水质发生变化如果不校准读数会慢慢漂移。日常使用中我每三天用pH 4.0和6.86的标准液做一次两点校准。因为麻烦所以我在系统里把pH设计成“可选上报字段”不是每次采样都要求有值但一旦上报就会保留下来足够画出一条相对平滑的趋势线。重量传感器用HX711模块比较多它是24位高精度ADC可以直接和称重传感器连接。这个模块最大的问题是容易受电源纹波影响在持续通电情况下单片机供电不稳会导致同一时刻读数来回跳。后面我会专门讲怎么用滤波解决。2.2 数据接口设计与防重处理采集端与核心层之间的数据交换我设计成一个极简的HTTP JSON接口。主控板每次采样后把数据拼成下面的格式POST到服务端payload { device_id: fermenter-01, ts: 1612345678, temp: 20.3, ph: 3.85, weight_grams: 21500 }每个字段的含义都很明确device_id标记数据来源ts是Unix时间戳后面是各个指标的数值。为什么用HTTP POST而不是MQTT原因有两个。第一家庭局域网环境下HTTP协议足够简单直接任何编程语言都能方便地构造请求第二MQTT需要一个独立的Broker服务对这套项目来说增加了不必要的维护成本。防重复是必须考虑的问题。主控板通过WiFi上报数据时经常会遇到网络抖动导致请求超时但服务端已经写库成功。如果采集端在超时后重试就会写入两条相同时间戳的数据。我的做法是在数据库层面加唯一约束同一设备同一时间戳只保留一条记录写入时使用INSERT OR IGNORE。这个处理虽然朴素但非常有效能彻底避免重复数据干扰曲线。2.3 数据库表结构与存储选型有人看到“数据库”三个字就想到要安装MySQL或PostgreSQL其实没必要。家庭项目的数据量很小一天撑死几千条记录用一个SQLite单文件数据库完全够用还免去了安装配置的麻烦。我的表结构设计如下CREATE TABLE readings ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, ts INTEGER NOT NULL, temp REAL, ph REAL, weight_grams REAL, UNIQUE(device_id, ts) ); CREATE TABLE alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, device_id TEXT NOT NULL, metric TEXT NOT NULL, value REAL, threshold REAL, message TEXT );两张表的分工很明确readings存原始采样数据alerts存报警事件。为什么报警要单独建表因为报警是“偏离正常区间”的离散事件和周期性的采样记录结构不同单独存放可以避免重复扫描全表也方便在界面上高亮显示异常时间点。SQLite有一个优势特别适合这个场景备份就是复制文件。每天晚上用一条crontab命令把madeira.db复制到另一块硬盘或者网盘数据安全就有了基本保障不用搞复杂的数据库主从复制。3. 实操过程与关键环节实现3.1 第一步搭建本地后端服务后端我用的是Python加FastAPI框架。选FastAPI而不是Flask主要是因为异步接口写起来顺手而且自带数据校验能力适合处理传感器上报这类高频小请求。安装环境的过程很简单mkdir madeira cd madeira python3 -m venv .venv source .venv/bin/activate pip install fastapi uvicorn然后在项目目录建一个main.py写下最核心的接收接口from fastapi import FastAPI, Request import sqlite3 import time app FastAPI() def get_db(): return sqlite3.connect(madeira.db) app.post(/api/readings) async def receive_reading(req: Request): data await req.json() # 校验必需字段 required {device_id, ts, temp} if not required.issubset(data.keys()): return {status: error, msg: missing fields} conn get_db() conn.execute( INSERT OR IGNORE INTO readings (device_id, ts, temp, ph, weight_grams) VALUES (?,?,?,?,?), (data[device_id], data[ts], data.get(temp), data.get(ph), data.get(weight_grams)) ) conn.commit() conn.close() return {status: ok}这段代码是整个系统的心脏逻辑很直白接收JSON校验必要字段写入数据库。INSERT OR IGNORE保证了重复数据不会插入。实测在局域网环境下这个接口单次响应时间在10毫秒以内哪怕一秒来一条数据也完全扛得住。初始化数据库可以用一条Python命令或者直接用sqlite3命令行工具执行建表语句。我习惯在main.py的同目录放一个init_db.py脚本每次测试前干净重建。3.2 第二步编写主控板采集程序主控板我选了ESP32搭配MicroPython环境。DS18B20温度探头接在一个GPIO引脚上HX711称重模块接在另外两个引脚上。采集程序的逻辑是启动后连接WiFi循环执行“读探头、拼JSON、POST上报、等待下一次采样”。下面是温度采集部分的简化代码import machine import onewire import ds18x20 import network import urequests import time # 初始化DS18B20温度传感器 ow onewire.OneWire(machine.Pin(4)) ds ds18x20.DS18X20(ow) roms ds.scan() # 连接WiFi wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(home-ap, password) while not wlan.isconnected(): time.sleep(1) def upload(temp, weight): payload {\device_id\:\fermenter-01\,\ts\:%d,\temp\:%.2f,\weight_grams\:%.1f} % (time.time(), temp, weight) try: res urequests.post(http://192.168.1.10:8000/api/readings, datapayload, headers{Content-Type: application/json}) res.close() except Exception as e: print(upload failed:, e) while True: ds.convert_temp() time.sleep_ms(750) temp ds.read_temp(roms[0]) upload(temp, read_weight()) time.sleep(600) # 每10分钟上报一次这里的采样间隔我设成了600秒也就是10分钟一次。为什么不是每秒钟都上报因为发酵本身是一个缓慢的过程温度变化很少会在几分钟内剧烈波动10分钟一个点已经能画出一条非常平滑的曲线同时还能显著降低ESP32的功耗和网络请求频率。如果你做的是对温度极敏感的发酵可以改成60秒但要注意WiFi模块频繁唤醒会发热主控板别放在封闭的泡沫箱里。HX711的读数函数需要单独处理直接读取原始值是不稳定的。我的做法是连续采样10次去掉最大最小值后取平均def read_weight(): values [] for _ in range(10): values.append(hx711.read()) time.sleep_ms(50) values.sort() return sum(values[2:-2]) / len(values[2:-2])这个简单的滤波已经能显著减少抖动。后面在常见问题里我还会补充更系统的处理方法。3.3 第三步实现曲线查询和报警判断数据进来之后光存在数据库里没有意义要能看、能报警才算完整。我先写一个曲线查询接口按设备和时间范围返回记录app.get(/api/curve) def get_curve(device_id: str, hours: int 24): start_ts int(time.time()) - hours * 3600 conn get_db() rows conn.execute( SELECT ts, temp, ph, weight_grams FROM readings WHERE device_id? AND ts? ORDER BY ts ASC, (device_id, start_ts) ).fetchall() conn.close() return {data: [dict(zip([ts, temp, ph, weight], r)) for r in rows]}前端用Chart.js做一个简单的曲线图每次页面刷新时调用这个接口。为了不引入构建工具我直接写了一个静态HTML文件放在FastAPI的static目录下通过浏览器打开就能看到实时曲线。这样做虽然简陋但足够可靠不需要Node环境和打包步骤。报警判断我放在了接收接口里每次写入读数后立刻检查该读数的值和最近N条数据的中位数如果偏差超过阈值就写入alerts表。为什么不直接用原始值判断因为单个突刺可能来自传感器干扰直接报警会造成大量误报。我在报警逻辑里加了一个滑动窗口取最近5个点的中值做平滑def should_alert(device_id, metric, value): conn get_db() rows conn.execute( SELECT metric FROM readings WHERE device_id? ORDER BY ts DESC LIMIT 5, (device_id,) ).fetchall() conn.close() if len(rows) 3: return False vals sorted([r[0] for r in rows if r[0] is not None]) median_val vals[len(vals) // 2] return abs(value - median_val) ALERT_THRESHOLD[metric]这个函数只在每个采样点调用一次性能开销完全可以忽略。当判断结果超过阈值时服务端会向一个钉钉群机器人或者自定义Webhook发一条通知方便我在手机上第一时间掌握异常情况。3.4 第四步部署到低功耗小主机并开机自启核心层我跑在一台旧笔记本上安装了Ubuntu Server连上路由器放在角落。为了确保重启后服务能自动启动我写了一个systemd服务单元文件[Unit] DescriptionMadeira record service Afternetwork.target [Service] WorkingDirectory/home/pi/madeira ExecStart/home/pi/madeira/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restartalways RestartSec5 [Install] WantedBymulti-user.target操作方法如下sudo cp madeira.service /etc/systemd/system/ sudo systemctl enable --now madeira.service sudo systemctl status madeira.service这段配置里我最看重Restartalways。传感器长期挂在现场偶尔会遇到电源波动或系统内存不足导致进程退出没有这个配置服务死了就是死了要等人发现才恢复。设置成5秒后自动重启能最大程度保证系统的无人值守能力。4. 常见问题与排查技巧实录4.1 温度探头读数整体偏高或偏低这个现象我遇到过好几次表现不是随机跳变而是整体偏移。比如某一天后半夜温度曲线突然比前一夜高了1℃其他条件都没变。排查后才发现是探头表面结了一层看不见的生物膜影响了热传导。处理办法很简单把探头拿出来用清水冲洗再用干净的软布擦干。注意不要用硬刷否则会破坏防水密封层。另外还有一种可能是探头位置移动了。发酵罐底部温度通常比液面中层高0.5℃左右如果探头从罐体上部滑到了底部曲线就会出现平台式抬升。解决办法是在安装时给探头做一个固定支架不让它在发酵液中飘动。4.2 数据库里连续几小时没有数据排查这种问题时我有一套固定的顺序检查项方法常见原因主控板连接状态看ESP32的LED指示灯或串口日志WiFi掉线网关连通性ping主控板的IP地址设备休眠、天线位置差服务端日志journalctl -u madeira.service服务崩溃、端口冲突数据库写入查询readings表最新记录接口校验失败最常见的坑是ESP32的WiFi长时间运行后自动断开。MicroPython环境下WiFi连接不是长连接掉线后不会自动重连必须在上报前检查连接状态如果断开就主动重新连接。我在代码里加了一个简单的检查逻辑if not wlan.isconnected(): wlan.disconnect() wlan.connect(home-ap, password) while not wlan.isconnected(): time.sleep(1)这段代码虽然简单但解决了我90%的掉线问题。4.3 重量数据频繁抖动HX711称重模块最大的敌人是电源纹波。ESP32在WiFi发射瞬间会有较大的电流波动这个波动会通过供电线路传到HX711的参考电压上导致ADC读数跳变。我用过两个解决方案效果都不错。第一个方案是硬件隔离给HX711模块单独做一组电源滤波用100微法电解电容加0.1微法陶瓷电容并联放在模块的VCC和GND之间。第二个方案是软件滤波在读取函数里执行“连续采样10次排序后去掉最大最小各两个取中间值平均”的算法。两者结合后我的称重模块读数稳定性从±5克降到了±2克以内完全满足失重法观察发酵速率的精度需求。4.4 报警太多成了狼来了报警功能刚上线时我最头疼的其实是误报。只要某一个采样点瞬时波动稍微大一点就会触发一次报警推送一天下来手机响个不停。后来我做了两个改进一是报警判断使用5个点的中值过滤而不是单个点比较二是同一个设备同一指标在30分钟内只报警一次防止重复轰炸。实现方式就是在alerts表后加一个查询检查最近一次报警的时间戳距离是否超过1800秒。这个方法已经被我提炼成一个独立的函数并针对不同指标单独配置冷却时间。发酵温度每天的变化本来就是缓慢的连续多天报警通常说明是真有问题而不是干扰。5. 关于这个项目我的实际操作体会整套系统从开始动手到稳定运行前后花了一个多月时间。中间踩过的坑不少但我最大的体会是这个项目真正值钱的不是那几根曲线也不是报警推送而是让我养成了“对数据负责”的习惯。以前看一坛酒发酵凭的是感觉现在打开仪表盘看曲线任何一个异常的拐点都能追溯到一个具体的时间点再结合当时的操作记录找到原因这种可回溯性帮我把工艺细节提升了一个台阶。最后再分享一个小技巧定时把SQLite数据库文件备份到另一个地方。我在小主机上挂了一块移动硬盘每小时执行一次复制命令数据安全又多了一层保障。如果再想往前走一步可以给系统加一个继电器控制端报警之后自动切断或启动加热棒让记录系统升级成闭环温控系统。这个项目本身不复杂但搭建起来之后后续的扩展空间很大值得花时间慢慢完善。