1. 项目概述打造你的桌面级空气质量监测站如果你和我一样对身边的空气质量有点“强迫症”总想知道窗外空气到底怎么样但又不想总去翻手机App那么这个项目就是为你量身定做的。我们将利用一块名为PyPortal的开发板结合CircuitPython亲手打造一个能联网、能显示、颜值还不错的桌面空气质量监测显示终端。它不仅能实时显示你所在区域的空气质量指数AQI还能通过颜色直观地告诉你空气状况——从代表“优”的清新绿色到警示“有害”的深红色。PyPortal本质上是一个为物联网IoT应用而生的“一体机”。它集成了彩色触摸屏、Wi-Fi模块、微控制器甚至还有温度传感器和音频输出开箱即用。而CircuitPython你可以把它理解为能在这种微型硬件上运行的、极度简化的Python。它的最大魅力在于你不需要安装复杂的编译器或IDE写完代码直接往板子里一拖就像操作U盘一样简单立刻就能看到效果。这对于嵌入式开发新手或者想快速验证想法的老手来说效率提升不是一点半点。这个项目的核心逻辑非常清晰让PyPortal连接你家Wi-Fi定期访问美国环保署的AirNow API当然你需要一个美国邮编来获取数据拿到JSON格式的空气质量数据解析出AQI数值最后在屏幕上用大号字体和对应的背景色展示出来。整个过程你会接触到嵌入式开发中几个非常经典且实用的环节环境搭建、网络配置、API调用、数据解析和图形化显示。无论你是想学习物联网开发还是单纯想做个有趣的桌面摆件这个项目都能给你带来扎实的收获。2. 硬件与软件准备清单动手之前清点一下“弹药”是成功的第一步。这个项目对硬件的要求比较明确软件生态则完全围绕Adafruit的CircuitPython展开。2.1 核心硬件组件解析Adafruit PyPortal开发板这是项目的绝对核心。我推荐直接选择官方标准版PyPortal。它内置了ESP32 Wi-Fi协处理器负责所有网络通信主控芯片是ATSAMD51性能足以流畅运行CircuitPython并驱动屏幕。板载的3.5英寸电阻触摸屏320x240像素是显示信息的主要窗口。选择它而不是自己用屏幕单片机拼接省去了最麻烦的驱动和兼容性调试能把精力完全集中在应用逻辑上。电源与数据线PyPortal通过Micro-USB接口供电和编程。务必准备一根可靠的数据线很多手机充电线只有供电功能无法传输数据会导致电脑无法识别设备。一个输出稳定的5V/2A USB电源适配器能保证PyPortal稳定运行尤其是在屏幕背光全开时。可选3D打印外壳与磁吸套件为了让你的监测站更美观、更稳固可以考虑为它打印一个外壳。Adafruit提供了开源的设计文件。外壳不仅能保护电路板其磁吸设计通过粘贴强磁铁可以让你把它轻松吸附在冰箱、文件柜等任何铁质表面真正实现“随处可放”。如果自己没有3D打印机很多在线打印服务如国内的嘉立创、国外的3D Hubs都能按需制作。2.2 软件环境与关键库软件层面我们完全在CircuitPython生态内操作无需在电脑上安装复杂的IDE。CircuitPython固件这是PyPortal的“操作系统”。你需要从CircuitPython官网下载专为PyPortal编译的最新版.uf2固件文件。固件版本最好与后续使用的库版本匹配能避免很多兼容性问题。Adafruit CircuitPython库包这是包含所有驱动和功能模块的“武器库”。你需要下载与固件版本对应的库包Bundle。对于本项目以下几个库是必须的我会解释每个库的作用这样你就知道为什么需要它adafruit_pyportal核心库它封装了屏幕控制、网络请求、图形界面创建等复杂操作提供了高级API让我们用几行代码就能实现复杂功能。adafruit_esp32spiESP32 Wi-Fi芯片的驱动库负责底层网络通信。adafruit_requests一个类似于Python标准库requests的HTTP客户端库让我们能像在电脑上一样方便地用get()、post()方法获取网络数据。adafruit_connection_manager管理网络连接池和SSL上下文被adafruit_requests依赖。adafruit_display_text与adafruit_bitmap_font负责在屏幕上渲染文本和加载字体文件。没有它们你无法显示任何文字信息。adafruit_imageload用于加载和显示图片文件本项目虽未直接使用但pyportal库内部依赖它处理背景。实操心得库文件管理下载的库包解压后是一个巨大的lib文件夹。对于PyPortal的8MB存储空间全部拷贝进去会占用大量空间。更高效的做法是只拷贝你项目必需的库文件到CIRCUITPY盘的lib目录下。如何知道需要哪些一个笨但有效的方法是先全部拷贝让项目跑起来然后在串口监视器中查看导入错误缺哪个补哪个。或者直接参考我上面列出的清单这些是本项目运行的最小依赖集。3. CircuitPython环境部署与网络配置这是将硬件“唤醒”并赋予其联网能力的关键一步。步骤不复杂但有几个细节容易踩坑。3.1 刷写CircuitPython固件PyPortal出厂可能运行其他固件我们需要将其转换为CircuitPython设备。进入引导加载模式用数据线连接PyPortal和电脑。快速双击板子中央的Reset按钮。此时板载的RGB NeoPixel LED通常靠近USB口会亮起绿色如果亮红色说明进入模式失败检查USB线或换一个USB端口。电脑上会弹出一个名为PORTALBOOT的U盘。拖入固件文件将之前下载的adafruit-circuitpython-pyportal-版本号.uf2文件直接拖拽到PORTALBOOT盘符中。完成启动拖入后LED会闪烁PORTALBOOT盘符消失稍等片刻会出现一个名为CIRCUITPY的新盘符。这表明CircuitPython系统已成功刷入并启动。打开CIRCUITPY盘你会看到一个boot_out.txt文件里面记录了固件版本信息。3.2 配置Wi-Fi与API密钥settings.toml在物联网项目中如何安全地管理Wi-Fi密码和API密钥是个大学问。CircuitPython 8.x之后推荐使用settings.toml文件来替代旧的secrets.py。它的好处是格式更标准且与代码文件分离方便分享代码时不泄露敏感信息。创建settings.toml文件在CIRCUITPY盘的根目录下注意不是任何文件夹里用任何文本编辑器如VS Code、Notepad甚至系统自带的记事本新建一个文件命名为settings.toml。编辑内容根据你的实际情况填入以下信息。下面我以一个虚构的配置为例并解释每个字段# PyPortal 空气质量监测站配置 CIRCUITPY_WIFI_SSID YourHomeWiFi CIRCUITPY_WIFI_PASSWORD YourWiFiPassword # AirNow API 密钥 (从 airnow.gov 注册获取) AIRNOW_TOKEN 12345678-ABCD-EFGH-IJKL-MNOPQRSTUVWX # Adafruit IO 账户信息 (用于网络时间同步) AIO_USERNAME your_adafruit_io_username AIO_KEY your_adafruit_io_aio_keyCIRCUITPY_WIFI_SSID/PASSWORD你的Wi-Fi名称和密码。确保PyPortal在信号覆盖范围内。AIRNOW_TOKEN这是从AirNow官网免费注册后获得的API访问令牌。重要提示AirNow数据仅覆盖美国部分地区通过邮编查询。如果你的位置不在其服务范围这个项目将无法直接获取数据。但整个代码框架网络连接、数据获取、显示逻辑是完全可复用的你可以寻找替代的、支持你所在区域的空气质量API例如一些城市提供的开放数据平台并修改代码中的API请求地址和JSON解析逻辑。AIO_USERNAME/AIO_KEYAdafruit IO的账户名和密钥。PyPortal内部用它来获取准确的网络时间以维持系统时钟。即使你不做数据上传这个时间服务也是免费可用的。在io.adafruit.com登录后点击View AIO Key即可看到。注意事项settings.toml的语法所有字符串必须用双引号包裹。键如CIRCUITPY_WIFI_SSID和值如YourHomeWiFi用等号连接。支持注释以#开头。确保文件以.toml扩展名保存且编码为UTF-8 without BOM在记事本另存为时可选择。错误的编码可能导致中文字符或特殊符号读取失败。3.3 验证网络连接在编写主程序前强烈建议先运行一个简单的网络测试脚本确保PyPortal能成功连接互联网。这能帮你提前排除Wi-Fi配置错误、信号弱或防火墙问题。将以下代码保存为CIRCUITPY盘根目录下的code.pyCircuitPython会自动运行它。import os import board import busio from digitalio import DigitalInOut import adafruit_connection_manager import adafruit_requests from adafruit_esp32spi import adafruit_esp32spi # 从 settings.toml 读取Wi-Fi配置 ssid os.getenv(CIRCUITPY_WIFI_SSID) password os.getenv(CIRCUITPY_WIFI_PASSWORD) # 配置ESP32 SPI接口PyPortal已预定义引脚 esp32_cs DigitalInOut(board.ESP_CS) esp32_ready DigitalInOut(board.ESP_BUSY) esp32_reset DigitalInOut(board.ESP_RESET) spi busio.SPI(board.SCK, board.MOSI, board.MISO) esp adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) # 创建网络会话 pool adafruit_connection_manager.get_radio_socketpool(esp) ssl_context adafruit_connection_manager.get_radio_ssl_context(esp) requests adafruit_requests.Session(pool, ssl_context) # 扫描并连接Wi-Fi print(扫描网络中...) for ap in esp.scan_networks(): print(f\t{ap.ssid:25} 信号强度: {ap.rssi} dBm) print(f正在连接: {ssid}) while not esp.is_connected: try: esp.connect_AP(ssid, password) except OSError as e: print(f连接失败重试中... 错误: {e}) continue print(f连接成功! SSID: {esp.ap_info.ssid}, RSSI: {esp.ap_info.rssi}) print(fIP 地址: {esp.ipv4_address}) # 测试HTTP请求 TEST_URL http://httpbin.org/get try: print(f正在测试网络请求: {TEST_URL}) response requests.get(TEST_URL) print(网络测试通过服务器响应头:, response.headers) response.close() except Exception as e: print(f网络请求测试失败: {e}) print(网络初始化完成。)打开串口监视器推荐使用Mu编辑器或如PuTTY、screen等工具波特率115200你应该能看到连接成功、获取到IP地址、并完成HTTP测试的输出。如果卡在连接步骤请检查settings.toml中的SSID和密码是否正确以及Wi-Fi是否为2.4GHz频段ESP32通常不支持5GHz。4. 空气质量监测程序深度解析与实现网络通了我们就可以开始构建核心的空气质量监测程序了。这段代码不长但每一部分都体现了CircuitPython项目开发的典型模式。4.1 主程序代码结构与工作流将以下代码保存为CIRCUITPY盘根目录下的code.py它会覆盖之前的测试代码。# SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT PyPortal 空气质量指数 (AQI) 显示终端 从 AirNow API 获取指定美国邮编的AQI数据并可视化显示。 import os import time import board from adafruit_pyportal import PyPortal # 用户配置区域 # 重要修改为你的美国邮编并确认该地区有数据 https://airnow.gov/ LOCATION_ZIP_CODE 10001 # 例如纽约曼哈顿 # API 与数据源配置 # AirNow API 端点 DATA_SOURCE http://www.airnowapi.org/aq/forecast/zipCode/?formatapplication/json # 将邮编和API密钥拼接到请求URL中 DATA_SOURCE zipCode LOCATION_ZIP_CODE API_KEY os.getenv(AIRNOW_TOKEN) # JSON数据解析路径获取返回数组第一个元素中的AQI字段 DATA_LOCATION [0, AQI] # 注意原教程是[1]但根据API返回结构通常第一个元素[0]是当前数据 # 初始化 PyPortal 显示对象 # 获取当前文件所在目录用于定位字体文件 cwd (/ __file__).rsplit(/, 1)[0] pyportal PyPortal( urlDATA_SOURCE, # 数据来源URL json_pathDATA_LOCATION, # JSON解析路径 status_neopixelboard.NEOPIXEL, # 使用板载NeoPixel作为状态指示灯 default_bg0x000000, # 默认背景色黑色 text_fontcwd /fonts/Helvetica-Bold-100.bdf, # 主AQI字体 text_position(120, 110), # 主AQI文本位置 (x, y) text_color0xFFFFFF, # 主AQI文本颜色白色 caption_textAQI: LOCATION_ZIP_CODE, # 底部标题文字 caption_fontcwd /fonts/HelveticaNeue-24.bdf, # 标题字体 caption_position(50, 220), # 标题位置 caption_color0xCCCCCC, # 标题颜色浅灰色 ) # 主循环 print(AQI 显示器启动。位置:, LOCATION_ZIP_CODE) while True: try: # 核心操作获取并解析数据返回的就是AQI数值 current_aqi pyportal.fetch() print(f[{time.monotonic():.0f}] 获取到AQI: {current_aqi}) # 根据AQI数值范围设置对应的背景色和状态灯颜色 # 颜色标准参考美国EPA AQI颜色规范 if 0 current_aqi 50: pyportal.set_background(0x00FF00) # 绿色 - 优 pyportal.neopixel.fill(0x00FF00) elif 51 current_aqi 100: pyportal.set_background(0xFFFF00) # 黄色 - 良 pyportal.neopixel.fill(0xFFFF00) elif 101 current_aqi 150: pyportal.set_background(0xFF9900) # 橙色 - 对敏感人群不健康 pyportal.neopixel.fill(0xFF9900) elif 151 current_aqi 200: pyportal.set_background(0xFF0000) # 红色 - 不健康 pyportal.neopixel.fill(0xFF0000) elif 201 current_aqi 300: pyportal.set_background(0x990066) # 紫色 - 非常不健康 pyportal.neopixel.fill(0x990066) elif 301 current_aqi 500: pyportal.set_background(0x660000) # 栗色 - 有害 pyportal.neopixel.fill(0x660000) else: # 处理意外数值 pyportal.set_background(0x000000) pyportal.neopixel.fill(0x0000FF) # 蓝色表示错误 print(警告接收到超出范围的AQI值:, current_aqi) except (RuntimeError, OSError) as e: # 网络错误或数据解析错误处理 print(f获取数据时出错: {e}) pyportal.set_background(0x000000) # 黑屏 pyportal.neopixel.fill(0xFF0000) # 红色状态灯表示错误 # 在屏幕上显示错误信息简单版 # 更复杂的错误处理可以创建新的文本标签 # 等待10分钟600秒后进行下一次查询 # AirNow API 数据更新频率通常为每小时一次10分钟查询一次是合理的 time.sleep(600)4.2 代码核心机制剖析初始化与配置 (PyPortal对象)PyPortal类是这个项目的“大脑”。初始化时我们通过参数告诉它数据从哪里来 (url)、数据是什么格式以及如何提取 (json_path)、屏幕如何显示字体、位置、颜色。json_path[0, AQI]是关键。它指示库按以下路径解析返回的JSON取响应列表的第一个元素索引0然后在该元素字典中取键为AQI的值。你需要根据实际API返回的JSON结构微调这个路径。数据获取与解析 (pyportal.fetch())这一行代码完成了大量幕后工作发起HTTP GET请求、接收响应、解析JSON、按json_path定位数值并返回结果。adafruit_pyportal库的封装极大简化了流程。可视化逻辑 (背景色与状态灯)根据美国环保署(EPA)的AQI标准我们用if-elif语句将AQI数值映射到对应的颜色。pyportal.set_background(颜色代码)用于改变屏幕背景色。颜色代码是十六进制RGB值例如0x00FF00代表纯绿色。pyportal.neopixel.fill(颜色代码)同步设置板载RGB LED的颜色提供额外的视觉状态指示。错误处理与循环网络请求可能因信号、服务器等问题失败。用try-except包裹fetch()调用捕获RuntimeError或OSError并在出错时提供视觉反馈如红色状态灯。time.sleep(600)让程序休眠10分钟。这是为了遵守API的使用礼节避免过于频繁的请求同时也符合空气质量数据通常每小时更新一次的实际情况。你可以根据需求调整这个间隔。4.3 字体文件与资源管理你可能注意到代码中引用了字体文件.bdf格式。这些文件需要放置在CIRCUITPY盘上。通常从Adafruit下载的项目包Project Bundle里会包含一个fonts文件夹。你需要将这个文件夹完整地拷贝到CIRCUITPY盘的根目录确保code.py中cwd /fonts/...的路径能正确找到它们。如果找不到原版字体你可以使用Adafruit CircuitPython Library Bundle中的字体或者使用在线工具将TTF字体转换为.bdf格式。字体大小直接影响显示效果Helvetica-Bold-100.bdf用于显示巨大的AQI数值HelveticaNeue-24.bdf用于底部较小的标题。5. 高级技巧、问题排查与功能扩展项目基本运行起来后我们可以探讨一些优化和深度定制的可能性并看看如何解决常见问题。5.1 提升稳定性与用户体验使用WiFiManager基础连接代码在网络不稳定时可能无法自动重连。adafruit_esp32spi_wifimanager库提供的WiFiManager类能更好地处理网络异常。它可以自动重连、在连接时提供视觉反馈如闪烁LED并简化HTTP请求。在主循环外初始化WiFiManager然后用wifi.get(url)替代requests.get()稳定性会好很多。添加视觉反馈在fetch()之前让状态灯闪烁蓝色表示“正在获取”成功后显示对应AQI颜色失败时闪烁红色。这能让设备状态一目了然。优化错误处理当前的错误处理比较简单。可以扩展为在屏幕上显示具体的错误信息如“Network Error”或“API Error”并尝试指数退避重连例如第一次失败等1分钟第二次等2分钟以此类推。5.2 常见问题排查速查表问题现象可能原因排查步骤与解决方案PyPortal连接电脑后无CIRCUITPY盘符1. 未成功进入引导模式2. USB线仅供电无数据3. 驱动问题罕见1. 重新双击Reset按钮观察NeoPixel是否变绿。2. 更换一根确认可传输数据的USB线。3. 尝试另一台电脑或USB端口。Wi-Fi连接失败1.settings.toml配置错误2. Wi-Fi信号弱或为5GHz3. 网络需要网页认证如酒店1. 检查CIRCUITPY_WIFI_SSID和PASSWORD拼写确保无多余空格。2. 将路由器频段改为2.4GHz或将设备移近路由器。3. PyPortal不支持Portal认证需连接无需网页认证的网络。程序运行但屏幕无显示或显示错误1. 字体文件缺失或路径错误2. 库文件版本不匹配或缺失3. 屏幕排线接触不良1. 确认fonts文件夹在CIRCUITPY根目录且文件名与代码中完全一致。2. 检查串口输出是否有ImportError并更新/添加对应库。3. 重新插拔PyPortal屏幕排线需谨慎操作。串口输出KeyError或IndexError1. API返回数据结构与预期不符2. API密钥无效或过期3. 指定邮编无数据1. 将DATA_SOURCEURL在浏览器中打开查看返回的实际JSON结构调整DATA_LOCATION。2. 登录AirNow确认API密钥状态。3. 在airnow.gov官网查询该邮编是否有AQI数据。获取数据一次后停止更新1. 程序因未捕获的异常崩溃2. Wi-Fi断开后未恢复1. 检查串口输出看是否有未在try-except中捕获的异常。2. 实现更健壮的网络重连逻辑或使用WiFiManager。AQI数值显示为None或奇怪的值json_path解析路径错误打印出pyportal.fetch()的原始返回值或打印response.json()如果直接使用requests仔细对照JSON结构修正路径。5.3 项目扩展思路这个项目是一个完美的起点你可以基于它扩展出更多功能更换数据源如果你不在美国可以寻找本地的空气质量API例如通过中国生态环境部的公开数据接口、World Air Quality Index项目等。你需要修改DATA_SOURCEURL根据新API的响应格式调整json_path可能需要调整请求参数或头部headers。显示更多信息除了AQI还可以显示PM2.5、PM10、臭氧浓度、更新时间等。这需要修改代码从API返回的JSON中提取更多字段并在屏幕上创建多个文本标签(adafruit_display_text.label)来分别显示。添加触摸交互PyPortal的屏幕是触摸屏。你可以添加按钮点击切换显示不同地点的AQI、查看历史趋势需要本地存储或云端记录、或者切换显示模式。与智能家居联动当AQI超过某个阈值如150时让PyPortal通过IFTTT或本地网络API发送信号自动关闭窗户、打开空气净化器。美化界面使用adafruit_imageload加载自定义背景图片设计更精美的UI布局而不仅仅是纯色背景和文字。实现这些扩展意味着你需要更深入地阅读adafruit_display_text、adafruit_touchscreen等库的文档并编写更复杂的控制逻辑。但这正是嵌入式开发的乐趣所在——从一个简单可行的原型出发逐步添加功能最终打造出一个完全符合你个人需求的专属设备。