Compare commits

..

25 Commits

Author SHA1 Message Date
J. Nick Koston
6ac6636127 Merge branch 'dev' into json-remove-stored-allocator 2026-02-15 20:58:32 -06:00
Cornelius A. Ludmann
f2cb5db9e0 [epaper_spi] Add Waveshare 7.5in e-Paper (H) (#13991) 2026-02-16 13:44:30 +11:00
Kevin Ahrendt
066419019f [audio] Support reallocating non-empty AudioTransferBuffer (#13979) 2026-02-15 16:09:35 -05:00
Pawelo
15da6d0a0b [epaper_spi] Add WeAct 3-color e-paper display support (#13894) 2026-02-16 07:58:51 +11:00
Jonathan Swoboda
6303bc3e35 [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:23:06 -05:00
Jonathan Swoboda
0f4dc6702d [fan] Fix preset_mode not restored on boot (#14002)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:50 -05:00
Jonathan Swoboda
f48c8a6444 [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:36 -05:00
J. Nick Koston
062f223876 [json, core] Remove stored RAMAllocator, make constructors constexpr
RAMAllocator with default flags is stateless — it's just a dispatch
wrapper over heap_caps_malloc/realloc/free. Remove the stored member
from SpiRamAllocator, using stack-local instances at each call site.

Also make RAMAllocator constructors constexpr so the compiler can
fully evaluate flag logic at compile time.

Note: SpiRamAllocator was initialized with RAMAllocator::NONE (0),
which is equivalent to default construction since the constructor
preserves the default ALLOC_INTERNAL | ALLOC_EXTERNAL flags when
no valid allocation flags are provided.

Co-Authored-By: J. Nick Koston <nick@koston.org>
2026-02-14 15:43:50 -07:00
dependabot[bot]
38404b2013 Bump ruff from 0.15.0 to 0.15.1 (#13980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-14 15:11:17 -07:00
AndreKR
5a6d64814a [http_request] Improve TLS logging on ESP8266 (#13985) 2026-02-14 10:08:26 -07:00
J. Nick Koston
36776b40c2 [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 08:21:04 -07:00
Jesse Hills
58c3ba7ac6 Merge branch 'beta' into dev 2026-02-14 16:03:25 +13:00
Jesse Hills
afa4047089 Merge pull request #13984 from esphome/bump-2026.2.0b2
2026.2.0b2
2026-02-14 16:02:49 +13:00
Jesse Hills
a8a324cbfb Bump version to 2026.2.0b2 2026-02-14 13:53:54 +13:00
J. Nick Koston
f6aeef2e68 [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) 2026-02-14 13:53:54 +13:00
Jonathan Swoboda
297dfb0db4 [docker] Suppress git detached HEAD advice (#13962)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:53:54 +13:00
Jonathan Swoboda
c08356b0c1 [alarm_control_panel] Fix flaky integration test race condition (#13964)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:53:54 +13:00
Lukáš Maňas
e9bf9bc691 [pulse_meter] Fix early edge detection (#12360)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-14 13:53:54 +13:00
J. Nick Koston
ead7937dbf [api] Extract cold code from APIServer::loop() hot path (#13902) 2026-02-14 13:53:54 +13:00
J. Nick Koston
844210519a [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-14 13:53:54 +13:00
Guillermo Ruffino
7c70b2e04e [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-14 13:53:54 +13:00
dependabot[bot]
931b47673c Bump github/codeql-action from 4.32.2 to 4.32.3 (#13981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 16:22:26 -06:00
J. Nick Koston
79d9fbf645 [nfc] Replace constant std::vector with static constexpr std::array (#13978) 2026-02-13 16:22:05 -06:00
J. Nick Koston
f24e7709ac [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out (#13973) 2026-02-13 16:21:50 -06:00
Kevin Ahrendt
903971de12 [runtime_image, online_image] Create runtime_image component to decode images (#10212) 2026-02-13 11:25:43 -05:00
80 changed files with 2039 additions and 1015 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.15.0 rev: v0.15.1
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti

View File

@@ -57,14 +57,8 @@ def maybe_conf(conf, *validators):
return validate return validate
def register_action( def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
name: str, return ACTION_REGISTRY.register(name, action_type, schema)
action_type: MockObjClass,
schema: cv.Schema,
*,
deferred: bool = False,
):
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema): def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -341,10 +335,7 @@ async def component_is_idle_condition_to_code(
@register_action( @register_action(
"delay", "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
deferred=True,
) )
async def delay_action_to_code( async def delay_action_to_code(
config: ConfigType, config: ConfigType,
@@ -454,7 +445,7 @@ _validate_wait_until = cv.maybe_simple_value(
) )
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True) @register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code( async def wait_until_action_to_code(
config: ConfigType, config: ConfigType,
action_id: ID, action_id: ID,
@@ -587,26 +578,6 @@ async def build_condition_list(
return conditions return conditions
def has_deferred_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any deferred actions.
Deferred actions (delay, wait_until, script.wait) store trigger args
for later execution, making non-owning types like StringRef unsafe.
"""
if isinstance(actions, list):
return any(has_deferred_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and ACTION_REGISTRY[key].deferred:
return True
return any(
has_deferred_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation( async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj: ) -> MockObj:

View File

@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_, "bool": cg.bool_,
"int": cg.int32, "int": cg.int32,
"float": cg.float_, "float": cg.float_,
"string": cg.StringRef, "string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -380,16 +380,9 @@ async def to_code(config: ConfigType) -> None:
if is_optional: if is_optional:
func_args.append((cg.bool_, "return_response")) func_args.append((cg.bool_, "return_response"))
# Check if action chain has deferred actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_deferred = automation.has_deferred_actions(conf.get(CONF_THEN, []))
service_arg_names: list[str] = [] service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items(): for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_] native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if deferred actions exist
if has_deferred and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native) service_template_args.append(native)
func_args.append((native, name)) func_args.append((native, name))
service_arg_names.append(name) service_arg_names.append(name)

View File

@@ -824,7 +824,7 @@ message HomeAssistantStateResponse {
option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1; string entity_id = 1;
string state = 2 [(null_terminate) = true]; string state = 2;
string attribute = 3; string attribute = 3;
} }
@@ -882,7 +882,7 @@ message ExecuteServiceArgument {
bool bool_ = 1; bool bool_ = 1;
int32 legacy_int = 2; int32 legacy_int = 2;
float float_ = 3; float float_ = 3;
string string_ = 4 [(null_terminate) = true]; string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value // ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5; sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true]; repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];

View File

@@ -1683,18 +1683,31 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
} }
for (auto &it : this->parent_->get_state_subs()) { for (auto &it : this->parent_->get_state_subs()) {
if (msg.entity_id != it.entity_id) { // Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id.size() ||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
continue; continue;
} }
// Compare attribute: either both have matching attribute, or both have none // Compare attribute: either both have matching attribute, or both have none
// it.attribute can be nullptr (meaning no attribute filter) size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) { if (sub_attr_len != msg.attribute.size() ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
continue; continue;
} }
// msg.state is already null-terminated in-place after protobuf decode // Create null-terminated state for callback (parse_number needs null-termination)
it.callback(msg.state); // HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
} }
} }
#endif #endif

View File

@@ -201,10 +201,9 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
} }
// Reserve space for body (+1 for null terminator so protobuf StringRef fields // Reserve space for body
// can be safely null-terminated in-place after decode) if (this->rx_buf_.size() != msg_size) {
if (this->rx_buf_.size() != msg_size + 1) { this->rx_buf_.resize(msg_size);
this->rx_buf_.resize(msg_size + 1);
} }
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {

View File

@@ -163,10 +163,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
// header reading done // header reading done
// Reserve space for body (+1 for null terminator so protobuf StringRef fields // Reserve space for body
// can be safely null-terminated in-place after decode) if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + 1) { this->rx_buf_.resize(this->rx_header_parsed_len_);
this->rx_buf_.resize(this->rx_header_parsed_len_ + 1);
} }
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {

View File

@@ -90,13 +90,4 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0}; // - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0}; // - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false]; optional bool packed_buffer = 50015 [default=false];
// null_terminate: Write a null byte after string data in the decode buffer.
// When set on a string field in a SOURCE_CLIENT (decodable) message, the
// generated decode() override writes '\0' at data[length] after decoding.
// This makes the StringRef safe for c_str() usage without copying.
// Safe because: (1) frame helpers reserve +1 byte in rx_buf_, and
// (2) the overwritten byte was already consumed during decode.
// Only mark fields that actually need null-terminated access.
optional bool null_terminate = 50016 [default=false];
} }

View File

@@ -953,12 +953,6 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
} }
return true; return true;
} }
void HomeAssistantStateResponse::decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode(buffer, length);
if (!this->state.empty()) {
const_cast<char *>(this->state.c_str())[this->state.size()] = '\0';
}
}
#endif #endif
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
@@ -1063,9 +1057,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9); uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array); this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length); ProtoDecodableMessage::decode(buffer, length);
if (!this->string_.empty()) {
const_cast<char *>(this->string_.c_str())[this->string_.size()] = '\0';
}
} }
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {

View File

@@ -1095,7 +1095,6 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
StringRef entity_id{}; StringRef entity_id{};
StringRef state{}; StringRef state{};
StringRef attribute{}; StringRef attribute{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override; const char *dump_to(DumpBuffer &out) const override;
#endif #endif

View File

@@ -1,6 +1,5 @@
#include "user_services.h" #include "user_services.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api { namespace esphome::api {
@@ -12,8 +11,6 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
} }
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -64,8 +61,6 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h // Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }

View File

@@ -2,6 +2,8 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <cstring>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
@@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const {
} }
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
if (this->buffer_length_ > 0) { if (this->buffer_ == nullptr) {
// Buffer currently has data, so reallocation is impossible return this->allocate_buffer_(new_buffer_size);
}
if (new_buffer_size < this->buffer_length_) {
// New size is too small to hold existing data
return false; return false;
} }
this->deallocate_buffer_();
return this->allocate_buffer_(new_buffer_size); // Shift existing data to the start of the buffer so realloc preserves it
if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
RAMAllocator<uint8_t> allocator;
uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size);
if (new_buffer == nullptr) {
// Reallocation failed, but the original buffer is still valid
return false;
}
this->buffer_ = new_buffer;
this->data_start_ = this->buffer_;
this->buffer_size_ = new_buffer_size;
return true;
} }
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
@@ -115,7 +137,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
if (pre_shift) { if (pre_shift) {
// Shift data in buffer to start // Shift data in buffer to start
if (this->buffer_length_ > 0) { if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_); std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
} }
this->data_start_ = this->buffer_; this->data_start_ = this->buffer_;
} }
@@ -150,7 +172,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
if (post_shift) { if (post_shift) {
// Shift unwritten data to the start of the buffer // Shift unwritten data to the start of the buffer
memmove(this->buffer_, this->data_start_, this->buffer_length_); std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_; this->data_start_ = this->buffer_;
} }

View File

@@ -56,6 +56,9 @@ class AudioTransferBuffer {
/// @return True if there is data, false otherwise. /// @return True if there is data, false otherwise.
virtual bool has_buffered_data() const; virtual bool has_buffered_data() const;
/// @brief Reallocates the transfer buffer, preserving any existing data.
/// @param new_buffer_size The new size in bytes. Must be at least as large as available().
/// @return True if successful, false otherwise. On failure, the original buffer remains valid.
bool reallocate(size_t new_buffer_size); bool reallocate(size_t new_buffer_size);
protected: protected:

View File

@@ -126,7 +126,7 @@ void LinearCombinationComponent::setup() {
} }
void LinearCombinationComponent::handle_new_value(float value) { void LinearCombinationComponent::handle_new_value(float value) {
// Multiplies each sensor state by a configured coeffecient and then sums // Multiplies each sensor state by a configured coefficient and then sums
if (!std::isfinite(value)) if (!std::isfinite(value))
return; return;

View File

@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -15,6 +17,8 @@ from esphome.const import (
) )
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@Cat-Ion", "@kahrendt"] CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
combination_ns = cg.esphome_ns.namespace("combination") combination_ns = cg.esphome_ns.namespace("combination")
@@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_(
"SumCombinationComponent", cg.Component, sensor.Sensor "SumCombinationComponent", cg.Component, sensor.Sensor
) )
CONF_COEFFECIENT = "coeffecient" CONF_COEFFICIENT = "coefficient"
CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0
CONF_ERROR = "error" CONF_ERROR = "error"
CONF_KALMAN = "kalman" CONF_KALMAN = "kalman"
CONF_LINEAR = "linear" CONF_LINEAR = "linear"
@@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema(
} }
) )
LINEAR_SOURCE_SCHEMA = cv.Schema(
def _migrate_coeffecient(config):
"""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.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_),
cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_),
} }
),
_migrate_coeffecient,
) )
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
@@ -162,12 +190,12 @@ async def to_code(config):
) )
cg.add(var.add_source(source, error)) cg.add(var.add_source(source, error))
elif config[CONF_TYPE] == CONF_LINEAR: elif config[CONF_TYPE] == CONF_LINEAR:
coeffecient = await cg.templatable( coefficient = await cg.templatable(
source_conf[CONF_COEFFECIENT], source_conf[CONF_COEFFICIENT],
[(float, "x")], [(float, "x")],
cg.float_, cg.float_,
) )
cg.add(var.add_source(source, coeffecient)) cg.add(var.add_source(source, coefficient))
else: else:
cg.add(var.add_source(source)) cg.add(var.add_source(source))

View File

@@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_(
) )
Transform = epaper_spi_ns.enum("Transform") Transform = epaper_spi_ns.enum("Transform")
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
# Import all models dynamically from the models package # Import all models dynamically from the models package
for module_info in pkgutil.iter_modules(models.__path__): for module_info in pkgutil.iter_modules(models.__path__):
importlib.import_module(f".models.{module_info.name}", package=__package__) importlib.import_module(f".models.{module_info.name}", package=__package__)

View File

@@ -0,0 +1,231 @@
#include "epaper_weact_3c.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_weact_3c";
// SSD1680 3-color display notes:
// - Buffer uses 1 bit per pixel, 8 pixels per byte
// - Buffer first half (black_offset): Black/White plane (1=black, 0=white)
// - Buffer second half (red_offset): Red plane (1=red, 0=no red)
// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8)
// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color)
void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
// Calculate position in the 1-bit buffer
const uint32_t pos = (x / 8) + (y * this->row_width_);
const uint8_t bit = 0x80 >> (x & 0x07);
const uint32_t red_offset = this->buffer_length_ / 2u;
// Use luminance threshold for B/W mapping
// Split at halfway point (382 = (255*3)/2)
bool is_white = (static_cast<int>(color.r) + color.g + color.b) > 382;
// Update black/white plane (first half of buffer)
if (is_white) {
// White pixel - clear bit in black plane
this->buffer_[pos] &= ~bit;
} else {
// Black pixel - set bit in black plane
this->buffer_[pos] |= bit;
}
// Update red plane (second half of buffer)
// Red if red component is dominant (r > g+b)
if (color.r > color.g + color.b) {
// Red pixel - set bit in red plane
this->buffer_[red_offset + pos] |= bit;
} else {
// Not red - clear bit in red plane
this->buffer_[red_offset + pos] &= ~bit;
}
}
void EPaperWeAct3C::fill(Color color) {
// For 3-color e-paper with 1-bit buffer format:
// - Black buffer: 1=black, 0=white
// - Red buffer: 1=red, 0=no red
// The buffer is stored as two halves: [black plane][red plane]
const size_t half_buffer = this->buffer_length_ / 2u;
// Use luminance threshold for B/W mapping
bool is_white = (static_cast<int>(color.r) + color.g + color.b) > 382;
bool is_red = color.r > color.g + color.b;
// Fill both planes
if (is_white) {
// White - both planes = 0x00
this->buffer_.fill(0x00);
} else if (is_red) {
// Red - black plane = 0x00, red plane = 0xFF
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[i] = 0x00;
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[half_buffer + i] = 0xFF;
} else {
// Black - black plane = 0xFF, red plane = 0x00
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[i] = 0xFF;
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[half_buffer + i] = 0x00;
}
}
void EPaperWeAct3C::clear() {
// Clear buffer to white, just like real paper.
this->fill(COLOR_ON);
}
void EPaperWeAct3C::set_window_() {
// For full screen refresh, we always start from (0,0)
// The y_low_/y_high_ values track the dirty region for optimization,
// but for display refresh we need to write from the beginning
uint16_t x_start = 0;
uint16_t x_end = this->width_ - 1;
uint16_t y_start = 0;
uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display
// Set RAM X address boundaries (0x44)
// X coordinates are byte-aligned (divided by 8)
this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)});
// Set RAM Y address boundaries (0x45)
// Format: Y start (LSB, MSB), Y end (LSB, MSB)
this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)});
// Reset RAM X counter to start (0x4E) - 1 byte
this->cmd_data(0x4E, {(uint8_t) (x_start / 8)});
// Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB)
this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)});
}
bool HOT EPaperWeAct3C::transfer_data() {
const uint32_t start_time = millis();
const size_t buffer_length = this->buffer_length_;
const size_t half_buffer = buffer_length / 2u;
ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer);
// Use a local buffer for SPI transfers
static constexpr size_t MAX_TRANSFER_SIZE = 128;
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
// First, send the RED buffer (0x26 = WRITE_COLOR)
// The red plane is in the second half of our buffer
// NOTE: Must set RAM window first to reset address counters!
if (this->current_data_index_ < half_buffer) {
if (this->current_data_index_ == 0) {
ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)");
this->set_window_(); // Reset RAM X/Y counters to start position
this->command(0x26);
}
this->start_data_();
size_t red_offset = half_buffer;
while (this->current_data_index_ < half_buffer) {
size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_);
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
return false;
}
}
this->disable();
}
// Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK)
// The black plane is in the first half of our buffer
if (this->current_data_index_ < buffer_length) {
if (this->current_data_index_ == half_buffer) {
ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)");
// Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior)
// this->set_window();
this->command(0x24);
// Continue using current_data_index_, but we need to map it to the start of the buffer
}
this->start_data_();
while (this->current_data_index_ < buffer_length) {
size_t remaining = buffer_length - this->current_data_index_;
size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining);
// Calculate offset into the BLACK buffer (which is at the start of this->buffer_)
// current_data_index_ goes from half_buffer to buffer_length
size_t buffer_offset = this->current_data_index_ - half_buffer;
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = this->buffer_[buffer_offset + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
return false;
}
}
this->disable();
}
this->current_data_index_ = 0;
ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer);
return true;
}
void EPaperWeAct3C::refresh_screen(bool partial) {
// SSD1680 refresh sequence:
// Reset RAM X/Y address counters to 0,0 so display reads from start
// 0x4E: RAM X counter - 1 byte (X / 8)
// 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB)
this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte)
this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes)
// Send UPDATE_FULL command (0x22) with display update control parameter
// Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7}
// 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content
this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7
this->command(0x20); // Activate display update
// COMMAND TERMINATE FRAME READ WRITE (required by SSD1680)
// Removed 0xFF based on working reference implementation
// this->command(0xFF);
}
void EPaperWeAct3C::power_on() {
// Power on sequence - send command to turn on power
// According to SSD1680 spec: 0x22, 0xF8 powers on the display
this->cmd_data(0x22, {0xF8}); // Power on
this->command(0x20); // Activate
}
void EPaperWeAct3C::power_off() {
// Power off sequence - send command to turn off power
// According to SSD1680 spec: 0x22, 0x83 powers off the display
this->cmd_data(0x22, {0x83}); // Power off
this->command(0x20); // Activate
}
void EPaperWeAct3C::deep_sleep() {
// Deep sleep sequence
this->cmd_data(0x10, {0x01}); // Deep sleep mode
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,39 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* WeAct 3-color e-paper displays (SSD1683 controller).
* Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc.
*
* Color scheme: Black, White, Red (BWR)
* Buffer layout: 1 bit per pixel, separate planes
* - Buffer first half: Black/White plane (1=black, 0=white)
* - Buffer second half: Red plane (1=red, 0=no red)
* - Total buffer: width * height / 4 bytes (2 * width * height / 8)
*/
class EPaperWeAct3C : public EPaperBase {
public:
EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height * 2;
}
void fill(Color color) override;
void clear() override;
protected:
void set_window_();
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_pixel_at(int x, int y, Color color) override;
bool transfer_data() override;
};
} // namespace esphome::epaper_spi

