Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston 2025-07-23 17:54:53 -10:00
commit 42862ec5b5
No known key found for this signature in database
38 changed files with 1011 additions and 841 deletions

View File

@ -36,6 +36,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename... Ts> class TemplatableKeyValuePair { template<typename... Ts> class TemplatableKeyValuePair {
public: public:
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key; std::string key;
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
@ -47,11 +50,16 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(key, value); } // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) { template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.emplace_back(key, value); this->data_template_.emplace_back(std::move(key), value);
}
template<typename T> void add_variable(std::string key, T value) {
this->variables_.emplace_back(std::move(key), value);
} }
template<typename T> void add_variable(std::string key, T value) { this->variables_.emplace_back(key, value); }
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;

View File

@ -3,8 +3,12 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_USE_PSRAM = "use_psram"

View File

@ -98,6 +98,16 @@ ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32S3, VARIANT_ESP32S3,
] ]
# Single-core ESP32 variants
SINGLE_CORE_VARIANTS = frozenset(
[
VARIANT_ESP32S2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
]
)
def get_cpu_frequencies(*frequencies): def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies] return [str(x) + "MHZ" for x in frequencies]
@ -714,7 +724,11 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_define(CoreModel.MULTI_ATOMICS) # Set threading model based on core count
if config[CONF_VARIANT] in SINGLE_CORE_VARIANTS:
cg.add_define(CoreModel.SINGLE)
else:
cg.add_define(CoreModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("lib_compat_mode", "strict")

View File

@ -1,77 +1,112 @@
# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 Import("env")
# pylint: disable=E0602
Import("env") # noqa
import os import os
import json
import shutil import shutil
import pathlib
import itertools
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: def merge_factory_bin(source, target, env):
try: """
import esptool Merges all flash sections into a single .factory.bin using esptool.
except ImportError: Attempts multiple methods to detect image layout: flasher_args.json, FLASH_EXTRA_IMAGES, fallback guesses.
env.Execute("$PYTHONEXE -m pip install esptool") """
else: firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
import subprocess build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
from SCons.Script import ARGUMENTS firmware_path = build_dir / firmware_name
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
chip = env.BoardConfig().get("build.mcu", "esp32")
# Copy over the default sdkconfig. sections = []
from os import path flasher_args_path = build_dir / "flasher_args.json"
if path.exists("./sdkconfig.defaults"): # 1. Try flasher_args.json
os.makedirs(".temp", exist_ok=True) if flasher_args_path.exists():
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") try:
with flasher_args_path.open() as f:
flash_data = json.load(f)
for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)):
file_path = pathlib.Path(fname)
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found - skipping")
except Exception as e:
print(f"Warning: Failed to parse flasher_args.json - {e}")
# 2. Try FLASH_EXTRA_IMAGES if flasher_args.json failed or was empty
if not sections:
flash_images = env.get("FLASH_EXTRA_IMAGES")
if flash_images:
print("Using FLASH_EXTRA_IMAGES from PlatformIO environment")
# flatten any nested lists
flat = list(itertools.chain.from_iterable(
x if isinstance(x, (list, tuple)) else [x] for x in flash_images
))
entries = [env.subst(x) for x in flat]
for i in range(0, len(entries) - 1, 2):
addr, fname = entries[i], entries[i + 1]
if isinstance(fname, (list, tuple)):
print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}")
continue
file_path = pathlib.Path(str(fname))
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found — skipping")
def esp32_create_combined_bin(source, target, env): # 3. Final fallback: guess standard image locations
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) if not sections:
if verbose: print("Fallback: guessing legacy image paths")
print("Generating combined binary for serial flashing") guesses = [
app_offset = 0x10000 ("0x0", build_dir / "bootloader" / "bootloader.bin"),
("0x8000", build_dir / "partition_table" / "partition-table.bin"),
("0xe000", build_dir / "ota_data_initial.bin"),
("0x10000", firmware_path)
]
for addr, file_path in guesses:
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found — skipping")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") # If no valid sections found, skip merge
sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) if not sections:
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") print("No valid flash sections found — skipping .factory.bin creation.")
chip = env.get("BOARD_MCU") return
flash_size = env.BoardConfig().get("upload.flash_size")
output_path = firmware_path.with_suffix(".factory.bin")
cmd = [ cmd = [
"--chip", "--chip", chip,
chip,
"merge_bin", "merge_bin",
"-o", "--flash_size", flash_size,
new_file_name, "--output", str(output_path)
"--flash_size",
flash_size,
] ]
if verbose: for addr, file_path in sections:
print(" Offset | File") cmd += [addr, file_path]
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
if verbose:
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
cmd += [hex(app_offset), firmware_name] print(f"Merging binaries into {output_path}")
result = env.Execute(
env.VerboseAction(
f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd),
"Merging binaries with esptool"
)
)
if verbose: if result == 0:
print(f" - {hex(app_offset)} | {firmware_name}") print(f"Successfully created {output_path}")
print()
print(f"Using esptool.py arguments: {' '.join(cmd)}")
print()
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
esptool.main(cmd)
else: else:
subprocess.run(["esptool.py", *cmd]) print(f"Error: esptool merge_bin failed with code {result}")
def esp32_copy_ota_bin(source, target, env): def esp32_copy_ota_bin(source, target, env):
"""
Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools.
"""
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name) shutil.copyfile(firmware_name, new_file_name)
print(f"Copied firmware to {new_file_name}")
# Run merge first, then ota copy second
# pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa

View File

@ -4,6 +4,7 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, light from esphome.components import esp32, light
from esphome.components.const import CONF_USE_PSRAM
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHIPSET, CONF_CHIPSET,
@ -57,7 +58,6 @@ CHIPSETS = {
"SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
} }
CONF_USE_PSRAM = "use_psram"
CONF_IS_WRGB = "is_wrgb" CONF_IS_WRGB = "is_wrgb"
CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low" CONF_BIT0_LOW = "bit0_low"

View File

@ -8,6 +8,8 @@ namespace gt911 {
static const char *const TAG = "gt911.touchscreen"; static const char *const TAG = "gt911.touchscreen";
static const uint8_t PRIMARY_ADDRESS = 0x5D; // default I2C address for GT911
static const uint8_t SECONDARY_ADDRESS = 0x14; // secondary I2C address for GT911
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \ #define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \ if ((err) != i2c::ERROR_OK) { \
ESP_LOGE(TAG, "Failed to communicate!"); \ this->status_set_warning("Communication failure"); \
this->status_set_warning(); \
return; \ return; \
} }
@ -30,31 +31,31 @@ void GT911Touchscreen::setup() {
this->reset_pin_->setup(); this->reset_pin_->setup();
this->reset_pin_->digital_write(false); this->reset_pin_->digital_write(false);
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
// The interrupt pin is used as an input during reset to select the I2C address. // temporarily set the interrupt pin to output to control address selection
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->interrupt_pin_->setup();
this->interrupt_pin_->digital_write(false); this->interrupt_pin_->digital_write(false);
} }
delay(2); delay(2);
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
delay(50); // NOLINT delay(50); // NOLINT
if (this->interrupt_pin_ != nullptr) { }
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup(); // set pre-configured input mode
} this->interrupt_pin_->setup();
} }
// check the configuration of the int line. // check the configuration of the int line.
uint8_t data[4]; uint8_t data[4];
err = this->write(GET_SWITCHES, 2); err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
this->address_ = SECONDARY_ADDRESS;
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
}
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->read(data, 1); err = this->read(data, 1);
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
// datasheet says NOT to use pullup/down on the int line.
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
} }
@ -63,7 +64,7 @@ void GT911Touchscreen::setup() {
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
// no calibration? Attempt to read the max values from the touchscreen. // no calibration? Attempt to read the max values from the touchscreen.
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2); err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES));
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data)); err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
@ -75,15 +76,12 @@ void GT911Touchscreen::setup() {
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!"); this->mark_failed("Failed to read calibration");
this->mark_failed();
return; return;
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!"); this->mark_failed("Failed to communicate");
this->mark_failed();
return;
} }
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() {
uint8_t touch_state = 0; uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE));
ERROR_CHECK(err); ERROR_CHECK(err);
err = this->read(&touch_state, 1); err = this->read(&touch_state, 1);
ERROR_CHECK(err); ERROR_CHECK(err);
@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() {
return; return;
} }
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES));
ERROR_CHECK(err); ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data // num_of_touches is guaranteed to be 0..5. Also read the key data
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
} }
} // namespace gt911 } // namespace gt911

View File

