《移动式面粉发酵监测容器的设计》
作品说明
一、背景分析与设计要求
摘要:本作品面向中学生物"微生物呼吸作用"实验,用行空板 M10 配合 SCI 采集模块,把 pH、二氧化碳、空气温湿度、酒精浓度五路参数集成进一个"笔盒大小"的抽拉式容器里,做到边发酵边监测、数据自动上云。容器内部用激光切割的木板与亚克力板拼成100×100×117mm 的方形发酵仓,顶部扣一块 3D 打印的绿色杯盖(104×104mm,厚 26mm,盖下凸台开 25 个通气孔),仓内埋设 pH 电极、MQ-3 酒精传感器与温湿度探头,盖面嵌行空板屏幕实时显示五路数值,并印出二维码供手机扫码进入 SIoT 平台看折线图。另一路 USB 摄像头把发酵画面转成 base64 数据流推到云平台,实现远程"看画面 + 看曲线"。整机材料清单中 6 项标价合计 1200 元。2025 年 2 月 12 日的一次 3 小时实测中,容器完整记录了酒精值 250→619、CO₂ 873→2333ppm、温度 17.06→21.01℃ 的发酵全过程,验证了"低成本、多参数、可远程"的设计目标。
关键词:面粉发酵监测 物联网(SIoT) 多传感器融合 激光切割 跨学科实践
1.1 作品背景
面粉发酵是食品工业里一道重要的生产环节,通常指小麦等谷物在缺氧条件下进行无氧呼吸的过程——酵母菌把糖类分解,产生二氧化碳和酒精。这个过程在初中生物"微生物的呼吸作用"、高中生物"细胞呼吸"里都是核心实验,学生往往要在课堂上亲自动手做一遍。
但在真实的课堂和家庭场景里,传统做法一直绕不开三个痛点:
- 数据采集粗放:判断发酵是否到位,主要靠肉眼观察面团体积膨胀、用鼻子闻酒精味,对温度、湿度、气体浓度这些关键参数没有量化记录,学生拿不到可以画图、可以比较的数字;
- 条件控制困难:自然环境里温湿度波动剧烈,实验过程中很难把条件稳定下来,同一个班不同小组的结果差异极大,很难做"控制变量"式的对照探究;
- 过程不可逆:发酵一旦开始就无法回退,靠手工定时记录容易漏掉关键节点,事后想回看"第 40 分钟发生了什么",往往只能凭印象描述。
《义务教育生物学课程标准》明确要求培养学生"科学探究"与"跨学科实践"素养,其中就包括"能够使用简单的仪器设备采集数据、分析数据并得出结论"。本项目正是基于这一要求,把物联网技术和传感器技术引入课堂实验,让学生从"看面团"升级为"读数据"。
1.2 作品主体结构
作品整体分为两个功能部分,这也是方案迭代后的最终形态:
| 区域 | 构成 | 作用 |
|---|---|---|
| 面粉收集区 | 激光切割木板 + 亚克力板拼成的方形盒子(100×100×117mm) | 放置待发酵的面粉;内部安装 MQ-3 酒精传感器、pH 电极与数字温湿度传感器;仓壁与盖板凸台上设置通气孔,便于气体交换也便于采集气体数据 |
| 数据处理区 | 顶部的 3D 打印杯盖(104×104mm)与嵌在盖面上的行空板 M10 | 行空板负责采集、运算与显示;屏幕实时刷新五路参数,二维码供手机扫码进入 SIoT 查看历史折线图;外接摄像头用于远程查看发酵画面 |
两个区域上下叠合、干湿分离:下面是装料与传感的"湿区",上面是电路与显示的"干区"。这样既保证了电子部件的安全,又让加料、取料、观察变得简单——拎起盖子就能添面粉,扣上盖子就能继续跑数据。
1.3 设计要求
1.3.1 实现功能要求
- 实时显示酸碱程度(pH 值);
- 获取空气情况(CO₂ 浓度)与温湿度;
- 行空板对采集到的数据进行分析处理,并给出当天发酵状况的判断;
- 获取酒精浓度。
1.3.2 工程结构要求
- 结构合理,不影响正常使用与放置;
- 结构牢固,不易脱落、损坏;
- 结构便携,方便移动与使用。
设计目标:围绕上面两组要求,团队给自己定下了四条硬指标——① 体积控制在"笔盒"级别,单人可携带;② 一次能容纳足够量的面粉,不因容器太小而影响发酵效果;③ 五路参数同框显示,数据可以远程访问;④ 材料尽量用学校现有的激光切割机与 3D 打印机加工,控制成本。
二、设计思维导图
整个项目的推理链条可以拆成"需求—选型—结构—验证—落地"五条支线。下图把这条思路整理成一棵放射状的思维导图:外围四个方框是四个维度的思考,中间一列自左向右依次是"背景需求 → 方案选型 → 结构设计 → 应用落地",纵向主干则是信号"输入 → 处理 → 输出 → 拓展"的完整通路。
背景需求温湿度波动难控肉眼判断不可量化方案选型五路参数 + 摄像头行空板 + SCI 采集结构设计激光切割 + 3D 打印笔盒大小·干湿分离应用落地家庭烘焙 / 食品工坊课堂分组探究实验面粉发酵监测容器104×104×143mm · 1200 元 · 5 路参数输入pH / CO₂ / 温湿度 / 酒精 4 类传感器经 SCI 采集处理行空板 M10 每轮读 5 个变量并刷新界面输出TFT 屏实时数值 + 二维码 + SIoT 折线图拓展摄像头远程看画面 / 预留称重模块接口
三、硬件选用与连接
作品的硬件走的是"一块主控 + 一个采集中枢 + 四类探头"的路线。行空板 M10 负责运算、显示和联网;SCI 采集模块作为传感器中枢,把 pH 计、空气质量传感器、温湿度传感器统一挂在 I²C 总线上;酒精传感器因为是模拟量输出,直接接在行空板的模拟引脚上;摄像头走 USB。整套接线只有 I²C 四线 + 一根模拟线 + 一根 USB 线,这也是作品能做到"笔盒大小"的原因。
3.1 硬件清单
下表是作品申报材料中的完整硬件清单。需要说明的是:MQ-3 酒精传感器在原始清单里只列了名称与功能说明,未填写数量与单价;其余 6 项均已标价,合计 1200 元。
| 序号 | 名称 | 型号 | 接口 | 数量 | 单价(元) | 功能说明 | 单位 |
|---|---|---|---|---|---|---|---|
| 1 | 行空板 | M10 | — | 1 | 399 | 主控、运算、显示、联网 | — |
| 2 | SCI 采集模块 | Gravity SCI | I²C | 1 | 269 | 传感器中枢,扩展 3 路传感器接口 | — |
| 3 | 数字温湿度传感器 | SHT31-F | I²C | 1 | 69 | Temp_Air—环境温度;Humi_Air—环境湿度 | ℃ / %RH |
| 4 | 空气质量传感器 | ENS160 | I²C | 1 | 129 | TVOC—总挥发性有机化合物;eCO₂—CO₂ 浓度;AQI—空气质量指数 | ppb / ppm / 等级 |
| 5 | pH 计 | Gravity pH V2 | 模拟(经 SCI) | 1 | 249 | PH_Water—酸碱度 | — |
| 6 | 酒精传感器 | MQ-3 | 模拟(接 P22) | 1 | 未标价 | ALcohol—酒精浓度(模拟量) | — |
| 7 | 摄像头 | FIT0892 | USB | 1 | 85 | 百万级 720P USB 广角摄像头(140°) | — |
| — | 合计 | — | — | 7 件 | 1200(6 项标价之和) | — | — |
清单里第 5 项 pH 计标注接口为"—",实际上 pH 计是通过 SCI 采集模块的模拟端口 ePort1 接入的(见 5.3 节的引脚实测),这里按源码实际连接方式标注。
3.2 硬件连接图
下图是作品的硬件连接示意。可以看到信号是"传感器 → SCI 采集模块 → 行空板"的汇流结构:pH 计、ENS160、SHT31 分别接在 SCI 采集模块的三路接口上,再由 SCI 采集模块用 I²C 总线把数据汇总给行空板;MQ-3 酒精传感器单独走一条模拟线到行空板;摄像头用 USB 直连。