View File

@@ -84,3 +84,35 @@ jd79660.extend(
(0xA5, 0x00,), (0xA5, 0x00,),
), ),
) )
# Waveshare 7.5-H
#
# Vendor init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/7in5_e-Paper_H/ESP32/EPD_7in5h.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
#
# fmt: off
jd79660.extend(
"Waveshare-7.5in-H",
width=800,
height=480,
initsequence=(
(0x00, 0x0F, 0x29,),
(0x06, 0x0F, 0x8B, 0x93, 0xA1,),
(0x41, 0x00,),
(0x50, 0x37,),
(0x60, 0x02, 0x02,),
(0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480
(0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,),
(0x65, 0x00, 0x00, 0x00, 0x00,),
(0xE7, 0x1C,),
(0xE3, 0x00,),
(0xE9, 0x01,),
(0x30, 0x08,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
)

View File

@@ -0,0 +1,75 @@
"""WeAct Black/White/Red e-paper displays using SSD1683 controller.
Supported models:
- weact-2.13in-3c: 122x250 pixels (2.13" display)
- weact-2.9in-3c: 128x296 pixels (2.9" display)
- weact-4.2in-3c: 400x300 pixels (4.2" display)
These displays use SSD1680 or SSD1683 controller and require a specific initialization
sequence. The DRV_OUT_CTL command is calculated from the display height.
"""
from . import EpaperModel
class WeActBWR(EpaperModel):
"""Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller."""
def __init__(self, name, **defaults):
super().__init__(name, "EPaperWeAct3C", **defaults)
def get_init_sequence(self, config):
"""Generate initialization sequence for WeAct BWR displays.
The initialization sequence is based on SSD1680 and SSD1683 controller datasheet
and the WeAct display specifications.
"""
_, height = self.get_dimensions(config)
# DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00)
height_minus_1 = height - 1
msb = height_minus_1 >> 8
lsb = height_minus_1 & 0xFF
return (
# Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now
# (0x12,),
# Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now
# Step 3: DRV_OUT_CTL - driver output control (height-dependent)
# Format: (command, LSB, MSB, gate setting)
(0x01, lsb, msb, 0x00),
# Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X)
(0x11, 0x03),
# Step 5: BORDER_FULL - border waveform control
(0x3C, 0x05),
# Step 6: TEMP_SENS - internal temperature sensor
(0x18, 0x80),
# Step 7: DISPLAY_UPDATE - display update control
(0x21, 0x00, 0x80),
)
# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller
weact_2p9in3c = WeActBWR(
"weact-2.9in-3c",
width=128,
height=296,
data_rate="10MHz",
minimum_update_interval="1s",
)
# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller
weact_2p13in3c = WeActBWR(
"weact-2.13in-3c",
width=122,
height=250,
data_rate="10MHz",
minimum_update_interval="1s",
)
# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller
weact_4p2in3c = WeActBWR(
"weact-4.2in-3c",
width=400,
height=300,
data_rate="10MHz",
minimum_update_interval="10s",
)

View File

@@ -1,8 +1,30 @@
from esphome.components import esp32 from esphome.components import esp32
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61}
def validate_rmt_not_supported(rmt_only_keys):
"""Validate that RMT-only config keys are not used on variants without RMT hardware."""
rmt_only_keys = set(rmt_only_keys)
def _validator(config):
if CORE.is_esp32:
variant = esp32.get_esp32_variant()
if variant in VARIANTS_NO_RMT:
unsupported = rmt_only_keys.intersection(config)
if unsupported:
keys = ", ".join(sorted(f"'{k}'" for k in unsupported))
raise cv.Invalid(
f"{keys} not available on {variant} (no RMT hardware)"
)
return config
return _validator
def validate_clock_resolution(): def validate_clock_resolution():
def _validator(value): def _validator(value):

View File

@@ -3,7 +3,7 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, light from esphome.components import esp32, esp32_rmt, light
from esphome.components.const import CONF_USE_PSRAM from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import include_builtin_idf_component from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -71,6 +71,10 @@ CONF_RESET_LOW = "reset_low"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
esp32.only_on_variant(
unsupported=list(esp32_rmt.VARIANTS_NO_RMT),
msg_prefix="ESP32 RMT LED strip",
),
light.ADDRESSABLE_LIGHT_SCHEMA.extend( light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),

View File

@@ -221,12 +221,17 @@ void Fan::publish_state() {
} }
// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB;
optional<FanRestoreState> Fan::restore_state_() { optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{}; FanRestoreState recovered{};
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION); this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered); bool restored = this->rtc_.load(&recovered);
if (!restored) {
// No valid saved data; ensure preset_mode sentinel is set
recovered.preset_mode = FanRestoreState::NO_PRESET;
}
switch (this->restore_mode_) { switch (this->restore_mode_) {
case FanRestoreMode::NO_RESTORE: case FanRestoreMode::NO_RESTORE:
return {}; return {};
@@ -264,6 +269,7 @@ void Fan::save_state_() {
state.oscillating = this->oscillating; state.oscillating = this->oscillating;
state.speed = this->speed; state.speed = this->speed;
state.direction = this->direction; state.direction = this->direction;
state.preset_mode = FanRestoreState::NO_PRESET;
if (this->has_preset_mode()) { if (this->has_preset_mode()) {
const auto &preset_modes = traits.supported_preset_modes(); const auto &preset_modes = traits.supported_preset_modes();

View File

@@ -91,11 +91,13 @@ class FanCall {
}; };
struct FanRestoreState { struct FanRestoreState {
static constexpr uint8_t NO_PRESET = UINT8_MAX;
bool state; bool state;
int speed; int speed;
bool oscillating; bool oscillating;
FanDirection direction; FanDirection direction;
uint8_t preset_mode; uint8_t preset_mode{NO_PRESET};
/// Convert this struct to a fan call that can be performed. /// Convert this struct to a fan call that can be performed.
FanCall to_call(Fan &fan); FanCall to_call(Fan &fan);

View File

@@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() {
} }
void HBridgeFan::setup() { void HBridgeFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
restore->apply(*this); restore->apply(*this);
this->write_state_(); this->write_state_();
} }
// Construct traits
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
} }
void HBridgeFan::dump_config() { void HBridgeFan::dump_config() {

View File

@@ -9,9 +9,20 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
// Include BearSSL error constants for TLS failure diagnostics
#ifdef USE_ESP8266
#include <bearssl/bearssl_ssl.h>
#endif
namespace esphome::http_request { namespace esphome::http_request {
static const char *const TAG = "http_request.arduino"; static const char *const TAG = "http_request.arduino";
#ifdef USE_ESP8266
static constexpr int RX_BUFFER_SIZE = 512;
static constexpr int TX_BUFFER_SIZE = 512;
// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
#endif
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method, std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body, const std::string &body,
@@ -47,7 +58,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
stream_ptr = std::make_unique<WiFiClientSecure>(); stream_ptr = std::make_unique<WiFiClientSecure>();
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get()); WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
secure_client->setBufferSizes(512, 512); secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE);
secure_client->setInsecure(); secure_client->setInsecure();
} else { } else {
stream_ptr = std::make_unique<WiFiClient>(); stream_ptr = std::make_unique<WiFiClient>();
@@ -107,13 +118,42 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
App.feed_wdt(); App.feed_wdt();
if (container->status_code < 0) { if (container->status_code < 0) {
#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS)
if (secure) {
WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
int last_error = secure_client->getLastSSLError();
if (last_error != 0) {
const LogString *error_msg;
switch (last_error) {
case ESP8266_SSL_ERR_OOM:
error_msg = LOG_STR("Unable to allocate buffer memory");
break;
case BR_ERR_TOO_LARGE:
error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)");
break;
default:
error_msg = LOG_STR("Unknown SSL error");
break;
}
ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error);
if (last_error == ESP8266_SSL_ERR_OOM) {
ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(),
static_cast<unsigned int>(RX_BUFFER_SIZE + TX_BUFFER_SIZE));
}
} else {
ESP_LOGW(TAG, "Connection failure with no error code");
}
}
#endif
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
HTTPClient::errorToString(container->status_code).c_str()); HTTPClient::errorToString(container->status_code).c_str());
this->status_momentary_error("failed", 1000); this->status_momentary_error("failed", 1000);
container->end(); container->end();
return nullptr; return nullptr;
} }
if (!is_success(container->status_code)) { if (!is_success(container->status_code)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
this->status_momentary_error("failed", 1000); this->status_momentary_error("failed", 1000);

View File

@@ -18,7 +18,10 @@ namespace json {
// Build an allocator for the JSON Library using the RAMAllocator class // Build an allocator for the JSON Library using the RAMAllocator class
// This is only compiled when PSRAM is enabled // This is only compiled when PSRAM is enabled
struct SpiRamAllocator : ArduinoJson::Allocator { struct SpiRamAllocator : ArduinoJson::Allocator {
void *allocate(size_t size) override { return allocator_.allocate(size); } void *allocate(size_t size) override {
RAMAllocator<uint8_t> allocator;
return allocator.allocate(size);
}
void deallocate(void *ptr) override { void deallocate(void *ptr) override {
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
@@ -31,11 +34,9 @@ struct SpiRamAllocator : ArduinoJson::Allocator {
} }
void *reallocate(void *ptr, size_t new_size) override { void *reallocate(void *ptr, size_t new_size) override {
return allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size); RAMAllocator<uint8_t> allocator;
return allocator.reallocate(static_cast<uint8_t *>(ptr), new_size);
} }
protected:
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>::NONE};
}; };
#endif #endif

View File

@@ -2,97 +2,34 @@ import logging
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_TRANSPARENCY,
IMAGE_SCHEMA,
Image_,
get_image_type_enum,
get_transparency_enum,
validate_settings,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_DITHER,
CONF_FILE,
CONF_FORMAT,
CONF_ID, CONF_ID,
CONF_ON_ERROR, CONF_ON_ERROR,
CONF_RESIZE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE,
CONF_URL, CONF_URL,
) )
from esphome.core import Lambda from esphome.core import Lambda
AUTO_LOAD = ["image"] AUTO_LOAD = ["image", "runtime_image"]
DEPENDENCIES = ["display", "http_request"] DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"] CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_PLACEHOLDER = "placeholder"
CONF_UPDATE = "update" CONF_UPDATE = "update"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
online_image_ns = cg.esphome_ns.namespace("online_image") online_image_ns = cg.esphome_ns.namespace("online_image")
ImageFormat = online_image_ns.enum("ImageFormat") OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
)
class Format:
def __init__(self, image_type):
self.image_type = image_type
@property
def enum(self):
return getattr(ImageFormat, self.image_type)
def actions(self):
pass
class BMPFormat(Format):
def __init__(self):
super().__init__("BMP")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT")
class JPEGFormat(Format):
def __init__(self):
super().__init__("JPEG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
class PNGFormat(Format):
def __init__(self):
super().__init__("PNG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.1.0")
IMAGE_FORMATS = {
x.image_type: x
for x in (
BMPFormat(),
JPEGFormat(),
PNGFormat(),
)
}
IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]})
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
# Actions # Actions
SetUrlAction = online_image_ns.class_( SetUrlAction = online_image_ns.class_(
@@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_(
) )
def remove_options(*options):
return {
cv.Optional(option): cv.invalid(
f"{option} is an invalid option for online_image"
)
for option in options
}
ONLINE_IMAGE_SCHEMA = ( ONLINE_IMAGE_SCHEMA = (
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) runtime_image.runtime_image_schema(OnlineImage)
.extend( .extend(
{ {
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
# Online Image specific options # Online Image specific options
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.url, cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)}) cv.Schema({cv.string: cv.templatable(cv.string)})
), ),
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema(
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0),
), ),
validate_settings, runtime_image.validate_runtime_image_settings,
) )
) )
@@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
image_format = IMAGE_FORMATS[config[CONF_FORMAT]] # Use the enhanced helper function to get all runtime image parameters
image_format.actions() settings = await runtime_image.process_runtime_image_config(config)
url = config[CONF_URL] url = config[CONF_URL]
width, height = config.get(CONF_RESIZE, (0, 0))
transparent = get_transparency_enum(config[CONF_TRANSPARENCY])
var = cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
url, url,
width, settings.width,
height, settings.height,
image_format.enum, settings.format_enum,
get_image_type_enum(config[CONF_TYPE]), settings.image_type_enum,
transparent, settings.transparent,
settings.placeholder or cg.nullptr,
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", settings.byte_order_big_endian,
) )
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
@@ -227,10 +150,6 @@ async def to_code(config):
else: else:
cg.add(var.add_request_header(key, value)) cg.add(var.add_request_header(key, value))
if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id)
cg.add(var.set_placeholder(placeholder))
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "cached")], conf) await automation.build_automation(trigger, [(bool, "cached")], conf)

