[rp2040] Add variant config option for RP2040/RP2350 (#16602)

This commit is contained in:
Jesse Hills
2026-05-25 10:43:39 +12:00
committed by GitHub
parent 1fb8c26704
commit 62b0a93e5e
6 changed files with 203 additions and 7 deletions
+104 -5
View File
@@ -1,8 +1,10 @@
from collections.abc import Callable
import logging
from pathlib import Path
import re
from string import ascii_letters, digits
import subprocess
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -12,6 +14,7 @@ from esphome.const import (
CONF_FRAMEWORK,
CONF_PLATFORM_VERSION,
CONF_SOURCE,
CONF_VARIANT,
CONF_VERSION,
CONF_WATCHDOG_TIMEOUT,
KEY_CORE,
@@ -21,12 +24,30 @@ from esphome.const import (
PLATFORM_RP2040,
ThreadModel,
)
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.core import (
CORE,
CoroPriority,
EsphomeCore,
EsphomeError,
coroutine_with_priority,
)
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed
from esphome.types import ConfigType
from . import boards
from .const import KEY_BOARD, KEY_LWIP_OPTS, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
from .const import (
KEY_BOARD,
KEY_LWIP_OPTS,
KEY_PIO_FILES,
KEY_RP2040,
KEY_VARIANT,
MCU_TO_VARIANT,
STANDARD_BOARDS,
VARIANT_FRIENDLY,
VARIANTS,
rp2040_ns,
)
# force import gpio to register pin schema
from .gpio import rp2040_pin_to_code # noqa
@@ -68,7 +89,7 @@ def board_id_has_wifi(board_id: str) -> bool:
return board_info.get("wifi", False)
def set_core_data(config):
def set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_RP2040] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
@@ -76,12 +97,46 @@ def set_core_data(config):
config[CONF_FRAMEWORK][CONF_VERSION]
)
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_RP2040][KEY_VARIANT] = config[CONF_VARIANT]
CORE.data[KEY_RP2040][KEY_PIO_FILES] = {}
return config
def get_rp2040_variant(core_obj: EsphomeCore | None = None) -> str:
return (core_obj or CORE).data[KEY_RP2040][KEY_VARIANT]
def only_on_variant(
*,
supported: str | list[str] | None = None,
unsupported: str | list[str] | None = None,
msg_prefix: str = "This feature",
) -> Callable[[Any], Any]:
"""Config validator for features only available on some RP2040 variants."""
if supported is not None and not isinstance(supported, list):
supported = [supported]
if unsupported is not None and not isinstance(unsupported, list):
unsupported = [unsupported]
def validator_(obj: Any) -> Any:
if not CORE.is_rp2040:
raise cv.Invalid(f"{msg_prefix} is only available on RP2040")
variant = get_rp2040_variant()
if supported is not None and variant not in supported:
raise cv.Invalid(
f"{msg_prefix} is only available on {', '.join(supported)}"
)
if unsupported is not None and variant in unsupported:
raise cv.Invalid(
f"{msg_prefix} is not available on {', '.join(unsupported)}"
)
return obj
return validator_
def get_download_types(storage_json):
"""Binary-download entries for a built RP2040 firmware.
@@ -192,12 +247,52 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
_arduino_check_versions,
)
def _detect_variant(value: ConfigType) -> ConfigType:
value = value.copy()
board: str | None = value.get(CONF_BOARD)
variant: str | None = value.get(CONF_VARIANT)
if board is None:
# `cv.has_at_least_one_key` guarantees variant is set here.
board = STANDARD_BOARDS[variant]
value[CONF_BOARD] = board
board_info = boards.BOARDS.get(board)
if board_info is None:
if variant is None:
raise cv.Invalid(
"This board is unknown; please specify the chip variant using "
f"the '{CONF_VARIANT}' option.",
path=[CONF_BOARD],
)
_LOGGER.warning(
"This board is unknown; the specified variant '%s' will be used "
"but this may not work as expected.",
variant,
)
else:
board_variant = MCU_TO_VARIANT[board_info["mcu"]]
if variant is None:
variant = board_variant
elif variant != board_variant:
raise cv.Invalid(
f"Option '{CONF_VARIANT}' ({variant}) does not match the "
f"selected board '{board}' ({board_variant}).",
path=[CONF_VARIANT],
)
value[CONF_VARIANT] = variant
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.All(
cv.Optional(CONF_BOARD): cv.All(
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="8388ms"): cv.All(
cv.positive_time_period_milliseconds,
@@ -206,6 +301,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
}
),
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
_detect_variant,
set_core_data,
)
@@ -223,7 +320,9 @@ async def to_code(config):
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_RP2040_VARIANT_{variant}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
+26
View File
@@ -4,5 +4,31 @@ KEY_BOARD = "board"
KEY_LWIP_OPTS = "lwip_opts"
KEY_RP2040 = "rp2040"
KEY_PIO_FILES = "pio_files"
KEY_VARIANT = "variant"
VARIANT_RP2040 = "RP2040"
VARIANT_RP2350 = "RP2350"
VARIANTS = [
VARIANT_RP2040,
VARIANT_RP2350,
]
VARIANT_FRIENDLY = {
VARIANT_RP2040: "RP2040",
VARIANT_RP2350: "RP2350",
}
# Map BOARDS[board]["mcu"] (lowercase) to canonical variant constant
MCU_TO_VARIANT = {
"rp2040": VARIANT_RP2040,
"rp2350": VARIANT_RP2350,
}
# Default board chosen when only `variant` is specified — the Raspberry Pi
# Foundation reference boards (Pico W / Pico 2 W).
STANDARD_BOARDS = {
VARIANT_RP2040: "rpipicow",
VARIANT_RP2350: "rpipico2w",
}
rp2040_ns = cg.esphome_ns.namespace("rp2040")
+1
View File
@@ -401,6 +401,7 @@
#define USE_LOGGER_USB_CDC
#define USE_SOCKET_IMPL_LWIP_TCP
#define USE_RP2040_BLE
#define USE_RP2040_VARIANT_RP2040
#define USE_SPI
#ifndef USE_ETHERNET
#define USE_ETHERNET
@@ -1,4 +1,5 @@
rp2040:
variant: rp2040
enable_full_printf: false
logger:
@@ -0,0 +1,6 @@
rp2040:
variant: rp2350
enable_full_printf: false
logger:
level: VERBOSE
+65 -2
View File
@@ -1,6 +1,11 @@
"""Tests for RP2040 component public helpers."""
"""Tests for RP2040 component public helpers and variant detection."""
from esphome.components.rp2040 import board_id_has_wifi
import pytest
from esphome.components.rp2040 import _detect_variant, board_id_has_wifi
from esphome.components.rp2040.const import VARIANT_RP2040, VARIANT_RP2350
import esphome.config_validation as cv
from esphome.const import CONF_BOARD, CONF_VARIANT
def test_board_id_has_wifi_for_known_wifi_board() -> None:
@@ -27,3 +32,61 @@ def test_board_id_has_wifi_for_unknown_board_returns_true() -> None:
"no CYW43" guard at compile time.
"""
assert board_id_has_wifi("not-a-real-board-id") is True
def test_detect_variant_derives_variant_from_board() -> None:
"""Board alone resolves to the matching variant."""
result = _detect_variant({CONF_BOARD: "rpipicow"})
assert result[CONF_BOARD] == "rpipicow"
assert result[CONF_VARIANT] == VARIANT_RP2040
def test_detect_variant_derives_variant_from_rp2350_board() -> None:
"""An RP2350 board resolves to ``RP2350``."""
result = _detect_variant({CONF_BOARD: "rpipico2"})
assert result[CONF_BOARD] == "rpipico2"
assert result[CONF_VARIANT] == VARIANT_RP2350
def test_detect_variant_only_picks_default_board_rp2040() -> None:
"""Variant alone picks Pico W as the canonical RP2040 board."""
result = _detect_variant({CONF_VARIANT: VARIANT_RP2040})
assert result[CONF_BOARD] == "rpipicow"
assert result[CONF_VARIANT] == VARIANT_RP2040
def test_detect_variant_only_picks_default_board_rp2350() -> None:
"""Variant alone picks Pico 2 W as the canonical RP2350 board."""
result = _detect_variant({CONF_VARIANT: VARIANT_RP2350})
assert result[CONF_BOARD] == "rpipico2w"
assert result[CONF_VARIANT] == VARIANT_RP2350
def test_detect_variant_matching_explicit_variant_passes() -> None:
"""Specifying both a board and the matching variant is allowed."""
result = _detect_variant({CONF_BOARD: "rpipico2", CONF_VARIANT: VARIANT_RP2350})
assert result[CONF_BOARD] == "rpipico2"
assert result[CONF_VARIANT] == VARIANT_RP2350
def test_detect_variant_mismatched_variant_raises() -> None:
"""Board/variant mismatch must be rejected and name the offending board."""
with pytest.raises(
cv.Invalid, match=r"does not match the selected board 'rpipicow'"
):
_detect_variant({CONF_BOARD: "rpipicow", CONF_VARIANT: VARIANT_RP2350})
def test_detect_variant_unknown_board_without_variant_raises() -> None:
"""Unknown board with no variant tells the user how to recover."""
with pytest.raises(cv.Invalid, match="please specify the chip variant"):
_detect_variant({CONF_BOARD: "not-a-real-board"})
def test_detect_variant_unknown_board_with_variant_passes() -> None:
"""Unknown board + explicit variant is accepted (with a warning)."""
result = _detect_variant(
{CONF_BOARD: "not-a-real-board", CONF_VARIANT: VARIANT_RP2040}
)
assert result[CONF_BOARD] == "not-a-real-board"
assert result[CONF_VARIANT] == VARIANT_RP2040