ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
vnc_client.cpp
Go to the documentation of this file.
1#include "vnc_client.h"
2
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
8
9#include <algorithm>
10#include <cinttypes>
11#include <cstring>
12#include <iterator>
13
14namespace esphome::vnc {
15
16ESPHOME_LOG_TAG(TAG, "vnc");
17
18// Below the 50 ms blocking warning, and the same slice it8951 uses for its transfers.
19static constexpr uint32_t LOOP_BUDGET_MS = 20;
20static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
21static constexpr uint32_t STABLE_SESSION_MS = 60000;
22static constexpr uint32_t MAX_BACKOFF_MS = 60000;
23static constexpr uint32_t CONFIG_RETRY_MS = 300000;
24static constexpr uint32_t HEARTBEAT_MS = 60000;
25static constexpr uint32_t MIN_REQUEST_SPACING_MS = 50;
26// The longest the first refresh waits for a display with a loop to park once after boot.
27static constexpr uint32_t DISPLAY_START_MS = 5000;
28// Longer than any driver's own busy timeout: a panel that stays busy this long is stuck
29static constexpr uint32_t DISPLAY_BUSY_TIMEOUT_MS = 120000;
30static constexpr uint16_t MAX_CURSOR_SIDE = 1024;
31static constexpr uint32_t MAX_TEXT_LEN = 63;
32
33static constexpr uint8_t SECURITY_NONE = 1;
34static constexpr uint8_t SECURITY_VNC_AUTH = 2;
35
36static constexpr int32_t ENCODING_RAW = 0;
37static constexpr int32_t ENCODING_HEXTILE = 5;
38static constexpr int32_t ENCODING_RICH_CURSOR = -239;
39// In preference order: TightVNC uses the first real encoding it supports for every rectangle.
40static constexpr int32_t ENCODINGS[] = {ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RICH_CURSOR};
41
42// Indexed by VncPixelFormat
43PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332");
44
45static constexpr uint8_t HEXTILE_RAW = 0x01;
46static constexpr uint8_t HEXTILE_BACKGROUND = 0x02;
47static constexpr uint8_t HEXTILE_FOREGROUND = 0x04;
48static constexpr uint8_t HEXTILE_ANY_SUBRECTS = 0x08;
49static constexpr uint8_t HEXTILE_SUBRECTS_COLOURED = 0x10;
50
51static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
52static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
53static constexpr uint8_t MSG_BELL = 2;
54static constexpr uint8_t MSG_SERVER_CUT_TEXT = 3;
55
56static inline uint16_t read_be16(const uint8_t *p) { return encode_uint16(p[0], p[1]); }
57static inline uint32_t read_be32(const uint8_t *p) { return encode_uint32(p[0], p[1], p[2], p[3]); }
58static inline void write_be16(uint8_t *p, uint16_t v) {
59 p[0] = v >> 8;
60 p[1] = v;
61}
62static inline void write_be32(uint8_t *p, uint32_t v) {
63 p[0] = v >> 24;
64 p[1] = v >> 16;
65 p[2] = v >> 8;
66 p[3] = v;
67}
68
69static int parse_three_digits(const uint8_t *p) {
70 int value = 0;
71 for (int i = 0; i < 3; i++) {
72 if (p[i] < '0' || p[i] > '9') {
73 return -1;
74 }
75 value = value * 10 + (p[i] - '0');
76 }
77 return value;
78}
79
82 this->link_.begin(TAG);
83 // Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
84 const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
85 static_cast<uint32_t>((this->display_->get_height() + 15) / 16);
86 const uint32_t cells = std::min<uint32_t>(wanted, UINT16_MAX);
87 if (cells < wanted) {
88 ESP_LOGW(TAG, "Display has %" PRIu32 " tiles, more than the %u the cache holds", wanted, UINT16_MAX);
89 }
90 RAMAllocator<uint32_t> allocator;
91 this->tile_hashes_ = allocator.allocate(cells);
92 if (this->tile_hashes_ == nullptr) {
93 ESP_LOGW(TAG, "Not enough memory to skip unchanged tiles");
94 return;
95 }
96 this->hash_capacity_ = cells;
97 this->clear_tiles_();
98}
99
101 this->need_full_ = true;
102 this->clear_tiles_();
103}
104
106 if (this->tile_hashes_ != nullptr) {
107 std::memset(this->tile_hashes_, 0, sizeof(uint32_t) * this->hash_capacity_);
108 }
109}
110
112 ESP_LOGCONFIG(TAG,
113 "VNC:\n"
114 " Server: %s:%u\n"
115 " Reconnect Interval: %" PRIu32 "ms\n"
116 " Update Delay: %" PRIu32 "ms\n"
117 " Max Update Delay: %" PRIu32 "ms\n"
118 " Pixel Format: %s\n"
119 " Tile Cache: %u tiles",
120 this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
121 this->max_update_delay_ms_,
122 LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
123 this->hash_capacity_);
124}
125
127
130 this->pass_start_ms_ = millis();
131 if (this->display_->is_failed()) {
132 this->link_.close(); // a connect still in flight would otherwise keep its socket for good
133 if (this->state_ != VncState::VNC_STATE_IDLE) {
134 this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display failed"));
135 }
136 this->status_set_error(LOG_STR("Display failed"));
137 this->disable_loop();
138 return;
139 }
140 // A display that parks its loop when idle is busy while that loop runs. Displays that never
141 // park (no loop, or one that always runs) update synchronously, so they are never busy.
142 const bool was_busy = this->display_busy_;
143 if (this->display_->is_idle()) {
144 this->display_parks_ = true;
145 this->display_busy_ = false;
146 } else {
147 this->display_busy_ = this->display_parks_;
148 }
149 if (was_busy && !this->display_busy_) {
150 // A refresh the driver gave up on (it sets its warning) leaves the panel behind the buffer: retry it
151 // once and forget the cached tiles, which no longer say what the panel shows
152 if (!this->display_->status_has_warning()) {
153 this->refresh_retried_ = false;
154 } else if (!this->refresh_retried_) {
155 this->refresh_retried_ = true;
156 this->clear_tiles_();
157 if (!this->refresh_pending_) {
158 this->first_change_ms_ = now;
159 }
160 this->last_change_ms_ = now;
161 this->refresh_pending_ = true;
162 }
163 }
164 // A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
165 if (!this->display_busy_) {
166 this->busy_since_ms_ = now;
167 if (this->busy_warned_) {
168 this->busy_warned_ = false;
169 // The connection path owns the warning while no session runs
170 if (this->state_ == VncState::VNC_STATE_RUNNING) {
171 this->status_clear_warning();
172 }
173 }
174 } else if (!this->busy_warned_ && now - this->busy_since_ms_ >= DISPLAY_BUSY_TIMEOUT_MS) {
175 this->busy_warned_ = true;
176 this->status_set_warning(LOG_STR("Display busy too long"));
177 }
178
179 if (!this->link_.connected()) {
180 if (this->state_ != VncState::VNC_STATE_IDLE) {
181 this->session_lost_(now);
182 }
183 this->connect_step_(now);
184 if (!this->link_.connected()) {
185 return;
186 }
187 this->start_handshake_(now);
188 }
189 if (this->state_ == VncState::VNC_STATE_HANDSHAKE && now - this->session_start_ms_ >= HANDSHAKE_TIMEOUT_MS) {
190 this->fail_(this->lost_failure_(), LOG_STR("Handshake timed out"));
191 return;
192 }
193
194 const bool blocked = this->in_update_ && this->display_busy_;
195 // A display still starting up (it8951) can go from its start-up straight into a refresh without parking.
196 const bool display_starting =
197 !this->display_parks_ && this->display_->has_overridden_loop() && now - this->setup_ms_ < DISPLAY_START_MS;
198 // Wait until the screen stops changing, so an animation becomes one refresh showing where it ended.
199 if (this->refresh_pending_ && !this->in_update_ && !this->display_busy_ && !display_starting &&
200 (now - this->last_change_ms_ >= this->update_delay_ms_ ||
201 now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
202 // Synchronous displays flush inside update(), so this pass does nothing else.
203 this->refresh_pending_ = false;
204 this->display_->update();
205 this->display_busy_ = this->display_parks_ && !this->display_->is_idle();
206 } else if (!blocked && (this->rx_pending_ || this->link_.ready())) {
207 this->service_rx_(now);
208 }
209 if (this->state_ == VncState::VNC_STATE_RUNNING) {
210 // A new session lays the screen out again for the new rotation and redraws all of it.
211 if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
212 this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
213 return;
214 }
215 // Mid-frame there is nothing to send, so a vanished server would never fail a write.
216 if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
217 this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
218 return;
219 }
220 this->pace_(now);
221 }
222 this->link_.flush_tx();
223 if (this->rx_pending_ && !(this->in_update_ && this->display_busy_)) {
224 this->high_freq_.start();
225 } else {
226 this->high_freq_.stop();
227 }
228}
229
231 const bool up = network::is_connected();
232 if (up && !this->network_was_up_) {
233 this->backoff_ms_ = 0;
234 this->hold_ms_ = 0;
235 }
236 this->network_was_up_ = up;
237 if (!up) {
238 return;
239 }
240 if (!this->link_.connecting() && (now - this->hold_start_ms_ < this->hold_ms_ || this->link_.in_backoff())) {
241 return;
242 }
243 this->link_.poll();
244 // DNS failures, refused connects and connect timeouts all leave no socket and start the link's backoff.
245 if (!this->link_.connected() && !this->link_.connecting() && this->link_.in_backoff()) {
246 this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Cannot reach the server"));
247 }
248}
249
251 // Pixels drawn but never shown would match the cache when the next session resends them.
252 if (this->refresh_pending_ || this->frame_drawn_) {
253 this->clear_tiles_();
254 }
255 // Hextile colours carry from tile to tile; a new server must not inherit the last one's
256 std::memset(this->hex_bg_, 0, sizeof(this->hex_bg_));
257 std::memset(this->hex_fg_, 0, sizeof(this->hex_fg_));
258 this->rx_pos_ = 0;
259 this->rx_len_ = 0;
260 this->rx_pending_ = false;
261 this->in_update_ = false;
262 this->request_outstanding_ = false;
263 this->refresh_pending_ = false;
264 this->need_full_ = false;
265 this->frame_drawn_ = false;
268 this->high_freq_.stop();
269}
270
272 this->reset_session_();
274 this->session_start_ms_ = now;
275 this->last_rx_ms_ = now;
276}
277
286
288 this->session_end_(now, this->lost_failure_(), LOG_STR("Connection lost"));
289}
290
291VncParse VncClient::fail_(VncFailure failure, const LogString *reason) {
292 this->link_.close();
293 this->link_.note_attempt();
294 this->session_end_(App.get_loop_component_start_time(), failure, reason);
296}
297
298void VncClient::session_end_(uint32_t now, VncFailure failure, const LogString *reason) {
299 const bool was_running = this->state_ == VncState::VNC_STATE_RUNNING;
300 this->reset_session_();
302 this->hold_start_ms_ = now;
303 if (was_running) {
304 const uint32_t lasted = now - this->session_start_ms_;
305 ESP_LOGW(TAG, "%s after %" PRIu32 "s", LOG_STR_ARG(reason), lasted / 1000);
306 this->disconnect_callback_.call();
307 this->status_set_warning();
308 if (lasted >= STABLE_SESSION_MS) {
309 this->backoff_ms_ = 0;
310 this->hold_ms_ = 0;
311 return;
312 }
314 }
315 switch (failure) {
317 this->hold_ms_ = CONFIG_RETRY_MS;
318 ESP_LOGE(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
319 this->status_set_error(reason);
320 return;
321 default: {
322 const uint32_t base = this->link_.reconnect_interval();
323 this->backoff_ms_ =
324 this->backoff_ms_ == 0 ? base : std::min(this->backoff_ms_ * 2, std::max(MAX_BACKOFF_MS, base));
325 this->hold_ms_ = this->backoff_ms_;
326 if (!was_running) {
327 ESP_LOGW(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
328 }
329 this->status_set_warning();
330 return;
331 }
332 }
333}
334
335bool VncClient::budget_exhausted_() const { return millis() - this->pass_start_ms_ >= LOOP_BUDGET_MS; }
336
338 for (;;) {
339 VncParse result = this->parse_();
340 if (result == VncParse::VNC_PARSE_STOP) {
341 return;
342 }
343 if (result == VncParse::VNC_PARSE_PAUSE) {
344 this->rx_pending_ = true;
345 return;
346 }
347 // Less than one item is left, so the buffer always has room after compacting.
348 if (this->rx_pos_ != 0) {
349 this->rx_len_ -= this->rx_pos_;
350 std::memmove(this->rx_, this->rx_ + this->rx_pos_, this->rx_len_);
351 this->rx_pos_ = 0;
352 }
353 if (this->budget_exhausted_()) {
354 this->rx_pending_ = true;
355 return;
356 }
357 ssize_t count = this->link_.read(this->rx_ + this->rx_len_, RX_BUFFER_SIZE - this->rx_len_);
358 if (count < 0) {
359 this->session_lost_(now);
360 return;
361 }
362 if (count == 0) {
363 this->rx_pending_ = false;
364 return;
365 }
366 this->rx_len_ += static_cast<uint16_t>(count);
367 this->rx_total_ += count;
368 this->last_rx_ms_ = now;
369 }
370}
371
377
379 const uint8_t shared = 1;
380 return this->send_(&shared, 1);
381}
382
384 for (;;) {
385 const uint8_t *p = this->rx_ + this->rx_pos_;
386 const size_t avail = this->rx_len_ - this->rx_pos_;
387 switch (this->step_) {
389 if (avail < 12) {
391 }
392 const int major = parse_three_digits(p + 4);
393 const int minor = parse_three_digits(p + 8);
394 if (std::memcmp(p, "RFB ", 4) != 0 || p[7] != '.' || p[11] != '\n' || major < 0 || minor < 0) {
395 return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Not a VNC server"));
396 }
397 if (major < 3) {
398 return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Unsupported RFB version"));
399 }
400 this->minor_ = (major > 3 || minor >= 8) ? 8 : (minor == 7 ? 7 : 3);
401 this->rx_pos_ += 12;
402 uint8_t reply[12] = {'R', 'F', 'B', ' ', '0', '0', '3', '.', '0', '0', '8', '\n'};
403 reply[10] = '0' + this->minor_;
404 if (!this->send_(reply, sizeof(reply))) {
405 return this->fail_send_();
406 }
408 break;
409 }
411 if (avail < 1) {
413 }
414 this->sec_count_ = p[0];
415 this->rx_pos_ += 1;
416 if (this->sec_count_ == 0) {
417 // A refusal before authentication, such as a lockout caused by another client.
419 } else {
421 }
422 break;
423 }
425 if (avail < this->sec_count_) {
427 }
428 uint8_t chosen = 0;
429 bool wants_password = false;
430 for (uint8_t i = 0; i < this->sec_count_; i++) {
431 if (p[i] == SECURITY_NONE) {
432 chosen = p[i];
433 break;
434 }
435 wants_password |= p[i] == SECURITY_VNC_AUTH;
436 }
437 this->rx_pos_ += this->sec_count_;
438 if (chosen == 0) {
439 return this->fail_(VncFailure::VNC_FAILURE_CONFIG, wants_password ? LOG_STR("The server requires a password")
440 : LOG_STR("No supported security type"));
441 }
442 if (!this->send_(&chosen, 1)) {
443 return this->fail_send_();
444 }
445 if (this->minor_ == 8) {
447 } else {
448 if (!this->send_client_init_()) {
449 return this->fail_send_();
450 }
452 }
453 break;
454 }
456 if (avail < 4) {
458 }
459 const uint32_t type = read_be32(p);
460 this->rx_pos_ += 4;
461 if (type == 0) {
463 } else if (type == SECURITY_NONE) {
464 if (!this->send_client_init_()) {
465 return this->fail_send_();
466 }
468 } else {
469 return this->fail_(VncFailure::VNC_FAILURE_CONFIG, type == SECURITY_VNC_AUTH
470 ? LOG_STR("The server requires a password")
471 : LOG_STR("No supported security type"));
472 }
473 break;
474 }
476 if (avail < 4) {
478 }
479 const uint32_t result = read_be32(p);
480 this->rx_pos_ += 4;
481 if (result == 0) {
482 if (!this->send_client_init_()) {
483 return this->fail_send_();
484 }
486 break;
487 }
490 break;
491 }
493 if (avail < 4) {
495 }
496 this->text_len_ = static_cast<uint8_t>(std::min(read_be32(p), MAX_TEXT_LEN));
497 this->rx_pos_ += 4;
499 break;
500 }
502 if (avail < this->text_len_) {
504 }
505 ESP_LOGE(TAG, "Server: %.*s", static_cast<int>(this->text_len_), reinterpret_cast<const char *>(p));
506 return this->fail_(this->reason_failure_, LOG_STR("Connection refused"));
507 }
509 if (avail < 24) {
511 }
512 this->fb_w_ = read_be16(p);
513 this->fb_h_ = read_be16(p + 2);
514 // Reused for the length of the name that follows.
515 this->skip_left_ = read_be32(p + 20);
516 this->rx_pos_ += 24;
517 if (this->fb_w_ == 0 || this->fb_h_ == 0) {
518 return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("The server screen is empty"));
519 }
520 this->text_len_ = static_cast<uint8_t>(std::min(this->skip_left_, MAX_TEXT_LEN));
522 break;
523 }
525 if (avail < this->text_len_) {
527 }
528 ESP_LOGI(TAG, "Server '%.*s', %ux%u, RFB 3.%u", static_cast<int>(this->text_len_),
529 reinterpret_cast<const char *>(p), this->fb_w_, this->fb_h_, this->minor_);
530 this->rx_pos_ += this->text_len_;
531 this->skip_(this->skip_left_ - this->text_len_, VncStep::VNC_STEP_SESSION_START);
532 break;
533 }
536 return this->fail_send_();
537 }
539 break;
541 if (avail < 1) {
543 }
544 switch (p[0]) {
545 case MSG_FRAMEBUFFER_UPDATE:
546 if (avail < 4) {
548 }
549 this->rects_left_ = read_be16(p + 2);
550 this->rx_pos_ += 4;
551 this->in_update_ = true;
552 this->update_start_ms_ = millis();
553 this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
554 this->drawn_tiles_ = 0;
555 this->rect_done_();
556 break;
557 case MSG_SET_COLOUR_MAP:
558 if (avail < 6) {
560 }
561 this->rx_pos_ += 6;
562 this->skip_(6u * read_be16(p + 4), VncStep::VNC_STEP_MESSAGE);
563 break;
564 case MSG_BELL:
565 this->rx_pos_ += 1;
566 break;
567 case MSG_SERVER_CUT_TEXT:
568 if (avail < 8) {
570 }
571 this->rx_pos_ += 8;
572 this->skip_(read_be32(p + 4), VncStep::VNC_STEP_MESSAGE);
573 break;
574 default:
575 ESP_LOGW(TAG, "Unknown message type %u", p[0]);
576 return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
577 }
578 break;
579 }
581 if (this->display_busy_) {
583 }
584 if (avail < 12) {
586 }
587 this->rect_x_ = read_be16(p);
588 this->rect_y_ = read_be16(p + 2);
589 this->rect_w_ = read_be16(p + 4);
590 this->rect_h_ = read_be16(p + 6);
591 const auto encoding = static_cast<int32_t>(read_be32(p + 8));
592 this->rx_pos_ += 12;
593 this->rects_left_--;
594 if (encoding == ENCODING_RAW || encoding == ENCODING_HEXTILE) {
595 if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
596 static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
597 ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
598 this->rect_x_, this->rect_y_, this->fb_w_, this->fb_h_);
599 return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
600 }
601 if (this->rect_w_ == 0 || this->rect_h_ == 0) {
602 this->rect_done_();
603 } else if (encoding == ENCODING_RAW) {
604 this->forget_tiles_(this->rect_x_, this->rect_y_, this->rect_w_, this->rect_h_);
605 this->pos_x_ = 0;
606 this->pos_y_ = 0;
608 } else {
609 this->pos_x_ = this->rect_x_;
610 this->pos_y_ = this->rect_y_;
612 }
613 } else if (encoding == ENCODING_RICH_CURSOR) {
614 if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
615 return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
616 }
617 const uint32_t size = static_cast<uint32_t>(this->rect_w_) * this->rect_h_ * this->bpp_ +
618 ((this->rect_w_ + 7u) / 8u) * this->rect_h_;
620 } else {
621 ESP_LOGW(TAG, "Unsupported encoding %" PRId32, encoding);
622 return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
623 }
624 break;
625 }
627 this->rect_done_();
628 break;
630 if (this->display_busy_) {
632 }
633 const size_t pixels = avail / this->bpp_;
634 if (pixels == 0) {
636 }
637 if (this->budget_exhausted_()) {
639 }
640 const auto count = static_cast<uint16_t>(std::min<size_t>(pixels, this->rect_w_ - this->pos_x_));
641 this->draw_row_(this->rect_x_ + this->pos_x_, this->rect_y_ + this->pos_y_, count, p);
642 this->rx_pos_ += count * this->bpp_;
643 this->pos_x_ += count;
644 if (this->pos_x_ == this->rect_w_) {
645 this->pos_x_ = 0;
646 if (++this->pos_y_ == this->rect_h_) {
647 this->rect_done_();
648 }
649 }
650 break;
651 }
653 if (this->display_busy_) {
655 }
656 if (avail < 1) {
658 }
659 if (this->budget_exhausted_()) {
661 }
662 const uint8_t flags = p[0];
663 this->tile_w_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_x_) + this->rect_w_ - this->pos_x_);
664 this->tile_h_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_y_) + this->rect_h_ - this->pos_y_);
665 this->tile_hidden_ = this->pos_x_ >= this->vis_w_ || this->pos_y_ >= this->vis_h_;
666 // With the raw bit set the other bits mean nothing and the pixels follow at once.
667 if (flags & HEXTILE_RAW) {
668 this->rx_pos_ += 1;
669 this->tile_fill_ = 0;
671 break;
672 }
673 const size_t need = 1 + ((flags & HEXTILE_BACKGROUND) ? this->bpp_ : 0) +
674 ((flags & HEXTILE_FOREGROUND) ? this->bpp_ : 0) + ((flags & HEXTILE_ANY_SUBRECTS) ? 1 : 0);
675 if (avail < need) {
677 }
678 const uint8_t *q = p + 1;
679 if (flags & HEXTILE_BACKGROUND) {
680 std::memcpy(this->hex_bg_, q, this->bpp_);
681 q += this->bpp_;
682 }
683 if (flags & HEXTILE_FOREGROUND) {
684 std::memcpy(this->hex_fg_, q, this->bpp_);
685 q += this->bpp_;
686 }
687 this->subrects_left_ = (flags & HEXTILE_ANY_SUBRECTS) ? *q : 0;
688 this->subrects_coloured_ = (flags & HEXTILE_SUBRECTS_COLOURED) != 0;
689 this->rx_pos_ += need;
690 this->fill_tile_(0, 0, this->tile_w_, this->tile_h_, this->hex_bg_);
692 break;
693 }
695 const size_t total = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
696 const size_t count = std::min(avail, total - this->tile_fill_);
697 if (count == 0) {
699 }
700 std::memcpy(this->tile_ + this->tile_fill_, p, count);
701 this->rx_pos_ += count;
702 this->tile_fill_ += count;
703 if (this->tile_fill_ == total) {
704 this->draw_tile_();
705 this->next_tile_();
706 }
707 break;
708 }
710 if (this->subrects_left_ == 0) {
711 this->draw_tile_();
712 this->next_tile_();
713 break;
714 }
715 const size_t need = (this->subrects_coloured_ ? this->bpp_ : 0) + 2;
716 if (avail < need) {
718 }
719 const uint8_t *pixel = this->hex_fg_;
720 const uint8_t *q = p;
721 if (this->subrects_coloured_) {
722 pixel = q;
723 q += this->bpp_;
724 }
725 this->fill_tile_(q[0] >> 4, q[0] & 0x0F, (q[1] >> 4) + 1, (q[1] & 0x0F) + 1, pixel);
726 this->rx_pos_ += need;
727 this->subrects_left_--;
728 break;
729 }
731 const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
732 this->rx_pos_ += count;
733 this->skip_left_ -= count;
734 if (this->skip_left_ != 0) {
736 }
737 this->step_ = this->after_skip_;
738 break;
739 }
740 }
741 }
742}
743
745 if (this->rects_left_ == 0) {
746 this->frame_done_();
747 } else {
749 }
750}
751
754 if (static_cast<uint32_t>(this->pos_x_) + 16 < static_cast<uint32_t>(this->rect_x_) + this->rect_w_) {
755 this->pos_x_ += 16;
756 return;
757 }
758 this->pos_x_ = this->rect_x_;
759 if (static_cast<uint32_t>(this->pos_y_) + 16 < static_cast<uint32_t>(this->rect_y_) + this->rect_h_) {
760 this->pos_y_ += 16;
761 return;
762 }
763 this->rect_done_();
764}
765
767 this->in_update_ = false;
768 this->request_outstanding_ = false;
770 if (this->frame_drawn_) {
772 this->frame_drawn_ = false;
773 if (!this->refresh_pending_) {
774 this->first_change_ms_ = now;
775 }
776 this->last_change_ms_ = now;
777 this->refresh_pending_ = true;
778 ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms, %u hextile tiles drawn",
779 this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
780 millis() - this->update_start_ms_, this->drawn_tiles_);
781 }
782}
783
784void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels) {
785 if (y >= this->vis_h_ || x >= this->vis_w_) {
786 return;
787 }
788 const int visible = std::min<int>(count, this->vis_w_ - x);
789 this->display_->draw_pixels_at(x, y, visible, 1, pixels, this->bitness_, this->big_endian_);
790 this->frame_drawn_ = true;
791}
792
794 if (this->tile_hidden_) {
795 return;
796 }
797 const int w = std::min<int>(this->tile_w_, this->vis_w_ - this->pos_x_);
798 const int h = std::min<int>(this->tile_h_, this->vis_h_ - this->pos_y_);
799 uint32_t *cell = nullptr;
800 uint32_t hash = 0;
801 if (this->hash_cols_ != 0) {
802 if ((this->pos_x_ | this->pos_y_) & 15) {
803 // An off-grid tile spans several cells, and a later write to any of them would make its hash stale.
804 this->forget_tiles_(this->pos_x_, this->pos_y_, w, h);
805 } else {
806 cell = this->tile_hashes_ + (this->pos_y_ / 16) * this->hash_cols_ + this->pos_x_ / 16;
807 hash = this->tile_hash_();
808 if (*cell == hash) {
809 return;
810 }
811 }
812 }
813 this->display_->draw_pixels_at(this->pos_x_, this->pos_y_, w, h, this->tile_, this->bitness_, this->big_endian_, 0, 0,
814 this->tile_w_ - w);
815 this->frame_drawn_ = true;
816 this->drawn_tiles_++;
817 if (cell != nullptr) {
818 *cell = hash;
819 }
820}
821
823 // The tile's size, pixel size and pixels; the cell gives its place. Zero means "unknown".
825 static_cast<uint32_t>(this->tile_w_) << 16 | this->tile_h_ << 8 | this->bpp_);
826 const size_t len = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
827 size_t i = 0;
828 for (; i + 4 <= len; i += 4) {
829 uint32_t word;
830 std::memcpy(&word, this->tile_ + i, sizeof(word)); // tile_ is aligned, so this is one load
831 // Byte by byte: XOR-ing whole words lets high-bit changes in two words cancel.
832 hash = fnv1a_hash_extend(hash, word);
833 }
834 for (; i < len; i++) {
835 hash = fnv1a_hash_extend(hash, this->tile_[i]);
836 }
837 return hash == 0 ? 1 : hash;
838}
839
840void VncClient::forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
841 if (this->hash_cols_ == 0 || w == 0 || h == 0) {
842 return;
843 }
844 const uint16_t col_end = std::min<uint16_t>((x + w - 1) / 16, this->hash_cols_ - 1);
845 const uint16_t row_end = std::min<uint16_t>((y + h - 1) / 16, this->hash_rows_ - 1);
846 for (uint16_t row = y / 16; row <= row_end; row++) {
847 for (uint16_t col = x / 16; col <= col_end; col++) {
848 this->tile_hashes_[row * this->hash_cols_ + col] = 0;
849 }
850 }
851}
852
853void VncClient::fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel) {
854 if (this->tile_hidden_ || x >= this->tile_w_ || y >= this->tile_h_) {
855 return;
856 }
857 w = std::min<uint8_t>(w, this->tile_w_ - x);
858 h = std::min<uint8_t>(h, this->tile_h_ - y);
859 const size_t stride = static_cast<size_t>(this->tile_w_) * this->bpp_;
860 const size_t row_len = static_cast<size_t>(w) * this->bpp_;
861 uint8_t *first = this->tile_ + y * stride + x * this->bpp_;
862 if (this->bpp_ == 1) {
863 std::memset(first, pixel[0], w);
864 } else {
865 for (size_t i = 0; i < row_len; i += 2) {
866 first[i] = pixel[0];
867 first[i + 1] = pixel[1];
868 }
869 }
870 for (uint8_t row = 1; row < h; row++) {
871 std::memcpy(first + row * stride, first, row_len);
872 }
873}
874
881 }
882 const bool rgb565 = format == VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
883 this->bpp_ = rgb565 ? 2 : 1;
885
886 const int disp_w = this->display_->get_width();
887 const int disp_h = this->display_->get_height();
888 this->vis_w_ = std::min<int>(this->fb_w_, disp_w);
889 this->vis_h_ = std::min<int>(this->fb_h_, disp_h);
890 if (this->fb_w_ != disp_w || this->fb_h_ != disp_h) {
891 ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
892 this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
893 }
894 const uint16_t cols = (this->vis_w_ + 15) / 16;
895 const uint16_t rows = (this->vis_h_ + 15) / 16;
896 const auto rotation = static_cast<uint8_t>(this->display_->get_rotation() / 90);
897 const bool rotated = rotation != this->hash_rotation_;
898 this->hash_rotation_ = rotation;
899 if (this->tile_hashes_ == nullptr) {
900 this->hash_cols_ = 0;
901 this->hash_rows_ = 0;
902 } else if (static_cast<uint32_t>(cols) * rows > this->hash_capacity_) {
903 ESP_LOGW(TAG, "Screen has %u tiles, more than the %u cached; every tile is drawn", cols * rows,
904 this->hash_capacity_);
905 this->hash_cols_ = 0;
906 this->hash_rows_ = 0;
907 } else if (cols != this->hash_cols_ || rows != this->hash_rows_ || rotated) {
908 this->hash_cols_ = cols;
909 this->hash_rows_ = rows;
910 this->clear_tiles_();
911 }
912
913 // SetPixelFormat: true colour, in the byte order the display takes
914 uint8_t pixel_format[20] = {};
915 pixel_format[4] = rgb565 ? 16 : 8;
916 pixel_format[5] = rgb565 ? 16 : 8;
917 pixel_format[6] = this->big_endian_;
918 pixel_format[7] = 1;
919 write_be16(pixel_format + 8, rgb565 ? 31 : 7);
920 write_be16(pixel_format + 10, rgb565 ? 63 : 7);
921 write_be16(pixel_format + 12, rgb565 ? 31 : 3);
922 pixel_format[14] = rgb565 ? 11 : 5;
923 pixel_format[15] = rgb565 ? 5 : 2;
924 if (!this->send_(pixel_format, sizeof(pixel_format))) {
925 return false;
926 }
927
928 // SetEncodings
929 uint8_t encodings[4 + 4 * std::size(ENCODINGS)] = {2};
930 write_be16(encodings + 2, std::size(ENCODINGS));
931 for (size_t i = 0; i < std::size(ENCODINGS); i++) {
932 write_be32(encodings + 4 + 4 * i, static_cast<uint32_t>(ENCODINGS[i]));
933 }
934 if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
935 return false;
936 }
938 this->session_start_ms_ = now;
939 if (!this->busy_warned_) {
940 this->status_clear_warning(); // a stuck display keeps its warning through a reconnect
941 }
942 this->status_clear_error();
943 this->connect_callback_.call();
944 return true;
945}
946
948 if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
949 return;
950 }
951 if (this->need_full_) {
952 if (this->send_update_request_(false, now)) {
953 this->need_full_ = false;
954 }
955 return;
956 }
957 // Repeating a parked request costs nothing on the server and detects a dead link.
958 if (!this->request_outstanding_ ||
959 (now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
960 this->send_update_request_(true, now);
961 }
962}
963
964bool VncClient::send_(const uint8_t *data, size_t len) {
965 if (this->link_.tx_free() < len) {
966 this->link_.flush_tx();
967 if (this->link_.tx_free() < len) {
968 return false;
969 }
970 }
971 this->link_.queue(data, len);
972 return true;
973}
974
975bool VncClient::send_update_request_(bool incremental, uint32_t now) {
976 uint8_t msg[10] = {3, static_cast<uint8_t>(incremental ? 1 : 0)};
977 write_be16(msg + 6, this->vis_w_);
978 write_be16(msg + 8, this->vis_h_);
979 if (!this->send_(msg, sizeof(msg))) {
980 return false;
981 }
982 this->request_outstanding_ = true;
983 this->last_request_ms_ = now;
984 return true;
985}
986
987} // namespace esphome::vnc
uint8_t h
Definition bl0906.h:2
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.
bool has_overridden_loop() const
Definition component.h:326
bool is_failed() const
Definition component.h:274
void status_clear_error()
Definition component.h:302
bool status_has_warning() const
Definition component.h:280
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:296
bool is_idle() const
Check if this component is idle.
Definition component.h:215
void stop()
Stop running the loop continuously.
Definition helpers.cpp:841
void start()
Start running the loop continuously.
Definition helpers.cpp:835
virtual void update()=0
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
T * allocate(size_t n)
Definition helpers.h:2325
virtual int get_height()
Get the calculated height of the display in pixels with rotation applied.
Definition display.h:338
virtual DisplayRotation get_rotation() const
Definition display.h:751
virtual int get_width()
Get the calculated width of the display in pixels with rotation applied.
Definition display.h:336
virtual DisplayType get_display_type()=0
Get the type of display that the buffer corresponds to.
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad)
Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
Definition display.h:370
LazyCallbackManager< void()> disconnect_callback_
Definition vnc_client.h:120
VncFailure lost_failure_() const
uint8_t tile_[TILE_BUFFER_SIZE]
Definition vnc_client.h:199
LazyCallbackManager< void()> connect_callback_
Definition vnc_client.h:119
void session_lost_(uint32_t now)
void forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
VncParse fail_(VncFailure failure, const LogString *reason)
display::ColorBitness bitness_
Definition vnc_client.h:176
display::Display * display_
Definition vnc_client.h:117
socket::TcpClientLink link_
Definition vnc_client.h:118
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels)
void dump_config() override
bool send_(const uint8_t *data, size_t len)
bool start_session_(uint32_t now)
void refresh()
Ask the server for the whole screen again.
void pace_(uint32_t now)
HighFrequencyLoopRequester high_freq_
Definition vnc_client.h:121
void service_rx_(uint32_t now)
uint32_t tile_hash_() const
void start_handshake_(uint32_t now)
static constexpr size_t RX_BUFFER_SIZE
Definition vnc_client.h:87
void session_end_(uint32_t now, VncFailure failure, const LogString *reason)
void connect_step_(uint32_t now)
VncPixelFormat pixel_format_
Definition vnc_client.h:177
bool budget_exhausted_() const
void setup() override
void on_shutdown() override
bool send_update_request_(bool incremental, uint32_t now)
void skip_(uint32_t count, VncStep next)
uint8_t rx_[RX_BUFFER_SIZE]
Definition vnc_client.h:198
void fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel)
uint16_t flags
uint8_t type
NameCopy * next
unsigned count
Definition gif.cpp:51
__int64 ssize_t
Definition httplib.h:178
const char * format
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
value[LAYOUT.day_index()]
ESPHOME_LOG_TAG(TAG, "vnc")
PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332")
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:950
constexpr uint32_t FNV1_OFFSET_BASIS
FNV-1 32-bit offset basis.
Definition helpers.h:923
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:1006
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:998
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6