View File

@@ -1,29 +1,10 @@
#include "image_decoder.h" #include "download_buffer.h"
#include "online_image.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cstring>
namespace esphome { namespace esphome::online_image {
namespace online_image {
static const char *const TAG = "online_image.decoder"; static const char *const TAG = "online_image.download_buffer";
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize_(width, height) > 0;
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
return success;
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) {
for (int j = y * this->y_scale_; j < height; j++) {
this->image_->draw_pixel_(i, j, color);
}
}
}
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
this->buffer_ = this->allocator_.allocate(size); this->buffer_ = this->allocator_.allocate(size);
@@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) {
} }
size_t DownloadBuffer::read(size_t len) { size_t DownloadBuffer::read(size_t len) {
this->unread_ -= len; if (len >= this->unread_) {
if (this->unread_ > 0) { this->unread_ = 0;
memmove(this->data(), this->data(len), this->unread_); return 0;
} }
this->unread_ -= len;
memmove(this->data(), this->data(len), this->unread_);
return this->unread_; return this->unread_;
} }
@@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) {
} }
} }
} // namespace online_image } // namespace esphome::online_image
} // namespace esphome

View File

@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/helpers.h"
#include <cstddef>
#include <cstdint>
namespace esphome::online_image {
/**
* @brief Buffer for managing downloaded data.
*
* This class provides a buffer for downloading data with tracking of
* unread bytes and dynamic resizing capabilities.
*/
class DownloadBuffer {
public:
DownloadBuffer(size_t size);
~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
uint8_t *data(size_t offset = 0);
uint8_t *append() { return this->data(this->unread_); }
size_t unread() const { return this->unread_; }
size_t size() const { return this->size_; }
size_t free_capacity() const { return this->size_ - this->unread_; }
size_t read(size_t len);
size_t write(size_t len) {
this->unread_ += len;
return this->unread_;
}
void reset() { this->unread_ = 0; }
size_t resize(size_t size);
protected:
RAMAllocator<uint8_t> allocator_{};
uint8_t *buffer_;
size_t size_;
/** Total number of downloaded bytes not yet read. */
size_t unread_;
};
} // namespace esphome::online_image

View File

@@ -1,6 +1,6 @@
#include "online_image.h" #include "online_image.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
static const char *const TAG = "online_image"; static const char *const TAG = "online_image";
static const char *const ETAG_HEADER_NAME = "etag"; static const char *const ETAG_HEADER_NAME = "etag";
@@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match";
static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified"; static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since"; static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
#include "image_decoder.h" namespace esphome::online_image {
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
#include "bmp_image.h" image::ImageType type, image::Transparency transparency, image::Image *placeholder,
#endif uint32_t buffer_size, bool is_big_endian)
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height),
#include "jpeg_image.h" download_buffer_(buffer_size),
#endif download_buffer_initial_size_(buffer_size) {
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "png_image.h"
#endif
namespace esphome {
namespace online_image {
using image::ImageType;
inline bool is_color_on(const Color &color) {
// This produces the most accurate monochrome conversion, but is slightly slower.
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
// Approximation using fast integer computations; produces acceptable results
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
}
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
: Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr),
download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size),
format_(format),
fixed_width_(width),
fixed_height_(height),
is_big_endian_(is_big_endian) {
this->set_url(url); this->set_url(url);
} }
void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { bool OnlineImage::validate_url_(const std::string &url) {
if (this->data_start_) { if (url.empty()) {
Image::draw(x, y, display, color_on, color_off); ESP_LOGE(TAG, "URL is empty");
} else if (this->placeholder_) { return false;
this->placeholder_->draw(x, y, display, color_on, color_off);
} }
} if (url.length() > 2048) {
ESP_LOGE(TAG, "URL is too long");
void OnlineImage::release() { return false;
if (this->buffer_) {
ESP_LOGV(TAG, "Deallocating old buffer");
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
this->data_start_ = nullptr;
this->buffer_ = nullptr;
this->width_ = 0;
this->height_ = 0;
this->buffer_width_ = 0;
this->buffer_height_ = 0;
this->last_modified_ = "";
this->etag_ = "";
this->end_connection_();
} }
} if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
ESP_LOGE(TAG, "URL must start with http:// or https://");
size_t OnlineImage::resize_(int width_in, int height_in) { return false;
int width = this->fixed_width_;
int height = this->fixed_height_;
if (this->is_auto_resize_()) {
width = width_in;
height = height_in;
if (this->width_ != width && this->height_ != height) {
this->release();
} }
} return true;
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;
} }
void OnlineImage::update() { void OnlineImage::update() {
if (this->decoder_) { if (this->is_decoding()) {
ESP_LOGW(TAG, "Image already being updated."); ESP_LOGW(TAG, "Image already being updated.");
return; return;
} }
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
std::list<http_request::Header> headers = {}; if (!this->validate_url_(this->url_)) {
ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str());
http_request::Header accept_header; this->download_error_callback_.call();
accept_header.name = "Accept"; return;
std::string accept_mime_type;
switch (this->format_) {
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
case ImageFormat::BMP:
accept_mime_type = "image/bmp";
break;
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
case ImageFormat::JPEG:
accept_mime_type = "image/jpeg";
break;
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
case ImageFormat::PNG:
accept_mime_type = "image/png";
break;
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
default:
accept_mime_type = "image/*";
} }
accept_header.value = accept_mime_type + ",*/*;q=0.8";
ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str());
std::list<http_request::Header> headers;
// Add caching headers if we have them
if (!this->etag_.empty()) { if (!this->etag_.empty()) {
headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_}); headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_});
} }
if (!this->last_modified_.empty()) { if (!this->last_modified_.empty()) {
headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
} }
headers.push_back(accept_header); // Add Accept header based on image format
const char *accept_mime_type;
switch (this->get_format()) {
#ifdef USE_RUNTIME_IMAGE_BMP
case runtime_image::BMP:
accept_mime_type = "image/bmp,*/*;q=0.8";
break;
#endif
#ifdef USE_RUNTIME_IMAGE_JPEG
case runtime_image::JPEG:
accept_mime_type = "image/jpeg,*/*;q=0.8";
break;
#endif
#ifdef USE_RUNTIME_IMAGE_PNG
case runtime_image::PNG:
accept_mime_type = "image/png,*/*;q=0.8";
break;
#endif
default:
accept_mime_type = "image/*,*/*;q=0.8";
break;
}
headers.push_back({"Accept", accept_mime_type});
// User headers last so they can override any of the above
for (auto &header : this->request_headers_) { for (auto &header : this->request_headers_) {
headers.push_back(http_request::Header{header.first, header.second.value()}); headers.push_back(http_request::Header{header.first, header.second.value()});
} }
@@ -175,186 +115,117 @@ void OnlineImage::update() {
ESP_LOGD(TAG, "Starting download"); ESP_LOGD(TAG, "Starting download");
size_t total_size = this->downloader_->content_length; size_t total_size = this->downloader_->content_length;
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT // Initialize decoder with the known format
if (this->format_ == ImageFormat::BMP) { if (!this->begin_decode(total_size)) {
ESP_LOGD(TAG, "Allocating BMP decoder"); ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format());
this->decoder_ = make_unique<BmpDecoder>(this); this->end_connection_();
this->enable_loop(); this->download_error_callback_.call();
return;
} }
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
if (this->format_ == ImageFormat::JPEG) {
ESP_LOGD(TAG, "Allocating JPEG decoder");
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
this->enable_loop();
}
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
if (this->format_ == ImageFormat::PNG) {
ESP_LOGD(TAG, "Allocating PNG decoder");
this->decoder_ = make_unique<PngDecoder>(this);
this->enable_loop();
}
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
if (!this->decoder_) { // JPEG requires the complete image in the download buffer before decoding
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) {
this->end_connection_(); this->download_buffer_.resize(total_size);
this->download_error_callback_.call();
return;
}
auto prepare_result = this->decoder_->prepare(total_size);
if (prepare_result < 0) {
this->end_connection_();
this->download_error_callback_.call();
return;
} }
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = ::time(nullptr); this->start_time_ = ::time(nullptr);
this->enable_loop();
} }
void OnlineImage::loop() { void OnlineImage::loop() {
if (!this->decoder_) { if (!this->is_decoding()) {
// Not decoding at the moment => nothing to do. // Not decoding at the moment => nothing to do.
this->disable_loop(); this->disable_loop();
return; return;
} }
if (!this->downloader_ || this->decoder_->is_finished()) {
this->data_start_ = buffer_; if (!this->downloader_) {
this->width_ = buffer_width_;
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_));
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"); ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
return;
}
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
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.");
this->end_connection_(); this->end_connection_();
this->download_error_callback_.call(); this->download_error_callback_.call();
return; return;
} }
this->download_buffer_.read(fed);
}
}
}
void OnlineImage::map_chroma_key(Color &color) { // Check if download is complete — use decoder's format-specific completion check
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { // to handle both known content-length and chunked transfer encoding
if (color.g == 1 && color.r == 0 && color.b == 0) { if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
color.g = 0; this->downloader_->get_bytes_read() >= this->downloader_->content_length &&
} this->download_buffer_.unread() == 0)) {
if (color.w < 0x80) { // Finalize decoding
color.r = 0; this->end_decode();
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
color.b = 0;
}
}
}
void OnlineImage::draw_pixel_(int x, int y, Color color) { ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
if (!this->buffer_) { (uint32_t) (::time(nullptr) - this->start_time_));
ESP_LOGE(TAG, "Buffer not allocated!");
// 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; 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); // Download and decode more data
size_t available = this->download_buffer_.free_capacity();
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);
// 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;
}
if (consumed > 0) {
this->download_buffer_.read(consumed);
}
} else if (len < 0) {
ESP_LOGE(TAG, "Error downloading image: %d", len);
this->end_connection_();
this->download_error_callback_.call();
return; 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 { } else {
this->buffer_[pos] &= ~bitno; // Buffer is full, need to decode some data first
} auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
break; if (consumed > 0) {
} this->download_buffer_.read(consumed);
case ImageType::IMAGE_TYPE_GRAYSCALE: { } else if (consumed < 0) {
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { this->end_connection_();
if (gray == 1) { this->download_error_callback_.call();
gray = 0; return;
}
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 { } else {
this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF); // Decoder can't process more data, might need complete image
this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF); // This is normal for JPEG which needs complete data
} ESP_LOGV(TAG, "Decoder waiting for more data");
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w;
}
break;
}
case ImageType::IMAGE_TYPE_RGB: {
this->map_chroma_key(color);
this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
break;
} }
} }
} }
void OnlineImage::end_connection_() { void OnlineImage::end_connection_() {
// Abort any in-progress decode to free decoder resources.
// Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
if (this->is_decoding()) {
RuntimeImage::release();
}
if (this->downloader_) { if (this->downloader_) {
this->downloader_->end(); this->downloader_->end();
this->downloader_ = nullptr; this->downloader_ = nullptr;
} }
this->decoder_.reset();
this->download_buffer_.reset(); this->download_buffer_.reset();
} this->disable_loop();
bool OnlineImage::validate_url_(const std::string &url) {
if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
return false;
}
return true;
} }
void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) { void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
@@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
this->download_error_callback_.add(std::move(callback)); this->download_error_callback_.add(std::move(callback));
} }
} // namespace online_image void OnlineImage::release() {
} // namespace esphome // Clear cache headers
this->etag_ = "";
this->last_modified_ = "";
// End any active connection
this->end_connection_();
// Call parent's release to free the image buffer
RuntimeImage::release();
}
} // namespace esphome::online_image

