diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index befef872f0..5d615f0edf 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -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))) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index de16c55cb0..bdd6615d43 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -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(©, 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_); diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 9ccea4d15a..a102da3ff6 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -1,5 +1,7 @@ #pragma once +#include + #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, "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 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_; diff --git a/tests/component_tests/remote_receiver/config/rc_switch_protocol.yaml b/tests/component_tests/remote_receiver/config/rc_switch_protocol.yaml new file mode 100644 index 0000000000..d79080cedf --- /dev/null +++ b/tests/component_tests/remote_receiver/config/rc_switch_protocol.yaml @@ -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 diff --git a/tests/component_tests/remote_receiver/test_rc_switch_protocol.py b/tests/component_tests/remote_receiver/test_rc_switch_protocol.py new file mode 100644 index 0000000000..5c0d14c794 --- /dev/null +++ b/tests/component_tests/remote_receiver/test_rc_switch_protocol.py @@ -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