1. 项目缘起与整体设计思路1.1 这个项目到底在做什么PyPortal Flyover Viewer 这个名字拆开看就三块信息PyPortal 是硬件载体Flyover 是“飞越”的意思Viewer 说明它是个查看器。连起来讲就是拿一块 Adafruit 出品的 PyPortal 开发板做成一个能实时显示卫星飞越本地天空情况的桌面小终端。它从 USGS美国地质调查局的公开数据接口拉取信息在屏幕上渲染出卫星轨迹、过境时间、地面覆盖范围这类内容让你抬头看天之前先低头看一眼板子知道接下来几分钟头顶会有什么东西经过。我第一次接触这个项目是在折腾 CircuitPython 生态的时候。PyPortal 这块板子本身定位很清晰——3.2 寸 320x240 的 TFT 触摸屏、ESP32 主控、自带 Wi-Fi 模块、外壳是亚克力加螺丝固定的成品形态插上 USB 就能跑代码。它不像树莓派那样需要装系统、配环境也不像裸 ESP32 那样要自己焊屏幕和外围电路。对想快速做一个“联网信息展示终端”的人来说PyPortal 几乎是开箱即用的选择。Flyover Viewer 解决的痛点很具体卫星过境信息在网页上查得到但每次都要打开手机、进网站、输入位置、看列表流程太长。做成一个常亮的小屏幕挂在墙上或摆在桌上数据自动刷新抬眼就能看到体验完全不一样。适合谁参考呢一是手里已经有 PyPortal 想找点实际用途的人二是想学 CircuitPython 联网项目但不知道从哪下手的人三是对卫星轨道、地理数据可视化感兴趣的爱好者。哪怕你之前只写过 Python 脚本、没碰过硬件这个项目的代码结构也足够友好改改参数就能跑。1.2 为什么选 PyPortal 而不是别的方案方案选型这件事我踩过不少坑。最早我想用树莓派 Zero 加一块小屏幕来做理由是 Linux 环境熟悉、Python 库多。实际做下来发现几个问题树莓派启动慢断电重启要等半分钟以上系统盘容易因为突然断电损坏功耗比单片机高一个量级长期插电虽然能接受但发热和稳定性都不如单片机方案。后来又考虑过 ESP32 自己配屏幕成本是低了但接线、调试、外壳加工全是额外工作量做出来的东西也不够“成品感”。PyPortal 的优势在于它把“联网 显示 外壳”这三件事打包好了。CircuitPython 是 Python 的微控制器版本语法和标准 Python 几乎一样但运行在单片机上没有操作系统代码直接解释执行。这意味着你不需要处理 Linux 的包管理、系统服务、权限这些杂事写一个code.py丢进去就完事。对于 Flyover Viewer 这种“定时拉数据、刷新屏幕”的单一任务单片机方案在稳定性和响应速度上都更合适。还有一个关键点是 Adafruit 的生态。CircuitPython 有大量官方维护的库比如adafruit_requests处理 HTTP 请求、adafruit_display_text做文字渲染、adafruit_bitmap_font加载自定义字体。这些库都针对单片机做了内存优化API 设计也统一。你不需要自己从 socket 层写起几行代码就能完成一次 HTTPS 请求。这对快速原型来说太重要了能把精力集中在业务逻辑上而不是底层通信。1.3 数据源的选择与接口逻辑USGS 提供的是地球观测和地质相关数据Flyover Viewer 用到的部分主要是卫星过境预测和地面轨迹数据。这里要说明一下USGS 的公开接口有很多种有地震数据、水文数据、地形数据也有和卫星观测相关的产品。项目里实际调用的是能返回卫星轨道参数或过境时间窗口的接口具体用哪个端点取决于你想展示什么内容。我自己的做法是先用浏览器或 curl 手动请求一次接口把返回的 JSON 结构看清楚再决定在 CircuitPython 里怎么解析。这一步很关键因为单片机的内存有限不能像在电脑上那样随便加载大文件。如果接口返回的数据量很大就需要在请求时加参数过滤只取自己需要的字段。比如只要未来 24 小时内的过境事件或者只要特定卫星的轨迹点。接口调用的频率也要考虑。卫星轨道数据更新不会特别频繁没必要每秒请求一次。我一般设置成每 15 到 30 分钟刷新一次既保证数据不会太旧又不会给接口造成压力也省电。CircuitPython 里可以用time.sleep()做简单延时但更好的方式是结合supervisor.ticks_ms()做非阻塞的定时判断这样在等待期间屏幕还能响应触摸或其他事件。2. 核心细节解析与实操要点2.1 硬件准备与接线确认PyPortal 的硬件准备比想象中简单因为它本身就是成品。你拿到手的东西包括PyPortal 主板一块、亚克力外壳一套、USB 数据线一根注意是数据线不是纯充电线、可能需要一个 USB 电源适配器。如果你买的是带 Wi-Fi 的版本天线已经集成在板子上了不需要额外接。接线方面PyPortal 只有一个 USB-C 口用于供电和编程没有其他必须连接的外设。屏幕、触摸、Wi-Fi 模块都是板载的。这一点对新手特别友好不存在接错线烧板子的风险。唯一需要注意的是 USB 线要能传数据我遇到过有人用只能充电的线插上后电脑识别不到设备折腾半天以为是驱动问题换根线就好了。如果你打算把 PyPortal 装进外壳长期使用建议在装壳之前先裸板测试一遍所有功能。因为装壳后如果想按复位键或者拔插排线会比较麻烦。测试内容包括屏幕能否正常显示、触摸是否灵敏、Wi-Fi 能否连上、USB 串口是否能看到输出。这些都没问题再装壳能省去很多返工的麻烦。2.2 CircuitPython 环境搭建PyPortal 出厂时通常已经烧录了 CircuitPython 固件但版本可能比较旧。我建议先更新到最新稳定版因为新版本对 Wi-Fi 库和内存管理都有优化。更新方法很简单按住板子上的复位键不放再按一下复位键或者直接双击复位键板子会进入 bootloader 模式电脑上会出现一个名为PORTALBOOT的 U 盘。把下载好的.uf2固件文件拖进去板子会自动重启然后出现一个名为CIRCUITPY的 U 盘。这个CIRCUITPY盘就是你的代码存放位置。CircuitPython 的规则是板子启动时会自动执行根目录下的code.py。所以你只需要把写好的代码保存为code.py放到这个盘里板子就会运行。不需要编译、不需要上传工具保存即运行这个体验非常爽。库文件放在CIRCUITPY盘下的lib文件夹里。CircuitPython 的库是.mpy格式的预编译文件体积小、加载快。你需要从 Adafruit 的 CircuitPython 库包通常叫adafruit-circuitpython-bundle-x.x.x-mpy里把用到的库复制进去。Flyover Viewer 至少需要这几个adafruit_requests、adafruit_display_text、adafruit_bitmap_font、adafruit_touchscreen如果用到触摸。如果库包太大可以只挑需要的复制不用整个 bundle 都放进去因为 PyPortal 的存储空间有限。注意CircuitPython 的版本和库的版本要匹配。比如 CircuitPython 8.x 的库不能直接用在 9.x 上反之亦然。下载库包时看清楚对应的 CircuitPython 版本号这个坑我踩过表现是导入库时报错提示语法或接口不兼容。2.3 网络配置与密钥管理联网是 Flyover Viewer 的核心能力。CircuitPython 里配置 Wi-Fi 有两种方式一种是在settings.toml文件里写 SSID 和密码另一种是在代码里硬编码。推荐用settings.toml因为这样代码可以分享给别人而不泄露自己的网络信息。settings.toml放在CIRCUITPY盘根目录内容格式如下CIRCUITPY_WIFI_SSID 你的WiFi名称 CIRCUITPY_WIFI_PASSWORD 你的WiFi密码 CIRCUITPY_WEB_API_PASSWORD 网页管理密码CircuitPython 启动时会自动读取这个文件os.getenv(CIRCUITPY_WIFI_SSID)就能拿到值。这样你的code.py里就不需要出现明文密码分享代码时也安全。如果 Wi-Fi 连不上先检查这几点SSID 是否区分大小写、密码是否正确、路由器是否开启了 MAC 地址过滤、是否连的是 2.4GHz 频段ESP32 不支持 5GHz。我遇到过路由器双频合一的情况板子有时候连到 5GHz 就连不上后来在路由器设置里把 2.4GHz 和 5GHz 分开命名才解决。2.4 数据请求与 JSON 解析CircuitPython 里发 HTTP 请求用adafruit_requests库基本用法和 Python 的requests很像但功能精简了很多。一个典型的请求流程是这样的import board import wifi import socketpool import ssl import adafruit_requests pool socketpool.SocketPool(wifi.radio) requests adafruit_requests.Session(pool, ssl.create_default_context()) url https://api.example.com/satellite/passes?latxxlonxx response requests.get(url) data response.json() response.close()这里有几个细节要注意。第一ssl.create_default_context()是必须的因为现在大部分接口都是 HTTPS没有 SSL 上下文会报错。第二response.close()一定要调用否则连接不会释放多次请求后可能耗尽 socket 资源。第三response.json()会把整个响应体解析成 Python 字典如果响应很大内存可能不够。这时候可以用response.text先拿到原始字符串再用json.loads()手动解析或者只取需要的字段。JSON 解析在 CircuitPython 里用的是标准json库和桌面 Python 一样。但要注意单片机内存有限解析后的字典不要保留太久用完就让它被垃圾回收。如果数据结构嵌套很深访问时要用.get()方法而不是直接索引避免键不存在时抛异常导致程序崩溃。3. 实操过程与核心环节实现3.1 屏幕布局设计与文字渲染PyPortal 的屏幕是 320x240 像素这个尺寸不大布局要精打细算。我的做法是把屏幕分成几个区域顶部放标题和当前时间中间放卫星轨迹图或过境列表底部放状态栏显示 Wi-Fi 信号和最后更新时间。文字渲染用adafruit_display_text的label对象。CircuitPython 支持两种字体点阵字体bitmap font和矢量字体vector font。点阵字体渲染快、内存占用小但放大后会模糊矢量字体清晰但加载慢。对于 PyPortal 这种小屏幕我推荐用点阵字体比如Helvetica-Bold-16.bdf大小合适、可读性好。创建文字标签的代码大概是这样import displayio from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font font bitmap_font.load_font(/fonts/Helvetica-Bold-16.bdf) text_area label.Label(font, text初始化中..., color0xFFFFFF) text_area.x 10 text_area.y 30 main_group.append(text_area)更新文字时直接改text_area.text 新内容就行不需要重新创建对象。但要注意频繁更新文字会导致屏幕闪烁因为每次更新都会重绘整个标签区域。如果数据刷新频率高可以考虑用displayio.Group做局部刷新或者降低刷新频率。3.2 卫星轨迹的可视化处理Flyover Viewer 最有意思的部分是把卫星轨迹画在屏幕上。USGS 返回的数据里通常包含一系列经纬度点表示卫星在地面上的投影轨迹。要把这些点画到 320x240 的屏幕上需要做坐标转换。最简单的做法是等距圆柱投影把经度映射到 x 轴纬度映射到 y 轴。假设屏幕显示范围是经度 -180 到 180、纬度 -90 到 90那么转换公式是x (lon 180) / 360 * 320 y (90 - lat) / 180 * 240但这样画出来的轨迹在极地附近会变形严重。如果只关心本地天空的过境可以只显示以你所在位置为中心的一个矩形区域比如经度 ±30 度、纬度 ±20 度。这样比例更合理轨迹也更清晰。画线用displayio.Shape或者直接操作bitmap的像素。displayio.Shape适合画简单线条但点数多了会慢。更高效的方式是创建一个bitmap用bitmap.fill()清空然后逐像素画点或线。如果轨迹点很密集可以每隔几个点取一个减少绘制量。我自己的实现里轨迹用亮青色画当前位置用红色圆点标记过境时间窗口用半透明矩形覆盖。这样一眼就能看出卫星什么时候经过头顶、从哪个方向来、往哪个方向去。3.3 定时刷新与内存管理CircuitPython 在单片机上跑内存是稀缺资源。PyPortal 的 ESP32 有大约 256KB 的 RAM 可供 CircuitPython 使用听起来不少但加载几个库、创建几个显示对象之后就所剩无几了。内存管理做不好程序跑几分钟就会因为MemoryError崩溃。我的经验是第一不要在循环里反复创建和销毁对象。比如文字标签创建一次就够了后面只改.text属性。第二用完的 JSON 数据及时释放可以显式调用gc.collect()触发垃圾回收。第三图片资源尽量用小的、压缩过的不要加载大图。第四如果某个库只用一次用完可以del掉再gc.collect()。定时刷新用supervisor.ticks_ms()做非阻塞判断import supervisor import time last_update supervisor.ticks_ms() update_interval 15 * 60 * 1000 # 15分钟 while True: now supervisor.ticks_ms() if now - last_update update_interval: update_data() last_update now time.sleep(0.1)这样主循环不会卡死屏幕保持响应也不会因为time.sleep(900)导致板子看起来像死机。3.4 完整代码结构与关键片段把上面这些串起来code.py的结构大概是# 1. 导入库 import board, displayio, wifi, socketpool, ssl, os, time, supervisor, gc import adafruit_requests from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font # 2. 初始化显示 display board.DISPLAY main_group displayio.Group() display.root_group main_group # 3. 加载字体和创建标签 font bitmap_font.load_font(/fonts/Helvetica-Bold-16.bdf) title_label label.Label(font, textFlyover Viewer, color0x00FFFF) title_label.x 10 title_label.y 20 main_group.append(title_label) # 4. 连接 Wi-Fi wifi.radio.connect(os.getenv(CIRCUITPY_WIFI_SSID), os.getenv(CIRCUITPY_WIFI_PASSWORD)) pool socketpool.SocketPool(wifi.radio) requests adafruit_requests.Session(pool, ssl.create_default_context()) # 5. 主循环 while True: try: response requests.get(API_URL) data response.json() response.close() # 解析数据、更新标签、绘制轨迹 gc.collect() except Exception as e: print(错误:, e) time.sleep(60)这个骨架跑起来之后再逐步往里填业务逻辑。先保证 Wi-Fi 能连、能拿到数据、能显示一行字然后再做轨迹绘制、定时刷新这些复杂功能。一次做一点每步都验证比一口气写完再调试效率高得多。4. 常见问题与排查技巧实录4.1 连不上 Wi-Fi 的几种情况Wi-Fi 连不上是新手最常遇到的问题。表现是串口输出里一直打印Connecting...然后超时。排查顺序我一般是这样先确认settings.toml里的 SSID 和密码有没有写错。特别注意 SSID 里的空格和特殊字符有时候从路由器后台复制过来会带不可见字符。可以先用一个简单的手机热点测试排除路由器设置的问题。如果热点能连、路由器连不上检查路由器是否开启了 AP 隔离或 MAC 过滤。有些企业级路由器默认开启这些安全功能会阻止新设备接入。另外确认路由器是 2.4GHz 频段ESP32 不支持 5GHz双频合一的路由器有时候会协商到 5GHz 导致连接失败。还有一种情况是 Wi-Fi 信号太弱。PyPortal 的板载天线增益有限隔两堵墙可能就只剩一格信号。可以试着调整板子位置或者用带外置天线的版本。我自己的板子放在金属架子旁边时信号明显变差挪开就好了。4.2 内存不足与程序崩溃MemoryError是 CircuitPython 项目最常见的崩溃原因。表现是程序运行一段时间后突然重启串口输出里能看到MemoryError字样。解决思路是减少同时存在的对象数量。一个实用的技巧是在代码里定期打印gc.mem_free()观察内存变化趋势。如果发现内存持续下降说明有对象没被释放。常见的原因是循环里创建了局部变量但被全局引用持有或者显示组里不断添加新对象而没有移除旧的。另一个技巧是精简库。adafruit_requests库本身不大但如果你还导入了adafruit_display_shapes、adafruit_progressbar这些内存占用会明显增加。只导入真正用到的库能省不少空间。如果实在不够用可以考虑用microPython而不是 CircuitPython前者内存占用更小但库生态没那么丰富需要自己写更多底层代码。对 Flyover Viewer 这种项目CircuitPython 通常够用优化一下代码结构就能跑稳。4.3 数据接口返回异常的处理接口调用失败的原因很多网络超时、接口地址变了、返回格式变了、请求频率太高被限流。代码里一定要做异常处理不能让一个请求失败导致整个程序崩溃。我的做法是用try/except包住请求和解析逻辑出错时在屏幕上显示错误信息然后等下一个刷新周期再试。错误信息要具体比如“连接超时”和“JSON解析失败”要区分开方便排查。如果接口经常超时可以加一个重试机制失败后等几秒再试一次连续失败三次才放弃。但重试次数不要太多否则会卡住主循环。更好的方式是把重试逻辑放在后台主循环继续跑屏幕显示上次成功获取的数据。还有一种情况是接口返回的数据结构和预期不一致。比如某个字段从字符串变成了数字或者数组变成了对象。解析时用.get()方法并给默认值能避免大部分KeyError和TypeError。对于关键字段解析后要检查类型和范围不合法就当作无效数据处理。4.4 屏幕显示异常与刷新问题屏幕显示异常通常有三种表现花屏、闪烁、局部残影。花屏一般是初始化顺序不对或者显示组在创建过程中被修改。确保所有显示对象在加入root_group之前就配置好不要在显示过程中动态添加大量对象。闪烁是因为每次更新都重绘整个屏幕。解决办法是只更新变化的部分比如只改文字标签的.text而不是清空整个组重新创建。如果必须重绘可以用双缓冲先在内存里画好再一次性切换。残影是 TFT 屏幕的常见现象尤其是显示静态内容久了之后。PyPortal 的屏幕质量不错但长时间显示同一画面还是会有轻微残影。可以定期做一次全屏刷新或者让画面有轻微变化比如时间秒数跳动能减轻这个问题。4.5 常见问题速查表问题现象可能原因排查方法解决方式串口无输出USB线只充电不传数据换一根数据线使用带数据传输的USB线连不上WiFiSSID/密码错误或频段不对用手机热点测试检查settings.toml确认2.4GHzMemoryError对象未释放或库太多打印gc.mem_free()精简库循环内不创建对象接口超时网络不稳或接口限流手动curl测试接口加重试降低请求频率屏幕花屏初始化顺序错误检查显示组创建流程先配置好再添加到root_group程序自动重启未捕获异常或看门狗查看串口错误信息加try/except喂看门狗轨迹画不出来坐标转换错误打印转换后的坐标检查经纬度范围和映射公式提示遇到问题时串口输出是最好的朋友。用print()把关键变量打出来比盯着屏幕猜要快得多。CircuitPython 的串口在电脑上表现为一个 COM 口或 /dev/ttyACM 设备用任意串口终端工具都能看。5. 项目扩展与个人实操体会5.1 可以继续加的功能基础版本跑通之后有几个方向可以继续折腾。一是加触摸交互点屏幕切换显示模式比如从轨迹图切到过境列表再切到卫星信息详情。PyPortal 的触摸屏是电容式的用adafruit_touchscreen库能拿到坐标配合按钮区域判断就能实现。二是加声音提示。PyPortal 板载一个小蜂鸣器可以在卫星即将过境时发出提示音。用board.DISPLAY和pwmio控制频率和时长代码不复杂。但要注意别在深夜响不然会被家人投诉。三是把数据记录到 SD 卡。PyPortal 有 microSD 卡槽可以定期把过境记录写进去积累一段时间后导出分析。CircuitPython 里用sdcard和storage库挂载然后像普通文件一样读写。四是做成多页面轮播。除了卫星过境还可以显示天气、日历、待办事项。用定时器每隔几十秒切换一个页面信息密度更高。但要注意内存页面多了容易崩需要做好资源释放。5.2 我踩过的几个坑第一个坑是字体文件太大。我一开始用了一个完整的 Unicode 字体加载后内存直接爆了。后来换成只包含 ASCII 字符的精简字体问题解决。CircuitPython 的字体文件可以自己裁剪只保留用到的字符集能省很多空间。第二个坑是时区处理。USGS 返回的时间通常是 UTC直接显示会差好几个小时。CircuitPython 的time库功能有限没有完整的时区数据库。我的做法是在代码里手动加偏移量比如local_time utc_time 8 * 3600。如果要处理夏令时就得自己维护一个规则表比较麻烦。第三个坑是屏幕亮度。PyPortal 默认亮度是 100%晚上看很刺眼。可以用board.DISPLAY.brightness 0.3调暗但要注意这个属性在某些固件版本上不支持。如果不支持就只能通过 PWM 控制背光引脚需要查板子的引脚定义。第四个坑是 USB 供电不足。我用了一个便宜的 USB 分线器结果板子频繁重启。后来直接插电脑 USB 口或者用质量好的电源适配器就好了。PyPortal 峰值电流能到 500mA 左右供电不足会导致各种奇怪问题。5.3 给新手的入门建议如果你刚拿到 PyPortal别急着写完整项目。先跑通官方示例确认硬件和开发环境没问题。Adafruit 的 Learn 系统里有大量教程从点亮 LED 到联网显示天气循序渐进。跟着做一遍对 CircuitPython 的工作方式就有感觉了。然后从修改示例开始。比如把天气示例里的城市改成你的城市把刷新频率改一改把显示的文字换一换。改代码的过程中会遇到各种报错解决这些报错就是最好的学习。不要怕出错CircuitPython 的好处是改错了顶多不运行不会烧硬件。最后再从头写自己的项目。这时候你已经知道库怎么导入、显示怎么初始化、网络怎么连接写起来会顺畅很多。Flyover Viewer 这种项目核心代码其实就两三百行难点不在代码量而在对数据格式的理解和显示效果的调试。5.4 关于数据可视化的一点思考把数据画到小屏幕上本质是信息压缩。320x240 的像素里要表达经纬度、时间、轨迹、状态必须做取舍。我的原则是最重要的信息用最大的字号、最亮的颜色次要信息用小字、暗色装饰性元素能省就省。颜色选择上深色背景配亮色文字在暗光环境下更舒服也更省电TFT 屏幕黑色像素耗电低。但白天光线强的时候深色背景反而不如浅色背景清晰。如果板子放在窗边可以考虑根据时间自动切换主题白天用浅色、晚上用深色。轨迹绘制的精度和性能要平衡。点太密画得慢点太疏轨迹不连贯。我的经验是每 5 到 10 个原始点取一个既能看出轨迹形状又不会拖慢刷新。如果轨迹跨越国际日期变更线还要处理经度从 180 跳到 -180 的情况否则会画出一条横穿屏幕的直线。这个项目我断断续续做了两周大部分时间花在调试显示效果和内存优化上。硬件和网络部分反而很快因为 CircuitPython 的库封装得很好。如果你也想做一个类似的联网信息终端PyPortal 是个很合适的起点成本不高、上手快、成品感强。做完之后挂在墙上每次看到它自动刷新数据那种“自己做的”满足感是买成品比不了的。
阅读完成 · 觉得有帮助?