View File

@@ -1,15 +1,14 @@
#pragma once #pragma once
#include "download_buffer.h"
#include "esphome/components/http_request/http_request.h" #include "esphome/components/http_request/http_request.h"
#include "esphome/components/image/image.h" #include "esphome/components/runtime_image/runtime_image.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "image_decoder.h" namespace esphome::online_image {
namespace esphome {
namespace online_image {
using t_http_codes = enum { using t_http_codes = enum {
HTTP_CODE_OK = 200, HTTP_CODE_OK = 200,
@@ -17,27 +16,13 @@ using t_http_codes = enum {
HTTP_CODE_NOT_FOUND = 404, HTTP_CODE_NOT_FOUND = 404,
}; };
/**
* @brief Format that the image is encoded with.
*/
enum ImageFormat {
/** Automatically detect from MIME type. Not supported yet. */
AUTO,
/** JPEG format. */
JPEG,
/** PNG format. */
PNG,
/** BMP format. */
BMP,
};
/** /**
* @brief Download an image from a given URL, and decode it using the specified decoder. * @brief Download an image from a given URL, and decode it using the specified decoder.
* The image will then be stored in a buffer, so that it can be re-displayed without the * The image will then be stored in a buffer, so that it can be re-displayed without the
* need to re-download or re-decode. * need to re-download or re-decode.
*/ */
class OnlineImage : public PollingComponent, class OnlineImage : public PollingComponent,
public image::Image, public runtime_image::RuntimeImage,
public Parented<esphome::http_request::HttpRequestComponent> { public Parented<esphome::http_request::HttpRequestComponent> {
public: public:
/** /**
@@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent,
* @param url URL to download the image from. * @param url URL to download the image from.
* @param width Desired width of the target image area. * @param width Desired width of the target image area.
* @param height Desired height of the target image area. * @param height Desired height of the target image area.
* @param format Format that the image is encoded in (@see ImageFormat). * @param format Format that the image is encoded in (@see runtime_image::ImageFormat).
* @param type The pixel format for the image.
* @param transparency The transparency type for the image.
* @param placeholder Optional placeholder image to show while loading.
* @param buffer_size Size of the buffer used to download the image. * @param buffer_size Size of the buffer used to download the image.
* @param is_big_endian Whether the image is stored in big-endian format.
*/ */
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type,
image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size,
bool is_big_endian = false);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void update() override; void update() override;
void loop() override; void loop() override;
void map_chroma_key(Color &color);
/** Set the URL to download the image from. */ /** Set the URL to download the image from. */
void set_url(const std::string &url) { void set_url(const std::string &url) {
@@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent,
/** Add the request header */ /** Add the request header */
template<typename V> void add_request_header(const std::string &header, V value) { template<typename V> void add_request_header(const std::string &header, V value) {
this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string> >(header, value)); this->request_headers_.push_back(std::pair<std::string, TemplatableValue<std::string>>(header, value));
} }
/**
* @brief Set the image that needs to be shown as long as the downloaded image
* is not available.
*
* @param placeholder Pointer to the (@link Image) to show as placeholder.
*/
void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; }
/** /**
* Release the buffer storing the image. The image will need to be downloaded again * Release the buffer storing the image. The image will need to be downloaded again
* to be able to be displayed. * to be able to be displayed.
*/ */
void release(); void release();
/**
* Resize the download buffer
*
* @param size The new size for the download buffer.
*/
size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); }
void add_on_finished_callback(std::function<void(bool)> &&callback); void add_on_finished_callback(std::function<void(bool)> &&callback);
void add_on_error_callback(std::function<void()> &&callback); void add_on_error_callback(std::function<void()> &&callback);
protected: protected:
bool validate_url_(const std::string &url); bool validate_url_(const std::string &url);
RAMAllocator<uint8_t> allocator_{};
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; }
int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
/**
* @brief Resize the image buffer to the requested dimensions.
*
* The buffer will be allocated if not existing.
* If the dimensions have been fixed in the yaml config, the buffer will be created
* with those dimensions and not resized, even on request.
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
* allocated
*
* @param width
* @param height
* @return 0 if no memory could be allocated, the size of the new buffer otherwise.
*/
size_t resize_(int width, int height);
/**
* @brief Draw a pixel into the buffer.
*
* This is used by the decoder to fill the buffer that will later be displayed
* by the `draw` method. This will internally convert the supplied 32 bit RGBA
* color into the requested image storage format.
*
* @param x Horizontal pixel position.
* @param y Vertical pixel position.
* @param color 32 bit color to put into the pixel.
*/
void draw_pixel_(int x, int y, Color color);
void end_connection_(); void end_connection_();
CallbackManager<void(bool)> download_finished_callback_{}; CallbackManager<void(bool)> download_finished_callback_{};
CallbackManager<void()> download_error_callback_{}; CallbackManager<void()> download_error_callback_{};
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr}; std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
std::unique_ptr<ImageDecoder> decoder_{nullptr};
uint8_t *buffer_;
DownloadBuffer download_buffer_; DownloadBuffer download_buffer_;
/** /**
* This is the *initial* size of the download buffer, not the current size. * This is the *initial* size of the download buffer, not the current size.
@@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent,
*/ */
size_t download_buffer_initial_size_; size_t download_buffer_initial_size_;
const ImageFormat format_;
image::Image *placeholder_{nullptr};
std::string url_{""}; std::string url_{""};
std::vector<std::pair<std::string, TemplatableValue<std::string> > > request_headers_; std::vector<std::pair<std::string, TemplatableValue<std::string>>> request_headers_;
/** width requested on configuration, or 0 if non specified. */
const int fixed_width_;
/** height requested on configuration, or 0 if non specified. */
const int fixed_height_;
/**
* Whether the image is stored in big-endian format.
* This is used to determine how to store 16 bit colors in the buffer.
*/
bool is_big_endian_;
/**
* Actual width of the current image. If fixed_width_ is specified,
* this will be equal to it; otherwise it will be set once the decoding
* starts and the original size is known.
* This needs to be separate from "BaseImage::get_width()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images).
*/
int buffer_width_;
/**
* Actual height of the current image. If fixed_height_ is specified,
* this will be equal to it; otherwise it will be set once the decoding
* starts and the original size is known.
* This needs to be separate from "BaseImage::get_height()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images).
*/
int buffer_height_;
/** /**
* The value of the ETag HTTP header provided in the last response. * The value of the ETag HTTP header provided in the last response.
*/ */
@@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent,
std::string last_modified_ = ""; std::string last_modified_ = "";
time_t start_time_; time_t start_time_;
friend bool ImageDecoder::set_size(int width, int height);
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
}; };
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
@@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> {
} }
}; };
} // namespace online_image } // namespace esphome::online_image
} // namespace esphome

View File

@@ -76,7 +76,7 @@ class PN532 : public PollingComponent {
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid); std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); bool write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid); bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid); bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
@@ -88,7 +88,7 @@ class PN532 : public PollingComponent {
uint16_t read_mifare_ultralight_capacity_(); uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); bool write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool clean_mifare_ultralight_(); bool clean_mifare_ultralight_();

View File

@@ -1,3 +1,4 @@
#include <array>
#include <memory> #include <memory>
#include "pn532.h" #include "pn532.h"
@@ -106,10 +107,10 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u
} }
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> blank_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> trailer_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
bool error = false; bool error = false;
@@ -118,20 +119,20 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
continue; continue;
} }
if (block != 0) { if (block != 0) {
if (!this->write_mifare_classic_block_(block, blank_buffer)) { if (!this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block); ESP_LOGE(TAG, "Unable to write block %d", block);
error = true; error = true;
} }
} }
if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { if (!this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block + 1); ESP_LOGE(TAG, "Unable to write block %d", block + 1);
error = true; error = true;
} }
if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { if (!this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block + 2); ESP_LOGE(TAG, "Unable to write block %d", block + 2);
error = true; error = true;
} }
if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { if (!this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block + 3); ESP_LOGE(TAG, "Unable to write block %d", block + 3);
error = true; error = true;
} }
@@ -141,28 +142,28 @@ bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
} }
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> empty_ndef_message( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> blank_block( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> block_1_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_2_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_3_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
std::vector<uint8_t> ndef_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) {
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!");
return false; return false;
} }
if (!this->write_mifare_classic_block_(1, block_1_data)) if (!this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()))
return false; return false;
if (!this->write_mifare_classic_block_(2, block_2_data)) if (!this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()))
return false; return false;
if (!this->write_mifare_classic_block_(3, block_3_trailer)) if (!this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()))
return false; return false;
ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); ESP_LOGD(TAG, "Sector 0 formatted to NDEF");
@@ -172,36 +173,36 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
return false; return false;
} }
if (block == 4) { if (block == 4) {
if (!this->write_mifare_classic_block_(block, empty_ndef_message)) { if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block); ESP_LOGE(TAG, "Unable to write block %d", block);
} }
} else { } else {
if (!this->write_mifare_classic_block_(block, blank_block)) { if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block); ESP_LOGE(TAG, "Unable to write block %d", block);
} }
} }
if (!this->write_mifare_classic_block_(block + 1, blank_block)) { if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block + 1); ESP_LOGE(TAG, "Unable to write block %d", block + 1);
} }
if (!this->write_mifare_classic_block_(block + 2, blank_block)) { if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) {
ESP_LOGE(TAG, "Unable to write block %d", block + 2); ESP_LOGE(TAG, "Unable to write block %d", block + 2);
} }
if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) { if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) {
ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3);
} }
} }
return true; return true;
} }
bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) { bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
std::vector<uint8_t> data({ std::vector<uint8_t> cmd({
PN532_COMMAND_INDATAEXCHANGE, PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card 0x01, // One card
nfc::MIFARE_CMD_WRITE, nfc::MIFARE_CMD_WRITE,
block_num, block_num,
}); });
data.insert(data.end(), write_data.begin(), write_data.end()); cmd.insert(cmd.end(), data, data + len);
if (!this->write_command_(data)) { if (!this->write_command_(cmd)) {
ESP_LOGE(TAG, "Error writing block %d", block_num); ESP_LOGE(TAG, "Error writing block %d", block_num);
return false; return false;
} }
@@ -243,8 +244,7 @@ bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *mes
} }
} }
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); if (!this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE)) {
if (!this->write_mifare_classic_block_(current_block, data)) {
return false; return false;
} }
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;

View File

@@ -1,3 +1,4 @@
#include <array>
#include <memory> #include <memory>
#include "pn532.h" #include "pn532.h"
@@ -143,8 +144,7 @@ bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
while (index < buffer_length) { while (index < buffer_length) {
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); if (!this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE)) {
if (!this->write_mifare_ultralight_page_(current_page, data)) {
return false; return false;
} }
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
@@ -157,25 +157,25 @@ bool PN532::clean_mifare_ultralight_() {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00}; static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
if (!this->write_mifare_ultralight_page_(i, blank_data)) { if (!this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size())) {
return false; return false;
} }
} }
return true; return true;
} }
bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) { bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
std::vector<uint8_t> data({ std::vector<uint8_t> cmd({
PN532_COMMAND_INDATAEXCHANGE, PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card 0x01, // One card
nfc::MIFARE_CMD_WRITE_ULTRALIGHT, nfc::MIFARE_CMD_WRITE_ULTRALIGHT,
page_num, page_num,
}); });
data.insert(data.end(), write_data.begin(), write_data.end()); cmd.insert(cmd.end(), write_data, write_data + len);
if (!this->write_command_(data)) { if (!this->write_command_(cmd)) {
ESP_LOGE(TAG, "Error writing page %u", page_num); ESP_LOGE(TAG, "Error writing page %u", page_num);
return false; return false;
} }

View File

@@ -236,7 +236,7 @@ class PN7150 : public nfc::Nfcc, public Component {
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
uint8_t sect_to_auth_(uint8_t block_num); uint8_t sect_to_auth_(uint8_t block_num);
uint8_t format_mifare_classic_mifare_(); uint8_t format_mifare_classic_mifare_();
@@ -250,7 +250,7 @@ class PN7150 : public nfc::Nfcc, public Component {
uint16_t read_mifare_ultralight_capacity_(); uint16_t read_mifare_ultralight_capacity_();
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_(); uint8_t clean_mifare_ultralight_();

View File

@@ -1,3 +1,4 @@
#include <array>
#include <memory> #include <memory>
#include "pn7150.h" #include "pn7150.h"
@@ -139,10 +140,10 @@ uint8_t PN7150::sect_to_auth_(const uint8_t block_num) {
} }
uint8_t PN7150::format_mifare_classic_mifare_() { uint8_t PN7150::format_mifare_classic_mifare_() {
std::vector<uint8_t> blank_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> trailer_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
auto status = nfc::STATUS_OK; auto status = nfc::STATUS_OK;
@@ -151,20 +152,20 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
continue; continue;
} }
if (block != 0) { if (block != 0) {
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} }
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 1); ESP_LOGE(TAG, "Unable to write block %u", block + 1);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 2); ESP_LOGE(TAG, "Unable to write block %u", block + 2);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 3); ESP_LOGE(TAG, "Unable to write block %u", block + 3);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
@@ -174,30 +175,30 @@ uint8_t PN7150::format_mifare_classic_mifare_() {
} }
uint8_t PN7150::format_mifare_classic_ndef_() { uint8_t PN7150::format_mifare_classic_ndef_() {
std::vector<uint8_t> empty_ndef_message( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> blank_block( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> block_1_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_2_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_3_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
std::vector<uint8_t> ndef_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
@@ -210,25 +211,26 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (block == 4) { if (block == 4) {
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} else { } else {
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} }
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 1); ESP_LOGE(TAG, "Unable to write block %u", block + 1);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 2); ESP_LOGE(TAG, "Unable to write block %u", block + 2);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
@@ -236,7 +238,7 @@ uint8_t PN7150::format_mifare_classic_ndef_() {
return status; return status;
} }
uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) { uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
nfc::NciMessage rx; nfc::NciMessage rx;
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
@@ -248,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8
} }
// write command part two // write command part two
tx.set_payload({XCHG_DATA_OID}); tx.set_payload({XCHG_DATA_OID});
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); tx.get_message().insert(tx.get_message().end(), data, data + len);
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message())); ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
@@ -294,8 +296,8 @@ uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
} }
} }
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;