图 3-1 硬件连接图:pH 计经转接板接入 SCI 采集模块,ENS160 与 SHT31 并联在采集模块上,采集模块再以 I²C 与行空板通信;酒精传感器走模拟口,摄像头走 USB
3.3 核心模块与参数
| 模块 | 型号 | 关键参数 | 在本作品中的角色 |
|---|---|---|---|
| 主控 | 行空板 M10 | 国产 4 核 1.2GHz CPU、512MB DDR3 内存 + 16GB eMMC;内置 Debian 系统,支持 2.4G Wi-Fi;板载 2.8 寸 240×320 TFT 彩屏、按键、麦克风、光线/加速度传感器与蜂鸣器;Type-C 供电(5V),工作电压 3.3V,最大工作电流 2000mA | 运算与显示核心;直接跑 Python 程序,屏幕充当设备人机界面 |
| 采集中枢 | Gravity SCI 采集模块 | I²C 通信,本作品使用地址 0x21;板载 OLED 显示屏,可现场读数;提供多路 ePort 接口,自带按键与拨码开关 | 把 pH 计、ENS160、SHT31 三个传感器统一挂到一条 I²C 总线上,既省引脚又简化接线 |
| 温湿度 | SHT31-F | 湿度 0—100%RH;温度 −40—125℃;工作电压 2.15—5.5V;PTFE 防水膜(IP67);I²C 地址出厂默认 0x45 | 提供发酵环境温度与湿度两个参数,是判断酵母活性的基础数据 |
| 空气质量 | ENS160 | 直接输出 TVOC、eCO₂、AQI 三类数据;预热时间小于 3 分钟 | 提供 CO₂ 浓度,用于观察酵母无氧呼吸释放二氧化碳的进程 |
| 酸碱度 | Gravity 可插拔 pH 计 V2 | 可插拔式 pH 电极,配自动清洗机构,可有效防止面团粘附 | 监测发酵体系的酸碱变化,为"是否过度发酵"提供参考 |
| 酒精 | MQ-3 半导体酒精传感器 | 半导体气敏原理,需预热约 30 秒完成校准;输出模拟电压 | 监测乙醇浓度——面粉无氧呼吸的直接产物,是判断发酵进程的主参数 |
| 视觉 | FIT0892 摄像头 | 百万级 720P,USB 广角 140° | 拍摄发酵画面并转 base64 上传,远端可以直观看到面团状态 |
3.4 引脚分配
下面这张表是最终定稿的引脚分配,全部来自 Mind+ 图形化程序与导出的 Python 源码,两者互相印证后确认:
| 信号 | 传感器 | 接入位置 | 读取方式 | 源码中的关键字 |
|---|---|---|---|---|
| 酸碱度 | pH 计 V2 | SCI 采集模块 ePort1 | I²C(模块地址 0x21) | PH_Water |
| 二氧化碳 | ENS160 | SCI 采集模块 ePort2 | I²C(模块地址 0x21) | ECO2 |
| 环境温度 | SHT31 | SCI 采集模块 ePort3 | I²C(模块地址 0x21) | Temp_Air |
| 环境湿度 | SHT31 | SCI 采集模块 ePort3 | I²C(模块地址 0x21) | Humi_Air |
| 酒精浓度 | MQ-3 | 行空板 P22 | 模拟量(ADC 0—4095) | Pin.P22 / ANALOG |
| 画面 | FIT0892 摄像头 | 行空板 USB 口 | OpenCV 取帧(设备号 −1 自动) | VideoCapture |
接线口诀可以记成一句话:"pH 在一、CO₂ 在二、温湿度在三,酒精单独上 P22,画面走 USB"。
接线注意:SHT31 出厂默认 I²C 地址为 0x45,如果同一总线接了两颗同址传感器需要拨动板载开关切换地址;本作品 SHT31 与 ENS160 地址不冲突,因此保持默认。另外 MQ-3 是半导体气敏元件,上电后需要约 30 秒预热,所以程序里把首轮数据视作基线、不宜直接采信。
四、结构安装与制作过程
结构是这件作品最花功夫的地方。团队最初的设想是做成"一体化"——上方一个倒面粉的漏斗,下方是传感器与处理中心,行空板的屏幕朝上显示数据。但画完图纸、切出样件之后发现一体化的问题很多:加料必须整体掀开,面粉容易洒进电路区,防水和防尘都很难处理,取料也不方便。最终方案改成"上下分体、干湿分离":下面是可以独立拿取的发酵仓,上面是集成了屏幕、电路与摄像头的盖体。
4.1 外观与结构设计
4.1.1 整体造型
最终定稿的造型是一个立方体:透明亚克力箱体保证学生能直接看到面团体积的变化,顶部一块绿色 3D 打印盖板把电路、屏幕与传感器接口全部收纳在里面,一根 pH 电极从盖面穿入、垂直悬在仓内,既不容易碰伤电极,也不影响加料。整个外形没有多余的装饰,一切以实用为本——这也是团队在"创新设计说明"里写下的原话。

