Type the API progmem name field as ProgmemStringRef

Progmem string fields must take the short encode path, the outlined encode
helper takes a ProgmemStringRef, and the hasher test checks the Python value.
This commit is contained in:
J. Nick Koston
2026-10-08 18:22:29 -10:00
parent b4289aee59
commit 13057b7ae3
10 changed files with 55 additions and 58 deletions
+1 -3
View File
@@ -474,9 +474,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
msg.key = entity->get_object_id_hash();
if (entity->has_own_name()) {
// name is a progmem field, so it is encoded straight from flash
const ProgmemStringRef &name = entity->get_name_progmem();
msg.name = StringRef(name.progmem_ptr(), name.size());
msg.name = entity->get_name_progmem(); // encoded straight from flash
}
// Set common EntityBase properties
+1 -1
View File
@@ -399,7 +399,7 @@ class InfoResponseProtoMessage : public ProtoMessage {
public:
StringRef object_id{};
uint32_t key{0};
StringRef name{};
ProgmemStringRef name{};
bool disabled_by_default{false};
#ifdef USE_ENTITY_ICON
StringRef icon{nullptr, 0}; // null until set, encode only
+3 -4
View File
@@ -136,11 +136,10 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
}
// Helper for string fields in flash: copies the shown characters out with progmem_memcpy first
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const StringRef &value, int indent = 2) {
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
char buf[DUMP_BYTES_MAX];
size_t len = std::min(value.size(), sizeof(buf));
progmem_memcpy(buf, value.c_str(), len);
dump_field(out, field_name, StringRef(buf, len), indent);
dump_field(out, field_name, StringRef(buf, value.write_to(buf, sizeof(buf))), indent);
}
#pragma GCC diagnostic pop
+4 -4
View File
@@ -418,16 +418,16 @@ class ProtoEncode {
std::memcpy(pos + 2, ref.c_str(), ref.size());
return pos + 2 + ref.size();
}
/// encode_short_string_force for a string in flash (PROGMEM), read with progmem_memcpy.
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const StringRef &ref) {
/// encode_short_string_force for a string that may be in flash (PROGMEM).
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *encode_short_progmem_string_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
progmem_memcpy(pos + 2, ref.c_str(), ref.size());
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
return pos + 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
+1 -1
View File
@@ -873,7 +873,7 @@ void Sprinkler::reset_resume() {
#ifndef USE_ESP8266
const char *Sprinkler::valve_name(const size_t valve_number) {
if (this->is_a_valid_valve(valve_number)) {
return this->valve_[valve_number].controller_switch->get_name_progmem().progmem_ptr(); // RAM off ESP8266
return this->valve_[valve_number].controller_switch->get_name_progmem().ram_ref().c_str();
}
return nullptr;
}
+1 -1
View File
@@ -82,7 +82,7 @@ class EntityBase {
}
/// Get the name of this Entity, copied out of flash into buffer.
StringRef get_name_to(std::span<char, ENTITY_NAME_BUF_SIZE> buffer) const {
return StringRef(buffer.data(), this->name_.write_to(buffer.data(), buffer.size()));
return StringRef(buffer.data(), this->write_name_to(buffer.data(), buffer.size()));
}
#else
ESPDEPRECATED("Use get_name_to() or get_log_name() instead. Will be removed in ESPHome 2027.5.0", "2026.11.0")
+1 -1
View File
@@ -262,7 +262,7 @@ class ProgmemStringRef {
constexpr StringRef ram_ref() const { return StringRef(this->base_, this->len_); }
#endif
// Remove before 2027.5.0 (helpers.h's ESPDEPRECATED is not reachable from here)
// Remove before 2027.5.0
[[deprecated(
"May point to flash; use progmem_ptr() or the owner's accessors. Removed in 2027.5.0")]] constexpr const char *
c_str() const {
+21 -26
View File
@@ -536,21 +536,24 @@ def create_field_type_info(
f"track_presence on field '{field.name}' has no effect; it requires "
"a non-repeated message field in a message that is decoded"
)
if get_field_opt(field, pb.progmem, False) and (
field.label == FieldDescriptorProto.LABEL_REPEATED
or (
field.type != 9
and (
field.type != 12
or not get_field_opt(field, pb.pointer_to_buffer, False)
or get_field_opt(field, pb.fixed_array_size) is not None
if get_field_opt(field, pb.progmem, False):
is_short_string = (
field.type == FieldDescriptorProto.TYPE_STRING
and get_field_opt(field, pb.force, False)
and (get_field_opt(field, pb.max_data_length) or 128) < 128
)
is_pointer_bytes = (
field.type == FieldDescriptorProto.TYPE_BYTES
and get_field_opt(field, pb.pointer_to_buffer, False)
and get_field_opt(field, pb.fixed_array_size) is None
)
if field.label == FieldDescriptorProto.LABEL_REPEATED or not (
is_short_string or is_pointer_bytes
):
raise ValueError(
f"progmem on field '{field.name}' requires a forced string field with "
"max_data_length below 128, or a bytes field with pointer_to_buffer"
)
)
):
raise ValueError(
f"progmem on field '{field.name}' requires a non-repeated string field, "
"or a bytes field with pointer_to_buffer"
)
if field.label == FieldDescriptorProto.LABEL_REPEATED:
# Check if this is a packed_buffer field (zero-copy packed repeated)
if get_field_opt(field, pb.packed_buffer, False):
@@ -1262,6 +1265,8 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def public_content(self) -> list[str]:
if self.progmem:
return [f"ProgmemStringRef {self.field_name}{{}};"]
if self._starts_null:
return [
f"StringRef {self.field_name}{{nullptr, 0}}; // null until set, encode only"
@@ -1283,14 +1288,6 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
else "encode_short_string_force"
)
return _encode_call(encode_fn, str(tag), f"this->{self.field_name}")
if self.progmem:
return _encode_call(
"encode_progmem_bytes",
str(self.number),
f"reinterpret_cast<const uint8_t *>(this->{self.field_name}.c_str())",
f"this->{self.field_name}.size()",
force=self.force,
)
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()",
@@ -3393,12 +3390,10 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
}
// Helper for string fields in flash: copies the shown characters out with progmem_memcpy first
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const StringRef &value,
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
char buf[DUMP_BYTES_MAX];
size_t len = std::min(value.size(), sizeof(buf));
progmem_memcpy(buf, value.c_str(), len);
dump_field(out, field_name, StringRef(buf, len), indent);
dump_field(out, field_name, StringRef(buf, value.write_to(buf, sizeof(buf))), indent);
}
#pragma GCC diagnostic pop
+5 -4
View File
@@ -440,12 +440,13 @@ TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) {
EXPECT_EQ(buf[255], 0xA5);
}
TEST(Fnv1HashObjectId, ProgmemVariantMatches) {
// Values match Python fnv1_hash_object_id(); the _P variant must hash identically
TEST(Fnv1HashObjectId, MatchesPythonThroughAnyReader) {
// 0x9d099819 is Python fnv1_hash_object_id("Living Room Temp")
static const char NAME[] = "Living Room Temp";
const size_t len = sizeof(NAME) - 1;
EXPECT_EQ(fnv1_hash_object_id_P(NAME, len), fnv1_hash_object_id(NAME, len));
EXPECT_EQ(fnv1_hash_object_id_P("", 0), fnv1_hash_object_id("", 0));
EXPECT_EQ(fnv1_hash_object_id(NAME, len), 0x9d099819u);
EXPECT_EQ(fnv1_hash_object_id_with(NAME, len, [](const char *p) { return *p; }), 0x9d099819u);
EXPECT_EQ(fnv1_hash_object_id_P(NAME, len), 0x9d099819u);
}
} // namespace esphome::core::testing
@@ -415,23 +415,27 @@ def test_progmem_rejected_on_non_bytes_field() -> None:
create_field_type_info(field, needs_decode=False)
def test_progmem_string_field_copies_from_flash() -> None:
"""A short forced (progmem) string field encodes and dumps through the progmem helpers."""
field = _field(9, force=True)
field.options.Extensions[pb.progmem] = True
field.options.Extensions[pb.max_data_length] = 120
ti = create_field_type_info(field, needs_decode=False)
assert "ProgmemStringRef value{};" in ti.public_content
assert "encode_short_progmem_string_force(" in ti.encode_content
assert "dump_progmem_string_field(" in ti.dump_content
@pytest.mark.parametrize(
("force", "max_len", "encode_fn"),
[
(True, 120, "encode_short_progmem_string_force("),
(False, None, "encode_progmem_bytes("),
(True, None, "encode_progmem_bytes_force("),
],
("force", "max_len"), [(False, 120), (True, None), (True, 200)]
)
def test_progmem_string_field_copies_from_flash(
force: bool, max_len: int | None, encode_fn: str
def test_progmem_string_requires_short_forced_field(
force: bool, max_len: int | None
) -> None:
"""A (progmem) string field encodes and dumps through the progmem_memcpy helpers."""
"""A (progmem) string field must take the short encode path, the only one that reads flash."""
field = _field(9, force=force)
field.options.Extensions[pb.progmem] = True
if max_len is not None:
field.options.Extensions[pb.max_data_length] = max_len
ti = create_field_type_info(field, needs_decode=False)
assert encode_fn in ti.encode_content
assert "encode_string" not in ti.encode_content
assert "dump_progmem_string_field(" in ti.dump_content
with pytest.raises(ValueError, match="progmem on field 'value'"):
create_field_type_info(field, needs_decode=False)