diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 2713bae241a..ae299370cd7 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -6,6 +6,8 @@ import esphome.codegen as cg from esphome.components import canbus from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed from esphome.components.esp32 import ( + CONF_MINIMUM_CHIP_REVISION, + KEY_ESP32, VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, @@ -20,6 +22,8 @@ from esphome.components.esp32 import ( ) import esphome.config_validation as cv from esphome.const import ( + CONF_ADVANCED, + CONF_FRAMEWORK, CONF_ID, CONF_MODE, CONF_RX_PIN, @@ -27,6 +31,7 @@ from esphome.const import ( CONF_TX_PIN, CONF_TX_QUEUE_LEN, ) +import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@Sympatron"] @@ -49,6 +54,7 @@ CAN_MODES = { # See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI) CAN_SPEEDS_ESP32 = { + "20KBPS": CanSpeed.CAN_20KBPS, "25KBPS": CanSpeed.CAN_25KBPS, "50KBPS": CanSpeed.CAN_50KBPS, "100KBPS": CanSpeed.CAN_100KBPS, @@ -114,6 +120,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( ) +def _final_validate(config: ConfigType) -> None: + if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS": + esp32_config = fv.full_config.get()[KEY_ESP32] + min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get( + CONF_MINIMUM_CHIP_REVISION + ) + if min_revision is None or min_revision < "2.0": + raise cv.Invalid( + "20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher", + path=[CONF_BIT_RATE], + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + def get_default_tx_enqueue_timeout(bit_rate: str) -> int: bit_rate_numeric = canbus.get_rate(bit_rate) bits_per_packet = 140 # ~max CAN message length diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index b98ba853e78..d84b58c5b0c 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config case canbus::CAN_16KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS(); return true; +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200) case canbus::CAN_20KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS(); return true; diff --git a/tests/components/esp32_can/test-20kbps.esp32-idf.yaml b/tests/components/esp32_can/test-20kbps.esp32-idf.yaml new file mode 100644 index 00000000000..fb60cb7363f --- /dev/null +++ b/tests/components/esp32_can/test-20kbps.esp32-idf.yaml @@ -0,0 +1,11 @@ +esp32: + framework: + advanced: + minimum_chip_revision: "2.0" + +canbus: + - platform: esp32_can + rx_pin: GPIO14 + tx_pin: GPIO12 + can_id: 4 + bit_rate: 20kbps diff --git a/tests/unit_tests/components/test_esp32_can.py b/tests/unit_tests/components/test_esp32_can.py new file mode 100644 index 00000000000..5b70ce1d812 --- /dev/null +++ b/tests/unit_tests/components/test_esp32_can.py @@ -0,0 +1,54 @@ +"""Tests for ESP32 CAN bitrate validation.""" + +import pytest + +from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT +from esphome.components.esp32_can import canbus +import esphome.config_validation as cv +from esphome.core import CORE +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("revision", "supported"), + [ + (None, False), + ("0.0", False), + ("1.0", False), + ("1.1", False), + ("2.0", True), + ("3.0", True), + ("3.1", True), + ], +) +def test_esp32_20kbps_minimum_revision(revision: str | None, supported: bool) -> None: + """Require the chip revision needed by the ESP-IDF timing macro.""" + CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32"} + advanced = {} if revision is None else {"minimum_chip_revision": revision} + token = fv.full_config.set({"esp32": {"framework": {"advanced": advanced}}}) + try: + config = {"bit_rate": canbus.validate_bit_rate("20kbps")} + if supported: + canbus.FINAL_VALIDATE_SCHEMA(config) + else: + with pytest.raises(cv.Invalid, match="minimum_chip_revision") as error: + canbus.FINAL_VALIDATE_SCHEMA(config) + assert error.value.path == ["bit_rate"] + finally: + fv.full_config.reset(token) + + +@pytest.mark.parametrize("variant", ["ESP32", "ESP32C3", "ESP32S3"]) +def test_other_bitrates(variant: str) -> None: + """Existing bitrates must not require a minimum chip revision.""" + CORE.data[KEY_ESP32] = {KEY_VARIANT: variant} + config = {"bit_rate": canbus.validate_bit_rate("25kbps")} + canbus.FINAL_VALIDATE_SCHEMA(config) + + +@pytest.mark.parametrize("variant", ["ESP32C3", "ESP32S2", "ESP32S3", "ESP32C6"]) +def test_other_variants_20kbps(variant: str) -> None: + """Other supported ESP32 variants must retain 20 kbit/s support.""" + CORE.data[KEY_ESP32] = {KEY_VARIANT: variant} + config = {"bit_rate": canbus.validate_bit_rate("20kbps")} + canbus.FINAL_VALIDATE_SCHEMA(config)