[core] Let PROGMEM_STRING_TABLE hold more than 255 bytes

This commit is contained in:
J. Nick Koston
2026-10-13 10:25:13 -10:00
parent 918b7d2553
commit c470bbb698
2 changed files with 62 additions and 5 deletions
+21 -5
View File
@@ -1,10 +1,12 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <new>
#include <string>
#include <type_traits>
#include "esphome/core/hal.h" // For PROGMEM definition
@@ -78,6 +80,10 @@ template<size_t N> struct FixedString {
template<FixedString... Strs> struct ProgmemStringTable {
static constexpr size_t COUNT = sizeof...(Strs);
static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1));
/// Length of the longest string, for sizing a RAM copy on ESP8266
static constexpr size_t MAX_LENGTH = std::max({Strs.size()...});
/// Offsets are one byte while the blob fits in 255 bytes and two bytes beyond that.
using offset_t = std::conditional_t<(BLOB_SIZE <= UINT8_MAX), uint8_t, uint16_t>;
/// Generate packed string blob at compile time
static constexpr auto make_blob() {
@@ -91,16 +97,25 @@ template<FixedString... Strs> struct ProgmemStringTable {
return result;
}
/// Generate offset table at compile time (uint8_t limits blob to 255 bytes)
/// Generate offset table at compile time
static constexpr auto make_offsets() {
static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string");
static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices");
static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings");
std::array<uint8_t, COUNT> result{};
static_assert(BLOB_SIZE <= UINT16_MAX, "PROGMEM_STRING_TABLE blob exceeds 65535 bytes; use fewer/shorter strings");
std::array<offset_t, COUNT> result{};
size_t pos = 0, idx = 0;
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
((result[idx++] = static_cast<offset_t>(pos), pos += Strs.size() + 1), ...);
return result;
}
/// Read one offset from the table, which may live in flash
static offset_t read_offset(const offset_t *offset) {
if constexpr (sizeof(offset_t) == 1) {
return progmem_read_byte(offset);
} else {
return progmem_read_uint16(offset);
}
}
};
// Forward declaration for LogString (defined in log.h)
@@ -115,6 +130,7 @@ struct LogString;
static constexpr size_t COUNT = Table::COUNT; \
static constexpr uint8_t LAST_INDEX = COUNT - 1; \
static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \
static constexpr size_t MAX_LENGTH = Table::MAX_LENGTH; \
static constexpr auto BLOB ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_blob(); \
static constexpr auto OFFSETS ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_offsets(); \
static const char *get_(uint8_t idx, uint8_t fallback) { \
@@ -122,7 +138,7 @@ struct LogString;
idx = fallback; \
/* std::launder is used here to prevent the inter-procedural analysis that */ \
/* causes the false positive that the string is not null terminated */ \
return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \
return std::launder(&BLOB[Table::read_offset(&OFFSETS[idx])]); \
} \
static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \
return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \
@@ -0,0 +1,41 @@
#include <gtest/gtest.h>
#include <cstring>
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::core::testing {
PROGMEM_STRING_TABLE(SmallStrings, "OFF", "HEAT", "COOL", "UNKNOWN");
// Eight strings of 40 characters: a 328-byte blob, past what one-byte offsets can address.
PROGMEM_STRING_TABLE(LargeStrings, "0123456789012345678901234567890123456789",
"1123456789012345678901234567890123456789", "2123456789012345678901234567890123456789",
"3123456789012345678901234567890123456789", "4123456789012345678901234567890123456789",
"5123456789012345678901234567890123456789", "6123456789012345678901234567890123456789",
"7123456789012345678901234567890123456789");
TEST(ProgmemStringTable, SmallTableKeepsOneByteOffsets) {
static_assert(SmallStrings::BLOB_SIZE <= 255);
static_assert(sizeof(SmallStrings::OFFSETS[0]) == 1);
static_assert(SmallStrings::MAX_LENGTH == 7);
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(0, SmallStrings::LAST_INDEX)), "OFF");
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(2, SmallStrings::LAST_INDEX)), "COOL");
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(200, SmallStrings::LAST_INDEX)), "UNKNOWN");
}
TEST(ProgmemStringTable, LargeTableUsesTwoByteOffsets) {
static_assert(LargeStrings::BLOB_SIZE > 255);
static_assert(sizeof(LargeStrings::OFFSETS[0]) == 2);
static_assert(LargeStrings::MAX_LENGTH == 40);
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(0, LargeStrings::LAST_INDEX)),
"0123456789012345678901234567890123456789");
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(7, LargeStrings::LAST_INDEX)),
"7123456789012345678901234567890123456789");
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(8, LargeStrings::LAST_INDEX)),
"7123456789012345678901234567890123456789");
EXPECT_EQ(LargeStrings::OFFSETS[7], 7 * 41);
}
} // namespace esphome::core::testing