ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
hdlc_reliability.cpp
Go to the documentation of this file.
1#include "hdlc_reliability.h"
2#ifdef USE_PACKET_INTERFACE_HDLC
4#include "esphome/core/log.h"
5
7
8ESPHOME_LOG_TAG(TAG, "packet_interface.hdlc");
9
10static constexpr uint32_t RETRANSMIT_TIMER_ID = 0;
11static constexpr uint32_t ACK_TIMER_ID = 1;
12static constexpr uint32_t SEND_RETRY_TIMER_ID = 2;
13// A frame or acknowledgement the lower layer refused, for example while a radio was receiving, is sent again after this
14static constexpr uint32_t SEND_RETRY_MS = 50;
15// SABM is repeated at this interval, or the timeout if longer, until the peer answers
16static constexpr uint32_t RESET_INTERVAL_MS = 1000;
17
18// Control byte formats, with the poll/final bit (0x10) ignored
19static constexpr uint8_t POLL_FINAL_BIT = 0x10;
20static constexpr uint8_t SUPERVISORY_FRAME = 0x01;
21static constexpr uint8_t SUPERVISORY_RR = 0x00;
22static constexpr uint8_t SUPERVISORY_REJ = 0x02;
23static constexpr uint8_t UNNUMBERED_SABM = 0x2F;
24static constexpr uint8_t UNNUMBERED_UA = 0x63;
25
26static uint16_t frame_crc(uint8_t control, const uint8_t *payload, size_t length) {
27 uint16_t crc = crc16(&control, 1);
28 return crc16(payload, length, crc);
29}
30
32 : PacketInterface(remaining_packet_size(lower->get_max_packet_size(), FRAME_OVERHEAD)), lower_(lower) {
34}
35
37 if (this->max_packet_size_ == 0) {
38 this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
39 return;
40 }
41 RAMAllocator<uint8_t> allocator;
42 this->tx_slots_ = allocator.allocate(size_t{this->max_queue_length_} * this->max_packet_size_);
43 if (this->tx_slots_ == nullptr) {
44 this->mark_failed(LOG_STR("Not enough memory for the send queue"));
45 return;
46 }
48 [this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
49}
50
52 ESP_LOGCONFIG(TAG,
53 "HDLC Reliability on '%s':\n"
54 " Max Packet Size: %zu\n"
55 " Timeout: %" PRIu32 "ms\n"
56 " Ack Delay: %" PRIu32 "ms\n"
57 " Max Queue Length: %u\n"
58 " Max Retries: %u",
59 this->name_, this->max_packet_size_, this->timeout_ms_, this->ack_delay_ms_, this->max_queue_length_,
60 this->max_retries_);
61}
62
64 // Disabled first, as sending may deliver a reply to this layer and request the loop again
65 this->disable_loop();
68 this->start_timer_();
69 this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
70 }
72 return;
73 // Queued frames go first, as each carries the acknowledgement
74 if (this->send_pending_() && this->ack_due_ && !this->send_supervisory_(SUPERVISORY_RR))
75 this->retry_send_();
76}
77
79 this->set_timeout(SEND_RETRY_TIMER_ID, SEND_RETRY_MS, [this]() { this->request_loop_(); });
80}
81
83 if (now || this->ack_delay_ms_ == 0) {
84 this->ack_due_ = true;
85 this->request_loop_();
86 } else if (!this->ack_pending_) {
87 // Waiting lets one acknowledgement cover several frames, or a reply carry it
88 this->set_timeout(ACK_TIMER_ID, this->ack_delay_ms_, [this]() {
89 this->ack_due_ = true;
90 this->request_loop_();
91 });
92 }
93 this->ack_pending_ = true;
94}
95
97 this->ack_pending_ = false;
98 this->ack_due_ = false;
99 this->cancel_timeout(ACK_TIMER_ID);
100}
101
103
105 if (this->tx_slots_ == nullptr)
106 return false;
107 size_t size = data.size();
108 if (size > this->max_packet_size_) {
109 ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
110 return false;
111 }
112 uint8_t outstanding = this->outstanding_();
113 if (outstanding >= this->max_queue_length_) {
115 ESP_LOGV(TAG, "%s: Send queue full", this->name_);
116 } else if (!this->reset_wait_logged_) {
117 // Once for each reset; the layers above keep trying while the peer is away
118 this->reset_wait_logged_ = true;
119 ESP_LOGD(TAG, "%s: Send queue full, waiting for the peer to answer the link reset", this->name_);
120 }
121 return false;
122 }
123 uint8_t seq = this->vs_;
124 data.copy_to(this->slot_(seq), size);
125 this->tx_lengths_[this->slot_index_(seq)] = size;
126 this->vs_ = (seq + 1) & 7;
127 // While the link is being reset, the frame waits in the queue and is sent when UA arrives
129 this->send_pending_();
130 if (outstanding == 0)
131 this->start_timer_();
132 }
133 return true;
134}
135
136bool HdlcReliability::send_frame_(uint8_t control, const uint8_t *payload, size_t length) {
137 uint16_t crc = frame_crc(control, payload, length);
138 uint8_t trailer[2] = {static_cast<uint8_t>(crc), static_cast<uint8_t>(crc >> 8)};
139 PacketBuffer body(payload, length);
140 PacketBuffer frame(&control, 1, length != 0 ? &body : nullptr, trailer, sizeof(trailer));
141 if (this->lower_->send_to_interface(frame, {}))
142 return true;
143 ESP_LOGV(TAG, "%s: Lower layer did not accept frame 0x%02X", this->name_, control);
144 return false;
145}
146
148 if (!this->send_frame_(SUPERVISORY_FRAME | (type << 2) | (this->vr_ << 5), nullptr, 0))
149 return false;
150 this->ack_sent_();
151 return true;
152}
153
154// Sends the next queued frame not yet sent, and returns true if none are left. One frame per loop pass lets other
155// components run between frames: on a radio, sending blocks, and the peer's frames are received in between. If the
156// lower layer refuses the frame, it is sent again shortly.
158 if (this->vn_ == this->vs_)
159 return true;
160 uint8_t control = (this->vn_ << 1) | (this->vr_ << 5);
161 if (!this->send_frame_(control, this->slot_(this->vn_), this->tx_lengths_[this->slot_index_(this->vn_)])) {
162 this->retry_send_();
163 return false;
164 }
165 // Every I-frame carries an acknowledgement, so a pending RR is no longer needed
166 this->ack_sent_();
167 this->vn_ = (this->vn_ + 1) & 7;
168 if (this->vn_ == this->vs_)
169 return true;
170 this->request_loop_();
171 return false;
172}
173
175 this->vn_ = this->va_;
176 this->send_pending_();
177}
178
180 uint32_t interval = this->timeout_ms_;
181 if (this->state_ != LinkState::LINK_STATE_CONNECTED && interval < RESET_INTERVAL_MS)
182 interval = RESET_INTERVAL_MS;
183 // Up to a quarter longer at random, so devices whose frames collided on a shared channel do not resend together
184 interval += random_uint32() % (interval / 4 + 1);
185 this->set_timeout(RETRANSMIT_TIMER_ID, interval, [this]() { this->on_timeout_(); });
186}
187
190 // Counted up to max_retries_ only, so the warning is logged once until the link comes up
192 this->retries_++;
193 if (this->retries_ == this->max_retries_) {
194 ESP_LOGW(TAG, "%s: No answer to the link reset after %u tries; check that the peer has a reliability layer",
195 this->name_, this->max_retries_);
196 }
197 }
198 this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
199 this->start_timer_();
200 return;
201 }
202 if (this->outstanding_() == 0)
203 return;
204 if (this->retries_ == this->max_retries_) {
205 ESP_LOGW(TAG, "%s: No acknowledgement after %u retries, resetting link", this->name_, this->max_retries_);
206 this->start_reset_();
207 return;
208 }
209 this->retries_++;
210 ESP_LOGV(TAG, "%s: Timeout, resending %u frames", this->name_, this->outstanding_());
211 this->resend_outstanding_();
212 this->start_timer_();
213}
214
215// Unacknowledged frames are discarded; frames queued from now on are numbered from 0 and sent once the link is up.
216// Resetting a connected link can lose data both ways, which is reported to the layers above.
218 bool was_connected = this->state_ == LinkState::LINK_STATE_CONNECTED;
220 this->va_ = this->vs_ = this->vn_ = 0;
221 this->head_ = 0;
222 this->vr_ = 0;
223 this->retries_ = 0;
224 this->rej_sent_ = false;
225 this->ack_sent_();
226 this->cancel_timeout(RETRANSMIT_TIMER_ID);
227 this->request_loop_();
228 if (was_connected)
229 this->session_callback_.call(false);
230}
231
233 ESP_LOGD(TAG, "%s: Link connected", this->name_);
235 this->reset_wait_logged_ = false;
236 this->peer_frame_seen_ = false;
237 this->retries_ = 0;
238 this->cancel_timeout(RETRANSMIT_TIMER_ID);
239 if (this->outstanding_() != 0) {
240 this->resend_outstanding_();
241 this->start_timer_();
242 }
243}
244
246 uint8_t acked = (nr - this->va_) & 7;
247 if (acked > this->outstanding_()) {
248 ESP_LOGV(TAG, "%s: Ignoring invalid N(R) %u", this->name_, nr);
249 return;
250 }
251 if (acked == 0)
252 return;
253 // The peer may have had frames that were refused when last resent
254 if (acked > ((this->vn_ - this->va_) & 7))
255 this->vn_ = nr;
256 this->head_ = (this->head_ + acked) % this->max_queue_length_;
257 this->va_ = nr;
258 this->retries_ = 0;
259 if (this->outstanding_() == 0) {
260 this->cancel_timeout(RETRANSMIT_TIMER_ID);
261 } else {
262 this->start_timer_();
263 }
264}
265
266void HdlcReliability::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
267 // Lower layers deliver packets in one piece; the payload is passed up without copying
268 if (!data.is_contiguous()) {
269 ESP_LOGW(TAG, "%s: Packet split across buffers is not supported", this->name_);
270 return;
271 }
272 auto frame = data.as_span();
273 if (frame.size() < FRAME_OVERHEAD) {
274 this->note_received_(false);
275 return;
276 }
277 size_t length = frame.size() - FRAME_OVERHEAD;
278 uint8_t control = frame[0];
279 const uint8_t *payload = frame.data() + 1;
280 uint16_t crc = frame[length + 1] | (frame[length + 2] << 8);
281 if (frame_crc(control, payload, length) != crc) {
282 ESP_LOGV(TAG, "%s: CRC error, frame dropped", this->name_);
283 this->note_received_(false);
284 return;
285 }
286 // The peer's packets may only be larger if it has a larger max_packet_size, which the layers above cannot take
287 if (length > this->max_packet_size_) {
288 if (!this->oversize_warned_) {
289 this->oversize_warned_ = true;
290 ESP_LOGW(TAG,
291 "%s: Frame of %zu bytes is larger than %zu, dropped; check that both devices use the same "
292 "max_packet_size",
293 this->name_, length, this->max_packet_size_);
294 } else {
295 ESP_LOGV(TAG, "%s: Frame of %zu bytes is larger than %zu, dropped", this->name_, length, this->max_packet_size_);
296 }
297 this->note_received_(false);
298 return;
299 }
300 this->note_received_(true);
301
302 uint8_t command = control & ~POLL_FINAL_BIT;
303 if (command == UNNUMBERED_SABM) {
304 // A peer that has not heard our UA repeats its SABM, and cannot have sent a numbered frame meanwhile; answering
305 // again keeps this side connected, and the queued frames, instead of treating the repeat as a restart.
307 // The peer has restarted, so frames it has not acknowledged are lost
308 ESP_LOGD(TAG, "%s: Link reset by peer", this->name_);
309 this->start_reset_();
310 }
311 this->vr_ = 0;
312 this->rej_sent_ = false;
313 this->send_frame_(UNNUMBERED_UA, nullptr, 0);
314 // Both sides now start from 0, so the link is up even if our own SABM is unanswered
315 this->link_up_();
316 return;
317 }
318 if (command == UNNUMBERED_UA) {
320 this->link_up_();
321 return;
322 }
323 // Numbered frames are ignored until the link is reset; the peer answers our SABM first
325 return;
326 this->peer_frame_seen_ = true;
327
328 uint8_t nr = control >> 5;
329 if ((control & 1) == 0) {
330 this->process_ack_(nr);
331 this->receive_info_(control, payload, length, meta_data);
332 return;
333 }
334 if ((control & 3) != SUPERVISORY_FRAME)
335 return;
336 this->process_ack_(nr);
337 if (((control >> 2) & 3) == SUPERVISORY_REJ && this->outstanding_() != 0) {
338 ESP_LOGV(TAG, "%s: Peer rejected from %u", this->name_, nr);
339 this->resend_outstanding_();
340 this->start_timer_();
341 }
342}
343
344void HdlcReliability::receive_info_(uint8_t control, const uint8_t *payload, size_t length,
345 const PacketMetaData &meta_data) {
346 uint8_t ns = (control >> 1) & 7;
347 if (ns != this->vr_) {
348 // A gap or a duplicate; ask once for a resend from the expected frame, then just acknowledge at once
349 if (!this->rej_sent_) {
350 this->rej_sent_ = this->send_supervisory_(SUPERVISORY_REJ);
351 if (this->rej_sent_)
352 return;
353 }
354 this->owe_ack_(true);
355 return;
356 }
357 this->vr_ = (this->vr_ + 1) & 7;
358 this->rej_sent_ = false;
359 // The acknowledgement is sent from loop(), so a reply sent from the callback can carry it instead
360 this->owe_ack_(false);
361 this->on_receive_from_interface_(PacketBuffer(payload, length), meta_data);
362}
363
364} // namespace esphome::packet_interface
365#endif
void mark_failed()
Mark this component as failed.
bool cancel_timeout(const char *name)
Cancel a timeout function.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void disable_loop()
Disable this component's loop.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
T * allocate(size_t n)
Definition helpers.h:2325
void owe_ack_(bool now)
An RR is owed: at once, or after the ack delay unless an I-frame carries it first.
std::array< uint16_t, MAX_WINDOW > tx_lengths_
void receive_info_(uint8_t control, const uint8_t *payload, size_t length, const PacketMetaData &meta_data)
bool send_frame_(uint8_t control, const uint8_t *payload, size_t length)
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data)
static constexpr size_t FRAME_OVERHEAD
Control byte plus CRC-16.
A buffer class that efficiently represents a composite buffer with three sections:
size_t copy_to(uint8_t *dest, size_t count, size_t offset=0) const
Copy data from the buffer to a destination pointer.
size_t size() const
Get the total size of the buffer.
A class that will transport data to and from an abstract channel.
void note_received_(bool understood)
Called by a layer for each received packet it understood, or could not make sense of.
void stack_on_(PacketInterface *lower, Capability capability)
Called by a layer's constructor to record itself as the layer above lower, adding its own capability.
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data)=0
Method to be implemented by child classes to send data to the interface.
void add_packet_interface_listener(F &&callback)
Add a listener that will be called when data is received.
LazyCallbackManager< void(bool)> session_callback_
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data)
This function should be called by implementing classes with received data to be passed to any listene...
A struct to hold metadata about a packet.
uint8_t type
constexpr size_t remaining_packet_size(size_t size, size_t used)
The room left for a packet of size bytes after used bytes, or 0 if it is too small.
ESPHOME_LOG_TAG(TAG, "packet_interface.session")
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
uint16_t size
Definition helpers.cpp:34
uint32_t random_uint32()
Return a random 32-bit unsigned integer.
Definition helpers.cpp:12
static void uint32_t
uint16_t seq
uint16_t trailer
uint16_t length
Definition tt21100.cpp:0