1. 项目概述与核心价值如果你正在用ESP32、ESP8266这类带WiFi的微控制器做物联网项目那么网络连接就是你绕不开的第一道坎。这事儿说起来简单不就是让板子连上家里的WiFi嘛但真动起手来你会发现一堆琐碎又关键的问题WiFi密码和API密钥这些敏感信息到底该放哪儿直接写在code.py里那下次想把代码分享到论坛或者开源到GitHub上岂不是得先当个“代码裁缝”小心翼翼地用查找替换把密码一个个抠掉还得提醒自己千万别漏了哪个。更别提万一不小心把带密码的代码传了上去那场面可就尴尬了。这就是settings.toml文件要解决的痛点。从CircuitPython 8开始这个文件成了管理项目“秘密”的标准方式。它的核心思想是“配置与代码分离”。你可以把它想象成项目的“钥匙串”所有不能公开的凭证比如WiFi的SSID、密码各种云服务像Adafruit IO、Thingspeak的API Key甚至是你自定义的一些配置项都集中放在这个文件里。而你的主程序code.py则通过标准接口去读取这些信息。这样做的好处显而易见你的核心代码变得干净、安全可以随意分享和版本控制而包含敏感信息的settings.toml文件你只需要把它留在本地或者通过.gitignore排除在版本库之外。本文将手把手带你搞懂settings.toml的方方面面。从最基础的创建和格式到如何在代码中安全地调用再到连接WiFi、测试网络、使用Adafruit IO等实战环节。我还会分享一些从实际项目中踩坑总结出来的经验比如变量名不匹配这种看似简单却最容易让人卡壳的问题以及如何为不同项目灵活组织配置。无论你是刚接触CircuitPython物联网开发的新手还是想优化现有项目结构的老手这篇文章都能给你提供一套清晰、安全、可复用的配置管理方案。2. settings.toml文件详解从创建到使用2.1 文件创建与基础格式首先settings.toml文件必须放在你的CIRCUITPY驱动器的根目录下。注意是根目录不能放在任何子文件夹里。你可以用任何纯文本编辑器来创建和编辑它比如VS Code、Notepad甚至是系统自带的记事本但保存时需注意编码后面会讲。这个文件的基本语法是TOML格式这是一种对人类友好也对机器友好的配置文件格式。其核心结构就是“键 值”对。对于CircuitPython来说最基础、必不可少的两个配置项就是WiFi信息CIRCUITPY_WIFI_SSID 你的WiFi名称 CIRCUITPY_WIFI_PASSWORD 你的WiFi密码这里有几个关键点需要注意变量名是大小写敏感的CIRCUITPY_WIFI_SSID和circuitpy_wifi_ssid会被认为是两个不同的变量。官方库和绝大多数示例代码都使用全大写加下划线的命名约定强烈建议你遵循这个约定避免不必要的麻烦。字符串必须用双引号包裹你的WiFi名称是正确的而你的WiFi名称没有引号或你的WiFi名称单引号在TOML中可能导致解析错误或无法识别。等号两边可以有空格这能让文件看起来更整洁但非必须。2.2 支持的数据类型与高级用法除了最基本的字符串settings.toml也支持其他数据类型这让你能存储更丰富的配置信息。整数直接写数字不需要引号。支持十进制和十六进制。CIRCUITPY_WEB_API_PORT 8080 # 十进制 magic_number 0xDEADBEEF # 十六进制布尔值true或false小写。enable_debug true use_metric false注释使用#号。注释对于说明某个配置项的用途至关重要尤其是在团队协作或项目搁置一段时间后回头再看时。# 这是Adafruit IO的配置区域 ADAFRUIT_AIO_USERNAME my_username # 你的Adafruit IO用户名 ADAFRUIT_AIO_KEY supersecretkeyabc123 # 你的Adafruit IO Active Key # 注意KEY不是你的登录密码需要在Adafruit IO网站上生成一个容易被忽略但极其重要的细节是字符编码和转义。CircuitPython期望settings.toml文件以“UTF-8 无 BOM”格式保存。BOMByte Order Mark是文件开头的一个特殊字符用于标识编码。对于Windows的记事本来说默认保存的“UTF-8”格式实际上是“UTF-8 with BOM”。这个额外的BOM字符可能会导致CircuitPython无法正确读取文件的第一行从而引发各种诡异的连接失败。实操心得避免编码坑我强烈建议不要使用Windows记事本编辑settings.toml。使用VS Code、Sublime Text、Notepad等现代编辑器并在保存时确认编码格式为“UTF-8”。在VS Code中你可以看右下角的状态栏确保显示的是“UTF-8”。如果显示“UTF-8 with BOM”点击它并选择“通过编码保存”然后选择“UTF-8”来移除BOM。如果你想在字符串中使用特殊字符或EmojiTOML支持Unicode转义。例如你想存储一个“大拇指”表情作为某个状态标志status_icon \U0001f44d # 这代表 这里使用的是\U后跟8位十六进制数的格式。注意\x两位十六进制和\ooo八进制转义在TOML中不可用。2.3 在代码中读取配置配置写好了怎么在code.py里用起来呢这需要用到Python的os模块。CircuitPython的os.getenv()函数是通往settings.toml的桥梁。基本用法非常简单import os ssid os.getenv(CIRCUITPY_WIFI_SSID) password os.getenv(CIRCUITPY_WIFI_PASSWORD) print(f我将要连接的网络是: {ssid}) # 注意在实际代码中永远不要打印密码os.getenv(“键名”)会返回对应键的字符串值。如果键不存在它会返回None。这一点非常重要因为它意味着你的代码必须处理配置缺失的情况否则在尝试使用一个None值去连接WiFi时程序会崩溃。一个更健壮的写法是import os ssid os.getenv(CIRCUITPY_WIFI_SSID) password os.getenv(CIRCUITPY_WIFI_PASSWORD) if not ssid or not password: print(错误请在 settings.toml 中配置 CIRCUITPY_WIFI_SSID 和 CIRCUITPY_WIFI_PASSWORD) # 这里可以进入错误处理流程比如让板子上的LED闪烁报警 while True: pass # 或者执行其他错误处理 else: # 进行WiFi连接 # ...这种“防御性编程”在物联网设备上尤其重要因为设备可能在没有人工干预的情况下运行清晰的错误提示能帮你快速定位问题。3. 实战CircuitPython WiFi连接全流程3.1 环境准备与库安装在开始编写连接代码前你需要确保两件事固件版本你的CircuitPython固件版本需要是8.0或更高以支持settings.toml。你可以通过连接到板子的串行REPL使用Mu编辑器、VS Code的串行监视器或screen/putty等工具然后输入import os; os.uname()来查看版本信息。必要的库WiFi功能需要adafruit_esp32spi或wifi库对于内置WiFi的ESP32-S2/S3等芯片。对于大多数现代ESP32板我们使用通用的wifi库。你还需要adafruit_requests库来处理HTTP请求以及socketpool。最方便的方法是下载“项目包”。在Adafruit的许多学习指南页面上都有一个“Download Project Bundle”按钮。点击它会下载一个包含code.py和所有必需库在lib文件夹内的zip文件。你只需要解压然后将整个lib文件夹和code.py文件复制到你的CIRCUITPY驱动器即可。如果你的板子已经有一些库注意合并而非覆盖lib文件夹。3.2 代码解析从扫描到请求下面我们以一个经典的网络测试脚本为例拆解每一步在做什么。这个脚本会扫描网络、连接WiFi、测试ping并获取网页内容。# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries # SPDX-License-Identifier: MIT import os import ipaddress import ssl import wifi import socketpool import adafruit_requests # 定义要访问的测试URL TEXT_URL http://wifitest.adafruit.com/testwifi/index.html JSON_QUOTES_URL https://www.adafruit.com/api/quotes.php JSON_STARS_URL https://api.github.com/repos/adafruit/circuitpython print(ESP32-S2 网络客户端测试) # 1. 打印MAC地址 - 设备的唯一网络标识符 print(f我的MAC地址: {[hex(i) for i in wifi.radio.mac_address]}) # 2. 扫描并列出所有可用的WiFi网络 print(可用的WiFi网络:) for network in wifi.radio.start_scanning_networks(): # 将SSID从字节串转换为字符串并打印信号强度(RSSI)和信道 print(f\t{str(network.ssid, utf-8)}\t\tRSSI: {network.rssi}\tChannel: {network.channel}) wifi.radio.stop_scanning_networks() # 停止扫描以释放资源 # 3. 连接WiFi - 核心步骤 print(f正在连接到 {os.getenv(CIRCUITPY_WIFI_SSID)}) # 从 settings.toml 读取凭证 wifi.radio.connect(os.getenv(CIRCUITPY_WIFI_SSID), os.getenv(CIRCUITPY_WIFI_PASSWORD)) print(f已连接到 {os.getenv(CIRCUITPY_WIFI_SSID)}) print(f我的IP地址: {wifi.radio.ipv4_address}) # 4. 测试网络连通性 - Ping谷歌DNS ping_ip ipaddress.IPv4Address(8.8.8.8) ping wifi.radio.ping(ipping_ip) # 如果第一次ping超时返回None重试一次 if ping is None: ping wifi.radio.ping(ipping_ip) if ping is None: print(无法成功ping通 google.com) else: # ping返回的是秒转换为毫秒更直观 print(fPing google.com 耗时: {ping * 1000:.2f} ms) # 5. 创建Socket池和Requests会话为HTTP请求做准备 pool socketpool.SocketPool(wifi.radio) requests adafruit_requests.Session(pool, ssl.create_default_context()) # 6. 发起HTTP GET请求获取纯文本内容 print(f从 {TEXT_URL} 获取文本) response requests.get(TEXT_URL) print(- * 40) print(response.text) # 直接打印响应的文本内容 print(- * 40) # 7. 获取并解析JSON数据 print(f从 {JSON_QUOTES_URL} 获取JSON) response requests.get(JSON_QUOTES_URL) print(- * 40) print(response.json()) # .json()方法直接解析JSON为Python字典/列表 print(- * 40) print() print(f从 {JSON_STARS_URL} 获取并解析JSON) response requests.get(JSON_STARS_URL) print(- * 40) # 从解析后的JSON字典中提取特定字段 print(fCircuitPython GitHub星标数: {response.json()[stargazers_count]}) print(- * 40) print(完成)关键步骤解读与避坑指南扫描网络start_scanning_networks()返回的是一个网络对象迭代器。network.ssid是字节类型需要解码。扫描会占用无线电资源完成后务必调用stop_scanning_networks()。连接WiFiwifi.radio.connect()是阻塞调用它会一直尝试直到成功或超时。超时时间取决于固件和网络环境。如果网络需要门户认证如酒店WiFi标准连接方式会失败需要额外处理。Ping测试wifi.radio.ping()返回的是往返时间秒失败时返回None。首次ping失败有时是暂时的所以代码中重试了一次。但请注意某些网络防火墙可能禁止ICMPping协议导致即使能上网也ping不通。因此ping失败不一定代表网络不通后续的HTTP请求才是更可靠的测试。SocketPool这是管理网络套接字的重要组件。它负责复用连接避免为每个请求都创建新连接的开销。对于大多数应用你只需要在初始化时创建一个SocketPool实例并在整个程序中使用它。Adafruit Requests这个库让HTTP请求变得像在桌面Python上使用requests库一样简单。response.text用于文本内容response.json()用于JSON它会自动处理编码和解析。3.3 连接失败排查清单当你的板子无法连接网络时可以按照以下清单逐步排查问题现象可能原因排查步骤串口输出Connecting to...后长时间无反应最终报错1. SSID或密码错误。2. 路由器设置了MAC地址过滤。3. 网络隐藏了SSID未广播。4. 信号太弱。1. 仔细检查settings.toml中的SSID和密码区分大小写和特殊字符。2. 登录路由器后台检查MAC过滤列表将板子的MAC地址代码开头打印的加入白名单。3. 在wifi.radio.connect()中对于隐藏网络可能需要额外参数某些版本库支持。4. 将板子靠近路由器。能连接WiFi并获得IP但Ping和HTTP请求都失败1. 路由器未分配有效IP或DNS错误。2. 防火墙阻止了出站连接。3. 需要网页认证Captive Portal。1. 检查打印的IP地址是否是169.254.x.xAPIPA地址表示DHCP失败。尝试重启路由器或板子。2. 检查路由器或网络防火墙设置。3. 这是常见问题。设备连接后需要打开浏览器完成认证。CircuitPython处理这个比较麻烦通常需要手动在能上网的设备上完成认证或者使用能处理门户的专用库如果存在。代码报AttributeError或ImportError1. 必要的库文件缺失。2. 库文件版本与固件不兼容。1. 确认lib文件夹下有wifi.mpy、socketpool.mpy、adafruit_requests.mpy等文件。2. 从官方渠道或项目包中重新下载与你的CircuitPython版本匹配的库文件。os.getenv()返回None1.settings.toml中变量名拼写错误。2. 文件未保存在CIRCUITPY根目录。3. 文件编码错误含BOM。1. 逐字符核对代码中的键名和settings.toml中的是否完全一致包括大小写。2. 确认文件直接在CIRCUITPY:下而不是在CIRCUITPY:/lib/或其他文件夹里。3. 用高级文本编辑器另存为“UTF-8 无BOM”格式。4. 集成Adafruit IO与多服务配置4.1 Adafruit IO配置与连接Adafruit IO是Adafruit提供的物联网数据平台非常适合用来记录传感器数据、创建仪表盘或远程控制设备。要使用它你需要在settings.toml中添加你的账号信息。CIRCUITPY_WIFI_SSID your_wifi_ssid CIRCUITPY_WIFI_PASSWORD your_wifi_password # Adafruit IO 配置 ADAFRUIT_AIO_USERNAME your_io_username ADAFRUIT_AIO_KEY your_io_active_key重要提示ADAFRUIT_AIO_KEY不是你的账户登录密码它是在Adafruit IO网站上生成的“Active Key”。你需要登录Adafruit IO点击右上角的“My Key”然后生成或复制一个Key。这个Key具有完全的API访问权限务必像保护密码一样保护它。在代码中你可以使用adafruit_io库来与平台交互。首先确保lib文件夹下有adafruit_io库然后参考以下示例发送数据import os import wifi import socketpool import adafruit_requests import adafruit_io # ... (WiFi连接代码同上) ... # 创建请求会话 pool socketpool.SocketPool(wifi.radio) requests adafruit_requests.Session(pool, ssl.create_default_context()) # 初始化Adafruit IO客户端 aio_username os.getenv(ADAFRUIT_AIO_USERNAME) aio_key os.getenv(ADAFRUIT_AIO_KEY) # 注意这里需要根据你使用的网络库选择正确的适配器 # 对于使用内置wifi的ESP32-S2/S3使用 adafruit_io.AdafruitIO_WiFi # 如果你使用ESP32-SPI Featherwing等外部模块则使用 adafruit_io.AdafruitIO_SPI io adafruit_io.AdafruitIO_WiFi(requests, aio_username, aio_key) # 向名为“temperature”的Feed发送一个数据点 try: io.send_data(temperature, 25.6) print(数据发送成功) except Exception as e: print(f发送数据时出错: {e})4.2 管理多个API密钥与项目配置一个真实的物联网项目往往不止连接一个服务。你可能同时使用Adafruit IO记录数据用Telegram Bot发送警报还用某个天气API获取信息。settings.toml可以很好地管理所有这些密钥。# 网络配置 CIRCUITPY_WIFI_SSID Home_Network CIRCUITPY_WIFI_PASSWORD SuperSecretPassword123 # 物联网平台 ADAFRUIT_AIO_USERNAME my_adafruit_user ADAFRUIT_AIO_KEY aio_AbCdEfGhIjKlMnOpQrStUvWxYz # 第三方API OPENWEATHERMAP_API_KEY your_openweathermap_key TELEGRAM_BOT_TOKEN 1234567890:ABCdefGHIjklMnOpQRstUvWxyz # 项目特定配置 SENSOR_READ_INTERVAL 300 # 传感器读取间隔单位秒 TIMEZONE Asia/Shanghai # 时区用于时间同步 ENABLE_DEBUG_LOGGING true # 是否开启调试日志在代码中你可以根据功能模块来组织读取这些配置import os class Config: def __init__(self): self.wifi_ssid os.getenv(CIRCUITPY_WIFI_SSID) self.wifi_password os.getenv(CIRCUITPY_WIFI_PASSWORD) self.aio_username os.getenv(ADAFRUIT_AIO_USERNAME) self.aio_key os.getenv(ADAFRUIT_AIO_KEY) self.weather_key os.getenv(OPENWEATHERMAP_API_KEY) self.bot_token os.getenv(TELEGRAM_BOT_TOKEN) self.read_interval int(os.getenv(SENSOR_READ_INTERVAL, 60)) # 提供默认值 self.timezone os.getenv(TIMEZONE, UTC) self.debug os.getenv(ENABLE_DEBUG_LOGGING, false).lower() true def validate(self): 验证必要配置是否存在 required [self.wifi_ssid, self.wifi_password] if not all(required): raise ValueError(缺少必要的WiFi配置请检查settings.toml文件。) # 可以添加更多验证逻辑 return True # 使用配置 config Config() if config.validate(): print(f调试模式: {config.debug}) print(f读取间隔: {config.read_interval}秒)这种面向对象的配置管理方式让代码更清晰也更容易测试和维护。os.getenv()的第二个参数可以指定默认值这在某些配置项可选时非常有用。4.3 变量名不匹配最常见的坑这是新手有时甚至是老手最容易踩的坑。问题在于代码中通过os.getenv()读取的键名必须与settings.toml文件中定义的变量名完全一致。假设你的代码是这样写的mqtt_broker os.getenv(“MQTT_SERVER”) # 代码中键名是 MQTT_SERVER而你的settings.toml文件却是mqtt_server “broker.hivemq.com” # 文件中小写且是 mqtt_server那么os.getenv(“MQTT_SERVER”)将永远返回None因为找不到大写的MQTT_SERVER这个键。排查和解决这个问题的技巧保持命名一致为你的项目建立一个命名约定并严格遵守。例如全部使用大写蛇形命名法SERVICE_API_KEY。在代码开头打印检查在连接WiFi或使用任何配置之前先打印出你读取的值确认不是None。key os.getenv(“SOME_KEY”) print(f“SOME_KEY 的值是: {key}”) # 如果是None立刻就能发现使用常量定义在代码中为配置键名定义常量避免拼写错误。SETTING_WIFI_SSID “CIRCUITPY_WIFI_SSID” SETTING_WIFI_PASS “CIRCUITPY_WIFI_PASSWORD” ssid os.getenv(SETTING_WIFI_SSID)5. 高级主题与最佳实践5.1 从secrets.py迁移到settings.toml在CircuitPython 8之前社区普遍使用secrets.py文件来存储敏感信息。它的格式是标准的Python字典secrets { ‘ssid’: ‘MyNetwork’, ‘password’: ‘MyPassword’, ‘aio_username’: ‘my_user’, ‘aio_key’: ‘my_key’, }在代码中通过import secrets来使用。迁移到settings.toml非常简单在CIRCUITPY根目录创建一个新的settings.toml文件。将secrets.py中的键值对转换为TOML格式。注意将键名改为全大写蛇形命名是一种好习惯但不是必须的只要代码中os.getenv()的键名与之匹配即可。在code.py中将import secrets和相关引用如secrets[‘ssid’]替换为import os和os.getenv(“SSID”)。删除或重命名旧的secrets.py文件建议先重命名备份如secrets.py.old确保新配置工作后再删除。settings.toml相比secrets.py的主要优势在于其标准化和工具链支持。TOML是一种通用的配置文件格式很多编辑器和工具对其有更好的语法高亮、验证和格式化支持。5.2 安全注意事项与版本控制settings.toml文件包含了你所有的秘密因此安全处理它至关重要。绝对不要提交到版本控制系统这是铁律。如果你使用Git确保将settings.toml添加到项目的.gitignore文件中。# .gitignore settings.toml提供配置模板为了方便协作和项目部署你应该在版本库中提供一个settings.toml.example或settings.toml.template文件。这个文件包含所有需要的键但值用占位符或空字符串代替。# settings.toml.example CIRCUITPY_WIFI_SSID YOUR_WIFI_SSID_HERE CIRCUITPY_WIFI_PASSWORD YOUR_WIFI_PASSWORD_HERE ADAFRUIT_AIO_USERNAME YOUR_AIO_USERNAME ADAFRUIT_AIO_KEY YOUR_AIO_ACTIVE_KEY其他开发者或未来的你只需要复制这个文件为settings.toml并填入真实信息即可。考虑环境变量高级在团队开发或CI/CD流水线中更安全的做法是不将任何秘密保存在文件中而是通过环境变量注入。虽然标准的CircuitPython运行环境不支持从系统环境变量读取但你可以通过构建脚本在部署前根据环境变量动态生成settings.toml文件。5.3 组织大型项目的配置当项目变得复杂你可能会有多个设备、多种运行环境开发、测试、生产。一个settings.toml文件可能变得臃肿。虽然CircuitPython本身不支持settings.toml的模块化或继承但我们可以通过一些模式来管理按功能分区注释在文件内用注释清晰划分区块。############### # 网络配置 ############### CIRCUITPY_WIFI_SSID ... # ... ############### # 硬件引脚定义 ############### LED_PIN board.D13 SENSOR_PIN board.A0 # 注意这里存储的是字符串代码中需要eval或映射 # 更好的做法是存储引脚编号如 LED_PIN_NUM 13 ############### # 应用逻辑参数 ############### UPLOAD_INTERVAL 300 THRESHOLD_TEMP 30.0使用前缀分组为不同模块的配置添加前缀。NET_WIFI_SSID ... NET_WIFI_PASS ... NET_MQTT_BROKER ... SENSOR_DHT_PIN 15 SENSOR_READ_DELAY 2 APP_UPLOAD_URL ... APP_DEVICE_ID ...动态配置代码级对于硬件引脚等配置在settings.toml中存储标识符如D13在代码中通过字典映射到实际的board对象。或者更简单直接地将引脚配置放在代码的常量区域只将真正的“秘密”和可能因环境改变的参数如服务器地址、间隔时间放在settings.toml中。我个人在实际项目中的体会是保持settings.toml的简洁性很重要。它应该只存放那些因环境而异或绝对不能公开的信息。硬件引脚映射、复杂的业务逻辑参数等更适合放在代码的配置模块或单独的、可版本控制的config.py文件中。settings.toml的职责越单一它的管理和维护成本就越低安全性也越高。最后无论采用哪种方式清晰的文档比如在项目README中说明如何设置settings.toml对于任何需要运行你项目的人来说都是无价的。