mirror of
https://github.com/esphome/esphome.git
synced 2026-09-26 14:30:23 +00:00
Merge remote-tracking branch 'origin/optimize-value-accuracy' into integration
# Conflicts: # esphome/core/application.cpp # esphome/core/application.h # tests/components/core/test_helpers.cpp
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
@@ -27,6 +31,7 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -50,6 +55,11 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
|
||||
|
||||
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
|
||||
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
|
||||
CONF_MIN_CHANNELS = "min_channels"
|
||||
@@ -208,6 +218,19 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
|
||||
if not data.mp3_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
+56
-12
@@ -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');
|
||||
}
|
||||
@@ -529,28 +543,58 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Fast float-to-string for accuracy_decimals 0-3 (covers virtually all sensor usage).
|
||||
// Avoids snprintf("%.*f") which pulls in heavy float formatting machinery.
|
||||
// Caller must guarantee value is finite and |value| * mult fits in uint32_t.
|
||||
static size_t value_accuracy_to_buf_fast(char *buf, float value, int8_t accuracy_decimals, uint32_t mult) {
|
||||
char *p = buf;
|
||||
if (std::signbit(value)) {
|
||||
*p++ = '-';
|
||||
value = -value;
|
||||
}
|
||||
// Cast to double for the multiply to match snprintf's rounding precision.
|
||||
// float*int loses bits at exact-half boundaries (e.g. 23.45f*10 = 234.5 in float,
|
||||
// but snprintf sees 234.500007... via double promotion and rounds differently).
|
||||
// llrint returns long long so the result fits even on 32-bit targets where
|
||||
// long is 32-bit; caller has already bounded |value * mult| to UINT32_MAX.
|
||||
uint32_t scaled = static_cast<uint32_t>(llrint(static_cast<double>(value) * mult));
|
||||
p = uint32_to_str_unchecked(p, scaled / mult);
|
||||
if (accuracy_decimals > 0) {
|
||||
*p++ = '.';
|
||||
p = frac_to_str_unchecked(p, scaled % mult, mult / 10);
|
||||
}
|
||||
*p = '\0';
|
||||
return static_cast<size_t>(p - buf);
|
||||
}
|
||||
|
||||
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) {
|
||||
normalize_accuracy_decimals(value, accuracy_decimals);
|
||||
// snprintf returns chars that would be written (excluding null), or negative on error
|
||||
|
||||
// Fast path for accuracy 0-3, finite values whose scaled magnitude fits in uint32_t.
|
||||
// For 3 decimals that's |value| < ~4.29e6; larger totals fall through to snprintf.
|
||||
if (accuracy_decimals <= 3 && std::isfinite(value)) {
|
||||
const uint32_t mult = small_pow10(accuracy_decimals);
|
||||
if (std::fabs(value) < static_cast<float>(UINT32_MAX) / mult) {
|
||||
return value_accuracy_to_buf_fast(buf.data(), value, accuracy_decimals, mult);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for NaN/Inf/high accuracy/out-of-range
|
||||
int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value);
|
||||
if (len < 0)
|
||||
return 0; // encoding error
|
||||
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
|
||||
return 0;
|
||||
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
|
||||
}
|
||||
|
||||
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
|
||||
int8_t accuracy_decimals, StringRef unit_of_measurement) {
|
||||
if (unit_of_measurement.empty()) {
|
||||
return value_accuracy_to_buf(buf, value, accuracy_decimals);
|
||||
size_t len = value_accuracy_to_buf(buf, value, accuracy_decimals);
|
||||
if (len == 0 || unit_of_measurement.empty()) {
|
||||
return len;
|
||||
}
|
||||
normalize_accuracy_decimals(value, accuracy_decimals);
|
||||
// snprintf returns chars that would be written (excluding null), or negative on error
|
||||
int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
|
||||
if (len < 0)
|
||||
return 0; // encoding error
|
||||
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
|
||||
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
|
||||
char *end = buf_append_sep_str(buf.data() + len, buf.size() - len, ' ', unit_of_measurement.c_str(),
|
||||
unit_of_measurement.size());
|
||||
return static_cast<size_t>(end - buf.data());
|
||||
}
|
||||
|
||||
int8_t step_to_accuracy_decimals(float step) {
|
||||
|
||||
@@ -1295,6 +1295,56 @@ inline char *int8_to_str(char *buf, int8_t val) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Append a separator char and a string to a buffer, respecting remaining space.
|
||||
/// Returns pointer past last char written. The buffer is always null-terminated
|
||||
/// when remaining >= 1 (even on the no-room early-return), so callers always get
|
||||
/// a valid C string.
|
||||
inline char *buf_append_sep_str(char *buf, size_t remaining, char separator, const char *str, size_t str_len) {
|
||||
if (remaining < 2) {
|
||||
if (remaining >= 1) {
|
||||
*buf = '\0';
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
*buf++ = separator;
|
||||
remaining--;
|
||||
size_t copy_len = std::min(str_len, remaining - 1);
|
||||
memcpy(buf, str, copy_len);
|
||||
buf += copy_len;
|
||||
*buf = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Return 10^n for small non-negative n (0-3) as uint32_t, avoiding float.
|
||||
inline uint32_t small_pow10(int8_t n) { return n == 3 ? 1000 : n == 2 ? 100 : n == 1 ? 10 : 1; }
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
/// Write fractional digits with leading zeros to buffer (internal, no size check).
|
||||
/// frac is the fractional value, divisor is the highest place value (e.g. 100 for 3 digits).
|
||||
/// Returns pointer past last char written.
|
||||
inline char *frac_to_str_unchecked(char *buf, uint32_t frac, uint32_t divisor) {
|
||||
while (divisor > 0) {
|
||||
*buf++ = '0' + static_cast<char>(frac / divisor);
|
||||
frac %= divisor;
|
||||
divisor /= 10;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ dependencies:
|
||||
version: "7.4.2"
|
||||
esphome/esp-audio-libs:
|
||||
version: 2.0.4
|
||||
esphome/micro-decoder:
|
||||
version: 0.1.1
|
||||
esphome/micro-flac:
|
||||
version: 0.1.1
|
||||
esphome/micro-opus:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -117,4 +117,100 @@ TEST(FormatHexChar, UppercaseDigits) {
|
||||
EXPECT_EQ(format_hex_pretty_char(15), 'F');
|
||||
}
|
||||
|
||||
// --- small_pow10() ---
|
||||
|
||||
TEST(SmallPow10, Zero) { EXPECT_EQ(small_pow10(0), 1u); }
|
||||
TEST(SmallPow10, One) { EXPECT_EQ(small_pow10(1), 10u); }
|
||||
TEST(SmallPow10, Two) { EXPECT_EQ(small_pow10(2), 100u); }
|
||||
TEST(SmallPow10, Three) { EXPECT_EQ(small_pow10(3), 1000u); }
|
||||
|
||||
// --- frac_to_str_unchecked() ---
|
||||
|
||||
TEST(FracToStr, OneDigit) {
|
||||
char buf[8];
|
||||
char *end = frac_to_str_unchecked(buf, 5, 1);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "5");
|
||||
EXPECT_EQ(end - buf, 1);
|
||||
}
|
||||
|
||||
TEST(FracToStr, TwoDigits) {
|
||||
char buf[8];
|
||||
char *end = frac_to_str_unchecked(buf, 46, 10);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "46");
|
||||
}
|
||||
|
||||
TEST(FracToStr, ThreeDigits) {
|
||||
char buf[8];
|
||||
char *end = frac_to_str_unchecked(buf, 456, 100);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "456");
|
||||
EXPECT_EQ(end - buf, 3);
|
||||
}
|
||||
|
||||
TEST(FracToStr, LeadingZeros) {
|
||||
char buf[8];
|
||||
char *end = frac_to_str_unchecked(buf, 1, 100);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "001");
|
||||
|
||||
end = frac_to_str_unchecked(buf, 5, 10);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "05");
|
||||
}
|
||||
|
||||
TEST(FracToStr, AllZeros) {
|
||||
char buf[8];
|
||||
char *end = frac_to_str_unchecked(buf, 0, 100);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "000");
|
||||
|
||||
end = frac_to_str_unchecked(buf, 0, 1);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "0");
|
||||
}
|
||||
|
||||
TEST(FracToStr, ZeroDivisor) {
|
||||
char buf[8];
|
||||
buf[0] = 'X';
|
||||
char *end = frac_to_str_unchecked(buf, 0, 0);
|
||||
EXPECT_EQ(end, buf); // writes nothing
|
||||
}
|
||||
|
||||
// --- buf_append_sep_str() ---
|
||||
|
||||
TEST(BufAppendSepStr, Basic) {
|
||||
char buf[32] = "23.46";
|
||||
char *start = buf + 5;
|
||||
char *end = buf_append_sep_str(start, sizeof(buf) - 5, ' ', "°C", 3);
|
||||
EXPECT_STREQ(buf, "23.46 °C");
|
||||
EXPECT_EQ(end - buf, 9); // "°C" is 3 bytes (UTF-8)
|
||||
}
|
||||
|
||||
TEST(BufAppendSepStr, EmptyString) {
|
||||
char buf[32] = "100";
|
||||
char *start = buf + 3;
|
||||
char *end = buf_append_sep_str(start, sizeof(buf) - 3, ' ', "", 0);
|
||||
EXPECT_STREQ(buf, "100 ");
|
||||
EXPECT_EQ(end - start, 1); // just the separator
|
||||
}
|
||||
|
||||
TEST(BufAppendSepStr, NoRoom) {
|
||||
char buf[8] = "1234567";
|
||||
char *start = buf + 7;
|
||||
char *end = buf_append_sep_str(start, 1, ' ', "unit", 4);
|
||||
EXPECT_EQ(end, start); // nothing written
|
||||
}
|
||||
|
||||
TEST(BufAppendSepStr, Truncation) {
|
||||
char buf[8] = "val";
|
||||
char *start = buf + 3;
|
||||
// remaining = 5, separator takes 1, so 3 chars of string fit + null
|
||||
char *end = buf_append_sep_str(start, 5, ' ', "longunit", 8);
|
||||
*end = '\0';
|
||||
EXPECT_STREQ(buf, "val lon");
|
||||
EXPECT_EQ(end - buf, 7);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,152 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
// Helper to call value_accuracy_to_buf and return as string
|
||||
static std::string va_to_string(float value, int8_t accuracy_decimals) {
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf);
|
||||
size_t len = value_accuracy_to_buf(sp, value, accuracy_decimals);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
// Helper: reference implementation using snprintf for comparison
|
||||
static std::string va_reference(float value, int8_t accuracy_decimals) {
|
||||
// Replicate normalize_accuracy_decimals logic
|
||||
if (accuracy_decimals < 0) {
|
||||
float divisor;
|
||||
if (accuracy_decimals == -1) {
|
||||
divisor = 10.0f;
|
||||
} else if (accuracy_decimals == -2) {
|
||||
divisor = 100.0f;
|
||||
} else {
|
||||
divisor = pow10_int(-accuracy_decimals);
|
||||
}
|
||||
value = roundf(value / divisor) * divisor;
|
||||
accuracy_decimals = 0;
|
||||
}
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
snprintf(buf, sizeof(buf), "%.*f", accuracy_decimals, value);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// --- Basic formatting ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, ZeroDecimals) {
|
||||
EXPECT_EQ(va_to_string(23.456f, 0), "23");
|
||||
EXPECT_EQ(va_to_string(0.0f, 0), "0");
|
||||
EXPECT_EQ(va_to_string(100.0f, 0), "100");
|
||||
EXPECT_EQ(va_to_string(1.0f, 0), "1");
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, OneDecimal) {
|
||||
EXPECT_EQ(va_to_string(23.456f, 1), "23.5");
|
||||
EXPECT_EQ(va_to_string(0.0f, 1), "0.0");
|
||||
EXPECT_EQ(va_to_string(1.05f, 1), va_reference(1.05f, 1));
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, TwoDecimals) {
|
||||
EXPECT_EQ(va_to_string(23.456f, 2), "23.46");
|
||||
EXPECT_EQ(va_to_string(0.0f, 2), "0.00");
|
||||
EXPECT_EQ(va_to_string(1.005f, 2), va_reference(1.005f, 2));
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, ThreeDecimals) {
|
||||
EXPECT_EQ(va_to_string(23.456f, 3), "23.456");
|
||||
EXPECT_EQ(va_to_string(0.0f, 3), "0.000");
|
||||
}
|
||||
|
||||
// --- Negative values ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, NegativeValues) {
|
||||
EXPECT_EQ(va_to_string(-23.456f, 2), "-23.46");
|
||||
EXPECT_EQ(va_to_string(-0.5f, 1), "-0.5");
|
||||
EXPECT_EQ(va_to_string(-100.0f, 0), "-100");
|
||||
}
|
||||
|
||||
// --- Negative accuracy_decimals (rounding to tens/hundreds) ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, NegativeAccuracy) {
|
||||
EXPECT_EQ(va_to_string(1234.0f, -1), va_reference(1234.0f, -1));
|
||||
EXPECT_EQ(va_to_string(1234.0f, -2), va_reference(1234.0f, -2));
|
||||
EXPECT_EQ(va_to_string(56.0f, -1), va_reference(56.0f, -1));
|
||||
}
|
||||
|
||||
// --- Special float values ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, NaN) {
|
||||
std::string result = va_to_string(NAN, 2);
|
||||
EXPECT_EQ(result, va_reference(NAN, 2));
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, Infinity) {
|
||||
std::string result = va_to_string(INFINITY, 2);
|
||||
EXPECT_EQ(result, va_reference(INFINITY, 2));
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, NegativeInfinity) {
|
||||
std::string result = va_to_string(-INFINITY, 2);
|
||||
EXPECT_EQ(result, va_reference(-INFINITY, 2));
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, VerySmallValues) {
|
||||
EXPECT_EQ(va_to_string(0.001f, 3), "0.001");
|
||||
EXPECT_EQ(va_to_string(0.001f, 2), "0.00");
|
||||
EXPECT_EQ(va_to_string(0.009f, 2), "0.01");
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, LargeValues) {
|
||||
EXPECT_EQ(va_to_string(999999.0f, 0), va_reference(999999.0f, 0));
|
||||
EXPECT_EQ(va_to_string(1013.25f, 2), "1013.25");
|
||||
}
|
||||
|
||||
TEST(ValueAccuracyToBuf, Rounding) {
|
||||
// 0.5 rounds up
|
||||
EXPECT_EQ(va_to_string(23.5f, 0), "24");
|
||||
EXPECT_EQ(va_to_string(23.45f, 1), "23.5"); // float: 23.45 -> 23.4 or 23.5
|
||||
EXPECT_EQ(va_to_string(23.45f, 1), va_reference(23.45f, 1));
|
||||
}
|
||||
|
||||
// --- Match snprintf for a range of typical sensor values ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, MatchesSnprintf) {
|
||||
float test_values[] = {0.0f, 1.0f, -1.0f, 23.456f, -23.456f, 100.0f, 0.1f, 0.01f, 99.99f, 1013.25f, -40.0f};
|
||||
int8_t test_accuracies[] = {0, 1, 2, 3};
|
||||
|
||||
for (float value : test_values) {
|
||||
for (int8_t acc : test_accuracies) {
|
||||
EXPECT_EQ(va_to_string(value, acc), va_reference(value, acc))
|
||||
<< "Mismatch for value=" << value << " accuracy=" << static_cast<int>(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Return value (length) ---
|
||||
|
||||
TEST(ValueAccuracyToBuf, ReturnsCorrectLength) {
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
std::span<char, VALUE_ACCURACY_MAX_LEN> sp(buf);
|
||||
|
||||
size_t len = value_accuracy_to_buf(sp, 23.456f, 2);
|
||||
EXPECT_EQ(len, 5u); // "23.46"
|
||||
EXPECT_EQ(strlen(buf), len);
|
||||
|
||||
len = value_accuracy_to_buf(sp, 0.0f, 0);
|
||||
EXPECT_EQ(len, 1u); // "0"
|
||||
EXPECT_EQ(strlen(buf), len);
|
||||
|
||||
len = value_accuracy_to_buf(sp, -100.0f, 1);
|
||||
EXPECT_EQ(len, 6u); // "-100.0"
|
||||
EXPECT_EQ(strlen(buf), len);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
Reference in New Issue
Block a user