From b2c12d88fe15d31710365a0b8759317527abc2d7 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 5 Mar 2026 23:24:11 +0100 Subject: [PATCH 01/16] [uart] init tx_pin, rx_pin, flow control, rx_buffer_size (#14524) --- esphome/components/uart/uart_component.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index b6ffbbd51f5..078ce64b30f 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -183,10 +183,10 @@ class UARTComponent { virtual void check_logger_conflict() = 0; bool check_read_timeout_(size_t len = 1); - InternalGPIOPin *tx_pin_; - InternalGPIOPin *rx_pin_; - InternalGPIOPin *flow_control_pin_; - size_t rx_buffer_size_; + InternalGPIOPin *tx_pin_{}; + InternalGPIOPin *rx_pin_{}; + InternalGPIOPin *flow_control_pin_{}; + size_t rx_buffer_size_{}; size_t rx_full_threshold_{1}; size_t rx_timeout_{0}; uint32_t baud_rate_{0}; From 8a8f6824a200a8396a66f1873a7731504e75cc0b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:29:44 -0500 Subject: [PATCH 02/16] [openthread][ethernet][wifi] Add IPv6 address array bounds assert (#14488) Co-authored-by: Claude Opus 4.6 --- esphome/components/ethernet/ethernet_component.cpp | 1 + esphome/components/openthread/openthread.h | 2 +- esphome/components/openthread/openthread_esp.cpp | 1 + esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 1 + esphome/components/wifi/wifi_component_pico_w.cpp | 3 +++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 098f7be972f..0bc67c8b033 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -470,6 +470,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index d853c58f958..c87f4fa7c10 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -90,7 +90,7 @@ class InstanceLock { otInstance *get_instance(); private: - // Use a private constructor in order to force thehandling + // Use a private constructor in order to force the handling // of acquisition failure InstanceLock() {} }; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 2296e32b7f7..cdc7a404b2d 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -197,6 +197,7 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { esp_netif_t *netif = esp_netif_get_default_netif(); count = esp_netif_get_all_ip6(netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 02ce59502b7..355832b4340 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #ifdef USE_WIFI_WPA2_EAP @@ -205,6 +206,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { network::IPAddresses addresses; uint8_t index = 0; for (auto &addr : addrList) { + assert(index < addresses.size()); addresses[index++] = addr.ipFromNetifNum(); } return addresses; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index bf432cea6e5..eca3f192490 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -585,6 +585,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { uint8_t count = 0; count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s); assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); for (int i = 0; i < count; i++) { addresses[i + 1] = network::IPAddress(&if_ip6s[i]); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 5140fb285e3..1cfeee3c1bf 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -3,6 +3,8 @@ #ifdef USE_WIFI #ifdef USE_RP2040 +#include + #include "lwip/dns.h" #include "lwip/err.h" #include "lwip/netif.h" @@ -285,6 +287,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (ip == ap_ip) { continue; } + assert(index < addresses.size()); addresses[index++] = ip; } return addresses; From 64098122e77cc893d5e2cc288a5b30e6a1744724 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 5 Mar 2026 16:30:13 -0600 Subject: [PATCH 03/16] [audio_file] Add media source platform (#14436) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + .../audio_file/media_source/__init__.py | 38 +++ .../media_source/audio_file_media_source.cpp | 283 ++++++++++++++++++ .../media_source/audio_file_media_source.h | 50 ++++ tests/components/audio_file/common.yaml | 4 + 5 files changed, 376 insertions(+) create mode 100644 esphome/components/audio_file/media_source/__init__.py create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.cpp create mode 100644 esphome/components/audio_file/media_source/audio_file_media_source.h diff --git a/CODEOWNERS b/CODEOWNERS index 7c37b20e099..8bf896d159e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 esphome/components/audio_file/* @kahrendt +esphome/components/audio_file/media_source/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan diff --git a/esphome/components/audio_file/media_source/__init__.py b/esphome/components/audio_file/media_source/__init__.py new file mode 100644 index 00000000000..e9e292a2b2e --- /dev/null +++ b/esphome/components/audio_file/media_source/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import media_source, psram +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.types import ConfigType + +CODEOWNERS = ["@kahrendt"] +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["audio_file"] + +audio_file_ns = cg.esphome_ns.namespace("audio_file") +AudioFileMediaSource = audio_file_ns.class_( + "AudioFileMediaSource", cg.Component, media_source.MediaSource +) + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + AudioFileMediaSource, + ) + .extend( + { + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + if CONF_TASK_STACK_IN_PSRAM in config: + cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp new file mode 100644 index 00000000000..120f871d2ff --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -0,0 +1,283 @@ +#include "audio_file_media_source.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio_decoder.h" + +#include + +namespace esphome::audio_file { + +namespace { // anonymous namespace for internal linkage +struct AudioSinkAdapter : public audio::AudioSinkCallback { + media_source::MediaSource *source; + audio::AudioStreamInfo stream_info; + + size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override { + return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info); + } +}; +} // namespace + +#if defined(USE_AUDIO_OPUS_SUPPORT) +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif + +static const char *const TAG = "audio_file_media_source"; + +enum EventGroupBits : uint32_t { + // Requests to start playback (set by play_uri, handled by loop) + REQUEST_START = (1 << 0), + // Commands from main loop to decode task + COMMAND_STOP = (1 << 1), + COMMAND_PAUSE = (1 << 2), + // Decode task lifecycle signals (one-shot, cleared by loop) + TASK_STARTING = (1 << 7), + TASK_RUNNING = (1 << 8), + TASK_STOPPING = (1 << 9), + TASK_STOPPED = (1 << 10), + TASK_ERROR = (1 << 11), + // Decode task state (level-triggered, set/cleared by decode task) + TASK_PAUSED = (1 << 12), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +void AudioFileMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, "Audio File Media Source:"); + ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No"); +} + +void AudioFileMediaSource::setup() { + this->disable_loop(); + + this->event_group_ = xEventGroupCreate(); + if (this->event_group_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + this->mark_failed(); + return; + } +} + +void AudioFileMediaSource::loop() { + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + + if (event_bits & REQUEST_START) { + xEventGroupClearBits(this->event_group_, REQUEST_START); + this->decoding_state_ = AudioFileDecodingState::START_TASK; + } + + switch (this->decoding_state_) { + case AudioFileDecodingState::START_TASK: { + if (!this->decode_task_.is_created()) { + xEventGroupClearBits(this->event_group_, ALL_BITS); + if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1, + this->task_stack_in_psram_)) { + ESP_LOGE(TAG, "Failed to create task"); + this->status_momentary_error("task_create", 1000); + this->set_state_(media_source::MediaSourceState::ERROR); + this->decoding_state_ = AudioFileDecodingState::IDLE; + return; + } + } + this->decoding_state_ = AudioFileDecodingState::DECODING; + break; + } + case AudioFileDecodingState::DECODING: { + if (event_bits & TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, TASK_STARTING); + } + + if (event_bits & TASK_RUNNING) { + ESP_LOGV(TAG, "Started"); + xEventGroupClearBits(this->event_group_, TASK_RUNNING); + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PAUSED); + } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if (event_bits & TASK_STOPPING) { + ESP_LOGV(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, TASK_STOPPING); + } + + if (event_bits & TASK_ERROR) { + // Report error so the orchestrator knows playback failed; task will have already logged the specific error + this->set_state_(media_source::MediaSourceState::ERROR); + } + + if (event_bits & TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ALL_BITS); + + this->decode_task_.deallocate(); + this->set_state_(media_source::MediaSourceState::IDLE); + this->decoding_state_ = AudioFileDecodingState::IDLE; + } + break; + } + case AudioFileDecodingState::IDLE: { + if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) { + this->set_state_(media_source::MediaSourceState::IDLE); + } + break; + } + } + + if ((this->decoding_state_ == AudioFileDecodingState::IDLE) && + (this->get_state() == media_source::MediaSourceState::IDLE)) { + this->disable_loop(); + } +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +bool AudioFileMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() || + xEventGroupGetBits(this->event_group_) & REQUEST_START) { + return false; + } + + // Check if source is already playing + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + // Validate URI starts with "audio-file://" + if (!uri.starts_with("audio-file://")) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + // Strip "audio-file://" prefix and find the file + const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters + + for (const auto &named_file : get_named_audio_files()) { + if (strcmp(named_file.file_id, file_id) == 0) { + this->current_file_ = named_file.file; + xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START); + this->enable_loop(); + return true; + } + } + + ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + return false; +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) { + if (this->decoding_state_ != AudioFileDecodingState::DECODING) { + return; + } + + switch (command) { + case media_source::MediaSourceCommand::STOP: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP); + break; + case media_source::MediaSourceCommand::PAUSE: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + case media_source::MediaSourceCommand::PLAY: + xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + default: + break; + } +} + +void AudioFileMediaSource::decode_task(void *params) { + AudioFileMediaSource *this_source = static_cast(params); + + do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING); + + // 0 bytes for input transfer buffer makes it an inplace buffer + std::unique_ptr decoder = make_unique(0, 4096); + + esp_err_t err = decoder->start(this_source->current_file_->file_type); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING); + break; + } + + // Add the file as a const data source + decoder->add_source(this_source->current_file_->data, this_source->current_file_->length); + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING); + + AudioSinkAdapter audio_sink; + bool has_stream_info = false; + + while (true) { + EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_); + + if (event_bits & EventGroupBits::COMMAND_STOP) { + break; + } + + bool paused = event_bits & EventGroupBits::COMMAND_PAUSE; + decoder->set_pause_output_state(paused); + if (paused) { + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + vTaskDelay(pdMS_TO_TICKS(20)); + } else { + xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + } + + // Will stop gracefully once finished with the current file + audio::AudioDecoderState decoder_state = decoder->decode(true); + + if (decoder_state == audio::AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == audio::AudioDecoderState::FAILED) { + ESP_LOGE(TAG, "Decoder failed"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { + has_stream_info = true; + + audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); + + ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(), + stream_info.get_channels(), stream_info.get_sample_rate()); + + if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { + ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + audio_sink.source = this_source; + audio_sink.stream_info = stream_info; + esp_err_t err = decoder->add_sink(&audio_sink); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + } + } + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING); + } while (false); + + // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed. + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED); + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it +} + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h new file mode 100644 index 00000000000..75e18c13b88 --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio_file/audio_file.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/core/component.h" +#include "esphome/core/static_task.h" + +#include +#include + +namespace esphome::audio_file { + +enum class AudioFileDecodingState : uint8_t { + START_TASK, + DECODING, + IDLE, +}; + +class AudioFileMediaSource : public Component, public media_source::MediaSource { + public: + void setup() override; + void loop() override; + void dump_config() override; + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); } + + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + protected: + static void decode_task(void *params); + + audio::AudioFile *current_file_{nullptr}; + AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE}; + EventGroupHandle_t event_group_{nullptr}; + StaticTask decode_task_; + + bool task_stack_in_psram_{false}; +}; + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/tests/components/audio_file/common.yaml b/tests/components/audio_file/common.yaml index 94042080946..e7f55b4806c 100644 --- a/tests/components/audio_file/common.yaml +++ b/tests/components/audio_file/common.yaml @@ -3,3 +3,7 @@ audio_file: file: type: local path: $component_dir/test.wav + +media_source: + - platform: audio_file + id: audio_file_source From 58ab63096587dcfd75d7ad74a397d8b0687fb41f Mon Sep 17 00:00:00 2001 From: Gnuspice Date: Fri, 6 Mar 2026 12:37:07 +1300 Subject: [PATCH 04/16] [ethernet] add get_eth_handle() function (#14527) --- esphome/components/ethernet/ethernet_component.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f5a31d78ebf..e54e1543e3f 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -115,6 +115,7 @@ class EthernetComponent : public Component { const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); + esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } bool powerdown(); #ifdef USE_ETHERNET_IP_STATE_LISTENERS From 44870323dab606efe85a21d93ac0f32eaab22aee Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:47:47 -0500 Subject: [PATCH 05/16] [host] Add null checks for getenv and fopen in preferences (#14531) Co-authored-by: Claude Opus 4.6 --- esphome/components/host/preferences.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 5ad87c1f2a0..275c202e3ed 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -4,6 +4,7 @@ #include #include "preferences.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace host { @@ -14,7 +15,12 @@ static const char *const TAG = "host.preferences"; void HostPreferences::setup_() { if (this->setup_complete_) return; - this->filename_.append(getenv("HOME")); + const char *home = getenv("HOME"); + if (home == nullptr) { + ESP_LOGE(TAG, "HOME environment variable is not set"); + abort(); + } + this->filename_.append(home); this->filename_.append("/.esphome"); this->filename_.append("/prefs"); fs::create_directories(this->filename_); @@ -44,9 +50,12 @@ void HostPreferences::setup_() { bool HostPreferences::sync() { this->setup_(); FILE *fp = fopen(this->filename_.c_str(), "wb"); - std::map>::iterator it; + if (fp == nullptr) { + ESP_LOGE(TAG, "Failed to open preferences file for writing: %s", this->filename_.c_str()); + return false; + } - for (it = this->data.begin(); it != this->data.end(); ++it) { + for (auto it = this->data.begin(); it != this->data.end(); ++it) { fwrite(&it->first, sizeof(uint32_t), 1, fp); uint8_t len = it->second.size(); fwrite(&len, sizeof(len), 1, fp); From 80fe54ed6948386a9b6218737b164c9e61c7d71e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:30:39 -0500 Subject: [PATCH 06/16] [bluetooth_proxy] Add null checks for api_connection (#14536) Co-authored-by: Claude Opus 4.6 --- .../bluetooth_proxy/bluetooth_connection.cpp | 25 +++++++++++++++---- .../bluetooth_proxy/bluetooth_proxy.cpp | 4 +++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 60f56fda547..b2000fbd943 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -415,11 +415,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -429,10 +432,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -442,10 +448,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -455,20 +464,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_NOTIFY_EVT: { ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index d45377b3f67..cab328e2f53 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -420,6 +420,8 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDevicePairingResponse call; call.address = address; call.paired = paired; @@ -429,6 +431,8 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDeviceUnpairingResponse call; call.address = address; call.success = success; From a2c0d70c2c1983cf996f045ba7801216ddcea9ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 08:00:17 +0100 Subject: [PATCH 07/16] [ble_nus] Add uart support (#14320) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/ble_nus/__init__.py | 53 +++++++- esphome/components/ble_nus/ble_nus.cpp | 125 +++++++++++++++--- esphome/components/ble_nus/ble_nus.h | 16 ++- esphome/core/defines.h | 2 + .../ble_nus/test-uart.nrf52-adafruit.yaml | 4 + 5 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 tests/components/ble_nus/test-uart.nrf52-adafruit.yaml diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py index 6581ce1cfab..c0837da4025 100644 --- a/esphome/components/ble_nus/__init__.py +++ b/esphome/components/ble_nus/__init__.py @@ -1,29 +1,64 @@ import esphome.codegen as cg from esphome.components.logger import request_log_listener +from esphome.components.uart import ( + UARTComponent, + debug_to_code, + maybe_empty_debug, + uart_ns, +) from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE +from esphome.const import ( + CONF_DEBUG, + CONF_ID, + CONF_LOGS, + CONF_RX_BUFFER_SIZE, + CONF_TX_BUFFER_SIZE, + CONF_TYPE, +) +from esphome.types import ConfigType -AUTO_LOAD = ["zephyr_ble_server"] +AUTO_LOAD = ["zephyr_ble_server", "uart"] CODEOWNERS = ["@tomaszduda23"] ble_nus_ns = cg.esphome_ns.namespace("ble_nus") -BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) +BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent) + +CONF_UART = "uart" + + +def validate_rx_buffer(config: ConfigType) -> ConfigType: + config = config.copy() + if config[CONF_TYPE] == CONF_LOGS: + if CONF_RX_BUFFER_SIZE in config: + raise cv.Invalid("logs does not support rx_buffer_size") + elif CONF_RX_BUFFER_SIZE not in config: + config[CONF_RX_BUFFER_SIZE] = 512 + return config + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLENUS), cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( - *[CONF_LOGS], lower=True + *[CONF_LOGS, CONF_UART], lower=True ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_RX_BUFFER_SIZE): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_framework("zephyr"), + validate_rx_buffer, ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) zephyr_add_prj_conf("BT_NUS", True) expose_log = config[CONF_TYPE] == CONF_LOGS @@ -31,3 +66,11 @@ async def to_code(config): if expose_log: request_log_listener() # Request a log listener slot for BLE NUS log streaming await cg.register_component(var, config) + cg.add_define("ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE", config[CONF_TX_BUFFER_SIZE]) + if CONF_RX_BUFFER_SIZE in config: + cg.add_define( + "ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE", config[CONF_RX_BUFFER_SIZE] + ) + if CONF_DEBUG in config: + cg.add_global(uart_ns.using) + await debug_to_code(config[CONF_DEBUG], var) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index a10132eb3ef..d1710100a09 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -11,25 +11,111 @@ namespace esphome::ble_nus { -constexpr size_t BLE_TX_BUF_SIZE = 2048; - // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) BLENUS *global_ble_nus; -RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); +RING_BUF_DECLARE(global_ble_tx_ring_buf, ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE +RING_BUF_DECLARE(global_ble_rx_ring_buf, ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE); +#endif // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) static const char *const TAG = "ble_nus"; -size_t BLENUS::write_array(const uint8_t *data, size_t len) { +void BLENUS::write_array(const uint8_t *data, size_t len) { if (atomic_get(&this->tx_status_) == TX_DISABLED) { - return 0; + return; + } + auto sent = ring_buf_put(&global_ble_tx_ring_buf, data, len); + if (sent < len) { + ESP_LOGE(TAG, "TX dropping %u bytes", len - sent); + return; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool BLENUS::peek_byte(uint8_t *data) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +#else + return false; +#endif +} + +bool BLENUS::read_array(uint8_t *data, size_t len) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (len == 0) { + return true; + } + if (this->available() < len) { + return false; + } + + // First, use the peek buffer if available + if (this->has_peek_) { + data[0] = this->peek_buffer_; + this->has_peek_ = false; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + if (ring_buf_get(&global_ble_rx_ring_buf, data, len) != len) { + ESP_LOGE(TAG, "UART BLE unexpected size"); + return false; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + return true; +#else + return false; +#endif +} + +size_t BLENUS::available() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + uint32_t size = ring_buf_size_get(&global_ble_rx_ring_buf); + ESP_LOGVV(TAG, "UART BLE available %u", size); + return size + (this->has_peek_ ? 1 : 0); +#else + return 0; +#endif +} + +void BLENUS::flush() { + constexpr uint32_t timeout_5sec = 5000; + uint32_t start = millis(); + while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { + if (millis() - start > timeout_5sec) { + ESP_LOGW(TAG, "Flush timeout"); + return; + } + delay(1); } - return ring_buf_put(&global_ble_tx_ring_buf, data, len); } void BLENUS::connected(bt_conn *conn, uint8_t err) { if (err == 0) { global_ble_nus->conn_.store(bt_conn_ref(conn)); + global_ble_nus->connected_ = true; } } @@ -38,6 +124,7 @@ void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { bt_conn_unref(global_ble_nus->conn_.load()); // Connection array is global static. // Reference can be kept even if disconnected. + global_ble_nus->connected_ = false; } } @@ -63,12 +150,19 @@ void BLENUS::send_enabled_callback(bt_nus_send_status status) { break; } } - void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { - ESP_LOGD(TAG, "Received %d bytes.", len); + ESP_LOGV(TAG, "Received %d bytes.", len); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + auto recv_len = ring_buf_put(&global_ble_rx_ring_buf, data, len); + if (recv_len < len) { + ESP_LOGE(TAG, "RX dropping %u bytes", len - recv_len); + } +#endif } - void BLENUS::setup() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + this->rx_buffer_size_ = ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE; +#endif bt_nus_cb callbacks = { .received = rx_callback, .sent = tx_callback, @@ -106,16 +200,17 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t #endif void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, - "ble nus:\n" - " log: %s", - YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); - if (conn) { + if (conn && this->connected_) { mtu = bt_nus_get_mtu(conn); } - ESP_LOGCONFIG(TAG, " MTU: %u", mtu); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s\n" + " connected: %s\n" + " MTU: %u", + YESNO(this->expose_log_), YESNO(this->connected_.load()), mtu); } void BLENUS::loop() { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b2b0ee7713a..67e9ae9f97a 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -10,7 +11,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public uart::UARTComponent, public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -21,7 +22,12 @@ class BLENUS : public Component { void setup() override; void dump_config() override; void loop() override; - size_t write_array(const uint8_t *data, size_t len); + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + void flush() override; + void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); @@ -37,6 +43,12 @@ class BLENUS : public Component { std::atomic conn_ = nullptr; bool expose_log_ = false; atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); + std::atomic connected_{}; +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + // RX buffer for peek functionality + uint8_t peek_buffer_{0}; + bool has_peek_{false}; +#endif }; } // namespace esphome::ble_nus diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1a6d9b3a803..be5fdc9006e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -356,6 +356,8 @@ #endif #ifdef USE_NRF52 +#define ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE 512 +#define ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE 512 #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_LOGGER_EARLY_MESSAGE #define USE_LOGGER_UART_SELECTION_USB_CDC diff --git a/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml new file mode 100644 index 00000000000..0d917ec1151 --- /dev/null +++ b/tests/components/ble_nus/test-uart.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +ble_nus: + type: uart + tx_buffer_size: 160 + rx_buffer_size: 160 From 666fb7cf39aeaf2e2a7890695099535ed8b5a67c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:28 -0500 Subject: [PATCH 08/16] [sx127x][sx126x][max6956] Fix null deref, unterminated string, and pin bounds check (#14529) Co-authored-by: Claude Opus 4.6 --- esphome/components/max6956/max6956.cpp | 2 ++ esphome/components/sx126x/sx126x.cpp | 3 ++- esphome/components/sx127x/sx127x.cpp | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index a350e66ee0e..ce45541b635 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -111,6 +111,8 @@ void MAX6956::write_brightness_mode() { } void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + if (pin < MAX6956_MIN || pin > MAX6956_MAX) + return; uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; uint8_t config = 0; uint8_t shift = 4 * (pin % 2); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 64cd24b1713..ec62fad10af 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -155,7 +155,8 @@ void SX126x::configure() { } // check silicon version to make sure hw is ok - this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16); + this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_)); + this->version_[sizeof(this->version_) - 1] = '\0'; if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) { this->mark_failed(); return; diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index f6aa11b6347..66957a73424 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -260,6 +260,11 @@ SX127xError SX127x::transmit_packet(const std::vector &packet) { return SX127xError::INVALID_PARAMS; } + if (this->dio0_pin_ == nullptr) { + ESP_LOGE(TAG, "DIO0 pin not configured, cannot wait for transmit completion"); + return SX127xError::INVALID_PARAMS; + } + SX127xError ret = SX127xError::NONE; if (this->modulation_ == MOD_LORA) { this->set_mode_standby(); From 6c07c15c504c7f0a66ab1a9e2c2f91943526ffa2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:18:56 -0500 Subject: [PATCH 09/16] [mipi_dsi][e131] Fix semaphore cast, missing return, and light count overread (#14530) Co-authored-by: Claude Opus 4.6 --- esphome/components/e131/e131_addressable_light_effect.cpp | 4 +++- esphome/components/mipi_dsi/mipi_dsi.cpp | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7d62f739a24..f6010a7cc9f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -54,8 +54,10 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received + // packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights + int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0; int output_end = - std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); + std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet)); auto *input_data = packet.values + 1; auto effect_name = get_name(); diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 4d45cfb7990..815b9d75a1d 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -10,7 +10,7 @@ namespace mipi_dsi { static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { - auto *sem = static_cast(user_ctx); + auto sem = static_cast(user_ctx); BaseType_t need_yield = pdFALSE; xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); @@ -190,6 +190,7 @@ void MIPI_DSI::draw_pixels_at(int x_start, int y_start, int w, int h, const uint if (bitness != this->color_depth_) { display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; } this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); } From c0b7f41397e41ff53ed2d551fcd0e3cf03f89147 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:21:44 +0100 Subject: [PATCH 10/16] [esp32] Fix wrong variable usage in P4 pin validation error msg (#14539) --- esphome/components/esp32/gpio_esp32_p4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index b98b567da2c..2726c5932fa 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -33,7 +33,7 @@ def esp32_p4_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 54: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)") if is_input: # All ESP32 pins support input mode pass From 5084c32f3c3da1bb857b79f6200825ec456000c8 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 6 Mar 2026 13:22:11 +0100 Subject: [PATCH 11/16] [esp32] Fix ESP32-S3 pin validation error message (#14540) --- esphome/components/esp32/gpio_esp32_s3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index 71205046933..aea378f499d 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) def esp32_s3_validate_gpio_pin(value): if value < 0 or value > 48: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") if value in _ESP_32S3_SPI_PSRAM_PINS: raise cv.Invalid( @@ -55,7 +55,7 @@ def esp32_s3_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 48: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)") if is_input: # All ESP32 pins support input mode pass From e59a2b3eded37c9de46d8148b2f8cb40b46d06ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 6 Mar 2026 17:25:44 +0100 Subject: [PATCH 12/16] [nrf52] prepare for usb cdc (#14174) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 2 ++ esphome/core/event_pool.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index a14d3af69ef..8ad84656ede 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -464,6 +464,8 @@ def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This featur unsupported = [unsupported] def validator_(obj): + if not CORE.is_esp32: + raise cv.Invalid(f"{msg_prefix} is only available on ESP32") variant = get_esp32_variant() if supported is not None and variant not in supported: raise cv.Invalid( diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 928a4e7dee2..99541d4a179 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_ZEPHYR) #include #include From 07e51886f3563061c09e6a8c9c36ab94300b0b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:57:52 -1000 Subject: [PATCH 13/16] [core] Move entity icon strings to PROGMEM on ESP8266 (#14437) --- esphome/components/api/api_connection.h | 3 +- esphome/components/mqtt/mqtt_component.cpp | 10 ++--- esphome/components/mqtt/mqtt_component.h | 4 +- esphome/components/web_server/web_server.cpp | 3 +- esphome/config_validation.py | 17 ++++--- esphome/core/config.py | 4 ++ esphome/core/entity_base.cpp | 38 ++++++++++++++-- esphome/core/entity_base.h | 33 +++++++++++--- esphome/core/entity_helpers.py | 47 +++++++++++++++++--- tests/unit_tests/core/test_entity_helpers.py | 17 +++++++ tests/unit_tests/test_config_validation.py | 12 +++++ 11 files changed, 158 insertions(+), 30 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index aae8db3c688..88f0ef82d66 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -354,7 +354,8 @@ class APIConnection final : public APIServerConnectionBase { // Set common EntityBase properties #ifdef USE_ENTITY_ICON - msg.icon = entity->get_icon_ref(); + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); #endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f49069960b3..98fa10def95 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -209,12 +209,11 @@ bool MQTTComponent::send_discovery_() { if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - const auto icon_ref = this->get_icon_ref_(); - if (!icon_ref.empty()) { - root[MQTT_ICON] = icon_ref; + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = this->get_icon_to_(icon_buf); + if (icon[0] != '\0') { + root[MQTT_ICON] = icon; } - // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); if (entity_category != ENTITY_CATEGORY_NONE) { @@ -413,7 +412,6 @@ const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity StringRef MQTTComponent::get_default_object_id_to_(std::span buf) const { return this->get_entity()->get_object_id_to(buf); } -StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::compute_is_internal_() { if (this->custom_state_topic_.has_value()) { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 0ffe6341d37..2403ef64ea3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -298,8 +298,8 @@ class MQTTComponent : public Component { /// Get the friendly name of this MQTT component. const StringRef &friendly_name_() const; - /// Get the icon field of this component as StringRef - StringRef get_icon_ref_() const; + /// Get the icon field of this component into a stack buffer + const char *get_icon_to_(std::span buf) const { return this->get_entity()->get_icon_to(buf); } /// Get whether the underlying Entity is disabled by default bool is_disabled_by_default_() const; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6b94a103cc9..bc90c88e57f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -568,7 +568,8 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J } #endif #ifdef USE_ENTITY_ICON - root[ESPHOME_F("icon")] = obj->get_icon_ref().c_str(); + char icon_buf[MAX_ICON_LENGTH]; + root[ESPHOME_F("icon")] = obj->get_icon_to(icon_buf); #endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3b0e4da298a..1eac53e9b20 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -400,14 +400,21 @@ def string_strict(value): def icon(value): """Validate that a given config value is a valid icon.""" + from esphome.core.config import ICON_MAX_LENGTH + value = string_strict(value) if not value: return value - if re.match("^[\\w\\-]+:[\\w\\-]+$", value): - return value - raise Invalid( - 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' - ) + if not re.match("^[\\w\\-]+:[\\w\\-]+$", value): + raise Invalid( + 'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"' + ) + if len(value) > ICON_MAX_LENGTH: + raise Invalid( + f"Icon string is too long ({len(value)} chars, max {ICON_MAX_LENGTH}). " + "Icons are stored in PROGMEM with a 64-byte buffer limit." + ) + return value def sub_device_id(value: str | None) -> core.ID | None: diff --git a/esphome/core/config.py b/esphome/core/config.py index 4f526404fe8..9093ab3fe9f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -188,6 +188,10 @@ else: # Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h FRIENDLY_NAME_MAX_LEN = 120 +# Max icon string length (63 chars + null = 64-byte PROGMEM buffer) +# Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h +ICON_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index eafc04f92a4..12652775722 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,6 +1,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" namespace esphome { @@ -72,7 +73,27 @@ std::string EntityBase::get_unit_of_measurement() const { return std::string(this->get_unit_of_measurement_ref().c_str()); } -// Entity icon (from index) +// Entity icon — buffer-based API for PROGMEM safety on ESP8266 +const char *EntityBase::get_icon_to([[maybe_unused]] std::span buffer) const { +#ifdef USE_ENTITY_ICON + const uint8_t idx = this->icon_idx_; +#else + const uint8_t idx = 0; +#endif +#ifdef USE_ESP8266 + if (idx == 0) + return ""; + const char *icon = entity_icon_lookup(idx); + ESPHOME_strncpy_P(buffer.data(), icon, buffer.size() - 1); + buffer[buffer.size() - 1] = '\0'; + return buffer.data(); +#else + return entity_icon_lookup(idx); +#endif +} + +#ifndef USE_ESP8266 +// Deprecated icon accessors — not available on ESP8266 (rodata is RAM) StringRef EntityBase::get_icon_ref() const { #ifdef USE_ENTITY_ICON return StringRef(entity_icon_lookup(this->icon_idx_)); @@ -80,7 +101,14 @@ StringRef EntityBase::get_icon_ref() const { return StringRef(entity_icon_lookup(0)); #endif } -std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); } +std::string EntityBase::get_icon() const { +#ifdef USE_ENTITY_ICON + return std::string(entity_icon_lookup(this->icon_idx_)); +#else + return std::string(entity_icon_lookup(0)); +#endif +} +#endif // !USE_ESP8266 // Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { @@ -154,8 +182,10 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve #ifdef USE_ENTITY_ICON void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { - if (!obj.get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); + char icon_buf[MAX_ICON_LENGTH]; + const char *icon = obj.get_icon_to(icon_buf); + if (icon[0] != '\0') { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, icon); } } #endif diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 54d4ae311f2..1ce1e658e02 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -36,6 +36,10 @@ static constexpr size_t OBJECT_ID_MAX_LEN = 128; // Maximum state length that Home Assistant will accept without raising ValueError static constexpr size_t MAX_STATE_LEN = 255; +// Maximum icon string buffer size (63 chars + null terminator) +// Icons are stored in PROGMEM; on ESP8266 they must be copied to a stack buffer. +static constexpr size_t MAX_ICON_LENGTH = 64; + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -124,12 +128,31 @@ class EntityBase { "2026.3.0") std::string get_unit_of_measurement() const; - // Get/set this entity's icon - ESPDEPRECATED( - "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", - "2025.11.0") - std::string get_icon() const; + // Get this entity's icon into a stack buffer. + // On ESP32: returns pointer to PROGMEM string directly (buffer unused). + // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. + const char *get_icon_to(std::span buffer) const; + +#ifdef USE_ESP8266 + // On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed + // directly as const char*. Use get_icon_to() with a stack buffer instead. + template StringRef get_icon_ref() const { + static_assert(sizeof(T) == 0, + "get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return StringRef(""); + } + template std::string get_icon() const { + static_assert(sizeof(T) == 0, + "get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer."); + return ""; + } +#else + // Deprecated: use get_icon_to() instead. Icons are in PROGMEM. + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") StringRef get_icon_ref() const; + ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") + std::string get_icon() const; +#endif #ifdef USE_DEVICES // Get/set this entity's device id diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 551e35df65c..01fa27b833a 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core.config import ICON_MAX_LENGTH from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -78,6 +79,8 @@ def _generate_category_code( table_var: str, lookup_fn: str, strings: dict[str, int], + *, + progmem_strings: bool = False, ) -> str: """Generate C++ code for one string category (PROGMEM pointer table + lookup). @@ -85,14 +88,40 @@ def _generate_category_code( in flash (via PROGMEM) and read with progmem_read_ptr(). String literals themselves remain in RAM but benefit from linker string deduplication. Index 0 means "not set" and returns empty string. + + When progmem_strings=True, each string is declared as a separate PROGMEM + char array. This ensures the string data itself is in flash on ESP8266 + (where .rodata is RAM). On other platforms PROGMEM is a no-op. """ if not strings: return "" sorted_strings = sorted(strings.items(), key=lambda x: x[1]) - entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) count = len(sorted_strings) + if progmem_strings: + # Emit individual PROGMEM char arrays so string data lives in flash + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + entries = ", ".join(var_names) + # Empty string must also be PROGMEM — on ESP8266, callers use strncpy_P + empty_var = f"{table_var}_EMPTY" + lines.append(f'static const char {empty_var}[] PROGMEM = "";') + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const char *{lookup_fn}(uint8_t index) {{") + lines.append(f" if (index == 0 || index > {count}) return {empty_var};") + lines.append(f" return progmem_read_ptr(&{table_var}[index - 1]);") + lines.append("}") + return "\n".join(lines) + "\n" + + entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings) + return ( f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n" f"const char *{lookup_fn}(uint8_t index) {{\n" @@ -103,9 +132,9 @@ def _generate_category_code( _CATEGORY_CONFIGS = ( - ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"), - ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"), - ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"), + ("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes", False), + ("ENTITY_UOM_TABLE", "entity_uom_lookup", "units", False), + ("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons", True), ) @@ -117,8 +146,10 @@ async def _generate_tables_job() -> None: """ pool = _get_pool() parts = ["namespace esphome {"] - for table_var, lookup_fn, attr in _CATEGORY_CONFIGS: - code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr)) + for table_var, lookup_fn, attr, progmem_strs in _CATEGORY_CONFIGS: + code = _generate_category_code( + table_var, lookup_fn, getattr(pool, attr), progmem_strings=progmem_strs + ) if code: parts.append(code) parts.append("} // namespace esphome") @@ -160,6 +191,10 @@ def register_unit_of_measurement(value: str) -> int: def register_icon(value: str) -> int: """Register an icon string and return its 1-based index.""" + if value and len(value) > ICON_MAX_LENGTH: + raise ValueError( + f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon") diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index a5cfad5ab69..79bc3095b92 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -23,6 +23,7 @@ from esphome.core.entity_helpers import ( _setup_entity_impl, entity_duplicate_validator, get_base_entity_object_id, + register_icon, setup_entity, ) from esphome.cpp_generator import MockObj @@ -909,6 +910,22 @@ def test_register_string_overflow() -> None: _register_string("overflow", category, 3, "test") +def test_register_icon_max_length() -> None: + """Test register_icon rejects icons exceeding 63 characters.""" + # 63 chars should succeed + max_icon = "mdi:" + "a" * 59 # 63 total + idx = register_icon(max_icon) + assert idx > 0 + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 total + with pytest.raises(ValueError, match="Icon string too long"): + register_icon(too_long) + + # Empty string returns 0 + assert register_icon("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9602010ad30..c1849daf4ba 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -148,6 +148,18 @@ def test_icon__invalid(): config_validation.icon("foo") +def test_icon__max_length(): + """Test that icons exceeding 63 characters are rejected.""" + # Exactly 63 chars should pass + max_icon = "mdi:" + "a" * 59 # 63 chars total + assert config_validation.icon(max_icon) == max_icon + + # 64 chars should fail + too_long = "mdi:" + "a" * 60 # 64 chars total + with pytest.raises(Invalid, match="Icon string is too long"): + config_validation.icon(too_long) + + @pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True)) def test_boolean__valid_true(value): assert config_validation.boolean(value) is True From 74e4b69654c2dd77f81cd712fe1c7ece3db78bfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 06:58:13 -1000 Subject: [PATCH 14/16] [core] Replace Application name/friendly_name std::string with StringRef (#14532) Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 6 +- esphome/components/openthread/openthread.cpp | 2 +- .../components/web_server/web_server_v1.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 2 +- esphome/core/application.h | 54 +++++++------ esphome/core/config.py | 60 +++++++++++++-- esphome/core/defines.h | 1 + esphome/core/entity_base.cpp | 6 +- tests/dummy_main.cpp | 4 +- tests/unit_tests/core/test_config.py | 77 +++++++++++++++++++ 14 files changed, 181 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3ae35e9be81..ba4f2f0642d 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -269,7 +269,7 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello - const std::string &name = App.get_name(); + const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 9d260188003..bbe972b9f33 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -273,7 +273,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5e5e1279d95..342a6e6c645 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -59,7 +59,7 @@ void MDNSComponent::compile_records_(StaticVectorget_port(); - const std::string &friendly_name = App.get_friendly_name(); + const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 98fa10def95..d31a78b0900 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -267,7 +267,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_UNIQUE_ID] = unique_id_buf; } - const std::string &node_name = App.get_name(); + const auto &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { // node_name (max 31) + "_" (1) + object_id (max 128) + null char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; @@ -275,8 +275,8 @@ bool MQTTComponent::send_discovery_() { root[MQTT_OBJECT_ID] = object_id_full; } - const std::string &friendly_name_ref = App.get_friendly_name(); - const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; + const auto &friendly_name_ref = App.get_friendly_name(); + const auto &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 9452f5a41eb..fb814812997 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() { // set the host name uint16_t size; char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size); - const std::string &host_name = App.get_name(); + const auto &host_name = App.get_name(); uint16_t host_name_len = host_name.size(); if (host_name_len > size) { ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name"); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index f7b90018dc6..85a4e80541b 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -75,7 +75,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); - const std::string &title = App.get_name(); + const auto &title = App.get_name(); stream->print(ESPHOME_F("")); stream->print(title.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b60810d28a..60764955cc9 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -913,7 +913,7 @@ void WiFiComponent::setup_ap_config_() { static constexpr size_t AP_SSID_PREFIX_LEN = 25; static constexpr size_t AP_SSID_SUFFIX_LEN = 7; - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); const char *name_ptr = app_name.c_str(); size_t name_len = app_name.length(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 355832b4340..a9b26c5935e 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -212,7 +212,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return addresses; } bool WiFiComponent::wifi_apply_hostname_() { - const std::string &hostname = App.get_name(); + const auto &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Set hostname failed"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 40f8a00edd3..87f9fdf59a1 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -138,26 +138,36 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { +#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. + void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); - this->name_add_mac_suffix_ = name_add_mac_suffix; - if (name_add_mac_suffix) { - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; - // MAC address suffix length (last 6 characters of 12-char MAC address string) - constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; - get_mac_address_into_buffer(mac_addr); - const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); - if (!friendly_name.empty()) { - this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); - } - } else { - this->name_ = name; - this->friendly_name_ = friendly_name; + this->name_add_mac_suffix_ = true; + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + // Overwrite the placeholder suffix in the mutable static buffers with actual MAC + // name is always non-empty (validated by validate_hostname in Python config) + memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); + if (friendly_name_len > 0) { + memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, + mac_address_suffix_len); } + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } +#else + /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. + void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { + arch_init(); + this->name_add_mac_suffix_ = false; + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); + } +#endif #ifdef USE_DEVICES void register_device(Device *device) { this->devices_.push_back(device); } @@ -274,10 +284,10 @@ class Application { void loop(); /// Get the name of this Application set by pre_setup(). - const std::string &get_name() const { return this->name_; } + const StringRef &get_name() const { return this->name_; } /// Get the friendly name of this Application set by pre_setup(). - const std::string &get_friendly_name() const { return this->friendly_name_; } + const StringRef &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). const char *get_area() const { @@ -627,9 +637,9 @@ class Application { #endif #endif - // std::string members (typically 24-32 bytes each) - std::string name_; - std::string friendly_name_; + // StringRef members (8 bytes each: pointer + size) + StringRef name_; + StringRef friendly_name_; // 4-byte members uint32_t last_loop_{0}; diff --git a/esphome/core/config.py b/esphome/core/config.py index 9093ab3fe9f..8631726a021 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -50,6 +50,7 @@ from esphome.core import ( ) from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, fnv1a_32bit_hash, get_str_env, walk_files, @@ -58,6 +59,38 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) +# C++ variable names and separators for app name buffers (used with MAC suffix) +_APP_NAME_BUF_VAR = "esphome_app_name_buf" +_APP_NAME_MAC_SEP = "-" +_APP_FRIENDLY_NAME_BUF_VAR = "esphome_app_friendly_name_buf" +_APP_FRIENDLY_NAME_MAC_SEP = " " +# Placeholder suffix for MAC address (last 6 hex chars) +_MAC_SUFFIX_PLACEHOLDER = "XXXXXX" + + +def make_app_name_cpp( + value: str, var_name: str, sep: str, *, add_mac_suffix: bool +) -> tuple[str, str | None, int]: + """Compute C++ expression and optional global declaration for an app name. + + Returns (cpp_expr, global_decl_or_none, byte_length). + - cpp_expr: The C++ expression to pass to pre_setup (var name or string literal). + - global_decl: A static char[] declaration string, or None if not needed. + - byte_length: The UTF-8 byte length of the string value. + """ + if add_mac_suffix: + buf_value = "" if not value else f"{value}{sep}{_MAC_SUFFIX_PLACEHOLDER}" + escaped = cpp_string_escape(buf_value) + return ( + var_name, + f"static char {var_name}[] = {escaped};", + len(buf_value.encode("utf-8")), + ) + if not value: + return '""', None, 0 + return cpp_string_escape(value), None, len(value.encode("utf-8")) + + StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() ) @@ -78,6 +111,8 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} def validate_hostname(config): # Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h + if not config[CONF_NAME]: + raise cv.Invalid("Hostname must not be empty", path=[CONF_NAME]) max_length = 31 if config[CONF_NAME_ADD_MAC_SUFFIX]: max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used @@ -555,13 +590,28 @@ async def to_code(config: ConfigType) -> None: # Construct App via placement new — see application.cpp for storage details cg.add_global(cg.RawStatement("#include <new>")) cg.add(cg.RawExpression("new (&App) Application()")) - cg.add( - cg.App.pre_setup( - config[CONF_NAME], - config[CONF_FRIENDLY_NAME], - config[CONF_NAME_ADD_MAC_SUFFIX], + name = config[CONF_NAME] + friendly_name = config[CONF_FRIENDLY_NAME] + name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] + + def _emit_app_name( + value: str, var_name: str, sep: str + ) -> tuple[cg.Expression, int]: + """Emit codegen for an app name and return (expression, byte_length).""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + value, var_name, sep, add_mac_suffix=name_add_mac_suffix ) + if global_decl is not None: + cg.add_global(cg.RawStatement(global_decl)) + return cg.RawExpression(cpp_expr), byte_len + + name_expr, name_len = _emit_app_name(name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP) + friendly_expr, friendly_len = _emit_app_name( + friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP ) + if name_add_mac_suffix: + cg.add_define("ESPHOME_NAME_ADD_MAC_SUFFIX") + cg.add(cg.App.pre_setup(name_expr, name_len, friendly_expr, friendly_len)) # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index be5fdc9006e..c5f38ab9aab 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -13,6 +13,7 @@ #define ESPHOME_PROJECT_VERSION "v2" #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" +#define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 12652775722..37e7fcc9987 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -23,13 +23,13 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { // Bug-for-bug compatibility with OLD behavior: // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name - const std::string &friendly = App.get_friendly_name(); + const auto &friendly = App.get_friendly_name(); if (App.is_name_add_mac_suffix_enabled()) { // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility - this->name_ = StringRef(friendly); + this->name_ = friendly; } else { // No MAC suffix - fallback to device name if friendly_name is empty - this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + this->name_ = !friendly.empty() ? friendly : App.get_name(); } } this->flags_.has_own_name = false; diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ccf35e04d2..6fa0c08aa3d 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,9 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", false); + static char name[] = "livingroom"; + static char friendly_name[] = "LivingRoom"; + App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); auto *log = new logger::Logger(115200); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 88801a9ca03..474d31a90af 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -23,6 +23,7 @@ from esphome.const import ( from esphome.core import CORE, config from esphome.core.config import ( Area, + make_app_name_cpp, preload_core_config, valid_include, valid_project_name, @@ -969,3 +970,79 @@ def test_config_hash_different_for_different_configs() -> None: hash2 = CORE.config_hash assert hash1 != hash2 + + +def test_make_app_name_cpp_no_mac_simple() -> None: + """Test simple name without MAC suffix returns string literal.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '"my-device"' + assert global_decl is None + assert byte_len == 9 + + +def test_make_app_name_cpp_no_mac_empty() -> None: + """Test empty name without MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '""' + assert global_decl is None + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix() -> None: + """Test name with MAC suffix emits static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert "my-device-XXXXXX" in global_decl + assert byte_len == len("my-device-XXXXXX") + + +def test_make_app_name_cpp_mac_suffix_empty() -> None: + """Test empty name with MAC suffix emits empty static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix_space_sep() -> None: + """Test friendly name uses space separator for MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "My Device", "esphome_app_friendly_name_buf", " ", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_friendly_name_buf" + assert global_decl is not None + assert "My Device XXXXXX" in global_decl + assert byte_len == len("My Device XXXXXX") + + +def test_make_app_name_cpp_non_ascii_utf8_length() -> None: + """Test non-ASCII characters use UTF-8 byte length.""" + _, global_decl, byte_len = make_app_name_cpp( + "café", "buf", "-", add_mac_suffix=False + ) + assert byte_len == len("café".encode()) # 5 bytes, not 4 chars + assert global_decl is None + + +def test_make_app_name_cpp_non_ascii_mac_suffix_utf8_length() -> None: + """Test non-ASCII with MAC suffix uses UTF-8 byte length.""" + _, _, byte_len = make_app_name_cpp("café", "buf", "-", add_mac_suffix=True) + assert byte_len == len("café-XXXXXX".encode()) + + +def test_make_app_name_cpp_special_chars_escaped() -> None: + """Test special characters are properly escaped in C++ string.""" + cpp_expr, _, _ = make_app_name_cpp('my "device"', "buf", "-", add_mac_suffix=False) + # cpp_string_escape uses octal escapes for quotes + assert '"' not in cpp_expr[1:-1] # no unescaped quotes inside the outer quotes From a16b8fc0ac30a015df61555d59fb98e19a9efe6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:00:31 -1000 Subject: [PATCH 15/16] [rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528) --- esphome/components/rp2040/boards.jinja2 | 25 + esphome/components/rp2040/boards.py | 2199 ++++++++++++++++- esphome/components/rp2040/generate_boards.py | 186 ++ esphome/components/rp2040/gpio.py | 22 +- .../components/test_rp2040_generate_boards.py | 273 ++ 5 files changed, 2688 insertions(+), 17 deletions(-) create mode 100644 esphome/components/rp2040/boards.jinja2 create mode 100644 esphome/components/rp2040/generate_boards.py create mode 100644 tests/unit_tests/components/test_rp2040_generate_boards.py diff --git a/esphome/components/rp2040/boards.jinja2 b/esphome/components/rp2040/boards.jinja2 new file mode 100644 index 00000000000..989fb83701a --- /dev/null +++ b/esphome/components/rp2040/boards.jinja2 @@ -0,0 +1,25 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= {{ cyw43_gpio_offset }} to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = {{ cyw43_gpio_offset }} +CYW43_MAX_GPIO = {{ cyw43_max_gpio }} +DEFAULT_MAX_PIN = {{ default_max_pin }} + +RP2040_BASE_PINS = {} + +RP2040_BOARD_PINS = { +{%- for name, pins in board_pins %} + {{ name | repr }}: {{ pins | format_pins }}, +{%- endfor %} +} + +BOARDS = { +{%- for name, info in boards %} + {{ name | repr }}: { + {%- for key, value in info.items() %} + {{ key | repr }}: {{ value | repr }}, + {%- endfor %} + }, +{%- endfor %} +} diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c761efba586..c99934567a1 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1,28 +1,2205 @@ +# Auto-generated by generate_boards.py — do not edit manually +# To regenerate: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs +CYW43_GPIO_OFFSET = 64 +CYW43_MAX_GPIO = 66 +DEFAULT_MAX_PIN = 29 + RP2040_BASE_PINS = {} RP2040_BOARD_PINS = { - "pico": { - "SDA": 4, - "SCL": 5, - "LED": 25, - "SDA1": 26, - "SCL1": 27, + "0xcb_helios": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL1": 3, + "SDA1": 2, + "SS": 21, + "TX": 0, }, - "rpipico": "pico", - "rpipicow": { - "SDA": 4, + "DudesCab": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 19, + "SCL1": 11, + "SDA": 18, + "SDA1": 10, + "SS": 5, + "TX": 0, + }, + "MyRP_2350B": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, "SCL": 5, - "LED": 32, - "SDA1": 26, "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "MyRP_bot": { + "LED": 25, + "MISO": 12, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 4, + "SDA": 16, + "SDA1": 5, + "SS": 13, + }, + "adafruit_feather": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 25, + "SDA": 2, + "SDA1": 24, + "SS": 17, + "TX": 0, + }, + "adafruit_feather_adalogger": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_can": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_dvi": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_prop_maker": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rfm": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_adalogger": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_rp2350_hstx": { + "LED": 7, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 31, + "SDA": 2, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "adafruit_feather_scorpio": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_thinkink": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_feather_usb_host": { + "LED": 13, + "MISO": 8, + "MOSI": 15, + "RX": 1, + "SCK": 14, + "SCL": 3, + "SDA": 2, + "SS": 13, + "TX": 0, + }, + "adafruit_floppsy": { + "LED": 28, + "MISO": 20, + "MOSI": 19, + "SCK": 18, + "SCL": 17, + "SDA": 16, + "SS": 24, + }, + "adafruit_fruitjam": { + "LED": 29, + "MISO": 36, + "MOSI": 35, + "RX": 9, + "SCK": 34, + "SCL": 21, + "SDA": 20, + "SS": 39, + "TX": 8, + }, + "adafruit_itsybitsy": { + "LED": 11, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 25, + "SCL1": 3, + "SDA": 24, + "SDA1": 2, + "TX": 0, + }, + "adafruit_kb2040": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "TX": 0, + }, + "adafruit_macropad2040": {"LED": 13, "SCL": 21, "SDA": 20}, + "adafruit_metro": { + "LED": 13, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "SS": 23, + "TX": 0, + }, + "adafruit_metro_rp2350": { + "LED": 23, + "MISO": 28, + "MOSI": 31, + "RX": 1, + "SCK": 30, + "SCL": 21, + "SDA": 20, + "SS": 29, + "TX": 0, + }, + "adafruit_qtpy": { + "MISO": 4, + "MOSI": 3, + "RX": 29, + "SCK": 6, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "TX": 28, + }, + "adafruit_stemmafriend": { + "LED": 12, + "MISO": 4, + "MOSI": 7, + "RX": 27, + "SCK": 2, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 1, + "TX": 26, + }, + "adafruit_trinkeyrp2040qt": {"RX": 17, "SCL": 17, "SDA": 16, "TX": 16}, + "akana_r1": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "amken_bunny": {"LED": 24, "RX": 1, "TX": 0}, + "amken_revelop": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "amken_revelop_es": {"LED": 5, "MISO": 0, "MOSI": 3, "SCK": 2, "SS": 1, "TX": 20}, + "amken_revelop_plus": {"LED": 24, "RX": 1, "SCL": 29, "SDA": 28, "TX": 0}, + "artronshop_rp2_nano": { + "LED": 13, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 19, + "SDA": 16, + "SDA1": 18, + "SS": 5, + "TX": 0, + }, + "bigtreetech_SKR_Pico": {"LED": 13, "RX": 1, "TX": 0}, + "breadstick_raspberry": { + "RX": 21, + "SCL": 13, + "SCL1": 23, + "SDA": 12, + "SDA1": 22, + "TX": 20, + }, + "bridgetek_idm2040_43a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "bridgetek_idm2040_7a": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "challenger_2040_lora": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_lte": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_nfc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SCL1": 11, + "SDA": 0, + "SDA1": 10, + "SS": 21, + "TX": 16, + }, + "challenger_2040_sdrtc": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_subghz": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_uwb": { + "LED": 24, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi6_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2040_wifi_ble": { + "LED": 10, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "challenger_2350_bconnect": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 11, + "SDA": 20, + "SDA1": 10, + "SS": 17, + "TX": 12, + }, + "challenger_2350_wifi6_ble5": { + "LED": 7, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 17, + "TX": 12, + }, + "challenger_nb_2040_wifi": { + "LED": 12, + "MISO": 24, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "connectivity_2040_lte_wifi_ble": { + "LED": 19, + "MISO": 20, + "MOSI": 23, + "RX": 17, + "SCK": 22, + "SCL": 1, + "SDA": 0, + "SS": 21, + "TX": 16, + }, + "cytron_iriv_io_controller": { + "LED": 29, + "MISO": 20, + "MOSI": 19, + "RX": 31, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 31, + }, + "cytron_maker_nano_rp2040": { + "LED": 2, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 1, + "SCL1": 27, + "SDA": 0, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "cytron_maker_pi_rp2040": { + "LED": 3, + "RX": 1, + "SCL": 17, + "SCL1": 3, + "SDA": 16, + "SDA1": 2, + "TX": 0, + }, + "cytron_maker_uno_rp2040": { + "LED": 3, + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 21, + "SCL1": 27, + "SDA": 20, + "SDA1": 26, + "SS": 13, + "TX": 0, + }, + "cytron_motion_2350_pro": { + "LED": 2, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 17, + "SCL1": 27, + "SDA": 16, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "datanoisetv_picoadk": { + "LED": 15, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "datanoisetv_picoadk_v2": { + "LED": 2, + "MISO": 8, + "MOSI": 7, + "RX": 13, + "SCK": 6, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 5, + "TX": 12, + }, + "degz_suibo": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "dfrobot_beetle_rp2040": { + "LED": 13, + "MISO": 0, + "MOSI": 3, + "RX": 29, + "SCK": 2, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 1, + "TX": 28, + }, + "electroniccats_huntercat_nfc": {"LED": 8, "RX": 1, "SCL": 5, "SDA": 4, "TX": 0}, + "evn_alpha": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 1, + "TX": 0, + }, + "extelec_rc2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SDA": 4, + "SS": 5, + "TX": 0, + }, + "flyboard2040_core": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 15, + "SDA": 16, + "SDA1": 14, + "SS": 5, + "TX": 0, + }, + "geeekpi_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "generic_rp2350": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "groundstudio_marble_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "ilabs_rpico32": { + "MISO": 24, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "jumperless_v1": { + "LED": 25, + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 5, + "SCL1": 19, + "SDA": 4, + "SDA1": 18, + "SS": 1, + "TX": 16, + }, + "jumperless_v5": { + "LED": 17, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 21, + "TX": 0, + }, + "melopero_cookie_rp2040": { + "LED": 21, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "melopero_shake_rp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 3, + "SDA": 8, + "SDA1": 2, + "SS": 1, + "TX": 0, + }, + "mksthr36": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "mksthr42": { + "MISO": 16, + "MOSI": 19, + "SCK": 18, + "SCL": 23, + "SDA": 22, + "SS": 17, + "TX": 6, + }, + "nekosystems_bl2040_mini": { + "LED": 6, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 25, + "SCL1": 23, + "SDA": 24, + "SDA1": 22, + "SS": 17, + "TX": 12, + }, + "newsan_archi": { + "MISO": 4, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 1, + "SCL1": 7, + "SDA": 0, + "SDA1": 6, + "SS": 5, + "TX": 16, + }, + "nullbits_bit_c_pro": { + "LED": 18, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 3, + "SCL1": 5, + "SDA": 2, + "SDA1": 4, + "SS": 21, + "TX": 0, + }, + "olimex_pico2bb48": { + "LED": 25, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 3, + "SDA": 12, + "SDA1": 2, + "SS": 5, + "TX": 0, + }, + "olimex_pico2xl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "olimex_pico2xxl": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "picolume": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "pimoroni_explorer": { + "MISO": 10, + "MOSI": 10, + "RX": 10, + "SCK": 10, + "SCL": 21, + "SCL1": 10, + "SDA": 20, + "SDA1": 10, + "SS": 10, + "TX": 10, + }, + "pimoroni_pico_plus_2": { + "LED": 25, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_pico_plus_2w": { + "LED": 64, + "MISO": 32, + "MOSI": 35, + "RX": 1, + "SCK": 34, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 33, + "TX": 0, + }, + "pimoroni_plasma2040": {"LED": 16, "SCL": 21, "SDA": 20}, + "pimoroni_plasma2350": { + "LED": 16, + "MISO": 31, + "MOSI": 31, + "RX": 31, + "SCK": 31, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 31, + "TX": 31, + }, + "pimoroni_plasma2350w": { + "LED": 16, + "MISO": 24, + "MOSI": 24, + "RX": 31, + "SCK": 29, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 25, + "TX": 31, + }, + "pimoroni_servo2040": {"LED": 18, "SCL": 21, "SDA": 20}, + "pimoroni_tiny2040": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "pimoroni_tiny2350": { + "LED": 19, + "MISO": 4, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 13, + "SCL1": 7, + "SDA": 12, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "pintronix_pinmax": { + "LED": 27, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SDA": 4, + "SS": 17, + "TX": 0, + }, + "rakwireless_rak11300": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 3, + "SCL1": 21, + "SDA": 2, + "SDA1": 20, + "SS": 17, + "TX": 0, + }, + "rpipico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipico2w": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "rpipicow": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "sea_picro": { + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "seeed_indicator_rp2040": { + "MISO": 0, + "MOSI": 3, + "RX": 17, + "SCK": 2, + "SCL": 21, + "SCL1": 15, + "SDA": 20, + "SDA1": 14, + "SS": 1, + "TX": 16, + }, + "seeed_xiao_rp2040": { + "LED": 17, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SDA": 6, + "TX": 0, + }, + "seeed_xiao_rp2350": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "silicognition_rp2040_shim": { + "MISO": 12, + "MOSI": 11, + "RX": 1, + "SCK": 10, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "soldered_nula_rp2350": { + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 9, + "SCL1": 31, + "SDA": 8, + "SDA1": 30, + "SS": 5, + "TX": 0, + }, + "solderparty_rp2040_stamp": { + "LED": 20, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "solderparty_rp2350_stamp": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "solderparty_rp2350_stamp_xl": { + "LED": 3, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 21, + "TX": 0, + }, + "sparkfun_iotnode_lorawanrp2350": { + "LED": 25, + "MISO": 12, + "MOSI": 15, + "RX": 19, + "SCK": 14, + "SCL": 21, + "SCL1": 31, + "SDA": 20, + "SDA1": 31, + "SS": 13, + "TX": 18, + }, + "sparkfun_iotredboard_rp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SCL1": 31, + "SDA": 4, + "SDA1": 30, + "SS": 21, + "TX": 0, + }, + "sparkfun_micromodrp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 5, + "SDA": 4, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2040": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SDA": 16, + "SS": 21, + "TX": 0, + }, + "sparkfun_promicrorp2350": { + "LED": 25, + "MISO": 20, + "MOSI": 23, + "RX": 1, + "SCK": 22, + "SCL": 17, + "SCL1": 31, + "SDA": 16, + "SDA1": 31, + "SS": 21, + "TX": 0, + }, + "sparkfun_thingplusrp2040": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 17, + "SCL1": 7, + "SDA": 16, + "SDA1": 6, + "TX": 0, + }, + "sparkfun_thingplusrp2350": { + "LED": 64, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 7, + "SCL1": 31, + "SDA": 6, + "SDA1": 31, + "SS": 9, + "TX": 0, + }, + "sparkfun_xrp_controller": { + "LED": 64, + "MISO": 16, + "MOSI": 19, + "RX": 13, + "SCK": 18, + "SCL": 5, + "SCL1": 39, + "SDA": 4, + "SDA1": 38, + "SS": 17, + "TX": 12, + }, + "sparkfun_xrp_controller_beta": {"LED": 64, "SCL": 19, "SDA": 18}, + "upesy_rp2040_devkit": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 23, + "SDA": 4, + "SDA1": 22, + "SS": 17, + "TX": 0, + }, + "vccgnd_yd_rp2040": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "vicharak_shrike-lite": { + "LED": 4, + "MISO": 20, + "MOSI": 19, + "RX": 17, + "SCK": 18, + "SCL": 25, + "SCL1": 7, + "SDA": 24, + "SDA1": 6, + "SS": 21, + "TX": 16, + }, + "viyalab_mizu": { + "LED": 25, + "MISO": 16, + "MOSI": 7, + "RX": 1, + "SCK": 6, + "SCL": 5, + "SCL1": 3, + "SDA": 4, + "SDA1": 2, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lcd_1_28": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_lora": { + "LED": 25, + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 7, + "SDA": 4, + "SDA1": 6, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_matrix": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_one": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2040_pizero": { + "MISO": 20, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 21, + "TX": 0, + }, + "waveshare_rp2040_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2040_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350_lcd_0_96": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_pizero": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_plus": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "waveshare_rp2350_zero": { + "MISO": 4, + "MOSI": 3, + "RX": 1, + "SCK": 2, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "waveshare_rp2350b_plus_w": { + "LED": 23, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 7, + "SDA": 8, + "SDA1": 6, + "SS": 17, + "TX": 0, + }, + "wiznet_55rp20_evb_pico": { + "LED": 19, + "MISO": 2, + "MOSI": 3, + "RX": 1, + "SCK": 4, + "SCL": 5, + "SCL1": 27, + "SDA": 4, + "SDA1": 26, + "SS": 5, + "TX": 0, + }, + "wiznet_wizfi360_evb_pico": { + "LED": 25, + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 27, + "SDA": 8, + "SDA1": 26, + "SS": 17, + "TX": 0, }, } BOARDS = { + "0xcb_helios": { + "name": "0xCB Helios", + "mcu": "rp2040", + "max_pin": 29, + }, + "DudesCab": { + "name": "L'atelier d'Arnoz DudesCab", + "mcu": "rp2040", + "max_pin": 29, + }, + "MyRP_2350B": { + "name": "MyMakers RP2350B", + "mcu": "rp2350", + "max_pin": 47, + }, + "MyRP_bot": { + "name": "MyMakers RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather": { + "name": "Adafruit Feather RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_adalogger": { + "name": "Adafruit Feather RP2040 Adalogger", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_can": { + "name": "Adafruit Feather RP2040 CAN", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_dvi": { + "name": "Adafruit Feather RP2040 DVI", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_prop_maker": { + "name": "Adafruit Feather RP2040 Prop-Maker", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rfm": { + "name": "Adafruit Feather RP2040 RFM", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_rp2350_adalogger": { + "name": "Adafruit Feather RP2350 Adalogger", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_rp2350_hstx": { + "name": "Adafruit Feather RP2350 HSTX", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_feather_scorpio": { + "name": "Adafruit Feather RP2040 SCORPIO", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_thinkink": { + "name": "Adafruit Feather RP2040 ThinkINK", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_feather_usb_host": { + "name": "Adafruit Feather RP2040 USB Host", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_floppsy": { + "name": "Adafruit Floppsy", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_fruitjam": { + "name": "Adafruit Fruit Jam RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_itsybitsy": { + "name": "Adafruit ItsyBitsy RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_kb2040": { + "name": "Adafruit KB2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_macropad2040": { + "name": "Adafruit MacroPad RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro": { + "name": "Adafruit Metro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_metro_rp2350": { + "name": "Adafruit Metro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "adafruit_qtpy": { + "name": "Adafruit QT Py RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_stemmafriend": { + "name": "Adafruit STEMMA Friend RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "adafruit_trinkeyrp2040qt": { + "name": "Adafruit Trinkey RP2040 QT", + "mcu": "rp2040", + "max_pin": 29, + }, + "akana_r1": { + "name": "METE HOCA Akana R1", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_bunny": { + "name": "Amken BunnyBoard", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop": { + "name": "Amken Revelop", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_es": { + "name": "Amken Revelop eS", + "mcu": "rp2040", + "max_pin": 29, + }, + "amken_revelop_plus": { + "name": "Amken Revelop Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "arduino_nano_connect": { + "name": "Arduino Nano RP2040 Connect", + "mcu": "rp2040", + "max_pin": 29, + }, + "artronshop_rp2_nano": { + "name": "ArtronShop RP2 Nano", + "mcu": "rp2040", + "max_pin": 29, + }, + "bigtreetech_SKR_Pico": { + "name": "BIGTREETECH SKR-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "breadstick_raspberry": { + "name": "Breadstick Raspberry", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_43a": { + "name": "BridgeTek IDM2040-43A", + "mcu": "rp2040", + "max_pin": 29, + }, + "bridgetek_idm2040_7a": { + "name": "BridgeTek IDM2040-7A", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lora": { + "name": "iLabs Challenger 2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_lte": { + "name": "iLabs Challenger 2040 LTE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_nfc": { + "name": "iLabs Challenger 2040 NFC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_sdrtc": { + "name": "iLabs Challenger 2040 SD/RTC", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_subghz": { + "name": "iLabs Challenger 2040 SubGHz", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_uwb": { + "name": "iLabs Challenger 2040 UWB", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi": { + "name": "iLabs Challenger 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi6_ble": { + "name": "iLabs Challenger 2040 WiFi6/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2040_wifi_ble": { + "name": "iLabs Challenger 2040 WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "challenger_2350_bconnect": { + "name": "iLabs Challenger 2350 BConnect", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_2350_wifi6_ble5": { + "name": "iLabs Challenger 2350 WiFi/BLE", + "mcu": "rp2350", + "max_pin": 47, + }, + "challenger_nb_2040_wifi": { + "name": "iLabs Challenger NB 2040 WiFi", + "mcu": "rp2040", + "max_pin": 29, + }, + "connectivity_2040_lte_wifi_ble": { + "name": "iLabs Connectivity 2040 LTE/WiFi/BLE", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_iriv_io_controller": { + "name": "Cytron IRIV IO Controller", + "mcu": "rp2350", + "max_pin": 47, + }, + "cytron_maker_nano_rp2040": { + "name": "Cytron Maker Nano RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_pi_rp2040": { + "name": "Cytron Maker Pi RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_maker_uno_rp2040": { + "name": "Cytron Maker Uno RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "cytron_motion_2350_pro": { + "name": "Cytron Motion 2350 Pro", + "mcu": "rp2350", + "max_pin": 47, + }, + "datanoisetv_picoadk": { + "name": "DatanoiseTV PicoADK", + "mcu": "rp2040", + "max_pin": 29, + }, + "datanoisetv_picoadk_v2": { + "name": "DatanoiseTV PicoADK v2", + "mcu": "rp2350", + "max_pin": 47, + }, + "degz_suibo": { + "name": "Degz Robotics Suibo RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "dfrobot_beetle_rp2040": { + "name": "DFRobot Beetle RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "electroniccats_huntercat_nfc": { + "name": "ElectronicCats HunterCat NFC RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "evn_alpha": { + "name": "EVN Alpha", + "mcu": "rp2040", + "max_pin": 29, + }, + "extelec_rc2040": { + "name": "ExtremeElectronics RC2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "flyboard2040_core": { + "name": "DeRuiLab FlyBoard2040Core", + "mcu": "rp2040", + "max_pin": 29, + }, + "geeekpi_rp2040_plus": { + "name": "GeeekPi RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic": { + "name": "Generic RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "generic_rp2350": { + "name": "Generic RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "groundstudio_marble_pico": { + "name": "GroundStudio Marble Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "ilabs_rpico32": { + "name": "iLabs RPICO32", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v1": { + "name": "Architeuthis Flux Jumperless", + "mcu": "rp2040", + "max_pin": 29, + }, + "jumperless_v5": { + "name": "Architeuthis Flux Jumperless V5", + "mcu": "rp2350", + "max_pin": 47, + }, + "melopero_cookie_rp2040": { + "name": "Melopero Cookie RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "melopero_shake_rp2040": { + "name": "Melopero Shake RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr36": { + "name": "Makerbase MKS THR36", + "mcu": "rp2040", + "max_pin": 29, + }, + "mksthr42": { + "name": "Makerbase MKS THR42", + "mcu": "rp2040", + "max_pin": 29, + }, + "nekosystems_bl2040_mini": { + "name": "Neko Systems BL2040 Mini", + "mcu": "rp2040", + "max_pin": 29, + }, + "newsan_archi": { + "name": "Newsan Archi", + "mcu": "rp2040", + "max_pin": 29, + }, + "nullbits_bit_c_pro": { + "name": "nullbits Bit-C PRO", + "mcu": "rp2040", + "max_pin": 29, + }, + "olimex_pico2bb48": { + "name": "Olimex Pico2BB48", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xl": { + "name": "Olimex Pico2XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_pico2xxl": { + "name": "Olimex Pico2XXL", + "mcu": "rp2350", + "max_pin": 47, + }, + "olimex_rp2040pico30": { + "name": "Olimex RP2040-Pico30", + "mcu": "rp2040", + "max_pin": 29, + }, + "picolume": { + "name": "PicoLume Transceiver", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_explorer": { + "name": "Pimoroni Explorer", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pga2040": { + "name": "Pimoroni PGA2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_pga2350": { + "name": "Pimoroni PGA2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2": { + "name": "Pimoroni PicoPlus2", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_pico_plus_2w": { + "name": "Pimoroni PicoPlus2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "pimoroni_plasma2040": { + "name": "Pimoroni Plasma2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_plasma2350": { + "name": "Pimoroni Plasma2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_plasma2350w": { + "name": "Pimoroni Plasma2350W", + "mcu": "rp2350", + "max_pin": 47, + }, + "pimoroni_servo2040": { + "name": "Pimoroni Servo2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2040": { + "name": "Pimoroni Tiny2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "pimoroni_tiny2350": { + "name": "Pimoroni Tiny2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "pintronix_pinmax": { + "name": "Pintronix PinMax", + "mcu": "rp2040", + "max_pin": 29, + }, + "rakwireless_rak11300": { + "name": "RAKwireless RAK11300", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_eins": { + "name": "redscorp RP2040-Eins", + "mcu": "rp2040", + "max_pin": 29, + }, + "redscorp_rp2040_promini": { + "name": "redscorp RP2040-ProMini", + "mcu": "rp2040", + "max_pin": 29, + }, "rpipico": { "name": "Raspberry Pi Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "rpipico2": { + "name": "Raspberry Pi Pico 2", + "mcu": "rp2350", + "max_pin": 47, + }, + "rpipico2w": { + "name": "Raspberry Pi Pico 2W", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "sea_picro": { + "name": "Generic Sea-Picro", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_indicator_rp2040": { + "name": "Seeed INDICATOR RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2040": { + "name": "Seeed XIAO RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "seeed_xiao_rp2350": { + "name": "Seeed XIAO RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "silicognition_rp2040_shim": { + "name": "Silicognition RP2040-Shim", + "mcu": "rp2040", + "max_pin": 29, + }, + "soldered_nula_rp2350": { + "name": "Soldered Electronics NULA RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2040_stamp": { + "name": "Solder Party RP2040 Stamp", + "mcu": "rp2040", + "max_pin": 29, + }, + "solderparty_rp2350_stamp": { + "name": "Solder Party RP2350 Stamp", + "mcu": "rp2350", + "max_pin": 47, + }, + "solderparty_rp2350_stamp_xl": { + "name": "Solder Party RP2350 Stamp XL", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotnode_lorawanrp2350": { + "name": "SparkFun IoT Node LoRaWAN", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_iotredboard_rp2350": { + "name": "SparkFun IoT RedBoard RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_micromodrp2040": { + "name": "SparkFun MicroMod RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2040": { + "name": "SparkFun ProMicro RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_promicrorp2350": { + "name": "SparkFun ProMicro RP2350", + "mcu": "rp2350", + "max_pin": 47, + }, + "sparkfun_thingplusrp2040": { + "name": "SparkFun Thing Plus RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "sparkfun_thingplusrp2350": { + "name": "SparkFun Thing Plus RP2350", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller": { + "name": "SparkFun XRP Controller", + "mcu": "rp2350", + "max_pin": 47, + "max_virtual_pin": 64, + }, + "sparkfun_xrp_controller_beta": { + "name": "SparkFun XRP Controller (Beta)", + "mcu": "rp2040", + "max_pin": 29, + "max_virtual_pin": 64, + }, + "upesy_rp2040_devkit": { + "name": "uPesy RP2040 DevKit", + "mcu": "rp2040", + "max_pin": 29, + }, + "vccgnd_yd_rp2040": { + "name": "VCC-GND YD RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "vicharak_shrike-lite": { + "name": "Vicharak Shrike-Lite", + "mcu": "rp2040", + "max_pin": 29, + }, + "viyalab_mizu": { + "name": "Viyalab Mizu RP2040", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_0_96": { + "name": "Waveshare RP2040 LCD 0.96", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lcd_1_28": { + "name": "Waveshare RP2040 LCD 1.28", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_lora": { + "name": "Waveshare RP2040 LoRa", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_matrix": { + "name": "Waveshare RP2040 Matrix", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_one": { + "name": "Waveshare RP2040 One", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_pizero": { + "name": "Waveshare RP2040 PiZero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_plus": { + "name": "Waveshare RP2040 Plus", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2040_zero": { + "name": "Waveshare RP2040 Zero", + "mcu": "rp2040", + "max_pin": 29, + }, + "waveshare_rp2350_lcd_0_96": { + "name": "Waveshare RP2350 LCD 0.96", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_pizero": { + "name": "Waveshare RP2350 PiZero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_plus": { + "name": "Waveshare RP2350 Plus", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350_zero": { + "name": "Waveshare RP2350 Zero", + "mcu": "rp2350", + "max_pin": 47, + }, + "waveshare_rp2350b_plus_w": { + "name": "Waveshare RP2350B Plus W", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5100s_evb_pico": { + "name": "WIZnet W5100S-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5100s_evb_pico2": { + "name": "WIZnet W5100S-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_5500_evb_pico": { + "name": "WIZnet W5500-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_5500_evb_pico2": { + "name": "WIZnet W5500-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_55rp20_evb_pico": { + "name": "WIZnet W55RP20-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico": { + "name": "WIZnet W6300-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, + }, + "wiznet_6300_evb_pico2": { + "name": "WIZnet W6300-EVB-Pico2", + "mcu": "rp2350", + "max_pin": 47, + }, + "wiznet_wizfi360_evb_pico": { + "name": "WIZnet WizFi360-EVB-Pico", + "mcu": "rp2040", + "max_pin": 29, }, } diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py new file mode 100644 index 00000000000..a0e3699f37b --- /dev/null +++ b/esphome/components/rp2040/generate_boards.py @@ -0,0 +1,186 @@ +"""Generate boards.py from arduino-pico board definitions. + +Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path> +""" + +import json +from pathlib import Path +import re +import sys + +from jinja2 import Environment, FileSystemLoader + +# Map arduino-pico pin defines to ESPHome-friendly names +PIN_NAME_MAP = { + "LED": "LED", + "WIRE0_SDA": "SDA", + "WIRE0_SCL": "SCL", + "WIRE1_SDA": "SDA1", + "WIRE1_SCL": "SCL1", + "SPI0_MISO": "MISO", + "SPI0_MOSI": "MOSI", + "SPI0_SCK": "SCK", + "SPI0_SS": "SS", + "SERIAL1_TX": "TX", + "SERIAL1_RX": "RX", +} + +# arduino-pico maps pins >= 64 to CYW43 wireless chip GPIOs (pin - 64) +CYW43_GPIO_OFFSET = 64 +# CYW43 has 3 GPIOs: 0=LED, 1=VBUS_SENSE, 2=REG_ON +CYW43_GPIO_COUNT = 3 + +# Max GPIO pin per MCU (hardware specs from datasheets) +MCU_MAX_PIN = { + "rp2040": 29, # GPIO 0-29 + "rp2350": 47, # GPIO 0-47 (RP2350A) +} +DEFAULT_MAX_PIN = 29 + +PIN_DEFINE_RE = re.compile(r"#define\s+PIN_(\w+)\s+\((\d+)u\)") + + +def parse_variant_pins(variant_dir: Path) -> dict[str, int]: + """Parse pins_arduino.h and return mapped pin names.""" + header = variant_dir / "pins_arduino.h" + if not header.exists(): + return {} + + pins = {} + for match in PIN_DEFINE_RE.finditer(header.read_text(encoding="utf-8")): + raw_name = match.group(1) + value = int(match.group(2)) + if raw_name in PIN_NAME_MAP: + pins[PIN_NAME_MAP[raw_name]] = value + return pins + + +def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: + """Load all board definitions and return (board_pins, boards) dicts.""" + json_dir = arduino_pico_path / "tools" / "json" + variants_dir = arduino_pico_path / "variants" + + board_pins = {} + boards = {} + variant_pins_cache: dict[str, dict[str, int]] = {} + + for json_file in sorted(json_dir.glob("*.json")): + board_name = json_file.stem + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + + build = data.get("build", {}) + mcu = build.get("mcu", "rp2040") + variant = build.get("variant", board_name) + name = data.get("name", board_name) + vendor = data.get("vendor", "") + + display_name = f"{vendor} {name}".strip() if vendor else name + + boards[board_name] = { + "name": display_name, + "mcu": mcu, + "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), + } + + # Get pins for this variant + if variant not in variant_pins_cache: + variant_dir = variants_dir / variant + variant_pins_cache[variant] = parse_variant_pins(variant_dir) + + pins = variant_pins_cache[variant] + if pins: + max_pin = boards[board_name]["max_pin"] + cyw43_max = CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1 + # Filter out placeholder values (e.g. 99 = "not connected") + filtered = { + name: value + for name, value in pins.items() + if value <= max_pin or CYW43_GPIO_OFFSET <= value <= cyw43_max + } + if filtered: + board_pins[board_name] = filtered + + # Compute max_virtual_pin per board from pin maps + for board_name, pins in board_pins.items(): + if isinstance(pins, str): + continue + virtual_pins = [v for v in pins.values() if v >= CYW43_GPIO_OFFSET] + if virtual_pins and board_name in boards: + boards[board_name]["max_virtual_pin"] = max(virtual_pins) + + # Deduplicate: if board pins match its variant's pins, use string alias + for board_name in list(board_pins.keys()): + if board_name not in boards: + continue + build_variant = _get_variant(json_dir / f"{board_name}.json") + if ( + build_variant + and build_variant != board_name + and build_variant in board_pins + and board_pins[board_name] == board_pins[build_variant] + ): + board_pins[board_name] = build_variant + + return board_pins, boards + + +def _get_variant(json_file: Path) -> str | None: + """Get variant name from a board JSON file.""" + if not json_file.exists(): + return None + with open(json_file, encoding="utf-8") as f: + data = json.load(f) + return data.get("build", {}).get("variant") + + +_TEMPLATE_DIR = Path(__file__).parent + + +def _format_pins(pins: dict[str, int] | str) -> str: + """Jinja2 filter to format a pin dict or alias as Python source.""" + if isinstance(pins, str): + return repr(pins) + items = ", ".join(f"{k!r}: {v}" for k, v in sorted(pins.items())) + return f"{{{items}}}" + + +_jinja_env = Environment( + loader=FileSystemLoader(_TEMPLATE_DIR), keep_trailing_newline=True +) +_jinja_env.filters["format_pins"] = _format_pins +_jinja_env.filters["repr"] = repr + + +def generate(arduino_pico_path: Path) -> str: + """Generate boards.py content.""" + board_pins, boards = load_boards(arduino_pico_path) + + template = _jinja_env.get_template("boards.jinja2") + return template.render( + cyw43_gpio_offset=CYW43_GPIO_OFFSET, + cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1, + default_max_pin=DEFAULT_MAX_PIN, + board_pins=sorted(board_pins.items()), + boards=sorted(boards.items()), + ) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} <arduino-pico-path>", file=sys.stderr) + sys.exit(1) + + arduino_pico_path = Path(sys.argv[1]) + if not (arduino_pico_path / "tools" / "json").exists(): + print(f"Error: {arduino_pico_path}/tools/json not found", file=sys.stderr) + sys.exit(1) + + output = generate(arduino_pico_path) + output_file = Path(__file__).parent / "boards.py" + output_file.write_text(output, encoding="utf-8") + print(f"Generated {output_file}") + + +if __name__ == "__main__": + main() diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 193e567d173..18fb09f76a4 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -54,19 +54,29 @@ def _translate_pin(value): return _lookup_pin(value) +def _board_max_virtual_pin(board): + """Get the max CYW43 virtual pin for this board, or None if no virtual pins.""" + return boards.BOARDS.get(board, {}).get("max_virtual_pin") + + def validate_gpio_pin(value): value = _translate_pin(value) board = CORE.data[KEY_RP2040][KEY_BOARD] - if board == "rpipicow" and value == 32: - return value # Special case for Pico-w LED pin - if value < 0 or value > 29: - raise cv.Invalid(f"RP2040: Invalid pin number: {value}") + max_virtual = _board_max_virtual_pin(board) + if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual: + return value + max_pin = boards.BOARDS.get(board, {}).get("max_pin", boards.DEFAULT_MAX_PIN) + if value < 0 or value > max_pin: + raise cv.Invalid(f"Invalid pin number: {value} (max {max_pin} for this board)") return value def validate_supports(value): board = CORE.data[KEY_RP2040][KEY_BOARD] - if board != "rpipicow" or value[CONF_NUMBER] != 32: + if ( + _board_max_virtual_pin(board) is None + or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET + ): return value mode = value[CONF_MODE] is_input = mode[CONF_INPUT] @@ -75,7 +85,7 @@ def validate_supports(value): is_pullup = mode[CONF_PULLUP] is_pulldown = mode[CONF_PULLDOWN] if not is_output or is_input or is_open_drain or is_pullup or is_pulldown: - raise cv.Invalid("Only output mode is supported for Pico-w LED pin") + raise cv.Invalid("Only output mode is supported for CYW43 virtual pins") return value diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py new file mode 100644 index 00000000000..2e40ed08ba1 --- /dev/null +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -0,0 +1,273 @@ +"""Tests for rp2040 generate_boards.py.""" + +from __future__ import annotations + +import json +from pathlib import Path +import textwrap + +import pytest + +from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins + +PICO_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_SERIAL1_TX (0u) + #define PIN_SERIAL1_RX (1u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (26u) + #define PIN_WIRE1_SCL (27u) + #define PIN_SPI0_MISO (16u) + #define PIN_SPI0_MOSI (19u) + #define PIN_SPI0_SCK (18u) + #define PIN_SPI0_SS (17u) + #include "../generic/common.h" +""") + +PICOW_PINS_HEADER = textwrap.dedent("""\ + #pragma once + #include <cyw43_wrappers.h> + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #include "../generic/common.h" +""") + + +@pytest.fixture() +def arduino_pico(tmp_path: Path) -> Path: + """Create a minimal arduino-pico directory structure.""" + json_dir = tmp_path / "tools" / "json" + json_dir.mkdir(parents=True) + variants_dir = tmp_path / "variants" + variants_dir.mkdir() + + generic_dir = variants_dir / "generic" + generic_dir.mkdir() + (generic_dir / "common.h").write_text("#pragma once\n") + + return tmp_path + + +def _add_board( + arduino_pico: Path, + board_name: str, + mcu: str = "rp2040", + variant: str | None = None, + vendor: str = "", + name: str | None = None, + pins_header: str | None = None, +) -> None: + """Add a board JSON and variant to the fake arduino-pico tree.""" + if variant is None: + variant = board_name + if name is None: + name = board_name + + json_dir = arduino_pico / "tools" / "json" + variants_dir = arduino_pico / "variants" + + board_json = { + "build": { + "mcu": mcu, + "variant": variant, + }, + "name": name, + "vendor": vendor, + } + (json_dir / f"{board_name}.json").write_text(json.dumps(board_json)) + + variant_dir = variants_dir / variant + variant_dir.mkdir(exist_ok=True) + if pins_header is not None: + (variant_dir / "pins_arduino.h").write_text(pins_header) + + +def test_parse_basic_pins(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipico" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICO_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 25 + assert pins["SDA"] == 4 + assert pins["SCL"] == 5 + assert pins["SDA1"] == 26 + assert pins["SCL1"] == 27 + assert pins["MISO"] == 16 + assert pins["MOSI"] == 19 + assert pins["SCK"] == 18 + assert pins["SS"] == 17 + assert pins["TX"] == 0 + assert pins["RX"] == 1 + + +def test_parse_cyw43_led_pin(tmp_path: Path) -> None: + variant_dir = tmp_path / "rpipicow" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text(PICOW_PINS_HEADER) + + pins = parse_variant_pins(variant_dir) + assert pins["LED"] == 64 + + +def test_parse_missing_header(tmp_path: Path) -> None: + variant_dir = tmp_path / "noheader" + variant_dir.mkdir() + assert parse_variant_pins(variant_dir) == {} + + +def test_parse_unmapped_defines_ignored(tmp_path: Path) -> None: + variant_dir = tmp_path / "custom" + variant_dir.mkdir() + (variant_dir / "pins_arduino.h").write_text( + "#define PIN_NEOPIXEL (16u)\n#define PIN_LED (25u)\n" + ) + + pins = parse_variant_pins(variant_dir) + assert "NEOPIXEL" not in pins + assert pins["LED"] == 25 + + +def test_load_basic_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + board_pins, boards = load_boards(arduino_pico) + + assert "rpipico" in boards + assert boards["rpipico"]["name"] == "Raspberry Pi Pico" + assert boards["rpipico"]["mcu"] == "rp2040" + assert boards["rpipico"]["max_pin"] == 29 + + assert "rpipico" in board_pins + assert board_pins["rpipico"]["LED"] == 25 + assert board_pins["rpipico"]["SDA"] == 4 + + +def test_load_rp2350_board(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico2", + mcu="rp2350", + vendor="Raspberry Pi", + name="Pico 2", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipico2"]["mcu"] == "rp2350" + assert boards["rpipico2"]["max_pin"] == 47 + + +def test_cyw43_board_has_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["max_virtual_pin"] == 64 + + +def test_non_cyw43_board_has_no_max_virtual_pin(arduino_pico: Path) -> None: + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + ) + + _, boards = load_boards(arduino_pico) + + assert "max_virtual_pin" not in boards["rpipico"] + + +def test_board_without_variant_header(arduino_pico: Path) -> None: + _add_board(arduino_pico, "novariant", name="No Variant") + + board_pins, boards = load_boards(arduino_pico) + + assert "novariant" in boards + assert "novariant" not in board_pins + + +def test_shared_variant_deduplicates(arduino_pico: Path) -> None: + """Two boards sharing the same variant should alias.""" + _add_board(arduino_pico, "base_board", pins_header=PICO_PINS_HEADER) + _add_board(arduino_pico, "alias_board", variant="base_board") + + board_pins, _ = load_boards(arduino_pico) + + assert board_pins["base_board"] == parse_variant_pins( + arduino_pico / "variants" / "base_board" + ) + assert board_pins["alias_board"] == "base_board" + + +def test_display_name_with_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="Acme", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Acme Widget" + + +def test_display_name_without_vendor(arduino_pico: Path) -> None: + _add_board(arduino_pico, "testboard", vendor="", name="Widget") + _, boards = load_boards(arduino_pico) + assert boards["testboard"]["name"] == "Widget" + + +def test_unknown_mcu_gets_default_max_pin(arduino_pico: Path) -> None: + _add_board(arduino_pico, "future", mcu="rp2450", pins_header=PICO_PINS_HEADER) + _, boards = load_boards(arduino_pico) + assert boards["future"]["max_pin"] == 29 + + +def test_placeholder_pins_filtered_out(arduino_pico: Path) -> None: + """Pins with placeholder values like 99 should be filtered out.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (25u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_WIRE1_SDA (99u) + #define PIN_WIRE1_SCL (99u) + """) + _add_board(arduino_pico, "placeholder", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "SDA1" not in board_pins["placeholder"] + assert "SCL1" not in board_pins["placeholder"] + assert board_pins["placeholder"]["LED"] == 25 + assert "max_virtual_pin" not in boards["placeholder"] + + +def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: + """Pin 99 should not cause max_virtual_pin to be set.""" + header = textwrap.dedent("""\ + #pragma once + #define PIN_LED (64u) + #define PIN_WIRE0_SDA (4u) + #define PIN_WIRE0_SCL (5u) + #define PIN_SPI0_MISO (99u) + """) + _add_board(arduino_pico, "badpin", pins_header=header) + + board_pins, boards = load_boards(arduino_pico) + + assert "MISO" not in board_pins["badpin"] + assert boards["badpin"]["max_virtual_pin"] == 64 From 82629c397f699af8b263e66e7cc904bebcc297d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Fri, 6 Mar 2026 07:01:50 -1000 Subject: [PATCH 16/16] [hlk_fm22x] Fix oversized response rejection breaking GET_ALL_FACE_IDS (#14506) --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 18d26f057a8..7c7c8782dee 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -133,24 +133,22 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { - ESP_LOGE(TAG, "Response too large: %u bytes", length); - // Discard exactly the remaining payload and checksum for this frame - for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i) - this->read(); - return; - } - + // Read up to buffer size; discard excess bytes while still computing checksum + // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) + // but handlers only need the first few bytes + size_t to_store = std::min(static_cast<size_t>(length), HLK_FM22X_MAX_RESPONSE_SIZE); for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - this->recv_buf_[idx] = byte; + if (idx < to_store) { + this->recv_buf_[idx] = byte; + } } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, - format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), to_store)); #endif byte = this->read(); @@ -160,10 +158,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(this->recv_buf_.data(), length); + this->handle_note_(this->recv_buf_.data(), to_store); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(this->recv_buf_.data(), length); + this->handle_reply_(this->recv_buf_.data(), to_store); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);