mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 07:17:33 +00:00
[api] Reply when an infrared/RF raw timings transmit has completed
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
Reference in New Issue
Block a user