From 83be2ddde105f7b132979394cf788fb90ca18339 Mon Sep 17 00:00:00 2001 From: Karsten Sperling Date: Thu, 29 Jan 2026 22:11:48 +1300 Subject: [PATCH 1/4] Mark host-only packages as build-only --- devel/gn/Makefile | 3 ++- devel/python3-host-ssl/Makefile | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/devel/gn/Makefile b/devel/gn/Makefile index 3a7eb35..3f14226 100644 --- a/devel/gn/Makefile +++ b/devel/gn/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2023 Project CHIP Authors +# Copyright (c) 2023-2026 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ define Package/gn CATEGORY:=Development TITLE:=gn URL:=https://gn.googlesource.com/gn + BUILDONLY:=1 endef define Package/gn/description diff --git a/devel/python3-host-ssl/Makefile b/devel/python3-host-ssl/Makefile index a8c6c87..ec9b5ab 100644 --- a/devel/python3-host-ssl/Makefile +++ b/devel/python3-host-ssl/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2025 Project CHIP Authors +# Copyright (c) 2025-2026 Project CHIP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ define Package/python3-host-ssl SECTION:=lang CATEGORY:=Languages TITLE:=Python3 host package SSL configuration + BUILDONLY:=1 endef define Package/python3-host-ssl/description From 74af21d33d94501359edb1d381cf31c4a1fc92ca Mon Sep 17 00:00:00 2001 From: Karsten Sperling Date: Thu, 29 Jan 2026 22:12:14 +1300 Subject: [PATCH 2/4] Add an OpenThread RCP firmware package based on openthread/ot-nrf528xx This initial version targets the NRF528408 MDK dongle because the UF2 bootloader makes it very easy to flash with minimal tooling. Also add an otbr-rcp script for finding / managing USB RCPs and use it to add hot plugging support to the otbr-agent service. Miscellaneous improvements: - Avoid clobbering /etc/config/otbr-agent - Add default configuration for a 'thread' firewall zone - Minor patches to otbr-agent to improve RCP device handling --- .config.ci | 1 + devel/arm-gnu-rm-toolchain/Makefile | 75 ++++ third_party/openthread-br/Makefile | 21 +- .../openthread-br/files/otbr-agent.config | 2 + .../openthread-br/files/otbr-agent.defaults | 38 ++ .../openthread-br/files/otbr-agent.init | 74 ++++ third_party/openthread-br/files/otbr-rcp | 333 ++++++++++++++++++ .../openthread-br/files/otbr-rcp.hotplug | 20 ++ .../patches/010-init-script.patch | 129 ------- .../patches/040-uart-exclusive.patch | 58 +++ .../openthread-br/patches/041-rcp-eof.patch | 23 ++ .../patches/042-settings-ifname.patch | 38 ++ third_party/openthread-rcp-nrf528xx/Makefile | 99 ++++++ .../files/nrf52840-mdk.sh | 95 +++++ .../openthread-rcp-nrf528xx/overrides.cmake | 28 ++ .../patches/010-uf2-bootloader.patch | 123 +++++++ .../patches/020-status-led.patch | 205 +++++++++++ tools/uf2-utils/Makefile | 41 +++ tools/uf2-utils/uf2.c | 175 +++++++++ 19 files changed, 1439 insertions(+), 139 deletions(-) create mode 100644 devel/arm-gnu-rm-toolchain/Makefile create mode 100644 third_party/openthread-br/files/otbr-agent.config create mode 100644 third_party/openthread-br/files/otbr-agent.defaults create mode 100644 third_party/openthread-br/files/otbr-agent.init create mode 100644 third_party/openthread-br/files/otbr-rcp create mode 100644 third_party/openthread-br/files/otbr-rcp.hotplug delete mode 100644 third_party/openthread-br/patches/010-init-script.patch create mode 100644 third_party/openthread-br/patches/040-uart-exclusive.patch create mode 100644 third_party/openthread-br/patches/041-rcp-eof.patch create mode 100644 third_party/openthread-br/patches/042-settings-ifname.patch create mode 100644 third_party/openthread-rcp-nrf528xx/Makefile create mode 100644 third_party/openthread-rcp-nrf528xx/files/nrf52840-mdk.sh create mode 100644 third_party/openthread-rcp-nrf528xx/overrides.cmake create mode 100644 third_party/openthread-rcp-nrf528xx/patches/010-uf2-bootloader.patch create mode 100644 third_party/openthread-rcp-nrf528xx/patches/020-status-led.patch create mode 100644 tools/uf2-utils/Makefile create mode 100644 tools/uf2-utils/uf2.c diff --git a/.config.ci b/.config.ci index 96291c8..26af7a3 100644 --- a/.config.ci +++ b/.config.ci @@ -6,3 +6,4 @@ CONFIG_PACKAGE_matter-netman-mbedtls=m CONFIG_PACKAGE_matter-netman-openssl=m CONFIG_PACKAGE_mdnsresponder=m CONFIG_PACKAGE_openthread-br=m +CONFIG_PACKAGE_openthread-rcp-nrf52840-mdk=m diff --git a/devel/arm-gnu-rm-toolchain/Makefile b/devel/arm-gnu-rm-toolchain/Makefile new file mode 100644 index 0000000..5dc6012 --- /dev/null +++ b/devel/arm-gnu-rm-toolchain/Makefile @@ -0,0 +1,75 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=arm-gnu-rm-toolchain +UPSTREAM_VERSION:=10.3-2021.10 +PKG_VERSION:=$(subst -,.,$(UPSTREAM_VERSION)) + +PrebuiltTarball=gcc-arm-none-eabi-$(UPSTREAM_VERSION)-$(1).tar.bz2 +define Download/prebuilt + URL:=https://developer.arm.com/-/media/files/downloads/gnu-rm/$(UPSTREAM_VERSION)/ + FILE:=$(call PrebuiltTarball,$(1)) +endef +define Download/x86_64-linux + $(call Download/prebuilt,x86_64-linux) + MD5SUM:=2383e4eb4ea23f248d33adc70dc3227e +endef +define Download/aarch64-linux + $(call Download/prebuilt,aarch64-linux) + MD5SUM:=3fe3d8bb693bd0a6e4615b6569443d0d +endef +define Download/mac + $(call Download/prebuilt,mac) + MD5SUM:=7f2a7b7b23797302a9d6182c6e482449 +endef + +PKG_LICENSE:=GPL-3.0-or-later LGPL-3.0-or-later MIT Zlib +PKG_LICENSE_FILES:=license.txt + +PKG_HOST_ONLY:=1 + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/host-build.mk + +define Package/arm-gnu-rm-toolchain + SECTION:=devel + CATEGORY:=Development + TITLE:=arm-gnu-rm-toolchain + URL:=https://developer.arm.com/downloads/-/gnu-rm + BUILDONLY:=1 +endef + +define Package/arm-gnu-rm-toolchain/description + The GNU Arm Embedded Toolchain for 32-bit Arm Cortex-A, Arm Cortex-M, and Arm Cortex-R processor families. +endef + +PREBUILT_FLAVOR:=$(if $(filter Darwin,$(HOST_OS)),mac,$(HOST_ARCH)-$(call tolower,$(HOST_OS))) +PREBUILT_TARBALL:=$(DL_DIR)/$(call PrebuiltTarball,$(PREBUILT_FLAVOR)) +$(if $(strip $(Download/$(PREBUILT_FLAVOR))),$(eval $(call Download,$(PREBUILT_FLAVOR)))) + +Host/Prepare:=$(empty) +Host/Configure:=$(empty) + +define Host/Compile + test -f $(PREBUILT_TARBALL) || { echo "ERROR: Unsupported host platform '$(PREBUILT_FLAVOR)'"; false; } +endef + +define Host/Install + bzcat $(PREBUILT_TARBALL) | $(HOST_TAR) -C $(1) --strip-components 1 -xf - +endef + +$(eval $(call BuildPackage,arm-gnu-rm-toolchain)) +$(eval $(call HostBuild)) diff --git a/third_party/openthread-br/Makefile b/third_party/openthread-br/Makefile index 8d85b26..60d4237 100644 --- a/third_party/openthread-br/Makefile +++ b/third_party/openthread-br/Makefile @@ -29,7 +29,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=openthread-br -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_URL:=https://github.com/openthread/ot-br-posix.git PKG_SOURCE_PROTO:=git-with-metadata PKG_SOURCE_MIRROR:=0 # don't try OpenWrt mirror @@ -119,7 +119,7 @@ endef # Disable firewall integration due to https://github.com/openthread/ot-br-posix/issues/1675 CMAKE_OPTIONS+= \ - -DOTBR_GIT_VERSION=$(call GitWithMetadata/resolve,OTBR_GIT_VERSION) \ + -DOTBR_GIT_VERSION=$(call GitWithMetadata/resolve,OTBR_GIT_VERSION)$(if $(PKG_RELEASE),-r$(PKG_RELEASE)) \ -DOT_PACKAGE_VERSION=$(call GitWithMetadata/resolve,OT_GIT_VERSION) \ -DCMAKE_BUILD_TYPE=$(if $(CONFIG_DEBUG),Debug,Release) \ -DCMAKE_INSTALL_PREFIX=/usr \ @@ -134,9 +134,10 @@ CMAKE_OPTIONS+= \ -DOT_POSIX_SETTINGS_PATH=\"/etc/openthread\" \ -DOT_READLINE=OFF -# OpenWrt uses /var/run instead of /run TARGET_CFLAGS+= \ -DOPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME=\\\"/var/run/openthread-%s\\\" \ + -DOPENTHREAD_POSIX_CONFIG_SETTINGS_USE_INTERFACE_NAME=1 \ + -DOPENTHREAD_POSIX_CONFIG_TMP_STORAGE_ENABLE=0 \ -DOPENTHREAD_CONFIG_LOG_PREPEND_UPTIME=0 \ -DOPENTHREAD_CONFIG_LOG_PREPEND_LEVEL=0 @@ -151,18 +152,18 @@ CMAKE_OPTIONS+= -DOTBR_MDNS=mDNSResponder endif define Package/openthread-br/install - $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config $(1)/etc/uci-defaults $(1)/etc/hotplug.d/usb $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/otbr-agent $(1)/usr/sbin $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/ot-ctl $(1)/usr/sbin - - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_BIN) $(PKG_INSTALL_DIR)/etc/init.d/* $(1)/etc/init.d - - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_CONF) $(PKG_INSTALL_DIR)/etc/config/* $(1)/etc/config + $(INSTALL_BIN) ./files/otbr-rcp $(1)/usr/sbin + $(INSTALL_BIN) ./files/otbr-agent.init $(1)/etc/init.d/otbr-agent + $(INSTALL_CONF) ./files/otbr-agent.config $(1)/etc/config/otbr-agent + $(INSTALL_DATA) ./files/otbr-agent.defaults $(1)/etc/uci-defaults/95-otbr-agent + $(INSTALL_DATA) ./files/otbr-rcp.hotplug $(1)/etc/hotplug.d/usb/50-otbr-rcp endef define Package/openthread-br/conffiles +/etc/config/otbr-agent /etc/openthread/ endef diff --git a/third_party/openthread-br/files/otbr-agent.config b/third_party/openthread-br/files/otbr-agent.config new file mode 100644 index 0000000..e04a6cc --- /dev/null +++ b/third_party/openthread-br/files/otbr-agent.config @@ -0,0 +1,2 @@ +config otbr-agent 'wpan0' + option infra_if_name 'br-lan' diff --git a/third_party/openthread-br/files/otbr-agent.defaults b/third_party/openthread-br/files/otbr-agent.defaults new file mode 100644 index 0000000..9f75309 --- /dev/null +++ b/third_party/openthread-br/files/otbr-agent.defaults @@ -0,0 +1,38 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# If the firewall is configured and the default otbr-agent instance +# exists, create a thread zone with forwarding to and from lan. +uci -q get firewall >/dev/null \ + && [ "$(uci -q get otbr-agent.wpan0)" = "otbr-agent" ] \ + && ! uci -q get otbr-agent.wpan0.thread_if_name >/dev/null \ + && ! uci -q show firewall | grep -q "^firewall\.@zone\[[0-9]*\]\.name='thread'" \ + && uci batch </dev/null +add firewall zone +set firewall.@zone[-1].name='thread' +set firewall.@zone[-1].input='ACCEPT' +set firewall.@zone[-1].output='ACCEPT' +set firewall.@zone[-1].forward='ACCEPT' +set firewall.@zone[-1].family='ipv6' +set firewall.@zone[-1].device='wpan0' +add firewall forwarding +set firewall.@forwarding[-1].src='thread' +set firewall.@forwarding[-1].dest='lan' +add firewall forwarding +set firewall.@forwarding[-1].src='lan' +set firewall.@forwarding[-1].dest='thread' +commit firewall +EOF + +exit 0 diff --git a/third_party/openthread-br/files/otbr-agent.init b/third_party/openthread-br/files/otbr-agent.init new file mode 100644 index 0000000..800720d --- /dev/null +++ b/third_party/openthread-br/files/otbr-agent.init @@ -0,0 +1,74 @@ +#!/bin/sh /etc/rc.common + +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +START=90 + +USE_PROCD=1 +AGENT_PROG=/usr/sbin/otbr-agent +RCP_PROG=/usr/sbin/otbr-rcp + +validate_section_otbr() +{ + uci_load_validate otbr-agent otbr-agent "$1" "$2" \ + 'thread_if_name:string' \ + 'infra_if_name:string' \ + 'uart_device:string:any' \ + 'uart_baudrate:uinteger:0' \ + 'uart_flow_control:bool:1' \ + 'rcp_hotplug:bool:1' \ + 'rcp_firmware_update:bool:1' +} + +otbr_instance() +{ + local cfg="$1" + if [ "$2" != 0 ]; then + echo "validation failed" + return 1 + fi + if [ -z "$infra_if_name" ]; then + echo "missing infra_if_name" + return 1 + fi + + local radio_url + if [ "${uart_device#/dev/}" != "${uart_device}" ]; then + # Directly run otbr-agent with the specified /dev/* device + set -- "$AGENT_PROG" + radio_url="spinel+hdlc+uart://${uart_device}" + else + # Use the otbr-rcp wrapper to locate the device + set -- "$RCP_PROG" + [ "$rcp_hotplug" -eq 0 ] || set -- "$@" --wait + [ "$rcp_firmware_update" -eq 0 ] || set -- "$@" --update + set -- "$@" "$uart_device" -- + radio_url="%rcpurl%" + fi + radio_url="${radio_url}?uart-exclusive" + [ "$uart_baudrate" -eq 0 ] || radio_url="${radio_url}&uart-baudrate=${uart_baudrate}" + [ "$uart_flow_control" -eq 0 ] || radio_url="${radio_url}&uart-flow-control" + + procd_open_instance + procd_set_param command "$@" -I "${thread_if_name:-$cfg}" -B "$infra_if_name" "${radio_url}" "trel://${infra_if_name}" + procd_set_param respawn + procd_close_instance +} + +start_service() +{ + config_load otbr-agent + config_foreach validate_section_otbr otbr-agent otbr_instance +} diff --git a/third_party/openthread-br/files/otbr-rcp b/third_party/openthread-br/files/otbr-rcp new file mode 100644 index 0000000..b49f138 --- /dev/null +++ b/third_party/openthread-br/files/otbr-rcp @@ -0,0 +1,333 @@ +#!/bin/sh + +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# otbr-rcp: Wrapper script for otbr-agent to manage an RCP dongle + +# RCP interfaces are recognized using the following USB properties: +# - interface has INTERFACE == 2/2/0 and DRIVER == cdc_acm +# - device product string contains the word "OpenThread" +# Additional an RCP selector string can be specified. This can be +# - "any" (the default) matches any eligible device +# - -

