mirror of
https://github.com/esphome/esphome.git
synced 2026-09-20 19:48:39 +00:00
Merge origin/dev into api_varint_split_32_64
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695
|
||||
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||
|
||||
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -115,6 +115,7 @@ jobs:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.0
|
||||
rev: v0.15.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.2.0-dev
|
||||
PROJECT_NUMBER = 2026.3.0-dev
|
||||
|
||||
# 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
|
||||
|
||||
+3
-2
@@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
|
||||
ARG BUILD_TYPE
|
||||
FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
RUN git config --system --add safe.directory "*" \
|
||||
&& git config --system advice.detachedHead false
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
@@ -23,7 +24,7 @@ RUN if command -v apk > /dev/null; then \
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
||||
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
||||
|
||||
COPY requirements.txt /
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# 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": [
|
||||
"mem_",
|
||||
@@ -794,7 +794,6 @@ SYMBOL_PATTERNS = {
|
||||
"s_dp",
|
||||
"s_ni",
|
||||
"s_reg_dump",
|
||||
"packet$",
|
||||
"d_mult_table",
|
||||
"K",
|
||||
"fcstab",
|
||||
|
||||
@@ -219,35 +219,8 @@ void APIConnection::loop() {
|
||||
this->process_batch_();
|
||||
}
|
||||
|
||||
switch (this->active_iterator_) {
|
||||
case ActiveIterator::LIST_ENTITIES:
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::INITIAL_STATE:
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::NONE:
|
||||
break;
|
||||
if (this->active_iterator_ != ActiveIterator::NONE) {
|
||||
this->process_active_iterator_();
|
||||
}
|
||||
|
||||
if (this->flags_.sent_ping) {
|
||||
@@ -283,6 +256,49 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::process_active_iterator_() {
|
||||
// Caller ensures active_iterator_ != NONE
|
||||
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_disconnect_response_() {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
@@ -1494,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
@@ -1848,6 +1864,8 @@ void APIConnection::on_fatal_error() {
|
||||
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,
|
||||
uint8_t aux_data_index) {
|
||||
// Check if we already have a message of this type for this entity
|
||||
@@ -1864,7 +1882,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
@@ -1872,7 +1890,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me
|
||||
// This avoids expensive vector::insert which shifts all elements
|
||||
// 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
|
||||
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) {
|
||||
// Swap the new high-priority item to the front
|
||||
std::swap(items.front(), items.back());
|
||||
@@ -1905,10 +1923,6 @@ bool APIConnection::schedule_batch_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
if (this->deferred_batch_.empty()) {
|
||||
this->flags_.batch_scheduled = false;
|
||||
return;
|
||||
@@ -1933,6 +1947,10 @@ void APIConnection::process_batch_() {
|
||||
for (size_t i = 0; i < num_items; i++) {
|
||||
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
||||
}
|
||||
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
|
||||
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
|
||||
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||
|
||||
@@ -1956,7 +1974,20 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
// Multi-message path — heavy stack frame isolated in separate noinline function
|
||||
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
|
||||
}
|
||||
|
||||
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
||||
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
const uint8_t frame_overhead = header_padding + footer_size;
|
||||
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
@@ -1983,7 +2014,7 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
||||
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
@@ -1999,42 +2030,38 @@ void APIConnection::process_batch_() {
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
if (items_processed == 0) {
|
||||
this->deferred_batch_.clear();
|
||||
return;
|
||||
}
|
||||
if (items_processed > 0) {
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
} else {
|
||||
// All items processed
|
||||
this->clear_batch_();
|
||||
// Partial batch — remove processed items and reschedule
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
this->schedule_batch_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// All items processed (or none could be processed)
|
||||
this->clear_batch_();
|
||||
}
|
||||
|
||||
// Dispatch message encoding based on message_type
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
class ComponentIterator;
|
||||
} // namespace esphome
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
@@ -366,20 +370,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||
void __attribute__((noinline)) process_active_iterator_();
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
// Helper method to process multiple entities from an iterator in a batch.
|
||||
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
|
||||
void process_iterator_batch_(ComponentIterator &iterator);
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
@@ -544,6 +541,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||
// 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);
|
||||
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
|
||||
void push_item(const BatchItem &item);
|
||||
|
||||
// Clear all items
|
||||
void clear() {
|
||||
@@ -551,8 +550,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
// Remove processed items from the front — noinline to keep memmove out of warm callers
|
||||
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
@@ -624,6 +623,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
bool schedule_batch_();
|
||||
void process_batch_();
|
||||
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) __attribute__((noinline));
|
||||
void clear_batch_() {
|
||||
this->deferred_batch_.clear();
|
||||
this->flags_.batch_scheduled = false;
|
||||
|
||||
@@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// During handshake phase, process as many actions as possible until we can't progress
|
||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read
|
||||
while (state_ != State::DATA && this->socket_->ready()) {
|
||||
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read.
|
||||
bool socket_ready = this->socket_->ready();
|
||||
while (state_ != State::DATA && socket_ready) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
|
||||
@@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(msg.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
|
||||
@@ -28,11 +28,7 @@ static const char *const TAG = "api";
|
||||
// APIServer
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
APIServer::APIServer() {
|
||||
global_api_server = this;
|
||||
// Pre-allocate shared write buffer
|
||||
shared_write_buffer_.reserve(64);
|
||||
}
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
void APIServer::setup() {
|
||||
ControllerRegistry::register_controller(this);
|
||||
@@ -117,37 +113,7 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -178,46 +144,88 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
}
|
||||
// Handle disconnection promptly - close socket to free LWIP PCB
|
||||
// resources and prevent retransmit crashes on ESP8266.
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
@@ -263,7 +268,11 @@ class APIServer : public Component,
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||
// since the buffer would almost always reallocate on first use.
|
||||
std::vector<uint8_t> shared_write_buffer_;
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#endif
|
||||
|
||||
@@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -150,7 +150,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Encode a varint directly into a pre-allocated buffer.
|
||||
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
|
||||
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
|
||||
while (val > 0x7F) {
|
||||
*buffer++ = static_cast<uint8_t>(val | 0x80);
|
||||
val >>= 7;
|
||||
}
|
||||
*buffer = static_cast<uint8_t>(val);
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -93,6 +103,7 @@ class ProtoVarInt {
|
||||
ProtoVarInt() : value_(0) {}
|
||||
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
||||
|
||||
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
|
||||
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(consumed != nullptr);
|
||||
@@ -148,50 +159,6 @@ class ProtoVarInt {
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
#endif
|
||||
/**
|
||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
||||
*
|
||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
||||
* @param len The size of the buffer in bytes
|
||||
*
|
||||
* @note The caller is responsible for ensuring the buffer is large enough
|
||||
* to hold the encoded value. Use ProtoSize::varint() to calculate
|
||||
* the exact size needed before calling this method.
|
||||
* @note No bounds checking is performed for performance reasons.
|
||||
*/
|
||||
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
buffer[0] = val;
|
||||
return;
|
||||
}
|
||||
size_t i = 0;
|
||||
while (val && i < len) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
buffer[i++] = temp | 0x80;
|
||||
} else {
|
||||
buffer[i++] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
void encode(std::vector<uint8_t> &out) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
out.push_back(val);
|
||||
return;
|
||||
}
|
||||
while (val) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
out.push_back(temp | 0x80);
|
||||
} else {
|
||||
out.push_back(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint64_t value_;
|
||||
@@ -251,8 +218,20 @@ class ProtoWriteBuffer {
|
||||
public:
|
||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
|
||||
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
|
||||
void encode_varint_raw(uint32_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
void encode_varint_raw_64(uint64_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
*
|
||||
@@ -302,13 +281,13 @@ class ProtoWriteBuffer {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||
this->encode_varint_raw(ProtoVarInt(value));
|
||||
this->encode_varint_raw_64(value);
|
||||
}
|
||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->write(0x01);
|
||||
this->buffer_->push_back(value ? 0x01 : 0x00);
|
||||
}
|
||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
@@ -933,13 +912,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
||||
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// Verify that the encoded size matches what we calculated
|
||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
|
||||
@@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const {
|
||||
}
|
||||
|
||||
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
|
||||
if (this->buffer_length_ > 0) {
|
||||
// Buffer currently has data, so reallocation is impossible
|
||||
if (this->buffer_ == nullptr) {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
@@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
|
||||
if (pre_shift) {
|
||||
// Shift data in buffer to start
|
||||
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_;
|
||||
}
|
||||
@@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
|
||||
|
||||
if (post_shift) {
|
||||
// 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_;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ class AudioTransferBuffer {
|
||||
/// @return True if there is data, false otherwise.
|
||||
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);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cbu": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231n-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = {
|
||||
},
|
||||
"cb3s": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA_0": 21,
|
||||
"WIRE1_SDA_1": 21,
|
||||
"WIRE1_SDA": 21,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_TX": 0,
|
||||
@@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7252": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cb3se": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL": 0,
|
||||
"WIRE2_SDA": 1,
|
||||
"SERIAL1_RX": 10,
|
||||
|
||||
@@ -59,10 +59,10 @@ namespace bl0942 {
|
||||
//
|
||||
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
||||
|
||||
static const float BL0942_PREF = 596; // taken from tasmota
|
||||
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
|
||||
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
|
||||
static const float BL0942_EREF = 3304.61127328; // Measured
|
||||
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
|
||||
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
|
||||
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
|
||||
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
|
||||
|
||||
struct DataPacket {
|
||||
uint8_t frame_header;
|
||||
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
|
||||
|
||||
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
|
||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
|
||||
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
||||
void set_address(uint8_t address) { this->address_ = address; }
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
#include "bmp3xx_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
|
||||
}
|
||||
}
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void BMP3XXComponent::setup() {
|
||||
|
||||
@@ -11,57 +11,26 @@
|
||||
*/
|
||||
|
||||
#include "bmp581_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
static const char *const TAG = "bmp581";
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
case Oversampling::OVERSAMPLING_X64:
|
||||
return LOG_STR("64x");
|
||||
case Oversampling::OVERSAMPLING_X128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void BMP581Component::dump_config() {
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
namespace esphome::camera {
|
||||
|
||||
BufferImpl::BufferImpl(size_t size) {
|
||||
this->data_ = this->allocator_.allocate(size);
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->data_ = allocator.allocate(size);
|
||||
this->size_ = size;
|
||||
}
|
||||
|
||||
BufferImpl::BufferImpl(CameraImageSpec *spec) {
|
||||
this->data_ = this->allocator_.allocate(spec->bytes_per_image());
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->data_ = allocator.allocate(spec->bytes_per_image());
|
||||
this->size_ = spec->bytes_per_image();
|
||||
}
|
||||
|
||||
BufferImpl::~BufferImpl() {
|
||||
if (this->data_ != nullptr)
|
||||
this->allocator_.deallocate(this->data_, this->size_);
|
||||
if (this->data_ != nullptr) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->data_, this->size_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::camera
|
||||
|
||||
@@ -18,7 +18,6 @@ class BufferImpl : public Buffer {
|
||||
~BufferImpl() override;
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_;
|
||||
size_t size_{};
|
||||
uint8_t *data_{};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace esphome::camera_encoder {
|
||||
|
||||
bool EncoderBufferImpl::set_buffer_size(size_t size) {
|
||||
if (size > this->capacity_) {
|
||||
uint8_t *p = this->allocator_.reallocate(this->data_, size);
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *p = allocator.reallocate(this->data_, size);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
|
||||
@@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) {
|
||||
}
|
||||
|
||||
EncoderBufferImpl::~EncoderBufferImpl() {
|
||||
if (this->data_ != nullptr)
|
||||
this->allocator_.deallocate(this->data_, this->capacity_);
|
||||
if (this->data_ != nullptr) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->data_, this->capacity_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::camera_encoder
|
||||
|
||||
@@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer {
|
||||
~EncoderBufferImpl() override;
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_;
|
||||
size_t capacity_{};
|
||||
size_t size_{};
|
||||
uint8_t *data_{};
|
||||
|
||||
@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
request->send(stream);
|
||||
}
|
||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
const auto &ssid = request->arg("ssid");
|
||||
const auto &psk = request->arg("psk");
|
||||
ESP_LOGI(TAG,
|
||||
"Requested WiFi Settings Change:\n"
|
||||
" SSID='%s'\n"
|
||||
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
ssid.c_str(), psk.c_str());
|
||||
#ifdef USE_ESP8266
|
||||
// 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
|
||||
// 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
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() {
|
||||
}
|
||||
|
||||
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))
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -15,6 +17,8 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
|
||||
|
||||
combination_ns = cg.esphome_ns.namespace("combination")
|
||||
@@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_(
|
||||
"SumCombinationComponent", cg.Component, sensor.Sensor
|
||||
)
|
||||
|
||||
CONF_COEFFECIENT = "coeffecient"
|
||||
CONF_COEFFICIENT = "coefficient"
|
||||
CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0
|
||||
CONF_ERROR = "error"
|
||||
CONF_KALMAN = "kalman"
|
||||
CONF_LINEAR = "linear"
|
||||
@@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
LINEAR_SOURCE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_),
|
||||
}
|
||||
|
||||
def _migrate_coeffecient(config):
|
||||
"""Migrate deprecated 'coeffecient' spelling to 'coefficient'."""
|
||||
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(
|
||||
@@ -162,12 +190,12 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.add_source(source, error))
|
||||
elif config[CONF_TYPE] == CONF_LINEAR:
|
||||
coeffecient = await cg.templatable(
|
||||
source_conf[CONF_COEFFECIENT],
|
||||
coefficient = await cg.templatable(
|
||||
source_conf[CONF_COEFFICIENT],
|
||||
[(float, "x")],
|
||||
cg.float_,
|
||||
)
|
||||
cg.add(var.add_source(source, coeffecient))
|
||||
cg.add(var.add_source(source, coefficient))
|
||||
else:
|
||||
cg.add(var.add_source(source))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_MQTT_JSON_STATE_PAYLOAD,
|
||||
CONF_ON_IDLE,
|
||||
CONF_ON_OPEN,
|
||||
CONF_POSITION,
|
||||
@@ -119,6 +120,9 @@ _COVER_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||
cv.requires_component("mqtt"), cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
@@ -148,6 +152,22 @@ _COVER_SCHEMA = (
|
||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||
|
||||
|
||||
def _validate_mqtt_state_topics(config):
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
if CONF_POSITION_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||
)
|
||||
if CONF_TILT_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
|
||||
|
||||
|
||||
def cover_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
@@ -195,6 +215,9 @@ async def setup_cover_core_(var, config):
|
||||
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
||||
) is not None:
|
||||
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
cg.add_define("USE_MQTT_COVER_JSON")
|
||||
cg.add(mqtt_.set_use_json_format(True))
|
||||
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
||||
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ESP8266
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <Esp.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -19,27 +20,38 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
|
||||
// clang-format off
|
||||
PROGMEM_STRING_TABLE(ResetReasonStrings,
|
||||
"Power On", // 0 = REASON_DEFAULT_RST
|
||||
"Hardware Watchdog", // 1 = REASON_WDT_RST
|
||||
"Exception", // 2 = REASON_EXCEPTION_RST
|
||||
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
|
||||
"Software/System restart", // 4 = REASON_SOFT_RESTART
|
||||
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
|
||||
"External System", // 6 = REASON_EXT_SYS_RST
|
||||
"Unknown" // 7 = fallback
|
||||
);
|
||||
// clang-format on
|
||||
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
|
||||
|
||||
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
|
||||
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
|
||||
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_DIO == 2, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
|
||||
|
||||
// Get reset reason string from reason code (no heap allocation)
|
||||
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
||||
static const LogString *get_reset_reason_str(uint32_t reason) {
|
||||
switch (reason) {
|
||||
case REASON_DEFAULT_RST:
|
||||
return LOG_STR("Power On");
|
||||
case REASON_WDT_RST:
|
||||
return LOG_STR("Hardware Watchdog");
|
||||
case REASON_EXCEPTION_RST:
|
||||
return LOG_STR("Exception");
|
||||
case REASON_SOFT_WDT_RST:
|
||||
return LOG_STR("Software Watchdog");
|
||||
case REASON_SOFT_RESTART:
|
||||
return LOG_STR("Software/System restart");
|
||||
case REASON_DEEP_SLEEP_AWAKE:
|
||||
return LOG_STR("Deep-Sleep Wake");
|
||||
case REASON_EXT_SYS_RST:
|
||||
return LOG_STR("External System");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Size for core version hex buffer
|
||||
@@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
const LogString *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
case FM_QIO:
|
||||
flash_mode = LOG_STR("QIO");
|
||||
break;
|
||||
case FM_QOUT:
|
||||
flash_mode = LOG_STR("QOUT");
|
||||
break;
|
||||
case FM_DIO:
|
||||
flash_mode = LOG_STR("DIO");
|
||||
break;
|
||||
case FM_DOUT:
|
||||
flash_mode = LOG_STR("DOUT");
|
||||
break;
|
||||
default:
|
||||
flash_mode = LOG_STR("UNKNOWN");
|
||||
}
|
||||
const LogString *flash_mode = FlashModeStrings::get_log_str(
|
||||
static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance)
|
||||
FlashModeStrings::LAST_INDEX);
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
|
||||
|
||||
@@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_(
|
||||
)
|
||||
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
|
||||
for module_info in pkgutil.iter_modules(models.__path__):
|
||||
importlib.import_module(f".models.{module_info.name}", package=__package__)
|
||||
|
||||
@@ -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
|
||||
@@ -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,),
|
||||
),
|
||||
)
|
||||
|
||||
# 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,),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -645,11 +645,12 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 6),
|
||||
"latest": cv.Version(3, 3, 6),
|
||||
"dev": cv.Version(3, 3, 6),
|
||||
"recommended": cv.Version(3, 3, 7),
|
||||
"latest": cv.Version(3, 3, 7),
|
||||
"dev": cv.Version(3, 3, 7),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
@@ -668,6 +669,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||
@@ -691,7 +693,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
@@ -708,8 +710,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 36),
|
||||
"latest": cv.Version(55, 3, 36),
|
||||
"recommended": cv.Version(55, 3, 37),
|
||||
"latest": cv.Version(55, 3, 37),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
@@ -1436,10 +1438,6 @@ async def to_code(config):
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
# Set the uv cache inside the data dir so "Clean All" clears it.
|
||||
# Avoids persistent corrupted cache from mid-stream download failures.
|
||||
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
|
||||
@@ -1686,6 +1686,10 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-C6-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"esp32-c61-devkitc1": {
|
||||
"name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)",
|
||||
"variant": VARIANT_ESP32C61,
|
||||
},
|
||||
"esp32-c61-devkitc1-n8r2": {
|
||||
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
|
||||
"variant": VARIANT_ESP32C61,
|
||||
@@ -1718,6 +1722,10 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-P4 rev.300 generic",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-p4_r3-evboard": {
|
||||
"name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-pico-devkitm-2": {
|
||||
"name": "Espressif ESP32-PICO-DevKitM-2",
|
||||
"variant": VARIANT_ESP32,
|
||||
@@ -2554,6 +2562,10 @@ BOARDS = {
|
||||
"name": "XinaBox CW02",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"yb_esp32s3_amp": {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_amp_v2": {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.2)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2562,6 +2574,10 @@ BOARDS = {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_dac": {
|
||||
"name": "YelloByte YB-ESP32-S3-DAC",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_drv": {
|
||||
"name": "YelloByte YB-ESP32-S3-DRV",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
|
||||
@@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
// goal try write all pending saves even if one fails
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
@@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
|
||||
@@ -369,42 +369,9 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
}
|
||||
|
||||
void ESP32BLE::loop() {
|
||||
switch (this->state_) {
|
||||
case BLE_COMPONENT_STATE_OFF:
|
||||
case BLE_COMPONENT_STATE_DISABLED:
|
||||
return;
|
||||
case BLE_COMPONENT_STATE_DISABLE: {
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
|
||||
ble_event_handler->ble_before_disabled_event_handler();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ble_dismantle_()) {
|
||||
ESP_LOGE(TAG, "Could not be dismantled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
||||
return;
|
||||
}
|
||||
case BLE_COMPONENT_STATE_ENABLE: {
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = BLE_COMPONENT_STATE_OFF;
|
||||
|
||||
if (!ble_setup_()) {
|
||||
ESP_LOGE(TAG, "Could not be set up");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||
return;
|
||||
}
|
||||
case BLE_COMPONENT_STATE_ACTIVE:
|
||||
break;
|
||||
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
|
||||
this->loop_handle_state_transition_not_active_();
|
||||
return;
|
||||
}
|
||||
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
@@ -520,6 +487,37 @@ void ESP32BLE::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLE::loop_handle_state_transition_not_active_() {
|
||||
// Caller ensures state_ != ACTIVE
|
||||
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
|
||||
ble_event_handler->ble_before_disabled_event_handler();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ble_dismantle_()) {
|
||||
ESP_LOGE(TAG, "Could not be dismantled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
||||
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = BLE_COMPONENT_STATE_OFF;
|
||||
|
||||
if (!ble_setup_()) {
|
||||
ESP_LOGE(TAG, "Could not be set up");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load new event data based on type
|
||||
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||
event->load_gap_event(e, p);
|
||||
|
||||
@@ -155,6 +155,10 @@ class ESP32BLE : public Component {
|
||||
#endif
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
|
||||
// Handle DISABLE and ENABLE transitions when not in the ACTIVE state.
|
||||
// Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops.
|
||||
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
|
||||
|
||||
bool ble_setup_();
|
||||
bool ble_dismantle_();
|
||||
bool ble_pre_setup_();
|
||||
|
||||
@@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf):
|
||||
action_conf,
|
||||
char_conf[CONF_CHAR_VALUE_ACTION_ID_],
|
||||
cg.TemplateArguments(),
|
||||
{},
|
||||
[],
|
||||
)
|
||||
cg.add(value_action.play())
|
||||
else:
|
||||
|
||||
@@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
if (this->handle_ != param->write.handle)
|
||||
break;
|
||||
|
||||
esp_gatt_status_t status = ESP_GATT_OK;
|
||||
|
||||
if (param->write.is_prep) {
|
||||
this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
|
||||
this->write_event_ = true;
|
||||
const size_t offset = param->write.offset;
|
||||
const size_t write_len = param->write.len;
|
||||
const size_t new_size = offset + write_len;
|
||||
// Clean the buffer on the first prepared write event
|
||||
if (offset == 0) {
|
||||
this->value_.clear();
|
||||
}
|
||||
|
||||
if (offset != this->value_.size()) {
|
||||
status = ESP_GATT_INVALID_OFFSET;
|
||||
} else if (new_size > ESP_GATT_MAX_ATTR_LEN) {
|
||||
status = ESP_GATT_INVALID_ATTR_LEN;
|
||||
} else {
|
||||
if (this->value_.size() < new_size) {
|
||||
this->value_.resize(new_size);
|
||||
}
|
||||
memcpy(this->value_.data() + offset, param->write.value, write_len);
|
||||
}
|
||||
} else {
|
||||
this->set_value(ByteBuffer::wrap(param->write.value, param->write.len));
|
||||
}
|
||||
@@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
memcpy(response.attr_value.value, param->write.value, param->write.len);
|
||||
|
||||
esp_err_t err =
|
||||
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response);
|
||||
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
|
||||
@@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
|
||||
}
|
||||
|
||||
case ESP_GATTS_EXEC_WRITE_EVT: {
|
||||
if (!this->write_event_)
|
||||
// BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes
|
||||
if (this->value_.empty())
|
||||
break;
|
||||
this->write_event_ = false;
|
||||
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
|
||||
if (this->on_write_callback_) {
|
||||
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
|
||||
|
||||
@@ -77,7 +77,6 @@ class BLECharacteristic {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool write_event_{false};
|
||||
BLEService *service_{};
|
||||
ESPBTUUID uuid_;
|
||||
esp_gatt_char_prop_t properties_;
|
||||
|
||||
@@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
return;
|
||||
}
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid);
|
||||
sta.set_password(command.password);
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
from esphome.components import esp32
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
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 _validator(value):
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from esphome import pins
|
||||
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.esp32 import include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
@@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low"
|
||||
|
||||
|
||||
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(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
|
||||
|
||||
@@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||
|
||||
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
#else
|
||||
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
|
||||
// This handles virtually all real-world preferences without heap allocation
|
||||
static constexpr size_t PREF_BUFFER_WORDS = 16;
|
||||
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
|
||||
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
@@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
|
||||
memcpy(buffer, data, len);
|
||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||
|
||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||
}
|
||||
@@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
if (!ret)
|
||||
return false;
|
||||
|
||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||
return false;
|
||||
|
||||
memcpy(data, buffer, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -130,11 +130,16 @@ ETHERNET_TYPES = {
|
||||
}
|
||||
|
||||
# PHY types that need compile-time defines for conditional compilation
|
||||
# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker
|
||||
_PHY_TYPE_TO_DEFINE = {
|
||||
"LAN8720": "USE_ETHERNET_LAN8720",
|
||||
"RTL8201": "USE_ETHERNET_RTL8201",
|
||||
"DP83848": "USE_ETHERNET_DP83848",
|
||||
"IP101": "USE_ETHERNET_IP101",
|
||||
"JL1101": "USE_ETHERNET_JL1101",
|
||||
"KSZ8081": "USE_ETHERNET_KSZ8081",
|
||||
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
|
||||
"LAN8670": "USE_ETHERNET_LAN8670",
|
||||
# Add other PHY types here only if they need conditional compilation
|
||||
}
|
||||
|
||||
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
|
||||
|
||||
@@ -186,31 +186,43 @@ void EthernetComponent::setup() {
|
||||
}
|
||||
#endif
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720: {
|
||||
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201: {
|
||||
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848: {
|
||||
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101: {
|
||||
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
case ETHERNET_TYPE_JL1101: {
|
||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
case ETHERNET_TYPE_KSZ8081RNA: {
|
||||
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670: {
|
||||
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
|
||||
@@ -343,26 +355,32 @@ void EthernetComponent::loop() {
|
||||
void EthernetComponent::dump_config() {
|
||||
const char *eth_type;
|
||||
switch (this->type_) {
|
||||
#ifdef USE_ETHERNET_LAN8720
|
||||
case ETHERNET_TYPE_LAN8720:
|
||||
eth_type = "LAN8720";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
case ETHERNET_TYPE_RTL8201:
|
||||
eth_type = "RTL8201";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_DP83848
|
||||
case ETHERNET_TYPE_DP83848:
|
||||
eth_type = "DP83848";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP101
|
||||
case ETHERNET_TYPE_IP101:
|
||||
eth_type = "IP101";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_JL1101
|
||||
case ETHERNET_TYPE_JL1101:
|
||||
eth_type = "JL1101";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_KSZ8081
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
eth_type = "KSZ8081";
|
||||
break;
|
||||
@@ -370,19 +388,22 @@ void EthernetComponent::dump_config() {
|
||||
case ETHERNET_TYPE_KSZ8081RNA:
|
||||
eth_type = "KSZ8081RNA";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_W5500
|
||||
case ETHERNET_TYPE_W5500:
|
||||
eth_type = "W5500";
|
||||
break;
|
||||
|
||||
case ETHERNET_TYPE_OPENETH:
|
||||
eth_type = "OPENETH";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#if CONFIG_ETH_SPI_ETHERNET_DM9051
|
||||
case ETHERNET_TYPE_DM9051:
|
||||
eth_type = "DM9051";
|
||||
break;
|
||||
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_OPENETH
|
||||
case ETHERNET_TYPE_OPENETH:
|
||||
eth_type = "OPENETH";
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_LAN8670
|
||||
case ETHERNET_TYPE_LAN8670:
|
||||
eth_type = "LAN8670";
|
||||
@@ -686,16 +707,22 @@ void EthernetComponent::dump_connect_params_() {
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" IP Address: %s\n"
|
||||
" Hostname: '%s'\n"
|
||||
" Subnet: %s\n"
|
||||
" Gateway: %s\n"
|
||||
" DNS1: %s\n"
|
||||
" DNS2: %s",
|
||||
" DNS2: %s\n"
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
|
||||
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
|
||||
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf));
|
||||
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
@@ -706,14 +733,6 @@ void EthernetComponent::dump_connect_params_() {
|
||||
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" MAC Address: %s\n"
|
||||
" Is Full Duplex: %s\n"
|
||||
" Link Speed: %u",
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
|
||||
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
@@ -837,13 +856,15 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
|
||||
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
|
||||
esp_err_t err;
|
||||
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Writing to PHY Register Address: 0x%02" PRIX32 "\n"
|
||||
@@ -852,11 +873,13 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
|
||||
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
|
||||
|
||||
#ifdef USE_ETHERNET_RTL8201
|
||||
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
|
||||
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
|
||||
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -110,6 +110,8 @@ class EthernetComponent : public Component {
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
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();
|
||||
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
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.
|
||||
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
|
||||
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB;
|
||||
optional<FanRestoreState> Fan::restore_state_() {
|
||||
FanRestoreState recovered{};
|
||||
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
|
||||
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_) {
|
||||
case FanRestoreMode::NO_RESTORE:
|
||||
return {};
|
||||
@@ -264,6 +269,7 @@ void Fan::save_state_() {
|
||||
state.oscillating = this->oscillating;
|
||||
state.speed = this->speed;
|
||||
state.direction = this->direction;
|
||||
state.preset_mode = FanRestoreState::NO_PRESET;
|
||||
|
||||
if (this->has_preset_mode()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
|
||||
@@ -91,11 +91,13 @@ class FanCall {
|
||||
};
|
||||
|
||||
struct FanRestoreState {
|
||||
static constexpr uint8_t NO_PRESET = UINT8_MAX;
|
||||
|
||||
bool state;
|
||||
int speed;
|
||||
bool oscillating;
|
||||
FanDirection direction;
|
||||
uint8_t preset_mode;
|
||||
uint8_t preset_mode{NO_PRESET};
|
||||
|
||||
/// Convert this struct to a fan call that can be performed.
|
||||
FanCall to_call(Fan &fan);
|
||||
|
||||
@@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() {
|
||||
}
|
||||
|
||||
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_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(*this);
|
||||
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() {
|
||||
|
||||
@@ -9,9 +9,20 @@
|
||||
#include "esphome/core/defines.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 {
|
||||
|
||||
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,
|
||||
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");
|
||||
stream_ptr = std::make_unique<WiFiClientSecure>();
|
||||
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();
|
||||
} else {
|
||||
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());
|
||||
App.feed_wdt();
|
||||
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(),
|
||||
HTTPClient::errorToString(container->status_code).c_str());
|
||||
|
||||
this->status_momentary_error("failed", 1000);
|
||||
container->end();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!is_success(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);
|
||||
|
||||
@@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
std::string response((char *) data, read_index);
|
||||
allocator.deallocate(data, container->content_length);
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
@@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
@@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
{ // Ensures the current version string falls out of scope and deallocates before the task ends
|
||||
std::string current_version;
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
current_version = ESPHOME_PROJECT_VERSION;
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
current_version = ESPHOME_VERSION;
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
this_update->update_info_.current_version = current_version;
|
||||
}
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
|
||||
@@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
switch (command.command) {
|
||||
case improv::WIFI_SETTINGS: {
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid);
|
||||
sta.set_password(command.password);
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(ssid);
|
||||
networks.push_back(std::move(ssid));
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) {
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
|
||||
}
|
||||
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||
JsonDocument doc = parse_json(data, len);
|
||||
if (doc.overflowed() || doc.isNull())
|
||||
return false;
|
||||
return f(doc.as<JsonObject>());
|
||||
|
||||
@@ -18,7 +18,10 @@ namespace json {
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
// This is only compiled when PSRAM is enabled
|
||||
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 {
|
||||
// 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 {
|
||||
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
|
||||
|
||||
@@ -50,6 +51,8 @@ std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
|
||||
|
||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||
|
||||
@@ -193,14 +193,14 @@ def _notify_old_style(config):
|
||||
# The dev and latest branches will be at *least* this version, which is what matters.
|
||||
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
||||
ARDUINO_VERSIONS = {
|
||||
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"latest": (
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
),
|
||||
"recommended": (
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
// goal try write all pending saves even if one fails
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
fdb_err_t last_err = FDB_NO_ERR;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
@@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
|
||||
@@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() {
|
||||
if (this->has_state())
|
||||
v.set_state(this->state_);
|
||||
|
||||
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
|
||||
// clamp_and_log_if_invalid already clamps in-place, so assign directly
|
||||
// to avoid redundant clamp code from the setter being inlined.
|
||||
#define VALIDATE_AND_APPLY(field, name_str, ...) \
|
||||
if (this->has_##field()) { \
|
||||
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
||||
v.setter(this->field##_); \
|
||||
v.field##_ = this->field##_; \
|
||||
}
|
||||
|
||||
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
|
||||
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
|
||||
VALIDATE_AND_APPLY(red, set_red, "Red")
|
||||
VALIDATE_AND_APPLY(green, set_green, "Green")
|
||||
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
|
||||
VALIDATE_AND_APPLY(white, set_white, "White")
|
||||
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
|
||||
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
|
||||
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
|
||||
traits.get_max_mireds())
|
||||
VALIDATE_AND_APPLY(brightness, "Brightness")
|
||||
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
|
||||
VALIDATE_AND_APPLY(red, "Red")
|
||||
VALIDATE_AND_APPLY(green, "Green")
|
||||
VALIDATE_AND_APPLY(blue, "Blue")
|
||||
VALIDATE_AND_APPLY(white, "White")
|
||||
VALIDATE_AND_APPLY(cold_white, "Cold white")
|
||||
VALIDATE_AND_APPLY(warm_white, "Warm white")
|
||||
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
|
||||
|
||||
#undef VALIDATE_AND_APPLY
|
||||
|
||||
|
||||
@@ -95,15 +95,18 @@ class LightColorValues {
|
||||
*/
|
||||
void normalize_color() {
|
||||
if (this->color_mode_ & ColorCapability::RGB) {
|
||||
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
|
||||
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
|
||||
// Assign directly to avoid redundant clamp in set_red/green/blue.
|
||||
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
|
||||
// and dividing by max_value (the largest) keeps results in [0,1].
|
||||
if (max_value == 0.0f) {
|
||||
this->set_red(1.0f);
|
||||
this->set_green(1.0f);
|
||||
this->set_blue(1.0f);
|
||||
this->red_ = 1.0f;
|
||||
this->green_ = 1.0f;
|
||||
this->blue_ = 1.0f;
|
||||
} else {
|
||||
this->set_red(this->get_red() / max_value);
|
||||
this->set_green(this->get_green() / max_value);
|
||||
this->set_blue(this->get_blue() / max_value);
|
||||
this->red_ /= max_value;
|
||||
this->green_ /= max_value;
|
||||
this->blue_ /= max_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +279,8 @@ class LightColorValues {
|
||||
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
||||
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
||||
|
||||
friend class LightCall;
|
||||
|
||||
protected:
|
||||
float state_; ///< ON / OFF, float for transition
|
||||
float brightness_;
|
||||
|
||||
@@ -154,28 +154,26 @@ LN882X_BOARD_PINS = {
|
||||
"A7": 21,
|
||||
},
|
||||
"wb02a": {
|
||||
"WIRE0_SCL_0": 7,
|
||||
"WIRE0_SCL_1": 5,
|
||||
"WIRE0_SCL_0": 1,
|
||||
"WIRE0_SCL_1": 2,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 10,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 1,
|
||||
"WIRE0_SCL_6": 4,
|
||||
"WIRE0_SCL_7": 5,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SCL_9": 24,
|
||||
"WIRE0_SCL_10": 25,
|
||||
"WIRE0_SDA_0": 7,
|
||||
"WIRE0_SDA_1": 5,
|
||||
"WIRE0_SCL_3": 4,
|
||||
"WIRE0_SCL_4": 5,
|
||||
"WIRE0_SCL_5": 7,
|
||||
"WIRE0_SCL_6": 9,
|
||||
"WIRE0_SCL_7": 10,
|
||||
"WIRE0_SCL_8": 24,
|
||||
"WIRE0_SCL_9": 25,
|
||||
"WIRE0_SDA_0": 1,
|
||||
"WIRE0_SDA_1": 2,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 10,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 1,
|
||||
"WIRE0_SDA_6": 4,
|
||||
"WIRE0_SDA_7": 5,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"WIRE0_SDA_9": 24,
|
||||
"WIRE0_SDA_10": 25,
|
||||
"WIRE0_SDA_3": 4,
|
||||
"WIRE0_SDA_4": 5,
|
||||
"WIRE0_SDA_5": 7,
|
||||
"WIRE0_SDA_6": 9,
|
||||
"WIRE0_SDA_7": 10,
|
||||
"WIRE0_SDA_8": 24,
|
||||
"WIRE0_SDA_9": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
@@ -221,32 +219,32 @@ LN882X_BOARD_PINS = {
|
||||
"A1": 4,
|
||||
},
|
||||
"wl2s": {
|
||||
"WIRE0_SCL_0": 7,
|
||||
"WIRE0_SCL_1": 12,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 10,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 0,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 11,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SCL_9": 24,
|
||||
"WIRE0_SCL_10": 25,
|
||||
"WIRE0_SCL_11": 5,
|
||||
"WIRE0_SCL_12": 1,
|
||||
"WIRE0_SDA_0": 7,
|
||||
"WIRE0_SDA_1": 12,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 10,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 0,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 11,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"WIRE0_SDA_9": 24,
|
||||
"WIRE0_SDA_10": 25,
|
||||
"WIRE0_SDA_11": 5,
|
||||
"WIRE0_SDA_12": 1,
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 5,
|
||||
"WIRE0_SCL_5": 7,
|
||||
"WIRE0_SCL_6": 9,
|
||||
"WIRE0_SCL_7": 10,
|
||||
"WIRE0_SCL_8": 11,
|
||||
"WIRE0_SCL_9": 12,
|
||||
"WIRE0_SCL_10": 19,
|
||||
"WIRE0_SCL_11": 24,
|
||||
"WIRE0_SCL_12": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 5,
|
||||
"WIRE0_SDA_5": 7,
|
||||
"WIRE0_SDA_6": 9,
|
||||
"WIRE0_SDA_7": 10,
|
||||
"WIRE0_SDA_8": 11,
|
||||
"WIRE0_SDA_9": 12,
|
||||
"WIRE0_SDA_10": 19,
|
||||
"WIRE0_SDA_11": 24,
|
||||
"WIRE0_SDA_12": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
@@ -301,24 +299,24 @@ LN882X_BOARD_PINS = {
|
||||
"A2": 1,
|
||||
},
|
||||
"ln-02": {
|
||||
"WIRE0_SCL_0": 11,
|
||||
"WIRE0_SCL_1": 19,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 24,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 25,
|
||||
"WIRE0_SCL_6": 1,
|
||||
"WIRE0_SCL_7": 0,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SDA_0": 11,
|
||||
"WIRE0_SDA_1": 19,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 24,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 25,
|
||||
"WIRE0_SDA_6": 1,
|
||||
"WIRE0_SDA_7": 0,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 9,
|
||||
"WIRE0_SCL_5": 11,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 24,
|
||||
"WIRE0_SCL_8": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 9,
|
||||
"WIRE0_SDA_5": 11,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 24,
|
||||
"WIRE0_SDA_8": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
|
||||
@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
bk72xx=768,
|
||||
ln882x=768,
|
||||
rtl87xx=768,
|
||||
nrf52=768,
|
||||
): cv.All(
|
||||
cv.only_on(
|
||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
PLATFORM_NRF52,
|
||||
]
|
||||
),
|
||||
cv.validate_bytes,
|
||||
cv.Any(
|
||||
@@ -313,11 +320,13 @@ async def to_code(config):
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
cg.add(log.create_pthread_key())
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||
elif CORE.is_host:
|
||||
cg.add(log.create_pthread_key())
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
@@ -417,6 +426,7 @@ async def to_code(config):
|
||||
pass
|
||||
|
||||
if CORE.is_nrf52:
|
||||
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||
if config[CONF_HARDWARE_UART] == UART0:
|
||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||
'\0', // NONE
|
||||
'1', // ERROR (31 = red)
|
||||
'3', // WARNING (33 = yellow)
|
||||
'2', // INFO (32 = green)
|
||||
'5', // CONFIG (35 = magenta)
|
||||
'6', // DEBUG (36 = cyan)
|
||||
'7', // VERBOSE (37 = gray)
|
||||
'8', // VERY_VERBOSE (38 = white)
|
||||
};
|
||||
|
||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
'\0', // NONE
|
||||
'E', // ERROR
|
||||
'W', // WARNING
|
||||
'I', // INFO
|
||||
'C', // CONFIG
|
||||
'D', // DEBUG
|
||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||
};
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
@@ -10,9 +10,9 @@ namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
|
||||
// Main thread/task always uses direct buffer access for console output and callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
|
||||
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
|
||||
//
|
||||
// For non-main threads/tasks:
|
||||
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
||||
@@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Get task handle once - used for both main task check and passing to non-main thread handler
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#elif (USE_ZEPHYR)
|
||||
k_tid_t current_task = k_current_get();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#else // USE_HOST
|
||||
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
||||
#endif
|
||||
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||
#else // USE_HOST
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
// This is safe to call from any context including ISRs
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
#endif
|
||||
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
||||
// This is a fallback mechanism to ensure critical log messages are visible
|
||||
// Note: This may cause interleaved/corrupted console output if multiple threads
|
||||
// log simultaneously, but it's better than losing important messages entirely
|
||||
#ifdef USE_HOST
|
||||
if (!message_sent) {
|
||||
if (!message_sent)
|
||||
#else
|
||||
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
|
||||
#endif
|
||||
{
|
||||
#ifdef USE_HOST
|
||||
// Host always has console output - no baud_rate check needed
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
#else
|
||||
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
|
||||
// Maximum size for console log messages (includes null terminator)
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
#endif
|
||||
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
||||
// Implementation for single-task platforms (ESP8266, RP2040)
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
#ifdef USE_ZEPHYR
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
||||
this->get_thread_name_(tmp));
|
||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
||||
// Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
||||
}
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
#ifdef USE_HOST
|
||||
// Host uses slot count instead of byte size
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
|
||||
#elif defined(USE_ESP32)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
||||
// The loop will be enabled automatically when messages arrive
|
||||
this->disable_loop_when_buffer_empty_();
|
||||
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::loop() { this->process_messages_(); }
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
|
||||
void Logger::loop() {
|
||||
this->process_messages_();
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||
this->cdc_loop_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::process_messages_() {
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// Process any buffered messages when available
|
||||
if (this->log_buffer_->has_messages()) {
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost::LogMessage *message;
|
||||
while (this->log_buffer_->get_message_main_loop(&message)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
|
||||
message->text_length, buf);
|
||||
this->log_buffer_->release_message_main_loop();
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer::LogMessage *message;
|
||||
const char *text;
|
||||
void *received_token;
|
||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
|
||||
uint16_t text_length;
|
||||
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
||||
message->text_length, buf);
|
||||
// Release the message to allow other tasks to use it as soon as possible
|
||||
this->log_buffer_->release_message_main_loop(received_token);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny::LogMessage *message;
|
||||
const char *text;
|
||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
||||
message->text_length, buf);
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
|
||||
message->text_data(), text_length, buf);
|
||||
// Release the message to allow other tasks to use it as soon as possible
|
||||
this->log_buffer_->release_message_main_loop();
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
// This reduces overhead when there's no async logging activity
|
||||
|
||||
@@ -13,15 +13,11 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
#include "log_buffer.h"
|
||||
#include "task_log_buffer_host.h"
|
||||
#elif defined(USE_ESP32)
|
||||
#include "task_log_buffer_esp32.h"
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include "task_log_buffer_libretiny.h"
|
||||
#endif
|
||||
#endif
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ESP8266)
|
||||
@@ -97,195 +93,10 @@ struct CStrCompare {
|
||||
};
|
||||
#endif
|
||||
|
||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||
'\0', // NONE
|
||||
'1', // ERROR (31 = red)
|
||||
'3', // WARNING (33 = yellow)
|
||||
'2', // INFO (32 = green)
|
||||
'5', // CONFIG (35 = magenta)
|
||||
'6', // DEBUG (36 = cyan)
|
||||
'7', // VERBOSE (37 = gray)
|
||||
'8', // VERY_VERBOSE (38 = white)
|
||||
};
|
||||
|
||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
'\0', // NONE
|
||||
'E', // ERROR
|
||||
'W', // WARNING
|
||||
'I', // INFO
|
||||
'C', // CONFIG
|
||||
'D', // DEBUG
|
||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||
};
|
||||
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Stack buffer size for retrieving thread/task names from the OS
|
||||
// macOS allows up to 64 bytes, Linux up to 16
|
||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -411,11 +222,14 @@ class Logger : public Component {
|
||||
bool &flag_;
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// Handles non-main thread logging only (~0.1% of calls)
|
||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
const char *thread_name);
|
||||
#endif
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||
void cdc_loop_();
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
@@ -534,13 +348,7 @@ class Logger : public Component {
|
||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Group smaller types together at the end
|
||||
@@ -552,7 +360,7 @@ class Logger : public Component {
|
||||
#ifdef USE_LIBRETINY
|
||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
bool main_task_recursion_guard_{false};
|
||||
#ifdef USE_LIBRETINY
|
||||
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
||||
@@ -595,8 +403,10 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
k_tid_t current_task = k_current_get();
|
||||
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
|
||||
if (current_task == nullptr) {
|
||||
current_task = k_current_get();
|
||||
}
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
}
|
||||
@@ -635,7 +445,7 @@ class Logger : public Component {
|
||||
// Create RAII guard for non-main task recursion
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
||||
// - Main task uses dedicated boolean (same as ESP32)
|
||||
// - Non-main tasks share a single recursion guard
|
||||
@@ -643,6 +453,8 @@ class Logger : public Component {
|
||||
// - Recursion from logging within logging is the main concern
|
||||
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
||||
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
||||
//
|
||||
// Zephyr use __thread as TLS
|
||||
|
||||
// Check if non-main task is already in recursion
|
||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
||||
@@ -651,7 +463,8 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace esphome::logger {
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
void Logger::cdc_loop_() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
|
||||
}
|
||||
|
||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
*received_token = received_item;
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
this->current_token_ = received_item;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
if (token == nullptr) {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
vRingbufferReturnItem(ring_buffer_, token);
|
||||
vRingbufferReturnItem(ring_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
// Update counter to mark all messages as processed
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
@@ -52,10 +52,10 @@ class TaskLogBuffer {
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop(void *token);
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
@@ -78,6 +78,7 @@ class TaskLogBuffer {
|
||||
// Atomic counter for message tracking (only differences matter)
|
||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||
void *current_token_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
|
||||
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
|
||||
// Allocate message slots
|
||||
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
||||
}
|
||||
|
||||
TaskLogBufferHost::~TaskLogBufferHost() {
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
// unique_ptr handles cleanup automatically
|
||||
}
|
||||
|
||||
int TaskLogBufferHost::acquire_write_slot_() {
|
||||
int TaskLogBuffer::acquire_write_slot_() {
|
||||
// Try to reserve a slot using compare-and-swap
|
||||
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
|
||||
}
|
||||
}
|
||||
|
||||
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||
// Mark the slot as ready for reading
|
||||
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
||||
if (message == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
||||
|
||||
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*message = &msg;
|
||||
message = &msg;
|
||||
text_length = msg.text_length;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBufferHost::release_message_main_loop() {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
|
||||
// Clear the ready flag
|
||||
|
||||
@@ -21,12 +21,12 @@ namespace esphome::logger {
|
||||
*
|
||||
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
||||
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
||||
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
|
||||
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
|
||||
*
|
||||
* Producers (multiple threads) Consumer (main loop only)
|
||||
* │ │
|
||||
* ▼ ▼
|
||||
* acquire_write_slot_() get_message_main_loop()
|
||||
* acquire_write_slot_() bool borrow_message_main_loop()
|
||||
* CAS on reserve_index_ read write_index_
|
||||
* │ check ready flag
|
||||
* ▼ │
|
||||
@@ -48,7 +48,7 @@ namespace esphome::logger {
|
||||
* - Atomic CAS for slot reservation allows multiple producers without locks
|
||||
* - Single consumer (main loop) processes messages in order
|
||||
*/
|
||||
class TaskLogBufferHost {
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Default number of message slots - host has plenty of memory
|
||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||
@@ -71,15 +71,16 @@ class TaskLogBufferHost {
|
||||
thread_name[0] = '\0';
|
||||
text[0] = '\0';
|
||||
}
|
||||
inline char *text_data() { return this->text; }
|
||||
};
|
||||
|
||||
/// Constructor that takes the number of message slots
|
||||
explicit TaskLogBufferHost(size_t slot_count);
|
||||
~TaskLogBufferHost();
|
||||
explicit TaskLogBuffer(size_t slot_count);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - get next message from buffer, only call from main loop
|
||||
// Returns true if a message was retrieved, false if buffer is empty
|
||||
bool get_message_main_loop(LogMessage **message);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release the message after processing, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
this->size_ = total_buffer_size;
|
||||
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
||||
this->mutex_ = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
if (this->mutex_ != nullptr) {
|
||||
vSemaphoreDelete(this->mutex_);
|
||||
this->mutex_ = nullptr;
|
||||
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
||||
size_t TaskLogBuffer::available_contiguous_space() const {
|
||||
if (this->head_ >= this->tail_) {
|
||||
// head is ahead of or equal to tail
|
||||
// Available space is from head to end, plus from start to tail
|
||||
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
|
||||
if (message == nullptr || text == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
// Check if buffer was initialized successfully
|
||||
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
||||
return false;
|
||||
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
|
||||
this->tail_ = 0;
|
||||
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
||||
}
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
this->current_message_size_ = message_total_size(msg->text_length);
|
||||
|
||||
// Keep mutex held until release_message_main_loop()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
// Advance tail past the current message
|
||||
this->tail_ += this->current_message_size_;
|
||||
|
||||
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
xSemaphoreGive(this->mutex_);
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
const char *thread_name, const char *format, va_list args) {
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace esphome::logger {
|
||||
* - Volatile counter enables fast has_messages() without lock overhead
|
||||
* - If message doesn't fit at end, padding is added and message wraps to start
|
||||
*/
|
||||
class TaskLogBufferLibreTiny {
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
@@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny {
|
||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
|
||||
~TaskLogBufferLibreTiny();
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text);
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
|
||||
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
|
||||
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
|
||||
}
|
||||
|
||||
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
|
||||
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
|
||||
}
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
// alignment to 4 bytes
|
||||
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
|
||||
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
|
||||
this->mpsc_config_.size = total_buffer_size;
|
||||
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
|
||||
this->mpsc_config_.get_wlen = get_wlen,
|
||||
|
||||
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
size_t total_size = total_size_in_32bit_words(text_length);
|
||||
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
char *text_area = msg->text_data();
|
||||
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||
|
||||
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
|
||||
if (ret < 0) {
|
||||
// this should not happen, vsnprintf was called already once
|
||||
// fill with '\n' to not call mpsc_pbuf_free from producer
|
||||
// it will be trimmed anyway
|
||||
for (size_t i = 0; i < text_length; ++i) {
|
||||
text_area[i] = '\n';
|
||||
}
|
||||
text_area[text_length] = 0;
|
||||
// do not return false to free the buffer from main thread
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
|
||||
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
|
||||
|
||||
if (this->current_token_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we claimed buffer already, const_cast is safe here
|
||||
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||
|
||||
text_length = message->text_length;
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <zephyr/sys/mpsc_pbuf.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
|
||||
uint16_t line; // Source code line number
|
||||
uint8_t level; // Log level (0-7)
|
||||
#if defined(CONFIG_THREAD_NAME)
|
||||
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
|
||||
#else
|
||||
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
|
||||
#endif
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||
|
||||
// Methods for accessing message contents
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
|
||||
|
||||
// Get the total buffer size in bytes
|
||||
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
protected:
|
||||
mpsc_pbuf_buffer_config mpsc_config_{};
|
||||
mpsc_pbuf_buffer log_buffer_{};
|
||||
const mpsc_pbuf_generic *current_token_{};
|
||||
};
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -45,9 +45,28 @@ class MDNSComponent : public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO)
|
||||
void loop() override;
|
||||
#endif
|
||||
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
|
||||
//
|
||||
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
|
||||
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
|
||||
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
|
||||
// by how often update() is called.
|
||||
//
|
||||
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
|
||||
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
|
||||
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
|
||||
// while completely removing mDNS from the per-iteration loop list.
|
||||
//
|
||||
// In steady state (after the ~8 second boot probe/announce phase completes), update()
|
||||
// checks timers that are set to never expire, making every call pure overhead.
|
||||
//
|
||||
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
|
||||
// interval is safe in production.
|
||||
//
|
||||
// By using set_interval() instead of overriding loop(), the component is excluded from
|
||||
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
|
||||
// including virtual dispatch.
|
||||
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
#ifdef USE_MDNS_EXTRA_SERVICES
|
||||
|
||||
@@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
|
||||
}
|
||||
}
|
||||
|
||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); }
|
||||
|
||||
void MDNSComponent::loop() { MDNS.update(); }
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_esp8266);
|
||||
// Schedule MDNS.update() via set_interval() instead of overriding loop().
|
||||
// This removes the component from the per-iteration loop list entirely,
|
||||
// eliminating virtual dispatch overhead on every main loop cycle.
|
||||
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
|
||||
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||
}
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
MDNS.close();
|
||||
|
||||
@@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
|
||||
}
|
||||
}
|
||||
|
||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); }
|
||||
|
||||
void MDNSComponent::loop() { MDNS.update(); }
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_rp2040);
|
||||
// Schedule MDNS.update() via set_interval() instead of overriding loop().
|
||||
// This removes the component from the per-iteration loop list entirely,
|
||||
// eliminating virtual dispatch overhead on every main loop cycle.
|
||||
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
|
||||
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||
}
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
MDNS.close();
|
||||
|
||||
@@ -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
|
||||
|
||||
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(
|
||||
"PICO-RESTOUCH-LCD-3.5",
|
||||
@@ -162,3 +185,61 @@ AXS15231.extend(
|
||||
cs_pin=9,
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() {
|
||||
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
(void) tag;
|
||||
if (level <= this->log_level_ && this->is_connected()) {
|
||||
this->publish({.topic = this->log_message_.topic,
|
||||
.payload = std::string(message, message_len),
|
||||
.qos = this->log_message_.qos,
|
||||
.retain = this->log_message_.retain});
|
||||
this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
|
||||
this->log_message_.retain);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
|
||||
|
||||
bool MQTTClimateComponent::publish_state_() {
|
||||
auto traits = this->device_->get_traits();
|
||||
// Reusable stack buffer for topic construction (avoids heap allocation per publish)
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
// mode
|
||||
bool success = true;
|
||||
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode)))
|
||||
if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode)))
|
||||
success = false;
|
||||
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
||||
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
||||
@@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
|
||||
!std::isnan(this->device_->current_temperature)) {
|
||||
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
|
||||
if (!this->publish(this->get_current_temperature_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
|
||||
if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
|
||||
if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
} else {
|
||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
|
||||
if (!this->publish(this->get_target_temperature_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
|
||||
!std::isnan(this->device_->current_humidity)) {
|
||||
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
|
||||
if (!this->publish(this->get_current_humidity_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
|
||||
!std::isnan(this->device_->target_humidity)) {
|
||||
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
|
||||
if (!this->publish(this->get_target_humidity_state_topic(), payload, len))
|
||||
if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||
if (this->device_->has_custom_preset()) {
|
||||
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
|
||||
if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str()))
|
||||
success = false;
|
||||
} else if (this->device_->preset.has_value()) {
|
||||
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
|
||||
if (!this->publish(this->get_preset_state_topic_to(topic_buf),
|
||||
climate_preset_to_mqtt_str(this->device_->preset.value())))
|
||||
success = false;
|
||||
} else if (!this->publish(this->get_preset_state_topic(), "")) {
|
||||
} else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action)))
|
||||
if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action)))
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_fan_modes()) {
|
||||
if (this->device_->has_custom_fan_mode()) {
|
||||
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
|
||||
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str()))
|
||||
success = false;
|
||||
} else if (this->device_->fan_mode.has_value()) {
|
||||
if (!this->publish(this->get_fan_mode_state_topic(),
|
||||
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf),
|
||||
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
|
||||
success = false;
|
||||
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
|
||||
} else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
|
||||
if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf),
|
||||
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
|
||||
success = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
||||
\
|
||||
public: \
|
||||
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
|
||||
StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \
|
||||
if (!this->custom_##name##_##type##_topic_.empty()) \
|
||||
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
|
||||
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
|
||||
} \
|
||||
std::string get_##name##_##type##_topic() const { \
|
||||
if (this->custom_##name##_##type##_topic_.empty()) \
|
||||
return this->get_default_topic_for_(#name "/" #type); \
|
||||
|
||||
@@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() {
|
||||
auto traits = this->cover_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
if (this->use_json_format_) {
|
||||
ESP_LOGCONFIG(TAG, " JSON State Payload: YES");
|
||||
} else {
|
||||
#endif
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str());
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str());
|
||||
}
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
}
|
||||
#endif
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
" Position Command Topic: '%s'",
|
||||
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Tilt State Topic: '%s'\n"
|
||||
" Tilt Command Topic: '%s'",
|
||||
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
|
||||
}
|
||||
}
|
||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
@@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
if (traits.get_is_assumed_state()) {
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
if (traits.get_supports_position()) {
|
||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
|
||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
|
||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
if (this->use_json_format_) {
|
||||
// JSON mode: all state published to state_topic as JSON, use templates to extract
|
||||
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
|
||||
if (traits.get_supports_position()) {
|
||||
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
|
||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
|
||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// Standard mode: separate topics for position and tilt
|
||||
if (traits.get_supports_position()) {
|
||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
|
||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
|
||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
||||
config.command_topic = false;
|
||||
@@ -111,20 +140,36 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
|
||||
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
||||
bool MQTTCoverComponent::publish_state() {
|
||||
auto traits = this->cover_->get_traits();
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
if (this->use_json_format_) {
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||
traits.get_supports_position());
|
||||
if (traits.get_supports_position()) {
|
||||
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
});
|
||||
}
|
||||
#endif
|
||||
bool success = true;
|
||||
if (traits.get_supports_position()) {
|
||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
|
||||
if (!this->publish(this->get_position_state_topic(), pos, len))
|
||||
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
|
||||
success = false;
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
|
||||
if (!this->publish(this->get_tilt_state_topic(), pos, len))
|
||||
if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len))
|
||||
success = false;
|
||||
}
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||
traits.get_supports_position())))
|
||||
|
||||
@@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
|
||||
bool publish_state();
|
||||
|
||||
void dump_config() override;
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
const char *component_type() const override;
|
||||
const EntityBase *get_entity() const override;
|
||||
|
||||
cover::Cover *cover_;
|
||||
#ifdef USE_MQTT_COVER_JSON
|
||||
bool use_json_format_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() {
|
||||
this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
bool failed = false;
|
||||
if (this->state_->get_traits().supports_direction()) {
|
||||
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
|
||||
bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
|
||||
fan_direction_to_mqtt_str(this->state_->direction));
|
||||
failed = failed || !success;
|
||||
}
|
||||
if (this->state_->get_traits().supports_oscillation()) {
|
||||
bool success =
|
||||
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating));
|
||||
bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf),
|
||||
fan_oscillation_to_mqtt_str(this->state_->oscillating));
|
||||
failed = failed || !success;
|
||||
}
|
||||
auto traits = this->state_->get_traits();
|
||||
if (traits.supports_speed()) {
|
||||
char buf[12];
|
||||
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
|
||||
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
|
||||
bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len);
|
||||
failed = failed || !success;
|
||||
}
|
||||
return !failed;
|
||||
|
||||
@@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }
|
||||
bool MQTTValveComponent::publish_state() {
|
||||
auto traits = this->valve_->get_traits();
|
||||
bool success = true;
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (traits.get_supports_position()) {
|
||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
|
||||
if (!this->publish(this->get_position_state_topic(), pos, len))
|
||||
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
|
||||
success = false;
|
||||
}
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
|
||||
traits.get_supports_position())))
|
||||
|
||||
@@ -2,97 +2,34 @@ import logging
|
||||
|
||||
from esphome import automation
|
||||
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.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
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
CONF_RESIZE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import Lambda
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
AUTO_LOAD = ["image", "runtime_image"]
|
||||
DEPENDENCIES = ["display", "http_request"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_PLACEHOLDER = "placeholder"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
|
||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||
|
||||
|
||||
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_)
|
||||
OnlineImage = online_image_ns.class_(
|
||||
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
|
||||
)
|
||||
|
||||
# Actions
|
||||
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 = (
|
||||
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
|
||||
runtime_image.runtime_image_schema(OnlineImage)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
# Online Image specific options
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
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.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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
rp2040_arduino=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):
|
||||
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
|
||||
image_format.actions()
|
||||
# Use the enhanced helper function to get all runtime image parameters
|
||||
settings = await runtime_image.process_runtime_image_config(config)
|
||||
|
||||
url = config[CONF_URL]
|
||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||
transparent = get_transparency_enum(config[CONF_TRANSPARENCY])
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
image_format.enum,
|
||||
get_image_type_enum(config[CONF_TYPE]),
|
||||
transparent,
|
||||
settings.width,
|
||||
settings.height,
|
||||
settings.format_enum,
|
||||
settings.image_type_enum,
|
||||
settings.transparent,
|
||||
settings.placeholder or cg.nullptr,
|
||||
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_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
@@ -227,10 +150,6 @@ async def to_code(config):
|
||||
else:
|
||||
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, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(bool, "cached")], conf)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::online_image {
|
||||
|
||||
static const char *const TAG = "online_image.download_buffer";
|
||||
|
||||
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->buffer_ = allocator.allocate(size);
|
||||
this->reset();
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Initial allocation of download buffer failed!");
|
||||
this->size_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *DownloadBuffer::data(size_t offset) {
|
||||
if (offset > this->size_) {
|
||||
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
|
||||
return this->buffer_;
|
||||
}
|
||||
return this->buffer_ + offset;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::read(size_t len) {
|
||||
if (len >= this->unread_) {
|
||||
this->unread_ = 0;
|
||||
return 0;
|
||||
}
|
||||
this->unread_ -= len;
|
||||
memmove(this->data(), this->data(len), this->unread_);
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::resize(size_t size) {
|
||||
if (this->size_ >= size) {
|
||||
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
|
||||
return this->size_;
|
||||
}
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
allocator.deallocate(this->buffer_, this->size_);
|
||||
this->buffer_ = allocator.allocate(size);
|
||||
this->reset();
|
||||
if (this->buffer_) {
|
||||
this->size_ = size;
|
||||
return size;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
|
||||
allocator.get_max_free_block_size());
|
||||
this->size_ = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -0,0 +1,46 @@
|
||||
#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() {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
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:
|
||||
uint8_t *buffer_;
|
||||
size_t size_;
|
||||
/** Total number of downloaded bytes not yet read. */
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -1,73 +0,0 @@
|
||||
#include "image_decoder.h"
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
static const char *const TAG = "online_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_->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) {
|
||||
this->buffer_ = this->allocator_.allocate(size);
|
||||
this->reset();
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Initial allocation of download buffer failed!");
|
||||
this->size_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *DownloadBuffer::data(size_t offset) {
|
||||
if (offset > this->size_) {
|
||||
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
|
||||
return this->buffer_;
|
||||
}
|
||||
return this->buffer_ + offset;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::read(size_t len) {
|
||||
this->unread_ -= len;
|
||||
if (this->unread_ > 0) {
|
||||
memmove(this->data(), this->data(len), this->unread_);
|
||||
}
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::resize(size_t size) {
|
||||
if (this->size_ >= size) {
|
||||
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
|
||||
return this->size_;
|
||||
}
|
||||
this->allocator_.deallocate(this->buffer_, this->size_);
|
||||
this->buffer_ = this->allocator_.allocate(size);
|
||||
this->reset();
|
||||
if (this->buffer_) {
|
||||
this->size_ = size;
|
||||
return size;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
|
||||
this->allocator_.get_max_free_block_size());
|
||||
this->size_ = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
|
||||
static const char *const TAG = "online_image";
|
||||
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 IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||
|
||||
#include "image_decoder.h"
|
||||
namespace esphome::online_image {
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
#include "bmp_image.h"
|
||||
#endif
|
||||
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#include "jpeg_image.h"
|
||||
#endif
|
||||
#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) {
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||
uint32_t buffer_size, bool is_big_endian)
|
||||
: RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height),
|
||||
download_buffer_(buffer_size),
|
||||
download_buffer_initial_size_(buffer_size) {
|
||||
this->set_url(url);
|
||||
}
|
||||
|
||||
void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
if (this->data_start_) {
|
||||
Image::draw(x, y, display, color_on, color_off);
|
||||
} else if (this->placeholder_) {
|
||||
this->placeholder_->draw(x, y, display, color_on, color_off);
|
||||
bool OnlineImage::validate_url_(const std::string &url) {
|
||||
if (url.empty()) {
|
||||
ESP_LOGE(TAG, "URL is empty");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::release() {
|
||||
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.length() > 2048) {
|
||||
ESP_LOGE(TAG, "URL is too long");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t OnlineImage::resize_(int width_in, int height_in) {
|
||||
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();
|
||||
}
|
||||
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
|
||||
ESP_LOGE(TAG, "URL must start with http:// or https://");
|
||||
return false;
|
||||
}
|
||||
size_t new_size = this->get_buffer_size_(width, height);
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnlineImage::update() {
|
||||
if (this->decoder_) {
|
||||
if (this->is_decoding()) {
|
||||
ESP_LOGW(TAG, "Image already being updated.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
|
||||
|
||||
std::list<http_request::Header> headers = {};
|
||||
|
||||
http_request::Header accept_header;
|
||||
accept_header.name = "Accept";
|
||||
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/*";
|
||||
if (!this->validate_url_(this->url_)) {
|
||||
ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str());
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
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()) {
|
||||
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()) {
|
||||
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_) {
|
||||
headers.push_back(http_request::Header{header.first, header.second.value()});
|
||||
}
|
||||
@@ -175,186 +115,117 @@ void OnlineImage::update() {
|
||||
ESP_LOGD(TAG, "Starting download");
|
||||
size_t total_size = this->downloader_->content_length;
|
||||
|
||||
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||
if (this->format_ == ImageFormat::BMP) {
|
||||
ESP_LOGD(TAG, "Allocating BMP decoder");
|
||||
this->decoder_ = make_unique<BmpDecoder>(this);
|
||||
this->enable_loop();
|
||||
// Initialize decoder with the known format
|
||||
if (!this->begin_decode(total_size)) {
|
||||
ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format());
|
||||
this->end_connection_();
|
||||
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_) {
|
||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
|
||||
this->end_connection_();
|
||||
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;
|
||||
// JPEG requires the complete image in the download buffer before decoding
|
||||
if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) {
|
||||
this->download_buffer_.resize(total_size);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = ::time(nullptr);
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void OnlineImage::loop() {
|
||||
if (!this->decoder_) {
|
||||
if (!this->is_decoding()) {
|
||||
// Not decoding at the moment => nothing to do.
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||
this->data_start_ = buffer_;
|
||||
this->width_ = buffer_width_;
|
||||
this->height_ = buffer_height_;
|
||||
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
|
||||
this->width_, this->height_);
|
||||
ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_));
|
||||
|
||||
if (!this->downloader_) {
|
||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
|
||||
|
||||
this->download_finished_callback_.call(false);
|
||||
this->end_connection_();
|
||||
return;
|
||||
}
|
||||
if (this->downloader_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||
return;
|
||||
}
|
||||
|
||||
// Download and decode more data
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available) {
|
||||
// Some decoders need to fully download the image before downloading.
|
||||
// In case of huge images, don't wait blocking until the whole image has been downloaded,
|
||||
// use smaller chunks
|
||||
if (available > 0) {
|
||||
// Download in chunks to avoid blocking
|
||||
available = std::min(available, this->download_buffer_initial_size_);
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
|
||||
if (len > 0) {
|
||||
this->download_buffer_.write(len);
|
||||
auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error when decoding image.");
|
||||
|
||||
// Feed data to decoder
|
||||
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->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->download_buffer_.read(fed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::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_ == 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;
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
}
|
||||
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: {
|
||||
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 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;
|
||||
} else {
|
||||
// Buffer is full, need to decode some data first
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
} else if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed);
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else {
|
||||
// Decoder can't process more data, might need complete image
|
||||
// This is normal for JPEG which needs complete data
|
||||
ESP_LOGV(TAG, "Decoder waiting for more data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_) {
|
||||
this->downloader_->end();
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->decoder_.reset();
|
||||
this->download_buffer_.reset();
|
||||
}
|
||||
|
||||
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;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
void OnlineImage::release() {
|
||||
// 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
|
||||
|
||||
#include "download_buffer.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/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
namespace esphome::online_image {
|
||||
|
||||
using t_http_codes = enum {
|
||||
HTTP_CODE_OK = 200,
|
||||
@@ -17,27 +16,13 @@ using t_http_codes = enum {
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
class OnlineImage : public PollingComponent,
|
||||
public image::Image,
|
||||
public runtime_image::RuntimeImage,
|
||||
public Parented<esphome::http_request::HttpRequestComponent> {
|
||||
public:
|
||||
/**
|
||||
@@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent,
|
||||
* @param url URL to download the image from.
|
||||
* @param width Desired width 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 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,
|
||||
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type,
|
||||
image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size,
|
||||
bool is_big_endian = false);
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void map_chroma_key(Color &color);
|
||||
|
||||
/** Set the URL to download the image from. */
|
||||
void set_url(const std::string &url) {
|
||||
@@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent,
|
||||
|
||||
/** Add the request header */
|
||||
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
|
||||
* to be able to be displayed.
|
||||
*/
|
||||
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_error_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
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_();
|
||||
|
||||
CallbackManager<void(bool)> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||
|
||||
uint8_t *buffer_;
|
||||
DownloadBuffer download_buffer_;
|
||||
/**
|
||||
* 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_;
|
||||
|
||||
const ImageFormat format_;
|
||||
image::Image *placeholder_{nullptr};
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent,
|
||||
std::string last_modified_ = "";
|
||||
|
||||
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...> {
|
||||
@@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
} // namespace esphome::online_image
|
||||
|
||||
@@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() {
|
||||
esp_cli_custom_command_init();
|
||||
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||
|
||||
otLinkModeConfig link_mode_config = {0};
|
||||
otLinkModeConfig link_mode_config{};
|
||||
#if CONFIG_OPENTHREAD_FTD
|
||||
link_mode_config.mRxOnWhenIdle = true;
|
||||
link_mode_config.mDeviceType = true;
|
||||
@@ -115,7 +115,7 @@ void OpenThreadComponent::ot_main() {
|
||||
ESP_LOGE(TAG, "Failed to set OpenThread pollperiod.");
|
||||
}
|
||||
uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance());
|
||||
ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period);
|
||||
ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, link_polling_period);
|
||||
}
|
||||
link_mode_config.mRxOnWhenIdle = this->poll_period == 0;
|
||||
link_mode_config.mDeviceType = false;
|
||||
|
||||
@@ -76,7 +76,7 @@ class PN532 : public PollingComponent {
|
||||
|
||||
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 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 format_mifare_classic_mifare_(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_();
|
||||
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
|
||||
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 clean_mifare_ultralight_();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user