mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user