ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
rf_bridge.cpp
Go to the documentation of this file.
1#include "rf_bridge.h"
4#include "esphome/core/log.h"
5#include <cinttypes>
6#include <cstring>
7
9
10static const char *const TAG = "rf_bridge";
11
13 ESP_LOGV(TAG, "Sending ACK");
14 this->write(RF_CODE_START);
15 this->write(RF_CODE_ACK);
16 this->write(RF_CODE_STOP);
17 this->flush();
18}
19
21 if (this->bucket_frame_candidate_ && byte == RF_CODE_START) {
22 // A queued next frame proves the trailing 0x55 really was the bucket
23 // frame's terminator: Portisch builds pulse entries from alternating
24 // signal edges, so the two level bits inside one pulse byte are always
25 // opposite — 0xAA (two high-level nibbles) cannot occur in pulse data.
26 // Finalize before this byte starts the new frame, so back-to-back
27 // deliveries are split even when loop() never observed a quiet gap
28 // between them.
30 }
31 size_t at = this->rx_buffer_.size();
32 this->rx_buffer_.push_back(byte);
33 const uint8_t *raw = &this->rx_buffer_[0];
34
35 ESP_LOGVV(TAG, "Processing byte: 0x%02X", byte);
36
37 // Byte 0: Start
38 if (at == 0)
39 return byte == RF_CODE_START;
40
41 // Byte 1: Action
42 if (at == 1)
43 return byte >= RF_CODE_ACK && byte <= RF_CODE_RFIN_BUCKET;
44 uint8_t action = raw[1];
45
46 switch (action) {
47 case RF_CODE_ACK:
48 ESP_LOGD(TAG, "Action OK");
49 break;
50 case RF_CODE_LEARN_KO:
51 ESP_LOGD(TAG, "Learning timeout");
52 break;
53 case RF_CODE_LEARN_OK:
54 case RF_CODE_RFIN: {
55 if (byte != RF_CODE_STOP || at < RF_MESSAGE_SIZE + 2)
56 return true;
57
58 RFBridgeData data;
59 data.sync = (raw[2] << 8) | raw[3];
60 data.low = (raw[4] << 8) | raw[5];
61 data.high = (raw[6] << 8) | raw[7];
62 data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10];
63
64 if (action == RF_CODE_LEARN_OK) {
65 ESP_LOGD(TAG, "Learning success");
66 }
67
68 ESP_LOGI(TAG,
69 "Received RFBridge Code: sync=0x%04" PRIX16 " low=0x%04" PRIX16 " high=0x%04" PRIX16
70 " code=0x%06" PRIX32,
71 data.sync, data.low, data.high, data.code);
72 this->data_callback_.call(data);
73 break;
74 }
75 case RF_CODE_LEARN_OK_NEW:
76 case RF_CODE_ADVANCED_RFIN: {
77 if (byte != RF_CODE_STOP) {
78 return at < (raw[2] + 3);
79 }
80
82
83 data.length = raw[2];
84 data.protocol = raw[3];
85 char next_byte[3]; // 2 hex chars + null
86 for (uint8_t i = 0; i + 1 < data.length; i++) {
87 buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[4 + i]);
88 data.code += next_byte;
89 }
90
91 ESP_LOGI(TAG, "Received RFBridge Advanced Code: length=0x%02X protocol=0x%02X code=0x%s", data.length,
92 data.protocol, data.code.c_str());
93 this->advanced_data_callback_.call(data);
94 break;
95 }
96 case RF_CODE_RFIN_BUCKET: {
97 if (at == 2) {
98 // The count byte: Portisch sends at most 7 buckets + sync, so 0 or
99 // >8 cannot be a genuine capture — reject before it can occupy the
100 // buffer for a full frame timeout.
101 return byte != 0 && byte <= B1_MAX_BUCKET_COUNT;
102 }
103 // 0x55 is legal DATA inside a B1 frame: bucket durations are sent
104 // with only their HIGH byte masked to 7 bits, so a duration such as
105 // 0x0155 puts a raw 0x55 low byte inside the table — the first 0x55
106 // must therefore not end the capture. The header declares the table
107 // length (raw[2] pairs), so a 0x55 there is always data; one at or
108 // past the first pulse index is a terminator CANDIDATE, confirmed
109 // once the UART goes quiet (finish_bucket_frame_ in loop()).
110 this->bucket_frame_candidate_ = byte == RF_CODE_STOP && at >= 3 + static_cast<size_t>(raw[2]) * 2;
111 return true;
112 }
113 default:
114 ESP_LOGW(TAG, "Unknown action: 0x%02X", action);
115 break;
116 }
117
118 ESP_LOGVV(TAG, "Parsed: 0x%02X", byte);
119
120 if (byte == RF_CODE_STOP && action != RF_CODE_ACK)
121 this->ack_();
122
123 // return false to reset buffer
124 return false;
125}
126
128 if (this->rx_buffer_.size() < 4) {
129 // The candidate flag requires a header + non-empty bucket table, so
130 // this cannot happen while flag and buffer stay consistent; guard the
131 // raw[2] / size-1 reads against any future divergence anyway.
132 this->rx_buffer_.clear();
133 this->bucket_frame_candidate_ = false;
134 return;
135 }
136 const uint8_t *raw = this->rx_buffer_.data();
137 const size_t at = this->rx_buffer_.size() - 1;
138
139 uint8_t buckets = raw[2] << 1;
140 std::string str;
141 char next_byte[3]; // 2 hex chars + null
142
143 for (uint32_t i = 0; i <= at; i++) {
144 buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]);
145 str += next_byte;
146 if ((i > 3) && buckets) {
147 buckets--;
148 }
149 if ((i < 3) || (buckets % 2) || (i == at - 1)) {
150 str += " ";
151 }
152 }
153 ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str());
154
155 // Deliberately NOT ACKed: Portisch's B1 command handler leaves its
156 // last_sniffing_command at the previous mode (RF_CODE_RFIN), and its
157 // host-ACK handler re-arms sniffing from that stale value — so ACKing a
158 // bucket delivery silently reverts the radio to standard sniffing and
159 // ends bucket capture. Its delivery path is fire-and-forget and never
160 // waits for a host ACK. Stock Itead firmware never sends B1 frames, so
161 // suppressing this ACK cannot change stock-firmware behavior.
162 // https://github.com/esphome/esphome/issues/17682
163
164 this->rx_buffer_.clear();
165 this->bucket_frame_candidate_ = false;
166}
167
168void RFBridgeComponent::write_byte_str_(const std::string &codes) {
169 uint8_t code;
170 int size = codes.length();
171 for (int i = 0; i < size; i += 2) {
172 code = strtol(codes.substr(i, 2).c_str(), nullptr, 16);
173 this->write(code);
174 }
175}
176
179 size_t avail = this->available();
180 if (avail == 0 && this->bucket_frame_candidate_ && now - this->last_bridge_byte_ > BUCKET_CANDIDATE_QUIET_MS) {
181 // The trailing 0x55 was followed by UART quiet, so it really was the
182 // frame terminator and not an interior data byte.
183 this->finish_bucket_frame_();
184 this->last_bridge_byte_ = now;
185 }
186 const bool receiving_bucket = this->rx_buffer_.size() >= 2 && this->rx_buffer_[1] == RF_CODE_RFIN_BUCKET;
187 if (receiving_bucket) {
188 // Never declare an in-progress bucket frame dead while its continuation
189 // bytes are already queued: a stalled loop() otherwise discards a live
190 // frame that the UART buffer proves is still arriving.
191 if (avail == 0 && now - this->last_bridge_byte_ > BUCKET_FRAME_TIMEOUT_MS) {
192 ESP_LOGD(TAG, "Discarding incomplete RFBridge Bucket frame (%u bytes)",
193 static_cast<unsigned>(this->rx_buffer_.size()));
194 this->rx_buffer_.clear();
195 this->bucket_frame_candidate_ = false;
196 this->last_bridge_byte_ = now;
197 }
198 } else if (now - this->last_bridge_byte_ > 50) {
199 this->rx_buffer_.clear();
200 this->bucket_frame_candidate_ = false;
201 this->last_bridge_byte_ = now;
202 }
203
204 while (avail > 0) {
205 uint8_t buf[64];
206 size_t to_read = std::min(avail, sizeof(buf));
207 if (!this->read_array(buf, to_read)) {
208 break;
209 }
210 avail -= to_read;
211 for (size_t i = 0; i < to_read; i++) {
212 if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) {
213 this->rx_buffer_.clear();
214 this->bucket_frame_candidate_ = false;
215 }
216 if (this->parse_bridge_byte_(buf[i])) {
217 ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
218 this->last_bridge_byte_ = now;
219 } else {
220 this->rx_buffer_.clear();
221 this->bucket_frame_candidate_ = false;
222 }
223 }
224 }
225}
226
228 ESP_LOGD(TAG, "Sending code: sync=0x%04" PRIX16 " low=0x%04" PRIX16 " high=0x%04" PRIX16 " code=0x%06" PRIX32,
229 data.sync, data.low, data.high, data.code);
230 this->write(RF_CODE_START);
231 this->write(RF_CODE_RFOUT);
232 this->write((data.sync >> 8) & 0xFF);
233 this->write(data.sync & 0xFF);
234 this->write((data.low >> 8) & 0xFF);
235 this->write(data.low & 0xFF);
236 this->write((data.high >> 8) & 0xFF);
237 this->write(data.high & 0xFF);
238 this->write((data.code >> 16) & 0xFF);
239 this->write((data.code >> 8) & 0xFF);
240 this->write(data.code & 0xFF);
241 this->write(RF_CODE_STOP);
242 this->flush();
243}
244
246 ESP_LOGD(TAG, "Sending advanced code: length=0x%02X protocol=0x%02X code=0x%s", data.length, data.protocol,
247 data.code.c_str());
248 this->write(RF_CODE_START);
249 this->write(RF_CODE_RFOUT_NEW);
250 this->write(data.length & 0xFF);
251 this->write(data.protocol & 0xFF);
252 this->write_byte_str_(data.code);
253 this->write(RF_CODE_STOP);
254 this->flush();
255}
256
258 ESP_LOGD(TAG, "Learning mode");
259 this->write(RF_CODE_START);
260 this->write(RF_CODE_LEARN);
261 this->write(RF_CODE_STOP);
262 this->flush();
263}
264
265void RFBridgeComponent::dump_config() { ESP_LOGCONFIG(TAG, "RF_Bridge:"); }
266
268 ESP_LOGI(TAG, "Advanced Sniffing on");
269 this->write(RF_CODE_START);
270 this->write(RF_CODE_SNIFFING_ON);
271 this->write(RF_CODE_STOP);
272 this->flush();
273}
274
276 ESP_LOGI(TAG, "Advanced Sniffing off");
277 this->write(RF_CODE_START);
278 this->write(RF_CODE_SNIFFING_OFF);
279 this->write(RF_CODE_STOP);
280 this->flush();
281}
282
284 ESP_LOGI(TAG, "Raw Bucket Sniffing on");
285 this->write(RF_CODE_START);
286 this->write(RF_CODE_RFIN_BUCKET);
287 this->write(RF_CODE_STOP);
288 this->flush();
289}
290
291void RFBridgeComponent::send_raw(const std::string &raw_code) {
292 ESP_LOGD(TAG, "Sending Raw Code: %s", raw_code.c_str());
293
294 this->write_byte_str_(raw_code);
295 this->flush();
296}
297
298void RFBridgeComponent::beep(uint16_t ms) {
299 ESP_LOGD(TAG, "Beeping for %hu ms", ms);
300
301 this->write(RF_CODE_START);
302 this->write(RF_CODE_BEEP);
303 this->write((ms >> 8) & 0xFF);
304 this->write(ms & 0xFF);
305 this->write(RF_CODE_STOP);
306 this->flush();
307}
308
309} // namespace esphome::rf_bridge
uint8_t raw[35]
Definition bl0939.h:0
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 send_raw(const std::string &code)
CallbackManager< void(RFBridgeAdvancedData)> advanced_data_callback_
Definition rf_bridge.h:89
void send_advanced_code(const RFBridgeAdvancedData &data)
CallbackManager< void(RFBridgeData)> data_callback_
Definition rf_bridge.h:88
void write_byte_str_(const std::string &codes)
void send_code(RFBridgeData data)
std::vector< uint8_t > rx_buffer_
Definition rf_bridge.h:84
UARTFlushResult flush()
Definition uart.h:49
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:39
size_t write(uint8_t data)
Definition uart.h:58
uint16_t size
Definition helpers.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t