图 4-1 透视效果图:透明箱体 + 绿色 3D 打印盖板,盖面嵌入行空板屏幕,pH 电极从盖面穿入仓内(图中屏幕数值为建模阶段的示意值)
4.1.2 主体尺寸
作品的正立面与侧立面尺寸完全一致,说明发酵仓是一个正方形截面的立方体。所有尺寸均按 SketchUp 设计图纸标注实测:
| 部位 | 尺寸 | 说明 |
|---|---|---|
| 发酵仓(亚克力箱体) | 100 × 100 × 117 mm | 透明亚克力板围合,可直观看到面团体积变化 |
| 盖板(3D 打印) | 104 × 104 mm,厚 26 mm | 比箱体四周各外扩 2mm,形成扣边 |
| 盖下凸台 | 60 × 60 mm,高 54 mm | 插入箱体内部定位,凸台面上开 5×5 = 25 个通气孔 |
| 屏幕窗口 | 52 × 71 mm | 盖面为行空板 2.8 寸屏预留的开窗 |
| 电极过孔 | ⌀20 mm | 位于盖面一角,供 pH 电极穿入,兼作气压平衡孔 |
| 整机外形 | 104 × 104 × 143 mm | 盖板 26mm + 箱体 117mm,接近一个笔盒的体积 |

图 4-2 正立面尺寸图:箱体 100×117mm,盖板厚 26mm,整机高 143mm

图 4-3 侧立面尺寸图:与正立面尺寸一致,确认发酵仓为正方形截面
尺寸核对说明:项目论文在描述结构时写作"透明亚克力观察窗 10×10×15cm、厚度 5mm",这是对整体外形的近似表述;本节的 100×100×117mm、143mm 等数值均取自SketchUp 设计图纸上的实测标注,以图纸为准。图纸未标注板厚,文中不作推测。
4.1.3 杯盖结构
杯盖是整个作品的结构核心,它同时承担四件事:固定行空板、固定传感器、留出气体交换通道、封闭电路区。盖板用 PLA 材料 3D 打印(切片软件 Bambu Studio),设计上分两层:上层是 104×104mm、厚 26mm 的盖板本体;下层是 60×60mm、高 54mm 的凸台,凸台正好卡进 100×100mm 的箱口完成定位。
凸台面上呈 5 行 × 5 列排布了 25 个通气孔——这是解决"密封与透气矛盾"的关键设计:孔开在下方的凸台上,既能保证仓内气体与传感器充分接触,又因为孔朝向仓内、上方还有盖板遮挡,粉尘不易直接落到电路上。

图 4-4 杯盖正立面尺寸图:盖板宽 104mm、厚 26mm;下方凸台宽 60mm、高 54mm,台面开设 25 个通气孔
4.1.4 盖面布局
盖面布局要同时满足"看得清"和"够得着"两个要求。行空板屏幕居中偏右嵌装,留出的 52×71mm 窗口正好对应 2.8 寸屏的可视区;pH 电极的过孔安排在盖面一角,避开屏幕与按键;屏幕界面下方还印有"观察折线图请登录 SIoT"与二维码,手机一扫就能进数据看板。

图 4-5 杯盖顶视尺寸图:盖板 104×104mm,一角设 ⌀20mm 过孔

图 4-6 顶视布局图:屏幕窗口 52×71mm,界面显示"发酵监测分析"标题与五路参数,并给出 SIoT 访问二维码(图中数值为建模阶段示意值)
4.2 制作过程
4.2.1 方案讨论与结构定型
制作从"怎么装"开始。团队先拿纸笔与建模软件反复推演了两个方案:一体化漏斗式与上下分体式。讨论中大家发现,一体化方案在"加面粉"这一步就已经很不方便——必须掀开整块盖板,而盖板上连着屏幕和所有传感器线,动作稍大就可能扯断杜邦线。这个结论促成了最终的分体方案。
4.2.2 程序设计与调试
结构确定后,程序与结构是并行推进的。程序主体在 Mind+ 里用图形化积木搭建,再导出为 Python 代码烧进行空板。这样做的好处是:积木层级把"初始化 → 采集 → 显示 → 上传"的流程摆得清清楚楚,出了问题能一眼看出卡在哪一块积木上。
4.2.3 切割与组装
外壳的加工走的是"激光切割 + 热熔胶拼装"路线:先绘制激光切割图纸,用激光切割机把木板与亚克力板切成指定外形,再把切好的材料用热熔胶组合拼装成箱体;3D 打印的杯盖单独加工完成后与箱体扣合。传感器与行空板的位置在拼装过程中反复调整——这部分的难点不是切割精度,而是"哪里能固定、哪里够得着"。
4.2.4 联调与排错
通电联调时最常碰到的是接触不良与读数异常。团队的做法是逐路排查:先单独验证 SCI 采集模块能否正常初始化(程序中有一个"重复执行直到初始化成功"的等待循环,如果传感器没接好,程序就会一直卡在这里),再逐路确认 pH、CO₂、温湿度、酒精四类数据是否都能读到合理值,最后才打开联网与上传功能。
4.3 迭代改进
从第一版样机到最终成品,结构上做了三轮明确的改进,每一轮都是被真实问题逼出来的:
| 轮次 | 问题 | 改进措施 | 效果 |
|---|---|---|---|
| 第一轮 | 一体式设计加料不便,面粉易洒入电路区,防水防尘都难处理 | 把整体拆成"面粉收集区 + 数据处理区"上下分体 | 加料只需取下盖体,电路与物料彻底分离,干湿分区 |
| 第二轮 | 仓内气体不流通,CO₂ 与酒精读数反应迟钝 | 在盖下凸台面上开 5×5 通气孔,并在仓体上沿留出气隙 | 气体交换顺畅,CO₂ 从 873ppm 逐步升至 2333ppm 的变化被完整记录下来 |
| 第三轮 | 内部传感器位置互相"打架",硬件固定位难以兼顾 | 更换长短不同的杜邦线,重新调整各传感器的固定位置 | 走线不再互相顶托,整体更美观,盖体扣合也更顺畅 |
4.4 成品与实测
最终成品体积接近一个笔盒,单人拎起即可搬动。实验时把配好的面团放入仓内,用毛巾包住箱体保温,扣上盖子即可开始记录。

