CircuitPython网络编程实战:从Wi-Fi连接到IPv6与JSON解析
2026/5/15 10:24:56 网站建设 项目流程

1. 项目概述与核心价值

如果你手头有一块ESP32或者类似的Wi-Fi开发板,并且已经用CircuitPython点亮了LED,那么恭喜你,你已经迈出了嵌入式开发的第一步。但很快你就会发现,让一个设备真正“智能”起来,离不开网络。无论是从天气API获取数据、向云端上报传感器读数,还是远程控制一个开关,网络编程都是绕不开的核心技能。这不仅仅是发送一个HTTP请求那么简单,它涉及到如何稳定地连接网络、如何处理不同格式的数据、如何应对现代网络环境(比如IPv6),以及如何高效地编写和调试代码。

我见过很多初学者,在复制了一段网络请求的示例代码后,发现要么连不上Wi-Fi,要么收不到数据,要么代码一跑起来板子就重启,问题层出不穷。这往往是因为对背后的原理和细节了解不够。网络编程是一个系统工程,从最基本的Wi-Fi配置、密钥管理,到HTTP请求的发送与响应处理,再到更高级的IPv6配置和代码编辑技巧,每一个环节都有其门道。

本文将带你从零开始,深入CircuitPython的网络编程世界。我们不仅会复现如何用adafruit_requests库获取网络数据,更会拆解每一步背后的逻辑:为什么要把Wi-Fi密码放在settings.toml里而不是代码里?adafruit_requests和标准Python的requests库在使用上有何异同?当你的设备获取到一个IPv6地址时,它到底意味着什么?更重要的是,我会分享大量在实战中踩过的坑和总结出的调试技巧,比如如何利用串行控制台(Serial Console)进行“打印调试”,如何在代码出错时快速定位问题,以及如何安全、高效地管理你的项目代码。

无论你是想做一个联网的天气站,还是一个能接收远程指令的智能开关,甚至是探索物联网的前沿协议,这篇文章都将为你提供一份详实的、可直接上手操作的路线图。我们不止于“怎么做”,更要弄清楚“为什么这么做”,以及“怎么做更好”。

2. 环境准备与基础配置

在开始写任何网络相关的代码之前,我们必须确保开发环境是正确且稳固的。这就像盖房子前要打好地基,很多后续的诡异问题,根源都出在环境配置这一步。

2.1 硬件与软件清单

首先,确认你的“武器库”是否齐全:

  • 开发板:一块支持Wi-Fi和CircuitPython的板子是必须的。本文以ESP32-S2/S3系列为例,因为它们对CircuitPython的支持非常完善且性能足够。其他如RP2040+Wi-Fi模组的板子(如Pico W)也完全适用,核心概念相通。
  • USB数据线:一根可靠的数据线至关重要。劣质线缆可能导致供电不稳或数据传输中断,引发各种难以排查的问题。我个人的经验是,优先使用板子原厂附带的线,或者选择品牌明确、线径较粗的短线。
  • 计算机:Windows、macOS或Linux均可。后续的步骤会因操作系统略有不同,我会分别说明。
  • CircuitPython固件:确保你的板子已经刷写了最新版本的CircuitPython固件。访问 CircuitPython官网 ,根据你的板子型号下载对应的.uf2文件。通常的刷写方法是:让板子进入引导加载模式(Bootloader),这时电脑上会出现一个名为RPI-RP2BOOT的U盘,把下载的.uf2文件拖进去即可。
  • 代码编辑器:强烈推荐Mu Editor。它是专为CircuitPython和MicroPython设计的,内置了串行控制台和代码自动完成功能,对新手极其友好。避免使用Windows记事本等纯文本编辑器,它们可能会在文件末尾添加隐藏字符,导致代码无法运行。

2.2 核心配置文件:settings.toml的奥秘

这是CircuitPython网络配置的灵魂所在,也是第一个容易出错的地方。很多教程让你把Wi-Fi密码直接写在代码里,这是极其不推荐的做法,原因有三:

  1. 安全风险:如果你的代码要分享给别人或上传到代码仓库(如GitHub),密码就直接泄露了。
  2. 维护困难:当你要更换网络或密码时,需要修改代码并重新上传。
  3. 违反惯例:CircuitPython社区和绝大多数库都默认从settings.toml读取配置。

