ESP32-C6 SuperMini当OpenThread边界路由

去年买了两台Panther X2,今年又买了一台OES Plus正好把几个2.5硬盘给整上,折腾了一段时间终于理顺,记录下。

分工

一台Panther X2当外部流量入口:

  1. frpc将nginx端口映射到VPS,外网ipv4可通过VPS访问内部网部;
  2. cron定时脚本更新cloudflare dns解析域名到ipv6地址,dns解析到ipv4至VPS,同一个域名都可以访问对外暴露的服务;
  3. cupsd服务,wireguard/tailscale连接后,在外可以连接另一个VLAN的打印机给小朋友打印作业;
  4. adguardhome服务,在路由器RouterOS中设置NetWatch,服务正常时使用此服务,服务不正常时使用外部DNS服务;
  5. zigbee2mqtt/mqtt/homeassistant服务,HA主要控制中心;
  6. openthread/border-router服务,用一个ESP32-C6当RCP,可做为一个Thread路由节点。

另一台Panther X2当安装了带NPU的Armbian

  1. openthread/border-router服务,另一个ZBT-1当RCP,做一个Thread路由节点;
  2. frigate服务,连接家里两个摄像头,录制视频通过NFS写入到OES Plus的硬盘中。其中莹石的一个摄像头需要把音频给禁掉(在RSTP流地址后面加上#audio=0),否则APP对讲机无法使用。

OESPlus性能和存储都较Panther X2强,有一些大的服务在上面跑:

  1. hermes-agent/docker,通过飞书或电报可以远程安装其干活;
  2. 两个navidrome容器,一个听歌,一个听书;
  3. qbittorrent/qbittorrent-ee,下载一些视频什么的;
  4. esphome,用业OTA更新固件,固件编译使用NAS或笔记本当后台build server;
  5. hass-otbr-docker/python-matter-server,用来当Thread主边界路由;
  6. 一个11年的老Intel 80G SSD当系统存储,另外两个机械盘做为下载与共享,通过NFS提供文件共享。

Panther X2有32G的eMMC,OESPlus只有8G,AI找了一些如何减少emmc写入的注意事项,尽量减少日志的写入,能不写日志的就不写日志,HomeAssistant只记录一些需要的数据(recorder只记录一些温度等数据),docker容器产生的一些临时文件存tmpfs,journal也写tmpfs等等。

ESP-IDF环境安装

之前安装ESP-IDF老是走GH,速度慢得很,后来发现官方有个EIM,可以走国内镜像,速度快不少,下载地址

也可以自己设置变量,也可以给ESPHOME编译提高速度。其他pypi镜像可以用aliyun或高校的。

1
2
3
4
5
6
7
export ESPHOME_IDF_FRAMEWORK_MIRRORS="\
https://dl.espressif.cn/github_assets/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;\
https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip"
export IDF_GITHUB_ASSETS="dl.espressif.cn/github_assets"
export ESP_IDF_CONSTRAINTS_MIRRORS="\
https://my.local.mirror/espidf.constraints.v{VERSION}.txt;\
https://dl.espressif.com/dl/esp-idf/espidf.constraints.v{VERSION}.txt"

ESP32-C6当OpenThread RCP

A Low-Cost Thread Border Router: ESP32-C6-WROOM as an OpenThread RCP for Home Assistant in Docker

Make your own Thread Border Router for just $5

就是ESP32-C6当RCP,通过USB与Docker的OTBR程序通信,可以做为一个Thread边界路由使用。${IDF_PATH}/examples/openthread/ot_rcp进行配置编译上传。

docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
otbr:
image: openthread/border-router
restart: always
volumes:
- ./data:/data
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
environment:
- TZ=Asia/Shanghai
env_file: otbr-env.list
cap_add:
- SYS_ADMIN
- NET_ADMIN
network_mode: host
devices:
- /dev/net/tun
- /dev/ttyACM0:/dev/ttyACM0
# 映射文件有问题,需要使用long context
# - /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_XX:XX:XX:XX:XX:XX-if00:/dev/ttyACM0
otbr-env.list
1
2
3
4
5
6
7
8
9
10
11
12
OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800
OT_INFRA_IF=end1
OT_THREAD_IF=wpan0
OT_LOG_LEVEL=1

OT_REST_LISTEN_ADDR=0.0.0.0
OT_REST_LISTEN_PORT=8081
OT_WEB_LISTEN_ADDR=0.0.0.0
OT_WEB_LISTEN_PORT=7586

NDP_PROXY=1
RA=1

开启转发,或者用脚本curl -sSL https://raw.githubusercontent.com/openthread/ot-br-posix/refs/heads/main/etc/docker/border-router/setup-host | bash

1
2
3
4
5
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv4.conf.all.forwarding = 1
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.accept_ra_rt_info_max_plen = 64
net.ipv6.conf.all.accept_ra = 2

另外有一个

docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
services:
otbr:
container_name: otbr
image: ownbee/hass-otbr-docker:latest
restart: unless-stopped
privileged: true
network_mode: host
cap_add:
- SYS_ADMIN
- NET_ADMIN
environment:
DEVICE: "/dev/ttyUSB0"
BACKBONE_IF: eth0
FLOW_CONTROL: 1
FIREWALL: 1
NAT64: 1
BAUDRATE: 460800
OTBR_REST_PORT: 8081
OTBR_WEB_PORT: 7586
OTBR_WEB: 1
AUTOFLASH_FIRMWARE: 0
OTBR_LOG_LEVEL: error
TZ: Asia/Shanghai
devices:
- /dev/ttyACM0:/dev/ttyUSB0
# - /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_XX:XX:XX:XX:XX:XX-if00:/dev/ttyUSB0
- /dev/net/tun:/dev/net/tun
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
- ./otbr_data:/data/thread
logging:
driver: "journald"


matter-server:
image: ghcr.nju.edu.cn/home-assistant-libs/python-matter-server:stable
container_name: matter-server
restart: unless-stopped
network_mode: host
privileged: true
security_opt:
- apparmor:unconfined
volumes:
- ./matter_data:/data/
- /run/dbus:/run/dbus:ro
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
logging:
driver: "journald"
command: >
--storage-path /data
--paa-root-cert-dir /data/credentials
--bluetooth-adapter 0
--log-level-sdk progress
--primary-interface eth0

双芯片方案

官方推荐的方案是ESP32 S3 + ESP32 H2,之前H2已经被我用到其他地方去了,S3在角落里面懒得找了。ESP32 C6手边上倒是还有4个,就想用两个ESP32 C6来试试。

嘉立创开源广场有位网友做了一个MINI边界路由器

现在只想快速验证可行性,没有画PCB板,直接用面包板接线,最后发现是可行的,只是也走了一些弯路。想法是一个C6当Host,UART连接另一个C6 RCP,然后WIFI连接主网络,与Home Assistant通信;另一个C6只当RCP,UART与Host通信。

最早是用IDF自带例子ot_brot_rcp,两个都刷成功,通过idf.py monitor开启WIFI,设置OpenThread网络信息,可以成功连接到网络,但是在固件里面开启自动启动WIFI与OpenThread时会报错,而且还没有HTTP服务,不能在HomeAssistant中增加Open Thread Border Router

后来换了官方推荐里面的仓库,填入WIFI信息,OpenThread信息,。。。报错。。。其中有两个花了不少时间来处理,一个是分区大小,仓库示例里面需要的存储是8M,但我的C6 SuperMini只有4M,这个搜索了不少时间,把分区大小改掉,然后不用OTA与RCP_UPDATE,最终解决。

partitions.csv
1
2
3
4
5
6
# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 2M,
web_storage,data, spiffs, , 512K,

另外一个是固件刷成功后,启动后无限重启,好像是Host与RCP通信错误,把错误信息丢给AI,没有解决。。。过了好长时间,试着把TX/RX引脚号对掉,问题解决。。。后来查了下代码,发现是我理解错了。配的TX是PIN_TO_RCP_RX,RX是PIN_TO_RCP_TX。。。。。

esp_ot_config.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG()              \
{ \
.radio_mode = RADIO_MODE_UART_RCP, \
.radio_uart_config = { \
.port = 0, \
.uart_config = \
{ \
.baud_rate = 460800, \
.data_bits = UART_DATA_8_BITS, \
.parity = UART_PARITY_DISABLE, \
.stop_bits = UART_STOP_BITS_1, \
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, \
.rx_flow_ctrl_thresh = 0, \
.source_clk = UART_SCLK_DEFAULT, \
}, \
.rx_pin = CONFIG_PIN_TO_RCP_TX, \
.tx_pin = CONFIG_PIN_TO_RCP_RX, \
}, \
}
#else

当然我只是验证方案是否可行,这个方案还不稳定,毕竟WIFI网络是有延迟或干扰问题。后续如果再自己做的话,可能打板走RJ45网串口吧。另外C6当RCP时,有一点发热,主频降到80MHZ,稍微有一点效果,但不是特别明显。