Compare commits

..
Author SHA1 Message Date
J. Nick Koston 5ec65b0cd6 Merge branch 'esp32-variant-pin-spec' into esp32-variant-pin-spec-migrate 2026-10-23 19:05:04 -10:00
J. Nick Koston daeab6987f Merge branch 'dev' into esp32-variant-pin-spec 2026-10-23 19:05:01 -10:00
J. Nick Koston 84a48c5386 [esp32] Name validate_gpio_pin in the ignore_pin_validation_error comment 2026-10-23 11:26:28 -10:00
J. Nick Koston 96a3ac823a Merge branch 'esp32-variant-pin-spec' into esp32-variant-pin-spec-migrate 2026-10-23 10:56:31 -10:00
J. Nick Koston 7a5ace9c21 Merge branch 'dev' into esp32-variant-pin-spec 2026-10-23 10:55:57 -10:00
J. Nick Koston fb48063230 [esp32] Rename _esp32_validations to _esp32_pin_specs and update the bypass comment 2026-10-22 22:37:31 -10:00
J. Nick Koston 977d92b0be Merge branch 'esp32-variant-pin-spec' into esp32-variant-pin-spec-migrate 2026-10-22 22:37:19 -10:00
J. Nick Koston 4db3f0407d [esp32] Note that the pin range check cannot be bypassed 2026-10-22 22:36:49 -10:00
J. Nick Koston d7bacd724d [esp32] Share the S2 and S3 flash and missing pin errors 2026-10-22 22:23:53 -10:00
J. Nick Koston 16e708fa4f Merge branch 'esp32-variant-pin-spec' into esp32-variant-pin-spec-migrate
# Conflicts:
#	esphome/components/esp32/gpio_common.py
2026-10-22 22:20:25 -10:00
J. Nick Koston 7e226093a5 [esp32] Make the pin spec a plain slotted class to keep import cheap 2026-10-22 22:20:08 -10:00
J. Nick Koston cdd35fdc6c [esp32] Map variants straight to their pin specs and inline the USB-JTAG checks 2026-10-22 22:12:29 -10:00
J. Nick Koston ad87b2fef0 [esp32] Describe each variant's GPIO rules with a pin spec 2026-10-22 22:09:05 -10:00
J. Nick Koston 3956a5b7ad [esp32] Add a data driven pin spec for the variant GPIO validators 2026-10-22 22:06:07 -10:00
19 changed files with 722 additions and 833 deletions

No files matched your search