正确做法是在你的CIRCUITPY磁盘的根目录下,创建一个名为settings.toml的文本文件。这个文件用来存放所有敏感的或需要配置的变量。

# settings.toml 示例 CIRCUITPY_WIFI_SSID = "你的Wi-Fi名称" CIRCUITPY_WIFI_PASSWORD = "你的Wi-Fi密码" ADAFRUIT_AIO_USERNAME = "你的Adafruit IO用户名" ADAFRUIT_AIO_KEY = "你的Adafruit IO Active Key" TIMEZONE = "Asia/Shanghai"

重要提示settings.toml中的键(Key)是大小写敏感的,并且必须是全大写字母和下划线组成。CIRCUITPY_WIFI_SSIDCIRCUITPY_WIFI_PASSWORD是CircuitPython Wi-Fi库识别的固定名称,不能写错。

为什么是.toml文件?TOML是一种比JSON更易读、比YAML更简单的配置文件格式。在CircuitPython中,你可以使用os.getenv()函数来读取这些变量,就像读取系统环境变量一样。这样做将配置与代码逻辑完全分离,是专业的开发实践。

创建与编辑技巧

  1. 在Mu Editor中,你可以直接点击“新建”文件,然后保存到CIRCUITPY盘符,并命名为settings.toml
  2. 在Windows上,你也可以用记事本创建,但保存时务必选择“所有文件”,并将文件名用英文引号括起来,如"settings.toml",否则会被保存为settings.toml.txt
  3. 保存后,建议立即在代码中写一个简单的测试脚本,验证是否能正确读取:
import os ssid = os.getenv("CIRCUITPY_WIFI_SSID") print("我的Wi-Fi SSID是:", ssid)

如果打印出None,说明文件没找到或键名拼写错误。

2.3 库文件管理:告别“全部拷贝”

当你下载了Adafruit的CircuitPython库合集(Bundle)后,会发现lib文件夹里有上百个库。一股脑全拷贝到板子的/lib目录下是一个常见的错误。这会导致CIRCUITPY磁盘空间迅速耗尽,尤其是那些存储空间较小的板子。

正确的库管理流程

  1. 按需索取:根据你的项目需求,只复制必要的库。例如,一个基本的网络项目通常需要:
    • adafruit_requests.mpy:用于HTTP请求。
    • adafruit_ntp.mpy:如果你需要从网络获取精确时间。
    • 对应的Wi-Fi驱动库,如adafruit_esp32spi(如果使用外部Wi-Fi模块)或确保板子固件已内置Wi-Fi支持(如ESP32)。
  2. 注意依赖:有些库依赖其他库。例如,adafruit_requests依赖socketpool。在CircuitPython库合集的requirements.txt文件里,或者库的文档里,通常会写明依赖关系。最保险的方法是,在复制一个库时,如果运行代码提示ModuleNotFoundError,再根据错误信息去补全它依赖的库。
  3. 使用.mpy文件:库文件有.py.mpy两种格式。.mpy是预编译的字节码,加载更快、更省内存。对于ESP32等性能较强的板子,两者区别不大,但优先使用.mpy是个好习惯。

一个干净、高效的/lib目录是项目健康的标志。定期清理不再使用的库,可以避免潜在的冲突和混乱。

3. 网络请求实战:从HTTP到JSON解析

配置好环境后,我们进入实战环节。网络请求是物联网设备与外界对话的主要方式,而HTTP/HTTPS协议是其中最通用的语言。

3.1 建立网络连接:不仅仅是输入密码

在代码中连接Wi-Fi,看似只是一行wifi.radio.connect(ssid, password),但背后有几个关键点需要理解。

import wifi import os # 1. 从 settings.toml 安全地读取凭证 ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") # 2. 扫描网络(可选,但有助于调试) print("正在扫描可用网络...") for network in wifi.radio.start_scanning_networks(): # 注意:network.ssid 是 bytes 类型,需要解码 print(f"\t{str(network.ssid, 'utf-8')}\t信号强度: {network.rssi} dBm") wifi.radio.stop_scanning_networks() # 3. 连接网络 print(f"正在连接至 {ssid}...") try: wifi.radio.connect(ssid, password) print("连接成功!") print(f"IP地址: {wifi.radio.ipv4_address}") print(f"子网掩码: {wifi.radio.ipv4_subnet}") print(f"网关: {wifi.radio.ipv4_gateway}") except Exception as e: print(f"连接失败: {e}") # 这里可以加入重试逻辑或进入深度睡眠