View File

@@ -1,3 +1,4 @@
#include <array>
#include <cinttypes> #include <cinttypes>
#include <memory> #include <memory>
@@ -144,8 +145,8 @@ uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
while (index < buffer_length) { while (index < buffer_length) {
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
@@ -158,19 +159,19 @@ uint8_t PN7150::clean_mifare_ultralight_() {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00}; static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
} }
return nfc::STATUS_OK; return nfc::STATUS_OK;
} }
uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) { uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
payload.insert(payload.end(), write_data.begin(), write_data.end()); payload.insert(payload.end(), write_data, write_data + len);
nfc::NciMessage rx; nfc::NciMessage rx;
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);

View File

@@ -253,7 +253,7 @@ class PN7160 : public nfc::Nfcc, public Component {
uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag);
uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); uint8_t write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len);
uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key);
uint8_t sect_to_auth_(uint8_t block_num); uint8_t sect_to_auth_(uint8_t block_num);
uint8_t format_mifare_classic_mifare_(); uint8_t format_mifare_classic_mifare_();
@@ -267,7 +267,7 @@ class PN7160 : public nfc::Nfcc, public Component {
uint16_t read_mifare_ultralight_capacity_(); uint16_t read_mifare_ultralight_capacity_();
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); uint8_t write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_(); uint8_t clean_mifare_ultralight_();

View File

@@ -1,3 +1,4 @@
#include <array>
#include <memory> #include <memory>
#include "pn7160.h" #include "pn7160.h"
@@ -139,10 +140,10 @@ uint8_t PN7160::sect_to_auth_(const uint8_t block_num) {
} }
uint8_t PN7160::format_mifare_classic_mifare_() { uint8_t PN7160::format_mifare_classic_mifare_() {
std::vector<uint8_t> blank_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BUFFER = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> trailer_buffer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> TRAILER_BUFFER = {
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
auto status = nfc::STATUS_OK; auto status = nfc::STATUS_OK;
@@ -151,20 +152,20 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
continue; continue;
} }
if (block != 0) { if (block != 0) {
if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} }
if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 1, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 1); ESP_LOGE(TAG, "Unable to write block %u", block + 1);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 2, BLANK_BUFFER.data(), BLANK_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 2); ESP_LOGE(TAG, "Unable to write block %u", block + 2);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 3, TRAILER_BUFFER.data(), TRAILER_BUFFER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 3); ESP_LOGE(TAG, "Unable to write block %u", block + 3);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
@@ -174,30 +175,30 @@ uint8_t PN7160::format_mifare_classic_mifare_() {
} }
uint8_t PN7160::format_mifare_classic_ndef_() { uint8_t PN7160::format_mifare_classic_ndef_() {
std::vector<uint8_t> empty_ndef_message( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> EMPTY_NDEF_MESSAGE = {
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> blank_block( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLANK_BLOCK = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
std::vector<uint8_t> block_1_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_1_DATA = {
{0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_2_data( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_2_DATA = {
{0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1};
std::vector<uint8_t> block_3_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> BLOCK_3_TRAILER = {
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
std::vector<uint8_t> ndef_trailer( static constexpr std::array<uint8_t, nfc::MIFARE_CLASSIC_BLOCK_SIZE> NDEF_TRAILER = {
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); 0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting");
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(1, BLOCK_1_DATA.data(), BLOCK_1_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(2, BLOCK_2_DATA.data(), BLOCK_2_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(3, BLOCK_3_TRAILER.data(), BLOCK_3_TRAILER.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
@@ -210,25 +211,26 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
if (block == 4) { if (block == 4) {
if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size()) !=
nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} else { } else {
if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block); ESP_LOGE(TAG, "Unable to write block %u", block);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
} }
if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 1); ESP_LOGE(TAG, "Unable to write block %u", block + 1);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write block %u", block + 2); ESP_LOGE(TAG, "Unable to write block %u", block + 2);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { if (this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size()) != nfc::STATUS_OK) {
ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3);
status = nfc::STATUS_FAILED; status = nfc::STATUS_FAILED;
} }
@@ -236,7 +238,7 @@ uint8_t PN7160::format_mifare_classic_ndef_() {
return status; return status;
} }
uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &write_data) { uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) {
nfc::NciMessage rx; nfc::NciMessage rx;
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num});
char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; char buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
@@ -248,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8
} }
// write command part two // write command part two
tx.set_payload({XCHG_DATA_OID}); tx.set_payload({XCHG_DATA_OID});
tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); tx.get_message().insert(tx.get_message().end(), data, data + len);
ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message())); ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes_to(buf, tx.get_message()));
if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) {
@@ -294,8 +296,8 @@ uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr<nfc::NdefMessage
} }
} }
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); if (this->write_mifare_classic_block_(current_block, encoded.data() + index, nfc::MIFARE_CLASSIC_BLOCK_SIZE) !=
if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; index += nfc::MIFARE_CLASSIC_BLOCK_SIZE;

View File

@@ -1,3 +1,4 @@
#include <array>
#include <cinttypes> #include <cinttypes>
#include <memory> #include <memory>
@@ -144,8 +145,8 @@ uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::sha
uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
while (index < buffer_length) { while (index < buffer_length) {
std::vector<uint8_t> data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); if (this->write_mifare_ultralight_page_(current_page, encoded.data() + index, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) !=
if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
@@ -158,19 +159,19 @@ uint8_t PN7160::clean_mifare_ultralight_() {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
std::vector<uint8_t> blank_data = {0x00, 0x00, 0x00, 0x00}; static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { if (this->write_mifare_ultralight_page_(i, BLANK_DATA.data(), BLANK_DATA.size()) != nfc::STATUS_OK) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
} }
return nfc::STATUS_OK; return nfc::STATUS_OK;
} }
uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data) { uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write_data, size_t len) {
std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; std::vector<uint8_t> payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num};
payload.insert(payload.end(), write_data.begin(), write_data.end()); payload.insert(payload.end(), write_data, write_data + len);
nfc::NciMessage rx; nfc::NciMessage rx;
nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload);

View File

@@ -119,6 +119,8 @@ class RemoteComponentBase {
}; };
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <soc/soc_caps.h>
#if SOC_RMT_SUPPORTED
class RemoteRMTChannel { class RemoteRMTChannel {
public: public:
void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; }
@@ -137,7 +139,8 @@ class RemoteRMTChannel {
uint32_t clock_resolution_{1000000}; uint32_t clock_resolution_{1000000};
uint32_t rmt_symbols_; uint32_t rmt_symbols_;
}; };
#endif #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
class RemoteTransmitterBase : public RemoteComponentBase { class RemoteTransmitterBase : public RemoteComponentBase {
public: public:

View File

@@ -65,6 +65,8 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
def validate_config(config): def validate_config(config):
if CORE.is_esp32: if CORE.is_esp32:
variant = esp32.get_esp32_variant() variant = esp32.get_esp32_variant()
if variant in esp32_rmt.VARIANTS_NO_RMT:
return config
if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2): if variant in (esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2):
max_idle = 65535 max_idle = 65535
else: else:
@@ -110,6 +112,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.SplitDefault( cv.SplitDefault(
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
esp32="10000b", esp32="10000b",
esp32_c2="1000b",
esp32_c61="1000b",
esp8266="1000b", esp8266="1000b",
bk72xx="1000b", bk72xx="1000b",
ln882x="1000b", ln882x="1000b",
@@ -131,9 +135,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.SplitDefault( cv.SplitDefault(
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
esp32=192, esp32=192,
esp32_c2=cv.UNDEFINED,
esp32_c3=96, esp32_c3=96,
esp32_c5=96, esp32_c5=96,
esp32_c6=96, esp32_c6=96,
esp32_c61=cv.UNDEFINED,
esp32_h2=96, esp32_h2=96,
esp32_p4=192, esp32_p4=192,
esp32_s2=192, esp32_s2=192,
@@ -145,6 +151,8 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.SplitDefault( cv.SplitDefault(
CONF_RECEIVE_SYMBOLS, CONF_RECEIVE_SYMBOLS,
esp32=192, esp32=192,
esp32_c2=cv.UNDEFINED,
esp32_c61=cv.UNDEFINED,
): cv.All(cv.only_on_esp32, cv.int_range(min=2)), ): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
cv.Optional(CONF_USE_DMA): cv.All( cv.Optional(CONF_USE_DMA): cv.All(
esp32.only_on_variant( esp32.only_on_variant(
@@ -152,24 +160,45 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
), ),
cv.boolean, cv.boolean,
), ),
cv.SplitDefault(CONF_CARRIER_DUTY_PERCENT, esp32=100): cv.All( cv.SplitDefault(
CONF_CARRIER_DUTY_PERCENT,
esp32=100,
esp32_c2=cv.UNDEFINED,
esp32_c61=cv.UNDEFINED,
): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
cv.percentage_int, cv.percentage_int,
cv.Range(min=1, max=100), cv.Range(min=1, max=100),
), ),
cv.SplitDefault(CONF_CARRIER_FREQUENCY, esp32="0Hz"): cv.All( cv.SplitDefault(
cv.only_on_esp32, cv.frequency, cv.int_ CONF_CARRIER_FREQUENCY,
), esp32="0Hz",
esp32_c2=cv.UNDEFINED,
esp32_c61=cv.UNDEFINED,
): cv.All(cv.only_on_esp32, cv.frequency, cv.int_),
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.add_extra(
esp32_rmt.validate_rmt_not_supported(
[
CONF_CLOCK_RESOLUTION,
CONF_USE_DMA,
CONF_RMT_SYMBOLS,
CONF_FILTER_SYMBOLS,
CONF_RECEIVE_SYMBOLS,
CONF_CARRIER_DUTY_PERCENT,
CONF_CARRIER_FREQUENCY,
]
)
)
.add_extra(validate_config) .add_extra(validate_config)
) )
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt") esp32.include_builtin_idf_component("esp_driver_rmt")
@@ -213,6 +242,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_IDF,
}, },
"remote_receiver.cpp": { "remote_receiver.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP8266_ARDUINO, PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.BK72XX_ARDUINO, PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,

View File

@@ -3,7 +3,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
namespace esphome::remote_receiver { namespace esphome::remote_receiver {

View File

@@ -6,12 +6,15 @@
#include <cinttypes> #include <cinttypes>
#if defined(USE_ESP32) #if defined(USE_ESP32)
#include <soc/soc_caps.h>
#if SOC_RMT_SUPPORTED
#include <driver/rmt_rx.h> #include <driver/rmt_rx.h>
#endif #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
namespace esphome::remote_receiver { namespace esphome::remote_receiver {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
struct RemoteReceiverComponentStore { struct RemoteReceiverComponentStore {
static void gpio_intr(RemoteReceiverComponentStore *arg); static void gpio_intr(RemoteReceiverComponentStore *arg);
@@ -35,7 +38,7 @@ struct RemoteReceiverComponentStore {
volatile bool prev_level{false}; volatile bool prev_level{false};
volatile bool overflow{false}; volatile bool overflow{false};
}; };
#elif defined(USE_ESP32) #elif defined(USE_ESP32) && SOC_RMT_SUPPORTED
struct RemoteReceiverComponentStore { struct RemoteReceiverComponentStore {
/// Stores RMT symbols and rx done event data /// Stores RMT symbols and rx done event data
volatile uint8_t *buffer{nullptr}; volatile uint8_t *buffer{nullptr};
@@ -54,7 +57,7 @@ struct RemoteReceiverComponentStore {
class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
public Component public Component
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
, ,
public remote_base::RemoteRMTChannel public remote_base::RemoteRMTChannel
#endif #endif
@@ -66,7 +69,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; }
void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; }
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
@@ -78,7 +81,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; }
protected: protected:
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); void decode_rmt_(rmt_symbol_word_t *item, size_t item_count);
rmt_channel_handle_t channel_{NULL}; rmt_channel_handle_t channel_{NULL};
uint32_t filter_symbols_{0}; uint32_t filter_symbols_{0};
@@ -94,7 +97,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
RemoteReceiverComponentStore store_; RemoteReceiverComponentStore store_;
#endif #endif
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
HighFrequencyLoopRequester high_freq_; HighFrequencyLoopRequester high_freq_;
#endif #endif

View File

@@ -2,6 +2,8 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <soc/soc_caps.h>
#if SOC_RMT_SUPPORTED
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_clk_tree.h> #include <esp_clk_tree.h>
@@ -248,4 +250,5 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c
} // namespace esphome::remote_receiver } // namespace esphome::remote_receiver
#endif #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32

View File

@@ -40,8 +40,10 @@ DigitalWriteAction = remote_transmitter_ns.class_(
cg.Parented.template(RemoteTransmitterComponent), cg.Parented.template(RemoteTransmitterComponent),
) )
MULTI_CONF = True MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = (
cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent), cv.GenerateID(): cv.declare_id(RemoteTransmitterComponent),
cv.Required(CONF_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
@@ -62,9 +64,11 @@ CONFIG_SCHEMA = cv.Schema(
cv.SplitDefault( cv.SplitDefault(
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
esp32=64, esp32=64,
esp32_c2=cv.UNDEFINED,
esp32_c3=48, esp32_c3=48,
esp32_c5=48, esp32_c5=48,
esp32_c6=48, esp32_c6=48,
esp32_c61=cv.UNDEFINED,
esp32_h2=48, esp32_h2=48,
esp32_p4=48, esp32_p4=48,
esp32_s2=64, esp32_s2=64,
@@ -74,11 +78,28 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
} }
).extend(cv.COMPONENT_SCHEMA) )
.extend(cv.COMPONENT_SCHEMA)
.add_extra(
esp32_rmt.validate_rmt_not_supported(
[
CONF_CLOCK_RESOLUTION,
CONF_EOT_LEVEL,
CONF_USE_DMA,
CONF_RMT_SYMBOLS,
CONF_NON_BLOCKING,
]
)
)
)
def _validate_non_blocking(config): def _validate_non_blocking(config):
if CORE.is_esp32 and CONF_NON_BLOCKING not in config: if (
CORE.is_esp32
and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT
and CONF_NON_BLOCKING not in config
):
_LOGGER.warning( _LOGGER.warning(
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n" "The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
@@ -111,7 +132,7 @@ async def digital_write_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32 and esp32.get_esp32_variant() not in esp32_rmt.VARIANTS_NO_RMT:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt") esp32.include_builtin_idf_component("esp_driver_rmt")
@@ -155,6 +176,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_IDF,
}, },
"remote_transmitter.cpp": { "remote_transmitter.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP8266_ARDUINO, PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.BK72XX_ARDUINO, PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,

View File

@@ -5,8 +5,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome::remote_transmitter {
namespace remote_transmitter {
template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> { template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> {
public: public:
@@ -14,5 +13,4 @@ template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public
void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); }
}; };
} // namespace remote_transmitter } // namespace esphome::remote_transmitter
} // namespace esphome

View File

@@ -2,10 +2,9 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
namespace esphome { namespace esphome::remote_transmitter {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter"; static const char *const TAG = "remote_transmitter";
@@ -105,7 +104,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
this->complete_trigger_.trigger(); this->complete_trigger_.trigger();
} }
} // namespace remote_transmitter } // namespace esphome::remote_transmitter
} // namespace esphome
#endif #endif

