mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4242bcb084 | ||
|
|
8a88e97497 | ||
|
|
d586e20a31 |
@@ -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():
|
||||
|
||||
@@ -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()")
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ¶m = 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user