Skip to content

Commit 98a7d04

Browse files
committed
openthread-br: locate the RCP dongle through otbr-rcp
The protocol handler required a hand-written radio_url, so pointing the border router at its dongle meant knowing which ttyACM the kernel had picked, and a replug that renumbered the device broke the interface until someone edited the config. Bring over the otbr-rcp wrapper from the matter-openwrt packaging: when no radio_url is set, the handler resolves the dongle by its USB properties (a cdc_acm interface on a device whose product string names OpenThread), with an rcp option to pin a bus position or a fixed serial device, and optional firmware install and update through handler plugins. Discovery runs in the setup phase rather than under the launched command, since a flash can take minutes and must not race the bounded wait for the agent's ubus object. Firmware install and update are off by default: flashing is an explicit act, and with the radio no longer required to be named, a configuration that mentions no RCP at all now reaches this path. The one value the handler needs is picked out of otbr-rcp's output rather than evaluated, since otbr-rcp sources every firmware handler plugin and stdout from one of those would otherwise run as root in the proto handler. A missing dongle fails setup with the restart block every failure needs, netifd retrying immediately and without backoff otherwise. The USB hotplug handler brings such interfaces up again when a device binds, keyed on the recorded RCP_NOT_FOUND error so interfaces an administrator took down on purpose stay down. An explicit radio_url keeps today's behaviour. Assisted-By: Claude Fable 5 Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
1 parent 233cfaa commit 98a7d04

5 files changed

Lines changed: 471 additions & 9 deletions

File tree

‎net/openthread-br/Makefile‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
66
PKG_NAME:=openthread-br
77
PKG_VERSION:=2026.08.0
88
PKG_SOURCE_VERSION:=v$(PKG_VERSION)
9-
PKG_RELEASE:=1
9+
PKG_RELEASE:=2
1010

1111
PKG_SOURCE_PROTO:=git
1212
PKG_SOURCE_URL=https://github.com/openthread/ot-br-posix.git
@@ -91,10 +91,13 @@ endef
9191