View File

@@ -6,13 +6,15 @@
#include <vector> #include <vector>
#if defined(USE_ESP32) #if defined(USE_ESP32)
#include <soc/soc_caps.h>
#if SOC_RMT_SUPPORTED
#include <driver/rmt_tx.h> #include <driver/rmt_tx.h>
#endif #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
namespace esphome { namespace esphome::remote_transmitter {
namespace remote_transmitter {
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// IDF version 5.5.1 and above is required because of a bug in // IDF version 5.5.1 and above is required because of a bug in
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
@@ -33,7 +35,7 @@ struct RemoteTransmitterComponentStore {
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
public Component public Component
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
, ,
public remote_base::RemoteRMTChannel public remote_base::RemoteRMTChannel
#endif #endif
@@ -51,7 +53,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
void digital_write(bool value); void digital_write(bool value);
#if defined(USE_ESP32) #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
@@ -62,7 +64,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
protected: protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override; void send_internal(uint32_t send_times, uint32_t send_wait) override;
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);
@@ -73,7 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
uint32_t target_time_; uint32_t target_time_;
#endif #endif
#ifdef USE_ESP32 #if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void configure_rmt_(); void configure_rmt_();
void wait_for_rmt_(); void wait_for_rmt_();
@@ -100,5 +102,4 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
Trigger<> complete_trigger_; Trigger<> complete_trigger_;
}; };
} // namespace remote_transmitter } // namespace esphome::remote_transmitter
} // namespace esphome

View File

@@ -3,10 +3,11 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <soc/soc_caps.h>
#if SOC_RMT_SUPPORTED
#include <driver/gpio.h> #include <driver/gpio.h>
namespace esphome { namespace esphome::remote_transmitter {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter"; static const char *const TAG = "remote_transmitter";
@@ -358,7 +359,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
} }
#endif #endif
} // namespace remote_transmitter } // namespace esphome::remote_transmitter
} // namespace esphome
#endif #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32

View File

@@ -0,0 +1,191 @@
from dataclasses import dataclass
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
from esphome.components.image import (
IMAGE_TYPE,
Image_,
validate_settings,
validate_transparency,
validate_type,
)
import esphome.config_validation as cv
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
AUTO_LOAD = ["image"]
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
CONF_PLACEHOLDER = "placeholder"
CONF_TRANSPARENCY = "transparency"
runtime_image_ns = cg.esphome_ns.namespace("runtime_image")
# Base decoder classes
ImageDecoder = runtime_image_ns.class_("ImageDecoder")
BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder)
JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder)
PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder)
# Runtime image class
RuntimeImage = runtime_image_ns.class_(
"RuntimeImage", cg.esphome_ns.namespace("image").class_("Image")
)
# Image format enum
ImageFormat = runtime_image_ns.enum("ImageFormat")
IMAGE_FORMAT_AUTO = ImageFormat.AUTO
IMAGE_FORMAT_JPEG = ImageFormat.JPEG
IMAGE_FORMAT_PNG = ImageFormat.PNG
IMAGE_FORMAT_BMP = ImageFormat.BMP
# Export enum for decode errors
DecodeError = runtime_image_ns.enum("DecodeError")
DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE
DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT
DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY
class Format:
"""Base class for image format definitions."""
def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None:
self.name = name
self.decoder_class = decoder_class
def actions(self) -> None:
"""Add defines and libraries needed for this format."""
class BMPFormat(Format):
"""BMP format decoder configuration."""
def __init__(self):
super().__init__("BMP", BmpDecoder)
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_BMP")
class JPEGFormat(Format):
"""JPEG format decoder configuration."""
def __init__(self):
super().__init__("JPEG", JpegDecoder)
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
class PNGFormat(Format):
"""PNG format decoder configuration."""
def __init__(self):
super().__init__("PNG", PngDecoder)
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_PNG")
cg.add_library("pngle", "1.1.0")
# Registry of available formats
IMAGE_FORMATS = {
"BMP": BMPFormat(),
"JPEG": JPEGFormat(),
"PNG": PNGFormat(),
"JPG": JPEGFormat(), # Alias for JPEG
}
def get_format(format_name: str) -> Format | None:
"""Get a format instance by name."""
return IMAGE_FORMATS.get(format_name.upper())
def enable_format(format_name: str) -> Format | None:
"""Enable a specific image format by adding its defines and libraries."""
format_obj = get_format(format_name)
if format_obj:
format_obj.actions()
return format_obj
return None
# Runtime image configuration schema base - to be extended by components
def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema:
"""Create a runtime image schema with the specified image class."""
return cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(image_class),
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
"BIG_ENDIAN", "LITTLE_ENDIAN", upper=True
),
cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
}
)
def validate_runtime_image_settings(config: dict) -> dict:
"""Apply validate_settings from image component to runtime image config."""
return validate_settings(config)
@dataclass
class RuntimeImageSettings:
"""Processed runtime image configuration parameters."""
width: int
height: int
format_enum: cg.MockObj
image_type_enum: cg.MockObj
transparent: cg.MockObj
byte_order_big_endian: bool
placeholder: cg.MockObj | None
async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
"""
Helper function to process common runtime image configuration parameters.
Handles format enabling and returns all necessary enums and parameters.
"""
from esphome.components.image import get_image_type_enum, get_transparency_enum
# Get resize dimensions with default (0, 0)
width, height = config.get(CONF_RESIZE, (0, 0))
# Handle format (required for runtime images)
format_name = config[CONF_FORMAT]
# Enable the format in the runtime_image component
enable_format(format_name)
# Map format names to enum values (handle JPG as alias for JPEG)
if format_name.upper() == "JPG":
format_name = "JPEG"
format_enum = getattr(ImageFormat, format_name.upper())
# Get image type enum
image_type_enum = get_image_type_enum(config[CONF_TYPE])
# Get transparency enum
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
# Get byte order (True for big endian, False for little endian)
byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN"
# Get placeholder if specified
placeholder = None
if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id)
return RuntimeImageSettings(
width=width,
height=height,
format_enum=format_enum,
image_type_enum=image_type_enum,
transparent=transparent,
byte_order_big_endian=byte_order_big_endian,
placeholder=placeholder,
)

View File

@@ -1,15 +1,14 @@
#include "bmp_image.h" #include "bmp_decoder.h"
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_RUNTIME_IMAGE_BMP
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
static const char *const TAG = "online_image.bmp"; static const char *const TAG = "image_decoder.bmp";
int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
size_t index = 0; size_t index = 0;
@@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
return DECODE_ERROR_INVALID_TYPE; return DECODE_ERROR_INVALID_TYPE;
} }
this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); // BMP file contains its own size in the header
size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]);
if (this->expected_size_ == 0) {
this->expected_size_ = file_size; // Use file header size if not provided
}
this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]);
this->current_index_ = 14; this->current_index_ = 14;
@@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
while (index < size) { while (index < size) {
uint8_t current_byte = buffer[index]; uint8_t current_byte = buffer[index];
for (uint8_t i = 0; i < 8; i++) { for (uint8_t i = 0; i < 8; i++) {
size_t x = (this->paint_index_ % this->width_) + i; size_t x = (this->paint_index_ % static_cast<size_t>(this->width_)) + i;
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
this->draw(x, y, 1, 1, c); this->draw(x, y, 1, 1, c);
} }
@@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
uint8_t b = buffer[index]; uint8_t b = buffer[index];
uint8_t g = buffer[index + 1]; uint8_t g = buffer[index + 1];
uint8_t r = buffer[index + 2]; uint8_t r = buffer[index + 2];
size_t x = this->paint_index_ % this->width_; size_t x = this->paint_index_ % static_cast<size_t>(this->width_);
size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / static_cast<size_t>(this->width_));
Color c = Color(r, g, b); Color c = Color(r, g, b);
this->draw(x, y, 1, 1, c); this->draw(x, y, 1, 1, c);
this->paint_index_++; this->paint_index_++;
@@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
return size; return size;
}; };
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT #endif // USE_RUNTIME_IMAGE_BMP

View File

@@ -1,27 +1,32 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_RUNTIME_IMAGE_BMP
#include "image_decoder.h" #include "image_decoder.h"
#include "runtime_image.h"
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
/** /**
* @brief Image decoder specialization for PNG images. * @brief Image decoder specialization for BMP images.
*/ */
class BmpDecoder : public ImageDecoder { class BmpDecoder : public ImageDecoder {
public: public:
/** /**
* @brief Construct a new BMP Decoder object. * @brief Construct a new BMP Decoder object.
* *
* @param display The image to decode the stream into. * @param image The RuntimeImage to decode the stream into.
*/ */
BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {}
int HOT decode(uint8_t *buffer, size_t size) override; int HOT decode(uint8_t *buffer, size_t size) override;
bool is_finished() const override {
// BMP is finished when we've decoded all pixel data
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
}
protected: protected:
size_t current_index_{0}; size_t current_index_{0};
size_t paint_index_{0}; size_t paint_index_{0};
@@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder {
uint8_t padding_bytes_{0}; uint8_t padding_bytes_{0};
}; };
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_BMP_SUPPORT #endif // USE_RUNTIME_IMAGE_BMP

View File

@@ -0,0 +1,28 @@
#include "image_decoder.h"
#include "runtime_image.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
namespace esphome::runtime_image {
static const char *const TAG = "image_decoder";
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize(width, height) > 0;
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
return success;
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) {
for (int j = y * this->y_scale_; j < height; j++) {
this->image_->draw_pixel(i, j, color);
}
}
}
} // namespace esphome::runtime_image

View File

@@ -1,8 +1,7 @@
#pragma once #pragma once
#include "esphome/core/color.h" #include "esphome/core/color.h"
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
enum DecodeError : int { enum DecodeError : int {
DECODE_ERROR_INVALID_TYPE = -1, DECODE_ERROR_INVALID_TYPE = -1,
@@ -10,7 +9,7 @@ enum DecodeError : int {
DECODE_ERROR_OUT_OF_MEMORY = -3, DECODE_ERROR_OUT_OF_MEMORY = -3,
}; };
class OnlineImage; class RuntimeImage;
/** /**
* @brief Class to abstract decoding different image formats. * @brief Class to abstract decoding different image formats.
@@ -20,19 +19,19 @@ class ImageDecoder {
/** /**
* @brief Construct a new Image Decoder object * @brief Construct a new Image Decoder object
* *
* @param image The image to decode the stream into. * @param image The RuntimeImage to decode the stream into.
*/ */
ImageDecoder(OnlineImage *image) : image_(image) {} ImageDecoder(RuntimeImage *image) : image_(image) {}
virtual ~ImageDecoder() = default; virtual ~ImageDecoder() = default;
/** /**
* @brief Initialize the decoder. * @brief Initialize the decoder.
* *
* @param download_size The total number of bytes that need to be downloaded for the image. * @param expected_size Hint about the expected data size (0 if unknown).
* @return int Returns 0 on success, a {@see DecodeError} value in case of an error. * @return int Returns 0 on success, a {@see DecodeError} value in case of an error.
*/ */
virtual int prepare(size_t download_size) { virtual int prepare(size_t expected_size) {
this->download_size_ = download_size; this->expected_size_ = expected_size;
return 0; return 0;
} }
@@ -73,49 +72,26 @@ class ImageDecoder {
*/ */
void draw(int x, int y, int w, int h, const Color &color); void draw(int x, int y, int w, int h, const Color &color);
bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } /**
* @brief Check if the decoder has finished processing.
*
* This should be overridden by decoders that can detect completion
* based on format-specific markers rather than byte counts.
*/
virtual bool is_finished() const {
if (this->expected_size_ > 0) {
return this->decoded_bytes_ >= this->expected_size_;
}
// If size is unknown, derived classes should override this
return false;
}
protected: protected:
OnlineImage *image_; RuntimeImage *image_;
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". size_t expected_size_ = 0; // Expected data size (0 if unknown)
// Will be overwritten anyway once the download size is known. size_t decoded_bytes_ = 0; // Bytes processed so far
size_t download_size_ = 1;
size_t decoded_bytes_ = 0;
double x_scale_ = 1.0; double x_scale_ = 1.0;
double y_scale_ = 1.0; double y_scale_ = 1.0;
}; };
class DownloadBuffer { } // namespace esphome::runtime_image
public:
DownloadBuffer(size_t size);
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
uint8_t *data(size_t offset = 0);
uint8_t *append() { return this->data(this->unread_); }
size_t unread() const { return this->unread_; }
size_t size() const { return this->size_; }
size_t free_capacity() const { return this->size_ - this->unread_; }
size_t read(size_t len);
size_t write(size_t len) {
this->unread_ += len;
return this->unread_;
}
void reset() { this->unread_ = 0; }
size_t resize(size_t size);
protected:
RAMAllocator<uint8_t> allocator_{};
uint8_t *buffer_;
size_t size_;
/** Total number of downloaded bytes not yet read. */
size_t unread_;
};
} // namespace online_image
} // namespace esphome

