[remote_base] Generate the byte payload transmit actions with no C++ class (#20568)

This commit is contained in:
J. Nick Koston
2026-10-12 07:14:19 -10:00
committed by GitHub
parent 9e8845885b
commit 72ea9ae458
17 changed files with 223 additions and 217 deletions
+77 -91
View File
@@ -322,18 +322,33 @@ def _protocol_field(
return automation.ApplyField(key, f"data.{member} = {{}}", type_)
_BYTES = cg.std_vector.template(cg.uint8)
def _code_table(config: ConfigType, value: list[int]) -> str:
return f"{automation.progmem_bytes('remote_base_code', value)}, {len(value)}"
def _bytes_field(conf_key: str, target: str) -> automation.ApplyField:
"""A constant's flash table and length, or a lambda's vector; the callee takes both."""
return automation.ApplyField(conf_key, target, _BYTES, const_fn=_code_table)
def register_protocol_action(
name: str,
protocol: str,
schema: cv.Schema,
*fields: ProtocolField,
validate: Callable[[ConfigType], ConfigType] | None = None,
call: str = "protocol_call",
) -> None:
"""Register ``transmit_<name>`` with no C++ action class.
One generated function fills ``<protocol>Data`` from ``(conf_key, member, type_)`` fields
and sends it; the action is a core ``ApplyAction``. ``ApplyField`` and ``ApplyCall`` pass through
and target the call object, whose protocol data is ``data``.
``call="encode_call"`` targets a plain transmit call whose ``encode()`` runs one of the
protocol's encoders, for payloads it encodes straight from a pointer and length.
"""
action, schema, validator = _transmit_action(name, schema, validate)
builder = automation.apply_action_builder(
@@ -344,7 +359,7 @@ def register_protocol_action(
automation.ApplyField(
(CONF_REPEAT, CONF_WAIT_TIME), "set_send_wait", cg.uint32
),
call=f"protocol_call<{ns.class_(f'{protocol}Protocol')}>",
call=f"{call}<{ns.class_(f'{protocol}Protocol')}>",
id_key=CONF_TRANSMITTER_ID,
)
@@ -2375,10 +2390,7 @@ register_protocol_action(
# Midea
MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol(
"Midea"
)
MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase)
MideaData, MideaBinarySensor, MideaTrigger, _, MideaDumper = declare_protocol("Midea")
MIDEA_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=5, max=5)),
@@ -2401,26 +2413,17 @@ def midea_dumper(var, config):
pass
async def _code_bytes(
value: Any, args: TemplateArgsType, set_template: MockObj, set_static: MockObj
) -> None:
"""Constant codes share one PROGMEM table pool across the remote protocols."""
await automation.templatable_bytes(
value, args, set_template, set_static, "remote_base_code"
)
@register_action("midea", MideaAction, MIDEA_SCHEMA)
async def midea_action(var, config, args):
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
register_protocol_action(
"midea",
"Midea",
MIDEA_SCHEMA,
_bytes_field(CONF_CODE, f"data = {MideaData}({{}})"),
automation.ApplyCall("data.finalize()"),
)
# AEHA
AEHAData, AEHABinarySensor, AEHATrigger, AEHAAction, AEHADumper = declare_protocol(
"AEHA"
)
AEHAData, AEHABinarySensor, AEHATrigger, _, AEHADumper = declare_protocol("AEHA")
AEHA_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
@@ -2452,9 +2455,9 @@ def aeha_dumper(var, config):
pass
@register_action(
register_protocol_action(
"aeha",
AEHAAction,
"AEHA",
AEHA_SCHEMA.extend(
{
cv.Optional(CONF_CARRIER_FREQUENCY, default="38000Hz"): cv.All(
@@ -2462,15 +2465,18 @@ def aeha_dumper(var, config):
),
}
),
automation.ApplyField(
CONF_CARRIER_FREQUENCY, "get_data()->set_carrier_frequency({})", cg.uint32
),
automation.ApplyCall(
"encode({}, {})",
(
(CONF_ADDRESS, cg.uint16),
(CONF_DATA, _BYTES, _code_table),
),
),
call="encode_call",
)
async def aeha_action(var, config, args):
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
cg.add(var.set_address(template_))
await _code_bytes(
config[CONF_DATA], args, var.set_data_template, var.set_data_static
)
templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
cg.add(var.set_carrier_frequency(templ))
# Hob2Hood
@@ -2529,10 +2535,7 @@ register_protocol_action(
# Haier
HaierData, HaierBinarySensor, HaierTrigger, HaierAction, HaierDumper = declare_protocol(
"Haier"
)
HaierAction = ns.class_("HaierAction", RemoteTransmitterActionBase)
HaierData, HaierBinarySensor, HaierTrigger, _, HaierDumper = declare_protocol("Haier")
HAIER_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.All(
@@ -2562,11 +2565,13 @@ def haier_dumper(var, config):
pass
@register_action("haier", HaierAction, HAIER_SCHEMA)
async def haier_action(var, config, args):
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
register_protocol_action(
"haier",
"Haier",
HAIER_SCHEMA,
_bytes_field(CONF_CODE, "encode({})"),
call="encode_call",
)
# ABBWelcome
@@ -2574,7 +2579,7 @@ async def haier_action(var, config, args):
ABBWelcomeData,
ABBWelcomeBinarySensor,
ABBWelcomeTrigger,
ABBWelcomeAction,
_,
ABBWelcomeDumper,
) = declare_protocol("ABBWelcome")
@@ -2626,49 +2631,28 @@ def abbwelcome_dumper(var, config):
pass
@register_action("abbwelcome", ABBWelcomeAction, ABB_WELCOME_SCHEMA)
async def abbwelcome_action(var, config, args):
cg.add(
var.set_three_byte_address(
await cg.templatable(config[CONF_THREE_BYTE_ADDRESS], args, cg.bool_)
)
)
cg.add(
var.set_source_address(
await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint32)
)
)
cg.add(
var.set_destination_address(
await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint32)
)
)
cg.add(
var.set_retransmission(
await cg.templatable(config[CONF_RETRANSMISSION], args, cg.bool_)
)
)
cg.add(
var.set_message_type(
await cg.templatable(config[CONF_MESSAGE_TYPE], args, cg.uint8)
)
)
template_ = await cg.templatable(CONF_MESSAGE_ID not in config, args, cg.bool_)
cg.add(var.set_auto_message_id(template_))
if CONF_MESSAGE_ID in config:
cg.add(
var.set_message_id(
await cg.templatable(config[CONF_MESSAGE_ID], args, cg.uint8)
)
)
# TemplatableBytes starts in lambda mode with no function, so a transmit without data sets an empty payload
await automation.templatable_bytes(
config.get(CONF_DATA, []),
args,
var.set_data_template,
var.set_data_static,
"abbwelcome_data",
)
register_protocol_action(
"abbwelcome",
"ABBWelcome",
ABB_WELCOME_SCHEMA,
# First: the address length decides where the other fields go
automation.ApplyField(
CONF_THREE_BYTE_ADDRESS, "data.set_three_byte_address({})", cg.bool_
),
automation.ApplyField(
CONF_SOURCE_ADDRESS, "data.set_source_address({})", cg.uint32
),
automation.ApplyField(
CONF_DESTINATION_ADDRESS, "data.set_destination_address({})", cg.uint32
),
automation.ApplyField(CONF_RETRANSMISSION, "data.set_retransmission({})", cg.bool_),
automation.ApplyField(CONF_MESSAGE_TYPE, "data.set_message_type({})", cg.uint8),
# Without message_id, finalize() picks one
automation.ApplyCall("data.auto_message_id = true"),
automation.ApplyField(CONF_MESSAGE_ID, "data.set_fixed_message_id({})", cg.uint8),
_bytes_field(CONF_DATA, "data.set_data({})"),
automation.ApplyCall("data.finalize()"),
)
# Mirage
@@ -2676,7 +2660,7 @@ async def abbwelcome_action(var, config, args):
MirageData,
MirageBinarySensor,
MirageTrigger,
MirageAction,
_,
MirageDumper,
) = declare_protocol("Mirage")
@@ -2702,11 +2686,13 @@ def mirage_dumper(var, config):
pass
@register_action("mirage", MirageAction, MIRAGE_SCHEMA)
async def mirage_action(var, config, args):
await _code_bytes(
config[CONF_CODE], args, var.set_code_template, var.set_code_static
)
register_protocol_action(
"mirage",
"Mirage",
MIRAGE_SCHEMA,
_bytes_field(CONF_CODE, "encode({})"),
call="encode_call",
)
# Toto
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "remote_base.h"
#include <array>
@@ -111,13 +112,19 @@ class ABBWelcomeData {
return (this->data_[7] << 16) + (this->data_[8] << 8) + this->data_[9];
}
void set_message_id(uint8_t message_id) { this->data_[4 + 2 * this->get_address_length()] = message_id; }
/// A configured id, which finalize() keeps instead of picking one
void set_fixed_message_id(uint8_t message_id) {
this->set_message_id(message_id);
this->auto_message_id = false;
}
uint8_t get_message_id() const { return this->data_[4 + 2 * this->get_address_length()]; }
/// data may be a PROGMEM table
void set_data(const uint8_t *data, size_t len) {
uint8_t size = static_cast<uint8_t>(std::min<size_t>(MAX_DATA_LENGTH, len));
this->data_[2] &= (0xff ^ DATA_LENGTH_MASK);
this->data_[2] |= (size & DATA_LENGTH_MASK);
if (size)
std::copy_n(data, size, this->data_.begin() + 5 + 2 * this->get_address_length());
progmem_memcpy(this->data_.data() + 5 + 2 * this->get_address_length(), data, size);
}
void set_data(const std::vector<uint8_t> &data) { this->set_data(data.data(), data.size()); }
std::vector<uint8_t> get_data() const {
@@ -226,30 +233,4 @@ class ABBWelcomeBinarySensor : public RemoteReceiverBinarySensorBase {
using ABBWelcomeTrigger = RemoteReceiverTrigger<ABBWelcomeProtocol>;
using ABBWelcomeDumper = RemoteReceiverDumper<ABBWelcomeProtocol>;
template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterActionBase<Ts...> {
TEMPLATABLE_VALUE(uint32_t, source_address)
TEMPLATABLE_VALUE(uint32_t, destination_address)
TEMPLATABLE_VALUE(bool, retransmission)
TEMPLATABLE_VALUE(bool, three_byte_address)
TEMPLATABLE_VALUE(uint8_t, message_type)
TEMPLATABLE_VALUE(uint8_t, message_id)
TEMPLATABLE_VALUE(bool, auto_message_id)
TEMPLATABLE_BYTES(data)
void encode(RemoteTransmitData *dst, Ts... x) override {
ABBWelcomeData data;
data.set_three_byte_address(this->three_byte_address_.value(x...));
data.set_source_address(this->source_address_.value(x...));
data.set_destination_address(this->destination_address_.value(x...));
data.set_retransmission(this->retransmission_.value(x...));
data.set_message_type(this->message_type_.value(x...));
data.set_message_id(this->message_id_.value(x...));
data.auto_message_id = this->auto_message_id_.value(x...);
this->data_.template visit<MAX_DATA_LENGTH>(
[&data](const uint8_t *bytes, size_t len) { data.set_data(bytes, len); }, x...);
data.finalize();
ABBWelcomeProtocol().encode(dst, data);
}
};
} // namespace esphome::remote_base
@@ -1,4 +1,5 @@
#include "aeha_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -28,7 +29,7 @@ void AEHAProtocol::encode(RemoteTransmitData *dst, uint16_t address, const uint8
}
for (size_t i = 0; i < len; i++) {
const uint8_t bit = data[i];
const uint8_t bit = progmem_read_byte(data + i);
for (uint8_t mask = 1 << 7; mask != 0; mask >>= 1) {
if (bit & mask) {
dst->item(BIT_HIGH_US, BIT_ONE_LOW_US);
+4 -14
View File
@@ -18,7 +18,11 @@ class AEHAProtocol : public RemoteProtocol<AEHAData> {
void encode(RemoteTransmitData *dst, const AEHAData &data) {
this->encode(dst, data.address, data.data.data(), data.data.size());
}
/// data may be a PROGMEM table
void encode(RemoteTransmitData *dst, uint16_t address, const uint8_t *data, size_t len);
void encode(RemoteTransmitData *dst, uint16_t address, const std::vector<uint8_t> &data) {
this->encode(dst, address, data.data(), data.size());
}
optional<AEHAData> decode(RemoteReceiveData src);
void dump(const AEHAData &data);
@@ -28,18 +32,4 @@ class AEHAProtocol : public RemoteProtocol<AEHAData> {
DECLARE_REMOTE_PROTOCOL(AEHA)
template<typename... Ts> class AEHAAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint16_t, address)
TEMPLATABLE_BYTES(data)
TEMPLATABLE_VALUE(uint32_t, carrier_frequency);
void encode(RemoteTransmitData *dst, Ts... x) override {
const uint16_t address = this->address_.value(x...);
dst->set_carrier_frequency(this->carrier_frequency_.value(x...));
this->data_.visit(
[dst, address](const uint8_t *data, size_t len) { AEHAProtocol().encode(dst, address, data, len); }, x...);
}
};
} // namespace esphome::remote_base
@@ -1,4 +1,5 @@
#include "haier_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -39,7 +40,7 @@ void HaierProtocol::encode(RemoteTransmitData *dst, const uint8_t *data, size_t
dst->mark(BIT_MARK_US);
uint8_t checksum = 0;
for (size_t i = 0; i < len; i++) {
const uint8_t item = data[i];
const uint8_t item = progmem_read_byte(data + i);
this->encode_byte_(dst, item);
checksum += item;
}
@@ -14,7 +14,11 @@ struct HaierData {
class HaierProtocol : public RemoteProtocol<HaierData> {
public:
void encode(RemoteTransmitData *dst, const HaierData &data) { this->encode(dst, data.data.data(), data.data.size()); }
/// data may be a PROGMEM table
void encode(RemoteTransmitData *dst, const uint8_t *data, size_t len);
void encode(RemoteTransmitData *dst, const std::vector<uint8_t> &data) {
this->encode(dst, data.data(), data.size());
}
optional<HaierData> decode(RemoteReceiveData src);
void dump(const HaierData &data);
@@ -24,13 +28,4 @@ class HaierProtocol : public RemoteProtocol<HaierData> {
DECLARE_REMOTE_PROTOCOL(Haier)
template<typename... Ts> class HaierAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
this->code_.visit([dst](const uint8_t *data, size_t len) { HaierProtocol().encode(dst, data, len); }, x...);
}
};
} // namespace esphome::remote_base
@@ -78,16 +78,4 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
DECLARE_REMOTE_PROTOCOL(Midea)
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
MideaData data = this->code_.is_static() ? MideaData(this->code_.data(), this->code_.size())
: MideaData(this->code_.value(x...));
data.finalize();
MideaProtocol().encode(dst, data);
}
};
} // namespace esphome::remote_base
@@ -1,7 +1,10 @@
#include "mirage_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::remote_base {
ESPHOME_LOG_TAG(TAG, "remote.mirage");
@@ -17,8 +20,12 @@ constexpr unsigned int MIRAGE_IR_PACKET_BIT_SIZE = 120;
constexpr size_t MIRAGE_MAX_DATA_BYTES = (MIRAGE_IR_PACKET_BIT_SIZE / 8);
void MirageProtocol::encode(RemoteTransmitData *dst, const uint8_t *data, size_t len) {
// data may be a PROGMEM table, so log a RAM copy
uint8_t log_buf[MIRAGE_MAX_DATA_BYTES];
const size_t log_len = std::min(len, MIRAGE_MAX_DATA_BYTES);
progmem_memcpy(log_buf, data, log_len);
char hex_buf[format_hex_pretty_size(MIRAGE_MAX_DATA_BYTES)];
ESP_LOGI(TAG, "Transmit Mirage: %s", format_hex_pretty_to(hex_buf, data, len));
ESP_LOGI(TAG, "Transmit Mirage: %s", format_hex_pretty_to(hex_buf, log_buf, log_len));
dst->set_carrier_frequency(38000);
dst->reserve(5 + ((len + 1) * 2));
dst->mark(HEADER_MARK_US);
@@ -26,7 +33,7 @@ void MirageProtocol::encode(RemoteTransmitData *dst, const uint8_t *data, size_t
dst->mark(BIT_MARK_US);
uint8_t checksum = 0;
for (size_t i = 0; i < len; i++) {
const uint8_t item = data[i];
const uint8_t item = progmem_read_byte(data + i);
this->encode_byte_(dst, item);
checksum += (item >> 4) + (item & 0xF);
}
@@ -16,7 +16,11 @@ class MirageProtocol : public RemoteProtocol<MirageData> {
void encode(RemoteTransmitData *dst, const MirageData &data) {
this->encode(dst, data.data.data(), data.data.size());
}
/// data may be a PROGMEM table
void encode(RemoteTransmitData *dst, const uint8_t *data, size_t len);
void encode(RemoteTransmitData *dst, const std::vector<uint8_t> &data) {
this->encode(dst, data.data(), data.size());
}
optional<MirageData> decode(RemoteReceiveData src);
void dump(const MirageData &data);
@@ -26,13 +30,4 @@ class MirageProtocol : public RemoteProtocol<MirageData> {
DECLARE_REMOTE_PROTOCOL(Mirage)
template<typename... Ts> class MirageAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_BYTES(code)
void encode(RemoteTransmitData *dst, Ts... x) override {
this->code_.visit([dst](const uint8_t *data, size_t len) { MirageProtocol().encode(dst, data, len); }, x...);
}
};
} // namespace esphome::remote_base
@@ -182,6 +182,11 @@ bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
/* RemoteTransmitterBase */
RemoteTransmitterBase::TransmitCall RemoteTransmitterBase::transmit() {
this->temp_.reset();
return TransmitCall(this, this->take_seq_());
}
void RemoteTransmitterBase::transmit_encoded_(EncodeFn encode, const void *data, uint32_t send_times,
uint32_t send_wait) {
auto call = this->transmit();
+11 -4
View File
@@ -187,10 +187,7 @@ class RemoteTransmitterBase : public RemoteComponentBase {
uint16_t seq_;
};
TransmitCall transmit() {
this->temp_.reset();
return TransmitCall(this, this->take_seq_());
}
TransmitCall transmit();
template<RemoteProtocolEncoder Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmit_encoded_(&encode_thunk<Protocol>, &data, send_times, send_wait);
@@ -213,6 +210,16 @@ class RemoteTransmitterBase : public RemoteComponentBase {
template<RemoteProtocolEncoder Protocol> ProtocolCall<Protocol> protocol_call() {
return ProtocolCall<Protocol>(this);
}
/// A transmit call whose encode() runs one of Protocol's encoders on its data, for a payload
/// the protocol encodes straight from a pointer and length; the call a generated action builds.
template<RemoteProtocolEncoder Protocol> class EncodeCall : public TransmitCall {
public:
explicit EncodeCall(TransmitCall call) : TransmitCall(call) {}
template<typename... Args> void encode(const Args &...args) { Protocol().encode(this->get_data(), args...); }
};
template<RemoteProtocolEncoder Protocol> EncodeCall<Protocol> encode_call() {
return EncodeCall<Protocol>(this->transmit());
}
protected:
using EncodeFn = void (*)(RemoteTransmitData *dst, const void *data);
@@ -3,7 +3,7 @@
import re
_PROTOCOL_CALL_BODY = re.compile(
r"auto apply_call = \S+->protocol_call<remote_base::\w+>\(\);\n(.*?)apply_call\.perform\(\);",
r"auto apply_call = \S+->(?:protocol|encode)_call<remote_base::\w+>\(\);\n(.*?)apply_call\.perform\(\);",
re.DOTALL,
)
@@ -0,0 +1,33 @@
esphome:
name: test
esp8266:
board: esp01_1m
remote_transmitter:
pin: GPIO4
carrier_duty_percent: 50%
button:
- platform: template
name: ABBWelcome fixed id
on_press:
- remote_transmitter.transmit_abbwelcome:
source_address: 0x123456
destination_address: 0x56789A
three_byte_address: true
message_type: 0x01
message_id: 0x42
data: [0x10]
- platform: template
name: AEHA lambda
on_press:
- remote_transmitter.transmit_aeha:
address: !lambda return 0x8008;
carrier_frequency: 36700Hz
data: !lambda return {0x00, 0x02};
- platform: template
name: Midea lambda
on_press:
- remote_transmitter.transmit_midea:
code: !lambda return {0xA2, 0x08, 0xFF, 0xFF, 0xFF};
@@ -9,13 +9,17 @@ def test_constant_data_shares_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal data shares one table, empty or missing data is an empty table, lambdas stay templates."""
"""Equal data shares one table, empty data is a null table, lambdas are called inline."""
main_cpp = generate_main(component_config_path("abbwelcome_payload_tables.yaml"))
tables = re.findall(
r"static constexpr uint8_t (\w+)\[\] PROGMEM = (\{[^}]*\});", main_cpp
)
assert [v for _, v in tables] == ["{0x10, 0x20, 0x30}"]
assert main_cpp.count(f"set_data_static({tables[0][0]}, 3);") == 2
assert main_cpp.count("set_data_static(nullptr, 0);") == 2
assert main_cpp.count("set_data_template(") == 1
assert main_cpp.count(f"apply_call.data.set_data({tables[0][0]}, 3);") == 2
# Empty data is a null table; missing data leaves the default empty payload
assert main_cpp.count("apply_call.data.set_data(nullptr, 0);") == 1
assert (
main_cpp.count("apply_call.data.set_data([]() -> std::vector<uint8_t> {") == 1
)
assert "std::vector<uint8_t>{" not in main_cpp
@@ -0,0 +1,33 @@
"""The byte payload transmit actions as generated code."""
from collections.abc import Callable
from pathlib import Path
from tests.component_tests.remote_transmitter import protocol_call_bodies
def test_byte_payloads(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("byte_payloads.yaml"))
abb, aeha, midea = protocol_call_bodies(main_cpp)
# The address length goes first, a configured id turns the automatic one off
assert abb.index("set_three_byte_address(true);") < abb.index("set_source_address(")
assert abb.index("auto_message_id = true;") < abb.index(
"set_fixed_message_id(0x42);"
)
assert abb.rstrip().endswith("apply_call.data.finalize();")
# The carrier is set before the payload is encoded
assert aeha.index("set_carrier_frequency(36700);") < aeha.index(
"apply_call.encode("
)
assert (
"apply_call.encode(static_cast<uint16_t>(0x8008), []() -> std::vector<uint8_t> {"
in aeha
)
assert (
"apply_call.data = remote_base::MideaData([]() -> std::vector<uint8_t> {"
in midea
)
assert midea.rstrip().endswith("apply_call.data.finalize();")
@@ -1,4 +1,4 @@
"""Constant IR byte codes live in shared PROGMEM tables; lambdas stay lambdas."""
"""Constant IR byte codes are encoded from shared PROGMEM tables; lambdas are called inline."""
from collections.abc import Callable
from pathlib import Path
@@ -16,9 +16,10 @@ def test_ir_codes_use_shared_progmem_tables(
)
assert len(tables) == 4 # Midea (shared by two actions), AEHA, Haier, Mirage
midea = tables[0]
assert main_cpp.count(f"->set_code_static({midea}, 5);") == 2
assert re.search(r"->set_data_static\(remote_base_code\w*, 6\);", main_cpp)
assert re.search(r"->set_code_static\(remote_base_code\w*, 13\);", main_cpp)
assert re.search(r"->set_code_static\(remote_base_code\w*, 14\);", main_cpp)
assert "->set_code_template([](" in main_cpp
assert main_cpp.count(f"apply_call.data = remote_base::MideaData({midea}, 5);") == 2
assert re.search(r"apply_call\.encode\(0x8008, remote_base_code\w*, 6\);", main_cpp)
assert re.search(r"apply_call\.encode\(remote_base_code\w*, 13\);", main_cpp)
assert re.search(r"apply_call\.encode\(remote_base_code\w*, 14\);", main_cpp)
assert "apply_call.encode([]() -> std::vector<uint8_t> {" in main_cpp
# Constant codes are read from the table, never copied into a vector
assert "std::vector<uint8_t>{" not in main_cpp
@@ -62,27 +62,15 @@ RawTimings with_trailing_space(RawTimings timings) {
return timings;
}
template<typename A> RawTimings encode(A &action) {
RemoteTransmitData dst;
action.encode(&dst);
return dst.get_data();
}
// Exposes the protected encode() of an action for testing
template<typename A> class Open : public A {
public:
using A::encode;
};
// A constant and a lambda code must both encode to the golden timings
template<typename A>
void expect_static_and_lambda_match(const uint8_t *code, size_t len, std::vector<uint8_t> (*lambda)(),
const RawTimings &golden) {
Open<A> fixed, from_lambda;
fixed.set_code_static(code, len);
from_lambda.set_code_template(lambda);
EXPECT_EQ(encode(fixed), golden);
EXPECT_EQ(encode(from_lambda), golden);
// A constant table and a lambda's vector take the two encoder overloads generated actions call
template<typename P, typename... Lead>
void expect_pointer_and_vector_match(const uint8_t *code, size_t len, const std::vector<uint8_t> &bytes,
const RawTimings &golden, const Lead &...lead) {
RemoteTransmitData fixed, from_vector;
P().encode(&fixed, lead..., code, len);
P().encode(&from_vector, lead..., bytes);
EXPECT_EQ(fixed.get_data(), golden);
EXPECT_EQ(from_vector.get_data(), golden);
}
template<typename P> void expect_decodes_to(const RawTimings &timings, const std::vector<uint8_t> &code) {
@@ -94,25 +82,16 @@ template<typename P> void expect_decodes_to(const RawTimings &timings, const std
} // namespace
TEST(IrCodeEncodersTest, HaierStaticAndLambdaMatchGolden) {
expect_static_and_lambda_match<HaierAction<>>(HAIER_CODE, sizeof(HAIER_CODE), haier_code, HAIER_GOLDEN);
TEST(IrCodeEncodersTest, HaierPointerAndVectorMatchGolden) {
expect_pointer_and_vector_match<HaierProtocol>(HAIER_CODE, sizeof(HAIER_CODE), haier_code(), HAIER_GOLDEN);
}
TEST(IrCodeEncodersTest, MirageStaticAndLambdaMatchGolden) {
expect_static_and_lambda_match<MirageAction<>>(MIRAGE_CODE, sizeof(MIRAGE_CODE), mirage_code, MIRAGE_GOLDEN);
TEST(IrCodeEncodersTest, MiragePointerAndVectorMatchGolden) {
expect_pointer_and_vector_match<MirageProtocol>(MIRAGE_CODE, sizeof(MIRAGE_CODE), mirage_code(), MIRAGE_GOLDEN);
}
TEST(IrCodeEncodersTest, AehaStaticAndLambdaMatchGolden) {
Open<AEHAAction<>> fixed, lambda;
for (auto *action : {&fixed, &lambda}) {
// TemplatableFn fields take functions, not raw constants
action->set_address([]() -> uint16_t { return AEHA_ADDRESS; });
action->set_carrier_frequency([]() -> uint32_t { return 38000; });
}
fixed.set_data_static(AEHA_CODE, sizeof(AEHA_CODE));
lambda.set_data_template(aeha_code);
EXPECT_EQ(encode(fixed), AEHA_GOLDEN);
EXPECT_EQ(encode(lambda), AEHA_GOLDEN);
TEST(IrCodeEncodersTest, AehaPointerAndVectorMatchGolden) {
expect_pointer_and_vector_match<AEHAProtocol>(AEHA_CODE, sizeof(AEHA_CODE), aeha_code(), AEHA_GOLDEN, AEHA_ADDRESS);
}
TEST(IrCodeEncodersTest, StructEncodersMatchGolden) {