☰
PyPortal卫星过境追踪器:CircuitPython实时显示ISS轨迹
2026/11/4 1:07:49 网站建设 项目流程

1. 从一块 3.2 寸小屏幕说起:PyPortal Flyover Viewer 到底在做什么

第一次看到 "PyPortal Flyover Viewer" 这个标题,很多人会以为是某种飞行模拟或者航班追踪工具。其实它的核心逻辑非常朴素:用一块带触摸屏的嵌入式开发板,实时显示卫星从你头顶飞过的轨迹信息。这里的 "Flyover" 指的是卫星过境,也就是卫星经过你所在位置上空的那段时间窗口。

这个项目解决的是一个很具体的需求:卫星过境的时间窗口通常只有几分钟到十几分钟,而且每天可能只有几次。如果你想知道国际空间站什么时候从你头顶飞过、该往哪个方向看,传统做法是打开手机 App 或者网站查询。但 PyPortal 这个方案把这件事变成了一个永远在线、抬头就能看的桌面设备——它自己联网、自己拉数据、自己算轨迹、自己画出来。

适合谁来参考这个项目?三类人最合适。第一类是手里已经有 Adafruit PyPortal 或者类似 CircuitPython 开发板,想找个真正有用的项目练手的嵌入式爱好者。第二类是对卫星追踪、航天数据感兴趣,但不想折腾复杂天文软件的普通用户。第三类是想学习 CircuitPython 网络请求、JSON 解析、屏幕绘图这一整套流程的开发者——这个项目把这几块知识串得非常完整。

我实际做下来最大的感受是:这个项目的难点不在代码本身,而在于数据源的对接和坐标转换。CircuitPython 的语法很简单,屏幕绘图 API 也很直观,但当你需要从外部 API 拿到卫星的经纬度、再转换成屏幕上能画出来的坐标时,中间涉及的数学和网络处理才是真正花时间的地方。下面我会把整个项目的来龙去脉、技术选型理由、实操步骤和我踩过的坑全部拆开讲。

2. 为什么选 PyPortal 而不是树莓派或 ESP32

2.1 PyPortal 的硬件特性决定了它的定位

PyPortal 是 Adafruit 推出的一块一体化开发板,核心是一颗 ATSAMD51 微控制器,自带 3.2 寸 320x240 的 TFT 触摸屏、ESP32 协处理器负责 WiFi 联网、还有光线传感器、温度传感器、扬声器和一个 microSD 卡槽。它的设计初衷就是做 "物联网信息展示终端"——你不需要额外接屏幕、不需要额外接 WiFi 模块,插上 USB 就能跑。

对比一下其他方案就很清楚了。树莓派性能强得多,但功耗高、启动慢、需要操作系统,做一个"永远显示卫星信息"的设备有点杀鸡用牛刀。ESP32 开发板便宜,但你要自己配屏幕、自己处理驱动、自己设计外壳,工作量翻倍。PyPortal 的优势在于开箱即用的完整度:一块板子解决了显示、联网、交互三个核心问题。

2.2 CircuitPython 降低了嵌入式开发的门槛

CircuitPython 是 Adafruit 从 MicroPython 分支出来的一个版本,最大的特点是把开发板当成 U 盘用。你插上 USB,电脑上出现一个磁盘,把代码文件拖进去,板子自动重启运行。不需要编译、不需要烧录工具、不需要配置工具链。

这对这个项目意味着什么?意味着你可以在几分钟内完成一次代码修改和测试的循环。卫星追踪涉及大量参数调整——刷新频率、显示范围、坐标映射方式——如果每次改代码都要编译烧录,效率会低到让人放弃。CircuitPython 的热修改特性让迭代变得非常顺畅。

2.3 屏幕尺寸和分辨率的取舍

320x240 的分辨率在今天看来很低,但对于显示卫星轨迹来说其实够用。关键在于你怎么利用这个空间。我的做法是把屏幕分成三个区域:上方显示当前时间和下次过境倒计时,中间是极坐标式的天空视图(以你所在位置为中心,画出卫星的方位角和仰角),下方显示卫星名称和状态信息。

注意:PyPortal 的屏幕刷新率有限,如果你每帧都全屏重绘,会出现明显的闪烁。正确做法是使用displayio的 Group 机制,只更新变化的部分。

3. 卫星数据从哪来:USGS 与轨道数据源的对接逻辑

3.1 标题里为什么会出现 USGS