View File

@@ -1,16 +1,19 @@
#include "jpeg_image.h" #include "jpeg_decoder.h"
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_RUNTIME_IMAGE_JPEG
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "online_image.h" #ifdef USE_ESP_IDF
static const char *const TAG = "online_image.jpeg"; #include "esp_task_wdt.h"
#endif
namespace esphome { static const char *const TAG = "image_decoder.jpeg";
namespace online_image {
namespace esphome::runtime_image {
/** /**
* @brief Callback method that will be called by the JPEGDEC engine when a chunk * @brief Callback method that will be called by the JPEGDEC engine when a chunk
@@ -22,8 +25,14 @@ static int draw_callback(JPEGDRAW *jpeg) {
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser; ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
// Some very big images take too long to decode, so feed the watchdog on each callback // Some very big images take too long to decode, so feed the watchdog on each callback
// to avoid crashing. // to avoid crashing if the executing task has a watchdog enabled.
#ifdef USE_ESP_IDF
if (esp_task_wdt_status(nullptr) == ESP_OK) {
#endif
App.feed_wdt(); App.feed_wdt();
#ifdef USE_ESP_IDF
}
#endif
size_t position = 0; size_t position = 0;
size_t height = static_cast<size_t>(jpeg->iHeight); size_t height = static_cast<size_t>(jpeg->iHeight);
size_t width = static_cast<size_t>(jpeg->iWidth); size_t width = static_cast<size_t>(jpeg->iWidth);
@@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) {
return 1; return 1;
} }
int JpegDecoder::prepare(size_t download_size) { int JpegDecoder::prepare(size_t expected_size) {
ImageDecoder::prepare(download_size); ImageDecoder::prepare(expected_size);
auto size = this->image_->resize_download_buffer(download_size); // JPEG decoder needs complete data before decoding
if (size < download_size) {
ESP_LOGE(TAG, "Download buffer resize failed!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
return 0; return 0;
} }
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
if (size < this->download_size_) { // JPEG decoder requires complete data
ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); // If we know the expected size, wait for it
if (this->expected_size_ > 0 && size < this->expected_size_) {
ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_);
return 0; return 0;
} }
// If size unknown, try to decode and see if it's valid
// The JPEGDEC library will fail gracefully if data is incomplete
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE; return DECODE_ERROR_INVALID_TYPE;
@@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
return size; return size;
} }
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #endif // USE_RUNTIME_IMAGE_JPEG

View File

@@ -1,12 +1,12 @@
#pragma once #pragma once
#include "image_decoder.h" #include "image_decoder.h"
#include "runtime_image.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_RUNTIME_IMAGE_JPEG
#include <JPEGDEC.h> #include <JPEGDEC.h>
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
/** /**
* @brief Image decoder specialization for JPEG images. * @brief Image decoder specialization for JPEG images.
@@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder {
/** /**
* @brief Construct a new JPEG Decoder object. * @brief Construct a new JPEG Decoder object.
* *
* @param display The image to decode the stream into. * @param image The RuntimeImage to decode the stream into.
*/ */
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {}
~JpegDecoder() override {} ~JpegDecoder() override {}
int prepare(size_t download_size) override; int prepare(size_t expected_size) override;
int HOT decode(uint8_t *buffer, size_t size) override; int HOT decode(uint8_t *buffer, size_t size) override;
protected: protected:
JPEGDEC jpeg_{}; JPEGDEC jpeg_{};
}; };
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #endif // USE_RUNTIME_IMAGE_JPEG

View File

@@ -1,15 +1,14 @@
#include "png_image.h" #include "png_decoder.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_RUNTIME_IMAGE_PNG
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
static const char *const TAG = "online_image.png"; static const char *const TAG = "image_decoder.png";
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
/** /**
* @brief Callback method that will be called by the PNGLE engine when the basic * @brief Callback method that will be called by the PNGLE engine when the basic
@@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
} }
} }
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) {
{ {
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
if (!pngle) { if (!pngle) {
@@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() {
} }
} }
int PngDecoder::prepare(size_t download_size) { int PngDecoder::prepare(size_t expected_size) {
ImageDecoder::prepare(download_size); ImageDecoder::prepare(expected_size);
if (!this->pngle_) { if (!this->pngle_) {
ESP_LOGE(TAG, "PNG decoder engine not initialized!"); ESP_LOGE(TAG, "PNG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY; return DECODE_ERROR_OUT_OF_MEMORY;
@@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGE(TAG, "PNG decoder engine not initialized!"); ESP_LOGE(TAG, "PNG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY; return DECODE_ERROR_OUT_OF_MEMORY;
} }
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { // PNG can be decoded progressively, but wait for a reasonable chunk
ESP_LOGD(TAG, "Waiting for data"); if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) {
ESP_LOGD(TAG, "Waiting for more data");
return 0; return 0;
} }
auto fed = pngle_feed(this->pngle_, buffer, size); auto fed = pngle_feed(this->pngle_, buffer, size);
@@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
return fed; return fed;
} }
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT #endif // USE_RUNTIME_IMAGE_PNG

View File

@@ -3,11 +3,11 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "image_decoder.h" #include "image_decoder.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "runtime_image.h"
#ifdef USE_RUNTIME_IMAGE_PNG
#include <pngle.h> #include <pngle.h>
namespace esphome { namespace esphome::runtime_image {
namespace online_image {
/** /**
* @brief Image decoder specialization for PNG images. * @brief Image decoder specialization for PNG images.
@@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder {
/** /**
* @brief Construct a new PNG Decoder object. * @brief Construct a new PNG Decoder object.
* *
* @param display The image to decode the stream into. * @param image The RuntimeImage to decode the stream into.
*/ */
PngDecoder(OnlineImage *image); PngDecoder(RuntimeImage *image);
~PngDecoder() override; ~PngDecoder() override;
int prepare(size_t download_size) override; int prepare(size_t expected_size) override;
int HOT decode(uint8_t *buffer, size_t size) override; int HOT decode(uint8_t *buffer, size_t size) override;
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
@@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder {
protected: protected:
RAMAllocator<pngle_t> allocator_; RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_; pngle_t *pngle_{nullptr};
uint32_t pixels_decoded_{0}; uint32_t pixels_decoded_{0};
}; };
} // namespace online_image } // namespace esphome::runtime_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT #endif // USE_RUNTIME_IMAGE_PNG

View File

@@ -0,0 +1,300 @@
#include "runtime_image.h"
#include "image_decoder.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cstring>
#ifdef USE_RUNTIME_IMAGE_BMP
#include "bmp_decoder.h"
#endif
#ifdef USE_RUNTIME_IMAGE_JPEG
#include "jpeg_decoder.h"
#endif
#ifdef USE_RUNTIME_IMAGE_PNG
#include "png_decoder.h"
#endif
namespace esphome::runtime_image {
static const char *const TAG = "runtime_image";
inline bool is_color_on(const Color &color) {
// This produces the most accurate monochrome conversion, but is slightly slower.
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
// Approximation using fast integer computations; produces acceptable results
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
}
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
: Image(nullptr, 0, 0, type, transparency),
format_(format),
fixed_width_(fixed_width),
fixed_height_(fixed_height),
placeholder_(placeholder),
is_big_endian_(is_big_endian) {}
RuntimeImage::~RuntimeImage() { this->release(); }
int RuntimeImage::resize(int width, int height) {
// Use fixed dimensions if specified (0 means auto-resize)
int target_width = this->fixed_width_ ? this->fixed_width_ : width;
int target_height = this->fixed_height_ ? this->fixed_height_ : height;
size_t result = this->resize_buffer_(target_width, target_height);
if (result > 0 && this->progressive_display_) {
// Update display dimensions for progressive display
this->width_ = this->buffer_width_;
this->height_ = this->buffer_height_;
this->data_start_ = this->buffer_;
}
return result;
}
void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!");
return;
}
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
return;
}
switch (this->type_) {
case image::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u;
uint32_t pos = x + y * width_8;
auto bitno = 0x80 >> (pos % 8u);
pos /= 8u;
auto on = is_color_on(color);
if (this->has_transparency() && color.w < 0x80)
on = false;
if (on) {
this->buffer_[pos] |= bitno;
} else {
this->buffer_[pos] &= ~bitno;
}
break;
}
case image::IMAGE_TYPE_GRAYSCALE: {
uint32_t pos = this->get_position_(x, y);
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
}
if (color.w < 0x80) {
gray = 1;
}
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
if (color.w != 0xFF)
gray = color.w;
}
this->buffer_[pos] = gray;
break;
}
case image::IMAGE_TYPE_RGB565: {
uint32_t pos = this->get_position_(x, y);
Color mapped_color = color;
this->map_chroma_key(mapped_color);
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
if (this->is_big_endian_) {
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
} else {
this->buffer_[pos + 0] = static_cast<uint8_t>(rgb565 & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
}
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w;
}
break;
}
case image::IMAGE_TYPE_RGB: {
uint32_t pos = this->get_position_(x, y);
Color mapped_color = color;
this->map_chroma_key(mapped_color);
this->buffer_[pos + 0] = mapped_color.r;
this->buffer_[pos + 1] = mapped_color.g;
this->buffer_[pos + 2] = mapped_color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
break;
}
}
}
void RuntimeImage::map_chroma_key(Color &color) {
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (color.g == 1 && color.r == 0 && color.b == 0) {
color.g = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1;
color.b = 0;
}
}
}
void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
if (this->data_start_) {
// If we have a complete image, use the base class draw method
Image::draw(x, y, display, color_on, color_off);
} else if (this->placeholder_) {
// Show placeholder while the runtime image is not available
this->placeholder_->draw(x, y, display, color_on, color_off);
}
// If no image is loaded and no placeholder, nothing to draw
}
bool RuntimeImage::begin_decode(size_t expected_size) {
if (this->decoder_) {
ESP_LOGW(TAG, "Decoding already in progress");
return false;
}
this->decoder_ = this->create_decoder_();
if (!this->decoder_) {
ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_);
return false;
}
this->total_size_ = expected_size;
this->decoded_bytes_ = 0;
// Initialize decoder
int result = this->decoder_->prepare(expected_size);
if (result < 0) {
ESP_LOGE(TAG, "Failed to prepare decoder: %d", result);
this->decoder_ = nullptr;
return false;
}
return true;
}
int RuntimeImage::feed_data(uint8_t *data, size_t len) {
if (!this->decoder_) {
ESP_LOGE(TAG, "No decoder initialized");
return -1;
}
int consumed = this->decoder_->decode(data, len);
if (consumed > 0) {
this->decoded_bytes_ += consumed;
}
return consumed;
}
bool RuntimeImage::end_decode() {
if (!this->decoder_) {
return false;
}
// Finalize the image for display
if (!this->progressive_display_) {
// Only now make the image visible
this->width_ = this->buffer_width_;
this->height_ = this->buffer_height_;
this->data_start_ = this->buffer_;
}
// Clean up decoder
this->decoder_ = nullptr;
ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_);
return true;
}
bool RuntimeImage::is_decode_finished() const {
if (!this->decoder_) {
return false;
}
return this->decoder_->is_finished();
}
void RuntimeImage::release() {
this->release_buffer_();
// Reset decoder separately — release() can be called from within the decoder
// (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here.
// The decoder lifecycle is managed by begin_decode()/end_decode().
this->decoder_ = nullptr;
}
void RuntimeImage::release_buffer_() {
if (this->buffer_) {
ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_));
this->buffer_ = nullptr;
this->data_start_ = nullptr;
this->width_ = 0;
this->height_ = 0;
this->buffer_width_ = 0;
this->buffer_height_ = 0;
}
}
size_t RuntimeImage::resize_buffer_(int width, int height) {
size_t new_size = this->get_buffer_size_(width, height);
if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
// Buffer already allocated with correct size
return new_size;
}
// Release old buffer if dimensions changed
if (this->buffer_) {
this->release_buffer_();
}
ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size);
this->buffer_ = this->allocator_.allocate(new_size);
if (!this->buffer_) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size,
this->allocator_.get_max_free_block_size());
return 0;
}
// Clear buffer
memset(this->buffer_, 0, new_size);
this->buffer_width_ = width;
this->buffer_height_ = height;
return new_size;
}
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
return (this->get_bpp() * width + 7u) / 8u * height;
}
int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_() {
switch (this->format_) {
#ifdef USE_RUNTIME_IMAGE_BMP
case BMP:
return make_unique<BmpDecoder>(this);
#endif
#ifdef USE_RUNTIME_IMAGE_JPEG
case JPEG:
return make_unique<JpegDecoder>(this);
#endif
#ifdef USE_RUNTIME_IMAGE_PNG
case PNG:
return make_unique<PngDecoder>(this);
#endif
default:
ESP_LOGE(TAG, "Unsupported image format: %d", this->format_);
return nullptr;
}
}
} // namespace esphome::runtime_image

View File

