mirror of
https://github.com/esphome/esphome.git
synced 2025-11-04 17:11:51 +00:00
Compare commits
5 Commits
integratio
...
select_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014722bcbb | ||
|
|
a6d98cec0b | ||
|
|
8e3d53cb03 | ||
|
|
1150f8e6ba | ||
|
|
55b89faac8 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -192,11 +192,6 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
- name: Determine which tests to run
|
||||
id: determine
|
||||
env:
|
||||
@@ -221,12 +216,6 @@ jobs:
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
|
||||
@@ -155,7 +155,6 @@ esphome/components/esp32_ble_tracker/* @bdraco
|
||||
esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
esphome/components/esp32_hosted/update/* @swoboda1337
|
||||
esphome/components/esp32_improv/* @jesserockz
|
||||
esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
@@ -480,7 +479,6 @@ esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
|
||||
@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Set memory analysis options in config
|
||||
if args.analyze_memory:
|
||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
||||
|
||||
if args.memory_report:
|
||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
||||
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -1209,17 +1202,6 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--analyze-memory",
|
||||
help="Analyze and display memory usage by component after compilation.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--memory-report",
|
||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
||||
type=str,
|
||||
metavar="FILE",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
|
||||
@@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
||||
}
|
||||
if (traits.supports_direction())
|
||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||
if (traits.supports_preset_modes() && fan->has_preset_mode())
|
||||
msg.set_preset_mode(StringRef(fan->get_preset_mode()));
|
||||
if (traits.supports_preset_modes())
|
||||
msg.set_preset_mode(StringRef(fan->preset_mode));
|
||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
|
||||
@@ -224,7 +224,7 @@ void APIServer::dump_config() {
|
||||
" Address: %s:%u\n"
|
||||
" Listen backlog: %u\n"
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
|
||||
@@ -11,5 +11,4 @@ CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
|
||||
@@ -12,7 +12,7 @@ void CopyFan::setup() {
|
||||
this->oscillating = source_->oscillating;
|
||||
this->speed = source_->speed;
|
||||
this->direction = source_->direction;
|
||||
this->set_preset_mode_(source_->get_preset_mode());
|
||||
this->preset_mode = source_->preset_mode;
|
||||
this->publish_state();
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ void CopyFan::setup() {
|
||||
this->oscillating = source_->oscillating;
|
||||
this->speed = source_->speed;
|
||||
this->direction = source_->direction;
|
||||
this->set_preset_mode_(source_->get_preset_mode());
|
||||
this->preset_mode = source_->preset_mode;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
|
||||
call2.set_speed(*call.get_speed());
|
||||
if (call.get_direction().has_value())
|
||||
call2.set_direction(*call.get_direction());
|
||||
if (call.has_preset_mode())
|
||||
if (!call.get_preset_mode().empty())
|
||||
call2.set_preset_mode(call.get_preset_mode());
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() {
|
||||
}
|
||||
|
||||
void DallasTemperatureSensor::setup() {
|
||||
if (!this->check_address_or_index_())
|
||||
if (!this->check_address_())
|
||||
return;
|
||||
if (!this->read_scratch_pad_())
|
||||
return;
|
||||
|
||||
@@ -210,7 +210,7 @@ class Display : public PollingComponent {
|
||||
/// Fill the entire screen with the given color.
|
||||
virtual void fill(Color color);
|
||||
/// Clear the entire screen by filling it with OFF pixels.
|
||||
virtual void clear();
|
||||
void clear();
|
||||
|
||||
/// Get the calculated width of the display in pixels with rotation applied.
|
||||
virtual int get_width() { return this->get_width_internal(); }
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
from esphome import core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.mipi import flatten_sequence, map_sequence
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_DURATION,
|
||||
CONF_RESET_PIN,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
|
||||
from . import models
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
|
||||
|
||||
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
|
||||
EPaperBase = epaper_spi_ns.class_(
|
||||
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||
@@ -38,78 +24,29 @@ EPaperBase = epaper_spi_ns.class_(
|
||||
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
|
||||
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
|
||||
|
||||
|
||||
# Import all models dynamically from the models package
|
||||
for module_info in pkgutil.iter_modules(models.__path__):
|
||||
importlib.import_module(f".models.{module_info.name}", package=__package__)
|
||||
|
||||
MODELS = models.EpaperModel.models
|
||||
|
||||
DIMENSION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
)
|
||||
MODELS = {
|
||||
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
|
||||
}
|
||||
|
||||
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
return (
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
)
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
model.option(pin): pins.gpio_output_pin_schema
|
||||
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN)
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
|
||||
cv.GenerateID(): cv.declare_id(class_name),
|
||||
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
|
||||
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
|
||||
model.option(CONF_ENABLE_PIN): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
|
||||
map_sequence
|
||||
),
|
||||
model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def customise_schema(config):
|
||||
"""
|
||||
Create a customised config schema for a specific model and validate the configuration.
|
||||
:param config: The configuration dictionary to validate
|
||||
:return: The validated configuration dictionary
|
||||
:raises cv.Invalid: If the configuration is invalid
|
||||
"""
|
||||
config = cv.Schema(
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
return model_schema(config)(config)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = customise_schema
|
||||
cv.GenerateID(): cv.declare_id(EPaperBase),
|
||||
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
"epaper_spi", require_miso=False, require_mosi=True
|
||||
@@ -119,23 +56,8 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
|
||||
init_sequence = config.get(CONF_INIT_SEQUENCE)
|
||||
if init_sequence is None:
|
||||
init_sequence = model.get_init_sequence(config)
|
||||
init_sequence = flatten_sequence(init_sequence)
|
||||
init_sequence_length = len(init_sequence)
|
||||
init_sequence_id = cg.static_const_array(
|
||||
config[CONF_INIT_SEQUENCE_ID], init_sequence
|
||||
)
|
||||
width, height = model.get_dimensions(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
model.name,
|
||||
width,
|
||||
height,
|
||||
init_sequence_id,
|
||||
init_sequence_length,
|
||||
)
|
||||
rhs = model.new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs, model)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -8,20 +8,33 @@ namespace esphome::epaper_spi {
|
||||
|
||||
static const char *const TAG = "epaper_spi";
|
||||
|
||||
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
|
||||
"IDLE", "UPDATE", "RESET", "RESET_END",
|
||||
|
||||
"SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
|
||||
};
|
||||
|
||||
const char *EPaperBase::epaper_state_to_string_() {
|
||||
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
|
||||
return EPAPER_STATE_STRINGS[idx];
|
||||
return "Unknown";
|
||||
static const LogString *epaper_state_to_string(EPaperState state) {
|
||||
switch (state) {
|
||||
case EPaperState::IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case EPaperState::UPDATE:
|
||||
return LOG_STR("UPDATE");
|
||||
case EPaperState::RESET:
|
||||
return LOG_STR("RESET");
|
||||
case EPaperState::INITIALISE:
|
||||
return LOG_STR("INITIALISE");
|
||||
case EPaperState::TRANSFER_DATA:
|
||||
return LOG_STR("TRANSFER_DATA");
|
||||
case EPaperState::POWER_ON:
|
||||
return LOG_STR("POWER_ON");
|
||||
case EPaperState::REFRESH_SCREEN:
|
||||
return LOG_STR("REFRESH_SCREEN");
|
||||
case EPaperState::POWER_OFF:
|
||||
return LOG_STR("POWER_OFF");
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
return LOG_STR("DEEP_SLEEP");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperBase::setup() {
|
||||
if (!this->init_buffer_(this->buffer_length_)) {
|
||||
if (!this->init_buffer_(this->get_buffer_length())) {
|
||||
this->mark_failed("Failed to initialise buffer");
|
||||
return;
|
||||
}
|
||||
@@ -37,7 +50,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperBase::setup_pins_() const {
|
||||
void EPaperBase::setup_pins_() {
|
||||
this->dc_pin_->setup(); // OUTPUT
|
||||
this->dc_pin_->digital_write(false);
|
||||
|
||||
@@ -68,7 +81,11 @@ void EPaperBase::data(uint8_t value) {
|
||||
// write a command followed by zero or more bytes of data.
|
||||
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
|
||||
// [COMMAND, LENGTH, DATA...]
|
||||
void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
|
||||
void EPaperBase::cmd_data(const uint8_t *data) {
|
||||
const uint8_t command = data[0];
|
||||
const uint8_t length = data[1];
|
||||
const uint8_t *ptr = data + 2;
|
||||
|
||||
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
|
||||
format_hex_pretty(ptr, length, '.', false).c_str());
|
||||
|
||||
@@ -82,146 +99,91 @@ void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
bool EPaperBase::is_idle_() const {
|
||||
bool EPaperBase::is_idle_() {
|
||||
if (this->busy_pin_ == nullptr) {
|
||||
return true;
|
||||
}
|
||||
return !this->busy_pin_->digital_read();
|
||||
return this->busy_pin_->digital_read();
|
||||
}
|
||||
|
||||
bool EPaperBase::reset_() const {
|
||||
void EPaperBase::reset() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
if (this->state_ == EPaperState::RESET) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
return false;
|
||||
}
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->reset_pin_->digital_write(false);
|
||||
this->disable_loop();
|
||||
this->set_timeout(this->reset_duration_, [this] {
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperBase::update() {
|
||||
if (this->state_ != EPaperState::IDLE) {
|
||||
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
|
||||
if (!this->state_queue_.empty()) {
|
||||
ESP_LOGE(TAG, "Display update already in progress - %s",
|
||||
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
|
||||
return;
|
||||
}
|
||||
this->set_state_(EPaperState::RESET);
|
||||
|
||||
this->state_queue_.push(EPaperState::UPDATE);
|
||||
this->state_queue_.push(EPaperState::RESET);
|
||||
this->state_queue_.push(EPaperState::INITIALISE);
|
||||
this->state_queue_.push(EPaperState::TRANSFER_DATA);
|
||||
this->state_queue_.push(EPaperState::POWER_ON);
|
||||
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
|
||||
this->state_queue_.push(EPaperState::POWER_OFF);
|
||||
this->state_queue_.push(EPaperState::DEEP_SLEEP);
|
||||
this->state_queue_.push(EPaperState::IDLE);
|
||||
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (should_wait) {
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
|
||||
}
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during the loop task.
|
||||
* First defer for any pending delays, then check if we are waiting for the display to become idle.
|
||||
* If not waiting for idle, process the state machine.
|
||||
*/
|
||||
|
||||
void EPaperBase::loop() {
|
||||
auto now = millis();
|
||||
if (this->delay_until_ != 0) {
|
||||
// using modulus arithmetic to handle wrap-around
|
||||
int diff = now - this->delay_until_;
|
||||
if (diff < 0) {
|
||||
return;
|
||||
}
|
||||
this->delay_until_ = 0;
|
||||
}
|
||||
if (this->waiting_for_idle_) {
|
||||
if (this->is_idle_()) {
|
||||
this->waiting_for_idle_ = false;
|
||||
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
this->waiting_for_idle_last_print_ = millis();
|
||||
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle");
|
||||
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
* IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF ->
|
||||
* DEEP_SLEEP -> IDLE
|
||||
*
|
||||
* Should a subclassed class need to override this, the method will need to be made virtual.
|
||||
*/
|
||||
void EPaperBase::process_state_() {
|
||||
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
|
||||
switch (this->state_) {
|
||||
default:
|
||||
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
|
||||
this->disable_loop();
|
||||
break;
|
||||
auto state = this->state_queue_.front();
|
||||
|
||||
switch (state) {
|
||||
case EPaperState::IDLE:
|
||||
this->disable_loop();
|
||||
break;
|
||||
case EPaperState::RESET:
|
||||
case EPaperState::RESET_END:
|
||||
if (this->reset_()) {
|
||||
this->set_state_(EPaperState::UPDATE);
|
||||
} else {
|
||||
this->set_state_(EPaperState::RESET_END);
|
||||
}
|
||||
break;
|
||||
case EPaperState::UPDATE:
|
||||
this->do_update_(); // Calls ESPHome (current page) lambda
|
||||
this->set_state_(EPaperState::INITIALISE);
|
||||
break;
|
||||
case EPaperState::RESET:
|
||||
this->reset();
|
||||
break;
|
||||
case EPaperState::INITIALISE:
|
||||
this->initialise_();
|
||||
this->set_state_(EPaperState::TRANSFER_DATA);
|
||||
break;
|
||||
case EPaperState::TRANSFER_DATA:
|
||||
if (!this->transfer_data()) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->set_state_(EPaperState::POWER_ON);
|
||||
break;
|
||||
case EPaperState::POWER_ON:
|
||||
this->power_on();
|
||||
this->set_state_(EPaperState::REFRESH_SCREEN);
|
||||
break;
|
||||
case EPaperState::REFRESH_SCREEN:
|
||||
this->refresh_screen();
|
||||
this->set_state_(EPaperState::POWER_OFF);
|
||||
break;
|
||||
case EPaperState::POWER_OFF:
|
||||
this->power_off();
|
||||
this->set_state_(EPaperState::DEEP_SLEEP);
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
|
||||
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
|
||||
this->state_ = state;
|
||||
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
|
||||
if (delay != 0) {
|
||||
this->delay_until_ = millis() + delay;
|
||||
} else {
|
||||
this->delay_until_ = 0;
|
||||
}
|
||||
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
|
||||
TRUEFALSE(this->waiting_for_idle_));
|
||||
this->state_queue_.pop();
|
||||
}
|
||||
|
||||
void EPaperBase::start_command_() {
|
||||
@@ -241,39 +203,25 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||
|
||||
void EPaperBase::initialise_() {
|
||||
size_t index = 0;
|
||||
|
||||
auto *sequence = this->init_sequence_;
|
||||
auto length = this->init_sequence_length_;
|
||||
while (index != length) {
|
||||
if (length - index < 2) {
|
||||
const auto &sequence = this->init_sequence_;
|
||||
const size_t sequence_size = this->init_sequence_length_;
|
||||
while (index != sequence_size) {
|
||||
if (sequence_size - index < 2) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
return;
|
||||
}
|
||||
const uint8_t cmd = sequence[index++];
|
||||
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
|
||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
const uint8_t num_args = x & 0x7F;
|
||||
if (length - index < num_args) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
|
||||
this->cmd_data(cmd, sequence + index, num_args);
|
||||
index += num_args;
|
||||
const auto *ptr = sequence + index;
|
||||
const uint8_t length = ptr[1];
|
||||
if (sequence_size - index < length + 2) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: %s", this->name_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->cmd_data(ptr);
|
||||
index += length + 2;
|
||||
}
|
||||
|
||||
this->power_on();
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -8,48 +8,36 @@
|
||||
#include <queue>
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
using namespace display;
|
||||
|
||||
enum class EPaperState : uint8_t {
|
||||
IDLE, // not doing anything
|
||||
UPDATE, // update the buffer
|
||||
RESET, // drive reset low (active)
|
||||
RESET_END, // drive reset high (inactive)
|
||||
|
||||
SHOULD_WAIT, // states higher than this should wait for the display to be not busy
|
||||
INITIALISE, // send the init sequence
|
||||
TRANSFER_DATA, // transfer data to the display
|
||||
POWER_ON, // power on the display
|
||||
REFRESH_SCREEN, // send refresh command
|
||||
POWER_OFF, // power off the display
|
||||
DEEP_SLEEP, // deep sleep the display
|
||||
IDLE,
|
||||
UPDATE,
|
||||
RESET,
|
||||
INITIALISE,
|
||||
TRANSFER_DATA,
|
||||
POWER_ON,
|
||||
REFRESH_SCREEN,
|
||||
POWER_OFF,
|
||||
DEEP_SLEEP,
|
||||
};
|
||||
|
||||
static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||
|
||||
class EPaperBase : public DisplayBuffer,
|
||||
class EPaperBase : public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_2MHZ> {
|
||||
public:
|
||||
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
|
||||
: name_(name),
|
||||
width_(width),
|
||||
height_(height),
|
||||
init_sequence_(init_sequence),
|
||||
init_sequence_length_(init_sequence_length),
|
||||
display_type_(display_type) {}
|
||||
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
|
||||
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||
float get_setup_priority() const override;
|
||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||
void dump_config() override;
|
||||
|
||||
void command(uint8_t value);
|
||||
void data(uint8_t value);
|
||||
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length);
|
||||
void cmd_data(const uint8_t *data);
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
@@ -58,84 +46,48 @@ class EPaperBase : public DisplayBuffer,
|
||||
|
||||
void on_safe_shutdown() override;
|
||||
|
||||
DisplayType get_display_type() override { return this->display_type_; };
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; };
|
||||
int get_width_internal() override { return this->width_; };
|
||||
void process_state_();
|
||||
|
||||
const char *epaper_state_to_string_();
|
||||
bool is_idle_() const;
|
||||
void setup_pins_() const;
|
||||
bool reset_() const;
|
||||
bool is_idle_();
|
||||
void setup_pins_();
|
||||
virtual void reset();
|
||||
void initialise_();
|
||||
void wait_for_idle_(bool should_wait);
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
|
||||
virtual int get_width_controller() { return this->get_width_internal(); };
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
*/
|
||||
virtual void deep_sleep() = 0;
|
||||
/**
|
||||
* Send data to the device via SPI
|
||||
* @return true if done, false if it should be called next loop
|
||||
* @return true if done, false if should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
virtual void refresh_screen() = 0;
|
||||
|
||||
/**
|
||||
* Power the display on
|
||||
*/
|
||||
virtual void power_on() = 0;
|
||||
/**
|
||||
* Power the display off
|
||||
*/
|
||||
virtual void power_off() = 0;
|
||||
|
||||
/**
|
||||
* Place the display into deep sleep
|
||||
*/
|
||||
virtual void deep_sleep() = 0;
|
||||
|
||||
void set_state_(EPaperState state, uint16_t delay = 0);
|
||||
virtual uint32_t get_buffer_length() = 0;
|
||||
|
||||
void start_command_();
|
||||
void end_command_();
|
||||
void start_data_();
|
||||
void end_data_();
|
||||
|
||||
// properties initialised in the constructor
|
||||
const char *name_;
|
||||
uint16_t width_;
|
||||
uint16_t height_;
|
||||
const uint8_t *init_sequence_;
|
||||
size_t init_sequence_length_;
|
||||
DisplayType display_type_;
|
||||
const size_t init_sequence_length_{0};
|
||||
|
||||
size_t buffer_length_{};
|
||||
size_t current_data_index_{0}; // used by data transfer to track progress
|
||||
size_t current_data_index_{0};
|
||||
uint32_t reset_duration_{200};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t transfer_start_time_{};
|
||||
uint32_t waiting_for_idle_last_print_{0};
|
||||
uint32_t waiting_for_idle_start_{0};
|
||||
#endif
|
||||
|
||||
GPIOPin *dc_pin_{};
|
||||
GPIOPin *busy_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
GPIOPin *dc_pin_;
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
|
||||
const uint8_t *init_sequence_{nullptr};
|
||||
|
||||
bool waiting_for_idle_{false};
|
||||
uint32_t delay_until_{0};
|
||||
|
||||
split_buffer::SplitBuffer buffer_;
|
||||
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "epaper_spi_model_7p3in_spectra_e6.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
|
||||
|
||||
void EPaper7p3InSpectraE6::power_on() {
|
||||
ESP_LOGI(TAG, "Power on");
|
||||
this->command(0x04);
|
||||
this->waiting_for_idle_ = true;
|
||||
}
|
||||
|
||||
void EPaper7p3InSpectraE6::power_off() {
|
||||
ESP_LOGI(TAG, "Power off");
|
||||
this->command(0x02);
|
||||
this->data(0x00);
|
||||
this->waiting_for_idle_ = true;
|
||||
}
|
||||
|
||||
void EPaper7p3InSpectraE6::refresh_screen() {
|
||||
ESP_LOGI(TAG, "Refresh");
|
||||
this->command(0x12);
|
||||
this->data(0x00);
|
||||
this->waiting_for_idle_ = true;
|
||||
}
|
||||
|
||||
void EPaper7p3InSpectraE6::deep_sleep() {
|
||||
ESP_LOGI(TAG, "Deep sleep");
|
||||
this->command(0x07);
|
||||
this->data(0xA5);
|
||||
}
|
||||
|
||||
void EPaper7p3InSpectraE6::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi_spectra_e6.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
|
||||
static constexpr const uint16_t WIDTH = 800;
|
||||
static constexpr const uint16_t HEIGHT = 480;
|
||||
// clang-format off
|
||||
|
||||
// Command, data length, data
|
||||
static constexpr uint8_t INIT_SEQUENCE[] = {
|
||||
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
|
||||
0x01, 1, 0x3F,
|
||||
0x00, 2, 0x5F, 0x69,
|
||||
0x03, 4, 0x00, 0x54, 0x00, 0x44,
|
||||
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
|
||||
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
|
||||
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
|
||||
0x30, 1, 0x03,
|
||||
0x50, 1, 0x3F,
|
||||
0x60, 2, 0x02, 0x00,
|
||||
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
|
||||
0x84, 1, 0x01,
|
||||
0xE3, 1, 0x2F,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
int get_width_internal() override { return WIDTH; };
|
||||
int get_height_internal() override { return HEIGHT; };
|
||||
|
||||
void refresh_screen() override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -1,166 +1,135 @@
|
||||
#include "epaper_spi_spectra_e6.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
static constexpr const char *const TAG = "epaper_spi.6c";
|
||||
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||
static constexpr unsigned char GRAY_THRESHOLD = 50;
|
||||
|
||||
enum E6Color {
|
||||
BLACK,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
RED,
|
||||
SKIP_1,
|
||||
BLUE,
|
||||
GREEN,
|
||||
CYAN,
|
||||
SKIP_2,
|
||||
};
|
||||
|
||||
static uint8_t color_to_hex(Color color) {
|
||||
// --- Step 1: Check for Grayscale (Black or White) ---
|
||||
// We define "grayscale" as a color where the min and max components
|
||||
// are close to each other.
|
||||
unsigned char max_rgb = std::max({color.r, color.g, color.b});
|
||||
unsigned char min_rgb = std::min({color.r, color.g, color.b});
|
||||
|
||||
if ((max_rgb - min_rgb) < GRAY_THRESHOLD) {
|
||||
// It's a shade of gray. Map to BLACK or WHITE.
|
||||
// We split the luminance at the halfway point (382 = (255*3)/2)
|
||||
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
|
||||
return WHITE;
|
||||
static inline uint8_t color_to_hex(Color color) {
|
||||
if (color.red > 127) {
|
||||
if (color.green > 170) {
|
||||
if (color.blue > 127) {
|
||||
return 0x1; // White
|
||||
} else {
|
||||
return 0x2; // Yellow
|
||||
}
|
||||
} else {
|
||||
return 0x3; // Red (or Magenta)
|
||||
}
|
||||
} else {
|
||||
if (color.green > 127) {
|
||||
if (color.blue > 127) {
|
||||
return 0x5; // Cyan -> Blue
|
||||
} else {
|
||||
return 0x6; // Green
|
||||
}
|
||||
} else {
|
||||
if (color.blue > 127) {
|
||||
return 0x5; // Blue
|
||||
} else {
|
||||
return 0x0; // Black
|
||||
}
|
||||
}
|
||||
return BLACK;
|
||||
}
|
||||
// --- Step 2: Check for Primary/Secondary Colors ---
|
||||
// If it's not gray, it's a color. We check which components are
|
||||
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
|
||||
bool r_on = (color.r > 128);
|
||||
bool g_on = (color.g > 128);
|
||||
bool b_on = (color.b > 128);
|
||||
|
||||
if (r_on && g_on && !b_on) {
|
||||
return YELLOW;
|
||||
}
|
||||
if (r_on && !g_on && !b_on) {
|
||||
return RED;
|
||||
}
|
||||
if (!r_on && g_on && !b_on) {
|
||||
return GREEN;
|
||||
}
|
||||
if (!r_on && !g_on && b_on) {
|
||||
return BLUE;
|
||||
}
|
||||
// Handle "impure" colors (Cyan, Magenta)
|
||||
if (!r_on && g_on && b_on) {
|
||||
// Cyan (G+B) -> Closest is Green or Blue. Pick Green.
|
||||
return GREEN;
|
||||
}
|
||||
if (r_on && !g_on) {
|
||||
// Magenta (R+B) -> Closest is Red or Blue. Pick Red.
|
||||
return RED;
|
||||
}
|
||||
// Handle the remaining corners (White-ish, Black-ish)
|
||||
if (r_on) {
|
||||
// All high (but not gray) -> White
|
||||
return WHITE;
|
||||
}
|
||||
// !r_on && !g_on && !b_on
|
||||
// All low (but not gray) -> Black
|
||||
return BLACK;
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::power_on() {
|
||||
ESP_LOGD(TAG, "Power on");
|
||||
this->command(0x04);
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::power_off() {
|
||||
ESP_LOGD(TAG, "Power off");
|
||||
this->command(0x02);
|
||||
this->data(0x00);
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::refresh_screen() {
|
||||
ESP_LOGD(TAG, "Refresh");
|
||||
this->command(0x12);
|
||||
this->data(0x00);
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::deep_sleep() {
|
||||
ESP_LOGD(TAG, "Deep sleep");
|
||||
this->command(0x07);
|
||||
this->data(0xA5);
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::fill(Color color) {
|
||||
auto pixel_color = color_to_hex(color);
|
||||
uint8_t pixel_color;
|
||||
if (color.is_on()) {
|
||||
pixel_color = color_to_hex(color);
|
||||
} else {
|
||||
pixel_color = 0x1;
|
||||
}
|
||||
|
||||
// We store 2 pixels per byte
|
||||
this->buffer_.fill(pixel_color + (pixel_color << 4));
|
||||
// We store 8 bitset<3> in 3 bytes
|
||||
// | byte 1 | byte 2 | byte 3 |
|
||||
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
|
||||
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
|
||||
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
|
||||
|
||||
const size_t buffer_length = this->get_buffer_length();
|
||||
for (size_t i = 0; i < buffer_length; i += 3) {
|
||||
this->buffer_[i + 0] = byte_1;
|
||||
this->buffer_[i + 1] = byte_2;
|
||||
this->buffer_[i + 2] = byte_3;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::clear() {
|
||||
// clear buffer to white, just like real paper.
|
||||
this->fill(COLOR_ON);
|
||||
uint32_t EPaperSpectraE6::get_buffer_length() {
|
||||
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
|
||||
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
|
||||
}
|
||||
|
||||
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
|
||||
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
|
||||
return;
|
||||
|
||||
auto pixel_bits = color_to_hex(color);
|
||||
uint8_t pixel_bits = color_to_hex(color);
|
||||
uint32_t pixel_position = x + y * this->get_width_controller();
|
||||
uint32_t byte_position = pixel_position / 2;
|
||||
auto original = this->buffer_[byte_position];
|
||||
if ((pixel_position & 1) != 0) {
|
||||
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
|
||||
uint32_t first_bit_position = pixel_position * 3;
|
||||
uint32_t byte_position = first_bit_position / 8u;
|
||||
uint32_t byte_subposition = first_bit_position % 8u;
|
||||
|
||||
if (byte_subposition <= 5) {
|
||||
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
|
||||
(pixel_bits << (5 - byte_subposition));
|
||||
} else {
|
||||
this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4);
|
||||
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
|
||||
(pixel_bits >> (byte_subposition - 5));
|
||||
|
||||
this->buffer_[byte_position + 1] =
|
||||
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
|
||||
(pixel_bits << (13 - byte_subposition));
|
||||
}
|
||||
}
|
||||
|
||||
bool HOT EPaperSpectraE6::transfer_data() {
|
||||
const uint32_t start_time = App.get_loop_component_start_time();
|
||||
const size_t buffer_length = this->buffer_length_;
|
||||
if (this->current_data_index_ == 0) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->transfer_start_time_ = millis();
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
|
||||
ESP_LOGV(TAG, "Sending data");
|
||||
this->command(0x10);
|
||||
}
|
||||
|
||||
size_t buf_idx = 0;
|
||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||
while (this->current_data_index_ != buffer_length) {
|
||||
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
|
||||
uint8_t bytes_to_send[4]{0};
|
||||
const size_t buffer_length = this->get_buffer_length();
|
||||
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
|
||||
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
|
||||
// 8 pixels are stored in 3 bytes
|
||||
// |aaabbbaa|abbbaaab|bbaaabbb|
|
||||
// | byte 1 | byte 2 | byte 3 |
|
||||
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
|
||||
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
|
||||
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
|
||||
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
|
||||
|
||||
if (buf_idx == sizeof bytes_to_send) {
|
||||
this->start_data_();
|
||||
this->write_array(bytes_to_send, buf_idx);
|
||||
this->end_data_();
|
||||
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
|
||||
buf_idx = 0;
|
||||
this->start_data_();
|
||||
this->write_array(bytes_to_send, sizeof(bytes_to_send));
|
||||
this->end_data_();
|
||||
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
return false;
|
||||
}
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
this->current_data_index_ = i + 3;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Finished the entire dataset
|
||||
if (buf_idx != 0) {
|
||||
this->start_data_();
|
||||
this->write_array(bytes_to_send, buf_idx);
|
||||
this->end_data_();
|
||||
}
|
||||
this->current_data_index_ = 0;
|
||||
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::reset() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->disable_loop();
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(20, [this] {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(20, [this] { this->enable_loop(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -6,23 +6,18 @@ namespace esphome::epaper_spi {
|
||||
|
||||
class EPaperSpectraE6 : public EPaperBase {
|
||||
public:
|
||||
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
|
||||
this->buffer_length_ = width * height / 2; // 2 pixels per byte
|
||||
}
|
||||
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
|
||||
: EPaperBase(init_sequence, init_sequence_length) {}
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
void fill(Color color) override;
|
||||
void clear() override;
|
||||
|
||||
protected:
|
||||
void refresh_screen() override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
uint32_t get_buffer_length() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void reset() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
from typing import Any, Self
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
|
||||
|
||||
|
||||
class EpaperModel:
|
||||
models: dict[str, Self] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
class_name: str,
|
||||
initsequence=None,
|
||||
**defaults,
|
||||
):
|
||||
name = name.upper()
|
||||
self.name = name
|
||||
self.class_name = class_name
|
||||
self.initsequence = initsequence
|
||||
self.defaults = defaults
|
||||
EpaperModel.models[name] = self
|
||||
|
||||
def get_default(self, key, fallback: Any = False) -> Any:
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
def get_init_sequence(self, config: dict):
|
||||
return self.initsequence
|
||||
|
||||
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
|
||||
if fallback is None and self.get_default(name, None) is None:
|
||||
return cv.Required(name)
|
||||
return cv.Optional(name, default=self.get_default(name, fallback))
|
||||
|
||||
def get_dimensions(self, config) -> tuple[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]
|
||||
else:
|
||||
(width, height) = dimensions
|
||||
|
||||
else:
|
||||
# Default dimensions, use model defaults
|
||||
width = self.get_default(CONF_WIDTH)
|
||||
height = self.get_default(CONF_HEIGHT)
|
||||
return width, height
|
||||
|
||||
def extend(self, name, **kwargs) -> "EpaperModel":
|
||||
"""
|
||||
Extend the current model with additional parameters or a modified init sequence.
|
||||
Parameters supplied here will override the defaults of the current model.
|
||||
if the initsequence is not provided, the current model's initsequence will be used.
|
||||
If add_init_sequence is provided, it will be appended to the current initsequence.
|
||||
:param name:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
initsequence = list(kwargs.pop("initsequence", self.initsequence) or ())
|
||||
initsequence.extend(kwargs.pop("add_init_sequence", ()))
|
||||
defaults = self.defaults.copy()
|
||||
defaults.update(kwargs)
|
||||
return self.__class__(name, initsequence=tuple(initsequence), **defaults)
|
||||
@@ -1,51 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class SpectraE6(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
|
||||
super().__init__(name, class_name, **kwargs)
|
||||
|
||||
# fmt: off
|
||||
def get_init_sequence(self, config: dict):
|
||||
width, height = self.get_dimensions(config)
|
||||
return (
|
||||
(0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,),
|
||||
(0x01, 0x3F,),
|
||||
(0x00, 0x5F, 0x69,),
|
||||
(0x03, 0x00, 0x54, 0x00, 0x44,),
|
||||
(0x05, 0x40, 0x1F, 0x1F, 0x2C,),
|
||||
(0x06, 0x6F, 0x1F, 0x17, 0x49,),
|
||||
(0x08, 0x6F, 0x1F, 0x1F, 0x22,),
|
||||
(0x30, 0x03,),
|
||||
(0x50, 0x3F,),
|
||||
(0x60, 0x02, 0x00,),
|
||||
(0x61, width // 256, width % 256, height // 256, height % 256,),
|
||||
(0x84, 0x01,),
|
||||
(0xE3, 0x2F,),
|
||||
)
|
||||
|
||||
def get_default(self, key, fallback: Any = False) -> Any:
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
|
||||
spectra_e6 = SpectraE6("spectra-e6")
|
||||
|
||||
spectra_e6.extend(
|
||||
"Seeed-reTerminal-E1002",
|
||||
width=800,
|
||||
height=480,
|
||||
data_rate="20MHz",
|
||||
cs_pin=10,
|
||||
dc_pin=11,
|
||||
reset_pin=12,
|
||||
busy_pin={
|
||||
"number": 13,
|
||||
"inverted": True,
|
||||
"mode": {
|
||||
"input": True,
|
||||
"pullup": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -558,7 +558,6 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
||||
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
|
||||
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
|
||||
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
|
||||
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||
@@ -655,9 +654,6 @@ FRAMEWORK_SCHEMA = cv.All(
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
|
||||
min=8192, max=32768
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
||||
@@ -930,10 +926,6 @@ async def to_code(config):
|
||||
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
|
||||
),
|
||||
)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_ARDUINO_LOOP_STACK_SIZE",
|
||||
conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
|
||||
)
|
||||
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
@@ -1079,10 +1071,6 @@ async def to_code(config):
|
||||
)
|
||||
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
|
||||
|
||||
cg.add_define(
|
||||
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
|
||||
)
|
||||
|
||||
cg.add_define(
|
||||
"USE_ESP_IDF_VERSION_CODE",
|
||||
cg.RawExpression(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
@@ -98,9 +97,9 @@ void loop_task(void *pv_params) {
|
||||
extern "C" void app_main() {
|
||||
esp32::setup_preferences();
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
|
||||
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
|
||||
#else
|
||||
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
|
||||
xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
@@ -22,7 +22,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
DOMAIN = "esp32_ble"
|
||||
|
||||
@@ -483,10 +482,13 @@ async def to_code(config):
|
||||
cg.add(var.set_name(name))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
|
||||
# BLE uses 1 UDP socket for event notification to wake up main loop from select()
|
||||
# This enables low-latency (~12μs) BLE event processing instead of waiting for
|
||||
# select() timeout (0-16ms). The wake socket is shared across all components.
|
||||
socket.require_wake_loop_threadsafe()
|
||||
# select() timeout (0-16ms). The socket is created in ble_setup_() and used to
|
||||
# wake lwip_select() when BLE events arrive from the BLE thread.
|
||||
# Note: Called during config generation, socket is created at runtime. In practice,
|
||||
# always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT.
|
||||
socket.consume_sockets(1, "esp32_ble")(config)
|
||||
|
||||
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
|
||||
@@ -27,6 +27,10 @@ extern "C" {
|
||||
#include <esp32-hal-bt.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
@@ -96,10 +100,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
@@ -297,10 +297,21 @@ bool ESP32BLE::ble_setup_() {
|
||||
// BLE takes some time to be fully set up, 200ms should be more than enough
|
||||
delay(200); // NOLINT
|
||||
|
||||
// Set up notification socket to wake main loop for BLE events
|
||||
// This enables low-latency (~12μs) event processing instead of waiting for select() timeout
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
this->setup_event_notification_();
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
// Clean up notification socket first before dismantling BLE stack
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
this->cleanup_event_notification_();
|
||||
#endif
|
||||
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
@@ -398,6 +409,12 @@ void ESP32BLE::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Drain any notification socket events first
|
||||
// This clears the socket so it doesn't stay "ready" in subsequent select() calls
|
||||
this->drain_event_notifications_();
|
||||
#endif
|
||||
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
while (ble_event != nullptr) {
|
||||
switch (ble_event->type_) {
|
||||
@@ -572,8 +589,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
GAP_SECURITY_EVENTS:
|
||||
enqueue_ble_event(event, param);
|
||||
// Wake up main loop to process security event immediately
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
return;
|
||||
|
||||
@@ -595,8 +612,8 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
|
||||
esp_ble_gatts_cb_param_t *param) {
|
||||
enqueue_ble_event(event, gatts_if, param);
|
||||
// Wake up main loop to process GATT event immediately
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -606,8 +623,8 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
enqueue_ble_event(event, gattc_if, param);
|
||||
// Wake up main loop to process GATT event immediately
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -648,6 +665,89 @@ void ESP32BLE::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
void ESP32BLE::setup_event_notification_() {
|
||||
// Create UDP socket for event notifications
|
||||
this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (this->notify_fd_ < 0) {
|
||||
ESP_LOGW(TAG, "Event socket create failed: %d", errno);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind to loopback with auto-assigned port
|
||||
struct sockaddr_in addr = {};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0; // Auto-assign port
|
||||
|
||||
if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket bind failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the assigned address and connect to it
|
||||
// Connecting a UDP socket allows using send() instead of sendto() for better performance
|
||||
struct sockaddr_in notify_addr;
|
||||
socklen_t len = sizeof(notify_addr);
|
||||
if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket address failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to self (loopback) - allows using send() instead of sendto()
|
||||
// After connect(), no need to store notify_addr - the socket remembers it
|
||||
if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket connect failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set non-blocking mode
|
||||
int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0);
|
||||
lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Register with application's select() loop
|
||||
if (!App.register_socket_fd(this->notify_fd_)) {
|
||||
ESP_LOGW(TAG, "Event socket register failed");
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Event socket ready");
|
||||
}
|
||||
|
||||
void ESP32BLE::cleanup_event_notification_() {
|
||||
if (this->notify_fd_ >= 0) {
|
||||
App.unregister_socket_fd(this->notify_fd_);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
ESP_LOGD(TAG, "Event socket closed");
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLE::drain_event_notifications_() {
|
||||
// Called from main loop to drain any pending notifications
|
||||
// Must check is_socket_ready() to avoid blocking on empty socket
|
||||
if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) {
|
||||
char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE];
|
||||
// Drain all pending notifications with non-blocking reads
|
||||
// Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
|
||||
// We control both ends of this loopback socket (always write 1 byte per event),
|
||||
// so no error checking needed - any errors indicate catastrophic system failure
|
||||
while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
|
||||
// Just draining, no action needed - actual BLE events are already queued
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||
|
||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
|
||||
uint64_t u = 0;
|
||||
u |= uint64_t(address[0] & 0xFF) << 40;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
// Maximum size of the BLE event queue
|
||||
@@ -118,7 +122,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
@@ -163,10 +166,12 @@ class ESP32BLE : public Component {
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
|
||||
// from BLE tasks. This enables low-latency (~12μs) event processing instead of
|
||||
// waiting for select() timeout (0-16ms). The wake socket is shared with other
|
||||
// components that need this functionality.
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
void setup_event_notification_(); // Create notification socket
|
||||
void cleanup_event_notification_(); // Close and unregister socket
|
||||
inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined)
|
||||
void drain_event_notifications_(); // Read pending notifications in main loop
|
||||
#endif
|
||||
|
||||
private:
|
||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||
@@ -202,6 +207,13 @@ class ESP32BLE : public Component {
|
||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
||||
uint32_t advertising_cycle_time_{}; // 4 bytes
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Event notification socket for waking up main loop from BLE thread
|
||||
// Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout
|
||||
// Socket is connected during setup, allowing use of send() instead of sendto() for efficiency
|
||||
int notify_fd_{-1}; // 4 bytes (file descriptor)
|
||||
#endif
|
||||
|
||||
// 2-byte aligned members
|
||||
uint16_t appearance_{0}; // 2 bytes
|
||||
|
||||
@@ -213,6 +225,29 @@ class ESP32BLE : public Component {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern ESP32BLE *global_ble;
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Inline implementations for hot-path functions
|
||||
// These are called from BLE thread (notify) and main loop (drain) on every event
|
||||
|
||||
// Small buffer for draining notification bytes (1 byte sent per BLE event)
|
||||
// Size allows draining multiple notifications per recvfrom() without wasting stack
|
||||
static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16;
|
||||
|
||||
inline void ESP32BLE::notify_main_loop_() {
|
||||
// Called from BLE thread context when events are queued
|
||||
// Wakes up lwip_select() in main loop by writing to connected loopback socket
|
||||
if (this->notify_fd_ >= 0) {
|
||||
const char dummy = 1;
|
||||
// Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
|
||||
// No error checking needed: we control both ends of this loopback socket, and the
|
||||
// BLE event is already queued. Notification is best-effort to reduce latency.
|
||||
// This is safe to call from BLE thread - send() is thread-safe in lwip
|
||||
// Socket is already connected to loopback address, so send() is faster than sendto()
|
||||
lwip_send(this->notify_fd_, &dummy, 1, 0);
|
||||
}
|
||||
}
|
||||
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||
|
||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(Ts... x) override { return global_ble->is_active(); }
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -389,8 +389,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (this->conn_id_ != param->search_res.conn_id)
|
||||
return false;
|
||||
this->service_count_++;
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// V3 clients don't need services initialized since
|
||||
// as they use the ESP APIs to get services.
|
||||
break;
|
||||
|
||||
@@ -4,7 +4,6 @@ from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.psram import DOMAIN as psram_domain
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
@@ -27,9 +26,10 @@ import esphome.final_validate as fv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["camera"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
AUTO_LOAD = ["camera", "psram"]
|
||||
|
||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
|
||||
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
|
||||
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
|
||||
@@ -163,14 +163,6 @@ CONF_ON_IMAGE = "on_image"
|
||||
|
||||
camera_range_param = cv.int_range(min=-2, max=2)
|
||||
|
||||
|
||||
def validate_fb_location_(value):
|
||||
validator = cv.enum(ENUM_FB_LOCATION, upper=True)
|
||||
if value.lower() == psram_domain:
|
||||
validator = cv.All(validator, cv.requires_component(psram_domain))
|
||||
return validator(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.ENTITY_BASE_SCHEMA.extend(
|
||||
{
|
||||
@@ -244,9 +236,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.framerate, cv.Range(min=0, max=1)
|
||||
),
|
||||
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
|
||||
cv.Optional(
|
||||
CONF_FRAME_BUFFER_LOCATION, default="PSRAM"
|
||||
): validate_fb_location_,
|
||||
cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum(
|
||||
ENUM_FB_LOCATION, upper=True
|
||||
),
|
||||
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import hashlib
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, update
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
|
||||
from esphome.core import CORE, HexInt
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
AUTO_LOAD = ["sha256", "watchdog"]
|
||||
DEPENDENCIES = ["esp32_hosted"]
|
||||
|
||||
CONF_SHA256 = "sha256"
|
||||
|
||||
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
|
||||
Esp32HostedUpdate = esp32_hosted_ns.class_(
|
||||
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
|
||||
)
|
||||
|
||||
|
||||
def _validate_sha256(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != 64:
|
||||
raise cv.Invalid("SHA256 must be 64 hexadecimal characters")
|
||||
try:
|
||||
bytes.fromhex(value)
|
||||
except ValueError as e:
|
||||
raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
|
||||
{
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
cv.Required(CONF_SHA256): _validate_sha256,
|
||||
}
|
||||
),
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
esp32.const.VARIANT_ESP32H2,
|
||||
esp32.const.VARIANT_ESP32P4,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _validate_firmware(config: dict[str, Any]) -> None:
|
||||
path = CORE.relative_config_path(config[CONF_PATH])
|
||||
with open(path, "rb") as f:
|
||||
firmware_data = f.read()
|
||||
calculated = hashlib.sha256(firmware_data).hexdigest()
|
||||
expected = config[CONF_SHA256].lower()
|
||||
if calculated != expected:
|
||||
raise cv.Invalid(
|
||||
f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_firmware
|
||||
|
||||
|
||||
async def to_code(config: dict[str, Any]) -> None:
|
||||
var = await update.new_update(config)
|
||||
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
|
||||
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
|
||||
cg.add(var.set_firmware_data(prog_arr))
|
||||
cg.add(var.set_firmware_size(len(firmware_data)))
|
||||
await cg.register_component(var, config)
|
||||
@@ -1,164 +0,0 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#include "esp32_hosted_update.h"
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
#include "esphome/components/sha256/sha256.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <esp_image_format.h>
|
||||
#include <esp_app_desc.h>
|
||||
#include <esp_hosted.h>
|
||||
|
||||
extern "C" {
|
||||
#include <esp_hosted_ota.h>
|
||||
}
|
||||
|
||||
namespace esphome::esp32_hosted {
|
||||
|
||||
static const char *const TAG = "esp32_hosted.update";
|
||||
|
||||
// older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
constexpr size_t CHUNK_SIZE = 1500;
|
||||
|
||||
void Esp32HostedUpdate::setup() {
|
||||
this->update_info_.title = "ESP32 Hosted Coprocessor";
|
||||
|
||||
// get coprocessor version
|
||||
esp_hosted_coprocessor_fwver_t ver_info;
|
||||
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
|
||||
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
|
||||
} else {
|
||||
this->update_info_.current_version = "unknown";
|
||||
}
|
||||
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
|
||||
|
||||
// get image version
|
||||
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
|
||||
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
|
||||
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
|
||||
if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
|
||||
ESP_LOGD(TAG, "Firmware version: %s", app_desc->version);
|
||||
ESP_LOGD(TAG, "Project name: %s", app_desc->project_name);
|
||||
ESP_LOGD(TAG, "Build date: %s", app_desc->date);
|
||||
ESP_LOGD(TAG, "Build time: %s", app_desc->time);
|
||||
ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver);
|
||||
this->update_info_.latest_version = app_desc->version;
|
||||
if (this->update_info_.latest_version != this->update_info_.current_version) {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
} else {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
|
||||
ESP_APP_DESC_MAGIC_WORD);
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Firmware too small to contain app description");
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
|
||||
// publish state
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP32 Hosted Update:\n"
|
||||
" Current Version: %s\n"
|
||||
" Latest Version: %s\n"
|
||||
" Latest Size: %zu bytes",
|
||||
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
|
||||
this->firmware_size_);
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::perform(bool force) {
|
||||
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
|
||||
ESP_LOGW(TAG, "Update not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
|
||||
ESP_LOGE(TAG, "No firmware data available");
|
||||
return;
|
||||
}
|
||||
|
||||
sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
hasher.add(this->firmware_data_, this->firmware_size_);
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
this->status_set_error("SHA256 verification failed");
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
|
||||
|
||||
watchdog::WatchdogManager watchdog(20000);
|
||||
update::UpdateState prev_state = this->state_;
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = false;
|
||||
this->publish_state();
|
||||
|
||||
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to begin OTA");
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t chunk[CHUNK_SIZE];
|
||||
const uint8_t *data_ptr = this->firmware_data_;
|
||||
size_t remaining = this->firmware_size_;
|
||||
while (remaining > 0) {
|
||||
size_t chunk_size = std::min(remaining, static_cast<size_t>(CHUNK_SIZE));
|
||||
memcpy(chunk, data_ptr, chunk_size);
|
||||
err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to write OTA data");
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
data_ptr += chunk_size;
|
||||
remaining -= chunk_size;
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
err = esp_hosted_slave_ota_end(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to end OTA");
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// activate new firmware
|
||||
err = esp_hosted_slave_ota_activate(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to activate OTA");
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// update state
|
||||
ESP_LOGI(TAG, "OTA update successful");
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
|
||||
// schedule a restart to ensure everything is in sync
|
||||
ESP_LOGI(TAG, "Restarting in 1 second");
|
||||
this->set_timeout(1000, []() { App.safe_reboot(); });
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32_hosted
|
||||
#endif
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#include <array>
|
||||
|
||||
namespace esphome::esp32_hosted {
|
||||
|
||||
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void perform(bool force) override;
|
||||
void check() override {}
|
||||
|
||||
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
|
||||
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
|
||||
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
|
||||
|
||||
protected:
|
||||
const uint8_t *firmware_data_{nullptr};
|
||||
size_t firmware_size_{0};
|
||||
std::array<uint8_t, 32> firmware_sha256_;
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_hosted
|
||||
|
||||
#endif
|
||||
@@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
"Over-The-Air updates:\n"
|
||||
" Address: %s:%u\n"
|
||||
" Version: %d",
|
||||
network::get_use_address(), this->port_, USE_OTA_VERSION);
|
||||
network::get_use_address().c_str(), this->port_, USE_OTA_VERSION);
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
if (!this->password_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Password configured");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation, core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket, wifi
|
||||
from esphome.components import wifi
|
||||
from esphome.components.udp import CONF_ON_RECEIVE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -17,7 +17,6 @@ from esphome.core import CORE, HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
|
||||
byte_vector = cg.std_vector.template(cg.uint8)
|
||||
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
||||
@@ -121,10 +120,6 @@ async def to_code(config):
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
|
||||
# This enables low-latency event processing instead of waiting for select() timeout
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
cg.add_define("USE_ESPNOW")
|
||||
if wifi_channel := config.get(CONF_CHANNEL):
|
||||
cg.add(var.set_wifi_channel(wifi_channel))
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "espnow_err.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -98,11 +97,6 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
|
||||
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
@@ -120,11 +114,6 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
|
||||
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
|
||||
|
||||
@@ -691,9 +691,9 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
|
||||
const std::string &EthernetComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
esp_err_t err;
|
||||
|
||||
@@ -88,8 +88,8 @@ class EthernetComponent : public Component {
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
network::IPAddress get_dns_address(uint8_t num);
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
std::string get_eth_mac_address_pretty();
|
||||
eth_duplex_t get_duplex_mode();
|
||||
@@ -114,6 +114,7 @@ class EthernetComponent : public Component {
|
||||
/// @brief Set arbitratry PHY registers from config.
|
||||
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
|
||||
|
||||
std::string use_address_;
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
uint8_t clk_pin_;
|
||||
uint8_t miso_pin_;
|
||||
@@ -157,11 +158,6 @@ class EthernetComponent : public Component {
|
||||
esp_eth_handle_t eth_handle_;
|
||||
esp_eth_phy_t *phy_{nullptr};
|
||||
optional<std::array<uint8_t, 6>> fixed_mac_;
|
||||
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -212,19 +212,18 @@ class FanPresetSetTrigger : public Trigger<std::string> {
|
||||
public:
|
||||
FanPresetSetTrigger(Fan *state) {
|
||||
state->add_on_state_callback([this, state]() {
|
||||
const auto *preset_mode = state->get_preset_mode();
|
||||
auto preset_mode = state->preset_mode;
|
||||
auto should_trigger = preset_mode != this->last_preset_mode_;
|
||||
this->last_preset_mode_ = preset_mode;
|
||||
if (should_trigger) {
|
||||
// Trigger with empty string when nullptr to maintain backward compatibility
|
||||
this->trigger(preset_mode != nullptr ? preset_mode : "");
|
||||
this->trigger(preset_mode);
|
||||
}
|
||||
});
|
||||
this->last_preset_mode_ = state->get_preset_mode();
|
||||
this->last_preset_mode_ = state->preset_mode;
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *last_preset_mode_{nullptr};
|
||||
std::string last_preset_mode_;
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
|
||||
@@ -17,27 +17,6 @@ const LogString *fan_direction_to_string(FanDirection direction) {
|
||||
}
|
||||
}
|
||||
|
||||
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
|
||||
|
||||
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
|
||||
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
|
||||
this->preset_mode_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Find and validate pointer from traits immediately
|
||||
auto traits = this->parent_.get_traits();
|
||||
const char *validated_mode = traits.find_preset_mode(preset_mode);
|
||||
if (validated_mode != nullptr) {
|
||||
this->preset_mode_ = validated_mode; // Store pointer from traits
|
||||
} else {
|
||||
// Preset mode not found in traits - log warning and don't set
|
||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
|
||||
this->preset_mode_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void FanCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
|
||||
this->validate_();
|
||||
@@ -53,8 +32,8 @@ void FanCall::perform() {
|
||||
if (this->direction_.has_value()) {
|
||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
|
||||
}
|
||||
if (this->has_preset_mode()) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
|
||||
if (!this->preset_mode_.empty()) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str());
|
||||
}
|
||||
this->parent_.control(*this);
|
||||
}
|
||||
@@ -67,15 +46,30 @@ void FanCall::validate_() {
|
||||
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
this->preset_mode_ = nullptr;
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
|
||||
if (!this->preset_mode_.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
bool found = false;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode_.c_str()) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// when turning on...
|
||||
if (!this->parent_.state && this->binary_state_.has_value() &&
|
||||
*this->binary_state_
|
||||
// ..,and no preset mode will be active...
|
||||
&& !this->has_preset_mode() &&
|
||||
this->parent_.get_preset_mode() == nullptr
|
||||
&& this->preset_mode_.empty() &&
|
||||
this->parent_.preset_mode.empty()
|
||||
// ...and neither current nor new speed is available...
|
||||
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
||||
// ...set speed to 100%
|
||||
@@ -123,13 +117,12 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
|
||||
auto traits = fan.get_traits();
|
||||
if (traits.supports_preset_modes()) {
|
||||
// Use stored preset index to get preset name from traits
|
||||
// Use stored preset index to get preset name
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (this->preset_mode < preset_modes.size()) {
|
||||
fan.set_preset_mode_(preset_modes[this->preset_mode]);
|
||||
fan.preset_mode = preset_modes[this->preset_mode];
|
||||
}
|
||||
}
|
||||
|
||||
fan.publish_state();
|
||||
}
|
||||
|
||||
@@ -138,29 +131,6 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall Fan::make_call() { return FanCall(*this); }
|
||||
|
||||
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
|
||||
|
||||
bool Fan::set_preset_mode_(const char *preset_mode) {
|
||||
if (preset_mode == nullptr) {
|
||||
// Treat nullptr as clearing the preset mode
|
||||
if (this->preset_mode_ == nullptr) {
|
||||
return false; // No change
|
||||
}
|
||||
this->clear_preset_mode_();
|
||||
return true;
|
||||
}
|
||||
const char *validated = this->find_preset_mode_(preset_mode);
|
||||
if (validated == nullptr || this->preset_mode_ == validated) {
|
||||
return false; // Preset mode not supported or no change
|
||||
}
|
||||
this->preset_mode_ = validated;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
|
||||
|
||||
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
|
||||
|
||||
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
void Fan::publish_state() {
|
||||
auto traits = this->get_traits();
|
||||
@@ -176,9 +146,8 @@ void Fan::publish_state() {
|
||||
if (traits.supports_direction()) {
|
||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
|
||||
}
|
||||
const char *preset = this->get_preset_mode();
|
||||
if (preset != nullptr) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", preset);
|
||||
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str());
|
||||
}
|
||||
this->state_callback_.call();
|
||||
this->save_state_();
|
||||
@@ -230,15 +199,16 @@ void Fan::save_state_() {
|
||||
state.speed = this->speed;
|
||||
state.direction = this->direction;
|
||||
|
||||
const char *preset = this->get_preset_mode();
|
||||
if (traits.supports_preset_modes() && preset != nullptr) {
|
||||
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
|
||||
for (size_t i = 0; i < preset_modes.size(); i++) {
|
||||
if (preset_modes[i] == preset) {
|
||||
// Store index of current preset mode
|
||||
size_t i = 0;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode.c_str()) == 0) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,10 +70,11 @@ class FanCall {
|
||||
return *this;
|
||||
}
|
||||
optional<FanDirection> get_direction() const { return this->direction_; }
|
||||
FanCall &set_preset_mode(const std::string &preset_mode);
|
||||
FanCall &set_preset_mode(const char *preset_mode);
|
||||
const char *get_preset_mode() const { return this->preset_mode_; }
|
||||
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
|
||||
FanCall &set_preset_mode(const std::string &preset_mode) {
|
||||
this->preset_mode_ = preset_mode;
|
||||
return *this;
|
||||
}
|
||||
std::string get_preset_mode() const { return this->preset_mode_; }
|
||||
|
||||
void perform();
|
||||
|
||||
@@ -85,7 +86,7 @@ class FanCall {
|
||||
optional<bool> oscillating_;
|
||||
optional<int> speed_;
|
||||
optional<FanDirection> direction_{};
|
||||
const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation)
|
||||
std::string preset_mode_{};
|
||||
};
|
||||
|
||||
struct FanRestoreState {
|
||||
@@ -111,6 +112,8 @@ class Fan : public EntityBase {
|
||||
int speed{0};
|
||||
/// The current direction of the fan
|
||||
FanDirection direction{FanDirection::FORWARD};
|
||||
// The current preset mode of the fan
|
||||
std::string preset_mode{};
|
||||
|
||||
FanCall turn_on();
|
||||
FanCall turn_off();
|
||||
@@ -127,15 +130,8 @@ class Fan : public EntityBase {
|
||||
/// Set the restore mode of this fan.
|
||||
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
/// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set)
|
||||
const char *get_preset_mode() const { return this->preset_mode_; }
|
||||
|
||||
/// Check if a preset mode is currently active
|
||||
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
|
||||
|
||||
protected:
|
||||
friend FanCall;
|
||||
friend struct FanRestoreState;
|
||||
|
||||
virtual void control(const FanCall &call) = 0;
|
||||
|
||||
@@ -144,21 +140,9 @@ class Fan : public EntityBase {
|
||||
|
||||
void dump_traits_(const char *tag, const char *prefix);
|
||||
|
||||
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||
bool set_preset_mode_(const char *preset_mode);
|
||||
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||
bool set_preset_mode_(const std::string &preset_mode);
|
||||
/// Clear the preset mode
|
||||
void clear_preset_mode_();
|
||||
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
|
||||
const char *find_preset_mode_(const char *preset_mode);
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
FanRestoreMode restore_mode_;
|
||||
|
||||
private:
|
||||
const char *preset_mode_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
@@ -45,17 +44,6 @@ class FanTraits {
|
||||
|
||||
/// Return if preset modes are supported
|
||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
|
||||
const char *find_preset_mode(const char *preset_mode) const {
|
||||
if (preset_mode == nullptr)
|
||||
return nullptr;
|
||||
for (const char *mode : this->preset_modes_) {
|
||||
if (strcmp(mode, preset_mode) == 0) {
|
||||
return mode; // Return pointer from traits
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool oscillation_{false};
|
||||
|
||||
@@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) {
|
||||
this->oscillating = *call.get_oscillating();
|
||||
if (call.get_direction().has_value())
|
||||
this->direction = *call.get_direction();
|
||||
this->set_preset_mode_(call.get_preset_mode());
|
||||
this->preset_mode = call.get_preset_mode();
|
||||
|
||||
this->write_state_();
|
||||
this->publish_state();
|
||||
|
||||
@@ -2,18 +2,11 @@ import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
zephyr_add_prj_conf,
|
||||
zephyr_data,
|
||||
)
|
||||
from esphome.components.zephyr.const import KEY_BOARD
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_SCAN,
|
||||
@@ -22,12 +15,10 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2040,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,7 +28,6 @@ I2CBus = i2c_ns.class_("I2CBus")
|
||||
InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
|
||||
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
|
||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
|
||||
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
|
||||
I2CDevice = i2c_ns.class_("I2CDevice")
|
||||
|
||||
|
||||
@@ -51,8 +41,6 @@ def _bus_declare_type(value):
|
||||
return cv.declare_id(ArduinoI2CBus)(value)
|
||||
if CORE.using_esp_idf:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_zephyr:
|
||||
return cv.declare_id(ZephyrI2CBus)(value)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -74,70 +62,23 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All(
|
||||
cv.only_with_esp_idf, cv.boolean
|
||||
),
|
||||
cv.SplitDefault(
|
||||
CONF_FREQUENCY,
|
||||
esp32="50kHz",
|
||||
esp8266="50kHz",
|
||||
rp2040="50kHz",
|
||||
nrf52="100kHz",
|
||||
): cv.All(
|
||||
cv.frequency,
|
||||
cv.Range(min=0, min_included=False),
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): cv.All(
|
||||
cv.only_with_framework(["arduino", "esp-idf"]),
|
||||
cv.positive_time_period,
|
||||
cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All(
|
||||
cv.frequency, cv.Range(min=0, min_included=False)
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): cv.positive_time_period,
|
||||
cv.Optional(CONF_SCAN, default=True): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
|
||||
validate_config,
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
full_config = fv.full_config.get()[CONF_I2C]
|
||||
if CORE.using_zephyr and len(full_config) > 1:
|
||||
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(config):
|
||||
cg.add_global(i2c_ns.using)
|
||||
cg.add_define("USE_I2C")
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("I2C", True)
|
||||
i2c = "i2c0"
|
||||
if zephyr_data()[KEY_BOARD] in ["xiao_ble"]:
|
||||
i2c = "i2c1"
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&pinctrl {{
|
||||
{i2c}_default: {i2c}_default {{
|
||||
group1 {{
|
||||
psels = <NRF_PSEL(TWIM_SDA, {config[CONF_SDA] // 32}, {config[CONF_SDA] % 32})>,
|
||||
<NRF_PSEL(TWIM_SCL, {config[CONF_SCL] // 32}, {config[CONF_SCL] % 32})>;
|
||||
}};
|
||||
}};
|
||||
{i2c}_sleep: {i2c}_sleep {{
|
||||
group1 {{
|
||||
psels = <NRF_PSEL(TWIM_SDA, {config[CONF_SDA] // 32}, {config[CONF_SDA] % 32})>,
|
||||
<NRF_PSEL(TWIM_SCL, {config[CONF_SCL] // 32}, {config[CONF_SCL] % 32})>;
|
||||
low-power-enable;
|
||||
}};
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))")
|
||||
)
|
||||
else:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_sda_pin(config[CONF_SDA]))
|
||||
@@ -256,6 +197,5 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
|
||||
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "i2c.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <memory>
|
||||
|
||||
@@ -24,8 +23,6 @@ void I2CBus::i2c_scan_() {
|
||||
} else if (err == ERROR_UNKNOWN) {
|
||||
scan_results_.emplace_back(address, false);
|
||||
}
|
||||
// it takes 16sec to scan on nrf52. It prevents board reset.
|
||||
arch_feed_wdt();
|
||||
}
|
||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
|
||||
esp_log_level_set("*", previous);
|
||||
|
||||
@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "i2c_bus_zephyr.h"
|
||||
#include <zephyr/drivers/i2c.h>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
static const char *const TAG = "i2c.zephyr";
|
||||
|
||||
void ZephyrI2CBus::setup() {
|
||||
if (!device_is_ready(this->i2c_dev_)) {
|
||||
ESP_LOGE(TAG, "I2C dev is not ready.");
|
||||
mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
int ret = i2c_configure(this->i2c_dev_, this->dev_config_);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "I2C: Failed to configure device");
|
||||
}
|
||||
|
||||
this->recovery_result_ = i2c_recover_bus(this->i2c_dev_);
|
||||
if (this->recovery_result_ != 0) {
|
||||
ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_);
|
||||
}
|
||||
if (this->scan_) {
|
||||
ESP_LOGV(TAG, "Scanning I2C bus for active devices...");
|
||||
this->i2c_scan_();
|
||||
}
|
||||
}
|
||||
|
||||
void ZephyrI2CBus::dump_config() {
|
||||
auto get_speed = [](uint32_t dev_config) {
|
||||
switch (I2C_SPEED_GET(dev_config)) {
|
||||
case I2C_SPEED_STANDARD:
|
||||
return "100 kHz";
|
||||
case I2C_SPEED_FAST:
|
||||
return "400 kHz";
|
||||
case I2C_SPEED_FAST_PLUS:
|
||||
return "1 MHz";
|
||||
case I2C_SPEED_HIGH:
|
||||
return "3.4 MHz";
|
||||
case I2C_SPEED_ULTRA:
|
||||
return "5 MHz";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"I2C Bus:\n"
|
||||
" SDA Pin: GPIO%u\n"
|
||||
" SCL Pin: GPIO%u\n"
|
||||
" Frequency: %s\n"
|
||||
" Name: %s",
|
||||
this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name);
|
||||
|
||||
if (this->recovery_result_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Recovery: failed, err %d", this->recovery_result_);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
|
||||
}
|
||||
if (this->scan_) {
|
||||
ESP_LOGI(TAG, "Results from I2C bus scan:");
|
||||
if (scan_results_.empty()) {
|
||||
ESP_LOGI(TAG, "Found no I2C devices!");
|
||||
} else {
|
||||
for (const auto &s : scan_results_) {
|
||||
if (s.second) {
|
||||
ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count) {
|
||||
if (!device_is_ready(this->i2c_dev_)) {
|
||||
return ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
i2c_msg msgs[2]{};
|
||||
size_t cnt = 0;
|
||||
uint8_t dst = 0x00; // dummy data to not use random value
|
||||
|
||||
if (read_count == 0 && write_count == 0) {
|
||||
msgs[cnt].buf = &dst;
|
||||
msgs[cnt].len = 0U;
|
||||
msgs[cnt++].flags = I2C_MSG_WRITE;
|
||||
} else {
|
||||
if (write_count) {
|
||||
// the same struct is used for read/write — const cast is fine; data isn't modified
|
||||
msgs[cnt].buf = const_cast<uint8_t *>(write_buffer);
|
||||
msgs[cnt].len = write_count;
|
||||
msgs[cnt++].flags = I2C_MSG_WRITE;
|
||||
}
|
||||
if (read_count) {
|
||||
msgs[cnt].buf = const_cast<uint8_t *>(read_buffer);
|
||||
msgs[cnt].len = read_count;
|
||||
msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART;
|
||||
}
|
||||
}
|
||||
|
||||
msgs[cnt - 1].flags |= I2C_MSG_STOP;
|
||||
|
||||
auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address);
|
||||
|
||||
if (err == -EIO) {
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
ESP_LOGE(TAG, "i2c transfer error %d", err);
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
return ERROR_OK;
|
||||
}
|
||||
|
||||
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
|
||||
this->dev_config_ &= ~I2C_SPEED_MASK;
|
||||
if (frequency >= 400000) {
|
||||
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST);
|
||||
} else {
|
||||
this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "i2c_bus.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
struct device;
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
class ZephyrI2CBus : public InternalI2CBus, public Component {
|
||||
public:
|
||||
explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_scan(bool scan) { scan_ = scan; }
|
||||
void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
|
||||
void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
|
||||
void set_frequency(uint32_t frequency);
|
||||
|
||||
int get_port() const override { return 0; }
|
||||
|
||||
protected:
|
||||
const device *i2c_dev_;
|
||||
int recovery_result_ = 0;
|
||||
uint8_t sda_pin_{};
|
||||
uint8_t scl_pin_{};
|
||||
uint32_t dev_config_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif
|
||||
@@ -20,7 +20,8 @@ import esphome.final_validate as fv
|
||||
|
||||
from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS
|
||||
|
||||
DEPENDENCIES = ["i2c", "esp32", "psram"]
|
||||
DEPENDENCIES = ["i2c", "esp32"]
|
||||
AUTO_LOAD = ["psram"]
|
||||
|
||||
CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin"
|
||||
CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin"
|
||||
|
||||
@@ -41,7 +41,10 @@ from .lv_validation import lv_bool, lv_images_used
|
||||
from .lvcode import LvContext, LvglComponent, lvgl_static
|
||||
from .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
FLEX_OBJ_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
LAYOUT_SCHEMAS,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
@@ -75,7 +78,6 @@ from .widgets.button import button_spec
|
||||
from .widgets.buttonmatrix import buttonmatrix_spec
|
||||
from .widgets.canvas import canvas_spec
|
||||
from .widgets.checkbox import checkbox_spec
|
||||
from .widgets.container import container_spec
|
||||
from .widgets.dropdown import dropdown_spec
|
||||
from .widgets.img import img_spec
|
||||
from .widgets.keyboard import keyboard_spec
|
||||
@@ -128,10 +130,20 @@ for w_type in (
|
||||
tileview_spec,
|
||||
qr_code_spec,
|
||||
canvas_spec,
|
||||
container_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
WIDGET_SCHEMA = any_widget_schema()
|
||||
|
||||
LAYOUT_SCHEMAS[df.TYPE_GRID] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_FLEX] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_NONE] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())
|
||||
}
|
||||
for w_type in WIDGET_TYPES.values():
|
||||
register_action(
|
||||
f"lvgl.{w_type.name}.update",
|
||||
@@ -398,7 +410,7 @@ def display_schema(config):
|
||||
def add_hello_world(config):
|
||||
if df.CONF_WIDGETS not in config and CONF_PAGES not in config:
|
||||
LOGGER.info("No pages or widgets configured, creating default hello_world page")
|
||||
config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world())
|
||||
config[df.CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world())
|
||||
return config
|
||||
|
||||
|
||||
@@ -438,7 +450,6 @@ LVGL_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)),
|
||||
**{
|
||||
cv.Optional(x): validate_automation(
|
||||
{
|
||||
@@ -448,6 +459,12 @@ LVGL_SCHEMA = cv.All(
|
||||
)
|
||||
for x in SIMPLE_TRIGGERS
|
||||
},
|
||||
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
|
||||
WIDGET_SCHEMA
|
||||
),
|
||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
container_schema(page_spec)
|
||||
),
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
|
||||
@@ -394,8 +394,6 @@ LV_FLEX_ALIGNMENTS = LvConstant(
|
||||
"SPACE_BETWEEN",
|
||||
)
|
||||
|
||||
LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH")
|
||||
|
||||
LV_MENU_MODES = LvConstant(
|
||||
"LV_MENU_HEADER_",
|
||||
"TOP_FIXED",
|
||||
@@ -438,7 +436,6 @@ CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
CONF_CONTAINER = "container"
|
||||
CONF_CONTROL = "control"
|
||||
CONF_DEFAULT_FONT = "default_font"
|
||||
CONF_DEFAULT_GROUP = "default_group"
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
import re
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH
|
||||
|
||||
from .defines import (
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
CONF_FLEX_ALIGN_MAIN,
|
||||
CONF_FLEX_ALIGN_TRACK,
|
||||
CONF_FLEX_FLOW,
|
||||
CONF_FLEX_GROW,
|
||||
CONF_GRID_CELL_COLUMN_POS,
|
||||
CONF_GRID_CELL_COLUMN_SPAN,
|
||||
CONF_GRID_CELL_ROW_POS,
|
||||
CONF_GRID_CELL_ROW_SPAN,
|
||||
CONF_GRID_CELL_X_ALIGN,
|
||||
CONF_GRID_CELL_Y_ALIGN,
|
||||
CONF_GRID_COLUMN_ALIGN,
|
||||
CONF_GRID_COLUMNS,
|
||||
CONF_GRID_ROW_ALIGN,
|
||||
CONF_GRID_ROWS,
|
||||
CONF_LAYOUT,
|
||||
CONF_PAD_COLUMN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_WIDGETS,
|
||||
FLEX_FLOWS,
|
||||
LV_CELL_ALIGNMENTS,
|
||||
LV_FLEX_ALIGNMENTS,
|
||||
LV_FLEX_CROSS_ALIGNMENTS,
|
||||
LV_GRID_ALIGNMENTS,
|
||||
TYPE_FLEX,
|
||||
TYPE_GRID,
|
||||
TYPE_NONE,
|
||||
LvConstant,
|
||||
)
|
||||
from .lv_validation import padding, size
|
||||
|
||||
cell_alignments = LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
FLEX_LAYOUT_SCHEMA = {
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_FLEX, lower=True),
|
||||
cv.Optional(CONF_FLEX_FLOW, default="row_wrap"): FLEX_FLOWS.one_of,
|
||||
cv.Optional(CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
|
||||
cv.Optional(
|
||||
CONF_FLEX_ALIGN_CROSS, default="start"
|
||||
): LV_FLEX_CROSS_ALIGNMENTS.one_of,
|
||||
cv.Optional(CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
cv.Optional(CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
FLEX_HV_STYLE = {
|
||||
CONF_FLEX_ALIGN_MAIN: "LV_FLEX_ALIGN_SPACE_EVENLY",
|
||||
CONF_FLEX_ALIGN_TRACK: "LV_FLEX_ALIGN_CENTER",
|
||||
CONF_FLEX_ALIGN_CROSS: "LV_FLEX_ALIGN_CENTER",
|
||||
CONF_TYPE: TYPE_FLEX,
|
||||
}
|
||||
|
||||
FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
|
||||
def flex_hv_schema(dir):
|
||||
dir = CONF_HEIGHT if dir == "horizontal" else CONF_WIDTH
|
||||
return {
|
||||
cv.Optional(CONF_FLEX_GROW, default=1): cv.int_,
|
||||
cv.Optional(dir, default="100%"): size,
|
||||
}
|
||||
|
||||
|
||||
def grid_free_space(value):
|
||||
value = cv.Upper(value)
|
||||
if value.startswith("FR(") and value.endswith(")"):
|
||||
value = value.removesuffix(")").removeprefix("FR(")
|
||||
return f"LV_GRID_FR({cv.positive_int(value)})"
|
||||
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
|
||||
|
||||
|
||||
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
|
||||
|
||||
class Layout:
|
||||
"""
|
||||
Define properties for a layout
|
||||
The base class is layout "none"
|
||||
"""
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_NONE
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
"""
|
||||
Get the layout and child schema for a given widget based on its layout type.
|
||||
"""
|
||||
return None, {}
|
||||
|
||||
def validate(self, config):
|
||||
"""
|
||||
Validate the layout configuration. This is called late in the schema validation
|
||||
:param config: The input configuration
|
||||
:return: The validated configuration
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
class FlexLayout(Layout):
|
||||
def get_type(self):
|
||||
return TYPE_FLEX
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_FLEX:
|
||||
return None, {}
|
||||
child_schema = FLEX_OBJ_SCHEMA
|
||||
if grow := layout.get(CONF_FLEX_GROW):
|
||||
child_schema = {cv.Optional(CONF_FLEX_GROW, default=grow): cv.int_}
|
||||
# Polyfill to implement stretch alignment for flex containers
|
||||
# LVGL does not support this natively, so we add a 100% size property to the children in the cross-axis
|
||||
if layout.get(CONF_FLEX_ALIGN_CROSS) == "LV_FLEX_ALIGN_STRETCH":
|
||||
dimension = (
|
||||
CONF_WIDTH
|
||||
if "COLUMN" in layout[CONF_FLEX_FLOW].upper()
|
||||
else CONF_HEIGHT
|
||||
)
|
||||
child_schema[cv.Optional(dimension, default="100%")] = size
|
||||
return FLEX_LAYOUT_SCHEMA, child_schema
|
||||
|
||||
def validate(self, config):
|
||||
"""
|
||||
Perform validation on the container and its children for this layout
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
class DirectionalLayout(FlexLayout):
|
||||
def __init__(self, direction: str, flow):
|
||||
"""
|
||||
:param direction: "horizontal" or "vertical"
|
||||
:param flow: "row" or "column"
|
||||
"""
|
||||
super().__init__()
|
||||
self.direction = direction
|
||||
self.flow = flow
|
||||
|
||||
def get_type(self):
|
||||
return self.direction
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
if config.get(CONF_LAYOUT, "").lower() != self.direction:
|
||||
return None, {}
|
||||
return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction)
|
||||
|
||||
def validate(self, config):
|
||||
assert config[CONF_LAYOUT].lower() == self.direction
|
||||
config[CONF_LAYOUT] = {
|
||||
**FLEX_HV_STYLE,
|
||||
CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(),
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
class GridLayout(Layout):
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_GRID
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
|
||||
return (
|
||||
cv.string,
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_ROW_SPAN, default=1
|
||||
): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_COLUMN_SPAN, default=1
|
||||
): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_X_ALIGN, default="center"
|
||||
): grid_alignments,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_Y_ALIGN, default="center"
|
||||
): grid_alignments,
|
||||
},
|
||||
)
|
||||
# Not a valid grid layout string
|
||||
return None, {}
|
||||
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_GRID:
|
||||
return None, {}
|
||||
return (
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
|
||||
cv.Required(CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
},
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
},
|
||||
)
|
||||
|
||||
def validate(self, config: dict):
|
||||
"""
|
||||
Validate the grid layout.
|
||||
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
|
||||
Either all cells must have a row and column,
|
||||
or none, in which case the grid layout is auto-generated.
|
||||
:param config:
|
||||
:return: The config updated with auto-generated values
|
||||
"""
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
# If the layout is a string, assume it is in the format "rows x columns", implying
|
||||
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
|
||||
layout = layout.strip()
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match:
|
||||
rows = int(match.group(1))
|
||||
cols = int(match.group(2))
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout format: {config}, expected 'rows x columns'",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
# should be guaranteed to be a dict at this point
|
||||
assert isinstance(layout, dict)
|
||||
assert layout.get(CONF_TYPE) == TYPE_GRID
|
||||
rows = len(layout[CONF_GRID_ROWS])
|
||||
columns = len(layout[CONF_GRID_COLUMNS])
|
||||
used_cells = [[None] * columns for _ in range(rows)]
|
||||
for index, widget in enumerate(config.get(CONF_WIDGETS, [])):
|
||||
_, w = next(iter(widget.items()))
|
||||
if (CONF_GRID_CELL_COLUMN_POS in w) != (CONF_GRID_CELL_ROW_POS in w):
|
||||
raise cv.Invalid(
|
||||
"Both row and column positions must be specified, or both omitted",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
if CONF_GRID_CELL_ROW_POS in w:
|
||||
row = w[CONF_GRID_CELL_ROW_POS]
|
||||
column = w[CONF_GRID_CELL_COLUMN_POS]
|
||||
else:
|
||||
try:
|
||||
row, column = next(
|
||||
(r_idx, c_idx)
|
||||
for r_idx, row in enumerate(used_cells)
|
||||
for c_idx, value in enumerate(row)
|
||||
if value is None
|
||||
)
|
||||
except StopIteration:
|
||||
raise cv.Invalid(
|
||||
"No free cells available in grid layout", [CONF_WIDGETS, index]
|
||||
) from None
|
||||
w[CONF_GRID_CELL_ROW_POS] = row
|
||||
w[CONF_GRID_CELL_COLUMN_POS] = column
|
||||
|
||||
for i in range(w[CONF_GRID_CELL_ROW_SPAN]):
|
||||
for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]):
|
||||
if row + i >= rows or column + j >= columns:
|
||||
raise cv.Invalid(
|
||||
f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} "
|
||||
f"exceeds grid size {rows}x{columns}",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
if used_cells[row + i][column + j] is not None:
|
||||
raise cv.Invalid(
|
||||
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
used_cells[row + i][column + j] = index
|
||||
|
||||
return config
|
||||
|
||||
|
||||
LAYOUT_CLASSES = (
|
||||
FlexLayout(),
|
||||
GridLayout(),
|
||||
DirectionalLayout("horizontal", "row"),
|
||||
DirectionalLayout("vertical", "column"),
|
||||
)
|
||||
LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES]
|
||||
|
||||
|
||||
def append_layout_schema(schema, config: dict):
|
||||
"""
|
||||
Get the child layout schema for a given widget based on its layout type.
|
||||
:param config: The config to check
|
||||
:return: A schema for the layout including a widgets key
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
if CONF_WIDGETS not in config:
|
||||
if CONF_LAYOUT in config:
|
||||
raise cv.Invalid(
|
||||
f"Layout {config[CONF_LAYOUT]} requires a {CONF_WIDGETS} key",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
return schema
|
||||
|
||||
from .schemas import any_widget_schema
|
||||
|
||||
if CONF_LAYOUT not in config:
|
||||
# If no layout is specified, return the schema as is
|
||||
return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()})
|
||||
|
||||
for layout_class in LAYOUT_CLASSES:
|
||||
layout_schema, child_schema = layout_class.get_layout_schemas(config)
|
||||
if layout_schema:
|
||||
layout_schema = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_LAYOUT): layout_schema,
|
||||
cv.Required(CONF_WIDGETS): any_widget_schema(child_schema),
|
||||
}
|
||||
)
|
||||
layout_schema.add_extra(layout_class.validate)
|
||||
return layout_schema.extend(schema)
|
||||
|
||||
# If no layout class matched, return a default schema
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_LAYOUT): cv.one_of(*LAYOUT_CHOICES, lower=True),
|
||||
cv.Optional(CONF_WIDGETS): any_widget_schema(),
|
||||
}
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -247,8 +246,6 @@ def pixels_or_percent_validator(value):
|
||||
return ["pixels", "..%"]
|
||||
if isinstance(value, str) and value.lower().endswith("px"):
|
||||
value = cv.int_(value[:-2])
|
||||
if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value):
|
||||
return value
|
||||
value = cv.Any(cv.int_, cv.percentage)(value)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
@@ -299,7 +299,6 @@ class LvExpr(MockLv):
|
||||
|
||||
# Top level mock for generic lv_ calls to be recorded
|
||||
lv = MockLv("lv_")
|
||||
LV = MockLv("LV_")
|
||||
# Just generate an expression
|
||||
lv_expr = LvExpr("lv_")
|
||||
# Mock for lv_obj_ calls
|
||||
@@ -328,7 +327,7 @@ def lv_assign(target, expression):
|
||||
lv_add(AssignmentExpression("", "", target, expression))
|
||||
|
||||
|
||||
def lv_Pvariable(type, name) -> MockObj:
|
||||
def lv_Pvariable(type, name):
|
||||
"""
|
||||
Create but do not initialise a pointer variable
|
||||
:param type: Type of the variable target
|
||||
@@ -344,7 +343,7 @@ def lv_Pvariable(type, name) -> MockObj:
|
||||
return var
|
||||
|
||||
|
||||
def lv_variable(type, name) -> MockObj:
|
||||
def lv_variable(type, name):
|
||||
"""
|
||||
Create but do not initialise a variable
|
||||
:param type: Type of the variable target
|
||||
|
||||
@@ -171,7 +171,6 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() =
|
||||
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
||||
auto width = lv_area_get_width(area);
|
||||
auto height = lv_area_get_height(area);
|
||||
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
|
||||
auto x1 = area->x1;
|
||||
auto y1 = area->y1;
|
||||
lv_color_t *dst = this->rotate_buf_;
|
||||
@@ -179,13 +178,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
for (lv_coord_t x = height; x-- != 0;) {
|
||||
for (lv_coord_t y = 0; y != width; y++) {
|
||||
dst[y * height_rounded + x] = *ptr++;
|
||||
dst[y * height + x] = *ptr++;
|
||||
}
|
||||
}
|
||||
y1 = x1;
|
||||
x1 = this->disp_drv_.ver_res - area->y1 - height;
|
||||
height = width;
|
||||
width = height_rounded;
|
||||
width = height;
|
||||
height = lv_area_get_width(area);
|
||||
break;
|
||||
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
@@ -201,13 +200,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
for (lv_coord_t x = 0; x != height; x++) {
|
||||
for (lv_coord_t y = width; y-- != 0;) {
|
||||
dst[y * height_rounded + x] = *ptr++;
|
||||
dst[y * height + x] = *ptr++;
|
||||
}
|
||||
}
|
||||
x1 = y1;
|
||||
y1 = this->disp_drv_.hor_res - area->x1 - width;
|
||||
height = width;
|
||||
width = height_rounded;
|
||||
width = height;
|
||||
height = lv_area_get_width(area);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -444,10 +443,8 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
|
||||
|
||||
void LvglComponent::setup() {
|
||||
auto *display = this->displays_[0];
|
||||
auto rounding = this->draw_rounding;
|
||||
// cater for displays with dimensions that don't divide by the required rounding
|
||||
auto width = (display->get_width() + rounding - 1) / rounding * rounding;
|
||||
auto height = (display->get_height() + rounding - 1) / rounding * rounding;
|
||||
auto width = display->get_width();
|
||||
auto height = display->get_height();
|
||||
auto frac = this->buffer_frac_;
|
||||
if (frac == 0)
|
||||
frac = 1;
|
||||
@@ -472,8 +469,9 @@ void LvglComponent::setup() {
|
||||
}
|
||||
this->buffer_frac_ = frac;
|
||||
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
|
||||
this->disp_drv_.hor_res = display->get_width();
|
||||
this->disp_drv_.ver_res = display->get_height();
|
||||
this->disp_drv_.hor_res = width;
|
||||
this->disp_drv_.ver_res = height;
|
||||
// this->setup_driver_(display->get_width(), display->get_height());
|
||||
lv_disp_drv_update(this->disp_, &this->disp_drv_);
|
||||
this->rotation = display->get_rotation();
|
||||
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
|
||||
@@ -12,21 +12,17 @@ from esphome.const import (
|
||||
CONF_TEXT,
|
||||
CONF_TIME,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core.config import StartupTrigger
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import requires_component, validate_printf
|
||||
from .layout import (
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
append_layout_schema,
|
||||
grid_alignments,
|
||||
)
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID
|
||||
from .helpers import add_lv_use, requires_component, validate_printf
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
|
||||
from .lvcode import LvglComponent, lv_event_t_ptr
|
||||
from .types import (
|
||||
@@ -76,9 +72,11 @@ def _validate_text(value):
|
||||
|
||||
|
||||
# A schema for text properties
|
||||
TEXT_SCHEMA = {
|
||||
cv.Optional(CONF_TEXT): _validate_text,
|
||||
}
|
||||
TEXT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_TEXT): _validate_text,
|
||||
}
|
||||
)
|
||||
|
||||
LIST_ACTION_SCHEMA = cv.ensure_list(
|
||||
cv.maybe_simple_value(
|
||||
@@ -138,7 +136,7 @@ STYLE_PROPS = {
|
||||
"arc_opa": lvalid.opacity,
|
||||
"arc_color": lvalid.lv_color,
|
||||
"arc_rounded": lvalid.lv_bool,
|
||||
"arc_width": lvalid.pixels,
|
||||
"arc_width": lvalid.lv_positive_int,
|
||||
"anim_time": lvalid.lv_milliseconds,
|
||||
"bg_color": lvalid.lv_color,
|
||||
"bg_grad": lv_gradient,
|
||||
@@ -225,6 +223,10 @@ STYLE_REMAP = {
|
||||
"image_recolor_opa": "img_recolor_opa",
|
||||
}
|
||||
|
||||
cell_alignments = df.LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = df.LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
# Complete object style schema
|
||||
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
|
||||
{
|
||||
@@ -264,8 +266,10 @@ def part_schema(parts):
|
||||
:param parts: The parts to include
|
||||
:return: The schema
|
||||
"""
|
||||
return STATE_SCHEMA.extend(FLAG_SCHEMA).extend(
|
||||
{cv.Optional(part): STATE_SCHEMA for part in parts}
|
||||
return (
|
||||
cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts})
|
||||
.extend(STATE_SCHEMA)
|
||||
.extend(FLAG_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
@@ -273,10 +277,10 @@ def automation_schema(typ: LvType):
|
||||
events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS
|
||||
if typ.has_on_value:
|
||||
events = events + (CONF_ON_VALUE,)
|
||||
args = typ.get_arg_type()
|
||||
args = typ.get_arg_type() if isinstance(typ, LvType) else []
|
||||
args.append(lv_event_t_ptr)
|
||||
return {
|
||||
**{
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(event): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@@ -285,11 +289,14 @@ def automation_schema(typ: LvType):
|
||||
}
|
||||
)
|
||||
for event in events
|
||||
},
|
||||
cv.Optional(CONF_ON_BOOT): validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)}
|
||||
),
|
||||
}
|
||||
}
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_BOOT): validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def base_update_schema(widget_type, parts):
|
||||
@@ -328,17 +335,75 @@ def obj_schema(widget_type: WidgetType):
|
||||
"""
|
||||
return (
|
||||
part_schema(widget_type.parts)
|
||||
.extend(LAYOUT_SCHEMA)
|
||||
.extend(ALIGN_TO_SCHEMA)
|
||||
.extend(automation_schema(widget_type.w_type))
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
|
||||
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
|
||||
}
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
|
||||
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _validate_grid_layout(config):
|
||||
layout = config[df.CONF_LAYOUT]
|
||||
rows = len(layout[df.CONF_GRID_ROWS])
|
||||
columns = len(layout[df.CONF_GRID_COLUMNS])
|
||||
used_cells = [[None] * columns for _ in range(rows)]
|
||||
for index, widget in enumerate(config[df.CONF_WIDGETS]):
|
||||
_, w = next(iter(widget.items()))
|
||||
if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w):
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
"Both row and column positions must be specified, or both omitted",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
if df.CONF_GRID_CELL_ROW_POS in w:
|
||||
row = w[df.CONF_GRID_CELL_ROW_POS]
|
||||
column = w[df.CONF_GRID_CELL_COLUMN_POS]
|
||||
else:
|
||||
try:
|
||||
row, column = next(
|
||||
(r_idx, c_idx)
|
||||
for r_idx, row in enumerate(used_cells)
|
||||
for c_idx, value in enumerate(row)
|
||||
if value is None
|
||||
)
|
||||
except StopIteration:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
"No free cells available in grid layout", [df.CONF_WIDGETS, index]
|
||||
)
|
||||
w[df.CONF_GRID_CELL_ROW_POS] = row
|
||||
w[df.CONF_GRID_CELL_COLUMN_POS] = column
|
||||
|
||||
for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]):
|
||||
for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]):
|
||||
if row + i >= rows or column + j >= columns:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} "
|
||||
f"exceeds grid size {rows}x{columns}",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
if used_cells[row + i][column + j] is not None:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
used_cells[row + i][column + j] = index
|
||||
|
||||
return config
|
||||
|
||||
|
||||
LAYOUT_SCHEMAS = {}
|
||||
LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout}
|
||||
|
||||
ALIGN_TO_SCHEMA = {
|
||||
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
|
||||
{
|
||||
@@ -351,6 +416,57 @@ ALIGN_TO_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
def grid_free_space(value):
|
||||
value = cv.Upper(value)
|
||||
if value.startswith("FR(") and value.endswith(")"):
|
||||
value = value.removesuffix(")").removeprefix("FR(")
|
||||
return f"LV_GRID_FR({cv.positive_int(value)})"
|
||||
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
|
||||
|
||||
|
||||
grid_spec = cv.Any(
|
||||
lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space
|
||||
)
|
||||
|
||||
LAYOUT_SCHEMA = {
|
||||
cv.Optional(df.CONF_LAYOUT): cv.typed_schema(
|
||||
{
|
||||
df.TYPE_GRID: {
|
||||
cv.Required(df.CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
|
||||
},
|
||||
df.TYPE_FLEX: {
|
||||
cv.Optional(
|
||||
df.CONF_FLEX_FLOW, default="row_wrap"
|
||||
): df.FLEX_FLOWS.one_of,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
|
||||
},
|
||||
},
|
||||
lower=True,
|
||||
)
|
||||
}
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
|
||||
FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
DISP_BG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
|
||||
@@ -382,11 +498,48 @@ ALL_STYLES = {
|
||||
}
|
||||
|
||||
|
||||
def container_validator(schema, widget_type: WidgetType):
|
||||
"""
|
||||
Create a validator for a container given the widget type
|
||||
:param schema: Base schema to extend
|
||||
:param widget_type:
|
||||
:return:
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
if w_sch := widget_type.schema:
|
||||
if isinstance(w_sch, dict):
|
||||
w_sch = cv.Schema(w_sch)
|
||||
# order is important here to preserve extras
|
||||
result = w_sch.extend(schema)
|
||||
else:
|
||||
result = schema
|
||||
ltype = df.TYPE_NONE
|
||||
if value and (layout := value.get(df.CONF_LAYOUT)):
|
||||
if not isinstance(layout, dict):
|
||||
raise cv.Invalid("Layout value must be a dict")
|
||||
ltype = layout.get(CONF_TYPE)
|
||||
if not ltype:
|
||||
raise (cv.Invalid("Layout schema requires type:"))
|
||||
add_lv_use(ltype)
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return result
|
||||
result = result.extend(
|
||||
LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE])
|
||||
)
|
||||
value = result(value)
|
||||
if layout_validator := LAYOUT_VALIDATORS.get(ltype):
|
||||
value = layout_validator(value)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def container_schema(widget_type: WidgetType, extras=None):
|
||||
"""
|
||||
Create a schema for a container widget of a given type. All obj properties are available, plus
|
||||
the extras passed in, plus any defined for the specific widget being specified.
|
||||
:param widget_type: The widget type, e.g. "image"
|
||||
:param widget_type: The widget type, e.g. "img"
|
||||
:param extras: Additional options to be made available, e.g. layout properties for children
|
||||
:return: The schema for this type of widget.
|
||||
"""
|
||||
@@ -396,49 +549,31 @@ def container_schema(widget_type: WidgetType, extras=None):
|
||||
if extras:
|
||||
schema = schema.extend(extras)
|
||||
# Delayed evaluation for recursion
|
||||
return container_validator(schema, widget_type)
|
||||
|
||||
schema = schema.extend(widget_type.schema)
|
||||
|
||||
def validator(value):
|
||||
return append_layout_schema(schema, value)(value)
|
||||
def widget_schema(widget_type: WidgetType, extras=None):
|
||||
"""
|
||||
Create a schema for a given widget type
|
||||
:param widget_type: The name of the widget
|
||||
:param extras:
|
||||
:return:
|
||||
"""
|
||||
validator = container_schema(widget_type, extras=extras)
|
||||
if required := widget_type.required_component:
|
||||
validator = cv.All(validator, requires_component(required))
|
||||
return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator
|
||||
|
||||
return validator
|
||||
|
||||
# All widget schemas must be defined before this is called.
|
||||
|
||||
|
||||
def any_widget_schema(extras=None):
|
||||
"""
|
||||
Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
widget under the widgets: key.
|
||||
|
||||
:param extras: Additional schema to be applied to each generated one
|
||||
:return: A validator for the Widgets key
|
||||
:return:
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
if isinstance(value, dict):
|
||||
# Convert to list
|
||||
value = [{k: v} for k, v in value.items()]
|
||||
if not isinstance(value, list):
|
||||
raise cv.Invalid("Expected a list of widgets")
|
||||
result = []
|
||||
for index, entry in enumerate(value):
|
||||
if not isinstance(entry, dict) or len(entry) != 1:
|
||||
raise cv.Invalid(
|
||||
"Each widget must be a dictionary with a single key", path=[index]
|
||||
)
|
||||
[(key, value)] = entry.items()
|
||||
# Validate the widget against its schema
|
||||
widget_type = WIDGET_TYPES.get(key)
|
||||
if not widget_type:
|
||||
raise cv.Invalid(f"Unknown widget type: {key}", path=[index])
|
||||
container_validator = container_schema(widget_type, extras=extras)
|
||||
if required := widget_type.required_component:
|
||||
container_validator = cv.All(
|
||||
container_validator, requires_component(required)
|
||||
)
|
||||
# Apply custom validation
|
||||
value = widget_type.validate(value or {})
|
||||
result.append({key: container_validator(value)})
|
||||
return result
|
||||
|
||||
return validator
|
||||
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values()))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.config_validation import Schema
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_types import esphome_ns
|
||||
@@ -136,14 +135,14 @@ class WidgetType:
|
||||
self.lv_name = lv_name or name
|
||||
self.w_type = w_type
|
||||
self.parts = parts
|
||||
if not isinstance(schema, Schema):
|
||||
schema = Schema(schema or {})
|
||||
self.schema = schema
|
||||
if schema is None:
|
||||
self.schema = {}
|
||||
else:
|
||||
self.schema = schema
|
||||
if modify_schema is None:
|
||||
modify_schema = schema
|
||||
if not isinstance(modify_schema, Schema):
|
||||
modify_schema = Schema(modify_schema)
|
||||
self.modify_schema = modify_schema
|
||||
self.modify_schema = self.schema
|
||||
else:
|
||||
self.modify_schema = modify_schema
|
||||
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
|
||||
|
||||
@property
|
||||
@@ -164,6 +163,7 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
:return: Generated code as a list of text lines
|
||||
"""
|
||||
return []
|
||||
|
||||
async def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
"""
|
||||
@@ -174,13 +174,6 @@ class WidgetType:
|
||||
"""
|
||||
return lv_expr.call(f"{self.lv_name}_create", parent)
|
||||
|
||||
def on_create(self, var: MockObj, config: dict):
|
||||
"""
|
||||
Called from to_code when the widget is created, to set up any initial properties
|
||||
:param var: The variable representing the widget
|
||||
:param config: Its configuration
|
||||
"""
|
||||
|
||||
def get_uses(self):
|
||||
"""
|
||||
Get a list of other widgets used by this one
|
||||
@@ -200,14 +193,6 @@ class WidgetType:
|
||||
def get_scale(self, config: dict):
|
||||
return 1.0
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Provides an opportunity for custom validation for a given widget type
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class NumberType(WidgetType):
|
||||
def get_max(self, config: dict):
|
||||
|
||||
@@ -339,10 +339,7 @@ async def set_obj_properties(w: Widget, config):
|
||||
if layout_type == TYPE_FLEX:
|
||||
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
|
||||
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
|
||||
cross = layout[CONF_FLEX_ALIGN_CROSS]
|
||||
if cross == "LV_FLEX_ALIGN_STRETCH":
|
||||
cross = "LV_FLEX_ALIGN_CENTER"
|
||||
cross = literal(cross)
|
||||
cross = literal(layout[CONF_FLEX_ALIGN_CROSS])
|
||||
track = literal(layout[CONF_FLEX_ALIGN_TRACK])
|
||||
lv_obj.set_flex_align(w.obj, main, cross, track)
|
||||
parts = collect_parts(config)
|
||||
@@ -449,11 +446,9 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
|
||||
if spec.is_compound():
|
||||
var = cg.new_Pvariable(wid)
|
||||
lv_add(var.set_obj(creator))
|
||||
spec.on_create(var.obj, w_cnfig)
|
||||
else:
|
||||
var = lv_Pvariable(lv_obj_t, wid)
|
||||
lv_assign(var, creator)
|
||||
spec.on_create(var, w_cnfig)
|
||||
|
||||
w = Widget.create(wid, var, spec, w_cnfig)
|
||||
if theme := theme_widget_map.get(w_type):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_ROWS
|
||||
from esphome.components.key_provider import KeyProvider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
|
||||
@@ -159,15 +159,18 @@ async def canvas_set_pixel(config, action_id, template_arg, args):
|
||||
)
|
||||
|
||||
|
||||
DRAW_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_X): pixels,
|
||||
cv.Required(CONF_Y): pixels,
|
||||
}
|
||||
DRAW_OPA_SCHEMA = {
|
||||
**DRAW_SCHEMA,
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
}
|
||||
DRAW_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_X): pixels,
|
||||
cv.Required(CONF_Y): pixels,
|
||||
}
|
||||
)
|
||||
DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args):
|
||||
@@ -221,14 +224,12 @@ RECT_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_rectangle",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
DRAW_SCHEMA.extend(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
}
|
||||
),
|
||||
},
|
||||
).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}),
|
||||
)
|
||||
async def canvas_draw_rect(config, action_id, template_arg, args):
|
||||
width = await pixels.process(config[CONF_WIDTH])
|
||||
@@ -260,14 +261,13 @@ TEXT_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_text",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_),
|
||||
**{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS},
|
||||
},
|
||||
),
|
||||
)
|
||||
.extend({cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}),
|
||||
)
|
||||
async def canvas_draw_text(config, action_id, template_arg, args):
|
||||
text = await lv_text.process(config[CONF_TEXT])
|
||||
@@ -293,15 +293,13 @@ IMG_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_image",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_SRC): lv_image,
|
||||
cv.Optional(CONF_PIVOT_X, default=0): pixels,
|
||||
cv.Optional(CONF_PIVOT_Y, default=0): pixels,
|
||||
**{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()},
|
||||
}
|
||||
),
|
||||
},
|
||||
).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}),
|
||||
)
|
||||
async def canvas_draw_image(config, action_id, template_arg, args):
|
||||
src = await lv_image.process(config[CONF_SRC])
|
||||
@@ -338,9 +336,8 @@ LINE_PROPS = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()},
|
||||
}
|
||||
),
|
||||
},
|
||||
).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}),
|
||||
)
|
||||
async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
points = [
|
||||
@@ -366,9 +363,8 @@ async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
},
|
||||
),
|
||||
).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}),
|
||||
)
|
||||
async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
points = [
|
||||
@@ -399,15 +395,13 @@ ARC_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_arc",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_RADIUS): pixels,
|
||||
cv.Required(CONF_START_ANGLE): lv_angle_degrees,
|
||||
cv.Required(CONF_END_ANGLE): lv_angle_degrees,
|
||||
**{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()},
|
||||
}
|
||||
),
|
||||
).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
|
||||
)
|
||||
async def canvas_draw_arc(config, action_id, template_arg, args):
|
||||
radius = await size.process(config[CONF_RADIUS])
|
||||
|
||||
@@ -17,10 +17,11 @@ class CheckboxType(WidgetType):
|
||||
CONF_CHECKBOX,
|
||||
LvBoolean("lv_checkbox_t"),
|
||||
(CONF_MAIN, CONF_INDICATOR),
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
Optional(CONF_PAD_COLUMN): padding,
|
||||
},
|
||||
TEXT_SCHEMA.extend(
|
||||
{
|
||||
Optional(CONF_PAD_COLUMN): padding,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HEIGHT, CONF_WIDTH
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
|
||||
from ..lv_validation import size
|
||||
from ..lvcode import lv
|
||||
from ..types import WidgetType, lv_obj_t
|
||||
|
||||
CONTAINER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_HEIGHT, default="100%"): size,
|
||||
cv.Optional(CONF_WIDTH, default="100%"): size,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ContainerType(WidgetType):
|
||||
"""
|
||||
A simple container widget that can hold other widgets and which defaults to a 100% size.
|
||||
Made from an obj with all styles removed
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
CONF_CONTAINER,
|
||||
lv_obj_t,
|
||||
(CONF_MAIN, CONF_SCROLLBAR),
|
||||
schema=CONTAINER_SCHEMA,
|
||||
modify_schema={},
|
||||
lv_name=CONF_OBJ,
|
||||
)
|
||||
self.styles = {}
|
||||
|
||||
def on_create(self, var: MockObj, config: dict):
|
||||
lv.obj_remove_style_all(var)
|
||||
|
||||
|
||||
container_spec = ContainerType()
|
||||
@@ -23,11 +23,12 @@ class LabelType(WidgetType):
|
||||
CONF_LABEL,
|
||||
LvText("lv_label_t"),
|
||||
(CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_RECOLOR): lv_bool,
|
||||
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
|
||||
},
|
||||
TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_RECOLOR): lv_bool,
|
||||
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
|
||||
@@ -14,12 +14,13 @@ CONF_QRCODE = "qrcode"
|
||||
CONF_DARK_COLOR = "dark_color"
|
||||
CONF_LIGHT_COLOR = "light_color"
|
||||
|
||||
QRCODE_SCHEMA = {
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
|
||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
|
||||
cv.Required(CONF_SIZE): cv.int_,
|
||||
}
|
||||
QRCODE_SCHEMA = TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
|
||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
|
||||
cv.Required(CONF_SIZE): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class QrCodeType(WidgetType):
|
||||
|
||||
@@ -21,14 +21,15 @@ CONF_TEXTAREA = "textarea"
|
||||
|
||||
lv_textarea_t = LvText("lv_textarea_t")
|
||||
|
||||
TEXTAREA_SCHEMA = {
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text,
|
||||
cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
|
||||
cv.Optional(CONF_ONE_LINE): lv_bool,
|
||||
cv.Optional(CONF_PASSWORD_MODE): lv_bool,
|
||||
cv.Optional(CONF_MAX_LENGTH): lv_int,
|
||||
}
|
||||
TEXTAREA_SCHEMA = TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text,
|
||||
cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
|
||||
cv.Optional(CONF_ONE_LINE): lv_bool,
|
||||
cv.Optional(CONF_PASSWORD_MODE): lv_bool,
|
||||
cv.Optional(CONF_MAX_LENGTH): lv_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TextareaType(WidgetType):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import key_provider
|
||||
from esphome.components.const import CONF_ROWS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID
|
||||
|
||||
CODEOWNERS = ["@ssieb"]
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
@@ -7,7 +7,7 @@ from urllib.parse import urljoin
|
||||
from esphome import automation, external_files, git
|
||||
from esphome.automation import register_action, register_condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, microphone, socket
|
||||
from esphome.components import esp32, microphone
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
@@ -32,7 +32,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@jesserockz"]
|
||||
DEPENDENCIES = ["microphone"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
DOMAIN = "micro_wake_word"
|
||||
|
||||
|
||||
@@ -444,10 +443,6 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Enable wake_loop_threadsafe() for low-latency wake word detection
|
||||
# The inference task queues detection events that need immediate processing
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE])
|
||||
cg.add(var.set_microphone_source(mic_source))
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -427,12 +426,6 @@ void MicroWakeWord::process_probabilities_() {
|
||||
if (vad_state.detected) {
|
||||
#endif
|
||||
xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY);
|
||||
|
||||
// Wake main loop immediately to process wake word detection
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
|
||||
model->reset_probabilities();
|
||||
#ifdef USE_MICRO_WAKE_WORD_VAD
|
||||
} else {
|
||||
|
||||
@@ -218,21 +218,6 @@ def map_sequence(value):
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def flatten_sequence(sequence: tuple | list):
|
||||
"""
|
||||
Flatten an init sequence into a single list of bytes.
|
||||
:param sequence: The list of tuples
|
||||
:return: a list of bytes
|
||||
"""
|
||||
return sum(
|
||||
tuple(
|
||||
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
|
||||
for x in sequence
|
||||
),
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
def delay(ms):
|
||||
return DELAY_FLAG, ms
|
||||
|
||||
@@ -471,7 +456,13 @@ class DriverChip:
|
||||
|
||||
# Flatten the sequence into a list of bytes, with the length of each command
|
||||
# or the delay flag inserted where needed
|
||||
return flatten_sequence(sequence), madctl
|
||||
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:
|
||||
|
||||
@@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
|
||||
const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF;
|
||||
if (static_cast<SensorType>(hardware_id) != STANDARD && static_cast<SensorType>(hardware_id) != XL &&
|
||||
static_cast<SensorType>(hardware_id) != ETRAILER && static_cast<SensorType>(hardware_id) != STANDARD_ALT) {
|
||||
static_cast<SensorType>(hardware_id) != ETRAILER) {
|
||||
ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace mopeka_std_check {
|
||||
enum SensorType {
|
||||
STANDARD = 0x02,
|
||||
XL = 0x03,
|
||||
STANDARD_ALT = 0x44,
|
||||
ETRAILER = 0x46,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import re
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import logger, socket
|
||||
from esphome.components import logger
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -66,9 +66,6 @@ DEPENDENCIES = ["network"]
|
||||
def AUTO_LOAD():
|
||||
if CORE.is_esp8266 or CORE.is_libretiny:
|
||||
return ["async_tcp", "json"]
|
||||
# ESP32 needs socket for wake_loop_threadsafe()
|
||||
if CORE.is_esp32:
|
||||
return ["json", "socket"]
|
||||
return ["json"]
|
||||
|
||||
|
||||
@@ -216,6 +213,8 @@ def validate_fingerprint(value):
|
||||
|
||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for MQTT component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# MQTT needs 1 socket for the broker connection
|
||||
socket.consume_sockets(1, "mqtt")(config)
|
||||
return config
|
||||
@@ -342,11 +341,6 @@ async def to_code(config):
|
||||
# https://github.com/heman/async-mqtt-client/blob/master/library.json
|
||||
cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0")
|
||||
|
||||
# MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler
|
||||
# This enables low-latency MQTT event processing instead of waiting for select() timeout
|
||||
if CORE.is_esp32:
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
cg.add_define("USE_MQTT")
|
||||
cg.add_global(mqtt_ns.using)
|
||||
|
||||
|
||||
@@ -190,11 +190,6 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b
|
||||
if (instance) {
|
||||
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
|
||||
instance->mqtt_events_.emplace(event);
|
||||
|
||||
// Wake main loop immediately to process MQTT event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ network::IPAddresses get_ip_addresses() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char *get_use_address() {
|
||||
const std::string &get_use_address() {
|
||||
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
|
||||
#ifdef USE_ETHERNET
|
||||
return ethernet::global_eth_component->get_use_address();
|
||||
@@ -105,7 +105,8 @@ const char *get_use_address() {
|
||||
|
||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
|
||||
// Fallback when no network component is defined (e.g., host platform)
|
||||
return "";
|
||||
static const std::string empty;
|
||||
return empty;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ bool is_connected();
|
||||
/// Return whether the network is disabled (only wifi for now)
|
||||
bool is_disabled();
|
||||
/// Get the active network hostname
|
||||
const char *get_use_address();
|
||||
const std::string &get_use_address();
|
||||
IPAddresses get_ip_addresses();
|
||||
|
||||
} // namespace network
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_INDEX
|
||||
from esphome.const import CONF_ADDRESS
|
||||
|
||||
CODEOWNERS = ["@ssieb"]
|
||||
|
||||
@@ -21,8 +21,7 @@ def one_wire_device_schema():
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus),
|
||||
cv.Exclusive(CONF_ADDRESS, "index_or_address"): cv.hex_uint64_t,
|
||||
cv.Exclusive(CONF_INDEX, "index_or_address"): cv.uint8_t,
|
||||
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,5 +37,3 @@ async def register_one_wire_device(var, config):
|
||||
cg.add(var.set_one_wire_bus(parent))
|
||||
if (address := config.get(CONF_ADDRESS)) is not None:
|
||||
cg.add(var.set_address(address))
|
||||
if (index := config.get(CONF_INDEX)) is not None:
|
||||
cg.add(var.set_index(index))
|
||||
|
||||
@@ -18,20 +18,10 @@ bool OneWireDevice::send_command_(uint8_t cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OneWireDevice::check_address_or_index_() {
|
||||
bool OneWireDevice::check_address_() {
|
||||
if (this->address_ != 0)
|
||||
return true;
|
||||
auto devices = this->bus_->get_devices();
|
||||
|
||||
if (this->index_ != INDEX_NOT_SET) {
|
||||
if (this->index_ >= devices.size()) {
|
||||
ESP_LOGE(TAG, "Index %d out of range, only %d devices found", this->index_, devices.size());
|
||||
return false;
|
||||
}
|
||||
this->address_ = devices[this->index_];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (devices.empty()) {
|
||||
ESP_LOGE(TAG, "No devices, can't auto-select address");
|
||||
return false;
|
||||
|
||||
@@ -17,8 +17,6 @@ class OneWireDevice {
|
||||
/// @param address of the device
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
|
||||
void set_index(uint8_t index) { this->index_ = index; }
|
||||
|
||||
/// @brief store the pointer to the OneWireBus to use
|
||||
/// @param bus pointer to the OneWireBus object
|
||||
void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; }
|
||||
@@ -27,16 +25,13 @@ class OneWireDevice {
|
||||
const std::string &get_address_name();
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t INDEX_NOT_SET = 255;
|
||||
|
||||
uint64_t address_{0};
|
||||
uint8_t index_{INDEX_NOT_SET};
|
||||
OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance
|
||||
std::string address_name_;
|
||||
|
||||
/// @brief find an address if necessary
|
||||
/// should be called from setup
|
||||
bool check_address_or_index_();
|
||||
bool check_address_();
|
||||
|
||||
/// @brief send command on the bus
|
||||
/// @param cmd command to send
|
||||
|
||||
@@ -161,9 +161,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
|
||||
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
||||
require_vfs_select()
|
||||
|
||||
# OpenThread SRP needs access to mDNS services after setup
|
||||
enable_mdns_storage()
|
||||
|
||||
|
||||
@@ -254,9 +254,9 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const char *OpenThreadComponent::get_use_address() const { return this->use_address_; }
|
||||
const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
} // namespace openthread
|
||||
} // namespace esphome
|
||||
|
||||
@@ -33,19 +33,15 @@ class OpenThreadComponent : public Component {
|
||||
void on_factory_reset(std::function<void()> callback);
|
||||
void defer_factory_reset_external_callback();
|
||||
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
|
||||
protected:
|
||||
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
||||
bool teardown_started_{false};
|
||||
bool teardown_complete_{false};
|
||||
std::function<void()> factory_reset_external_callback_;
|
||||
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
std::string use_address_;
|
||||
};
|
||||
|
||||
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import textwrap
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -105,17 +104,6 @@ def get_config_schema(config):
|
||||
if not speeds:
|
||||
raise cv.Invalid("PSRAM is not supported on this chip")
|
||||
modes = SPIRAM_MODES[variant]
|
||||
if CONF_MODE not in config and len(modes) != 1:
|
||||
raise (
|
||||
cv.Invalid(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
{variant} requires PSRAM mode selection; one of {", ".join(modes)}
|
||||
Selection of the wrong mode for the board will cause a runtime failure to initialise PSRAM
|
||||
"""
|
||||
)
|
||||
)
|
||||
)
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PsramComponent),
|
||||
|
||||
@@ -35,7 +35,7 @@ void Select::publish_state(size_t index) {
|
||||
this->state_callback_.call(std::string(option), index);
|
||||
}
|
||||
|
||||
const char *Select::current_option() const { return this->option_at(this->active_index_); }
|
||||
const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; }
|
||||
|
||||
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
|
||||
@@ -77,35 +77,32 @@ class Select : public EntityBase {
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control().
|
||||
* Override this to work directly with indices and avoid string conversions.
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
size_t active_index_{0};
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding this index-based version is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control(const std::string&).
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
/** Set the value of the select, this is a virtual method that each select integration can implement.
|
||||
*
|
||||
* This method is called by control(size_t) when not overridden, or directly by external code.
|
||||
* Integrations can either:
|
||||
* 1. Override this method to handle string-based control (traditional approach)
|
||||
* 2. Override control(size_t) instead to work with indices directly (recommended)
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding control(size_t) is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by control(size_t) when not overridden, or directly by external code.
|
||||
* Default implementation converts to index and calls control(size_t).
|
||||
*
|
||||
* Delegation chain:
|
||||
* - SelectCall::perform() → control(size_t) → [if not overridden] → control(string)
|
||||
* - External code → control(string) → publish_state(string) → publish_state(size_t)
|
||||
*
|
||||
* @param value The value as validated by the SelectCall.
|
||||
* @param value The value as validated by the caller.
|
||||
*/
|
||||
virtual void control(const std::string &value) {
|
||||
auto index = this->index_of(value);
|
||||
|
||||
@@ -15,9 +15,6 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
||||
# Components register their socket needs and platforms read this to configure appropriately
|
||||
KEY_SOCKET_CONSUMERS = "socket_consumers"
|
||||
|
||||
# Wake loop threadsafe support tracking
|
||||
KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required"
|
||||
|
||||
|
||||
def consume_sockets(
|
||||
value: int, consumer: str
|
||||
@@ -40,30 +37,6 @@ def consume_sockets(
|
||||
return _consume_sockets
|
||||
|
||||
|
||||
def require_wake_loop_threadsafe() -> None:
|
||||
"""Mark that wake_loop_threadsafe support is required by a component.
|
||||
|
||||
Call this from components that need to wake the main event loop from background threads.
|
||||
This enables the shared UDP loopback socket mechanism (~208 bytes RAM).
|
||||
The socket is shared across all components that use this feature.
|
||||
|
||||
IMPORTANT: This is for background thread context only, NOT ISR context.
|
||||
Socket operations are not safe to call from ISR handlers.
|
||||
|
||||
Example:
|
||||
from esphome.components import socket
|
||||
|
||||
async def to_code(config):
|
||||
socket.require_wake_loop_threadsafe()
|
||||
"""
|
||||
# Only set up once (idempotent - multiple components can call this)
|
||||
if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False):
|
||||
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
|
||||
cg.add_define("USE_WAKE_LOOP_THREADSAFE")
|
||||
# Consume 1 socket for the shared wake notification socket
|
||||
consume_sockets(1, "socket.wake_loop_threadsafe")({})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
|
||||
@@ -26,12 +26,21 @@ from esphome.const import (
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
from esphome.external_files import download_content
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
load = ["audio"]
|
||||
if (
|
||||
not config
|
||||
or config.get(CONF_TASK_STACK_IN_PSRAM)
|
||||
or config.get(CONF_CODEC_SUPPORT_ENABLED)
|
||||
):
|
||||
return load + ["psram"]
|
||||
return load
|
||||
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
||||
DOMAIN = "media_player"
|
||||
@@ -217,19 +226,12 @@ def _validate_repeated_speaker(config):
|
||||
return config
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
# Default to using codec if psram is enabled
|
||||
if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None:
|
||||
use_codec = psram.DOMAIN in full_config.get()
|
||||
conf_id = config[CONF_ID].id
|
||||
core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}})
|
||||
core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec
|
||||
|
||||
def _validate_supported_local_file(config):
|
||||
for file_config in config.get(CONF_FILES, []):
|
||||
_, media_file_type = _read_audio_file_and_type(file_config)
|
||||
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
|
||||
raise cv.Invalid("Unsupported local media file")
|
||||
if not use_codec and str(media_file_type) != str(
|
||||
if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str(
|
||||
audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
):
|
||||
# Only wav files are supported
|
||||
@@ -288,11 +290,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
|
||||
min=4000, max=4000000
|
||||
),
|
||||
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_CODEC_SUPPORT_ENABLED, default=psram.supported()
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
|
||||
cv.boolean, cv.requires_component(psram.DOMAIN)
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
|
||||
cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage,
|
||||
cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage,
|
||||
@@ -315,12 +317,12 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
_final_validate,
|
||||
_validate_supported_local_file,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
|
||||
if config[CONF_CODEC_SUPPORT_ENABLED]:
|
||||
# Compile all supported audio codecs and optimize the wifi settings
|
||||
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
|
||||
@@ -350,8 +352,8 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))
|
||||
if config[CONF_TASK_STACK_IN_PSRAM]:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ void SpeedFan::control(const fan::FanCall &call) {
|
||||
this->oscillating = *call.get_oscillating();
|
||||
if (call.get_direction().has_value())
|
||||
this->direction = *call.get_direction();
|
||||
this->set_preset_mode_(call.get_preset_mode());
|
||||
this->preset_mode = call.get_preset_mode();
|
||||
|
||||
this->write_state_();
|
||||
this->publish_state();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::split_buffer {
|
||||
|
||||
static constexpr const char *const TAG = "split_buffer";
|
||||
|
||||
SplitBuffer::~SplitBuffer() { this->free(); }
|
||||
@@ -101,44 +102,32 @@ void SplitBuffer::free() {
|
||||
this->total_length_ = 0;
|
||||
}
|
||||
|
||||
const uint8_t &SplitBuffer::operator[](size_t index) const {
|
||||
uint8_t &SplitBuffer::operator[](size_t index) {
|
||||
if (index >= this->total_length_) {
|
||||
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_);
|
||||
// Return reference to a static dummy byte since we can't throw exceptions.
|
||||
// the byte is non-const since it will also be used by the non-const [] overload.
|
||||
// Return reference to a static dummy byte to avoid crash
|
||||
static uint8_t dummy = 0;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
const auto buffer_index = index / this->buffer_size_;
|
||||
const auto offset_in_buffer = index % this->buffer_size_;
|
||||
size_t buffer_index = index / this->buffer_size_;
|
||||
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index;
|
||||
|
||||
return this->buffers_[buffer_index][offset_in_buffer];
|
||||
}
|
||||
|
||||
// non-const version of operator[] for write access
|
||||
uint8_t &SplitBuffer::operator[](size_t index) {
|
||||
// avoid code duplication. These casts are safe since we know the object is not const.
|
||||
return const_cast<uint8_t &>(static_cast<const SplitBuffer *>(this)->operator[](index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the entire buffer with a single byte value
|
||||
* @param value Fill value
|
||||
*/
|
||||
void SplitBuffer::fill(uint8_t value) const {
|
||||
if (this->buffer_count_ == 0)
|
||||
return;
|
||||
// clear all the full sized buffers
|
||||
size_t i = 0;
|
||||
for (; i != this->buffer_count_ - 1; i++) {
|
||||
memset(this->buffers_[i], value, this->buffer_size_);
|
||||
const uint8_t &SplitBuffer::operator[](size_t index) const {
|
||||
if (index >= this->total_length_) {
|
||||
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_);
|
||||
// Return reference to a static dummy byte to avoid crash
|
||||
static const uint8_t DUMMY = 0;
|
||||
return DUMMY;
|
||||
}
|
||||
// clear the last, potentially short, buffer.
|
||||
// `i` is guaranteed to equal the last index since the loop terminates at that value.
|
||||
// where all buffers are the same size, the modulus must return the size, not 0.
|
||||
auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1;
|
||||
memset(this->buffers_[i], value, size_last);
|
||||
|
||||
size_t buffer_index = index / this->buffer_size_;
|
||||
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index;
|
||||
|
||||
return this->buffers_[buffer_index][offset_in_buffer];
|
||||
}
|
||||
|
||||
} // namespace esphome::split_buffer
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::split_buffer {
|
||||
/**
|
||||
* A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers
|
||||
* to facilitate allocation of large buffers on devices with fragmented memory spaces.
|
||||
* Each sub-buffer is the same size, except for the last one which may be smaller.
|
||||
* Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory,
|
||||
* there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector.
|
||||
*/
|
||||
|
||||
class SplitBuffer {
|
||||
public:
|
||||
SplitBuffer() = default;
|
||||
@@ -25,13 +19,13 @@ class SplitBuffer {
|
||||
// Access operators
|
||||
uint8_t &operator[](size_t index);
|
||||
const uint8_t &operator[](size_t index) const;
|
||||
void fill(uint8_t value) const;
|
||||
|
||||
// Get the total length
|
||||
size_t size() const { return this->total_length_; }
|
||||
|
||||
// Get buffer information
|
||||
size_t get_buffer_count() const { return this->buffer_count_; }
|
||||
size_t get_buffer_size() const { return this->buffer_size_; }
|
||||
|
||||
// Check if successfully initialized
|
||||
bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; }
|
||||
|
||||
@@ -29,7 +29,7 @@ void TemplateFan::control(const fan::FanCall &call) {
|
||||
this->oscillating = *call.get_oscillating();
|
||||
if (call.get_direction().has_value() && this->has_direction_)
|
||||
this->direction = *call.get_direction();
|
||||
this->set_preset_mode_(call.get_preset_mode());
|
||||
this->preset_mode = call.get_preset_mode();
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
CONFLICTS_WITH = ["usb_host"]
|
||||
|
||||
CONF_USB_LANG_ID = "usb_lang_id"
|
||||
CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str"
|
||||
CONF_USB_PRODUCT_ID = "usb_product_id"
|
||||
CONF_USB_PRODUCT_STR = "usb_product_str"
|
||||
CONF_USB_SERIAL_STR = "usb_serial_str"
|
||||
CONF_USB_VENDOR_ID = "usb_vendor_id"
|
||||
|
||||
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
|
||||
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TinyUSB),
|
||||
cv.Optional(CONF_USB_PRODUCT_ID, default=0x4001): cv.uint16_t,
|
||||
cv.Optional(CONF_USB_VENDOR_ID, default=0x303A): cv.uint16_t,
|
||||
cv.Optional(CONF_USB_LANG_ID, default=0x0409): cv.uint16_t,
|
||||
cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string,
|
||||
cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string,
|
||||
cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
esp32.only_on_variant(
|
||||
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Set USB device descriptor properties
|
||||
cg.add(var.set_usb_desc_product_id(config[CONF_USB_PRODUCT_ID]))
|
||||
cg.add(var.set_usb_desc_vendor_id(config[CONF_USB_VENDOR_ID]))
|
||||
cg.add(var.set_usb_desc_lang_id(config[CONF_USB_LANG_ID]))
|
||||
cg.add(var.set_usb_desc_manufacturer(config[CONF_USB_MANUFACTURER_STR]))
|
||||
cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR]))
|
||||
if config[CONF_USB_SERIAL_STR]:
|
||||
cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR]))
|
||||
|
||||
add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1")
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False)
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False)
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100)
|
||||
@@ -1,44 +0,0 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "tinyusb_component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::tinyusb {
|
||||
|
||||
static const char *TAG = "tinyusb";
|
||||
|
||||
void TinyUSB::setup() {
|
||||
// Use the device's MAC address as its serial number if no serial number is defined
|
||||
if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) {
|
||||
static char mac_addr_buf[13];
|
||||
get_mac_address_into_buffer(mac_addr_buf);
|
||||
this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf;
|
||||
}
|
||||
|
||||
this->tusb_cfg_ = {
|
||||
.descriptor = &this->usb_descriptor_,
|
||||
.string_descriptor = this->string_descriptor_,
|
||||
.string_descriptor_count = SIZE,
|
||||
.external_phy = false,
|
||||
};
|
||||
|
||||
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
|
||||
if (result != ESP_OK) {
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void TinyUSB::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"TinyUSB:\n"
|
||||
" Product ID: 0x%04X\n"
|
||||
" Vendor ID: 0x%04X\n"
|
||||
" Manufacturer: '%s'\n"
|
||||
" Product: '%s'\n"
|
||||
" Serial: '%s'\n",
|
||||
this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER],
|
||||
this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]);
|
||||
}
|
||||
|
||||
} // namespace esphome::tinyusb
|
||||
#endif
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "tinyusb.h"
|
||||
#include "tusb.h"
|
||||
|
||||
namespace esphome::tinyusb {
|
||||
|
||||
enum USBDStringDescriptor : uint8_t {
|
||||
LANGUAGE_ID = 0,
|
||||
MANUFACTURER = 1,
|
||||
PRODUCT = 2,
|
||||
SERIAL_NUMBER = 3,
|
||||
INTERFACE = 4,
|
||||
TERMINATOR = 5,
|
||||
SIZE = 6,
|
||||
};
|
||||
|
||||
static const char *DEFAULT_USB_STR = "ESPHome";
|
||||
|
||||
class TinyUSB : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; }
|
||||
void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; }
|
||||
void set_usb_desc_lang_id(uint16_t lang_id) {
|
||||
this->usb_desc_lang_id_[0] = lang_id & 0xFF;
|
||||
this->usb_desc_lang_id_[1] = lang_id >> 8;
|
||||
}
|
||||
void set_usb_desc_manufacturer(const char *usb_desc_manufacturer) {
|
||||
this->string_descriptor_[MANUFACTURER] = usb_desc_manufacturer;
|
||||
}
|
||||
void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; }
|
||||
void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; }
|
||||
|
||||
protected:
|
||||
char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english
|
||||
|
||||
const char *string_descriptor_[SIZE] = {
|
||||
this->usb_desc_lang_id_, // 0: supported language is English (0x0409)
|
||||
DEFAULT_USB_STR, // 1: Manufacturer
|
||||
DEFAULT_USB_STR, // 2: Product
|
||||
nullptr, // 3: Serial Number
|
||||
nullptr, // 4: Interface
|
||||
nullptr, // 5: Terminator
|
||||
};
|
||||
|
||||
tinyusb_config_t tusb_cfg_{};
|
||||
tusb_desc_device_t usb_descriptor_{
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A,
|
||||
.idProduct = 0x4001,
|
||||
.bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE,
|
||||
.iManufacturer = 1,
|
||||
.iProduct = 2,
|
||||
.iSerialNumber = 3,
|
||||
.bNumConfigurations = 1,
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace esphome::tinyusb
|
||||
#endif
|
||||
@@ -1,5 +1,4 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
@@ -12,7 +11,7 @@ from esphome.const import CONF_DEVICES, CONF_ID
|
||||
from esphome.cpp_types import Component
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["bytebuffer", "socket"]
|
||||
AUTO_LOAD = ["bytebuffer"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
usb_host_ns = cg.esphome_ns.namespace("usb_host")
|
||||
@@ -72,11 +71,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
max_requests = config[CONF_MAX_TRANSFER_REQUESTS]
|
||||
cg.add_define("USB_HOST_MAX_REQUESTS", max_requests)
|
||||
|
||||
# USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task
|
||||
# This enables low-latency (~12μs) USB event processing instead of waiting for
|
||||
# select() timeout (0-16ms). The wake socket is shared across all components.
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
for device in config.get(CONF_DEVICES) or ():
|
||||
|
||||
@@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7;
|
||||
static const uint8_t USB_DIR_OUT = 0;
|
||||
static const size_t SETUP_PACKET_SIZE = 8;
|
||||
|
||||
static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible.
|
||||
static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible.
|
||||
static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32");
|
||||
|
||||
// Select appropriate bitmask type for tracking allocation of TransferRequest slots.
|
||||
@@ -65,7 +65,6 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet
|
||||
// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32.
|
||||
// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated.
|
||||
using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type;
|
||||
static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1;
|
||||
|
||||
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
|
||||
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
|
||||
@@ -134,11 +133,11 @@ class USBClient : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
void on_opened(uint8_t addr);
|
||||
void on_removed(usb_device_handle_t handle);
|
||||
bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
|
||||
bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
|
||||
void control_transfer_callback(const usb_transfer_t *xfer) const;
|
||||
void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
|
||||
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
|
||||
void dump_config() override;
|
||||
void release_trq(TransferRequest *trq);
|
||||
trq_bitmask_t get_trq_in_use() const { return trq_in_use_; }
|
||||
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
|
||||
const std::vector<uint8_t> &data = {});
|
||||
|
||||
@@ -148,6 +147,7 @@ class USBClient : public Component {
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||
|
||||
protected:
|
||||
bool register_();
|
||||
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
||||
virtual void disconnect();
|
||||
virtual void on_connected() {}
|
||||
@@ -158,7 +158,7 @@ class USBClient : public Component {
|
||||
|
||||
// USB task management
|
||||
static void usb_task_fn(void *arg);
|
||||
[[noreturn]] void usb_task_loop() const;
|
||||
void usb_task_loop();
|
||||
|
||||
TaskHandle_t usb_task_handle_{nullptr};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "usb_host.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -175,11 +174,6 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *
|
||||
|
||||
// Push to lock-free queue (always succeeds since pool size == queue size)
|
||||
client->event_queue.push(event);
|
||||
|
||||
// Wake main loop immediately to process USB event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
void USBClient::setup() {
|
||||
usb_host_client_config_t config{.is_synchronous = false,
|
||||
@@ -194,9 +188,9 @@ void USBClient::setup() {
|
||||
}
|
||||
// Pre-allocate USB transfer buffers for all slots at startup
|
||||
// This avoids any dynamic allocation during runtime
|
||||
for (auto &request : this->requests_) {
|
||||
usb_host_transfer_alloc(64, 0, &request.transfer);
|
||||
request.client = this; // Set once, never changes
|
||||
for (size_t i = 0; i < MAX_REQUESTS; i++) {
|
||||
usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer);
|
||||
this->requests_[i].client = this; // Set once, never changes
|
||||
}
|
||||
|
||||
// Create and start USB task
|
||||
@@ -216,7 +210,8 @@ void USBClient::usb_task_fn(void *arg) {
|
||||
auto *client = static_cast<USBClient *>(arg);
|
||||
client->usb_task_loop();
|
||||
}
|
||||
void USBClient::usb_task_loop() const {
|
||||
|
||||
void USBClient::usb_task_loop() {
|
||||
while (true) {
|
||||
usb_host_client_handle_events(this->handle_, portMAX_DELAY);
|
||||
}
|
||||
@@ -339,23 +334,22 @@ static void control_callback(const usb_transfer_t *xfer) {
|
||||
// This multi-threaded access is intentional for performance - USB task can
|
||||
// immediately restart transfers without waiting for main loop scheduling.
|
||||
TransferRequest *USBClient::get_trq_() {
|
||||
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
|
||||
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
|
||||
|
||||
// Find first available slot (bit = 0) and try to claim it atomically
|
||||
// We use a while loop to allow retrying the same slot after CAS failure
|
||||
for (;;) {
|
||||
if (mask == ALL_REQUESTS_IN_USE) {
|
||||
ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
|
||||
return nullptr;
|
||||
size_t i = 0;
|
||||
while (i != MAX_REQUESTS) {
|
||||
if (mask & (static_cast<trq_bitmask_t>(1) << i)) {
|
||||
// Slot is in use, move to next slot
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// find the least significant zero bit
|
||||
trq_bitmask_t lsb = ~mask & (mask + 1);
|
||||
|
||||
// Slot i appears available, try to claim it atomically
|
||||
trq_bitmask_t desired = mask | lsb;
|
||||
trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(1) << i); // Set bit i to mark as in-use
|
||||
|
||||
if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) {
|
||||
auto i = __builtin_ctz(lsb); // count trailing zeroes
|
||||
if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) {
|
||||
// Successfully claimed slot i - prepare the TransferRequest
|
||||
auto *trq = &this->requests_[i];
|
||||
trq->transfer->context = trq;
|
||||
@@ -364,9 +358,13 @@ TransferRequest *USBClient::get_trq_() {
|
||||
}
|
||||
// CAS failed - another thread modified the bitmask
|
||||
// mask was already updated by compare_exchange_weak with the current value
|
||||
// No need to reload - the CAS already did that for us
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
|
||||
return nullptr;
|
||||
}
|
||||
void USBClient::disconnect() {
|
||||
this->on_disconnected();
|
||||
auto err = usb_host_device_close(this->handle_, this->device_handle_);
|
||||
@@ -448,11 +446,11 @@ static void transfer_callback(usb_transfer_t *xfer) {
|
||||
*
|
||||
* @throws None.
|
||||
*/
|
||||
bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
|
||||
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
|
||||
auto *trq = this->get_trq_();
|
||||
if (trq == nullptr) {
|
||||
ESP_LOGE(TAG, "Too many requests queued");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
trq->callback = callback;
|
||||
trq->transfer->callback = transfer_callback;
|
||||
@@ -462,9 +460,7 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
|
||||
this->release_trq(trq);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,11 +476,11 @@ bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
|
||||
*
|
||||
* @throws None.
|
||||
*/
|
||||
bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
|
||||
void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
|
||||
auto *trq = this->get_trq_();
|
||||
if (trq == nullptr) {
|
||||
ESP_LOGE(TAG, "Too many requests queued");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
trq->callback = callback;
|
||||
trq->transfer->callback = transfer_callback;
|
||||
@@ -495,9 +491,7 @@ bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback,
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
|
||||
this->release_trq(trq);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void USBClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -511,7 +505,7 @@ void USBClient::dump_config() {
|
||||
// - Main loop: When transfer submission fails
|
||||
//
|
||||
// THREAD SAFETY: Lock-free using atomic AND to clear bit
|
||||
// Thread-safe atomic operation allows multithreaded deallocation
|
||||
// Thread-safe atomic operation allows multi-threaded deallocation
|
||||
void USBClient::release_trq(TransferRequest *trq) {
|
||||
if (trq == nullptr)
|
||||
return;
|
||||
@@ -523,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Atomically clear the bit to mark slot as available
|
||||
// Atomically clear bit i to mark slot as available
|
||||
// fetch_and with inverted bitmask clears the bit atomically
|
||||
trq_bitmask_t mask = ~(static_cast<trq_bitmask_t>(1) << index);
|
||||
this->trq_in_use_.fetch_and(mask, std::memory_order_release);
|
||||
trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index;
|
||||
this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release);
|
||||
}
|
||||
|
||||
} // namespace usb_host
|
||||
|
||||
@@ -214,7 +214,7 @@ void USBUartComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
if (!channel->initialised_.load())
|
||||
if (!channel->initialised_.load() || channel->input_started_.load())
|
||||
return;
|
||||
// THREAD CONTEXT: Called from both USB task and main loop threads
|
||||
// - USB task: Immediate restart after successful transfer for continuous data flow
|
||||
@@ -226,18 +226,12 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
//
|
||||
// The underlying transfer_in() uses lock-free atomic allocation from the
|
||||
// TransferRequest pool, making this multi-threaded access safe
|
||||
|
||||
// if already started, don't restart. A spurious failure in compare_exchange_weak
|
||||
// is not a problem, as it will be retried on the next read_array()
|
||||
auto started = false;
|
||||
if (!channel->input_started_.compare_exchange_weak(started, true))
|
||||
return;
|
||||
const auto *ep = channel->cdc_dev_.in_ep;
|
||||
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
|
||||
auto callback = [this, channel](const usb_host::TransferStatus &status) {
|
||||
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
|
||||
if (!status.success) {
|
||||
ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code));
|
||||
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
|
||||
// On failure, don't restart - let next read_array() trigger it
|
||||
channel->input_started_.store(false);
|
||||
return;
|
||||
@@ -269,9 +263,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
channel->input_started_.store(false);
|
||||
this->start_input(channel);
|
||||
};
|
||||
if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
|
||||
channel->input_started_.store(false);
|
||||
}
|
||||
channel->input_started_.store(true);
|
||||
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
||||
}
|
||||
|
||||
void USBUartComponent::start_output(USBUartChannel *channel) {
|
||||
@@ -364,12 +357,11 @@ void USBUartTypeCdcAcm::on_disconnected() {
|
||||
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||
}
|
||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
|
||||
// Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
|
||||
channel->input_started_.store(true);
|
||||
channel->output_started_.store(true);
|
||||
channel->initialised_.store(false);
|
||||
channel->input_started_.store(false);
|
||||
channel->output_started_.store(false);
|
||||
channel->input_buffer_.clear();
|
||||
channel->output_buffer_.clear();
|
||||
channel->initialised_.store(false);
|
||||
}
|
||||
USBClient::on_disconnected();
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ void WebServer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Web Server:\n"
|
||||
" Address: %s:%u",
|
||||
network::get_use_address(), this->base_->get_port());
|
||||
network::get_use_address().c_str(), this->base_->get_port());
|
||||
}
|
||||
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
|
||||
|
||||
|
||||
@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
void AsyncResponseStream::print(float value) {
|
||||
// Use stack buffer to avoid temporary string allocation
|
||||
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
|
||||
constexpr size_t float_buf_size = 32;
|
||||
char buf[float_buf_size];
|
||||
int len = snprintf(buf, float_buf_size, "%f", value);
|
||||
char buf[32];
|
||||
int len = snprintf(buf, sizeof(buf), "%f", value);
|
||||
this->content_.append(buf, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -273,8 +273,8 @@ network::IPAddress WiFiComponent::get_dns_address(int num) {
|
||||
}
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const char *WiFiComponent::get_use_address() const { return this->use_address_; }
|
||||
void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
const std::string &WiFiComponent::get_use_address() const { return this->use_address_; }
|
||||
void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
void WiFiComponent::setup_ap_config_() {
|
||||
|
||||
@@ -283,8 +283,8 @@ class WiFiComponent : public Component {
|
||||
|
||||
network::IPAddress get_dns_address(int num);
|
||||
network::IPAddresses get_ip_addresses();
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
|
||||
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||
|
||||
@@ -393,6 +393,7 @@ class WiFiComponent : public Component {
|
||||
void wifi_scan_done_callback_();
|
||||
#endif
|
||||
|
||||
std::string use_address_;
|
||||
FixedVector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
@@ -444,11 +445,6 @@ class WiFiComponent : public Component {
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
Trigger<> *disconnect_trigger_{new Trigger<>()};
|
||||
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
};
|
||||
|
||||
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user