关键词里出现了 USGS(美国地质调查局),这看起来和卫星追踪没什么关系。但仔细想一下:卫星过境的可视化,本质上需要两类数据——卫星的轨道位置和观察者的地理位置。USGS 提供的是地形和地理信息数据,在某些实现中,Flyover Viewer 会结合 USGS 的地图数据来显示卫星经过的区域地形。

不过更常见的做法是:卫星轨道数据来自 TLE(Two-Line Element)数据集,这是描述卫星轨道的标准格式,包含轨道倾角、偏心率、升交点赤经等参数。有了 TLE,再配合 SGP4 轨道传播算法,就能算出卫星在任意时刻的位置。

3.2 TLE 数据的获取与更新

TLE 数据会随时间衰减精度,通常需要每天更新一次。数据源方面,很多公开的卫星追踪服务都提供 TLE 的 API 接口。在 CircuitPython 里获取 TLE 的流程是这样的:

import wifi import socketpool import ssl import adafruit_requests pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) TLE_URL = "https://example-tle-source.com/api/tle?norad=25544" response = requests.get(TLE_URL) tle_data = response.text

拿到 TLE 之后,你需要解析出两行核心数据。第一行包含卫星编号和轨道参数,第二行包含更精确的轨道角度信息。解析逻辑不复杂,但要注意TLE 的格式是固定列宽的,不能用简单的 split 来分割,必须按字符位置截取。

3.3 坐标转换:从经纬度到屏幕像素

这是整个项目最核心的数学部分。卫星的 TLE 给出的是地心惯性坐标系下的位置,你需要把它转换成相对于你所在位置的方位角和仰角。具体步骤:

  1. 计算卫星在地心坐标系中的位置矢量
  2. 将观察者的经纬度转换为地心坐标系中的位置
  3. 计算卫星相对于观察者的方向矢量
  4. 将方向矢量转换为方位角(0-360 度)和仰角(0-90 度)

方位角决定卫星在水平方向的位置,仰角决定卫星在垂直方向的高度。在屏幕上,你可以用极坐标投影:中心点代表天顶(仰角 90 度),边缘代表地平线(仰角 0 度),角度对应方位角。

import math def az_el_to_screen(az, el, center_x, center_y, radius): r = radius * (1 - el / 90.0) theta = math.radians(az) x = center_x + r * math.sin(theta) y = center_y - r * math.cos(theta) return int(x), int(y)

这段代码的逻辑是:仰角越高,距离中心越近;方位角决定方向。北对应屏幕上方,东对应右方,以此类推。

提示:SGP4 算法在 CircuitPython 上运行需要一定的计算量。如果你发现刷新太慢,可以降低计算频率,比如每 30 秒更新一次卫星位置,而不是每帧都算。

4. 屏幕布局与交互设计的实操细节

4.1 天空视图的绘制方式

天空视图是整个界面的核心。我试过两种方案:一种是画同心圆表示仰角,画放射线表示方位角;另一种是只画一个圆,用点来表示卫星位置。第一种方案信息量大但视觉杂乱,第二种方案简洁但缺少参考。

最终我采用的是折中方案:画三个同心圆(分别代表仰角 30 度、60 度、90 度),加上四条放射线(北、东、南、西),卫星用一个小圆点表示,后面拖一条短尾巴显示运动方向。这样既不会太乱,又能让用户快速判断卫星的大致方位。

import displayio import vectorio # 创建天空视图的 Group sky_group = displayio.Group() # 画外圈 outer_circle = vectorio.Circle( pixel_shader=palette, radius=100, x=160, y=120, color_index=1 ) sky_group.append(outer_circle) # 画内圈 inner_circle = vectorio.Circle( pixel_shader=palette, radius=50, x=160, y=120, color_index=2 ) sky_group.append(inner_circle)

4.2 触摸交互的设计取舍

PyPortal 带电阻触摸屏,理论上你可以做很复杂的交互。但我的经验是:这个设备的定位是"信息展示",不是"交互终端"。用户大部分时间只是瞥一眼,不会去点它。所以交互设计应该尽量简单。

我最终只保留了两个触摸功能:点击屏幕切换显示的卫星(如果你追踪多颗卫星),长按屏幕强制刷新数据。其他所有操作都通过配置文件完成,不需要在屏幕上点来点去。

4.3 字体和颜色的选择

在 320x240 的屏幕上,字体选择非常关键。我试过好几种字体,最终选定了 Adafruit 提供的Helvetica-Bold-16.bdf作为主字体,Helvetica-Bold-12.bdf作为辅助信息字体。颜色方面,深色背景配亮色文字在户外环境下可读性最好,但如果你把设备放在室内,浅色背景可能更舒服。

