[core] Keep log tags in flash on ESP8266 with ESPHOME_LOG_TAG (#20290)

This commit is contained in:
J. Nick Koston
2026-10-07 09:51:17 -10:00
committed by GitHub
parent 6f940a486b
commit 1ba323e9a3
12 changed files with 84 additions and 13 deletions
+14 -1
View File
@@ -3,11 +3,14 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::logger {
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// Longest tag the header budget allows
static constexpr size_t MAX_TAG_LENGTH = 32;
// ANSI color code last digit (30-38 range, store only last digit to save RAM on ESP8266)
static const char LOG_LEVEL_COLOR_DIGIT[] PROGMEM = {
@@ -189,13 +192,23 @@ struct LogBuffer {
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
// Caller is responsible for ensuring buffer has sufficient space. Tags may be in PROGMEM on ESP8266.
void copy_string_(char *&p, const char *str) {
#ifdef USE_ESP8266
// Only ESP8266 caps the length; tags may be in PROGMEM, so read each byte once
for (size_t i = 0; i < MAX_TAG_LENGTH; i++) {
const char c = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(str + i)));
if (c == '\0')
break;
*p++ = c;
}
#else
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
#endif
}
};
+4 -1
View File
@@ -144,7 +144,10 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag);
// Skip the lookup when no per-tag levels are set
if (this->log_levels_.empty())
return this->current_level_;
auto it = this->log_levels_.find(FlashTag{tag});
if (it != this->log_levels_.end())
return it->second;
#endif
+17 -3
View File
@@ -12,6 +12,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "log_buffer.h"
#include "task_log_buffer_host.h"
@@ -85,12 +86,16 @@ class LoggerLevelListener {
};
#endif
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
// Comparison function for const char* keys in log_levels_ map
// RAM map keys compared with a tag that may be in PROGMEM, without copying it
struct FlashTag {
const char *tag;
};
struct CStrCompare {
using is_transparent = void;
bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; }
bool operator()(const char *key, FlashTag t) const { return ESPHOME_strcmp_P(key, t.tag) < 0; }
bool operator()(FlashTag t, const char *key) const { return ESPHOME_strcmp_P(key, t.tag) > 0; }
};
#endif
// Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16
@@ -488,6 +493,12 @@ class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const c
[](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
auto *trigger = static_cast<LoggerMessageTrigger *>(self);
if (level <= trigger->level_) {
#ifdef USE_ESP8266
// User lambdas may strcmp the tag, which may be in PROGMEM. The copy lives in
// the trigger so an automation that suspends (delay) still sees a valid tag.
ESPHOME_strncpy_P(trigger->ram_tag_, tag, MAX_TAG_LENGTH);
tag = trigger->ram_tag_;
#endif
trigger->trigger(level, tag, message);
}
});
@@ -495,6 +506,9 @@ class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const c
protected:
uint8_t level_;
#ifdef USE_ESP8266
char ram_tag_[MAX_TAG_LENGTH + 1]{};
#endif
};
} // namespace esphome::logger
@@ -5,6 +5,7 @@
namespace esphome::touchscreen {
static const char *const TAG = "touchscreen";
static constexpr uint32_t TOUCH_TIMEOUT_ID = 0;
void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; }
@@ -63,9 +64,9 @@ void Touchscreen::loop() {
// Simulate a touch after <this->touch_timeout_> ms. This will reset any existing timeout operation.
// This is to detect touch release.
if (this->is_touched_) {
this->set_timeout(TAG, this->touch_timeout_, [this]() { this->store_.touched = true; });
this->set_timeout(TOUCH_TIMEOUT_ID, this->touch_timeout_, [this]() { this->store_.touched = true; });
} else {
this->cancel_timeout(TAG);
this->cancel_timeout(TOUCH_TIMEOUT_ID);
}
}
}
+1 -1
View File
@@ -36,7 +36,7 @@
namespace esphome {
static const char *const TAG = "app";
ESPHOME_LOG_TAG(TAG, "app");
// Delay after setup() finishes before trimming the scheduler freelist of its post-boot peak.
// 10 s is well past the bulk of post-setup async work (Wi-Fi/MQTT connects, first-read latency).
+1 -1
View File
@@ -12,7 +12,7 @@
namespace esphome {
static const char *const TAG = "component";
ESPHOME_LOG_TAG(TAG, "component");
// Global vectors for component data that doesn't belong in every instance.
// Using vector instead of unordered_map for both because:
+2 -1
View File
@@ -1,12 +1,13 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome {
static const char *const TAG = "entity_base";
ESPHOME_LOG_TAG(TAG, "entity_base");
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
this->name_ = StringRef(name);
+1 -1
View File
@@ -28,7 +28,7 @@ std::string progmem_string(ProgmemStr str) {
}
#endif
static const char *const TAG = "helpers";
ESPHOME_LOG_TAG(TAG, "helpers");
__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity,
size_t elem_size) {
+8
View File
@@ -190,4 +190,12 @@ struct LogString;
#endif
// Declares a static log tag at namespace scope in a .cpp file; on ESP8266 it lives in flash, so read it with
// the _P functions, never strcmp/strlen.
#ifdef USE_ESP8266
#define ESPHOME_LOG_TAG(name, tag) static const char name[] PROGMEM = tag
#else
#define ESPHOME_LOG_TAG(name, tag) static constexpr const char *const name = tag
#endif
} // namespace esphome
+1 -1
View File
@@ -12,7 +12,7 @@
namespace esphome {
static const char *const TAG = "scheduler";
ESPHOME_LOG_TAG(TAG, "scheduler");
// Maximum number of logically deleted (cancelled) items before forcing cleanup.
// Empirically chosen to balance cleanup overhead against tombstone accumulation in items_.
+1 -1
View File
@@ -17,7 +17,7 @@
namespace esphome {
#ifdef ESPHOME_DEBUG_SCHEDULER
static const char *const TAG = "time_64";
ESPHOME_LOG_TAG(TAG, "time_64");
#endif
#ifdef ESPHOME_THREAD_SINGLE
+31
View File
@@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include <map>
#include <string>
#include "esphome/components/logger/log_buffer.h"
#include "esphome/components/logger/logger.h"
#include "esphome/core/log.h"
namespace esphome::logger::testing {
ESPHOME_LOG_TAG(TEST_TAG, "test.tag");
TEST(LogTagTest, HeaderContainsTagAndLine) {
char data[256];
LogBuffer buf{data, sizeof(data)};
buf.write_header(5, TEST_TAG, 42, nullptr);
const std::string header(data, buf.pos);
EXPECT_NE(header.find("[D][test.tag:042]"), std::string::npos) << header;
}
TEST(LogTagTest, PerTagLevelLookupMatchesByContent) {
std::map<const char *, uint8_t, CStrCompare> levels{{"api", 1}, {"sensor", 2}, {"wifi", 3}};
// Copies, so a match must compare content rather than pointers
char api[] = "api", sensor[] = "sensor", wifi[] = "wifi", missing[] = "switch";
EXPECT_EQ(levels.find(FlashTag{api})->second, 1);
EXPECT_EQ(levels.find(FlashTag{sensor})->second, 2);
EXPECT_EQ(levels.find(FlashTag{wifi})->second, 3);
EXPECT_EQ(levels.find(FlashTag{missing}), levels.end());
}
} // namespace esphome::logger::testing