图 4-11 成品正面:绿色盖板扣在透明箱体上,盖面可见行空板屏幕

图 4-12 发酵仓内部:白色面团置于透明仓体底部,周边留有气体交换空间

图 4-13 发酵前的状态:面团放入仓内,设备尚未加盖

图 4-14 用毛巾包裹箱体保温:发酵需要相对稳定的温度环境

图 4-15 包裹就位后的设备:屏幕上数据持续刷新,箱体保持保温状态
实验配方与注意事项:160 克面粉 + 100 克温水 + 3 克酵母 + 5 克糖,可根据实际情况等比例调整。本次实验中 pH 电极没有浸入面团内部,而是留在仓内空气中,因此 pH 读数不能代表面团的真实酸碱度(详见 5.8 节说明)。
五、编程流程分析
程序采用 Python 编写(运行在行空板内置的 Debian 系统上),主体结构是"一次初始化 + 双循环并行":主循环负责采集、显示与上传,另一个线程专门负责刷新屏幕上的日期与时间。这样的分工让时间显示始终是秒级的,不会因为采集周期而卡顿。
5.1 程序总体流程
下面这张流程图是作品在项目材料中给出的真实流程,把程序的执行顺序完整画了出来:程序启动后先导入库、初始化 GUI 与板子、连接 SIoT 服务器并启动网络循环;随后在屏幕上绘制初始文本;接着进入传感器初始化环节,这里有一个"初始化成功?"的判断——失败就打印错误信息并等待 1 秒后重试,成功才继续往下走进主循环。
5.2 界面设计
行空板的 2.8 寸屏幕被划分成固定的几行区域,所有显示对象在程序启动时一次性创建,之后每轮只更新文本内容。这种"一次创建、多次刷新"的做法比每轮重建对象要省资源得多。

图 5-2 界面设计积木:11 个显示对象在启动阶段一次性创建,包含标题、日期时间、五路参数、二维码入口与提示文字
下表是按积木参数整理的界面对象清单,坐标与字号均取自程序中真实设定:
| 对象名 | 初始文本 | X | Y | 字号 | 颜色 | 作用 |
|---|---|---|---|---|---|---|
| cc | 面粉发酵监测 | 45 | 5 | 18 | 黑色 | 界面主标题 |
| Date | (空) | 25 | 35 | 14 | 黑色 | 显示日期,由线程每秒刷新 |
| Time | (空) | 130 | 35 | 14 | 黑色 | 显示时间,由线程每秒刷新 |
| PH | PH: | 20 | 65 | 16 | 蓝色 #0000FF | 酸碱度读数 |
| ALcohol | 酒精: | 20 | 90 | 16 | 蓝色 #0000FF | 酒精浓度读数 |
| CO2 | CO2: | 20 | 115 | 16 | 蓝色 #0000FF | 二氧化碳读数(单位 ppm) |
| Temp | 温度: | 20 | 140 | 16 | 黑色 | 环境温度读数(单位 ℃) |
| Humi | 温度: | 20 | 165 | 16 | 黑色 | 环境湿度读数(单位 %RH) |
| login | (二维码) | 80 | 190 | 边长 80 | — | 扫码进入 SIoT 数据看板 |
| soumin | 观察折线图请登录SIoT | 25 | 265 | 13 | 蓝色 #0000FF | 二维码的文字引导 |
| IP | 联网提示文字 | 15 | 285 | 10 | 蓝色 #0000FF | 屏幕底部的联网信息行 |
源码细节:Humi 对象的初始文本在程序中误写成了"温度:",但每轮刷新时会用"湿度:"覆盖,所以实际显示是正确的;IP 对象用于在屏幕底部提示联网信息,其中包含网络凭据,本文在展示时已隐去具体内容。
5.3 数据采集
每个采集周期里,程序会把四类传感器的读数分别存进五个变量,再把这些变量拼接进界面文本。这一段的积木非常直观地体现了"引脚 → 变量"的对应关系:

图 5-3 数据采集积木:pH 取自 SCI 采集模块 Port1,CO₂ 取自 Port2,温度与湿度取自 Port3;酒精走行空板 P22 模拟口
| 变量名 | 数据来源 | 取值方式 |
|---|---|---|
| 酸碱度 | SCI 采集模块 Port1 | 读取 PH_Water 键值 |
| 酒精 | 行空板 P22 | 读取模拟量(ADC 0—4095) |
| co2 | SCI 采集模块 Port2 | 读取 ECO2 键值 |
| 温度 | SCI 采集模块 Port3 | 读取 Temp_Air 键值 |
| 湿度 | SCI 采集模块 Port3 | 读取 Humi_Air 键值 |
可以看出温度与湿度来自同一路 Port3——SHT31 本身就把两个参数量放在一颗芯片里,所以一次读取就能拿到两组数据。而酒精传感器输出的是连续电压,程序读取的是 0—4095 的原始 ADC 值,这也是实验记录里酒精数值是 250、619 这类整数而非物理量的原因。
5.4 摄像头初始化与画面推流
摄像头是作品的"眼睛",程序在初始化阶段就要确认它被正确识别:先创建 VideoCapture 对象并打开设备,再用一个"重复执行直到初始化完成"的等待循环防止摄像头还没就绪就开始取帧。