+42 -91
View File
@@ -1,7 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from esphome import pins
import esphome.codegen as cg
@@ -41,23 +39,20 @@ from .const import (
VARIANT_ESP32S31,
esp32_ns,
)
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_supports
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import (
esp32_s3_final_validate_pins,
esp32_s3_validate_gpio_pin,
esp32_s3_validate_supports,
)
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
from .gpio_common import ESP32PinSpec
from .gpio_esp32 import ESP32_PIN_SPEC
from .gpio_esp32_c2 import ESP32_C2_PIN_SPEC
from .gpio_esp32_c3 import ESP32_C3_PIN_SPEC
from .gpio_esp32_c5 import ESP32_C5_PIN_SPEC
from .gpio_esp32_c6 import ESP32_C6_PIN_SPEC
from .gpio_esp32_c61 import ESP32_C61_PIN_SPEC
from .gpio_esp32_h2 import ESP32_H2_PIN_SPEC
from .gpio_esp32_h4 import ESP32_H4_PIN_SPEC
from .gpio_esp32_h21 import ESP32_H21_PIN_SPEC
from .gpio_esp32_p4 import ESP32_P4_PIN_SPEC
from .gpio_esp32_s2 import ESP32_S2_PIN_SPEC
from .gpio_esp32_s3 import ESP32_S3_PIN_SPEC, esp32_s3_final_validate_pins
from .gpio_esp32_s31 import ESP32_S31_PIN_SPEC
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
@@ -98,66 +93,23 @@ def _translate_pin(value):
return _lookup_pin(value)
@dataclass
class ESP32ValidationFunctions:
pin_validation: Callable[[int], int]
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
final_validate: Callable[[ConfigType], None] | None = None
_esp32_validations = {
VARIANT_ESP32: ESP32ValidationFunctions(
pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports
),
VARIANT_ESP32C2: ESP32ValidationFunctions(
pin_validation=esp32_c2_validate_gpio_pin,
usage_validation=esp32_c2_validate_supports,
),
VARIANT_ESP32C3: ESP32ValidationFunctions(
pin_validation=esp32_c3_validate_gpio_pin,
usage_validation=esp32_c3_validate_supports,
),
VARIANT_ESP32C5: ESP32ValidationFunctions(
pin_validation=esp32_c5_validate_gpio_pin,
usage_validation=esp32_c5_validate_supports,
),
VARIANT_ESP32C6: ESP32ValidationFunctions(
pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports,
),
VARIANT_ESP32C61: ESP32ValidationFunctions(
pin_validation=esp32_c61_validate_gpio_pin,
usage_validation=esp32_c61_validate_supports,
),
VARIANT_ESP32H2: ESP32ValidationFunctions(
pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports,
),
VARIANT_ESP32H4: ESP32ValidationFunctions(
pin_validation=esp32_h4_validate_gpio_pin,
usage_validation=esp32_h4_validate_supports,
),
VARIANT_ESP32H21: ESP32ValidationFunctions(
pin_validation=esp32_h21_validate_gpio_pin,
usage_validation=esp32_h21_validate_supports,
),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
),
VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports,
),
VARIANT_ESP32S3: ESP32ValidationFunctions(
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
final_validate=esp32_s3_final_validate_pins,
),
VARIANT_ESP32S31: ESP32ValidationFunctions(
pin_validation=esp32_s31_validate_gpio_pin,
usage_validation=esp32_s31_validate_supports,
),
_esp32_pin_specs: dict[str, ESP32PinSpec] = {
VARIANT_ESP32: ESP32_PIN_SPEC,
VARIANT_ESP32C2: ESP32_C2_PIN_SPEC,
VARIANT_ESP32C3: ESP32_C3_PIN_SPEC,
VARIANT_ESP32C5: ESP32_C5_PIN_SPEC,
VARIANT_ESP32C6: ESP32_C6_PIN_SPEC,
VARIANT_ESP32C61: ESP32_C61_PIN_SPEC,
VARIANT_ESP32H2: ESP32_H2_PIN_SPEC,
VARIANT_ESP32H4: ESP32_H4_PIN_SPEC,
VARIANT_ESP32H21: ESP32_H21_PIN_SPEC,
VARIANT_ESP32P4: ESP32_P4_PIN_SPEC,
VARIANT_ESP32S2: ESP32_S2_PIN_SPEC,
VARIANT_ESP32S3: ESP32_S3_PIN_SPEC,
VARIANT_ESP32S31: ESP32_S31_PIN_SPEC,
}
_final_validations: dict[str, Callable[[ConfigType], None]] = {
VARIANT_ESP32S3: esp32_s3_final_validate_pins,
}
@@ -174,7 +126,7 @@ def gpio_pin_number_validator(value):
return value
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
if variant not in _esp32_pin_specs:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
return value
@@ -182,12 +134,12 @@ def gpio_pin_number_validator(value):
def validate_gpio_pin(pin):
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
if variant not in _esp32_pin_specs:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
ignore_pin_validation_warning = pin[CONF_IGNORE_PIN_VALIDATION_ERROR]
try:
pin[CONF_NUMBER] = _esp32_validations[variant].pin_validation(pin[CONF_NUMBER])
pin[CONF_NUMBER] = _esp32_pin_specs[variant].validate_gpio_pin(pin[CONF_NUMBER])
except cv.Invalid as exc:
if not ignore_pin_validation_warning:
raise
@@ -198,10 +150,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
# `ignore_pin_validation_error` only suppresses an error raised by
# validate_gpio_pin above (e.g. SPI flash/PSRAM pins); pin numbers out of
# range still fail validate_supports. If nothing raised, the option is a
# no-op -- warn so the user can clean it up, but don't block the build.
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
@@ -217,7 +169,7 @@ def validate_supports(value):
is_output = mode[CONF_OUTPUT]
is_open_drain = mode[CONF_OPEN_DRAIN]
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
if variant not in _esp32_validations:
if variant not in _esp32_pin_specs:
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
if is_open_drain and not is_output:
@@ -225,7 +177,7 @@ def validate_supports(value):
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
)
return _esp32_validations[variant].usage_validation(value)
return _esp32_pin_specs[variant].validate_supports(value)
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
@@ -276,6 +228,5 @@ async def esp32_pin_to_code(config):
def final_validate_pins(full_config: ConfigType) -> None:
"""Run the active variant's pin final-validation, if it defines one."""
funcs = _esp32_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])
if funcs is not None and funcs.final_validate is not None:
funcs.final_validate(full_config)
if (final := _final_validations.get(CORE.data[KEY_ESP32][KEY_VARIANT])) is not None:
final(full_config)
+121 -18
View File
@@ -1,26 +1,129 @@
"""Pin checks shared by the ESP32 variant validators."""
from logging import Logger
from typing import Any
from collections.abc import Iterable, Mapping
import logging
from types import MappingProxyType
from esphome.components.const import CONF_HOLD_STATE
from esphome.const import CONF_NUMBER
import esphome.config_validation as cv
from esphome.const import (
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
_NO_PINS: Mapping[int, str] = MappingProxyType({})
_INPUT_ONLY_REJECTED_MODES = (
(CONF_OUTPUT, "does not support output pin mode."),
(CONF_PULLUP, "does not support pullups."),
(CONF_PULLDOWN, "does not support pulldowns."),
)
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
if num in usb_jtag_pins:
logger.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
num,
)
def spi_psram_errors(variant: str, functions: Mapping[int, str]) -> dict[int, str]:
"""Error messages for pins taken by the SPI flash/PSRAM interface."""
return {
num: f"This pin cannot be used on {variant}s and is already used by the"
f" SPI/PSRAM interface (function: {function})"
for num, function in functions.items()
}
def check_usb_jtag_hold(
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
) -> None:
num = conf[CONF_NUMBER]
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
logger.warning(
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
)
def unusable_errors(variant: str, pins: Iterable[int]) -> dict[int, str]:
"""Error messages for pins that are missing from the GPIO matrix."""
return {num: f"The pin GPIO{num} is not usable on {variant}s." for num in pins}
class ESP32PinSpec:
"""Pin facts of one ESP32 variant, with the two validators built from them.
`error_pins` fail pin validation, which `ignore_pin_validation_error` bypasses;
`fatal_pins` and pins above `max_pin` fail both validators, so the option cannot bypass them.
`input_only_label` is inserted after "GPIOn" in the input-only errors.
A plain class, not a dataclass: building a dataclass costs ~0.5 ms of import.
"""
__slots__ = (
"error_pins",
"fatal_pins",
"input_only_label",
"input_only_pins",
"max_pin",
"no_hold_pins",
"strapping_pins",
"usb_jtag_pins",
"warning_pins",
)
def __init__(
self,
*,
max_pin: int,
strapping_pins: frozenset[int],
usb_jtag_pins: frozenset[int] = frozenset(),
error_pins: Mapping[int, str] = _NO_PINS,
fatal_pins: Mapping[int, str] = _NO_PINS,
warning_pins: Mapping[int, str] = _NO_PINS,
input_only_pins: frozenset[int] = frozenset(),
input_only_label: str = "",
no_hold_pins: frozenset[int] = frozenset(),
) -> None:
self.max_pin = max_pin
self.strapping_pins = strapping_pins
self.usb_jtag_pins = usb_jtag_pins
self.error_pins = error_pins
self.fatal_pins = fatal_pins
self.warning_pins = warning_pins
self.input_only_pins = input_only_pins
self.input_only_label = input_only_label
self.no_hold_pins = no_hold_pins
def _check_pin_exists(self, num: int) -> None:
if num < 0 or num > self.max_pin:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-{self.max_pin})")
if (message := self.fatal_pins.get(num)) is not None:
raise cv.Invalid(message)
def validate_gpio_pin(self, value: int) -> int:
self._check_pin_exists(value)
if (message := self.error_pins.get(value)) is not None:
raise cv.Invalid(message)
if (message := self.warning_pins.get(value)) is not None:
_LOGGER.warning("%s", message)
if value in self.usb_jtag_pins:
_LOGGER.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG.",
value,
)
return value
def validate_supports(self, value: ConfigType) -> ConfigType:
num = value[CONF_NUMBER]
self._check_pin_exists(num)
hold = value.get(CONF_HOLD_STATE)
if num in self.input_only_pins:
prefix = f"GPIO{num}{self.input_only_label}"
mode = value[CONF_MODE]
for key, problem in _INPUT_ONLY_REJECTED_MODES:
if mode[key]:
raise cv.Invalid(f"{prefix} {problem}", [CONF_MODE, key])
if hold:
raise cv.Invalid(
f"{prefix} is input-only and cannot be held.", [CONF_HOLD_STATE]
)
if hold and num in self.no_hold_pins:
raise cv.Invalid(f"GPIO{num} has no hold function.", [CONF_HOLD_STATE])
if hold and num in self.usb_jtag_pins:
_LOGGER.warning(
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
)
check_strapping_pin(value, self.strapping_pins, _LOGGER)
return value
+24 -75
View File
@@ -1,17 +1,4 @@
import logging
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
import esphome.config_validation as cv
from esphome.const import (
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
from .gpio_common import ESP32PinSpec, unusable_errors
_ESP_SDIO_PINS = {
6: "Flash Clock",
@@ -20,66 +7,28 @@ _ESP_SDIO_PINS = {
11: "Flash Command",
}
_ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 39:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
if value in _ESP_SDIO_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32s and is already used by the flash interface"
f" (function: {_ESP_SDIO_PINS[value]})."
f" If you are using an ESP32 module that uses a different flash pin"
f" configuration (e.g. ESP32-PICO-V3-02), you can set"
f" 'ignore_pin_validation_error: true' to bypass this check."
)
if 9 <= value <= 10:
_LOGGER.warning(
"Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.",
value,
)
if value in (24, 28, 29, 30, 31):
ESP32_PIN_SPEC = ESP32PinSpec(
max_pin=39,
strapping_pins=frozenset({0, 2, 5, 12, 15}),
error_pins={
**{
num: "This pin cannot be used on ESP32s and is already used by the flash interface"
f" (function: {function})."
" If you are using an ESP32 module that uses a different flash pin"
" configuration (e.g. ESP32-PICO-V3-02), you can set"
" 'ignore_pin_validation_error: true' to bypass this check."
for num, function in _ESP_SDIO_PINS.items()
},
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.")
return value
def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
is_hold = value.get(CONF_HOLD_STATE)
if is_input:
# All ESP32 pins support input mode
pass
if is_output and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support output pin mode.",
[CONF_MODE, CONF_OUTPUT],
)
if is_pullup and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support pullups.", [CONF_MODE, CONF_PULLUP]
)
if is_pulldown and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
)
if is_hold and 34 <= num <= 39:
raise cv.Invalid(
f"GPIO{num} (34-39) is input-only and cannot be held.",
[CONF_HOLD_STATE],
)
if is_hold and num == 20:
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
raise cv.Invalid("GPIO20 has no hold function.", [CONF_HOLD_STATE])
check_strapping_pin(value, _ESP32_STRAPPING_PINS, _LOGGER)
return value
**unusable_errors("ESP32", (24, 28, 29, 30, 31)),
},
warning_pins={
num: f"Pin {num} (9-10) might already be used by the flash interface in QUAD IO flash mode."
for num in (9, 10)
},
input_only_pins=frozenset(range(34, 40)),
input_only_label=" (34-39)",
# Not in the digital hold mask and not an RTC pad, so the driver cannot hold it
no_hold_pins=frozenset({20}),
)
+2 -32
View File
@@ -1,33 +1,3 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32C2_STRAPPING_PINS = {8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 20:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)")
return value
def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 20:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32C2_STRAPPING_PINS, _LOGGER)
return value
ESP32_C2_PIN_SPEC = ESP32PinSpec(max_pin=20, strapping_pins=frozenset({8, 9}))
+17 -51
View File
@@ -1,52 +1,18 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec, spi_psram_errors
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
_ESP32C3_SPI_PSRAM_PINS = {
12: "SPIHD",
13: "SPIWP",
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
}
_ESP32C3_USB_JTAG_PINS = {18, 19}
_ESP32C3_STRAPPING_PINS = {2, 8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 21:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)")
if value in _ESP32C3_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 21:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C3_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C3_STRAPPING_PINS, _LOGGER)
return value
ESP32_C3_PIN_SPEC = ESP32PinSpec(
max_pin=21,
strapping_pins=frozenset({2, 8, 9}),
usb_jtag_pins=frozenset({18, 19}),
error_pins=spi_psram_errors(
"ESP32-C3",
{
12: "SPIHD",
13: "SPIWP",
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
},
),
)
+19 -50
View File
@@ -1,59 +1,28 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from esphome.const import CONF_SCL, CONF_SDA
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
from .gpio_common import ESP32PinSpec, spi_psram_errors
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h
_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3}
_ESP32C5_SPI_PSRAM_PINS = {
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
}
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
_ESP32C5_USB_JTAG_PINS = {13, 14}
_LOGGER = logging.getLogger(__name__)
def esp32_c5_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 28:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
if value in _ESP32C5_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 28:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C5_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
return value
ESP32_C5_PIN_SPEC = ESP32PinSpec(
max_pin=28,
strapping_pins=frozenset({2, 7, 27, 28}),
usb_jtag_pins=frozenset({13, 14}),
error_pins=spi_psram_errors(
"ESP32-C5",
{
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
},
),
)
def esp32_c5_validate_lp_i2c(value):
+19 -49
View File
@@ -1,58 +1,28 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from esphome.const import CONF_SCL, CONF_SDA
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
from .gpio_common import ESP32PinSpec, spi_psram_errors
# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h
_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7}
_ESP32C6_SPI_PSRAM_PINS = {
24: "SPICS0",
25: "SPIQ",
26: "SPIWP",
27: "VDD_SPI",
28: "SPIHD",
29: "SPICLK",
30: "SPID",
}
_ESP32C6_USB_JTAG_PINS = {12, 13}
_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_c6_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 30:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)")
if value in _ESP32C6_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 30:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C6_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER)
return value
ESP32_C6_PIN_SPEC = ESP32PinSpec(
max_pin=30,
strapping_pins=frozenset({4, 5, 8, 9, 15}),
usb_jtag_pins=frozenset({12, 13}),
error_pins=spi_psram_errors(
"ESP32-C6",
{
24: "SPICS0",
25: "SPIQ",
26: "SPIWP",
27: "VDD_SPI",
28: "SPIHD",
29: "SPICLK",
30: "SPID",
},
),
)
def esp32_c6_validate_lp_i2c(value):
+19 -53
View File
@@ -1,54 +1,20 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec, spi_psram_errors
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
_ESP32C61_SPI_PSRAM_PINS = {
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
19: "SPIWP",
20: "SPIHD",
21: "VDD_SPI",
}
_ESP32C61_STRAPPING_PINS = {8, 9}
_ESP32C61_USB_JTAG_PINS = {12, 13}
_LOGGER = logging.getLogger(__name__)
def esp32_c61_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 29:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
if value in _ESP32C61_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})"
)
check_usb_jtag_pin(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
return value
def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 29:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)")
if is_input:
# All ESP32-C61 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32C61_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER)
return value
ESP32_C61_PIN_SPEC = ESP32PinSpec(
max_pin=29,
strapping_pins=frozenset({8, 9}),
usb_jtag_pins=frozenset({12, 13}),
# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM
error_pins=spi_psram_errors(
"ESP32-C61",
{
14: "SPICS0",
15: "SPICLK",
16: "SPID",
17: "SPIQ",
19: "SPIWP",
20: "SPIHD",
21: "VDD_SPI",
},
),
)
+12 -45
View File
@@ -1,46 +1,13 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
_ESP32H2_USB_JTAG_PINS = {26, 27}
_ESP32H2_STRAPPING_PINS = {8, 9, 25}
_LOGGER = logging.getLogger(__name__)
def esp32_h2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 27:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
if value in _ESP32H2_SPI_FLASH_PINS:
_LOGGER.warning(
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html",
value,
)
check_usb_jtag_pin(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
return value
def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 27:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32H2_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
return value
ESP32_H2_PIN_SPEC = ESP32PinSpec(
max_pin=27,
strapping_pins=frozenset({8, 9, 25}),
usb_jtag_pins=frozenset({26, 27}),
warning_pins={
num: f"GPIO{num} is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
"Utilizing SPI-reserved pins could cause unexpected failures.\n"
"See https://docs.espressif.com/projects/esp-idf/en/latest/esp32h2/api-reference/peripherals/gpio.html"
for num in (6, 7, 15, 16, 17, 18, 19, 20, 21)
},
)
+9 -40
View File
@@ -1,41 +1,10 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
_ESP32H21_USB_JTAG_PINS = {17, 18}
_LOGGER = logging.getLogger(__name__)
def esp32_h21_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 25:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
check_usb_jtag_pin(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
return value
def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 25:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-25)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32H21_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
return value
ESP32_H21_PIN_SPEC = ESP32PinSpec(
max_pin=25,
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
strapping_pins=frozenset({13, 14}),
usb_jtag_pins=frozenset({17, 18}),
)
+9 -40
View File
@@ -1,41 +1,10 @@
import logging
from typing import Any
from .gpio_common import ESP32PinSpec
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
_ESP32H4_USB_JTAG_PINS = {13, 14}
_LOGGER = logging.getLogger(__name__)
def esp32_h4_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 39:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
check_usb_jtag_pin(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
return value
def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 39:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-39)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32H4_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
return value
ESP32_H4_PIN_SPEC = ESP32PinSpec(
max_pin=39,
# Partial set from the ESP-IDF / esptool boot-mode docs:
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
strapping_pins=frozenset({13, 14}),
usb_jtag_pins=frozenset({13, 14}),
)
+7 -34
View File
@@ -1,43 +1,16 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from esphome.const import CONF_SCL, CONF_SDA
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
from .gpio_common import ESP32PinSpec
# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf
_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
_ESP32P4_USB_JTAG_PINS = {24, 25}
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
_LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
check_usb_jtag_pin(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
return value
def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 54:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32P4_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
return value
ESP32_P4_PIN_SPEC = ESP32PinSpec(
max_pin=54,
strapping_pins=frozenset({34, 35, 36, 37, 38}),
usb_jtag_pins=frozenset({24, 25}),
)
def esp32_p4_validate_lp_i2c(value):
+24 -75
View File
@@ -1,81 +1,30 @@
import logging
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
import esphome.config_validation as cv
from esphome.const import (
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
_ESP32S2_SPI_PSRAM_PINS = {
26: "SPICS1",
27: "SPIHD",
28: "SPIWP",
29: "SPICS0",
30: "SPICLK",
31: "SPIQ",
32: "SPID",
}
_ESP32S2_STRAPPING_PINS = {0, 45, 46}
_LOGGER = logging.getLogger(__name__)
from .gpio_common import ESP32PinSpec, spi_psram_errors, unusable_errors
def esp32_s2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 46:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)")
if value in _ESP32S2_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-S2s and is already used by the SPI/PSRAM interface (function: {_ESP32S2_SPI_PSRAM_PINS[value]})"
)
if value in (22, 23, 24, 25):
def s2_s3_error_pins(variant: str) -> dict[int, str]:
"""Errors for the flash/PSRAM and missing pins, which S2 and S3 share."""
return {
**spi_psram_errors(
variant,
{
26: "SPICS1",
27: "SPIHD",
28: "SPIWP",
29: "SPICS0",
30: "SPICLK",
31: "SPIQ",
32: "SPID",
},
),
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S2s.")
return value
**unusable_errors(variant, (22, 23, 24, 25)),
}
def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
if num < 0 or num > 46:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)")
if is_input:
# All ESP32 pins support input mode
pass
if is_output and num == 46:
raise cv.Invalid(
f"GPIO{num} does not support output pin mode.",
[CONF_MODE, CONF_OUTPUT],
)
if is_pullup and num == 46:
raise cv.Invalid(
f"GPIO{num} does not support pullups.", [CONF_MODE, CONF_PULLUP]
)
if is_pulldown and num == 46:
raise cv.Invalid(
f"GPIO{num} does not support pulldowns.", [CONF_MODE, CONF_PULLDOWN]
)
if value.get(CONF_HOLD_STATE) and num == 46:
raise cv.Invalid(
f"GPIO{num} is input-only and cannot be held.",
[CONF_HOLD_STATE],
)
check_strapping_pin(value, _ESP32S2_STRAPPING_PINS, _LOGGER)
return value
ESP32_S2_PIN_SPEC = ESP32PinSpec(
max_pin=46,
strapping_pins=frozenset({0, 45, 46}),
error_pins=s2_s3_error_pins("ESP32-S2"),
input_only_pins=frozenset({46}),
)
+10 -61
View File
@@ -1,28 +1,11 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import (
CONF_DISABLED,
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
PLATFORM_ESP32,
)
from esphome.pins import PIN_SCHEMA_REGISTRY, check_strapping_pin
from esphome.const import CONF_DISABLED, CONF_MODE, PLATFORM_ESP32
from esphome.pins import PIN_SCHEMA_REGISTRY
from esphome.types import ConfigType
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
_ESP32S3_SPI_PSRAM_PINS = {
26: "SPICS1",
27: "SPIHD",
28: "SPIWP",
29: "SPICS0",
30: "SPICLK",
31: "SPIQ",
32: "SPID",
}
from .gpio_common import ESP32PinSpec
from .gpio_esp32_s2 import s2_s3_error_pins
_ESP32S3R8_PSRAM_PINS = {
33: "SPIIO4",
@@ -32,48 +15,14 @@ _ESP32S3R8_PSRAM_PINS = {
37: "SPIDQS",
}
_ESP32S3_USB_JTAG_PINS = {19, 20}
_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46}
_LOGGER = logging.getLogger(__name__)
def esp32_s3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 48:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)")
if value in _ESP32S3_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})"
)
# GPIO33-37 (_ESP32S3R8_PSRAM_PINS) are only taken by the PSRAM interface in
# octal mode -- whether that applies isn't known here, so the warning is
# deferred to final_validate_pins() in gpio.py once the PSRAM mode is resolved.
if value in (22, 23, 24, 25):
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.")
check_usb_jtag_pin(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
return value
def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 48:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32S3_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER)
return value
ESP32_S3_PIN_SPEC = ESP32PinSpec(
max_pin=48,
strapping_pins=frozenset({0, 3, 45, 46}),
usb_jtag_pins=frozenset({19, 20}),
error_pins=s2_s3_error_pins("ESP32-S3"),
)
def esp32_s3_final_validate_pins(full_config: ConfigType) -> None:
+18 -51
View File
@@ -1,60 +1,27 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
from esphome.const import CONF_SCL, CONF_SDA
from .gpio_common import check_usb_jtag_hold, check_usb_jtag_pin
from .gpio_common import ESP32PinSpec
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
# Per the ESP32-S31 IDF DOCS and datasheet:
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
_ESP32S31_USB_JTAG_PINS = {33, 34}
_LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
)
check_usb_jtag_pin(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
return value
def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
check_usb_jtag_hold(value, _ESP32S31_USB_JTAG_PINS, _LOGGER)
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
return value
ESP32_S31_PIN_SPEC = ESP32PinSpec(
max_pin=61,
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
strapping_pins=frozenset({36, 37, 60, 61}),
usb_jtag_pins=frozenset({33, 34}),
error_pins={
num: f"GPIO{num} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
for num in (26, 27, 28, 30, 31, 32)
},
# Not bonded, so they can never work
fatal_pins={num: f"GPIO{num} does not exist on ESP32-S31." for num in (29, 41)},
)
def esp32_s31_validate_lp_i2c(value):
@@ -15,32 +15,25 @@ ESPHOME_LOG_TAG(TAG, "http_request.ota");
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
OtaHttpRequestComponent::Request &OtaHttpRequestComponent::get_request_() {
if (this->request_ == nullptr)
this->request_ = make_unique<Request>();
return *this->request_;
}
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
if (!this->validate_url_(url)) {
this->get_request_().md5_url.clear(); // URL was not valid; prevent flashing until it is
this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
return;
}
Request &request = this->get_request_();
request.md5_url = url;
request.md5_expected.clear(); // to be retrieved later
this->md5_url_ = url;
this->md5_expected_.clear(); // to be retrieved later
}
void OtaHttpRequestComponent::set_url(const std::string &url) {
if (!this->validate_url_(url)) {
this->get_request_().url.clear(); // URL was not valid; prevent flashing until it is
this->url_.clear(); // URL was not valid; prevent flashing until it is
return;
}
this->get_request_().url = url;
this->url_ = url;
}
void OtaHttpRequestComponent::flash() {
if (this->request_ == nullptr || this->request_->url.empty()) {
if (this->url_.empty()) {
ESP_LOGE(TAG, "URL not set; cannot start update");
return;
}
@@ -65,8 +58,8 @@ void OtaHttpRequestComponent::flash() {
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
#endif
this->request_->md5_computed.clear(); // will be reset at next attempt
this->request_->md5_expected.clear(); // will be reset at next attempt
this->md5_computed_.clear(); // will be reset at next attempt
this->md5_expected_.clear(); // will be reset at next attempt
break;
}
}
@@ -88,18 +81,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
md5::MD5Digest md5_receive;
char md5_receive_str[33];
if (this->request_->md5_expected.empty() && !this->http_get_md5_()) {
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
return OTA_MD5_INVALID;
}
ESP_LOGD(TAG, "MD5 expected: %s", this->request_->md5_expected.c_str());
ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
auto url_with_auth = this->get_url_with_auth_(this->request_->url);
auto url_with_auth = this->get_url_with_auth_(this->url_);
if (url_with_auth.empty()) {
return OTA_BAD_URL;
}
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->request_->url.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
auto container = this->parent_->get(url_with_auth);
@@ -186,9 +179,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// verify MD5 is as expected and act accordingly
md5_receive.calculate();
md5_receive.get_hex(md5_receive_str);
this->request_->md5_computed = md5_receive_str;
if (strncmp(this->request_->md5_computed.c_str(), this->request_->md5_expected.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->request_->md5_computed.c_str());
this->md5_computed_ = md5_receive_str;
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
@@ -230,15 +223,11 @@ static std::string url_encode(const std::string &str) {
return result;
}
void OtaHttpRequestComponent::set_password(const std::string &password) {
this->get_request_().password = url_encode(password);
}
void OtaHttpRequestComponent::set_username(const std::string &username) {
this->get_request_().username = url_encode(username);
}
void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); }
void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); }
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
if (this->request_->username.empty() || this->request_->password.empty()) {
if (this->username_.empty() || this->password_.empty()) {
return url;
}
@@ -251,23 +240,23 @@ std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url)
ESP_LOGD(TAG, "Using basic HTTP authentication");
start_char += 3; // skip '://' characters
auto url_with_auth = url.substr(0, start_char) + this->request_->username + ":" + this->request_->password + "@" +
url.substr(start_char);
auto url_with_auth =
url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
return url_with_auth;
}
bool OtaHttpRequestComponent::http_get_md5_() {
if (this->request_->md5_url.empty()) {
if (this->md5_url_.empty()) {
return false;
}
auto url_with_auth = this->get_url_with_auth_(this->request_->md5_url);
auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
if (url_with_auth.empty()) {
return false;
}
ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->request_->md5_url.c_str());
ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
auto container = this->parent_->get(url_with_auth);
if (container == nullptr) {
ESP_LOGE(TAG, "Failed to connect to MD5 URL");
@@ -284,8 +273,8 @@ bool OtaHttpRequestComponent::http_get_md5_() {
return false;
}
this->request_->md5_expected.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->request_->md5_expected.data(), MD5_SIZE, MD5_SIZE,
this->md5_expected_.resize(MD5_SIZE);
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
container->end();
@@ -27,35 +27,29 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_md5_url(const std::string &md5_url);
void set_md5(const std::string &md5) { this->get_request_().md5_expected = md5; }
void set_md5(const std::string &md5) { this->md5_expected_ = md5; }
void set_password(const std::string &password);
void set_url(const std::string &url);
void set_username(const std::string &username);
std::string md5_computed() { return this->request_ != nullptr ? this->request_->md5_computed : std::string(); }
std::string md5_expected() { return this->request_ != nullptr ? this->request_->md5_expected : std::string(); }
std::string md5_computed() { return this->md5_computed_; }
std::string md5_expected() { return this->md5_expected_; }
void flash();
protected:
/// Allocated by the first setter, which only runs before a flash, and never freed, so a flash never loses it.
struct Request {
std::string md5_computed;
std::string md5_expected;
std::string md5_url;
std::string password;
std::string username;
std::string url;
};
Request &get_request_();
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
bool validate_url_(const std::string &url);
std::unique_ptr<Request> request_;
std::string md5_computed_{};
std::string md5_expected_{};
std::string md5_url_{};
std::string password_{};
std::string username_{};
std::string url_{};
int status_ = -1;
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
};
@@ -0,0 +1,336 @@
"""Tests for the data driven ESP32 pin validators."""
import pytest
from esphome.components.const import CONF_HOLD_STATE
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
)
from esphome.components.esp32.gpio import _esp32_pin_specs
from esphome.components.esp32.gpio_common import (
ESP32PinSpec,
spi_psram_errors,
unusable_errors,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_IGNORE_STRAPPING_WARNING,
CONF_INPUT,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.types import ConfigType
SPEC = ESP32PinSpec(
max_pin=40,
strapping_pins=frozenset({0, 3}),
usb_jtag_pins=frozenset({3, 19}),
error_pins={
**spi_psram_errors("ESP32-X1", {26: "SPICS1"}),
**unusable_errors("ESP32-X1", (22,)),
},
fatal_pins={29: "GPIO29 does not exist on ESP32-X1."},
warning_pins={9: "GPIO9 might be used by flash."},
input_only_pins=frozenset({34, 35}),
input_only_label=" (34-35)",
no_hold_pins=frozenset({20}),
)
def _conf(num: int, hold: bool = False, **mode: bool) -> ConfigType:
return {
CONF_NUMBER: num,
CONF_MODE: {
CONF_INPUT: False,
CONF_OUTPUT: False,
CONF_PULLUP: False,
CONF_PULLDOWN: False,
CONF_OPEN_DRAIN: False,
**mode,
},
CONF_HOLD_STATE: hold,
}
@pytest.mark.parametrize(
("num", "message"),
[
(-1, "Invalid pin number: -1 (must be 0-40)"),
(41, "Invalid pin number: 41 (must be 0-40)"),
(29, "GPIO29 does not exist on ESP32-X1."),
],
)
def test_rejected_by_both_validators(num: int, message: str) -> None:
for validate, value in (
(SPEC.validate_gpio_pin, num),
(SPEC.validate_supports, _conf(num, input=True)),
):
with pytest.raises(cv.Invalid) as exc:
validate(value)
assert exc.value.msg == message
@pytest.mark.parametrize(
("num", "message"),
[
(
26,
(
"This pin cannot be used on ESP32-X1s and is already used by the"
" SPI/PSRAM interface (function: SPICS1)"
),
),
(22, "The pin GPIO22 is not usable on ESP32-X1s."),
],
)
def test_error_pins_only_fail_pin_validation(num: int, message: str) -> None:
with pytest.raises(cv.Invalid) as exc:
SPEC.validate_gpio_pin(num)
assert exc.value.msg == message
SPEC.validate_supports(_conf(num, output=True))
def test_warning_and_usb_jtag_pins(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level("WARNING"):
assert SPEC.validate_gpio_pin(9) == 9
assert SPEC.validate_gpio_pin(19) == 19
assert SPEC.validate_gpio_pin(5) == 5
assert caplog.messages == [
"GPIO9 might be used by flash.",
(
"GPIO19 is used by the USB-Serial-JTAG interface."
" Using this pin as GPIO will conflict with USB-Serial-JTAG."
),
]
@pytest.mark.parametrize(
("mode", "problem"),
[
(CONF_OUTPUT, "does not support output pin mode."),
(CONF_PULLUP, "does not support pullups."),
(CONF_PULLDOWN, "does not support pulldowns."),
],
)
def test_input_only_pins_reject_modes(mode: str, problem: str) -> None:
with pytest.raises(cv.Invalid) as exc:
SPEC.validate_supports(_conf(35, input=True, **{mode: True}))
assert exc.value.msg == f"GPIO35 (34-35) {problem}"
assert exc.value.path == [CONF_MODE, mode]
# The same modes are fine on a normal pin
SPEC.validate_supports(_conf(5, input=True, **{mode: True}))
@pytest.mark.parametrize(
("num", "message"),
[
(34, "GPIO34 (34-35) is input-only and cannot be held."),
(20, "GPIO20 has no hold function."),
],
)
def test_pins_that_cannot_hold(num: int, message: str) -> None:
SPEC.validate_supports(_conf(num, input=True))
with pytest.raises(cv.Invalid) as exc:
SPEC.validate_supports(_conf(num, hold=True, input=True))
assert exc.value.msg == message
assert exc.value.path == [CONF_HOLD_STATE]
def test_usb_jtag_hold_and_strapping_warnings(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level("WARNING"):
SPEC.validate_supports(_conf(3, hold=True, output=True))
assert len(caplog.messages) == 2
assert caplog.messages[0] == (
"GPIO3 cannot hold at low level during wakeup from deep sleep."
)
assert caplog.messages[1].startswith("GPIO3 is a strapping pin")
def test_ignore_strapping_warning_on_normal_pin_rejected() -> None:
conf = _conf(5, input=True)
conf[CONF_IGNORE_STRAPPING_WARNING] = True
with pytest.raises(cv.Invalid, match="GPIO5 is not a strapping pin"):
SPEC.validate_supports(conf)
_USB_JTAG = "is used by the USB-Serial-JTAG interface"
@pytest.mark.parametrize(
("variant", "num", "error", "warning"),
[
(VARIANT_ESP32, 4, None, None),
(
VARIANT_ESP32,
6,
"used by the flash interface (function: Flash Clock).",
None,
),
(VARIANT_ESP32, 9, None, "Pin 9 (9-10) might already be used by the flash"),
(VARIANT_ESP32, 24, "The pin GPIO24 is not usable on ESP32s.", None),
(VARIANT_ESP32, 40, "Invalid pin number: 40 (must be 0-39)", None),
(VARIANT_ESP32C2, 8, None, None),
(VARIANT_ESP32C2, 21, "Invalid pin number: 21 (must be 0-20)", None),
(
VARIANT_ESP32C3,
12,
"used on ESP32-C3s and is already used by the SPI/PSRAM",
None,
),
(VARIANT_ESP32C3, 18, None, _USB_JTAG),
(
VARIANT_ESP32C5,
19,
"ESP32-C5s and is already used by the SPI/PSRAM interface",
None,
),
(VARIANT_ESP32C5, 13, None, _USB_JTAG),
(VARIANT_ESP32C6, 27, "SPI/PSRAM interface (function: VDD_SPI)", None),
(VARIANT_ESP32C6, 12, None, _USB_JTAG),
(
VARIANT_ESP32C61,
21,
"used on ESP32-C61s and is already used by the SPI/PSRAM",
None,
),
(VARIANT_ESP32C61, 18, None, None),
(
VARIANT_ESP32H2,
6,
None,
"GPIO6 is reserved for SPI Flash communication on some",
),
(VARIANT_ESP32H2, 26, None, _USB_JTAG),
(VARIANT_ESP32H2, 28, "Invalid pin number: 28 (must be 0-27)", None),
(VARIANT_ESP32H21, 17, None, _USB_JTAG),
(VARIANT_ESP32H4, 13, None, _USB_JTAG),
(VARIANT_ESP32H4, 40, "Invalid pin number: 40 (must be 0-39)", None),
(VARIANT_ESP32P4, 24, None, _USB_JTAG),
(VARIANT_ESP32P4, 55, "Invalid pin number: 55 (must be 0-54)", None),
(
VARIANT_ESP32S2,
26,
"ESP32-S2s and is already used by the SPI/PSRAM interface",
None,
),
(VARIANT_ESP32S2, 22, "The pin GPIO22 is not usable on ESP32-S2s.", None),
(VARIANT_ESP32S3, 33, None, None),
(VARIANT_ESP32S3, 19, None, _USB_JTAG),
(VARIANT_ESP32S3, 25, "The pin GPIO25 is not usable on ESP32-S3s.", None),
(VARIANT_ESP32S3, 32, "SPI/PSRAM interface (function: SPID)", None),
(
VARIANT_ESP32S31,
26,
"GPIO26 is reserved for the SPI flash interface on ESP32-S31",
None,
),
(VARIANT_ESP32S31, 29, "GPIO29 does not exist on ESP32-S31.", None),
(VARIANT_ESP32S31, 33, None, _USB_JTAG),
(VARIANT_ESP32S31, 62, "Invalid pin number: 62 (must be 0-61)", None),
],
)
def test_variant_pin_validation(
variant: str,
num: int,
error: str | None,
warning: str | None,
caplog: pytest.LogCaptureFixture,
) -> None:
spec = _esp32_pin_specs[variant]
with caplog.at_level("WARNING"):
if error is None:
assert spec.validate_gpio_pin(num) == num
else:
with pytest.raises(cv.Invalid) as exc:
spec.validate_gpio_pin(num)
assert error in exc.value.msg
if warning is None:
assert not caplog.messages
else:
assert len(caplog.messages) == 1
assert warning in caplog.messages[0]
@pytest.mark.parametrize(
("variant", "conf", "error"),
[
(
VARIANT_ESP32,
_conf(36, output=True),
"GPIO36 (34-39) does not support output",
),
(
VARIANT_ESP32,
_conf(39, pullup=True),
"GPIO39 (34-39) does not support pullups.",
),
(
VARIANT_ESP32,
_conf(34, hold=True),
"GPIO34 (34-39) is input-only and cannot",
),
(VARIANT_ESP32, _conf(20, hold=True), "GPIO20 has no hold function."),
(VARIANT_ESP32, _conf(40), "Invalid pin number: 40 (must be 0-39)"),
(
VARIANT_ESP32S2,
_conf(46, pulldown=True),
"GPIO46 does not support pulldowns.",
),
(
VARIANT_ESP32S2,
_conf(46, hold=True),
"GPIO46 is input-only and cannot be held.",
),
(VARIANT_ESP32S31, _conf(41), "GPIO41 does not exist on ESP32-S31."),
(VARIANT_ESP32C2, _conf(21), "Invalid pin number: 21 (must be 0-20)"),
],
)
def test_variant_supports_errors(variant: str, conf: ConfigType, error: str) -> None:
with pytest.raises(cv.Invalid) as exc:
_esp32_pin_specs[variant].validate_supports(conf)
assert error in exc.value.msg
@pytest.mark.parametrize(
("variant", "conf", "warnings"),
[
(VARIANT_ESP32, _conf(0, output=True), ["GPIO0 is a strapping pin"]),
(VARIANT_ESP32, _conf(35, input=True), []),
(VARIANT_ESP32C3, _conf(18, hold=True), ["GPIO18 cannot hold at low level"]),
(
VARIANT_ESP32H4,
_conf(13, hold=True),
["GPIO13 cannot hold at low level", "GPIO13 is a strapping pin"],
),
(VARIANT_ESP32S3, _conf(46, input=True), ["GPIO46 is a strapping pin"]),
],
)
def test_variant_supports_warnings(
variant: str,
conf: ConfigType,
warnings: list[str],
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level("WARNING"):
_esp32_pin_specs[variant].validate_supports(conf)
assert len(caplog.messages) == len(warnings)
for message, expected in zip(caplog.messages, warnings, strict=True):
assert message.startswith(expected)
-17
View File
@@ -72,23 +72,6 @@ button:
id(ota_update).set_source_url(std::string("http://my.ha.net:8123/local/esphome/manifest.json"));
ESP_LOGI("test", "Manifest is now %s", id(ota_update).get_source_url());
- component.update: ota_update
# The OTA component's public setters and getters as lambdas use them; every shape must compile.
- platform: template
name: Flash from lambda
on_press:
then:
- lambda: |-
std::string url = "http://my.ha.net:8123/local/esphome/";
url += "firmware.bin";
id(http_request_ota).set_md5_url("http://my.ha.net:8123/local/esphome/firmware.md5");
id(http_request_ota).set_md5(std::string("0123456789abcdef0123456789abcdef"));
id(http_request_ota).set_username("user");
id(http_request_ota).set_password(std::string("pass"));
id(http_request_ota).set_url(url);
id(http_request_ota).set_url(url.c_str());
ESP_LOGI("test", "MD5 %s / %s", id(http_request_ota).md5_expected().c_str(),
id(http_request_ota).md5_computed().c_str());
id(http_request_ota).flash();
update:
- platform: http_request