@ -94,7 +94,7 @@ class I2CBus {
protected: protected:
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag. /// that contains the address and the corresponding bool presence flag.
void i2c_scan_() { virtual void i2c_scan() {
for (uint8_t address = 8; address < 120; address++) { for (uint8_t address = 8; address < 120; address++) {
auto err = writev(address, nullptr, 0); auto err = writev(address, nullptr, 0);
if (err == ERROR_OK) { if (err == ERROR_OK) {

View File

@ -42,7 +42,7 @@ void ArduinoI2CBus::setup() {
this->initialized_ = true; this->initialized_ = true;
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices"); ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_(); this->i2c_scan();
} }
} }

View File

@ -1,13 +1,13 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "i2c_bus_esp_idf.h" #include "i2c_bus_esp_idf.h"
#include <driver/gpio.h>
#include <cinttypes> #include <cinttypes>
#include <cstring> #include <cstring>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <driver/gpio.h>
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0)
#define SOC_HP_I2C_NUM SOC_I2C_NUM #define SOC_HP_I2C_NUM SOC_I2C_NUM
@ -78,7 +78,7 @@ void IDFI2CBus::setup() {
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning for devices"); ESP_LOGV(TAG, "Scanning for devices");
this->i2c_scan_(); this->i2c_scan();
} }
#else #else
#if SOC_HP_I2C_NUM > 1 #if SOC_HP_I2C_NUM > 1
@ -125,7 +125,7 @@ void IDFI2CBus::setup() {
initialized_ = true; initialized_ = true;
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices"); ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_(); this->i2c_scan();
} }
#endif #endif
} }
@ -167,6 +167,17 @@ void IDFI2CBus::dump_config() {
} }
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
void IDFI2CBus::i2c_scan() {
for (uint8_t address = 8; address < 120; address++) {
auto err = i2c_master_probe(this->bus_, address, 20);
if (err == ESP_OK) {
this->scan_results_.emplace_back(address, true);
}
}
}
#endif
ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
// logging is only enabled with vv level, if warnings are shown the caller // logging is only enabled with vv level, if warnings are shown the caller
// should log them // should log them

View File

@ -2,9 +2,9 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "esp_idf_version.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "i2c_bus.h" #include "i2c_bus.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#else #else
@ -46,6 +46,7 @@ class IDFI2CBus : public InternalI2CBus, public Component {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_master_dev_handle_t dev_; i2c_master_dev_handle_t dev_;
i2c_master_bus_handle_t bus_; i2c_master_bus_handle_t bus_;
void i2c_scan() override;
#endif #endif
i2c_port_t port_; i2c_port_t port_;
uint8_t sda_pin_; uint8_t sda_pin_;

View File

@ -1,6 +1,6 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C3, VARIANT_ESP32C3,
@ -258,6 +258,10 @@ async def to_code(config):
if use_legacy(): if use_legacy():
cg.add_define("USE_I2S_LEGACY") cg.add_define("USE_I2S_LEGACY")
# Helps avoid callbacks being skipped due to processor load
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config: if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))

View File