图 5-4 摄像头初始化积木:创建 VideoCapture 对象、打开设备,并等待直到初始化完成才继续
取到画面之后,程序并不直接上传原始图像,而是先压缩再编码:先用 OpenCV 把 BGR 转成 RGB,再用 PIL 存成 JPEG 字节流,最后用 base64 编码成一个纯文本字符串,加上 data:image/png;base64, 前缀后发布到 siot/摄像头 主题。这样做的意义在于:MQTT 是文本协议,只有把二进制图像转成文本,才能借用同一条数据通道把"数值 + 画面"一起送上去。
5.5 MQTT 通信设计
作品使用 SIoT 平台作为数据中转。程序启动时会订阅一批主题,每个采集周期再向对应主题发布数据——订阅是为了能收到平台下发的控制指令,发布则是把本地数据送上去。

图 5-5 MQTT 订阅积木:程序启动时一次性订阅 8 个主题,覆盖五路参数、两条组合数据与摄像头画面

图 5-6 数据发布积木:每轮把五路参数逐个发布,同时额外发布"CO₂+酒精"与"温度+湿度"两条组合数据
主题设计如下,其中带"组合"字样的两条是为折线图对比分析准备的:把两个相关的参量拼成一条 数值,数值 格式的消息,在 SIoT 的折线图里就能同图对比两条曲线。
| 主题 | 数据内容 | 用途 |
|---|---|---|
| siot/PH | 酸碱度 | 酸碱度单参数记录 |
| siot/ALcohol | 酒精 | 酒精浓度单参数记录 |
| siot/CO2 | co2 | 二氧化碳单参数记录 |
| siot/Temp | 温度 | 温度单参数记录 |
| siot/Humi | 湿度 | 湿度单参数记录 |
| siot/CO2ALC | "co2,酒精" 组合字符串 | 同图对比二氧化碳与酒精的变化趋势 |
| siot/TempHUMI | "温度,湿度" 组合字符串 | 同图对比温度与湿度的变化趋势 |
| siot/摄像头 | base64 编码的 JPEG 图像 | 远程查看发酵画面 |
本文展示的程序与截图中,SIoT 服务器的地址、账号与密码一律以占位符形式呈现。SIoT 是开源物联网平台,学校部署在内网环境中使用,实际部署时请把地址、账号、密码换成自己服务器的配置。
5.6 时间刷新线程
屏幕上的日期与时间需要每秒都走,但如果放在主循环里做,采集与上传就会被迫拖慢。团队的做法是把它单独放到一个线程里:线程一旦启动就进入无限循环,每秒拼接一次"年-月-日"和"时:分:秒",刷新两个文本对象后休眠 1 秒。

图 5-7 时间线程积木:线程启动后每秒刷新日期与时间,与主循环并行运行
这是本程序里唯一一处"多线程"设计,也是程序结构上最值得学习的一点——把"对时间精度要求高"的任务和"对时间精度要求低"的任务分开,互不干扰。
5.7 主程序结构
把各个模块按执行顺序串起来,就得到主程序的完整骨架。下图把"启动一次、并行两路"的结构画了出来——初始化段只执行一次,随后主循环与时间线程并行运行,两条线都把自己的数据送到 SIoT 平台:
初始化(启动阶段只执行一次)GUI → Board → SIoT 连接 → P22 模拟口 → 摄像头就绪 → 11 个界面对象 → SCI 自检(0x21)→ 订阅 8 个主题① 主循环 采集 + 上传 + 推流抓取一帧画面 → 转成 base64 文本发布到 siot/摄像头调用 mqtt_msg():读 5 路参数刷新屏幕 5 个文本对象发布 5 路 + 2 条组合数据 → 等待 1 秒② 时间线程 thread1 每秒刷新读取 年 / 月 / 日 并拼成日期读取 时 : 分 : 秒刷新 Date / Time 两个对象等待 1 秒与主循环并行运行,互不阻塞SIoT 物联网平台五路参数曲线 + 两条组合对比曲线 + 摄像头画面手机扫码或电脑端均可查看
这一步的设计要点在于"分工":主循环负责周期性采集与上传,时间线程负责每秒刷新时钟。如果把刷新时钟也塞进主循环,采集周期就会被拖慢;反过来,如果让主循环去等时钟,数据上传就会出现长时间空档。两条线并行,各管一摊,程序才稳。下图是初始化段的积木:

图 5-8 初始化段积木:SCI 采集模块初始化(直到成功)→ pinpong 板型自动识别 → MQTT 初始化 → 启动时间线程 → 进入主循环

图 5-9 程序积木全图:可看到界面设计、初始化、MQTT 订阅与发布、时间线程各段的位置关系
需要特别说明的是:主循环里除了采集数据,还有一段"抓帧并发布画面"的逻辑——如果摄像头有下一帧就抓取并转码上传,没有则打印提示。这意味着画面推流与数据上传是同一个循环里交替进行的。
程序参数差异提示:导出的 Python 代码中主循环的等待时间为 1 秒,而 Mind+ 图形化积木里该处设置的是 5 秒。两者是不同版本程序的参数,本文按图形化积木版本说明程序结构,实际运行以板上烧录版本为准。
5.8 程序运行效果与发酵数据
程序跑通之后,团队做了一次完整的发酵实测:2025 年 2 月 12 日上午 9 时 49 分装入面团,用毛巾包裹保温,连续监测到中午 13 时,历时约 3 小时 10 分钟。下面六张图是不同时刻的行空板屏幕实拍,可以看到五路参数随着发酵推进的变化:

图 5-10 09:49:07 实验起始:酒精 250、CO₂ 873ppm、温度 17.06℃、湿度 72.47%RH

图 5-11 09:59:44 酵母启动:温度升到 19.07℃,CO₂ 升至 1337ppm

图 5-12 10:50:20 发酵旺盛:CO₂ 1815ppm,湿度升至 81.95%RH

图 5-13 11:23:00 持续产气:CO₂ 1944ppm,酒精 424

图 5-14 11:51:22 酒精峰值:达到 619,为本次实验的最高值

