[api] Reply when an infrared/RF raw timings transmit has completed

This commit is contained in:
J. Nick Koston
2026-09-10 19:26:39 -05:00
parent 3e3822e554
commit a9bf807f4a
23 changed files with 309 additions and 53 deletions
+15
View File
@@ -2697,6 +2697,21 @@ 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 could not be started (unknown key, no transmitter, no or invalid timings).
// 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 || USE_RADIO_FREQUENCY";
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 transmit never started
}
// ==================== RADIO FREQUENCY ====================
// Lists available radio frequency entity instances
+25 -4
View File
@@ -1516,6 +1516,18 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
#ifdef USE_DEVICES
const uint32_t device_id = msg.device_id;
#else
const uint32_t device_id = 0;
#endif
// Register before perform(): blocking transmitters report completion from inside it, and the
// non-blocking RMT path flushes the previous frame's completion there
const bool want_reply = this->client_supports_api_version(1, 18);
if (want_reply) {
this->parent_->register_pending_ir_rf_transmit(device_id, msg.key, this);
}
bool started = false;
// 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
@@ -1525,8 +1537,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.perform();
return;
started = call.perform();
}
#endif
#ifdef USE_RADIO_FREQUENCY
@@ -1537,9 +1548,12 @@ 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.perform();
started = call.perform();
}
#endif
if (want_reply && !started) {
this->parent_->fail_pending_ir_rf_transmit(device_id, msg.key, this);
}
}
#endif
@@ -1551,6 +1565,13 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
}
}
void APIConnection::send_infrared_rf_transmit_complete_response(const InfraredRFTransmitCompleteResponse &msg) {
if (!this->send_message(msg)) {
// a lost reply stalls the client's pacing until the server side expiry
API_LOG_MSG_DROPPED(TAG, "IR/RF transmit complete");
}
}
#endif
#ifdef USE_SERIAL_PROXY
@@ -1813,7 +1834,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());
+1
View File
@@ -236,6 +236,7 @@ class APIConnection final : public APIServerConnectionBase {
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
void send_infrared_rf_transmit_complete_response(const InfraredRFTransmitCompleteResponse &msg);
#endif
#ifdef USE_SERIAL_PROXY
+18
View File
@@ -4082,6 +4082,24 @@ InfraredRFReceiveEvent::calculate_size() const {
}
return size;
}
uint8_t *InfraredRFTransmitCompleteResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
#ifdef USE_DEVICES
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id);
#endif
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key);
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->success);
return pos;
}
uint32_t InfraredRFTransmitCompleteResponse::calculate_size() const {
uint32_t size = 0;
#ifdef USE_DEVICES
size += ProtoSize::calc_uint32(1, this->device_id);
#endif
size += 5;
size += ProtoSize::calc_bool(1, this->success);
return size;
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
+20
View File
@@ -3241,6 +3241,26 @@ 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};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#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 {
+9
View File
@@ -2740,6 +2740,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 {
+76
View File
@@ -38,6 +38,9 @@ void APIServer::socket_failed_(const LogString *msg) {
}
void APIServer::setup() {
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
this->pending_ir_rf_transmits_.reserve(this->clients_.size());
#endif
ControllerRegistry::register_controller(this);
#ifdef USE_API_NOISE
@@ -193,6 +196,9 @@ void APIServer::remove_client_(uint8_t client_index) {
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
this->unregister_pending_ir_rf_transmits_for_connection_(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
@@ -417,6 +423,76 @@ 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);
}
// Safety net for transmitters that never report completion (for example a failed component)
static constexpr uint32_t IR_RF_TRANSMIT_TIMEOUT_MS = 30000;
static constexpr char IR_RF_TRANSMIT_TIMEOUT[] = "ir_rf_tx";
void APIServer::register_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn) {
this->pending_ir_rf_transmits_.push_back({device_id, key, App.get_loop_component_start_time(), conn});
if (this->pending_ir_rf_transmits_.size() == 1) {
this->set_timeout(IR_RF_TRANSMIT_TIMEOUT, IR_RF_TRANSMIT_TIMEOUT_MS,
[this]() { this->expire_pending_ir_rf_transmits_(); });
}
}
void APIServer::fail_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn) {
auto &pending = this->pending_ir_rf_transmits_;
for (size_t i = pending.size(); i-- > 0;) {
if (pending[i].connection == conn && pending[i].key == key && pending[i].device_id == device_id) {
this->complete_pending_ir_rf_transmit_(i, false);
return;
}
}
}
void APIServer::send_infrared_rf_transmit_complete(const EntityBase &entity) {
uint32_t device_id = 0;
#ifdef USE_DEVICES
device_id = entity.get_device_id();
#endif
const uint32_t key = entity.get_object_id_hash();
auto &pending = this->pending_ir_rf_transmits_;
for (size_t i = 0; i < pending.size(); i++) {
if (pending[i].key == key && pending[i].device_id == device_id) {
this->complete_pending_ir_rf_transmit_(i, true);
return;
}
}
}
void APIServer::complete_pending_ir_rf_transmit_(size_t index, bool success) {
auto &pending = this->pending_ir_rf_transmits_;
InfraredRFTransmitCompleteResponse resp{};
#ifdef USE_DEVICES
resp.device_id = pending[index].device_id;
#endif
resp.key = pending[index].key;
resp.success = success;
pending[index].connection->send_infrared_rf_transmit_complete_response(resp);
pending.erase(pending.begin() + index);
}
void APIServer::unregister_pending_ir_rf_transmits_for_connection_(APIConnection *conn) {
auto &pending = this->pending_ir_rf_transmits_;
pending.erase(std::remove_if(pending.begin(), pending.end(),
[conn](const PendingIrRfTransmit &entry) { return entry.connection == conn; }),
pending.end());
}
// A timer firing on an empty list is harmless, so nothing cancels it
void APIServer::expire_pending_ir_rf_transmits_() {
const uint32_t now = App.get_loop_component_start_time();
auto &pending = this->pending_ir_rf_transmits_;
while (!pending.empty() && now - pending.front().registered_ms >= IR_RF_TRANSMIT_TIMEOUT_MS) {
ESP_LOGW(TAG, "IR/RF transmit %" PRIu32 " never reported completion", pending.front().key);
this->complete_pending_ir_rf_transmit_(0, false);
}
if (!pending.empty()) {
this->set_timeout(IR_RF_TRANSMIT_TIMEOUT, IR_RF_TRANSMIT_TIMEOUT_MS - (now - pending.front().registered_ms),
[this]() { this->expire_pending_ir_rf_transmits_(); });
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
+20
View File
@@ -196,6 +196,10 @@ class APIServer final : public Component,
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
// Completion replies for InfraredRFTransmitRawTimingsRequest (API 1.18+); register before perform()
void register_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn);
void fail_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn);
void send_infrared_rf_transmit_complete(const EntityBase &entity);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
@@ -342,6 +346,22 @@ class APIServer final : public Component,
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
struct PendingIrRfTransmit {
uint32_t device_id;
uint32_t key;
uint32_t registered_ms;
APIConnection *connection;
};
// FIFO per entity: entities forward only completions of frames they submitted, and each
// completion pops the oldest match, so entries are never reordered.
// Unlike action calls, transmits share one named expiry timer: they are frequent and
// a scheduler item per entry would churn the heap.
std::vector<PendingIrRfTransmit> pending_ir_rf_transmits_;
void complete_pending_ir_rf_transmit_(size_t index, bool success);
void unregister_pending_ir_rf_transmits_for_connection_(APIConnection *conn);
void expire_pending_ir_rf_transmits_();
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse {
uint32_t call_id;
+24 -10
View File
@@ -47,11 +47,7 @@ InfraredCall &InfraredCall::set_repeat_count(uint32_t count) {
return *this;
}
void InfraredCall::perform() {
if (this->parent_ != nullptr) {
this->parent_->control(*this);
}
}
bool InfraredCall::perform() { return this->parent_ != nullptr && this->parent_->control(*this); }
// ========== Infrared ==========
@@ -64,6 +60,15 @@ void Infrared::setup() {
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
#if defined(USE_API) && defined(USE_IR_RF)
if (this->transmitter_ != nullptr) {
// only frames this entity submitted; YAML automations share the transmitter
this->transmitter_->set_on_complete_callback([this](uint32_t seq) {
if (seq == this->inflight_seq_)
this->notify_transmit_complete_();
});
}
#endif
}
void Infrared::dump_config() {
@@ -75,15 +80,15 @@ void Infrared::dump_config() {
YESNO(this->traits_.get_supports_receiver()));
}
void Infrared::control(const InfraredCall &call) {
bool Infrared::control(const InfraredCall &call) {
if (this->transmitter_ == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
return false;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
return false;
}
// Create transmit data object
@@ -107,7 +112,7 @@ void Infrared::control(const InfraredCall &call) {
// 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;
return false;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
@@ -115,7 +120,7 @@ void Infrared::control(const InfraredCall &call) {
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;
return false;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
@@ -133,7 +138,16 @@ void Infrared::control(const InfraredCall &call) {
}
// Perform transmission
this->inflight_seq_ = transmit_call.get_seq();
transmit_call.perform();
return true;
}
void Infrared::notify_transmit_complete_() {
#if defined(USE_API) && defined(USE_IR_RF)
if (api::global_api_server != nullptr)
api::global_api_server->send_infrared_rf_transmit_complete(*this);
#endif
}
uint32_t Infrared::get_capability_flags() const {
+7 -4
View File
@@ -54,8 +54,8 @@ class InfraredCall {
/// 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();
/// Perform the transmission; returns true if a frame was handed to the transmitter
bool perform();
/// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
@@ -145,8 +145,11 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
protected:
friend class InfraredCall;
/// Perform the actual transmission (called by InfraredCall)
virtual void control(const InfraredCall &call);
/// Perform the actual transmission (called by InfraredCall); false if nothing was transmitted
virtual bool control(const InfraredCall &call);
/// Forwards the transmitter's completion to the API server
void notify_transmit_complete_();
uint32_t inflight_seq_{0}; // seq of the frame this entity submitted last
// Underlying hardware components
remote_base::RemoteReceiverBase *receiver_{nullptr};
+21 -10
View File
@@ -12,16 +12,16 @@ static const char *const TAG = "ir_rf_proxy";
// 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) {
static bool transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
const CallT &call, uint32_t &inflight_seq) {
if (transmitter == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
return false;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
return false;
}
auto transmit_call = transmitter->transmit();
@@ -36,14 +36,14 @@ static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter
} else if (call.is_base64url()) {
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
return false;
}
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;
return false;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
@@ -58,7 +58,9 @@ static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter
transmit_call.set_send_times(call.get_repeat_count());
}
inflight_seq = transmit_call.get_seq();
transmit_call.perform();
return true;
}
// ========== IrRfProxy (Infrared platform) ==========
@@ -80,9 +82,9 @@ void IrRfProxy::dump_config() {
}
}
void IrRfProxy::control(const infrared::InfraredCall &call) {
bool IrRfProxy::control(const infrared::InfraredCall &call) {
uint32_t carrier = call.get_carrier_frequency().value_or(0);
transmit_raw_timings(this->transmitter_, carrier, call);
return transmit_raw_timings(this->transmitter_, carrier, call, this->inflight_seq_);
}
#endif // USE_IR_RF
@@ -101,6 +103,15 @@ void RfProxy::setup() {
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
if (this->transmitter_ != nullptr) {
// only frames this entity submitted; YAML automations share the transmitter
this->transmitter_->set_on_complete_callback([this](uint32_t seq) {
if (seq == this->inflight_seq_)
this->notify_transmit_complete_();
});
}
#endif
}
void RfProxy::dump_config() {
@@ -122,11 +133,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 transmit_raw_timings(this->transmitter_, 0, call, this->inflight_seq_);
}
#endif // USE_RADIO_FREQUENCY
+2 -2
View File
@@ -35,7 +35,7 @@ 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;
bool control(const infrared::InfraredCall &call) override;
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
@@ -63,7 +63,7 @@ class RfProxy final : public radio_frequency::RadioFrequency {
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;
bool control(const radio_frequency::RadioFrequencyCall &call) override;
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
remote_base::RemoteReceiverBase *receiver_{nullptr};
@@ -52,14 +52,15 @@ RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t 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);
bool RadioFrequencyCall::perform() {
if (this->parent_ == nullptr) {
return false;
}
// 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);
return this->parent_->control(*this);
}
// ========== RadioFrequency ==========
@@ -108,4 +109,11 @@ bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) {
return false; // Don't consume the event, allow other listeners to process it
}
void RadioFrequency::notify_transmit_complete_() {
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
if (api::global_api_server != nullptr)
api::global_api_server->send_infrared_rf_transmit_complete(*this);
#endif
}
} // namespace esphome::radio_frequency
@@ -64,8 +64,8 @@ class RadioFrequencyCall {
/// 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();
/// Perform the transmission; returns true if a frame was handed to the transmitter
bool perform();
/// Get the frequency in Hz
const optional<uint32_t> &get_frequency() const { return this->frequency_hz_; }
@@ -184,7 +184,11 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
/// 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 if nothing was transmitted.
virtual bool control(const RadioFrequencyCall &call) = 0;
/// Forwards the transmitter's completion to the API server; platforms hook their transmitter to it
void notify_transmit_complete_();
uint32_t inflight_seq_{0}; // seq of the frame this entity submitted last
// Traits describing capabilities
RadioFrequencyTraits traits_;
@@ -163,7 +163,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, uint32_t seq) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();
char buffer[256];
@@ -195,6 +195,7 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
ESP_LOGVV(TAG, "%s", buffer);
}
#endif
this->current_seq_ = seq;
this->send_internal(send_times, send_wait);
}
} // namespace esphome::remote_base
+29 -5
View File
@@ -146,21 +146,24 @@ class RemoteTransmitterBase : public RemoteComponentBase {
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
class TransmitCall {
public:
explicit TransmitCall(RemoteTransmitterBase *parent) : parent_(parent) {}
TransmitCall(RemoteTransmitterBase *parent, uint32_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 callback
uint32_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};
uint32_t seq_;
};
TransmitCall transmit() {
this->temp_.reset();
return TransmitCall(this);
return TransmitCall(this, ++this->next_seq_);
}
template<typename Protocol>
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
@@ -170,11 +173,32 @@ class RemoteTransmitterBase : public RemoteComponentBase {
call.set_send_wait(send_wait);
call.perform();
}
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
/// Called with the TransmitCall's seq once that transmission has finished, after the last
/// repeat and before the on_complete trigger. One slot: a transmitter is driven by a single
/// infrared or radio_frequency entity.
template<typename F> void set_on_complete_callback(F &&callback) {
this->complete_callback_ = Callback<void(uint32_t)>::create(std::forward<F>(callback));
}
#endif
protected:
void send_(uint32_t send_times, uint32_t send_wait);
void send_(uint32_t send_times, uint32_t send_wait, uint32_t seq);
virtual void send_internal(uint32_t send_times, uint32_t send_wait) = 0;
void send_single_() { this->send_(1, 0); }
void send_single_() { this->send_(1, 0, ++this->next_seq_); }
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void notify_complete_(uint32_t seq) {
if (this->complete_callback_.fn_ != nullptr)
this->complete_callback_.call(seq);
}
Callback<void(uint32_t)> complete_callback_{};
#else
void notify_complete_(uint32_t seq) {}
#endif
// seq handed to the platform by send_(); platforms copy it when they accept the frame
uint32_t next_seq_{0};
uint32_t current_seq_{0};
/// Use same vector for all transmits, avoids many allocations
RemoteTransmitData temp_;
@@ -80,6 +80,7 @@ void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
this->inflight_seq_ = this->current_seq_;
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->transmit_trigger_.trigger();
@@ -114,7 +115,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
}
}
this->complete_trigger_.trigger();
this->fire_complete_();
}
} // namespace esphome::remote_transmitter
@@ -82,6 +82,12 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override;
// completion callbacks run before the user's on_complete automation
void fire_complete_() {
this->notify_complete_(this->inflight_seq_);
this->complete_trigger_.trigger();
}
uint32_t inflight_seq_{0}; // seq of the frame whose completion is still to be reported
#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_();
}
// Waits until no chain is in flight, delivering any deferred completions; a completion
@@ -137,6 +137,7 @@ void RemoteTransmitterComponent::wait_until_idle_() {
// Stages the repeat schedule and stall deadline, then starts the interrupt chain
void RemoteTransmitterComponent::arm_chain_(uint32_t send_times, uint32_t send_wait) {
this->inflight_seq_ = this->current_seq_;
this->isr_repeats_left_ = send_times;
this->isr_send_wait_ = send_wait;
this->isr_index_ = 0;
@@ -213,7 +213,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
this->status_set_warning();
}
this->complete_trigger_.trigger();
this->fire_complete_();
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
@@ -228,6 +228,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
this->inflight_seq_ = this->current_seq_;
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
@@ -300,6 +301,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed())
return;
this->inflight_seq_ = this->current_seq_;
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
@@ -364,7 +366,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (i + 1 < send_times)
delayMicroseconds(send_wait);
}
this->complete_trigger_.trigger();
this->fire_complete_();
}
#endif
@@ -149,6 +149,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
return;
}
ESP_LOGD(TAG, "Sending remote code");
this->inflight_seq_ = this->current_seq_;
const uint32_t carrier_frequency = this->temp_.get_carrier_frequency();
// unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
float mark_duty =
@@ -194,7 +195,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
}
}
this->complete_trigger_.trigger();
this->fire_complete_();
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
@@ -19,7 +19,7 @@ class InfraredCall {
return *this;
}
InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; }
void perform() {}
bool perform() { return false; }
protected:
Infrared *parent_;
@@ -23,7 +23,7 @@ class RadioFrequencyCall {
RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
return *this;
}
void perform() {}
bool perform() { return false; }
protected:
RadioFrequency *parent_;