9292
define Package/openthread-br/install
9393
$(INSTALL_DIR) \
94+
$(1)/etc/hotplug.d/usb \
9495
$(1)/etc/init.d \
9596
$(1)/lib/netifd/proto \
9697
$(1)/usr/sbin
9798
$(INSTALL_BIN) ./files/openthread-proto.sh $(1)/lib/netifd/proto/openthread.sh
99+
$(INSTALL_BIN) ./files/otbr-rcp $(1)/usr/sbin
100+
$(INSTALL_DATA) ./files/otbr-rcp.hotplug $(1)/etc/hotplug.d/usb/50-otbr-rcp
98101
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/* $(1)/usr/sbin
99102
endef
100103

‎net/openthread-br/README.md‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,57 @@ config interface 'thread'
126126
option device 'wpan0'
127127
option proto 'openthread'
128128
option backbone_network 'lan'
129-
option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800'
129+
option rcp '2-1'
130+
option uart_baudrate '460800'
130131
list prefix 'fd6f:5772:5468:7200::/64 paros'
131132
option verbose '0'
132133
```
133134

134-
Only backbone_network, device and radio_url are required; the protocol handler
135-
fails the interface if one of them is missing, or if backbone_network names an
136-
interface that has no device. Everything else — dataset, prefix, verbose,
135+
Only backbone_network and device are required. A radio need not be named at
136+
all: with neither rcp nor radio_url set the handler behaves as if `rcp 'any'`
137+
were given and auto-discovers a dongle, failing the interface with
138+
RCP_NOT_FOUND only when none is found. It also fails if backbone_network names
139+
an interface that has no device. Everything else — dataset, prefix, verbose,
137140
rest_listen_address and rest_listen_port — is optional. See
138141
[REST Server](#rest-server) before moving the REST API off the loopback
139-
default. If something isn't working, check ifstatus for the OpenThread
142+
default.
143+
144+
### Finding the RCP
145+
146+
The radio is named with the `rcp` option rather than a full radio URL. It takes
147+
one of three forms:
148+
149+
| value | meaning |
150+
| --- | --- |
151+
| `/dev/ttyACM0` | a fixed serial device; no discovery is done |
152+
| `2-1` | a USB bus position, resolved to whatever serial device it currently exposes |
153+
| `any` (the default) | pick a dongle automatically |
154+
155+
`uart_baudrate` and `uart_flow_control` are appended to the resulting URL, and
156+
the port is always opened exclusively. `uart_baudrate` is unset by default,
157+
leaving the port at otbr-agent's own default; `uart_flow_control` defaults to
158+
1, so hardware flow control is on unless it is set to 0. Turn it off for a
159+
3-wire UART, or for a dongle that never asserts CTS. Setting `radio_url`
160+
directly still works and overrides all of this.
161+
162+
Discovery resolves USB dongles, which requires the cdc_acm driver: only an
163+
interface bound to it is accepted, and the `/dev/ttyACM*` node does not exist
164+
without it. Install `kmod-usb-acm` if it is not already present. A
165+
UART-attached radio named through `/dev/tty*` or `radio_url` needs none of
166+
this.
167+
168+
Prefer a bus position to `any` unless the dongle advertises itself. Unattended
169+
selection only accepts a device whose USB product string contains the word
170+
"OpenThread", which many dongles — the Home Assistant Connect ZBT-2 among them
171+
— do not. Naming the bus position is the operator saying "this one is the RCP",
172+
so no product string is needed. `ls /sys/bus/usb/devices/` shows the positions.
173+
174+
`otbr-rcp` also has a plugin point for installing or updating dongle firmware,
175+
used when `rcp_firmware_update` is set. It is off by default, and no handlers
176+
ship with this package, so nothing is flashed unless you both add a handler and
177+
ask for it.
178+
179+
If something isn't working, check ifstatus for the OpenThread
140180
interface:
141181

142182
```
@@ -234,7 +274,8 @@ config interface 'thread'
234274
option backbone_network 'lan'
235275
option dataset '0e080000000000010000000300000f35060004001fffe0020836b86cd9746ab3080708fd9850cbe719b1d205101f11a11320828c7a6ebc2f2e675c0dca030e686f6d652d617373697374616e740102716f041025804ed78614258ebedf4e2db37b3b6e0c0402a0f7f8'
236276
list prefix 'fd6f:5772:5468:7200::/64 paros'
237-
option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800'
277+
option rcp '2-1'
278+
option uart_baudrate '460800'
238279
option verbose '0'
239280
```
240281

‎net/openthread-br/files/openthread-proto.sh‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
OTCTL="/usr/sbin/ot-ctl"
77
PROG="/usr/sbin/otbr-agent"
8+
RCP_PROG="/usr/sbin/otbr-rcp"
89

910
[ -x "$PROG" ] || exit 0
1011

@@ -27,6 +28,10 @@ proto_openthread_init_config() {
2728
proto_config_add_string backbone_network
2829
proto_config_add_string dataset
2930
proto_config_add_string radio_url
31+
proto_config_add_string rcp
32+
proto_config_add_boolean rcp_firmware_update
33+
proto_config_add_int uart_baudrate
34+
proto_config_add_boolean uart_flow_control
3035
proto_config_add_string rest_listen_address
3136
proto_config_add_int rest_listen_port
3237

@@ -44,21 +49,60 @@ proto_openthread_setup_error() {
4449
exit 1
4550
}
4651

52+
proto_openthread_setup_retry() {
53+
# A missing RCP dongle is not a configuration error, but the interface
54+
# must still be blocked: netifd re-runs a failed setup immediately and
55+
# without backoff, which would busy-loop until a dongle appears. The
56+
# hotplug handler's ifup lifts the block, so recovery is unaffected;
57+
# this helper differs from proto_openthread_setup_error only in intent.
58+
proto_openthread_setup_error "$@"
59+
}
60+
4761
proto_openthread_setup() {
4862
interface="$1"
4963
device="$2"
5064

5165
mkdir -p /var/lib/thread
5266

53-
json_get_vars backbone_network dataset device radio_url rest_listen_address rest_listen_port verbose:0
67+
json_get_vars backbone_network dataset device radio_url rcp \
68+
rcp_firmware_update:0 uart_baudrate:0 uart_flow_control:1 \
69+
rest_listen_address rest_listen_port verbose:0
5470

5571
[ -n "$backbone_network" ] || proto_openthread_setup_error "$interface" MISSING_BACKBONE_NETWORK
5672
proto_add_host_dependency "$interface" "" "$backbone_network"
5773
network_get_device backbone_ifname "$backbone_network"
5874

5975
[ -n "$backbone_ifname" ] || proto_openthread_setup_error "$interface" MISSING_BACKBONE_IFNAME
6076
[ -n "$device" ] || proto_openthread_setup_error "$interface" MISSING_DEVICE
61-
[ -n "$radio_url" ] || proto_openthread_setup_error "$interface" MISSING_RADIO_URL
77+
if [ -z "$radio_url" ]; then
78+
case "$rcp" in
79+
/dev/*)
80+
# A fixed serial device needs no discovery.
81+
radio_url="spinel+hdlc+uart://$rcp"
82+
;;
83+
*)
84+
# Let otbr-rcp locate the dongle by its USB properties and,
85+
# when a handler knows how, install or update its firmware.
86+
# This runs here rather than under the launched command: a
87+
# flash can take minutes, and it must not race the bounded
88+
# wait for the agent's ubus object below.
89+
#
90+
# Pick the one value we need out of the output rather than
91+
# evaluating it: otbr-rcp sources every firmware handler in
92+
# /usr/share/openthread-rcp/, and a handler that prints to
93+
# stdout would otherwise have its output run as root here.
94+
RCPTTY="$("$RCP_PROG" \
95+
$([ "$rcp_firmware_update" -eq 0 ] || echo --update) \
96+
"${rcp:-any}" | sed -n 's/^RCPTTY=//p')"
97+
[ -n "$RCPTTY" ] || \
98+
proto_openthread_setup_retry "$interface" RCP_NOT_FOUND
99+
radio_url="spinel+hdlc+uart://$RCPTTY"
100+
;;
101+
esac
102+
radio_url="${radio_url}?uart-exclusive"
103+
[ "$uart_baudrate" -eq 0 ] || radio_url="${radio_url}&uart-baudrate=${uart_baudrate}"
104+
[ "$uart_flow_control" -eq 0 ] || radio_url="${radio_url}&uart-flow-control"
105+
fi
62106

63107
opts="--auto-attach=0"
64108
[ "$verbose" -eq 0 ] || append opts -v

0 commit comments

Comments
 (0)