Compare commits

...
Author SHA1 Message Date
4242bcb084 [vnc] Add the Hextile encoding and skip unchanged tiles (#20625)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-22 21:52:53 +00:00
J. Nick Koston 8a88e97497 [weikai] Share print_buffer between the SPI and I2C drivers (#20721) 2026-10-22 21:21:41 +00:00
Frédéric MetrichandClaude d586e20a31 [teleinfo] Fix default units of unitless, text and PMAX tags (#21367)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 17:21:09 -04:00
22 changed files with 1617 additions and 151 deletions
+52 -3
View File
@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
@@ -9,6 +11,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_REACTIVE_ENERGY,
@@ -17,12 +20,15 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HOUR,
UNIT_KILOVOLT_AMPS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_MILLIAMP,
UNIT_MILLIVOLT,
UNIT_MINUTE,
UNIT_SECOND,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
@@ -33,6 +39,8 @@ from esphome.types import ConfigType
from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns
_LOGGER = logging.getLogger(__name__)
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
@@ -59,6 +67,9 @@ _APPARENT_POWER_KVA = _preset(
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
_DURATION_MIN = _preset(UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT)
# Numeric values without a physical unit (indexes, bit fields, versions)
_NO_UNIT: dict[str, str] = {}
# Legacy defaults, kept for tags without a known preset
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
@@ -75,6 +86,7 @@ _COMPATIBLE_UNITS = {
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
DEVICE_CLASS_DURATION: {UNIT_SECOND, UNIT_MINUTE, UNIT_HOUR},
}
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
@@ -90,6 +102,10 @@ TIC_TAG_CONFIGS = {
"CC": _POWER,
"PREF": _APPARENT_POWER_KVA,
"PCOUP": _APPARENT_POWER_KVA,
"VTIC": _NO_UNIT,
"NTARF": _NO_UNIT,
"NJOURF": _NO_UNIT,
"RELAIS": _NO_UNIT,
# Historical mode
"BASE": _ENERGY,
"HCH": _ENERGY,
@@ -101,15 +117,42 @@ TIC_TAG_CONFIGS = {
"ADPS": _CURRENT,
"ADIR": _CURRENT,
"PAPP": _APPARENT_POWER_VA,
"PMAX": _POWER,
"PMAX": _APPARENT_POWER_VA,
"PEJP": _DURATION_MIN,
}
# Prefixes of text tags, which a sensor cannot publish; a text_sensor should be used
# instead. They get no preset, and no prefix here is a prefix of a preset one either.
TIC_TEXT_TAGS = (
# Standard mode
"ADSC",
"DATE",
"NGTF",
"LTARF",
"STGE",
"DPM",
"FPM",
"MSG",
"PRM",
"PJOURF",
"PPOINTE",
# Historical mode
"ADCO",
"OPTARIF",
"PTEC",
"DEMAIN",
"HHPHC",
"MOTDETAT",
"PPOT",
)
def apply_tag_config(config: ConfigType) -> ConfigType:
"""Apply preset configurations based on the tag name.
Only keys not set by the user are filled in. For known tags, a preset unit or
device class is dropped if it is incompatible with the one set by the user.
Known text tags get no preset and log a warning, as a sensor cannot publish them.
Unknown tags get the legacy defaults, merged key by key as before.
"""
if CONF_TAG_NAME not in config:
@@ -117,6 +160,12 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
# Runs before cv.string, so the raw value may not be a string yet
tag_name = str(config[CONF_TAG_NAME])
if tag_name.startswith(TIC_TEXT_TAGS):
_LOGGER.warning(
"TeleInfo tag '%s' holds a text value, use a text_sensor instead", tag_name
)
return config
preset = next(
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
None,
@@ -128,8 +177,8 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
config.setdefault(key, value)
return config
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset.get(CONF_UNIT_OF_MEASUREMENT))
device_class = config.get(CONF_DEVICE_CLASS, preset.get(CONF_DEVICE_CLASS))
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
for key, value in preset.items():
+14
View File
@@ -29,9 +29,15 @@ MULTI_CONF = True
CONF_MAX_UPDATE_DELAY = "max_update_delay"
CONF_UPDATE_DELAY = "update_delay"
CONF_PIXEL_FORMAT = "pixel_format"
vnc_ns = cg.esphome_ns.namespace("vnc")
VncClient = vnc_ns.class_("VncClient", cg.Component)
VncPixelFormat = vnc_ns.enum("VncPixelFormat", is_class=True)
PIXEL_FORMATS = VncPixelFormat.options(
"auto", "rgb565", "rgb332", prefix="VNC_PIXEL_FORMAT_"
)
# Drivers that cannot take another component's pixels yet.
UNSUPPORTED_DISPLAYS = dict.fromkeys(
@@ -63,6 +69,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_MAX_UPDATE_DELAY, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_PIXEL_FORMAT, default="auto"): cv.enum(
PIXEL_FORMATS, lower=True
),
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
}
@@ -128,6 +137,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
cg.add(var.set_pixel_format(config[CONF_PIXEL_FORMAT]))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@@ -135,4 +145,8 @@ VNC_ID_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(VncClient)},
)
automation.register_apply_action(
"vnc.refresh", VNC_ID_SCHEMA, automation.ApplyCall("refresh()")
)
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
+296 -11
View File
@@ -4,10 +4,12 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <iterator>
namespace esphome::vnc {
@@ -32,7 +34,19 @@ static constexpr uint8_t SECURITY_NONE = 1;
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
static constexpr int32_t ENCODING_RAW = 0;
static constexpr int32_t ENCODING_HEXTILE = 5;
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
// In preference order: TightVNC uses the first real encoding it supports for every rectangle.
static constexpr int32_t ENCODINGS[] = {ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RICH_CURSOR};
// Indexed by VncPixelFormat
PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332");
static constexpr uint8_t HEXTILE_RAW = 0x01;
static constexpr uint8_t HEXTILE_BACKGROUND = 0x02;
static constexpr uint8_t HEXTILE_FOREGROUND = 0x04;
static constexpr uint8_t HEXTILE_ANY_SUBRECTS = 0x08;
static constexpr uint8_t HEXTILE_SUBRECTS_COLOURED = 0x10;
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
@@ -66,6 +80,32 @@ static int parse_three_digits(const uint8_t *p) {
void VncClient::setup() {
this->setup_ms_ = App.get_loop_component_start_time();
this->link_.begin(TAG);
// Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
static_cast<uint32_t>((this->display_->get_height() + 15) / 16);
const uint32_t cells = std::min<uint32_t>(wanted, UINT16_MAX);
if (cells < wanted) {
ESP_LOGW(TAG, "Display has %" PRIu32 " tiles, more than the %u the cache holds", wanted, UINT16_MAX);
}
RAMAllocator<uint32_t> allocator;
this->tile_hashes_ = allocator.allocate(cells);
if (this->tile_hashes_ == nullptr) {
ESP_LOGW(TAG, "Not enough memory to skip unchanged tiles");
return;
}
this->hash_capacity_ = cells;
this->clear_tiles_();
}
void VncClient::refresh() {
this->need_full_ = true;
this->clear_tiles_();
}
void VncClient::clear_tiles_() {
if (this->tile_hashes_ != nullptr) {
std::memset(this->tile_hashes_, 0, sizeof(uint32_t) * this->hash_capacity_);
}
}
void VncClient::dump_config() {
@@ -74,9 +114,13 @@ void VncClient::dump_config() {
" Server: %s:%u\n"
" Reconnect Interval: %" PRIu32 "ms\n"
" Update Delay: %" PRIu32 "ms\n"
" Max Update Delay: %" PRIu32 "ms",
" Max Update Delay: %" PRIu32 "ms\n"
" Pixel Format: %s\n"
" Tile Cache: %u tiles",
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
this->max_update_delay_ms_);
this->max_update_delay_ms_,
LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
this->hash_capacity_);
}
void VncClient::on_shutdown() { this->link_.close(); }
@@ -95,12 +139,28 @@ void VncClient::loop() {
}
// A display that parks its loop when idle is busy while that loop runs. Displays that never
// park (no loop, or one that always runs) update synchronously, so they are never busy.
const bool was_busy = this->display_busy_;
if (this->display_->is_idle()) {
this->display_parks_ = true;
this->display_busy_ = false;
} else {
this->display_busy_ = this->display_parks_;
}
if (was_busy && !this->display_busy_) {
// A refresh the driver gave up on (it sets its warning) leaves the panel behind the buffer: retry it
// once and forget the cached tiles, which no longer say what the panel shows
if (!this->display_->status_has_warning()) {
this->refresh_retried_ = false;
} else if (!this->refresh_retried_) {
this->refresh_retried_ = true;
this->clear_tiles_();
if (!this->refresh_pending_) {
this->first_change_ms_ = now;
}
this->last_change_ms_ = now;
this->refresh_pending_ = true;
}
}
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
if (!this->display_busy_) {
this->busy_since_ms_ = now;
@@ -147,6 +207,11 @@ void VncClient::loop() {
this->service_rx_(now);
}
if (this->state_ == VncState::VNC_STATE_RUNNING) {
// A new session lays the screen out again for the new rotation and redraws all of it.
if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
return;
}
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
@@ -183,12 +248,20 @@ void VncClient::connect_step_(uint32_t now) {
}
void VncClient::reset_session_() {
// Pixels drawn but never shown would match the cache when the next session resends them.
if (this->refresh_pending_ || this->frame_drawn_) {
this->clear_tiles_();
}
// Hextile colours carry from tile to tile; a new server must not inherit the last one's
std::memset(this->hex_bg_, 0, sizeof(this->hex_bg_));
std::memset(this->hex_fg_, 0, sizeof(this->hex_fg_));
this->rx_pos_ = 0;
this->rx_len_ = 0;
this->rx_pending_ = false;
this->in_update_ = false;
this->request_outstanding_ = false;
this->refresh_pending_ = false;
this->need_full_ = false;
this->frame_drawn_ = false;
this->step_ = VncStep::VNC_STEP_VERSION;
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
@@ -478,6 +551,7 @@ VncParse VncClient::parse_() {
this->in_update_ = true;
this->update_start_ms_ = millis();
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
this->drawn_tiles_ = 0;
this->rect_done_();
break;
case MSG_SET_COLOUR_MAP:
@@ -517,7 +591,7 @@ VncParse VncClient::parse_() {
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
this->rx_pos_ += 12;
this->rects_left_--;
if (encoding == ENCODING_RAW) {
if (encoding == ENCODING_RAW || encoding == ENCODING_HEXTILE) {
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
@@ -526,10 +600,15 @@ VncParse VncClient::parse_() {
}
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
this->rect_done_();
} else {
} else if (encoding == ENCODING_RAW) {
this->forget_tiles_(this->rect_x_, this->rect_y_, this->rect_w_, this->rect_h_);
this->pos_x_ = 0;
this->pos_y_ = 0;
this->step_ = VncStep::VNC_STEP_RAW;
} else {
this->pos_x_ = this->rect_x_;
this->pos_y_ = this->rect_y_;
this->step_ = VncStep::VNC_STEP_TILE;
}
} else if (encoding == ENCODING_RICH_CURSOR) {
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
@@ -570,6 +649,84 @@ VncParse VncClient::parse_() {
}
break;
}
case VncStep::VNC_STEP_TILE: {
if (this->display_busy_) {
return VncParse::VNC_PARSE_PAUSE;
}
if (avail < 1) {
return VncParse::VNC_PARSE_MORE;
}
if (this->budget_exhausted_()) {
return VncParse::VNC_PARSE_PAUSE;
}
const uint8_t flags = p[0];
this->tile_w_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_x_) + this->rect_w_ - this->pos_x_);
this->tile_h_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_y_) + this->rect_h_ - this->pos_y_);
this->tile_hidden_ = this->pos_x_ >= this->vis_w_ || this->pos_y_ >= this->vis_h_;
// With the raw bit set the other bits mean nothing and the pixels follow at once.
if (flags & HEXTILE_RAW) {
this->rx_pos_ += 1;
this->tile_fill_ = 0;
this->step_ = VncStep::VNC_STEP_TILE_RAW;
break;
}
const size_t need = 1 + ((flags & HEXTILE_BACKGROUND) ? this->bpp_ : 0) +
((flags & HEXTILE_FOREGROUND) ? this->bpp_ : 0) + ((flags & HEXTILE_ANY_SUBRECTS) ? 1 : 0);
if (avail < need) {
return VncParse::VNC_PARSE_MORE;
}
const uint8_t *q = p + 1;
if (flags & HEXTILE_BACKGROUND) {
std::memcpy(this->hex_bg_, q, this->bpp_);
q += this->bpp_;
}
if (flags & HEXTILE_FOREGROUND) {
std::memcpy(this->hex_fg_, q, this->bpp_);
q += this->bpp_;
}
this->subrects_left_ = (flags & HEXTILE_ANY_SUBRECTS) ? *q : 0;
this->subrects_coloured_ = (flags & HEXTILE_SUBRECTS_COLOURED) != 0;
this->rx_pos_ += need;
this->fill_tile_(0, 0, this->tile_w_, this->tile_h_, this->hex_bg_);
this->step_ = VncStep::VNC_STEP_SUBRECTS;
break;
}
case VncStep::VNC_STEP_TILE_RAW: {
const size_t total = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
const size_t count = std::min(avail, total - this->tile_fill_);
if (count == 0) {
return VncParse::VNC_PARSE_MORE;
}
std::memcpy(this->tile_ + this->tile_fill_, p, count);
this->rx_pos_ += count;
this->tile_fill_ += count;
if (this->tile_fill_ == total) {
this->draw_tile_();
this->next_tile_();
}
break;
}
case VncStep::VNC_STEP_SUBRECTS: {
if (this->subrects_left_ == 0) {
this->draw_tile_();
this->next_tile_();
break;
}
const size_t need = (this->subrects_coloured_ ? this->bpp_ : 0) + 2;
if (avail < need) {
return VncParse::VNC_PARSE_MORE;
}
const uint8_t *pixel = this->hex_fg_;
const uint8_t *q = p;
if (this->subrects_coloured_) {
pixel = q;
q += this->bpp_;
}
this->fill_tile_(q[0] >> 4, q[0] & 0x0F, (q[1] >> 4) + 1, (q[1] & 0x0F) + 1, pixel);
this->rx_pos_ += need;
this->subrects_left_--;
break;
}
case VncStep::VNC_STEP_SKIP: {
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
this->rx_pos_ += count;
@@ -592,6 +749,20 @@ void VncClient::rect_done_() {
}
}
void VncClient::next_tile_() {
this->step_ = VncStep::VNC_STEP_TILE;
if (static_cast<uint32_t>(this->pos_x_) + 16 < static_cast<uint32_t>(this->rect_x_) + this->rect_w_) {
this->pos_x_ += 16;
return;
}
this->pos_x_ = this->rect_x_;
if (static_cast<uint32_t>(this->pos_y_) + 16 < static_cast<uint32_t>(this->rect_y_) + this->rect_h_) {
this->pos_y_ += 16;
return;
}
this->rect_done_();
}
void VncClient::frame_done_() {
this->in_update_ = false;
this->request_outstanding_ = false;
@@ -604,9 +775,9 @@ void VncClient::frame_done_() {
}
this->last_change_ms_ = now;
this->refresh_pending_ = true;
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms, %u hextile tiles drawn",
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
millis() - this->update_start_ms_);
millis() - this->update_start_ms_, this->drawn_tiles_);
}
}
@@ -619,8 +790,96 @@ void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t
this->frame_drawn_ = true;
}
void VncClient::draw_tile_() {
if (this->tile_hidden_) {
return;
}
const int w = std::min<int>(this->tile_w_, this->vis_w_ - this->pos_x_);
const int h = std::min<int>(this->tile_h_, this->vis_h_ - this->pos_y_);
uint32_t *cell = nullptr;
uint32_t hash = 0;
if (this->hash_cols_ != 0) {
if ((this->pos_x_ | this->pos_y_) & 15) {
// An off-grid tile spans several cells, and a later write to any of them would make its hash stale.
this->forget_tiles_(this->pos_x_, this->pos_y_, w, h);
} else {
cell = this->tile_hashes_ + (this->pos_y_ / 16) * this->hash_cols_ + this->pos_x_ / 16;
hash = this->tile_hash_();
if (*cell == hash) {
return;
}
}
}
this->display_->draw_pixels_at(this->pos_x_, this->pos_y_, w, h, this->tile_, this->bitness_, this->big_endian_, 0, 0,
this->tile_w_ - w);
this->frame_drawn_ = true;
this->drawn_tiles_++;
if (cell != nullptr) {
*cell = hash;
}
}
uint32_t VncClient::tile_hash_() const {
// The tile's size, pixel size and pixels; the cell gives its place. Zero means "unknown".
uint32_t hash = fnv1a_hash_extend(FNV1_OFFSET_BASIS,
static_cast<uint32_t>(this->tile_w_) << 16 | this->tile_h_ << 8 | this->bpp_);
const size_t len = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
size_t i = 0;
for (; i + 4 <= len; i += 4) {
uint32_t word;
std::memcpy(&word, this->tile_ + i, sizeof(word)); // tile_ is aligned, so this is one load
// Byte by byte: XOR-ing whole words lets high-bit changes in two words cancel.
hash = fnv1a_hash_extend(hash, word);
}
for (; i < len; i++) {
hash = fnv1a_hash_extend(hash, this->tile_[i]);
}
return hash == 0 ? 1 : hash;
}
void VncClient::forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
if (this->hash_cols_ == 0 || w == 0 || h == 0) {
return;
}
const uint16_t col_end = std::min<uint16_t>((x + w - 1) / 16, this->hash_cols_ - 1);
const uint16_t row_end = std::min<uint16_t>((y + h - 1) / 16, this->hash_rows_ - 1);
for (uint16_t row = y / 16; row <= row_end; row++) {
for (uint16_t col = x / 16; col <= col_end; col++) {
this->tile_hashes_[row * this->hash_cols_ + col] = 0;
}
}
}
void VncClient::fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel) {
if (this->tile_hidden_ || x >= this->tile_w_ || y >= this->tile_h_) {
return;
}
w = std::min<uint8_t>(w, this->tile_w_ - x);
h = std::min<uint8_t>(h, this->tile_h_ - y);
const size_t stride = static_cast<size_t>(this->tile_w_) * this->bpp_;
const size_t row_len = static_cast<size_t>(w) * this->bpp_;
uint8_t *first = this->tile_ + y * stride + x * this->bpp_;
if (this->bpp_ == 1) {
std::memset(first, pixel[0], w);
} else {
for (size_t i = 0; i < row_len; i += 2) {
first[i] = pixel[0];
first[i + 1] = pixel[1];
}
}
for (uint8_t row = 1; row < h; row++) {
std::memcpy(first + row * stride, first, row_len);
}
}
bool VncClient::start_session_(uint32_t now) {
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
VncPixelFormat format = this->pixel_format_;
if (format == VncPixelFormat::VNC_PIXEL_FORMAT_AUTO) {
format = this->display_->get_display_type() == display::DISPLAY_TYPE_BINARY
? VncPixelFormat::VNC_PIXEL_FORMAT_RGB332
: VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
}
const bool rgb565 = format == VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
this->bpp_ = rgb565 ? 2 : 1;
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
@@ -632,6 +891,24 @@ bool VncClient::start_session_(uint32_t now) {
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
}
const uint16_t cols = (this->vis_w_ + 15) / 16;
const uint16_t rows = (this->vis_h_ + 15) / 16;
const auto rotation = static_cast<uint8_t>(this->display_->get_rotation() / 90);
const bool rotated = rotation != this->hash_rotation_;
this->hash_rotation_ = rotation;
if (this->tile_hashes_ == nullptr) {
this->hash_cols_ = 0;
this->hash_rows_ = 0;
} else if (static_cast<uint32_t>(cols) * rows > this->hash_capacity_) {
ESP_LOGW(TAG, "Screen has %u tiles, more than the %u cached; every tile is drawn", cols * rows,
this->hash_capacity_);
this->hash_cols_ = 0;
this->hash_rows_ = 0;
} else if (cols != this->hash_cols_ || rows != this->hash_rows_ || rotated) {
this->hash_cols_ = cols;
this->hash_rows_ = rows;
this->clear_tiles_();
}
// SetPixelFormat: true colour, in the byte order the display takes
uint8_t pixel_format[20] = {};
@@ -648,10 +925,12 @@ bool VncClient::start_session_(uint32_t now) {
return false;
}
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
uint8_t encodings[12] = {2, 0, 0, 2};
write_be32(encodings + 4, ENCODING_RAW);
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
// SetEncodings
uint8_t encodings[4 + 4 * std::size(ENCODINGS)] = {2};
write_be16(encodings + 2, std::size(ENCODINGS));
for (size_t i = 0; i < std::size(ENCODINGS); i++) {
write_be32(encodings + 4 + 4 * i, static_cast<uint32_t>(ENCODINGS[i]));
}
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
return false;
}
@@ -669,6 +948,12 @@ void VncClient::pace_(uint32_t now) {
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
return;
}
if (this->need_full_) {
if (this->send_update_request_(false, now)) {
this->need_full_ = false;
}
return;
}
// Repeating a parked request costs nothing on the server and detects a dead link.
if (!this->request_outstanding_ ||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
+41 -1
View File
@@ -10,6 +10,12 @@
namespace esphome::vnc {
enum class VncPixelFormat : uint8_t {
VNC_PIXEL_FORMAT_AUTO,
VNC_PIXEL_FORMAT_RGB565,
VNC_PIXEL_FORMAT_RGB332,
};
enum class VncState : uint8_t {
VNC_STATE_IDLE,
VNC_STATE_HANDSHAKE,
@@ -31,6 +37,9 @@ enum class VncStep : uint8_t {
VNC_STEP_RECT,
VNC_STEP_RECT_DONE,
VNC_STEP_RAW,
VNC_STEP_TILE,
VNC_STEP_TILE_RAW,
VNC_STEP_SUBRECTS,
VNC_STEP_SKIP,
};
@@ -55,6 +64,7 @@ class VncClient : public Component {
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
void set_pixel_format(VncPixelFormat format) { this->pixel_format_ = format; }
void setup() override;
void loop() override;
@@ -63,6 +73,8 @@ class VncClient : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
/// Ask the server for the whole screen again.
void refresh();
template<typename F> void add_on_connect_callback(F &&callback) {
this->connect_callback_.add(std::forward<F>(callback));
@@ -73,6 +85,7 @@ class VncClient : public Component {
protected:
static constexpr size_t RX_BUFFER_SIZE = 512;
static constexpr size_t TILE_BUFFER_SIZE = 16 * 16 * 2;
void connect_step_(uint32_t now);
void start_handshake_(uint32_t now);
@@ -83,7 +96,13 @@ class VncClient : public Component {
bool send_client_init_();
void frame_done_();
void rect_done_();
void next_tile_();
void draw_tile_();
uint32_t tile_hash_() const;
void clear_tiles_();
void forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
void fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel);
void skip_(uint32_t count, VncStep next);
bool budget_exhausted_() const;
bool send_(const uint8_t *data, size_t len);
@@ -100,6 +119,9 @@ class VncClient : public Component {
LazyCallbackManager<void()> connect_callback_;
LazyCallbackManager<void()> disconnect_callback_;
HighFrequencyLoopRequester high_freq_;
// Fingerprint of the tile last drawn at each 16x16 cell, so tiles the server resends unchanged are skipped.
// A 32-bit FNV-1a collision keeps a stale tile until it changes again or vnc.refresh runs; accepted.
uint32_t *tile_hashes_{nullptr};
uint32_t update_delay_ms_{200};
uint32_t max_update_delay_ms_{1000};
@@ -131,15 +153,28 @@ class VncClient : public Component {
uint16_t rect_y_{0};
uint16_t rect_w_{0};
uint16_t rect_h_{0};
// Raw: column and row inside the rect.
// Raw: column and row inside the rect. Hextile: absolute origin of the current tile.
uint16_t pos_x_{0};
uint16_t pos_y_{0};
uint16_t tile_fill_{0};
uint16_t drawn_tiles_{0};
uint16_t hash_capacity_{0};
uint16_t hash_cols_{0};
uint16_t hash_rows_{0};
uint8_t tile_w_{0};
uint8_t tile_h_{0};
uint8_t subrects_left_{0};
uint8_t bpp_{2};
uint8_t minor_{8};
uint8_t sec_count_{0};
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
// The display rotation the cached tiles were drawn at, in quarter turns.
uint8_t hash_rotation_{0};
uint8_t hex_bg_[2]{};
uint8_t hex_fg_[2]{};
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
VncPixelFormat pixel_format_{VncPixelFormat::VNC_PIXEL_FORMAT_AUTO};
VncState state_{VncState::VNC_STATE_IDLE};
VncStep step_{VncStep::VNC_STEP_VERSION};
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
@@ -149,14 +184,19 @@ class VncClient : public Component {
bool in_update_{false};
bool request_outstanding_{false};
bool refresh_pending_{false};
bool need_full_{false};
bool frame_drawn_{false};
bool subrects_coloured_{false};
bool tile_hidden_{false};
bool display_parks_{false};
bool display_busy_{false};
bool busy_warned_{false};
bool refresh_retried_{false}; // one retry per refresh the driver abandoned
bool network_was_up_{false};
bool big_endian_; // the pixel order the display takes
uint8_t rx_[RX_BUFFER_SIZE];
alignas(4) uint8_t tile_[TILE_BUFFER_SIZE];
};
} // namespace esphome::vnc
+4 -22
View File
@@ -38,19 +38,19 @@ const char *p2s(uart::UARTParityOptions parity) {
}
/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug
void print_buffer(const uint8_t *data, size_t length) {
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < length; i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
if (i % 32 == 31) {
ESP_LOGVV(TAG, " %s", hex_buffer);
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
}
}
if (length % 32) {
// null terminate if incomplete line
hex_buffer[3 * (length % 32) + 2] = 0;
ESP_LOGVV(TAG, " %s", hex_buffer);
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
}
}
@@ -495,24 +495,6 @@ class Increment {
uint8_t i_;
};
/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line).
/// @param buffer contains the values to display
void print_buffer(std::vector<uint8_t> buffer) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < buffer.size(); i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
if (i % 32 == 31) {
ESP_LOGI(TAG, " %s", hex_buffer);
}
}
if (buffer.size() % 32) {
// null terminate if incomplete line
hex_buffer[3 * (buffer.size() % 32) + 1] = 0;
ESP_LOGI(TAG, " %s", hex_buffer);
}
}
/// @brief test the write_array method
void WeikaiChannel::uart_send_test_(char *message) {
auto start_exec = micros();
@@ -560,7 +542,7 @@ bool WeikaiChannel::uart_receive_test_(char *message) {
for (size_t i = 0; i < RING_BUFFER_SIZE; i++) {
if (buffer[i] != i % XFER_MAX_SIZE) {
ESP_LOGE(TAG, "Read buffer contains error b=%x i=%x", buffer[i], i % XFER_MAX_SIZE);
print_buffer(buffer);
print_buffer(TAG, buffer.data(), buffer.size(), ESPHOME_LOG_LEVEL_INFO);
status = false;
break;
}
+3
View File
@@ -43,6 +43,9 @@ constexpr size_t RING_BUFFER_SIZE = FIFO_SIZE;
/// @brief Name of a register for the log messages
const LogString *reg_to_str(int reg, bool page1);
/// Logs a buffer in hexadecimal, 32 values per line, under @p tag at @p level.
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level = ESPHOME_LOG_LEVEL_VERY_VERBOSE);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// @brief This is an helper class that provides a simple ring buffers that works as a FIFO
/// @details This ring buffer is used to buffer the bytes received in the FIFO of the Weika device. The best way to read
+2 -19
View File
@@ -8,23 +8,6 @@
namespace esphome::weikai_i2c {
ESPHOME_LOG_TAG(TAG, "weikai_i2c");
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
void print_buffer(const uint8_t *data, size_t length) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < length; i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
if (i % 32 == 31) {
ESP_LOGVV(TAG, " %s", hex_buffer);
}
}
if (length % 32) {
// null terminate if incomplete line
hex_buffer[3 * (length % 32) + 2] = 0;
ESP_LOGVV(TAG, " %s", hex_buffer);
}
}
using namespace weikai;
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
@@ -77,7 +60,7 @@ void WeikaiRegisterI2C::read_fifo(uint8_t *data, size_t length) const {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
(int) error, length);
print_buffer(data, length);
print_buffer(TAG, data, length);
#endif
} else { // error
this->comp_->status_set_warning();
@@ -112,7 +95,7 @@ void WeikaiRegisterI2C::write_fifo(uint8_t *data, size_t length) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "WK2168Reg::write_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
(int) error, length);
print_buffer(data, length);
print_buffer(TAG, data, length);
#endif
} else { // error
this->comp_->status_set_warning();
+2 -19
View File
@@ -35,23 +35,6 @@ const char *p2s(uart::UARTParityOptions parity) {
}
}
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
void print_buffer(const uint8_t *data, size_t length) {
char hex_buffer[100];
hex_buffer[(3 * 32) + 1] = 0;
for (size_t i = 0; i < length; i++) {
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
if (i % 32 == 31) {
ESP_LOGVV(TAG, " %s", hex_buffer);
}
}
if (length % 32) {
// null terminate if incomplete line
hex_buffer[3 * (length % 32) + 2] = 0;
ESP_LOGVV(TAG, " %s", hex_buffer);
}
}
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
enum CmdType { WRITE_CMD = 0, READ_CMD = 1 }; ///< Read or Write transfer
@@ -100,7 +83,7 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
this->channel_, length);
print_buffer(data, length);
print_buffer(TAG, data, length);
#endif
}
@@ -127,7 +110,7 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
this->channel_, length);
print_buffer(data, length);
print_buffer(TAG, data, length);
#endif
}
+52 -2
View File
@@ -1,9 +1,15 @@
"""Tests for teleinfo sensor tag presets."""
import logging
import pytest
from esphome.components.teleinfo import CONF_TAG_NAME
from esphome.components.teleinfo.sensor import apply_tag_config
from esphome.components.teleinfo.sensor import (
TIC_TAG_CONFIGS,
TIC_TEXT_TAGS,
apply_tag_config,
)
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
@@ -11,6 +17,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_REACTIVE_ENERGY,
@@ -22,6 +29,7 @@ from esphome.const import (
UNIT_KILOVOLT_AMPS,
UNIT_KILOWATT_HOURS,
UNIT_MILLIAMP,
UNIT_MINUTE,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
@@ -60,7 +68,13 @@ from esphome.const import (
("IINST1", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
("ADIR2", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
("PAPP", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT),
("PMAX", UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT),
(
"PMAX",
UNIT_VOLT_AMPS,
DEVICE_CLASS_APPARENT_POWER,
STATE_CLASS_MEASUREMENT,
),
("PEJP", UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT),
],
)
def test_known_tag_preset(
@@ -73,6 +87,42 @@ def test_known_tag_preset(
assert CONF_ICON not in config
def test_no_prefix_is_prefix_of_another() -> None:
prefixes = [*TIC_TAG_CONFIGS, *TIC_TEXT_TAGS]
assert len(prefixes) == len(set(prefixes))
for prefix in prefixes:
for other in prefixes:
assert prefix == other or not other.startswith(prefix)
@pytest.mark.parametrize("tag", ["NTARF", "NJOURF", "NJOURF+1", "RELAIS", "VTIC"])
def test_unitless_tag_gets_no_unit(tag: str, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING):
config = apply_tag_config({CONF_TAG_NAME: tag})
assert config == {CONF_TAG_NAME: tag}
assert not caplog.records
@pytest.mark.parametrize("tag", ["LTARF", "NGTF", "PTEC", "OPTARIF", "MSG1", "PPOT"])
def test_text_tag_gets_no_unit_and_warns(
tag: str, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING):
config = apply_tag_config({CONF_TAG_NAME: tag})
assert config == {CONF_TAG_NAME: tag}
assert "text_sensor" in caplog.text
def test_unitless_tag_keeps_user_settings() -> None:
config = apply_tag_config(
{CONF_TAG_NAME: "NTARF", CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT}
)
assert config == {
CONF_TAG_NAME: "NTARF",
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
}
def test_unknown_tag_keeps_legacy_defaults() -> None:
config = apply_tag_config({CONF_TAG_NAME: "MYTAG"})
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_WATT_HOURS
+28
View File
@@ -168,6 +168,7 @@ def test_it8951_owned_by_vnc(
assert "set_reconnect_interval(5000);" in main_cpp
assert "set_update_delay(200);" in main_cpp
assert "set_max_update_delay(1000);" in main_cpp
assert "set_pixel_format(vnc::VncPixelFormat::VNC_PIXEL_FORMAT_AUTO);" in main_cpp
def test_epaper_spi_owned_by_vnc(
@@ -221,6 +222,7 @@ vnc:
reconnect_interval: 2s
update_delay: 0ms
max_update_delay: 3s
pixel_format: RGB332
on_connect:
- logger.log: up
on_disconnect:
@@ -234,6 +236,7 @@ vnc:
assert "set_reconnect_interval(2000);" in main_cpp
assert "set_update_delay(0);" in main_cpp
assert "set_max_update_delay(3000);" in main_cpp
assert "set_pixel_format(vnc::VncPixelFormat::VNC_PIXEL_FORMAT_RGB332);" in main_cpp
assert "add_on_connect_callback(" in main_cpp
assert "add_on_disconnect_callback(" in main_cpp
@@ -260,6 +263,28 @@ vnc:
assert TEST_CARD not in main_cpp
def test_action_codegen(
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
) -> None:
main_cpp = generate_main(
write_yaml(
IT8951,
"""
vnc:
- id: remote
host: 192.168.1.20
interval:
- interval: 60s
then:
- vnc.refresh
""",
)
)
assert "remote->refresh();" in main_cpp
@pytest.mark.parametrize(
("display_options", "error"),
[
@@ -389,6 +414,9 @@ display:
id="reconnect",
),
pytest.param(" port: 0\n", "value must be at least 1", id="port"),
pytest.param(
" pixel_format: rgb888\n", "Unknown value 'rgb888'", id="format"
),
],
)
def test_option_rejected(
+12
View File
@@ -100,6 +100,14 @@ sensor:
name: pcoup
tag_name: PCOUP
teleinfo_id: test_teleinfo
- platform: teleinfo
name: ntarf
tag_name: NTARF
teleinfo_id: test_teleinfo
- platform: teleinfo
name: pejp
tag_name: PEJP
teleinfo_id: test_teleinfo
# Explicit override - user-specified unit should not be overwritten
- platform: teleinfo
name: hchp_override
@@ -117,3 +125,7 @@ text_sensor:
name: optarif
tag_name: OPTARIF
teleinfo_id: test_teleinfo
- platform: teleinfo
name: ltarf
tag_name: LTARF
teleinfo_id: test_teleinfo
+4 -1
View File
@@ -6,6 +6,7 @@ vnc:
reconnect_interval: 10s
update_delay: 300ms
max_update_delay: 2s
pixel_format: rgb565
on_connect:
- logger.log: VNC connected
on_disconnect:
@@ -14,6 +15,7 @@ vnc:
interval:
- interval: 60s
then:
- vnc.refresh: vnc_client
- if:
condition:
vnc.is_connected: vnc_client
@@ -24,4 +26,5 @@ interval:
vnc.is_connected:
id: vnc_client
then:
- logger.log: VNC is still connected
- vnc.refresh:
id: vnc_client
+1 -1
View File
@@ -132,7 +132,7 @@ TEST(VncBackoff, NextSessionStartsClean) {
set_now(now_ms() + 100);
// Leave a frame half parsed and a refresh pending, then lose the link.
s.deliver(Rfb().fbu(1).raw_rect(0, 0, 2, 1, {1, 2}));
s.deliver(Rfb().fbu(1).rect(0, 0, 4, 4, 0).u8(0x02));
s.deliver(Rfb().fbu(1).rect(0, 0, 4, 4, 5).u8(0x02));
ASSERT_TRUE(s.client.in_update());
s.close_peer();
s.loop();
+362 -35
View File
@@ -26,21 +26,7 @@ static void expect_block(const FakeDisplay &d, int x, int y, int w, int h, const
}
}
static void expect_solid(const FakeDisplay &d, int x, int y, int w, int h, uint16_t px, uint8_t bpp) {
expect_block(d, x, y, w, h, std::vector<uint16_t>(static_cast<size_t>(w) * h, px), bpp);
}
class VncDecode : public ::testing::Test {
protected:
void start(FakeDisplay &display, uint16_t fb_w, uint16_t fb_h) {
this->session_ = std::make_unique<Session>(&display);
this->session_->start(fb_w, fb_h);
// Past the 50 ms request spacing, so the next request goes out as soon as a frame ends.
set_now(now_ms() + 100);
}
Session &s() { return *this->session_; }
std::unique_ptr<Session> session_;
};
class VncDecode : public VncSessionTest {};
TEST_F(VncDecode, Raw16bpp) {
FakeDisplay d(64, 48);
@@ -85,6 +71,225 @@ TEST_F(VncDecode, EmptyUpdateRequestsAgainWithoutRefresh) {
EXPECT_EQ(d.updates(), 0);
}
TEST_F(VncDecode, HextileRawTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
auto px = pattern(256, 0x0F0F);
Rfb f;
f.fbu(1).rect(16, 16, 16, 16, HEXTILE).u8(0x01);
for (auto p : px) {
f.px(p);
}
s().deliver(f);
expect_block(d, 16, 16, 16, 16, px, 2);
EXPECT_EQ(d.drawn_count(), 256u);
}
TEST_F(VncDecode, HextileRawTileIgnoresOtherFlags) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
auto px = pattern(4 * 3, 0x7777);
Rfb f;
// 0x1F: Raw plus every other bit. Only the pixels follow.
f.fbu(2).rect(0, 0, 4, 3, HEXTILE).u8(0x1F);
for (auto p : px) {
f.px(p);
}
f.raw_rect(10, 10, 1, 1, {0xABCD});
s().deliver(f);
expect_block(d, 0, 0, 4, 3, px, 2);
EXPECT_EQ(d.pixel(10, 10), expect_px(0xABCD, 2));
EXPECT_TRUE(s().client.is_connected());
EXPECT_EQ(hex(s().take()), hex(fbur(true, 64, 48)));
}
TEST_F(VncDecode, HextileBackgroundOnly) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
s().deliver(Rfb().fbu(1).rect(0, 0, 16, 16, HEXTILE).u8(0x02).px(0x1234));
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x1234));
EXPECT_EQ(d.drawn_count(), 256u);
}
TEST(VncDecodeBigEndian, HextileTilesKeepTheServerByteOrder) {
FakeDisplay d(64, 48);
Session s(&d, 1000, true);
s.start(64, 48);
set_now(now_ms() + 100);
s.deliver(
Rfb().fbu(2).rect(0, 0, 16, 16, HEXTILE).u8(0x02).u16(0x1234).rect(16, 0, 1, 1, HEXTILE).u8(0x01).u16(0xF800));
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x1234));
EXPECT_EQ(d.pixel(16, 0), expect_px(0xF800, 2));
}
TEST_F(VncDecode, HextileForegroundSubrects) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
// bg 0x1111, fg 0x2222, two subrects: (1,2) 3x4 and (10,0) 6x1.
s().deliver(Rfb()
.fbu(1)
.rect(0, 0, 16, 16, HEXTILE)
.u8(0x02 | 0x04 | 0x08)
.px(0x1111)
.px(0x2222)
.u8(2)
.u8(0x12)
.u8(0x23)
.u8(0xA0)
.u8(0x50));
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
const bool fg = (x >= 1 && x < 4 && y >= 2 && y < 6) || (x >= 10 && y == 0);
ASSERT_EQ(d.pixel(x, y), expect_px(fg ? 0x2222 : 0x1111, 2)) << x << "," << y;
}
}
}
TEST_F(VncDecode, HextileColouredSubrects) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
s().deliver(Rfb()
.fbu(1)
.rect(16, 0, 16, 16, HEXTILE)
.u8(0x02 | 0x08 | 0x10)
.px(0x0001)
.u8(2)
.px(0xF800)
.u8(0x00)
.u8(0x11)
.px(0x07E0)
.u8(0xFF)
.u8(0x00));
EXPECT_TRUE(solid(d, 16, 0, 2, 2, 0xF800));
EXPECT_EQ(d.pixel(31, 15), expect_px(0x07E0, 2));
EXPECT_EQ(d.pixel(18, 0), expect_px(0x0001, 2));
EXPECT_EQ(d.pixel(30, 15), expect_px(0x0001, 2));
}
TEST_F(VncDecode, HextileCarriesColoursBetweenTiles) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
s().deliver(Rfb()
.fbu(1)
.rect(0, 0, 48, 16, HEXTILE)
// Tile 1: bg A, fg B, one subrect.
.u8(0x02 | 0x04 | 0x08)
.px(0xAAAA)
.px(0xBBBB)
.u8(1)
.u8(0x00)
.u8(0x00)
// Tile 2: nothing specified, so solid A.
.u8(0x00)
// Tile 3: subrects in the carried fg B on the carried bg A.
.u8(0x08)
.u8(1)
.u8(0xFF)
.u8(0x00));
EXPECT_EQ(d.pixel(0, 0), expect_px(0xBBBB, 2));
EXPECT_TRUE(solid(d, 1, 1, 15, 15, 0xAAAA));
EXPECT_TRUE(solid(d, 16, 0, 16, 16, 0xAAAA));
EXPECT_EQ(d.pixel(47, 15), expect_px(0xBBBB, 2));
EXPECT_TRUE(solid(d, 32, 0, 15, 15, 0xAAAA));
}
TEST_F(VncDecode, HextileCarriesColoursAcrossRawTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
auto raw = pattern(256, 0x0400);
Rfb f;
f.fbu(1).rect(0, 16, 48, 16, HEXTILE).u8(0x02 | 0x04).px(0xCCCC).px(0xDDDD).u8(0x01);
for (auto p : raw) {
f.px(p);
}
f.u8(0x08).u8(1).u8(0x00).u8(0x00);
s().deliver(f);
EXPECT_TRUE(solid(d, 0, 16, 16, 16, 0xCCCC));
expect_block(d, 16, 16, 16, 16, raw, 2);
EXPECT_EQ(d.pixel(32, 16), expect_px(0xDDDD, 2));
EXPECT_EQ(d.pixel(33, 16), expect_px(0xCCCC, 2));
EXPECT_EQ(d.pixel(47, 31), expect_px(0xCCCC, 2));
}
TEST_F(VncDecode, HextileSingleEdgeTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
auto px = pattern(13 * 7, 0x3000);
Rfb f;
f.fbu(1).rect(5, 3, 13, 7, HEXTILE).u8(0x01);
for (auto p : px) {
f.px(p);
}
s().deliver(f);
expect_block(d, 5, 3, 13, 7, px, 2);
EXPECT_EQ(d.drawn_count(), 13u * 7u);
}
TEST_F(VncDecode, HextileTileGrid) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
// 37x21 is 3x2 tiles: 16, 16, 5 wide and 16, 5 tall.
Rfb f;
f.fbu(1).rect(2, 1, 37, 21, HEXTILE);
for (int i = 0; i < 6; i++) {
f.u8(0x02).px(0x1000 + i);
}
s().deliver(f);
const int xs[] = {2, 18, 34};
const int ws[] = {16, 16, 5};
const int ys[] = {1, 17};
const int hs[] = {16, 5};
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 3; col++) {
EXPECT_TRUE(solid(d, xs[col], ys[row], ws[col], hs[row], 0x1000 + row * 3 + col));
}
}
EXPECT_EQ(d.drawn_count(), 37u * 21u);
}
TEST_F(VncDecode, HextileSubrectClippedToTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
// A 5x5 edge tile with a subrect at 3,3 claiming 16x16, and one starting outside the tile.
s().deliver(Rfb()
.fbu(1)
.rect(59, 43, 5, 5, HEXTILE)
.u8(0x02 | 0x04 | 0x08)
.px(0x0101)
.px(0xFFFF)
.u8(2)
.u8(0x33)
.u8(0xFF)
.u8(0x90)
.u8(0x00));
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 5; x++) {
const bool fg = x >= 3 && y >= 3;
ASSERT_EQ(d.pixel(59 + x, 43 + y), expect_px(fg ? 0xFFFF : 0x0101, 2)) << x << "," << y;
}
}
EXPECT_EQ(d.drawn_count(), 25u);
EXPECT_TRUE(s().client.is_connected());
}
TEST_F(VncDecode, Hextile8bpp) {
FakeDisplay d(64, 48, display::DISPLAY_TYPE_BINARY);
this->start(d, 64, 48);
auto raw = pattern(4, 0x00E3);
Rfb f;
f.fbu(1).rect(0, 0, 18, 2, HEXTILE).u8(0x02 | 0x04 | 0x08).px(0x00, 1).px(0xFF, 1).u8(1).u8(0x10).u8(0x00).u8(0x01);
for (auto p : raw) {
f.px(p & 0xFF, 1);
}
s().deliver(f);
EXPECT_EQ(d.pixel(0, 0), expect_px(0x00, 1));
EXPECT_EQ(d.pixel(1, 0), expect_px(0xFF, 1));
EXPECT_EQ(d.pixel(1, 1), expect_px(0x00, 1));
for (int i = 0; i < 4; i++) {
EXPECT_EQ(d.pixel(16 + i % 2, i / 2), expect_px(raw[i] & 0xFF, 1));
}
}
TEST_F(VncDecode, RichCursorIsSkippedAndNeverRefreshes) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
@@ -161,9 +366,15 @@ TEST(VncDecodeClip, ServerLargerThanDisplay) {
auto raw = pattern(90 * 70, 0x0021);
Rfb f;
f.fbu(3).raw_rect(5, 5, 90, 70, raw);
// A raw rect straddling both edges, and one entirely off screen.
f.raw_rect(56, 40, 20, 20, std::vector<uint16_t>(20 * 20, 0x5000));
f.raw_rect(70, 50, 16, 16, pattern(256, 1));
// A hextile rect straddling both edges, and one entirely off screen.
f.rect(56, 40, 20, 20, HEXTILE);
for (int i = 0; i < 4; i++) {
f.u8(0x02).px(0x5000 + i);
}
f.rect(70, 50, 16, 16, HEXTILE).u8(0x01);
for (auto p : pattern(256, 1)) {
f.px(p);
}
s.deliver(f);
EXPECT_EQ(d.out_of_bounds(), 0);
for (int y = 5; y < 40; y++) {
@@ -171,7 +382,7 @@ TEST(VncDecodeClip, ServerLargerThanDisplay) {
ASSERT_EQ(d.pixel(x, y), expect_px(raw[(y - 5) * 90 + (x - 5)], 2)) << x << "," << y;
}
}
expect_solid(d, 56, 40, 8, 8, 0x5000, 2);
EXPECT_TRUE(solid(d, 56, 40, 8, 8, 0x5000));
EXPECT_EQ(d.pixel(4, 4), UNSET);
EXPECT_TRUE(s.client.is_connected());
EXPECT_EQ(hex(s.take()), hex(fbur(true, 64, 48)));
@@ -228,8 +439,8 @@ TEST_P(VncRectOutside, Reconnects) {
INSTANTIATE_TEST_SUITE_P(Rects, VncRectOutside,
::testing::Values(OutsideCase{60, 0, 5, 1, RAW}, OutsideCase{0, 47, 1, 2, RAW},
OutsideCase{65535, 0, 2, 1, RAW}, OutsideCase{0, 65535, 1, 2, RAW},
OutsideCase{64, 0, 1, 1, RAW}, OutsideCase{0, 0, 65, 1, RAW}));
OutsideCase{65535, 0, 2, 1, RAW}, OutsideCase{0, 65535, 1, 2, HEXTILE},
OutsideCase{64, 0, 1, 1, HEXTILE}, OutsideCase{0, 0, 65, 1, HEXTILE}));
// --- A frame whose last step needs no bytes must still finish when its last byte arrives.
@@ -251,6 +462,43 @@ class VncZeroByteEnd : public VncDecode {
}
};
TEST_F(VncZeroByteEnd, SolidHextileTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
Rfb f;
f.fbu(1).rect(0, 0, 32, 16, HEXTILE).u8(0x01);
for (auto p : pattern(256, 3)) {
f.px(p);
}
f.u8(0x02).px(0x4444);
this->deliver_bytewise(f.bytes, f.bytes.size());
EXPECT_TRUE(solid(d, 16, 0, 16, 16, 0x4444));
s().run_for(300);
EXPECT_EQ(d.updates(), 1);
}
TEST_F(VncZeroByteEnd, CarriedBackgroundTile) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
Rfb f;
f.fbu(1).rect(0, 0, 32, 16, HEXTILE).u8(0x02).px(0x5555).u8(0x00);
this->deliver_bytewise(f.bytes, f.bytes.size());
EXPECT_TRUE(solid(d, 0, 0, 32, 16, 0x5555));
s().run_for(300);
EXPECT_EQ(d.updates(), 1);
}
TEST_F(VncZeroByteEnd, TileWithZeroSubrects) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
Rfb f;
f.fbu(1).rect(0, 0, 16, 16, HEXTILE).u8(0x02 | 0x08).px(0x6666).u8(0);
this->deliver_bytewise(f.bytes, f.bytes.size());
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x6666));
s().run_for(300);
EXPECT_EQ(d.updates(), 1);
}
TEST_F(VncZeroByteEnd, ZeroSizeCursor) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
@@ -285,17 +533,14 @@ TEST_F(VncZeroByteEnd, EmptyCutTextAfterFrame) {
EXPECT_EQ(d.pixel(1, 0), expect_px(0x7002, 2));
}
// --- The 20 ms budget: with a slow display, one row per pass.
// --- The 20 ms budget: with a slow display, one tile per pass.
TEST_F(VncDecode, BudgetPausesBetweenRectsAndFinishesOnTheLastOne) {
TEST_F(VncDecode, BudgetPausesBetweenTilesAndFinishesOnTheLastOne) {
FakeDisplay d(64, 48);
this->start(d, 64, 48);
d.set_draw_delay_us(25000);
Rfb f;
f.fbu(3)
.raw_rect(0, 0, 16, 1, std::vector<uint16_t>(16, 0x0A0A))
.raw_rect(16, 0, 16, 1, std::vector<uint16_t>(16, 0x0B0B))
.raw_rect(32, 0, 16, 1, std::vector<uint16_t>(16, 0x0C0C));
f.fbu(1).rect(0, 0, 48, 16, HEXTILE).u8(0x02).px(0x0A0A).u8(0x02).px(0x0B0B).u8(0x02).px(0x0C0C);
s().send(f);
s().loop();
EXPECT_EQ(d.draw_calls(), 1);
@@ -312,7 +557,7 @@ TEST_F(VncDecode, BudgetPausesBetweenRectsAndFinishesOnTheLastOne) {
s().loop(2);
EXPECT_FALSE(s().client.rx_pending());
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
expect_solid(d, 32, 0, 16, 1, 0x0C0C, 2);
EXPECT_TRUE(solid(d, 32, 0, 16, 16, 0x0C0C));
}
TEST_F(VncDecode, BudgetPausesInsideARawRect) {
@@ -393,6 +638,8 @@ static Generated generate(uint32_t seed) {
g.handshake_tx = cat({g.handshake_tx, bpp == 2 ? set_pixel_format_565() : set_pixel_format_332(), set_encodings(),
fbur(false, g.vis_w, g.vis_h)});
uint16_t bg = 0;
uint16_t fg = 0;
const int frames = rnd(3, 8);
for (int frame = 0; frame < frames; frame++) {
Rfb f;
@@ -420,19 +667,99 @@ static Generated generate(uint32_t seed) {
if (kind == 9) {
const int x = rnd(0, fb_w);
const int y = rnd(0, fb_h);
f.rect(x, y, rnd(0, 1) ? 0 : rnd(0, fb_w - x), 0, RAW);
f.rect(x, y, rnd(0, 1) ? 0 : rnd(0, fb_w - x), 0, rnd(0, 1) ? RAW : HEXTILE);
continue;
}
const int x = rnd(0, fb_w - 1);
const int y = rnd(0, fb_h - 1);
const int w = rnd(1, std::min(fb_w - x, 48));
const int h = rnd(1, std::min(fb_h - y, 40));
f.rect(x, y, w, h, RAW);
for (int yy = 0; yy < h; yy++) {
for (int xx = 0; xx < w; xx++) {
uint16_t p = pixel();
f.px(p, bpp);
g.model.set(x + xx, y + yy, p);
if (kind < 4) {
f.rect(x, y, w, h, RAW);
for (int yy = 0; yy < h; yy++) {
for (int xx = 0; xx < w; xx++) {
uint16_t p = pixel();
f.px(p, bpp);
g.model.set(x + xx, y + yy, p);
}
}
continue;
}
f.rect(x, y, w, h, HEXTILE);
bool bg_valid = false;
bool fg_valid = false;
for (int ty = y; ty < y + h; ty += 16) {
for (int tx = x; tx < x + w; tx += 16) {
const int tw = std::min(16, x + w - tx);
const int th = std::min(16, y + h - ty);
const int choice = rnd(0, 5);
if (choice == 0) {
f.u8(static_cast<uint8_t>(0x01 | (rnd(0, 15) << 1)));
for (int yy = 0; yy < th; yy++) {
for (int xx = 0; xx < tw; xx++) {
uint16_t p = pixel();
f.px(p, bpp);
g.model.set(tx + xx, ty + yy, p);
}
}
continue;
}
uint8_t flags = 0;
const bool set_bg = !bg_valid || choice == 1 || rnd(0, 3) == 0;
const bool subrects = choice >= 3;
const bool coloured = choice == 5;
const bool set_fg = subrects && !coloured && (!fg_valid || choice == 3);
const bool extra_fg = coloured && rnd(0, 1);
if (set_bg) {
flags |= 0x02;
bg = pixel();
bg_valid = true;
}
if (set_fg || extra_fg) {
flags |= 0x04;
fg = pixel();
fg_valid = true;
}
if (subrects) {
flags |= 0x08;
}
if (coloured) {
flags |= 0x10;
}
f.u8(flags);
if (flags & 0x02) {
f.px(bg, bpp);
}
if (flags & 0x04) {
f.px(fg, bpp);
}
std::vector<uint16_t> tile(static_cast<size_t>(tw) * th, bg);
if (subrects) {
const int count = rnd(0, 6);
f.u8(count);
for (int i = 0; i < count; i++) {
uint16_t colour = fg;
if (coloured) {
colour = pixel();
f.px(colour, bpp);
}
const int sx = rnd(0, 15);
const int sy = rnd(0, 15);
const int sw = rnd(1, 16);
const int sh = rnd(1, 16);
f.u8(static_cast<uint8_t>((sx << 4) | sy)).u8(static_cast<uint8_t>(((sw - 1) << 4) | (sh - 1)));
for (int yy = sy; yy < std::min(sy + sh, th); yy++) {
for (int xx = sx; xx < std::min(sx + sw, tw); xx++) {
tile[yy * tw + xx] = colour;
}
}
}
}
for (int yy = 0; yy < th; yy++) {
for (int xx = 0; xx < tw; xx++) {
g.model.set(tx + xx, ty + yy, tile[yy * tw + xx]);
}
}
}
}
}
+11 -4
View File
@@ -41,6 +41,7 @@ TEST(VncHandshake, Rfb38NonePipelined) {
struct PixelFormatCase {
display::DisplayType type;
VncPixelFormat format;
bool rgb565;
};
@@ -50,6 +51,7 @@ TEST_P(VncPixelFormatSelection, SetPixelFormatMatchesTheDisplay) {
const auto &param = GetParam();
FakeDisplay display(64, 48, param.type);
Session s(&display);
s.client.set_pixel_format(param.format);
s.attach();
s.deliver(handshake_38(64, 48));
Bytes tx = s.take();
@@ -59,10 +61,13 @@ TEST_P(VncPixelFormatSelection, SetPixelFormatMatchesTheDisplay) {
EXPECT_EQ(s.client.bpp(), param.rgb565 ? 2 : 1);
}
INSTANTIATE_TEST_SUITE_P(Formats, VncPixelFormatSelection,
::testing::Values(PixelFormatCase{display::DISPLAY_TYPE_COLOR, true},
PixelFormatCase{display::DISPLAY_TYPE_GRAYSCALE, true},
PixelFormatCase{display::DISPLAY_TYPE_BINARY, false}));
INSTANTIATE_TEST_SUITE_P(
Formats, VncPixelFormatSelection,
::testing::Values(PixelFormatCase{display::DISPLAY_TYPE_COLOR, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, true},
PixelFormatCase{display::DISPLAY_TYPE_GRAYSCALE, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, true},
PixelFormatCase{display::DISPLAY_TYPE_BINARY, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, false},
PixelFormatCase{display::DISPLAY_TYPE_COLOR, VncPixelFormat::VNC_PIXEL_FORMAT_RGB332, false},
PixelFormatCase{display::DISPLAY_TYPE_BINARY, VncPixelFormat::VNC_PIXEL_FORMAT_RGB565, true}));
TEST(VncHandshake, Rfb37NoneHasNoSecurityResult) {
FakeDisplay display(64, 48);
@@ -249,6 +254,8 @@ TEST(VncHandshake, DumpConfig) {
FakeDisplay display(64, 48);
Session s(&display);
s.client.dump_config();
s.client.set_pixel_format(VncPixelFormat::VNC_PIXEL_FORMAT_RGB332);
s.client.dump_config();
}
TEST(VncHandshake, HandshakeTimeout) {
+91 -4
View File
@@ -9,6 +9,7 @@ static Bytes frame(uint16_t x = 0, uint16_t pixel = 0x1234) {
}
static const Bytes INCREMENTAL = fbur(true, 64, 48);
static const Bytes FULL = fbur(false, 64, 48);
// The session starts at t=1000, when the first (full) request goes out.
class VncPacing : public ::testing::Test {
@@ -148,6 +149,40 @@ TEST_F(VncPacing, CursorOnlyFrameNeverRefreshes) {
EXPECT_EQ(d_.updates(), 0);
}
TEST_F(VncPacing, RefreshSendsAFullRequestEvenWithOneOutstanding) {
set_now(1100);
s().client.refresh();
s().loop();
EXPECT_EQ(hex(s().take()), hex(FULL));
s().run_for(1000);
EXPECT_EQ(s().take().size(), 0u);
}
TEST_F(VncPacing, RefreshKeepsThe50msFloor) {
set_now(1100);
s().deliver(frame());
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
s().client.refresh();
s().at(1149);
EXPECT_EQ(s().take().size(), 0u);
s().at(1150);
EXPECT_EQ(hex(s().take()), hex(FULL));
}
TEST_F(VncPacing, RefreshDuringAFrameWaitsForItsEnd) {
Bytes f = Rfb().fbu(1).raw_rect(0, 0, 4, 1, {1, 2, 3, 4}).bytes;
set_now(1100);
s().deliver(Bytes(f.begin(), f.end() - 2));
s().client.refresh();
s().run_for(500);
EXPECT_EQ(s().take().size(), 0u);
s().deliver(Bytes(f.end() - 2, f.end()));
// Only the full request: the incremental one it replaces is not sent as well.
EXPECT_EQ(hex(s().take()), hex(FULL));
s().run_for(500);
EXPECT_EQ(s().take().size(), 0u);
}
TEST_F(VncPacing, HeartbeatAfterAMinuteOfSilence) {
set_now(1100);
s().deliver(frame());
@@ -233,6 +268,22 @@ TEST_F(VncPacingParked, BusyDisplayHoldsDrawsAndRequests) {
EXPECT_EQ(d_.updates(), 2);
}
// The pass that starts a refresh already sees the panel busy, so it sends no request while the refresh runs.
TEST_F(VncPacingParked, NoRequestInThePassThatStartsARefresh) {
set_now(1100);
s().deliver(frame());
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
// vnc.refresh from a button handled earlier in the same main loop iteration.
s().client.refresh();
s().at(1300);
ASSERT_EQ(d_.updates(), 1);
ASSERT_TRUE(d_.busy());
EXPECT_EQ(s().take().size(), 0u) << "request sent while the panel refreshes";
d_.finish();
s().tick(10);
EXPECT_EQ(hex(s().take()), hex(FULL));
}
TEST(VncPacingParkedDelay, NoRequestInTheRefreshPassWithZeroDelay) {
FakeParkingDisplay d;
Session s(&d);
@@ -266,16 +317,16 @@ TEST_F(VncPacingParked, FailedDisplayEndsTheSessionAndStops) {
}
TEST_F(VncPacingParked, NoRequestWhileBusyBetweenFrames) {
// Inside the 50 ms floor, so the next request is still due when the panel turns busy.
set_now(1010);
set_now(1100);
s().deliver(Rfb().fbu(0));
EXPECT_EQ(s().take().size(), 0u);
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
d_.external_update();
s().client.refresh();
s().run_for(2000);
EXPECT_EQ(s().take().size(), 0u);
d_.finish();
s().tick(10);
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
EXPECT_EQ(hex(s().take()), hex(FULL));
}
TEST_F(VncPacingParked, ExternalUpdateMidFrameStopsReading) {
@@ -310,6 +361,9 @@ template<typename D> static void runs_frames_without_deadlock() {
EXPECT_EQ(d.updates(), i + 1) << "frame " << i;
EXPECT_EQ(d.pixel(i * 4, 0), expect_px(0x1000 + i, 2));
}
s.client.refresh();
s.tick(100);
EXPECT_EQ(hex(s.take()), hex(FULL));
}
TEST(VncPacingDisplays, PlainDisplay) { runs_frames_without_deadlock<FakeDisplay>(); }
@@ -368,6 +422,39 @@ TEST_F(VncPacingParked, DisplayBusyTooLongSetsAWarningUntilItParks) {
EXPECT_FALSE(s().client.status_has_warning());
}
// A refresh the driver gave up on (its warning is set when it parks) is retried once; a retry that fails
// too is not repeated until the driver finishes a refresh cleanly.
TEST_F(VncPacingParked, AbandonedRefreshIsRetriedOnce) {
set_now(1100);
s().deliver(frame());
s().at(1300);
ASSERT_EQ(d_.updates(), 1);
d_.status_set_warning(LOG_STR("Busy pin timeout"));
d_.finish();
s().tick(10);
EXPECT_TRUE(s().client.refresh_pending());
s().run_for(300);
EXPECT_EQ(d_.updates(), 2);
d_.finish();
s().run_for(3000);
EXPECT_EQ(d_.updates(), 2) << "a failed retry was retried again";
// A clean refresh arms the retry again
d_.status_clear_warning();
s().deliver(frame(16, 0x4321));
s().run_for(300);
ASSERT_EQ(d_.updates(), 3);
d_.finish();
s().tick(10);
d_.status_set_warning(LOG_STR("Busy pin timeout"));
s().deliver(frame(32, 0x5678));
s().run_for(300);
ASSERT_EQ(d_.updates(), 4);
d_.finish();
s().tick(10);
EXPECT_TRUE(s().client.refresh_pending());
}
TEST_F(VncPacingParked, DisplayParkingKeepsAConnectionWarning) {
d_.external_update();
s().run_for(120000, 1000);
@@ -0,0 +1,360 @@
#include "vnc_test_helpers.h"
#ifdef USE_HOST
namespace esphome::vnc::testing {
struct Tile {
uint16_t x;
uint16_t y;
uint16_t colour;
};
/// One update with a 16x16 hextile rect per tile, each a solid background.
static Bytes tiles(std::initializer_list<Tile> list) {
Rfb f;
f.fbu(static_cast<uint16_t>(list.size()));
for (const auto &t : list) {
f.rect(t.x, t.y, 16, 16, HEXTILE).u8(0x02).px(t.colour);
}
return f.bytes;
}
/// One update covering the whole w x h screen with solid tiles.
static Bytes screen(uint16_t w, uint16_t h, uint16_t seed) {
Rfb f;
f.fbu(1).rect(0, 0, w, h, HEXTILE);
const int count = ((w + 15) / 16) * ((h + 15) / 16);
for (int i = 0; i < count; i++) {
f.u8(0x02).px(static_cast<uint16_t>(seed + i * 0x0841));
}
return f.bytes;
}
static Bytes off_grid(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t colour) {
return Rfb().fbu(1).rect(x, y, w, h, HEXTILE).u8(0x02).px(colour).bytes;
}
static int count_draws(Session &s, const FakeDisplay &d, const Bytes &bytes) {
const int before = d.draw_calls();
s.deliver(bytes);
return d.draw_calls() - before;
}
class VncTileCache : public VncSessionTest {
protected:
void SetUp() override { this->start(this->d_, 64, 48); }
int draws(const Bytes &bytes) { return count_draws(s(), this->d_, bytes); }
void reconnect() {
s().close_peer();
s().loop();
ASSERT_FALSE(s().client.is_connected());
s().start(64, 48);
set_now(now_ms() + 100);
}
FakeDisplay d_;
};
TEST_F(VncTileCache, UnchangedTileIsNotDrawnAgain) {
const Bytes f = tiles({{16, 16, 0x1234}});
EXPECT_EQ(this->draws(f), 1);
s().run_for(300);
ASSERT_EQ(d_.updates(), 1);
s().take();
EXPECT_EQ(this->draws(f), 0);
EXPECT_FALSE(s().client.refresh_pending());
EXPECT_EQ(hex(s().take()), hex(fbur(true, 64, 48)));
s().run_for(2000);
EXPECT_EQ(d_.updates(), 1);
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x1234));
}
TEST_F(VncTileCache, OnlyChangedTilesAreDrawn) {
EXPECT_EQ(this->draws(tiles({{0, 0, 0x1111}, {16, 0, 0x2222}, {32, 0, 0x3333}})), 3);
s().run_for(300);
EXPECT_EQ(this->draws(tiles({{0, 0, 0x1111}, {16, 0, 0x4444}, {32, 0, 0x3333}})), 1);
EXPECT_TRUE(solid(d_, 16, 0, 16, 16, 0x4444));
s().run_for(300);
EXPECT_EQ(d_.updates(), 2);
}
TEST_F(VncTileCache, SkippedFrameDoesNotPostponeThePendingRefresh) {
const Bytes f = tiles({{0, 0, 0x1111}});
set_now(1200);
ASSERT_EQ(this->draws(f), 1);
set_now(1350);
EXPECT_EQ(this->draws(f), 0);
s().at(1399);
EXPECT_EQ(d_.updates(), 0);
s().at(1400);
EXPECT_EQ(d_.updates(), 1);
}
TEST_F(VncTileCache, SameBytesInAnotherShapeAreDrawn) {
const Bytes wide = Rfb().fbu(1).rect(0, 0, 16, 8, HEXTILE).u8(0x02).px(0x1234).bytes;
const Bytes tall = Rfb().fbu(1).rect(0, 0, 8, 16, HEXTILE).u8(0x02).px(0x1234).bytes;
EXPECT_EQ(this->draws(wide), 1);
EXPECT_EQ(this->draws(tall), 1);
EXPECT_TRUE(solid(d_, 0, 8, 8, 8, 0x1234));
EXPECT_EQ(this->draws(tall), 0);
}
TEST_F(VncTileCache, RawRectForgetsOnlyTheCellsItCovers) {
const Bytes f = tiles({{0, 16, 0x1111}, {16, 16, 0x2222}, {32, 32, 0x3333}});
ASSERT_EQ(this->draws(f), 3);
// Ends on the last column of the cell at 0,16.
s().deliver(Rfb().fbu(1).raw_rect(0, 20, 16, 1, std::vector<uint16_t>(16, 0xF800)));
ASSERT_EQ(d_.pixel(15, 20), expect_px(0xF800, 2));
EXPECT_EQ(this->draws(f), 1);
EXPECT_TRUE(solid(d_, 0, 16, 16, 16, 0x1111));
s().deliver(Rfb().fbu(1).raw_rect(20, 20, 1, 1, {0xF800}));
ASSERT_EQ(d_.pixel(20, 20), expect_px(0xF800, 2));
EXPECT_EQ(this->draws(f), 1);
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x2222));
}
TEST_F(VncTileCache, OffGridTileForgetsTheCellsItCovers) {
const Bytes f = tiles({{0, 0, 0x1111}, {16, 0, 0x2222}, {0, 16, 0x3333}, {16, 16, 0x4444}, {32, 0, 0x5555}});
ASSERT_EQ(this->draws(f), 5);
const Bytes across = off_grid(8, 8, 9, 9, 0xF800);
// Never skipped itself.
EXPECT_EQ(this->draws(across), 1);
EXPECT_EQ(this->draws(across), 1);
EXPECT_EQ(this->draws(f), 4);
EXPECT_TRUE(solid(d_, 0, 0, 16, 16, 0x1111));
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x4444));
// Ends on the last pixel of the first cell.
EXPECT_EQ(this->draws(off_grid(8, 8, 8, 8, 0xF800)), 1);
EXPECT_EQ(this->draws(f), 1);
EXPECT_TRUE(solid(d_, 0, 0, 16, 16, 0x1111));
}
TEST_F(VncTileCache, RefreshForgetsEveryTile) {
const Bytes f = tiles({{16, 16, 0x1234}});
ASSERT_EQ(this->draws(f), 1);
s().run_for(300);
ASSERT_EQ(d_.updates(), 1);
s().take();
s().client.refresh();
s().loop();
EXPECT_EQ(hex(s().take()), hex(fbur(false, 64, 48)));
EXPECT_EQ(this->draws(f), 1);
s().run_for(300);
EXPECT_EQ(d_.updates(), 2);
}
TEST_F(VncTileCache, RefreshedTilesStayCachedInTheNextSession) {
const Bytes full = screen(64, 48, 0x1000);
ASSERT_EQ(this->draws(full), 12);
s().run_for(300);
ASSERT_EQ(d_.updates(), 1);
this->reconnect();
EXPECT_EQ(this->draws(full), 0);
s().run_for(300);
EXPECT_EQ(d_.updates(), 1);
}
TEST_F(VncTileCache, DropBeforeTheRefreshRedrawsInTheNextSession) {
const Bytes full = screen(64, 48, 0x1000);
ASSERT_EQ(this->draws(full), 12);
s().tick(100);
ASSERT_TRUE(s().client.refresh_pending());
this->reconnect();
EXPECT_EQ(d_.updates(), 0);
EXPECT_EQ(this->draws(full), 12);
s().run_for(300);
EXPECT_EQ(d_.updates(), 1);
}
TEST_F(VncTileCache, DropMidFrameRedrawsInTheNextSession) {
ASSERT_EQ(this->draws(screen(64, 48, 0x1000)), 12);
s().run_for(300);
ASSERT_EQ(d_.updates(), 1);
const Bytes next = screen(64, 48, 0x2000);
// The update and rect headers, then six of the twelve tiles.
ASSERT_EQ(this->draws(Bytes(next.begin(), next.begin() + 4 + 12 + 6 * 3)), 6);
ASSERT_TRUE(s().client.in_update());
ASSERT_FALSE(s().client.refresh_pending());
this->reconnect();
EXPECT_EQ(this->draws(next), 12);
s().run_for(300);
EXPECT_EQ(d_.updates(), 2);
}
enum class SessionEnd { PEER_CLOSED, PROTOCOL_ERROR };
class VncTileCacheSessionEnd : public VncTileCache, public ::testing::WithParamInterface<SessionEnd> {};
TEST_P(VncTileCacheSessionEnd, PixelsNotYetShownAreDrawnAgain) {
const Bytes full = screen(64, 48, 0x1000);
ASSERT_EQ(this->draws(full), 12);
switch (GetParam()) {
case SessionEnd::PEER_CLOSED:
s().close_peer();
s().loop();
break;
case SessionEnd::PROTOCOL_ERROR:
s().deliver(Rfb().u8(7));
break;
}
ASSERT_FALSE(s().client.is_connected());
s().start(64, 48);
set_now(now_ms() + 100);
EXPECT_EQ(this->draws(full), 12);
s().run_for(300);
EXPECT_EQ(d_.updates(), 1);
}
INSTANTIATE_TEST_SUITE_P(Ends, VncTileCacheSessionEnd,
::testing::Values(SessionEnd::PEER_CLOSED, SessionEnd::PROTOCOL_ERROR));
// Bit 7 of every fourth byte only reached bit 31 of the old word-wise hash, so an even number of flips cancelled.
TEST(VncTileCacheHash, Rgb332TilesDifferingInEveryFourthByteAreBothDrawn) {
FakeDisplay d(64, 48, display::DISPLAY_TYPE_BINARY);
Session s(&d);
s.start(64, 48);
ASSERT_EQ(s.client.bpp(), 1);
set_now(now_ms() + 100);
const Bytes a(256, 0x00);
Bytes b = a;
const int flips[][2] = {{3, 0}, {7, 2}, {11, 4}, {15, 6}};
for (const auto &f : flips) {
b[f[1] * 16 + f[0]] ^= 0x80;
}
auto raw_tile = [](const Bytes &px) { return Rfb().fbu(1).rect(16, 16, 16, 16, HEXTILE).u8(0x01).raw(px).bytes; };
EXPECT_EQ(count_draws(s, d, raw_tile(a)), 1);
EXPECT_EQ(count_draws(s, d, raw_tile(b)), 1);
for (const auto &f : flips) {
EXPECT_EQ(d.pixel(16 + f[0], 16 + f[1]), expect_px(0x80, 1)) << f[0] << "," << f[1];
}
EXPECT_EQ(count_draws(s, d, raw_tile(a)), 1);
for (const auto &f : flips) {
EXPECT_EQ(d.pixel(16 + f[0], 16 + f[1]), expect_px(0x00, 1)) << f[0] << "," << f[1];
}
}
// --- The display size can change between sessions; the cache was sized for 64x48 (12 cells) at setup.
class VncTileCacheLayout : public VncSessionTest {
protected:
void SetUp() override {
this->session_ = std::make_unique<Session>(&this->d_);
ASSERT_EQ(c().hash_capacity(), 12u);
}
TestVncClient &c() { return s().client; }
int draws(const Bytes &bytes) { return count_draws(s(), this->d_, bytes); }
/// Ends any session, resizes the display and starts a session with a server of the same size.
void session(int w, int h) {
if (c().is_connected()) {
s().close_peer();
s().loop();
}
this->d_.resize(w, h);
s().start(w, h);
set_now(now_ms() + 100);
}
FakeDisplay d_{64, 48};
};
TEST_F(VncTileCacheLayout, EveryLayoutWithinTheCellsUsesTheCache) {
const int sizes[][2] = {{64, 48}, {48, 64}, {96, 32}, {32, 96}, {192, 16}, {16, 192}, {50, 40}};
for (const auto &size : sizes) {
const int w = size[0];
const int h = size[1];
this->session(w, h);
EXPECT_EQ(c().hash_cols(), (w + 15) / 16) << w << "x" << h;
EXPECT_EQ(c().hash_rows(), (h + 15) / 16) << w << "x" << h;
const Bytes full = screen(w, h, 0x1000);
EXPECT_EQ(this->draws(full), 12) << w << "x" << h;
EXPECT_EQ(this->draws(full), 0) << w << "x" << h;
s().run_for(300);
}
EXPECT_EQ(d_.out_of_bounds(), 0);
}
TEST_F(VncTileCacheLayout, RotationForgetsTheTiles) {
this->session(64, 48);
const Bytes tile = tiles({{16, 0, 0x1234}});
ASSERT_EQ(this->draws(tile), 1);
s().run_for(300);
this->session(48, 64);
// Same place, same pixels, but the panel turned underneath them.
EXPECT_EQ(this->draws(tile), 1);
EXPECT_EQ(this->draws(tile), 0);
}
TEST_F(VncTileCacheLayout, HalfTurnForgetsTheTiles) {
this->session(64, 48);
const Bytes full = screen(64, 48, 0x1000);
ASSERT_EQ(this->draws(full), 12);
s().run_for(300);
// A half turn keeps the cell grid but puts every tile somewhere else on the panel.
d_.set_rotation(display::DISPLAY_ROTATION_180_DEGREES);
this->session(64, 48);
EXPECT_EQ(this->draws(full), 12);
EXPECT_EQ(this->draws(full), 0);
}
TEST_F(VncTileCacheLayout, RotationDuringASessionReconnects) {
this->session(64, 48);
ASSERT_EQ(this->draws(screen(64, 48, 0x1000)), 12);
s().run_for(300);
d_.set_rotation(display::DISPLAY_ROTATION_180_DEGREES);
s().tick(10);
EXPECT_EQ(c().state(), VncState::VNC_STATE_IDLE);
EXPECT_FALSE(c().link().connected());
}
TEST_F(VncTileCacheLayout, TooManyCellsTurnsTheCacheOff) {
this->session(64, 48);
const Bytes tile = tiles({{0, 0, 0x1234}});
ASSERT_EQ(this->draws(tile), 1);
s().run_for(300);
this->session(80, 64);
EXPECT_EQ(c().hash_cols(), 0);
const Bytes full = screen(80, 64, 0x2000);
EXPECT_EQ(this->draws(full), 20);
EXPECT_EQ(this->draws(full), 20);
s().deliver(Rfb()
.fbu(2)
.raw_rect(70, 60, 10, 4, std::vector<uint16_t>(40, 0x1111))
.rect(72, 56, 8, 8, HEXTILE)
.u8(0x02)
.px(0x2222));
EXPECT_TRUE(c().is_connected());
s().run_for(300);
// The cache saw nothing of the last session, so it starts empty.
this->session(64, 48);
EXPECT_EQ(c().hash_cols(), 4);
EXPECT_EQ(this->draws(tile), 1);
EXPECT_EQ(this->draws(tile), 0);
EXPECT_EQ(d_.out_of_bounds(), 0);
}
TEST(VncTileCacheClip, ServerLargerThanTheDisplayUsesTheVisibleCells) {
FakeDisplay d(72, 48);
Session s(&d);
s.start(100, 80);
ASSERT_EQ(s.client.hash_capacity(), 15u);
EXPECT_EQ(s.client.hash_cols(), 5);
EXPECT_EQ(s.client.hash_rows(), 3);
set_now(now_ms() + 100);
// Six tiles: 48,32 is fully visible, 64,32 is cut at the right edge, the rest are off screen.
Rfb f;
f.fbu(1).rect(48, 32, 48, 32, HEXTILE);
for (int i = 0; i < 6; i++) {
f.u8(0x02).px(0x1000 + i);
}
EXPECT_EQ(count_draws(s, d, f.bytes), 2);
EXPECT_EQ(count_draws(s, d, f.bytes), 0);
EXPECT_TRUE(solid(d, 64, 32, 8, 16, 0x1001));
EXPECT_EQ(d.out_of_bounds(), 0);
}
} // namespace esphome::vnc::testing
#endif // USE_HOST
@@ -7,7 +7,8 @@ vnc:
interval:
- interval: 60s
then:
- vnc.refresh
- if:
condition: vnc.is_connected
then:
- logger.log: VNC is connected
- vnc.refresh
+39 -2
View File
@@ -119,6 +119,12 @@ class FakeDisplay : public display::Display {
int out_of_bounds() const { return this->out_of_bounds_; }
int draws_while_busy() const { return this->draws_while_busy_; }
void set_draw_delay_us(uint32_t us) { this->draw_delay_us_ = us; }
/// A new size for the next session, like a rotation changed at runtime. Clears the pixels.
void resize(int width, int height) {
this->width_ = width;
this->height_ = height;
this->pixels_.assign(static_cast<size_t>(width) * height, UNSET);
}
protected:
int width_;
@@ -259,6 +265,9 @@ class TestVncClient : public VncClient {
uint16_t vis_w() const { return this->vis_w_; }
uint16_t vis_h() const { return this->vis_h_; }
uint8_t bpp() const { return this->bpp_; }
uint16_t hash_cols() const { return this->hash_cols_; }
uint16_t hash_rows() const { return this->hash_rows_; }
uint32_t hash_capacity() const { return this->hash_capacity_; }
};
// ---------------------------------------------------------------------------
@@ -358,7 +367,7 @@ inline Bytes fbur(bool incremental, uint16_t w, uint16_t h) {
}
inline Bytes set_pixel_format_565() { return {0, 0, 0, 0, 16, 16, 0, 1, 0, 31, 0, 63, 0, 31, 11, 5, 0, 0, 0, 0}; }
inline Bytes set_pixel_format_332() { return {0, 0, 0, 0, 8, 8, 0, 1, 0, 7, 0, 7, 0, 3, 5, 2, 0, 0, 0, 0}; }
inline Bytes set_encodings() { return {2, 0, 0, 2, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x11}; }
inline Bytes set_encodings() { return {2, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x11}; }
inline Bytes version_reply(char minor) { return text(std::string("RFB 003.00") + minor + "\n"); }
/// Splits a stream of client messages that are all FramebufferUpdateRequests.
@@ -439,7 +448,8 @@ class Listener {
class Session {
public:
explicit Session(display::Display *display, uint32_t start_ms = 1000) : client(display) {
explicit Session(display::Display *display, uint32_t start_ms = 1000, bool big_endian = false)
: client(display, big_endian) {
signal(SIGPIPE, SIG_IGN);
set_now(start_ms);
this->client.set_host("127.0.0.1");
@@ -576,6 +586,33 @@ class Session {
size_t out_pos_{0};
};
inline constexpr int32_t HEXTILE = 5;
/// Whether the w x h block at x,y shows one RGB565 wire colour.
inline bool solid(const FakeDisplay &d, int x, int y, int w, int h, uint16_t colour) {
for (int row = y; row < y + h; row++) {
for (int col = x; col < x + w; col++) {
if (d.pixel(col, row) != expect_px(colour, 2)) {
return false;
}
}
}
return true;
}
class VncSessionTest : public ::testing::Test {
protected:
void TearDown() override { this->session_.reset(); }
/// Starts a session, then moves past the 50 ms request spacing so the next request goes out as soon as a frame ends.
void start(FakeDisplay &display, uint16_t fb_w, uint16_t fb_h) {
this->session_ = std::make_unique<Session>(&display);
this->session_->start(fb_w, fb_h);
set_now(now_ms() + 100);
}
Session &s() { return *this->session_; }
std::unique_ptr<Session> session_;
};
} // namespace esphome::vnc::testing
#endif // USE_HOST
@@ -12,6 +12,9 @@ api:
- snapshot.take:
id: disp
filename: !lambda return filename;
- action: vnc_refresh
then:
- vnc.refresh: remote
logger:
level: DEBUG
+130 -4
View File
@@ -5,7 +5,7 @@ handshake for RFB 3.3, 3.7 and 3.8, the same security lists and failure messages
connection that closes on any client message type it does not know.
The framebuffer is an RGB888 image the test draws into. Updates are translated into the pixel
format the client asked for and encoded as Raw or a RichCursor shape. Every client
format the client asked for and encoded as Raw, Hextile or a RichCursor shape. Every client
message is recorded, and the test decides when to answer: ``wait_for("fbur")`` then
``send_update(...)``.
"""
@@ -13,6 +13,7 @@ message is recorded, and the test decides when to answer: ``wait_for("fbur")`` t
from __future__ import annotations
import asyncio
from collections import Counter
from collections.abc import Callable, Iterable, Sequence
import contextlib
from dataclasses import asdict, dataclass
@@ -28,8 +29,15 @@ SECURITY_NONE = 1
SECURITY_TIGHT = 16
ENCODING_RAW = 0
ENCODING_HEXTILE = 5
ENCODING_RICH_CURSOR = -239
HEXTILE_RAW = 0x01
HEXTILE_BACKGROUND = 0x02
HEXTILE_FOREGROUND = 0x04
HEXTILE_ANY_SUBRECTS = 0x08
HEXTILE_SUBRECTS_COLOURED = 0x10
#: ``chunk`` value that writes in random 1-7 byte pieces.
HOSTILE_CHUNK = -1
@@ -38,12 +46,13 @@ MAX_CLIENT_CUT_TEXT = 1 << 20
RGB = tuple[int, int, int]
Encoding = Literal["raw", "rich-cursor"]
Encoding = Literal["raw", "hextile", "hextile-raw-tiles", "rich-cursor"]
Rect = tuple[int, int, int, int, Encoding]
Message = dict[str, Any]
_PIXEL_FORMAT = struct.Struct(">BBBBHHHBBB3x")
_VERSION_RE = re.compile(rb"RFB (\d{3})\.(\d{3})\n")
_TILE = 16
@dataclass(frozen=True)
@@ -312,17 +321,24 @@ class FakeRfbServer:
rects: Sequence[Rect],
*,
pixel_format: PixelFormat | None = None,
always_send_bg: bool = False,
raw_tile_flags: int = HEXTILE_RAW,
) -> bytes:
"""Encode one FramebufferUpdate from the image.
Each rect is ``(x, y, w, h, encoding)``. For ``rich-cursor`` x and y are the hotspot
and the shape is a fixed pattern.
and the shape is a fixed pattern. ``hextile-raw-tiles`` sends every tile raw.
Args:
rects: The rectangles, in order.
pixel_format: Format to encode in; defaults to the current client's.
always_send_bg: Send the Hextile background on every tile, even when unchanged.
raw_tile_flags: Subencoding byte of raw Hextile tiles. Bits other than Raw are
ignored by decoders, so 0x1F is valid.
"""
fmt = pixel_format or self.pixel_format
if not raw_tile_flags & HEXTILE_RAW:
raise ValueError("raw_tile_flags must include the Raw bit")
out = bytearray(struct.pack(">BxH", 0, len(rects)))
for x, y, w, h, encoding in rects:
if encoding == "rich-cursor":
@@ -334,6 +350,15 @@ class FakeRfbServer:
if encoding == "raw":
out += struct.pack(">HHHHi", x, y, w, h, ENCODING_RAW)
out += _pixels(fmt, (v for row in values for v in row))
elif encoding in ("hextile", "hextile-raw-tiles"):
out += struct.pack(">HHHHi", x, y, w, h, ENCODING_HEXTILE)
out += _hextile(
fmt,
values,
force_raw=encoding == "hextile-raw-tiles",
always_send_bg=always_send_bg,
raw_tile_flags=raw_tile_flags,
)
else:
raise ValueError(f"unknown encoding {encoding!r}")
return bytes(out)
@@ -343,6 +368,8 @@ class FakeRfbServer:
rects: Sequence[Rect],
chunk: int | None = None,
*,
always_send_bg: bool = False,
raw_tile_flags: int = HEXTILE_RAW,
delay: float = 0.0,
) -> None:
"""Send one FramebufferUpdate built by ``build_update()``.
@@ -351,10 +378,17 @@ class FakeRfbServer:
rects: The rectangles, in order.
chunk: Split the write into pieces of at most this many bytes; ``HOSTILE_CHUNK``
uses seeded random 1-7 byte pieces.
always_send_bg: See ``build_update()``.
raw_tile_flags: See ``build_update()``.
delay: Sleep between pieces.
"""
session = self._require_session()
data = self.build_update(rects, pixel_format=session.pixel_format)
data = self.build_update(
rects,
pixel_format=session.pixel_format,
always_send_bg=always_send_bg,
raw_tile_flags=raw_tile_flags,
)
await self._write(session, data, chunk, delay)
async def send_cut_text(
@@ -627,3 +661,95 @@ def _cursor_shape(fmt: PixelFormat, w: int, h: int) -> bytes:
light if (x + y) % 2 else dark for y in range(h) for x in range(w)
)
return pixels + b"\x5a" * (((w + 7) // 8) * h)
def _subrects(tile: list[list[int]], bg: int) -> list[tuple[int, int, int, int, int]]:
"""Cover every non-background pixel with same-coloured rects, greedily."""
height, width = len(tile), len(tile[0])
done = [[False] * width for _ in range(height)]
rects = []
for y in range(height):
for x in range(width):
colour = tile[y][x]
if colour == bg or done[y][x]:
continue
right = x
while (
right + 1 < width
and tile[y][right + 1] == colour
and not done[y][right + 1]
):
right += 1
bottom = y
while bottom + 1 < height and all(
tile[bottom + 1][i] == colour and not done[bottom + 1][i]
for i in range(x, right + 1)
):
bottom += 1
for line in range(y, bottom + 1):
done[line][x : right + 1] = [True] * (right + 1 - x)
rects.append((x, y, right + 1 - x, bottom + 1 - y, colour))
return rects
def _hextile(
fmt: PixelFormat,
values: list[list[int]],
*,
force_raw: bool,
always_send_bg: bool,
raw_tile_flags: int,
) -> bytes:
"""Hextile-encode a region given as rows of pixel values.
Background and foreground carry over between tiles, but never past a raw tile or a
coloured-subrect tile, as libvncserver's encoder does.
"""
out = bytearray()
height = len(values)
width = len(values[0]) if values else 0
bg: int | None = None
fg: int | None = None
for ty in range(0, height, _TILE):
for tx in range(0, width, _TILE):
tile = [row[tx : tx + _TILE] for row in values[ty : ty + _TILE]]
flat = [v for row in tile for v in row]
raw_size = len(flat) * fmt.bytes_per_pixel
if not force_raw:
counts = Counter(flat)
tile_bg = counts.most_common(1)[0][0]
flags = 0
body = bytearray()
if tile_bg != bg or always_send_bg:
flags |= HEXTILE_BACKGROUND
body += fmt.to_bytes(tile_bg)
if len(counts) == 1:
out.append(flags)
out += body
bg = tile_bg
continue
subrects = _subrects(tile, tile_bg)
coloured = len(counts) > 2
flags |= HEXTILE_ANY_SUBRECTS
if coloured:
flags |= HEXTILE_SUBRECTS_COLOURED
tile_fg = None
else:
tile_fg = next(c for c in counts if c != tile_bg)
if tile_fg != fg:
flags |= HEXTILE_FOREGROUND
body += fmt.to_bytes(tile_fg)
body.append(len(subrects))
for x, y, w, h, colour in subrects:
if coloured:
body += fmt.to_bytes(colour)
body += bytes([(x << 4) | y, ((w - 1) << 4) | (h - 1)])
if len(body) < raw_size:
out.append(flags)
out += body
bg, fg = tile_bg, tile_fg
continue
out.append(raw_tile_flags)
out += _pixels(fmt, flat)
bg = fg = None
return bytes(out)
+108 -22
View File
@@ -1,8 +1,9 @@
"""Integration test for the vnc component against a fake RFB server.
One compiled device runs every step in order against one server: the first frame, request
pacing, a reconnect after the server closes the link, a stream cut into tiny pieces, and an
unknown message type that makes the client drop the link.
pacing, a reconnect after the server closes the link, a stream cut into tiny pieces, an
unknown message type that makes the client drop the link, and tiles that are skipped when sent
again unchanged.
The client asks for the next frame only once the previous one is fully drawn, so snapshots are
taken after that request arrives. The snapshot display draws as soon as pixels arrive, so the
@@ -57,11 +58,12 @@ RECONNECT_S = 1.0
# Long enough for a frame that drew something to be logged.
LOG_SETTLE_S = 0.5
# Raw rects that cross the right and bottom edges of the display.
# Raw across the right edge, then two Hextile rects with edge tiles that cross the right and
# bottom edges of the display.
FULL_FRAME: list[Rect] = [
(0, 0, SERVER_WIDTH, 13, "raw"),
(0, 13, 37, SERVER_HEIGHT - 13, "raw"),
(37, 13, SERVER_WIDTH - 37, SERVER_HEIGHT - 13, "raw"),
(0, 13, 37, SERVER_HEIGHT - 13, "hextile"),
(37, 13, SERVER_WIDTH - 37, SERVER_HEIGHT - 13, "hextile"),
]
LogMatch = str | re.Pattern[str] | Callable[[str], bool]
@@ -124,9 +126,11 @@ class LogWatch:
def paint(server: FakeRfbServer, seed: int) -> None:
"""Draw solid bands, boxes and noise.
"""Draw a picture that gives every kind of Hextile tile.
Noise also runs along the right and bottom edges of the display, so clipping errors show.
Solid bands give background-only tiles, boxes in one accent colour give foreground
subrects, boxes in many colours give coloured subrects, and noise gives raw tiles. Noise
also runs along the right and bottom edges of the display, so clipping errors show.
"""
rng = random.Random(seed)
@@ -205,6 +209,12 @@ def full_request(incremental: bool) -> Message:
}
def combine_updates(*updates: bytes) -> bytes:
"""Merge FramebufferUpdates into one, so rects built with different options share it."""
count = sum(struct.unpack_from(">H", update, 2)[0] for update in updates)
return struct.pack(">BxH", 0, count) + b"".join(update[4:] for update in updates)
def cut_text(text: bytes) -> bytes:
return struct.pack(">B3xI", 3, len(text)) + text
@@ -272,6 +282,15 @@ async def expect_logged(log: LogWatch, frame: bytes, before: int) -> None:
)
def fill_noise(
server: FakeRfbServer, x: int, y: int, w: int, h: int, seed: int
) -> None:
rng = random.Random(seed)
for row in range(y, y + h):
for col in range(x, x + w):
server.set_pixel(col, row, (rng.randrange(256), rng.randrange(256), 0))
@pytest.mark.asyncio
async def test_vnc_client(
yaml_config: str,
@@ -303,7 +322,7 @@ async def test_vnc_client(
# The snapshot display declares no byte order, so the client asks for big-endian pixels
assert pixel_format == {"type": "set_pixel_format", **asdict(RGB565_BE)}
encodings = await server.wait_for("set_encodings")
assert encodings["encodings"] == [0, -239]
assert encodings["encodings"] == [5, 0, -239]
request, _ = await next_request(server)
assert request == full_request(incremental=False)
assert [m["type"] for m in server.messages[:3]] == [
@@ -331,7 +350,7 @@ async def test_vnc_client(
server.fill_rect(30, 10, 10, 10, (250, 10, 10))
server.fill_rect(58, 40, 22, 20, (10, 250, 10))
frame = server.build_update(
[(30, 10, 10, 10, "raw"), (58, 40, 22, 20, "raw")]
[(30, 10, 10, 10, "raw"), (58, 40, 22, 20, "hextile")]
)
elapsed, _ = await exchange(server, frame, log=log)
assert elapsed < QUICK_REQUEST_S, f"next request after {elapsed:.3f}s"
@@ -363,7 +382,7 @@ async def test_vnc_client(
DISPLAY_HEIGHT,
SERVER_WIDTH,
SERVER_HEIGHT - DISPLAY_HEIGHT,
"raw",
"hextile",
),
]
)
@@ -391,6 +410,19 @@ async def test_vnc_client(
)
assert_shows(await device.snapshot("hidden"), server)
# vnc.refresh asks for the whole screen at once, even with a request parked, and
# redraws it even where nothing changed.
whole = server.build_update(
[(0, 0, SERVER_WIDTH, SERVER_HEIGHT, "hextile")]
)
await exchange(server, whole)
await asyncio.sleep(LOG_SETTLE_S)
await device.run("vnc_refresh")
request, _ = await next_request(server)
assert request == full_request(incremental=False)
await exchange(server, whole, log=log)
assert_shows(await device.snapshot("refresh"), server)
# Reconnect: the server closes the link; the client comes back after the backoff
# and sets the session up again.
server.discard_pending()
@@ -406,24 +438,39 @@ async def test_vnc_client(
)
await log.wait_count(CONNECTED, 2)
assert await server.wait_for("set_pixel_format") == pixel_format
assert (await server.wait_for("set_encodings"))["encodings"] == [0, -239]
assert (await server.wait_for("set_encodings"))["encodings"] == [5, 0, -239]
request, _ = await next_request(server)
assert request == full_request(incremental=False)
assert server.sessions == 2
# Hostile stream: 1-7 byte pieces, a long cut text, bells, a colour map and
# cursor rects around a frame.
# cursor rects around a frame, with raw tiles flagged 0x1F.
paint(server, seed=2)
frame = server.build_update(
[
(3, 4, 12, 18, "rich-cursor"),
(0, 0, DISPLAY_WIDTH, 20, "raw"),
(DISPLAY_WIDTH, 0, SERVER_WIDTH - DISPLAY_WIDTH, 60, "raw"),
(0, 0, 0, 0, "rich-cursor"),
(0, 20, 30, SERVER_HEIGHT - 20, "raw"),
(30, 20, SERVER_WIDTH - 30, SERVER_HEIGHT - 20, "raw"),
(1, 1, 7, 9, "rich-cursor"),
]
frame = combine_updates(
server.build_update(
[
(3, 4, 12, 18, "rich-cursor"),
(0, 0, DISPLAY_WIDTH, 20, "raw"),
(DISPLAY_WIDTH, 0, SERVER_WIDTH - DISPLAY_WIDTH, 60, "raw"),
(0, 0, 0, 0, "rich-cursor"),
]
),
server.build_update(
[(0, 20, 30, SERVER_HEIGHT - 20, "hextile")], always_send_bg=True
),
server.build_update(
[
(
30,
20,
SERVER_WIDTH - 30,
SERVER_HEIGHT - 20,
"hextile-raw-tiles",
)
],
raw_tile_flags=0x1F,
),
server.build_update([(1, 1, 7, 9, "rich-cursor")]),
)
colour_map = struct.pack(">BxHH", 1, 0, 2) + bytes(12)
long_cut = bytes(
@@ -462,5 +509,44 @@ async def test_vnc_client(
await exchange(server, server.build_update(FULL_FRAME), log=log)
assert_shows(await device.snapshot("third"), server)
# A tile on the 16 pixel grid that is sent again unchanged is skipped, so the
# frame draws nothing.
aligned: Rect = (16, 16, 16, 16, "hextile")
fill_noise(server, *aligned[:4], seed=9)
await exchange(server, server.build_update([aligned]), log=log)
logged = log.count(FRAME_LOG)
await exchange(server, server.build_update([aligned]))
await asyncio.sleep(LOG_SETTLE_S)
assert log.count(FRAME_LOG) == logged, "an unchanged tile was drawn again"
assert_shows(await device.snapshot("tiles-skip"), server)
# It is drawn again once something else drew over part of it: an off-grid tile, or
# raw rows that start in another cell. A tile off the grid spans several cells and
# is never skipped, even when the overlap is in a cell other than its first.
failures = []
for i, (tile, overwrite) in enumerate(
[
(aligned, (10, 10, 16, 16, "hextile")),
(aligned, (8, 20, 12, 6, "raw")),
((5, 3, 16, 16, "hextile"), aligned),
]
):
fill_noise(server, *tile[:4], seed=10 + i)
await exchange(server, server.build_update([tile]))
server.fill_rect(*overwrite[:4], (255, 255, 0))
await exchange(server, server.build_update([overwrite]))
fill_noise(server, *tile[:4], seed=10 + i)
await exchange(server, server.build_update([tile]))
try:
assert_shows(await device.snapshot(f"tiles-{i}"), server)
except AssertionError as err:
failures.append(
f"tile {tile[:4]}, {overwrite[4]} {overwrite[:4]}: {err}"
)
assert not failures, (
"a skipped tile left pixels another rect had drawn over it: "
+ "; ".join(failures)
)
assert server.protocol_errors == []
assert log.count(LONG_OPERATION) == 0