[nrf52] add OTA for Adafruit_nRF52_Bootloader (#17381)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
tomaszduda23
2026-07-27 16:03:22 -04:00
committed by GitHub
co-authored by Jonathan Swoboda
parent e209bbb5ae
commit ca08803425
4 changed files with 157 additions and 18 deletions
+46 -8
View File
@@ -877,6 +877,21 @@ def run_compile(args, config: ConfigType) -> bool:
zephyr_dir = build_dir / "zephyr"
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
bootloader = zephyr_data()[KEY_BOOTLOADER]
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: "0x7EAED30A",
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: "0xADA52840",
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: "0xADA52840",
}
# SDK < 2.9.2 places artifacts directly in build_dir/zephyr/.
# SDK >= 2.9.2 nests them one level deeper (build_dir/zephyr/zephyr/);
# copy files to match get_download_types layout.
@@ -888,20 +903,43 @@ def run_compile(args, config: ConfigType) -> bool:
_copy_if_exists(west_out / "zephyr.signed.bin", zephyr_dir / "app_update.bin")
_copy_if_exists(build_dir / "merged.hex", zephyr_dir / "merged.hex")
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
}
bootloader = zephyr_data()[KEY_BOOTLOADER]
# For Adafruit bootloader builds, regenerate the UF2 from merged.hex,
# whose records carry the correct flash addresses. The build's own
# zephyr.uf2 uses the board's default offset, which is wrong in some cases.
merged_hex = zephyr_dir / "merged.hex"
if bootloader in _UF2_FAMILY_IDS and merged_hex.is_file():
# Drop the build's own wrong-offset UF2 so it isn't shipped alongside.
app_uf2 = west_out / "zephyr.uf2"
if app_uf2.is_file():
app_uf2.unlink()
uf2conv = (
paths["framework_path"] / "zephyr" / "scripts" / "build" / "uf2conv.py"
)
if not run_command_ok(
[
str(paths["python_executable"]),
str(uf2conv),
"-f",
_UF2_FAMILY_IDS[bootloader],
"-c",
"-o",
str(zephyr_dir / "zephyr.uf2"),
str(merged_hex),
],
env=env,
stream_output=True,
):
raise EsphomeError("Failed to generate UF2 from merged hex")
if bootloader in (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
):
hex_file = west_out / "zephyr.hex"
# no fallback is needed for adafruit case. merged merged.hex is always generated.
# get_download_types needs fallback for mcuboot (non adafruit)
hex_file = zephyr_dir / "merged.hex"
dfu_package = build_dir / "firmware.zip"
genpkg_cmd = [
str(paths["python_executable"]),
+24 -3
View File
@@ -26,7 +26,26 @@ from .const import (
CODEOWNERS = ["@tomaszduda23"]
PrjConfValueType = bool | str | int
class HexValue:
"""Wrap an integer so it is written as 0x... in prj.conf (required for hex Kconfig types)."""
def __init__(self, value: int) -> None:
self.value = value
def __eq__(self, other: object) -> bool:
if isinstance(other, HexValue):
return self.value == other.value
return NotImplemented
def __repr__(self) -> str:
return f"HexValue(0x{self.value:X})"
def __str__(self) -> str:
return f"0x{self.value:X}"
PrjConfValueType = bool | str | int | HexValue
class Section:
@@ -164,6 +183,8 @@ def zephyr_setup_preferences():
def _format_prj_conf_val(value: PrjConfValueType) -> str:
if isinstance(value, bool):
return "y" if value else "n"
if isinstance(value, HexValue):
return hex(value.value)
if isinstance(value, int):
return str(value)
if isinstance(value, str):
@@ -249,7 +270,7 @@ def copy_files() -> None:
)
if image:
path = CORE.relative_build_path(f"sysbuild/{image}.conf")
path = CORE.relative_build_path(f"zephyr/sysbuild/{image}.conf")
else:
path = CORE.relative_build_path("zephyr/prj.conf")
@@ -257,7 +278,7 @@ def copy_files() -> None:
for image, content in zephyr_data()[KEY_OVERLAY].items():
if image:
path = CORE.relative_build_path(f"sysbuild/{image}.overlay")
path = CORE.relative_build_path(f"zephyr/sysbuild/{image}.overlay")
else:
path = CORE.relative_build_path("zephyr/app.overlay")
changed |= write_file_if_changed(path, content)
@@ -1,6 +1,8 @@
import esphome.codegen as cg
from esphome.components.nrf52.boards import BOOTLOADER_CONFIG
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
from esphome.components.zephyr import (
HexValue,
zephyr_add_cdc_acm,
zephyr_add_overlay,
zephyr_add_prj_conf,
@@ -72,12 +74,6 @@ CONFIG_SCHEMA = cv.All(
)
def _validate_mcumgr_bootloader(config: ConfigType) -> None:
bootloader = zephyr_data()[KEY_BOOTLOADER]
if bootloader != BOOTLOADER_MCUBOOT:
raise cv.Invalid(f"'{bootloader}' bootloader does not support OTA")
KEY_ZEPHYR_BLE_SERVER = "zephyr_ble_server"
@@ -89,9 +85,22 @@ def _validate_ble_server(config: ConfigType) -> None:
raise cv.Invalid(f"'{KEY_ZEPHYR_BLE_SERVER}' component is required for BLE OTA")
def _validate_bootloader(config: ConfigType) -> None:
bootloader = zephyr_data()[KEY_BOOTLOADER]
if bootloader == BOOTLOADER_MCUBOOT:
return
if bootloader not in BOOTLOADER_CONFIG:
raise cv.Invalid(f"{bootloader} does not support OTA")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if framework_ver < cv.Version(2, 9, 2):
raise cv.Invalid(
"OTA with Adafruit_nRF52_Bootloader requires at least SDK 2.9.2"
)
def _final_validate(config: ConfigType) -> None:
_validate_mcumgr_bootloader(config)
_validate_ble_server(config)
_validate_bootloader(config)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -152,3 +161,70 @@ async def to_code(config: ConfigType) -> None:
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if framework_ver >= cv.Version(2, 9, 2):
zephyr_data()[KEY_SYSBUILD] = True
bootloader = zephyr_data()[KEY_BOOTLOADER]
if bootloader != BOOTLOADER_MCUBOOT:
sections = BOOTLOADER_CONFIG[bootloader]
# Derive partition addresses from the SoftDevice and bootloader sections so
# that the DTS flash map matches what the Partition Manager produces:
# MCUboot sits immediately after the SoftDevice, then slot0, then slot1.
mcuboot_size = 0x9000
sd_end = next(s.address + s.size for s in sections if "SoftDevice" in s.name)
bl_start = next(s.address for s in sections if "Adafruit" in s.name)
slot0_start = sd_end + mcuboot_size
# Align slot size down to a 4 KB sector boundary
slot_size = ((bl_start - slot0_start) // 2 // 0x1000) * 0x1000
slot1_start = slot0_start + slot_size
def _mcuboot_partition_overlay() -> str:
def part(name, start, size):
return f"""
{name}: partition@{start:x} {{
reg = <0x{start:x} 0x{size:x}>;
}};"""
return f"""
/delete-node/ &boot_partition;
/delete-node/ &storage_partition;
/delete-node/ &code_partition;
/delete-node/ &reserved_partition_0;
&flash0 {{
partitions {{
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
{part("slot0_partition", slot0_start, slot_size)}
{part("slot1_partition", slot1_start, slot_size)}
}};
}};
"""
def _code_partition_overlay() -> str:
return """
/ {
chosen {
zephyr,code-partition = &slot0_partition;
};
};
"""
zephyr_add_overlay(_mcuboot_partition_overlay())
zephyr_add_overlay(_mcuboot_partition_overlay(), "mcuboot")
zephyr_add_overlay(_code_partition_overlay())
zephyr_add_overlay(_code_partition_overlay(), "mcuboot")
# mcuboot is second bootloader. It's only task is to swap partitions.
# recovery can be done by first bootloader. Keep it small.
zephyr_add_overlay(
"""
&zephyr_udc0 {
status = "disabled";
};
""",
"mcuboot",
)
zephyr_add_prj_conf("USB_DEVICE_STACK", False, image="mcuboot")
zephyr_add_prj_conf("CONSOLE", False, image="mcuboot")
zephyr_add_prj_conf(
"PM_PARTITION_SIZE_MCUBOOT", HexValue(mcuboot_size), image="mcuboot"
)
@@ -0,0 +1,4 @@
zephyr_ble_server:
ota:
- platform: zephyr_mcumgr