关键点解析

  • 异常处理:网络连接可能因为信号弱、密码错误、路由器拒绝等原因失败。使用try...except块捕获异常是必须的,否则程序会崩溃。在生产项目中,你还需要加入重试机制和失败后的处理逻辑(如闪烁LED报警)。
  • 信号强度network.rssi是接收信号强度指示,单位为dBm。数值越接近0(例如-50)信号越好,低于-80则连接可能不稳定。在扫描阶段打印出来,可以帮助你选择最佳接入点或定位设备摆放位置。
  • IP信息:成功连接后,打印出获取到的IP地址、网关等信息,是验证网络层是否通畅的第一步。如果这里显示的是0.0.0.0或奇怪的地址,说明DHCP获取失败,需要检查路由器设置或网络环境。

3.2 使用adafruit_requests发送HTTP请求

adafruit_requests是CircuitPython版的requests库,API设计上尽量向标准库看齐,但由于运行在资源受限的单片机上,它有一些自己的特点。

基础GET请求

import ssl import socketpool import adafruit_requests # 创建Socket池和请求会话 pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) # 示例1:获取纯文本 TEXT_URL = "http://httpbin.org/get" print(f"从 {TEXT_URL} 获取文本") response = requests.get(TEXT_URL) print("-" * 40) print(f"状态码: {response.status_code}") print(f"响应头: {response.headers}") print(f"响应文本: {response.text[:200]}...") # 只打印前200字符避免刷屏 response.close() # 重要:关闭响应,释放资源 print("-" * 40)

与标准Python requests的主要区别

  1. 必须手动管理连接池:需要先创建socketpool.SocketPool,然后将其传递给adafruit_requests.Session。这是因为单片机没有操作系统来管理底层网络套接字。
  2. 必须显式关闭响应:调用response.close()来释放socket连接。虽然在某些情况下垃圾回收器最终会处理,但显式关闭是保证资源及时释放、避免内存泄漏的最佳实践。你也可以使用with语句来自动管理:
    with requests.get(TEXT_URL) as response: print(response.text) # 退出with块后,response会自动关闭
  3. SSL上下文:对于HTTPS请求,必须提供SSL上下文ssl.create_default_context()。如果你的板子没有足够的根证书,或者访问的是自签名证书的服务器,可能需要加载自定义证书,这比较复杂,初学者建议先使用HTTP或可信的HTTPS网站进行测试。

3.3 解析JSON数据:物联网的通用语

现代Web API绝大多数都返回JSON格式的数据。adafruit_requests提供了方便的.json()方法来解析。

import adafruit_requests # ... 省略之前的连接和session创建代码 ... # 示例2:获取并解析JSON JSON_QUOTES_URL = "https://quotes.rest/qod" print(f"从 {JSON_QUOTES_URL} 获取JSON") try: response = requests.get(JSON_QUOTES_URL) if response.status_code == 200: data = response.json() # 直接解析为Python字典/列表 print("-" * 40) # 假设返回格式是 {"contents": {"quotes": [{"quote": "..."}]}} if 'contents' in data and 'quotes' in data['contents']: quote = data['contents']['quotes'][0]['quote'] print(f"今日名言: {quote}") else: print("返回的JSON结构不符合预期:", data) else: print(f"请求失败,状态码: {response.status_code}") except ValueError as e: print(f"JSON解析错误: {e}") except Exception as e: print(f"请求发生错误: {e}") finally: response.close()

实战技巧:处理动态数据很多API返回的JSON结构嵌套很深,或者字段可能不存在。直接像data['contents']['quotes'][0]['quote']这样访问,一旦中间某个键不存在,就会抛出KeyError导致程序崩溃。

