☰
自建发酵数据记录系统:传感器+本地数据库实现温度、pH、重量自动监测与报警
2026/10/1 14:11:08 网站建设 项目流程

去年在朋友的小酒厂盯了整整一个发酵季的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", data=payload, 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] Description=Madeira record service After=network.target [Service] WorkingDirectory=/home/pi/madeira ExecStart=/home/pi/madeira/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

操作方法如下:

sudo cp madeira.service /etc/systemd/system/ sudo systemctl enable --now madeira.service sudo systemctl status madeira.service

这段配置里我最看重Restart=always。传感器长期挂在现场,偶尔会遇到电源波动或系统内存不足导致进程退出,没有这个配置,服务死了就是死了,要等人发现才恢复。设置成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数据库文件备份到另一个地方。我在小主机上挂了一块移动硬盘,每小时执行一次复制命令,数据安全又多了一层保障。如果再想往前走一步,可以给系统加一个继电器控制端,报警之后自动切断或启动加热棒,让记录系统升级成闭环温控系统。这个项目本身不复杂,但搭建起来之后,后续的扩展空间很大,值得花时间慢慢完善。

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

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

立即咨询