这个项目来自一次接单需求,目标是完成《物联网通信与控制技术》课程设计中的客户端软件部分。题目方向是物联网通信与控制系统,具体场景选为水产养殖智能测控。
背景
这个项目来自一次接单需求,目标是完成《物联网通信与控制技术》课程设计中的客户端软件部分。题目方向是物联网通信与控制系统,具体场景选为水产养殖智能测控。
客户的核心诉求并不是接入真实云平台,而是完成一个能本地运行、能和 ModSim 模拟传感器通信、能展示测控过程、能打包成 EXE 的课程设计系统。因此技术选型上没有走复杂的前后端工程化路线,而是选择了更稳妥的本地 Web 服务:
- Python 标准库实现 HTTP 服务和 Modbus TCP 通信。
- SQLite 做本地持久化。
- 原生 HTML/CSS/JavaScript 实现客户端页面。
- pywebview 将本地 Web 页面封装为桌面程序。
- PyInstaller 打包为 EXE。
这个组合的好处是部署简单、依赖少、容易解释,也比较符合课程设计对“通信协议、传感器模拟、客户端软件开发”的考核重点。
快速预览







系统目标
系统围绕水产养殖池塘中的五个关键指标展开:
| 指标 | 单位 | 说明 |
|---|---|---|
| 溶解氧含量 | mg/L | 反映水体供氧情况 |
| 水位值 | m | 反映养殖池水位 |
| pH 值 | pH | 反映水体酸碱性 |
| 养殖水温 | °C | 反映养殖水环境温度 |
| 氧气浓度 | % | 反映供氧环境状态 |
对应的执行设备包括:
- 增氧机
- 补水泵
- 排水阀
- 循环泵
- 恒温调节器
- pH 调节泵
- 氧气调节阀
最初容易把系统做成一个“数据看板”:ModSim 改数值,页面显示数值。但这种效果比较薄,答辩时也很难体现“控制”。因此实现中额外加入了设备连续运行模型,让设备开关和运转速率可以持续改变传感器指标。
例如打开补水泵后,水位会持续上升;打开增氧机后,溶解氧和氧气浓度会持续上升;pH 调节泵则会让 pH 逐步靠近后台设置的目标值。
技术架构
系统可以分为六层:
- 传感器模拟层:ModSim32 模拟保持寄存器。
- 通信层:Python 实现 Modbus TCP 读取和写入。
- 数据处理层:采集线程处理设备联动、报警、存储。
- 服务接口层:HTTP API 和 SSE 实时推送。
- 前端展示层:场景监控、曲线、历史、用户管理。
- 桌面封装层:pywebview + PyInstaller。
核心代码集中在几个文件中:
| 文件 | 作用 |
|---|---|
aquaculture_iot/app.py |
后端主程序,包含数据库、Modbus、采集线程、算法和接口 |
aquaculture_iot/static/app.js |
前端交互逻辑,处理实时刷新、设备控制和曲线绘制 |
aquaculture_iot/static/index.html |
页面结构 |
aquaculture_iot/static/style.css |
页面样式 |
desktop_app.py |
桌面程序入口 |
build_exe.ps1 |
打包脚本 |
Modbus 寄存器设计
系统使用 Modbus TCP 保持寄存器。程序内部地址从 0 开始,而 ModSim 页面通常显示为 40001 起算。
| ModSim 地址 | 程序地址 | 数据项 | 缩放 |
|---|---|---|---|
| 40001 | 0 | 溶解氧含量 | 原始值 × 0.01 |
| 40002 | 1 | 水位值 | 原始值 × 0.001 |
| 40003 | 2 | pH 值 | 原始值 × 0.01 |
| 40004 | 3 | 养殖水温 | 原始值 × 0.1 |
| 40005 | 4 | 氧气浓度 | 原始值 × 0.1 |
| 40011-40017 | 10-16 | 执行器速率/开度 | 0-100 |
后端 ModbusTcpClient 实现了三个功能码:
- 03:读取保持寄存器。
- 06:写单个保持寄存器。
- 16:批量写保持寄存器。
设备控制时,页面会把执行器速率写入 40011-40017。采集线程读取传感器值后,会根据设备状态计算新的指标值,并尝试回写到 40001-40005。这样在 ModSim 中也能看到数据随设备运行变化。
设备联动算法
这个项目比较关键的部分是设备联动算法。为了让控制逻辑可解释,系统没有写死各种 if-else,而是设计了一张设备影响比例矩阵。
直接影响型设备使用公式:
1 | 新值 = 当前值 + (速率 / 100) × 整体倍率 × 影响比例 × 采样间隔 |
目标调节型设备使用公式:
1 | 新值 = 当前值 + (基础目标值 - 当前值) × (速率 / 100) × 整体倍率 × 影响比例 × 采样间隔 |
默认矩阵如下:
| 设备 | 溶解氧 | 水位 | pH | 水温 | 氧气浓度 | 类型 |
|---|---|---|---|---|---|---|
| 增氧机 | +0.280 | 0 | 0 | 0 | +0.140 | 直接影响 |
| 补水泵 | 0 | +0.040 | 0 | 0 | 0 | 直接影响 |
| 排水阀 | 0 | -0.045 | 0 | 0 | 0 | 直接影响 |
| 循环泵 | +0.060 | 0 | +0.055 | +0.055 | 0 | 目标调节 |
| 恒温调节器 | 0 | 0 | 0 | +0.140 | 0 | 目标调节 |
| pH 调节泵 | 0 | 0 | +0.180 | 0 | 0 | 目标调节 |
| 氧气调节阀 | 0 | 0 | 0 | 0 | +0.350 | 直接影响 |
例如补水泵速率为 60%,水位影响比例为 0.040,则每秒水位变化约为:
1 | 0.60 × 1.0 × 0.040 = 0.024 m/s |
前端设备卡片会实时显示这个换算结果。用户拖动速率滑块时,卡片中的“实际影响值”也会同步变化,这比只显示一个固定比例更直观。
一个实际修复:水位为什么会卡在 1.55m
开发中遇到过一个很典型的问题:当用户把水位阈值上限调高后,补水泵仍然只能把水位推到 1.55 m,不再继续增加。
原因是早期代码里为了避免仿真数据跑飞,把水位硬编码限制在 1.55 m。这个限制在默认阈值下没有问题,但当用户手动把报警上限调高时,就变成了 bug。
后来改成动态过程边界:
1 | 过程上限 = max(默认过程上限, 当前报警上限 + 0.10) |
同时再设置一个绝对安全边界,例如水位不超过 5.0 m,pH 不超过 14。这样既能尊重用户设置的阈值,又不会让仿真值无限增长。
对应代码在:
1 | process_limits() |
前端实现细节
前端没有使用大型框架,而是使用原生 JavaScript。原因很简单:这是课程设计项目,减少构建工具和依赖能降低交付风险。
前端主要做了几件事:
- 登录后加载配置。
- 根据传感器配置生成指标卡片。
- 根据执行器配置生成设备控制卡片。
- 使用 SSE 接收后端实时数据。
- 使用 Canvas 绘制动态曲线。
- 根据报警记录高亮场景传感器。
- 支持历史数据查询、CSV 导出和用户管理。
其中 SSE 是比较适合这个项目的方案。相比前端每秒轮询,SSE 可以由服务端主动推送数据,代码也比 WebSocket 简单。
关键函数包括:
| 函数 | 作用 |
|---|---|
bootstrap() |
页面初始化,检查登录状态 |
startEvents() |
建立 SSE 实时连接 |
applySnapshot() |
将后端快照同步到页面 |
renderActuatorCards() |
渲染设备卡片 |
formatEffectChip() |
显示当前速率下的实际影响值 |
drawTrend() |
绘制实时曲线 |
refreshUsers() |
管理员用户管理 |
桌面封装
桌面封装的入口是 desktop_app.py。它不是重新写一个桌面 GUI,而是启动本地 Web 服务,再用 pywebview 打开本地页面。
流程如下:
- 从 8000-8999 中寻找可用端口。
- 启动 Python HTTP 服务。
- 创建 pywebview 桌面窗口。
- 窗口关闭时停止服务。
这种方案的好处是:开发时可以直接浏览器调试,交付时又可以打包成 EXE,兼顾开发效率和交付体验。
项目收获
这个项目看起来是一个课程设计,但里面包含了不少真实交付中常见的问题:
- 如何在不引入复杂依赖的情况下完成本地客户端。
- 如何让模拟数据不是静态变化,而是有设备控制反馈。
- 如何处理 EXE 首次启动时数据库初始化顺序。
- 如何让客户调参数时,页面展示和后台算法保持一致。
- 如何排除客户资料、交付包、运行数据库等不适合公开的内容。
最终系统不只是“能显示数据”,而是具备通信、控制、存储、报警、用户管理和打包交付的一套完整本地测控软件。
代码仓
GitHub:https://github.com/ORI2333/aquaculture-iot-control-system
如果对您有帮助,请帮我点个star!