更健壮的访问方式

  1. 使用.get()方法data.get('contents', {}),如果键不存在,返回默认值(这里是一个空字典{})。
  2. 循序渐进的检查
    contents = data.get('contents') if not isinstance(contents, dict): print("contents字段不是字典") return quotes = contents.get('quotes') if not isinstance(quotes, list) or len(quotes) == 0: print("quotes字段不是列表或为空") return first_quote = quotes[0] if not isinstance(first_quote, dict): print("列表第一项不是字典") return final_quote = first_quote.get('quote', '未找到名言') print(f"今日名言: {final_quote}")

虽然代码看起来啰嗦,但在嵌入式开发中,稳定性远比简洁性重要。一个未处理的异常可能导致整个设备重启。

示例3:从GitHub API获取具体信息这是一个更真实的例子,获取CircuitPython仓库的星标数。

JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython" print(f"从GitHub API获取信息") response = requests.get(JSON_STARS_URL) data = response.json() print("-" * 40) # 直接访问字典键,因为GitHub API结构稳定。但生产代码仍建议加判断。 star_count = data.get('stargazers_count', 'N/A') print(f"CircuitPython GitHub 星标数: {star_count}") print("-" * 40) response.close()

4. IPv6配置与网络编程进阶

随着IPv4地址的耗尽,IPv6正在加速普及。你的物联网设备很可能在未来的某个网络中只获得一个IPv6地址。CircuitPython 9.2开始为Espressif芯片提供了实验性的IPv6支持,了解它很有必要。

4.1 IPv6基础与隐私考量

IPv6地址长达128位,通常表示为8组16进制数(如2001:0db8:85a3::8a2e:0370:7334)。与IPv4最大的不同之一在于地址的生成方式。

隐私风险:在默认的“无状态地址自动配置”(SLAAC)中,设备通常会用自己的MAC地址生成接口标识符(EUI-64格式)。这意味着你的设备IPv6地址后半部分在全球任何网络中都是固定的。任何你连接的服务都可以通过这个地址追踪到你的设备本身,而不是仅仅追踪到你的家庭网络网关。这是一个实实在在的隐私问题。

地址类型

  • 全局单播地址:以2000::/3开头(如2001:2002:),可以在互联网上路由,相当于公网IP。
  • 唯一本地地址:以fc00::/7开头(如fd00:),用于私有网络,类似IPv4的10.0.0.0/8192.168.0.0/16,但理论上全球唯一,避免合并网络时冲突。
  • 链路本地地址:以fe80::/10开头,仅用于同一物理链路上的通信,路由器不转发。

4.2 在CircuitPython中启用与使用IPv6

由于上述隐私问题,CircuitPython默认不启用IPv6。你需要显式地开启它。

import wifi import ipaddress # 1. 连接到Wi-Fi (IPv4) ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") wifi.radio.connect(ssid, password) print(f"IPv4地址: {wifi.radio.ipv4_address}") # 2. 显式启动IPv6 DHCP客户端 print("正在启用IPv6...") wifi.start_dhcp_client(ipv6=True) # 关键步骤! print("IPv6已启用。") # 3. 检查所有网络地址 print("所有网络接口地址:") for addr in wifi.radio.addresses: print(f" - {addr}") # 判断地址类型 ip_obj = ipaddress.ip_address(addr) if ip_obj.version == 6: if ip_obj.is_link_local: print(" (链路本地地址)") elif ip_obj.is_private: # 对于IPv6,这检查是否是唯一本地地址 (fc00::/7) print(" (唯一本地地址/私有地址)") else: print(" (全局单播地址)")

代码解读

  • wifi.start_dhcp_client(ipv6=True):这行代码是启用IPv6的关键。它会触发设备向路由器发送DHCPv6请求或进行SLAAC,从而获取一个或多个IPv6地址。
  • wifi.radio.addresses:这个属性返回一个包含所有IP地址(IPv4和IPv6)的元组。遍历它可以查看设备获取到的所有地址。
  • ipaddress模块:这是Python标准库的一部分,CircuitPython也包含了它。用它可以方便地分析IP地址的属性。

设置IPv6 DNS

# 查看当前DNS服务器 print("当前DNS服务器:", wifi.radio.dns) # 设置DNS服务器(例如Cloudflare的IPv6 DNS) wifi.radio.dns = ("2606:4700:4700::1111", "2001:4860:4860::8888") # Cloudflare和Google的IPv6 DNS print("设置后DNS服务器:", wifi.radio.dns)

4.3 使用IPv6套接字进行通信

