1. 项目概述打造你的专属太空画廊几年前当我第一次把一块小小的PyPortal开发板连上家里的Wi-Fi看着它从浩瀚的互联网中抓取并显示出一张壮丽的星云照片时那种感觉非常奇妙。这不仅仅是一个技术Demo更像是在桌面上打开了一扇随时窥探宇宙的窗户。今天要分享的就是如何用CircuitPython和PyPortal亲手制作一个NASA“每日天文图”APOD显示器。这个项目完美诠释了物联网IoT的精髓一个简单的嵌入式设备通过网络获取云端数据并以直观的方式呈现出来。它不需要复杂的Linux系统或庞大的计算资源仅凭一块微控制器、一块屏幕和一份Python代码就能实现。无论你是想学习嵌入式开发、CircuitPython编程还是单纯想做一个酷炫的桌面摆件这个项目都是一个绝佳的起点。整个过程涉及网络连接、API调用、JSON数据解析和图形显示是打通物联网全栈流程的经典案例。2. 核心硬件与软件栈解析2.1 为什么选择PyPortalPyPortal是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提供了一个免费的开放APIapi.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.mpyESP32 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栈大小可能不足以处理图像转换和网络请求的复杂操作会导致MemoryError或Pystack exhausted错误。将其增加到2048或更大可以解决此问题。实操心得在编辑settings.toml时确保使用纯文本编辑器如VS Code、Notepad、Mu编辑器并以UTF-8无BOM格式保存。错误的编码可能导致CircuitPython无法正确解析其中的字符串特别是当你的Wi-Fi密码包含特殊字符时。3.3 网络连接测试与排错在运行主程序前强烈建议先运行一个简单的网络测试脚本验证硬件、库和配置是否正确。将以下代码保存为code.py并放入CIRCUITPYimport 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 PortalPyPortal无法自动处理。检查库版本确保使用的库文件与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_keyDEMO_KEY # 2. 定义JSON数据中字段的路径 IMAGE_LOCATION [url] TITLE_LOCATION [title] DATE_LOCATION [date] # 获取当前代码所在目录 cwd (/__file__).rsplit(/, 1)[0] # 3. 初始化PyPortal对象 pyportal PyPortal(urlDATA_SOURCE, json_path(TITLE_LOCATION, DATE_LOCATION), status_neopixelboard.NEOPIXEL, default_bgcwd/nasa_background.bmp, text_fontcwd/fonts/Arial-12.bdf, text_position((5, 220), (5, 200)), text_color(0xFFFFFF, 0xFFFFFF), text_maxlen(50, 50), image_json_pathIMAGE_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?urlNASA图片链接width320height240。服务端转换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模块重启或设备意外复位。这个项目本身功耗不高是一个非常适合长期展示的“永动”艺术品。看着它每天自动为你带来一片新的宇宙你会觉得所有的调试和等待都是值得的。