@@ -0,0 +1,214 @@
#pragma once
#include "esphome/components/image/image.h"
#include "esphome/core/helpers.h"
namespace esphome::runtime_image {
// Forward declaration
class ImageDecoder;
/**
* @brief Image format types that can be decoded dynamically.
*/
enum ImageFormat {
/** Automatically detect from data. Not implemented yet. */
AUTO,
/** JPEG format. */
JPEG,
/** PNG format. */
PNG,
/** BMP format. */
BMP,
};
/**
* @brief A dynamic image that can be loaded and decoded at runtime.
*
* This class provides dynamic buffer allocation and management for images
* that are decoded at runtime, as opposed to static images compiled into
* the firmware. It serves as a base class for components that need to
* load images dynamically from various sources.
*/
class RuntimeImage : public image::Image {
public:
/**
* @brief Construct a new RuntimeImage object.
*
* @param format The image format to decode.
* @param type The pixel format for the image.
* @param transparency The transparency type for the image.
* @param placeholder Optional placeholder image to show while loading.
* @param is_big_endian Whether the image is stored in big-endian format.
* @param fixed_width Fixed width for the image (0 for auto-resize).
* @param fixed_height Fixed height for the image (0 for auto-resize).
*/
RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0,
int fixed_height = 0);
~RuntimeImage();
// Decoder interface methods
/**
* @brief Resize the image buffer to the requested dimensions.
*
* The buffer will be allocated if not existing.
* If fixed dimensions have been specified in the constructor, the buffer will be created
* with those dimensions and not resized, even on request.
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
* dimensions allocated.
*
* @param width Requested width (ignored if fixed_width_ is set)
* @param height Requested height (ignored if fixed_height_ is set)
* @return Size of the allocated buffer in bytes, or 0 if allocation failed.
*/
int resize(int width, int height);
void draw_pixel(int x, int y, const Color &color);
void map_chroma_key(Color &color);
int get_buffer_width() const { return this->buffer_width_; }
int get_buffer_height() const { return this->buffer_height_; }
// Image drawing interface
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
/**
* @brief Begin decoding an image.
*
* @param expected_size Optional hint about the expected data size.
* @return true if decoder was successfully initialized.
*/
bool begin_decode(size_t expected_size = 0);
/**
* @brief Feed data to the decoder.
*
* @param data Pointer to the data buffer.
* @param len Length of data to process.
* @return Number of bytes consumed by the decoder.
*/
int feed_data(uint8_t *data, size_t len);
/**
* @brief Complete the decoding process.
*
* @return true if decoding completed successfully.
*/
bool end_decode();
/**
* @brief Check if decoding is currently in progress.
*/
bool is_decoding() const { return this->decoder_ != nullptr; }
/**
* @brief Check if the decoder has finished processing all data.
*
* This delegates to the decoder's format-specific completion check,
* which handles both known-size and chunked transfer cases.
*/
bool is_decode_finished() const;
/**
* @brief Check if an image is currently loaded.
*/
bool is_loaded() const { return this->buffer_ != nullptr; }
/**
* @brief Get the image format.
*/
ImageFormat get_format() const { return this->format_; }
/**
* @brief Release the image buffer and free memory.
*/
void release();
/**
* @brief Set whether to allow progressive display during decode.
*
* When enabled, the image can be displayed even while still decoding.
* When disabled, the image is only displayed after decoding completes.
*/
void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; }
protected:
/**
* @brief Resize the image buffer to the requested dimensions.
*
* @param width New width in pixels.
* @param height New height in pixels.
* @return Size of the allocated buffer, or 0 on failure.
*/
size_t resize_buffer_(int width, int height);
/**
* @brief Release only the image buffer without resetting the decoder.
*
* This is safe to call from within the decoder (e.g., during resize).
*/
void release_buffer_();
/**
* @brief Get the buffer size in bytes for given dimensions.
*/
size_t get_buffer_size_(int width, int height) const;
/**
* @brief Get the position in the buffer for a pixel.
*/
int get_position_(int x, int y) const;
/**
* @brief Create decoder instance for the image's format.
*/
std::unique_ptr<ImageDecoder> create_decoder_();
// Memory management
RAMAllocator<uint8_t> allocator_{};
uint8_t *buffer_{nullptr};
// Decoder management
std::unique_ptr<ImageDecoder> decoder_{nullptr};
/** The image format this RuntimeImage is configured to decode. */
const ImageFormat format_;
/**
* Actual width of the current image.
* This needs to be separate from "Image::get_width()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images). When progressive_display_ is enabled, Image dimensions
* are updated during decoding to allow rendering in progress.
*/
int buffer_width_{0};
/**
* Actual height of the current image.
* This needs to be separate from "Image::get_height()" because the latter
* must return 0 until the image has been decoded (to avoid showing partially
* decoded images). When progressive_display_ is enabled, Image dimensions
* are updated during decoding to allow rendering in progress.
*/
int buffer_height_{0};
// Decoding state
size_t total_size_{0};
size_t decoded_bytes_{0};
/** Fixed width requested on configuration, or 0 if not specified. */
const int fixed_width_{0};
/** Fixed height requested on configuration, or 0 if not specified. */
const int fixed_height_{0};
/** Placeholder image to show when the runtime image is not available. */
image::Image *placeholder_{nullptr};
// Configuration
bool progressive_display_{false};
/**
* Whether the image is stored in big-endian format.
* This is used to determine how to store 16 bit colors in the buffer.
*/
bool is_big_endian_{false};
};
} // namespace esphome::runtime_image

View File

@@ -219,7 +219,6 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
"script.wait", "script.wait",
ScriptWaitAction, ScriptWaitAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}), maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
deferred=True,
) )
async def script_wait_action_to_code(config, action_id, template_arg, args): async def script_wait_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])

View File

@@ -7,15 +7,15 @@ namespace speed {
static const char *const TAG = "speed.fan"; static const char *const TAG = "speed.fan";
void SpeedFan::setup() { void SpeedFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
restore->apply(*this); restore->apply(*this);
this->write_state_(); this->write_state_();
} }
// Construct traits
this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
} }
void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); }

View File

@@ -6,15 +6,15 @@ namespace esphome::template_ {
static const char *const TAG = "template.fan"; static const char *const TAG = "template.fan";
void TemplateFan::setup() { void TemplateFan::setup() {
// Construct traits before restore so preset modes can be looked up by index
this->traits_ =
fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
restore->apply(*this); restore->apply(*this);
} }
// Construct traits
this->traits_ =
fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_);
this->traits_.set_supported_preset_modes(this->preset_modes_);
} }
void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); }

View File

@@ -216,23 +216,16 @@ bool WiFiComponent::wifi_apply_hostname_() {
ESP_LOGV(TAG, "Set hostname failed"); ESP_LOGV(TAG, "Set hostname failed");
} }
// inform dhcp server of hostname change using dhcp_renew() // Update hostname on all lwIP interfaces so DHCP packets include it.
// lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically
// via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is
// fixed at compile time and never changes at runtime.
for (netif *intf = netif_list; intf; intf = intf->next) { for (netif *intf = netif_list; intf; intf = intf->next) {
// unconditionally update all known interfaces
#if LWIP_VERSION_MAJOR == 1 #if LWIP_VERSION_MAJOR == 1
intf->hostname = (char *) wifi_station_get_hostname(); intf->hostname = (char *) wifi_station_get_hostname();
#else #else
intf->hostname = wifi_station_get_hostname(); intf->hostname = wifi_station_get_hostname();
#endif #endif
if (netif_dhcp_data(intf) != nullptr) {
// renew already started DHCP leases
err_t lwipret = dhcp_renew(intf);
if (lwipret != ERR_OK) {
ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname,
(int) lwipret, intf->name[0], intf->name[1], intf->num);
ret = false;
}
}
} }
return ret; return ret;

View File

@@ -148,9 +148,9 @@
#define USE_MQTT #define USE_MQTT
#define USE_MQTT_COVER_JSON #define USE_MQTT_COVER_JSON
#define USE_NETWORK #define USE_NETWORK
#define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_RUNTIME_IMAGE_BMP
#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_RUNTIME_IMAGE_PNG
#define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_RUNTIME_IMAGE_JPEG
#define USE_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_LISTENER #define USE_OTA_STATE_LISTENER

View File

@@ -152,11 +152,13 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m
this->unit_of_measurement_ = unit_of_measurement; this->unit_of_measurement_ = unit_of_measurement;
} }
#ifdef USE_ENTITY_ICON
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
if (!obj.get_icon_ref().empty()) { if (!obj.get_icon_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str());
} }
} }
#endif
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
if (!obj.get_device_class_ref().empty()) { if (!obj.get_device_class_ref().empty()) {

View File

@@ -231,8 +231,13 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
}; };
/// Log entity icon if set (for use in dump_config) /// Log entity icon if set (for use in dump_config)
#ifdef USE_ENTITY_ICON
#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) #define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj)
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj);
#else
#define LOG_ENTITY_ICON(tag, prefix, obj) ((void) 0)
inline void log_entity_icon(const char *, const char *, const EntityBase &) {}
#endif
/// Log entity device class if set (for use in dump_config) /// Log entity device class if set (for use in dump_config)
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) #define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);

View File

@@ -1673,13 +1673,10 @@ template<class T> class RAMAllocator {
ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility. ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility.
}; };
RAMAllocator() = default; constexpr RAMAllocator() = default;
RAMAllocator(uint8_t flags) { constexpr RAMAllocator(uint8_t flags)
// default is both external and internal : flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL))
flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; : (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {}
if (flags != 0)
this->flags_ = flags;
}
template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {} template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {}
T *allocate(size_t n) { return this->allocate(n, sizeof(T)); } T *allocate(size_t n) { return this->allocate(n, sizeof(T)); }

View File

@@ -81,19 +81,6 @@ class StringRef {
operator std::string() const { return str(); } operator std::string() const { return str(); }
/// Compare with a null-terminated C string (compatible with std::string::compare)
int compare(const char *s) const {
size_t s_len = std::strlen(s);
int result = std::memcmp(base_, s, std::min(len_, s_len));
if (result != 0)
return result;
if (len_ < s_len)
return -1;
if (len_ > s_len)
return 1;
return 0;
}
/// Find first occurrence of substring, returns std::string::npos if not found. /// Find first occurrence of substring, returns std::string::npos if not found.
/// Note: Requires the underlying string to be null-terminated. /// Note: Requires the underlying string to be null-terminated.
size_type find(const char *s, size_type pos = 0) const { size_type find(const char *s, size_type pos = 0) const {

View File

@@ -24,14 +24,11 @@ class RegistryEntry:
fun: Callable[..., Any], fun: Callable[..., Any],
type_id: "MockObjClass", type_id: "MockObjClass",
schema: "Schema", schema: "Schema",
*,
deferred: bool = False,
): ):
self.name = name self.name = name
self.fun = fun self.fun = fun
self.type_id = type_id self.type_id = type_id
self.raw_schema = schema self.raw_schema = schema
self.deferred = deferred
@property @property
def coroutine_fun(self): def coroutine_fun(self):
@@ -52,16 +49,9 @@ class Registry(dict[str, RegistryEntry]):
self.base_schema = base_schema or {} self.base_schema = base_schema or {}
self.type_id_key = type_id_key self.type_id_key = type_id_key
def register( def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
self,
name: str,
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
):
def decorator(fun: Callable[..., Any]): def decorator(fun: Callable[..., Any]):
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred) self[name] = RegistryEntry(name, fun, type_id, schema)
return fun return fun
return decorator return decorator

View File

@@ -1,6 +1,6 @@
pylint==4.0.4 pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.15.0 # also change in .pre-commit-config.yaml when updating ruff==0.15.1 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View File

@@ -2020,8 +2020,6 @@ def build_message_type(
# Collect fixed_vector fields for custom decode generation # Collect fixed_vector fields for custom decode generation
fixed_vector_fields = [] fixed_vector_fields = []
# Collect fields with (null_terminate) = true option
null_terminate_fields = []
for field in desc.field: for field in desc.field:
# Skip deprecated fields completely # Skip deprecated fields completely
@@ -2064,10 +2062,6 @@ def build_message_type(
ti = create_field_type_info(field, needs_decode, needs_encode) ti = create_field_type_info(field, needs_decode, needs_encode)
# Collect fields with (null_terminate) = true for post-decode null-termination
if needs_decode and get_field_opt(field, pb.null_terminate, False):
null_terminate_fields.append(ti.field_name)
# Skip field declarations for fields that are in the base class # Skip field declarations for fields that are in the base class
# but include their encode/decode logic # but include their encode/decode logic
if field.name not in common_field_names: if field.name not in common_field_names:
@@ -2174,8 +2168,8 @@ def build_message_type(
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
protected_content.insert(0, prot) protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector or null_terminate fields # Generate custom decode() override for messages with FixedVector fields
if fixed_vector_fields or null_terminate_fields: if fixed_vector_fields:
# Generate the decode() implementation in cpp # Generate the decode() implementation in cpp
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n" o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
# Count and init each FixedVector field # Count and init each FixedVector field
@@ -2184,13 +2178,6 @@ def build_message_type(
o += f" this->{field_name}.init(count_{field_name});\n" o += f" this->{field_name}.init(count_{field_name});\n"
# Call parent decode to populate the fields # Call parent decode to populate the fields
o += " ProtoDecodableMessage::decode(buffer, length);\n" o += " ProtoDecodableMessage::decode(buffer, length);\n"
# Null-terminate fields marked with (null_terminate) = true in-place.
# Safe: decode is complete, byte after string was already parsed (next field tag)
# or is the +1 reserved byte at end of rx_buf_.
for field_name in null_terminate_fields:
o += f" if (!this->{field_name}.empty()) {{\n"
o += f" const_cast<char *>(this->{field_name}.c_str())[this->{field_name}.size()] = '\\0';\n"
o += " }\n"
o += "}\n" o += "}\n"
cpp += o cpp += o
# Generate the decode() declaration in header (public method) # Generate the decode() declaration in header (public method)

View File

@@ -27,9 +27,9 @@ sensor:
name: Linearly combined temperatures name: Linearly combined temperatures
sources: sources:
- source: template_temperature1 - source: template_temperature1
coeffecient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;" coefficient: !lambda "return 0.4 + std::abs(x - 25) * 0.023;"
- source: template_temperature2 - source: template_temperature2
coeffecient: 1.5 coefficient: 1.5
- platform: combination - platform: combination
type: max type: max
name: Max of combined temperatures name: Max of combined temperatures

View File

@@ -57,6 +57,23 @@ display:
allow_other_uses: true allow_other_uses: true
number: GPIO4 number: GPIO4
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-7.5in-H
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
inverted: true
- platform: epaper_spi - platform: epaper_spi
model: seeed-reterminal-e1002 model: seeed-reterminal-e1002
- platform: epaper_spi - platform: epaper_spi
@@ -64,3 +81,66 @@ display:
# Override pins to avoid conflict with other display configs # Override pins to avoid conflict with other display configs
busy_pin: 43 busy_pin: 43
dc_pin: 42 dc_pin: 42
# WeAct 2.13" 3-color e-paper (122x250, SSD1680)
- platform: epaper_spi
spi_id: spi_bus
model: weact-2.13in-3c
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# WeAct 2.9" 3-color e-paper (128x296, SSD1683)
- platform: epaper_spi
spi_id: spi_bus
model: weact-2.9in-3c
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# WeAct 4.2" 3-color e-paper (400x300, SSD1683)
- platform: epaper_spi
spi_id: spi_bus
model: weact-4.2in-3c
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color(255, 0, 0));

View File

@@ -0,0 +1,12 @@
remote_receiver:
id: rcvr
pin: GPIO2
dump: all
<<: !include common-actions.yaml
binary_sensor:
- platform: remote_receiver
name: Panasonic Remote Input
panasonic:
address: 0x4004
command: 0x100BCBD

View File

@@ -0,0 +1,7 @@
remote_transmitter:
id: xmitr
pin: GPIO2
carrier_duty_percent: 50%
packages:
buttons: !include common-buttons.yaml