@ -9,6 +9,7 @@
#endif #endif
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@ -19,72 +20,33 @@
namespace esphome { namespace esphome {
namespace i2s_audio { namespace i2s_audio {
static const uint8_t DMA_BUFFER_DURATION_MS = 15; static const uint32_t DMA_BUFFER_DURATION_MS = 15;
static const size_t DMA_BUFFERS_COUNT = 4; static const size_t DMA_BUFFERS_COUNT = 4;
static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2;
static const size_t TASK_STACK_SIZE = 4096; static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23; static const ssize_t TASK_PRIORITY = 19;
static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
static const char *const TAG = "i2s_audio.speaker"; static const char *const TAG = "i2s_audio.speaker";
enum SpeakerEventGroupBits : uint32_t { enum SpeakerEventGroupBits : uint32_t {
COMMAND_START = (1 << 0), // starts the speaker task COMMAND_START = (1 << 0), // indicates loop should start speaker task
COMMAND_STOP = (1 << 1), // stops the speaker task COMMAND_STOP = (1 << 1), // stops the speaker task
COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11), TASK_STARTING = (1 << 10),
STATE_STOPPING = (1 << 12), TASK_RUNNING = (1 << 11),
STATE_STOPPED = (1 << 13), TASK_STOPPING = (1 << 12),
ERR_TASK_FAILED_TO_START = (1 << 14), TASK_STOPPED = (1 << 13),
ERR_ESP_INVALID_STATE = (1 << 15),
ERR_ESP_NOT_SUPPORTED = (1 << 16),
ERR_ESP_INVALID_ARG = (1 << 17),
ERR_ESP_INVALID_SIZE = (1 << 18),
ERR_ESP_NO_MEM = (1 << 19), ERR_ESP_NO_MEM = (1 << 19),
ERR_ESP_FAIL = (1 << 20),
ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_NOT_SUPPORTED | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | WARN_DROPPED_EVENT = (1 << 20),
ERR_ESP_NO_MEM | ERR_ESP_FAIL,
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
}; };
// Translates a SpeakerEventGroupBits ERR_ESP bit to the coressponding esp_err_t
static esp_err_t err_bit_to_esp_err(uint32_t bit) {
switch (bit) {
case SpeakerEventGroupBits::ERR_ESP_INVALID_STATE:
return ESP_ERR_INVALID_STATE;
case SpeakerEventGroupBits::ERR_ESP_INVALID_ARG:
return ESP_ERR_INVALID_ARG;
case SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE:
return ESP_ERR_INVALID_SIZE;
case SpeakerEventGroupBits::ERR_ESP_NO_MEM:
return ESP_ERR_NO_MEM;
case SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED:
return ESP_ERR_NOT_SUPPORTED;
default:
return ESP_FAIL;
}
}
/// @brief Multiplies the input array of Q15 numbers by a Q15 constant factor
///
/// Based on `dsps_mulc_s16_ansi` from the esp-dsp library:
/// https://github.com/espressif/esp-dsp/blob/master/modules/math/mulc/fixed/dsps_mulc_s16_ansi.c
/// (accessed on 2024-09-30).
/// @param input Array of Q15 numbers
/// @param output Array of Q15 numbers
/// @param len Length of array
/// @param c Q15 constant factor
static void q15_multiplication(const int16_t *input, int16_t *output, size_t len, int16_t c) {
for (int i = 0; i < len; i++) {
int32_t acc = (int32_t) input[i] * (int32_t) c;
output[i] = (int16_t) (acc >> 15);
}
}
// Lists the Q15 fixed point scaling factor for volume reduction. // Lists the Q15 fixed point scaling factor for volume reduction.
// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. // Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB.
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
@ -132,51 +94,80 @@ void I2SAudioSpeaker::dump_config() {
void I2SAudioSpeaker::loop() { void I2SAudioSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
ESP_LOGD(TAG, "Starting");
this->state_ = speaker::STATE_STARTING; this->state_ = speaker::STATE_STARTING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) {
// Handle the task's state
if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
}
if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) {
ESP_LOGD(TAG, "Started"); ESP_LOGD(TAG, "Started");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
this->state_ = speaker::STATE_RUNNING; this->state_ = speaker::STATE_RUNNING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING);
this->status_clear_warning();
this->status_clear_error();
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
ESP_LOGD(TAG, "Stopping"); ESP_LOGD(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
this->state_ = speaker::STATE_STOPPING; this->state_ = speaker::STATE_STOPPING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) {
if (!this->task_created_) { ESP_LOGD(TAG, "Stopped");
ESP_LOGD(TAG, "Stopped");
this->state_ = speaker::STATE_STOPPED; vTaskDelete(this->speaker_task_handle_);
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); this->speaker_task_handle_ = nullptr;
this->speaker_task_handle_ = nullptr;
} this->stop_i2s_driver_();
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
this->status_clear_error();
this->state_ = speaker::STATE_STOPPED;
} }
if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { // Log any errors encounted by the task
this->status_set_error("Failed to start task"); if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); ESP_LOGE(TAG, "Not enough memory");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} }
if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { // Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy
uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) {
ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced");
this->status_set_warning(); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT);
} }
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { // Handle the speaker's state
this->status_set_error("Failed to adjust bus to match incoming audio"); switch (this->state_) {
ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", case speaker::STATE_STARTING:
this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), if (this->status_has_error()) {
this->audio_stream_info_.get_bits_per_sample()); break;
} }
xEventGroupClearBits(this->event_group_, ALL_ERR_ESP_BITS); if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) {
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver-faiure", 1000);
break;
}
if (this->speaker_task_handle_ == nullptr) {
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
}
break;
case speaker::STATE_RUNNING: // Intentional fallthrough
case speaker::STATE_STOPPING: // Intentional fallthrough
case speaker::STATE_STOPPED:
break;
}
} }
void I2SAudioSpeaker::set_volume(float volume) { void I2SAudioSpeaker::set_volume(float volume) {
@ -227,83 +218,76 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
this->start(); this->start();
} }
if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) { if (this->state_ != speaker::STATE_RUNNING) {
// Unable to write data to a running speaker, so delay the max amount of time so it can get ready // Unable to write data to a running speaker, so delay the max amount of time so it can get ready
vTaskDelay(ticks_to_wait); vTaskDelay(ticks_to_wait);
ticks_to_wait = 0; ticks_to_wait = 0;
} }
size_t bytes_written = 0; size_t bytes_written = 0;
if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { if (this->state_ == speaker::STATE_RUNNING) {
// Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
// attempting to write to it. if (temp_ring_buffer.use_count() == 2) {
// Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to
// Temporarily share ownership of the ring buffer so it won't be deallocated while writing bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_; }
bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
} }
return bytes_written; return bytes_written;
} }
bool I2SAudioSpeaker::has_buffered_data() const { bool I2SAudioSpeaker::has_buffered_data() const {
if (this->audio_ring_buffer_ != nullptr) { if (this->audio_ring_buffer_.use_count() > 0) {
return this->audio_ring_buffer_->available() > 0; std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
return temp_ring_buffer->available() > 0;
} }
return false; return false;
} }
void I2SAudioSpeaker::speaker_task(void *params) { void I2SAudioSpeaker::speaker_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
this_speaker->task_created_ = true;
uint32_t event_group_bits = xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING);
xEventGroupWaitBits(this_speaker->event_group_,
SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP |
SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY, // Bit message to read
pdTRUE, // Clear the bits on exit
pdFALSE, // Don't wait for all the bits,
portMAX_DELAY); // Block indefinitely until a bit is set
if (event_group_bits & (SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY)) {
// Received a stop signal before the task was requested to start
this_speaker->delete_task_(0);
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING);
audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_;
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers // Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_); const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_);
// The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info
const size_t data_buffer_size = audio_stream_info.ms_to_bytes(dma_buffers_duration_ms); const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration);
const size_t ring_buffer_size = audio_stream_info.ms_to_bytes(ring_buffer_duration);
const size_t single_dma_buffer_input_size = data_buffer_size / DMA_BUFFERS_COUNT; const uint32_t frames_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS);
const size_t bytes_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(data_buffer_size, ring_buffer_size))) { bool successful_setup = false;
// Failed to allocate buffers std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer =
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer);
this_speaker->delete_task_(data_buffer_size);
if (transfer_buffer != nullptr) {
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer.use_count() == 1) {
transfer_buffer->set_source(temp_ring_buffer);
this_speaker->audio_ring_buffer_ = temp_ring_buffer;
successful_setup = true;
}
} }
if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) { if (!successful_setup) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
bool stop_gracefully = false; bool stop_gracefully = false;
bool tx_dma_underflow = true;
uint32_t frames_written = 0;
uint32_t last_data_received_time = millis(); uint32_t last_data_received_time = millis();
bool tx_dma_underflow = false;
this_speaker->accumulated_frames_written_ = 0; xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
// Keep looping if paused, there is no timeout configured, or data was received more recently than the configured
// timeout
while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() || while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() ||
(millis() - last_data_received_time) <= this_speaker->timeout_.value()) { (millis() - last_data_received_time) <= this_speaker->timeout_.value()) {
event_group_bits = xEventGroupGetBits(this_speaker->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP); xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
@ -314,7 +298,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
stop_gracefully = true; stop_gracefully = true;
} }
if (this_speaker->audio_stream_info_ != audio_stream_info) { if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings. // Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break; break;
} }
@ -326,36 +310,75 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
} }
#else #else
bool overflow; int64_t write_timestamp;
while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) { while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) {
if (overflow) { // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
// on the timing info via the audio_output_callback.
uint32_t frames_sent = frames_to_fill_single_dma_buffer;
if (frames_to_fill_single_dma_buffer > frames_written) {
tx_dma_underflow = true; tx_dma_underflow = true;
frames_sent = frames_written;
const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written;
write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed);
} else {
tx_dma_underflow = false;
}
frames_written -= frames_sent;
if (frames_sent > 0) {
this_speaker->audio_output_callback_(frames_sent, write_timestamp);
} }
} }
#endif #endif
if (this_speaker->pause_state_) { if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe // Pause state is accessed atomically, so thread safe
// Delay so the task can yields, then skip transferring audio data // Delay so the task yields, then skip transferring audio data
delay(TASK_DELAY_MS); vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
continue; continue;
} }
size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, data_buffer_size, // Wait half the duration of the data already written to the DMA buffers for new audio data
pdMS_TO_TICKS(TASK_DELAY_MS)); // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000
const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
if (bytes_read > 0) { if (bytes_read > 0) {
if ((audio_stream_info.get_bits_per_sample() == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { if (this_speaker->q15_volume_factor_ < INT16_MAX) {
// Scale samples by the volume factor in place // Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it,
q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, // multiplying by the volume factor, and packing the sample back into the original bytes per sample.
bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_);
const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
// Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31
int32_t shift = 15; // Q31 -> Q16
int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15
if (bytes_per_sample >= 3) {
// Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31
shift = 8; // Q31 -> Q23
gain_factor >>= 7; // Q15 -> Q8
}
for (uint32_t i = 0; i < len; ++i) {
int32_t sample =
audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31
sample >>= shift;
sample *= gain_factor; // Q31
audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample);
}
} }
#ifdef USE_ESP32_VARIANT_ESP32 #ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 8/16 bit mono mode samples need to be switched. // For ESP32 8/16 bit mono mode samples need to be switched.
if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) { if (this_speaker->current_stream_info_.get_channels() == 1 &&
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
size_t len = bytes_read / sizeof(int16_t); size_t len = bytes_read / sizeof(int16_t);
int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_; int16_t *tmp_buf = (int16_t *) new_data;
for (int i = 0; i < len; i += 2) { for (int i = 0; i < len; i += 2) {
int16_t tmp = tmp_buf[i]; int16_t tmp = tmp_buf[i];
tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i] = tmp_buf[i + 1];
@ -363,62 +386,87 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
} }
#endif #endif
// Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played }
// callback.
const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size;
for (uint32_t i = 0; i < batches; ++i) { if (transfer_buffer->available() == 0) {
size_t bytes_written = 0;
size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read);
#ifdef USE_I2S_LEGACY
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(),
this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write,
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
}
#else
i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
#endif
int64_t now = esp_timer_get_time();
if (bytes_written != bytes_to_write) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
}
bytes_read -= bytes_written;
this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written),
now + dma_buffers_duration_ms * 1000);
tx_dma_underflow = false;
last_data_received_time = millis();
}
} else {
// No data received
if (stop_gracefully && tx_dma_underflow) { if (stop_gracefully && tx_dma_underflow) {
break; break;
} }
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2));
} else {
size_t bytes_written = 0;
#ifdef USE_I2S_LEGACY
if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
} else if (this_speaker->current_stream_info_.get_bits_per_sample() <
(uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(),
this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
}
#else
if (tx_dma_underflow) {
// Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing
// callbacks are accurate. Preload the data.
i2s_channel_disable(this_speaker->tx_handle_);
const i2s_event_callbacks_t callbacks = {
.on_sent = nullptr,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written);
} else {
// Audio is already playing, use regular I2S write to add to the DMA buffers
i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written, DMA_BUFFER_DURATION_MS);
}
#endif
if (bytes_written > 0) {
last_data_received_time = millis();
frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written);
transfer_buffer->decrease_buffer_length(bytes_written);
if (tx_dma_underflow) {
tx_dma_underflow = false;
#ifndef USE_I2S_LEGACY
// Reset the event queue timestamps
// Enable the on_sent callback to accurately track the timestamps of played audio
// Enable the I2S channel to start sending the preloaded audio
xQueueReset(this_speaker->i2s_event_queue_);
const i2s_event_callbacks_t callbacks = {
.on_sent = i2s_on_sent_cb,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_enable(this_speaker->tx_handle_);
#endif
}
#ifdef USE_I2S_LEGACY
// The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but
// less accurate approach.
this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written),
esp_timer_get_time() + dma_buffers_duration_ms * 1000);
#endif
}
} }
} }
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this_speaker->parent_->get_port());
#else
i2s_channel_disable(this_speaker->tx_handle_);
i2s_del_channel(this_speaker->tx_handle_);
#endif
this_speaker->parent_->unlock();
} }
this_speaker->delete_task_(data_buffer_size); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
if (transfer_buffer != nullptr) {
transfer_buffer.reset();
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
vTaskDelay(pdMS_TO_TICKS(10));
}
} }
void I2SAudioSpeaker::start() { void I2SAudioSpeaker::start() {
@ -427,16 +475,7 @@ void I2SAudioSpeaker::start() {
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return; return;
if (!this->task_created_ && (this->speaker_task_handle_ == nullptr)) { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
} else {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
}
}
} }
void I2SAudioSpeaker::stop() { this->stop_(false); } void I2SAudioSpeaker::stop() { this->stop_(false); }
@ -456,61 +495,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) {
} }
} }
bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) {
switch (err) {
case ESP_OK:
return false;
case ESP_ERR_INVALID_STATE:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_STATE);
return true;
case ESP_ERR_INVALID_ARG:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_ARG);
return true;
case ESP_ERR_INVALID_SIZE:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
return true;
case ESP_ERR_NO_MEM:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
return true;
case ESP_ERR_NOT_SUPPORTED:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED);
return true;
default:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL);
return true;
}
}
esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) {
if (this->data_buffer_ == nullptr) {
// Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus
RAMAllocator<uint8_t> allocator;
this->data_buffer_ = allocator.allocate(data_buffer_size);
}
if (this->data_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
if (this->audio_ring_buffer_.use_count() == 0) {
// Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated.
this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size);
}
if (this->audio_ring_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use
#ifdef USE_I2S_LEGACY #ifdef USE_I2S_LEGACY
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#else #else
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#endif #endif
// Can't reconfigure I2S bus, so the sample rate must match the configured value // Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration");
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
@ -521,10 +515,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
#endif #endif
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported");
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
if (!this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
ESP_LOGE(TAG, "Parent I2S bus not free");
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
@ -575,6 +571,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
esp_err_t err = esp_err_t err =
i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to install I2S legacy driver");
// Failed to install the driver, so unlock the I2S port // Failed to install the driver, so unlock the I2S port
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
@ -595,6 +592,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
if (err != ESP_OK) { if (err != ESP_OK) {
// Failed to set the data out pin, so uninstall the driver and unlock the I2S port // Failed to set the data out pin, so uninstall the driver and unlock the I2S port
ESP_LOGE(TAG, "Failed to set the data out pin");
i2s_driver_uninstall(this->parent_->get_port()); i2s_driver_uninstall(this->parent_->get_port());
this->parent_->unlock(); this->parent_->unlock();
} }
@ -605,10 +603,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
.dma_desc_num = DMA_BUFFERS_COUNT, .dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length, .dma_frame_num = dma_buffer_length,
.auto_clear = true, .auto_clear = true,
.intr_priority = 3,
}; };
/* Allocate a new TX channel and get the handle of this channel */ /* Allocate a new TX channel and get the handle of this channel */
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to allocate new I2S channel");
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
} }
@ -652,7 +652,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
// per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to
// make it play at the correct speed while sending more bits per slot. // make it play at the correct speed while sending more bits per slot.
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
std_slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_); uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
std_slot_cfg.ws_width = configured_bit_width;
if (configured_bit_width > 16) {
std_slot_cfg.msb_right = false;
}
} }
#else #else
std_slot_cfg.slot_bit_width = this->slot_bit_width_; std_slot_cfg.slot_bit_width = this->slot_bit_width_;
@ -670,54 +674,56 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize channel");
i2s_del_channel(this->tx_handle_); i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
} }
if (this->i2s_event_queue_ == nullptr) { if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool)); this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t));
} }
const i2s_event_callbacks_t callbacks = {
.on_send_q_ovf = i2s_overflow_cb,
};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
/* Before reading data, start the TX channel first */
i2s_channel_enable(this->tx_handle_); i2s_channel_enable(this->tx_handle_);
if (err != ESP_OK) {
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
}
#endif #endif
return err; return err;
} }
void I2SAudioSpeaker::delete_task_(size_t buffer_size) { #ifndef USE_I2S_LEGACY
this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
int64_t now = esp_timer_get_time();
if (this->data_buffer_ != nullptr) { BaseType_t need_yield1 = pdFALSE;
RAMAllocator<uint8_t> allocator; BaseType_t need_yield2 = pdFALSE;
allocator.deallocate(this->data_buffer_, buffer_size); BaseType_t need_yield3 = pdFALSE;
this->data_buffer_ = nullptr;
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
// Queue is full, so discard the oldest event and set the warning flag to inform the user
int64_t dummy;
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2);
} }
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
this->task_created_ = false; return need_yield1 | need_yield2 | need_yield3;
vTaskDelete(nullptr);
}
#ifndef USE_I2S_LEGACY
bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
bool overflow = true;
xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow);
return false;
} }
#endif #endif
void I2SAudioSpeaker::stop_i2s_driver_() {
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this->parent_->get_port());
#else
i2s_channel_disable(this->tx_handle_);
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
#endif
this->parent_->unlock();
}
} // namespace i2s_audio } // namespace i2s_audio
} // namespace esphome } // namespace esphome

