mirror of
https://github.com/esphome/esphome.git
synced 2026-10-20 04:56:48 +00:00
[st7305] Add reflective LCD display driver (#21099)
This commit is contained in:
@@ -602,6 +602,7 @@ esphome/components/ssd1331_spi/* @kbx81
|
||||
esphome/components/ssd1351_base/* @kbx81
|
||||
esphome/components/ssd1351_spi/* @kbx81
|
||||
esphome/components/st7123/* @miniskipper
|
||||
esphome/components/st7305/* @clydebarrow
|
||||
esphome/components/st7567_base/* @latonita
|
||||
esphome/components/st7567_i2c/* @latonita
|
||||
esphome/components/st7567_spi/* @latonita
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DOMAIN = "st7305"
|
||||
@@ -0,0 +1,224 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.mipi import (
|
||||
DriverChip,
|
||||
flatten_sequence,
|
||||
map_sequence,
|
||||
model_schema_extractor,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import models
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
CONF_COLUMN_START = "column_start"
|
||||
CONF_COLUMN_END = "column_end"
|
||||
CONF_PORTRAIT = "portrait"
|
||||
CONF_ROW_START = "row_start"
|
||||
CONF_ROW_END = "row_end"
|
||||
|
||||
st7305_ns = cg.esphome_ns.namespace("st7305")
|
||||
ST7305 = st7305_ns.class_("ST7305", display.DisplayBuffer, spi.SPIDevice)
|
||||
|
||||
Orientation = st7305_ns.enum("Orientation")
|
||||
|
||||
# CUSTOM must be registered before the model modules are imported.
|
||||
DriverChip("CUSTOM")
|
||||
for module_info in pkgutil.iter_modules(models.__path__):
|
||||
importlib.import_module(f".models.{module_info.name}", package=__package__)
|
||||
MODELS = DriverChip.get_models()
|
||||
|
||||
DEFAULT_MODEL = "WAVESHARE-400X300"
|
||||
|
||||
|
||||
def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
if model.initsequence is None
|
||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||
)
|
||||
return (
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
default_mode="MODE0",
|
||||
)
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ST7305),
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_WIDTH): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_HEIGHT): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_PORTRAIT): cv.boolean,
|
||||
cv.Optional(CONF_COLUMN_START): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_COLUMN_END): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_ROW_START): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_ROW_END): cv.int_range(min=0, max=255),
|
||||
iseqconf: cv.ensure_list(map_sequence),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _resolve(config: ConfigType, model: DriverChip) -> None:
|
||||
"""Fill in model defaults and validate the geometry and address window."""
|
||||
if (width := config.get(CONF_WIDTH)) is None:
|
||||
if (width := model.get_default(CONF_WIDTH, None)) is None:
|
||||
raise cv.Invalid("'width' is required for this model", path=[CONF_WIDTH])
|
||||
config[CONF_WIDTH] = width
|
||||
if (height := config.get(CONF_HEIGHT)) is None:
|
||||
if (height := model.get_default(CONF_HEIGHT, None)) is None:
|
||||
raise cv.Invalid("'height' is required for this model", path=[CONF_HEIGHT])
|
||||
config[CONF_HEIGHT] = height
|
||||
|
||||
if config.get(CONF_PORTRAIT) is None:
|
||||
if (default := model.get_default(CONF_PORTRAIT, None)) is None:
|
||||
raise cv.Invalid(
|
||||
"'portrait' is required for this model", path=[CONF_PORTRAIT]
|
||||
)
|
||||
config[CONF_PORTRAIT] = default
|
||||
|
||||
if config[CONF_PORTRAIT]:
|
||||
if width % 12 or height % 2:
|
||||
raise cv.Invalid(
|
||||
"portrait panels require a width divisible by 12 and a height divisible by 2"
|
||||
)
|
||||
columns = width // 12
|
||||
rows = height // 2
|
||||
else:
|
||||
if width % 2 or height % 12:
|
||||
raise cv.Invalid(
|
||||
"landscape panels require a width divisible by 2 and a height divisible by 12"
|
||||
)
|
||||
columns = height // 12
|
||||
rows = width // 2
|
||||
|
||||
column_start = config.get(
|
||||
CONF_COLUMN_START, model.get_default(CONF_COLUMN_START, 0)
|
||||
)
|
||||
row_start = config.get(CONF_ROW_START, model.get_default(CONF_ROW_START, 0))
|
||||
column_end = config.get(
|
||||
CONF_COLUMN_END, model.get_default(CONF_COLUMN_END, column_start + columns - 1)
|
||||
)
|
||||
row_end = config.get(
|
||||
CONF_ROW_END, model.get_default(CONF_ROW_END, row_start + rows - 1)
|
||||
)
|
||||
if column_end < column_start or row_end < row_start:
|
||||
raise cv.Invalid("address window end must not be before its start")
|
||||
if not (
|
||||
0 <= column_start <= 255
|
||||
and 0 <= column_end <= 255
|
||||
and 0 <= row_start <= 255
|
||||
and 0 <= row_end <= 255
|
||||
):
|
||||
raise cv.Invalid("address window values must be between 0 and 255")
|
||||
if (column_end - column_start + 1) * (row_end - row_start + 1) * 3 < (
|
||||
width * height // 8
|
||||
):
|
||||
raise cv.Invalid("address window is too small for the display dimensions")
|
||||
config[CONF_COLUMN_START] = column_start
|
||||
config[CONF_COLUMN_END] = column_end
|
||||
config[CONF_ROW_START] = row_start
|
||||
config[CONF_ROW_END] = row_end
|
||||
|
||||
if config.get(CONF_INIT_SEQUENCE) is None:
|
||||
config[CONF_INIT_SEQUENCE] = model.initsequence
|
||||
|
||||
for key, label in ((CONF_CS_PIN, "cs_pin"), (CONF_DC_PIN, "dc_pin")):
|
||||
if key not in config:
|
||||
raise cv.Invalid(f"'{label}' is required for this model", path=[key])
|
||||
|
||||
|
||||
def model_schema(config: ConfigType) -> cv.Schema:
|
||||
"""Return the schema for the model selected in the config."""
|
||||
return _model_schema(MODELS[config[CONF_MODEL]])
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
def customise_schema(config: ConfigType) -> ConfigType:
|
||||
config = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.one_of(
|
||||
*MODELS, upper=True
|
||||
)
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
config = _model_schema(model)(config)
|
||||
_resolve(config, model)
|
||||
|
||||
display.add_metadata(
|
||||
config[CONF_ID],
|
||||
config[CONF_WIDTH],
|
||||
config[CONF_HEIGHT],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=cv.UNDEFINED,
|
||||
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = customise_schema
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
"st7305", require_miso=False, require_mosi=True
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
sequence = flatten_sequence(config[CONF_INIT_SEQUENCE])
|
||||
table = cg.shared_progmem_array("st7305_init_sequence", cg.uint8, sequence)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_MODEL],
|
||||
config[CONF_WIDTH],
|
||||
config[CONF_HEIGHT],
|
||||
Orientation.ORIENTATION_PORTRAIT
|
||||
if config[CONF_PORTRAIT]
|
||||
else Orientation.ORIENTATION_LANDSCAPE,
|
||||
config[CONF_COLUMN_START],
|
||||
config[CONF_COLUMN_END],
|
||||
config[CONF_ROW_START],
|
||||
config[CONF_ROW_END],
|
||||
table,
|
||||
len(sequence),
|
||||
)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await cg.add_pin(var.set_dc_pin, config[CONF_DC_PIN])
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
await display.add_writer(var, config)
|
||||
@@ -0,0 +1,60 @@
|
||||
from esphome.components.mipi import DriverChip, delay
|
||||
|
||||
# Waveshare ESP32-S3-RLCD-4.2, 4.2" reflective panel. The same controller command set is
|
||||
# used by the 300x400 ST7306 panels. Values follow the vendor demo code, which is the sequence
|
||||
# proven on this panel; the datasheet's referential sequence is close but not identical.
|
||||
DriverChip(
|
||||
"WAVESHARE-400X300",
|
||||
width=400,
|
||||
height=300,
|
||||
portrait=False,
|
||||
cs_pin=40,
|
||||
dc_pin=5,
|
||||
reset_pin=41,
|
||||
column_start=0x12,
|
||||
column_end=0x2A,
|
||||
row_start=0x00,
|
||||
row_end=0xC7,
|
||||
initsequence=(
|
||||
(0xD6, 0x17, 0x02), # NVM load control
|
||||
(0xD1, 0x01), # booster enable
|
||||
(0xC0, 0x11, 0x04), # gate voltage
|
||||
(0xC1, 0x69, 0x69, 0x69, 0x69), # VSHP
|
||||
(0xC2, 0x19, 0x19, 0x19, 0x19), # VSLP
|
||||
(0xC4, 0x4B, 0x4B, 0x4B, 0x4B), # VSHN
|
||||
(0xC5, 0x19, 0x19, 0x19, 0x19), # VSLN
|
||||
(0xD8, 0x80, 0xE9), # oscillator
|
||||
(0xB2, 0x02), # frame rate
|
||||
(
|
||||
0xB3,
|
||||
0xE5,
|
||||
0xF6,
|
||||
0x05,
|
||||
0x46,
|
||||
0x77,
|
||||
0x77,
|
||||
0x77,
|
||||
0x77,
|
||||
0x76,
|
||||
0x45,
|
||||
), # HPM gate EQ
|
||||
(0xB4, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45), # LPM gate EQ
|
||||
(0x62, 0x32, 0x03, 0x1F), # gate timing
|
||||
(0xB7, 0x13), # source EQ
|
||||
(0xB0, 0x64), # gate line setting, 400 lines
|
||||
(0x11,), # sleep out
|
||||
delay(200),
|
||||
(0xC9, 0x00), # source voltage select
|
||||
(0x36, 0x48), # MADCTL: MX=1, DO=1
|
||||
(0x3A, 0x11), # 1-bit data format, 3 bytes per 24-pixel block
|
||||
(0xB9, 0x20), # gamma, mono
|
||||
(0xB8, 0x29), # panel setting
|
||||
(0x21,), # display inversion on
|
||||
(0x2A, 0x12, 0x2A), # column address window
|
||||
(0x2B, 0x00, 0xC7), # row address window
|
||||
(0x35, 0x00), # tearing effect line on
|
||||
(0xD0, 0xFF), # auto power down
|
||||
(0x38,), # high power mode
|
||||
(0x29,), # display on
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "st7305.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::st7305 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "st7305");
|
||||
|
||||
void ST7305::setup() {
|
||||
this->spi_setup();
|
||||
this->dc_pin_->setup();
|
||||
if (this->reset_pin_ != nullptr)
|
||||
this->reset_pin_->setup();
|
||||
|
||||
if (!this->init_buffer_())
|
||||
return;
|
||||
|
||||
this->reset_();
|
||||
}
|
||||
|
||||
bool ST7305::init_buffer_() {
|
||||
this->init_internal_(this->buffer_length_);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||
return false;
|
||||
}
|
||||
memset(this->buffer_, 0x00, this->buffer_length_);
|
||||
this->clear_dirty_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ST7305::reset_() {
|
||||
static constexpr uint32_t RESET_HIGH_MS = 50;
|
||||
static constexpr uint32_t RESET_LOW_MS = 20;
|
||||
static constexpr uint32_t RESET_SETTLE_MS = 50;
|
||||
static constexpr uint32_t SOFT_RESET_MS = 5;
|
||||
|
||||
if (this->reset_pin_ == nullptr) {
|
||||
this->enable();
|
||||
this->send_command_(0x01); // software reset
|
||||
this->disable();
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, SOFT_RESET_MS, [this]() { this->send_init_sequence_(); });
|
||||
return;
|
||||
}
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, RESET_HIGH_MS, [this]() {
|
||||
this->reset_pin_->digital_write(false);
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, RESET_LOW_MS, [this]() {
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, RESET_SETTLE_MS, [this]() { this->send_init_sequence_(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ST7305::send_init_sequence_() {
|
||||
while (this->init_index_ < this->init_sequence_length_) {
|
||||
if (this->init_sequence_length_ - this->init_index_ < 2) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t command = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
const uint8_t count = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
if (count == 0xFF) {
|
||||
// Resume the sequence after the delay instead of blocking the main loop.
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, command, [this]() { this->send_init_sequence_(); });
|
||||
return;
|
||||
}
|
||||
if (this->init_sequence_length_ - this->init_index_ < count) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
this->enable();
|
||||
this->send_command_(command);
|
||||
if (count != 0) {
|
||||
this->dc_pin_->digital_write(true);
|
||||
for (uint8_t i = 0; i < count; i++)
|
||||
this->write_byte(progmem_read_byte(&this->init_sequence_[this->init_index_ + i]));
|
||||
}
|
||||
this->disable();
|
||||
this->init_index_ += count;
|
||||
}
|
||||
this->finish_init_();
|
||||
}
|
||||
|
||||
void ST7305::finish_init_() {
|
||||
this->write_full_frame_();
|
||||
this->clear_dirty_();
|
||||
this->ready_ = true;
|
||||
}
|
||||
|
||||
void ST7305::write_full_frame_() {
|
||||
const uint8_t caset[] = {this->column_start_, this->column_end_};
|
||||
const uint8_t raset[] = {this->row_start_, this->row_end_};
|
||||
this->enable();
|
||||
this->send_command_(0x2A);
|
||||
this->send_data_(caset, sizeof(caset));
|
||||
this->send_command_(0x2B);
|
||||
this->send_data_(raset, sizeof(raset));
|
||||
this->send_command_(0x2C);
|
||||
this->send_data_(this->buffer_, this->buffer_length_);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
ST7305::Window ST7305::compute_window_() const {
|
||||
Window window{};
|
||||
if (this->orientation_ == Orientation::ORIENTATION_LANDSCAPE) {
|
||||
// RASET rows are x pairs; CASET columns are 12-pixel bands of y blocks.
|
||||
window.stride = static_cast<uint16_t>(this->height_ / 4);
|
||||
window.outer_start = static_cast<uint16_t>(this->x_low_ >> 1);
|
||||
window.outer_end = static_cast<uint16_t>(this->x_high_ >> 1);
|
||||
const int inv_min = this->height_ - 1 - this->y_high_;
|
||||
const int inv_max = this->height_ - 1 - this->y_low_;
|
||||
window.block_start = static_cast<uint16_t>((inv_min >> 2) / 3 * 3);
|
||||
window.block_end =
|
||||
static_cast<uint16_t>(std::min(((inv_max >> 2) / 3) * 3 + 2, static_cast<int>(window.stride) - 1));
|
||||
} else {
|
||||
// RASET rows are y pairs; CASET columns are 12-pixel bands of x blocks.
|
||||
window.stride = static_cast<uint16_t>(this->width_ / 4);
|
||||
window.outer_start = static_cast<uint16_t>(this->y_low_ >> 1);
|
||||
window.outer_end = static_cast<uint16_t>(this->y_high_ >> 1);
|
||||
const int block_min = this->x_low_ >> 2;
|
||||
const int block_max = this->x_high_ >> 2;
|
||||
window.block_start = static_cast<uint16_t>(block_min / 3 * 3);
|
||||
window.block_end = static_cast<uint16_t>(std::min((block_max / 3) * 3 + 2, static_cast<int>(window.stride) - 1));
|
||||
}
|
||||
window.caset_start = static_cast<uint8_t>(this->column_start_ + window.block_start / 3);
|
||||
window.caset_end = static_cast<uint8_t>(this->column_start_ + window.block_end / 3);
|
||||
return window;
|
||||
}
|
||||
|
||||
void ST7305::write_window_(const Window &window) {
|
||||
const uint8_t caset[] = {window.caset_start, window.caset_end};
|
||||
const uint8_t raset[] = {static_cast<uint8_t>(this->row_start_ + window.outer_start),
|
||||
static_cast<uint8_t>(this->row_start_ + window.outer_end)};
|
||||
this->enable();
|
||||
this->send_command_(0x2A);
|
||||
this->send_data_(caset, sizeof(caset));
|
||||
this->send_command_(0x2B);
|
||||
this->send_data_(raset, sizeof(raset));
|
||||
this->send_command_(0x2C);
|
||||
if (window.block_start == 0 && window.block_end == window.stride - 1) {
|
||||
// Full-height window: every outer unit's data is contiguous with the next.
|
||||
this->send_data_(this->buffer_ + window.outer_start * window.stride,
|
||||
static_cast<size_t>(window.outer_end - window.outer_start + 1) * window.stride);
|
||||
} else {
|
||||
const size_t length = window.block_end - window.block_start + 1;
|
||||
for (uint16_t outer = window.outer_start; outer <= window.outer_end; outer++)
|
||||
this->send_data_(this->buffer_ + outer * window.stride + window.block_start, length);
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void ST7305::update() {
|
||||
if (!this->ready_)
|
||||
return;
|
||||
|
||||
this->do_update_();
|
||||
if (!this->is_dirty_())
|
||||
return;
|
||||
|
||||
const Window window = this->compute_window_();
|
||||
if (window.data_length() > this->buffer_length_ / 2) {
|
||||
this->write_full_frame_();
|
||||
} else {
|
||||
this->write_window_(window);
|
||||
}
|
||||
this->clear_dirty_();
|
||||
}
|
||||
|
||||
void HOT ST7305::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x < 0 || y < 0 || x >= this->width_ || y >= this->height_)
|
||||
return;
|
||||
size_t index;
|
||||
uint8_t mask;
|
||||
if (this->orientation_ == Orientation::ORIENTATION_LANDSCAPE) {
|
||||
const int inv_y = this->height_ - 1 - y;
|
||||
index = static_cast<size_t>(x >> 1) * (this->height_ / 4) + (inv_y >> 2);
|
||||
mask = static_cast<uint8_t>(1 << (7 - (((inv_y & 3) << 1) | (x & 1))));
|
||||
} else {
|
||||
index = static_cast<size_t>(y >> 1) * (this->width_ / 4) + (x >> 2);
|
||||
mask = static_cast<uint8_t>(1 << (7 - (((x & 3) << 1) | (y & 1))));
|
||||
}
|
||||
// The init sequence enables INVON, so a set bit is white and a clear bit is black.
|
||||
uint8_t &value = this->buffer_[index];
|
||||
const uint8_t previous = value;
|
||||
if (color.is_on()) {
|
||||
value |= mask;
|
||||
} else {
|
||||
value &= ~mask;
|
||||
}
|
||||
if (value == previous)
|
||||
return;
|
||||
this->extend_dirty_(x, y, x, y);
|
||||
}
|
||||
|
||||
void ST7305::fill(Color color) {
|
||||
if (this->get_clipping().is_set()) {
|
||||
display::Display::fill(color);
|
||||
return;
|
||||
}
|
||||
if (this->buffer_ == nullptr)
|
||||
return;
|
||||
|
||||
const uint8_t value = color.is_on() ? 0xFF : 0x00;
|
||||
size_t first = this->buffer_length_;
|
||||
size_t last = 0;
|
||||
for (size_t index = 0; index < this->buffer_length_; index++) {
|
||||
if (this->buffer_[index] == value)
|
||||
continue;
|
||||
this->buffer_[index] = value;
|
||||
if (first == this->buffer_length_)
|
||||
first = index;
|
||||
last = index;
|
||||
}
|
||||
if (first == this->buffer_length_)
|
||||
return;
|
||||
|
||||
// Widen the changed index range to whole outer units, and the other axis to the full
|
||||
// display. The extra pixels are current in the buffer, so sending them is harmless.
|
||||
if (this->orientation_ == Orientation::ORIENTATION_LANDSCAPE) {
|
||||
const int stride = this->height_ / 4;
|
||||
this->extend_dirty_(static_cast<int>(first / stride) * 2, 0, static_cast<int>(last / stride) * 2 + 1,
|
||||
this->height_ - 1);
|
||||
} else {
|
||||
const int stride = this->width_ / 4;
|
||||
this->extend_dirty_(0, static_cast<int>(first / stride) * 2, this->width_ - 1,
|
||||
static_cast<int>(last / stride) * 2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void ST7305::extend_dirty_(int x0, int y0, int x1, int y1) {
|
||||
if (x0 < this->x_low_)
|
||||
this->x_low_ = x0;
|
||||
if (x1 > this->x_high_)
|
||||
this->x_high_ = x1;
|
||||
if (y0 < this->y_low_)
|
||||
this->y_low_ = y0;
|
||||
if (y1 > this->y_high_)
|
||||
this->y_high_ = y1;
|
||||
}
|
||||
|
||||
void ST7305::dump_config() {
|
||||
LOG_DISPLAY("", "ST7305", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", this->model_);
|
||||
ESP_LOGCONFIG(TAG, " Orientation: %s",
|
||||
this->orientation_ == Orientation::ORIENTATION_LANDSCAPE ? LOG_STR_LITERAL("landscape")
|
||||
: LOG_STR_LITERAL("portrait"));
|
||||
ESP_LOGCONFIG(TAG, " Window: columns %u..%u, rows %u..%u", this->column_start_, this->column_end_, this->row_start_,
|
||||
this->row_end_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::st7305
|
||||
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::st7305 {
|
||||
|
||||
enum Orientation : uint8_t {
|
||||
ORIENTATION_LANDSCAPE = 0,
|
||||
ORIENTATION_PORTRAIT = 1,
|
||||
};
|
||||
|
||||
/// Sitronix ST7305/ST7306 reflective monochrome LCD, 1 bit per pixel.
|
||||
///
|
||||
/// The controller RAM is addressed in 12-column x 2-row blocks (3 bytes per address). With a
|
||||
/// landscape panel one byte covers 2 pixels in x and 4 in y; with a portrait panel it covers
|
||||
/// 4 pixels in x and 2 in y.
|
||||
class ST7305 : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_10MHZ> {
|
||||
public:
|
||||
ST7305(const char *model, int width, int height, Orientation orientation, uint8_t column_start, uint8_t column_end,
|
||||
uint8_t row_start, uint8_t row_end, const uint8_t *init_sequence, size_t init_sequence_length)
|
||||
: model_(model),
|
||||
width_(width),
|
||||
height_(height),
|
||||
orientation_(orientation),
|
||||
column_start_(column_start),
|
||||
column_end_(column_end),
|
||||
row_start_(row_start),
|
||||
row_end_(row_end),
|
||||
buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8),
|
||||
init_sequence_(init_sequence),
|
||||
init_sequence_length_(init_sequence_length) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
|
||||
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
void fill(Color color) override;
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; }
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
protected:
|
||||
/// An addressable write window. RASET addresses are the outer index (x pairs in landscape,
|
||||
/// y pairs in portrait); CASET addresses are 12-pixel bands of the inner index.
|
||||
struct Window {
|
||||
uint16_t outer_start;
|
||||
uint16_t outer_end;
|
||||
uint16_t block_start;
|
||||
uint16_t block_end;
|
||||
uint16_t stride;
|
||||
uint8_t caset_start;
|
||||
uint8_t caset_end;
|
||||
|
||||
size_t data_length() const {
|
||||
return static_cast<size_t>(this->outer_end - this->outer_start + 1) *
|
||||
static_cast<size_t>(this->block_end - this->block_start + 1);
|
||||
}
|
||||
};
|
||||
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
bool init_buffer_();
|
||||
void reset_();
|
||||
void send_init_sequence_();
|
||||
void finish_init_();
|
||||
void write_full_frame_();
|
||||
void write_window_(const Window &window);
|
||||
Window compute_window_() const;
|
||||
|
||||
/// Scheduler id for the non-blocking reset and init sequence. The steps are sequential,
|
||||
/// so one id is enough; a new timeout replaces any pending one.
|
||||
static constexpr uint32_t STARTUP_TIMEOUT_ID = 0;
|
||||
|
||||
void send_command_(uint8_t command) {
|
||||
this->dc_pin_->digital_write(false);
|
||||
this->write_byte(command);
|
||||
}
|
||||
void send_data_(const uint8_t *data, size_t length) {
|
||||
this->dc_pin_->digital_write(true);
|
||||
this->write_array(data, length);
|
||||
}
|
||||
|
||||
bool is_dirty_() const { return this->x_high_ >= this->x_low_ && this->y_high_ >= this->y_low_; }
|
||||
void clear_dirty_() {
|
||||
this->x_low_ = this->width_;
|
||||
this->x_high_ = -1;
|
||||
this->y_low_ = this->height_;
|
||||
this->y_high_ = -1;
|
||||
}
|
||||
void extend_dirty_(int x0, int y0, int x1, int y1);
|
||||
|
||||
GPIOPin *dc_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
|
||||
const char *model_;
|
||||
int width_;
|
||||
int height_;
|
||||
Orientation orientation_;
|
||||
uint8_t column_start_;
|
||||
uint8_t column_end_;
|
||||
uint8_t row_start_;
|
||||
uint8_t row_end_;
|
||||
size_t buffer_length_;
|
||||
const uint8_t *init_sequence_;
|
||||
size_t init_sequence_length_;
|
||||
size_t init_index_{0};
|
||||
bool ready_{false};
|
||||
|
||||
int x_low_{0};
|
||||
int x_high_{-1};
|
||||
int y_low_{0};
|
||||
int y_high_{-1};
|
||||
};
|
||||
|
||||
} // namespace esphome::st7305
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Tests for the st7305 configuration validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.st7305.display import (
|
||||
CONF_COLUMN_END,
|
||||
CONF_COLUMN_START,
|
||||
CONF_PORTRAIT,
|
||||
CONF_ROW_END,
|
||||
MODELS,
|
||||
_resolve,
|
||||
)
|
||||
from esphome.const import CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_WIDTH
|
||||
|
||||
|
||||
def _config(**overrides):
|
||||
config = {
|
||||
CONF_WIDTH: 400,
|
||||
CONF_HEIGHT: 300,
|
||||
CONF_PORTRAIT: False,
|
||||
CONF_CS_PIN: 1,
|
||||
CONF_DC_PIN: 2,
|
||||
}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def test_landscape_derives_window() -> None:
|
||||
config = _config()
|
||||
_resolve(config, MODELS["CUSTOM"])
|
||||
assert config[CONF_COLUMN_START] == 0
|
||||
assert config[CONF_COLUMN_END] == 24
|
||||
assert config[CONF_ROW_END] == 199
|
||||
|
||||
|
||||
@pytest.mark.parametrize("height", [160, 200, 296, 308])
|
||||
def test_landscape_rejects_inner_not_multiple_of_12(height: int) -> None:
|
||||
with pytest.raises(cv.Invalid, match="height divisible by 12"):
|
||||
_resolve(_config(**{CONF_HEIGHT: height}), MODELS["CUSTOM"])
|
||||
|
||||
|
||||
def test_portrait_derives_window() -> None:
|
||||
config = _config(
|
||||
**{
|
||||
CONF_WIDTH: 168,
|
||||
CONF_HEIGHT: 384,
|
||||
CONF_PORTRAIT: True,
|
||||
CONF_COLUMN_START: 0x17,
|
||||
}
|
||||
)
|
||||
_resolve(config, MODELS["CUSTOM"])
|
||||
assert config[CONF_COLUMN_END] == 0x24
|
||||
assert config[CONF_ROW_END] == 191
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", [160, 200, 208])
|
||||
def test_portrait_rejects_inner_not_multiple_of_12(width: int) -> None:
|
||||
with pytest.raises(cv.Invalid, match="width divisible by 12"):
|
||||
_resolve(
|
||||
_config(
|
||||
**{
|
||||
CONF_WIDTH: width,
|
||||
CONF_HEIGHT: 384,
|
||||
CONF_PORTRAIT: True,
|
||||
}
|
||||
),
|
||||
MODELS["CUSTOM"],
|
||||
)
|
||||
|
||||
|
||||
def test_window_ends_must_fit_in_a_byte() -> None:
|
||||
with pytest.raises(cv.Invalid, match="between 0 and 255"):
|
||||
_resolve(_config(**{CONF_WIDTH: 1024, CONF_HEIGHT: 300}), MODELS["CUSTOM"])
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/st7305/st7305.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::st7305::testing {
|
||||
|
||||
class TestableST7305 : public ST7305 {
|
||||
public:
|
||||
TestableST7305(int width = 400, int height = 300, Orientation orientation = Orientation::ORIENTATION_LANDSCAPE,
|
||||
uint8_t column_start = 0x12, uint8_t column_end = 0x2A, uint8_t row_start = 0x00,
|
||||
uint8_t row_end = 0xC7)
|
||||
: ST7305("TEST", width, height, orientation, column_start, column_end, row_start, row_end, nullptr, 0) {}
|
||||
|
||||
bool init_buffer() { return this->init_buffer_(); }
|
||||
void clear_dirty() { this->clear_dirty_(); }
|
||||
bool is_dirty() const { return this->is_dirty_(); }
|
||||
int x_low() const { return this->x_low_; }
|
||||
int x_high() const { return this->x_high_; }
|
||||
int y_low() const { return this->y_low_; }
|
||||
int y_high() const { return this->y_high_; }
|
||||
Window compute_window() const { return this->compute_window_(); }
|
||||
void write_window() { this->write_window_(this->compute_window_()); }
|
||||
uint8_t *buffer() { return this->buffer_; }
|
||||
void attach_spi(spi::SPIDelegate *delegate, GPIOPin *dc_pin) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(dc_pin);
|
||||
}
|
||||
|
||||
size_t buffer_length() const { return this->buffer_length_; }
|
||||
int width() const { return this->width_; }
|
||||
int height() const { return this->height_; }
|
||||
};
|
||||
|
||||
/// GPIO pin that remembers the last level written to it.
|
||||
class RecordingPin : public GPIOPin {
|
||||
public:
|
||||
void setup() override {}
|
||||
void pin_mode(gpio::Flags flags) override {}
|
||||
gpio::Flags get_flags() const override { return gpio::Flags::FLAG_NONE; }
|
||||
bool digital_read() override { return false; }
|
||||
void digital_write(bool value) override { this->level = value; }
|
||||
size_t dump_summary(char *buffer, size_t len) const override { return snprintf(buffer, len, "recording"); }
|
||||
|
||||
bool level{true};
|
||||
};
|
||||
|
||||
/// SPI delegate that records every payload and whether it was a command or data phase.
|
||||
class RecordingDelegate : public spi::SPIDelegate {
|
||||
public:
|
||||
explicit RecordingDelegate(const RecordingPin *dc)
|
||||
: spi::SPIDelegate(10'000'000, spi::BIT_ORDER_MSB_FIRST, spi::MODE0, nullptr), dc_(dc) {}
|
||||
|
||||
uint8_t transfer(uint8_t data) override {
|
||||
this->record_(&data, 1);
|
||||
return 0;
|
||||
}
|
||||
void write_array(const uint8_t *ptr, size_t length) override { this->record_(ptr, length); }
|
||||
|
||||
std::vector<uint8_t> bytes;
|
||||
std::vector<std::pair<bool, size_t>> chunks; // (data phase, length)
|
||||
|
||||
protected:
|
||||
void record_(const uint8_t *ptr, size_t length) {
|
||||
this->chunks.emplace_back(this->dc_->level, length);
|
||||
this->bytes.insert(this->bytes.end(), ptr, ptr + length);
|
||||
}
|
||||
|
||||
const RecordingPin *dc_;
|
||||
};
|
||||
|
||||
} // namespace esphome::st7305::testing
|
||||
@@ -0,0 +1,6 @@
|
||||
display:
|
||||
- platform: st7305
|
||||
model: WAVESHARE-400X300
|
||||
id: st7305_display
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
@@ -0,0 +1,255 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::st7305::testing {
|
||||
|
||||
class ST7305Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { ASSERT_TRUE(this->display_.init_buffer()); }
|
||||
|
||||
void reset_screen_() {
|
||||
this->display_.fill(Color::BLACK);
|
||||
this->display_.clear_dirty();
|
||||
}
|
||||
|
||||
TestableST7305 display_;
|
||||
};
|
||||
|
||||
TEST_F(ST7305Test, FillSetsAllBytes) {
|
||||
this->display_.fill(Color::WHITE);
|
||||
for (size_t i = 0; i < this->display_.buffer_length(); i++)
|
||||
EXPECT_EQ(this->display_.buffer()[i], 0xFF) << "byte " << i;
|
||||
|
||||
this->display_.fill(Color::BLACK);
|
||||
for (size_t i = 0; i < this->display_.buffer_length(); i++)
|
||||
EXPECT_EQ(this->display_.buffer()[i], 0x00) << "byte " << i;
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, PixelMapping) {
|
||||
struct PixelCase {
|
||||
int x;
|
||||
int y;
|
||||
size_t index;
|
||||
uint8_t mask;
|
||||
};
|
||||
const PixelCase cases[] = {
|
||||
{0, 299, 0, 0x80}, {1, 299, 0, 0x40}, {0, 298, 0, 0x20}, {0, 297, 0, 0x08}, {0, 296, 0, 0x02},
|
||||
{2, 299, 75, 0x80}, {0, 0, 74, 0x02}, {1, 0, 74, 0x01}, {200, 150, 7537, 0x20}, {399, 0, 14999, 0x01},
|
||||
};
|
||||
|
||||
for (const auto &pixel : cases) {
|
||||
this->display_.draw_pixel_at(pixel.x, pixel.y, Color::WHITE);
|
||||
EXPECT_EQ(this->display_.buffer()[pixel.index], pixel.mask) << "pixel " << pixel.x << "," << pixel.y;
|
||||
this->display_.draw_pixel_at(pixel.x, pixel.y, Color::BLACK);
|
||||
EXPECT_EQ(this->display_.buffer()[pixel.index], 0x00) << "pixel " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, ClearingOnePixelLeavesNeighbours) {
|
||||
this->display_.draw_pixel_at(0, 299, Color::WHITE);
|
||||
this->display_.draw_pixel_at(1, 299, Color::WHITE);
|
||||
EXPECT_EQ(this->display_.buffer()[0], 0xC0);
|
||||
|
||||
this->display_.draw_pixel_at(0, 299, Color::BLACK);
|
||||
EXPECT_EQ(this->display_.buffer()[0], 0x40);
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, DirtyBoundsFollowChanges) {
|
||||
EXPECT_FALSE(this->display_.is_dirty());
|
||||
|
||||
this->display_.draw_pixel_at(10, 20, Color::WHITE);
|
||||
EXPECT_TRUE(this->display_.is_dirty());
|
||||
EXPECT_EQ(this->display_.x_low(), 10);
|
||||
EXPECT_EQ(this->display_.x_high(), 10);
|
||||
EXPECT_EQ(this->display_.y_low(), 20);
|
||||
EXPECT_EQ(this->display_.y_high(), 20);
|
||||
|
||||
this->display_.clear_dirty();
|
||||
this->display_.draw_pixel_at(10, 20, Color::WHITE);
|
||||
EXPECT_FALSE(this->display_.is_dirty());
|
||||
|
||||
this->display_.draw_pixel_at(10, 20, Color::BLACK);
|
||||
EXPECT_TRUE(this->display_.is_dirty());
|
||||
|
||||
this->display_.clear_dirty();
|
||||
this->display_.draw_pixel_at(10, 20, Color::WHITE);
|
||||
this->display_.draw_pixel_at(100, 200, Color::WHITE);
|
||||
EXPECT_EQ(this->display_.x_low(), 10);
|
||||
EXPECT_EQ(this->display_.x_high(), 100);
|
||||
EXPECT_EQ(this->display_.y_low(), 20);
|
||||
EXPECT_EQ(this->display_.y_high(), 200);
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, FillOnlyDirtiesChangedBytes) {
|
||||
this->display_.clear_dirty();
|
||||
this->display_.fill(Color::BLACK);
|
||||
EXPECT_FALSE(this->display_.is_dirty());
|
||||
|
||||
this->display_.fill(Color::WHITE);
|
||||
EXPECT_TRUE(this->display_.is_dirty());
|
||||
EXPECT_EQ(this->display_.x_low(), 0);
|
||||
EXPECT_EQ(this->display_.x_high(), 399);
|
||||
EXPECT_EQ(this->display_.y_low(), 0);
|
||||
EXPECT_EQ(this->display_.y_high(), 299);
|
||||
|
||||
this->display_.clear_dirty();
|
||||
this->display_.fill(Color::WHITE);
|
||||
EXPECT_FALSE(this->display_.is_dirty());
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, WindowAlignsToAddressBlocks) {
|
||||
this->reset_screen_();
|
||||
this->display_.draw_pixel_at(0, 299, Color::WHITE);
|
||||
this->display_.draw_pixel_at(1, 299, Color::WHITE);
|
||||
|
||||
const auto window = this->display_.compute_window();
|
||||
EXPECT_EQ(window.outer_start, 0);
|
||||
EXPECT_EQ(window.outer_end, 0);
|
||||
EXPECT_EQ(window.block_start, 0);
|
||||
EXPECT_EQ(window.block_end, 2);
|
||||
EXPECT_EQ(window.caset_start, 0x12);
|
||||
EXPECT_EQ(window.caset_end, 0x12);
|
||||
EXPECT_EQ(window.data_length(), 3u);
|
||||
|
||||
this->reset_screen_();
|
||||
this->display_.draw_pixel_at(200, 150, Color::WHITE);
|
||||
|
||||
const auto middle = this->display_.compute_window();
|
||||
EXPECT_EQ(middle.outer_start, 100);
|
||||
EXPECT_EQ(middle.outer_end, 100);
|
||||
EXPECT_EQ(middle.block_start, 36);
|
||||
EXPECT_EQ(middle.block_end, 38);
|
||||
EXPECT_EQ(middle.caset_start, 0x1E);
|
||||
EXPECT_EQ(middle.caset_end, 0x1E);
|
||||
EXPECT_EQ(middle.data_length(), 3u);
|
||||
|
||||
this->reset_screen_();
|
||||
this->display_.draw_pixel_at(0, 0, Color::WHITE);
|
||||
this->display_.draw_pixel_at(0, 299, Color::WHITE);
|
||||
|
||||
const auto full_height = this->display_.compute_window();
|
||||
EXPECT_EQ(full_height.outer_start, 0);
|
||||
EXPECT_EQ(full_height.outer_end, 0);
|
||||
EXPECT_EQ(full_height.block_start, 0);
|
||||
EXPECT_EQ(full_height.block_end, 74);
|
||||
EXPECT_EQ(full_height.caset_start, 0x12);
|
||||
EXPECT_EQ(full_height.caset_end, 0x2A);
|
||||
EXPECT_EQ(full_height.data_length(), 75u);
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, WriteWindowEmitsWindowCommands) {
|
||||
for (size_t i = 0; i < this->display_.buffer_length(); i++)
|
||||
this->display_.buffer()[i] = static_cast<uint8_t>(i & 0xFF);
|
||||
|
||||
RecordingPin dc_pin;
|
||||
RecordingDelegate delegate(&dc_pin);
|
||||
this->display_.attach_spi(&delegate, &dc_pin);
|
||||
|
||||
this->display_.draw_pixel_at(200, 151, Color::WHITE);
|
||||
this->display_.write_window();
|
||||
|
||||
const uint8_t expected[] = {
|
||||
0x2A,
|
||||
0x1E,
|
||||
0x1E,
|
||||
0x2B,
|
||||
100,
|
||||
100,
|
||||
0x2C,
|
||||
this->display_.buffer()[100 * 75 + 36],
|
||||
this->display_.buffer()[100 * 75 + 37],
|
||||
this->display_.buffer()[100 * 75 + 38],
|
||||
};
|
||||
ASSERT_EQ(delegate.bytes.size(), sizeof(expected));
|
||||
for (size_t i = 0; i < sizeof(expected); i++)
|
||||
EXPECT_EQ(delegate.bytes[i], expected[i]) << "byte " << i;
|
||||
|
||||
ASSERT_EQ(delegate.chunks.size(), 6u);
|
||||
EXPECT_EQ(delegate.chunks[0], std::make_pair(false, size_t{1}));
|
||||
EXPECT_EQ(delegate.chunks[1], std::make_pair(true, size_t{2}));
|
||||
EXPECT_EQ(delegate.chunks[2], std::make_pair(false, size_t{1}));
|
||||
EXPECT_EQ(delegate.chunks[3], std::make_pair(true, size_t{2}));
|
||||
EXPECT_EQ(delegate.chunks[4], std::make_pair(false, size_t{1}));
|
||||
EXPECT_EQ(delegate.chunks[5], std::make_pair(true, size_t{3}));
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, PortraitPixelMapping) {
|
||||
TestableST7305 portrait(168, 384, Orientation::ORIENTATION_PORTRAIT, 0x17, 0x24, 0x00, 0xBF);
|
||||
ASSERT_TRUE(portrait.init_buffer());
|
||||
ASSERT_EQ(portrait.get_width(), 168);
|
||||
ASSERT_EQ(portrait.get_height(), 384);
|
||||
|
||||
struct PixelCase {
|
||||
int x;
|
||||
int y;
|
||||
size_t index;
|
||||
uint8_t mask;
|
||||
};
|
||||
const PixelCase cases[] = {
|
||||
{0, 0, 0, 0x80}, {1, 0, 0, 0x20}, {2, 0, 0, 0x08}, {3, 0, 0, 0x02},
|
||||
{0, 1, 0, 0x40}, {4, 0, 1, 0x80}, {0, 2, 42, 0x80}, {167, 383, 8063, 0x01},
|
||||
};
|
||||
|
||||
for (const auto &pixel : cases) {
|
||||
portrait.draw_pixel_at(pixel.x, pixel.y, Color::WHITE);
|
||||
EXPECT_EQ(portrait.buffer()[pixel.index], pixel.mask) << "pixel " << pixel.x << "," << pixel.y;
|
||||
portrait.draw_pixel_at(pixel.x, pixel.y, Color::BLACK);
|
||||
EXPECT_EQ(portrait.buffer()[pixel.index], 0x00) << "pixel " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ST7305Test, PortraitWindowAlignsToAddressBlocks) {
|
||||
TestableST7305 portrait(168, 384, Orientation::ORIENTATION_PORTRAIT, 0x17, 0x24, 0x00, 0xBF);
|
||||
ASSERT_TRUE(portrait.init_buffer());
|
||||
|
||||
portrait.draw_pixel_at(84, 192, Color::WHITE);
|
||||
const auto window = portrait.compute_window();
|
||||
EXPECT_EQ(window.outer_start, 96);
|
||||
EXPECT_EQ(window.outer_end, 96);
|
||||
EXPECT_EQ(window.stride, 42);
|
||||
EXPECT_EQ(window.block_start, 21);
|
||||
EXPECT_EQ(window.block_end, 23);
|
||||
EXPECT_EQ(window.caset_start, 0x1E);
|
||||
EXPECT_EQ(window.caset_end, 0x1E);
|
||||
EXPECT_EQ(window.data_length(), 3u);
|
||||
|
||||
portrait.fill(Color::BLACK);
|
||||
portrait.clear_dirty();
|
||||
portrait.draw_pixel_at(0, 0, Color::WHITE);
|
||||
portrait.draw_pixel_at(3, 1, Color::WHITE);
|
||||
const auto corner = portrait.compute_window();
|
||||
EXPECT_EQ(corner.outer_start, 0);
|
||||
EXPECT_EQ(corner.outer_end, 0);
|
||||
EXPECT_EQ(corner.block_start, 0);
|
||||
EXPECT_EQ(corner.block_end, 2);
|
||||
EXPECT_EQ(corner.caset_start, 0x17);
|
||||
EXPECT_EQ(corner.caset_end, 0x17);
|
||||
EXPECT_EQ(corner.data_length(), 3u);
|
||||
|
||||
portrait.fill(Color::BLACK);
|
||||
portrait.clear_dirty();
|
||||
portrait.draw_pixel_at(167, 200, Color::WHITE);
|
||||
const auto right_edge = portrait.compute_window();
|
||||
EXPECT_EQ(right_edge.outer_start, 100);
|
||||
EXPECT_EQ(right_edge.outer_end, 100);
|
||||
EXPECT_EQ(right_edge.block_start, 39);
|
||||
EXPECT_EQ(right_edge.block_end, 41);
|
||||
EXPECT_EQ(right_edge.caset_start, 0x24);
|
||||
EXPECT_EQ(right_edge.caset_end, 0x24);
|
||||
EXPECT_EQ(right_edge.data_length(), 3u);
|
||||
|
||||
portrait.fill(Color::BLACK);
|
||||
portrait.clear_dirty();
|
||||
portrait.draw_pixel_at(167, 383, Color::WHITE);
|
||||
const auto bottom_right = portrait.compute_window();
|
||||
EXPECT_EQ(bottom_right.outer_start, 191);
|
||||
EXPECT_EQ(bottom_right.outer_end, 191);
|
||||
EXPECT_EQ(bottom_right.block_start, 39);
|
||||
EXPECT_EQ(bottom_right.block_end, 41);
|
||||
EXPECT_EQ(bottom_right.caset_start, 0x24);
|
||||
EXPECT_EQ(bottom_right.caset_end, 0x24);
|
||||
EXPECT_EQ(bottom_right.data_length(), 3u);
|
||||
}
|
||||
|
||||
} // namespace esphome::st7305::testing
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO17
|
||||
miso_pin: GPIO16
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
st7305: !include common.yaml
|
||||
@@ -0,0 +1,21 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO12
|
||||
dc_pin: GPIO13
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
display:
|
||||
- platform: st7305
|
||||
model: CUSTOM
|
||||
width: 400
|
||||
height: 300
|
||||
portrait: false
|
||||
cs_pin: ${cs_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
init_sequence:
|
||||
- [0x01]
|
||||
- [0x11, 0x00]
|
||||
- [0x29]
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
@@ -0,0 +1,22 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO12
|
||||
dc_pin: GPIO13
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
display:
|
||||
- platform: st7305
|
||||
model: CUSTOM
|
||||
width: 168
|
||||
height: 384
|
||||
portrait: true
|
||||
column_start: 0x17
|
||||
cs_pin: ${cs_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
init_sequence:
|
||||
- [0x01]
|
||||
- [0x11, 0x00]
|
||||
- [0x29]
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
Reference in New Issue
Block a user