mirror of
https://github.com/esphome/esphome.git
synced 2026-02-15 22:09:36 -07:00
Compare commits
35 Commits
beta
...
json-remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac6636127 | ||
|
|
f2cb5db9e0 | ||
|
|
066419019f | ||
|
|
15da6d0a0b | ||
|
|
6303bc3e35 | ||
|
|
0f4dc6702d | ||
|
|
f48c8a6444 | ||
|
|
062f223876 | ||
|
|
38404b2013 | ||
|
|
5a6d64814a | ||
|
|
36776b40c2 | ||
|
|
58c3ba7ac6 | ||
|
|
931b47673c | ||
|
|
79d9fbf645 | ||
|
|
f24e7709ac | ||
|
|
903971de12 | ||
|
|
b04e427f01 | ||
|
|
e0c03b2dfa | ||
|
|
7dff631dcb | ||
|
|
36aba385af | ||
|
|
136d17366f | ||
|
|
db7870ef5f | ||
|
|
bbc88d92ea | ||
|
|
1604b5d6e4 | ||
|
|
7fd535179e | ||
|
|
e3a457e402 | ||
|
|
0dcff82bb4 | ||
|
|
cde8b66719 | ||
|
|
0e1433329d | ||
|
|
60fef5e656 | ||
|
|
725e774fe7 | ||
|
|
9aa98ed6c6 | ||
|
|
7b251dcc31 | ||
|
|
8a08c688f6 | ||
|
|
d6461251f9 |
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to ghcr by digest
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -73,7 +73,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to dockerhub by digest
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.15.0
|
rev: v0.15.1
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
|||||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||||
esphome/components/rtl87xx/* @kuba2k2
|
esphome/components/rtl87xx/* @kuba2k2
|
||||||
esphome/components/rtttl/* @glmnet
|
esphome/components/rtttl/* @glmnet
|
||||||
|
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||||
esphome/components/runtime_stats/* @bdraco
|
esphome/components/runtime_stats/* @bdraco
|
||||||
esphome/components/rx8130/* @beormund
|
esphome/components/rx8130/* @beormund
|
||||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||||
|
|||||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.2.0b2
|
PROJECT_NUMBER = 2026.3.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
|
|||||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||||
# Order matters! More specific categories must come before general ones.
|
# Order matters! More specific categories must come before general ones.
|
||||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||||
"mdns_lib": ["mdns"],
|
"mdns_lib": ["mdns", "packet$"],
|
||||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||||
"memory_mgmt": [
|
"memory_mgmt": [
|
||||||
"mem_",
|
"mem_",
|
||||||
@@ -794,7 +794,6 @@ SYMBOL_PATTERNS = {
|
|||||||
"s_dp",
|
"s_dp",
|
||||||
"s_ni",
|
"s_ni",
|
||||||
"s_reg_dump",
|
"s_reg_dump",
|
||||||
"packet$",
|
|
||||||
"d_mult_table",
|
"d_mult_table",
|
||||||
"K",
|
"K",
|
||||||
"fcstab",
|
"fcstab",
|
||||||
|
|||||||
@@ -1864,6 +1864,8 @@ void APIConnection::on_fatal_error() {
|
|||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); }
|
||||||
|
|
||||||
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||||
uint8_t aux_data_index) {
|
uint8_t aux_data_index) {
|
||||||
// Check if we already have a message of this type for this entity
|
// Check if we already have a message of this type for this entity
|
||||||
@@ -1880,7 +1882,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No existing item found (or event), add new one
|
// No existing item found (or event), add new one
|
||||||
items.push_back({entity, message_type, estimated_size, aux_data_index});
|
this->push_item({entity, message_type, estimated_size, aux_data_index});
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||||
@@ -1888,7 +1890,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me
|
|||||||
// This avoids expensive vector::insert which shifts all elements
|
// This avoids expensive vector::insert which shifts all elements
|
||||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
||||||
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||||
items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||||
if (items.size() > 1) {
|
if (items.size() > 1) {
|
||||||
// Swap the new high-priority item to the front
|
// Swap the new high-priority item to the front
|
||||||
std::swap(items.front(), items.back());
|
std::swap(items.front(), items.back());
|
||||||
|
|||||||
@@ -541,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||||
// Add item to the front of the batch (for high priority messages like ping)
|
// Add item to the front of the batch (for high priority messages like ping)
|
||||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||||
|
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
|
||||||
|
void push_item(const BatchItem &item);
|
||||||
|
|
||||||
// Clear all items
|
// Clear all items
|
||||||
void clear() {
|
void clear() {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ void APIServer::remove_client_(size_t client_index) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIServer::accept_new_connections_() {
|
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||||
while (true) {
|
while (true) {
|
||||||
struct sockaddr_storage source_addr;
|
struct sockaddr_storage source_addr;
|
||||||
socklen_t addr_len = sizeof(source_addr);
|
socklen_t addr_len = sizeof(source_addr);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
|
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
|
||||||
if (this->buffer_length_ > 0) {
|
if (this->buffer_ == nullptr) {
|
||||||
// Buffer currently has data, so reallocation is impossible
|
return this->allocate_buffer_(new_buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_buffer_size < this->buffer_length_) {
|
||||||
|
// New size is too small to hold existing data
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this->deallocate_buffer_();
|
|
||||||
return this->allocate_buffer_(new_buffer_size);
|
// Shift existing data to the start of the buffer so realloc preserves it
|
||||||
|
if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) {
|
||||||
|
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
||||||
|
this->data_start_ = this->buffer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
RAMAllocator<uint8_t> allocator;
|
||||||
|
uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size);
|
||||||
|
if (new_buffer == nullptr) {
|
||||||
|
// Reallocation failed, but the original buffer is still valid
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->buffer_ = new_buffer;
|
||||||
|
this->data_start_ = this->buffer_;
|
||||||
|
this->buffer_size_ = new_buffer_size;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
|
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
|
||||||
@@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
|
|||||||
if (pre_shift) {
|
if (pre_shift) {
|
||||||
// Shift data in buffer to start
|
// Shift data in buffer to start
|
||||||
if (this->buffer_length_ > 0) {
|
if (this->buffer_length_ > 0) {
|
||||||
memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
||||||
}
|
}
|
||||||
this->data_start_ = this->buffer_;
|
this->data_start_ = this->buffer_;
|
||||||
}
|
}
|
||||||
@@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
|
|||||||
|
|
||||||
if (post_shift) {
|
if (post_shift) {
|
||||||
// Shift unwritten data to the start of the buffer
|
// Shift unwritten data to the start of the buffer
|
||||||
memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
||||||
this->data_start_ = this->buffer_;
|
this->data_start_ = this->buffer_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class AudioTransferBuffer {
|
|||||||
/// @return True if there is data, false otherwise.
|
/// @return True if there is data, false otherwise.
|
||||||
virtual bool has_buffered_data() const;
|
virtual bool has_buffered_data() const;
|
||||||
|
|
||||||
|
/// @brief Reallocates the transfer buffer, preserving any existing data.
|
||||||
|
/// @param new_buffer_size The new size in bytes. Must be at least as large as available().
|
||||||
|
/// @return True if successful, false otherwise. On failure, the original buffer remains valid.
|
||||||
bool reallocate(size_t new_buffer_size);
|
bool reallocate(size_t new_buffer_size);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
|||||||
request->send(stream);
|
request->send(stream);
|
||||||
}
|
}
|
||||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||||
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
|
const auto &ssid = request->arg("ssid");
|
||||||
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
|
const auto &psk = request->arg("psk");
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"Requested WiFi Settings Change:\n"
|
"Requested WiFi Settings Change:\n"
|
||||||
" SSID='%s'\n"
|
" SSID='%s'\n"
|
||||||
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
|||||||
ssid.c_str(), psk.c_str());
|
ssid.c_str(), psk.c_str());
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
// ESP8266 is single-threaded, call directly
|
// ESP8266 is single-threaded, call directly
|
||||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
|
||||||
#else
|
#else
|
||||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
|
||||||
#endif
|
#endif
|
||||||
request->redirect(ESPHOME_F("/?save"));
|
request->redirect(ESPHOME_F("/?save"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LinearCombinationComponent::handle_new_value(float value) {
|
void LinearCombinationComponent::handle_new_value(float value) {
|
||||||
// Multiplies each sensor state by a configured coeffecient and then sums
|
// Multiplies each sensor state by a configured coefficient and then sums
|
||||||
|
|
||||||
if (!std::isfinite(value))
|
if (!std::isfinite(value))
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@@ -15,6 +17,8 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core.entity_helpers import inherit_property_from
|
from esphome.core.entity_helpers import inherit_property_from
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
||||||
|
|
||||||
combination_ns = cg.esphome_ns.namespace("combination")
|
combination_ns = cg.esphome_ns.namespace("combination")
|
||||||
@@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_(
|
|||||||
"SumCombinationComponent", cg.Component, sensor.Sensor
|
"SumCombinationComponent", cg.Component, sensor.Sensor
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_COEFFECIENT = "coeffecient"
|
CONF_COEFFICIENT = "coefficient"
|
||||||
|
CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0
|
||||||
CONF_ERROR = "error"
|
CONF_ERROR = "error"
|
||||||
CONF_KALMAN = "kalman"
|
CONF_KALMAN = "kalman"
|
||||||
CONF_LINEAR = "linear"
|
CONF_LINEAR = "linear"
|
||||||
@@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
LINEAR_SOURCE_SCHEMA = cv.Schema(
|
|
||||||
{
|
def _migrate_coeffecient(config):
|
||||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
"""Migrate deprecated 'coeffecient' spelling to 'coefficient'."""
|
||||||
cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_),
|
if CONF_COEFFECIENT in config:
|
||||||
}
|
if CONF_COEFFICIENT in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'"
|
||||||
|
)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0",
|
||||||
|
CONF_COEFFECIENT,
|
||||||
|
CONF_COEFFICIENT,
|
||||||
|
)
|
||||||
|
config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT)
|
||||||
|
elif CONF_COEFFICIENT not in config:
|
||||||
|
raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
LINEAR_SOURCE_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||||
|
cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_),
|
||||||
|
cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_migrate_coeffecient,
|
||||||
)
|
)
|
||||||
|
|
||||||
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
|
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
|
||||||
@@ -162,12 +190,12 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add(var.add_source(source, error))
|
cg.add(var.add_source(source, error))
|
||||||
elif config[CONF_TYPE] == CONF_LINEAR:
|
elif config[CONF_TYPE] == CONF_LINEAR:
|
||||||
coeffecient = await cg.templatable(
|
coefficient = await cg.templatable(
|
||||||
source_conf[CONF_COEFFECIENT],
|
source_conf[CONF_COEFFICIENT],
|
||||||
[(float, "x")],
|
[(float, "x")],
|
||||||
cg.float_,
|
cg.float_,
|
||||||
)
|
)
|
||||||
cg.add(var.add_source(source, coeffecient))
|
cg.add(var.add_source(source, coefficient))
|
||||||
else:
|
else:
|
||||||
cg.add(var.add_source(source))
|
cg.add(var.add_source(source))
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_(
|
|||||||
)
|
)
|
||||||
Transform = epaper_spi_ns.enum("Transform")
|
Transform = epaper_spi_ns.enum("Transform")
|
||||||
|
|
||||||
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
|
|
||||||
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
|
|
||||||
|
|
||||||
|
|
||||||
# Import all models dynamically from the models package
|
# Import all models dynamically from the models package
|
||||||
for module_info in pkgutil.iter_modules(models.__path__):
|
for module_info in pkgutil.iter_modules(models.__path__):
|
||||||
importlib.import_module(f".models.{module_info.name}", package=__package__)
|
importlib.import_module(f".models.{module_info.name}", package=__package__)
|
||||||
|
|||||||
231
esphome/components/epaper_spi/epaper_weact_3c.cpp
Normal file
231
esphome/components/epaper_spi/epaper_weact_3c.cpp
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include "epaper_weact_3c.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "epaper_weact_3c";
|
||||||
|
|
||||||
|
// SSD1680 3-color display notes:
|
||||||
|
// - Buffer uses 1 bit per pixel, 8 pixels per byte
|
||||||
|
// - Buffer first half (black_offset): Black/White plane (1=black, 0=white)
|
||||||
|
// - Buffer second half (red_offset): Red plane (1=red, 0=no red)
|
||||||
|
// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8)
|
||||||
|
// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color)
|
||||||
|
|
||||||
|
void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) {
|
||||||
|
if (!this->rotate_coordinates_(x, y))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Calculate position in the 1-bit buffer
|
||||||
|
const uint32_t pos = (x / 8) + (y * this->row_width_);
|
||||||
|
const uint8_t bit = 0x80 >> (x & 0x07);
|
||||||
|
const uint32_t red_offset = this->buffer_length_ / 2u;
|
||||||
|
|
||||||
|
// Use luminance threshold for B/W mapping
|
||||||
|
// Split at halfway point (382 = (255*3)/2)
|
||||||
|
bool is_white = (static_cast<int>(color.r) + color.g + color.b) > 382;
|
||||||
|
|
||||||
|
// Update black/white plane (first half of buffer)
|
||||||
|
if (is_white) {
|
||||||
|
// White pixel - clear bit in black plane
|
||||||
|
this->buffer_[pos] &= ~bit;
|
||||||
|
} else {
|
||||||
|
// Black pixel - set bit in black plane
|
||||||
|
this->buffer_[pos] |= bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update red plane (second half of buffer)
|
||||||
|
// Red if red component is dominant (r > g+b)
|
||||||
|
if (color.r > color.g + color.b) {
|
||||||
|
// Red pixel - set bit in red plane
|
||||||
|
this->buffer_[red_offset + pos] |= bit;
|
||||||
|
} else {
|
||||||
|
// Not red - clear bit in red plane
|
||||||
|
this->buffer_[red_offset + pos] &= ~bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::fill(Color color) {
|
||||||
|
// For 3-color e-paper with 1-bit buffer format:
|
||||||
|
// - Black buffer: 1=black, 0=white
|
||||||
|
// - Red buffer: 1=red, 0=no red
|
||||||
|
// The buffer is stored as two halves: [black plane][red plane]
|
||||||
|
const size_t half_buffer = this->buffer_length_ / 2u;
|
||||||
|
|
||||||
|
// Use luminance threshold for B/W mapping
|
||||||
|
bool is_white = (static_cast<int>(color.r) + color.g + color.b) > 382;
|
||||||
|
bool is_red = color.r > color.g + color.b;
|
||||||
|
|
||||||
|
// Fill both planes
|
||||||
|
if (is_white) {
|
||||||
|
// White - both planes = 0x00
|
||||||
|
this->buffer_.fill(0x00);
|
||||||
|
} else if (is_red) {
|
||||||
|
// Red - black plane = 0x00, red plane = 0xFF
|
||||||
|
for (size_t i = 0; i < half_buffer; i++)
|
||||||
|
this->buffer_[i] = 0x00;
|
||||||
|
for (size_t i = 0; i < half_buffer; i++)
|
||||||
|
this->buffer_[half_buffer + i] = 0xFF;
|
||||||
|
} else {
|
||||||
|
// Black - black plane = 0xFF, red plane = 0x00
|
||||||
|
for (size_t i = 0; i < half_buffer; i++)
|
||||||
|
this->buffer_[i] = 0xFF;
|
||||||
|
for (size_t i = 0; i < half_buffer; i++)
|
||||||
|
this->buffer_[half_buffer + i] = 0x00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::clear() {
|
||||||
|
// Clear buffer to white, just like real paper.
|
||||||
|
this->fill(COLOR_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::set_window_() {
|
||||||
|
// For full screen refresh, we always start from (0,0)
|
||||||
|
// The y_low_/y_high_ values track the dirty region for optimization,
|
||||||
|
// but for display refresh we need to write from the beginning
|
||||||
|
uint16_t x_start = 0;
|
||||||
|
uint16_t x_end = this->width_ - 1;
|
||||||
|
uint16_t y_start = 0;
|
||||||
|
uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display
|
||||||
|
|
||||||
|
// Set RAM X address boundaries (0x44)
|
||||||
|
// X coordinates are byte-aligned (divided by 8)
|
||||||
|
this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)});
|
||||||
|
|
||||||
|
// Set RAM Y address boundaries (0x45)
|
||||||
|
// Format: Y start (LSB, MSB), Y end (LSB, MSB)
|
||||||
|
this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)});
|
||||||
|
|
||||||
|
// Reset RAM X counter to start (0x4E) - 1 byte
|
||||||
|
this->cmd_data(0x4E, {(uint8_t) (x_start / 8)});
|
||||||
|
|
||||||
|
// Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB)
|
||||||
|
this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HOT EPaperWeAct3C::transfer_data() {
|
||||||
|
const uint32_t start_time = millis();
|
||||||
|
const size_t buffer_length = this->buffer_length_;
|
||||||
|
const size_t half_buffer = buffer_length / 2u;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer);
|
||||||
|
|
||||||
|
// Use a local buffer for SPI transfers
|
||||||
|
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||||
|
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||||
|
|
||||||
|
// First, send the RED buffer (0x26 = WRITE_COLOR)
|
||||||
|
// The red plane is in the second half of our buffer
|
||||||
|
// NOTE: Must set RAM window first to reset address counters!
|
||||||
|
if (this->current_data_index_ < half_buffer) {
|
||||||
|
if (this->current_data_index_ == 0) {
|
||||||
|
ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)");
|
||||||
|
this->set_window_(); // Reset RAM X/Y counters to start position
|
||||||
|
this->command(0x26);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->start_data_();
|
||||||
|
size_t red_offset = half_buffer;
|
||||||
|
while (this->current_data_index_ < half_buffer) {
|
||||||
|
size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||||
|
bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
this->write_array(bytes_to_send, bytes_to_copy);
|
||||||
|
|
||||||
|
this->current_data_index_ += bytes_to_copy;
|
||||||
|
|
||||||
|
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||||
|
// Let the main loop run and come back next loop
|
||||||
|
this->disable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK)
|
||||||
|
// The black plane is in the first half of our buffer
|
||||||
|
if (this->current_data_index_ < buffer_length) {
|
||||||
|
if (this->current_data_index_ == half_buffer) {
|
||||||
|
ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)");
|
||||||
|
|
||||||
|
// Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior)
|
||||||
|
// this->set_window();
|
||||||
|
this->command(0x24);
|
||||||
|
// Continue using current_data_index_, but we need to map it to the start of the buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
this->start_data_();
|
||||||
|
while (this->current_data_index_ < buffer_length) {
|
||||||
|
size_t remaining = buffer_length - this->current_data_index_;
|
||||||
|
size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining);
|
||||||
|
|
||||||
|
// Calculate offset into the BLACK buffer (which is at the start of this->buffer_)
|
||||||
|
// current_data_index_ goes from half_buffer to buffer_length
|
||||||
|
size_t buffer_offset = this->current_data_index_ - half_buffer;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||||
|
bytes_to_send[i] = this->buffer_[buffer_offset + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
this->write_array(bytes_to_send, bytes_to_copy);
|
||||||
|
|
||||||
|
this->current_data_index_ += bytes_to_copy;
|
||||||
|
|
||||||
|
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||||
|
// Let the main loop run and come back next loop
|
||||||
|
this->disable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->current_data_index_ = 0;
|
||||||
|
ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::refresh_screen(bool partial) {
|
||||||
|
// SSD1680 refresh sequence:
|
||||||
|
// Reset RAM X/Y address counters to 0,0 so display reads from start
|
||||||
|
// 0x4E: RAM X counter - 1 byte (X / 8)
|
||||||
|
// 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB)
|
||||||
|
this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte)
|
||||||
|
this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes)
|
||||||
|
|
||||||
|
// Send UPDATE_FULL command (0x22) with display update control parameter
|
||||||
|
// Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7}
|
||||||
|
// 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content
|
||||||
|
this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7
|
||||||
|
this->command(0x20); // Activate display update
|
||||||
|
|
||||||
|
// COMMAND TERMINATE FRAME READ WRITE (required by SSD1680)
|
||||||
|
// Removed 0xFF based on working reference implementation
|
||||||
|
// this->command(0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::power_on() {
|
||||||
|
// Power on sequence - send command to turn on power
|
||||||
|
// According to SSD1680 spec: 0x22, 0xF8 powers on the display
|
||||||
|
this->cmd_data(0x22, {0xF8}); // Power on
|
||||||
|
this->command(0x20); // Activate
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::power_off() {
|
||||||
|
// Power off sequence - send command to turn off power
|
||||||
|
// According to SSD1680 spec: 0x22, 0x83 powers off the display
|
||||||
|
this->cmd_data(0x22, {0x83}); // Power off
|
||||||
|
this->command(0x20); // Activate
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPaperWeAct3C::deep_sleep() {
|
||||||
|
// Deep sleep sequence
|
||||||
|
this->cmd_data(0x10, {0x01}); // Deep sleep mode
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
||||||
39
esphome/components/epaper_spi/epaper_weact_3c.h
Normal file
39
esphome/components/epaper_spi/epaper_weact_3c.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "epaper_spi.h"
|
||||||
|
|
||||||
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WeAct 3-color e-paper displays (SSD1683 controller).
|
||||||
|
* Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc.
|
||||||
|
*
|
||||||
|
* Color scheme: Black, White, Red (BWR)
|
||||||
|
* Buffer layout: 1 bit per pixel, separate planes
|
||||||
|
* - Buffer first half: Black/White plane (1=black, 0=white)
|
||||||
|
* - Buffer second half: Red plane (1=red, 0=no red)
|
||||||
|
* - Total buffer: width * height / 4 bytes (2 * width * height / 8)
|
||||||
|
*/
|
||||||
|
class EPaperWeAct3C : public EPaperBase {
|
||||||
|
public:
|
||||||
|
EPaperWeAct3C(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_BINARY) {
|
||||||
|
this->buffer_length_ = this->row_width_ * height * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fill(Color color) override;
|
||||||
|
void clear() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_window_();
|
||||||
|
void refresh_screen(bool partial) override;
|
||||||
|
void power_on() override;
|
||||||
|
void power_off() override;
|
||||||
|
void deep_sleep() override;
|
||||||
|
void draw_pixel_at(int x, int y, Color color) override;
|
||||||
|
|
||||||
|
bool transfer_data() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::epaper_spi
|
||||||
@@ -84,3 +84,35 @@ jd79660.extend(
|
|||||||
(0xA5, 0x00,),
|
(0xA5, 0x00,),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Waveshare 7.5-H
|
||||||
|
#
|
||||||
|
# Vendor init derived from vendor sample code
|
||||||
|
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/7in5_e-Paper_H/ESP32/EPD_7in5h.cpp>
|
||||||
|
# Compatible MIT license, see esphome/LICENSE file.
|
||||||
|
#
|
||||||
|
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
|
||||||
|
#
|
||||||
|
# fmt: off
|
||||||
|
jd79660.extend(
|
||||||
|
"Waveshare-7.5in-H",
|
||||||
|
width=800,
|
||||||
|
height=480,
|
||||||
|
|
||||||
|
initsequence=(
|
||||||
|
(0x00, 0x0F, 0x29,),
|
||||||
|
(0x06, 0x0F, 0x8B, 0x93, 0xA1,),
|
||||||
|
(0x41, 0x00,),
|
||||||
|
(0x50, 0x37,),
|
||||||
|
(0x60, 0x02, 0x02,),
|
||||||
|
(0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480
|
||||||
|
(0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,),
|
||||||
|
(0x65, 0x00, 0x00, 0x00, 0x00,),
|
||||||
|
(0xE7, 0x1C,),
|
||||||
|
(0xE3, 0x00,),
|
||||||
|
(0xE9, 0x01,),
|
||||||
|
(0x30, 0x08,),
|
||||||
|
# Power On (0x04): Must be early part of init seq = Disabled later!
|
||||||
|
(0x04,),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
75
esphome/components/epaper_spi/models/weact_bwr.py
Normal file
75
esphome/components/epaper_spi/models/weact_bwr.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""WeAct Black/White/Red e-paper displays using SSD1683 controller.
|
||||||
|
|
||||||
|
Supported models:
|
||||||
|
- weact-2.13in-3c: 122x250 pixels (2.13" display)
|
||||||
|
- weact-2.9in-3c: 128x296 pixels (2.9" display)
|
||||||
|
- weact-4.2in-3c: 400x300 pixels (4.2" display)
|
||||||
|
|
||||||
|
These displays use SSD1680 or SSD1683 controller and require a specific initialization
|
||||||
|
sequence. The DRV_OUT_CTL command is calculated from the display height.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import EpaperModel
|
||||||
|
|
||||||
|
|
||||||
|
class WeActBWR(EpaperModel):
|
||||||
|
"""Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller."""
|
||||||
|
|
||||||
|
def __init__(self, name, **defaults):
|
||||||
|
super().__init__(name, "EPaperWeAct3C", **defaults)
|
||||||
|
|
||||||
|
def get_init_sequence(self, config):
|
||||||
|
"""Generate initialization sequence for WeAct BWR displays.
|
||||||
|
|
||||||
|
The initialization sequence is based on SSD1680 and SSD1683 controller datasheet
|
||||||
|
and the WeAct display specifications.
|
||||||
|
"""
|
||||||
|
_, height = self.get_dimensions(config)
|
||||||
|
# DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00)
|
||||||
|
height_minus_1 = height - 1
|
||||||
|
msb = height_minus_1 >> 8
|
||||||
|
lsb = height_minus_1 & 0xFF
|
||||||
|
return (
|
||||||
|
# Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now
|
||||||
|
# (0x12,),
|
||||||
|
# Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now
|
||||||
|
# Step 3: DRV_OUT_CTL - driver output control (height-dependent)
|
||||||
|
# Format: (command, LSB, MSB, gate setting)
|
||||||
|
(0x01, lsb, msb, 0x00),
|
||||||
|
# Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X)
|
||||||
|
(0x11, 0x03),
|
||||||
|
# Step 5: BORDER_FULL - border waveform control
|
||||||
|
(0x3C, 0x05),
|
||||||
|
# Step 6: TEMP_SENS - internal temperature sensor
|
||||||
|
(0x18, 0x80),
|
||||||
|
# Step 7: DISPLAY_UPDATE - display update control
|
||||||
|
(0x21, 0x00, 0x80),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller
|
||||||
|
weact_2p9in3c = WeActBWR(
|
||||||
|
"weact-2.9in-3c",
|
||||||
|
width=128,
|
||||||
|
height=296,
|
||||||
|
data_rate="10MHz",
|
||||||
|
minimum_update_interval="1s",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller
|
||||||
|
weact_2p13in3c = WeActBWR(
|
||||||
|
"weact-2.13in-3c",
|
||||||
|
width=122,
|
||||||
|
height=250,
|
||||||
|
data_rate="10MHz",
|
||||||
|
minimum_update_interval="1s",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller
|
||||||
|
weact_4p2in3c = WeActBWR(
|
||||||
|
"weact-4.2in-3c",
|
||||||
|
width=400,
|
||||||
|
height=300,
|
||||||
|
data_rate="10MHz",
|
||||||
|
minimum_update_interval="10s",
|
||||||
|
)
|
||||||
@@ -1,8 +1,30 @@
|
|||||||
from esphome.components import esp32
|
from esphome.components import esp32
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
|
|
||||||
|
VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rmt_not_supported(rmt_only_keys):
|
||||||
|
"""Validate that RMT-only config keys are not used on variants without RMT hardware."""
|
||||||
|
rmt_only_keys = set(rmt_only_keys)
|
||||||
|
|
||||||
|
def _validator(config):
|
||||||
|
if CORE.is_esp32:
|
||||||
|
variant = esp32.get_esp32_variant()
|
||||||
|
if variant in VARIANTS_NO_RMT:
|
||||||
|
unsupported = rmt_only_keys.intersection(config)
|
||||||
|
if unsupported:
|
||||||
|
keys = ", ".join(sorted(f"'{k}'" for k in unsupported))
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{keys} not available on {variant} (no RMT hardware)"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
return _validator
|
||||||
|
|
||||||
|
|
||||||
def validate_clock_resolution():
|
def validate_clock_resolution():
|
||||||
def _validator(value):
|
def _validator(value):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32, light
|
from esphome.components import esp32, esp32_rmt, light
|
||||||
from esphome.components.const import CONF_USE_PSRAM
|
from esphome.components.const import CONF_USE_PSRAM
|
||||||
from esphome.components.esp32 import include_builtin_idf_component
|
from esphome.components.esp32 import include_builtin_idf_component
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low"
|
|||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
esp32.only_on_variant(
|
||||||
|
unsupported=list(esp32_rmt.VARIANTS_NO_RMT),
|
||||||
|
msg_prefix="ESP32 RMT LED strip",
|
||||||
|
),
|
||||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ class EthernetComponent : public Component {
|
|||||||
const char *get_use_address() const;
|
const char *get_use_address() const;
|
||||||
void set_use_address(const char *use_address);
|
void set_use_address(const char *use_address);
|
||||||
void get_eth_mac_address_raw(uint8_t *mac);
|
void get_eth_mac_address_raw(uint8_t *mac);
|
||||||
|
// Remove before 2026.9.0
|
||||||
|
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
|
||||||
std::string get_eth_mac_address_pretty();
|
std::string get_eth_mac_address_pretty();
|
||||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||||
eth_duplex_t get_duplex_mode();
|
eth_duplex_t get_duplex_mode();
|
||||||
|
|||||||
@@ -221,12 +221,17 @@ void Fan::publish_state() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
|
// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
|
||||||
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
|
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB;
|
||||||
optional<FanRestoreState> Fan::restore_state_() {
|
optional<FanRestoreState> Fan::restore_state_() {
|
||||||
FanRestoreState recovered{};
|
FanRestoreState recovered{};
|
||||||
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
|
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
|
||||||
bool restored = this->rtc_.load(&recovered);
|
bool restored = this->rtc_.load(&recovered);
|
||||||
|
|
||||||
|
if (!restored) {
|
||||||
|
// No valid saved data; ensure preset_mode sentinel is set
|
||||||
|
recovered.preset_mode = FanRestoreState::NO_PRESET;
|
||||||
|
}
|
||||||
|
|
||||||
switch (this->restore_mode_) {
|
switch (this->restore_mode_) {
|
||||||
case FanRestoreMode::NO_RESTORE:
|
case FanRestoreMode::NO_RESTORE:
|
||||||
return {};
|
return {};
|
||||||
@@ -264,6 +269,7 @@ void Fan::save_state_() {
|
|||||||
state.oscillating = this->oscillating;
|
state.oscillating = this->oscillating;
|
||||||
state.speed = this->speed;
|
state.speed = this->speed;
|
||||||
state.direction = this->direction;
|
state.direction = this->direction;
|
||||||
|
state.preset_mode = FanRestoreState::NO_PRESET;
|
||||||
|
|
||||||
if (this->has_preset_mode()) {
|
if (this->has_preset_mode()) {
|
||||||
const auto &preset_modes = traits.supported_preset_modes();
|
const auto &preset_modes = traits.supported_preset_modes();
|
||||||
|
|||||||
@@ -91,11 +91,13 @@ class FanCall {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct FanRestoreState {
|
struct FanRestoreState {
|
||||||
|
static constexpr uint8_t NO_PRESET = UINT8_MAX;
|
||||||
|
|
||||||
bool state;
|
bool state;
|
||||||
int speed;
|
int speed;
|
||||||
bool oscillating;
|
bool oscillating;
|
||||||
FanDirection direction;
|
FanDirection direction;
|
||||||
uint8_t preset_mode;
|
uint8_t preset_mode{NO_PRESET};
|
||||||
|
|
||||||
/// Convert this struct to a fan call that can be performed.
|
/// Convert this struct to a fan call that can be performed.
|
||||||
FanCall to_call(Fan &fan);
|
FanCall to_call(Fan &fan);
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HBridgeFan::setup() {
|
void HBridgeFan::setup() {
|
||||||
|
// Construct traits before restore so preset modes can be looked up by index
|
||||||
|
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
|
||||||
|
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
||||||
|
|
||||||
auto restore = this->restore_state_();
|
auto restore = this->restore_state_();
|
||||||
if (restore.has_value()) {
|
if (restore.has_value()) {
|
||||||
restore->apply(*this);
|
restore->apply(*this);
|
||||||
this->write_state_();
|
this->write_state_();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct traits
|
|
||||||
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
|
|
||||||
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HBridgeFan::dump_config() {
|
void HBridgeFan::dump_config() {
|
||||||
|
|||||||
@@ -9,9 +9,20 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
// Include BearSSL error constants for TLS failure diagnostics
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
#include <bearssl/bearssl_ssl.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::http_request {
|
namespace esphome::http_request {
|
||||||
|
|
||||||
static const char *const TAG = "http_request.arduino";
|
static const char *const TAG = "http_request.arduino";
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
static constexpr int RX_BUFFER_SIZE = 512;
|
||||||
|
static constexpr int TX_BUFFER_SIZE = 512;
|
||||||
|
// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM
|
||||||
|
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
|
||||||
|
#endif
|
||||||
|
|
||||||
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
|
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
|
||||||
const std::string &body,
|
const std::string &body,
|
||||||
@@ -47,7 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
|||||||
ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
|
ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
|
||||||
stream_ptr = std::make_unique<WiFiClientSecure>();
|
stream_ptr = std::make_unique<WiFiClientSecure>();
|
||||||
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
|
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
|
||||||
secure_client->setBufferSizes(512, 512);
|
secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE);
|
||||||
secure_client->setInsecure();
|
secure_client->setInsecure();
|
||||||
} else {
|
} else {
|
||||||
stream_ptr = std::make_unique<WiFiClient>();
|
stream_ptr = std::make_unique<WiFiClient>();
|
||||||
@@ -107,13 +118,42 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
|||||||
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
|
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
|
||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
if (container->status_code < 0) {
|
if (container->status_code < 0) {
|
||||||
|
#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS)
|
||||||
|
if (secure) {
|
||||||
|
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
|
||||||
|
int last_error = secure_client->getLastSSLError();
|
||||||
|
|
||||||
|
if (last_error != 0) {
|
||||||
|
const LogString *error_msg;
|
||||||
|
switch (last_error) {
|
||||||
|
case ESP8266_SSL_ERR_OOM:
|
||||||
|
error_msg = LOG_STR("Unable to allocate buffer memory");
|
||||||
|
break;
|
||||||
|
case BR_ERR_TOO_LARGE:
|
||||||
|
error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error_msg = LOG_STR("Unknown SSL error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error);
|
||||||
|
if (last_error == ESP8266_SSL_ERR_OOM) {
|
||||||
|
ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(),
|
||||||
|
static_cast<unsigned int>(RX_BUFFER_SIZE + TX_BUFFER_SIZE));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Connection failure with no error code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
|
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
|
||||||
HTTPClient::errorToString(container->status_code).c_str());
|
HTTPClient::errorToString(container->status_code).c_str());
|
||||||
|
|
||||||
this->status_momentary_error("failed", 1000);
|
this->status_momentary_error("failed", 1000);
|
||||||
container->end();
|
container->end();
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_success(container->status_code)) {
|
if (!is_success(container->status_code)) {
|
||||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
|
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
|
||||||
this->status_momentary_error("failed", 1000);
|
this->status_momentary_error("failed", 1000);
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ namespace json {
|
|||||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||||
// This is only compiled when PSRAM is enabled
|
// This is only compiled when PSRAM is enabled
|
||||||
struct SpiRamAllocator : ArduinoJson::Allocator {
|
struct SpiRamAllocator : ArduinoJson::Allocator {
|
||||||
void *allocate(size_t size) override { return allocator_.allocate(size); }
|
void *allocate(size_t size) override {
|
||||||
|
RAMAllocator<uint8_t> allocator;
|
||||||
|
return allocator.allocate(size);
|
||||||
|
}
|
||||||
|
|
||||||
void deallocate(void *ptr) override {
|
void deallocate(void *ptr) override {
|
||||||
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
|
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
|
||||||
@@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void *reallocate(void *ptr, size_t new_size) override {
|
void *reallocate(void *ptr, size_t new_size) override {
|
||||||
return allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
|
RAMAllocator<uint8_t> allocator;
|
||||||
|
return allocator.reallocate(static_cast<uint8_t *>(ptr), new_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
|
||||||
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>::NONE};
|
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
from esphome.components.mipi import DriverChip
|
from esphome.components.mipi import (
|
||||||
|
ETMOD,
|
||||||
|
FRMCTR2,
|
||||||
|
GMCTRN1,
|
||||||
|
GMCTRP1,
|
||||||
|
IFCTR,
|
||||||
|
MODE_RGB,
|
||||||
|
PWCTR1,
|
||||||
|
PWCTR3,
|
||||||
|
PWCTR4,
|
||||||
|
PWCTR5,
|
||||||
|
PWSET,
|
||||||
|
DriverChip,
|
||||||
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
from .amoled import CO5300
|
from .amoled import CO5300
|
||||||
@@ -129,6 +142,16 @@ DriverChip(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
ST7789P = DriverChip(
|
||||||
|
"ST7789P",
|
||||||
|
# Max supported dimensions
|
||||||
|
width=240,
|
||||||
|
height=320,
|
||||||
|
# SPI: RGB layout
|
||||||
|
color_order=MODE_RGB,
|
||||||
|
invert_colors=True,
|
||||||
|
draw_rounding=1,
|
||||||
|
)
|
||||||
|
|
||||||
ILI9488_A.extend(
|
ILI9488_A.extend(
|
||||||
"PICO-RESTOUCH-LCD-3.5",
|
"PICO-RESTOUCH-LCD-3.5",
|
||||||
@@ -162,3 +185,61 @@ AXS15231.extend(
|
|||||||
cs_pin=9,
|
cs_pin=9,
|
||||||
reset_pin=21,
|
reset_pin=21,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Waveshare 1.83-v2
|
||||||
|
#
|
||||||
|
# Do not use on 1.83-v1: Vendor warning on different chip!
|
||||||
|
ST7789P.extend(
|
||||||
|
"WAVESHARE-1.83-V2",
|
||||||
|
# Panel size smaller than ST7789 max allowed
|
||||||
|
width=240,
|
||||||
|
height=284,
|
||||||
|
# Vendor specific init derived from vendor sample code
|
||||||
|
# "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp"
|
||||||
|
# Compatible MIT license, see esphome/LICENSE file.
|
||||||
|
initsequence=(
|
||||||
|
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
|
||||||
|
(ETMOD, 0x35),
|
||||||
|
(0xBB, 0x19),
|
||||||
|
(PWCTR1, 0x2C),
|
||||||
|
(PWCTR3, 0x01),
|
||||||
|
(PWCTR4, 0x12),
|
||||||
|
(PWCTR5, 0x20),
|
||||||
|
(IFCTR, 0x0F),
|
||||||
|
(PWSET, 0xA4, 0xA1),
|
||||||
|
(
|
||||||
|
GMCTRP1,
|
||||||
|
0xD0,
|
||||||
|
0x04,
|
||||||
|
0x0D,
|
||||||
|
0x11,
|
||||||
|
0x13,
|
||||||
|
0x2B,
|
||||||
|
0x3F,
|
||||||
|
0x54,
|
||||||
|
0x4C,
|
||||||
|
0x18,
|
||||||
|
0x0D,
|
||||||
|
0x0B,
|
||||||
|
0x1F,
|
||||||
|
0x23,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
GMCTRN1,
|
||||||
|
0xD0,
|
||||||
|
0x04,
|
||||||
|
0x0C,
|
||||||
|
0x11,
|
||||||
|
0x13,
|
||||||
|
0x2C,
|
||||||
|
0x3F,
|
||||||
|
0x44,
|
||||||
|
0x51,
|
||||||
|
0x2F,
|
||||||
|
0x1F,
|
||||||
|
0x1F,
|
||||||
|
0x20,
|
||||||
|
0x23,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,97 +2,34 @@ import logging
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS
|
from esphome.components import runtime_image
|
||||||
|
from esphome.components.const import CONF_REQUEST_HEADERS
|
||||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||||
from esphome.components.image import (
|
|
||||||
CONF_INVERT_ALPHA,
|
|
||||||
CONF_TRANSPARENCY,
|
|
||||||
IMAGE_SCHEMA,
|
|
||||||
Image_,
|
|
||||||
get_image_type_enum,
|
|
||||||
get_transparency_enum,
|
|
||||||
validate_settings,
|
|
||||||
)
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
CONF_DITHER,
|
|
||||||
CONF_FILE,
|
|
||||||
CONF_FORMAT,
|
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_ON_ERROR,
|
CONF_ON_ERROR,
|
||||||
CONF_RESIZE,
|
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_TYPE,
|
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
)
|
)
|
||||||
from esphome.core import Lambda
|
from esphome.core import Lambda
|
||||||
|
|
||||||
AUTO_LOAD = ["image"]
|
AUTO_LOAD = ["image", "runtime_image"]
|
||||||
DEPENDENCIES = ["display", "http_request"]
|
DEPENDENCIES = ["display", "http_request"]
|
||||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||||
CONF_PLACEHOLDER = "placeholder"
|
|
||||||
CONF_UPDATE = "update"
|
CONF_UPDATE = "update"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||||
|
|
||||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
OnlineImage = online_image_ns.class_(
|
||||||
|
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
|
||||||
|
)
|
||||||
class Format:
|
|
||||||
def __init__(self, image_type):
|
|
||||||
self.image_type = image_type
|
|
||||||
|
|
||||||
@property
|
|
||||||
def enum(self):
|
|
||||||
return getattr(ImageFormat, self.image_type)
|
|
||||||
|
|
||||||
def actions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BMPFormat(Format):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("BMP")
|
|
||||||
|
|
||||||
def actions(self):
|
|
||||||
cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT")
|
|
||||||
|
|
||||||
|
|
||||||
class JPEGFormat(Format):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("JPEG")
|
|
||||||
|
|
||||||
def actions(self):
|
|
||||||
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
|
|
||||||
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
|
|
||||||
|
|
||||||
|
|
||||||
class PNGFormat(Format):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("PNG")
|
|
||||||
|
|
||||||
def actions(self):
|
|
||||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
|
||||||
cg.add_library("pngle", "1.1.0")
|
|
||||||
|
|
||||||
|
|
||||||
IMAGE_FORMATS = {
|
|
||||||
x.image_type: x
|
|
||||||
for x in (
|
|
||||||
BMPFormat(),
|
|
||||||
JPEGFormat(),
|
|
||||||
PNGFormat(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]})
|
|
||||||
|
|
||||||
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
SetUrlAction = online_image_ns.class_(
|
SetUrlAction = online_image_ns.class_(
|
||||||
@@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_options(*options):
|
|
||||||
return {
|
|
||||||
cv.Optional(option): cv.invalid(
|
|
||||||
f"{option} is an invalid option for online_image"
|
|
||||||
)
|
|
||||||
for option in options
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ONLINE_IMAGE_SCHEMA = (
|
ONLINE_IMAGE_SCHEMA = (
|
||||||
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
|
runtime_image.runtime_image_schema(OnlineImage)
|
||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
|
||||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
|
||||||
# Online Image specific options
|
# Online Image specific options
|
||||||
|
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||||
cv.Required(CONF_URL): cv.url,
|
cv.Required(CONF_URL): cv.url,
|
||||||
|
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
||||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||||
),
|
),
|
||||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
|
|
||||||
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
|
|
||||||
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
|
|
||||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
@@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
rp2040_arduino=cv.Version(0, 0, 0),
|
rp2040_arduino=cv.Version(0, 0, 0),
|
||||||
host=cv.Version(0, 0, 0),
|
host=cv.Version(0, 0, 0),
|
||||||
),
|
),
|
||||||
validate_settings,
|
runtime_image.validate_runtime_image_settings,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
|
# Use the enhanced helper function to get all runtime image parameters
|
||||||
image_format.actions()
|
settings = await runtime_image.process_runtime_image_config(config)
|
||||||
|
|
||||||
url = config[CONF_URL]
|
url = config[CONF_URL]
|
||||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
|
||||||
transparent = get_transparency_enum(config[CONF_TRANSPARENCY])
|
|
||||||
|
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
url,
|
url,
|
||||||
width,
|
settings.width,
|
||||||
height,
|
settings.height,
|
||||||
image_format.enum,
|
settings.format_enum,
|
||||||
get_image_type_enum(config[CONF_TYPE]),
|
settings.image_type_enum,
|
||||||
transparent,
|
settings.transparent,
|
||||||
|
settings.placeholder or cg.nullptr,
|
||||||
config[CONF_BUFFER_SIZE],
|
config[CONF_BUFFER_SIZE],
|
||||||
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN",
|
settings.byte_order_big_endian,
|
||||||
)
|
)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||||
@@ -227,10 +150,6 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
cg.add(var.add_request_header(key, value))
|
cg.add(var.add_request_header(key, value))
|
||||||
|
|
||||||
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
|
||||||
placeholder = await cg.get_variable(placeholder_id)
|
|
||||||
cg.add(var.set_placeholder(placeholder))
|
|
||||||
|
|
||||||
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
await automation.build_automation(trigger, [(bool, "cached")], conf)
|
await automation.build_automation(trigger, [(bool, "cached")], conf)
|
||||||
|
|||||||
@@ -1,29 +1,10 @@
|
|||||||
#include "image_decoder.h"
|
#include "download_buffer.h"
|
||||||
#include "online_image.h"
|
|
||||||
|
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::online_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
static const char *const TAG = "online_image.decoder";
|
static const char *const TAG = "online_image.download_buffer";
|
||||||
|
|
||||||
bool ImageDecoder::set_size(int width, int height) {
|
|
||||||
bool success = this->image_->resize_(width, height) > 0;
|
|
||||||
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
|
|
||||||
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
|
||||||
auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
|
||||||
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
|
||||||
for (int i = x * this->x_scale_; i < width; i++) {
|
|
||||||
for (int j = y * this->y_scale_; j < height; j++) {
|
|
||||||
this->image_->draw_pixel_(i, j, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
|
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
|
||||||
this->buffer_ = this->allocator_.allocate(size);
|
this->buffer_ = this->allocator_.allocate(size);
|
||||||
@@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t DownloadBuffer::read(size_t len) {
|
size_t DownloadBuffer::read(size_t len) {
|
||||||
this->unread_ -= len;
|
if (len >= this->unread_) {
|
||||||
if (this->unread_ > 0) {
|
this->unread_ = 0;
|
||||||
memmove(this->data(), this->data(len), this->unread_);
|
return 0;
|
||||||
}
|
}
|
||||||
|
this->unread_ -= len;
|
||||||
|
memmove(this->data(), this->data(len), this->unread_);
|
||||||
return this->unread_;
|
return this->unread_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::online_image
|
||||||
} // namespace esphome
|
|
||||||
44
esphome/components/online_image/download_buffer.h
Normal file
44
esphome/components/online_image/download_buffer.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace esphome::online_image {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Buffer for managing downloaded data.
|
||||||
|
*
|
||||||
|
* This class provides a buffer for downloading data with tracking of
|
||||||
|
* unread bytes and dynamic resizing capabilities.
|
||||||
|
*/
|
||||||
|
class DownloadBuffer {
|
||||||
|
public:
|
||||||
|
DownloadBuffer(size_t size);
|
||||||
|
~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
||||||
|
|
||||||
|
uint8_t *data(size_t offset = 0);
|
||||||
|
uint8_t *append() { return this->data(this->unread_); }
|
||||||
|
|
||||||
|
size_t unread() const { return this->unread_; }
|
||||||
|
size_t size() const { return this->size_; }
|
||||||
|
size_t free_capacity() const { return this->size_ - this->unread_; }
|
||||||
|
|
||||||
|
size_t read(size_t len);
|
||||||
|
size_t write(size_t len) {
|
||||||
|
this->unread_ += len;
|
||||||
|
return this->unread_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() { this->unread_ = 0; }
|
||||||
|
size_t resize(size_t size);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
RAMAllocator<uint8_t> allocator_{};
|
||||||
|
uint8_t *buffer_;
|
||||||
|
size_t size_;
|
||||||
|
/** Total number of downloaded bytes not yet read. */
|
||||||
|
size_t unread_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::online_image
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "online_image.h"
|
#include "online_image.h"
|
||||||
|
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
static const char *const TAG = "online_image";
|
static const char *const TAG = "online_image";
|
||||||
static const char *const ETAG_HEADER_NAME = "etag";
|
static const char *const ETAG_HEADER_NAME = "etag";
|
||||||
@@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match";
|
|||||||
static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
|
static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
|
||||||
static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||||
|
|
||||||
#include "image_decoder.h"
|
namespace esphome::online_image {
|
||||||
|
|
||||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||||
#include "bmp_image.h"
|
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||||
#endif
|
uint32_t buffer_size, bool is_big_endian)
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
: RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height),
|
||||||
#include "jpeg_image.h"
|
download_buffer_(buffer_size),
|
||||||
#endif
|
download_buffer_initial_size_(buffer_size) {
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
|
||||||
#include "png_image.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
using image::ImageType;
|
|
||||||
|
|
||||||
inline bool is_color_on(const Color &color) {
|
|
||||||
// This produces the most accurate monochrome conversion, but is slightly slower.
|
|
||||||
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
|
||||||
|
|
||||||
// Approximation using fast integer computations; produces acceptable results
|
|
||||||
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
|
||||||
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
|
||||||
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
|
|
||||||
: Image(nullptr, 0, 0, type, transparency),
|
|
||||||
buffer_(nullptr),
|
|
||||||
download_buffer_(download_buffer_size),
|
|
||||||
download_buffer_initial_size_(download_buffer_size),
|
|
||||||
format_(format),
|
|
||||||
fixed_width_(width),
|
|
||||||
fixed_height_(height),
|
|
||||||
is_big_endian_(is_big_endian) {
|
|
||||||
this->set_url(url);
|
this->set_url(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
bool OnlineImage::validate_url_(const std::string &url) {
|
||||||
if (this->data_start_) {
|
if (url.empty()) {
|
||||||
Image::draw(x, y, display, color_on, color_off);
|
ESP_LOGE(TAG, "URL is empty");
|
||||||
} else if (this->placeholder_) {
|
return false;
|
||||||
this->placeholder_->draw(x, y, display, color_on, color_off);
|
|
||||||
}
|
}
|
||||||
}
|
if (url.length() > 2048) {
|
||||||
|
ESP_LOGE(TAG, "URL is too long");
|
||||||
void OnlineImage::release() {
|
return false;
|
||||||
if (this->buffer_) {
|
|
||||||
ESP_LOGV(TAG, "Deallocating old buffer");
|
|
||||||
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
|
||||||
this->data_start_ = nullptr;
|
|
||||||
this->buffer_ = nullptr;
|
|
||||||
this->width_ = 0;
|
|
||||||
this->height_ = 0;
|
|
||||||
this->buffer_width_ = 0;
|
|
||||||
this->buffer_height_ = 0;
|
|
||||||
this->last_modified_ = "";
|
|
||||||
this->etag_ = "";
|
|
||||||
this->end_connection_();
|
|
||||||
}
|
}
|
||||||
}
|
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
|
||||||
|
ESP_LOGE(TAG, "URL must start with http:// or https://");
|
||||||
size_t OnlineImage::resize_(int width_in, int height_in) {
|
return false;
|
||||||
int width = this->fixed_width_;
|
|
||||||
int height = this->fixed_height_;
|
|
||||||
if (this->is_auto_resize_()) {
|
|
||||||
width = width_in;
|
|
||||||
height = height_in;
|
|
||||||
if (this->width_ != width && this->height_ != height) {
|
|
||||||
this->release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
size_t new_size = this->get_buffer_size_(width, height);
|
return true;
|
||||||
if (this->buffer_) {
|
|
||||||
// Buffer already allocated => no need to resize
|
|
||||||
return new_size;
|
|
||||||
}
|
|
||||||
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
|
|
||||||
this->buffer_ = this->allocator_.allocate(new_size);
|
|
||||||
if (this->buffer_ == nullptr) {
|
|
||||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
|
|
||||||
this->allocator_.get_max_free_block_size());
|
|
||||||
this->end_connection_();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
this->buffer_width_ = width;
|
|
||||||
this->buffer_height_ = height;
|
|
||||||
this->width_ = width;
|
|
||||||
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
|
|
||||||
return new_size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnlineImage::update() {
|
void OnlineImage::update() {
|
||||||
if (this->decoder_) {
|
if (this->is_decoding()) {
|
||||||
ESP_LOGW(TAG, "Image already being updated.");
|
ESP_LOGW(TAG, "Image already being updated.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
|
|
||||||
|
|
||||||
std::list<http_request::Header> headers = {};
|
if (!this->validate_url_(this->url_)) {
|
||||||
|
ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str());
|
||||||
http_request::Header accept_header;
|
this->download_error_callback_.call();
|
||||||
accept_header.name = "Accept";
|
return;
|
||||||
std::string accept_mime_type;
|
|
||||||
switch (this->format_) {
|
|
||||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
|
||||||
case ImageFormat::BMP:
|
|
||||||
accept_mime_type = "image/bmp";
|
|
||||||
break;
|
|
||||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
|
||||||
case ImageFormat::JPEG:
|
|
||||||
accept_mime_type = "image/jpeg";
|
|
||||||
break;
|
|
||||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
|
||||||
case ImageFormat::PNG:
|
|
||||||
accept_mime_type = "image/png";
|
|
||||||
break;
|
|
||||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
|
||||||
default:
|
|
||||||
accept_mime_type = "image/*";
|
|
||||||
}
|
}
|
||||||
accept_header.value = accept_mime_type + ",*/*;q=0.8";
|
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str());
|
||||||
|
|
||||||
|
std::list<http_request::Header> headers;
|
||||||
|
|
||||||
|
// Add caching headers if we have them
|
||||||
if (!this->etag_.empty()) {
|
if (!this->etag_.empty()) {
|
||||||
headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_});
|
headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->last_modified_.empty()) {
|
if (!this->last_modified_.empty()) {
|
||||||
headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
|
headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.push_back(accept_header);
|
// Add Accept header based on image format
|
||||||
|
const char *accept_mime_type;
|
||||||
|
switch (this->get_format()) {
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||||
|
case runtime_image::BMP:
|
||||||
|
accept_mime_type = "image/bmp,*/*;q=0.8";
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||||
|
case runtime_image::JPEG:
|
||||||
|
accept_mime_type = "image/jpeg,*/*;q=0.8";
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||||
|
case runtime_image::PNG:
|
||||||
|
accept_mime_type = "image/png,*/*;q=0.8";
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
accept_mime_type = "image/*,*/*;q=0.8";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
headers.push_back({"Accept", accept_mime_type});
|
||||||
|
|
||||||
|
// User headers last so they can override any of the above
|
||||||
for (auto &header : this->request_headers_) {
|
for (auto &header : this->request_headers_) {
|
||||||
headers.push_back(http_request::Header{header.first, header.second.value()});
|
headers.push_back(http_request::Header{header.first, header.second.value()});
|
||||||
}
|
}
|
||||||
@@ -175,186 +115,117 @@ void OnlineImage::update() {
|
|||||||
ESP_LOGD(TAG, "Starting download");
|
ESP_LOGD(TAG, "Starting download");
|
||||||
size_t total_size = this->downloader_->content_length;
|
size_t total_size = this->downloader_->content_length;
|
||||||
|
|
||||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
// Initialize decoder with the known format
|
||||||
if (this->format_ == ImageFormat::BMP) {
|
if (!this->begin_decode(total_size)) {
|
||||||
ESP_LOGD(TAG, "Allocating BMP decoder");
|
ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format());
|
||||||
this->decoder_ = make_unique<BmpDecoder>(this);
|
this->end_connection_();
|
||||||
this->enable_loop();
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
|
||||||
if (this->format_ == ImageFormat::JPEG) {
|
|
||||||
ESP_LOGD(TAG, "Allocating JPEG decoder");
|
|
||||||
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
|
|
||||||
this->enable_loop();
|
|
||||||
}
|
|
||||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
|
||||||
if (this->format_ == ImageFormat::PNG) {
|
|
||||||
ESP_LOGD(TAG, "Allocating PNG decoder");
|
|
||||||
this->decoder_ = make_unique<PngDecoder>(this);
|
|
||||||
this->enable_loop();
|
|
||||||
}
|
|
||||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
|
||||||
|
|
||||||
if (!this->decoder_) {
|
// JPEG requires the complete image in the download buffer before decoding
|
||||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
|
if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) {
|
||||||
this->end_connection_();
|
this->download_buffer_.resize(total_size);
|
||||||
this->download_error_callback_.call();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto prepare_result = this->decoder_->prepare(total_size);
|
|
||||||
if (prepare_result < 0) {
|
|
||||||
this->end_connection_();
|
|
||||||
this->download_error_callback_.call();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||||
this->start_time_ = ::time(nullptr);
|
this->start_time_ = ::time(nullptr);
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnlineImage::loop() {
|
void OnlineImage::loop() {
|
||||||
if (!this->decoder_) {
|
if (!this->is_decoding()) {
|
||||||
// Not decoding at the moment => nothing to do.
|
// Not decoding at the moment => nothing to do.
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
|
||||||
this->data_start_ = buffer_;
|
if (!this->downloader_) {
|
||||||
this->width_ = buffer_width_;
|
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||||
this->height_ = buffer_height_;
|
this->end_connection_();
|
||||||
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
|
this->download_error_callback_.call();
|
||||||
this->width_, this->height_);
|
return;
|
||||||
ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_));
|
}
|
||||||
|
|
||||||
|
// Check if download is complete — use decoder's format-specific completion check
|
||||||
|
// to handle both known content-length and chunked transfer encoding
|
||||||
|
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
|
||||||
|
this->downloader_->get_bytes_read() >= this->downloader_->content_length &&
|
||||||
|
this->download_buffer_.unread() == 0)) {
|
||||||
|
// Finalize decoding
|
||||||
|
this->end_decode();
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
|
||||||
|
(uint32_t) (::time(nullptr) - this->start_time_));
|
||||||
|
|
||||||
|
// Save caching headers
|
||||||
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
|
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
|
||||||
this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
|
this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
|
||||||
|
|
||||||
this->download_finished_callback_.call(false);
|
this->download_finished_callback_.call(false);
|
||||||
this->end_connection_();
|
this->end_connection_();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->downloader_ == nullptr) {
|
|
||||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
// Download and decode more data
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t available = this->download_buffer_.free_capacity();
|
size_t available = this->download_buffer_.free_capacity();
|
||||||
if (available) {
|
if (available > 0) {
|
||||||
// Some decoders need to fully download the image before downloading.
|
// Download in chunks to avoid blocking
|
||||||
// In case of huge images, don't wait blocking until the whole image has been downloaded,
|
|
||||||
// use smaller chunks
|
|
||||||
available = std::min(available, this->download_buffer_initial_size_);
|
available = std::min(available, this->download_buffer_initial_size_);
|
||||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||||
|
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
this->download_buffer_.write(len);
|
this->download_buffer_.write(len);
|
||||||
auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
|
|
||||||
if (fed < 0) {
|
// Feed data to decoder
|
||||||
ESP_LOGE(TAG, "Error when decoding image.");
|
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||||
|
|
||||||
|
if (consumed < 0) {
|
||||||
|
ESP_LOGE(TAG, "Error decoding image: %d", consumed);
|
||||||
this->end_connection_();
|
this->end_connection_();
|
||||||
this->download_error_callback_.call();
|
this->download_error_callback_.call();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->download_buffer_.read(fed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnlineImage::map_chroma_key(Color &color) {
|
if (consumed > 0) {
|
||||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
this->download_buffer_.read(consumed);
|
||||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
|
||||||
color.g = 0;
|
|
||||||
}
|
|
||||||
if (color.w < 0x80) {
|
|
||||||
color.r = 0;
|
|
||||||
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
|
|
||||||
color.b = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|
||||||
if (!this->buffer_) {
|
|
||||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
|
||||||
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t pos = this->get_position_(x, y);
|
|
||||||
switch (this->type_) {
|
|
||||||
case ImageType::IMAGE_TYPE_BINARY: {
|
|
||||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
|
||||||
pos = x + y * width_8;
|
|
||||||
auto bitno = 0x80 >> (pos % 8u);
|
|
||||||
pos /= 8u;
|
|
||||||
auto on = is_color_on(color);
|
|
||||||
if (this->has_transparency() && color.w < 0x80)
|
|
||||||
on = false;
|
|
||||||
if (on) {
|
|
||||||
this->buffer_[pos] |= bitno;
|
|
||||||
} else {
|
|
||||||
this->buffer_[pos] &= ~bitno;
|
|
||||||
}
|
}
|
||||||
break;
|
} else if (len < 0) {
|
||||||
|
ESP_LOGE(TAG, "Error downloading image: %d", len);
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
} else {
|
||||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
// Buffer is full, need to decode some data first
|
||||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||||
if (gray == 1) {
|
if (consumed > 0) {
|
||||||
gray = 0;
|
this->download_buffer_.read(consumed);
|
||||||
}
|
} else if (consumed < 0) {
|
||||||
if (color.w < 0x80) {
|
ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed);
|
||||||
gray = 1;
|
this->end_connection_();
|
||||||
}
|
this->download_error_callback_.call();
|
||||||
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
return;
|
||||||
if (color.w != 0xFF)
|
} else {
|
||||||
gray = color.w;
|
// Decoder can't process more data, might need complete image
|
||||||
}
|
// This is normal for JPEG which needs complete data
|
||||||
this->buffer_[pos] = gray;
|
ESP_LOGV(TAG, "Decoder waiting for more data");
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ImageType::IMAGE_TYPE_RGB565: {
|
|
||||||
this->map_chroma_key(color);
|
|
||||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
|
||||||
if (this->is_big_endian_) {
|
|
||||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
|
||||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
|
||||||
} else {
|
|
||||||
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
|
|
||||||
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
|
||||||
}
|
|
||||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
|
||||||
this->buffer_[pos + 2] = color.w;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ImageType::IMAGE_TYPE_RGB: {
|
|
||||||
this->map_chroma_key(color);
|
|
||||||
this->buffer_[pos + 0] = color.r;
|
|
||||||
this->buffer_[pos + 1] = color.g;
|
|
||||||
this->buffer_[pos + 2] = color.b;
|
|
||||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
|
||||||
this->buffer_[pos + 3] = color.w;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnlineImage::end_connection_() {
|
void OnlineImage::end_connection_() {
|
||||||
|
// Abort any in-progress decode to free decoder resources.
|
||||||
|
// Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
|
||||||
|
if (this->is_decoding()) {
|
||||||
|
RuntimeImage::release();
|
||||||
|
}
|
||||||
if (this->downloader_) {
|
if (this->downloader_) {
|
||||||
this->downloader_->end();
|
this->downloader_->end();
|
||||||
this->downloader_ = nullptr;
|
this->downloader_ = nullptr;
|
||||||
}
|
}
|
||||||
this->decoder_.reset();
|
|
||||||
this->download_buffer_.reset();
|
this->download_buffer_.reset();
|
||||||
}
|
this->disable_loop();
|
||||||
|
|
||||||
bool OnlineImage::validate_url_(const std::string &url) {
|
|
||||||
if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
|
|
||||||
ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
|
void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
|
||||||
@@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
|
|||||||
this->download_error_callback_.add(std::move(callback));
|
this->download_error_callback_.add(std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace online_image
|
void OnlineImage::release() {
|
||||||
} // namespace esphome
|
// Clear cache headers
|
||||||
|
this->etag_ = "";
|
||||||
|
this->last_modified_ = "";
|
||||||
|
|
||||||
|
// End any active connection
|
||||||
|
this->end_connection_();
|
||||||
|
|
||||||
|
// Call parent's release to free the image buffer
|
||||||
|
RuntimeImage::release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::online_image
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "download_buffer.h"
|
||||||
#include "esphome/components/http_request/http_request.h"
|
#include "esphome/components/http_request/http_request.h"
|
||||||
#include "esphome/components/image/image.h"
|
#include "esphome/components/runtime_image/runtime_image.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include "image_decoder.h"
|
namespace esphome::online_image {
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
using t_http_codes = enum {
|
using t_http_codes = enum {
|
||||||
HTTP_CODE_OK = 200,
|
HTTP_CODE_OK = 200,
|
||||||
@@ -17,27 +16,13 @@ using t_http_codes = enum {
|
|||||||
HTTP_CODE_NOT_FOUND = 404,
|
HTTP_CODE_NOT_FOUND = 404,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Format that the image is encoded with.
|
|
||||||
*/
|
|
||||||
enum ImageFormat {
|
|
||||||
/** Automatically detect from MIME type. Not supported yet. */
|
|
||||||
AUTO,
|
|
||||||
/** JPEG format. */
|
|
||||||
JPEG,
|
|
||||||
/** PNG format. */
|
|
||||||
PNG,
|
|
||||||
/** BMP format. */
|
|
||||||
BMP,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||||
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||||
* need to re-download or re-decode.
|
* need to re-download or re-decode.
|
||||||
*/
|
*/
|
||||||
class OnlineImage : public PollingComponent,
|
class OnlineImage : public PollingComponent,
|
||||||
public image::Image,
|
public runtime_image::RuntimeImage,
|
||||||
public Parented<esphome::http_request::HttpRequestComponent> {
|
public Parented<esphome::http_request::HttpRequestComponent> {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
@@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent,
|
|||||||
* @param url URL to download the image from.
|
* @param url URL to download the image from.
|
||||||
* @param width Desired width of the target image area.
|
* @param width Desired width of the target image area.
|
||||||
* @param height Desired height of the target image area.
|
* @param height Desired height of the target image area.
|
||||||
* @param format Format that the image is encoded in (@see ImageFormat).
|
* @param format Format that the image is encoded in (@see runtime_image::ImageFormat).
|
||||||
|
* @param type The pixel format for the image.
|
||||||
|
* @param transparency The transparency type for the image.
|
||||||
|
* @param placeholder Optional placeholder image to show while loading.
|
||||||
* @param buffer_size Size of the buffer used to download the image.
|
* @param buffer_size Size of the buffer used to download the image.
|
||||||
|
* @param is_big_endian Whether the image is stored in big-endian format.
|
||||||
*/
|
*/
|
||||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type,
|
||||||
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size,
|
||||||
|
bool is_big_endian = false);
|
||||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
|
||||||
|
|
||||||
void update() override;
|
void update() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void map_chroma_key(Color &color);
|
|
||||||
|
|
||||||
/** Set the URL to download the image from. */
|
/** Set the URL to download the image from. */
|
||||||
void set_url(const std::string &url) {
|
void set_url(const std::string &url) {
|
||||||
@@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent,
|
|||||||
|
|
||||||
/** Add the request header */
|
/** Add the request header */
|
||||||
template<typename V> void add_request_header(const std::string &header, V value) {
|
template<typename V> void add_request_header(const std::string &header, V value) {
|
||||||
this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string> >(header, value));
|
this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string>>(header, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the image that needs to be shown as long as the downloaded image
|
|
||||||
* is not available.
|
|
||||||
*
|
|
||||||
* @param placeholder Pointer to the (@link Image) to show as placeholder.
|
|
||||||
*/
|
|
||||||
void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the buffer storing the image. The image will need to be downloaded again
|
* Release the buffer storing the image. The image will need to be downloaded again
|
||||||
* to be able to be displayed.
|
* to be able to be displayed.
|
||||||
*/
|
*/
|
||||||
void release();
|
void release();
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize the download buffer
|
|
||||||
*
|
|
||||||
* @param size The new size for the download buffer.
|
|
||||||
*/
|
|
||||||
size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); }
|
|
||||||
|
|
||||||
void add_on_finished_callback(std::function<void(bool)> &&callback);
|
void add_on_finished_callback(std::function<void(bool)> &&callback);
|
||||||
void add_on_error_callback(std::function<void()> &&callback);
|
void add_on_error_callback(std::function<void()> &&callback);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool validate_url_(const std::string &url);
|
bool validate_url_(const std::string &url);
|
||||||
|
|
||||||
RAMAllocator<uint8_t> allocator_{};
|
|
||||||
|
|
||||||
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
|
|
||||||
int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; }
|
|
||||||
|
|
||||||
int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
|
|
||||||
|
|
||||||
ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Resize the image buffer to the requested dimensions.
|
|
||||||
*
|
|
||||||
* The buffer will be allocated if not existing.
|
|
||||||
* If the dimensions have been fixed in the yaml config, the buffer will be created
|
|
||||||
* with those dimensions and not resized, even on request.
|
|
||||||
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
|
|
||||||
* allocated
|
|
||||||
*
|
|
||||||
* @param width
|
|
||||||
* @param height
|
|
||||||
* @return 0 if no memory could be allocated, the size of the new buffer otherwise.
|
|
||||||
*/
|
|
||||||
size_t resize_(int width, int height);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Draw a pixel into the buffer.
|
|
||||||
*
|
|
||||||
* This is used by the decoder to fill the buffer that will later be displayed
|
|
||||||
* by the `draw` method. This will internally convert the supplied 32 bit RGBA
|
|
||||||
* color into the requested image storage format.
|
|
||||||
*
|
|
||||||
* @param x Horizontal pixel position.
|
|
||||||
* @param y Vertical pixel position.
|
|
||||||
* @param color 32 bit color to put into the pixel.
|
|
||||||
*/
|
|
||||||
void draw_pixel_(int x, int y, Color color);
|
|
||||||
|
|
||||||
void end_connection_();
|
void end_connection_();
|
||||||
|
|
||||||
CallbackManager<void(bool)> download_finished_callback_{};
|
CallbackManager<void(bool)> download_finished_callback_{};
|
||||||
CallbackManager<void()> download_error_callback_{};
|
CallbackManager<void()> download_error_callback_{};
|
||||||
|
|
||||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
|
||||||
|
|
||||||
uint8_t *buffer_;
|
|
||||||
DownloadBuffer download_buffer_;
|
DownloadBuffer download_buffer_;
|
||||||
/**
|
/**
|
||||||
* This is the *initial* size of the download buffer, not the current size.
|
* This is the *initial* size of the download buffer, not the current size.
|
||||||
@@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent,
|
|||||||
*/
|
*/
|
||||||
size_t download_buffer_initial_size_;
|
size_t download_buffer_initial_size_;
|
||||||
|
|
||||||
const ImageFormat format_;
|
|
||||||
image::Image *placeholder_{nullptr};
|
|
||||||
|
|
||||||
std::string url_{""};
|
std::string url_{""};
|
||||||
|
|
||||||
std::vector<std::pair<std::string, TemplatableValue<std::string> > > request_headers_;
|
std::vector<std::pair<std::string, TemplatableValue<std::string>>> request_headers_;
|
||||||
|
|
||||||
/** width requested on configuration, or 0 if non specified. */
|
|
||||||
const int fixed_width_;
|
|
||||||
/** height requested on configuration, or 0 if non specified. */
|
|
||||||
const int fixed_height_;
|
|
||||||
/**
|
|
||||||
* Whether the image is stored in big-endian format.
|
|
||||||
* This is used to determine how to store 16 bit colors in the buffer.
|
|
||||||
*/
|
|
||||||
bool is_big_endian_;
|
|
||||||
/**
|
|
||||||
* Actual width of the current image. If fixed_width_ is specified,
|
|
||||||
* this will be equal to it; otherwise it will be set once the decoding
|
|
||||||
* starts and the original size is known.
|
|
||||||
* This needs to be separate from "BaseImage::get_width()" because the latter
|
|
||||||
* must return 0 until the image has been decoded (to avoid showing partially
|
|
||||||
* decoded images).
|
|
||||||
*/
|
|
||||||
int buffer_width_;
|
|
||||||
/**
|
|
||||||
* Actual height of the current image. If fixed_height_ is specified,
|
|
||||||
* this will be equal to it; otherwise it will be set once the decoding
|
|
||||||
* starts and the original size is known.
|
|
||||||
* This needs to be separate from "BaseImage::get_height()" because the latter
|
|
||||||
* must return 0 until the image has been decoded (to avoid showing partially
|
|
||||||
* decoded images).
|
|
||||||
*/
|
|
||||||
int buffer_height_;
|
|
||||||
/**
|
/**
|
||||||
* The value of the ETag HTTP header provided in the last response.
|
* The value of the ETag HTTP header provided in the last response.
|
||||||
*/
|
*/
|
||||||
@@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent,
|
|||||||
std::string last_modified_ = "";
|
std::string last_modified_ = "";
|
||||||
|
|
||||||
time_t start_time_;
|
time_t start_time_;
|
||||||
|
|
||||||
friend bool ImageDecoder::set_size(int width, int height);
|
|
||||||
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||||
@@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::online_image
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class PN532 : public PollingComponent {
|
|||||||
|
|
||||||
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
|
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
|
||||||
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||||
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||||
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||||
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
|
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
|
||||||
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
|
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
|
||||||
@@ -88,7 +88,7 @@ class PN532 : public PollingComponent {
|
|||||||
uint16_t read_mifare_ultralight_capacity_();
|
uint16_t read_mifare_ultralight_capacity_();
|
||||||
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||||
uint8_t &message_start_index);
|
uint8_t &message_start_index);
|
||||||
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||||
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
|
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
|
||||||
bool clean_mifare_ultralight_();
|
bool clean_mifare_ultralight_();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "pn532.h"
|
#include "pn532.h"
|
||||||
@@ -106,10 +107,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
||||||
std::vector<uint8_t> blank_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> trailer_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
bool error = false;
|
bool error = false;
|
||||||
|
|
||||||
@@ -118,20 +119,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (block != 0) {
|
if (block != 0) {
|
||||||
if (!this->write_mifare_classic_block_(block, blank_buffer)) {
|
if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) {
|
if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) {
|
if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) {
|
if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block + 3);
|
ESP_LOGE(TAG, "Unable to write block %d", block + 3);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
@@ -141,28 +142,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
|
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
|
||||||
std::vector<uint8_t> empty_ndef_message(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> blank_block(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> block_1_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_2_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_3_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
std::vector<uint8_t> ndef_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) {
|
if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) {
|
||||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!");
|
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(1, block_1_data))
|
if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()))
|
||||||
return false;
|
return false;
|
||||||
if (!this->write_mifare_classic_block_(2, block_2_data))
|
if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()))
|
||||||
return false;
|
return false;
|
||||||
if (!this->write_mifare_classic_block_(3, block_3_trailer))
|
if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Sector 0 formatted to NDEF");
|
ESP_LOGD(TAG, "Sector 0 formatted to NDEF");
|
||||||
@@ -172,36 +173,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (block == 4) {
|
if (block == 4) {
|
||||||
if (!this->write_mifare_classic_block_(block, empty_ndef_message)) {
|
if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this->write_mifare_classic_block_(block, blank_block)) {
|
if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block);
|
ESP_LOGE(TAG, "Unable to write block %d", block);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 1, blank_block)) {
|
if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %d", block + 1);
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 2, blank_block)) {
|
if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %d", block + 2);
|
||||||
}
|
}
|
||||||
if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) {
|
if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) {
|
||||||
ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3);
|
ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||||
std::vector<uint8_t> data({
|
std::vector<uint8_t> cmd({
|
||||||
PN532_COMMAND_INDATAEXCHANGE,
|
PN532_COMMAND_INDATAEXCHANGE,
|
||||||
0x01, // One card
|
0x01, // One card
|
||||||
nfc::MIFARE_CMD_WRITE,
|
nfc::MIFARE_CMD_WRITE,
|
||||||
block_num,
|
block_num,
|
||||||
});
|
});
|
||||||
data.insert(data.end(), write_data.begin(), write_data.end());
|
cmd.insert(cmd.end(), data, data + len);
|
||||||
if (!this->write_command_(data)) {
|
if (!this->write_command_(cmd)) {
|
||||||
ESP_LOGE(TAG, "Error writing block %d", block_num);
|
ESP_LOGE(TAG, "Error writing block %d", block_num);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -243,8 +244,7 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) {
|
||||||
if (!this->write_mifare_classic_block_(current_block, data)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "pn532.h"
|
#include "pn532.h"
|
||||||
@@ -143,8 +144,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *
|
|||||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
while (index < buffer_length) {
|
while (index < buffer_length) {
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) {
|
||||||
if (!this->write_mifare_ultralight_page_(current_page, data)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||||
@@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() {
|
|||||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||||
if (!this->write_mifare_ultralight_page_(i, blank_data)) {
|
if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||||
std::vector<uint8_t> data({
|
std::vector<uint8_t> cmd({
|
||||||
PN532_COMMAND_INDATAEXCHANGE,
|
PN532_COMMAND_INDATAEXCHANGE,
|
||||||
0x01, // One card
|
0x01, // One card
|
||||||
nfc::MIFARE_CMD_WRITE_ULTRALIGHT,
|
nfc::MIFARE_CMD_WRITE_ULTRALIGHT,
|
||||||
page_num,
|
page_num,
|
||||||
});
|
});
|
||||||
data.insert(data.end(), write_data.begin(), write_data.end());
|
cmd.insert(cmd.end(), write_data, write_data + len);
|
||||||
if (!this->write_command_(data)) {
|
if (!this->write_command_(cmd)) {
|
||||||
ESP_LOGE(TAG, "Error writing page %u", page_num);
|
ESP_LOGE(TAG, "Error writing page %u", page_num);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component {
|
|||||||
|
|
||||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||||
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||||
uint8_t sect_to_auth_(uint8_t block_num);
|
uint8_t sect_to_auth_(uint8_t block_num);
|
||||||
uint8_t format_mifare_classic_mifare_();
|
uint8_t format_mifare_classic_mifare_();
|
||||||
@@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component {
|
|||||||
uint16_t read_mifare_ultralight_capacity_();
|
uint16_t read_mifare_ultralight_capacity_();
|
||||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||||
uint8_t &message_start_index);
|
uint8_t &message_start_index);
|
||||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||||
uint8_t clean_mifare_ultralight_();
|
uint8_t clean_mifare_ultralight_();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "pn7150.h"
|
#include "pn7150.h"
|
||||||
@@ -139,10 +140,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7150::format_mifare_classic_mifare_() {
|
uint8_t PN7150::format_mifare_classic_mifare_() {
|
||||||
std::vector<uint8_t> blank_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> trailer_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
auto status = nfc::STATUS_OK;
|
auto status = nfc::STATUS_OK;
|
||||||
|
|
||||||
@@ -151,20 +152,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (block != 0) {
|
if (block != 0) {
|
||||||
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
@@ -174,30 +175,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7150::format_mifare_classic_ndef_() {
|
uint8_t PN7150::format_mifare_classic_ndef_() {
|
||||||
std::vector<uint8_t> empty_ndef_message(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> blank_block(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> block_1_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_2_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_3_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
std::vector<uint8_t> ndef_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,25 +211,26 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
|
|||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (block == 4) {
|
if (block == 4) {
|
||||||
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
|
||||||
|
nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
@@ -236,7 +238,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||||
nfc::NciMessage rx;
|
nfc::NciMessage rx;
|
||||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||||
|
|
||||||
@@ -248,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8
|
|||||||
}
|
}
|
||||||
// write command part two
|
// write command part two
|
||||||
tx.set_payload({XCHG_DATA_OID});
|
tx.set_payload({XCHG_DATA_OID});
|
||||||
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end());
|
tx.get_message().insert(tx.get_message().end(), data, data + len);
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||||
@@ -294,8 +296,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
|
||||||
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) {
|
nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -144,8 +145,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
|
|||||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
while (index < buffer_length) {
|
while (index < buffer_length) {
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
|
||||||
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) {
|
nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||||
@@ -158,19 +159,19 @@ uint8_t PN7150::clean_mifare_ultralight_() {
|
|||||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||||
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nfc::STATUS_OK;
|
return nfc::STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||||
payload.insert(payload.end(), write_data.begin(), write_data.end());
|
payload.insert(payload.end(), write_data, write_data + len);
|
||||||
|
|
||||||
nfc::NciMessage rx;
|
nfc::NciMessage rx;
|
||||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component {
|
|||||||
|
|
||||||
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
|
||||||
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
||||||
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
|
uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
|
||||||
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
|
||||||
uint8_t sect_to_auth_(uint8_t block_num);
|
uint8_t sect_to_auth_(uint8_t block_num);
|
||||||
uint8_t format_mifare_classic_mifare_();
|
uint8_t format_mifare_classic_mifare_();
|
||||||
@@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component {
|
|||||||
uint16_t read_mifare_ultralight_capacity_();
|
uint16_t read_mifare_ultralight_capacity_();
|
||||||
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||||
uint8_t &message_start_index);
|
uint8_t &message_start_index);
|
||||||
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
|
uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
|
||||||
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
|
||||||
uint8_t clean_mifare_ultralight_();
|
uint8_t clean_mifare_ultralight_();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "pn7160.h"
|
#include "pn7160.h"
|
||||||
@@ -139,10 +140,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7160::format_mifare_classic_mifare_() {
|
uint8_t PN7160::format_mifare_classic_mifare_() {
|
||||||
std::vector<uint8_t> blank_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> trailer_buffer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
|
||||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
auto status = nfc::STATUS_OK;
|
auto status = nfc::STATUS_OK;
|
||||||
|
|
||||||
@@ -151,20 +152,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (block != 0) {
|
if (block != 0) {
|
||||||
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 3);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
@@ -174,30 +175,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7160::format_mifare_classic_ndef_() {
|
uint8_t PN7160::format_mifare_classic_ndef_() {
|
||||||
std::vector<uint8_t> empty_ndef_message(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
|
||||||
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> blank_block(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
|
||||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
std::vector<uint8_t> block_1_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
|
||||||
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_2_data(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
|
||||||
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1});
|
0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
|
||||||
std::vector<uint8_t> block_3_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
|
||||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
std::vector<uint8_t> ndef_trailer(
|
static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
|
||||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF});
|
0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||||
|
|
||||||
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,25 +211,26 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
|
|||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (block == 4) {
|
if (block == 4) {
|
||||||
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
|
||||||
|
nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block);
|
ESP_LOGE(TAG, "Unable to write block %u", block);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 1);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
ESP_LOGE(TAG, "Unable to write block %u", block + 2);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) {
|
if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
|
||||||
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
|
||||||
status = nfc::STATUS_FAILED;
|
status = nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
@@ -236,7 +238,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) {
|
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
|
||||||
nfc::NciMessage rx;
|
nfc::NciMessage rx;
|
||||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
|
||||||
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
|
||||||
@@ -248,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8
|
|||||||
}
|
}
|
||||||
// write command part two
|
// write command part two
|
||||||
tx.set_payload({XCHG_DATA_OID});
|
tx.set_payload({XCHG_DATA_OID});
|
||||||
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end());
|
tx.get_message().insert(tx.get_message().end(), data, data + len);
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
|
||||||
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
|
||||||
@@ -294,8 +296,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE);
|
if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
|
||||||
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) {
|
nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#include <array>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -144,8 +145,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
|
|||||||
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
while (index < buffer_length) {
|
while (index < buffer_length) {
|
||||||
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE);
|
if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
|
||||||
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) {
|
nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
|
||||||
@@ -158,19 +159,19 @@ uint8_t PN7160::clean_mifare_ultralight_() {
|
|||||||
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
uint32_t capacity = this->read_mifare_ultralight_capacity_();
|
||||||
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
|
||||||
|
|
||||||
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00};
|
static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
|
||||||
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) {
|
if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
|
||||||
return nfc::STATUS_FAILED;
|
return nfc::STATUS_FAILED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nfc::STATUS_OK;
|
return nfc::STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) {
|
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
|
||||||
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
|
||||||
payload.insert(payload.end(), write_data.begin(), write_data.end());
|
payload.insert(payload.end(), write_data, write_data + len);
|
||||||
|
|
||||||
nfc::NciMessage rx;
|
nfc::NciMessage rx;
|
||||||
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ class RemoteComponentBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
#include <soc/soc_caps.h>
|
||||||
|
#if SOC_RMT_SUPPORTED
|
||||||
class RemoteRMTChannel {
|
class RemoteRMTChannel {
|
||||||
public:
|
public:
|
||||||
void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; }
|
void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; }
|
||||||
@@ -137,7 +139,8 @@ class RemoteRMTChannel {
|
|||||||
uint32_t clock_resolution_{1000000};
|
uint32_t clock_resolution_{1000000};
|
||||||
uint32_t rmt_symbols_;
|
uint32_t rmt_symbols_;
|
||||||
};
|
};
|
||||||
#endif
|
#endif // SOC_RMT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
class RemoteTransmitterBase : public RemoteComponentBase {
|
class RemoteTransmitterBase : public RemoteComponentBase {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
|
|||||||
def validate_config(config):
|
def validate_config(config):
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
variant = esp32.get_esp32_variant()
|
variant = esp32.get_esp32_variant()
|
||||||
|
if variant in esp32_rmt.VARIANTS_NO_RMT:
|
||||||
|
return config
|
||||||
if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2):
|
if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2):
|
||||||
max_idle = 65535
|
max_idle = 65535
|
||||||
else:
|
else:
|
||||||
@@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
|||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
esp32="10000b",
|
esp32="10000b",
|
||||||
|
esp32_c2="1000b",
|
||||||
|
esp32_c61="1000b",
|
||||||
esp8266="1000b",
|
esp8266="1000b",
|
||||||
bk72xx="1000b",
|
bk72xx="1000b",
|
||||||
ln882x="1000b",
|
ln882x="1000b",
|
||||||
@@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
|||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_RMT_SYMBOLS,
|
CONF_RMT_SYMBOLS,
|
||||||
esp32=192,
|
esp32=192,
|
||||||
|
esp32_c2=cv.UNDEFINED,
|
||||||
esp32_c3=96,
|
esp32_c3=96,
|
||||||
esp32_c5=96,
|
esp32_c5=96,
|
||||||
esp32_c6=96,
|
esp32_c6=96,
|
||||||
|
esp32_c61=cv.UNDEFINED,
|
||||||
esp32_h2=96,
|
esp32_h2=96,
|
||||||
esp32_p4=192,
|
esp32_p4=192,
|
||||||
esp32_s2=192,
|
esp32_s2=192,
|
||||||
@@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
|||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_RECEIVE_SYMBOLS,
|
CONF_RECEIVE_SYMBOLS,
|
||||||
esp32=192,
|
esp32=192,
|
||||||
|
esp32_c2=cv.UNDEFINED,
|
||||||
|
esp32_c61=cv.UNDEFINED,
|
||||||
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
|
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
|
||||||
cv.Optional(CONF_USE_DMA): cv.All(
|
cv.Optional(CONF_USE_DMA): cv.All(
|
||||||
esp32.only_on_variant(
|
esp32.only_on_variant(
|
||||||
@@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
|||||||
),
|
),
|
||||||
cv.boolean,
|
cv.boolean,
|
||||||
),
|
),
|
||||||
cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All(
|
cv.SplitDefault(
|
||||||
|
CONF_CARRIER_DUTY_PERCENT,
|
||||||
|
esp32=100,
|
||||||
|
esp32_c2=cv.UNDEFINED,
|
||||||
|
esp32_c61=cv.UNDEFINED,
|
||||||
|
): cv.All(
|
||||||
cv.only_on_esp32,
|
cv.only_on_esp32,
|
||||||
cv.percentage_int,
|
cv.percentage_int,
|
||||||
cv.Range(min=1, max=100),
|
cv.Range(min=1, max=100),
|
||||||
),
|
),
|
||||||
cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All(
|
cv.SplitDefault(
|
||||||
cv.only_on_esp32, cv.frequency, cv.int_
|
CONF_CARRIER_FREQUENCY,
|
||||||
),
|
esp32="0Hz",
|
||||||
|
esp32_c2=cv.UNDEFINED,
|
||||||
|
esp32_c61=cv.UNDEFINED,
|
||||||
|
): cv.All(cv.only_on_esp32, cv.frequency, cv.int_),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
.add_extra(
|
||||||
|
esp32_rmt.validate_rmt_not_supported(
|
||||||
|
[
|
||||||
|
CONF_CLOCK_RESOLUTION,
|
||||||
|
CONF_USE_DMA,
|
||||||
|
CONF_RMT_SYMBOLS,
|
||||||
|
CONF_FILTER_SYMBOLS,
|
||||||
|
CONF_RECEIVE_SYMBOLS,
|
||||||
|
CONF_CARRIER_DUTY_PERCENT,
|
||||||
|
CONF_CARRIER_FREQUENCY,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
.add_extra(validate_config)
|
.add_extra(validate_config)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT:
|
||||||
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
|
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
|
||||||
esp32.include_builtin_idf_component("esp_driver_rmt")
|
esp32.include_builtin_idf_component("esp_driver_rmt")
|
||||||
|
|
||||||
@@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.ESP32_IDF,
|
PlatformFramework.ESP32_IDF,
|
||||||
},
|
},
|
||||||
"remote_receiver.cpp": {
|
"remote_receiver.cpp": {
|
||||||
|
PlatformFramework.ESP32_ARDUINO,
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
PlatformFramework.ESP8266_ARDUINO,
|
PlatformFramework.ESP8266_ARDUINO,
|
||||||
PlatformFramework.BK72XX_ARDUINO,
|
PlatformFramework.BK72XX_ARDUINO,
|
||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040)
|
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||||
|
|
||||||
namespace esphome::remote_receiver {
|
namespace esphome::remote_receiver {
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
#if defined(USE_ESP32)
|
#if defined(USE_ESP32)
|
||||||
|
#include <soc/soc_caps.h>
|
||||||
|
#if SOC_RMT_SUPPORTED
|
||||||
#include <driver/rmt_rx.h>
|
#include <driver/rmt_rx.h>
|
||||||
#endif
|
#endif // SOC_RMT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
namespace esphome::remote_receiver {
|
namespace esphome::remote_receiver {
|
||||||
|
|
||||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040)
|
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||||
struct RemoteReceiverComponentStore {
|
struct RemoteReceiverComponentStore {
|
||||||
static void gpio_intr(RemoteReceiverComponentStore *arg);
|
static void gpio_intr(RemoteReceiverComponentStore *arg);
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore {
|
|||||||
volatile bool prev_level{false};
|
volatile bool prev_level{false};
|
||||||
volatile bool overflow{false};
|
volatile bool overflow{false};
|
||||||
};
|
};
|
||||||
#elif defined(USE_ESP32)
|
#elif defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
struct RemoteReceiverComponentStore {
|
struct RemoteReceiverComponentStore {
|
||||||
/// Stores RMT symbols and rx done event data
|
/// Stores RMT symbols and rx done event data
|
||||||
volatile uint8_t *buffer{nullptr};
|
volatile uint8_t *buffer{nullptr};
|
||||||
@@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore {
|
|||||||
|
|
||||||
class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
|
class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
|
||||||
public Component
|
public Component
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
,
|
,
|
||||||
public remote_base::RemoteRMTChannel
|
public remote_base::RemoteRMTChannel
|
||||||
#endif
|
#endif
|
||||||
@@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; }
|
void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; }
|
||||||
void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; }
|
void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; }
|
||||||
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
||||||
@@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
|
|||||||
void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; }
|
void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
void decode_rmt_(rmt_symbol_word_t *item, size_t item_count);
|
void decode_rmt_(rmt_symbol_word_t *item, size_t item_count);
|
||||||
rmt_channel_handle_t channel_{NULL};
|
rmt_channel_handle_t channel_{NULL};
|
||||||
uint32_t filter_symbols_{0};
|
uint32_t filter_symbols_{0};
|
||||||
@@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
|
|||||||
RemoteReceiverComponentStore store_;
|
RemoteReceiverComponentStore store_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040)
|
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||||
HighFrequencyLoopRequester high_freq_;
|
HighFrequencyLoopRequester high_freq_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
#include <soc/soc_caps.h>
|
||||||
|
#if SOC_RMT_SUPPORTED
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
#include <esp_clk_tree.h>
|
#include <esp_clk_tree.h>
|
||||||
|
|
||||||
@@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c
|
|||||||
|
|
||||||
} // namespace esphome::remote_receiver
|
} // namespace esphome::remote_receiver
|
||||||
|
|
||||||
#endif
|
#endif // SOC_RMT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|||||||
@@ -40,45 +40,66 @@ DigitalWriteAction = remote_transmitter_ns.class_(
|
|||||||
cg.Parented.template(RemoteTransmitterComponent),
|
cg.Parented.template(RemoteTransmitterComponent),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = (
|
||||||
{
|
cv.Schema(
|
||||||
cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent),
|
{
|
||||||
cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
|
cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent),
|
||||||
cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All(
|
cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
|
||||||
cv.percentage_int, cv.Range(min=1, max=100)
|
cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All(
|
||||||
),
|
cv.percentage_int, cv.Range(min=1, max=100)
|
||||||
cv.Optional(CONF_CLOCK_RESOLUTION): cv.All(
|
|
||||||
cv.only_on_esp32,
|
|
||||||
esp32_rmt.validate_clock_resolution(),
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean),
|
|
||||||
cv.Optional(CONF_USE_DMA): cv.All(
|
|
||||||
esp32.only_on_variant(
|
|
||||||
supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
|
|
||||||
),
|
),
|
||||||
cv.boolean,
|
cv.Optional(CONF_CLOCK_RESOLUTION): cv.All(
|
||||||
),
|
cv.only_on_esp32,
|
||||||
cv.SplitDefault(
|
esp32_rmt.validate_clock_resolution(),
|
||||||
CONF_RMT_SYMBOLS,
|
),
|
||||||
esp32=64,
|
cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean),
|
||||||
esp32_c3=48,
|
cv.Optional(CONF_USE_DMA): cv.All(
|
||||||
esp32_c5=48,
|
esp32.only_on_variant(
|
||||||
esp32_c6=48,
|
supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3]
|
||||||
esp32_h2=48,
|
),
|
||||||
esp32_p4=48,
|
cv.boolean,
|
||||||
esp32_s2=64,
|
),
|
||||||
esp32_s3=48,
|
cv.SplitDefault(
|
||||||
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
|
CONF_RMT_SYMBOLS,
|
||||||
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
|
esp32=64,
|
||||||
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
|
esp32_c2=cv.UNDEFINED,
|
||||||
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
|
esp32_c3=48,
|
||||||
}
|
esp32_c5=48,
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
esp32_c6=48,
|
||||||
|
esp32_c61=cv.UNDEFINED,
|
||||||
|
esp32_h2=48,
|
||||||
|
esp32_p4=48,
|
||||||
|
esp32_s2=64,
|
||||||
|
esp32_s3=48,
|
||||||
|
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
|
||||||
|
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
|
||||||
|
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
|
||||||
|
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
.add_extra(
|
||||||
|
esp32_rmt.validate_rmt_not_supported(
|
||||||
|
[
|
||||||
|
CONF_CLOCK_RESOLUTION,
|
||||||
|
CONF_EOT_LEVEL,
|
||||||
|
CONF_USE_DMA,
|
||||||
|
CONF_RMT_SYMBOLS,
|
||||||
|
CONF_NON_BLOCKING,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_non_blocking(config):
|
def _validate_non_blocking(config):
|
||||||
if CORE.is_esp32 and CONF_NON_BLOCKING not in config:
|
if (
|
||||||
|
CORE.is_esp32
|
||||||
|
and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT
|
||||||
|
and CONF_NON_BLOCKING not in config
|
||||||
|
):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
|
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
|
||||||
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
|
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
|
||||||
@@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT:
|
||||||
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
|
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
|
||||||
esp32.include_builtin_idf_component("esp_driver_rmt")
|
esp32.include_builtin_idf_component("esp_driver_rmt")
|
||||||
|
|
||||||
@@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.ESP32_IDF,
|
PlatformFramework.ESP32_IDF,
|
||||||
},
|
},
|
||||||
"remote_transmitter.cpp": {
|
"remote_transmitter.cpp": {
|
||||||
|
PlatformFramework.ESP32_ARDUINO,
|
||||||
|
PlatformFramework.ESP32_IDF,
|
||||||
PlatformFramework.ESP8266_ARDUINO,
|
PlatformFramework.ESP8266_ARDUINO,
|
||||||
PlatformFramework.BK72XX_ARDUINO,
|
PlatformFramework.BK72XX_ARDUINO,
|
||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::remote_transmitter {
|
||||||
namespace remote_transmitter {
|
|
||||||
|
|
||||||
template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> {
|
template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> {
|
||||||
public:
|
public:
|
||||||
@@ -14,5 +13,4 @@ template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public
|
|||||||
void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); }
|
void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace remote_transmitter
|
} // namespace esphome::remote_transmitter
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
|
||||||
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040)
|
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::remote_transmitter {
|
||||||
namespace remote_transmitter {
|
|
||||||
|
|
||||||
static const char *const TAG = "remote_transmitter";
|
static const char *const TAG = "remote_transmitter";
|
||||||
|
|
||||||
@@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
|||||||
this->complete_trigger_.trigger();
|
this->complete_trigger_.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace remote_transmitter
|
} // namespace esphome::remote_transmitter
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#if defined(USE_ESP32)
|
#if defined(USE_ESP32)
|
||||||
|
#include <soc/soc_caps.h>
|
||||||
|
#if SOC_RMT_SUPPORTED
|
||||||
#include <driver/rmt_tx.h>
|
#include <driver/rmt_tx.h>
|
||||||
#endif
|
#endif // SOC_RMT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::remote_transmitter {
|
||||||
namespace remote_transmitter {
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||||
// IDF version 5.5.1 and above is required because of a bug in
|
// IDF version 5.5.1 and above is required because of a bug in
|
||||||
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
|
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
|
||||||
@@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore {
|
|||||||
|
|
||||||
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
||||||
public Component
|
public Component
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
,
|
,
|
||||||
public remote_base::RemoteRMTChannel
|
public remote_base::RemoteRMTChannel
|
||||||
#endif
|
#endif
|
||||||
@@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
|||||||
|
|
||||||
void digital_write(bool value);
|
void digital_write(bool value);
|
||||||
|
|
||||||
#if defined(USE_ESP32)
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
||||||
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
|
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
|
||||||
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
|
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
|
||||||
@@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void send_internal(uint32_t send_times, uint32_t send_wait) override;
|
void send_internal(uint32_t send_times, uint32_t send_wait) override;
|
||||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040)
|
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||||
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
|
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
|
||||||
|
|
||||||
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);
|
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);
|
||||||
@@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
|||||||
uint32_t target_time_;
|
uint32_t target_time_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
|
||||||
void configure_rmt_();
|
void configure_rmt_();
|
||||||
void wait_for_rmt_();
|
void wait_for_rmt_();
|
||||||
|
|
||||||
@@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
|||||||
Trigger<> complete_trigger_;
|
Trigger<> complete_trigger_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace remote_transmitter
|
} // namespace esphome::remote_transmitter
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
#include <soc/soc_caps.h>
|
||||||
|
#if SOC_RMT_SUPPORTED
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::remote_transmitter {
|
||||||
namespace remote_transmitter {
|
|
||||||
|
|
||||||
static const char *const TAG = "remote_transmitter";
|
static const char *const TAG = "remote_transmitter";
|
||||||
|
|
||||||
@@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace remote_transmitter
|
} // namespace esphome::remote_transmitter
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif
|
#endif // SOC_RMT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|||||||
191
esphome/components/runtime_image/__init__.py
Normal file
191
esphome/components/runtime_image/__init__.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import CONF_BYTE_ORDER
|
||||||
|
from esphome.components.image import (
|
||||||
|
IMAGE_TYPE,
|
||||||
|
Image_,
|
||||||
|
validate_settings,
|
||||||
|
validate_transparency,
|
||||||
|
validate_type,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
|
||||||
|
|
||||||
|
AUTO_LOAD = ["image"]
|
||||||
|
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||||
|
|
||||||
|
CONF_PLACEHOLDER = "placeholder"
|
||||||
|
CONF_TRANSPARENCY = "transparency"
|
||||||
|
|
||||||
|
runtime_image_ns = cg.esphome_ns.namespace("runtime_image")
|
||||||
|
|
||||||
|
# Base decoder classes
|
||||||
|
ImageDecoder = runtime_image_ns.class_("ImageDecoder")
|
||||||
|
BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder)
|
||||||
|
JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder)
|
||||||
|
PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder)
|
||||||
|
|
||||||
|
# Runtime image class
|
||||||
|
RuntimeImage = runtime_image_ns.class_(
|
||||||
|
"RuntimeImage", cg.esphome_ns.namespace("image").class_("Image")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Image format enum
|
||||||
|
ImageFormat = runtime_image_ns.enum("ImageFormat")
|
||||||
|
IMAGE_FORMAT_AUTO = ImageFormat.AUTO
|
||||||
|
IMAGE_FORMAT_JPEG = ImageFormat.JPEG
|
||||||
|
IMAGE_FORMAT_PNG = ImageFormat.PNG
|
||||||
|
IMAGE_FORMAT_BMP = ImageFormat.BMP
|
||||||
|
|
||||||
|
# Export enum for decode errors
|
||||||
|
DecodeError = runtime_image_ns.enum("DecodeError")
|
||||||
|
DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE
|
||||||
|
DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT
|
||||||
|
DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY
|
||||||
|
|
||||||
|
|
||||||
|
class Format:
|
||||||
|
"""Base class for image format definitions."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.decoder_class = decoder_class
|
||||||
|
|
||||||
|
def actions(self) -> None:
|
||||||
|
"""Add defines and libraries needed for this format."""
|
||||||
|
|
||||||
|
|
||||||
|
class BMPFormat(Format):
|
||||||
|
"""BMP format decoder configuration."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("BMP", BmpDecoder)
|
||||||
|
|
||||||
|
def actions(self) -> None:
|
||||||
|
cg.add_define("USE_RUNTIME_IMAGE_BMP")
|
||||||
|
|
||||||
|
|
||||||
|
class JPEGFormat(Format):
|
||||||
|
"""JPEG format decoder configuration."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("JPEG", JpegDecoder)
|
||||||
|
|
||||||
|
def actions(self) -> None:
|
||||||
|
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||||
|
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
|
||||||
|
|
||||||
|
|
||||||
|
class PNGFormat(Format):
|
||||||
|
"""PNG format decoder configuration."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("PNG", PngDecoder)
|
||||||
|
|
||||||
|
def actions(self) -> None:
|
||||||
|
cg.add_define("USE_RUNTIME_IMAGE_PNG")
|
||||||
|
cg.add_library("pngle", "1.1.0")
|
||||||
|
|
||||||
|
|
||||||
|
# Registry of available formats
|
||||||
|
IMAGE_FORMATS = {
|
||||||
|
"BMP": BMPFormat(),
|
||||||
|
"JPEG": JPEGFormat(),
|
||||||
|
"PNG": PNGFormat(),
|
||||||
|
"JPG": JPEGFormat(), # Alias for JPEG
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_format(format_name: str) -> Format | None:
|
||||||
|
"""Get a format instance by name."""
|
||||||
|
return IMAGE_FORMATS.get(format_name.upper())
|
||||||
|
|
||||||
|
|
||||||
|
def enable_format(format_name: str) -> Format | None:
|
||||||
|
"""Enable a specific image format by adding its defines and libraries."""
|
||||||
|
format_obj = get_format(format_name)
|
||||||
|
if format_obj:
|
||||||
|
format_obj.actions()
|
||||||
|
return format_obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Runtime image configuration schema base - to be extended by components
|
||||||
|
def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema:
|
||||||
|
"""Create a runtime image schema with the specified image class."""
|
||||||
|
return cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.declare_id(image_class),
|
||||||
|
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
|
||||||
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
|
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
|
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
|
||||||
|
"BIG_ENDIAN", "LITTLE_ENDIAN", upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(),
|
||||||
|
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_runtime_image_settings(config: dict) -> dict:
|
||||||
|
"""Apply validate_settings from image component to runtime image config."""
|
||||||
|
return validate_settings(config)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RuntimeImageSettings:
|
||||||
|
"""Processed runtime image configuration parameters."""
|
||||||
|
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
format_enum: cg.MockObj
|
||||||
|
image_type_enum: cg.MockObj
|
||||||
|
transparent: cg.MockObj
|
||||||
|
byte_order_big_endian: bool
|
||||||
|
placeholder: cg.MockObj | None
|
||||||
|
|
||||||
|
|
||||||
|
async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||||
|
"""
|
||||||
|
Helper function to process common runtime image configuration parameters.
|
||||||
|
Handles format enabling and returns all necessary enums and parameters.
|
||||||
|
"""
|
||||||
|
from esphome.components.image import get_image_type_enum, get_transparency_enum
|
||||||
|
|
||||||
|
# Get resize dimensions with default (0, 0)
|
||||||
|
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||||
|
|
||||||
|
# Handle format (required for runtime images)
|
||||||
|
format_name = config[CONF_FORMAT]
|
||||||
|
# Enable the format in the runtime_image component
|
||||||
|
enable_format(format_name)
|
||||||
|
# Map format names to enum values (handle JPG as alias for JPEG)
|
||||||
|
if format_name.upper() == "JPG":
|
||||||
|
format_name = "JPEG"
|
||||||
|
format_enum = getattr(ImageFormat, format_name.upper())
|
||||||
|
|
||||||
|
# Get image type enum
|
||||||
|
image_type_enum = get_image_type_enum(config[CONF_TYPE])
|
||||||
|
|
||||||
|
# Get transparency enum
|
||||||
|
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
|
||||||
|
|
||||||
|
# Get byte order (True for big endian, False for little endian)
|
||||||
|
byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN"
|
||||||
|
|
||||||
|
# Get placeholder if specified
|
||||||
|
placeholder = None
|
||||||
|
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
||||||
|
placeholder = await cg.get_variable(placeholder_id)
|
||||||
|
|
||||||
|
return RuntimeImageSettings(
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
format_enum=format_enum,
|
||||||
|
image_type_enum=image_type_enum,
|
||||||
|
transparent=transparent,
|
||||||
|
byte_order_big_endian=byte_order_big_endian,
|
||||||
|
placeholder=placeholder,
|
||||||
|
)
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
#include "bmp_image.h"
|
#include "bmp_decoder.h"
|
||||||
|
|
||||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||||
|
|
||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
static const char *const TAG = "online_image.bmp";
|
static const char *const TAG = "image_decoder.bmp";
|
||||||
|
|
||||||
int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||||
size_t index = 0;
|
size_t index = 0;
|
||||||
@@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
return DECODE_ERROR_INVALID_TYPE;
|
return DECODE_ERROR_INVALID_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
|
// BMP file contains its own size in the header
|
||||||
|
size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
|
||||||
|
if (this->expected_size_ == 0) {
|
||||||
|
this->expected_size_ = file_size; // Use file header size if not provided
|
||||||
|
}
|
||||||
this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]);
|
this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]);
|
||||||
|
|
||||||
this->current_index_ = 14;
|
this->current_index_ = 14;
|
||||||
@@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
while (index < size) {
|
while (index < size) {
|
||||||
uint8_t current_byte = buffer[index];
|
uint8_t current_byte = buffer[index];
|
||||||
for (uint8_t i = 0; i < 8; i++) {
|
for (uint8_t i = 0; i < 8; i++) {
|
||||||
size_t x = (this->paint_index_ % this->width_) + i;
|
size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i;
|
||||||
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_);
|
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
|
||||||
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
|
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
|
||||||
this->draw(x, y, 1, 1, c);
|
this->draw(x, y, 1, 1, c);
|
||||||
}
|
}
|
||||||
@@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
uint8_t b = buffer[index];
|
uint8_t b = buffer[index];
|
||||||
uint8_t g = buffer[index + 1];
|
uint8_t g = buffer[index + 1];
|
||||||
uint8_t r = buffer[index + 2];
|
uint8_t r = buffer[index + 2];
|
||||||
size_t x = this->paint_index_ % this->width_;
|
size_t x = this->paint_index_ % static_cast<size_t>(this->width_);
|
||||||
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_);
|
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
|
||||||
Color c = Color(r, g, b);
|
Color c = Color(r, g, b);
|
||||||
this->draw(x, y, 1, 1, c);
|
this->draw(x, y, 1, 1, c);
|
||||||
this->paint_index_++;
|
this->paint_index_++;
|
||||||
@@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
return size;
|
return size;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_BMP
|
||||||
@@ -1,27 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||||
|
|
||||||
#include "image_decoder.h"
|
#include "image_decoder.h"
|
||||||
|
#include "runtime_image.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Image decoder specialization for PNG images.
|
* @brief Image decoder specialization for BMP images.
|
||||||
*/
|
*/
|
||||||
class BmpDecoder : public ImageDecoder {
|
class BmpDecoder : public ImageDecoder {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Construct a new BMP Decoder object.
|
* @brief Construct a new BMP Decoder object.
|
||||||
*
|
*
|
||||||
* @param display The image to decode the stream into.
|
* @param image The RuntimeImage to decode the stream into.
|
||||||
*/
|
*/
|
||||||
BmpDecoder(OnlineImage *image) : ImageDecoder(image) {}
|
BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {}
|
||||||
|
|
||||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||||
|
|
||||||
|
bool is_finished() const override {
|
||||||
|
// BMP is finished when we've decoded all pixel data
|
||||||
|
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
size_t current_index_{0};
|
size_t current_index_{0};
|
||||||
size_t paint_index_{0};
|
size_t paint_index_{0};
|
||||||
@@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder {
|
|||||||
uint8_t padding_bytes_{0};
|
uint8_t padding_bytes_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_BMP
|
||||||
28
esphome/components/runtime_image/image_decoder.cpp
Normal file
28
esphome/components/runtime_image/image_decoder.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include "image_decoder.h"
|
||||||
|
#include "runtime_image.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace esphome::runtime_image {
|
||||||
|
|
||||||
|
static const char *const TAG = "image_decoder";
|
||||||
|
|
||||||
|
bool ImageDecoder::set_size(int width, int height) {
|
||||||
|
bool success = this->image_->resize(width, height) > 0;
|
||||||
|
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||||
|
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||||
|
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||||
|
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||||
|
for (int i = x * this->x_scale_; i < width; i++) {
|
||||||
|
for (int j = y * this->y_scale_; j < height; j++) {
|
||||||
|
this->image_->draw_pixel(i, j, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::runtime_image
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "esphome/core/color.h"
|
#include "esphome/core/color.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
enum DecodeError : int {
|
enum DecodeError : int {
|
||||||
DECODE_ERROR_INVALID_TYPE = -1,
|
DECODE_ERROR_INVALID_TYPE = -1,
|
||||||
@@ -10,7 +9,7 @@ enum DecodeError : int {
|
|||||||
DECODE_ERROR_OUT_OF_MEMORY = -3,
|
DECODE_ERROR_OUT_OF_MEMORY = -3,
|
||||||
};
|
};
|
||||||
|
|
||||||
class OnlineImage;
|
class RuntimeImage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Class to abstract decoding different image formats.
|
* @brief Class to abstract decoding different image formats.
|
||||||
@@ -20,19 +19,19 @@ class ImageDecoder {
|
|||||||
/**
|
/**
|
||||||
* @brief Construct a new Image Decoder object
|
* @brief Construct a new Image Decoder object
|
||||||
*
|
*
|
||||||
* @param image The image to decode the stream into.
|
* @param image The RuntimeImage to decode the stream into.
|
||||||
*/
|
*/
|
||||||
ImageDecoder(OnlineImage *image) : image_(image) {}
|
ImageDecoder(RuntimeImage *image) : image_(image) {}
|
||||||
virtual ~ImageDecoder() = default;
|
virtual ~ImageDecoder() = default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initialize the decoder.
|
* @brief Initialize the decoder.
|
||||||
*
|
*
|
||||||
* @param download_size The total number of bytes that need to be downloaded for the image.
|
* @param expected_size Hint about the expected data size (0 if unknown).
|
||||||
* @return int Returns 0 on success, a {@see DecodeError} value in case of an error.
|
* @return int Returns 0 on success, a {@see DecodeError} value in case of an error.
|
||||||
*/
|
*/
|
||||||
virtual int prepare(size_t download_size) {
|
virtual int prepare(size_t expected_size) {
|
||||||
this->download_size_ = download_size;
|
this->expected_size_ = expected_size;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,49 +72,26 @@ class ImageDecoder {
|
|||||||
*/
|
*/
|
||||||
void draw(int x, int y, int w, int h, const Color &color);
|
void draw(int x, int y, int w, int h, const Color &color);
|
||||||
|
|
||||||
bool is_finished() const { return this->decoded_bytes_ == this->download_size_; }
|
/**
|
||||||
|
* @brief Check if the decoder has finished processing.
|
||||||
|
*
|
||||||
|
* This should be overridden by decoders that can detect completion
|
||||||
|
* based on format-specific markers rather than byte counts.
|
||||||
|
*/
|
||||||
|
virtual bool is_finished() const {
|
||||||
|
if (this->expected_size_ > 0) {
|
||||||
|
return this->decoded_bytes_ >= this->expected_size_;
|
||||||
|
}
|
||||||
|
// If size is unknown, derived classes should override this
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
OnlineImage *image_;
|
RuntimeImage *image_;
|
||||||
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
|
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||||
// Will be overwritten anyway once the download size is known.
|
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||||
size_t download_size_ = 1;
|
|
||||||
size_t decoded_bytes_ = 0;
|
|
||||||
double x_scale_ = 1.0;
|
double x_scale_ = 1.0;
|
||||||
double y_scale_ = 1.0;
|
double y_scale_ = 1.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DownloadBuffer {
|
} // namespace esphome::runtime_image
|
||||||
public:
|
|
||||||
DownloadBuffer(size_t size);
|
|
||||||
|
|
||||||
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
|
||||||
|
|
||||||
uint8_t *data(size_t offset = 0);
|
|
||||||
|
|
||||||
uint8_t *append() { return this->data(this->unread_); }
|
|
||||||
|
|
||||||
size_t unread() const { return this->unread_; }
|
|
||||||
size_t size() const { return this->size_; }
|
|
||||||
size_t free_capacity() const { return this->size_ - this->unread_; }
|
|
||||||
|
|
||||||
size_t read(size_t len);
|
|
||||||
size_t write(size_t len) {
|
|
||||||
this->unread_ += len;
|
|
||||||
return this->unread_;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() { this->unread_ = 0; }
|
|
||||||
|
|
||||||
size_t resize(size_t size);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
RAMAllocator<uint8_t> allocator_{};
|
|
||||||
uint8_t *buffer_;
|
|
||||||
size_t size_;
|
|
||||||
/** Total number of downloaded bytes not yet read. */
|
|
||||||
size_t unread_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace online_image
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
#include "jpeg_image.h"
|
#include "jpeg_decoder.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||||
|
|
||||||
#include "esphome/components/display/display_buffer.h"
|
#include "esphome/components/display/display_buffer.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include "online_image.h"
|
#ifdef USE_ESP_IDF
|
||||||
static const char *const TAG = "online_image.jpeg";
|
#include "esp_task_wdt.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
static const char *const TAG = "image_decoder.jpeg";
|
||||||
namespace online_image {
|
|
||||||
|
namespace esphome::runtime_image {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
|
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
|
||||||
@@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
|||||||
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
|
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
|
||||||
|
|
||||||
// Some very big images take too long to decode, so feed the watchdog on each callback
|
// Some very big images take too long to decode, so feed the watchdog on each callback
|
||||||
// to avoid crashing.
|
// to avoid crashing if the executing task has a watchdog enabled.
|
||||||
App.feed_wdt();
|
#ifdef USE_ESP_IDF
|
||||||
|
if (esp_task_wdt_status(nullptr) == ESP_OK) {
|
||||||
|
#endif
|
||||||
|
App.feed_wdt();
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
}
|
||||||
|
#endif
|
||||||
size_t position = 0;
|
size_t position = 0;
|
||||||
size_t height = static_cast<size_t>(jpeg->iHeight);
|
size_t height = static_cast<size_t>(jpeg->iHeight);
|
||||||
size_t width = static_cast<size_t>(jpeg->iWidth);
|
size_t width = static_cast<size_t>(jpeg->iWidth);
|
||||||
@@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int JpegDecoder::prepare(size_t download_size) {
|
int JpegDecoder::prepare(size_t expected_size) {
|
||||||
ImageDecoder::prepare(download_size);
|
ImageDecoder::prepare(expected_size);
|
||||||
auto size = this->image_->resize_download_buffer(download_size);
|
// JPEG decoder needs complete data before decoding
|
||||||
if (size < download_size) {
|
|
||||||
ESP_LOGE(TAG, "Download buffer resize failed!");
|
|
||||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||||
if (size < this->download_size_) {
|
// JPEG decoder requires complete data
|
||||||
ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_);
|
// If we know the expected size, wait for it
|
||||||
|
if (this->expected_size_ > 0 && size < this->expected_size_) {
|
||||||
|
ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If size unknown, try to decode and see if it's valid
|
||||||
|
// The JPEGDEC library will fail gracefully if data is incomplete
|
||||||
|
|
||||||
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
|
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
|
||||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||||
return DECODE_ERROR_INVALID_TYPE;
|
return DECODE_ERROR_INVALID_TYPE;
|
||||||
@@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "image_decoder.h"
|
#include "image_decoder.h"
|
||||||
|
#include "runtime_image.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||||
#include <JPEGDEC.h>
|
#include <JPEGDEC.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Image decoder specialization for JPEG images.
|
* @brief Image decoder specialization for JPEG images.
|
||||||
@@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder {
|
|||||||
/**
|
/**
|
||||||
* @brief Construct a new JPEG Decoder object.
|
* @brief Construct a new JPEG Decoder object.
|
||||||
*
|
*
|
||||||
* @param display The image to decode the stream into.
|
* @param image The RuntimeImage to decode the stream into.
|
||||||
*/
|
*/
|
||||||
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {}
|
JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {}
|
||||||
~JpegDecoder() override {}
|
~JpegDecoder() override {}
|
||||||
|
|
||||||
int prepare(size_t download_size) override;
|
int prepare(size_t expected_size) override;
|
||||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
JPEGDEC jpeg_{};
|
JPEGDEC jpeg_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
#include "png_image.h"
|
#include "png_decoder.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||||
|
|
||||||
#include "esphome/components/display/display_buffer.h"
|
#include "esphome/components/display/display_buffer.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
static const char *const TAG = "online_image.png";
|
static const char *const TAG = "image_decoder.png";
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||||
@@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
|
PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) {
|
||||||
{
|
{
|
||||||
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
|
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
|
||||||
if (!pngle) {
|
if (!pngle) {
|
||||||
@@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int PngDecoder::prepare(size_t download_size) {
|
int PngDecoder::prepare(size_t expected_size) {
|
||||||
ImageDecoder::prepare(download_size);
|
ImageDecoder::prepare(expected_size);
|
||||||
if (!this->pngle_) {
|
if (!this->pngle_) {
|
||||||
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
||||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||||
@@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
|
||||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||||
}
|
}
|
||||||
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
|
// PNG can be decoded progressively, but wait for a reasonable chunk
|
||||||
ESP_LOGD(TAG, "Waiting for data");
|
if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) {
|
||||||
|
ESP_LOGD(TAG, "Waiting for more data");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||||
@@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
|||||||
return fed;
|
return fed;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_PNG
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "image_decoder.h"
|
#include "image_decoder.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
#include "runtime_image.h"
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||||
#include <pngle.h>
|
#include <pngle.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::runtime_image {
|
||||||
namespace online_image {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Image decoder specialization for PNG images.
|
* @brief Image decoder specialization for PNG images.
|
||||||
@@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder {
|
|||||||
/**
|
/**
|
||||||
* @brief Construct a new PNG Decoder object.
|
* @brief Construct a new PNG Decoder object.
|
||||||
*
|
*
|
||||||
* @param display The image to decode the stream into.
|
* @param image The RuntimeImage to decode the stream into.
|
||||||
*/
|
*/
|
||||||
PngDecoder(OnlineImage *image);
|
PngDecoder(RuntimeImage *image);
|
||||||
~PngDecoder() override;
|
~PngDecoder() override;
|
||||||
|
|
||||||
int prepare(size_t download_size) override;
|
int prepare(size_t expected_size) override;
|
||||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||||
|
|
||||||
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
|
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
|
||||||
@@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
RAMAllocator<pngle_t> allocator_;
|
RAMAllocator<pngle_t> allocator_;
|
||||||
pngle_t *pngle_;
|
pngle_t *pngle_{nullptr};
|
||||||
uint32_t pixels_decoded_{0};
|
uint32_t pixels_decoded_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace online_image
|
} // namespace esphome::runtime_image
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
#endif // USE_RUNTIME_IMAGE_PNG
|
||||||
300
esphome/components/runtime_image/runtime_image.cpp
Normal file
300
esphome/components/runtime_image/runtime_image.cpp
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#include "runtime_image.h"
|
||||||
|
#include "image_decoder.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||||
|
#include "bmp_decoder.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||||
|
#include "jpeg_decoder.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||||
|
#include "png_decoder.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome::runtime_image {
|
||||||
|
|
||||||
|
static const char *const TAG = "runtime_image";
|
||||||
|
|
||||||
|
inline bool is_color_on(const Color &color) {
|
||||||
|
// This produces the most accurate monochrome conversion, but is slightly slower.
|
||||||
|
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
||||||
|
|
||||||
|
// Approximation using fast integer computations; produces acceptable results
|
||||||
|
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
||||||
|
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||||
|
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
|
||||||
|
: Image(nullptr, 0, 0, type, transparency),
|
||||||
|
format_(format),
|
||||||
|
fixed_width_(fixed_width),
|
||||||
|
fixed_height_(fixed_height),
|
||||||
|
placeholder_(placeholder),
|
||||||
|
is_big_endian_(is_big_endian) {}
|
||||||
|
|
||||||
|
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||||
|
|
||||||
|
int RuntimeImage::resize(int width, int height) {
|
||||||
|
// Use fixed dimensions if specified (0 means auto-resize)
|
||||||
|
int target_width = this->fixed_width_ ? this->fixed_width_ : width;
|
||||||
|
int target_height = this->fixed_height_ ? this->fixed_height_ : height;
|
||||||
|
|
||||||
|
size_t result = this->resize_buffer_(target_width, target_height);
|
||||||
|
if (result > 0 && this->progressive_display_) {
|
||||||
|
// Update display dimensions for progressive display
|
||||||
|
this->width_ = this->buffer_width_;
|
||||||
|
this->height_ = this->buffer_height_;
|
||||||
|
this->data_start_ = this->buffer_;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||||
|
if (!this->buffer_) {
|
||||||
|
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
||||||
|
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this->type_) {
|
||||||
|
case image::IMAGE_TYPE_BINARY: {
|
||||||
|
const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u;
|
||||||
|
uint32_t pos = x + y * width_8;
|
||||||
|
auto bitno = 0x80 >> (pos % 8u);
|
||||||
|
pos /= 8u;
|
||||||
|
auto on = is_color_on(color);
|
||||||
|
if (this->has_transparency() && color.w < 0x80)
|
||||||
|
on = false;
|
||||||
|
if (on) {
|
||||||
|
this->buffer_[pos] |= bitno;
|
||||||
|
} else {
|
||||||
|
this->buffer_[pos] &= ~bitno;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case image::IMAGE_TYPE_GRAYSCALE: {
|
||||||
|
uint32_t pos = this->get_position_(x, y);
|
||||||
|
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||||
|
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||||
|
if (gray == 1) {
|
||||||
|
gray = 0;
|
||||||
|
}
|
||||||
|
if (color.w < 0x80) {
|
||||||
|
gray = 1;
|
||||||
|
}
|
||||||
|
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
|
if (color.w != 0xFF)
|
||||||
|
gray = color.w;
|
||||||
|
}
|
||||||
|
this->buffer_[pos] = gray;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case image::IMAGE_TYPE_RGB565: {
|
||||||
|
uint32_t pos = this->get_position_(x, y);
|
||||||
|
Color mapped_color = color;
|
||||||
|
this->map_chroma_key(mapped_color);
|
||||||
|
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
|
||||||
|
if (this->is_big_endian_) {
|
||||||
|
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||||
|
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||||
|
} else {
|
||||||
|
this->buffer_[pos + 0] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||||
|
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
|
this->buffer_[pos + 2] = color.w;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case image::IMAGE_TYPE_RGB: {
|
||||||
|
uint32_t pos = this->get_position_(x, y);
|
||||||
|
Color mapped_color = color;
|
||||||
|
this->map_chroma_key(mapped_color);
|
||||||
|
this->buffer_[pos + 0] = mapped_color.r;
|
||||||
|
this->buffer_[pos + 1] = mapped_color.g;
|
||||||
|
this->buffer_[pos + 2] = mapped_color.b;
|
||||||
|
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
|
this->buffer_[pos + 3] = color.w;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeImage::map_chroma_key(Color &color) {
|
||||||
|
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||||
|
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||||
|
color.g = 0;
|
||||||
|
}
|
||||||
|
if (color.w < 0x80) {
|
||||||
|
color.r = 0;
|
||||||
|
color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1;
|
||||||
|
color.b = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||||
|
if (this->data_start_) {
|
||||||
|
// If we have a complete image, use the base class draw method
|
||||||
|
Image::draw(x, y, display, color_on, color_off);
|
||||||
|
} else if (this->placeholder_) {
|
||||||
|
// Show placeholder while the runtime image is not available
|
||||||
|
this->placeholder_->draw(x, y, display, color_on, color_off);
|
||||||
|
}
|
||||||
|
// If no image is loaded and no placeholder, nothing to draw
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeImage::begin_decode(size_t expected_size) {
|
||||||
|
if (this->decoder_) {
|
||||||
|
ESP_LOGW(TAG, "Decoding already in progress");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->decoder_ = this->create_decoder_();
|
||||||
|
if (!this->decoder_) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->total_size_ = expected_size;
|
||||||
|
this->decoded_bytes_ = 0;
|
||||||
|
|
||||||
|
// Initialize decoder
|
||||||
|
int result = this->decoder_->prepare(expected_size);
|
||||||
|
if (result < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to prepare decoder: %d", result);
|
||||||
|
this->decoder_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RuntimeImage::feed_data(uint8_t *data, size_t len) {
|
||||||
|
if (!this->decoder_) {
|
||||||
|
ESP_LOGE(TAG, "No decoder initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int consumed = this->decoder_->decode(data, len);
|
||||||
|
if (consumed > 0) {
|
||||||
|
this->decoded_bytes_ += consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeImage::end_decode() {
|
||||||
|
if (!this->decoder_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the image for display
|
||||||
|
if (!this->progressive_display_) {
|
||||||
|
// Only now make the image visible
|
||||||
|
this->width_ = this->buffer_width_;
|
||||||
|
this->height_ = this->buffer_height_;
|
||||||
|
this->data_start_ = this->buffer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up decoder
|
||||||
|
this->decoder_ = nullptr;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeImage::is_decode_finished() const {
|
||||||
|
if (!this->decoder_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this->decoder_->is_finished();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeImage::release() {
|
||||||
|
this->release_buffer_();
|
||||||
|
// Reset decoder separately — release() can be called from within the decoder
|
||||||
|
// (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here.
|
||||||
|
// The decoder lifecycle is managed by begin_decode()/end_decode().
|
||||||
|
this->decoder_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeImage::release_buffer_() {
|
||||||
|
if (this->buffer_) {
|
||||||
|
ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
|
||||||
|
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
|
||||||
|
this->buffer_ = nullptr;
|
||||||
|
this->data_start_ = nullptr;
|
||||||
|
this->width_ = 0;
|
||||||
|
this->height_ = 0;
|
||||||
|
this->buffer_width_ = 0;
|
||||||
|
this->buffer_height_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t RuntimeImage::resize_buffer_(int width, int height) {
|
||||||
|
size_t new_size = this->get_buffer_size_(width, height);
|
||||||
|
|
||||||
|
if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
|
||||||
|
// Buffer already allocated with correct size
|
||||||
|
return new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release old buffer if dimensions changed
|
||||||
|
if (this->buffer_) {
|
||||||
|
this->release_buffer_();
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size);
|
||||||
|
this->buffer_ = this->allocator_.allocate(new_size);
|
||||||
|
|
||||||
|
if (!this->buffer_) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size,
|
||||||
|
this->allocator_.get_max_free_block_size());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear buffer
|
||||||
|
memset(this->buffer_, 0, new_size);
|
||||||
|
|
||||||
|
this->buffer_width_ = width;
|
||||||
|
this->buffer_height_ = height;
|
||||||
|
|
||||||
|
return new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
|
||||||
|
return (this->get_bpp() * width + 7u) / 8u * height;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
|
||||||
|
|
||||||
|
std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_() {
|
||||||
|
switch (this->format_) {
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||||
|
case BMP:
|
||||||
|
return make_unique<BmpDecoder>(this);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||||
|
case JPEG:
|
||||||
|
return make_unique<JpegDecoder>(this);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_RUNTIME_IMAGE_PNG
|
||||||
|
case PNG:
|
||||||
|
return make_unique<PngDecoder>(this);
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "Unsupported image format: %d", this->format_);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::runtime_image
|
||||||
214
esphome/components/runtime_image/runtime_image.h
Normal file
214
esphome/components/runtime_image/runtime_image.h
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/image/image.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome::runtime_image {
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
class ImageDecoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Image format types that can be decoded dynamically.
|
||||||
|
*/
|
||||||
|
enum ImageFormat {
|
||||||
|
/** Automatically detect from data. Not implemented yet. */
|
||||||
|
AUTO,
|
||||||
|
/** JPEG format. */
|
||||||
|
JPEG,
|
||||||
|
/** PNG format. */
|
||||||
|
PNG,
|
||||||
|
/** BMP format. */
|
||||||
|
BMP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A dynamic image that can be loaded and decoded at runtime.
|
||||||
|
*
|
||||||
|
* This class provides dynamic buffer allocation and management for images
|
||||||
|
* that are decoded at runtime, as opposed to static images compiled into
|
||||||
|
* the firmware. It serves as a base class for components that need to
|
||||||
|
* load images dynamically from various sources.
|
||||||
|
*/
|
||||||
|
class RuntimeImage : public image::Image {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a new RuntimeImage object.
|
||||||
|
*
|
||||||
|
* @param format The image format to decode.
|
||||||
|
* @param type The pixel format for the image.
|
||||||
|
* @param transparency The transparency type for the image.
|
||||||
|
* @param placeholder Optional placeholder image to show while loading.
|
||||||
|
* @param is_big_endian Whether the image is stored in big-endian format.
|
||||||
|
* @param fixed_width Fixed width for the image (0 for auto-resize).
|
||||||
|
* @param fixed_height Fixed height for the image (0 for auto-resize).
|
||||||
|
*/
|
||||||
|
RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||||
|
image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0,
|
||||||
|
int fixed_height = 0);
|
||||||
|
|
||||||
|
~RuntimeImage();
|
||||||
|
|
||||||
|
// Decoder interface methods
|
||||||
|
/**
|
||||||
|
* @brief Resize the image buffer to the requested dimensions.
|
||||||
|
*
|
||||||
|
* The buffer will be allocated if not existing.
|
||||||
|
* If fixed dimensions have been specified in the constructor, the buffer will be created
|
||||||
|
* with those dimensions and not resized, even on request.
|
||||||
|
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
|
||||||
|
* dimensions allocated.
|
||||||
|
*
|
||||||
|
* @param width Requested width (ignored if fixed_width_ is set)
|
||||||
|
* @param height Requested height (ignored if fixed_height_ is set)
|
||||||
|
* @return Size of the allocated buffer in bytes, or 0 if allocation failed.
|
||||||
|
*/
|
||||||
|
int resize(int width, int height);
|
||||||
|
void draw_pixel(int x, int y, const Color &color);
|
||||||
|
void map_chroma_key(Color &color);
|
||||||
|
int get_buffer_width() const { return this->buffer_width_; }
|
||||||
|
int get_buffer_height() const { return this->buffer_height_; }
|
||||||
|
|
||||||
|
// Image drawing interface
|
||||||
|
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Begin decoding an image.
|
||||||
|
*
|
||||||
|
* @param expected_size Optional hint about the expected data size.
|
||||||
|
* @return true if decoder was successfully initialized.
|
||||||
|
*/
|
||||||
|
bool begin_decode(size_t expected_size = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Feed data to the decoder.
|
||||||
|
*
|
||||||
|
* @param data Pointer to the data buffer.
|
||||||
|
* @param len Length of data to process.
|
||||||
|
* @return Number of bytes consumed by the decoder.
|
||||||
|
*/
|
||||||
|
int feed_data(uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Complete the decoding process.
|
||||||
|
*
|
||||||
|
* @return true if decoding completed successfully.
|
||||||
|
*/
|
||||||
|
bool end_decode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if decoding is currently in progress.
|
||||||
|
*/
|
||||||
|
bool is_decoding() const { return this->decoder_ != nullptr; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if the decoder has finished processing all data.
|
||||||
|
*
|
||||||
|
* This delegates to the decoder's format-specific completion check,
|
||||||
|
* which handles both known-size and chunked transfer cases.
|
||||||
|
*/
|
||||||
|
bool is_decode_finished() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if an image is currently loaded.
|
||||||
|
*/
|
||||||
|
bool is_loaded() const { return this->buffer_ != nullptr; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the image format.
|
||||||
|
*/
|
||||||
|
ImageFormat get_format() const { return this->format_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release the image buffer and free memory.
|
||||||
|
*/
|
||||||
|
void release();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set whether to allow progressive display during decode.
|
||||||
|
*
|
||||||
|
* When enabled, the image can be displayed even while still decoding.
|
||||||
|
* When disabled, the image is only displayed after decoding completes.
|
||||||
|
*/
|
||||||
|
void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/**
|
||||||
|
* @brief Resize the image buffer to the requested dimensions.
|
||||||
|
*
|
||||||
|
* @param width New width in pixels.
|
||||||
|
* @param height New height in pixels.
|
||||||
|
* @return Size of the allocated buffer, or 0 on failure.
|
||||||
|
*/
|
||||||
|
size_t resize_buffer_(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release only the image buffer without resetting the decoder.
|
||||||
|
*
|
||||||
|
* This is safe to call from within the decoder (e.g., during resize).
|
||||||
|
*/
|
||||||
|
void release_buffer_();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the buffer size in bytes for given dimensions.
|
||||||
|
*/
|
||||||
|
size_t get_buffer_size_(int width, int height) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the position in the buffer for a pixel.
|
||||||
|
*/
|
||||||
|
int get_position_(int x, int y) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create decoder instance for the image's format.
|
||||||
|
*/
|
||||||
|
std::unique_ptr<ImageDecoder> create_decoder_();
|
||||||
|
|
||||||
|
// Memory management
|
||||||
|
RAMAllocator<uint8_t> allocator_{};
|
||||||
|
uint8_t *buffer_{nullptr};
|
||||||
|
|
||||||
|
// Decoder management
|
||||||
|
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||||
|
/** The image format this RuntimeImage is configured to decode. */
|
||||||
|
const ImageFormat format_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actual width of the current image.
|
||||||
|
* This needs to be separate from "Image::get_width()" because the latter
|
||||||
|
* must return 0 until the image has been decoded (to avoid showing partially
|
||||||
|
* decoded images). When progressive_display_ is enabled, Image dimensions
|
||||||
|
* are updated during decoding to allow rendering in progress.
|
||||||
|
*/
|
||||||
|
int buffer_width_{0};
|
||||||
|
/**
|
||||||
|
* Actual height of the current image.
|
||||||
|
* This needs to be separate from "Image::get_height()" because the latter
|
||||||
|
* must return 0 until the image has been decoded (to avoid showing partially
|
||||||
|
* decoded images). When progressive_display_ is enabled, Image dimensions
|
||||||
|
* are updated during decoding to allow rendering in progress.
|
||||||
|
*/
|
||||||
|
int buffer_height_{0};
|
||||||
|
|
||||||
|
// Decoding state
|
||||||
|
size_t total_size_{0};
|
||||||
|
size_t decoded_bytes_{0};
|
||||||
|
|
||||||
|
/** Fixed width requested on configuration, or 0 if not specified. */
|
||||||
|
const int fixed_width_{0};
|
||||||
|
/** Fixed height requested on configuration, or 0 if not specified. */
|
||||||
|
const int fixed_height_{0};
|
||||||
|
|
||||||
|
/** Placeholder image to show when the runtime image is not available. */
|
||||||
|
image::Image *placeholder_{nullptr};
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
bool progressive_display_{false};
|
||||||
|
/**
|
||||||
|
* Whether the image is stored in big-endian format.
|
||||||
|
* This is used to determine how to store 16 bit colors in the buffer.
|
||||||
|
*/
|
||||||
|
bool is_big_endian_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::runtime_image
|
||||||
@@ -7,15 +7,15 @@ namespace speed {
|
|||||||
static const char *const TAG = "speed.fan";
|
static const char *const TAG = "speed.fan";
|
||||||
|
|
||||||
void SpeedFan::setup() {
|
void SpeedFan::setup() {
|
||||||
|
// Construct traits before restore so preset modes can be looked up by index
|
||||||
|
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_);
|
||||||
|
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
||||||
|
|
||||||
auto restore = this->restore_state_();
|
auto restore = this->restore_state_();
|
||||||
if (restore.has_value()) {
|
if (restore.has_value()) {
|
||||||
restore->apply(*this);
|
restore->apply(*this);
|
||||||
this->write_state_();
|
this->write_state_();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct traits
|
|
||||||
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_);
|
|
||||||
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); }
|
void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); }
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ namespace esphome::template_ {
|
|||||||
static const char *const TAG = "template.fan";
|
static const char *const TAG = "template.fan";
|
||||||
|
|
||||||
void TemplateFan::setup() {
|
void TemplateFan::setup() {
|
||||||
|
// Construct traits before restore so preset modes can be looked up by index
|
||||||
|
this->traits_ =
|
||||||
|
fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_);
|
||||||
|
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
||||||
|
|
||||||
auto restore = this->restore_state_();
|
auto restore = this->restore_state_();
|
||||||
if (restore.has_value()) {
|
if (restore.has_value()) {
|
||||||
restore->apply(*this);
|
restore->apply(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct traits
|
|
||||||
this->traits_ =
|
|
||||||
fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_);
|
|
||||||
this->traits_.set_supported_preset_modes(this->preset_modes_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); }
|
void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); }
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ class USBClient : public Component {
|
|||||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void handle_open_state_();
|
||||||
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
|
||||||
virtual void disconnect();
|
virtual void disconnect();
|
||||||
virtual void on_connected() {}
|
virtual void on_connected() {}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <span>
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace usb_host {
|
namespace usb_host {
|
||||||
|
|
||||||
@@ -142,18 +143,23 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
|
|||||||
} while (next_desc != NULL);
|
} while (next_desc != NULL);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
|
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
|
||||||
char buffer[256];
|
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
|
||||||
if (desc == nullptr)
|
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
|
||||||
|
|
||||||
|
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
|
||||||
|
if (desc == nullptr || desc->bLength < 2)
|
||||||
return "(unspecified)";
|
return "(unspecified)";
|
||||||
char *p = buffer;
|
int char_count = (desc->bLength - 2) / 2;
|
||||||
for (int i = 0; i != desc->bLength / 2; i++) {
|
char *p = buffer.data();
|
||||||
|
char *end = p + buffer.size() - 1;
|
||||||
|
for (int i = 0; i != char_count && p < end; i++) {
|
||||||
auto c = desc->wData[i];
|
auto c = desc->wData[i];
|
||||||
if (c < 0x100)
|
if (c < 0x100)
|
||||||
*p++ = static_cast<char>(c);
|
*p++ = static_cast<char>(c);
|
||||||
}
|
}
|
||||||
*p = '\0';
|
*p = '\0';
|
||||||
return {buffer};
|
return buffer.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||||
@@ -259,60 +265,63 @@ void USBClient::loop() {
|
|||||||
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this->state_) {
|
if (this->state_ == USB_CLIENT_OPEN) {
|
||||||
case USB_CLIENT_OPEN: {
|
this->handle_open_state_();
|
||||||
int err;
|
}
|
||||||
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
|
}
|
||||||
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
|
|
||||||
if (err != ESP_OK) {
|
void USBClient::handle_open_state_() {
|
||||||
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
int err;
|
||||||
this->state_ = USB_CLIENT_INIT;
|
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
|
||||||
break;
|
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
|
||||||
}
|
if (err != ESP_OK) {
|
||||||
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
|
||||||
const usb_device_desc_t *desc;
|
this->state_ = USB_CLIENT_INIT;
|
||||||
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
return;
|
||||||
if (err != ESP_OK) {
|
}
|
||||||
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
|
||||||
this->disconnect();
|
const usb_device_desc_t *desc;
|
||||||
} else {
|
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
|
||||||
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
if (err != ESP_OK) {
|
||||||
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
|
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
|
||||||
usb_device_info_t dev_info;
|
this->disconnect();
|
||||||
err = usb_host_device_info(this->device_handle_, &dev_info);
|
return;
|
||||||
if (err != ESP_OK) {
|
}
|
||||||
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
|
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
|
||||||
this->disconnect();
|
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
|
||||||
break;
|
if (this->vid_ != 0 || this->pid_ != 0) {
|
||||||
}
|
ESP_LOGD(TAG, "Not our device, closing");
|
||||||
this->state_ = USB_CLIENT_CONNECTED;
|
this->disconnect();
|
||||||
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
|
return;
|
||||||
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
|
}
|
||||||
get_descriptor_string(dev_info.str_desc_product).c_str(),
|
}
|
||||||
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
|
usb_device_info_t dev_info;
|
||||||
|
err = usb_host_device_info(this->device_handle_, &dev_info);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
|
||||||
|
this->disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->state_ = USB_CLIENT_CONNECTED;
|
||||||
|
char buf_manuf[DESC_STRING_BUF_SIZE];
|
||||||
|
char buf_product[DESC_STRING_BUF_SIZE];
|
||||||
|
char buf_serial[DESC_STRING_BUF_SIZE];
|
||||||
|
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
|
||||||
|
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
|
||||||
|
get_descriptor_string(dev_info.str_desc_product, buf_product),
|
||||||
|
get_descriptor_string(dev_info.str_desc_serial_num, buf_serial));
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
const usb_device_desc_t *device_desc;
|
const usb_device_desc_t *device_desc;
|
||||||
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
|
||||||
if (err == ESP_OK)
|
if (err == ESP_OK)
|
||||||
usb_client_print_device_descriptor(device_desc);
|
usb_client_print_device_descriptor(device_desc);
|
||||||
const usb_config_desc_t *config_desc;
|
const usb_config_desc_t *config_desc;
|
||||||
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
|
||||||
if (err == ESP_OK)
|
if (err == ESP_OK)
|
||||||
usb_client_print_config_descriptor(config_desc, nullptr);
|
usb_client_print_config_descriptor(config_desc, nullptr);
|
||||||
#endif
|
#endif
|
||||||
this->on_connected();
|
this->on_connected();
|
||||||
} else {
|
|
||||||
ESP_LOGD(TAG, "Not our device, closing");
|
|
||||||
this->disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void USBClient::on_opened(uint8_t addr) {
|
void USBClient::on_opened(uint8_t addr) {
|
||||||
|
|||||||
@@ -198,7 +198,8 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
|
|||||||
|
|
||||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||||
// helper for allowing only unique entries in the queue
|
// helper for allowing only unique entries in the queue
|
||||||
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
void __attribute__((flatten))
|
||||||
|
DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||||
DeferredEvent item(source, message_generator);
|
DeferredEvent item(source, message_generator);
|
||||||
|
|
||||||
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
|
||||||
@@ -557,7 +558,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
|||||||
root[ESPHOME_F("device")] = device_name;
|
root[ESPHOME_F("device")] = device_name;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_ENTITY_ICON
|
||||||
root[ESPHOME_F("icon")] = obj->get_icon_ref();
|
root[ESPHOME_F("icon")] = obj->get_icon_ref();
|
||||||
|
#endif
|
||||||
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
|
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
|
||||||
bool is_disabled = obj->is_disabled_by_default();
|
bool is_disabled = obj->is_disabled_by_default();
|
||||||
if (is_disabled)
|
if (is_disabled)
|
||||||
@@ -583,8 +586,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
|
|||||||
|
|
||||||
// Helper to get request detail parameter
|
// Helper to get request detail parameter
|
||||||
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||||
auto *param = request->getParam(ESPHOME_F("detail"));
|
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
|
||||||
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
@@ -861,10 +863,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
|||||||
}
|
}
|
||||||
auto call = is_on ? obj->turn_on() : obj->turn_off();
|
auto call = is_on ? obj->turn_on() : obj->turn_off();
|
||||||
|
|
||||||
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
|
||||||
|
|
||||||
if (request->hasParam(ESPHOME_F("oscillation"))) {
|
if (request->hasArg(ESPHOME_F("oscillation"))) {
|
||||||
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
|
auto speed = request->arg(ESPHOME_F("oscillation"));
|
||||||
auto val = parse_on_off(speed.c_str());
|
auto val = parse_on_off(speed.c_str());
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case PARSE_ON:
|
case PARSE_ON:
|
||||||
@@ -1040,14 +1042,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto traits = obj->get_traits();
|
auto traits = obj->get_traits();
|
||||||
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
|
||||||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
(request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
|
||||||
request->send(409);
|
request->send(409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||||
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1106,7 +1108,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto call = obj->make_call();
|
auto call = obj->make_call();
|
||||||
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1174,12 +1176,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
|||||||
|
|
||||||
auto call = obj->make_call();
|
auto call = obj->make_call();
|
||||||
|
|
||||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
const auto &value = request->arg(ESPHOME_F("value"));
|
||||||
|
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||||
|
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||||
request->send(409);
|
request->send(409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
call.set_date(value.c_str(), value.length());
|
||||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
|
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1234,12 +1237,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
|||||||
|
|
||||||
auto call = obj->make_call();
|
auto call = obj->make_call();
|
||||||
|
|
||||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
const auto &value = request->arg(ESPHOME_F("value"));
|
||||||
|
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||||
|
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||||
request->send(409);
|
request->send(409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
call.set_time(value.c_str(), value.length());
|
||||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
|
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1293,12 +1297,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
|||||||
|
|
||||||
auto call = obj->make_call();
|
auto call = obj->make_call();
|
||||||
|
|
||||||
if (!request->hasParam(ESPHOME_F("value"))) {
|
const auto &value = request->arg(ESPHOME_F("value"));
|
||||||
|
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||||
|
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||||
request->send(409);
|
request->send(409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
call.set_datetime(value.c_str(), value.length());
|
||||||
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
|
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1477,10 +1482,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
|||||||
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
|
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
|
||||||
|
|
||||||
// Parse temperature parameters
|
// Parse temperature parameters
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
|
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
|
||||||
&decltype(call)::set_target_temperature_high);
|
using ClimateCall = decltype(call);
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
|
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
|
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
|
||||||
|
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
|
||||||
|
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
|
||||||
|
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
|
||||||
|
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1721,12 +1730,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto traits = obj->get_traits();
|
auto traits = obj->get_traits();
|
||||||
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
|
||||||
request->send(409);
|
request->send(409);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
|
||||||
|
|
||||||
DEFER_ACTION(call, call.perform());
|
DEFER_ACTION(call, call.perform());
|
||||||
request->send(200);
|
request->send(200);
|
||||||
@@ -1870,12 +1879,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
|||||||
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
|
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
|
||||||
|
|
||||||
// Parse temperature parameters
|
// Parse temperature parameters
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
|
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
|
||||||
&water_heater::WaterHeaterCall::set_target_temperature);
|
&water_heater::WaterHeaterCall::set_target_temperature);
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
|
||||||
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
&water_heater::WaterHeaterCall::set_target_temperature_low);
|
||||||
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
|
||||||
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
&water_heater::WaterHeaterCall::set_target_temperature_high);
|
||||||
|
|
||||||
// Parse away mode parameter
|
// Parse away mode parameter
|
||||||
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
|
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
|
||||||
@@ -1979,16 +1988,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
|||||||
auto call = obj->make_call();
|
auto call = obj->make_call();
|
||||||
|
|
||||||
// Parse carrier frequency (optional)
|
// Parse carrier frequency (optional)
|
||||||
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
|
{
|
||||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
|
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
|
||||||
if (value.has_value()) {
|
if (value.has_value()) {
|
||||||
call.set_carrier_frequency(*value);
|
call.set_carrier_frequency(*value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse repeat count (optional, defaults to 1)
|
// Parse repeat count (optional, defaults to 1)
|
||||||
if (request->hasParam(ESPHOME_F("repeat_count"))) {
|
{
|
||||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
|
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
|
||||||
if (value.has_value()) {
|
if (value.has_value()) {
|
||||||
call.set_repeat_count(*value);
|
call.set_repeat_count(*value);
|
||||||
}
|
}
|
||||||
@@ -1996,18 +2005,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
|||||||
|
|
||||||
// Parse base64url-encoded raw timings (required)
|
// Parse base64url-encoded raw timings (required)
|
||||||
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
||||||
if (!request->hasParam(ESPHOME_F("data"))) {
|
const auto &data_arg = request->arg(ESPHOME_F("data"));
|
||||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
|
||||||
std::string encoded =
|
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||||
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
|
||||||
|
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
|
||||||
// Validate base64url is not empty
|
|
||||||
if (encoded.empty()) {
|
|
||||||
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2015,7 +2018,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
|
|||||||
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
||||||
// must remain valid until perform() completes.
|
// must remain valid until perform() completes.
|
||||||
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
|
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
|
||||||
this->defer([call, encoded = std::move(encoded)]() mutable {
|
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
|
||||||
call.set_raw_timings_base64url(encoded);
|
call.set_raw_timings_base64url(encoded);
|
||||||
call.perform();
|
call.perform();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -513,11 +513,9 @@ class WebServer : public Controller,
|
|||||||
template<typename T, typename Ret>
|
template<typename T, typename Ret>
|
||||||
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
|
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
|
||||||
float scale = 1.0f) {
|
float scale = 1.0f) {
|
||||||
if (request->hasParam(param_name)) {
|
auto value = parse_number<float>(request->arg(param_name).c_str());
|
||||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
if (value.has_value()) {
|
||||||
if (value.has_value()) {
|
(call.*setter)(*value / scale);
|
||||||
(call.*setter)(*value / scale);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,34 +523,19 @@ class WebServer : public Controller,
|
|||||||
template<typename T, typename Ret>
|
template<typename T, typename Ret>
|
||||||
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||||
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
||||||
if (request->hasParam(param_name)) {
|
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
|
||||||
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
|
if (value.has_value()) {
|
||||||
if (value.has_value()) {
|
(call.*setter)(*value * scale);
|
||||||
(call.*setter)(*value * scale);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Generic helper to parse and apply a float parameter
|
// Generic helper to parse and apply a numeric parameter
|
||||||
template<typename T, typename Ret>
|
template<typename NumT, typename T, typename Ret>
|
||||||
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
|
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
|
||||||
if (request->hasParam(param_name)) {
|
auto value = parse_number<NumT>(request->arg(param_name).c_str());
|
||||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
if (value.has_value()) {
|
||||||
if (value.has_value()) {
|
(call.*setter)(*value);
|
||||||
(call.*setter)(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic helper to parse and apply an int parameter
|
|
||||||
template<typename T, typename Ret>
|
|
||||||
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
|
|
||||||
if (request->hasParam(param_name)) {
|
|
||||||
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
|
|
||||||
if (value.has_value()) {
|
|
||||||
(call.*setter)(*value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,10 +543,9 @@ class WebServer : public Controller,
|
|||||||
template<typename T, typename Ret>
|
template<typename T, typename Ret>
|
||||||
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
|
||||||
Ret (T::*setter)(const std::string &)) {
|
Ret (T::*setter)(const std::string &)) {
|
||||||
if (request->hasParam(param_name)) {
|
if (request->hasArg(param_name)) {
|
||||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
const auto &value = request->arg(param_name);
|
||||||
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
(call.*setter)(std::string(value.c_str(), value.length()));
|
||||||
(call.*setter)(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,8 +555,9 @@ class WebServer : public Controller,
|
|||||||
// Invalid values are ignored (setter not called)
|
// Invalid values are ignored (setter not called)
|
||||||
template<typename T, typename Ret>
|
template<typename T, typename Ret>
|
||||||
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
|
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
|
||||||
if (request->hasParam(param_name)) {
|
const auto ¶m_value = request->arg(param_name);
|
||||||
auto param_value = request->getParam(param_name)->value();
|
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
|
||||||
|
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
|
||||||
// First check on/off (default), then true/false (custom)
|
// First check on/off (default), then true/false (custom)
|
||||||
auto val = parse_on_off(param_value.c_str());
|
auto val = parse_on_off(param_value.c_str());
|
||||||
if (val == PARSE_NONE) {
|
if (val == PARSE_NONE) {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#include <memory>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "http_parser.h"
|
#include "http_parser.h"
|
||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
namespace esphome::web_server_idf {
|
namespace esphome::web_server_idf {
|
||||||
|
|
||||||
static const char *const TAG = "web_server_idf_utils";
|
|
||||||
|
|
||||||
size_t url_decode(char *str) {
|
size_t url_decode(char *str) {
|
||||||
char *start = str;
|
char *start = str;
|
||||||
char *ptr = str, buf;
|
char *ptr = str, buf;
|
||||||
@@ -54,32 +50,15 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
|
|||||||
return {str};
|
return {str};
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<std::string> request_get_url_query(httpd_req_t *req) {
|
|
||||||
auto len = httpd_req_get_url_query_len(req);
|
|
||||||
if (len == 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string str;
|
|
||||||
str.resize(len);
|
|
||||||
|
|
||||||
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
|
|
||||||
if (res != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {str};
|
|
||||||
}
|
|
||||||
|
|
||||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
|
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
|
||||||
if (query_url == nullptr || query_len == 0) {
|
if (query_url == nullptr || query_len == 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use stack buffer for typical query strings, heap fallback for large ones
|
// Value can't exceed query_len. Use small stack buffer for typical values,
|
||||||
SmallBufferWithHeapFallback<256, char> val(query_len);
|
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
|
||||||
|
// since callers may also have stack buffers for the query string.
|
||||||
|
SmallBufferWithHeapFallback<128, char> val(query_len);
|
||||||
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
|
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -88,6 +67,18 @@ optional<std::string> query_key_value(const char *query_url, size_t query_len, c
|
|||||||
return {val.get()};
|
return {val.get()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool query_has_key(const char *query_url, size_t query_len, const char *key) {
|
||||||
|
if (query_url == nullptr || query_len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Minimal buffer — we only care if the key exists, not the value
|
||||||
|
char buf[1];
|
||||||
|
// httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found
|
||||||
|
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
|
||||||
|
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
|
||||||
|
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function for case-insensitive string region comparison
|
// Helper function for case-insensitive string region comparison
|
||||||
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||||
for (size_t i = 0; i < n; i++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ size_t url_decode(char *str);
|
|||||||
|
|
||||||
bool request_has_header(httpd_req_t *req, const char *name);
|
bool request_has_header(httpd_req_t *req, const char *name);
|
||||||
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
||||||
optional<std::string> request_get_url_query(httpd_req_t *req);
|
|
||||||
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
|
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
|
||||||
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
|
bool query_has_key(const char *query_url, size_t query_len, const char *key);
|
||||||
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function for case-insensitive character comparison
|
// Helper function for case-insensitive character comparison
|
||||||
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
|
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
|
||||||
|
|||||||
@@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Look up value from query strings
|
// Look up value from query strings
|
||||||
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
|
auto val = this->find_query_value_(name);
|
||||||
if (!val.has_value()) {
|
|
||||||
auto url_query = request_get_url_query(*this);
|
|
||||||
if (url_query.has_value()) {
|
|
||||||
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't cache misses to avoid wasting memory when handlers check for
|
// Don't cache misses to avoid wasting memory when handlers check for
|
||||||
// optional parameters that don't exist in the request
|
// optional parameters that don't exist in the request
|
||||||
@@ -412,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
|
|||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search post_query then URL query with a callback.
|
||||||
|
/// Returns first truthy result, or value-initialized default.
|
||||||
|
/// URL query is accessed directly from req->uri (same pattern as url_to()).
|
||||||
|
template<typename Func>
|
||||||
|
static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
|
||||||
|
-> decltype(func(nullptr, size_t{0}, name)) {
|
||||||
|
if (!post_query.empty()) {
|
||||||
|
auto result = func(post_query.c_str(), post_query.size(), name);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use httpd API for query length, then access string directly from URI.
|
||||||
|
// http_parser identifies components by offset/length without modifying the URI string.
|
||||||
|
// This is the same pattern used by url_to().
|
||||||
|
auto len = httpd_req_get_url_query_len(req);
|
||||||
|
if (len == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const char *query = strchr(req->uri, '?');
|
||||||
|
if (query == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
query++; // skip '?'
|
||||||
|
return func(query, len, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
|
||||||
|
return search_query_sources(this->req_, this->post_query_, name,
|
||||||
|
[](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncWebServerRequest::hasArg(const char *name) {
|
||||||
|
return search_query_sources(this->req_, this->post_query_, name, query_has_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AsyncWebServerRequest::arg(const char *name) {
|
||||||
|
auto val = this->find_query_value_(name);
|
||||||
|
if (val.has_value()) {
|
||||||
|
return std::move(val.value());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||||
httpd_resp_set_hdr(*this->req_, name, value);
|
httpd_resp_set_hdr(*this->req_, name, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ class AsyncWebServerRequest {
|
|||||||
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
|
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
|
||||||
/// URL is decoded (e.g., %20 -> space).
|
/// URL is decoded (e.g., %20 -> space).
|
||||||
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
|
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
|
||||||
/// Get URL as std::string. Prefer url_to() to avoid heap allocation.
|
// Remove before 2026.9.0
|
||||||
|
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||||
std::string url() const {
|
std::string url() const {
|
||||||
char buffer[URL_BUF_SIZE];
|
char buffer[URL_BUF_SIZE];
|
||||||
return std::string(this->url_to(buffer));
|
return std::string(this->url_to(buffer));
|
||||||
@@ -170,14 +171,8 @@ class AsyncWebServerRequest {
|
|||||||
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
|
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
bool hasArg(const char *name) { return this->hasParam(name); }
|
bool hasArg(const char *name);
|
||||||
std::string arg(const char *name) {
|
std::string arg(const char *name);
|
||||||
auto *param = this->getParam(name);
|
|
||||||
if (param) {
|
|
||||||
return param->value();
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
|
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
|
||||||
|
|
||||||
operator httpd_req_t *() const { return this->req_; }
|
operator httpd_req_t *() const { return this->req_; }
|
||||||
@@ -192,6 +187,7 @@ class AsyncWebServerRequest {
|
|||||||
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
|
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
|
||||||
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
|
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
|
||||||
// handlers check for optional parameters that don't exist.
|
// handlers check for optional parameters that don't exist.
|
||||||
|
optional<std::string> find_query_value_(const char *name) const;
|
||||||
std::vector<AsyncWebParameter *> params_;
|
std::vector<AsyncWebParameter *> params_;
|
||||||
std::string post_query_;
|
std::string post_query_;
|
||||||
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
|
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
|
||||||
|
|||||||
@@ -487,6 +487,19 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) {
|
||||||
|
for (auto &it : this->sta_priorities_) {
|
||||||
|
if (it.bssid == bssid) {
|
||||||
|
it.priority = priority;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->sta_priorities_.push_back(WiFiSTAPriority{
|
||||||
|
.bssid = bssid,
|
||||||
|
.priority = priority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
|
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
// Skip logging during roaming scans to avoid log buffer overflow
|
// Skip logging during roaming scans to avoid log buffer overflow
|
||||||
|
|||||||
@@ -488,20 +488,11 @@ class WiFiComponent : public Component {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
void set_sta_priority(const bssid_t bssid, int8_t priority) {
|
void set_sta_priority(bssid_t bssid, int8_t priority);
|
||||||
for (auto &it : this->sta_priorities_) {
|
|
||||||
if (it.bssid == bssid) {
|
|
||||||
it.priority = priority;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this->sta_priorities_.push_back(WiFiSTAPriority{
|
|
||||||
.bssid = bssid,
|
|
||||||
.priority = priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
network::IPAddresses wifi_sta_ip_addresses();
|
network::IPAddresses wifi_sta_ip_addresses();
|
||||||
|
// Remove before 2026.9.0
|
||||||
|
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0")
|
||||||
std::string wifi_ssid();
|
std::string wifi_ssid();
|
||||||
/// Write SSID to buffer without heap allocation.
|
/// Write SSID to buffer without heap allocation.
|
||||||
/// Returns pointer to buffer, or empty string if not connected.
|
/// Returns pointer to buffer, or empty string if not connected.
|
||||||
|
|||||||
@@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() {
|
|||||||
ESP_LOGV(TAG, "Set hostname failed");
|
ESP_LOGV(TAG, "Set hostname failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// inform dhcp server of hostname change using dhcp_renew()
|
// Update hostname on all lwIP interfaces so DHCP packets include it.
|
||||||
|
// lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically
|
||||||
|
// via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is
|
||||||
|
// fixed at compile time and never changes at runtime.
|
||||||
for (netif *intf = netif_list; intf; intf = intf->next) {
|
for (netif *intf = netif_list; intf; intf = intf->next) {
|
||||||
// unconditionally update all known interfaces
|
|
||||||
#if LWIP_VERSION_MAJOR == 1
|
#if LWIP_VERSION_MAJOR == 1
|
||||||
intf->hostname = (char *) wifi_station_get_hostname();
|
intf->hostname = (char *) wifi_station_get_hostname();
|
||||||
#else
|
#else
|
||||||
intf->hostname = wifi_station_get_hostname();
|
intf->hostname = wifi_station_get_hostname();
|
||||||
#endif
|
#endif
|
||||||
if (netif_dhcp_data(intf) != nullptr) {
|
|
||||||
// renew already started DHCP leases
|
|
||||||
err_t lwipret = dhcp_renew(intf);
|
|
||||||
if (lwipret != ERR_OK) {
|
|
||||||
ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname,
|
|
||||||
(int) lwipret, intf->name[0], intf->name[1], intf->num);
|
|
||||||
ret = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2026.2.0b2"
|
__version__ = "2026.3.0-dev"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
|||||||
@@ -148,9 +148,9 @@
|
|||||||
#define USE_MQTT
|
#define USE_MQTT
|
||||||
#define USE_MQTT_COVER_JSON
|
#define USE_MQTT_COVER_JSON
|
||||||
#define USE_NETWORK
|
#define USE_NETWORK
|
||||||
#define USE_ONLINE_IMAGE_BMP_SUPPORT
|
#define USE_RUNTIME_IMAGE_BMP
|
||||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
#define USE_RUNTIME_IMAGE_PNG
|
||||||
#define USE_ONLINE_IMAGE_JPEG_SUPPORT
|
#define USE_RUNTIME_IMAGE_JPEG
|
||||||
#define USE_OTA
|
#define USE_OTA
|
||||||
#define USE_OTA_PASSWORD
|
#define USE_OTA_PASSWORD
|
||||||
#define USE_OTA_STATE_LISTENER
|
#define USE_OTA_STATE_LISTENER
|
||||||
|
|||||||
@@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m
|
|||||||
this->unit_of_measurement_ = unit_of_measurement;
|
this->unit_of_measurement_ = unit_of_measurement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ENTITY_ICON
|
||||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
|
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||||
if (!obj.get_icon_ref().empty()) {
|
if (!obj.get_icon_ref().empty()) {
|
||||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
|
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
|
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
|
||||||
if (!obj.get_device_class_ref().empty()) {
|
if (!obj.get_device_class_ref().empty()) {
|
||||||
|
|||||||
@@ -231,8 +231,13 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Log entity icon if set (for use in dump_config)
|
/// Log entity icon if set (for use in dump_config)
|
||||||
|
#ifdef USE_ENTITY_ICON
|
||||||
#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj)
|
#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj)
|
||||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj);
|
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj);
|
||||||
|
#else
|
||||||
|
#define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0)
|
||||||
|
inline void log_entity_icon(const char *, const char *, const EntityBase &) {}
|
||||||
|
#endif
|
||||||
/// Log entity device class if set (for use in dump_config)
|
/// Log entity device class if set (for use in dump_config)
|
||||||
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
|
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
|
||||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
|
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
|
||||||
|
|||||||
@@ -1083,6 +1083,9 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
|
|||||||
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||||
* Optionally includes the total byte count in parentheses at the end.
|
* Optionally includes the total byte count in parentheses at the end.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @param data Pointer to the byte array to format.
|
* @param data Pointer to the byte array to format.
|
||||||
* @param length Number of bytes in the array.
|
* @param length Number of bytes in the array.
|
||||||
* @param separator Character to use between hex bytes (default: '.').
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
@@ -1108,6 +1111,9 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
|
|||||||
*
|
*
|
||||||
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @param data Pointer to the 16-bit word array to format.
|
* @param data Pointer to the 16-bit word array to format.
|
||||||
* @param length Number of 16-bit words in the array.
|
* @param length Number of 16-bit words in the array.
|
||||||
* @param separator Character to use between hex words (default: '.').
|
* @param separator Character to use between hex words (default: '.').
|
||||||
@@ -1131,6 +1137,9 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
|
|||||||
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||||
* uppercase hex value with customizable separator.
|
* uppercase hex value with customizable separator.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @param data Vector of bytes to format.
|
* @param data Vector of bytes to format.
|
||||||
* @param separator Character to use between hex bytes (default: '.').
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
@@ -1154,6 +1163,9 @@ std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator =
|
|||||||
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||||
* as a 4-digit uppercase hex value in big-endian order.
|
* as a 4-digit uppercase hex value in big-endian order.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @param data Vector of 16-bit words to format.
|
* @param data Vector of 16-bit words to format.
|
||||||
* @param separator Character to use between hex words (default: '.').
|
* @param separator Character to use between hex words (default: '.').
|
||||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||||
@@ -1176,6 +1188,9 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator
|
|||||||
* Treats each character in the string as a byte and formats it in hex.
|
* Treats each character in the string as a byte and formats it in hex.
|
||||||
* Useful for debugging binary data stored in std::string containers.
|
* Useful for debugging binary data stored in std::string containers.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @param data String whose bytes should be formatted as hex.
|
* @param data String whose bytes should be formatted as hex.
|
||||||
* @param separator Character to use between hex bytes (default: '.').
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
@@ -1198,6 +1213,9 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo
|
|||||||
* Converts the integer to big-endian byte order and formats each byte as hex.
|
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||||
* The most significant byte appears first in the output string.
|
* The most significant byte appears first in the output string.
|
||||||
*
|
*
|
||||||
|
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||||
|
* Causes heap fragmentation on long-running devices.
|
||||||
|
*
|
||||||
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||||
* @param val The unsigned integer value to format.
|
* @param val The unsigned integer value to format.
|
||||||
* @param separator Character to use between hex bytes (default: '.').
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
@@ -1655,13 +1673,10 @@ template<class T> class RAMAllocator {
|
|||||||
ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility.
|
ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility.
|
||||||
};
|
};
|
||||||
|
|
||||||
RAMAllocator() = default;
|
constexpr RAMAllocator() = default;
|
||||||
RAMAllocator(uint8_t flags) {
|
constexpr RAMAllocator(uint8_t flags)
|
||||||
// default is both external and internal
|
: flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL))
|
||||||
flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL;
|
: (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {}
|
||||||
if (flags != 0)
|
|
||||||
this->flags_ = flags;
|
|
||||||
}
|
|
||||||
template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {}
|
template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {}
|
||||||
|
|
||||||
T *allocate(size_t n) { return this->allocate(n, sizeof(T)); }
|
T *allocate(size_t n) { return this->allocate(n, sizeof(T)); }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pylint==4.0.4
|
pylint==4.0.4
|
||||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||||
ruff==0.15.0 # also change in .pre-commit-config.yaml when updating
|
ruff==0.15.1 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ sensor:
|
|||||||
name: Linearly combined temperatures
|
name: Linearly combined temperatures
|
||||||
sources:
|
sources:
|
||||||
- source: template_temperature1
|
- source: template_temperature1
|
||||||
coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;"
|
coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;"
|
||||||
- source: template_temperature2
|
- source: template_temperature2
|
||||||
coeffecient: 1.5
|
coefficient: 1.5
|
||||||
- platform: combination
|
- platform: combination
|
||||||
type: max
|
type: max
|
||||||
name: Max of combined temperatures
|
name: Max of combined temperatures
|
||||||
|
|||||||
@@ -57,6 +57,23 @@ display:
|
|||||||
allow_other_uses: true
|
allow_other_uses: true
|
||||||
number: GPIO4
|
number: GPIO4
|
||||||
|
|
||||||
|
- platform: epaper_spi
|
||||||
|
spi_id: spi_bus
|
||||||
|
model: waveshare-7.5in-H
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO5
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO17
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO16
|
||||||
|
busy_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO4
|
||||||
|
inverted: true
|
||||||
|
|
||||||
- platform: epaper_spi
|
- platform: epaper_spi
|
||||||
model: seeed-reterminal-e1002
|
model: seeed-reterminal-e1002
|
||||||
- platform: epaper_spi
|
- platform: epaper_spi
|
||||||
@@ -64,3 +81,66 @@ display:
|
|||||||
# Override pins to avoid conflict with other display configs
|
# Override pins to avoid conflict with other display configs
|
||||||
busy_pin: 43
|
busy_pin: 43
|
||||||
dc_pin: 42
|
dc_pin: 42
|
||||||
|
|
||||||
|
# WeAct 2.13" 3-color e-paper (122x250, SSD1680)
|
||||||
|
- platform: epaper_spi
|
||||||
|
spi_id: spi_bus
|
||||||
|
model: weact-2.13in-3c
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO5
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO17
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO16
|
||||||
|
busy_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO4
|
||||||
|
lambda: |-
|
||||||
|
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
|
||||||
|
|
||||||
|
# WeAct 2.9" 3-color e-paper (128x296, SSD1683)
|
||||||
|
- platform: epaper_spi
|
||||||
|
spi_id: spi_bus
|
||||||
|
model: weact-2.9in-3c
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO5
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO17
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO16
|
||||||
|
busy_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO4
|
||||||
|
lambda: |-
|
||||||
|
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
|
||||||
|
|
||||||
|
# WeAct 4.2" 3-color e-paper (400x300, SSD1683)
|
||||||
|
- platform: epaper_spi
|
||||||
|
spi_id: spi_bus
|
||||||
|
model: weact-4.2in-3c
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO5
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO17
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO16
|
||||||
|
busy_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO4
|
||||||
|
lambda: |-
|
||||||
|
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK);
|
||||||
|
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0));
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ display:
|
|||||||
spi_16: true
|
spi_16: true
|
||||||
pixel_mode: 18bit
|
pixel_mode: 18bit
|
||||||
model: ili9488
|
model: ili9488
|
||||||
dc_pin: ${dc_pin}
|
dc_pin:
|
||||||
cs_pin: ${cs_pin}
|
allow_other_uses: true
|
||||||
reset_pin: ${reset_pin}
|
number: ${dc_pin}
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: ${cs_pin}
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: ${reset_pin}
|
||||||
data_rate: 20MHz
|
data_rate: 20MHz
|
||||||
invert_colors: true
|
invert_colors: true
|
||||||
show_test_card: true
|
show_test_card: true
|
||||||
@@ -24,3 +30,15 @@ display:
|
|||||||
height: 200
|
height: 200
|
||||||
enable_pin: ${enable_pin}
|
enable_pin: ${enable_pin}
|
||||||
bus_mode: single
|
bus_mode: single
|
||||||
|
|
||||||
|
- platform: mipi_spi
|
||||||
|
model: WAVESHARE-1.83-V2
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: ${dc_pin}
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: ${cs_pin}
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: ${reset_pin}
|
||||||
|
|||||||
12
tests/components/remote_receiver/test.esp32-c2-idf.yaml
Normal file
12
tests/components/remote_receiver/test.esp32-c2-idf.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
remote_receiver:
|
||||||
|
id: rcvr
|
||||||
|
pin: GPIO2
|
||||||
|
dump: all
|
||||||
|
<<: !include common-actions.yaml
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: remote_receiver
|
||||||
|
name: Panasonic Remote Input
|
||||||
|
panasonic:
|
||||||
|
address: 0x4004
|
||||||
|
command: 0x100BCBD
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
remote_transmitter:
|
||||||
|
id: xmitr
|
||||||
|
pin: GPIO2
|
||||||
|
carrier_duty_percent: 50%
|
||||||
|
|
||||||
|
packages:
|
||||||
|
buttons: !include common-buttons.yaml
|
||||||
Reference in New Issue
Block a user