CircuitPython 的 ESP-NOW
基于 Adafruit CircuitPython 10.x 及 ESP32-C3 平台的调试经验总结
CircuitPython 的 ESP-NOW 更简洁,但必须严格遵守 信道锁定 + Peer 对象 + 发送传入 peer 的范式,切勿生搬 MicroPython 的用法。
## 一、核心差异:MicroPython vs CircuitPython
CircuitPython 的 espnow 模块 API 与 MicroPython **不兼容**,切勿混用。
| 操作 | MicroPython | CircuitPython |
|------|-------------|---------------|
| **激活** | `esp.active(True)` | **无需调用**,创建实例即激活 |
| **添加对等端** | `esp.add_peer(mac)` | 创建 `Peer` 对象并加入 `esp.peers` 列表 |
| **发送** | `esp.send(message)` 或 `esp.send(mac, message)` | `esp.send(message, peer)`(**必须传入 peer 对象**) |
| **广播地址** | 可直接使用 | **必须作为 Peer 添加** |
## 二、正确的初始化流程
```python
import wifi
import espnow
# 1. 激活 WiFi 硬件(无需连接 AP)
wifi.radio.enabled = True
# 2. 获取信道(优先使用已连接 AP 的信道)
channel = 6
if wifi.radio.ap_info is not None:
channel = wifi.radio.ap_info.channel
# 3. 锁定信道(ESP32-C3 必须,通过临时 AP)
wifi.radio.start_ap("temp", "", channel=channel, max_connections=0)
wifi.radio.stop_ap()
# 4. 创建 ESP-NOW 实例
e = espnow.ESPNow()
# 5. 添加广播对等端(广播地址必须显式添加)
broadcast_mac = b'\xff\xff\xff\xff\xff\xff'
peer = espnow.Peer(mac=broadcast_mac, channel=channel)
e.peers.append(peer)
# 6. 发送消息(必须传入 peer)
message = b'\x01\x00\x01\x02\x03\x07' # 自定义报文
e.send(message, peer)
```
## 三、关键注意事项
### 3.1 信道锁定是必需的
即使已获取到当前信道,也必须通过 `start_ap() + stop_ap()` 的"空 AP"方式让 ESP-NOW 锁定到该信道。
若不锁定,发送可能成功但接收方收不到数据,或直接报错 `0x3069`。
```python
# 正确做法
wifi.radio.start_ap("_temp_espnow", "", channel=channel, max_connections=0)
wifi.radio.stop_ap()
```
### 3.2 广播地址必须作为 Peer 添加
即使发送目标是广播地址 `FF:FF:FF:FF:FF:FF`,也必须创建 Peer 对象并加入 `peers` 列表。
否则会返回 `ESP-NOW error 0x3069`(底层错误:对等端未找到)。
```python
# 必须创建 Peer 对象
peer = espnow.Peer(b'\xff\xff\xff\xff\xff\xff', channel=channel)
e.peers.append(peer)
```
### 3.3 发送时需传入 peer 参数
**正确**:`e.send(message, peer)`
**错误**:`e.send(message)` 或 `e.send(peer, message)`(参数顺序反了)
### 3.4 避免重复初始化
使用 `initialized` 标志位,确保 `init()` 只执行一次。
CircuitPython 的 `espnow.ESPNow()` 实例创建即激活,再次调用 `active(True)` 会抛出 `Already running` 异常(因为该方法不存在)。
```python
class ESPNowManager:
def __init__(self):
self.initialized = False
def init(self):
if self.initialized:
return True
# ... 初始化逻辑 ...
self.initialized = True
return True
```
### 3.5 信道一致性
ESP-NOW 通信双方必须在同一信道。
若设备同时连接 WiFi 路由器,ESP-NOW 的信道必须与路由器信道一致,否则 WiFi 连接会强制切换信道,导致 ESP-NOW 失效。
## 四、常见错误及解决方法
| 错误信息 | 原因 | 解决方案 |
|----------|------|----------|
| `ESP-NOW error 0x3069` | 未添加对等端,或信道不匹配 | 按正确流程添加 Peer,锁定信道 |
| `object with buffer protocol required` | `send()` 参数错误(传入了 Peer 而非 message) | 使用 `send(message, peer)` |
| `'ESPNow' object has no attribute 'active'` | 混用 MicroPython API | 删除 `.active(True)` 调用 |
| `Already running` | 尝试重复激活(或之前存在残留实例) | 检查初始化逻辑,确保只调用一次 |
| `ssid length must be 1-32` | `start_ap()` 的 SSID 为空字符串 | 提供临时 SSID,如 `"temp"` |
## 五、调试建议
### 5.1 先用独立脚本验证
编写最小测试脚本(只包含 WiFi 激活 + ESP-NOW 初始化 + 发送),排除调度器和其他模块干扰。
### 5.2 打印关键状态
- 本机 MAC 地址
- 使用的信道
- 是否成功添加 Peer
- 每次发送的结果
### 5.3 接收端测试
在另一台设备上运行接收示例(同样需要正确锁定信道),确认广播能被收到。
### 5.4 减少日志输出
稳定运行后,可将详细日志的触发间隔调大(如每 50 或 100 次打印一次),避免串口拥塞。
## 六、完整示例参考
### 6.1 最小发送示例
```python
# test_espnow.py - 最小发送示例
import wifi, espnow
# 信道锁定
channel = 6 # 或从 wifi.radio.ap_info.channel 获取
wifi.radio.enabled = True
wifi.radio.start_ap("test", "", channel=channel, max_connections=0)
wifi.radio.stop_ap()
# 初始化
e = espnow.ESPNow()
peer = espnow.Peer(b'\xff\xff\xff\xff\xff\xff', channel=channel)
e.peers.append(peer)
# 发送
msg = b'\x01\x00\x01\x02\x03\x07'
e.send(msg, peer)
print("Sent")
```
### 6.2 完整类示例
# test_espnow.py - ESP-NOW 测试脚本 (CircuitPython 版本,已修正)
import espnow
import wifi
print("[TEST] 激活WiFi硬件...")
wifi.radio.enabled = True
mac = wifi.radio.mac_address
mac_str = ":".join("{:02x}".format(b) for b in mac)
print(f"[TEST] 本机MAC: {mac_str}")
# 关键步骤1:获取并锁定信道
channel = 6
if wifi.radio.ap_info is not None:
channel = wifi.radio.ap_info.channel
print(f"[TEST] 检测到已连接WiFi,使用其信道: {channel}")
else:
print(f"[TEST] 未连接WiFi,使用默认信道: {channel}")
print("[TEST] 为ESP32-C3锁定ESP-NOW信道...")
wifi.radio.start_ap("espnow_temp", "", channel=channel, max_connections=0)
wifi.radio.stop_ap()
print("[TEST] 初始化ESP-NOW...")
e = espnow.ESPNow()
# 关键步骤2:添加广播对等端
broadcast_mac = b'\xff\xff\xff\xff\xff\xff'
peer = espnow.Peer(mac=broadcast_mac, channel=channel)
e.peers.append(peer)
print(f"[TEST] 已添加广播对等端: {broadcast_mac.hex()}, 信道: {channel}")
# 构造测试消息
test_message = b'\x01\x00\x01\x02\x03\x07'
print(f"[TEST] 发送消息 (hex): {test_message.hex()}")
# 关键步骤3:发送时传入peer参数
try:
e.send(test_message, peer)
print("[TEST] 消息发送成功!")
except Exception as err:
print(f"[TEST] 消息发送失败: {err}")

