Merge remote-tracking branch 'origin/progmem-string-table-uint16' into bme680_bsec-tables-progmem

This commit is contained in:
J. Nick Koston
2026-10-13 11:16:26 -10:00
2 changed files with 93 additions and 7 deletions
+28 -7
View File
@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
@@ -68,6 +69,7 @@ template<size_t N> struct FixedString {
};
/// Compile-time string table that packs strings into a single blob with offset lookup.
/// Blobs over 255 bytes have no offsets and are scanned, so keep those for rarely read strings.
/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266.
///
/// Example:
@@ -78,6 +80,11 @@ 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()...});
/// One-byte offsets address up to 255 bytes; a larger blob has no offset table and is scanned instead,
/// which costs less flash than wider offsets for the rarely read tables that get this big.
static constexpr bool INDEXED = BLOB_SIZE <= UINT8_MAX;
/// Generate packed string blob at compile time
static constexpr auto make_blob() {
@@ -91,18 +98,28 @@ 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{};
size_t pos = 0, idx = 0;
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
return result;
if constexpr (INDEXED) {
std::array<uint8_t, COUNT> result{};
size_t pos = 0, idx = 0;
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
return result;
} else {
return std::array<uint8_t, 0>{};
}
}
};
/// Skip idx strings in a packed NUL-separated blob that may live in flash.
inline const char *progmem_skip_strings(const char *blob, uint8_t idx) {
while (idx-- > 0)
blob += ESPHOME_strlen_P(blob) + 1;
return blob;
}
// Forward declaration for LogString (defined in log.h)
struct LogString;
@@ -115,6 +132,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 +140,10 @@ 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])]); \
if constexpr (Table::INDEXED) \
return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \
else \
return std::launder(::esphome::progmem_skip_strings(BLOB.data(), idx)); \
} \
static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \
return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \
@@ -0,0 +1,65 @@
#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, so it is scanned.
PROGMEM_STRING_TABLE(LargeStrings, "0123456789012345678901234567890123456789",
"1123456789012345678901234567890123456789", "2123456789012345678901234567890123456789",
"3123456789012345678901234567890123456789", "4123456789012345678901234567890123456789",
"5123456789012345678901234567890123456789", "6123456789012345678901234567890123456789",
"7123456789012345678901234567890123456789");
// Five 50-character strings pack into exactly 255 bytes; one more byte switches to scanning.
PROGMEM_STRING_TABLE(Exactly255, "01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789");
PROGMEM_STRING_TABLE(Exactly256, "01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789",
"01234567890123456789012345678901234567890123456789x");
TEST(ProgmemStringTable, ScansInsteadOfIndexingAfter255Bytes) {
static_assert(Exactly255::BLOB_SIZE == 255);
static_assert(Exactly255::Table::INDEXED);
static_assert(Exactly256::BLOB_SIZE == 256);
static_assert(!Exactly256::Table::INDEXED);
static_assert(Exactly256::OFFSETS.empty());
EXPECT_EQ(Exactly255::OFFSETS[4], 204);
EXPECT_STREQ(reinterpret_cast<const char *>(Exactly256::get_progmem_str(4, 0)),
"01234567890123456789012345678901234567890123456789x");
}
TEST(ProgmemStringTable, SmallTableKeepsOneByteOffsets) {
static_assert(SmallStrings::BLOB_SIZE <= 255);
static_assert(SmallStrings::Table::INDEXED);
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, LargeTableIsScanned) {
static_assert(LargeStrings::BLOB_SIZE > 255);
static_assert(!LargeStrings::Table::INDEXED);
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_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(3, LargeStrings::LAST_INDEX)),
"3123456789012345678901234567890123456789");
}
} // namespace esphome::core::testing