[core] Add uint32_to_str helper and use in preferences (#15597)

This commit is contained in:
J. Nick Koston
2026-04-14 07:49:44 -10:00
committed by GitHub
parent 5ba8c644e4
commit cf01163c8c
6 changed files with 170 additions and 16 deletions
+4 -8
View File
@@ -4,7 +4,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <nvs_flash.h>
#include <cinttypes>
#include <cstring>
#include <vector>
@@ -12,9 +11,6 @@ namespace esphome::esp32 {
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
@@ -51,8 +47,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
@@ -108,8 +104,8 @@ bool ESP32Preferences::sync() {
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
+4 -8
View File
@@ -3,7 +3,6 @@
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
#include <vector>
@@ -11,9 +10,6 @@ namespace esphome::libretiny {
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
@@ -50,8 +46,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) {
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, this->key);
fdb_blob_make(this->blob, data, len);
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
if (actual_len != len) {
@@ -92,8 +88,8 @@ bool LibreTinyPreferences::sync() {
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
+14
View File
@@ -380,6 +380,20 @@ static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t
return buffer;
}
char *uint32_to_str_unchecked(char *buf, uint32_t val) {
if (val == 0) {
*buf++ = '0';
return buf;
}
char *start = buf;
while (val > 0) {
*buf++ = '0' + (val % 10);
val /= 10;
}
std::reverse(start, buf);
return buf;
}
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
return format_hex_internal(buffer, buffer_size, data, length, 0, 'a');
}
+15
View File
@@ -1295,6 +1295,21 @@ inline char *int8_to_str(char *buf, int8_t val) {
return buf;
}
/// Minimum buffer size for uint32_to_str: 10 digits + null terminator.
static constexpr size_t UINT32_MAX_STR_SIZE = 11;
/// Write unsigned 32-bit integer to buffer (internal, no size check).
/// Buffer must have at least 10 bytes free. Returns pointer past last char written.
char *uint32_to_str_unchecked(char *buf, uint32_t val);
/// Write unsigned 32-bit integer to buffer with compile-time size check.
/// Null-terminates the output. Returns number of chars written (excluding null).
inline size_t uint32_to_str(std::span<char, UINT32_MAX_STR_SIZE> buf, uint32_t val) {
char *end = uint32_to_str_unchecked(buf.data(), val);
*end = '\0';
return static_cast<size_t>(end - buf.data());
}
/// Format byte array as lowercase hex to buffer (base implementation).
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
+56
View File
@@ -1,4 +1,6 @@
#include <benchmark/benchmark.h>
#include <cinttypes>
#include <cstdio>
#include "esphome/core/helpers.h"
@@ -307,4 +309,58 @@ static void Base64Decode_32Bytes(benchmark::State &state) {
}
BENCHMARK(Base64Decode_32Bytes);
// --- uint32_to_str() vs snprintf ---
static void Uint32ToStr_Small(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_to_str(buf, 12345);
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Uint32ToStr_Small);
static void Snprintf_Uint32_Small(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(12345));
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Snprintf_Uint32_Small);
static void Uint32ToStr_Large(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_to_str(buf, 4294967295u);
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Uint32ToStr_Large);
static void Snprintf_Uint32_Large(benchmark::State &state) {
char buf[UINT32_MAX_STR_SIZE];
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
snprintf(buf, sizeof(buf), "%" PRIu32, static_cast<uint32_t>(4294967295u));
benchmark::DoNotOptimize(buf);
benchmark::ClobberMemory();
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Snprintf_Uint32_Large);
} // namespace esphome::benchmarks
@@ -0,0 +1,77 @@
#include <gtest/gtest.h>
#include "esphome/core/helpers.h"
namespace esphome::core::testing {
// --- uint32_to_str_unchecked() (internal, raw pointer) ---
TEST(Uint32ToStr, InternalZero) {
char buf[UINT32_MAX_STR_SIZE];
char *end = uint32_to_str_unchecked(buf, 0);
*end = '\0';
EXPECT_STREQ(buf, "0");
EXPECT_EQ(end - buf, 1);
}
TEST(Uint32ToStr, InternalSingleDigit) {
char buf[UINT32_MAX_STR_SIZE];
char *end = uint32_to_str_unchecked(buf, 7);
*end = '\0';
EXPECT_STREQ(buf, "7");
}
TEST(Uint32ToStr, InternalMultiDigit) {
char buf[UINT32_MAX_STR_SIZE];
char *end = uint32_to_str_unchecked(buf, 12345);
*end = '\0';
EXPECT_STREQ(buf, "12345");
EXPECT_EQ(end - buf, 5);
}
TEST(Uint32ToStr, InternalMaxValue) {
char buf[UINT32_MAX_STR_SIZE];
char *end = uint32_to_str_unchecked(buf, 4294967295u);
*end = '\0';
EXPECT_STREQ(buf, "4294967295");
EXPECT_EQ(end - buf, 10);
}
TEST(Uint32ToStr, InternalPowersOfTen) {
char buf[UINT32_MAX_STR_SIZE];
char *end;
end = uint32_to_str_unchecked(buf, 10);
*end = '\0';
EXPECT_STREQ(buf, "10");
end = uint32_to_str_unchecked(buf, 100);
*end = '\0';
EXPECT_STREQ(buf, "100");
end = uint32_to_str_unchecked(buf, 1000000);
*end = '\0';
EXPECT_STREQ(buf, "1000000");
}
// --- uint32_to_str() (public, span API) ---
TEST(Uint32ToStr, SpanZero) {
char buf[UINT32_MAX_STR_SIZE];
EXPECT_EQ(uint32_to_str(buf, 0), 1u);
EXPECT_STREQ(buf, "0");
}
TEST(Uint32ToStr, SpanMultiDigit) {
char buf[UINT32_MAX_STR_SIZE];
EXPECT_EQ(uint32_to_str(buf, 12345), 5u);
EXPECT_STREQ(buf, "12345");
}
TEST(Uint32ToStr, SpanMaxValue) {
char buf[UINT32_MAX_STR_SIZE];
EXPECT_EQ(uint32_to_str(buf, 4294967295u), 10u);
EXPECT_STREQ(buf, "4294967295");
}
} // namespace esphome::core::testing