mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 05:46:12 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97866e2891 | ||
|
|
1adebf9356 | ||
|
|
09baebcf00 | ||
|
|
e9a5f4be67 |
@@ -71,6 +71,7 @@ from esphome.types import ConfigType
|
||||
from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG, SELF_RESET_PINS
|
||||
from .const import (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
@@ -200,12 +201,19 @@ def set_framework(config: ConfigType) -> ConfigType:
|
||||
# DFU bootloader with Nordic's nrfutil; mcuboot uses mcumgr instead
|
||||
SERIAL_DFU_BOOTLOADERS = (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
BOOTLOADER_NRF,
|
||||
)
|
||||
BOOTLOADERS = [*SERIAL_DFU_BOOTLOADERS, BOOTLOADER_MCUBOOT]
|
||||
# SoftDevice id and major version in front of the application, for the debug component's check
|
||||
SOFTDEVICES: dict[str, tuple[int, int | None]] = {
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: (132, None),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: (140, 6),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: (140, 7),
|
||||
}
|
||||
|
||||
|
||||
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
|
||||
@@ -427,13 +435,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
||||
cg.add_define("USE_BOOTLOADER_MCUBOOT")
|
||||
elif "_sd" in config[KEY_BOOTLOADER]:
|
||||
bootloader = config[KEY_BOOTLOADER].split("_")
|
||||
sd_id = bootloader[2][2:]
|
||||
cg.add_define("USE_SOFTDEVICE_ID", int(sd_id))
|
||||
if (len(bootloader)) > 3:
|
||||
sd_version = bootloader[3][1:]
|
||||
cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version))
|
||||
elif softdevice := SOFTDEVICES.get(config[KEY_BOOTLOADER]):
|
||||
sd_id, sd_version = softdevice
|
||||
cg.add_define("USE_SOFTDEVICE_ID", sd_id)
|
||||
if sd_version is not None:
|
||||
cg.add_define("USE_SOFTDEVICE_VERSION", sd_version)
|
||||
|
||||
zephyr_setup_preferences()
|
||||
zephyr_to_code(config)
|
||||
@@ -1149,12 +1155,14 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD: ("0x0052", "0x00"), # no SoftDevice to require
|
||||
}
|
||||
# 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",
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD: "0xADA52840",
|
||||
}
|
||||
|
||||
# SDK < 2.9.2 places artifacts directly in build_dir/zephyr/.
|
||||
|
||||
@@ -3,6 +3,7 @@ from esphome.components.zephyr.const import KEY_BOOTLOADER
|
||||
|
||||
from .const import (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
@@ -36,6 +37,16 @@ BOARDS_ZEPHYR = {
|
||||
},
|
||||
# Nordic nRF52840 Dongle (PCA10059): factory Open DFU bootloader at 0xE0000, kept in place
|
||||
"nrf52840dongle": {KEY_BOOTLOADER: [BOOTLOADER_NRF]},
|
||||
# Makerdiary nRF52840 MDK USB Dongle: its UF2 bootloader is an Adafruit fork. Dongles made since
|
||||
# 2023 carry version 0.4.0 or newer without a SoftDevice; older ones carry 0.2.x with S140 6.1.1
|
||||
"nrf52840_mdk_usb_dongle": {
|
||||
KEY_BOOTLOADER: [
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
# Boards with a GPIO wired to their RESET pin: driving it low is a pin reset, which the factory
|
||||
@@ -54,22 +65,34 @@ MCUMGR_OTA_BOOTLOADERS = (
|
||||
|
||||
# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go
|
||||
# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map
|
||||
# The bootloader with its settings and the MBR parameters, at the top of the flash
|
||||
_ADAFRUIT_BOOTLOADER = Section(
|
||||
"Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"
|
||||
)
|
||||
|
||||
|
||||
def _softdevice_layout(end: int) -> list[Section]:
|
||||
"""Nordic's MBR page, the SoftDevice behind it up to ``end``, and the Adafruit bootloader.
|
||||
|
||||
The MBR page is named as the partition manager names it, so on a board that declares the
|
||||
nRF5 bootloader it is not added a second time behind the SoftDevice, which would move the
|
||||
application away from where the SoftDevice jumps to."""
|
||||
return [
|
||||
Section("nrf5_mbr", 0x0, 0x1000, "flash_primary"),
|
||||
Section("SoftDevice", 0x1000, end - 0x1000, "flash_primary"),
|
||||
_ADAFRUIT_BOOTLOADER,
|
||||
]
|
||||
|
||||
|
||||
BOOTLOADER_CONFIG = {
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: [
|
||||
Section("SoftDevice", 0x0, 0x26000, "flash_primary"),
|
||||
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
|
||||
],
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [
|
||||
Section("SoftDevice", 0x0, 0x26000, "flash_primary"),
|
||||
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
|
||||
],
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [
|
||||
Section("SoftDevice", 0x0, 0x27000, "flash_primary"),
|
||||
Section("Adafruit_nRF52_Bootloader", 0xF4000, 0xC000, "flash_primary"),
|
||||
],
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: _softdevice_layout(0x26000),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: _softdevice_layout(0x26000),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: _softdevice_layout(0x27000),
|
||||
# The board's BOARD_HAS_NRF5_BOOTLOADER already reserves the MBR page at 0x0, so the application
|
||||
# links at 0x1000, where the bootloader loads it; only the bootloader itself is reserved here
|
||||
BOOTLOADER_NRF: [
|
||||
Section("open_bootloader", 0xE0000, 0x20000, "flash_primary"),
|
||||
],
|
||||
# Same MBR page and application start as above
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD: [_ADAFRUIT_BOOTLOADER],
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ BOOTLOADER_ADAFRUIT = "adafruit"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD = "adafruit_nrf52_nosd"
|
||||
BOOTLOADER_NRF = "nrf"
|
||||
|
||||
EXTRA_ADC = [
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
nrf52:
|
||||
dcdc: False
|
||||
dfu: true # the 1200 bps touch reboots into the UF2 bootloader
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: componenttestnrf52
|
||||
friendly_name: $component_name
|
||||
|
||||
nrf52:
|
||||
board: nrf52840_mdk_usb_dongle
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
||||
@@ -7,7 +7,10 @@ from unittest.mock import Mock, patch
|
||||
import pytest
|
||||
|
||||
from esphome.components import nrf52
|
||||
from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT_NRF52_SD140_V7
|
||||
from esphome.components.nrf52.const import (
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
)
|
||||
from esphome.components.zephyr.const import KEY_BOARD, KEY_BOOTLOADER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
|
||||
@@ -24,7 +27,9 @@ def build_dir(tmp_path: Path) -> Path:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_cmd(tmp_path: Path) -> Iterator[Mock]:
|
||||
def run_cmd(tmp_path: Path, request: pytest.FixtureRequest) -> Iterator[Mock]:
|
||||
"""The build's command runner; an indirect parameter picks the bootloader."""
|
||||
bootloader = getattr(request, "param", BOOTLOADER_ADAFRUIT_NRF52_SD140_V7)
|
||||
with (
|
||||
patch.object(nrf52, "check_and_install"),
|
||||
patch.object(nrf52, "_generate_cmake_lists", return_value=False),
|
||||
@@ -37,10 +42,7 @@ def run_cmd(tmp_path: Path) -> Iterator[Mock]:
|
||||
patch.object(
|
||||
nrf52,
|
||||
"zephyr_data",
|
||||
return_value={
|
||||
KEY_BOARD: "board",
|
||||
KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
},
|
||||
return_value={KEY_BOARD: "board", KEY_BOOTLOADER: bootloader},
|
||||
),
|
||||
patch.object(nrf52.pch, "pch_enabled", return_value=False),
|
||||
patch.object(nrf52, "run_command_ok", return_value=True) as run,
|
||||
@@ -48,13 +50,20 @@ def run_cmd(tmp_path: Path) -> Iterator[Mock]:
|
||||
yield run
|
||||
|
||||
|
||||
def _uf2_cmd(run_cmd: Mock) -> list[str]:
|
||||
return next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0])
|
||||
|
||||
|
||||
def _genpkg_cmd(run_cmd: Mock) -> list[str]:
|
||||
return next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
|
||||
|
||||
|
||||
def _uf2_hex(run_cmd: Mock) -> str:
|
||||
cmd = next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0])
|
||||
return cmd[-1]
|
||||
return _uf2_cmd(run_cmd)[-1]
|
||||
|
||||
|
||||
def _dfu_hex(run_cmd: Mock) -> str:
|
||||
cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
|
||||
cmd = _genpkg_cmd(run_cmd)
|
||||
return cmd[cmd.index("--application") + 1]
|
||||
|
||||
|
||||
@@ -130,11 +139,22 @@ def test_stale_merged_hex_is_removed_before_building(
|
||||
assert _dfu_hex(run_cmd) == app_hex
|
||||
|
||||
|
||||
def test_sd140_v7_dfu_accepts_all_s140_7_softdevices(
|
||||
build_dir: Path, run_cmd: Mock
|
||||
@pytest.mark.parametrize(
|
||||
("run_cmd", "sd_req"),
|
||||
[
|
||||
# Boards such as the Seeed XIAO nRF52840 ship S140 7.3.0, not 7.0.1
|
||||
(BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ["0x00CA", "0x0100", "0x0123"]),
|
||||
# The Makerdiary MDK USB Dongle's bootloader has no SoftDevice to require
|
||||
(BOOTLOADER_ADAFRUIT_NRF52_NOSD, ["0x00"]),
|
||||
],
|
||||
indirect=["run_cmd"],
|
||||
)
|
||||
def test_dfu_package_requires_the_bootloaders_softdevices(
|
||||
build_dir: Path, run_cmd: Mock, sd_req: list[str]
|
||||
) -> None:
|
||||
"""Boards such as the Seeed XIAO nRF52840 ship S140 7.3.0, not 7.0.1."""
|
||||
_build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd)
|
||||
cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
|
||||
sd_req = cmd[cmd.index("--sd-req") + 1].split(",")
|
||||
assert sd_req == ["0x00CA", "0x0100", "0x0123"]
|
||||
cmd = _genpkg_cmd(run_cmd)
|
||||
assert cmd[cmd.index("--dev-type") + 1] == "0x0052"
|
||||
assert cmd[cmd.index("--sd-req") + 1].split(",") == sd_req
|
||||
uf2 = _uf2_cmd(run_cmd)
|
||||
assert uf2[uf2.index("-f") + 1] == "0xADA52840"
|
||||
|
||||
@@ -7,21 +7,22 @@ from esphome.components.zephyr import Section
|
||||
|
||||
|
||||
def test_testing_mode_sections_move_only_the_bootloader() -> None:
|
||||
"""The bootloader pinned to the end of flash follows the faked end; the
|
||||
SoftDevice at the start stays."""
|
||||
"""The bootloader pinned to the end of flash follows the faked end; the MBR page
|
||||
and the SoftDevice at the start stay."""
|
||||
sections = nrf52._testing_mode_sections(
|
||||
BOOTLOADER_CONFIG[BOOTLOADER_ADAFRUIT_NRF52_SD140_V7]
|
||||
)
|
||||
|
||||
assert [(s.name, s.address, s.size) for s in sections] == [
|
||||
("SoftDevice", 0x0, 0x27000),
|
||||
("nrf5_mbr", 0x0, 0x1000),
|
||||
("SoftDevice", 0x1000, 0x26000),
|
||||
(
|
||||
"Adafruit_nRF52_Bootloader",
|
||||
nrf52.TESTING_FLASH_SIZE - 0xC000,
|
||||
0xC000,
|
||||
),
|
||||
]
|
||||
assert sections[1].end_address == nrf52.TESTING_FLASH_SIZE
|
||||
assert sections[2].end_address == nrf52.TESTING_FLASH_SIZE
|
||||
|
||||
|
||||
def test_testing_mode_sections_leave_unpinned_sections() -> None:
|
||||
|
||||
@@ -11,7 +11,11 @@ from esphome.components.nrf52 import (
|
||||
set_core_data,
|
||||
)
|
||||
from esphome.components.nrf52.boards import BOOTLOADER_CONFIG
|
||||
from esphome.components.nrf52.const import BOOTLOADER_NRF
|
||||
from esphome.components.nrf52.const import (
|
||||
BOOTLOADER_ADAFRUIT_NRF52_NOSD,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_NRF,
|
||||
)
|
||||
from esphome.components.zephyr.const import KEY_PM_STATIC, KEY_ZEPHYR
|
||||
from esphome.components.zephyr_mcumgr.ota import _validate_bootloader
|
||||
from esphome.config import path_context
|
||||
@@ -26,10 +30,18 @@ def test_detect_bootloader_reports_a_missing_board() -> None:
|
||||
_detect_bootloader({})
|
||||
|
||||
|
||||
def test_dongle_defaults_to_nrf_bootloader() -> None:
|
||||
"""The dongle's only bootloader is the factory one, so it is selected without a config line."""
|
||||
config = _detect_bootloader({"board": "nrf52840dongle"})
|
||||
assert config["bootloader"] == BOOTLOADER_NRF
|
||||
@pytest.mark.parametrize(
|
||||
("board", "bootloader"),
|
||||
[
|
||||
# The dongle's only bootloader is the factory one
|
||||
("nrf52840dongle", BOOTLOADER_NRF),
|
||||
# Makerdiary's UF2 bootloader is an Adafruit fork that ships without a SoftDevice
|
||||
("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_NOSD),
|
||||
],
|
||||
)
|
||||
def test_board_selects_its_factory_bootloader(board: str, bootloader: str) -> None:
|
||||
"""The factory bootloader is selected without a config line."""
|
||||
assert _detect_bootloader({"board": board})["bootloader"] == bootloader
|
||||
|
||||
|
||||
def test_dongle_rejects_foreign_bootloader() -> None:
|
||||
@@ -37,12 +49,14 @@ def test_dongle_rejects_foreign_bootloader() -> None:
|
||||
_detect_bootloader({"board": "nrf52840dongle", "bootloader": "mcuboot"})
|
||||
|
||||
|
||||
def _dongle_core_data() -> None:
|
||||
def _dongle_core_data(
|
||||
board: str = "nrf52840dongle", bootloader: str = BOOTLOADER_NRF
|
||||
) -> None:
|
||||
CORE.data[KEY_CORE] = {}
|
||||
CORE.testing_mode = (
|
||||
False # the real flash, not the larger one faked for grouped tests
|
||||
)
|
||||
set_core_data({"board": "nrf52840dongle", "bootloader": BOOTLOADER_NRF})
|
||||
set_core_data({"board": board, "bootloader": bootloader})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -64,6 +78,25 @@ def test_nrf_bootloader_reserves_factory_region() -> None:
|
||||
assert registered["open_bootloader"].end_address == 0x100000
|
||||
|
||||
|
||||
def test_softdevice_layout_names_the_mbr_page() -> None:
|
||||
"""On a board with an nRF5 bootloader the partition manager adds an MBR page of its own
|
||||
unless one is declared, which would push the app behind where the SoftDevice jumps to."""
|
||||
_dongle_core_data("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_SD140_V6)
|
||||
registered = {s.name: s for s in CORE.data[KEY_ZEPHYR][KEY_PM_STATIC]}
|
||||
assert registered["nrf5_mbr"].address == 0x0
|
||||
assert registered["nrf5_mbr"].end_address == registered["SoftDevice"].address
|
||||
assert registered["SoftDevice"].end_address == 0x26000
|
||||
|
||||
|
||||
def test_adafruit_nosd_reserves_only_the_bootloader() -> None:
|
||||
"""Without a SoftDevice the app follows the MBR page, as with the Open DFU bootloader."""
|
||||
_dongle_core_data("nrf52840_mdk_usb_dongle", BOOTLOADER_ADAFRUIT_NRF52_NOSD)
|
||||
registered = {s.name: s for s in CORE.data[KEY_ZEPHYR][KEY_PM_STATIC]}
|
||||
assert set(registered) == {"Adafruit_nRF52_Bootloader"}
|
||||
assert registered["Adafruit_nRF52_Bootloader"].address == 0xF4000
|
||||
assert registered["Adafruit_nRF52_Bootloader"].end_address == 0x100000
|
||||
|
||||
|
||||
def test_nrf_bootloader_rejects_mcumgr_ota() -> None:
|
||||
"""OTA with mcumgr places its slot against a SoftDevice, which this layout has none of."""
|
||||
_dongle_core_data()
|
||||
|
||||
Reference in New Issue
Block a user