mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 07:06:20 +00:00
[nrf52] add OTA for Adafruit_nRF52_Bootloader (#17381)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
co-authored by
Jonathan Swoboda
parent
e209bbb5ae
commit
ca08803425
@@ -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"]),
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user