ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ir_rf_base.cpp
Go to the documentation of this file.
1#include "ir_rf_base.h"
2
3#include <algorithm>
4#include <cinttypes>
5
6#include "esphome/core/log.h"
7
8#ifdef USE_API
12#endif
13
15
16ESPHOME_LOG_TAG(TAG, "ir_rf");
17
18#if defined(USE_API) && defined(USE_IR_RF)
19// A missing completion is answered as failed this long after the frame should have left the
20// wire, and an owed reply the client never reads is dropped this long after the first refusal
21static constexpr uint32_t API_REPLY_TIMEOUT_MS = 30000;
22#endif
23#ifdef USE_IR_RF_TRANSMIT_COMPLETE
24// Longest air time added to the deadline, in 16 ms ticks (8 min); with the 30 s above the
25// deadline stays within the signed 16 bit tick window loop() compares against
26static constexpr uint16_t API_REPLY_MAX_AIR_TICKS = 30000;
27#endif
28
29#if defined(USE_API) && defined(USE_IR_RF)
30static uint16_t api_reply_ticks_from_now() {
31 return static_cast<uint16_t>((App.get_loop_component_start_time() >> 4) + (API_REPLY_TIMEOUT_MS >> 4));
32}
33#endif
34
36 // merged, not assigned: a platform may have set a flag for its own hardware before setup()
37 if (this->has_transmitter())
38 this->supports_transmitter_ = true;
39 if (this->has_receiver())
40 this->supports_receiver_ = true;
41}
42
43bool IrRfEntity::transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz) {
44 if (this->transmitter_ == nullptr) {
45 ESP_LOGW(TAG, "No transmitter configured");
46 return false;
47 }
48
49 if (!call.has_raw_timings()) {
50 ESP_LOGE(TAG, "No raw timings provided");
51 return false;
52 }
53
54 auto transmit_call = this->transmitter_->transmit();
55 auto *transmit_data = transmit_call.get_data();
56 transmit_data->set_carrier_frequency(carrier_frequency_hz);
57
58 if (call.is_packed()) {
59 // Zero-copy from packed protobuf data
60 transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
61 call.get_packed_count());
62 ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
63 call.get_repeat_count());
64 } else if (call.is_base64url()) {
65 // Decode base64url (URL-safe) into transmit buffer
66 if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
67 ESP_LOGE(TAG, "Invalid base64url data");
68 return false;
69 }
70 constexpr int32_t max_timing_us = 500000; // 500ms absolute max
71 for (int32_t timing : transmit_data->get_data()) {
72 int32_t abs_timing = timing < 0 ? -timing : timing;
73 if (abs_timing > max_timing_us) {
74 ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
75 return false;
76 }
77 }
78 ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
79 call.get_repeat_count());
80 } else {
81 // From vector (lambdas/automations)
82 transmit_data->set_data(call.get_raw_timings());
83 ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
84 call.get_repeat_count());
85 }
86
87 // one answer for every backend: a frame that decoded to nothing is refused here
88 if (transmit_data->get_data().empty()) {
89 ESP_LOGE(TAG, "No raw timings provided");
90 return false;
91 }
92
93 if (call.get_repeat_count() > 0) {
94 transmit_call.set_send_times(call.get_repeat_count());
95 }
96
97#ifdef USE_IR_RF_TRANSMIT_COMPLETE
98 // only the API frame expect_api_reply_() armed claims the seq and extends its deadline
99 if (call.wants_api_reply() && this->api_reply_ == ApiReply::API_REPLY_WAITING) {
100 this->inflight_seq_ = transmit_call.get_seq();
101 // a long frame must not be answered as failed while still on the wire: the 30 s safety net
102 // starts after this frame's own air time (capped so the tick comparison cannot wrap)
103 uint64_t frame_us = 0;
104 for (const int64_t timing : transmit_data->get_data())
105 frame_us += timing < 0 ? -timing : timing;
106 const uint64_t air_ticks = (frame_us * std::max<uint32_t>(call.get_repeat_count(), 1) / 1000) >> 4;
107 this->api_reply_deadline_ += static_cast<uint16_t>(std::min<uint64_t>(air_ticks, API_REPLY_MAX_AIR_TICKS));
108 }
109#endif
110 transmit_call.perform();
111 return true;
112}
113
115#if defined(USE_API) && defined(USE_IR_RF)
116 if (api::global_api_server != nullptr) {
117#ifdef USE_DEVICES
118 uint32_t device_id = this->get_device_id();
119#else
120 uint32_t device_id = 0;
121#endif
123 }
124#endif
125 return false; // Don't consume the event, allow other listeners to process it
126}
127
128#if defined(USE_API) && defined(USE_IR_RF)
129
130#ifdef USE_IR_RF_TRANSMIT_COMPLETE
132 // only the frame this entity submitted; YAML automations and other entities share the transmitter
133 if (transmitter == this->transmitter_ && seq == this->inflight_seq_ &&
135 this->finish_api_reply_(sent);
136}
137#endif
138
140 // only an unpaced client gets here: its earlier request is answered as not started, and a reply
141 // the buffer still owes gets one last try; if that fails too the slot stays with it and the
142 // new request is refused rather than answered to nobody
144 this->finish_api_reply_(false);
145 if (this->api_reply_ != ApiReply::API_REPLY_NONE && !this->send_api_reply_()) {
146 ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("refused, reply still owed"));
147 this->refuse_api_call_(conn);
148 return false;
149 }
150 this->api_reply_connection_ = conn;
151 this->api_reply_deadline_ = api_reply_ticks_from_now();
153 return true;
154}
155
157 uint32_t device_id = 0;
158#ifdef USE_DEVICES
159 device_id = this->get_device_id();
160#endif
161 // no slot to retry from; a drop here is covered by the client's own timeout
162 if (!conn->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(), false)) {
163 API_LOG_MSG_DROPPED(TAG, "IR/RF reply");
164 }
165}
166
169 if (!this->send_api_reply_()) {
170 // the retry gets its own window
171 this->api_reply_deadline_ = api_reply_ticks_from_now();
172 }
173}
174
176 uint32_t device_id = 0;
177#ifdef USE_DEVICES
178 device_id = this->get_device_id();
179#endif
180 // Refused by a full TCP buffer: the reply stays owed and loop() retries it, since a lost
181 // reply would stall the client's pacing for good (same shape as bluetooth_proxy)
182 if (!this->api_reply_connection_->send_infrared_rf_transmit_complete(device_id, this->get_object_id_hash(),
183 this->api_reply_ == ApiReply::API_REPLY_OWED_OK))
184 return false;
185 this->clear_api_reply_();
186 return true;
187}
188
189// Only runs while an API reply is pending: retries an owed one until it goes out or the client is
190// gone, and expires a transmit that never reported (a platform overriding control() without wiring
191// its transmitter's completion)
194 this->disable_loop();
195 return;
196 }
197 const auto remaining = static_cast<int16_t>(this->api_reply_deadline_ - (App.get_loop_component_start_time() >> 4));
199 if (this->send_api_reply_() || remaining > 0)
200 return;
201 ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("reply dropped, client not reading"));
202 this->clear_api_reply_();
203 return;
204 }
205 if (remaining > 0)
206 return;
207 ESP_LOGW(TAG, "'%s': transmit %s", this->get_name().c_str(), LOG_STR_LITERAL("never reported completion"));
208 this->finish_api_reply_(false);
209}
210
212 if (this->api_reply_connection_ == conn) {
213 this->clear_api_reply_();
214 }
215}
216#endif
217
218} // namespace esphome::ir_rf_base
219
220#ifdef USE_IR_RF_TRANSMIT_COMPLETE
222
223void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent) {
224#ifdef USE_INFRARED
225 for (auto *entity : App.get_infrareds()) {
226 entity->on_transmit_complete(transmitter, seq, sent);
227 }
228#endif
229#ifdef USE_RADIO_FREQUENCY
230 for (auto *entity : App.get_radio_frequencies()) {
231 entity->on_transmit_complete(transmitter, seq, sent);
232 }
233#endif
234}
235
236} // namespace esphome::remote_base
237#endif
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void disable_loop()
Disable this component's loop.
const StringRef & get_name() const
Definition entity_base.h:71
uint32_t get_object_id_hash() const
Definition entity_base.h:77
uint32_t get_device_id() const
bool send_infrared_rf_transmit_complete(uint32_t device_id, uint32_t key, bool success)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector< int32_t > *timings)
Raw timings of a transmit call, in one of three caller-owned forms.
Definition ir_rf_base.h:30
void on_transmit_complete(remote_base::RemoteTransmitterBase *transmitter, uint16_t seq, bool sent)
Called for every finished frame on every transmitter; answers the API request when the frame is the o...
void refuse_api_call_(api::APIConnection *conn)
api::APIConnection * api_reply_connection_
Definition ir_rf_base.h:165
bool on_receive(remote_base::RemoteReceiveData data) override
Forwards a received frame to the API; never consumes it, so other listeners still run.
bool expect_api_reply_(api::APIConnection *conn)
Claims the reply slot for conn; false when it still owes a reply that cannot be sent.
remote_base::RemoteTransmitterBase * transmitter_
Definition ir_rf_base.h:169
void setup() override
Reports the configured transports, listens on the receiver and hooks the transmitter's completion; a ...
void finish_api_reply_(bool success)
bool transmit_raw_(const IrRfCallData &call, uint32_t carrier_frequency_hz)
Hands the call's timings to the transmitter; the default transmit path of both entity types.
void on_api_connection_closed(api::APIConnection *conn)
The API server calls this when a client disconnects, so no reply goes to a stale pointer.
const RawTimings & get_raw_data() const
Definition remote_base.h:60
void set_carrier_frequency(uint32_t carrier_frequency)
Definition remote_base.h:31
APIServer * global_api_server
ESPHOME_LOG_TAG(TAG, "ir_rf")
void ir_rf_transmit_complete(RemoteTransmitterBase *transmitter, uint16_t seq, bool sent)
Defined by ir_rf_base: answers the API request waiting on the entity that submitted seq; sent is fals...
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t seq