mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[api] Reply when an infrared/RF raw timings transmit has completed (#19086)
This commit is contained in:
@@ -287,6 +287,7 @@ esphome/components/inkplate/* @jesserockz @JosipKuci
|
||||
esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/ir_rf_base/* @bdraco @kbx81
|
||||
esphome/components/ir_rf_proxy/* @kbx81
|
||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
|
||||
@@ -2692,7 +2692,7 @@ message ListEntitiesInfraredResponse {
|
||||
message InfraredRFTransmitRawTimingsRequest {
|
||||
option (id) = 136;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
|
||||
@@ -2706,7 +2706,7 @@ message InfraredRFTransmitRawTimingsRequest {
|
||||
message InfraredRFReceiveEvent {
|
||||
option (id) = 137;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
@@ -2715,6 +2715,28 @@ message InfraredRFReceiveEvent {
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
|
||||
// transmitter reports that the transmission (all repeats) has finished, or immediately with
|
||||
// success=false if it was refused before reaching the transmitter (unknown key, no transmitter,
|
||||
// no or invalid timings) or the transmitter could not send it (not set up, hardware error).
|
||||
// success=false also answers a request superseded by a newer request on the same entity, from
|
||||
// any client, and a transmit that reported nothing within 30 s past its expected air time, so a
|
||||
// client should not retry on it blindly. The device serializes transmits per transmitter and
|
||||
// several entities may share one, so a client should keep at most one transmit outstanding per
|
||||
// device, not per entity. A reply for an unknown key or a refused request is sent once and can be lost
|
||||
// when the device's send buffer is full, so a client should also stop waiting on its own after the
|
||||
// expected air time plus a margin. Lets clients pace requests instead of estimating durations (since API 1.18)
|
||||
message InfraredRFTransmitCompleteResponse {
|
||||
option (id) = 153;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2 [(force) = true]; // Key of the transmitter entity from the request
|
||||
bool success = 3; // false if the request was refused, not sent, superseded, or never reported
|
||||
}
|
||||
|
||||
// ==================== RADIO FREQUENCY ====================
|
||||
|
||||
// Lists available radio frequency entity instances
|
||||
|
||||
@@ -198,6 +198,17 @@ APIConnection::~APIConnection() {
|
||||
proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// entities holding a transmit reply for this client must not answer into a freed connection
|
||||
#ifdef USE_INFRARED
|
||||
for (auto *infrared : App.get_infrareds()) {
|
||||
infrared->on_api_connection_closed(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
for (auto *radio_frequency : App.get_radio_frequencies()) {
|
||||
radio_frequency->on_api_connection_closed(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1517,8 +1528,10 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
// Clients on API 1.18+ are told when the frame has left the transmitter; the entity owns that reply
|
||||
const bool want_reply = this->client_supports_api_version(1, 18);
|
||||
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
|
||||
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
|
||||
#ifdef USE_INFRARED
|
||||
@@ -1528,6 +1541,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.set_api_connection(want_reply ? this : nullptr);
|
||||
call.perform();
|
||||
return;
|
||||
}
|
||||
@@ -1540,13 +1554,38 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
|
||||
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_api_connection(want_reply ? this : nullptr);
|
||||
call.perform();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ESP_LOGW(TAG, "IR/RF transmit for unknown key %" PRIu32, msg.key);
|
||||
if (want_reply) {
|
||||
// nothing will ever report for an unknown key, so answer as not started right away
|
||||
#ifdef USE_DEVICES
|
||||
const uint32_t device_id = msg.device_id;
|
||||
#else
|
||||
const uint32_t device_id = 0;
|
||||
#endif
|
||||
if (!this->send_infrared_rf_transmit_complete(device_id, msg.key, false)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "IR/RF reply");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_infrared_rf_transmit_complete([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
bool success) {
|
||||
InfraredRFTransmitCompleteResponse resp{};
|
||||
#ifdef USE_DEVICES
|
||||
resp.device_id = device_id;
|
||||
#endif
|
||||
resp.key = key;
|
||||
resp.success = success;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
// V: fires per decoded frame with no subscription gate, so a warning
|
||||
@@ -1554,6 +1593,7 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
|
||||
ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
@@ -1816,7 +1856,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 17;
|
||||
resp.api_version_minor = 18;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
|
||||
@@ -233,9 +233,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
// Reply to an InfraredRFTransmitRawTimingsRequest (API 1.18+); false when the TCP buffer is
|
||||
// full, the entity that owns the reply retries it then
|
||||
[[nodiscard]] bool send_infrared_rf_transmit_complete(uint32_t device_id, uint32_t key, bool success);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
|
||||
@@ -3934,7 +3934,7 @@ uint32_t ListEntitiesInfraredResponse::calc_size_msg(const void *self) {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
proto_varint_value_t scalar) {
|
||||
auto &msg = *static_cast<InfraredRFTransmitRawTimingsRequest *>(self);
|
||||
@@ -3994,6 +3994,27 @@ InfraredRFReceiveEvent::calc_size_msg(const void *self) {
|
||||
}
|
||||
return size;
|
||||
}
|
||||
uint8_t *InfraredRFTransmitCompleteResponse::encode_msg(const void *self,
|
||||
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||
const auto &msg = *static_cast<const InfraredRFTransmitCompleteResponse *>(self);
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
#ifdef USE_DEVICES
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.device_id);
|
||||
#endif
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.success);
|
||||
return pos;
|
||||
}
|
||||
uint32_t InfraredRFTransmitCompleteResponse::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const InfraredRFTransmitCompleteResponse *>(self);
|
||||
uint32_t size = 0;
|
||||
#ifdef USE_DEVICES
|
||||
size += ProtoSize::calc_uint32(1, msg.device_id);
|
||||
#endif
|
||||
size += 5;
|
||||
size += ProtoSize::calc_bool(1, msg.success);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
uint8_t *ListEntitiesRadioFrequencyResponse::encode_msg(const void *self,
|
||||
@@ -4329,7 +4350,7 @@ static_assert(!std::is_polymorphic_v<UpdateCommandRequest>, "decodable messages
|
||||
static_assert(!std::is_polymorphic_v<ZWaveProxyFrame>, "decodable messages carry no vtable");
|
||||
static_assert(!std::is_polymorphic_v<ZWaveProxyRequest>, "decodable messages carry no vtable");
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
static_assert(!std::is_polymorphic_v<InfraredRFTransmitRawTimingsRequest>, "decodable messages carry no vtable");
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
|
||||
@@ -3681,7 +3681,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 136;
|
||||
@@ -3733,6 +3733,30 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class InfraredRFTransmitCompleteResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 153;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_complete_response"); }
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
bool success{false};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self);
|
||||
uint32_t calculate_size() const { return calc_size_msg(this); }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
|
||||
|
||||
@@ -2720,7 +2720,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest"));
|
||||
#ifdef USE_DEVICES
|
||||
@@ -2749,6 +2749,15 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
return out.c_str();
|
||||
}
|
||||
const char *InfraredRFTransmitCompleteResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitCompleteResponse"));
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_field(out, ESPHOME_PSTR("success"), this->success);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -628,7 +628,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
|
||||
InfraredRFTransmitRawTimingsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
|
||||
@@ -213,7 +213,7 @@ class APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -504,7 +504,7 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
InfraredRFReceiveEvent resp{};
|
||||
@@ -517,6 +517,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
|
||||
@@ -203,7 +203,7 @@ class APIServer final : public Component
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,20 +9,21 @@ Once the API is considered stable, this warning will be removed.
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ir_rf_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import queue_entity_register, setup_entity
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
AUTO_LOAD = ["ir_rf_base"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
infrared_ns = cg.esphome_ns.namespace("infrared")
|
||||
Infrared = infrared_ns.class_("Infrared", cg.EntityBase, cg.Component)
|
||||
Infrared = infrared_ns.class_("Infrared", ir_rf_base.IrRfEntity)
|
||||
InfraredCall = infrared_ns.class_("InfraredCall")
|
||||
InfraredTraits = infrared_ns.class_("InfraredTraits")
|
||||
|
||||
@@ -52,11 +53,8 @@ async def setup_infrared_core_(var: cg.MockObj, config: ConfigType) -> None:
|
||||
|
||||
async def register_infrared(var: cg.MockObj, config: ConfigType) -> None:
|
||||
"""Register an infrared device with the core."""
|
||||
cg.add_define("USE_IR_RF")
|
||||
await cg.register_component(var, config)
|
||||
queue_entity_register("infrared", config)
|
||||
await ir_rf_base.register_ir_rf_entity(var, config, "infrared")
|
||||
await setup_infrared_core_(var, config)
|
||||
CORE.register_platform_component("infrared", var)
|
||||
|
||||
|
||||
async def new_infrared(config: ConfigType, *args: SafeExpType) -> cg.MockObj:
|
||||
|
||||
@@ -1,161 +1,18 @@
|
||||
#include "infrared.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::infrared {
|
||||
|
||||
static const char *const TAG = "infrared";
|
||||
|
||||
// ========== InfraredCall ==========
|
||||
|
||||
InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
|
||||
this->carrier_frequency_ = frequency;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
|
||||
this->base64url_ptr_ = &base64url;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->packed_data_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
|
||||
this->packed_data_ = data;
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_repeat_count(uint32_t count) {
|
||||
this->repeat_count_ = count;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void InfraredCall::perform() {
|
||||
if (this->parent_ != nullptr) {
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Infrared ==========
|
||||
|
||||
void Infrared::setup() {
|
||||
// Set up traits based on configuration
|
||||
this->traits_.set_supports_transmitter(this->has_transmitter());
|
||||
this->traits_.set_supports_receiver(this->has_receiver());
|
||||
}
|
||||
|
||||
void Infrared::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Infrared '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create transmit data object
|
||||
auto transmit_call = this->transmitter_->transmit();
|
||||
auto *transmit_data = transmit_call.get_data();
|
||||
|
||||
// Set carrier frequency
|
||||
auto freq = call.get_carrier_frequency();
|
||||
if (freq.has_value()) {
|
||||
transmit_data->set_carrier_frequency(*freq);
|
||||
}
|
||||
|
||||
// Set timings based on format
|
||||
if (call.is_packed()) {
|
||||
// Zero-copy from packed protobuf data
|
||||
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base64url()) {
|
||||
// Decode base64url (URL-safe) into transmit buffer
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
}
|
||||
// Sanity check: validate timing values are within reasonable bounds
|
||||
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
// From vector (lambdas/automations)
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
|
||||
call.get_repeat_count());
|
||||
}
|
||||
|
||||
// Set repeat count
|
||||
if (call.get_repeat_count() > 0) {
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
// Perform transmission
|
||||
transmit_call.perform();
|
||||
}
|
||||
|
||||
uint32_t Infrared::get_capability_flags() const {
|
||||
uint32_t flags = 0;
|
||||
|
||||
// Add transmit/receive capability based on traits
|
||||
if (this->traits_.get_supports_transmitter())
|
||||
flags |= InfraredCapability::CAPABILITY_TRANSMITTER;
|
||||
if (this->traits_.get_supports_receiver())
|
||||
flags |= InfraredCapability::CAPABILITY_RECEIVER;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool Infrared::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Forward received IR data to API server
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (api::global_api_server != nullptr) {
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id = this->get_device_id();
|
||||
#else
|
||||
uint32_t device_id = 0;
|
||||
#endif
|
||||
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
|
||||
}
|
||||
#endif
|
||||
return false; // Don't consume the event, allow other listeners to process it
|
||||
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
|
||||
YESNO(this->get_supports_receiver()));
|
||||
}
|
||||
|
||||
} // namespace esphome::infrared
|
||||
|
||||
@@ -4,131 +4,48 @@
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/components/ir_rf_base/ir_rf_base.h"
|
||||
|
||||
namespace esphome::infrared {
|
||||
|
||||
/// Capability flags for individual infrared instances
|
||||
enum InfraredCapability : uint32_t {
|
||||
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
|
||||
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
|
||||
};
|
||||
using ir_rf_base::CAPABILITY_RECEIVER;
|
||||
using ir_rf_base::CAPABILITY_TRANSMITTER;
|
||||
|
||||
/// Forward declarations
|
||||
class Infrared;
|
||||
|
||||
/// InfraredCall - Builder pattern for transmitting infrared signals
|
||||
class InfraredCall {
|
||||
class InfraredCall : public ir_rf_base::IrRfCall<InfraredCall, Infrared> {
|
||||
public:
|
||||
explicit InfraredCall(Infrared *parent) : parent_(parent) {}
|
||||
explicit InfraredCall(Infrared *parent) : IrRfCall(parent) {}
|
||||
|
||||
/// Set the carrier frequency in Hz
|
||||
InfraredCall &set_carrier_frequency(uint32_t frequency);
|
||||
|
||||
// ===== Raw Timings Methods =====
|
||||
// All set_raw_timings_* methods store pointers/references to external data.
|
||||
// The referenced data must remain valid until perform() completes.
|
||||
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
|
||||
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
|
||||
|
||||
/// Set the raw timings from a vector (positive = mark, negative = space)
|
||||
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
|
||||
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
|
||||
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
|
||||
|
||||
/// Set the raw timings from base64url-encoded little-endian int32 data
|
||||
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
|
||||
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
|
||||
/// @note Decoding happens at perform() time, directly into the transmit buffer.
|
||||
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
|
||||
|
||||
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
|
||||
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
|
||||
/// @note Usage: For API component where data comes directly from the protobuf message.
|
||||
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
|
||||
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
InfraredCall &set_repeat_count(uint32_t count);
|
||||
|
||||
/// Perform the transmission
|
||||
void perform();
|
||||
|
||||
InfraredCall &set_carrier_frequency(uint32_t frequency) {
|
||||
this->carrier_frequency_ = frequency;
|
||||
return *this;
|
||||
}
|
||||
/// Get the carrier frequency
|
||||
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
|
||||
/// Get the raw timings (only valid if set via set_raw_timings)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (any format)
|
||||
bool has_raw_timings() const {
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
|
||||
}
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Check if using base64url data format
|
||||
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
|
||||
/// Get the base64url data string
|
||||
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
uint16_t get_packed_count() const { return this->packed_count_; }
|
||||
/// Get the repeat count
|
||||
uint32_t get_repeat_count() const { return this->repeat_count_; }
|
||||
|
||||
protected:
|
||||
uint32_t repeat_count_{1};
|
||||
Infrared *parent_;
|
||||
optional<uint32_t> carrier_frequency_;
|
||||
// Pointer to vector-based timings (caller-owned, must outlive perform())
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base64url_ptr_{nullptr};
|
||||
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
uint16_t packed_count_{0};
|
||||
};
|
||||
|
||||
/// InfraredTraits - Describes the capabilities of an infrared implementation
|
||||
class InfraredTraits {
|
||||
public:
|
||||
bool get_supports_transmitter() const { return this->supports_transmitter_; }
|
||||
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
|
||||
|
||||
bool get_supports_receiver() const { return this->supports_receiver_; }
|
||||
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
|
||||
|
||||
uint32_t get_receiver_frequency_hz() const { return this->receiver_frequency_hz_; }
|
||||
void set_receiver_frequency_hz(uint32_t freq) { this->receiver_frequency_hz_ = freq; }
|
||||
|
||||
protected:
|
||||
bool supports_transmitter_{false};
|
||||
bool supports_receiver_{false};
|
||||
uint32_t receiver_frequency_hz_{0}; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
|
||||
};
|
||||
|
||||
/// Infrared - Base class for infrared remote control implementations
|
||||
class Infrared : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
|
||||
class Infrared : public ir_rf_base::IrRfEntity {
|
||||
public:
|
||||
Infrared() = default;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Set the remote receiver component; the listener registration happens from codegen, see
|
||||
/// remote_base.attach_receiver
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
|
||||
/// Check if this infrared has a transmitter configured
|
||||
bool has_transmitter() const { return this->transmitter_ != nullptr; }
|
||||
/// Check if this infrared has a receiver configured
|
||||
bool has_receiver() const { return this->receiver_ != nullptr; }
|
||||
|
||||
/// Get the traits for this infrared implementation
|
||||
InfraredTraits &get_traits() { return this->traits_; }
|
||||
@@ -137,21 +54,16 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
/// Create a call object for transmitting
|
||||
InfraredCall make_call() { return InfraredCall(this); }
|
||||
|
||||
/// Get capability flags for this infrared instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
/// Called when IR data is received (from RemoteReceiverListener)
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
|
||||
protected:
|
||||
friend class InfraredCall;
|
||||
friend class ir_rf_base::IrRfCall<InfraredCall, Infrared>;
|
||||
|
||||
/// Perform the actual transmission (called by InfraredCall)
|
||||
virtual void control(const InfraredCall &call);
|
||||
|
||||
// Underlying hardware components
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
void on_call_(const InfraredCall &) {}
|
||||
/// Perform the actual transmission (called by InfraredCall); false only when no frame was handed
|
||||
/// to the transmitter, in which case no completion follows
|
||||
/// Without a remote_base transmitter, call api_transmit_done_() once the frame is out
|
||||
virtual bool control(const InfraredCall &call) {
|
||||
return this->transmit_raw_(call, call.get_carrier_frequency().value_or(0));
|
||||
}
|
||||
|
||||
// Traits describing capabilities
|
||||
InfraredTraits traits_;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Shared base for the infrared and radio_frequency entity components."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import remote_base
|
||||
from esphome.const import CONF_API
|
||||
from esphome.core import CORE
|
||||
from esphome.core.entity_helpers import queue_entity_register
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81", "@bdraco"]
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
|
||||
ir_rf_base_ns = cg.esphome_ns.namespace("ir_rf_base")
|
||||
IrRfEntity = ir_rf_base_ns.class_("IrRfEntity", cg.EntityBase, cg.Component)
|
||||
|
||||
|
||||
async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> None:
|
||||
"""Link the configured transmitter to an entity.
|
||||
|
||||
With the API configured this also compiles in the transmit completion
|
||||
tracking that answers API transmit requests; the transmitter platform has
|
||||
to report completion through notify_complete_(), as remote_transmitter
|
||||
does. Without this call anywhere in the build a request is answered as
|
||||
soon as the frame is handed over.
|
||||
"""
|
||||
await remote_base.register_transmittable(var, config, key)
|
||||
if CONF_API in CORE.config:
|
||||
cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE")
|
||||
|
||||
|
||||
async def register_ir_rf_entity(
|
||||
var: cg.MockObj, config: ConfigType, domain: str
|
||||
) -> None:
|
||||
"""Register an infrared or radio_frequency entity; USE_IR_RF covers both kinds."""
|
||||
cg.add_define("USE_IR_RF")
|
||||
await cg.register_component(var, config)
|
||||
queue_entity_register(domain, config)
|
||||
CORE.register_platform_component(domain, var)
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "ir_rf_base.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "esphome/core/application.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ir_rf_base {
|
||||
|
||||
static const char *const TAG = "ir_rf";
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
// A missing completion is answered as failed this long after the frame should have left the
|
||||
// wire, and an owed reply the client never reads is dropped this long after the first refusal
|
||||
static constexpr uint32_t API_REPLY_TIMEOUT_MS = 30000;
|
||||
#endif
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
// Longest air time added to the deadline, in 16 ms ticks (8 min); with the 30 s above the
|
||||
// deadline stays within the signed 16 bit tick window loop() compares against
|
||||
static constexpr uint16_t API_REPLY_MAX_AIR_TICKS = 30000;
|
||||
#endif
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
static uint16_t api_reply_ticks_from_now() {
|
||||
return static_cast<uint16_t>((App.get_loop_component_start_time() >> 4) + (API_REPLY_TIMEOUT_MS >> 4));
|
||||
}
|
||||
#endif
|
||||
|
||||
void IrRfEntity::setup() {
|
||||
// merged, not assigned: a platform may have set a flag for its own hardware before setup()
|
||||
if (this->has_transmitter())
|
||||
this->supports_transmitter_ = true;
|
||||
if (this->has_receiver())
|
||||
this->supports_receiver_ = true;
|
||||
}
|
||||
|
||||
bool IrRfEntity::transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto transmit_call = this->transmitter_->transmit();
|
||||
auto *transmit_data = transmit_call.get_data();
|
||||
transmit_data->set_carrier_frequency(carrier_frequency_hz);
|
||||
|
||||
if (call.is_packed()) {
|
||||
// Zero-copy from packed protobuf data
|
||||
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base64url()) {
|
||||
// Decode base64url (URL-safe) into transmit buffer
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return false;
|
||||
}
|
||||
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
// From vector (lambdas/automations)
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
|
||||
call.get_repeat_count());
|
||||
}
|
||||
|
||||
// one answer for every backend: a frame that decoded to nothing is refused here
|
||||
if (transmit_data->get_data().empty()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (call.get_repeat_count() > 0) {
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
// only the API frame expect_api_reply_() armed claims the seq and extends its deadline
|
||||
if (call.wants_api_reply() && this->api_reply_ == ApiReply::API_REPLY_WAITING) {
|
||||
this->inflight_seq_ = transmit_call.get_seq();
|
||||
// a long frame must not be answered as failed while still on the wire: the 30 s safety net
|
||||
// starts after this frame's own air time (capped so the tick comparison cannot wrap)
|
||||
uint64_t frame_us = 0;
|
||||
for (const int64_t timing : transmit_data->get_data())
|
||||
frame_us += timing < 0 ? -timing : timing;
|
||||
const uint64_t air_ticks = (frame_us * std::max<uint32_t>(call.get_repeat_count(), 1) / 1000) >> 4;
|
||||
this->api_reply_deadline_ += static_cast<uint16_t>(std::min<uint64_t>(air_ticks, API_REPLY_MAX_AIR_TICKS));
|
||||
}
|
||||
#endif
|
||||
transmit_call.perform();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IrRfEntity::on_receive(remote_base::RemoteReceiveData data) {
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (api::global_api_server != nullptr) {
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id = this->get_device_id();
|
||||
#else
|
||||
uint32_t device_id = 0;
|
||||
#endif
|
||||
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
|
||||
}
|
||||
#endif
|
||||
return false; // Don't consume the event, allow other listeners to process it
|
||||
}
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
void IrRfEntity::on_transmit_complete(remote_base::RemoteTransmitterBase *transmitter, uint16_t seq, bool sent) {
|
||||
// only the frame this entity submitted; YAML automations and other entities share the transmitter
|
||||
if (transmitter == this->transmitter_ && seq == this->inflight_seq_ &&
|
||||
this->api_reply_ == ApiReply::API_REPLY_WAITING)
|
||||
this->finish_api_reply_(sent);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool IrRfEntity::expect_api_reply_(api::APIConnection *conn) {
|
||||
// only an unpaced client gets here: its earlier request is answered as not started, and a reply
|
||||
// the buffer still owes gets one last try; if that fails too the slot stays with it and the
|
||||
// new request is refused rather than answered to nobody
|
||||
if (this->api_reply_ == ApiReply::API_REPLY_WAITING)
|
||||
this->finish_api_reply_(false);
|
||||
if (this->api_reply_ != ApiReply::API_REPLY_NONE && !this->send_api_reply_()) {
|
||||
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("refused, reply still owed"));
|
||||
this->refuse_api_call_(conn);
|
||||
return false;
|
||||
}
|
||||
this->api_reply_connection_ = conn;
|
||||
this->api_reply_deadline_ = api_reply_ticks_from_now();
|
||||
this->api_reply_ = ApiReply::API_REPLY_WAITING;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IrRfEntity::refuse_api_call_(api::APIConnection *conn) {
|
||||
uint32_t device_id = 0;
|
||||
#ifdef USE_DEVICES
|
||||
device_id = this->get_device_id();
|
||||
#endif
|
||||
// no slot to retry from; a drop here is covered by the client's own timeout
|
||||
if (!conn->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(), false)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "IR/RF reply");
|
||||
}
|
||||
}
|
||||
|
||||
void IrRfEntity::finish_api_reply_(bool success) {
|
||||
this->api_reply_ = success ? ApiReply::API_REPLY_OWED_OK : ApiReply::API_REPLY_OWED_FAILED;
|
||||
if (!this->send_api_reply_()) {
|
||||
// the retry gets its own window
|
||||
this->api_reply_deadline_ = api_reply_ticks_from_now();
|
||||
}
|
||||
}
|
||||
|
||||
bool IrRfEntity::send_api_reply_() {
|
||||
uint32_t device_id = 0;
|
||||
#ifdef USE_DEVICES
|
||||
device_id = this->get_device_id();
|
||||
#endif
|
||||
// Refused by a full TCP buffer: the reply stays owed and loop() retries it, since a lost
|
||||
// reply would stall the client's pacing for good (same shape as bluetooth_proxy)
|
||||
if (!this->api_reply_connection_->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(),
|
||||
this->api_reply_ == ApiReply::API_REPLY_OWED_OK))
|
||||
return false;
|
||||
this->clear_api_reply_();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only runs while an API reply is pending: retries an owed one until it goes out or the client is
|
||||
// gone, and expires a transmit that never reported (a platform overriding control() without wiring
|
||||
// its transmitter's completion)
|
||||
void IrRfEntity::loop() {
|
||||
if (this->api_reply_ == ApiReply::API_REPLY_NONE) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
const auto remaining = static_cast<int16_t>(this->api_reply_deadline_ - (App.get_loop_component_start_time() >> 4));
|
||||
if (this->api_reply_ != ApiReply::API_REPLY_WAITING) {
|
||||
if (this->send_api_reply_() || remaining > 0)
|
||||
return;
|
||||
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("reply dropped, client not reading"));
|
||||
this->clear_api_reply_();
|
||||
return;
|
||||
}
|
||||
if (remaining > 0)
|
||||
return;
|
||||
ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("never reported completion"));
|
||||
this->finish_api_reply_(false);
|
||||
}
|
||||
|
||||
void IrRfEntity::on_api_connection_closed(api::APIConnection *conn) {
|
||||
if (this->api_reply_connection_ == conn) {
|
||||
this->clear_api_reply_();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ir_rf_base
|
||||
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
namespace esphome::remote_base {
|
||||
|
||||
void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent) {
|
||||
#ifdef USE_INFRARED
|
||||
for (auto *entity : App.get_infrareds()) {
|
||||
entity->on_transmit_complete(transmitter, seq, sent);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
for (auto *entity : App.get_radio_frequencies()) {
|
||||
entity->on_transmit_complete(transmitter, seq, sent);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
#endif
|
||||
@@ -0,0 +1,273 @@
|
||||
#pragma once
|
||||
|
||||
// WARNING: This component is EXPERIMENTAL. The API may change at any time
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
namespace esphome::api {
|
||||
class APIConnection;
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
||||
namespace esphome::ir_rf_base {
|
||||
|
||||
/// Capability flags reported by infrared and radio frequency entities
|
||||
enum IrRfCapability : uint32_t {
|
||||
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
|
||||
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
|
||||
};
|
||||
|
||||
/// Raw timings of a transmit call, in one of three caller-owned forms
|
||||
class IrRfCallData {
|
||||
public:
|
||||
/// Get the raw timings (only valid if set via set_raw_timings)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (any format)
|
||||
bool has_raw_timings() const {
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
|
||||
}
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Check if using base64url data format
|
||||
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
|
||||
/// Get the base64url data string
|
||||
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
uint16_t get_packed_count() const { return this->packed_count_; }
|
||||
/// Get the repeat count
|
||||
uint32_t get_repeat_count() const { return this->repeat_count_; }
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
/// True for a frame an API client is waiting on
|
||||
bool wants_api_reply() const { return this->api_connection_ != nullptr; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
uint32_t repeat_count_{1};
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
api::APIConnection *api_connection_{nullptr};
|
||||
#endif
|
||||
// Pointer to vector-based timings (caller-owned, must outlive perform())
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base64url_ptr_{nullptr};
|
||||
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
uint16_t packed_count_{0};
|
||||
};
|
||||
|
||||
template<typename Call, typename Entity> class IrRfCall;
|
||||
|
||||
/// Everything an infrared or radio frequency entity does that does not depend on the medium:
|
||||
/// the remote_base transport, forwarding received frames to the API, and answering the API
|
||||
/// once a transmit it started has left the transmitter.
|
||||
class IrRfEntity : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
|
||||
public:
|
||||
/// Reports the configured transports, listens on the receiver and hooks the transmitter's
|
||||
/// completion; a platform with its own setup() calls it first
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Set the remote receiver component; the listener registration happens from codegen, see
|
||||
/// remote_base.attach_receiver
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
bool has_transmitter() const { return this->transmitter_ != nullptr; }
|
||||
bool has_receiver() const { return this->receiver_ != nullptr; }
|
||||
|
||||
/// What the entity can do; platforms with their own hardware set these from their own
|
||||
/// setup(), remote_base ones get them from IrRfEntity::setup()
|
||||
bool get_supports_transmitter() const { return this->supports_transmitter_; }
|
||||
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
|
||||
bool get_supports_receiver() const { return this->supports_receiver_; }
|
||||
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
|
||||
/// Capability flags as reported to the API and web server
|
||||
uint32_t get_capability_flags() const {
|
||||
uint32_t flags = 0;
|
||||
if (this->supports_transmitter_)
|
||||
flags |= CAPABILITY_TRANSMITTER;
|
||||
if (this->supports_receiver_)
|
||||
flags |= CAPABILITY_RECEIVER;
|
||||
return flags;
|
||||
}
|
||||
|
||||
/// Forwards a received frame to the API; never consumes it, so other listeners still run
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
/// Called for every finished frame on every transmitter; answers the API request when the
|
||||
/// frame is the one this entity submitted for it, with sent as the outcome
|
||||
void on_transmit_complete(remote_base::RemoteTransmitterBase *transmitter, uint16_t seq, bool sent);
|
||||
#endif
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
void loop() override;
|
||||
/// The API server calls this when a client disconnects, so no reply goes to a stale pointer
|
||||
void on_api_connection_closed(api::APIConnection *conn);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
template<typename, typename> friend class IrRfCall;
|
||||
|
||||
/// Hands the call's timings to the transmitter; the default transmit path of both entity types
|
||||
bool transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz);
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
// One reply slot: a pacing client has at most one transmit outstanding, and a second request
|
||||
// from an unpaced client displaces the first. Retried and expired from loop(), which only
|
||||
// runs while a reply is pending; polling the expiry there costs nothing while idle, where a
|
||||
// scheduler timeout would allocate per frame.
|
||||
enum class ApiReply : uint8_t {
|
||||
API_REPLY_NONE,
|
||||
API_REPLY_WAITING, // frame handed to the transmitter, completion not reported yet
|
||||
API_REPLY_OWED_OK, // reply refused by a full TCP buffer; loop() retries it
|
||||
API_REPLY_OWED_FAILED // same, for a transmit that did not start
|
||||
};
|
||||
/// Claims the reply slot for conn; false when it still owes a reply that cannot be sent
|
||||
bool expect_api_reply_(api::APIConnection *conn);
|
||||
void refuse_api_call_(api::APIConnection *conn);
|
||||
/// After control(): a false start is answered now, and the loop only runs once something is
|
||||
/// pending, since a blocking transmitter has already answered inside control()
|
||||
void settle_api_reply_(bool started) {
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
if (!started)
|
||||
this->finish_api_reply_(false);
|
||||
#else
|
||||
// no transmitter in this build reports completion, so the hand-over is the answer
|
||||
this->finish_api_reply_(started);
|
||||
#endif
|
||||
if (this->api_reply_ != ApiReply::API_REPLY_NONE)
|
||||
this->enable_loop();
|
||||
}
|
||||
void finish_api_reply_(bool success);
|
||||
/// Answers the API request for a control() override that transmits without a remote_base transmitter
|
||||
void api_transmit_done_(bool sent) {
|
||||
if (this->api_reply_ == ApiReply::API_REPLY_WAITING)
|
||||
this->finish_api_reply_(sent);
|
||||
}
|
||||
bool send_api_reply_();
|
||||
void clear_api_reply_() {
|
||||
this->api_reply_ = ApiReply::API_REPLY_NONE;
|
||||
this->api_reply_connection_ = nullptr;
|
||||
this->disable_loop();
|
||||
}
|
||||
api::APIConnection *api_reply_connection_{nullptr};
|
||||
#endif
|
||||
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
// 16 ms ticks: 30 s for completion (plus capped air time) or for delivering an owed reply
|
||||
uint16_t api_reply_deadline_{0};
|
||||
#endif
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
uint16_t inflight_seq_{0}; // seq of the API frame this entity submitted last
|
||||
#endif
|
||||
// short members last, so the derived traits start on the next word without a gap
|
||||
bool supports_transmitter_{false};
|
||||
bool supports_receiver_{false};
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
ApiReply api_reply_{ApiReply::API_REPLY_NONE};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Builder for a transmit; Call is the concrete call type and Entity its entity, so the fluent
|
||||
/// setters return the concrete type and control() sees the medium specific fields
|
||||
template<typename Call, typename Entity> class IrRfCall : public IrRfCallData {
|
||||
// only the concrete call may construct the CRTP base
|
||||
friend Call;
|
||||
explicit IrRfCall(Entity *parent) : parent_(parent) {}
|
||||
|
||||
public:
|
||||
// ===== Raw Timings Methods =====
|
||||
// All set_raw_timings_* methods store pointers/references to external data.
|
||||
// The referenced data must remain valid until perform() completes.
|
||||
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
|
||||
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
|
||||
|
||||
/// Set the raw timings from a vector (positive = mark, negative = space)
|
||||
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
|
||||
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
|
||||
Call &set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return this->self_();
|
||||
}
|
||||
|
||||
/// Set the raw timings from base64url-encoded little-endian int32 data
|
||||
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
|
||||
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
|
||||
/// @note Decoding happens at perform() time, directly into the transmit buffer.
|
||||
Call &set_raw_timings_base64url(const std::string &base64url) {
|
||||
this->base64url_ptr_ = &base64url;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->packed_data_ = nullptr;
|
||||
return this->self_();
|
||||
}
|
||||
|
||||
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
|
||||
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
|
||||
/// @note Usage: For API component where data comes directly from the protobuf message.
|
||||
Call &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
|
||||
this->packed_data_ = data;
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return this->self_();
|
||||
}
|
||||
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
Call &set_repeat_count(uint32_t count) {
|
||||
this->repeat_count_ = count;
|
||||
return this->self_();
|
||||
}
|
||||
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
/// Reply to this API client once the frame has left the transmitter (API 1.18+)
|
||||
Call &set_api_connection(api::APIConnection *conn) {
|
||||
this->api_connection_ = conn;
|
||||
return this->self_();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Perform the transmission; returns true if a frame was handed to the transmitter
|
||||
bool perform() {
|
||||
// make_call() always sets the parent
|
||||
Entity *parent = this->parent_;
|
||||
if (parent == nullptr)
|
||||
return false;
|
||||
parent->on_call_(this->self_());
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
// Before control(): blocking transmitters report completion from inside it, and the
|
||||
// non-blocking RMT path flushes the previous frame's completion there
|
||||
if (this->api_connection_ != nullptr && !parent->expect_api_reply_(this->api_connection_))
|
||||
return false;
|
||||
#endif
|
||||
const bool started = parent->control(this->self_());
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (this->api_connection_ != nullptr)
|
||||
parent->settle_api_reply_(started);
|
||||
#endif
|
||||
return started;
|
||||
}
|
||||
|
||||
protected:
|
||||
Call &self_() { return static_cast<Call &>(*this); }
|
||||
|
||||
Entity *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ir_rf_base
|
||||
@@ -3,12 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import (
|
||||
infrared,
|
||||
remote_base,
|
||||
remote_receiver,
|
||||
remote_transmitter,
|
||||
)
|
||||
from esphome.components import infrared, ir_rf_base, remote_base, remote_receiver
|
||||
from esphome.components.const import CONF_RECEIVER_FREQUENCY
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
|
||||
@@ -30,7 +25,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
remote_receiver.RemoteReceiverComponent
|
||||
),
|
||||
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
|
||||
remote_transmitter.RemoteTransmitterComponent
|
||||
remote_base.RemoteTransmitterBase
|
||||
),
|
||||
}
|
||||
),
|
||||
@@ -82,8 +77,7 @@ async def to_code(config: dict[str, Any]) -> None:
|
||||
|
||||
# Link transmitter if specified
|
||||
if CONF_REMOTE_TRANSMITTER_ID in config:
|
||||
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter))
|
||||
await ir_rf_base.attach_transmitter(var, config, CONF_REMOTE_TRANSMITTER_ID)
|
||||
|
||||
# Link receiver if specified
|
||||
if CONF_REMOTE_RECEIVER_ID in config:
|
||||
|
||||
@@ -8,70 +8,17 @@ namespace esphome::ir_rf_proxy {
|
||||
|
||||
static const char *const TAG = "ir_rf_proxy";
|
||||
|
||||
// ========== Shared transmit helper ==========
|
||||
// Static template: all instantiations occur in this translation unit.
|
||||
|
||||
template<typename CallT>
|
||||
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
|
||||
const CallT &call) {
|
||||
if (transmitter == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
}
|
||||
|
||||
auto transmit_call = transmitter->transmit();
|
||||
auto *transmit_data = transmit_call.get_data();
|
||||
transmit_data->set_carrier_frequency(carrier_frequency);
|
||||
|
||||
if (call.is_packed()) {
|
||||
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base64url()) {
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
}
|
||||
constexpr int32_t max_timing_us = 500000;
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
|
||||
call.get_repeat_count());
|
||||
}
|
||||
|
||||
if (call.get_repeat_count() > 0) {
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
transmit_call.perform();
|
||||
}
|
||||
|
||||
// ========== IrRfProxy (Infrared platform) ==========
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#ifdef USE_INFRARED
|
||||
|
||||
void IrRfProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"IR Proxy '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
|
||||
YESNO(this->get_supports_receiver()));
|
||||
|
||||
if (this->is_rf()) {
|
||||
ESP_LOGCONFIG(TAG, " Hardware Type: RF (%.3f MHz)", this->frequency_khz_ / 1e3f);
|
||||
@@ -80,21 +27,14 @@ void IrRfProxy::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void IrRfProxy::control(const infrared::InfraredCall &call) {
|
||||
uint32_t carrier = call.get_carrier_frequency().value_or(0);
|
||||
transmit_raw_timings(this->transmitter_, carrier, call);
|
||||
}
|
||||
|
||||
#endif // USE_IR_RF
|
||||
#endif // USE_INFRARED
|
||||
|
||||
// ========== RfProxy (Radio Frequency platform) ==========
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
|
||||
void RfProxy::setup() {
|
||||
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
|
||||
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
|
||||
|
||||
ir_rf_base::IrRfEntity::setup();
|
||||
// remote_transmitter/receiver always uses OOK (on-off keying)
|
||||
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
|
||||
}
|
||||
@@ -104,8 +44,8 @@ void RfProxy::dump_config() {
|
||||
"RF Proxy '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
|
||||
YESNO(this->get_supports_receiver()));
|
||||
|
||||
const auto &traits = this->traits_;
|
||||
if (traits.get_frequency_min_hz() > 0) {
|
||||
@@ -118,11 +58,11 @@ void RfProxy::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
|
||||
bool RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
|
||||
// RF: no IR carrier modulation. Any RF front-end coordination (state turnaround, retuning)
|
||||
// happens via the radio_frequency entity's on_control trigger and remote_transmitter's
|
||||
// on_transmit/on_complete triggers — wired up in user YAML.
|
||||
transmit_raw_timings(this->transmitter_, 0, call);
|
||||
return this->transmit_raw_(call, 0);
|
||||
}
|
||||
|
||||
#endif // USE_RADIO_FREQUENCY
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#ifdef USE_INFRARED
|
||||
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
|
||||
class IrRfProxy final : public infrared::Infrared {
|
||||
public:
|
||||
@@ -35,12 +35,10 @@ class IrRfProxy final : public infrared::Infrared {
|
||||
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
|
||||
|
||||
protected:
|
||||
void control(const infrared::InfraredCall &call) override;
|
||||
|
||||
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
|
||||
uint32_t frequency_khz_{0};
|
||||
};
|
||||
#endif // USE_IR_RF
|
||||
#endif // USE_INFRARED
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend.
|
||||
@@ -54,20 +52,11 @@ class RfProxy final : public radio_frequency::RadioFrequency {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
/// Set the remote receiver component; the listener registration happens from codegen, see
|
||||
/// remote_base.attach_receiver
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
|
||||
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
|
||||
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
|
||||
|
||||
protected:
|
||||
void control(const radio_frequency::RadioFrequencyCall &call) override;
|
||||
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
bool control(const radio_frequency::RadioFrequencyCall &call) override;
|
||||
};
|
||||
#endif // USE_RADIO_FREQUENCY
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import (
|
||||
radio_frequency,
|
||||
remote_base,
|
||||
remote_receiver,
|
||||
remote_transmitter,
|
||||
)
|
||||
from esphome.components import ir_rf_base, radio_frequency, remote_base, remote_receiver
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
|
||||
import esphome.final_validate as fv
|
||||
@@ -27,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
remote_receiver.RemoteReceiverComponent
|
||||
),
|
||||
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
|
||||
remote_transmitter.RemoteTransmitterComponent
|
||||
remote_base.RemoteTransmitterBase
|
||||
),
|
||||
}
|
||||
),
|
||||
@@ -67,8 +62,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
|
||||
|
||||
if CONF_REMOTE_TRANSMITTER_ID in config:
|
||||
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter))
|
||||
await ir_rf_base.attach_transmitter(var, config, CONF_REMOTE_TRANSMITTER_ID)
|
||||
|
||||
if CONF_REMOTE_RECEIVER_ID in config:
|
||||
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
|
||||
|
||||
@@ -10,22 +10,21 @@ Once the API is considered stable, this warning will be removed.
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ir_rf_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_CONTROL
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import queue_entity_register, setup_entity
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
AUTO_LOAD = ["ir_rf_base"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
radio_frequency_ns = cg.esphome_ns.namespace("radio_frequency")
|
||||
RadioFrequency = radio_frequency_ns.class_(
|
||||
"RadioFrequency", cg.EntityBase, cg.Component
|
||||
)
|
||||
RadioFrequency = radio_frequency_ns.class_("RadioFrequency", ir_rf_base.IrRfEntity)
|
||||
RadioFrequencyCall = radio_frequency_ns.class_("RadioFrequencyCall")
|
||||
RadioFrequencyTraits = radio_frequency_ns.class_("RadioFrequencyTraits")
|
||||
RadioFrequencyModulation = radio_frequency_ns.enum("RadioFrequencyModulation")
|
||||
@@ -55,11 +54,8 @@ async def setup_radio_frequency_core_(var: cg.MockObj, config: ConfigType) -> No
|
||||
|
||||
async def register_radio_frequency(var: cg.MockObj, config: ConfigType) -> None:
|
||||
"""Register a radio frequency device with the core."""
|
||||
cg.add_define("USE_RADIO_FREQUENCY")
|
||||
await cg.register_component(var, config)
|
||||
queue_entity_register("radio_frequency", config)
|
||||
await ir_rf_base.register_ir_rf_entity(var, config, "radio_frequency")
|
||||
await setup_radio_frequency_core_(var, config)
|
||||
CORE.register_platform_component("radio_frequency", var)
|
||||
|
||||
for conf in config.get(CONF_ON_CONTROL, []):
|
||||
await automation.build_callback_automation(
|
||||
|
||||
@@ -4,73 +4,17 @@
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::radio_frequency {
|
||||
|
||||
static const char *const TAG = "radio_frequency";
|
||||
|
||||
// ========== RadioFrequencyCall ==========
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_frequency(uint32_t frequency_hz) {
|
||||
this->frequency_hz_ = frequency_hz;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_modulation(RadioFrequencyModulation modulation) {
|
||||
this->modulation_ = modulation;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_base64url(const std::string &base64url) {
|
||||
this->base64url_ptr_ = &base64url;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->packed_data_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
|
||||
this->packed_data_ = data;
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr;
|
||||
this->base64url_ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) {
|
||||
this->repeat_count_ = count;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void RadioFrequencyCall::perform() {
|
||||
if (this->parent_ != nullptr) {
|
||||
// Fire any on_control hooks (user-wired automations) before handing off to
|
||||
// the platform-specific control() — gives users a chance to react to call
|
||||
// parameters (e.g. retune an external RF front-end based on call.get_frequency()).
|
||||
this->parent_->control_callback_.call(*this);
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RadioFrequency ==========
|
||||
|
||||
void RadioFrequency::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Radio Frequency '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
this->get_name().c_str(), YESNO(this->get_supports_transmitter()),
|
||||
YESNO(this->get_supports_receiver()));
|
||||
if (this->traits_.get_frequency_min_hz() > 0) {
|
||||
if (this->traits_.get_frequency_min_hz() == this->traits_.get_frequency_max_hz()) {
|
||||
ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz (fixed)", this->traits_.get_frequency_min_hz());
|
||||
@@ -81,31 +25,9 @@ void RadioFrequency::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t RadioFrequency::get_capability_flags() const {
|
||||
uint32_t flags = 0;
|
||||
if (this->traits_.get_supports_transmitter())
|
||||
flags |= RadioFrequencyCapability::CAPABILITY_TRANSMITTER;
|
||||
if (this->traits_.get_supports_receiver())
|
||||
flags |= RadioFrequencyCapability::CAPABILITY_RECEIVER;
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Invoke local callbacks
|
||||
this->receive_callback_.call(data);
|
||||
|
||||
// Forward received RF data to API server
|
||||
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
|
||||
if (api::global_api_server != nullptr) {
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id = this->get_device_id();
|
||||
#else
|
||||
uint32_t device_id = 0;
|
||||
#endif
|
||||
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
|
||||
}
|
||||
#endif
|
||||
return false; // Don't consume the event, allow other listeners to process it
|
||||
return IrRfEntity::on_receive(data);
|
||||
}
|
||||
|
||||
} // namespace esphome::radio_frequency
|
||||
|
||||
@@ -4,20 +4,12 @@
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/components/ir_rf_base/ir_rf_base.h"
|
||||
|
||||
namespace esphome::radio_frequency {
|
||||
|
||||
/// Capability flags for individual radio frequency instances
|
||||
enum RadioFrequencyCapability : uint32_t {
|
||||
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
|
||||
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
|
||||
};
|
||||
using ir_rf_base::CAPABILITY_RECEIVER;
|
||||
using ir_rf_base::CAPABILITY_TRANSMITTER;
|
||||
|
||||
/// Modulation types supported by radio frequency implementations
|
||||
enum RadioFrequencyModulation : uint8_t {
|
||||
@@ -25,95 +17,36 @@ enum RadioFrequencyModulation : uint8_t {
|
||||
// Future: RADIO_FREQUENCY_MODULATION_FSK, RADIO_FREQUENCY_MODULATION_GFSK, etc.
|
||||
};
|
||||
|
||||
/// Forward declarations
|
||||
class RadioFrequency;
|
||||
|
||||
/// RadioFrequencyCall - Builder pattern for transmitting radio frequency signals
|
||||
class RadioFrequencyCall {
|
||||
class RadioFrequencyCall : public ir_rf_base::IrRfCall<RadioFrequencyCall, RadioFrequency> {
|
||||
public:
|
||||
explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {}
|
||||
explicit RadioFrequencyCall(RadioFrequency *parent) : IrRfCall(parent) {}
|
||||
|
||||
/// Set the carrier frequency in Hz (e.g. 433920000 for 433.92 MHz)
|
||||
RadioFrequencyCall &set_frequency(uint32_t frequency_hz);
|
||||
|
||||
RadioFrequencyCall &set_frequency(uint32_t frequency_hz) {
|
||||
this->frequency_hz_ = frequency_hz;
|
||||
return *this;
|
||||
}
|
||||
/// Set the modulation type (defaults to OOK)
|
||||
RadioFrequencyCall &set_modulation(RadioFrequencyModulation modulation);
|
||||
|
||||
// ===== Raw Timings Methods =====
|
||||
// All set_raw_timings_* methods store pointers/references to external data.
|
||||
// The referenced data must remain valid until perform() completes.
|
||||
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
|
||||
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
|
||||
|
||||
/// Set the raw timings from a vector (positive = mark, negative = space)
|
||||
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
|
||||
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
|
||||
RadioFrequencyCall &set_raw_timings(const std::vector<int32_t> &timings);
|
||||
|
||||
/// Set the raw timings from base64url-encoded little-endian int32 data
|
||||
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
|
||||
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
|
||||
/// @note Decoding happens at perform() time, directly into the transmit buffer.
|
||||
RadioFrequencyCall &set_raw_timings_base64url(const std::string &base64url);
|
||||
|
||||
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
|
||||
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
|
||||
/// @note Usage: For API component where data comes directly from the protobuf message.
|
||||
RadioFrequencyCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
|
||||
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
RadioFrequencyCall &set_repeat_count(uint32_t count);
|
||||
|
||||
/// Perform the transmission
|
||||
void perform();
|
||||
|
||||
RadioFrequencyCall &set_modulation(RadioFrequencyModulation modulation) {
|
||||
this->modulation_ = modulation;
|
||||
return *this;
|
||||
}
|
||||
/// Get the frequency in Hz
|
||||
const optional<uint32_t> &get_frequency() const { return this->frequency_hz_; }
|
||||
/// Get the modulation type
|
||||
RadioFrequencyModulation get_modulation() const { return this->modulation_; }
|
||||
/// Get the raw timings (only valid if set via set_raw_timings)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (any format)
|
||||
bool has_raw_timings() const {
|
||||
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
|
||||
}
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Check if using base64url data format
|
||||
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
|
||||
/// Get the base64url data string
|
||||
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
uint16_t get_packed_count() const { return this->packed_count_; }
|
||||
/// Get the repeat count
|
||||
uint32_t get_repeat_count() const { return this->repeat_count_; }
|
||||
|
||||
protected:
|
||||
optional<uint32_t> frequency_hz_{};
|
||||
uint32_t repeat_count_{1};
|
||||
RadioFrequency *parent_;
|
||||
// Pointer to vector-based timings (caller-owned, must outlive perform())
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
|
||||
const std::string *base64url_ptr_{nullptr};
|
||||
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
uint16_t packed_count_{0};
|
||||
RadioFrequencyModulation modulation_{RADIO_FREQUENCY_MODULATION_OOK};
|
||||
};
|
||||
|
||||
/// RadioFrequencyTraits - Describes the capabilities of a radio frequency implementation
|
||||
class RadioFrequencyTraits {
|
||||
public:
|
||||
bool get_supports_transmitter() const { return this->supports_transmitter_; }
|
||||
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
|
||||
|
||||
bool get_supports_receiver() const { return this->supports_receiver_; }
|
||||
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
|
||||
|
||||
/// Hardware-supported tunable frequency range in Hz.
|
||||
/// If min == max (and both non-zero): fixed-frequency hardware.
|
||||
/// If both 0: range unspecified.
|
||||
@@ -140,17 +73,14 @@ class RadioFrequencyTraits {
|
||||
uint32_t frequency_min_hz_{0}; // Minimum tunable frequency in Hz (0 = unspecified)
|
||||
uint32_t frequency_max_hz_{0}; // Maximum tunable frequency in Hz (0 = unspecified)
|
||||
uint32_t supported_modulations_{0}; // Bitmask of supported RadioFrequencyModulation values
|
||||
bool supports_transmitter_{false};
|
||||
bool supports_receiver_{false};
|
||||
};
|
||||
|
||||
/// RadioFrequency - Base class for radio frequency implementations
|
||||
class RadioFrequency : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
|
||||
class RadioFrequency : public ir_rf_base::IrRfEntity {
|
||||
public:
|
||||
RadioFrequency() = default;
|
||||
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Get the traits for this radio frequency implementation
|
||||
RadioFrequencyTraits &get_traits() { return this->traits_; }
|
||||
@@ -159,9 +89,6 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
|
||||
/// Create a call object for transmitting
|
||||
RadioFrequencyCall make_call() { return RadioFrequencyCall(this); }
|
||||
|
||||
/// Get capability flags for this radio frequency instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
/// Called when RF data is received (from RemoteReceiverListener)
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
|
||||
@@ -180,11 +107,16 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class RadioFrequencyCall;
|
||||
friend class ir_rf_base::IrRfCall<RadioFrequencyCall, RadioFrequency>;
|
||||
|
||||
/// Fires the on_control hooks before the platform-specific control() runs
|
||||
void on_call_(const RadioFrequencyCall &call) { this->control_callback_.call(call); }
|
||||
/// Perform the actual transmission (called by RadioFrequencyCall::perform())
|
||||
/// Platforms must override this to implement hardware-specific transmission.
|
||||
virtual void control(const RadioFrequencyCall &call) = 0;
|
||||
/// Returns false only when no frame was handed to the transmitter, in which case no
|
||||
/// completion follows.
|
||||
/// Without a remote_base transmitter, call api_transmit_done_() once the frame is out
|
||||
virtual bool control(const RadioFrequencyCall &call) = 0;
|
||||
|
||||
// Traits describing capabilities
|
||||
RadioFrequencyTraits traits_;
|
||||
|
||||
@@ -136,8 +136,8 @@ async def attach_receiver(
|
||||
add_listener(receiver, var)
|
||||
|
||||
|
||||
async def register_transmittable(var, config):
|
||||
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
|
||||
async def register_transmittable(var, config, key: str = CONF_TRANSMITTER_ID):
|
||||
transmitter_ = await cg.get_variable(config[key])
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) {
|
||||
|
||||
// Typically a DISH device needs to get a command a total of
|
||||
// at least 4 times to accept it.
|
||||
for (uint i = 0; i < 4; i++) {
|
||||
for (uint32_t i = 0; i < 4; i++) {
|
||||
// COMMAND (function, in MSB)
|
||||
for (uint8_t mask = 1UL << 5; mask; mask >>= 1) {
|
||||
if (data.command & mask) {
|
||||
@@ -39,7 +39,7 @@ void DishProtocol::encode(RemoteTransmitData *dst, const DishData &data) {
|
||||
}
|
||||
}
|
||||
// PADDING
|
||||
for (uint j = 0; j < 6; j++)
|
||||
for (uint32_t j = 0; j < 6; j++)
|
||||
dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US);
|
||||
|
||||
// FOOTER
|
||||
@@ -73,7 +73,7 @@ optional<DishData> DishProtocol::decode(RemoteReceiveData src) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
for (uint j = 0; j < 6; j++) {
|
||||
for (uint32_t j = 0; j < 6; j++) {
|
||||
if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
|
||||
|
||||
/* RemoteTransmitterBase */
|
||||
|
||||
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
|
||||
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait, [[maybe_unused]] uint16_t seq) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
const auto &vec = this->temp_.get_data();
|
||||
char buffer[256];
|
||||
@@ -213,6 +213,10 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
|
||||
if (pos != 0) {
|
||||
ESP_LOGVV(TAG, "%s", buffer);
|
||||
}
|
||||
#endif
|
||||
this->flush_pending_completion();
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
this->current_seq_ = seq;
|
||||
#endif
|
||||
this->send_internal(send_times, send_wait);
|
||||
}
|
||||
|
||||
@@ -143,6 +143,14 @@ class RemoteRMTChannel {
|
||||
#endif // SOC_RMT_SUPPORTED
|
||||
#endif // USE_ESP32
|
||||
|
||||
class RemoteTransmitterBase;
|
||||
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
/// Defined by ir_rf_base: answers the API request waiting on the entity that submitted seq;
|
||||
/// sent is false when the platform never put the frame on the wire.
|
||||
/// One function for the whole build instead of a callback list on every transmitter.
|
||||
void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent);
|
||||
#endif
|
||||
// Protocol shapes, checked where a protocol is used so a missing method fails at the use site
|
||||
// instead of deep inside a template body. Receive-only protocols such as RCSwitchBase decode
|
||||
// without encoding.
|
||||
@@ -164,21 +172,24 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
|
||||
class TransmitCall {
|
||||
public:
|
||||
explicit TransmitCall(RemoteTransmitterBase *parent) : parent_(parent) {}
|
||||
TransmitCall(RemoteTransmitterBase *parent, uint16_t seq) : parent_(parent), seq_(seq) {}
|
||||
RemoteTransmitData *get_data() { return &this->parent_->temp_; }
|
||||
void set_send_times(uint32_t send_times) { send_times_ = send_times; }
|
||||
void set_send_wait(uint32_t send_wait) { send_wait_ = send_wait; }
|
||||
void perform() { this->parent_->send_(this->send_times_, this->send_wait_); }
|
||||
/// Identifies this transmission in the completion hook
|
||||
uint16_t get_seq() const { return this->seq_; }
|
||||
void perform() { this->parent_->send_(this->send_times_, this->send_wait_, this->seq_); }
|
||||
|
||||
protected:
|
||||
RemoteTransmitterBase *parent_;
|
||||
uint32_t send_times_{1};
|
||||
uint32_t send_wait_{0};
|
||||
uint16_t seq_;
|
||||
};
|
||||
|
||||
TransmitCall transmit() {
|
||||
this->temp_.reset();
|
||||
return TransmitCall(this);
|
||||
return TransmitCall(this, this->take_seq_());
|
||||
}
|
||||
template<RemoteProtocolEncoder Protocol>
|
||||
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
@@ -190,9 +201,25 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
void send_(uint32_t send_times, uint32_t send_wait);
|
||||
void send_(uint32_t send_times, uint32_t send_wait, uint16_t seq);
|
||||
virtual void send_internal(uint32_t send_times, uint32_t send_wait) = 0;
|
||||
void send_single_() { this->send_(1, 0); }
|
||||
/// Platforms that report completion later wait out the previous frame here, before send_()
|
||||
/// assigns the next seq, so that completion carries the seq it belongs to
|
||||
virtual void flush_pending_completion() {}
|
||||
void send_single_() { this->send_(1, 0, this->take_seq_()); }
|
||||
#ifdef USE_IR_RF_TRANSMIT_COMPLETE
|
||||
/// Reports the frame handed to the platform last, after its final repeat and before the
|
||||
/// on_complete trigger; a seq only has to be unique within the 30 s reply window
|
||||
void notify_complete_(bool sent) { ir_rf_transmit_complete(this, this->current_seq_, sent); }
|
||||
uint16_t take_seq_() { return ++this->next_seq_; }
|
||||
|
||||
uint16_t next_seq_{0};
|
||||
uint16_t current_seq_{0};
|
||||
#else
|
||||
// seq tracking only exists for the API completion reply
|
||||
void notify_complete_(bool /*sent*/) {}
|
||||
static uint16_t take_seq_() { return 0; }
|
||||
#endif
|
||||
|
||||
/// Use same vector for all transmits, avoids many allocations
|
||||
RemoteTransmitData temp_;
|
||||
|
||||
@@ -114,7 +114,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
}
|
||||
}
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_(send_times != 0); // same answer as the ISR backend for a frame sent zero times
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_transmitter
|
||||
|
||||
@@ -82,6 +82,16 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t send_times, uint32_t send_wait) override;
|
||||
// the API reply is answered before the user's on_complete automation; sent is false on a
|
||||
// bail-out that never put the frame on the wire, on_complete fires either way
|
||||
void fire_complete_(bool sent = true) {
|
||||
this->notify_complete_(sent);
|
||||
this->complete_trigger_.trigger();
|
||||
}
|
||||
#if (defined(USE_ESP32) && SOC_RMT_SUPPORTED) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || \
|
||||
defined(REMOTE_TRANSMITTER_BK_PWM)
|
||||
void flush_pending_completion() override;
|
||||
#endif
|
||||
#if defined(USE_ESP8266) || \
|
||||
(defined(USE_LIBRETINY) && !defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
|
||||
defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||
|
||||
@@ -110,7 +110,7 @@ void RemoteTransmitterComponent::deliver_completion_() {
|
||||
if (!this->stall_aborted_)
|
||||
this->status_clear_warning();
|
||||
this->complete_pending_ = false;
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_(!this->stall_aborted_);
|
||||
}
|
||||
|
||||
// Waits until no chain is in flight, delivering any deferred completions; a completion
|
||||
@@ -152,20 +152,21 @@ void RemoteTransmitterComponent::arm_chain_(uint32_t send_times, uint32_t send_w
|
||||
this->start_isr_item_(0);
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::flush_pending_completion() { this->wait_until_idle_(); }
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
if (!this->envelope_ready_()) {
|
||||
// both triggers still fire, so an on_complete-sequenced automation does not stall
|
||||
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
|
||||
this->transmit_trigger_.trigger();
|
||||
this->deliver_completion_();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
this->wait_until_idle_();
|
||||
if (send_times == 0) {
|
||||
// parity with the loop-based implementations: transmit nothing, but both triggers
|
||||
// still fire so an on_complete-sequenced automation does not stall
|
||||
this->transmit_trigger_.trigger();
|
||||
this->deliver_completion_();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
@@ -175,7 +176,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (this->isr_data_.empty()) {
|
||||
ESP_LOGW(TAG, "Empty data");
|
||||
this->transmit_trigger_.trigger();
|
||||
this->deliver_completion_();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
// trigger first: the deadline computed in arm_chain_ must not be charged for user code
|
||||
|
||||
@@ -200,25 +200,35 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_(error == ESP_OK);
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
void RemoteTransmitterComponent::flush_pending_completion() {
|
||||
// a frame still on the wire is waited out, and its completion reported, before the next one
|
||||
if (this->non_blocking_ && this->cancel_timeout("complete")) {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
uint64_t total_duration = 0;
|
||||
|
||||
if (this->is_failed()) {
|
||||
// both triggers still fire, so a paced API client or on_complete automation is not left waiting
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the timeout was cancelled, block until the tx is complete
|
||||
if (this->non_blocking_ && this->cancel_timeout("complete")) {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
|
||||
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
|
||||
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
|
||||
this->configure_rmt_();
|
||||
if (this->is_failed()) { // the carrier change failed, there is no channel to send on
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->rmt_temp_.clear();
|
||||
@@ -258,6 +268,8 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
|
||||
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
|
||||
ESP_LOGE(TAG, "Empty data");
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,9 +285,11 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
// nothing was queued, so there is no frame to wait for
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
if (this->non_blocking_) {
|
||||
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
|
||||
@@ -284,13 +298,23 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
}
|
||||
}
|
||||
#else
|
||||
void RemoteTransmitterComponent::flush_pending_completion() {}
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
if (this->is_failed())
|
||||
if (this->is_failed()) {
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
|
||||
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
|
||||
this->configure_rmt_();
|
||||
if (this->is_failed()) { // the carrier change failed, there is no channel to send on
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->rmt_temp_.clear();
|
||||
@@ -328,9 +352,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
|
||||
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.empty()) {
|
||||
ESP_LOGE(TAG, "Empty data");
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
this->transmit_trigger_.trigger();
|
||||
bool sent = send_times != 0; // same answer as the ISR backend for a frame sent zero times
|
||||
for (uint32_t i = 0; i < send_times; i++) {
|
||||
rmt_transmit_config_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
@@ -340,18 +367,21 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
sent = false;
|
||||
}
|
||||
error = rmt_tx_wait_all_done(this->channel_, -1);
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
|
||||
this->status_set_warning();
|
||||
sent = false;
|
||||
}
|
||||
if (i + 1 < send_times)
|
||||
delayMicroseconds(send_wait);
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
// a later repeat must not clear the warning a failed one raised
|
||||
if (sent)
|
||||
this->status_clear_warning();
|
||||
this->fire_complete_(sent);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ void RemoteTransmitterComponent::await_target_time_() {
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
if (this->pwm_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
|
||||
this->transmit_trigger_.trigger();
|
||||
this->fire_complete_(false);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
@@ -194,7 +196,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
}
|
||||
}
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
@@ -2080,10 +2080,8 @@ void WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config,
|
||||
|
||||
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
|
||||
root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
|
||||
root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
|
||||
root[ESPHOME_F("supports_transmitter")] = obj->get_supports_transmitter();
|
||||
root[ESPHOME_F("supports_receiver")] = obj->get_supports_receiver();
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
#define USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#define USE_INFRARED
|
||||
#define USE_IR_RF
|
||||
#define USE_IR_RF_TRANSMIT_COMPLETE
|
||||
#define USE_JSON
|
||||
#define USE_JSON_ARENA
|
||||
#define USE_RADIO_FREQUENCY
|
||||
|
||||
@@ -149,7 +149,7 @@ BENCHMARK(Decode_SerialProxyWriteRequest);
|
||||
// --- InfraredRFReceiveEvent encode (100 sint32 timings) +
|
||||
// InfraredRFTransmitRawTimingsRequest decode (hand-built wire bytes) ---
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
#ifdef USE_IR_RF
|
||||
|
||||
// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings).
|
||||
static std::vector<int32_t> make_ir_timings_100() {
|
||||
@@ -275,6 +275,6 @@ static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state)
|
||||
}
|
||||
BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest);
|
||||
|
||||
#endif // USE_IR_RF || USE_RADIO_FREQUENCY
|
||||
#endif // USE_IR_RF
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
|
||||
@@ -19,7 +19,8 @@ class InfraredCall {
|
||||
return *this;
|
||||
}
|
||||
InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; }
|
||||
void perform() {}
|
||||
template<typename T> InfraredCall &set_api_connection(T * /*conn*/) { return *this; }
|
||||
bool perform() { return false; }
|
||||
|
||||
protected:
|
||||
Infrared *parent_;
|
||||
@@ -37,6 +38,7 @@ class Infrared : public Component, public EntityBase {
|
||||
const InfraredTraits &get_traits() const { return this->traits_; }
|
||||
InfraredCall make_call() { return InfraredCall(this); }
|
||||
uint32_t get_capability_flags() const { return 0; }
|
||||
template<typename T> void on_api_connection_closed(T * /*conn*/) {}
|
||||
|
||||
protected:
|
||||
InfraredTraits traits_;
|
||||
|
||||
@@ -23,7 +23,8 @@ class RadioFrequencyCall {
|
||||
RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
|
||||
return *this;
|
||||
}
|
||||
void perform() {}
|
||||
template<typename T> RadioFrequencyCall &set_api_connection(T * /*conn*/) { return *this; }
|
||||
bool perform() { return false; }
|
||||
|
||||
protected:
|
||||
RadioFrequency *parent_;
|
||||
@@ -43,6 +44,7 @@ class RadioFrequency : public Component, public EntityBase {
|
||||
const RadioFrequencyTraits &get_traits() const { return this->traits_; }
|
||||
RadioFrequencyCall make_call() { return RadioFrequencyCall(this); }
|
||||
uint32_t get_capability_flags() const { return 0; }
|
||||
template<typename T> void on_api_connection_closed(T * /*conn*/) {}
|
||||
|
||||
protected:
|
||||
RadioFrequencyTraits traits_;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Host-only stand-in for remote_transmitter used by the IR/RF integration tests."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
MULTI_CONF = True
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
|
||||
remote_transmitter_mock_ns = cg.esphome_ns.namespace("remote_transmitter_mock")
|
||||
MockRemoteTransmitter = remote_transmitter_mock_ns.class_(
|
||||
"MockRemoteTransmitter", remote_base.RemoteTransmitterBase, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(): cv.declare_id(MockRemoteTransmitter)}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#include "remote_transmitter_mock.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::remote_transmitter_mock {
|
||||
|
||||
static const char *const TAG = "remote_transmitter_mock";
|
||||
|
||||
void MockRemoteTransmitter::dump_config() { ESP_LOGCONFIG(TAG, "Mock Remote Transmitter"); }
|
||||
|
||||
void MockRemoteTransmitter::flush_pending_completion() {
|
||||
if (!this->busy_)
|
||||
return;
|
||||
// The RMT backend blocks here until the hardware is idle; the mock only reports the overlap
|
||||
ESP_LOGW(TAG, "Overlap: a frame was requested while seq=%" PRIu16 " was in flight", this->current_seq_);
|
||||
this->cancel_timeout("complete");
|
||||
this->finish_();
|
||||
}
|
||||
|
||||
void MockRemoteTransmitter::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
uint64_t total_us = static_cast<uint64_t>(send_wait) * (send_times - 1);
|
||||
for (int32_t value : this->temp_.get_data()) {
|
||||
total_us += static_cast<uint64_t>(value < 0 ? -value : value) * send_times;
|
||||
}
|
||||
const auto duration_ms = static_cast<uint32_t>(total_us / 1000);
|
||||
|
||||
this->busy_ = true;
|
||||
ESP_LOGI(TAG, "TX seq=%" PRIu16 " timings=%zu repeat=%" PRIu32 " duration=%" PRIu32 "ms", this->current_seq_,
|
||||
this->temp_.get_data().size(), send_times, duration_ms);
|
||||
this->set_timeout("complete", duration_ms, [this]() { this->finish_(); });
|
||||
}
|
||||
|
||||
void MockRemoteTransmitter::finish_() {
|
||||
this->busy_ = false;
|
||||
ESP_LOGI(TAG, "Complete seq=%" PRIu16, this->current_seq_);
|
||||
this->notify_complete_(true);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_transmitter_mock
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE
|
||||
//
|
||||
// Emulates a non-blocking remote transmitter: a frame "occupies" the
|
||||
// transmitter for its real duration and the completion callback fires from a
|
||||
// scheduler timeout, the same way the ESP32 RMT backend reports completion.
|
||||
// A frame submitted while another is in flight is logged as an overlap; the
|
||||
// real backends block the main loop in that case.
|
||||
// ============================================================================
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome::remote_transmitter_mock {
|
||||
|
||||
class MockRemoteTransmitter : public remote_base::RemoteTransmitterBase, public Component {
|
||||
public:
|
||||
MockRemoteTransmitter() : remote_base::RemoteTransmitterBase(nullptr) {}
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void flush_pending_completion() override;
|
||||
void send_internal(uint32_t send_times, uint32_t send_wait) override;
|
||||
void finish_();
|
||||
|
||||
bool busy_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::remote_transmitter_mock
|
||||
@@ -0,0 +1,30 @@
|
||||
esphome:
|
||||
name: ir-rf-transmit-complete-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
remote_transmitter_mock:
|
||||
- id: rf_tx
|
||||
- id: ir_tx
|
||||
|
||||
# two entities on one transmitter: each must get only its own completion
|
||||
radio_frequency:
|
||||
- platform: ir_rf_proxy
|
||||
name: RF Transmitter A
|
||||
remote_transmitter_id: rf_tx
|
||||
- platform: ir_rf_proxy
|
||||
name: RF Transmitter B
|
||||
remote_transmitter_id: rf_tx
|
||||
|
||||
infrared:
|
||||
- platform: ir_rf_proxy
|
||||
name: IR Transmitter
|
||||
remote_transmitter_id: ir_tx
|
||||
@@ -0,0 +1,131 @@
|
||||
"""IR/RF transmit completion replies (API 1.18) and the client pacing built on them.
|
||||
|
||||
The transmitter is a host-only mock that takes a frame's real duration to
|
||||
"send" it and reports completion from a scheduler timeout, like the ESP32 RMT
|
||||
backend. The client is expected to hold the next frame until the device
|
||||
replies, so the mock never sees an overlapping frame.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from aioesphomeapi import InfraredInfo, RadioFrequencyInfo
|
||||
from aioesphomeapi.api_pb2 import InfraredRFTransmitCompleteResponse
|
||||
import pytest
|
||||
|
||||
from .state_utils import find_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
FRAME_COUNT = 5
|
||||
# 10 marks and 10 spaces of 500 us, sent twice: 20 ms per frame
|
||||
TIMINGS = [500, -500] * 10
|
||||
REPEAT = 2
|
||||
MOCK_EVENT = re.compile(
|
||||
r"remote_transmitter_mock[^\]]*\]: (TX|Complete|Overlap)\b.*?seq=(\d+)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.shared_yaml("ir_rf_transmit_complete")
|
||||
@pytest.mark.asyncio
|
||||
async def test_ir_rf_transmit_complete_boot(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The host build with the mock transmitters boots and lists all entities on any client."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
assert find_entity(entities, "rf_transmitter_a", RadioFrequencyInfo) is not None
|
||||
assert find_entity(entities, "rf_transmitter_b", RadioFrequencyInfo) is not None
|
||||
assert find_entity(entities, "ir_transmitter", InfraredInfo) is not None
|
||||
|
||||
|
||||
@pytest.mark.shared_yaml("ir_rf_transmit_complete")
|
||||
@pytest.mark.asyncio
|
||||
async def test_ir_rf_transmit_complete(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Frames are answered once they leave the transmitter, never overlap, and a refused
|
||||
request is answered at once. Two RF entities share a transmitter and each gets its own
|
||||
reply; the infrared entity on its own transmitter is answered the same way."""
|
||||
loop = asyncio.get_running_loop()
|
||||
events: list[tuple[str, int]] = []
|
||||
all_sent = loop.create_future()
|
||||
all_logged = loop.create_future()
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if (match := MOCK_EVENT.search(line)) is None:
|
||||
return
|
||||
events.append((match.group(1), int(match.group(2))))
|
||||
if (
|
||||
match.group(1) == "Complete"
|
||||
and not all_sent.done()
|
||||
and sum(kind == "Complete" for kind, _ in events) == FRAME_COUNT
|
||||
):
|
||||
all_sent.set_result(None)
|
||||
# the log reader stops with the device, so wait for the last line before asserting on it
|
||||
if len(events) == 2 * (FRAME_COUNT + 1) and not all_logged.done():
|
||||
all_logged.set_result(None)
|
||||
|
||||
completions: list[InfraredRFTransmitCompleteResponse] = []
|
||||
all_replied = loop.create_future()
|
||||
ir_replied = loop.create_future()
|
||||
refused = loop.create_future()
|
||||
|
||||
def on_complete(msg: InfraredRFTransmitCompleteResponse) -> None:
|
||||
if not msg.success:
|
||||
if not refused.done():
|
||||
refused.set_result(msg)
|
||||
return
|
||||
completions.append(msg)
|
||||
if len(completions) == FRAME_COUNT and not all_replied.done():
|
||||
all_replied.set_result(None)
|
||||
if len(completions) == FRAME_COUNT + 1 and not ir_replied.done():
|
||||
ir_replied.set_result(None)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
rf = find_entity(entities, "rf_transmitter_a", RadioFrequencyInfo)
|
||||
rf_b = find_entity(entities, "rf_transmitter_b", RadioFrequencyInfo)
|
||||
ir = find_entity(entities, "ir_transmitter", InfraredInfo)
|
||||
assert rf is not None and rf_b is not None, "RF transmitter entities not found"
|
||||
assert ir is not None, "IR transmitter entity not found"
|
||||
|
||||
client._connection.add_message_callback(
|
||||
on_complete, (InfraredRFTransmitCompleteResponse,)
|
||||
)
|
||||
# alternate between the two entities sharing the transmitter
|
||||
keys = [rf.key if i % 2 == 0 else rf_b.key for i in range(FRAME_COUNT)]
|
||||
for key in keys:
|
||||
client.radio_frequency_transmit_raw_timings(
|
||||
key, 433920000, TIMINGS, repeat_count=REPEAT
|
||||
)
|
||||
|
||||
await asyncio.wait_for(all_replied, timeout=10)
|
||||
await asyncio.wait_for(all_sent, timeout=10)
|
||||
|
||||
# the infrared entity goes through the same path on its own transmitter
|
||||
client.infrared_rf_transmit_raw_timings(ir.key, 38000, TIMINGS)
|
||||
await asyncio.wait_for(ir_replied, timeout=10)
|
||||
await asyncio.wait_for(all_logged, timeout=10)
|
||||
|
||||
# A request the entity refuses is answered right away with success false;
|
||||
# no timings, so the proxy rejects it before it reaches the transmitter
|
||||
client.radio_frequency_transmit_raw_timings(rf.key, 433920000, [])
|
||||
refused_msg = await asyncio.wait_for(refused, timeout=10)
|
||||
|
||||
assert [msg.key for msg in completions] == [*keys, ir.key]
|
||||
assert refused_msg.key == rf.key
|
||||
|
||||
# The mocks saw one frame at a time: every transmit follows the previous completion
|
||||
kinds = [kind for kind, _ in events]
|
||||
assert kinds == ["TX", "Complete"] * (FRAME_COUNT + 1), events
|
||||
seqs = [seq for _, seq in events]
|
||||
assert seqs[::2] == seqs[1::2], events
|
||||
Reference in New Issue
Block a user