[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
@@ -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) {