# espnow_message.py - ESP-NOW 广播消息发送(CircuitPython版本)
# 支持四种模拟数据:
# - F1线圈状态 (类型0x01)
# - F2离散量输入 (类型0x02)
# - F3保持寄存器 (类型0x03)
# - F4输入寄存器 (类型0x04)
# 报文格式:
# - F1/F2: 1字节类型 + 4字节数据 + 1字节校验和
# - F3/F4: 1字节类型 + 32字节数据 + 1字节校验和
import espnow
import time
import random
import struct
import wifi
# ESP-NOW 配置
BROADCAST_INTERVAL = 1000 # 发送间隔(ms)
BROADCAST_MAC = b'\xff\xff\xff\xff\xff\xff' # 广播地址
# 数据类型定义
TYPE_F1 = 0x01 # F1线圈状态 (Coils)
TYPE_F2 = 0x02 # F2离散量输入 (Discrete Inputs)
TYPE_F3 = 0x03 # F3保持寄存器 (Holding Registers - 4xxxx区)
TYPE_F4 = 0x04 # F4输入寄存器 (Input Registers - 3xxxx区)
class ESPNowMessage:
"""ESP-NOW 消息发送器(CircuitPython版本)"""
def __init__(self):
self.espnow = None
self.peer = None # 广播对等端对象
self.msg_count = 0
self.msg_count_f1 = 0
self.msg_count_f2 = 0
self.msg_count_f3 = 0
self.msg_count_f4 = 0
self.initialized = False
self.enabled = False
self.wifi_ready = False
self.current_type = TYPE_F1
self.start_delay_ms = 5000
self.start_time_ms = 0
self.mac_address = None
def init(self):
"""初始化ESP-NOW(CircuitPython规范)"""
if self.initialized:
return True
try:
print('[ESP-NOW] 初始化...')
# 1. 激活WiFi硬件
if not wifi.radio.enabled:
print('[ESP-NOW] 启用WiFi硬件...')
wifi.radio.enabled = True
print('[ESP-NOW] WiFi状态: 已激活')
# 2. 确定信道(优先使用已连接AP的信道)
channel = 6
if wifi.radio.ap_info is not None:
channel = wifi.radio.ap_info.channel
print(f'[ESP-NOW] 当前WiFi信道: {channel}')
else:
print(f'[ESP-NOW] 未连接WiFi,使用默认信道: {channel}')
# 3. 锁定信道(ESP32-C3必须通过临时AP设置)
print('[ESP-NOW] 锁定ESP-NOW信道...')
wifi.radio.start_ap("espnow_temp", "", channel=channel, max_connections=0)
wifi.radio.stop_ap()
# 4. 创建ESP-NOW实例并添加广播对等端
self.espnow = espnow.ESPNow()
self.peer = espnow.Peer(mac=BROADCAST_MAC, channel=channel)
self.espnow.peers.append(self.peer)
print(f'[ESP-NOW] 已添加广播对等端: {BROADCAST_MAC.hex()}, 信道: {channel}')
# 5. 获取本机MAC地址
mac_bytes = wifi.radio.mac_address
self.mac_address = ':'.join(f'{b:02x}' for b in mac_bytes)
print(f'[ESP-NOW] 本机MAC: {self.mac_address}')
self.initialized = True
print('[ESP-NOW] 初始化完成 - 广播模式')
print('[ESP-NOW] 报文格式:')
print('[ESP-NOW] - F1/F2: 1字节类型 + 4字节数据 + 1字节校验和')
print('[ESP-NOW] - F3/F4: 1字节类型 + 32字节数据 + 1字节校验和')
print('[ESP-NOW] 支持四种数据类型:')
print('[ESP-NOW] - 类型0x01: F1线圈状态 (Coils)')
print('[ESP-NOW] - 类型0x02: F2离散量输入 (Discrete Inputs)')
print('[ESP-NOW] - 类型0x03: F3保持寄存器 (Holding Registers - 4xxxx区)')
print('[ESP-NOW] - 类型0x04: F4输入寄存器 (Input Registers - 3xxxx区)')
return True
except Exception as e:
print(f'[ESP-NOW] 初始化失败: {e}')
self.espnow = None
self.peer = None
self.initialized = False
return False
def generate_random_32bit(self):
"""生成32位随机数(用于F1/F2)"""
return random.getrandbits(32)
def generate_registers(self):
"""生成32字节寄存器数据(16个16位寄存器)"""
data = bytearray()
for _ in range(16):
reg_value = random.getrandbits(16)
data.extend(struct.pack('>H', reg_value))
return bytes(data)
def calculate_checksum(self, data_bytes):
"""计算校验和(累加取低8位)"""
return sum(data_bytes) & 0xFF
def create_message(self, data_type=TYPE_F2):
"""创建消息包"""
type_byte = bytes([data_type])
if data_type in (TYPE_F3, TYPE_F4):
data_bytes = self.generate_registers()
value = data_bytes
else:
value = self.generate_random_32bit()
data_bytes = struct.pack('>I', value)
checksum = self.calculate_checksum(type_byte + data_bytes)
message = type_byte + data_bytes + bytes([checksum])
label_map = {TYPE_F1: "F1", TYPE_F2: "F2", TYPE_F3: "F3", TYPE_F4: "F4"}
return message, value, checksum, label_map[data_type]
def send_broadcast(self):
"""发送广播消息(循环F1→F2→F3→F4)"""
try:
if not self.initialized:
if not self.init():
return False
# 生成消息
message, value, checksum, type_label = self.create_message(self.current_type)
# 发送
try:
print(f'[ESP-NOW] 发送目标: {BROADCAST_MAC.hex()}')
print(f'[ESP-NOW] 消息长度: {len(message)} 字节')
print(f'[ESP-NOW] 消息内容: {message.hex()}')
self.espnow.send(message, self.peer) # 必须传入peer
print(f'[ESP-NOW] 发送成功 #{self.msg_count + 1}')
except Exception as e:
print(f'[ESP-NOW] 发送失败: {e}')
return False
self.msg_count += 1
# 计数
if self.current_type == TYPE_F1:
self.msg_count_f1 += 1
type_count = self.msg_count_f1
elif self.current_type == TYPE_F2:
self.msg_count_f2 += 1
type_count = self.msg_count_f2
elif self.current_type == TYPE_F3:
self.msg_count_f3 += 1
type_count = self.msg_count_f3
else:
self.msg_count_f4 += 1
type_count = self.msg_count_f4
# 每10次打印详细日志
if type_count % 10 == 0:
hex_str = ' '.join(f'{b:02x}' for b in message)
print('-' * 50)
print(f'[ESP-NOW] {type_label} #{type_count} (总 #{self.msg_count})')
if self.current_type in (TYPE_F3, TYPE_F4):
reg_label = "F3" if self.current_type == TYPE_F3 else "F4"
for i in range(16):
reg_value = struct.unpack('>H', value[2*i:2*i+2])[0]
print(f'[ESP-NOW] {reg_label}[{i}] 0x{reg_value:04X} ({reg_value})')
else:
bits_str = ''.join(f'{b:08b}' for b in message[1:5])
print(f'[ESP-NOW] 数据 0x{value:08X}')
print(f'[ESP-NOW] 二进制 {bits_str}')
print(f'[ESP-NOW] 报文 {hex_str}')
print(f'[ESP-NOW] MAC {self.mac_address}')
print('-' * 50)
# 切换数据类型
if self.current_type == TYPE_F1:
self.current_type = TYPE_F2
elif self.current_type == TYPE_F2:
self.current_type = TYPE_F3
elif self.current_type == TYPE_F3:
self.current_type = TYPE_F4
else:
self.current_type = TYPE_F1
return True
except Exception as e:
print(f'[ESP-NOW] 发送异常: {e}')
return False
def _check_start_condition(self):
"""检查是否满足发送条件(延迟启动)"""
try:
if not wifi.radio.enabled:
print('[ESP-NOW] WiFi硬件未激活')
return False
if not self.wifi_ready:
self.wifi_ready = True
print('[ESP-NOW] WiFi硬件已激活,准备发送')
except Exception as e:
print(f'[ESP-NOW] WiFi检查失败: {e}')
return False
if self.start_time_ms == 0:
self.start_time_ms = time.monotonic() * 1000
if (time.monotonic() * 1000 - self.start_time_ms) >= self.start_delay_ms:
if not self.enabled:
self.enabled = True
print('[ESP-NOW] 延迟启动完成,开始发送报文')
return True
return False
def task_callback(self):
"""调度器回调函数"""
if not self._check_start_condition():
return True
return self.send_broadcast()
def create_espnow_task(scheduler):
"""创建ESP-NOW发送任务"""
sender = ESPNowMessage()
sender.init() # 预初始化,避免运行时重复初始化
task = scheduler.add_task(
name='espnow',
interval_ms=BROADCAST_INTERVAL,
callback=sender.task_callback,
enabled=True
)
print(f'[ESP-NOW] 任务已注册 (间隔: {BROADCAST_INTERVAL}ms)')
return task
def register_tasks(scheduler):
"""注册ESP-NOW任务到调度器"""
create_espnow_task(scheduler)
## 七、参考资料
- **Adafruit Learn Guide**: ESP-NOW in CircuitPython
- **CircuitPython 文档**: espnow 模块
## 八、总结
**CircuitPython 的 ESP-NOW 更简洁,但必须严格遵守 信道锁定 + Peer 对象 + 发送传入 peer 的范式,切勿生搬 MicroPython 的用法。**
| 要点 | 说明 |
|------|------|
| **信道锁定** | ESP32-C3 上使用 ESP-NOW 的**关键步骤**,必须执行 `start_ap/stop_ap` |
| **Peer 对象** | 必须显式创建并添加到 `peers` 列表,不能直接使用 MAC 地址 |
| **发送参数** | `send(message, peer)`,**必须传入 peer 对象** |
| **初始化控制** | 使用标志位避免重复初始化,防止 "Already running" 错误 |
| **WiFi 连接** | ESP-NOW **不需要 WiFi 连接**,但需要 WiFi 硬件激活 |
更多推荐

所有评论(0)