恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
嵌入式Python实战:从传感器到Web的环境监测系统
首页
资讯中心
/
嵌入式Python实战:从传感器到Web的环境监测系统
嵌入式Python实战:从传感器到Web的环境监测系统
发布时间:2026/9/10 8:15:30
之前在嵌入式相关的社区里泡久了经常看到有人问“Linux板子上能不能用Python做正经项目”或者“学了Python怎么跟硬件沾边”。我自己的感受是嵌入式开发不一定非得是C语言从头写到尾的苦差事碰上环境监测这类对实时性要求不算变态、但特别看重开发效率和后期维护的场景Python反而是一把好手。这篇文章就把我实际做的一个项目完整剥开从硬件选型、传感器接线、驱动代码到数据入库、Web展示、开机自启把每一步怎么想的、为什么这么做、踩过什么坑全给捋一遍。这个项目做的事情很简单一台跑着Linux的小板子外接几个传感器定时采集温湿度、气压、光照这些环境参数存到本地数据库再通过一个网页把实时数据和历史曲线展示出来。整体代码量不大但胜在链路完整非常适合想从纯软件切到嵌入式、或者正在做课程设计/毕设/小型产品原型的人参考。1. 从需求到方案为什么选嵌入式Python1.1 这个应用解决什么问题先说说环境监测这个场景本身。农业大棚、机房、实验室、仓库、居家室内都存在对环境参数的监控需求。大部分情况下并不需要微秒级的控制响应而是要“持续记录、随时可查、异常能告警”。这类诉求天然适合用带操作系统的板子来做而不是裸机单片机。设备端要干的事情无非是三件定时读传感器、把数据存下来、提供一个查询入口。如果要加告警就再加一条“超限就推送通知”的逻辑。整体逻辑不复杂但涉及的东西很杂硬件驱动、Linux系统、网络服务、数据存储、前端展示。用C从零写到能跑通没个一两周不行换Python两三天就能看到完整效果。1.2 为什么选Python而不是C我知道有人看到“嵌入式”三个字就默认必须上C。这个观点对了一半。对于电机控制、高频采集、资源锁死的场景C确实不可替代。但环境监测属于另一种典型场景它要求的是“快速迭代、易于修改、生态丰富”。Python最大的优势是生态。读取传感器有现成库写数据库有内置sqlite3做Web服务有Flask处理数据有pandas——如果项目需要。这意味着你把大部分精力放在业务逻辑上而不是拿着数据手册去翻寄存器。它的劣势也得说清楚GIL锁全局解释器锁限制多线程效率Python解释器本身也比编译型语言慢。但在环境监测场景下采集间隔通常是秒级甚至分钟级这个性能短板根本体现不出来。真正需要算力的地方比如大量历史数据的聚合分析可以放到服务端或者用SQL直接处理不让Python在板子上硬算。顺便提一句如果你未来打算深入嵌入式C语言里的面向对象思想还是很值得补一补的。Python的class写多了之后反过来理解Linux内核里用结构体加函数指针模拟出来的“对象”会有一种豁然开朗的感觉。这条学习路线我觉得是通的先用Python把业务跑起来再回头啃C理解底层两边互不耽误。1.3 硬件选型与系统准备硬件上我用的是一块全志H616芯片的核心板1GB内存板载Wi-Fi跑的是Armbian系统Debian的ARM版本。同类选择很多树莓派Zero 2 W、香橙派Zero 2W、芒果派等都可以只要满足三个条件——能跑完整Linux、带I2C/GPIO/ADC接口、有稳定的供电方案。这块板子现在的状态是已经刷好系统并接入局域网了。检查硬件基础环境有几个命令值得确认一下# 查看系统架构和内核版本 uname -a # 检查内存和CPU占用 free -h lscpu # 查看I2C总线设备是否挂载 ls /dev/i2c-* # 检查GPIO是否可用 ls /sys/class/gpio/如果/dev/i2c-0这类设备节点看不到大概率是内核没启用对应的设备树配置。这时候需要重新编译设备树或者在/boot/armbianEnv.txt里加上overlays配置。这一步是后面所有传感器工作的前提务必先确认好。Python环境我用了系统自带的Python 3.9并没有升级。这里有个经验不要在Linux板子上手贱去换默认Python版本系统很多工具依赖它换乱了容易把整个系统搞崩。正确的做法是只用venv为项目创建独立虚拟环境依赖随便装不污染系统。mkdir ~/env_monitor cd ~/env_monitor python3 -m venv venv source venv/bin/activate后面所有依赖都在这个虚拟环境里装。2. 系统架构从超级大循环到事件驱动的思路2.1 经典大循环架构与它的瓶颈很多刚接触嵌入式Linux的人写出来的第一个版本程序长这样while True: temperature, humidity read_dht22() save_to_database(temperature, humidity) time.sleep(2)这个结构本身没错逻辑也能跑但它是个典型的“超级大循环”——所有事情排队执行前面的任务耗时多少后面的任务就得等多久。当程序只有一个采集任务时这个while循环完全够用。可一旦数据要同时展示在网页上又要定期上传云端还可能要做本地告警判断单循环就很容易互相阻塞网页请求响应慢一点传感器读取就卡了传感器驱动里有个不稳定的小延时整个服务就像死掉一样。这就是嵌入式架构从“超级大循环”升级到“事件驱动”或“多线程/多进程”的分水岭。操作系统的存在就是用来解决这类并发问题的放着不用实在可惜。2.2 本应用的层次化设计我最终把整个应用拆成了四个模块用不同的Python文件管理各司其职env_monitor/ ├── main.py # 主入口负责启动各线程 ├── sensors.py # 传感器驱动层统一读取接口 ├── database.py # 数据存储层封装SQLite读写 ├── server.py # Web展示层Flask应用 ├── requirements.txt # 依赖清单 └── venv/ # Python虚拟环境这个分层的思路和Linux驱动的分层思想一致上层业务不关心传感器具体是I2C还是单总线传感器驱动也不关心数据最后是存数据库还是发云端。每层只做好自己的一件事层与层之间用函数接口对接。这比把几十个函数全塞在一个main.py里好维护得多。我见过太多“一个文件走天下”的嵌入式Python项目前期写起来爽后期加一个功能就要从头看一遍代码真的痛苦。2.3 采集线程、存储与Web服务如何协同应用启动后三个线程同时运行采集线程按固定间隔读取传感器把数据写入数据库。Flask服务线程处理HTTP请求从数据库读取数据返回给前端页面。告警线程可选定时检查最新数据是否越界越界则触发通知。线程间通过共享数据库来实现数据交互而不是直接用全局变量。这个设计的好处是不用加锁避免了一堆并发问题。因为SQLite本身就自带锁机制多个线程同时写入时数据库层面已经处理了冲突。当然SQLite对并发写有限制但环境监测这种低频写入场景完全够用。采集线程的间隔我用的是threading.Timer而不是while True sleep。区别在于Timer是真正的事件驱动当采集函数执行时间超过了间隔时间下一个周期不会被压到当前周期末尾而是按计划的间隔重新调度长期运行不会积累延迟。import threading import time def schedule_next(interval, func): timer threading.Timer(interval, run_loop, args(interval, func)) timer.daemon True timer.start() def run_loop(interval, func): current time.time() print(f[{time.strftime(%Y-%m-%d %H:%M:%S)}] 采集开始耗时 {current - last_time:.3f}s) func() schedule_next(interval, func)这个方案跑了一周采集时间点始终稳定在计划时刻附近没有一次出现因为某次读取卡住导致后续采集错乱的情况。3. 传感器驱动与数据采集实现3.1 I2C与单总线传感器接入项目里我接了三类传感器SHT30温湿度传感器I2C接口数字输出精度高价格也不贵BMP280气压传感器I2C接口和SHT30可以挂同一条I2C总线上BH1750光照传感器I2C接口测勒克斯值实际使用中我发现I2C传感器比单总线的DHT系列稳得多。DHT22我也试过但它的时序协议对GPIO延时的精度要求很高——Linux是分时操作系统一个线程调度延迟就能导致读取失败。后来直接全部换成了I2C传感器几行代码就能搞定而且I2C可以一条总线挂多个设备接线也清爽。接线方式很简单所有I2C设备都走4根线VCC接3.3VGND接GNDSCL接I2C时钟线通常是引脚3SDA接I2C数据线通常是引脚2。注意是3.3V供电千万别直接上5V不然板子上的I2C接口可能直接烧掉。确认设备是否被内核识别sudo i2cdetect -y 0 # 或者 sudo i2cdetect -y 1执行之后能看到I2C总线上所有设备的地址。常见地址SHT30是0x44BMP280是0x76BH1750是0x23。如果看不到设备先检查接线和供电再确认内核有没有加载i2c-dev驱动。3.2 温湿度传感器读取代码解析SHT30的驱动代码很简洁核心逻辑就是向传感器发送测量命令然后读取6字节的返回数据再解析温度和高湿度。import smbus2 import time class SHT30: def __init__(self, bus1, addr0x44): self.bus smbus2.SMBus(bus) self.addr addr def read(self): # 发送0x2C 0x06命令开启高重复性测量 self.bus.write_i2c_block_data(self.addr, 0x2C, [0x06]) time.sleep(0.1) # 读取6字节数据温度MSB, 温度LSB, CRC, 湿度MSB, 湿度LSB, CRC data self.bus.read_i2c_block_data(self.addr, 0x00, 6) temp_raw (data[0] 8) | data[1] hum_raw (data[3] 8) | data[4] temperature -45 175 * temp_raw / 65535.0 humidity 100 * hum_raw / 65535.0 return round(temperature, 2), round(humidity, 2)这里有几个值得讲的点。第一是time.sleep(0.1)传感器从收到测量命令到数据准备好需要一定时间这个延时必须给够否则读回来的数据全是0。第二是CRC校验SHT30的返回数据里有校验字节严谨的做法是要做CRC8校验防止总线噪声导致数据错误。为了代码简洁我没写但实际项目里建议加上不然偶尔冒出来的异常数据会让人抓狂。我在采集循环里加了一层异常捕获和数据过滤超过合理范围的数据直接丢弃def read_sensor_with_retry(sensor, retries3): for i in range(retries): try: temp, hum sensor.read() if -20 temp 50 and 0 hum 100: return temp, hum except Exception as e: print(f[WARNING] 传感器读取异常: {e}, 重试 {i1}/{retries}) time.sleep(0.5) return None, None3.3 模拟量传感器与ADC扩展如果要做土壤湿度、空气质量模拟输出型这类传感器板子本身没有模拟输入引脚就需要外接一个ADC芯片。我用的是ADS11154通道16位ADC走I2C接口在Linux下用起来非常简单。import adafruit_ads1x15.ads1115 as ADS from adafruit_ads1x15.analog_in import AnalogIn import busio import board i2c busio.I2C(board.SCL, board.SDA) ads ADS.ADS1115(i2c, address0x48) chan AnalogIn(ads, ADS.P0) # 通道0 # 读取原始值转换电压 voltage chan.voltage # 根据传感器数据手册的公式计算实际物理量ADS1115的输入电压范围可以通过ads.gain设置支持±0.256V到±6.144V档位。接传感器之前一定要看清楚传感器输出的是什么电压范围选错量程轻则读数不准重则烧掉ADC输入。3.4 采样策略与数据预处理环境参数本身变化缓慢没必要每秒采一次。我实际采用的策略是每分钟采集一次每次连续读3次取平均值作为当前值同时把3次读数的标准差作为稳定性参考。这样既平滑了传感器噪声又不会产生太多冗余数据。“每分钟一条”听起来数据量很小但一天1440条、一年52万条积累下来还是值得做表结构规划的。我在数据库层面做了按天分表的思路查询历史数据时按日期范围路由到对应表避免单表数据量过大后查询变慢。数据入库前还有一步预处理越过合理阈值的数据直接丢弃连续异常的传感器在日志里标记并等待下次恢复。这个策略帮我在一次传感器老化失效时及时发现了问题否则展现在网页上的数据曲线会有一截诡异的尖峰。4. SQLite存储与数据接口4.1 为什么选SQLite环境监测这种写入频率低、单机部署、不需要高并发的场景SQLite是无可争议的最佳选择而不是MySQL或者PostgreSQL。SQLite不需要单独的数据库服务进程数据就是一个文件备份只需要拷贝文件即使断电也不会损坏数据库文件有WAL日志模式加持。嵌入式场景里最忌讳的就是引入与功能不匹配的重量级组件。为了存几万条温湿度数据去装一个MySQL服务内存和CPU都被吃掉了还增加维护复杂度。SQLite几十KB的库可以做到同样的事情这就是选型时要算清楚的一笔账。4.2 建表与写入优化数据库模块的代码这样设计import sqlite3 import time from datetime import datetime class Database: def __init__(self, db_pathenv_data.db): self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.execute(PRAGMA journal_modeWAL;) self.conn.execute( CREATE TABLE IF NOT EXISTS env_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, temperature REAL, humidity REAL, pressure REAL, illuminance REAL ); ) self.conn.commit()有几点值得注意。check_same_threadFalse是必须的因为Flask的请求处理器和采集线程会轮流访问这个连接对象。同时我打开了WALWrite-Ahead Logging模式这个模式下读写可以并行读取时不会阻塞写入。写入数据时不要一行行commit批量插入效率高得多。每分钟离线攒一批数据再写库def insert_batch(self, rows): sql INSERT INTO env_data (timestamp, temperature, humidity, pressure, illuminance) VALUES (?, ?, ?, ?, ?) self.conn.executemany(sql, rows) self.conn.commit()executemany和单条循环比写入速度能提升好几倍。不过环境监测的数据量小这里不追求极限性能但这是一个好习惯数据量大了以后不用返工。数据库文件存放路径我放在了项目的data子目录下方便备份。另外建议定期用VACUUM命令压缩数据库文件SQLite删除数据后文件大小不会自动变小时间久了会产生碎片。4.3 Flask API与前端展示Web服务用的是Flask轻量几行代码就能起一个HTTP服务。我提供了三个接口from flask import Flask, jsonify, render_template import time app Flask(__name__) db Database() app.route(/) def index(): return render_template(index.html) app.route(/api/latest) def api_latest(): row db.query_latest() return jsonify(row) app.route(/api/history) def api_history(): hours request.args.get(hours, default24, typeint) rows db.query_history(hours) return jsonify(rows)前端页面用纯HTML Chart.js画实时曲线不依赖任何重型前端框架。Chart.js通过fetch接口每30秒拉一次最新数据自动更新曲线实现基本够用。考虑到板子资源有限我把Flask的默认开发服务器换成了waitress一个纯Python实现的WSGI服务器比自带的开发服务器稳定得多支持并发请求不用额外装Nginx。5. 部署与开机自启5.1 依赖管理与虚拟环境依赖清单很简单只有几个库smbus20.4.2 flask3.0.3 waitress3.0.0 pyserial3.5把requirements.txt和代码放在一起换设备部署时直接在虚拟环境里执行pip install -r requirements.txt十几秒装完。这样比拿着一堆pip freeze导出的包名去手动装要省心太多。5.2 systemd服务配置开机自启我用的systemd。在/etc/systemd/system/env-monitor.service创建服务文件[Unit] DescriptionEnvironment Monitor Service Afternetwork.target [Service] Userpi WorkingDirectory/home/pi/env_monitor ExecStart/home/pi/env_monitor/venv/bin/python /home/pi/env_monitor/main.py Restartalways RestartSec10 [Install] WantedBymulti-user.target这里有个坑ExecStart必须写虚拟环境里的Python绝对路径不能写python main.py否则systemd会去系统PATH里找Python找不到或者版本不对服务起不来。之后执行sudo systemctl daemon-reload sudo systemctl enable env-monitor sudo systemctl start env-monitor sudo systemctl status env-monitorRestartalways这个参数很重要程序因为异常退出时systemd会自动重新拉起。环境监测服务需要在无人值守的情况下长期运行没有这个参数一次偶发崩溃就可能导致整晚数据缺失。5.3 日志、时间与资源管理日志这块我打印到systemd的journald里查看日志直接journalctl -u env-monitor -f不要自己写一套日志文件逻辑。systemd已经帮我们处理了日志轮转、按时间过滤、持久化存储没必要重复造轮子。系统时间必须校准。我在这上面吃过亏板子刚上电时如果没连网络系统时间停留在上次关机时间点采集到的数据时间戳全是错的。解决方案是安装并启用chrony或systemd-timesyncd开机后自动对齐NTP时间服务器。另外在程序里加了一行启动时检查from datetime import datetime if datetime.now().year 2025: print([ERROR] 系统时间异常请检查NTP同步) sys.exit(1)这个粗判断虽然简单但确实能挡住不少低级错误。6. 常见问题与排查实录6.1 I2C读取失败与毛刺问题现象是程序跑着跑着突然报IOError: [Errno 121] Remote I/O error然后采集就断了。排查下来发现原因有两种一是I2C传感器引线过长且没有屏蔽受到板子Wi-Fi模块的电磁干扰二是传感器供电电压偏低SHT30在3.2V左右会出现时序混乱。解决方法很直接把I2C引线控制在20cm以内在传感器电源脚和地之间并联一个100μF的电解电容加0.1μF的陶瓷电容。加电容后毛刺问题基本绝迹。同时采集函数必须做异常捕获单次失败不退出程序等下一周期重试。6.2 系统内存持续增长跑了几个小时后发现free -h显示可用内存不断下降。一开始以为是内存泄漏后来定位到是Flask的debug模式下静态文件缓存没有释放。关掉debug模式使用waitress生产模式后内存占用稳定在80MB以下。另外传感器读取时创建的smbus2.SMBus实例每次读取完记得关闭长期运行时文件描述符堆积也是内存上涨的常见原因。6.3 无线连接不稳定导致数据上传中断如果项目还要把数据上报到云端Wi-Fi断线重连是个不能忽略的问题。我在程序里加了一个网络状态监控import socket def is_network_up(): try: socket.create_connection((8.8.8.8, 53), timeout5) return True except OSError: return False每10分钟检查一次如果网络不可达就调用系统命令重启Wi-Fi接口这是最后的手段能不动就不动。比频繁重连更好的办法是让系统层的wpa_supplicant自己处理重连程序只管上报数据网络问题交给系统。6.4 排查问题速查表现象可能原因排查方法解决办法i2cdetect找不到设备接线错误或供电不足检查VCC/GND电压重新接线加滤波电容温湿度全是0读取太快或传感器损坏增大测量后延时延时加到100ms以上数据库文件暴涨采样频率过高查看采集间隔设置调整为分钟级采样Flask页面加载慢开发服务器并发能力弱查看请求耗时换waitress或gunicorn服务启动即退出依赖未安装或路径错误journalctl看日志确认虚拟环境路径7. 项目扩展的三种可能这个项目做稳定之后我顺手扩展了几个方向这里分享一下思路。第一种是告警通知。在采集线程里加一个阈值判断温度超限就调用消息推送接口比如Server酱或者企业微信机器人。代码量只增加十几行但实用性大增家里有蔬菜大棚的能用上。第二种是历史数据的进一步分析。SQLite里的数据可以用Python脚本导出成CSV配合pandas做温度变化趋势分析帮助判断设备运行状态。比如制冷设备的能效评估或者温室保温性能的量化分析。第三种是接入物联网平台。把采集到的数据通过MQTT协议定时上报到云平台实现远程监控和多设备管理。这部分需要外部MQTT broker板子端用paho-mqtt库接入简单网上示例也很多。我自己的经验是环境监测这个项目虽然看起来简单但它几乎是嵌入式Linux下Python开发的一个“样板间”——把传感器驱动、数据库、Web服务、进程管理、系统部署全都串起来了。做完这一个项目你对Linux系统调用、进程模型、网络编程这些概念的理解会比刷十本教程更深刻。最后分享一个我在多个项目里验证过的体会嵌入式开发的瓶颈往往不是硬件能力而是工具的合理运用。Python的意义不是在所有嵌入式场景里取代C而是帮你把属于应用层的逻辑更快更稳地落地。如果你也在纠结嵌入式项目要不要用Python我的建议是——先看实时性需求再算开发周期存在Linux和Python交叉点的场景放心大胆用就对了。