用CircuitPython与PyPortal打造NASA每日天文图显示器
1. 项目概述:打造你的专属太空画廊
几年前,当我第一次把一块小小的PyPortal开发板连上家里的Wi-Fi,看着它从浩瀚的互联网中抓取并显示出一张壮丽的星云照片时,那种感觉非常奇妙。这不仅仅是一个技术Demo,更像是在桌面上打开了一扇随时窥探宇宙的窗户。今天要分享的,就是如何用CircuitPython和PyPortal,亲手制作一个NASA“每日天文图”(APOD)显示器。这个项目完美诠释了物联网(IoT)的精髓:一个简单的嵌入式设备,通过网络获取云端数据,并以直观的方式呈现出来。它不需要复杂的Linux系统或庞大的计算资源,仅凭一块微控制器、一块屏幕和一份Python代码就能实现。无论你是想学习嵌入式开发、CircuitPython编程,还是单纯想做一个酷炫的桌面摆件,这个项目都是一个绝佳的起点。整个过程涉及网络连接、API调用、JSON数据解析和图形显示,是打通物联网全栈流程的经典案例。
2. 核心硬件与软件栈解析
2.1 为什么选择PyPortal?
PyPortal是Adafruit推出的一款“开箱即用”的物联网显示设备。它的核心是一颗ATSAMD51微控制器,但真正让它脱颖而出的,是其高度集成的特性。你不需要再额外连接Wi-Fi模块、屏幕、SD卡槽甚至各种传感器,这些都已经板载集成。对于这个项目,几个关键组件至关重要:
- 3.2英寸TFT触摸屏(320x240像素):这是我们的画布,分辨率足以清晰展示NASA提供的精美图片。
- ESP32 Wi-Fi协处理器:负责所有的网络通信,通过SPI与主控芯片通信,让微控制器无需处理复杂的网络协议栈。
- 8MB QSPI Flash存储:用于存储CircuitPython解释器、你的代码、必要的库文件,以及缓存下载的图片。
- NeoPixel RGB LED:一个可编程的状态指示灯,在代码中我们可以用它来显示网络连接状态或错误信息。
选择PyPortal,意味着你跳过了最繁琐的硬件连接和底层驱动调试阶段,可以直接聚焦在应用逻辑和用户体验上。对于快速原型开发和爱好者项目来说,这种“电池包含”的体验是无价的。
2.2 CircuitPython:嵌入式开发的“快速通道”
如果你熟悉Python,那么CircuitPython会让你感到无比亲切。它是MicroPython的一个分支,由Adafruit主导开发,特别注重易用性和教育性。其核心设计哲学是“迭代速度至上”:
- 无需编译:你的代码文件(
code.py)直接以文本形式存放在名为CIRCUITPY的U盘里。保存文件即等于刷入程序,几乎瞬间生效。 - 交互式编程:通过串行REPL(交互式解释器),你可以像在电脑上使用Python一样,实时查询传感器数值、测试函数,进行调试。
- 丰富的“库”生态:Adafruit维护着一个庞大的CircuitPython库集合(Bundle),从驱动特定传感器到处理网络请求(
adafruit_requests)、显示图形(adafruit_pyportal)都有现成的库。本项目用到的核心库大多来源于此。
这种设计将嵌入式开发的门槛降到了极低。你不再需要面对复杂的IDE配置、编译工具链和底层寄存器操作,而是用高级语言直接描述“做什么”。当然,这种便利性是以牺牲一部分运行效率和底层控制力为代价的,但对于绝大多数网络交互、数据展示类的应用来说,性能完全足够。
2.3 NASA APOD API:数据的源泉
NASA的“每日天文图”(Astronomy Picture of the Day, APOD)是一个运行了数十年的科普项目,每天都会发布一张不同的宇宙影像或插图,并配有专业天文学家的解释。幸运的是,NASA提供了一个免费的开放API(api.nasa.gov)供开发者调用。
- API端点:
https://api.nasa.gov/planetary/apod - 认证方式:需要API Key,但申请完全免费,仅需提供姓名和邮箱。
- 返回数据:调用API会返回一个JSON对象,其中包含
title(图片标题)、date(日期)、explanation(解释)、url(标准图片链接)、hdurl(高清图片链接)和media_type(媒体类型,通常是image)等关键字段。 - 限制:每小时最多30次请求,每天最多50次。对于我们的显示器(每30分钟更新一次)来说绰绰有余。
这个API设计得非常友好,结构清晰,是学习RESTful API和JSON数据处理的理想范例。我们的代码核心任务,就是向这个地址发起HTTP GET请求,然后从返回的JSON中提取出url和title等信息。
3. 项目环境搭建与配置详解
3.1 固件刷写与基础文件准备
拿到PyPortal后,第一步是让它运行CircuitPython。这个过程被设计得非常简单,类似于为U盘拷贝文件:
- 下载固件:访问CircuitPython官网,根据你的PyPortal具体型号(如PyPortal、PyPortal Pynt等)下载最新的
.uf2固件文件。务必确认型号匹配。 - 进入引导加载模式:用一条数据线(强调:必须是支持数据传输的USB线,充电线不行)连接PyPortal和电脑。快速双击板子上的
Reset按钮。此时,板载的NeoPixel LED应变为绿色,电脑上会出现一个名为PORTALBOOT的U盘。 - 刷入固件:将下载好的
.uf2文件拖入PORTALBOOT盘符。PyPortal会自动重启,PORTALBOOT盘符消失,取而代之的是一个名为CIRCUITPY的新盘符。这表示CircuitPython系统已成功启动。
注意:如果双击
Reset后LED变红,或PORTALBOOT未出现,请优先检查USB线缆和电脑USB端口。这是新手最常遇到的问题。
此时,CIRCUITPY驱动器中只有一个boot_out.txt文件,这是正常的。接下来需要安装必要的库文件。从Adafruit的GitHub Releases页面下载对应你CircuitPython版本的库合集(Library Bundle)。解压后,你会看到一个lib文件夹。对于本项目,至少需要将以下库文件复制到CIRCUITPY驱动器的lib目录下:
adafruit_pyportal.mpy:项目核心库,封装了显示、网络请求等复杂操作。adafruit_requests.mpy:用于发起HTTP/HTTPS请求。adafruit_esp32spi.mpy:ESP32 Wi-Fi模块的驱动。adafruit_connection_manager.mpy:管理网络连接和套接字池。adafruit_imageload.mpy:图像加载库。adafruit_display_text.mpy:用于在屏幕上显示文本。adafruit_bitmap_font.mpy:支持点阵字体。adafruit_portalbase.mpy:adafruit_pyportal的基础库。adafruit_touchscreen.mpy:触摸屏驱动(本项目虽未用到触摸功能,但库依赖需要)。
3.2 安全配置:settings.toml文件的奥秘
将敏感信息(如Wi-Fi密码、API密钥)硬编码在code.py中是极不安全的,也不利于代码分享。CircuitPython 8及以上版本引入了settings.toml文件来解决这个问题。它是一个纯文本配置文件,存储在CIRCUITPY根目录,代码通过os.getenv()函数来读取其中的值。
你需要创建一个名为settings.toml的文件,内容如下:
CIRCUITPY_WIFI_SSID = "你的Wi-Fi名称" CIRCUITPY_WIFI_PASSWORD = "你的Wi-Fi密码" AIO_USERNAME = "你的Adafruit IO用户名" AIO_KEY = "你的Adafruit IO密钥" CIRCUITPY_PYSTACK_SIZE = 2048逐项解释:
CIRCUITPY_WIFI_SSID/PASSWORD:让PyPortal连接本地网络。AIO_USERNAME/AIO_KEY:本项目必须项。PyPortal的adafruit_pyportal库在显示网络图片时,会先将图片URL发送到Adafruit IO的图片转换服务,将JPG/PNG等格式转换为PyPortal屏幕原生支持的BMP格式。因此你需要一个免费的Adafruit IO账户,并在其网站的个人信息页获取AIO_KEY。CIRCUITPY_PYSTACK_SIZE = 2048:关键配置。默认的Python栈大小可能不足以处理图像转换和网络请求的复杂操作,会导致MemoryError或Pystack exhausted错误。将其增加到2048或更大可以解决此问题。
实操心得:在编辑
settings.toml时,确保使用纯文本编辑器(如VS Code、Notepad++、Mu编辑器),并以UTF-8无BOM格式保存。错误的编码可能导致CircuitPython无法正确解析其中的字符串,特别是当你的Wi-Fi密码包含特殊字符时。
3.3 网络连接测试与排错
在运行主程序前,强烈建议先运行一个简单的网络测试脚本,验证硬件、库和配置是否正确。将以下代码保存为code.py并放入CIRCUITPY:
import os import board import busio from digitalio import DigitalInOut import adafruit_esp32spi.adafruit_esp32spi_socket as socket from adafruit_esp32spi import adafruit_esp32spi import adafruit_requests as requests # 从settings.toml读取Wi-Fi信息 ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") # 初始化ESP32 SPI接口 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) # 创建请求会话 from adafruit_connection_manager import get_radio_socketpool, get_radio_ssl_context pool = get_radio_socketpool(esp) ssl_context = get_radio_ssl_context(esp) requests = requests.Session(pool, ssl_context) # 扫描并连接Wi-Fi print("扫描网络...") for ap in esp.scan_networks(): print("\t%-23s RSSI: %d" % (str(ap.ssid, 'utf-8'), ap.rssi)) print("连接至", ssid) while not esp.is_connected: try: esp.connect_AP(ssid, password) except RuntimeError as e: print("连接失败,重试中:", e) continue print("连接成功!IP地址:", esp.ipv4_address) # 测试HTTP请求 TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" try: response = requests.get(TEXT_URL) print("网络测试通过,服务器返回:", response.text) response.close() except Exception as e: print("网络请求失败:", e)通过串行终端(如Mu编辑器的串行模式、PuTTY或screen/tio命令)查看输出。如果看到“连接成功”和测试网页内容,恭喜你,PyPortal已经成功联入互联网。如果失败,请按以下顺序排查:
- 检查
settings.toml:变量名拼写是否正确?值是否在引号内? - 检查Wi-Fi信号:PyPortal距离路由器是否太远?RSSI值(信号强度)是否优于-70dBm?
- 检查网络类型:某些企业或公共网络可能有门户认证(Captive Portal),PyPortal无法自动处理。
- 检查库版本:确保使用的库文件与CircuitPython固件版本匹配。
4. 核心代码实现与工作原理剖析
4.1 项目代码结构解析
完成基础配置后,我们从项目页面下载完整的项目包(Project Bundle)。解压后,将PyPortal_NASA文件夹内的所有文件复制到CIRCUITPY驱动器的根目录。最终的文件结构应包含:
code.py:主程序文件。boot.py:启动脚本(由之前的unsafe_boot.py重命名而来,用于启用文件系统缓存)。settings.toml:你的配置文件。nasa_background.bmp:启动时的NASA背景图。fonts/Arial-12.bdf:用于显示标题和日期的字体文件。lib/目录:存放所有必要的库文件。
现在,我们深入code.py,看看这个魔法是如何发生的。
import time import board from adafruit_pyportal import PyPortal # 1. 定义数据源(NASA APOD API) DATA_SOURCE = "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY" # 2. 定义JSON数据中字段的路径 IMAGE_LOCATION = ["url"] TITLE_LOCATION = ["title"] DATE_LOCATION = ["date"] # 获取当前代码所在目录 cwd = ("/"+__file__).rsplit('/', 1)[0] # 3. 初始化PyPortal对象 pyportal = PyPortal(url=DATA_SOURCE, json_path=(TITLE_LOCATION, DATE_LOCATION), status_neopixel=board.NEOPIXEL, default_bg=cwd+"/nasa_background.bmp", text_font=cwd+"/fonts/Arial-12.bdf", text_position=((5, 220), (5, 200)), text_color=(0xFFFFFF, 0xFFFFFF), text_maxlen=(50, 50), image_json_path=IMAGE_LOCATION, image_resize=(320, 240), image_position=(0, 0)) # 4. 主循环 while True: try: response = pyportal.fetch() # 获取数据并更新显示 print("Response is", response) except RuntimeError as e: print("Some error occurred, retrying! -", e) time.sleep(30*60) # 等待30分钟代码逐行解读:
DATA_SOURCE:这是程序的起点,即NASA API的请求地址。你需要将DEMO_KEY替换为你从NASA获取的真实API密钥。- JSON路径:
IMAGE_LOCATION、TITLE_LOCATION、DATE_LOCATION这三个列表,指明了在返回的JSON对象中如何找到所需数据。["url"]表示取JSON根对象下的url字段值。如果数据结构是嵌套的,例如{"data": {"image": {"url": "..."}}}, 则路径应写为["data", "image", "url"]。 - PyPortal对象初始化:这是核心配置。
url:指定数据来源。json_path:指定要提取的文本字段及其路径(这里传入了两个路径:标题和日期)。status_neopixel:指定状态指示灯。default_bg:指定启动时和网络错误时显示的背景图。text_font,text_position,text_color,text_maxlen:分别配置文本的字体、位置(两个文本的位置)、颜色(白色)和最大长度(防止过长标题溢出屏幕)。image_json_path:指定图片URL在JSON中的路径。image_resize:将下载的图片缩放到屏幕尺寸(320x240)。image_position:图片在屏幕上的起始位置(左上角)。
- 主循环:程序进入一个无限循环,每隔30分钟调用一次
pyportal.fetch()。这个方法是一个“全能选手”,它会自动完成以下工作:连接Wi-Fi、向DATA_SOURCE发起请求、解析JSON、根据image_json_path找到图片URL、通过Adafruit IO服务转换图片格式、下载并缓存BMP图片、加载图片到屏幕、根据json_path提取文本并渲染到屏幕上。
4.2boot.py的作用与“不安全”警告
你可能会注意到,项目中要求将unsafe_boot.py重命名为boot.py。这个文件的作用是启用文件系统的写入缓存功能。因为频繁地下载和保存图片到Flash存储器,如果每次都直接写入,速度会很慢且影响Flash寿命。启用缓存后,数据会先写在RAM或一个临时区域,再批量写入,提升了性能。
重命名后重启,你会在串行终端看到一段“WARNING”警告,提示你正在将文件系统用作可写缓存,存在风险。这是预期内的提示,并非错误。它只是提醒你,这种操作模式在意外断电时可能有数据丢失风险。对于我们这个以读取为主、偶尔缓存图片的应用来说,可以接受。如果不想看到此警告,可以删除boot.py,但图片加载性能会下降。
4.3 图像处理流程揭秘
这是整个项目中最精妙也最容易被忽略的环节。PyPortal的屏幕原生支持显示BMP格式的图片,但NASA API返回的url链接指向的往往是JPG或PNG格式。adafruit_pyportal库巧妙地解决了这个格式转换问题:
- 请求与解析:
pyportal.fetch()获取JSON,并提取出图片url。 - 转换请求:库不会直接去下载
url的图片,而是将这个url作为参数,发送给一个由Adafruit维护的在线图片转换服务(这也是为什么需要Adafruit IO密钥的原因)。请求的格式大致是:https://io.adafruit.com/api/v2/你的用户名/image-transform.png?url=NASA图片链接&width=320&height=240。 - 服务端转换:Adafruit IO的服务接收到请求后,会去抓取NASA的图片,将其转换为320x240像素的16位RGB BMP格式。
- 下载与显示:转换后的BMP图片被下载到PyPortal,缓存到文件系统中,然后显示在屏幕上。
这个过程对开发者是完全透明的,但了解其原理有助于调试。例如,如果图片一直无法显示,但文本正常,问题可能出在:1) 你的Adafruit IO密钥配置错误;2) Adafruit IO服务暂时不可达;3) NASA的图片链接本身失效。
5. 高级定制与故障排除指南
5.1 个性化你的太空画廊
基础功能运行起来后,你可以从多个维度进行定制,让它更符合你的品味:
- 更换背景与字体:替换
nasa_background.bmp为你喜欢的任何320x240的BMP图片。你还可以使用Adafruit提供的工具,将TTF字体转换为BDF格式,替换fonts/目录下的字体文件,并修改code.py中的text_font路径。 - 调整布局:修改
text_position参数可以移动标题和日期的位置。例如,((10, 10), (10, 30))会将第一个文本(标题)放在(10,10),第二个文本(日期)放在(10,30)。 - 改变更新频率:修改
time.sleep(30*60)中的数值。例如,time.sleep(60*60)将变为每小时更新一次。请务必遵守NASA API每天50次的调用限制。 - 显示更多信息:NASA的JSON数据还包含
explanation(解释说明)和copyright(版权信息)字段。你可以修改json_path和text_position等参数,尝试在屏幕上显示更多内容,但需要注意屏幕空间有限。 - 添加交互功能:PyPortal的屏幕是触摸屏。你可以通过
adafruit_touchscreen库读取触摸事件,实现点击切换图片、查看详情等功能。这需要你修改主循环,加入触摸状态判断逻辑。
5.2 常见问题与解决方案实录
在实际制作和教学过程中,我遇到了不少典型问题。这里汇总一份排查清单:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 屏幕始终显示NASA背景图,无更新 | 1. Wi-Fi未连接。 2. settings.toml配置错误。3. NASA API密钥未替换或无效。 | 1. 查看串口输出,确认Wi-Fi连接成功并获取到IP。 2. 检查 settings.toml文件名、变量名拼写、值是否在引号内。3. 确保 code.py中的DATA_SOURCE里的DEMO_KEY已替换为你的真实密钥。 |
| 显示“Pystack exhausted”错误 | Python栈内存不足。 | 在settings.toml中确保已添加CIRCUITPY_PYSTACK_SIZE = 2048(或更大值如4096)。 |
| 图片加载失败,显示错误或空白 | 1. Adafruit IO密钥配置错误。 2. 网络超时。 3. NASA当日APOD媒体类型非图片(可能是视频)。 | 1. 确认AIO_USERNAME和AIO_KEY正确。2. 尝试增加 adafruit_requests库中的超时时间(需修改库文件,有一定难度)。3. 视频日无法显示图片,这是API内容决定的。可以尝试在代码中检查 json["media_type"],如果是"video"则跳过或显示默认图。 |
| 文本显示乱码或位置不对 | 1. 字体文件路径错误或损坏。 2. 文本坐标超出屏幕范围。 3. 文本颜色与背景色太接近。 | 1. 检查text_font路径,确保fonts文件夹和.bdf文件存在。2. 确保 text_position的坐标在(0,0)到(319,239)之间。3. 尝试将 text_color改为更醒目的颜色,如红色0xFF0000。 |
| 程序运行几次后死机 | 1. 内存泄漏(在循环中未正确释放资源)。 2. 文件系统缓存出错。 | 1. 确保在异常处理中也有适当的延迟和资源释放。主循环结构应保持简洁健壮。 2. 尝试格式化 CIRCUITPY驱动器(备份代码和库后),重新部署文件。 |
| 串口输出显示SSL证书错误 | 系统时间不正确,导致SSL证书验证失败。 | PyPortal没有实时时钟(RTC),需要先通过网络更新时间。可以在代码开始时增加一段使用ntp或世界时间API同步时间的逻辑。 |
一个关键的调试技巧:充分利用串行输出(REPL)。在code.py的关键步骤添加print()语句,例如打印获取到的JSON、网络状态、图片URL等。这是诊断物联网设备问题最直接有效的方法。当你对代码进行修改后,如果遇到问题,首先查看串口输出,通常错误信息会直接显示在那里。
最后,关于电源。PyPortal通过USB供电,非常方便。如果你想让它脱离电脑长期运行,可以使用一个5V/2A的USB电源适配器。确保电源稳定,不稳定的电源可能导致Wi-Fi模块重启或设备意外复位。这个项目本身功耗不高,是一个非常适合长期展示的“永动”艺术品。看着它每天自动为你带来一片新的宇宙,你会觉得所有的调试和等待都是值得的。
