[core] Add PROGMEM_STRING_TABLE macro for flash-optimized string lookups

This commit is contained in:
J. Nick Koston
2026-01-30 21:05:01 -06:00
parent 5e3561d60b
commit e46de0c40a
3 changed files with 85 additions and 50 deletions

View File

@@ -9,32 +9,17 @@ namespace esphome::light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
// Get JSON string for color mode.
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
switch (ColorModeBitPolicy::to_bit(mode)) {
case 1:
return ESPHOME_F("onoff");
case 2:
return ESPHOME_F("brightness");
case 3:
return ESPHOME_F("white");
case 4:
return ESPHOME_F("color_temp");
case 5:
return ESPHOME_F("cwww");
case 6:
return ESPHOME_F("rgb");
case 7:
return ESPHOME_F("rgbw");
case 8:
return ESPHOME_F("rgbct");
case 9:
return ESPHOME_F("rgbww");
default:
return nullptr;
}
// Color mode JSON strings - packed into flash with compile-time generated offsets.
// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff").
PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct",
"rgbww");
// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0).
static const char *get_color_mode_json_str(ColorMode mode) {
unsigned bit = ColorModeBitPolicy::to_bit(mode);
if (bit == 0)
return nullptr;
return ColorModeStrings::get(bit - 1);
}
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {

View File

@@ -4,6 +4,7 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::logger {
@@ -291,34 +292,15 @@ UARTSelection Logger::get_uart() const { return this->uart_; }
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
#ifdef USE_STORE_LOG_STR_IN_FLASH
// ESP8266: PSTR() cannot be used in array initializers, so we need to declare
// each string separately as a global constant first
static const char LOG_LEVEL_NONE[] PROGMEM = "NONE";
static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR";
static const char LOG_LEVEL_WARN[] PROGMEM = "WARN";
static const char LOG_LEVEL_INFO[] PROGMEM = "INFO";
static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG";
static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG";
static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE";
static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE";
static const LogString *const LOG_LEVELS[] = {
reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR),
reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO),
reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG),
reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE),
};
#else
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
#endif
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
void Logger::dump_config() {
ESP_LOGCONFIG(TAG,
"Logger:\n"
" Max Level: %s\n"
" Initial Level: %s",
LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_]));
LogLevelStrings::get(ESPHOME_LOG_LEVEL), LogLevelStrings::get(this->current_level_));
#ifndef USE_HOST
ESP_LOGCONFIG(TAG,
" Log Baud Rate: %" PRIu32 "\n"
@@ -337,7 +319,7 @@ void Logger::dump_config() {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
for (auto &it : this->log_levels_) {
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LogLevelStrings::get(it.second));
}
#endif
}
@@ -345,7 +327,7 @@ void Logger::dump_config() {
void Logger::set_log_level(uint8_t level) {
if (level > ESPHOME_LOG_LEVEL) {
level = ESPHOME_LOG_LEVEL;
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]));
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LogLevelStrings::get(ESPHOME_LOG_LEVEL));
}
this->current_level_ = level;
#ifdef USE_LOGGER_LEVEL_LISTENERS

View File

@@ -1,5 +1,9 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
// Platform-agnostic macros for PROGMEM string handling
// On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings
// On other platforms: Use plain strings (no PROGMEM)
@@ -16,6 +20,7 @@
#define ESPHOME_strcasecmp_P strcasecmp_P
#define ESPHOME_strncmp_P strncmp_P
#define ESPHOME_strncasecmp_P strncasecmp_P
#define progmem_read_byte(addr) pgm_read_byte(addr)
// Type for pointers to PROGMEM strings (for use with ESPHOME_F return values)
using ProgmemStr = const __FlashStringHelper *;
#else
@@ -29,6 +34,69 @@ using ProgmemStr = const __FlashStringHelper *;
#define ESPHOME_strcasecmp_P strcasecmp
#define ESPHOME_strncmp_P strncmp
#define ESPHOME_strncasecmp_P strncasecmp
#define progmem_read_byte(addr) (*(addr))
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
using ProgmemStr = const char *;
#endif
namespace esphome {
/// Helper for C++20 string literal template arguments
template<size_t N> struct FixedString {
char data[N]{};
constexpr FixedString(const char (&str)[N]) {
for (size_t i = 0; i < N; ++i)
data[i] = str[i];
}
constexpr size_t size() const { return N - 1; } // exclude null terminator
};
/// Compile-time string table that packs strings into a single blob with offset lookup.
/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266.
///
/// Example:
/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz");
/// const char *str = MyStrings::get(index); // 0-based index
///
template<FixedString... Strs> struct ProgmemStringTable {
static constexpr size_t COUNT = sizeof...(Strs);
static constexpr size_t BLOB_SIZE = (... + (Strs.size() + 1));
/// Generate packed string blob at compile time
static constexpr auto make_blob() {
std::array<char, BLOB_SIZE> result{};
size_t pos = 0;
auto copy = [&](const auto &str) {
for (size_t i = 0; i <= str.size(); ++i)
result[pos++] = str.data[i];
};
(copy(Strs), ...);
return result;
}
/// Generate offset table at compile time
static constexpr auto make_offsets() {
std::array<uint8_t, COUNT> result{};
size_t pos = 0, idx = 0;
((result[idx++] = pos, pos += Strs.size() + 1), ...);
return result;
}
};
/// Instantiate a ProgmemStringTable with PROGMEM storage.
/// Creates: Name::get(index), Name::COUNT, Name::BLOB_SIZE
#define PROGMEM_STRING_TABLE(Name, ...) \
struct Name { \
using Table = ProgmemStringTable<__VA_ARGS__>; \
static constexpr size_t COUNT = Table::COUNT; \
static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \
static constexpr auto BLOB PROGMEM = Table::make_blob(); \
static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \
static const char *get(uint8_t index) { \
if (index >= COUNT) \
return nullptr; \
return &BLOB[progmem_read_byte(&OFFSETS[index])]; \
} \
}
} // namespace esphome