启用IPv6后,你就可以创建IPv6套接字进行网络通信了。这与IPv4套接字编程非常相似,主要区别在于地址族(Address Family)。

示例:向IPv6服务器发送UDP数据包(模拟NTP请求)

import socket import struct import time # 目标NTP服务器地址和端口 (示例中使用的是一个私有地址,你需要替换成真实的IPv6 NTP服务器) # 公共的IPv6 NTP服务器如:2001:4860:4806:8:: (time.google.com) NTP_SERVER = "2001:4860:4806:8::" # time.google.com 的IPv6地址 NTP_PORT = 123 NTP_DELTA = 2208988800 # 秒,NTP时间(1900年至今)与Unix时间(1970年至今)的差值 def get_ntp_time_v6(): # 创建IPv6 UDP套接字 # socket.AF_INET6 指定使用IPv6地址族 # socket.SOCK_DGRAM 指定使用UDP协议 with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as sock: sock.settimeout(2.0) # 设置超时时间为2秒 # 构建NTP协议数据包 (简化版,仅第一个字节有意义) # 0x1B = 00 (LI), 011 (版本3), 011 (客户端模式) ntp_packet = bytearray(48) ntp_packet[0] = 0x1B # 设置协议版本和模式 # 发送请求 sock.sendto(ntp_packet, (NTP_SERVER, NTP_PORT)) # 接收响应 data, address = sock.recvfrom(48) print(f"从 {address} 收到响应") # 解析响应,提取传输时间戳(位于第40-43字节) # NTP时间戳是64位定点数,高32位是秒,低32位是小数秒 # 我们只取高32位的整数秒部分 transmit_timestamp = struct.unpack('!I', data[40:44])[0] # 转换为Unix时间戳 unix_time = transmit_timestamp - NTP_DELTA return unix_time try: print("尝试通过IPv6获取NTP时间...") timestamp = get_ntp_time_v6() local_time = time.localtime(timestamp) print(f"从NTP服务器获取的Unix时间戳: {timestamp}") print(f"转换后的本地时间: {time.strftime('%Y-%m-%d %H:%M:%S', local_time)}") except socket.timeout: print("错误:连接NTP服务器超时。请检查网络或服务器地址。") except OSError as e: print(f"网络错误: {e}") except Exception as e: print(f"未知错误: {e}")

关键点解析

  • socket.AF_INET6:这是创建IPv6套接字的核心参数。如果使用socket.AF_INET,则创建的是IPv4套接字。
  • 地址格式:在sendtorecvfrom中,地址是一个元组(host, port),其中host可以是IPv6地址字符串(如"2001:4860:4806:8::")或域名(如"ipv6.google.com")。系统会自动解析。
  • 错误处理:网络操作极易超时或失败。务必使用try...except包裹,并针对socket.timeoutOSError进行捕获,给用户明确的反馈。
  • 防火墙与路由:如果你的设备处在家庭路由器后,且路由器没有配置IPv6或防火墙阻止了出站请求,那么访问外网IPv6地址可能会失败。测试时,可以先尝试ping一个IPv6地址:wifi.radio.ping("ipv6.google.com"),看是否能通。

IPv6 Ping测试

# IPv4 Ping ipv4_target = ipaddress.ip_address("8.8.8.8") print(f"Ping IPv4 (8.8.8.8): {wifi.radio.ping(ipv4_target)} ms") # IPv6 Ping (使用域名,系统会解析出IPv6地址) print(f"Ping IPv6 (ipv6.google.com): {wifi.radio.ping('ipv6.google.com')} ms") # 也可以直接使用IPv6地址字符串 print(f"Ping IPv6 (直接地址): {wifi.radio.ping('2001:4860:4860::8888')} ms")

wifi.radio.ping方法兼容IPv4和IPv6地址,是测试网络连通性的快捷工具。

5. 代码编辑、调试与项目管理最佳实践

掌握了网络通信的核心技能后,如何高效、稳健地编写和调试代码,就成了提升开发效率的关键。这一部分分享的很多技巧,都是我在无数个调试的深夜总结出来的。

5.1 串行控制台:你的“上帝之眼”

串行控制台(Serial Console/REPL)是嵌入式开发中最重要的调试工具,没有之一。它不仅是输出print信息的地方,更是一个交互式的Python环境。