, e.g. "1-1.2.3" matches a specific bus/port path +# If the --wait option is given, the script will wait for a +# suitable device to be hot-plugged if necessary. +# +# If the --update option is given and suitable firmware handler +# plugins are available, the RCP firmware will be auto-updated +# or auto-installed. The latter is only performed if the USB device +# has been manually placed into boot loader mode. + +usage() { + echo "Usage: $0 [OPTIONS...] [RCP] [-- AGENT-ARGS...]" >&2 + exit 2 +} + +RC_NOT_FOUND=3 +RC_NOT_SUPPORTED=4 +RC_INTERNAL=7 +RC_BREAK=63 + +AGENT_PROG=/usr/sbin/otbr-agent + +main() { + # Parse options / arguments + local wait= update= + while [ $# -gt 0 ]; do + case "$1" in + --) break;; + --wait) wait=1; shift;; + --update) update=1; shift;; + -*) echo "Unrecognized option: ${1#-}" >&2; usage;; + *) break;; + esac + done + + local rcp=any + if [ $# -gt 0 -a "$1" != "--" ]; then + rcp="$1" + validate_rcp_selector "$rcp" || usage + shift + fi + + local launch= + if [ $# -gt 0 ]; then + [ "$1" = "--" -a $# -gt 1 ] || usage + launch=1 + shift + fi + + # Find and/or wait for the RCP device + [ -z "$update" ] || load_firmware_handlers + if [ -n "$wait" ]; then + log debug "Looking for RCP devices matching selector '$rcp'" + hotplug_wait find_rcp "$rcp" "$update" + elif ! find_rcp "$rcp" "$update"; then + log error "No RCP device found for selector '$rcp'" + return "$RC_NOT_FOUND" + fi + local rcpdev="${REPLY% *}" installhid="${REPLY#* }" + + # Install firmware if necessary + if [ -n "$update" -a "$installhid" != "-" ]; then + install_rcp "$rcpdev" "$installhid" || return $? + rcpdev="${REPLY% *}" update= + fi + + # Find the corresponding TTY + if ! usb_wait_devnode "$rcpdev" tty ttyACM; then + log error "Unable to resolve TTY for USB device $rcpdev" + return "$RC_INTERNAL" + fi + local rcptty="$REPLY" + local rcpurl="spinel+hdlc+uart://$rcptty" + + # Update firmware if requested, supported, and necessary + if [ -n "$update" ]; then + update_rcp "${rcpdev%:*}" "$rcptty" "$rcpurl" || return $? + fi + + # Exec otbr-agent if requested + if [ -n "$launch" ]; then + local arg + for arg in "$@"; do + shift + set -- "$@" "${arg//%rcpurl%/$rcpurl}" + done + log debug "Executing $AGENT_PROG $*" + exec "$AGENT_PROG" "$@" + else + echo "RCPDEV=$rcpdev" + echo "RCPTTY=$rcptty" + return 0 + fi +} + +validate_rcp_selector() { # selector + case "$1" in + any) return 0;; + *-*) return 0;; # USB DEVICENAME / -, e.g. "1-1", "3-2.1.1" + esac + return 1 +} + +find_rcp() { # selector [include-installable] => $REPLY + local rcpdev= installdev= installhid= + usb_device_foreach _find_rcp_cb "$@" + if [ $? -eq "$RC_BREAK" -a -n "$rcpdev" ]; then + log debug "Found RCP interface $rcpdev for selector '$1'" + REPLY="$rcpdev -" + return 0 + fi + if [ -n "$2" -a -n "$installdev" -a -n "$installhid" ]; then + log debug "Found installable $installhid device $installdev for selector '$1'" + REPLY="$installdev $installhid" + return 0 + fi + return "$RC_NOT_FOUND" +} +_find_rcp_cb() { # selector [include-installable] + # Must match the selector (if any) + case "$1" in + *-*) [ "$1" = "$DEVICENAME" ] || return 0 + esac + + # Check for a usable cdc_acm interface and OpenThread product string + usb_interface_foreach "" _find_rcp_if_cb + [ $? -eq "$RC_BREAK" -a -n "$rcpdev" ] \ + && usb_read_property '' product && REPLY=" $REPLY " && [ "${REPLY/ OpenThread /}" != "$REPLY" ] \ + && return "$RC_BREAK" + + # If requested, check if we could install RCP firmware on this device + if [ -n "$2" -a -n "$RCPFWHANDLERS" -a -z "$installdev" ]; then + local hid + for hid in $RCPFWHANDLERS; do + call_firmware_handler "$hid" installable "$DEVICENAME" && installdev="$DEVICENAME" installhid="$hid" && break + done + fi +} +_find_rcp_if_cb() { + usb_read_property '' INTERFACE && [ "$REPLY" = 2/2/0 ] || return 0 + usb_read_property '' DRIVER && [ "$REPLY" = cdc_acm ] || return 0 + rcpdev="$DEVICENAME" + return "$RC_BREAK" +} + +update_rcp() { # rcpdev rcptty rcpurl + local rcpdev="$1" rcptty="$2" rcpurl="$3" + [ -n "$RCPFWHANDLERS" ] || return 0 + + # Use uart-exclusive to ensure we don't interfere with a running otbr-agent + log debug "Determining current firmware for USB device $rcpdev ($rcptty)" + local currentfw="$("$AGENT_PROG" --radio-version "${rcpurl}?uart-exclusive" 2>/dev/null)" + if [ -z "$currentfw" ]; then + log notice "Not updating USB device $rcpdev, unable to determine current firmware version" + return 0 + fi + log debug " -> $currentfw" + + local hid rc + for hid in $RCPFWHANDLERS; do + call_firmware_handler "$hid" update "$@" "$currentfw"; rc=$? + if [ "$rc" -eq "$RC_NOT_SUPPORTED" ]; then + continue + elif [ "$rc" -ne 0 ]; then + log error "Failed to update $hid device $rcpdev" + return "$rc" + fi + return 0 + done + log notice "Not updating USB device $1, not supported by any firmware handler" +} + +install_rcp() { # rcpdev hid => $REPLY + local rcpdev="$1" hid="$2" retry + log notice "Attempting to install RCP firmware on $hid device $rcpdev" + if ! call_firmware_handler "$hid" install "$rcpdev"; then + log error "Failed to install firmware on USB device $rcpdev" + return "$RC_INTERNAL" + fi + for retry in 2 1 0; do + find_rcp "$rcpdev" && return 0 + [ "$retry" -gt 0 ] && sleep 1 + done + log error "Failed to find RCP device after firmware install" + return "$RC_INTERNAL" +} + +usb_device_foreach() { # callback ... (with $DEVICENAME) + local _dev DEVICENAME + for _dev in /sys/bus/usb/devices/*; do + DEVICENAME="${_dev##*/}" # e.g. "1-1" + [ "${DEVICENAME%:*}" = "$DEVICENAME" -a -f "$_dev/bDeviceClass" ] || continue + "$@"; [ $? -ne "$RC_BREAK" ] || return "$RC_BREAK" + done +} + +usb_interface_foreach() { # devicename callback ... (with $DEVICENAME) + local _dn="${1:-$DEVICENAME}" _if DEVICENAME; shift + for _if in "/sys/bus/usb/devices/$_dn/$_dn:"*; do + [ -d "$_if" -a -f "$_if/bInterfaceClass" ] || continue + DEVICENAME="${_if##*/}" # e.g. "1-1:1.0" + "$@"; [ $? -ne "$RC_BREAK" ] || return "$RC_BREAK" + done +} + +usb_read_property() { # devicename property => $REPLY + local dev="/sys/bus/usb/devices/${1:-$DEVICENAME}" u v w + REPLY= + case "$2" in + # Provide some processed / composite values that align with hotplug environment variables + DEVPATH) v="$(readlink -f "$dev" 2>/dev/null)" && REPLY="${v#/sys}";; # e.g. "/devices/platform/1e1c0000.xhci/usb1/1-1" + DRIVER) v="$(readlink "$dev/driver" 2>/dev/null)" && REPLY="${v##*/}";; # e.g. "cdc_acm" + INTERFACE) usb_read_property "$1" bInterfaceClass && u="0x0$REPLY" \ + && usb_read_property "$1" bInterfaceSubClass && v="0x0$REPLY" \ + && usb_read_property "$1" bInterfaceProtocol && w="0x0$REPLY" \ + && REPLY="$(printf '%d/%d/%d' "$u" "$v" "$w")";; # e.g. "2/2/0" + TYPE) usb_read_property "$1" bDeviceClass && u="0x0$REPLY" \ + && usb_read_property "$1" bDeviceSubClass && v="0x0$REPLY" \ + && usb_read_property "$1" bDeviceProtocol && w="0x0$REPLY" \ + && REPLY="$(printf '%d/%d/%d' "$u" "$v" "$w")";; # e.g. "239/2/1" + PRODUCT) usb_read_property "$1" idVendor && u="0x0$REPLY" \ + && usb_read_property "$1" idProduct && v="0x0$REPLY" \ + && usb_read_property "$1" bcdDevice && w="0x0$REPLY" \ + && REPLY="$(printf '%x/%x/%x' "$u" "$v" "$w")";; # e.g. "e8d/7612/100" + *) [ -r "$dev/$2" ] && read -r <"$dev/$2" 2>/dev/null;; + esac +} + +usb_find_devnode() { # devicename class [prefix] => $REPLY + local devpath node path + usb_read_property "$1" DEVPATH && devpath="$REPLY" || return $? + for node in "/sys/class/$2/$3"*; do + path="$(readlink "$node" 2>/dev/null)" && [ "${path/$devpath/}" != "$path" ] || continue + REPLY="/dev/${node##*/}" && [ -e "$REPLY" ] && return 0 + done + REPLY= + return "$RC_NOT_FOUND" +} + +usb_wait_devnode() { # devicename class [prefix] => $REPLY + local retry + for retry in 2 1 0; do + usb_find_devnode "$@" && return 0 + [ "$retry" -gt 0 ] && sleep 1 + done + return "$RC_NOT_FOUND" +} + +hotplug_wait() { # callback ... + # Note: This function MUST NOT be called from a sub-shell! + # Create a fifo that acts as a flag file for the otbr-rcp hotplug handler. + # A fifo is used so we can block interruptibly by reading from it. The fifo + # is not actually written to by the hotplug handler, since this would require + # logic to avoid hanging / blocking if this listener goes away. Instead, the + # hotplug handler directly sends an ALRM signal to wake up listeners. + local sig ignored fifo="/var/run/otbr-rcp.hotplug.$$" + trap "rm -f '$fifo' 2>/dev/null" 0 + trap 'sig=1' ALRM + mkfifo -m 0400 "$fifo" || return $? + exec 9<>"$fifo" + while true; do + # reset before polling + sig=; "$@" && break + log debug "Waiting for ALRM signal from otbr-rcp hotplug handler" + read -r ignored <&9 2>/dev/null + done + exec 9>&- + rm "$fifo" + trap '' ALRM # ignore further / later signals + trap - 0 +} + +load_firmware_handlers() { # => $RCPFWHANDLERS + local _script _hname _hid _type + RCPFWHANDLERS= + for _script in /usr/share/openthread-rcp/*.sh; do + [ -f "$_script" -a -r "$_script" ] || continue + _hname="${_script##*/}" + _hid="${_hname%.sh}"; _hid="${_hid//-/_}" + if [ -n "${_hid//[a-z0-9_]}" ]; then + log warning "Not loading firmware handler '$_hname', invalid file name" + continue + fi + log debug "Loading firmware handler '$_hname'" + . "$_script" + _type="$(type "rcp_fw_$_hid")" + if [ "${_type/function}" = "$_type" ]; then + log warning "Ignoring firmware handler '$_hname', missing entry point" + continue + fi + RCPFWHANDLERS="${RCPFWHANDLERS}${RCPFWHANDLERS:+ }${_hid}" + done + [ -n "$RCPFWHANDLERS" ] || log debug "No firmware handlers available" +} + +call_firmware_handler() { # hid args... + local hid="$1"; shift + "rcp_fw_$hid" "$@" +} + +log() { # {debug|info|notice|warning|error|...} message + logger -t otbr-rcp -p "$1" "$2" + ! [ -t 2 ] || echo "$2" >&2 +} + +# Force signal handlers to respect an EXIT (0) trap +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +main "$@" diff --git a/third_party/openthread-br/files/otbr-rcp.hotplug b/third_party/openthread-br/files/otbr-rcp.hotplug new file mode 100644 index 0000000..d7b9ef2 --- /dev/null +++ b/third_party/openthread-br/files/otbr-rcp.hotplug @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ "$ACTION" = bind ]; then + for listener in /var/run/otbr-rcp.hotplug.*; do + pid="${listener#/var/run/otbr-rcp.hotplug.}" + [ "$pid" != "*" ] && kill -ALRM "$pid" + done +fi diff --git a/third_party/openthread-br/patches/010-init-script.patch b/third_party/openthread-br/patches/010-init-script.patch deleted file mode 100644 index cfc8f22..0000000 --- a/third_party/openthread-br/patches/010-init-script.patch +++ /dev/null @@ -1,129 +0,0 @@ -commit a36f28e4618013c0c942169eaf6b097f1a0721d6 -Author: Karsten Sperling -Date: Fri Oct 10 13:29:43 2025 +1300 - - OpenWrt init script: Make uart-flow-control configurable - - Also tidy up the way the configuration is handled. - -diff --git a/etc/openwrt/openthread-br/README.md b/etc/openwrt/openthread-br/README.md -index a99d5bf643..6d03772b25 100644 ---- a/etc/openwrt/openthread-br/README.md -+++ b/etc/openwrt/openthread-br/README.md -@@ -65,7 +65,7 @@ Start otbr-agent manually: - /usr/sbin/otbr-agent -I wpan0 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800' - ``` - --Edit the service file `/etc/init.d/otbr-agent` if RCP device is not `/dev/ttyACM0` and then start with: -+Edit the service file `/etc/config/otbr-agent` if RCP device is not `/dev/ttyACM0` and then start with: - - ```bash - service otbr-agent start -@@ -80,7 +80,7 @@ If you need to change the thread network interface (`wpan0` by default), you nee - ```bash - service otbr-firewall stop - service otbr-agent stop --uci set otbr-agent.service.thread_if_name=wpan1 -+uci rename otbr-agent.wpan0=wpan1 - uci commit otbr-agent - service otbr-firewall start - service otbr-agent start -diff --git a/src/openwrt/CMakeLists.txt b/src/openwrt/CMakeLists.txt -index 8237f38b42..52f90306a9 100644 ---- a/src/openwrt/CMakeLists.txt -+++ b/src/openwrt/CMakeLists.txt -@@ -28,19 +28,19 @@ - - add_subdirectory(ubus) - --configure_file(otbr-agent.init.in otbr-agent.init) -+configure_file(otbr-agent.init.in otbr-agent.init @ONLY) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/otbr-agent.init - DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/init.d - RENAME otbr-agent) - --configure_file(otbr-agent.uci-config.in otbr-agent.uci-config) -+configure_file(otbr-agent.uci-config.in otbr-agent.uci-config @ONLY) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/otbr-agent.uci-config - DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/config - RENAME otbr-agent) - - - if(OT_FIREWALL) -- configure_file(otbr-firewall.init.in otbr-firewall.init) -+ configure_file(otbr-firewall.init.in otbr-firewall.init @ONLY) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/otbr-firewall.init - DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/init.d - RENAME otbr-firewall) -diff --git a/src/openwrt/otbr-agent.init.in b/src/openwrt/otbr-agent.init.in -index 46505ff434..43f1999cd6 100755 ---- a/src/openwrt/otbr-agent.init.in -+++ b/src/openwrt/otbr-agent.init.in -@@ -30,15 +30,46 @@ - START=90 - - USE_PROCD=1 -+PROG=@CMAKE_INSTALL_FULL_SBINDIR@/otbr-agent - --start_service() -+validate_section_otbr() - { -- local uci_thread_if_name=$(uci -q get otbr-agent.service.thread_if_name) -- local uci_infra_if_name=$(uci -q get otbr-agent.service.infra_if_name) -- local uci_uart_device=$(uci -q get otbr-agent.service.uart_device) -- local uci_uart_baudrate=$(uci -q get otbr-agent.service.uart_baudrate) -+ uci_load_validate otbr-agent otbr-agent "$1" "$2" \ -+ 'thread_if_name:string' \ -+ 'infra_if_name:string' \ -+ 'uart_device:string' \ -+ 'uart_baudrate:uinteger:460800' \ -+ 'uart_flow_control:bool:1' -+} -+ -+otbr_instance() -+{ -+ local cfg="$1" -+ if [ "$2" != 0 ]; then -+ echo "validation failed" -+ return 1 -+ fi -+ if [ -z "$infra_if_name" ]; then -+ echo "missing infra_if_name" -+ return 1 -+ fi -+ if [ -z "$uart_device" ]; then -+ echo "missing uart_device" -+ return 1 -+ fi -+ -+ local radio_url="spinel+hdlc+uart://${uart_device}?uart-baudrate=${uart_baudrate}" -+ [ "$uart_flow_control" = 0 ] || radio_url="${radio_url}&uart-flow-control" -+ [ -n "$thread_if_name" ] || thread_if_name="$cfg" - - procd_open_instance -- procd_set_param command @CMAKE_INSTALL_FULL_SBINDIR@/otbr-agent -I $uci_thread_if_name -B $uci_infra_if_name spinel+hdlc+uart://$uci_uart_device?uart-baudrate=$uci_uart_baudrate trel://$uci_infra_if_name -+ procd_set_param command "$PROG" -I "$thread_if_name" -B "$infra_if_name" "$radio_url" "trel://${infra_if_name}" -+ procd_set_param respawn - procd_close_instance - } -+ -+start_service() -+{ -+ config_load otbr-agent -+ config_foreach validate_section_otbr otbr-agent otbr_instance -+} -diff --git a/src/openwrt/otbr-agent.uci-config.in b/src/openwrt/otbr-agent.uci-config.in -index eb18eda2a3..252ee18df9 100644 ---- a/src/openwrt/otbr-agent.uci-config.in -+++ b/src/openwrt/otbr-agent.uci-config.in -@@ -1,5 +1,5 @@ --config otbr-agent 'service' -- option thread_if_name "wpan0" -- option infra_if_name "eth0" -- option uart_device "/dev/ttyACM0" -- option uart_baudrate 460800 -+config otbr-agent 'wpan0' -+ option infra_if_name '@OTBR_INFRA_IF_NAME@' -+ option uart_device '/dev/ttyACM0' -+ option uart_baudrate '460800' -+ option uart_flow_control '1' diff --git a/third_party/openthread-br/patches/040-uart-exclusive.patch b/third_party/openthread-br/patches/040-uart-exclusive.patch new file mode 100644 index 0000000..71d4819 --- /dev/null +++ b/third_party/openthread-br/patches/040-uart-exclusive.patch @@ -0,0 +1,58 @@ +commit 796d3ed23771a922b7185af1516c9c2a5f1527cf +Author: Karsten Sperling +Date: Thu Feb 5 10:09:50 2026 +1300 + + [posix] Add uart-exclusive option to enable flock / TIOCEXCL + +diff --git a/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp b/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp +index afe0842ef..bfedc9858 100644 +--- a/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp ++++ b/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp +@@ -49,6 +49,7 @@ + #endif + #include + #include ++#include + #include + #include + #include +@@ -459,6 +460,27 @@ int HdlcInterface::OpenFile(const Url::Url &aRadioUrl) + ExitNow(); + } + ++ if (aRadioUrl.HasParam("uart-exclusive")) ++ { ++ // Lock the device early to prevent concurrent access ++ if (flock(fd, LOCK_EX | LOCK_NB) == -1) ++ { ++ // LogCrit("flock failed: %s (device may already be in use)", strerror(errno)); ++ perror("flock uart failed, device already in use"); ++ close(fd); ++ fd = -1; ++ ExitNow(); ++ } ++ ++#ifdef TIOCEXCL ++ // Set exclusive access mode if supported by the platform ++ if (ioctl(fd, TIOCEXCL) == -1) ++ { ++ LogWarn("ioctl(TIOCEXCL) failed: %s", strerror(errno)); ++ } ++#endif ++ } ++ + if (isatty(fd)) + { + struct termios tios; +diff --git a/third_party/openthread/repo/src/posix/platform/radio_url.cpp b/third_party/openthread/repo/src/posix/platform/radio_url.cpp +index 0a0cf73a6..7d6653e00 100644 +--- a/third_party/openthread/repo/src/posix/platform/radio_url.cpp ++++ b/third_party/openthread/repo/src/posix/platform/radio_url.cpp +@@ -85,6 +85,7 @@ const char *otSysGetRadioUrlHelpString(void) + " uart-flow-control Enable flow control, disabled by default.\n" \ + " uart-init-deassert Deassert lines on init when flow control is disabled.\n" \ + " uart-reset Reset connection after hard resetting RCP(USB CDC ACM).\n" \ ++ " uart-exclusive Lock uart device using flock / TIOCEXCL.\n" \ + "\n" + #else + #define OT_SPINEL_HDLC_RADIO_URL_HELP_BUS diff --git a/third_party/openthread-br/patches/041-rcp-eof.patch b/third_party/openthread-br/patches/041-rcp-eof.patch new file mode 100644 index 0000000..fe93e65 --- /dev/null +++ b/third_party/openthread-br/patches/041-rcp-eof.patch @@ -0,0 +1,23 @@ +commit 42f28873fb6dfaa8e655eb342f1f299de44cf2f3 +Author: Karsten Sperling +Date: Tue Feb 10 17:25:52 2026 +1300 + + [posix] Handle RCP disconnection (EOF from read()) + +diff --git a/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp b/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp +index bfedc9858..2df744533 100644 +--- a/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp ++++ b/third_party/openthread/repo/src/posix/platform/hdlc_interface.cpp +@@ -202,7 +202,11 @@ void HdlcInterface::Read(void) + { + Decode(buffer, static_cast(rval)); + } +- else if ((rval < 0) && (errno != EAGAIN) && (errno != EINTR)) ++ else if (rval == 0) ++ { ++ DieNowWithMessage("RCP device disconnected (EOF)", OT_EXIT_FAILURE); ++ } ++ else if ((errno != EAGAIN) && (errno != EINTR)) + { + DieNow(OT_EXIT_ERROR_ERRNO); + } diff --git a/third_party/openthread-br/patches/042-settings-ifname.patch b/third_party/openthread-br/patches/042-settings-ifname.patch new file mode 100644 index 0000000..847a129 --- /dev/null +++ b/third_party/openthread-br/patches/042-settings-ifname.patch @@ -0,0 +1,38 @@ +commit 2fc05d1b37a7cbe9b66e422d5bba58a048d8be62 +Author: Karsten Sperling +Date: Tue Feb 10 16:03:00 2026 +1300 + + [posix] Optionally name the posix settings file based on the interface + + ... instead of the default _ naming. This allows the + settings file to remain stable when replacing the RCP dongle. + +diff --git a/third_party/openthread/repo/src/posix/platform/settings.cpp b/third_party/openthread/repo/src/posix/platform/settings.cpp +index 8faef601d..132df3808 100644 +--- a/third_party/openthread/repo/src/posix/platform/settings.cpp ++++ b/third_party/openthread/repo/src/posix/platform/settings.cpp +@@ -84,13 +84,22 @@ static otError settingsFileInit(otInstance *aInstance) + { + static constexpr size_t kMaxFileBaseNameSize = 32; + char fileBaseName[kMaxFileBaseNameSize]; +- const char *offset = getenv("PORT_OFFSET"); +- uint64_t nodeId; + ++#if defined(OPENTHREAD_POSIX_CONFIG_SETTINGS_USE_INTERFACE_NAME) && OPENTHREAD_POSIX_CONFIG_SETTINGS_USE_INTERFACE_NAME ++ // Use interface name as the settings file base name ++ OT_UNUSED_VARIABLE(aInstance); ++ const char *netifName = otSysGetThreadNetifName(); ++ VerifyOrDie(netifName != nullptr && strlen(netifName) > 0, OT_EXIT_FAILURE); ++ snprintf(fileBaseName, sizeof(fileBaseName), "%s", netifName); ++#else ++ // Use port offset and EUI-64 as the settings file base name ++ const char *offset = getenv("PORT_OFFSET"); ++ uint64_t nodeId; + otPlatRadioGetIeeeEui64(aInstance, reinterpret_cast(&nodeId)); + nodeId = ot::BigEndian::HostSwap64(nodeId); + + snprintf(fileBaseName, sizeof(fileBaseName), "%s_%" PRIx64, offset == nullptr ? "0" : offset, nodeId); ++#endif + VerifyOrDie(strlen(fileBaseName) < kMaxFileBaseNameSize, OT_EXIT_FAILURE); + + return sSettingsFile.Init(fileBaseName); diff --git a/third_party/openthread-rcp-nrf528xx/Makefile b/third_party/openthread-rcp-nrf528xx/Makefile new file mode 100644 index 0000000..b367587 --- /dev/null +++ b/third_party/openthread-rcp-nrf528xx/Makefile @@ -0,0 +1,99 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=openthread-rcp-nrf528xx +PKG_RELEASE:=1 +PKG_SOURCE_URL:=https://github.com/openthread/ot-nrf528xx.git +PKG_SOURCE_PROTO:=git-with-metadata +PKG_SOURCE_MIRROR:=0 # don't try OpenWrt mirror + +# Note: The commit picked for this package should be aligned with that of the +# openthread-br package so that the underlying OpenThread version matches. +PKG_SOURCE_DATE:=2025-10-28 +PKG_SOURCE_VERSION:=580f7bc7b432ee98c0767fbca13563c18f0144e0 +PKG_MIRROR_HASH:=2f1e8283a7de7191851041106cec87e832f314d7f486e193c6b013bcdcdbe8fb + +PKG_LICENSE:=BSD-3-Clause +PKG_LICENSE_FILES:=LICENSE + +PKG_BUILD_DEPENDS:=arm-gnu-rm-toolchain/host +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk +include ../../include/git-with-metadata.mk + +define GitWithMetadata/gather +echo -n OT_GIT_VERSION= && git -C openthread describe --dirty --always +endef + +define Package/openthread-rcp-nrf52840-mdk + SECTION:=net + CATEGORY:=Network + TITLE:=OpenThread RCP firmware for NRF52840-MDK + URL:=https://github.com/openthread/ot-nrf528xx + VARIANT:=nrf52840-mdk + DEPENDS:=+uf2-utils + PKGARCH:=all +endef + +define Package/openthread-rcp-nrf52840-mdk/description + This variant targets the Makerdiary nRF52840 MDK USB Dongle +endef + +BUILD_BINDIR:=$(PKG_BUILD_DIR)$(if $(USE_SOURCE_DIR),/build) +BUILD_PLATFORM:=$(word 1,$(subst -, ,$(BUILD_VARIANT))) +BUILD_PLATFORM_INFO:=$(call toupper,$(BUILD_VARIANT)) +BUILD_OPTIONS:=\ + -DCMAKE_TOOLCHAIN_FILE=src/$(BUILD_PLATFORM)/arm-none-eabi.cmake \ + -DCMAKE_PROJECT_ot-nrf528xx_INCLUDE=$(CURDIR)/overrides.cmake \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DNRF_PLATFORM=$(BUILD_PLATFORM) \ + -DOT_PLATFORM=external \ + -DOT_EXTERNAL_MBEDTLS=nordicsemi-mbedtls \ + -DOT_PACKAGE_VERSION=$(call GitWithMetadata/resolve,OT_GIT_VERSION)$(if $(PKG_RELEASE),-r$(PKG_RELEASE)) \ + -DOVR_OT_PLATFORM_INFO="$(BUILD_PLATFORM_INFO)" \ + -DOT_SLAAC=ON \ + -DOT_USB=ON + +ifeq ($(BUILD_VARIANT),nrf52840-mdk) + BUILD_OPTIONS+= \ + -DOVR_USBD_PRODUCT_NAME="nRF52840-MDK OpenThread Device" \ + -DOT_BOOTLOADER=UF2 \ + -DOT_STATUS_LED_PINS="(0,23),(0,22),(0,24)" \ + -DOVR_USBD_BUS_POWERED=ON +endif + +define Build/Configure + mkdir -p $(BUILD_BINDIR) && cd $(BUILD_BINDIR) && \ + rm -f CMakeCache.txt && \ + cmake -GNinja $(BUILD_OPTIONS) $(PKG_BUILD_DIR) +endef + +define Build/Compile + +$(NINJA) -C $(BUILD_BINDIR) bin/ot-rcp + arm-none-eabi-objcopy -O binary $(BUILD_BINDIR)/bin/ot-rcp $(BUILD_BINDIR)/bin/ot-rcp.bin + arm-none-eabi-strings $(BUILD_BINDIR)/bin/ot-rcp | grep ^OPENTHREAD/ >$(BUILD_BINDIR)/bin/ot-rcp.version \ + && grep -Fq "; $(BUILD_PLATFORM_INFO);" $(BUILD_BINDIR)/bin/ot-rcp.version +endef + +define Package/openthread-rcp-nrf52840-mdk/install + $(INSTALL_DIR) $(1)/usr/share/openthread-rcp + $(INSTALL_DATA) $(BUILD_BINDIR)/bin/ot-rcp.bin $(1)/usr/share/openthread-rcp/$(BUILD_VARIANT).bin + $(INSTALL_DATA) $(BUILD_BINDIR)/bin/ot-rcp.version $(1)/usr/share/openthread-rcp/$(BUILD_VARIANT).version + $(INSTALL_DATA) ./files/$(BUILD_VARIANT).sh $(1)/usr/share/openthread-rcp/$(BUILD_VARIANT).sh +endef + +$(eval $(call BuildPackage,openthread-rcp-nrf52840-mdk)) diff --git a/third_party/openthread-rcp-nrf528xx/files/nrf52840-mdk.sh b/third_party/openthread-rcp-nrf528xx/files/nrf52840-mdk.sh new file mode 100644 index 0000000..418c587 --- /dev/null +++ b/third_party/openthread-rcp-nrf528xx/files/nrf52840-mdk.sh @@ -0,0 +1,95 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +rcp_fw_nrf52840_mdk() { + local PREFIX=/usr/share/openthread-rcp/nrf52840-mdk + local verb="$1"; shift + case "$verb" in + update) rcp_fw_nrf52840_mdk_update "$@";; + installable) rcp_fw_nrf52840_mdk_installable "$@";; + install) rcp_fw_nrf52840_mdk_install "$@";; + *) return "$RC_INTERNAL";; + esac +} + +rcp_fw_nrf52840_mdk_update() { # devicename tty url currentfw + local dev="$1" tty="$2" currentfw="$4" + [ "${currentfw/; NRF52840-MDK;/}" != "$currentfw" ] || return "$RC_NOT_SUPPORTED" + + local version + [ -r "${PREFIX}.version" ] && read -r version <"${PREFIX}.version" && [ -n "$version" ] || return "$RC_INTERNAL" + if [ "$currentfw" = "$version" ]; then + log debug "Firmware of USB device $dev is up to date" + return 0 + fi + + # Sanity check that the binary we're about to flash matches the version we think it has + [ -r "${PREFIX}.bin" ] && grep -qF "$version" "${PREFIX}.bin" || return "$RC_INTERNAL" + log notice "Attempting firmware update of nrf52840_mdk device $dev" + log notice " -> $version" + + rcp_fw_nrf52840_mdk_bootloader "$dev" "$tty" || return $? + rcp_fw_nrf52840_mdk_install "$dev" +} + +rcp_fw_nrf52840_mdk_bootloader() { # devicename tty + local dev="$1" tty="$2" retry + for retry in 2 1 0; do + log debug "Triggering reset into UF2 boot loader" + uf2 reset "$tty" + sleep 1 + usb_read_property "$dev" TYPE && [ "$REPLY" = 239/2/1 ] && return 0 + done + log error "Failed to trigger UF2 boot loader on USB device $dev" + return "$RC_INTERNAL" +} + +rcp_fw_nrf52840_mdk_installable() { # devicename + # The vendor and product ids can't be used easily because they vary depending on the boot loader version + usb_read_property "$1" TYPE && [ "$REPLY" = 239/2/1 ] || return "$RC_NOT_SUPPORTED" + usb_read_property "$1" product && REPLY="${REPLY// /-}" && [ "${REPLY#nRF52840-MDK-}" != "$REPLY" ] || return "$RC_NOT_SUPPORTED" + [ -r "${PREFIX}.bin" ] || return "$RC_INTERNAL" + return 0 +} + +rcp_fw_nrf52840_mdk_install() { # devicename + local dev="$1" + + if ! usb_wait_devnode "$dev" block sd; then + log error "Failed to find UF2 block device for USB device $dev" + return "$RC_INTERNAL" + fi + local blockdev="$REPLY" + + local mnt="/tmp/rcp-uf2.$$" + local cleanup="{ umount -f '$mnt'; rmdir '$mnt'; } 2>/dev/null" + trap "$cleanup" 0; cleanup="trap - 0; $cleanup" # otbr-rcp ensures HUP/INT/TERM trigger exit + log debug "Mounting UF2 block device $blockdev" + if ! mkdir -p -m 0600 "$mnt" || ! mount -t vfat -o noatime,shortname=win95,umask=0077 "$blockdev" "$mnt"; then + log error "Failed to mount UF2 block device $blockdev for USB device $dev" + eval "$cleanup"; return "$RC_INTERNAL" + fi + + local info="$mnt/INFO_UF2.TXT" + if ! [ -r "$info" ] || ! read -r info <"$info"; then + log error "Failed to read UF2 boot loader information from $blockdev for USB device $dev" + eval "$cleanup"; return "$RC_INTERNAL" + fi + log debug " -> $info" + + log notice "Flashing USB device $dev via UF2 boot loader on $blockdev" + uf2 convert 0xADA52840 0x1000 "${PREFIX}.bin" "$mnt/FLASH.UF2" # exit status may not be reliable + eval "$cleanup" + return 0 +} diff --git a/third_party/openthread-rcp-nrf528xx/overrides.cmake b/third_party/openthread-rcp-nrf528xx/overrides.cmake new file mode 100644 index 0000000..0230251 --- /dev/null +++ b/third_party/openthread-rcp-nrf528xx/overrides.cmake @@ -0,0 +1,28 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set (OVR_OT_PLATFORM_INFO "" CACHE STRING "OpenThread Platform Name") +if(NOT "${OVR_OT_PLATFORM_INFO}" STREQUAL "") + list(APPEND OT_PLATFORM_DEFINES "OPENTHREAD_CONFIG_PLATFORM_INFO=\"${OVR_OT_PLATFORM_INFO}\"") +endif() + +set(OVR_USBD_PRODUCT_NAME "" CACHE STRING "USB Product Name") +if(NOT "${OVR_USBD_PRODUCT_NAME}" STREQUAL "") + list(APPEND OT_PLATFORM_DEFINES "APP_USBD_STRINGS_PRODUCT=APP_USBD_STRING_DESC(\"${OVR_USBD_PRODUCT_NAME}\")") +endif() + +set(OVR_USBD_BUS_POWERED OFF CACHE STRING "USB device is bus-powered") +if(OVR_USBD_BUS_POWERED) + list(APPEND OT_PLATFORM_DEFINES "APP_USBD_CONFIG_SELF_POWERED=0") +endif() diff --git a/third_party/openthread-rcp-nrf528xx/patches/010-uf2-bootloader.patch b/third_party/openthread-rcp-nrf528xx/patches/010-uf2-bootloader.patch new file mode 100644 index 0000000..6a037af --- /dev/null +++ b/third_party/openthread-rcp-nrf528xx/patches/010-uf2-bootloader.patch @@ -0,0 +1,123 @@ +commit 5fa897db7870c86047ee9d29021434180c05f171 +Author: Karsten Sperling +Date: Wed Jan 28 16:58:58 2026 +1300 + + Add support for UF2 boot loader with "1200 baud touch" reset + +diff --git a/src/nrf52840/nrf52840.cmake b/src/nrf52840/nrf52840.cmake +index 85226e1..ad68d48 100644 +--- a/src/nrf52840/nrf52840.cmake ++++ b/src/nrf52840/nrf52840.cmake +@@ -30,6 +30,9 @@ option(OT_BOOTLOADER "OT nrf bootloader type") + if(OT_BOOTLOADER STREQUAL "USB") + list(APPEND OT_PLATFORM_DEFINES "APP_USBD_NRF_DFU_TRIGGER_ENABLED=1") + set(LD_FILE "${CMAKE_CURRENT_SOURCE_DIR}/nrf52840/nrf52840_bootloader_usb.ld") ++elseif(OT_BOOTLOADER STREQUAL "UF2") ++ list(APPEND OT_PLATFORM_DEFINES "UF2_BOOTLOADER_ENABLED=1") ++ set(LD_FILE "${CMAKE_CURRENT_SOURCE_DIR}/nrf52840/nrf52840_bootloader_usb.ld") + elseif(OT_BOOTLOADER STREQUAL "UART") + set(LD_FILE "${CMAKE_CURRENT_SOURCE_DIR}/nrf52840/nrf52840_bootloader_uart.ld") + elseif(OT_BOOTLOADER STREQUAL "BLE") +@@ -37,6 +40,9 @@ elseif(OT_BOOTLOADER STREQUAL "BLE") + else() + set(LD_FILE "${CMAKE_CURRENT_SOURCE_DIR}/nrf52840/nrf52840.ld") + endif() ++if(NOT OT_BOOTLOADER STREQUAL "UF2") ++ list(APPEND OT_PLATFORM_DEFINES "CONFIG_GPIO_AS_PINRESET") ++endif() + + list(APPEND OT_PLATFORM_DEFINES + "OPENTHREAD_CORE_CONFIG_PLATFORM_CHECK_FILE=\"openthread-core-nrf52840-config-check.h\"" +@@ -66,7 +72,6 @@ list(APPEND OT_PUBLIC_INCLUDES "${PROJECT_SOURCE_DIR}/third_party/NordicSemicond + set(OT_PUBLIC_INCLUDES ${OT_PUBLIC_INCLUDES} PARENT_SCOPE) + + set(COMM_FLAGS +- -DCONFIG_GPIO_AS_PINRESET + -DNRF52840_XXAA + -DUSE_APP_CONFIG=1 + -Wno-unused-parameter +diff --git a/src/src/transport/usb-cdc-uart.c b/src/src/transport/usb-cdc-uart.c +index 5bcb091..7640a3b 100644 +--- a/src/src/transport/usb-cdc-uart.c ++++ b/src/src/transport/usb-cdc-uart.c +@@ -60,6 +60,10 @@ + #include "nrf_drv_power.h" + #include "class/cdc/acm/app_usbd_cdc_acm.h" + ++#if defined(UF2_BOOTLOADER_ENABLED) && UF2_BOOTLOADER_ENABLED ++#include "hal/nrf_power.h" ++#endif ++ + #if (USB_CDC_AS_SERIAL_TRANSPORT == 1) + + static void cdcAcmUserEventHandler(app_usbd_class_inst_t const *aInstance, app_usbd_cdc_acm_user_event_t aEvent); +@@ -260,6 +264,30 @@ static void processTransmit(void) + } + } + ++#if defined(UF2_BOOTLOADER_ENABLED) && UF2_BOOTLOADER_ENABLED ++// Magic value to write to GPREGRET to trigger UF2 bootloader mode. ++#ifndef UF2_BOOTLOADER_GPREGRET_VALUE ++#define UF2_BOOTLOADER_GPREGRET_VALUE 0x57 ++#endif ++ ++static void checkUf2BootloaderTrigger(void) ++{ ++ // Detect 1200 baud touch: reset to bootloader when baud rate changes to 1200 ++ static uint32_t sPrevBaudrate = 0; ++ uint32_t baudrate = uint32_decode(sAppCdcAcm.specific.p_data->ctx.line_coding.dwDTERate); ++ ++ if (baudrate != sPrevBaudrate) ++ { ++ sPrevBaudrate = baudrate; ++ if (baudrate == 1200) ++ { ++ nrf_power_gpregret_set(UF2_BOOTLOADER_GPREGRET_VALUE); ++ NVIC_SystemReset(); ++ } ++ } ++} ++#endif // UF2_BOOTLOADER_ENABLED ++ + void nrf5UartInit(void) + { + static const app_usbd_config_t usbdConfig = { +@@ -286,6 +314,15 @@ void nrf5UartInit(void) + ret = app_usbd_power_events_enable(); + assert(ret == NRF_SUCCESS); + ++#if defined(UF2_BOOTLOADER_ENABLED) && UF2_BOOTLOADER_ENABLED ++ // Workaround: When transitioning from UF2 bootloader after flashing, ++ // APP_USBD_EVT_POWER_DETECTED won't fire since VBUS will stay detected during the soft reset. ++ if (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) ++ { ++ usbdUserEventHandler(APP_USBD_EVT_POWER_DETECTED); ++ } ++#endif ++ + OT_UNUSED_VARIABLE(ret); + } + +@@ -324,6 +361,10 @@ void nrf5UartProcess(void) + processConnection(); + processReceive(); + processTransmit(); ++ ++#if defined(UF2_BOOTLOADER_ENABLED) && UF2_BOOTLOADER_ENABLED ++ checkUf2BootloaderTrigger(); ++#endif + } + + otError otPlatUartEnable(void) +diff --git a/third_party/NordicSemiconductor/CMakeLists.txt b/third_party/NordicSemiconductor/CMakeLists.txt +index fa99d44..300e825 100644 +--- a/third_party/NordicSemiconductor/CMakeLists.txt ++++ b/third_party/NordicSemiconductor/CMakeLists.txt +@@ -77,7 +77,6 @@ set(COMMON_FLAG + -DSPIS0_ENABLED=1 + -DNRFX_SPIS_ENABLED=1 + -DNRFX_SPIS0_ENABLED=1 +- -DCONFIG_GPIO_AS_PINRESET + -DENABLE_FEM=1 + -DUSE_APP_CONFIG=1 + ) diff --git a/third_party/openthread-rcp-nrf528xx/patches/020-status-led.patch b/third_party/openthread-rcp-nrf528xx/patches/020-status-led.patch new file mode 100644 index 0000000..490bce6 --- /dev/null +++ b/third_party/openthread-rcp-nrf528xx/patches/020-status-led.patch @@ -0,0 +1,205 @@ +commit b55da5a120d01b19ec9f17ea3873f2fa9c674510 +Author: Karsten Sperling +Date: Thu Jan 29 13:01:03 2026 +1300 + + Add basic support for an RGB status LED + +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index fffb99b..8d2b455 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -39,6 +39,11 @@ else() + endif() + endif() + ++option(OT_STATUS_LED_PINS "Status LED pin configuration (R,G,B)" "") ++if(OT_STATUS_LED_PINS) ++ list(APPEND OT_PLATFORM_DEFINES "STATUS_LED_PINS=${OT_STATUS_LED_PINS}") ++endif() ++ + set(NRF_COMM_SOURCES + src/alarm.c + src/crypto.c +@@ -49,6 +54,7 @@ set(NRF_COMM_SOURCES + src/logging.c + src/misc.c + src/radio.c ++ src/status_led.c + src/system.c + src/temp.c + ) +diff --git a/src/src/radio.c b/src/src/radio.c +index a626f68..0490d99 100644 +--- a/src/src/radio.c ++++ b/src/src/radio.c +@@ -56,6 +56,7 @@ + #include "openthread-system.h" + #include "platform-fem.h" + #include "platform-nrf5.h" ++#include "status_led.h" + + #include + #include +@@ -446,6 +447,7 @@ otError otPlatRadioEnable(otInstance *aInstance) + { + sDisabled = false; + error = OT_ERROR_NONE; ++ nrf5SetStatusLed(STATUS_COLOR_RADIO_ENABLED); + } + else + { +@@ -464,6 +466,7 @@ otError otPlatRadioDisable(otInstance *aInstance) + error = OT_ERROR_INVALID_STATE); + + sDisabled = true; ++ nrf5SetStatusLed(STATUS_COLOR_RADIO_DISABLED); + + exit: + return error; +diff --git a/src/src/status_led.c b/src/src/status_led.c +new file mode 100644 +index 0000000..f25afce +--- /dev/null ++++ b/src/src/status_led.c +@@ -0,0 +1,76 @@ ++ ++#include "status_led.h" ++ ++#ifdef STATUS_LED_PINS ++ ++#include ++#include ++#include ++ ++// Default PWM configuration ++#ifndef STATUS_LED_PWM ++#define STATUS_LED_PWM NRF_PWM0 ++#endif ++#ifndef STATUS_LED_PWM_FREQUENCY ++#define STATUS_LED_PWM_FREQUENCY NRF_PWM_CLK_4MHz ++#endif ++ ++// Extract a pin from a pin map tuple ++#define SELECT_GPIO_PIN(i, ...) SELECT_GPIO_PIN_##i(__VA_ARGS__) ++#define SELECT_GPIO_PIN_0(p1, ...) NRF_GPIO_PIN_MAP p1 ++#define SELECT_GPIO_PIN_1(p1, p2, ...) NRF_GPIO_PIN_MAP p2 ++#define SELECT_GPIO_PIN_2(p1, p2, p3, ...) NRF_GPIO_PIN_MAP p3 ++ ++static inline uint16_t srgbToLinear(uint8_t c) ++{ ++ return (c < 11) ? c / 3 : 3 + ((c - 11) * (c - 11) * 10) / 593; ++} ++ ++void nrf5SetStatusLed(int color) ++{ ++ // PWM sequence data, must be in RAM for DMA access ++ static nrf_pwm_values_individual_t pwm_values = { 0 }; ++ static nrf_pwm_sequence_t const pwm_seq = { ++ .values.p_individual = &pwm_values, ++ .length = NRF_PWM_VALUES_LENGTH(pwm_values), ++ .repeats = 0, ++ .end_delay = 0 ++ }; ++ ++ static bool initialized = false; ++ if (!initialized) ++ { ++ // Configure pins for output and initialize to high (LEDs are active low) ++ uint32_t pins[] = { ++ SELECT_GPIO_PIN(0, STATUS_LED_PINS), // R ++ SELECT_GPIO_PIN(1, STATUS_LED_PINS), // G ++ SELECT_GPIO_PIN(2, STATUS_LED_PINS), // B ++ NRF_PWM_PIN_NOT_CONNECTED ++ }; ++ for (int i=0; i<3; i++) ++ { ++ nrf_gpio_pin_set(pins[i]); ++ nrf_gpio_cfg_output(pins[i]); ++ } ++ ++ // Configure PWM for 10 bit range, looping disabled (last duty cycle will be held) ++ nrf_pwm_disable(STATUS_LED_PWM); ++ nrf_pwm_pins_set(STATUS_LED_PWM, pins); ++ nrf_pwm_configure(STATUS_LED_PWM, STATUS_LED_PWM_FREQUENCY, NRF_PWM_MODE_UP, 1023); ++ nrf_pwm_decoder_set(STATUS_LED_PWM, NRF_PWM_LOAD_INDIVIDUAL, NRF_PWM_STEP_AUTO); ++ nrf_pwm_loop_set(STATUS_LED_PWM, 0); ++ nrf_pwm_shorts_set(STATUS_LED_PWM, 0); ++ nrf_pwm_sequence_set(STATUS_LED_PWM, 0, &pwm_seq); ++ nrf_pwm_enable(STATUS_LED_PWM); ++ ++ initialized = true; ++ } ++ ++ // MSB controls polarity, 0 = RisingEdge aligns with active low LEDs ++ pwm_values.channel_0 = srgbToLinear((color >> 16) & 0xff); // R ++ pwm_values.channel_1 = srgbToLinear((color >> 8) & 0xff); // G ++ pwm_values.channel_2 = srgbToLinear((color >> 0) & 0xff); // B ++ nrf_pwm_task_trigger(STATUS_LED_PWM, NRF_PWM_TASK_SEQSTART0); ++} ++ ++#endif // STATUS_LED_PINS +diff --git a/src/src/status_led.h b/src/src/status_led.h +new file mode 100644 +index 0000000..2ec7cda +--- /dev/null ++++ b/src/src/status_led.h +@@ -0,0 +1,30 @@ ++#ifndef STATUS_LED_H_ ++#define STATUS_LED_H_ ++ ++#include ++ ++#define STATUS_COLOR_INITIALIZING 0xffcc00 ++#define STATUS_COLOR_RADIO_DISABLED 0x000044 ++#define STATUS_COLOR_RADIO_ENABLED 0x0000ff ++ ++#ifdef STATUS_LED_PINS ++ ++/** ++ * Set the status RGB LED color, in 0xRRGGBB format (e.g. 0xFF8000 is orange). ++ * Uses hardware PWM for smooth color control with 8-bit resolution per channel. ++ * ++ * To enable support, STATUS_LED_PINS needs to be defined to a triple of ++ * (port,pin) pairs (see NRF_GPIO_PIN_MAP) at compile time, e.g. ++ * -DSTATUS_LED_PINS="(0,23),(0,22),(0,24)", assumed to be active low. ++ * ++ * STATUS_LED_PWM and STATUS_LED_PWM_FREQUENCY can be used to configure the ++ * PWM instance and frequence; the defaults are NRF_PWM0 and NRF_PWM_CLK_4MHz. ++ */ ++void nrf5SetStatusLed(int color); ++ ++#else ++ ++static inline void nrf5SetStatusLed(int color) { (void)color; } ++ ++#endif // STATUS_LED_PINS ++#endif // STATUS_LED_H_ +diff --git a/src/src/system.c b/src/src/system.c +index 025bf4e..cd1582b 100644 +--- a/src/src/system.c ++++ b/src/src/system.c +@@ -41,6 +41,7 @@ + #include "platform-fem.h" + #include "platform-nrf5-transport.h" + #include "platform-nrf5.h" ++#include "status_led.h" + #include + #include + +@@ -72,6 +73,8 @@ void otSysInit(int argc, char *argv[]) + otSysDeinit(); + } + ++ nrf5SetStatusLed(STATUS_COLOR_INITIALIZING); ++ + #if ((!SOFTDEVICE_PRESENT) && (NRF52840_XXAA)) + // Enable I-code cache + NRF_NVMC->ICACHECNF = NVMC_ICACHECNF_CACHEEN_Enabled; +@@ -104,6 +107,7 @@ void otSysInit(int argc, char *argv[]) + nrf5FemInit(); + nrf5CryptoInit(); + ++ nrf5SetStatusLed(STATUS_COLOR_RADIO_DISABLED); + gPlatformPseudoResetWasRequested = false; + } + diff --git a/tools/uf2-utils/Makefile b/tools/uf2-utils/Makefile new file mode 100644 index 0000000..ec90ab3 --- /dev/null +++ b/tools/uf2-utils/Makefile @@ -0,0 +1,41 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=uf2-utils +PKG_VERSION:=0.1.0 + +PKG_LICENSE:=Apache-2.0 +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/uf2-utils + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Utilities for interacting with UF2 bootloaders + DEPENDS:=+kmod-usb-storage +kmod-fs-vfat +endef + +define Build/Compile + $(TARGET_CC) $(TARGET_CPPFLAGS) $(TARGET_CFLAGS) -o $(PKG_BUILD_DIR)/uf2 uf2.c $(TARGET_LDFLAGS) +endef + +define Package/uf2-utils/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/uf2 $(1)/usr/sbin +endef + +$(eval $(call BuildPackage,uf2-utils)) diff --git a/tools/uf2-utils/uf2.c b/tools/uf2-utils/uf2.c new file mode 100644 index 0000000..6acc5a9 --- /dev/null +++ b/tools/uf2-utils/uf2.c @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void log_error(char const *what) { + fprintf(stderr, "%s failed: %s\n", what, strerror(errno)); +} + +static int parse_uint32(char const *str, uint32_t *out) { + char *endptr; + *out = strtoul(str, &endptr, 0); + return *endptr != '\0'; +} + +// Perform a "1200 baud touch" to reset the given device +static int reset(char const *device) { + int fd = open(device, O_RDWR | O_NOCTTY); + if (fd < 0) { + log_error("open"); + return 2; + } + + struct termios tio; + if (tcgetattr(fd, &tio) < 0) { + log_error("tcgetattr"); + goto failed; + } + + cfsetispeed(&tio, B1200); + cfsetospeed(&tio, B1200); + + if (tcsetattr(fd, TCSAFLUSH, &tio) < 0) { + log_error("tcsetattr"); + goto failed; + } + + usleep(10000); // allow a moment for the device to notice the change + close(fd); + return 0; + +failed: + close(fd); + return 2; +} + +struct uf2_block { + uint32_t magic_start0; + uint32_t magic_start1; + uint32_t flags; + uint32_t target_addr; + uint32_t payload_size; + uint32_t block_no; + uint32_t num_blocks; + uint32_t file_size; + uint8_t data[476]; + uint32_t magic_end; +} __attribute__((packed)); + +// Convert a binary file to UF2 and write to an output file or stdout +static int convert(uint32_t family, uint32_t base, char const *bin, char const *uf2) { + int status = 2; + FILE *fbin = NULL, *fuf2 = NULL; + + if (!(fbin = fopen(bin, "rb"))) { + log_error("fopen (input)"); + goto failed; + } + if (fseek(fbin, 0, SEEK_END) < 0) { + log_error("fseek"); + goto failed; + } + long file_size = ftell(fbin); + if (file_size < 0) { + log_error("ftell"); + goto failed; + } + rewind(fbin); + + FILE *fout = stdout; + if (uf2 && !(fout = fuf2 = fopen(uf2, "wb"))) { + log_error("fopen (output)"); + goto failed; + } + + const uint32_t chunk_size = 256; + uint32_t num_blocks = (file_size + chunk_size - 1) / chunk_size; + + struct uf2_block block = { 0 }; + block.magic_start0 = htole32(0x0A324655); + block.magic_start1 = htole32(0x9E5D5157); + block.magic_end = htole32(0x0AB16F30); + block.flags = htole32(0x00002000); // Family ID Present + block.file_size = htole32(family); + block.num_blocks = htole32(num_blocks); + block.payload_size = htole32(chunk_size); // same for all chunks (may implicitly pad the last chunk) + + long bytes_remaining = file_size; + for (uint32_t block_no = 0; block_no < num_blocks; block_no++) { + block.block_no = htole32(block_no); + block.target_addr = htole32(base + (block_no * chunk_size)); + + size_t read_size = bytes_remaining < chunk_size ? bytes_remaining : chunk_size; + size_t bytes_read = fread(block.data, 1, read_size, fbin); + if (bytes_read != read_size) { + log_error("fread"); + goto failed; + } + + bytes_remaining -= bytes_read; + if (bytes_read < chunk_size) { + memset(block.data + bytes_read, 0, chunk_size - bytes_read); + } + + if (fwrite(&block, 1, sizeof(block), fout) != sizeof(block)) { + log_error("fwrite"); + goto failed; + } + } + + if (fflush(fout)) { + log_error("fflush"); + goto failed; + } + + // Best-effort fdatasync; usually fails when flashing because the + // bootloader will already disconnect while the kernel is still trying + // to write meta-data. Will also fail if the output is not a file. + (void)fdatasync(fileno(fout)); + status = 0; // success + +failed: + if (fbin) { + fclose(fbin); + } + if (fuf2) { + fclose(fuf2); + } + return status; +} + +int main(int argc, char *argv[]) { + if (argc == 3 && !strcmp(argv[1], "reset")) { + return reset(argv[2]); + } + uint32_t family, base; + if ((argc == 5 || argc == 6) && !strcmp(argv[1], "convert") && + !parse_uint32(argv[2], &family) && + !parse_uint32(argv[3], &base)) { + return convert(family, base, argv[4], (argc == 6) ? argv[5] : NULL); + } + fprintf(stderr, "Usage: %s { reset TTY | convert 0xFAMILY 0xBASE BINFILE [UF2FILE] }\n", argv[0]); + return 1; +} From 9af8e25ad2b6c5caf33b4c3f6ae14e2d044b5dcd Mon Sep 17 00:00:00 2001 From: Karsten Sperling Date: Mon, 23 Feb 2026 11:53:36 +1300 Subject: [PATCH 3/4] Address suggestion from review --- tools/uf2-utils/uf2.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/uf2-utils/uf2.c b/tools/uf2-utils/uf2.c index 6acc5a9..2d3c9eb 100644 --- a/tools/uf2-utils/uf2.c +++ b/tools/uf2-utils/uf2.c @@ -73,7 +73,7 @@ struct uf2_block { uint32_t payload_size; uint32_t block_no; uint32_t num_blocks; - uint32_t file_size; + uint32_t file_size_or_family_id; uint8_t data[476]; uint32_t magic_end; } __attribute__((packed)); @@ -112,7 +112,7 @@ static int convert(uint32_t family, uint32_t base, char const *bin, char const * block.magic_start1 = htole32(0x9E5D5157); block.magic_end = htole32(0x0AB16F30); block.flags = htole32(0x00002000); // Family ID Present - block.file_size = htole32(family); + block.file_size_or_family_id = htole32(family); block.num_blocks = htole32(num_blocks); block.payload_size = htole32(chunk_size); // same for all chunks (may implicitly pad the last chunk) From 4fe49591b821c3e1693a01c42b3060799442f516 Mon Sep 17 00:00:00 2001 From: Karsten Sperling Date: Tue, 24 Feb 2026 08:31:48 +1300 Subject: [PATCH 4/4] Address parse_uint32 error hanling per review --- tools/uf2-utils/uf2.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/uf2-utils/uf2.c b/tools/uf2-utils/uf2.c index 2d3c9eb..3efcc2f 100644 --- a/tools/uf2-utils/uf2.c +++ b/tools/uf2-utils/uf2.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include @@ -28,10 +29,19 @@ static void log_error(char const *what) { fprintf(stderr, "%s failed: %s\n", what, strerror(errno)); } +// Parse a uint32_t in decimal, octal, or hex static int parse_uint32(char const *str, uint32_t *out) { + if (!isdigit(*str)) { + return 1; + } + errno = 0; char *endptr; - *out = strtoul(str, &endptr, 0); - return *endptr != '\0'; + unsigned long value = strtoul(str, &endptr, 0); + if (errno != 0 || *endptr != '\0' || value > UINT32_MAX) { + return 1; + } + *out = value; + return 0; } // Perform a "1200 baud touch" to reset the given device