[uart] Keep constant switch, button and write payloads in flash (#20082)

This commit is contained in:
J. Nick Koston
2026-10-12 01:16:34 +00:00
committed by GitHub
parent ed6ab4565d
commit 0cd239dfd1
14 changed files with 137 additions and 80 deletions
+8 -13
View File
@@ -137,6 +137,10 @@ def validate_raw_data(value):
)
# Lengths are stored as uint16_t; reject longer payloads at validation.
validate_raw_payload = cv.All(validate_raw_data, cv.Length(max=65535))
def validate_rx_pin(value):
value = pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266 and value[CONF_NUMBER] >= 16:
@@ -596,7 +600,7 @@ async def register_uart_device(var, config):
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(UARTComponent),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
cv.Required(CONF_DATA): cv.templatable(validate_raw_payload),
},
key=CONF_DATA,
),
@@ -605,18 +609,9 @@ async def register_uart_device(var, config):
async def uart_write_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, "uart_data"
)
return var
+4 -25
View File
@@ -8,36 +8,15 @@
namespace esphome::uart {
template<typename... Ts> class UARTWriteAction final : public Action<Ts...>, public Parented<UARTComponent> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
this->code_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
// Store pointer to static data in flash (no RAM copy)
void set_data_static(const uint8_t *data, size_t len) {
this->code_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
}
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
if (this->len_ >= 0) {
// Static mode: use pointer and length
this->parent_->write_array(this->code_.data, static_cast<size_t>(this->len_));
if (this->data_.is_static()) {
this->parent_->write_array_progmem(this->data_.data(), this->data_.size());
} else {
// Template mode: call function
auto val = this->code_.func(x...);
this->parent_->write_array(val);
this->parent_->write_array(this->data_.value(x...));
}
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Code {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} code_;
};
} // namespace esphome::uart
+4 -6
View File
@@ -1,10 +1,10 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import button, uart
import esphome.config_validation as cv
from esphome.const import CONF_DATA
from esphome.core import HexInt
from .. import uart_ns, validate_raw_data
from .. import uart_ns, validate_raw_payload
CODEOWNERS = ["@ssieb"]
@@ -17,7 +17,7 @@ CONFIG_SCHEMA = (
button.button_schema(UARTButton)
.extend(
{
cv.Required(CONF_DATA): validate_raw_data,
cv.Required(CONF_DATA): validate_raw_payload,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
@@ -31,6 +31,4 @@ async def to_code(config):
await uart.register_uart_device(var, config)
data = config[CONF_DATA]
if isinstance(data, bytes):
data = [HexInt(x) for x in data]
cg.add(var.set_data(cg.ArrayInitializer(*data)))
cg.add(var.set_data(automation.progmem_bytes("uart_data", data), len(data)))
@@ -7,7 +7,7 @@ ESPHOME_LOG_TAG(TAG, "uart.button");
void UARTButton::press_action() {
ESP_LOGD(TAG, "'%s': Sending data", this->get_name().c_str());
this->write_array(this->data_.data(), this->data_.size());
this->write_array_progmem(this->data_, this->data_len_);
}
void UARTButton::dump_config() { LOG_BUTTON("", "UART Button", this); }
+6 -5
View File
@@ -4,20 +4,21 @@
#include "esphome/components/uart/uart.h"
#include "esphome/components/button/button.h"
#include <vector>
namespace esphome::uart {
class UARTButton final : public button::Button, public UARTDevice, public Component {
public:
void set_data(std::vector<uint8_t> &&data) { this->data_ = std::move(data); }
void set_data(std::initializer_list<uint8_t> data) { this->data_ = std::vector<uint8_t>(data); }
void set_data(const uint8_t *data, uint16_t len) {
this->data_ = data;
this->data_len_ = len;
}
void dump_config() override;
protected:
void press_action() override;
std::vector<uint8_t> data_;
const uint8_t *data_{nullptr};
uint16_t data_len_{0};
};
} // namespace esphome::uart
+15 -19
View File
@@ -1,10 +1,10 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import switch, uart
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_SEND_EVERY
from esphome.core import HexInt
from .. import uart_ns, validate_raw_data
from .. import uart_ns, validate_raw_payload
DEPENDENCIES = ["uart"]
@@ -18,11 +18,11 @@ CONFIG_SCHEMA = (
.extend(
{
cv.Required(CONF_DATA): cv.Any(
validate_raw_data,
validate_raw_payload,
cv.Schema(
{
cv.Optional(CONF_TURN_OFF): validate_raw_data,
cv.Optional(CONF_TURN_ON): validate_raw_data,
cv.Optional(CONF_TURN_OFF): validate_raw_payload,
cv.Optional(CONF_TURN_ON): validate_raw_payload,
}
),
),
@@ -40,20 +40,16 @@ async def to_code(config):
await uart.register_uart_device(var, config)
data = config[CONF_DATA]
if isinstance(data, dict):
if data_on := data.get(CONF_TURN_ON):
if isinstance(data_on, bytes):
data_on = [HexInt(x) for x in data_on]
cg.add(var.set_data_on(cg.ArrayInitializer(*data_on)))
if data_off := data.get(CONF_TURN_OFF):
if isinstance(data_off, bytes):
data_off = [HexInt(x) for x in data_off]
cg.add(var.set_data_off(cg.ArrayInitializer(*data_off)))
else:
data = config[CONF_DATA]
if isinstance(data, bytes):
data = [HexInt(x) for x in data]
cg.add(var.set_data_on(cg.ArrayInitializer(*data)))
single_state = not isinstance(data, dict)
if single_state:
data = {CONF_TURN_ON: data}
for key, setter in (
(CONF_TURN_ON, var.set_data_on),
(CONF_TURN_OFF, var.set_data_off),
):
if payload := data.get(key):
cg.add(setter(automation.progmem_bytes("uart_data", payload), len(payload)))
if single_state:
cg.add(var.set_single_state(True))
if CONF_SEND_EVERY in config:
cg.add(var.set_send_every(config[CONF_SEND_EVERY]))
@@ -17,13 +17,13 @@ void UARTSwitch::loop() {
}
void UARTSwitch::write_command_(bool state) {
if (state && !this->data_on_.empty()) {
if (state && this->data_on_len_ != 0) {
ESP_LOGD(TAG, "'%s': Sending on data", this->get_name().c_str());
this->write_array(this->data_on_.data(), this->data_on_.size());
this->write_array_progmem(this->data_on_, this->data_on_len_);
}
if (!state && !this->data_off_.empty()) {
if (!state && this->data_off_len_ != 0) {
ESP_LOGD(TAG, "'%s': Sending off data", this->get_name().c_str());
this->write_array(this->data_off_.data(), this->data_off_.size());
this->write_array_progmem(this->data_off_, this->data_off_len_);
}
}
+12 -7
View File
@@ -5,7 +5,6 @@
#include "esphome/components/switch/switch.h"
#include <cinttypes>
#include <vector>
namespace esphome::uart {
@@ -13,10 +12,14 @@ class UARTSwitch final : public switch_::Switch, public UARTDevice, public Compo
public:
void loop() override;
void set_data_on(std::vector<uint8_t> &&data) { this->data_on_ = std::move(data); }
void set_data_on(std::initializer_list<uint8_t> data) { this->data_on_ = std::vector<uint8_t>(data); }
void set_data_off(std::vector<uint8_t> &&data) { this->data_off_ = std::move(data); }
void set_data_off(std::initializer_list<uint8_t> data) { this->data_off_ = std::vector<uint8_t>(data); }
void set_data_on(const uint8_t *data, uint16_t len) {
this->data_on_ = data;
this->data_on_len_ = len;
}
void set_data_off(const uint8_t *data, uint16_t len) {
this->data_off_ = data;
this->data_off_len_ = len;
}
void set_send_every(uint32_t send_every) { this->send_every_ = send_every; }
void set_single_state(bool single) { this->single_state_ = single; }
@@ -25,8 +28,10 @@ class UARTSwitch final : public switch_::Switch, public UARTDevice, public Compo
protected:
void write_command_(bool state);
void write_state(bool state) override;
std::vector<uint8_t> data_on_;
std::vector<uint8_t> data_off_;
const uint8_t *data_on_{nullptr};
const uint8_t *data_off_{nullptr};
uint16_t data_on_len_{0};
uint16_t data_off_len_{0};
bool single_state_{false};
uint32_t send_every_;
uint32_t last_transmission_;
+1
View File
@@ -25,6 +25,7 @@ class UARTDevice {
size_t get_rx_timeout() { return this->parent_->get_rx_timeout(); }
void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); }
void write_array_progmem(const uint8_t *data, size_t len) { this->parent_->write_array_progmem(data, len); }
void write_array(const std::vector<uint8_t> &data) { this->parent_->write_array(data); }
template<size_t N> void write_array(const std::array<uint8_t, N> &data) {
this->parent_->write_array(data.data(), data.size());
@@ -46,4 +46,17 @@ void UARTComponent::set_rx_full_threshold_ms(uint8_t time) {
this->set_rx_full_threshold(val);
}
#ifdef USE_ESP8266
void UARTComponent::write_array_progmem(const uint8_t *data, size_t len) {
uint8_t buf[32];
while (len > 0) {
const size_t chunk = std::min(len, sizeof(buf));
progmem_memcpy(buf, data, chunk);
this->write_array(buf, chunk);
data += chunk;
len -= chunk;
}
}
#endif
} // namespace esphome::uart
+7
View File
@@ -65,6 +65,13 @@ class UARTComponent {
// @param len Length of the array.
virtual void write_array(const uint8_t *data, size_t len) = 0;
// Writes bytes that may live in PROGMEM; ESP8266 cannot read flash bytewise, so it copies chunks.
#ifdef USE_ESP8266
void write_array_progmem(const uint8_t *data, size_t len);
#else
void write_array_progmem(const uint8_t *data, size_t len) { this->write_array(data, len); }
#endif
// Reads a single byte from the UART bus.
// @param data Pointer to the byte where the read data will be stored.
// @return True if a byte was successfully read, false otherwise.
@@ -0,0 +1,34 @@
---
esphome:
name: test
on_boot:
then:
- uart.write: [0x01, 0x02, 0x03]
- uart.write: !lambda return {0x09};
- uart.write: ""
esp32:
board: esp32dev
uart:
tx_pin: 17
rx_pin: 16
baud_rate: 9600
switch:
- platform: uart
id: shared_switch
name: Shared Switch
data: [0x01, 0x02, 0x03]
- platform: uart
id: on_off_switch
name: On Off Switch
data:
turn_on: "ON"
turn_off: [0x04]
button:
- platform: uart
id: shared_button
name: Shared Button
data: [0x01, 0x02, 0x03]
@@ -0,0 +1,28 @@
"""Tests for UART constant payloads in shared PROGMEM tables."""
from collections.abc import Callable
from pathlib import Path
import re
def test_constant_payloads_share_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal payloads share one table across switch, button and write action."""
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
tables = dict(
re.findall(
r"static constexpr uint8_t (\w+)\[\] PROGMEM = (\{[^}]*\});", main_cpp
)
)
shared = next(k for k, v in tables.items() if v == "{0x01, 0x02, 0x03}")
assert sorted(tables.values()) == sorted(
["{0x01, 0x02, 0x03}", "{0x4F, 0x4E}", "{0x04}"]
)
assert f"shared_switch->set_data_on({shared}, 3);" in main_cpp
assert f"shared_button->set_data({shared}, 3);" in main_cpp
assert f"set_data_static({shared}, 3);" in main_cpp
assert "set_data_static(nullptr, 0);" in main_cpp
assert "set_data_template(" in main_cpp