如何有效利用串行控制台

  1. 打印调试法:在代码的关键节点插入print语句,输出变量状态、函数是否被调用等信息。

    def connect_to_wifi(ssid, password): print(f"[DEBUG] 尝试连接SSID: {ssid}") try: wifi.radio.connect(ssid, password) print(f"[DEBUG] 连接成功,IP: {wifi.radio.ipv4_address}") return True except Exception as e: print(f"[ERROR] 连接失败: {type(e).__name__}: {e}") return False

    使用[DEBUG][INFO][ERROR]等前缀,可以在大量输出中快速定位不同级别的信息。

  2. 捕获异常回溯:当代码崩溃时,CircuitPython会在串行控制台输出完整的异常回溯信息(Traceback)。这是定位错误行号和原因的最直接依据。务必养成在出错后第一时间查看控制台输出的习惯。

  3. 使用REPL进行交互式探索

    • 在代码运行中,按Ctrl+C可以中断程序,进入REPL。
    • 在REPL中,你可以直接查询或修改变量、导入模块测试函数、甚至直接操作硬件GPIO。
    • 例如,当网络连接失败时,你可以在REPL中手动执行import wifi; wifi.radio.connect(...)来单独测试,排除代码其他部分的干扰。
    • 输入help(modules)可以查看已安装的模块,dir(wifi.radio)可以查看wifi.radio对象的所有属性和方法。

Mu Editor的串行控制台技巧

  • 自动滚动:确保开启自动滚动,以便看到最新输出。
  • 清除输出:使用Ctrl+L可以快速清屏。
  • 中断与软重启Ctrl+C中断程序,Ctrl+D执行软重启(相当于按了一下板子的复位键),这会重新运行code.py。软重启不会断开USB连接,比重启快得多。

5.2 代码编辑与文件管理避坑指南

文件命名与自动重载: CircuitPython会按照code.txt->code.py->main.txt->main.py的顺序寻找并执行文件。强烈建议始终使用code.py作为主文件。如果你不小心创建了main.py,即使你修改code.py,板子也会执行main.py,这会导致你以为代码没更新的困惑。

保存与文件系统损坏: 这是新手最容易导致板子“变砖”(实际是文件系统损坏)的操作。当你通过电脑向CIRCUITPY磁盘写入文件时,操作系统会缓存写入操作。如果你在文件没有完全写完时拔掉USB线或按复位键,就可能损坏文件系统。

规避方法

  1. 使用Mu Editor:Mu在保存文件时会强制同步,相对安全。
  2. 手动同步(非Mu用户)
    • Windows:在系统托盘右键点击CIRCUITPY磁盘,选择“弹出”。即使弹出失败,这个过程也会强制完成写入。
    • macOS/Linux:在终端执行sync命令。
  3. 代码保护:在code.py的最开头加入以下代码,可以禁用CircuitPython的自动重载功能,给你更充裕的保存时间窗口,但代价是你需要手动软重启(Ctrl+D)来运行新代码。
    import supervisor supervisor.runtime.autoreload = False # 你的其他代码...

项目文件组织: 对于稍复杂的项目,不要把所有的代码都堆在code.py里。

  1. 模块化:将相关的函数和类放在单独的.py文件中,例如network_manager.pysensor_reader.py
  2. 使用lib和根目录:第三方库放在/lib下,自己的模块可以放在根目录或新建的文件夹(如/modules)里,然后在code.py中用from modules import network_manager导入。
  3. 配置文件分离:我们已经将密钥放在settings.toml中。其他配置(如服务器地址、采样间隔)也可以放在这里,或者放在一个单独的config.py中。

5.3 错误处理与程序健壮性

物联网设备往往需要长时间无人值守运行,健壮性至关重要。

网络请求的通用重试模式

import time def robust_http_get(url, retries=3, delay=5): for attempt in range(retries): try: print(f"尝试 {attempt + 1}/{retries}...") response = requests.get(url) if response.status_code == 200: return response # 成功,返回响应对象 else: print(f"HTTP错误: {response.status_code}") except OSError as e: # 捕获网络相关的OSError print(f"网络错误: {e}") except Exception as e: print(f"未知错误: {e}") # 如果不是最后一次尝试,则等待后重试 if attempt < retries - 1: print(f"{delay}秒后重试...") time.sleep(delay) print("所有重试均失败。") return None # 使用示例 data_response = robust_http_get("https://api.example.com/data", retries=5, delay=10) if data_response: process_data(data_response.json()) data_response.close() else: # 进入错误处理状态,例如闪烁LED报警 enter_error_state()

