当前位置: 首页 > news >正文

用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主导开发,特别注重易用性和教育性。其核心设计哲学是“迭代速度至上”:

  1. 无需编译:你的代码文件(code.py)直接以文本形式存放在名为CIRCUITPY的U盘里。保存文件即等于刷入程序,几乎瞬间生效。
  2. 交互式编程:通过串行REPL(交互式解释器),你可以像在电脑上使用Python一样,实时查询传感器数值、测试函数,进行调试。
  3. 丰富的“库”生态: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中提取出urltitle等信息。

3. 项目环境搭建与配置详解

3.1 固件刷写与基础文件准备

拿到PyPortal后,第一步是让它运行CircuitPython。这个过程被设计得非常简单,类似于为U盘拷贝文件:

  1. 下载固件:访问CircuitPython官网,根据你的PyPortal具体型号(如PyPortal、PyPortal Pynt等)下载最新的.uf2固件文件。务必确认型号匹配。
  2. 进入引导加载模式:用一条数据线(强调:必须是支持数据传输的USB线,充电线不行)连接PyPortal和电脑。快速双击板子上的Reset按钮。此时,板载的NeoPixel LED应变为绿色,电脑上会出现一个名为PORTALBOOT的U盘。
  3. 刷入固件:将下载好的.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.mpyadafruit_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栈大小可能不足以处理图像转换和网络请求的复杂操作,会导致MemoryErrorPystack 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已经成功联入互联网。如果失败,请按以下顺序排查:

  1. 检查settings.toml:变量名拼写是否正确?值是否在引号内?
  2. 检查Wi-Fi信号:PyPortal距离路由器是否太远?RSSI值(信号强度)是否优于-70dBm?
  3. 检查网络类型:某些企业或公共网络可能有门户认证(Captive Portal),PyPortal无法自动处理。
  4. 检查库版本:确保使用的库文件与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分钟

代码逐行解读

  1. DATA_SOURCE:这是程序的起点,即NASA API的请求地址。你需要将DEMO_KEY替换为你从NASA获取的真实API密钥。
  2. JSON路径IMAGE_LOCATIONTITLE_LOCATIONDATE_LOCATION这三个列表,指明了在返回的JSON对象中如何找到所需数据。["url"]表示取JSON根对象下的url字段值。如果数据结构是嵌套的,例如{"data": {"image": {"url": "..."}}}, 则路径应写为["data", "image", "url"]
  3. 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:图片在屏幕上的起始位置(左上角)。
  4. 主循环:程序进入一个无限循环,每隔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库巧妙地解决了这个格式转换问题:

  1. 请求与解析pyportal.fetch()获取JSON,并提取出图片url
  2. 转换请求:库不会直接去下载url的图片,而是将这个url作为参数,发送给一个由Adafruit维护的在线图片转换服务(这也是为什么需要Adafruit IO密钥的原因)。请求的格式大致是:https://io.adafruit.com/api/v2/你的用户名/image-transform.png?url=NASA图片链接&width=320&height=240
  3. 服务端转换:Adafruit IO的服务接收到请求后,会去抓取NASA的图片,将其转换为320x240像素的16位RGB BMP格式。
  4. 下载与显示:转换后的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_pathtext_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_USERNAMEAIO_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模块重启或设备意外复位。这个项目本身功耗不高,是一个非常适合长期展示的“永动”艺术品。看着它每天自动为你带来一片新的宇宙,你会觉得所有的调试和等待都是值得的。

http://www.cnnetsun.cn/news/2430254.html

相关文章:

  • 如何深度挖掘NVIDIA显卡隐藏性能:NVIDIA Profile Inspector实战指南
  • 基于STM32的铁路自动围栏系统:嵌入式开发全流程实战解析
  • 移动通信芯片自研挑战:拆解高通技术、生态与供应链壁垒
  • ARM CCI-500寄存器配置与缓存一致性管理详解
  • 2026届必备的十大AI论文助手实测分析
  • 终极指南:bilibili-downloader快速下载B站4K视频完整教程
  • ZVS电路里的‘能量搬运工’:扼流圈L3与谐振回路参数设计的实战指南
  • 当滑块验证码遇上VMP:浅析某讯前端混淆方案与自写解释器的踩坑记录
  • 为什么你的ElevenLabs叫号语音被顾客投诉“像机器人”?——声纹温度调节、语速断句、本地化停顿的3层情感增强技术揭秘
  • 终极指南:MAA明日方舟助手从入门到精通的全流程解析
  • 3步掌握UABEA:跨平台Unity游戏资源编辑器终极指南
  • Magpie-LuckyDraw:企业级开源抽奖系统的全平台部署方案
  • R3nzSkin国服换肤工具:五分钟免费解锁英雄联盟全皮肤体验
  • RabbitMQ镜像队列与集群
  • 3个淘金币自动化方案:告别手动点击,每日轻松赚取淘宝金币
  • Simulink嵌入式代码生成实战:从模型到C代码的完整指南
  • 长期使用Taotoken后对账单追溯与审计日志功能的实际评价
  • 自托管信息聚合器FeedMe:全栈部署与高效信息管理实践
  • 基于奇异值分解(SVD)的图片压缩:原理、Python实现与效果量化分析
  • 从原理到批量利用:深入剖析Apache Superset默认密钥漏洞(CVE-2023-27524)
  • Umi-CUT:3分钟搞定100张图片黑边裁剪的智能批量处理神器
  • 华为悦盒EC6108V9C刷Linux踩坑实录:从ADB连接到Docker跑Alist,我遇到的5个问题及解决方法
  • Legacy iOS Kit终极指南:如何让经典iPhone和iPad重获新生
  • 小白程序员必看:收藏这份大模型Agent开发学习指南,轻松入门字节跳动暑期实习
  • STM32驱动SYN6288语音合成模块:从零构建智能语音交互系统(附完整工程)
  • AI Agent如何重塑软件开发:从代码生成到自动化测试的完整生态分析
  • 如何永久珍藏你的微信数字记忆?WeChatMsg让聊天记录成为永恒财富!
  • 基于Go与SQLite构建私有化RESTful笔记API:Rocketnotes部署与二次开发指南
  • 3分钟学会:如何用开源工具Unlock Music免费解锁加密音乐文件
  • LrcHelper:网易云音乐双语歌词下载神器 - 5分钟快速上手指南