Merge pull request #17532 from esphome/bump-2026.7.0b2
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Check import esphome.__main__ time (push) Has been cancelled
CI / Test downstream esphome/device-builder (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.12) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.12) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.12) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (${{ matrix.bucket.name }}) (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF 1/3 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF 2/3 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF 3/3 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 C6 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 P4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 S3 (push) Has been cancelled
CI / Test components batch (${{ matrix.batch.components }}) (push) Has been cancelled
CI / Test esp32 components with PlatformIO (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI - GitHub Scripts / Test auto-label-pr scripts (push) Has been cancelled

2026.7.0b2
This commit is contained in:
Jesse Hills
2026-07-13 13:24:45 +12:00
committed by GitHub
98 changed files with 1787 additions and 415 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.7.0b1
PROJECT_NUMBER = 2026.7.0b2
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.3.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.4.3
RUN \
platformio settings set enable_telemetry No \
+5 -2
View File
@@ -488,8 +488,11 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
# No key provided, but encryption desired
# This will allow a plaintext client to provide a noise key,
# send it to the device, and then switch to noise.
# Until a key is set, the device accepts both Noise connections
# using the well-known all-zeros PSK (preferred: the key travels
# encrypted, protecting against passive sniffing) and plaintext
# connections (deprecated, remove after 2027.2.0) so a client can
# provide a noise key and the device then switches to noise only.
# The key will be saved in flash and used for future connections
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
+5
View File
@@ -310,6 +310,11 @@ message DeviceInfoResponse {
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
// Device is unprovisioned and accepts Noise handshakes with the well-known
// all-zeros PSK, so the api encryption key can be provisioned without being
// sent in plaintext (protects against passive sniffing, not active MITM)
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
}
message ListEntitiesRequest {
+50 -1
View File
@@ -198,6 +198,29 @@ APIConnection::~APIConnection() {
#endif
}
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
void APIConnection::upgrade_helper_to_noise_() {
// The client opened with a Noise hello while this device has no encryption
// key set. Replace the plaintext helper with a Noise helper so the key can
// be provisioned over an encrypted channel: the noise context PSK is all
// zeros when unprovisioned, and NNpsk0 still runs a fresh ephemeral X25519
// exchange, so a passive listener cannot read the session. A publicly known
// PSK authenticates nobody; this protects against sniffing only.
auto *plaintext = static_cast<APIPlaintextFrameHelper *>(this->helper_.get());
uint8_t header[3];
uint8_t header_len = plaintext->get_consumed_header(header);
auto *noise = new APINoiseFrameHelper(plaintext->release_socket_for_switch(), this->parent_->get_noise_ctx());
// Carry over the peername-based client name (Hello has not arrived yet)
const char *name = plaintext->get_client_name();
noise->set_client_name(name, strlen(name));
this->helper_.reset(noise); // destroys the plaintext helper
APIError err = noise->init_from_handoff(header, header_len);
if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Noise handoff failed"), err);
}
}
#endif // USE_API_NOISE && USE_API_PLAINTEXT
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
@@ -256,6 +279,15 @@ void APIConnection::loop() {
// No more data available
break;
} else if (err != APIError::OK) {
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Checked inside the error branch to keep the hot err == OK path
// free of it; this can only fire on the first bytes of a plaintext
// helper on an unprovisioned device
if (err == APIError::PROTOCOL_SWITCH_TO_NOISE) {
this->upgrade_helper_to_noise_();
return;
}
#endif
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
return;
} else {
@@ -1351,7 +1383,7 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet
#ifdef USE_ZWAVE_PROXY
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
zwave_proxy::global_zwave_proxy->send_frame(this, msg.data, msg.data_len);
}
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
@@ -1860,6 +1892,12 @@ bool APIConnection::send_device_info_response_() {
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#ifndef USE_API_NOISE_PSK_FROM_YAML
// No key from YAML: while no key is set, the key can be provisioned over a
// zero-PSK Noise connection. Gated on the YAML define (not the plaintext
// one) so this advertisement survives the plaintext removal in 2027.2.0.
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
#endif
#endif
#ifdef USE_DEVICES
size_t device_index = 0;
@@ -2037,10 +2075,21 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (APINoiseContext::is_all_zeros(psk)) {
// Accepting the reserved provisioning PSK would report success without
// enabling encryption (or silently clear an existing key)
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
} else {
resp.success = true;
#ifdef USE_API_PLAINTEXT
if (this->helper_->frame_footer_size() == 0) {
// Plaintext transport has no frame footer; Noise always has the MAC footer.
// Remove after 2027.2.0 together with plaintext support on keyless devices.
ESP_LOGW(TAG, "Key received over plaintext; deprecated, will be removed in 2027.2.0");
}
#endif
}
return this->send_message(resp);
+5
View File
@@ -626,6 +626,11 @@ class APIConnection final : public APIServerConnectionBase {
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
void finalize_iterator_sync_();
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Swap the plaintext helper for a Noise helper after the client opened
// with a Noise hello on an unprovisioned device (zero-PSK provisioning).
void upgrade_helper_to_noise_();
#endif
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -97,6 +97,8 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE");
}
#endif
// PROTOCOL_SWITCH_TO_NOISE is intercepted in APIConnection::loop() before
// any logging can happen, so it intentionally has no entry here.
return LOG_STR("UNKNOWN");
}
+11
View File
@@ -88,6 +88,11 @@ enum class APIError : uint16_t {
HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021,
#endif
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Not an error: an unprovisioned device received a Noise client hello on a
// plaintext connection; the caller must hand the socket off to a Noise helper.
PROTOCOL_SWITCH_TO_NOISE = 1023,
#endif
};
const LogString *api_error_to_logstr(APIError err);
@@ -200,6 +205,12 @@ class APIFrameHelper {
// or track that they stopped early and retry without this check.
// See Socket::ready() for details.
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
// Move the socket out of this helper so a replacement helper can take it
// over (plaintext to Noise handoff on unprovisioned devices). The drained
// helper must be destroyed right after.
std::unique_ptr<socket::Socket> release_socket_for_switch() { return std::move(this->socket_); }
#endif
// Release excess memory from internal buffers after initial sync
void release_buffers() {
// rx_buf_: Safe to clear only if no partial read in progress.
@@ -109,6 +109,40 @@ APIError APINoiseFrameHelper::init() {
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
#ifdef USE_API_PLAINTEXT
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
APIError err = this->init();
if (err != APIError::OK) {
return err;
}
// Seed the header bytes the plaintext helper consumed before detecting the
// Noise indicator; try_read_frame_ resumes from rx_header_buf_len_.
std::memcpy(this->rx_header_buf_, header, header_len);
this->rx_header_buf_len_ = header_len;
// Pump the handshake without gating on socket_->ready(): on LWIP the
// plaintext helper's partial read can drain rcvevent while the rest of the
// client hello sits in the lastdata cache, so ready() may report false even
// though data is available.
return this->pump_handshake_();
}
#endif // USE_API_PLAINTEXT
/// Drive the handshake state machine until DATA, WOULD_BLOCK, or a fatal
/// error. WOULD_BLOCK is not an error: reads stop naturally on EWOULDBLOCK
/// and resume on the next loop().
APIError APINoiseFrameHelper::pump_handshake_() {
while (this->state_ != State::DATA) {
APIError err = this->state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
// Helper for handling handshake frame errors
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
if (aerr == APIError::BAD_INDICATOR) {
@@ -131,16 +165,13 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
// that must follow reads, deadlocking the handshake. state_action() will return
// WOULD_BLOCK when no more data is available to read.
bool socket_ready = this->socket_->ready();
while (state_ != State::DATA && socket_ready) {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
// Check ready() once, not per state transition. On ESP8266 LWIP raw TCP,
// ready() returns false once the rx buffer is consumed. Re-checking each
// iteration would block handshake writes that must follow reads,
// deadlocking the handshake. pump_handshake_() stops on WOULD_BLOCK when
// no more data is available to read.
if (state_ != State::DATA && this->socket_->ready()) {
APIError err = this->pump_handshake_();
if (err != APIError::OK) {
return err;
}
@@ -22,12 +22,20 @@ class APINoiseFrameHelper final : public APIFrameHelper {
}
~APINoiseFrameHelper() override;
APIError init() override;
#ifdef USE_API_PLAINTEXT
// Take over a connection whose first bytes were consumed by a plaintext
// helper on an unprovisioned device (see APIError::PROTOCOL_SWITCH_TO_NOISE).
// Seeds the already-read header bytes and pumps the handshake state machine
// until it would block.
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
#endif
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
APIError pump_handshake_();
APIError state_action_();
APIError state_action_client_hello_();
APIError state_action_server_hello_();
@@ -89,6 +89,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// If this was the first read, validate the indicator byte
if (rx_header_buf_pos_ == 0 && received > 0) {
if (rx_header_buf_[0] != 0x00) {
#ifdef USE_API_NOISE
// Dual build (encryption supported but no key set): a 0x01 first byte
// is a Noise client hello. Hand the connection off to a Noise helper
// running the all-zeros provisioning PSK so the encryption key can be
// set without crossing the wire in plaintext. Preserve the bytes we
// already consumed; they are the start of the Noise 3-byte header.
if (rx_header_buf_[0] == 0x01) {
rx_header_buf_pos_ = static_cast<uint8_t>(received);
return APIError::PROTOCOL_SWITCH_TO_NOISE;
}
#endif
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
@@ -23,6 +23,15 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
#ifdef USE_API_NOISE
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
// header bytes already consumed from the socket (at most 3, the size of the
// Noise fixed header) so the replacement Noise helper can be seeded with them.
uint8_t get_consumed_header(uint8_t out[3]) const {
memcpy(out, this->rx_header_buf_, this->rx_header_buf_pos_);
return this->rx_header_buf_pos_;
}
#endif
protected:
APIError try_read_frame_();
+12 -5
View File
@@ -10,13 +10,20 @@ using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
bool has_psk = false;
for (auto i : psk) {
has_psk |= i;
}
this->has_psk_ = has_psk;
this->has_psk_ = !is_all_zeros(psk);
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
+6
View File
@@ -170,6 +170,9 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
for (const auto &it : this->serial_proxies) {
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it);
}
#endif
#ifdef USE_API_NOISE
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
#endif
return pos;
}
@@ -232,6 +235,9 @@ uint32_t DeviceInfoResponse::calculate_size() const {
for (const auto &it : this->serial_proxies) {
size += ProtoSize::calc_message_force(2, it.calculate_size());
}
#endif
#ifdef USE_API_NOISE
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
#endif
return size;
}
+4 -1
View File
@@ -533,7 +533,7 @@ class SerialProxyInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 309;
static constexpr uint16_t ESTIMATED_SIZE = 312;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
#endif
@@ -588,6 +588,9 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_SERIAL_PROXY
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
#endif
#ifdef USE_API_NOISE
bool api_encryption_provisionable{false};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
+3
View File
@@ -982,6 +982,9 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
it.dump_to(out);
out.append("\n");
}
#endif
#ifdef USE_API_NOISE
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
#endif
return out.c_str();
}
+3 -1
View File
@@ -448,7 +448,9 @@ _BINARY_SENSOR_SCHEMA = (
cv.Exclusive(
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
+3 -1
View File
@@ -50,7 +50,9 @@ _BUTTON_SCHEMA = (
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
}
)
+3 -1
View File
@@ -131,7 +131,9 @@ _COVER_SCHEMA = (
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic
),
@@ -1,13 +1,36 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/wake.h"
#include <zephyr/sys/poweroff.h>
#include <algorithm>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
// The Zephyr watchdog has a short window (2s, or 10s with Zigbee) and
// WDT_OPT_PAUSE_IN_SLEEP only pauses it during true hardware sleep — not while a
// radio thread (e.g. the Zigbee stack) keeps the CPU busy in k_sem_take(). Feed
// it at least this often while waiting so it does not reset the device.
static const uint32_t WDT_FEED_INTERVAL_MS = 1000;
static bool wakeable_delay_feed_wdt(uint32_t ms) {
while (ms > 0) {
const uint32_t step = std::min(ms, WDT_FEED_INTERVAL_MS);
esphome::internal::wakeable_delay(step);
esphome::arch_feed_wdt();
if (esphome::wake_request_take()) {
return true;
}
if (ms != UINT32_MAX) {
ms -= step;
}
}
return false;
}
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
@@ -15,8 +38,9 @@ void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
bool woke = false;
if (this->sleep_duration_.has_value()) {
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
woke = wakeable_delay_feed_wdt(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
@@ -29,10 +53,9 @@ void DeepSleepComponent::deep_sleep_() {
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#else
esphome::internal::wakeable_delay(UINT32_MAX);
woke = wakeable_delay_feed_wdt(UINT32_MAX);
#endif
}
const bool woke = esphome::wake_request_take();
if (woke) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
+3 -1
View File
@@ -50,7 +50,9 @@ _EVENT_SCHEMA = (
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
cv.GenerateID(): cv.declare_id(Event),
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
}
)
+2 -2
View File
@@ -342,7 +342,7 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) {
ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx);
uint8_t addr = this->get_wiper_address_(wiper_idx);
uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::INCREMENT);
auto err = this->write(&this->address_, reg);
auto err = this->write(&reg, 1);
if (err != i2c::ERROR_OK) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
@@ -373,7 +373,7 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) {
ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx);
uint8_t addr = this->get_wiper_address_(wiper_idx);
uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::DECREMENT);
auto err = this->write(&this->address_, reg);
auto err = this->write(&reg, 1);
if (err != i2c::ERROR_OK) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
+36 -10
View File
@@ -47,7 +47,7 @@ void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_regi
auto &services = services_storage;
#endif
#ifdef USE_API
#ifdef USE_MDNS_DEVICE_INFO_TXT
#ifdef USE_MDNS_STORE_SERVICES
get_mac_address_into_buffer(this->mac_address_);
char *mac_ptr = this->mac_address_;
@@ -70,17 +70,20 @@ void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_regi
platform_register(this, services);
}
void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf,
char *config_hash_buf) {
void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services,
const char *mac_address_buf, const char *config_hash_buf) {
// IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
// in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_MDNS_DEVICE_INFO_TXT
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash");
#endif
#ifdef USE_API
MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash");
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
@@ -107,7 +110,13 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_count++; // network
#endif
#ifdef USE_API_NOISE
const bool api_has_psk = api::global_api_server->get_noise_ctx().has_psk();
txt_count++; // api_encryption or api_encryption_supported
#ifndef USE_API_NOISE_PSK_FROM_YAML
if (!api_has_psk) {
txt_count++; // api_provisioning
}
#endif
#endif
#ifdef ESPHOME_PROJECT_NAME
txt_count += 2; // project_name and project_version
@@ -163,9 +172,18 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
bool has_psk = api::global_api_server->get_noise_ctx().has_psk();
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
const char *encryption_key = api_has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
#ifndef USE_API_NOISE_PSK_FROM_YAML
if (!api_has_psk) {
// Unprovisioned device without a YAML key: advertise that the encryption
// key can be provisioned over a zero-PSK Noise connection. Gated on the
// YAML define so this survives the plaintext removal in 2027.2.0.
MDNS_STATIC_CONST_CHAR(TXT_API_PROVISIONING, "api_provisioning");
MDNS_STATIC_CONST_CHAR(VALUE_ZERO_PSK, "zero-psk");
txt_records.push_back({MDNS_STR(TXT_API_PROVISIONING), MDNS_STR(VALUE_ZERO_PSK)});
}
#endif
#endif
#ifdef ESPHOME_PROJECT_NAME
@@ -212,12 +230,18 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
#ifndef USE_API
// Without the native API there is no _esphomelib service, so publish the
// device info here for the device builder to discover.
web_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)},
{MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)},
{MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}};
#endif
#endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \
!defined(USE_MDNS_EXTRA_SERVICES)
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
// Publish "http" service if not using native API or any other services
// This is just to have *some* mDNS service so that .local resolution works
@@ -225,7 +249,9 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)},
{MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)},
{MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}};
#endif
}
+12 -3
View File
@@ -22,6 +22,15 @@
#endif
#endif
// Device info TXT records (version, mac, config_hash) are published on the _esphomelib service
// when the native API is enabled, otherwise on the _http service (web_server's or the fallback one).
// When neither applies (only prometheus, sendspin or user-defined services are configured), no
// device info records are published and the buffers below are not needed.
#if defined(USE_API) || defined(USE_WEBSERVER) || \
(!defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_MDNS_EXTRA_SERVICES))
#define USE_MDNS_DEVICE_INFO_TXT
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -136,7 +145,7 @@ class MDNSComponent final : public Component
StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
#endif
#if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES)
#if defined(USE_MDNS_DEVICE_INFO_TXT) && defined(USE_MDNS_STORE_SERVICES)
/// Fixed buffer for MAC address (only needed when services are stored)
char mac_address_[MAC_ADDRESS_BUFFER_SIZE];
/// Fixed buffer for config hash hex string (only needed when services are stored)
@@ -149,8 +158,8 @@ class MDNSComponent final : public Component
// RP2040 defers MDNS.begin() until the first IP-up event; this tracks that.
bool initialized_{false};
#endif
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf,
char *config_hash_buf);
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, const char *mac_address_buf,
const char *config_hash_buf);
};
} // namespace esphome::mdns
+1 -1
View File
@@ -12,7 +12,7 @@ namespace esphome::mdns {
void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
#ifdef USE_API
#ifdef USE_MDNS_DEVICE_INFO_TXT
get_mac_address_into_buffer(this->mac_address_);
char *mac_ptr = this->mac_address_;
format_hex_to(this->config_hash_str_, App.get_config_hash());
+84 -13
View File
@@ -26,16 +26,22 @@ from esphome.const import (
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import TimePeriod
from esphome.core import CORE, TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
LOGGER = cv.logging.getLogger(__name__)
CONF_TRANSFORMS = "transforms"
# All axis transforms a model may support, in the order they appear in the schema.
ALL_TRANSFORMS = (CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY)
ColorOrder = display_ns.enum("ColorMode")
NOP = 0x00
@@ -302,7 +308,8 @@ class DriverChip:
"""
A class representing a MIPI DBI driver chip model.
The parameters supplied as defaults will be used to provide default values for the display configuration.
Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes.
Pass a ``transforms`` set to restrict which axis transforms (mirror_x, mirror_y, swap_xy) the model
supports; by default all three are available.
"""
models: dict[str, Self] = {}
@@ -387,11 +394,15 @@ class DriverChip:
"""
Return the available transforms for this model.
"""
if (transforms := self.get_default(CONF_TRANSFORMS, None)) is not None:
return transforms
if self.get_default("no_transform", False):
return set()
if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED:
return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
return {CONF_MIRROR_X, CONF_MIRROR_Y}
raise ValueError(
"Setting 'swap_xy' to 'cv.UNDEFINED' is no longer supported; set 'transforms' instead"
)
def has_hardware_transform(self, config) -> bool:
"""
@@ -533,17 +544,31 @@ class DriverChip:
transform[CONF_TRANSFORM] = self.rotation_as_transform(config)
return transform
def swap_xy_schema(self):
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def transform_schema(self):
"""
Build the schema for the ``transform`` config option of this model.
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
Each transform the model supports is a required boolean. A transform the model does not
support may be omitted or set to ``false``; setting it to ``true`` reports a clear error
naming the unsupported transform instead of a generic "extra keys not allowed".
"""
supported = self.transforms
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def unsupported(name):
def validator(value):
if cv.boolean(value):
raise cv.Invalid(f"'{name}' is not supported by this model")
return False
return validator
schema = {}
for name in ALL_TRANSFORMS:
if name in supported:
schema[cv.Required(name)] = cv.boolean
else:
schema[cv.Optional(name, default=False)] = unsupported(name)
return cv.Any(cv.Schema(schema), cv.one_of(CONF_DISABLED, lower=True))
def get_madctl(self, transform: dict, config: dict) -> int:
"""
@@ -577,12 +602,15 @@ class DriverChip:
"""
return self.get_default(f"no_{command.lower()}", False)
def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]:
def get_sequence(self, config, add_madctl=True, add_reset=False) -> tuple[int, ...]:
"""
Create the init sequence for the display.
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
MADCTL will be set if add_madctl is True
If add_reset is True, a reset is prepended: a software reset when no reset pin
is configured (and the model doesn't skip it), followed by a settling delay that
both a software and a hardware reset require.
Returns the init sequence
"""
sequence = list(self.initsequence or ())
@@ -591,6 +619,15 @@ class DriverChip:
# Ensure each command is a tuple
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
if add_reset:
reset: list = []
# A software reset is only needed when there is no hardware reset pin.
if CONF_RESET_PIN not in config and not self.skip_command("SWRESET"):
reset.append((SWRESET,))
# Both a software and a hardware reset need a settling delay before further commands.
reset.append(delay(10))
sequence = reset + sequence
# Set pixel format if not already in the custom sequence
pixel_mode = config[CONF_PIXEL_MODE]
if not isinstance(pixel_mode, int):
@@ -611,13 +648,47 @@ class DriverChip:
sequence.append((BRIGHTNESS, brightness))
# Add a SLPOUT command if required.
if not self.skip_command("SLPOUT"):
# A zero delay will delay until 120ms after reset
sequence.append(delay(0))
sequence.append((SLPOUT,))
sequence.append(delay(10))
sequence.append((DISPON,))
# Add a delay here because additional commands may be added after this at runtime.
sequence.append(delay(10))
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return flatten_sequence(sequence)
def check_requirements(self) -> None:
"""
Raise a friendly error if any component this model requires is not configured.
This runs during schema validation (before ID references are resolved) so that a
model whose default pins live on a pin expander reports the missing expander clearly
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
Also logs a warning if the model is deprecated.
"""
if deprecation_reason := self.get_default("deprecation_reason"):
LOGGER.warning(
"Display model %s is deprecated: %s", self.name, deprecation_reason
)
if requirements := self.get_default("requires", set()):
# ``raw_config`` is populated before any component schema runs during a real
# validation, so presence of a required component is simply a top-level key.
# When it is absent (e.g. a unit test that invokes the schema directly) there
# is no config to check against, so skip.
global_config = CORE.raw_config
if global_config is None:
return
missing = {x for x in requirements if x not in global_config}
if missing:
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
raise cv.Invalid(
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
)
def requires_buffer(config) -> bool:
"""
+4 -18
View File
@@ -41,24 +41,21 @@ from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.final_validate import full_config
from . import mipi_dsi_ns, models
from .models import DsiDriverChip
# Currently only ESP32-P4 is supported, so esp_ldo and psram are required
DEPENDENCIES = ["esp32", "esp_ldo", "psram"]
@@ -73,7 +70,7 @@ ColorBitness = display.display_ns.enum("ColorBitness")
CONF_LANE_BIT_RATE = "lane_bit_rate"
CONF_LANES = "lanes"
DriverChip("CUSTOM")
DsiDriverChip("CUSTOM")
# Import all models dynamically from the models package
@@ -90,19 +87,7 @@ COLOR_DEPTHS = {
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by DSI displays"
),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
@@ -172,6 +157,7 @@ def _config_schema(config):
)(config)
config = model_schema(config)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
model.get_dimensions(config)
)
@@ -0,0 +1,14 @@
from esphome.components.mipi import DriverChip
from esphome.const import CONF_SWAP_XY
class DsiDriverChip(DriverChip):
"""A driver chip for MIPI DSI displays."""
@property
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
DSI displays do not support axis swapping, so this method removes CONF_SWAP_XY
"""
return super().transforms - {CONF_SWAP_XY}
@@ -1,8 +1,7 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
DriverChip(
DsiDriverChip(
"JC1060P470",
width=1024,
height=600,
@@ -14,7 +13,6 @@ DriverChip(
vsync_front_porch=12,
pclk_frequency="54MHz",
lane_bit_rate="750Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00),
@@ -46,7 +44,7 @@ DriverChip(
# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42)
# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
DsiDriverChip(
"JC4880P443",
width=480,
height=800,
@@ -58,7 +56,6 @@ DriverChip(
vsync_front_porch=166,
pclk_frequency="34MHz",
lane_bit_rate="500Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=5,
initsequence=[
@@ -111,7 +108,7 @@ DriverChip(
# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40)
# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
DsiDriverChip(
"JC8012P4A1",
width=800,
height=1280,
@@ -123,7 +120,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="60MHz",
lane_bit_rate="1Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
+59 -7
View File
@@ -1,8 +1,7 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
DriverChip(
DsiDriverChip(
"M5STACK-TAB5",
height=1280,
width=720,
@@ -14,7 +13,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="60MHz",
lane_bit_rate="730Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xFF, 0x98, 0x81, 0x01), # Select Page 1
@@ -56,8 +54,8 @@ DriverChip(
],
)
DriverChip(
"M5STACK-TAB5-V2",
TAB5_ST7123 = DsiDriverChip(
"M5STACK-TAB5-ST7123",
height=1280,
width=720,
hsync_back_porch=40,
@@ -68,7 +66,6 @@ DriverChip(
vsync_front_porch=220,
pclk_frequency="80MHz",
lane_bit_rate="960Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x01,),
@@ -97,3 +94,58 @@ DriverChip(
(0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
],
)
TAB5_ST7123.extend(
"M5STACK-TAB5-V2",
deprecation_reason="Use 'M5STACK-TAB5-ST7123' or 'M5STACK-TAB5-ST7121' instead."
)
# Some Tab5 "v2" units ship with an ST7121 controller instead of the ST7123.
# The two are distinguishable at runtime by the touch controller firmware version (the M5
# factory firmware branches on it), but ESPHome selects the panel at compile time, so ST7121
# units must select this model explicitly. Values taken from M5's factory source
# (m5stack/M5Tab5-UserDemo: m5stack_tab5.c is_st7121 path + esp_lcd_st7121.c default table).
DsiDriverChip(
"M5STACK-TAB5-ST7121",
height=1280,
width=720,
hsync_back_porch=40,
hsync_pulse_width=2,
hsync_front_porch=40,
vsync_back_porch=24,
vsync_pulse_width=20,
vsync_front_porch=200,
pclk_frequency="70MHz",
lane_bit_rate="965Mbps",
color_order="RGB",
initsequence=[
(0x01,),
(0x60, 0x71, 0x21, 0xA2),
(0x60, 0x71, 0x21, 0xA3),
(0x60, 0x71, 0x21, 0xA4),
(0x78, 0x21),
(0x79, 0xEF),
(0xA4, 0x31),
(0xB7, 0x00, 0x00, 0x5F, 0x5F, 0x44, 0x1A),
(0xB0, 0x22, 0x6B, 0x11, 0x89, 0x25, 0x43, 0x43),
(0xBF, 0xA7, 0xA7),
(0xA5, 0xF0, 0x03),
(0xD7, 0x10, 0x2C, 0x14, 0x2A, 0x80, 0x80),
(0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x11, 0x21),
(0xA3, 0x80, 0x01, 0x8C, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x10, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x10, 0xEF, 0x58, 0x00, 0x00, 0x00, 0xFF),
(0xA6, 0x0A, 0x00, 0x24, 0x71, 0x36, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x24, 0x71, 0x37, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x24, 0x71, 0x00, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x00, 0x2C, 0x71, 0x00, 0x01, 0x00, 0x00, 0x68, 0x68, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00),
(0xA7, 0x1A, 0x1A, 0xC0, 0x64, 0x40, 0x04, 0x15, 0x40, 0x00, 0x40, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x26, 0x37, 0x40, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x8C, 0x9D, 0x40, 0x00, 0x00, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0xAE, 0xBF, 0x00, 0x00, 0x20, 0x00, 0x68, 0x68, 0x91, 0xFF, 0x08, 0x80, 0x79),
(0xAC, 0x1D, 0x18, 0x19, 0x1D, 0x18, 0x19, 0x04, 0x1C, 0x1D, 0x08, 0x0A, 0x10, 0x12, 0x0C, 0x0E, 0x14, 0x16, 0x00, 0x1D, 0x1D, 0x1D, 0x1D, 0x1D, 0x18, 0x19, 0x1D, 0x18, 0x19, 0x06, 0x1C, 0x1D, 0x09, 0x0B, 0x11, 0x13, 0x0D, 0x0F, 0x15, 0x17, 0x02, 0x1D, 0x1D, 0x1D, 0x1D),
(0xAD, 0x0C, 0x40, 0x46, 0x00, 0x07, 0x4B, 0x4B, 0xFF, 0xFF, 0xF0, 0x40, 0x0E, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF),
(0xAE, 0xF0, 0xFF, 0x03, 0xF0, 0xFF, 0x03, 0x00),
(0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0x2D, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x60, 0xFD, 0x20, 0xC0, 0x00),
(0xE8, 0x20, 0x60, 0x04, 0x8E, 0x8E, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E),
(0x75, 0x03, 0x04),
(0xE7, 0x4B, 0x00, 0x00, 0xBE, 0x4B, 0x8C, 0x20, 0x1A, 0xF0, 0x7D, 0x14, 0x7D, 0x14, 0x7D, 0x14, 0x7D, 0x14, 0xFF, 0x00, 0x32, 0x30, 0x73, 0x00, 0x00, 0xC8, 0x6A, 0xFF, 0x5A, 0x64, 0x38, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x64, 0x01, 0x01, 0x7C, 0xFF, 0x1A, 0x51),
(0xE1, 0x0C, 0x0C),
(0xEA, 0x15, 0x00, 0x01),
(0xC8, 0x00, 0x00, 0x04, 0x08, 0x10, 0x00, 0x1F, 0x01, 0x39, 0x3E, 0x00, 0x78, 0x06, 0xE2, 0x02, 0x11, 0x33, 0x01, 0x7A, 0x0D, 0x21, 0xC4, 0x0B, 0x19, 0x08, 0x32, 0xA0, 0x08, 0x1A, 0x0A, 0xF3, 0x7F, 0x0E, 0xC5, 0xE8, 0x03, 0xFF),
(0xC9, 0x00, 0x00, 0x04, 0x08, 0x10, 0x00, 0x1F, 0x01, 0x39, 0x3E, 0x00, 0x78, 0x06, 0xE2, 0x02, 0x11, 0x33, 0x01, 0x7A, 0x0D, 0x21, 0xC4, 0x0B, 0x19, 0x08, 0x32, 0xA0, 0x08, 0x1A, 0x0A, 0xF3, 0x7F, 0x0E, 0xC5, 0xE8, 0x03, 0xFF),
(0x60, 0x71, 0x21, 0x00),
],
)
+3 -4
View File
@@ -1,9 +1,8 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# Standalone display
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
DriverChip(
DsiDriverChip(
"SEEED-RETERMINAL-D1001",
height=1280,
width=800,
@@ -15,10 +14,10 @@ DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
reset_pin={"xl9535": None, "number": 2},
requires={"psram", "xl9535"},
initsequence=(
(0xE0, 0x00),
(0xE1, 0x93),
@@ -1,12 +1,11 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1
# Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage
JD9365_10_1_DSI_TOUCH_A = DriverChip(
JD9365_10_1_DSI_TOUCH_A = DsiDriverChip(
"WAVESHARE-P4-NANO-10.1",
height=1280,
width=800,
@@ -18,7 +17,6 @@ JD9365_10_1_DSI_TOUCH_A = DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -65,7 +63,7 @@ JD9365_10_1_DSI_TOUCH_A.extend(
# Source for parameters and initsequence:
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703
# Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO
DriverChip(
DsiDriverChip(
"WAVESHARE-P4-86-PANEL",
height=720,
width=720,
@@ -77,7 +75,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="38MHz",
lane_bit_rate="480Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
@@ -109,7 +106,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B
DriverChip(
DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B",
height=600,
width=1024,
@@ -139,7 +136,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C
JD9365_3_4_DSI_TOUCH_C = DriverChip(
JD9365_3_4_DSI_TOUCH_C = DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
height=800,
width=800,
@@ -151,7 +148,6 @@ JD9365_3_4_DSI_TOUCH_C = DriverChip(
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -197,7 +193,7 @@ JD9365_3_4_DSI_TOUCH_C.extend(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C
JD9365_4_DSI_TOUCH_C = DriverChip(
JD9365_4_DSI_TOUCH_C = DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
height=720,
width=720,
@@ -209,7 +205,6 @@ JD9365_4_DSI_TOUCH_C = DriverChip(
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -255,7 +250,7 @@ JD9365_4_DSI_TOUCH_C.extend(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A
DriverChip(
DsiDriverChip(
"WAVESHARE-8-DSI-TOUCH-A",
height=1280,
width=800,
@@ -267,7 +262,6 @@ DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -304,7 +298,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c
# Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A
DriverChip(
DsiDriverChip(
"WAVESHARE-7-DSI-TOUCH-A",
height=1280,
width=720,
+12 -19
View File
@@ -18,6 +18,8 @@ from esphome.components.mipi import (
CONF_HSYNC_BACK_PORCH,
CONF_HSYNC_FRONT_PORCH,
CONF_HSYNC_PULSE_WIDTH,
CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED,
CONF_PCLK_PIN,
CONF_PIXEL_MODE,
CONF_USE_AXIS_FLIPS,
@@ -34,9 +36,11 @@ from esphome.components.mipi import (
power_of_two,
requires_buffer,
)
from esphome.components.rpi_dpi_rgb.display import (
CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED,
from esphome.components.spi import (
CONF_SPI_MODE,
SPI_DATA_RATE_SCHEMA,
SPI_MODE_OPTIONS,
SPIComponent,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -48,7 +52,6 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_GREEN,
CONF_HSYNC_PIN,
@@ -57,8 +60,6 @@ from esphome.const import (
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_NUMBER,
CONF_RED,
@@ -72,10 +73,10 @@ from esphome.const import (
)
from esphome.final_validate import full_config
from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent
from . import models
from .models import RgbDriverChip
DEPENDENCIES = ["esp32", "psram"]
DEPENDENCIES = ["esp32"]
mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb")
mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component)
@@ -86,7 +87,7 @@ ColorOrder = display.display_ns.enum("ColorMode")
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
DriverChip("CUSTOM")
RgbDriverChip("CUSTOM")
# Import all models dynamically from the models package
@@ -120,16 +121,7 @@ def data_pin_set(length):
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# RPI model does not use an init sequence, indicates with empty list
if model.initsequence is None:
# Custom model requires an init sequence
@@ -235,6 +227,7 @@ def _config_schema(config):
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
model.get_dimensions(config)
)
@@ -0,0 +1,14 @@
from esphome.components.mipi import DriverChip
from esphome.const import CONF_SWAP_XY
class RgbDriverChip(DriverChip):
"""A driver chip for MIPI RGB displays."""
@property
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY
"""
return super().transforms - {CONF_SWAP_XY}
@@ -5,6 +5,7 @@ st7701s.extend(
width=480,
height=480,
data_rate="2MHz",
requires={"psram"},
cs_pin=39,
de_pin=18,
hsync_pin=16,
+2 -4
View File
@@ -1,5 +1,3 @@
from esphome.config_validation import UNDEFINED
from .st7701s import ST7701S
# fmt: off
@@ -8,10 +6,10 @@ ST7701S(
width=480,
height=480,
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
cs_pin={"xl9535": None, "number": 17},
reset_pin={"xl9535": None, "number": 5},
requires={"psram", "xl9535"},
hsync_pin=39,
vsync_pin=40,
pclk_pin=41,
@@ -57,9 +55,9 @@ t_rgb = ST7701S(
height=480,
pixel_mode="18bit",
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
cs_pin={"xl9535": None, "number": 3},
requires={"psram", "xl9535"},
de_pin=45,
hsync_pin=47,
vsync_pin=41,
+2 -4
View File
@@ -1,9 +1,7 @@
from esphome.components.mipi import DriverChip
from esphome.config_validation import UNDEFINED
from . import RgbDriverChip
# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence
DriverChip(
RgbDriverChip(
"RPI",
swap_xy=UNDEFINED,
initsequence=(),
)
+8 -10
View File
@@ -1,17 +1,12 @@
from esphome.components.mipi import (
MADCTL,
MADCTL_ML,
MADCTL_XFLIP,
MODE_BGR,
DriverChip,
)
from esphome.config_validation import UNDEFINED
from esphome.components.mipi import MADCTL, MADCTL_ML, MADCTL_XFLIP, MODE_BGR
from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y
from . import RgbDriverChip
SDIR_CMD = 0xC7
class ST7701S(DriverChip):
class ST7701S(RgbDriverChip):
# The ST7701s does not use the standard MADCTL bits for x/y mirroring
def add_madctl(self, sequence: list, config: dict):
transform = self.get_transform(config)
@@ -45,7 +40,6 @@ st7701s = ST7701S(
"ST7701S",
width=480,
height=864,
swap_xy=UNDEFINED,
hsync_front_porch=20,
hsync_back_porch=10,
hsync_pulse_width=10,
@@ -85,6 +79,7 @@ st7701s.extend(
height=480,
invert_colors=True,
pixel_mode="18bit",
requires={"psram"},
cs_pin=1,
de_pin={
"number": 45,
@@ -117,6 +112,7 @@ st7701s.extend(
vsync_pulse_width=8,
vsync_back_porch=20,
cs_pin={"pca9554": None, "number": 4},
requires={"psram", "pca9554"},
de_pin=18,
hsync_pin=16,
vsync_pin=17,
@@ -134,6 +130,7 @@ st7701s.extend(
width=480,
height=480,
pixel_mode="18bit",
requires={"psram"},
cs_pin=18,
reset_pin=8,
de_pin=17,
@@ -177,6 +174,7 @@ st7701s.extend(
width=480,
height=480,
pixel_mode="18bit",
requires={"psram"},
cs_pin=21,
de_pin=39,
vsync_pin=48,
+3 -5
View File
@@ -1,14 +1,13 @@
from esphome.components.mipi import DriverChip
from esphome.config_validation import UNDEFINED
from . import RgbDriverChip
# fmt: off
sunton = DriverChip(
sunton = RgbDriverChip(
"ESP32-8048S070",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="12.5MHz",
requires={"psram"},
de_pin=41,
hsync_pin=39,
vsync_pin=40,
@@ -28,7 +27,6 @@ sunton = DriverChip(
sunton.extend(
"ESP32-8048S050",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
@@ -1,18 +1,18 @@
from esphome.components.mipi import DriverChip, delay
from esphome.config_validation import UNDEFINED
from esphome.components.mipi import delay
from . import RgbDriverChip
from .st7701s import st7701s
# fmt: off
wave_4_3 = DriverChip(
wave_4_3 = RgbDriverChip(
"ESP32-S3-TOUCH-LCD-4.3",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="16MHz",
reset_pin={"ch422g": None, "number": 3},
enable_pin={"ch422g": None, "number": 2},
requires={"psram", "ch422g"},
de_pin=5,
hsync_pin={"number": 46, "ignore_strapping_warning": True},
vsync_pin={"number": 3, "ignore_strapping_warning": True},
@@ -69,6 +69,7 @@ st7701s.extend(
pclk_pin=41,
pclk_frequency="12MHz",
pclk_inverted=False,
requires={"psram"},
data_pins={
"red": [46, 3, 8, 18, 17],
"green": [14, 13, 12, 11, 10, 9],
@@ -80,6 +81,7 @@ st7701s.extend(
"WAVESHARE-3.16-320X820",
width=320,
height=820,
requires={"psram"},
de_pin=40,
hsync_pin=38,
vsync_pin=39,
+3 -14
View File
@@ -41,14 +41,11 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
@@ -138,16 +135,7 @@ def denominator(config):
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE]
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
@@ -265,6 +253,7 @@ def customise_schema(config):
extra=ALLOW_EXTRA,
)(config)
model = MODELS[config[CONF_MODEL]]
model.check_requirements()
bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL)
config = cv.Schema(
{
@@ -408,7 +397,7 @@ def get_instance(config):
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
var_id = config[CONF_ID]
init_sequence = model.get_sequence(config, False)
init_sequence = model.get_sequence(config, add_madctl=False, add_reset=True)
var_id.type, templateargs = get_instance(config)
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
cg.add(var.set_init_sequence(init_sequence))
+6 -19
View File
@@ -13,6 +13,8 @@ constexpr static const char *const TAG = "display.mipi_spi";
// Maximum bytes to log for commands (truncated if larger)
static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64;
// Command codes for MIPI SPI displays. Not all currently used, kept here for reference.
static constexpr uint8_t SW_RESET_CMD = 0x01;
static constexpr uint8_t SLEEP_OUT = 0x11;
static constexpr uint8_t NORON = 0x13;
@@ -151,14 +153,11 @@ class MipiSpi : public display::Display,
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
} else {
// no reset pin, send software reset command
this->write_command_(SW_RESET_CMD);
// required delay after reset is already in the init sequence, don't duplicate
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
@@ -170,6 +169,9 @@ class MipiSpi : public display::Display,
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
if (cmd == 0) {
cmd = clamp_at_least((int) (when - millis()), 0);
}
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
@@ -179,24 +181,9 @@ class MipiSpi : public display::Display,
this->mark_failed();
return;
}
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
esph_log_d(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
default:
break;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->reset_params_();
@@ -13,6 +13,7 @@ ST7789V.extend(
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
requires={"psram"},
)
ST7789V.extend(
@@ -25,4 +26,5 @@ ST7789V.extend(
dc_pin=39,
reset_pin=40,
invert_colors=True,
requires={"psram"},
)
+5 -2
View File
@@ -16,7 +16,7 @@ from esphome.components.mipi import (
delay,
)
from esphome.components.spi import TYPE_QUAD
from esphome.config_validation import UNDEFINED
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
DriverChip(
"T-DISPLAY-S3-AMOLED",
@@ -29,6 +29,7 @@ DriverChip(
brightness=0xD0,
color_order=MODE_RGB,
no_slpout=True, # SLPOUT is in the init sequence, early
requires={"psram"},
initsequence=(SLPOUT,),
)
@@ -43,6 +44,7 @@ DriverChip(
data_rate="40MHz",
brightness=0xD0,
color_order=MODE_RGB,
requires={"psram"},
initsequence=(
(PAGESEL, 4),
(0x6A, 0x00),
@@ -90,6 +92,7 @@ T4_S3_AMOLED = RM690B0.extend(
reset_pin=13,
enable_pin=9,
bus_mode=TYPE_QUAD,
requires={"psram"},
)
CO5300 = DriverChip(
@@ -98,7 +101,7 @@ CO5300 = DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
no_slpout=True,
swap_xy=UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
width=480,
height=480,
initsequence=(
@@ -314,6 +314,7 @@ DriverChip(
data_rate="40MHz",
dc_pin=4,
cs_pin=5,
requires={"psram"},
# reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48},
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
@@ -379,6 +380,7 @@ DriverChip(
cs_pin=5,
dc_pin=4,
reset_pin=48,
requires={"psram"},
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
(0xCF, 0x00, 0xC1, 0x30),
@@ -711,6 +713,7 @@ ST7796.extend(
reset_pin=4,
dc_pin={"number": 0, "ignore_strapping_warning": True},
invert_colors=True,
requires={"psram"},
)
ST7789V.extend(
+12 -3
View File
@@ -1,14 +1,19 @@
from esphome.components.mipi import MODE_RGB, DriverChip
from esphome.components.spi import TYPE_QUAD
import esphome.config_validation as cv
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
from esphome.const import (
CONF_IGNORE_STRAPPING_WARNING,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_NUMBER,
)
AXS15231 = DriverChip(
"AXS15231",
draw_rounding=8,
swap_xy=cv.UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
no_swreset=True,
initsequence=(
(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5),
(0xC1, 0x33),
@@ -22,6 +27,7 @@ AXS15231.extend(
height=480,
cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True},
data_rate="40MHz",
requires={"psram"},
)
DriverChip(
@@ -36,6 +42,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="40MHz",
requires={"psram"},
initsequence=(
(0xF0, 0x08),
(0xF2, 0x08),
@@ -267,6 +274,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="40MHz",
requires={"psram"},
initsequence=(
(0xF0, 0x28),
(0xF2, 0x28),
@@ -495,6 +503,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="20MHz",
requires={"psram"},
initsequence=(
(0xFF, 0xA5),
(0x41, 0x03),
@@ -10,4 +10,5 @@ ST7789V.extend(
cs_pin=22,
dc_pin=21,
reset_pin=18,
requires={"psram"},
)
@@ -15,6 +15,7 @@ ST7789V.extend(
dc_pin=13,
reset_pin=9,
data_rate="80MHz",
requires={"psram"},
)
ST7789V.extend(
@@ -42,6 +43,7 @@ ST7789V.extend(
enable_pin=[9, 15],
data_rate="10MHz",
bus_mode=TYPE_OCTAL,
requires={"psram"},
)
ST7796.extend(
@@ -55,4 +57,5 @@ ST7796.extend(
dc_pin=9,
backlight_pin=48,
invert_colors=True,
requires={"psram"},
)
@@ -49,6 +49,7 @@ ILI9341.extend(
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
requires={"psram"},
)
GC9107 = ST7789V.extend(
@@ -68,4 +69,5 @@ GC9107.extend(
reset_pin=48,
dc_pin=42,
cs_pin=14,
requires={"psram"},
)
@@ -12,7 +12,7 @@ from esphome.components.mipi import (
PWSET,
DriverChip,
)
import esphome.config_validation as cv
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
from .amoled import CO5300
from .ili import ILI9488_A, ST7789V
@@ -155,7 +155,7 @@ ST7789P = DriverChip(
ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5",
swap_xy=cv.UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
spi_16=True,
pixel_mode="16bit",
mirror_x=True,
@@ -175,6 +175,7 @@ CO5300.extend(
offset_width=6,
cs_pin=12,
reset_pin=39,
requires={"psram"},
)
# Waveshare ESP32-S3 Touch AMOLED 2.16" (CO5300 controller)
@@ -189,6 +190,7 @@ CO5300.extend(
cs_pin=12,
reset_pin=39,
data_rate="40MHz",
requires={"psram"},
)
AXS15231.extend(
@@ -198,6 +200,7 @@ AXS15231.extend(
data_rate="80MHz",
cs_pin=9,
reset_pin=21,
requires={"psram"},
)
# Waveshare 1.83-v2
@@ -281,6 +284,7 @@ ST7789V.extend(
offset_height=40,
invert_colors=True,
data_rate="40MHz",
requires={"psram"},
)
CO5300.extend(
@@ -291,4 +295,5 @@ CO5300.extend(
cs_pin=9,
reset_pin=21,
enable_pin=1,
requires={"psram"},
)

Some files were not shown because too many files have changed in this diff Show More