From 12c03d539ac7a712d2d79f2dbd6a5b7ac35e5d18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Oct 2026 10:54:56 -1000 Subject: [PATCH] [cc1101] Keep constant send_packet payloads in shared flash tables (#20264) --- esphome/components/cc1101/__init__.py | 24 +++++------ esphome/components/cc1101/cc1101.cpp | 28 +++++++++++-- esphome/components/cc1101/cc1101.h | 25 +++-------- tests/component_tests/cc1101/__init__.py | 0 .../cc1101/config/payload_tables.yaml | 25 +++++++++++ .../cc1101/test_cc1101_payload_tables.py | 41 +++++++++++++++++++ 6 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 tests/component_tests/cc1101/__init__.py create mode 100644 tests/component_tests/cc1101/config/payload_tables.yaml create mode 100644 tests/component_tests/cc1101/test_cc1101_payload_tables.py diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 5de39a16ce..65d5d6d9f1 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -13,7 +13,6 @@ from esphome.const import ( CONF_VALUE, CONF_WAIT_TIME, ) -from esphome.core import ID CODEOWNERS = ["@lygris", "@gabest11"] DEPENDENCIES = ["spi"] @@ -384,7 +383,10 @@ def validate_raw_data(value): SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(CC1101Component), - cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + # TX FIFO size + cv.Required(CONF_DATA): cv.templatable( + cv.All(validate_raw_data, cv.Length(max=64)) + ), }, key=CONF_DATA, ) @@ -399,17 +401,13 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - data = config[CONF_DATA] - if isinstance(data, bytes): - data = list(data) - if cg.is_template(data): - templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) - cg.add(var.set_data_template(templ)) - else: - # Generate static array in flash to avoid RAM copy - arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) - arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) - cg.add(var.set_data_static(arr, len(data))) + await automation.templatable_bytes( + config[CONF_DATA], + args, + var.set_data_template, + var.set_data_static, + "cc1101_data", + ) return var diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index f7b90b91cf..91a29d8d53 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -3,10 +3,16 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include +#include + +#ifdef USE_ESP32 +#include +#endif namespace esphome::cc1101 { static const char *const TAG = "cc1101"; +static constexpr size_t TX_FIFO_SIZE = 64; static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) { int e_tmp; @@ -379,18 +385,32 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) { this->disable(); } -CC1101Error CC1101Component::transmit_packet(const std::vector &packet) { +CC1101Error CC1101Component::transmit_packet(const uint8_t *data, size_t len) { if (this->state_.PKT_FORMAT != static_cast(PacketFormat::PACKET_FORMAT_FIFO)) { return CC1101Error::PARAMS; } + const bool variable = this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE); + // In variable length mode the FIFO also holds the length byte + if (len + (variable ? 1 : 0) > TX_FIFO_SIZE) { + ESP_LOGE(TAG, "Packet of %u bytes does not fit the TX FIFO", static_cast(len)); + return CC1101Error::PARAMS; + } // Write packet this->enter_idle_(); this->strobe_(Command::FTX); - if (this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE)) { - this->write_(Register::FIFO, static_cast(packet.size())); + if (variable) { + this->write_(Register::FIFO, static_cast(len)); } - this->write_(Register::FIFO, packet.data(), packet.size()); +#ifdef USE_ESP32 + // SPI DMA cannot read flash or unaligned buffers; stage them here so the driver does not allocate a copy + alignas(4) uint8_t staged[TX_FIFO_SIZE]; + if (len != 0 && (!esp_ptr_dma_capable(data) || (reinterpret_cast(data) & 3) != 0)) { + std::memcpy(staged, data, len); + data = staged; + } +#endif + this->write_(Register::FIFO, data, len); // Calibrate PLL if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) { diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 499f77a809..d7065c6048 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -89,7 +89,10 @@ class CC1101Component final : public Component, void set_whitening(bool value); // Packet mode operations - CC1101Error transmit_packet(const std::vector &packet); + CC1101Error transmit_packet(const std::vector &packet) { + return this->transmit_packet(packet.data(), packet.size()); + } + CC1101Error transmit_packet(const uint8_t *data, size_t len); void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); } Trigger, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; } @@ -131,27 +134,11 @@ class CC1101Component final : public Component, // Action Wrappers template class SendPacketAction final : public Action, public Parented { - public: - void set_data_template(std::function(Ts...)> func) { this->data_func_ = func; } - void set_data_static(const uint8_t *data, size_t len) { - this->data_static_ = data; - this->data_static_len_ = len; - } + TEMPLATABLE_BYTES(data) void play(const Ts &...x) override { - if (this->data_func_) { - auto data = this->data_func_(x...); - this->parent_->transmit_packet(data); - } else if (this->data_static_ != nullptr) { - std::vector data(this->data_static_, this->data_static_ + this->data_static_len_); - this->parent_->transmit_packet(data); - } + this->data_.visit([this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...); } - - protected: - std::function(Ts...)> data_func_{}; - const uint8_t *data_static_{nullptr}; - size_t data_static_len_{0}; }; } // namespace esphome::cc1101 diff --git a/tests/component_tests/cc1101/__init__.py b/tests/component_tests/cc1101/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/cc1101/config/payload_tables.yaml b/tests/component_tests/cc1101/config/payload_tables.yaml new file mode 100644 index 0000000000..b478cd6bdb --- /dev/null +++ b/tests/component_tests/cc1101/config/payload_tables.yaml @@ -0,0 +1,25 @@ +--- +esphome: + name: test + on_boot: + then: + - cc1101.send_packet: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - cc1101.send_packet: + data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + - cc1101.send_packet: !lambda return {0x09}; + +esp32: + board: esp32dev + +spi: + clk_pin: 18 + mosi_pin: 23 + miso_pin: 19 + +cc1101: + id: transceiver + cs_pin: 5 + gdo0_pin: 4 + frequency: 433.92MHz + packet_mode: true + packet_length: 8 diff --git a/tests/component_tests/cc1101/test_cc1101_payload_tables.py b/tests/component_tests/cc1101/test_cc1101_payload_tables.py new file mode 100644 index 0000000000..f2d3264a32 --- /dev/null +++ b/tests/component_tests/cc1101/test_cc1101_payload_tables.py @@ -0,0 +1,41 @@ +"""Tests for cc1101 constant payloads in shared PROGMEM tables.""" + +from collections.abc import Callable +from pathlib import Path +import re + +import pytest + +from esphome.components.cc1101 import SEND_PACKET_ACTION_SCHEMA +import esphome.config_validation as cv + + +def test_constant_payloads_share_progmem_tables( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Equal send_packet payloads share one table; lambdas stay templates.""" + main_cpp = generate_main(component_config_path("payload_tables.yaml")) + + tables = re.findall( + r"static constexpr uint8_t (\w+)\[\] PROGMEM = (\{[^}]*\});", main_cpp + ) + assert [v for _, v in tables] == [ + "{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}" + ] + assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2 + assert "set_data_template(" in main_cpp + + +@pytest.mark.parametrize("size", [64, 65]) +@pytest.mark.parametrize("make", [lambda n: [0x01] * n, lambda n: "a" * n]) +def test_send_packet_tx_fifo_limit( + size: int, make: Callable[[int], list[int] | str] +) -> None: + """Payloads up to the 64 byte TX FIFO are accepted, longer ones rejected.""" + config = {"id": "transceiver", "data": make(size)} + if size <= 64: + assert len(SEND_PACKET_ACTION_SCHEMA(config)["data"]) == size + else: + with pytest.raises(cv.Invalid, match="at most 64"): + SEND_PACKET_ACTION_SCHEMA(config)