注意:CircuitPython 的字体文件需要放在板子的fonts目录下,而且 BDF 格式的字体文件大小直接影响加载速度。不要放太多字体文件,两三个够用就行。

5. 网络请求与数据缓存的稳定性处理

5.1 WiFi 连接的容错机制

PyPortal 的 WiFi 连接不是 100% 可靠的。我遇到过好几次板子运行几天后突然断网的情况。如果你的代码没有处理这种情况,屏幕就会一直显示旧数据,用户根本不知道数据已经过期了。

我的做法是:每次成功获取数据后,记录一个时间戳。如果超过 30 分钟没有更新,就在屏幕上显示一个警告图标。同时,在每次网络请求外面包一层 try-except,失败时自动重试三次,每次间隔 5 秒。

import time def fetch_with_retry(url, max_retries=3): for attempt in range(max_retries): try: response = requests.get(url, timeout=10) return response.text except Exception as e: print(f"Attempt {attempt + 1} failed: {e}") time.sleep(5) return None

5.2 数据缓存的必要性

卫星的 TLE 数据不需要每次刷新都重新获取。TLE 的精度在一天内变化很小,所以你可以把 TLE 缓存到本地文件,每天只更新一次。这样既减少了网络请求,也降低了因为网络问题导致数据不可用的风险。

import os TLE_CACHE = "/cache/tle.txt" def get_tle(norad_id): if os.path.exists(TLE_CACHE): with open(TLE_CACHE, "r") as f: cached = f.read() # 检查缓存是否过期(超过 24 小时) if time.time() - os.stat(TLE_CACHE)[8] < 86400: return cached # 缓存过期或不存在,重新获取 fresh = fetch_with_retry(TLE_URL) if fresh: with open(TLE_CACHE, "w") as f: f.write(fresh) return fresh return None

5.3 内存管理的坑

CircuitPython 在微控制器上运行,内存非常有限。PyPortal 的 ATSAMD51 有 192KB 的 RAM,听起来不少,但 CircuitPython 本身要占一部分,displayio 要占一部分,剩下的给用户代码就不多了。

我踩过最大的坑是:在循环中不断创建新的对象而不释放。比如每次刷新都创建一个新的 Label 对象,跑几个小时之后内存就耗尽了。正确做法是复用已有的对象,只更新它们的属性。

# 错误做法:每次循环都创建新对象 while True: label = displayio.Label(font, text="New text") # ... # 正确做法:创建一次,之后只更新属性 label = displayio.Label(font, text="") while True: label.text = "Updated text" # ...

6. 从零搭建的完整步骤与配置清单

6.1 环境准备

你需要准备的东西:

  • Adafruit PyPortal 一块
  • USB-C 数据线一根(注意是数据线,不是纯充电线)
  • 电脑一台(Windows、macOS、Linux 都可以)
  • 稳定的 WiFi 网络

软件方面,你需要安装 CircuitPython 固件和必要的库文件。具体步骤:

  1. 从 CircuitPython 官网下载对应 PyPortal 的最新固件(.uf2 文件)
  2. 双击板子上的复位按钮,进入 bootloader 模式
  3. 把 .uf2 文件拖入出现的磁盘中,板子会自动重启
  4. 重启后会出现一个名为CIRCUITPY的磁盘

6.2 库文件的安装

CircuitPython 的库文件需要手动放到CIRCUITPY/lib目录下。这个项目需要的库包括:

库名称用途
adafruit_requestsHTTP 请求
adafruit_display_text文字显示
adafruit_bitmap_font字体加载
adafruit_sgp4轨道计算(如果有的话)
neopixel板载 LED 控制

你可以从 Adafruit 的 CircuitPython Bundle 中下载这些库,然后把对应的 .mpy 文件复制到 lib 目录。

6.3 配置文件的设计

我把所有可配置的参数都放在一个config.py文件里,这样修改配置不需要动主程序:

# config.py WIFI_SSID = "your_wifi_name" WIFI_PASSWORD = "your_wifi_password" LATITUDE = 39.9042 LONGITUDE = 116.4074 SATELLITES = [ {"name": "ISS", "norad": 25544}, {"name": "NOAA-18", "norad": 28654}, ] REFRESH_INTERVAL = 30 TLE_UPDATE_INTERVAL = 86400

这种设计的好处是:如果你要把设备送给朋友,只需要改 config.py 里的 WiFi 和坐标信息,主程序完全不用动。

