[remote_base] Keep RC switch protocols in flash (#20081)

This commit is contained in:
J. Nick Koston
2026-10-05 10:54:03 -05:00
committed by GitHub
parent a058937fe0
commit 666cf169b9
5 changed files with 133 additions and 19 deletions
+31 -13
View File
@@ -1538,9 +1538,7 @@ def validate_rc_switch_raw_code(value):
return value
def build_rc_switch_protocol(config):
if isinstance(config, int):
return rc_switch_protocol(config)
def build_custom_rc_switch_protocol(config: ConfigType) -> MockObj:
pl = config[CONF_PULSE_LENGTH]
return RCSwitchBase(
config[CONF_SYNC][0] * pl,
@@ -1553,6 +1551,24 @@ def build_rc_switch_protocol(config):
)
def rc_switch_protocol_in_flash(config: int | ConfigType) -> MockObj:
"""Pointer to the protocol in flash: a built-in table entry or a shared custom table."""
if isinstance(config, int):
return cg.RawExpression(f"&{RC_SWITCH_PROTOCOLS}[{config}]")
return cg.shared_progmem_array(
"rc_switch_custom_protocol",
RCSwitchBase,
[build_custom_rc_switch_protocol(config)],
)
def rc_switch_protocol_value(config: int | ConfigType) -> MockObj:
"""RAM copy of a constant protocol for the transmit actions, read from its flash table."""
if isinstance(config, int):
return rc_switch_protocol(config)
return rc_switch_protocol_copy(rc_switch_protocol_in_flash(config))
RC_SWITCH_RAW_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): validate_rc_switch_raw_code,
@@ -1628,6 +1644,8 @@ RC_SWITCH_TRANSMITTER = cv.Schema(
)
rc_switch_protocol = ns.rc_switch_protocol
rc_switch_protocol_copy = ns.rc_switch_protocol_copy
RC_SWITCH_PROTOCOLS = ns.RC_SWITCH_PROTOCOLS
RCSwitchData = ns.struct("RCSwitchData")
RCSwitchBase = ns.class_("RCSwitchBase")
RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger)
@@ -1642,7 +1660,7 @@ RCSwitchRawReceiver = ns.class_("RCSwitchRawReceiver", RemoteReceiverBinarySenso
@register_binary_sensor("rc_switch_raw", RCSwitchRawReceiver, RC_SWITCH_RAW_SCHEMA)
def rc_switch_raw_binary_sensor(var, config):
cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL])))
cg.add(var.set_protocol(rc_switch_protocol_in_flash(config[CONF_PROTOCOL])))
cg.add(var.set_code(config[CONF_CODE]))
@@ -1653,7 +1671,7 @@ def rc_switch_raw_binary_sensor(var, config):
)
async def rc_switch_raw_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_code(await cg.templatable(config[CONF_CODE], args, cg.std_string)))
@@ -1663,7 +1681,7 @@ async def rc_switch_raw_action(var, config, args):
"rc_switch_type_a", RCSwitchRawReceiver, RC_SWITCH_TYPE_A_SCHEMA
)
def rc_switch_type_a_binary_sensor(var, config):
cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL])))
cg.add(var.set_protocol(rc_switch_protocol_in_flash(config[CONF_PROTOCOL])))
cg.add(var.set_type_a(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE]))
@@ -1674,7 +1692,7 @@ def rc_switch_type_a_binary_sensor(var, config):
)
async def rc_switch_type_a_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
@@ -1688,7 +1706,7 @@ async def rc_switch_type_a_action(var, config, args):
"rc_switch_type_b", RCSwitchRawReceiver, RC_SWITCH_TYPE_B_SCHEMA
)
def rc_switch_type_b_binary_sensor(var, config):
cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL])))
cg.add(var.set_protocol(rc_switch_protocol_in_flash(config[CONF_PROTOCOL])))
cg.add(
var.set_type_b(config[CONF_ADDRESS], config[CONF_CHANNEL], config[CONF_STATE])
)
@@ -1701,7 +1719,7 @@ def rc_switch_type_b_binary_sensor(var, config):
)
async def rc_switch_type_b_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)))
@@ -1713,7 +1731,7 @@ async def rc_switch_type_b_action(var, config, args):
"rc_switch_type_c", RCSwitchRawReceiver, RC_SWITCH_TYPE_C_SCHEMA
)
def rc_switch_type_c_binary_sensor(var, config):
cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL])))
cg.add(var.set_protocol(rc_switch_protocol_in_flash(config[CONF_PROTOCOL])))
cg.add(
var.set_type_c(
config[CONF_FAMILY],
@@ -1731,7 +1749,7 @@ def rc_switch_type_c_binary_sensor(var, config):
)
async def rc_switch_type_c_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(
@@ -1748,7 +1766,7 @@ async def rc_switch_type_c_action(var, config, args):
RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
def rc_switch_type_d_binary_sensor(var, config):
cg.add(var.set_protocol(build_rc_switch_protocol(config[CONF_PROTOCOL])))
cg.add(var.set_protocol(rc_switch_protocol_in_flash(config[CONF_PROTOCOL])))
cg.add(var.set_type_d(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE]))
@@ -1759,7 +1777,7 @@ def rc_switch_type_d_binary_sensor(var, config):
)
async def rc_switch_type_d_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
@@ -9,12 +9,16 @@ namespace esphome::remote_base {
static const char *const TAG = "remote.rc_switch";
RCSwitchBase rc_switch_protocol(uint8_t index) {
RCSwitchBase protocol;
// entry 0 is the all-zero protocol, so an out of range index from a lambda transmits nothing
if (index >= std::size(RC_SWITCH_PROTOCOLS))
index = 0;
progmem_memcpy(&protocol, &RC_SWITCH_PROTOCOLS[index], sizeof(protocol));
return protocol;
return rc_switch_protocol_copy(&RC_SWITCH_PROTOCOLS[index]);
}
RCSwitchBase rc_switch_protocol_copy(const RCSwitchBase *protocol) {
RCSwitchBase copy;
progmem_memcpy(&copy, protocol, sizeof(copy));
return copy;
}
void RCSwitchBase::one(RemoteTransmitData *dst) const {
@@ -232,7 +236,7 @@ uint64_t decode_binary_string_mask(const std::string &data) {
bool RCSwitchRawReceiver::matches(RemoteReceiveData src) {
uint64_t decoded_code;
uint8_t decoded_nbits;
if (!this->protocol_.decode(src, &decoded_code, &decoded_nbits))
if (!this->protocol_->decode(src, &decoded_code, &decoded_nbits))
return false;
return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_);
@@ -1,5 +1,7 @@
#pragma once
#include <type_traits>
#include "esphome/core/component.h"
#include "remote_base.h"
@@ -66,6 +68,9 @@ class RCSwitchBase {
uint32_t one_high_{};
uint32_t one_low_{};
uint32_t inverted_{}; // bool widened so every field is a word: the table is read from flash
// A bool here would still pad to 28 bytes, so the size check below alone would not catch it.
static_assert(std::is_same_v<decltype(inverted_), uint32_t>, "inverted_ must stay a word for flash reads");
};
// Constant-initialized and kept in flash on every platform. The decoder reads entries in place
@@ -87,6 +92,9 @@ inline constexpr RCSwitchBase RC_SWITCH_PROTOCOLS[] PROGMEM = {
/// RAM copy of RC_SWITCH_PROTOCOLS[index] (0 when out of range) for the transmit actions and the dumper, made with
/// progmem_memcpy so no byte load ever touches the flash table on ESP8266
RCSwitchBase rc_switch_protocol(uint8_t index);
/// RAM copy of a protocol stored in flash, made with progmem_memcpy (own name: `rc_switch_protocol(0)` stays
/// unambiguous)
RCSwitchBase rc_switch_protocol_copy(const RCSwitchBase *protocol);
uint64_t decode_binary_string(const std::string &data);
@@ -200,7 +208,8 @@ template<typename... Ts> class RCSwitchTypeDAction : public RemoteTransmitterAct
class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase {
public:
void set_protocol(const RCSwitchBase &a_protocol) { this->protocol_ = a_protocol; }
/// `protocol` must outlive the receiver: a RC_SWITCH_PROTOCOLS entry or a codegen flash table.
void set_protocol(const RCSwitchBase *protocol) { this->protocol_ = protocol; }
void set_code(uint64_t code) { this->code_ = code; }
void set_code(const std::string &code) {
this->code_ = decode_binary_string(code);
@@ -228,7 +237,7 @@ class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase {
protected:
bool matches(RemoteReceiveData src) override;
RCSwitchBase protocol_;
const RCSwitchBase *protocol_{nullptr}; // in flash; decoded in place (word-only fields)
uint64_t code_;
uint64_t mask_{0xFFFFFFFFFFFFFFFF};
uint8_t nbits_;
@@ -0,0 +1,58 @@
---
esphome:
name: test
esp8266:
board: esp01_1m
remote_transmitter:
pin: GPIO4
carrier_duty_percent: 100%
remote_receiver:
pin: GPIO5
button:
- platform: template
name: Numbered
on_press:
- remote_transmitter.transmit_rc_switch_raw:
code: '1000100000000000'
protocol: 1
- platform: template
name: Custom A
on_press:
- remote_transmitter.transmit_rc_switch_raw:
code: '1010101010101010'
protocol:
pulse_length: 320
- platform: template
name: Custom B
on_press:
- remote_transmitter.transmit_rc_switch_type_b:
address: 4
channel: 2
state: true
protocol:
pulse_length: 320
- platform: template
name: Lambda
on_press:
- remote_transmitter.transmit_rc_switch_raw:
code: '1010101010101011'
protocol: !lambda return remote_base::rc_switch_protocol(2);
binary_sensor:
- platform: remote_receiver
name: Numbered RX
rc_switch_raw:
code: '1000100000000000'
protocol: 1
- platform: remote_receiver
name: Custom RX
rc_switch_raw:
code: '1010101010101010'
protocol:
pulse_length: 320
- platform: template
id: rc_switch_custom_protocol
@@ -0,0 +1,25 @@
"""RC switch protocols are read from flash tables instead of being copied per entity."""
from collections.abc import Callable
from pathlib import Path
def test_rc_switch_protocols_share_flash_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("rc_switch_protocol.yaml"))
# One shared table for the identical custom protocols; the user id keeps its name.
assert main_cpp.count("static constexpr remote_base::RCSwitchBase") == 1
assert (
"remote_base::RCSwitchBase rc_switch_custom_protocol_2[] PROGMEM = "
"{remote_base::RCSwitchBase(320, 9920, 320, 960, 960, 320, false)};"
) in main_cpp
assert main_cpp.count("rc_switch_protocol_copy(rc_switch_custom_protocol_2)") == 2
# Receivers point into flash instead of holding a copy.
assert "->set_protocol(&remote_base::RC_SWITCH_PROTOCOLS[1]);" in main_cpp
assert "->set_protocol(rc_switch_custom_protocol_2);" in main_cpp
# Numbered and lambda protocols keep their existing paths.
assert "return remote_base::rc_switch_protocol(1);" in main_cpp
assert "return remote_base::rc_switch_protocol(2);" in main_cpp