图 5-15 12:59:24 趋于平稳:CO₂ 2322ppm,温度稳定在 20.94℃
把各时间点的记录整理成表,发酵过程的全貌就清楚了:
| 时间 | ALC(酒精) | CO₂(ppm) | TEMP(℃) | HUMI(%RH) | PH |
|---|---|---|---|---|---|
| 09:49 | 250 | 873 | 17.06 | 72.47 | 14.47(无效) |
| 09:59 | 295 | 1337 | 19.07 | 86.30 | 14.31 |
| 10:50 | 391 | 1815 | 20.77 | 81.95 | 14.29 |
| 11:23 | 424 | 1944 | 20.95 | 81.14 | 14.44 |
| 11:51 | 619 | 2095 | 20.94 | 83.03 | 14.31 |
| 12:33 | 595 | 2287 | 21.01 | 86.62 | 14.38 |
| 12:59 | 601 | 2322 | 20.94 | 88.49 | 14.41 |
| 13:00 | 593 | 2333 | 20.92 | 88.48 | 14.48 |
这些数据同步上传到 SIoT 平台后,可以导出成折线图做趋势分析:

图 5-16 酒精(ALC)折线图:从 250 起步,11:51 达到峰值 619 后回落并趋稳

图 5-17 二氧化碳折线图:全程单调上升,873ppm → 2333ppm,是本次实验中最典型的发酵信号

图 5-18 温度折线图:起始 17.06℃,前 20 分钟内快速升至 20.77℃,之后稳定在 20.9—21.0℃

