Compare commits

...

6 Commits

Author SHA1 Message Date
J. Nick Koston
f42f222e82 Merge branch 'dev' into scanf_bloat 2026-02-03 03:14:23 +01:00
J. Nick Koston
ccf5c1f7e9 [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, twai, openthread, ulp) (#13664) 2026-02-03 03:12:12 +01:00
dependabot[bot]
efecea9450 Bump github/codeql-action from 4.32.0 to 4.32.1 (#13726)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 02:27:34 +01:00
J. Nick Koston
26e4cda610 [logger] Use vsnprintf_P directly for ESP8266 flash format strings (#13716) 2026-02-03 02:25:54 +01:00
J. Nick Koston
e68b302bba [ci] Block new scanf() usage to prevent ~9.8KB flash bloat 2026-01-30 20:21:43 -06:00
J. Nick Koston
3e11a9d8a5 [ci] Block new scanf() usage to prevent ~9.8KB flash bloat 2026-01-30 20:20:24 -06:00
9 changed files with 88 additions and 59 deletions

View File

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

View File

@@ -124,10 +124,14 @@ COMPILER_OPTIMIZATIONS = {
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
@@ -138,9 +142,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"openthread", # Thread protocol - only needed by openthread component
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"ulp", # ULP coprocessor - not currently used by any ESPHome component
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation

View File

@@ -15,6 +15,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate):
async def to_code(config):
# Legacy driver component provides driver/twai.h header
include_builtin_idf_component("driver")
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)

View File

@@ -1,7 +1,12 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import output
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
@@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config):
include_builtin_idf_component("esp_driver_dac")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)

View File

@@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens")
# Legacy driver component provides driver/touch_sensor.h header
include_builtin_idf_component("driver")
touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config)

View File

@@ -128,22 +128,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
//
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// The buffer is used in a special way to avoid allocating extra memory:
//
// Memory layout during execution:
// Step 1: Copy format string from flash to buffer
// tx_buffer_: [format_string][null][.....................]
// tx_buffer_at_: ------------------^
// msg_start: saved here -----------^
//
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
// and writes formatted output starting at msg_start position
// tx_buffer_: [format_string][null][formatted_message][null]
// tx_buffer_at_: -------------------------------------^
//
// Step 3: Output the formatted message (starting at msg_start)
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
// which points to: [formatted_message][null]
// Uses vsnprintf_P to read the format string directly from flash without copying to RAM.
//
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
@@ -153,35 +138,25 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
RecursionGuard guard(global_recursion_guard_);
this->tx_buffer_at_ = 0;
// Copy format string from progmem
auto *format_pgm_p = reinterpret_cast<const uint8_t *>(format);
char ch = '.';
while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') {
this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++);
}
// Write header, format body directly from flash, and write footer
this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_,
this->tx_buffer_size_);
this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_,
reinterpret_cast<PGM_P>(format), args);
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
// Buffer full from copying format - RAII guard handles cleanup on return
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
return;
}
// Save the offset before calling format_log_to_buffer_with_terminator_
// since it will increment tx_buffer_at_ to the end of the formatted string
uint16_t msg_start = this->tx_buffer_at_;
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
&this->tx_buffer_at_, this->tx_buffer_size_);
uint16_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
// Ensure null termination
uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_;
this->tx_buffer_[null_pos] = '\0';
// Listeners get message first (before console write)
#ifdef USE_LOG_LISTENERS
for (auto *listener : this->log_listeners_)
listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length);
listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_);
#endif
// Write to console starting at the msg_start
this->write_tx_buffer_to_console_(msg_start, &msg_length);
// Write to console
this->write_tx_buffer_to_console_();
}
#endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -597,31 +597,40 @@ class Logger : public Component {
*buffer_at = pos;
}
// Helper to process vsnprintf return value and strip trailing newlines.
// Updates buffer_at with the formatted length, handling truncation:
// - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
__attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at,
uint16_t remaining, int ret) {
if (ret < 0)
return; // Encoding error, do not increment buffer_at
*buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast<uint16_t>(ret);
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n')
(*buffer_at)--;
}
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,
va_list args) {
// Get remaining capacity in the buffer
// Check remaining capacity in the buffer
if (*buffer_at >= buffer_size)
return;
const uint16_t remaining = buffer_size - *buffer_at;
const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args);
if (ret < 0) {
return; // Encoding error, do not increment buffer_at
}
// Update buffer_at with the formatted length (handle truncation)
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
*buffer_at += formatted_len;
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') {
(*buffer_at)--;
}
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
// ESP8266 variant that reads format string directly from flash using vsnprintf_P
inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format,
va_list args) {
if (*buffer_at >= buffer_size)
return;
const uint16_t remaining = buffer_size - *buffer_at;
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args));
}
#endif
inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);

View File

@@ -4,6 +4,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C6,
VARIANT_ESP32H2,
add_idf_sdkconfig_option,
include_builtin_idf_component,
only_on_variant,
require_vfs_select,
)
@@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
# Re-enable openthread IDF component (excluded by default)
include_builtin_idf_component("openthread")
cg.add_define("USE_OPENTHREAD")
# OpenThread SRP needs access to mDNS services after setup

View File

@@ -756,6 +756,28 @@ def lint_no_sprintf(fname, match):
)
@lint_re_check(
# Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf
# Also match std:: prefixed versions
# [^\w] ensures we match function calls, not substrings
r"[^\w]((?:std::)?v?[fs]?scanf)\s*\(" + CPP_RE_EOL,
include=cpp_include,
)
def lint_no_scanf(fname, match):
func = match.group(1)
return (
f"{highlight(func + '()')} is not allowed in new ESPHome code. The scanf family "
f"pulls in ~7KB flash on ESP8266 and ~9KB on ESP32, and ESPHome doesn't otherwise "
f"need this code.\n"
f"Please use alternatives:\n"
f" - {highlight('parse_number<T>(str)')} for parsing integers/floats from strings\n"
f" - {highlight('strtol()/strtof()')} for C-style number parsing with error checking\n"
f" - {highlight('parse_hex()')} for hex string parsing\n"
f" - Manual parsing for simple fixed formats\n"
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
)
@lint_content_find_check(
"ESP_LOG",
include=["*.h", "*.tcc"],