View File

@ -72,70 +72,57 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
protected: protected:
/// @brief Function for the FreeRTOS task handling audio output. /// @brief Function for the FreeRTOS task handling audio output.
/// After receiving the COMMAND_START signal, allocates space for the buffers, starts the I2S driver, and reads /// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops
/// audio from the ring buffer and writes audio to the I2S port. Stops immmiately after receiving the COMMAND_STOP /// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving
/// signal and stops only after the ring buffer is empty after receiving the COMMAND_STOP_GRACEFULLY signal. Stops if /// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds.
/// the ring buffer hasn't read data for more than timeout_ milliseconds. When stopping, it deallocates the buffers, /// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``.
/// stops the I2S driver, unlocks the I2S port, and deletes the task. It communicates the state and any errors via
/// event_group_.
/// @param params I2SAudioSpeaker component /// @param params I2SAudioSpeaker component
static void speaker_task(void *params); static void speaker_task(void *params);
/// @brief Sends a stop command to the speaker task via event_group_. /// @brief Sends a stop command to the speaker task via ``event_group_``.
/// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal.
void stop_(bool wait_on_empty); void stop_(bool wait_on_empty);
/// @brief Sets the corresponding ERR_ESP event group bits.
/// @param err esp_err_t error code.
/// @return True if an ERR_ESP bit is set and false if err == ESP_OK
bool send_esp_err_to_event_group_(esp_err_t err);
#ifndef USE_I2S_LEGACY #ifndef USE_I2S_LEGACY
static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); /// @brief Callback function used to send playback timestamps the to the speaker task.
/// @param handle (i2s_chan_handle_t)
/// @param event (i2s_event_data_t)
/// @param user_ctx (void*) User context pointer that the callback accesses
/// @return True if a higher priority task was interrupted
static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
#endif #endif
/// @brief Allocates the data buffer and ring buffer
/// @param data_buffer_size Number of bytes to allocate for the data buffer.
/// @param ring_buffer_size Number of bytes to allocate for the ring buffer.
/// @return ESP_ERR_NO_MEM if either buffer fails to allocate
/// ESP_OK if successful
esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size);
/// @brief Starts the ESP32 I2S driver. /// @brief Starts the ESP32 I2S driver.
/// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out
/// pin. If it fails, it will unlock the I2S port and uninstall the driver, if necessary. /// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary.
/// @param audio_stream_info Stream information for the I2S driver. /// @param audio_stream_info Stream information for the I2S driver.
/// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream. /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream.
/// ESP_ERR_INVALID_STATE if the I2S port is already locked. /// ESP_ERR_INVALID_STATE if the I2S port is already locked.
/// ESP_ERR_INVALID_ARG if nstalling the driver or setting the data outpin fails due to a parameter error. /// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error.
/// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error.
/// ESP_FAIL if setting the data out pin fails due to an IO error ESP_OK if successful /// ESP_FAIL if setting the data out pin fails due to an IO error
/// ESP_OK if successful
esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info); esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info);
/// @brief Deletes the speaker's task. /// @brief Stops the I2S driver and unlocks the I2S port
/// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by void stop_i2s_driver_();
/// the speaker_task itself.
/// @param buffer_size The allocated size of the data_buffer_.
void delete_task_(size_t buffer_size);
TaskHandle_t speaker_task_handle_{nullptr}; TaskHandle_t speaker_task_handle_{nullptr};
EventGroupHandle_t event_group_{nullptr}; EventGroupHandle_t event_group_{nullptr};
QueueHandle_t i2s_event_queue_; QueueHandle_t i2s_event_queue_;
uint8_t *data_buffer_; std::weak_ptr<RingBuffer> audio_ring_buffer_;
std::shared_ptr<RingBuffer> audio_ring_buffer_;
uint32_t buffer_duration_ms_; uint32_t buffer_duration_ms_;
optional<uint32_t> timeout_; optional<uint32_t> timeout_;
bool task_created_{false};
bool pause_state_{false}; bool pause_state_{false};
int16_t q15_volume_factor_{INT16_MAX}; int16_t q15_volume_factor_{INT16_MAX};
size_t bytes_written_{0}; audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info
#ifdef USE_I2S_LEGACY #ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
@ -148,8 +135,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
std::string i2s_comm_fmt_; std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_; i2s_chan_handle_t tx_handle_;
#endif #endif
uint32_t accumulated_frames_written_{0};
}; };
} // namespace i2s_audio } // namespace i2s_audio

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
namespace esphome { namespace esphome {
@ -8,16 +9,12 @@ namespace interval {
class IntervalTrigger : public Trigger<>, public PollingComponent { class IntervalTrigger : public Trigger<>, public PollingComponent {
public: public:
void update() override { void update() override { this->trigger(); }
if (this->started_)
this->trigger();
}
void setup() override { void setup() override {
if (this->startup_delay_ == 0) { if (this->startup_delay_ != 0) {
this->started_ = true; this->stop_poller();
} else { this->set_timeout(this->startup_delay_, [this] { this->start_poller(); });
this->set_timeout(this->startup_delay_, [this] { this->started_ = true; });
} }
} }
@ -25,7 +22,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent {
protected: protected:
uint32_t startup_delay_{0}; uint32_t startup_delay_{0};
bool started_{false};
}; };
} // namespace interval } // namespace interval

View File

@ -477,10 +477,11 @@ void LD2450Component::handle_periodic_data_() {
// X // X
start = TARGET_X + index * 8; start = TARGET_X + index * 8;
is_moving = false; is_moving = false;
// tx is used for further calculations, so always needs to be populated
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val;
sensor::Sensor *sx = this->move_x_sensors_[index]; sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) { if (sx != nullptr) {
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
tx = val;
if (this->cached_target_data_[index].x != val) { if (this->cached_target_data_[index].x != val) {
sx->publish_state(val); sx->publish_state(val);
this->cached_target_data_[index].x = val; this->cached_target_data_[index].x = val;
@ -488,10 +489,11 @@ void LD2450Component::handle_periodic_data_() {
} }
// Y // Y
start = TARGET_Y + index * 8; start = TARGET_Y + index * 8;
// ty is used for further calculations, so always needs to be populated
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val;
sensor::Sensor *sy = this->move_y_sensors_[index]; sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) { if (sy != nullptr) {
val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
ty = val;
if (this->cached_target_data_[index].y != val) { if (this->cached_target_data_[index].y != val) {
sy->publish_state(val); sy->publish_state(val);
this->cached_target_data_[index].y = val; this->cached_target_data_[index].y = val;

View File

@ -43,12 +43,15 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_ACCOUNT_GROUP, icon=ICON_ACCOUNT_GROUP,
accuracy_decimals=0,
), ),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_HUMAN_GREETING_PROXIMITY, icon=ICON_HUMAN_GREETING_PROXIMITY,
accuracy_decimals=0,
), ),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_ACCOUNT_SWITCH, icon=ICON_ACCOUNT_SWITCH,
accuracy_decimals=0,
), ),
} }
) )
@ -95,12 +98,15 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{ {
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT, icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
), ),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT, icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
), ),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT, icon=ICON_MAP_MARKER_ACCOUNT,
accuracy_decimals=0,
), ),
} }
) )