6.4 主程序的启动流程

主程序的逻辑顺序很重要。我的做法是:

  1. 先初始化屏幕和显示组
  2. 连接 WiFi
  3. 加载缓存的 TLE 数据
  4. 进入主循环:计算卫星位置、更新屏幕、处理触摸事件
import board import displayio import wifi import config # 初始化显示 display = board.DISPLAY main_group = displayio.Group() display.show(main_group) # 连接 WiFi wifi.radio.connect(config.WIFI_SSID, config.WIFI_PASSWORD) # 主循环 while True: for sat in config.SATELLITES: az, el = calculate_position(sat["norad"]) update_screen(sat["name"], az, el) time.sleep(config.REFRESH_INTERVAL)

7. 实际运行中遇到的典型问题与排查思路

7.1 屏幕闪烁和撕裂

这是最常见的问题。原因是 displayio 在更新屏幕时,如果一次性更新太多内容,会出现明显的闪烁。解决方案是使用displayio.Group的层级结构,把静态内容(背景、圆圈、标签)和动态内容(卫星位置)分开放在不同的 Group 里,只更新动态 Group。

7.2 网络请求超时

PyPortal 的 WiFi 模块在某些网络环境下会出现连接不稳定的情况。我试过几种解决方案:增加超时时间、减少请求频率、使用 HTTP 而不是 HTTPS(如果数据源支持的话)。最终发现最有效的方法是在请求之间加入足够的延迟,不要连续快速发起请求。

7.3 坐标计算偏差

如果你发现卫星位置和实际观测不符,大概率是坐标转换出了问题。检查这几个点:经纬度的正负号是否正确(北纬为正,南纬为负;东经为正,西经为负)、时间是否使用了 UTC、TLE 数据是否过期。

提示:TLE 数据超过 7 天后精度会明显下降,超过 30 天后基本不可用。确保你的设备每天至少更新一次 TLE。

7.4 内存不足导致崩溃

如果你看到MemoryError或者板子突然重启,说明内存不够了。排查方法:减少同时显示的卫星数量、降低屏幕刷新频率、检查是否有对象泄漏。

8. 这个项目还能怎么扩展

8.1 加入过境预警功能

目前的实现是实时显示卫星位置,但更有用的是提前预警。你可以在代码里加入一个计算逻辑:如果卫星将在未来 10 分钟内过境,就触发板载 LED 闪烁或者扬声器发出提示音。

8.2 显示更多卫星信息

除了位置,你还可以显示卫星的高度、速度、下次过境时间等信息。这些数据都可以从 TLE 计算得出,只是需要更多的屏幕空间和计算资源。

8.3 多设备组网

如果你有多个 PyPortal,可以把它们放在不同位置,各自显示本地可见的卫星。通过一个中心服务器协调数据更新,这样每个设备只需要拉取自己需要的数据。

8.4 结合 USGS 地形数据

回到关键词里的 USGS,如果你想让显示更丰富,可以拉取 USGS 的地形数据,在屏幕上显示卫星经过区域的地形轮廓。不过这需要额外的数据处理和存储空间,对 PyPortal 来说可能有点吃力。

9. 一些个人经验和小技巧

做了几个版本的迭代之后,我总结了几条比较实用的经验。第一条是不要追求一次做完所有功能。我第一版就想把卫星轨迹、地形、预警全部做进去,结果代码复杂到自己都看不懂,调试了整整一个周末。后来拆成三个版本逐步迭代,反而更快完成了。

第二条是善用 print 调试。CircuitPython 的 print 输出会直接显示在串口终端里,你可以用 Mu Editor 或者 screen 命令查看。在关键位置加 print,比盯着屏幕猜问题高效得多。

第三条是备份你的 config.py。我至少有三四次因为改配置改错了导致设备连不上网,最后只能重新刷固件。后来我把 config.py 单独备份了一份,出问题直接覆盖回去,省了很多时间。

第四条是关于电源的。PyPortal 用 USB 供电时,如果电脑的 USB 口供电不足,屏幕亮度会不稳定。建议用一个独立的 5V 2A 电源适配器,特别是如果你打算让它 24 小时运行。

最后一条是定期检查 TLE 数据源。我用过的一个数据源在某个时间点突然改了 API 格式,导致设备连续几天显示错误数据。后来我加了一个简单的数据校验逻辑:如果解析出来的 TLE 行数不对或者格式异常,就跳过这次更新,继续用缓存数据。这个小小的改动避免了很多次"设备看起来在运行但数据是错的"的情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询