图 5-19 湿度折线图:从 72.47%RH 升至 88.48%RH,与面团产气产水的过程相吻合
从数据里能读出三个明确的结论:
- 酒精与二氧化碳同向上升,印证了无氧呼吸。CO₂ 从 873ppm 增长到 2333ppm,酒精值从 250 涨到峰值 619,两条曲线虽然量纲不同,但上升段高度重合——这正是酵母菌无氧呼吸"生成酒精和二氧化碳"的直接证据,比课本上的一句话描述要有说服力得多;
- 温度稳定说明保温措施有效。整个 3 小时里,温度只在前 20 分钟从 17.06℃ 跃升到 20.77℃,之后就一直稳定在 20.9—21.0℃ 的窄区间内,说明毛巾包裹确实起到了保温作用,也为"控制变量"提供了条件;
- 发酵存在明显的阶段特征。11:51 前后酒精值出现峰值后回落(619→595→601→593),而 CO₂ 仍在缓慢上升,说明此时产气仍在继续、但酒精积累已接近上限。如果能结合称重数据推断面团失重率,就能进一步判断发酵终点。
关于 pH 数据:本次实验中 pH 读数在 14.29—14.48 之间小幅波动,明显超出 0—14 的常规 pH 量程,属于无效数据。项目材料中对此有明确说明——实验时 pH 电极没有浸入面团,而是留在仓内空气中,因此这个读数不能代表面团的真实酸碱度。pH 通道在硬件与程序上均已打通,后续实验中只需把电极插入面团即可获得有效读数。
为了验证远程访问功能,团队还在数据平台上查看了实时监控曲线。下面这组截图是 SIoT 平台的消息监控界面,横轴为真实时间戳,可以确认数据确实按周期上报到了平台:
![图 5-20 SIoT 平台 [siot/Temp] 消息监控界面:横轴为真实时间戳,曲线持续上报](https://img.dfrobot.com.cn/mc-front/5d1c0c254db88f1df9d7edb3/76296a81f1465e1bf580733e301a54db.jpg?imageView2/2/format/webp/q/80)
图 5-20 SIoT 平台 [siot/Temp] 消息监控界面:横轴为真实时间戳,曲线持续上报
![图 5-21 SIoT 平台 [siot/Humi] 消息监控界面](https://img.dfrobot.com.cn/mc-front/5d1c0c254db88f1df9d7edb3/90f182e0590e53a2ca2445fed1d966bd.jpg?imageView2/2/format/webp/q/80)
图 5-21 SIoT 平台 [siot/Humi] 消息监控界面
![图 5-22 SIoT 平台 [siot/CO2] 消息监控界面](https://img.dfrobot.com.cn/mc-front/5d1c0c254db88f1df9d7edb3/2593e84869a6eb55a9e682d538236942.jpg?imageView2/2/format/webp/q/80)
图 5-22 SIoT 平台 [siot/CO2] 消息监控界面
![图 5-23 SIoT 平台 [siot/ALcohol] 消息监控界面](https://img.dfrobot.com.cn/mc-front/5d1c0c254db88f1df9d7edb3/85e8d4307f3a4cbe3d698cdafee7a119.jpg?imageView2/2/format/webp/q/80)
图 5-23 SIoT 平台 [siot/ALcohol] 消息监控界面
![图 5-24 SIoT 平台 [siot/PH] 消息监控界面](https://img.dfrobot.com.cn/mc-front/5d1c0c254db88f1df9d7edb3/c437767f20c4f94a83a8f9fb83f214fc.jpg?imageView2/2/format/webp/q/80)
图 5-24 SIoT 平台 [siot/PH] 消息监控界面
截图批次说明:上面 5 张平台截图的时间戳为 2025-02-09,是设备联调阶段的记录,与 2 月 12 日的正式发酵实验不是同一批次,因此曲线上的数值区间与 5.8 节的数据表并不对应。这里保留它们是为了证明"数据能够稳定上报到平台"这一功能本身。
六、参考代码
下面是由 Mind+ 图形化程序导出的 Python 源码(运行环境 Python 3.6 + 行空板 Debian 系统),已逐行加上中文注释。需要特别说明的是:源码中的 SIoT 服务器地址、账号、密码、客户端 ID 以及屏幕底部提示的 Wi-Fi 名称与密码,在本文中一律以占位符形式呈现,实际部署时请替换为自己服务器的配置。
# -*- coding: UTF-8 -*-
# MindPlus
# Python
import cv2 # OpenCV:摄像头取帧
import time # 时间模块:延时、时间格式化
import siot # SIoT 物联网库:MQTT 通信
import datetime # 日期时间模块
from unihiker import GUI # 行空板屏幕 GUI 库
from pinpong.board import Board # pinpong 板卡库
from pinpong.board import Board, Pin # 引脚对象
from pinpong.extension.unihiker import * # 行空板板载资源扩展
import base64 # base64 编码:把图像转成文本
from io import BytesIO # 内存字节流:JPEG 编码中转
from PIL import Image # 图像处理
import sys
# 把 SCI 采集模块的第三方扩展库加进搜索路径
sys.path.append("/root/mindplus/.lib/thirdExtension/liliang-gravitysci-thirdex")
from dfrobot_rp2040_sci import * # SCI 采集模块驱动
# ================= 配置区:部署时请改为自己的服务器参数 =================
SIOT_SERVER = "0.0.0.0" # SIoT 服务器地址(占位符)
SIOT_PORT = 1883 # MQTT 端口
SIOT_USER = "****" # 平台账号(占位符)
SIOT_PWD = "****" # 平台密码(占位符)
SIOT_CLIENT = "****" # 客户端 ID(占位符)
WIFI_SSID = "****" # 屏幕底部提示的 Wi-Fi 名称(占位符)
WIFI_PWD = "****" # 屏幕底部提示的 Wi-Fi 密码(占位符)
# 自定义函数
def MQTT():
"""启动阶段一次性订阅全部主题:五路参数 + 两条组合数据 + 摄像头画面"""
siot.getsubscribe(topic="siot/PH") # 订阅酸碱度
siot.getsubscribe(topic="siot/ALcohol") # 订阅酒精浓度
siot.getsubscribe(topic="siot/CO2") # 订阅二氧化碳
siot.getsubscribe(topic="siot/Temp") # 订阅温度
siot.getsubscribe(topic="siot/Humi") # 订阅湿度
siot.getsubscribe(topic="siot/CO2ALC") # 订阅"CO2+酒精"组合数据
siot.getsubscribe(topic="siot/TempHUMI") # 订阅"温度+湿度"组合数据
siot.getsubscribe(topic="siot/摄像头") # 订阅摄像头画面
def mqtt_msg():
"""单个采集周期:读 5 路传感器 → 刷新屏幕 → 发布到 SIoT"""
# ---- 1. 采集:四类传感器共读 5 个参量 ----
SuanJianDu = SCI1.get_value1(SCI1.ePort1, "PH_Water") # pH 值,取自采集模块 Port1
JiuJing = p_p22_analog.read_analog() # 酒精原始 ADC 值(0-4095),P22 模拟口
co2 = SCI1.get_value1(SCI1.ePort2, "ECO2") # 二氧化碳,取自 Port2
WenDu = SCI1.get_value1(SCI1.ePort3, "Temp_Air") # 空气温度,取自 Port3
ShiDu = SCI1.get_value1(SCI1.ePort3, "Humi_Air") # 空气湿度,同取 Port3
# ---- 2. 显示:把读数拼进界面文本并刷新 ----
PH.config(text=(str("P H: ") + str(SuanJianDu))) # 酸碱度
ALcohol.config(text=(str("酒精:") + str(JiuJing))) # 酒精
CO2.config(text=(str("CO2: ") + str((str(co2) + str(" ppm"))))) # CO2,带 ppm 单位
Temp.config(text=(str("温度:") + str((str(WenDu) + str(" ℃"))))) # 温度,带 ℃ 单位
Humi.config(text=(str("湿度:") + str((str(ShiDu) + str(" %RH"))))) # 湿度,带 %RH 单位
# ---- 3. 上传:逐个参数发布到对应主题 ----
siot.publish_save(topic="siot/PH", data=SuanJianDu) # 酸碱度
siot.publish_save(topic="siot/Temp", data=WenDu) # 温度
siot.publish_save(topic="siot/Humi", data=ShiDu) # 湿度
siot.publish_save(topic="siot/CO2", data=co2) # 二氧化碳
siot.publish_save(topic="siot/ALcohol", data=JiuJing) # 酒精
# 组合数据:拼成"值1,值2",便于在平台上同图对比两条曲线
siot.publish_save(topic="siot/CO2ALC",
data=(str(co2) + str((str(",") + str(JiuJing)))))
siot.publish_save(topic="siot/TempHUMI",
data=(str(WenDu) + str((str(",") + str(ShiDu)))))
# 事件回调函数
def u_thread1_function():
"""时间刷新线程:每秒更新一次屏幕上的日期与时间"""
while True:
# 拼接"年-月-日"(源码把三个 get_time 结果逐层嵌套,此处等效果表达)
RiQi = (str(datetime.datetime.now().year) + str((str("-") + str((
str(datetime.datetime.now().month) + str((str("-") + str(
datetime.datetime.now().day))))))))
ShiJian = time.strftime("%H:%M:%S") # 取"时:分:秒"
Date.config(text=RiQi) # 刷新日期文本
Time.config(text=ShiJian) # 刷新时间文本
time.sleep(1) # 每秒一次
# ================= 主程序 =================
u_gui = GUI() # 创建 GUI 对象
Board().begin() # 板卡初始化(自动识别板型)
# 连接 SIoT 平台(地址/账号/密码/客户端 ID 见配置区占位符)
siot.init(client_id=SIOT_CLIENT, server=SIOT_SERVER, port=SIOT_PORT,
user=SIOT_USER, password=SIOT_PWD)
siot.connect() # 建立 MQTT 连接
siot.loop() # 启动网络循环(后台持续收发)
p_p22_analog = Pin(Pin.P22, Pin.ANALOG) # 把 P22 设为模拟输入(酒精传感器)
def frame2base64(frame):
"""把一帧图像转成 base64 文本:BGR→RGB→JPEG→base64"""
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # OpenCV 默认 BGR,先转 RGB
img = Image.fromarray(frame) # 转成 PIL 图像对象
output_buffer = BytesIO() # 建一个内存缓冲区
img.save(output_buffer, format='JPEG') # 存成 JPEG 写入缓冲
byte_data = output_buffer.getvalue() # 取出字节数据
base64_data = base64.b64encode(byte_data) # 编码为 base64
return base64_data # 返回 base64 文本
def base642base64(frame):
"""给 base64 文本补上 data URI 前缀,方便前端直接渲染"""
data = str('data:image/png;base64,') # 前缀
base64data = str(frame2base64(frame)) # 取 base64 文本
framedata = base64data[2:(len(base64data) - 1)] # 去掉 b' 与结尾的 '
base642base64_data = data + str(framedata) # 拼接
return base642base64_data
vd = cv2.VideoCapture() # 创建摄像头对象
vd.open(-1) # 打开设备(-1 表示自动选择)
while not (vd.isOpened()): # 直到摄像头就绪才继续
print("摄像头初始化中......")
print("摄像头初始化完成")
# ---- 创建界面对象(一次创建,后续只改文本)----
cc = u_gui.draw_text(text="面粉发酵监测", x=45, y=5, font_size=18, color="#000000")
Date = u_gui.draw_text(text=" ", x=25, y=35, font_size=14, color="#000000")
Time = u_gui.draw_text(text=" ", x=130, y=35, font_size=14, color="#000000")
PH = u_gui.draw_text(text="PH:", x=20, y=65, font_size=16, color="#0000FF")
ALcohol = u_gui.draw_text(text="酒精:", x=20, y=90, font_size=16, color="#0000FF")
CO2 = u_gui.draw_text(text="CO2:", x=20, y=115, font_size=16, color="#0000FF")
Temp = u_gui.draw_text(text="温度:", x=20, y=140, font_size=16, color="#000000")
Humi = u_gui.draw_text(text="湿度:", x=20, y=165, font_size=16, color="#000000")
# 二维码:扫码即可进入 SIoT 数据看板(地址见配置区占位符)
login = u_gui.draw_qr_code(text="http://" + SIOT_SERVER + ":8080", x=80, y=190, w=80)
soumin = u_gui.draw_text(text="观察折线图请登录SIOT", x=25, y=265, font_size=13, color="#0000FF")
# 屏幕底部联网提示行:原程序在此处显示 Wi-Fi 名称与密码,本文以占位符变量呈现
WIFI_HINT = "Wi-Fi:" + WIFI_SSID + " 密码:" + WIFI_PWD
IP = u_gui.draw_text(text=WIFI_HINT, x=15, y=285, font_size=10, color="#0000FF")
# ---- SCI 采集模块初始化:失败就重试 ----
SCI1 = DFRobot_RP2040_SCI_IIC(addr=0x21) # I²C 地址 0x21
while SCI1.begin() != 0: # 返回非 0 表示初始化失败
print("Initialization Sensor Universal Adapter Board failed.")
time.sleep(1) # 等 1 秒后重试
print("Initialization Sensor Universal Adapter Board done.")
MQTT() # 订阅全部主题
print("MQTT初始化完成")
thread1 = u_gui.start_thread(u_thread1_function) # 启动时间刷新线程
# ---- 主循环:推流画面 + 采集上传 ----
while True:
if vd.grab(): # 摄像头有下一帧
ret, grab = vd.read() # 读出该帧
siot.publish(topic="siot/摄像头",
data=base642base64(grab)) # 转 base64 后上传
else:
print("没有下一帧") # 没帧则提示
mqtt_msg() # 采集 5 路并上传
time.sleep(1) # 每轮间隔 1 秒(积木版为 5 秒)
代码阅读要点:① 五个变量名(酸碱度、酒精、co2、温度、湿度)对应五路数据;② siot.publish_save 与 siot.publish 的区别在于前者会把数据同时存入平台数据库(这也是能导出折线图的原因);③ 摄像头取帧与数据采集在同一个循环里,靠 vd.grab() 的返回值判断是否有新帧。
七、作品创新点
- 物联网功能:把作品采集并处理好的数据发送到 SIoT 服务器,可供远程访问与操作,便于后期对数据进行分析处理、得出结论。屏幕上的二维码让学生用手机扫一下就能看到折线图,不需要任何额外软件。
- 简单便携:整个作品设计为"笔盒"大小(104×104×143mm),便于单人携带;数据由行空板程序自动分析处理,使用者无需进行过多操作,扣上盖子就开始记录,这在课堂分组实验场景中非常关键。
- 立体化分析:如果只检测一个酒精浓度去判断面粉发酵情况,显然是比较片面的。作品在检测酒精浓度的同时增加了 pH 计、空气质量传感器与温湿度传感器,检测结果更加立体合理,便于学生进行分组探究分析。
- 外观合理:没有使用过多的装饰性设计,完全以实用性为本——透明箱体保证能看见面团、绿色盖板把电路藏起来、盖下凸台开孔兼顾透气与防尘。
- 视觉远程查看:使用 USB 摄像头对发酵过程进行拍摄,画面转成 base64 文本后借助同一条 MQTT 通道上传,便于远程查看发酵进度并进行管理。
值得单独一提的是第 5 条的设计巧思:MQTT 本身是文本协议,无法直接传图像。团队用"JPEG 压缩 + base64 编码"把图像变成字符串,复用了已有的数据通道,没有额外增加服务器或协议——这对中学生项目来说是一个相当务实的工程取舍。
八、项目应用拓展
这件作品的价值不限于一次演示实验。把它放到不同的场景里,还能延伸出多种用法:
| 应用场景 | 使用方式 | 价值 |
|---|---|---|
| 中学生物课堂分组实验 | 每组一套设备,同时跑发酵实验,数据实时汇总到同一平台 | 把"看面团膨胀"变成"读数据曲线",为控制变量与对照实验提供量化依据 |
| 跨学科项目式学习 | 生物讲呼吸作用、物理讲传感器原理、信息技术讲物联网协议,共用一套设备 | 同一件作品串起三大学科知识点,是典型的多学科融合载体 |
| 家庭烘焙与食品加工 | 把容器放在面团旁边,随时掌握发酵进度与温度 | 减少"发过头"或"发不起来"的情况,让经验判断有了数据支撑 |
| 食品工坊与小型生产线 | 多台设备并联使用,统一查看各批次发酵状态 | 在不高的成本下实现多点位监控,便于标准化管理 |
| 功能扩展:称重监测 | 利用作品预留的 I²C 接口扩展称重模块,记录面团质量损失率 | 质量损失率是发酵强度的另一个重要指标,可与 CO₂ 数据互相印证 |
| 功能扩展:发酵终点预测 | 基于已积累的历史数据训练简单模型,自动判断发酵是否完成 | 把"什么时候发好"这个老师傅的经验问题,变成可以计算的问题 |
团队在项目材料中也提到,后续会参考市面上的相似商品,在现有基础上增加远程遥控、蓝牙等功能。而 3D 打印模型文件采用 PLA 材料,学校可以自行加工容器外壳,这也意味着作品具备被复制和推广的基础——一套图纸、一份代码,其他学校就能做出自己的版本。