View File

@ -119,9 +119,6 @@ void Logger::pre_setup() {
#ifdef USE_LOGGER_USB_CDC #ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC: case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial; this->hw_serial_ = &Serial;
#if ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection
#endif
Serial.begin(this->baud_rate_); Serial.begin(this->baud_rate_);
break; break;
#endif #endif

View File

@ -2,7 +2,7 @@ import logging
from esphome.automation import build_automation, register_action, validate_automation from esphome.automation import build_automation, register_action, validate_automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_DRAW_ROUNDING from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
from esphome.components.display import Display from esphome.components.display import Display
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -186,7 +186,7 @@ def multi_conf_validate(configs: list[dict]):
for config in configs[1:]: for config in configs[1:]:
for item in ( for item in (
df.CONF_LOG_LEVEL, df.CONF_LOG_LEVEL,
df.CONF_COLOR_DEPTH, CONF_COLOR_DEPTH,
df.CONF_BYTE_ORDER, df.CONF_BYTE_ORDER,
df.CONF_TRANSPARENCY_KEY, df.CONF_TRANSPARENCY_KEY,
): ):
@ -275,11 +275,11 @@ async def to_code(configs):
"LVGL_LOG_LEVEL", "LVGL_LOG_LEVEL",
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"),
) )
add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH]) add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
for font in helpers.lv_fonts_used: for font in helpers.lv_fonts_used:
add_define(f"LV_FONT_{font.upper()}") add_define(f"LV_FONT_{font.upper()}")
if config_0[df.CONF_COLOR_DEPTH] == 16: if config_0[CONF_COLOR_DEPTH] == 16:
add_define( add_define(
"LV_COLOR_16_SWAP", "LV_COLOR_16_SWAP",
"1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0",
@ -416,7 +416,7 @@ LVGL_SCHEMA = cv.All(
{ {
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
cv.GenerateID(df.CONF_DISPLAYS): display_schema, cv.GenerateID(df.CONF_DISPLAYS): display_schema,
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16),
cv.Optional( cv.Optional(
df.CONF_DEFAULT_FONT, default="montserrat_14" df.CONF_DEFAULT_FONT, default="montserrat_14"
): lvalid.lv_font, ): lvalid.lv_font,

View File

@ -418,7 +418,6 @@ CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate" CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button" CONF_CLOSE_BUTTON = "close_button"
CONF_COLOR_DEPTH = "color_depth"
CONF_CONTROL = "control" CONF_CONTROL = "control"
CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group" CONF_DEFAULT_GROUP = "default_group"

View File

@ -0,0 +1,403 @@
# Various constants used in MIPI DBI communication
# Various configuration constants for MIPI displays
# Various utility functions for MIPI DBI configuration
from typing import Any
from esphome.components.const import CONF_COLOR_DEPTH
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
import esphome.config_validation as cv
from esphome.const import (
CONF_BRIGHTNESS,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import TimePeriod
LOGGER = cv.logging.getLogger(__name__)
ColorOrder = display_ns.enum("ColorMode")
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
RDMODE = 0x0A
RDMADCTL = 0x0B
RDPIXFMT = 0x0C
RDIMGFMT = 0x0D
RDSELFDIAG = 0x0F
SLEEP_IN = 0x10
SLPIN = 0x10
SLEEP_OUT = 0x11
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVERT_OFF = 0x20
INVOFF = 0x20
INVERT_ON = 0x21
INVON = 0x21
ALL_ON = 0x23
WRAM = 0x24
GAMMASET = 0x26
MIPI = 0x26
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
PASET = 0x2B
RASET = 0x2B
RAMWR = 0x2C
WDATA = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
VSCRDEF = 0x33
TEON = 0x35
MADCTL = 0x36
MADCTL_CMD = 0x36
VSCRSADD = 0x37
IDMOFF = 0x38
IDMON = 0x39
COLMOD = 0x3A
PIXFMT = 0x3A
GETSCANLINE = 0x45
BRIGHTNESS = 0x51
WRDISBV = 0x51
RDDISBV = 0x52
WRCTRLD = 0x53
SWIRE1 = 0x5A
SWIRE2 = 0x5B
IFMODE = 0xB0
FRMCTR1 = 0xB1
FRMCTR2 = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DFUNCTR = 0xB6
ETMOD = 0xB7
PWCTR1 = 0xC0
PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
VMCTR1 = 0xC5
IFCTR = 0xC6
VMCTR2 = 0xC7
GMCTR = 0xC8
SETEXTC = 0xC8
PWSET = 0xD0
VMCTR = 0xD1
PWSETN = 0xD2
RDID4 = 0xD3
RDINDEX = 0xD9
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDIDX = 0xDD
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1
CSCON = 0xF0
PWCTR6 = 0xF6
ADJCTL3 = 0xF7
PAGESEL = 0xFE
MADCTL_MY = 0x80 # Bit 7 Bottom to top
MADCTL_MX = 0x40 # Bit 6 Right to left
MADCTL_MV = 0x20 # Bit 5 Reverse Mode
MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top
MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order
MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order
MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect
# partial updates.
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
MADCTL_YFLIP = 0x01 # Mirror the display vertically
# Special constant for delays in command sequences
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
CONF_PIXEL_MODE = "pixel_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips"
PIXEL_MODE_24BIT = "24bit"
PIXEL_MODE_18BIT = "18bit"
PIXEL_MODE_16BIT = "16bit"
PIXEL_MODES = {
PIXEL_MODE_16BIT: 0x55,
PIXEL_MODE_18BIT: 0x66,
PIXEL_MODE_24BIT: 0x77,
}
MODE_RGB = "RGB"
MODE_BGR = "BGR"
COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
}
CONF_HSYNC_BACK_PORCH = "hsync_back_porch"
CONF_HSYNC_FRONT_PORCH = "hsync_front_porch"
CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width"
CONF_VSYNC_BACK_PORCH = "vsync_back_porch"
CONF_VSYNC_FRONT_PORCH = "vsync_front_porch"
CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width"
CONF_PCLK_FREQUENCY = "pclk_frequency"
CONF_PCLK_INVERTED = "pclk_inverted"
CONF_NATIVE_HEIGHT = "native_height"
CONF_NATIVE_WIDTH = "native_width"
CONF_DE_PIN = "de_pin"
CONF_PCLK_PIN = "pclk_pin"
def power_of_two(value):
value = cv.int_range(1, 128)(value)
if value & (value - 1) != 0:
raise cv.Invalid("value must be a power of two")
return value
def validate_dimension(rounding):
def validator(value):
value = cv.positive_int(value)
if value % rounding != 0:
raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}")
return value
return validator
def dimension_schema(rounding):
return cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): validate_dimension(rounding),
cv.Required(CONF_HEIGHT): validate_dimension(rounding),
cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension(
rounding
),
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
}
),
)
def map_sequence(value):
"""
Maps one entry in a sequence to a command and data bytes.
The format is a repeated sequence of [CMD, <data>] where <data> is s a sequence of bytes. The length is inferred
from the length of the sequence and should not be explicit.
A single integer can be provided where there are no data bytes, in which case it is treated as a command.
A delay can be inserted by specifying "- delay N" where N is in ms
"""
if isinstance(value, str) and value.lower().startswith("delay "):
value = value.lower()[6:]
delay_value = cv.All(
cv.positive_time_period_milliseconds,
cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)),
)(value)
return DELAY_FLAG, delay_value.total_milliseconds
value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value)
return tuple(value)
def delay(ms):
return DELAY_FLAG, ms
class DriverChip:
models = {}
def __init__(
self,
name: str,
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.initsequence = initsequence
self.defaults = defaults
DriverChip.models[name] = self
def extend(self, name, **kwargs) -> "DriverChip":
defaults = self.defaults.copy()
if (
CONF_WIDTH in defaults
and CONF_OFFSET_WIDTH in kwargs
and CONF_NATIVE_WIDTH not in defaults
):
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
if (
CONF_HEIGHT in defaults
and CONF_OFFSET_HEIGHT in kwargs
and CONF_NATIVE_HEIGHT not in defaults
):
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
defaults.update(kwargs)
return DriverChip(name, initsequence=self.initsequence, **defaults)
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
def option(self, name, fallback=False) -> cv.Optional:
return cv.Optional(name, default=self.get_default(name, fallback))
def rotation_as_transform(self, config) -> bool:
"""
Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
"""
rotation = config.get(CONF_ROTATION, 0)
return rotation and (
self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
)
def get_dimensions(self, config) -> tuple[int, int, int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
return width, height, offset_width, offset_height
(width, height) = dimensions
return width, height, 0, 0
# Default dimensions, use model defaults
transform = self.get_transform(config)
width = self.get_default(CONF_WIDTH)
height = self.get_default(CONF_HEIGHT)
offset_width = self.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = self.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def get_transform(self, config) -> dict[str, bool]:
can_transform = self.rotation_as_transform(config)
transform = config.get(
CONF_TRANSFORM,
{
CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False),
CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False),
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False),
},
)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
else:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform[CONF_TRANSFORM] = True
return transform
def get_sequence(self, config) -> tuple[tuple[int, ...], 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
Pixel format, color order, and orientation will be set.
Returns a tuple of the init sequence and the computed MADCTL value.
"""
sequence = list(self.initsequence)
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
sequence.extend(custom_sequence)
# Ensure each command is a tuple
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
# Set pixel format if not already in the custom sequence
pixel_mode = config[CONF_PIXEL_MODE]
if not isinstance(pixel_mode, int):
pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config.get(CONF_USE_AXIS_FLIPS)
madctl = 0
transform = self.get_transform(config)
if self.rotation_as_transform(config):
LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X):
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
if transform.get(CONF_MIRROR_Y):
madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY
if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined
madctl |= MADCTL_MV
if config[CONF_COLOR_ORDER] == MODE_BGR:
madctl |= MADCTL_BGR
sequence.append((MADCTL, madctl))
if config[CONF_INVERT_COLORS]:
sequence.append((INVON,))
else:
sequence.append((INVOFF,))
if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)):
sequence.append((BRIGHTNESS, brightness))
sequence.append((SLPOUT,))
sequence.append((DISPON,))
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return sum(
tuple(
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
for x in sequence
),
(),
), madctl
def requires_buffer(config) -> bool:
"""
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
:param config:
:return: True if a buffer is required, False otherwise
"""
return any(
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
)
def get_color_depth(config) -> int:
"""
Get the color depth in bits from the configuration.
"""
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))

View File

@ -3,11 +3,4 @@ CODEOWNERS = ["@clydebarrow"]
DOMAIN = "mipi_spi" DOMAIN = "mipi_spi"
CONF_SPI_16 = "spi_16" CONF_SPI_16 = "spi_16"
CONF_PIXEL_MODE = "pixel_mode"
CONF_BUS_MODE = "bus_mode" CONF_BUS_MODE = "bus_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips"
CONF_NATIVE_WIDTH = "native_width"
CONF_NATIVE_HEIGHT = "native_height"
MODE_RGB = "RGB"
MODE_BGR = "BGR"

View File

@ -9,6 +9,20 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING, CONF_DRAW_ROUNDING,
) )
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.mipi import (
CONF_PIXEL_MODE,
CONF_USE_AXIS_FLIPS,
MADCTL,
MODE_BGR,
MODE_RGB,
PIXFMT,
DriverChip,
dimension_schema,
get_color_depth,
map_sequence,
power_of_two,
requires_buffer,
)
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
@ -21,7 +35,6 @@ from esphome.const import (
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS, CONF_DIMENSIONS,
CONF_ENABLE_PIN, CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE, CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS, CONF_INVERT_COLORS,
@ -29,49 +42,18 @@ from esphome.const import (
CONF_MIRROR_X, CONF_MIRROR_X,
CONF_MIRROR_Y, CONF_MIRROR_Y,
CONF_MODEL, CONF_MODEL,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_ROTATION, CONF_ROTATION,
CONF_SWAP_XY, CONF_SWAP_XY,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import CORE, TimePeriod from esphome.core import CORE
from esphome.cpp_generator import TemplateArguments from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config from esphome.final_validate import full_config
from . import ( from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN
CONF_BUS_MODE, from .models import adafruit, amoled, cyd, ili, jc, lanbon, lilygo, waveshare
CONF_NATIVE_HEIGHT,
CONF_NATIVE_WIDTH,
CONF_PIXEL_MODE,
CONF_SPI_16,
CONF_USE_AXIS_FLIPS,
DOMAIN,
MODE_BGR,
MODE_RGB,
)
from .models import (
DELAY_FLAG,
MADCTL_BGR,
MADCTL_MV,
MADCTL_MX,
MADCTL_MY,
MADCTL_XFLIP,
MADCTL_YFLIP,
DriverChip,
adafruit,
amoled,
cyd,
ili,
jc,
lanbon,
lilygo,
waveshare,
)
from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@ -124,45 +106,6 @@ DISPLAY_PIXEL_MODES = {
} }
def get_dimensions(config):
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
return width, height, offset_width, offset_height
(width, height) = dimensions
return width, height, 0, 0
# Default dimensions, use model defaults
transform = get_transform(config)
model = MODELS[config[CONF_MODEL]]
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def denominator(config): def denominator(config):
""" """
Calculate the best denominator for a buffer size fraction. Calculate the best denominator for a buffer size fraction.
@ -171,10 +114,11 @@ def denominator(config):
:config: The configuration dictionary containing the buffer size fraction and display dimensions :config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction :return: The denominator to use for the buffer size fraction
""" """
model = MODELS[config[CONF_MODEL]]
frac = config.get(CONF_BUFFER_SIZE) frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75: if frac is None or frac > 0.75:
return 1 return 1
height, _width, _offset_width, _offset_height = get_dimensions(config) height, _width, _offset_width, _offset_height = model.get_dimensions(config)
try: try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration: except StopIteration:
@ -183,58 +127,6 @@ def denominator(config):
) from StopIteration ) from StopIteration
def validate_dimension(rounding):
def validator(value):
value = cv.positive_int(value)
if value % rounding != 0:
raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}")
return value
return validator
def map_sequence(value):
"""
The format is a repeated sequence of [CMD, <data>] where <data> is s a sequence of bytes. The length is inferred
from the length of the sequence and should not be explicit.
A delay can be inserted by specifying "- delay N" where N is in ms
"""
if isinstance(value, str) and value.lower().startswith("delay "):
value = value.lower()[6:]
delay = cv.All(
cv.positive_time_period_milliseconds,
cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)),
)(value)
return DELAY_FLAG, delay.total_milliseconds
if isinstance(value, int):
return (value,)
value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value)
return tuple(value)
def power_of_two(value):
value = cv.int_range(1, 128)(value)
if value & (value - 1) != 0:
raise cv.Invalid("value must be a power of two")
return value
def dimension_schema(rounding):
return cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): validate_dimension(rounding),
cv.Required(CONF_HEIGHT): validate_dimension(rounding),
cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension(
rounding
),
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
}
),
)
def swap_xy_schema(model): def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
@ -250,7 +142,7 @@ def swap_xy_schema(model):
def model_schema(config): def model_schema(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) bus_mode = config[CONF_BUS_MODE]
transform = cv.Schema( transform = cv.Schema(
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
@ -340,18 +232,6 @@ def model_schema(config):
return schema return schema
def is_rotation_transformable(config):
"""
Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
"""
model = MODELS[config[CONF_MODEL]]
rotation = config.get(CONF_ROTATION, 0)
return rotation and (
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
)
def customise_schema(config): def customise_schema(config):
""" """
Create a customised config schema for a specific model and validate the configuration. Create a customised config schema for a specific model and validate the configuration.
@ -367,7 +247,7 @@ def customise_schema(config):
extra=ALLOW_EXTRA, extra=ALLOW_EXTRA,
)(config) )(config)
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
bus_modes = model.modes bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL)
config = cv.Schema( config = cv.Schema(
{ {
model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True), model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True),
@ -375,7 +255,7 @@ def customise_schema(config):
}, },
extra=ALLOW_EXTRA, extra=ALLOW_EXTRA,
)(config) )(config)
bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) bus_mode = config[CONF_BUS_MODE]
config = model_schema(config)(config) config = model_schema(config)(config)
# Check for invalid combinations of MADCTL config # Check for invalid combinations of MADCTL config
if init_sequence := config.get(CONF_INIT_SEQUENCE): if init_sequence := config.get(CONF_INIT_SEQUENCE):
@ -400,23 +280,9 @@ def customise_schema(config):
CONFIG_SCHEMA = customise_schema CONFIG_SCHEMA = customise_schema
def requires_buffer(config):
"""
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
:param config:
:return: True if a buffer is required, False otherwise
"""
return any(
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
)
def get_color_depth(config):
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
def _final_validate(config): def _final_validate(config):
global_config = full_config.get() global_config = full_config.get()
model = MODELS[config[CONF_MODEL]]
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
@ -433,7 +299,7 @@ def _final_validate(config):
return config return config
color_depth = get_color_depth(config) color_depth = get_color_depth(config)
frac = denominator(config) frac = denominator(config)
height, width, _offset_width, _offset_height = get_dimensions(config) height, width, _offset_width, _offset_height = model.get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB # Target a buffer size of 20kB
@ -463,7 +329,7 @@ def get_transform(config):
:return: :return:
""" """
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
can_transform = is_rotation_transformable(config) can_transform = model.rotation_as_transform(config)
transform = config.get( transform = config.get(
CONF_TRANSFORM, CONF_TRANSFORM,
{ {
@ -489,63 +355,6 @@ def get_transform(config):
return transform return transform
def get_sequence(model, config):
"""
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
Pixel format, color order, and orientation will be set.
"""
sequence = list(model.initsequence)
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
sequence.extend(custom_sequence)
# Ensure each command is a tuple
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
commands = [x[0] for x in sequence]
# Set pixel format if not already in the custom sequence
pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
sequence.append((PIXFMT, pixel_mode[0]))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config[CONF_USE_AXIS_FLIPS]
if MADCTL not in commands:
madctl = 0
transform = get_transform(config)
if transform.get(CONF_TRANSFORM):
LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X):
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
if transform.get(CONF_MIRROR_Y):
madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY
if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined
madctl |= MADCTL_MV
if config[CONF_COLOR_ORDER] == MODE_BGR:
madctl |= MADCTL_BGR
sequence.append((MADCTL, madctl))
if INVON not in commands and INVOFF not in commands:
if config[CONF_INVERT_COLORS]:
sequence.append((INVON,))
else:
sequence.append((INVOFF,))
if BRIGHTNESS not in commands:
if brightness := config.get(
CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS)
):
sequence.append((BRIGHTNESS, brightness))
if SLPOUT not in commands:
sequence.append((SLPOUT,))
sequence.append((DISPON,))
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return sum(
tuple(
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
for x in sequence
),
(),
)
def get_instance(config): def get_instance(config):
""" """
Get the type of MipiSpi instance to create based on the configuration, Get the type of MipiSpi instance to create based on the configuration,
@ -553,7 +362,8 @@ def get_instance(config):
:param config: :param config:
:return: type, template arguments :return: type, template arguments
""" """
width, height, offset_width, offset_height = get_dimensions(config) model = MODELS[config[CONF_MODEL]]
width, height, offset_width, offset_height = model.get_dimensions(config)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth] bufferpixels = COLOR_DEPTHS[color_depth]
@ -568,7 +378,7 @@ def get_instance(config):
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config) frac = denominator(config)
rotation = DISPLAY_ROTATIONS[ rotation = DISPLAY_ROTATIONS[
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0) 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
] ]
templateargs = [ templateargs = [
buffer_type, buffer_type,
@ -594,8 +404,9 @@ async def to_code(config):
var_id = config[CONF_ID] var_id = config[CONF_ID]
var_id.type, templateargs = get_instance(config) var_id.type, templateargs = get_instance(config)
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
cg.add(var.set_init_sequence(get_sequence(model, config))) init_sequence, _madctl = model.get_sequence(config)
if is_rotation_transformable(config): cg.add(var.set_init_sequence(init_sequence))
if model.rotation_as_transform(config):
if CONF_TRANSFORM in config: if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else: else:

View File

@ -1,65 +0,0 @@
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv
from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH
from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH
MADCTL_MY = 0x80 # Bit 7 Bottom to top
MADCTL_MX = 0x40 # Bit 6 Right to left
MADCTL_MV = 0x20 # Bit 5 Reverse Mode
MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top
MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order
MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order
MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect
# partial updates.
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
MADCTL_YFLIP = 0x01 # Mirror the display vertically
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
def delay(ms):
return DELAY_FLAG, ms
class DriverChip:
models = {}
def __init__(
self,
name: str,
modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL),
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.modes = modes
self.initsequence = initsequence
self.defaults = defaults
DriverChip.models[name] = self
def extend(self, name, **kwargs):
defaults = self.defaults.copy()
if (
CONF_WIDTH in defaults
and CONF_OFFSET_WIDTH in kwargs
and CONF_NATIVE_WIDTH not in defaults
):
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
if (
CONF_HEIGHT in defaults
and CONF_OFFSET_HEIGHT in kwargs
and CONF_NATIVE_HEIGHT not in defaults
):
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
defaults.update(kwargs)
return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults)
def get_default(self, key, fallback=False):
return self.defaults.get(key, fallback)
def option(self, name, fallback=False):
return cv.Optional(name, default=self.get_default(name, fallback))

View File

@ -1,9 +1,19 @@
from esphome.components.mipi import (
MIPI,
MODE_RGB,
NORON,
PAGESEL,
PIXFMT,
SLPOUT,
SWIRE1,
SWIRE2,
TEON,
WRAM,
DriverChip,
delay,
)
from esphome.components.spi import TYPE_QUAD from esphome.components.spi import TYPE_QUAD
from .. import MODE_RGB
from . import DriverChip, delay
from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM
DriverChip( DriverChip(
"T-DISPLAY-S3-AMOLED", "T-DISPLAY-S3-AMOLED",
width=240, width=240,

View File

@ -1,82 +0,0 @@
# MIPI DBI commands
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
RDMODE = 0x0A
RDMADCTL = 0x0B
RDPIXFMT = 0x0C
RDIMGFMT = 0x0D
RDSELFDIAG = 0x0F
SLEEP_IN = 0x10
SLPIN = 0x10
SLEEP_OUT = 0x11
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVERT_OFF = 0x20
INVOFF = 0x20
INVERT_ON = 0x21
INVON = 0x21
ALL_ON = 0x23
WRAM = 0x24
GAMMASET = 0x26
MIPI = 0x26
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
PASET = 0x2B
RASET = 0x2B
RAMWR = 0x2C
WDATA = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
VSCRDEF = 0x33
TEON = 0x35
MADCTL = 0x36
MADCTL_CMD = 0x36
VSCRSADD = 0x37
IDMOFF = 0x38
IDMON = 0x39
COLMOD = 0x3A
PIXFMT = 0x3A
GETSCANLINE = 0x45
BRIGHTNESS = 0x51
WRDISBV = 0x51
RDDISBV = 0x52
WRCTRLD = 0x53
SWIRE1 = 0x5A
SWIRE2 = 0x5B
IFMODE = 0xB0
FRMCTR1 = 0xB1
FRMCTR2 = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DFUNCTR = 0xB6
ETMOD = 0xB7
PWCTR1 = 0xC0
PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
VMCTR1 = 0xC5
IFCTR = 0xC6
VMCTR2 = 0xC7
GMCTR = 0xC8
SETEXTC = 0xC8
PWSET = 0xD0
VMCTR = 0xD1
PWSETN = 0xD2
RDID4 = 0xD3
RDINDEX = 0xD9
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDIDX = 0xDD
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1
CSCON = 0xF0
PWCTR6 = 0xF6
ADJCTL3 = 0xF7
PAGESEL = 0xFE

View File

@ -1,8 +1,4 @@
from esphome.components.spi import TYPE_OCTAL from esphome.components.mipi import (
from .. import MODE_RGB
from . import DriverChip, delay
from .commands import (
ADJCTL3, ADJCTL3,
CSCON, CSCON,
DFUNCTR, DFUNCTR,
@ -18,6 +14,7 @@ from .commands import (
IFCTR, IFCTR,
IFMODE, IFMODE,
INVCTR, INVCTR,
MODE_RGB,
NORON, NORON,
PWCTR1, PWCTR1,
PWCTR2, PWCTR2,
@ -32,7 +29,10 @@ from .commands import (
VMCTR1, VMCTR1,
VMCTR2, VMCTR2,
VSCRSADD, VSCRSADD,
DriverChip,
delay,
) )
from esphome.components.spi import TYPE_OCTAL
DriverChip( DriverChip(
"M5CORE", "M5CORE",

View File

@ -1,10 +1,8 @@
from esphome.components.mipi import MODE_RGB, DriverChip
from esphome.components.spi import TYPE_QUAD from esphome.components.spi import TYPE_QUAD
import esphome.config_validation as cv 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_NUMBER
from .. import MODE_RGB
from . import DriverChip
AXS15231 = DriverChip( AXS15231 = DriverChip(
"AXS15231", "AXS15231",
draw_rounding=8, draw_rounding=8,

View File

@ -1,6 +1,6 @@
from esphome.components.mipi import MODE_BGR
from esphome.components.spi import TYPE_OCTAL from esphome.components.spi import TYPE_OCTAL
from .. import MODE_BGR
from .ili import ST7789V, ST7796 from .ili import ST7789V, ST7796
ST7789V.extend( ST7789V.extend(

View File

@ -1,6 +1,6 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv import esphome.config_validation as cv
from . import DriverChip
from .ili import ILI9488_A from .ili import ILI9488_A
DriverChip( DriverChip(

View File

@ -13,15 +13,27 @@
#include "esphome/components/openthread/openthread.h" #include "esphome/components/openthread/openthread.h"
#endif #endif
#ifdef USE_MODEM
#include "esphome/components/modem/modem_component.h"
#endif
namespace esphome { namespace esphome {
namespace network { namespace network {
// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as
// an AP that use a previous interface for NAT).
bool is_connected() { bool is_connected() {
#ifdef USE_ETHERNET #ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true; return true;
#endif #endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->is_connected();
#endif
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_connected(); return wifi::global_wifi_component->is_connected();
@ -39,6 +51,11 @@ bool is_connected() {
} }
bool is_disabled() { bool is_disabled() {
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->is_disabled();
#endif
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_disabled(); return wifi::global_wifi_component->is_disabled();
@ -51,6 +68,12 @@ network::IPAddresses get_ip_addresses() {
if (ethernet::global_eth_component != nullptr) if (ethernet::global_eth_component != nullptr)
return ethernet::global_eth_component->get_ip_addresses(); return ethernet::global_eth_component->get_ip_addresses();
#endif #endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->get_ip_addresses();
#endif
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->get_ip_addresses(); return wifi::global_wifi_component->get_ip_addresses();
@ -67,6 +90,12 @@ std::string get_use_address() {
if (ethernet::global_eth_component != nullptr) if (ethernet::global_eth_component != nullptr)
return ethernet::global_eth_component->get_use_address(); return ethernet::global_eth_component->get_use_address();
#endif #endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->get_use_address();
#endif
#ifdef USE_WIFI #ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->get_use_address(); return wifi::global_wifi_component->get_use_address();

View File

@ -2,6 +2,18 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display from esphome.components import display
from esphome.components.esp32 import const, only_on_variant from esphome.components.esp32 import const, only_on_variant
from esphome.components.mipi import (
CONF_DE_PIN,
CONF_HSYNC_BACK_PORCH,
CONF_HSYNC_FRONT_PORCH,
CONF_HSYNC_PULSE_WIDTH,
CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED,
CONF_PCLK_PIN,
CONF_VSYNC_BACK_PORCH,
CONF_VSYNC_FRONT_PORCH,
CONF_VSYNC_PULSE_WIDTH,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLUE, CONF_BLUE,
@ -27,18 +39,6 @@ from esphome.const import (
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CONF_DE_PIN = "de_pin"
CONF_PCLK_PIN = "pclk_pin"
CONF_HSYNC_FRONT_PORCH = "hsync_front_porch"
CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width"
CONF_HSYNC_BACK_PORCH = "hsync_back_porch"
CONF_VSYNC_FRONT_PORCH = "vsync_front_porch"
CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width"
CONF_VSYNC_BACK_PORCH = "vsync_back_porch"
CONF_PCLK_FREQUENCY = "pclk_frequency"
CONF_PCLK_INVERTED = "pclk_inverted"
rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb") rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb")
RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component) RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component)
ColorOrder = display.display_ns.enum("ColorMode") ColorOrder = display.display_ns.enum("ColorMode")

View File

@ -23,7 +23,6 @@ void RpiDpiRgb::setup() {
config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.flags.pclk_active_neg = this->pclk_inverted_;
config.timings.pclk_hz = this->pclk_frequency_; config.timings.pclk_hz = this->pclk_frequency_;
config.clk_src = LCD_CLK_SRC_PLL160M; config.clk_src = LCD_CLK_SRC_PLL160M;
config.psram_trans_align = 64;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) { for (size_t i = 0; i != data_pin_count; i++) {
config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); config.data_gpio_nums[i] = this->data_pins_[i]->get_pin();

View File

@ -2,9 +2,17 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.esp32 import const, only_on_variant from esphome.components.esp32 import const, only_on_variant
from esphome.components.rpi_dpi_rgb.display import ( from esphome.components.mipi import (
CONF_DE_PIN,
CONF_HSYNC_BACK_PORCH,
CONF_HSYNC_FRONT_PORCH,
CONF_HSYNC_PULSE_WIDTH,
CONF_PCLK_FREQUENCY, CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED, CONF_PCLK_INVERTED,
CONF_PCLK_PIN,
CONF_VSYNC_BACK_PORCH,
CONF_VSYNC_FRONT_PORCH,
CONF_VSYNC_PULSE_WIDTH,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -36,16 +44,6 @@ from esphome.core import TimePeriod
from .init_sequences import ST7701S_INITS, cmd from .init_sequences import ST7701S_INITS, cmd
CONF_DE_PIN = "de_pin"
CONF_PCLK_PIN = "pclk_pin"
CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width"
CONF_HSYNC_BACK_PORCH = "hsync_back_porch"
CONF_HSYNC_FRONT_PORCH = "hsync_front_porch"
CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width"
CONF_VSYNC_BACK_PORCH = "vsync_back_porch"
CONF_VSYNC_FRONT_PORCH = "vsync_front_porch"
DEPENDENCIES = ["spi", "esp32"] DEPENDENCIES = ["spi", "esp32"]
st7701s_ns = cg.esphome_ns.namespace("st7701s") st7701s_ns = cg.esphome_ns.namespace("st7701s")

View File

@ -25,7 +25,6 @@ void ST7701S::setup() {
config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.flags.pclk_active_neg = this->pclk_inverted_;
config.timings.pclk_hz = this->pclk_frequency_; config.timings.pclk_hz = this->pclk_frequency_;
config.clk_src = LCD_CLK_SRC_PLL160M; config.clk_src = LCD_CLK_SRC_PLL160M;
config.psram_trans_align = 64;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) { for (size_t i = 0; i != data_pin_count; i++) {
config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); config.data_gpio_nums[i] = this->data_pins_[i]->get_pin();

View File

@ -369,11 +369,10 @@ bool Component::has_overridden_loop() const {
PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {}
void PollingComponent::call_setup() { void PollingComponent::call_setup() {
// init the poller before calling setup, allowing setup to cancel it if desired
this->start_poller();
// Let the polling component subclass setup their HW. // Let the polling component subclass setup their HW.
this->setup(); this->setup();
// init the poller
this->start_poller();
} }
void PollingComponent::start_poller() { void PollingComponent::start_poller() {

View File

@ -578,21 +578,28 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
/// Helper class to deduplicate items in a series of values. /// Helper class to deduplicate items in a series of values.
template<typename T> class Deduplicator { template<typename T> class Deduplicator {
public: public:
/// Feeds the next item in the series to the deduplicator and returns whether this is a duplicate. /// Feeds the next item in the series to the deduplicator and returns false if this is a duplicate.
bool next(T value) { bool next(T value) {
if (this->has_value_) { if (this->has_value_ && !this->value_unknown_ && this->last_value_ == value) {
if (this->last_value_ == value) return false;
return false;
} }
this->has_value_ = true; this->has_value_ = true;
this->value_unknown_ = false;
this->last_value_ = value; this->last_value_ = value;
return true; return true;
} }
/// Returns whether this deduplicator has processed any items so far. /// Returns true if the deduplicator's value was previously known.
bool next_unknown() {
bool ret = !this->value_unknown_;
this->value_unknown_ = true;
return ret;
}
/// Returns true if this deduplicator has processed any items.
bool has_value() const { return this->has_value_; } bool has_value() const { return this->has_value_; }
protected: protected:
bool has_value_{false}; bool has_value_{false};
bool value_unknown_{false};
T last_value_{}; T last_value_{};
}; };

View File

@ -17,6 +17,8 @@ static const char *const TAG = "scheduler";
static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
// Half the 32-bit range - used to detect rollovers vs normal time progression // Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2; static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
// max delay to start an interval sequence
static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
// Uncomment to debug scheduler // Uncomment to debug scheduler
// #define ESPHOME_DEBUG_SCHEDULER // #define ESPHOME_DEBUG_SCHEDULER
@ -100,9 +102,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
// Type-specific setup // Type-specific setup
if (type == SchedulerItem::INTERVAL) { if (type == SchedulerItem::INTERVAL) {
item->interval = delay; item->interval = delay;
// Calculate random offset (0 to interval/2) // first execution happens immediately after a random smallish offset
uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; // Calculate random offset (0 to min(interval/2, 5s))
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
item->next_execution_ = now + offset; item->next_execution_ = now + offset;
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr, delay, offset);
} else { } else {
item->interval = 0; item->interval = 0;
item->next_execution_ = now + delay; item->next_execution_ = now + delay;

View File

@ -16,9 +16,9 @@ from esphome.components.esp32 import (
VARIANTS, VARIANTS,
) )
from esphome.components.esp32.gpio import validate_gpio_pin from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.components.mipi import CONF_NATIVE_HEIGHT
from esphome.components.mipi_spi.display import ( from esphome.components.mipi_spi.display import (
CONF_BUS_MODE, CONF_BUS_MODE,
CONF_NATIVE_HEIGHT,
CONFIG_SCHEMA, CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA, FINAL_VALIDATE_SCHEMA,
MODELS, MODELS,