
1. 项目概述一个会“说话”的桌面信息站如果你和我一样桌上总摆着几块开发板那你肯定理解那种想把网络上的动态信息“实体化”的冲动。比如你常泡的Discord社区有多少人正在线与其每次打开手机或电脑查看不如让它变成一个常驻桌面的小摆件一目了然。这就是我们今天要动手实现的项目用一块Adafruit PyPortal制作一个Discord在线人数显示器。PyPortal是Adafruit推出的一款极具创意的物联网显示设备。它本质上是一块集成了ESP32 Wi-Fi模块、3.5英寸触摸屏、MicroSD卡槽和一系列传感器的“全能型”开发板。它的核心魅力在于你无需焊接任何外围电路也无需复杂的底层驱动开发通过CircuitPython这种对初学者极其友好的编程语言就能快速实现从网络获取数据到图形化显示的全过程。这大大降低了物联网应用的门槛让你能把创意焦点集中在“做什么”而不是“怎么做”上。这个项目的核心逻辑非常清晰让PyPortal定期访问一个特定的网络服务shields.io该服务会生成一个包含Discord在线人数的SVG格式小图标俗称“徽章”。我们的代码需要下载这个SVG文件从中“挖出”代表在线人数的数字最后将这个数字漂亮地显示在屏幕上。整个过程涉及网络连接、数据抓取、文本解析正则表达式和图形渲染是一个典型的、完整的嵌入式物联网应用案例。无论你是想监控社区活跃度还是想以此为蓝本显示天气、股价、GitHub星标数这个项目都能为你提供一个坚实的起点。2. 硬件与软件环境准备2.1 核心硬件认识你的PyPortal工欲善其事必先利其器。首先你需要准备以下硬件Adafruit PyPortal本项目的主角。确保你拿到的是标准版PyPortal而非Titano大屏版或Pynt无屏幕版因为驱动和分辨率可能有所不同。USB数据线一条可靠的Micro-USB数据线。这里有个新手常踩的坑务必使用能传输数据的数据线而不是仅能充电的线。很多手机附赠的短线就是充电专用线会导致电脑无法识别设备。如果你不确定最好用板子原配的线或者找一条曾经成功用于数据传输的线。MicroSD卡可选但推荐虽然PyPortal板载8MB存储但存放字体、图片等资源后空间会紧张。我强烈建议插上一张格式化为FAT32的MicroSD卡建议容量不超过32GB这样你可以存放更多自定义素材项目灵活性大增。5V 2A电源适配器长期运行必备当你调试完成想让它7x24小时运行时一个稳定的5V 2A电源适配器比电脑USB口供电更可靠能避免因供电不足导致的意外重启。PyPortal的硬件布局很直观正面是触摸屏顶部中央有一个复位按钮Reset旁边是RGB NeoPixel状态指示灯。背部可以看到ESP32模块、SD卡槽和主要的连接器。首次拿到板子建议先通过USB线连接到电脑看看系统是否能识别出一个名为PORTALBOOT的U盘这是刷写固件的第一步。2.2 软件基石安装CircuitPythonCircuitPython是MicroPython的一个分支由Adafruit主导开发其设计哲学就是“极简”和“教育”。它最大的特点是当你把开发板连接到电脑后它会显示为一个名为CIRCUITPY的U盘。你的代码文件code.py和库文件直接放在这个U盘里板子就会自动运行无需任何复杂的编译、烧录工具链。安装步骤详解下载固件访问 CircuitPython官网 找到对应PyPortal的最新版本.uf2文件并下载。请务必确认文件名中包含“pyportal”而不是其他型号。进入引导加载模式用USB线连接PyPortal和电脑。快速双击板子顶部的Reset按钮。此时板载的NeoPixel指示灯应该会变成绿色如果变红通常是USB线或端口问题。电脑上会出现一个名为PORTALBOOT的新磁盘驱动器。刷写固件将刚才下载的.uf2文件直接拖拽或复制到PORTALBOOT磁盘中。完成后PORTALBOOT盘符会消失稍等片刻会出现一个新的名为CIRCUITPY的磁盘驱动器。这就意味着CircuitPython系统已经成功安装并启动了。注意第一次进入CIRCUITPY驱动器你可能只看到一个boot_out.txt文件这是正常的。它记录了固件版本信息。你的代码和库文件需要后续手动添加。2.3 安装必需的库文件CircuitPython的强大功能依赖于各种库文件。Adafruit将这些库打包成一个“捆绑包”Bundle。你需要根据刚刚安装的CircuitPython版本下载对应的库捆绑包。下载库捆绑包前往 Adafruit CircuitPython库捆绑包发布页面 。找到文件名类似adafruit-circuitpython-bundle-py-2025xxxx.zip用于新版或adafruit-circuitpython-bundle-8.x-mpy-2025xxxx.zip用于8.x版的文件并下载。请确保版本号的大版本如7.x, 8.x, 9.x与你boot_out.txt中记录的CircuitPython主版本号一致。解压并复制库文件解压下载的ZIP文件你会看到一个lib文件夹。对于PyPortal项目我们至少需要以下核心库。你可以将整个lib文件夹复制到CIRCUITPY根目录但这样会占用较多空间。更推荐的做法是只复制需要的库adafruit_esp32spi/用于通过ESP32进行SPI通信和Wi-Fi连接。adafruit_requests/用于发起HTTP/HTTPS网络请求类似于Python中的requests库。adafruit_connection_manager/被adafruit_requests用来管理网络连接。adafruit_pyportal/核心库提供了高级API简化了在PyPortal上显示图形、文本和获取网络数据的操作。adafruit_portalbase/adafruit_pyportal库的基础库。adafruit_display_text/用于在屏幕上显示文本。adafruit_bitmap_font/用于加载和解析点阵字体文件。adafruit_imageload/用于加载和显示图像文件如BMP格式背景图。adafruit_touchscreen/用于处理电阻触摸屏的输入。neopixel.mpy用于控制板载的RGB NeoPixel指示灯。将这些库的文件夹或.mpy文件复制到CIRCUITPY驱动器下的lib目录中如果lib目录不存在就新建一个。3. 项目核心配置与网络连接3.1 安全第一配置settings.toml文件在物联网项目中Wi-Fi密码、API密钥等敏感信息绝不能硬编码在代码里。CircuitPython 8及以上版本引入了settings.toml文件来安全地管理这些配置。它位于CIRCUITPY根目录代码通过os.getenv()函数来读取。在你的CIRCUITPY驱动器根目录下用任何文本编辑器如VS Code、Notepad创建一个新文件命名为settings.toml。文件内容如下CIRCUITPY_WIFI_SSID 你的Wi-Fi网络名称 CIRCUITPY_WIFI_PASSWORD 你的Wi-Fi密码重要提示与避坑指南格式严格等号两边有空格字符串值必须用双引号括起来。编码确保文本编辑器以UTF-8无BOM格式保存此文件。在Windows的记事本中保存时选择“另存为”编码选择“UTF-8”。变量名CIRCUITPY_WIFI_SSID和CIRCUITPY_WIFI_PASSWORD是CircuitPython网络库识别的标准变量名不要随意更改。隐藏文件在某些系统上settings.toml可能被隐藏。如果连接Wi-Fi失败首先检查这个文件是否确实存在于CIRCUITPY根目录并且内容正确。3.2 测试网络连接在编写主程序前我们先确保PyPortal能成功连接网络。将以下测试代码保存为CIRCUITPY根目录下的code.py如果已有请先备份。# SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT import os import time 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引脚PyPortal已预定义 esp32_cs DigitalInOut(board.ESP_CS) esp32_ready DigitalInOut(board.ESP_BUSY) esp32_reset DigitalInOut(board.ESP_RESET) # 初始化SPI总线 spi busio.SPI(board.SCK, board.MOSI, board.MISO) esp adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) # 初始化requests会话 requests.set_socket(socket, esp) print(PyPortal Wi-Fi连接测试) print(MAC地址:, :.join(%02X % b for b in esp.MAC_address)) # 扫描网络可选 print(扫描网络中...) for ap in esp.scan_networks(): print(\t%-23s 信号强度: %d dBm % (ap.ssid, ap.rssi)) # 连接Wi-Fi print(正在连接: %s % ssid) while not esp.is_connected: try: esp.connect_AP(ssid, password) except RuntimeError as e: print(连接失败重试中..., e) time.sleep(2) continue print(已连接到:, esp.ap_info.ssid) print(IP地址:, esp.ipv4_address) # 测试HTTP请求 print(\n测试网络连通性...) TEXT_URL http://wifitest.adafruit.com/testwifi/index.html try: response requests.get(TEXT_URL) print(HTTP响应状态码:, response.status_code) print(响应内容前100字符:, response.text[:100]) response.close() except Exception as e: print(HTTP请求失败:, e) print(\n测试完成。)保存文件后PyPortal会自动重启并运行。打开串口监视器如Mu编辑器、VS Code的串口插件或Putty波特率设置为115200。你应该能看到连接过程日志最终打印出IP地址和测试网页的内容。如果卡在连接阶段请按顺序检查settings.toml文件、Wi-Fi信号强度、USB数据线。实操心得在初期调试时我强烈建议使用WiFiManager这个更健壮的类来管理连接。它内置了自动重连机制能有效应对网络波动。上述基础连接代码仅用于理解原理在实际项目中尤其是需要长时间稳定运行的情况下使用WiFiManager是更专业的选择。4. 核心代码解析与实现4.1 数据获取的巧思从Shields.io到正则表达式本项目的数据源头并非一个标准的REST API返回的JSON而是一个由 shields.io 服务生成的SVG图像。Shields.io通常用于在GitHub等地方生成“徽章”显示构建状态、版本号等信息。对于Discord它提供了一个直接显示服务器在线人数的SVG端点。数据链路分析目标URLhttps://img.shields.io/discord/327254708534116352.svg。其中的数字327254708534116352是Adafruit Discord服务器的ID。如果你想监控自己的服务器需要替换成你的服务器ID。数据载体访问上述URL你会得到一个SVG可缩放矢量图形文件。在浏览器中它显示为一个美观的徽章。但在代码层面它是一段XML文本。数据提取挑战我们需要的是徽章上显示的数字如“1169 online”。这段文本就“埋藏”在SVG的XML代码中。由于它不是结构化的数据如JSON的key: value我们不能直接按字段解析。这时正则表达式Regular Expression, RegEx就派上了用场。正则表达式实战拆解我们需要的模式是一个大于号后面跟着一组数字然后是一个空格和单词“online”最后是一个小于号。在SVG代码中它看起来像这样1169 online。对应的正则表达式为r([0-9] online)r表示这是一个原始字符串避免Python对反斜杠进行转义。匹配字面量的大于号。(和)捕获括号括号内的内容将被提取出来作为匹配结果。[0-9][0-9]匹配任意一个数字表示前面的元素数字出现一次或多次。因此这部分匹配一个或多个连续的数字。online匹配一个空格后跟着单词“online”。匹配字面量的小于号。所以这个表达式会在SVG文本中寻找数字 online这样的模式并将数字 online例如“1169 online”这个整体捕获出来。这就是我们最终要显示在屏幕上的字符串。4.2 主程序代码逐行解读理解了数据来源和提取方法后我们来看完整的code.py。与之前的测试代码不同这里我们使用更高级的adafruit_pyportal库它能极大地简化显示和网络操作。# SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT import time import board from adafruit_pyportal import PyPortal from adafruit_portalbase.network import CONTENT_TEXT # 1. 定义数据源Discord在线人数徽章的SVG地址 DATA_SOURCE https://img.shields.io/discord/327254708534116352.svg # 2. 定义正则表达式路径用于从SVG中提取数据 DATA_LOCATION [r([0-9] online)] # 3. 获取当前代码所在目录用于定位资源文件 cwd (/__file__).rsplit(/, 1)[0] # 4. 创建PyPortal对象这是核心控制器 pyportal PyPortal( urlDATA_SOURCE, # 要请求的URL regexp_pathDATA_LOCATION, # 用于解析响应的正则表达式 status_neopixelboard.NEOPIXEL, # 使用板载NeoPixel作为状态指示灯 default_bgcwd/discord_background.bmp, # 背景图片路径 text_fontcwd/fonts/Collegiate-50.bdf, # 文本字体文件路径 text_position(70, 216), # 文本在屏幕上的显示位置 (x, y) text_color0x000000, # 文本颜色黑色十六进制RGB ) # 5. 主循环定期获取并更新显示 while True: try: # 发起网络请求并强制将内容按文本处理然后应用正则表达式 value pyportal.fetch(force_content_typeCONTENT_TEXT) print(当前在线人数, value) # 串口输出用于调试 except (RuntimeError, OSError) as e: # 网络错误处理打印错误并继续尝试 print(获取数据时出错重试中... -, e) # 等待60秒后再次更新 time.sleep(60)代码关键点解析PyPortal构造函数这个对象封装了几乎所有复杂操作。你只需告诉它数据在哪url、如何解析regexp_path、显示什么default_bg,text_font等它就会自动处理网络连接、图形渲染和触摸事件本例未使用。资源路径cwd变量通过魔术方法__file__获取当前脚本所在目录。这意味着你可以把code.py、背景图discord_background.bmp和字体文件都放在CIRCUITPY的同一个文件夹下代码会自动找到它们。这是一种非常便携的部署方式。fetch()方法这是核心的“获取-解析-更新”方法。force_content_typeCONTENT_TEXT参数至关重要它告诉库将响应体作为纯文本处理而不是尝试解析为JSON或其他格式这样正则表达式才能生效。错误处理网络请求可能因信号、服务中断等原因失败。用try...except包裹fetch()调用并打印错误信息可以让你在串口监视器中看到问题所在而不是让程序完全崩溃。程序会在捕获异常后等待60秒继续尝试。更新频率time.sleep(60)意味着每分钟更新一次。对于在线人数这种变化不频繁的数据这个间隔是合理的。你可以根据需求调整但请注意过于频繁的请求可能对服务器不友好且会增加功耗。4.3 资源文件准备与部署代码依赖于两个资源文件背景图片和字体。背景图片 (discord_background.bmp)格式必须是320x240像素的16位RGB BMP位图。这是PyPortal屏幕的原生分辨率。制作你可以用Photoshop、GIMP或在线工具制作。一个简单的办法是先创建一张320x240的画布设计好背景然后务必另存为“BMP”格式并选择“16位 R5 G6 B5”或类似的16位RGB选项。保存为24位或32位BMP可能导致显示异常。放置将制作好的discord_background.bmp文件放在与code.py相同的目录下。字体文件 (Collegiate-50.bdf)来源Adafruit提供了丰富的BDF格式点阵字体。你可以在 CircuitPython库捆绑包的fonts/目录 中找到Collegiate-50.bdf和其他字体。-50表示字体高度约为50像素在240像素高的屏幕上比较醒目。自定义如果你想使用其他字体需要将其转换为BDF格式。Adafruit提供了工具和指南但过程稍复杂。初期建议直接使用捆绑包内的字体。放置在CIRCUITPY驱动器上创建一个fonts文件夹将Collegiate-50.bdf字体文件放进去。代码中的路径cwd/fonts/Collegiate-50.bdf会指向它。最终文件结构 你的CIRCUITPY驱动器看起来应该是这样的lib目录已省略CIRCUITPY/ ├── code.py ├── settings.toml ├── discord_background.bmp └── fonts/ └── Collegiate-50.bdf将所有这些文件代码、配置、图片、字体按照上述结构准备好并放入CIRCUITPY后PyPortal会自动重启运行。稍等片刻你应该就能在屏幕上看到自定义的背景图以及从网络获取并解析出的Discord在线人数了。5. 深度定制与优化指南基础功能实现后我们可以让它变得更个性、更稳定。5.1 视觉定制打造专属风格更换背景这是最直接的个性化方式。准备任何你喜欢的320x240的16位BMP图片。可以是静态图案也可以设计成带有信息框的模板让数字显示在特定区域。确保图片对比度足够文字颜色text_color要能与背景区分开。调整文字属性位置 (text_position)参数是(x, y)元组原点(0,0)在屏幕左上角。你可以通过微调这两个值让数字精确对齐背景图中的某个位置。一个技巧是先在背景图上用绘图软件标记出理想位置再将其坐标填入代码。颜色 (text_color)使用十六进制RGB值如0xFF0000是红色0x00FF00是绿色0x0000FF是蓝色0xFFFFFF是白色。黑色是0x000000。字体尝试捆绑包中的其他BDF字体如Arial-12.bdf小字或LucidaGrande-24.bdf。不同字体的风格和大小会影响整体视觉效果。5.2 功能扩展不止于Discord这个项目的框架具有通用性。任何能通过HTTP获取并包含可解析文本数据的服务都可以成为数据源。示例显示GitHub仓库星标数假设你想显示某个GitHub仓库的Star数量。Shields.io也提供此类徽章。修改数据源DATA_SOURCE https://img.shields.io/github/stars/adafruit/circuitpython.svg修改正则表达式观察下载的SVG文件星标数的模式可能是([0-9k])数字可能带“k”后缀。你需要根据实际SVG内容调整表达式例如r([0-9k\.])来匹配数字和“k”。更新显示文本你可能需要在代码中对提取的字符串进行后处理比如在显示时加上“ stars”后缀。示例显示天气信息许多天气API如OpenWeatherMap提供JSON格式的数据解析起来比正则表达式更简单。使用JSON数据源将url指向一个返回JSON的API端点。使用json_path代替regexp_pathPyPortal对象支持json_path参数你可以直接指定JSON的键路径来提取数据例如DATA_LOCATION [current, temp]。注意API Key如果需要API Key将其添加到settings.toml中例如OPENWEATHER_API_KEY your_key_here然后在代码中通过os.getenv读取并拼接到URL中。5.3 稳定性优化引入WiFiManager之前的代码在网络异常时仅进行简单重试。对于需要长期运行的设备使用WiFiManager是更好的选择。它能自动处理网络断开重连、ESP32硬件复位等复杂情况。优化后的主循环结构示例import time from adafruit_pyportal import PyPortal from adafruit_portalbase.network import CONTENT_TEXT import board # ... DATA_SOURCE, DATA_LOCATION, cwd 定义同上 ... # 初始化PyPortal时启用status_neopixelWiFiManager会用它指示状态 pyportal PyPortal( urlDATA_SOURCE, regexp_pathDATA_LOCATION, status_neopixelboard.NEOPIXEL, # WiFiManager将使用此LED default_bgcwd/discord_background.bmp, text_fontcwd/fonts/Collegiate-50.bdf, text_position(70, 216), text_color0x000000, ) # 主循环 while True: try: # pyportal.fetch() 内部已经使用了WiFiManager的增强功能 value pyportal.fetch(force_content_typeCONTENT_TEXT, timeout30) # 增加超时 print(更新成功:, value) # 成功获取后可以添加一个成功状态的视觉反馈例如点亮LED pyportal.set_text(value) # 更新屏幕显示 except Exception as e: # 此处捕获的异常通常是fetch内部重试多次后仍失败抛出的最终异常 print(严重错误更新失败:, e) # 可以在这里让NeoPixel闪烁红色报警 pyportal.neopixel.fill((255, 0, 0)) time.sleep(0.5) pyportal.neopixel.fill((0, 0, 0)) time.sleep(60) # 更新间隔使用adafruit_pyportal库初始化的对象其底层的网络连接默认就由WiFiManager管理因此稳定性已经比手动管理ESP32连接要好得多。timeout参数可以防止单次请求无限期挂起。6. 常见问题排查与调试技巧即使按照步骤操作也可能会遇到问题。以下是几个常见故障点及其解决方法。6.1 问题排查速查表现象可能原因排查步骤屏幕白屏或无显示1. 未正确供电。2. 代码执行出错卡住。3. 背景图路径错误或格式不对。1. 检查USB线连接尝试换端口或电源。2. 查看串口输出是否有Python错误信息。3. 确认default_bg路径正确图片是320x240 16位BMP。串口输出Connecting to AP...后卡住1.settings.toml配置错误或丢失。2. Wi-Fi密码错误。3. 网络信号太弱。4. ESP32固件问题。1. 确认CIRCUITPY根目录下有settings.toml且SSID/密码正确格式无误UTF-8无BOM。2. 检查密码注意大小写和特殊字符。3. 将PyPortal移近路由器。4. 尝试在代码中硬编码SSID/密码仅用于测试排除文件问题。串口显示连接成功但获取数据失败1. 网络防火墙或屏蔽了shields.io。2. 正则表达式不匹配数据格式已变化。3.force_content_type参数未设置或设置错误。1. 用电脑浏览器直接访问DATA_SOURCE的URL看是否能打开并看到徽章。2. 将fetch()返回的原始文本打印出来print(response.text)检查SVG内容是否变化并调整正则表达式。3. 确保调用fetch()时传入了force_content_typeCONTENT_TEXT。显示“MemoryError”或程序崩溃1. 内存不足可能因图片/字体过大或内存泄漏。2. 库文件不完整或版本冲突。1. 尝试使用更小的背景图确保仍是320x240但颜色简单或更小的字体文件。2. 确保从捆绑包复制的库是完整的且与CircuitPython版本匹配。清理CIRCUITPY上不必要的文件。数字显示位置或颜色不对text_position或text_color参数设置错误。1. 调整text_position的x, y值。注意y轴向下为正。2. 检查text_color的十六进制值是否正确例如红色是0xFF0000而非#FF0000。更新间隔不稳定网络请求耗时波动大或异常处理导致循环时间不固定。在主循环开始记录时间戳在time.sleep()前计算实际耗时动态调整休眠时间使更新间隔尽量固定。6.2 高级调试技巧充分利用串口输出在代码关键位置添加print()语句是嵌入式调试最基本也最有效的方法。打印网络状态、获取的原始数据、解析后的值等。模拟数据测试在开发时可以先绕过网络请求使用一个本地字符串来测试显示逻辑。例如将value pyportal.fetch(...)暂时替换为value 1234 online这样可以快速验证图形显示部分是否正常。检查电源不稳定的电源会导致ESP32重启或屏幕闪烁。如果你打算长期插电运行务必使用质量合格的5V 2A电源适配器避免使用电脑USB口或充电宝。管理SD卡如果使用了SD卡确保其格式化为FAT32并且没有损坏。有时读取SD卡上的资源失败会导致程序静默失败。可以尝试将资源文件图片、字体放在板载闪存中测试以排除SD卡问题。这个项目就像一把钥匙为你打开了用硬件便捷访问和显示网络信息的大门。它的价值不在于仅仅显示一个数字而在于提供了一个清晰、可复用的模式。当你掌握了从网络获取数据、解析数据、渲染显示这一完整链条后你的桌面信息站就可以轻松变身为天气预报站、航班状态板、待办事项提醒器甚至是智能家居的控制中心。硬件就在你手中数据在云端剩下的就是你的想象力了。