看门狗定时器: 对于可能因为未知原因卡死的程序,可以使用microcontroller.watchdog来设置看门狗。

import microcontroller import time # 设置看门狗超时时间为10秒 watchdog_timeout = 10000 # 毫秒 microcontroller.watchdog.timeout = watchdog_timeout microcontroller.watchdog.mode = microcontroller.WatchDogMode.RAISE # 超时后引发异常,而非复位 try: while True: # 你的主循环代码 do_main_work() # 定期喂狗,重置看门狗计时器 microcontroller.watchdog.feed() time.sleep(1) except Exception as e: # 看门狗超时或其他异常会被捕获到这里 print(f"程序异常或看门狗超时: {e}") # 在这里可以进行一些紧急日志记录或状态保存 # 然后可以选择软重启或进入安全模式 microcontroller.reset() # 硬重启

使用看门狗需要非常小心,确保在主循环中定期feed(),否则设备会不断重启。

6. 综合项目示例:一个联网的时钟与天气显示器

我们将前面所有的知识融合起来,构建一个简单的综合项目:一个通过Wi-Fi连接,从Adafruit IO获取时间,并从公共API获取天气信息,并显示在串行控制台(或未来可以连接到屏幕)的设备。

项目结构

CIRCUITPY/ ├── settings.toml # 配置文件 ├── code.py # 主程序 ├── lib/ │ ├── adafruit_requests.mpy │ └── ... (其他所需库) └── utils.py # 工具函数模块(可选)

settings.toml内容

CIRCUITPY_WIFI_SSID = "Your_WiFi" CIRCUITPY_WIFI_PASSWORD = "Your_Password" ADAFRUIT_AIO_USERNAME = "your_username" ADAFRUIT_AIO_KEY = "your_key" TIMEZONE = "Asia/Shanghai" WEATHER_API_KEY = "your_openweathermap_key" # 假设使用OpenWeatherMap CITY = "Shanghai"

code.py主程序

