[core] Move comment to PROGMEM on ESP8266

This commit is contained in:
J. Nick Koston
2025-12-18 06:41:23 -10:00
parent 2cf6ed2af7
commit cd93468225
6 changed files with 88 additions and 22 deletions
+3 -1
View File
@@ -287,7 +287,9 @@ std::string WebServer::get_config_json() {
JsonObject root = builder.root();
root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
root[ESPHOME_F("comment")] = App.get_comment_ref();
char comment_buffer[ESPHOME_COMMENT_SIZE];
App.get_comment_string(comment_buffer, sizeof(comment_buffer));
root[ESPHOME_F("comment")] = comment_buffer;
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
#else
+17 -8
View File
@@ -12,6 +12,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/scheduler.h"
#include "esphome/core/string_ref.h"
#include "esphome/core/version.h"
@@ -104,8 +105,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
bool name_add_mac_suffix) {
void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) {
@@ -124,7 +124,6 @@ class Application {
this->name_ = name;
this->friendly_name_ = friendly_name;
}
this->comment_ = comment;
}
#ifdef USE_DEVICES
@@ -257,10 +256,21 @@ class Application {
return "";
}
/// Get the comment of this Application set by pre_setup().
std::string get_comment() const { return this->comment_; }
/// Get the comment as StringRef (avoids allocation)
StringRef get_comment_ref() const { return StringRef(this->comment_); }
/// Copy the comment string into the provided buffer
/// Buffer must be ESPHOME_COMMENT_SIZE bytes
void get_comment_string(char *buffer, size_t size) {
ESPHOME_strncpy_P(buffer, ESPHOME_COMMENT_STR, size);
buffer[size - 1] = '\0';
}
/// Get the comment of this Application (deprecated, use get_comment_string() instead)
// Remove before 2026.7.0
ESPDEPRECATED("Use get_comment_string() instead. Removed in 2026.7.0", "2026.1.0")
std::string get_comment() {
char buffer[ESPHOME_COMMENT_SIZE];
this->get_comment_string(buffer, sizeof(buffer));
return std::string(buffer);
}
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
@@ -501,7 +511,6 @@ class Application {
// Pointer-sized members first
Component *current_component_{nullptr};
const char *comment_{nullptr};
// std::vector (3 pointers each: begin, end, capacity)
// Partitioned vector design for looping components
+2
View File
@@ -7,4 +7,6 @@
#define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT
#define ESPHOME_BUILD_TIME 1700000000 // NOLINT
#define ESPHOME_COMMENT_SIZE 1 // NOLINT
static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000";
static const char ESPHOME_COMMENT_STR[] = "";
+1 -2
View File
@@ -209,7 +209,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_NAME): cv.valid_name,
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)),
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.string,
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
cv.Required(CONF_BUILD_PATH): cv.string,
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
{
@@ -505,7 +505,6 @@ async def to_code(config: ConfigType) -> None:
cg.App.pre_setup(
config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
config.get(CONF_COMMENT, ""),
config[CONF_NAME_ADD_MAC_SUFFIX],
)
)
+21 -7
View File
@@ -271,7 +271,7 @@ def copy_src_tree():
"esphome", "core", "build_info_data.h"
)
build_info_json_path = CORE.relative_build_path("build_info.json")
config_hash, build_time, build_time_str = get_build_info()
config_hash, build_time, build_time_str, comment = get_build_info()
# Defensively force a rebuild if the build_info files don't exist, or if
# there was a config change which didn't actually cause a source change
@@ -292,7 +292,9 @@ def copy_src_tree():
if sources_changed:
write_file(
build_info_data_h_path,
generate_build_info_data_h(config_hash, build_time, build_time_str),
generate_build_info_data_h(
config_hash, build_time, build_time_str, comment
),
)
write_file(
build_info_json_path,
@@ -332,31 +334,43 @@ def generate_version_h():
)
def get_build_info() -> tuple[int, int, str]:
def get_build_info() -> tuple[int, int, str, str]:
"""Calculate build_info values from current config.
Returns:
Tuple of (config_hash, build_time, build_time_str)
Tuple of (config_hash, build_time, build_time_str, comment)
"""
config_hash = CORE.config_hash
build_time = int(time.time())
build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time))
return config_hash, build_time, build_time_str
comment = CORE.comment or ""
return config_hash, build_time, build_time_str, comment
def _escape_c_string(s: str) -> str:
"""Escape a string for use in a C string literal."""
return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
def generate_build_info_data_h(
config_hash: int, build_time: int, build_time_str: str
config_hash: int, build_time: int, build_time_str: str, comment: str
) -> str:
"""Generate build_info_data.h header with config hash and build time."""
"""Generate build_info_data.h header with config hash, build time, and comment."""
escaped_comment = _escape_c_string(comment)
# +1 for null terminator
comment_size = len(comment) + 1
return f"""#pragma once
// Auto-generated build_info data
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT
#define ESPHOME_BUILD_TIME {build_time} // NOLINT
#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT
#ifdef USE_ESP8266
#include <pgmspace.h>
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
#else
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
#endif
"""
+44 -4
View File
@@ -1357,36 +1357,67 @@ def test_generate_build_info_data_h_format() -> None:
config_hash = 0x12345678
build_time = 1700000000
build_time_str = "2023-11-14 22:13:20 +0000"
comment = "Test comment"
result = generate_build_info_data_h(config_hash, build_time, build_time_str)
result = generate_build_info_data_h(
config_hash, build_time, build_time_str, comment
)
assert "#pragma once" in result
assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result
assert "#define ESPHOME_BUILD_TIME 1700000000" in result
assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1
assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result
assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result
def test_generate_build_info_data_h_esp8266_progmem() -> None:
"""Test generate_build_info_data_h includes PROGMEM for ESP8266."""
result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test")
result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment")
# Should have ESP8266 PROGMEM conditional
assert "#ifdef USE_ESP8266" in result
assert "#include <pgmspace.h>" in result
assert "PROGMEM" in result
# Both build time and comment should have PROGMEM versions
assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result
assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result
def test_generate_build_info_data_h_hash_formatting() -> None:
"""Test generate_build_info_data_h formats hash with leading zeros."""
# Test with small hash value that needs leading zeros
result = generate_build_info_data_h(0x00000001, 0, "test")
result = generate_build_info_data_h(0x00000001, 0, "test", "")
assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result
# Test with larger hash value
result = generate_build_info_data_h(0xFFFFFFFF, 0, "test")
result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "")
assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result
def test_generate_build_info_data_h_comment_escaping() -> None:
"""Test generate_build_info_data_h properly escapes special characters in comment."""
# Test backslash escaping
result = generate_build_info_data_h(0, 0, "test", "backslash\\here")
assert 'ESPHOME_COMMENT_STR[] = "backslash\\\\here"' in result
# Test quote escaping
result = generate_build_info_data_h(0, 0, "test", 'has "quotes"')
assert 'ESPHOME_COMMENT_STR[] = "has \\"quotes\\""' in result
# Test newline escaping
result = generate_build_info_data_h(0, 0, "test", "line1\nline2")
assert 'ESPHOME_COMMENT_STR[] = "line1\\nline2"' in result
def test_generate_build_info_data_h_empty_comment() -> None:
"""Test generate_build_info_data_h handles empty comment."""
result = generate_build_info_data_h(0, 0, "test", "")
assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator
assert 'ESPHOME_COMMENT_STR[] = ""' in result
@patch("esphome.writer.CORE")
@patch("esphome.writer.iter_components")
@patch("esphome.writer.walk_files")
@@ -1445,6 +1476,7 @@ def test_copy_src_tree_writes_build_info_files(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = "Test comment"
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = [("core", mock_component)]
@@ -1466,6 +1498,8 @@ def test_copy_src_tree_writes_build_info_files(
assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content
assert "#define ESPHOME_BUILD_TIME" in build_info_h_content
assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content
assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content
assert "ESPHOME_COMMENT_STR" in build_info_h_content
# Verify build_info.json was written
build_info_json_path = build_path / "build_info.json"
@@ -1517,6 +1551,7 @@ def test_copy_src_tree_detects_config_hash_change(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF # Different from existing
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = []
@@ -1578,6 +1613,7 @@ def test_copy_src_tree_detects_version_change(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = []
@@ -1627,6 +1663,7 @@ def test_copy_src_tree_handles_invalid_build_info_json(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = []
@@ -1700,6 +1737,7 @@ def test_copy_src_tree_build_info_timestamp_behavior(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = [("test", mock_component)]
@@ -1794,6 +1832,7 @@ def test_copy_src_tree_detects_removed_source_file(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = [] # No components = file should be removed
@@ -1855,6 +1894,7 @@ def test_copy_src_tree_ignores_removed_generated_file(
mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args)
mock_core.defines = []
mock_core.config_hash = 0xDEADBEEF
mock_core.comment = ""
mock_core.target_platform = "test_platform"
mock_core.config = {}
mock_iter_components.return_value = []