import os import time import wifi import socketpool import ssl import adafruit_requests import microcontroller # 用于看门狗 # --- 配置 --- MAX_RETRIES = 3 RETRY_DELAY = 10 # --- 初始化网络和请求会话 --- def init_network(): ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") for i in range(MAX_RETRIES): try: print(f"网络连接尝试 {i+1}/{MAX_RETRIES}") wifi.radio.connect(ssid, password) print(f"连接成功! IPv4: {wifi.radio.ipv4_address}") # 可选:启用IPv6 # wifi.start_dhcp_client(ipv6=True) # for addr in wifi.radio.addresses: # print(f" - {addr}") return True except Exception as e: print(f"连接失败: {e}") if i < MAX_RETRIES - 1: print(f"{RETRY_DELAY}秒后重试...") time.sleep(RETRY_DELAY) print("网络连接彻底失败。") return False # --- 从Adafruit IO获取时间 --- def get_adafruit_io_time(requests_session): username = os.getenv("ADAFRUIT_AIO_USERNAME") key = os.getenv("ADAFRUIT_AIO_KEY") timezone = os.getenv("TIMEZONE", "UTC") # 默认UTC # 构建请求URL (Adafruit IO Time API) time_url = f"https://io.adafruit.com/api/v2/{username}/integrations/time/strftime" params = { "x-aio-key": key, "tz": timezone, "fmt": "%Y-%m-%d %H:%M:%S %Z" # 格式: 年-月-日 时:分:秒 时区 } # 注意:adafruit_requests 不支持直接的params参数,需要手动拼接URL # 这里简化处理,使用一个固定的格式URL # 实际使用请参考Adafruit IO文档构建正确的URL time_url = f"{time_url}?x-aio-key={key}&tz={timezone}&fmt=%25Y-%25m-%25d+%25H%3A%25M%3A%25S+%25Z" try: response = requests_session.get(time_url) if response.status_code == 200: return response.text.strip() else: print(f"时间API错误: {response.status_code}") return None except Exception as e: print(f"获取时间失败: {e}") return None finally: if 'response' in locals(): response.close() # --- 从OpenWeatherMap获取天气(示例) --- def get_weather(requests_session): api_key = os.getenv("WEATHER_API_KEY") city = os.getenv("CITY", "London") if not api_key: print("未配置WEATHER_API_KEY") return None weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric" try: response = requests_session.get(weather_url) if response.status_code == 200: data = response.json() main = data.get('main', {}) weather_list = data.get('weather', [{}]) weather_desc = weather_list[0].get('description', 'N/A') if weather_list else 'N/A' temp = main.get('temp', 'N/A') humidity = main.get('humidity', 'N/A') return { 'description': weather_desc, 'temperature': temp, 'humidity': humidity, 'city': data.get('name', city) } else: print(f"天气API错误: {response.status_code}") return None except Exception as e: print(f"获取天气失败: {e}") return None finally: if 'response' in locals(): response.close() # --- 主循环 --- def main(): print("=== 联网时钟与天气显示器启动 ===") # 初始化看门狗 (超时60秒) watchdog_timeout = 60000 microcontroller.watchdog.timeout = watchdog_timeout microcontroller.watchdog.mode = microcontroller.WatchDogMode.RAISE if not init_network(): # 网络失败,进入深度睡眠或错误状态 print("无法初始化网络,程序结束。") return # 创建网络会话 pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) last_time_update = 0 last_weather_update = 0 update_interval_time = 300 # 5分钟更新一次时间 update_interval_weather = 900 # 15分钟更新一次天气 current_time = "N/A" current_weather = None try: while True: now = time.monotonic() # 单调时间,不受系统时间影响 # 更新时间 if now - last_time_update > update_interval_time: print("\n--- 更新时间 ---") new_time = get_adafruit_io_time(requests) if new_time: current_time = new_time last_time_update = now print(f"当前时间: {current_time}") else: print("时间更新失败,使用旧时间。") # 更新天气 if now - last_weather_update > update_interval_weather: print("\n--- 更新天气 ---") new_weather = get_weather(requests) if new_weather: current_weather = new_weather last_weather_update = now print(f"城市: {current_weather['city']}") print(f"天气: {current_weather['description']}") print(f"温度: {current_weather['temperature']}°C") print(f"湿度: {current_weather['humidity']}%") else: print("天气更新失败,使用旧数据。") # 每秒打印一次状态(模拟显示刷新) print(f"\r时间: {current_time} | ", end="") if current_weather: print(f"天气: {current_weather['description']} {current_weather['temperature']}°C", end="") else: print("天气: 获取中...", end="") # 喂狗 microcontroller.watchdog.feed() time.sleep(1) # 主循环延迟 except Exception as e: print(f"\n主循环发生异常: {e}") # 看门狗超时或其他异常 # 尝试保存状态或记录错误 time.sleep(5) microcontroller.reset() # 重启设备 finally: # 程序正常退出前关闭看门狗(通常不会执行到这里) microcontroller.watchdog.deinit() if __name__ == "__main__": main()

这个示例项目涵盖了:

  1. 健壮的网络连接:带有重试机制的Wi-Fi连接。
  2. 模块化函数:将网络初始化、获取时间、获取天气分离成函数,提高代码可读性和可维护性。
  3. 错误处理:对所有网络请求进行了try...except包装,并提供了降级处理(使用旧数据)。
  4. 资源管理:使用finally块确保HTTP响应被关闭。
  5. 看门狗:加入了看门狗定时器,防止程序死锁。
  6. 配置外部化:所有敏感信息和配置都存放在settings.toml中。
  7. 周期性任务:使用time.monotonic()和间隔时间来控制更新频率,避免频繁请求API。

你可以将此代码作为基础,进一步扩展,例如将输出从串行控制台转移到OLED屏幕、添加更多的传感器数据、或者将数据上传到其他物联网平台。通过这个完整的流程,你应该对如何使用CircuitPython进行稳健的网络编程有了一个全面而深入的理解。记住,嵌入式网络编程的关键在于耐心细致,处理好每一个可能出错的地方,你的设备才能长久稳定地运行。

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

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

立即咨询