mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
222d330464 | ||
|
|
0fffaa5c4f | ||
|
|
55a6c1183c | ||
|
|
a0ab3d2634 | ||
|
|
f8598751bc | ||
|
|
89961864cf | ||
|
|
7a51ca87b4 | ||
|
|
87e7acdc2b | ||
|
|
247e31e4d2 | ||
|
|
6521dd8269 | ||
|
|
bdf4643b97 | ||
|
|
bb9ec63e6b | ||
|
|
aeb77b1e6b | ||
|
|
e627f13b42 | ||
|
|
c4ebe611dd | ||
|
|
3061f9a9c1 | ||
|
|
c4133a1af5 | ||
|
|
c2f651b803 | ||
|
|
c7472e9068 | ||
|
|
2fdd4434dd | ||
|
|
acc7fd2143 | ||
|
|
fc9743e5c7 | ||
|
|
08880dda0a | ||
|
|
a26ab932e2 | ||
|
|
3fd62a666c | ||
|
|
79f1c56ca8 | ||
|
|
3ebb022f55 | ||
|
|
cf5c08c07b | ||
|
|
e732191329 | ||
|
|
2be6a24507 | ||
|
|
2b355fcb14 | ||
|
|
5a838cf0e6 | ||
|
|
ff8a3ea2be | ||
|
|
3a8e90615a | ||
|
|
d3963a82db | ||
|
|
206ab92115 | ||
|
|
426efc1666 | ||
|
|
2c7748606e | ||
|
|
907b3c6d6a | ||
|
|
967c61da4c | ||
|
|
7f7e4af3ea | ||
|
|
a230ffd928 | ||
|
|
62d074aae6 | ||
|
|
250ed963cc | ||
|
|
007eadedb3 | ||
|
|
50f206f9ff | ||
|
|
ed076e9210 | ||
|
|
9aa0a835c4 | ||
|
|
8c9d117be5 | ||
|
|
1c33d6b960 | ||
|
|
79ccc2b5c1 | ||
|
|
be88427d00 | ||
|
|
fbeb90208b | ||
|
|
7d5757e78e | ||
|
|
b593034a2f | ||
|
|
576c635138 | ||
|
|
1dd7f6045d | ||
|
|
a644d8d9a0 | ||
|
|
f2651d794d | ||
|
|
2ed6453467 | ||
|
|
9f6b78930b | ||
|
|
1bca56ba04 | ||
|
|
cc7a9db510 | ||
|
|
a20755a857 | ||
|
|
9389b5b078 | ||
|
|
5859db98cb | ||
|
|
3161162207 | ||
|
|
12ee725395 | ||
|
|
9a8fbf2e2b | ||
|
|
c0ff8d7e98 | ||
|
|
15aaaae719 | ||
|
|
88eaaa86c8 | ||
|
|
a493403ef5 | ||
|
|
aac2f28df2 | ||
|
|
5daa9693d0 | ||
|
|
1e6a3c55b6 |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import math
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
@@ -301,8 +302,6 @@ ApplyFilter = sensor_ns.class_("ApplyFilter", Filter)
|
||||
APPLY_FILTER = automation.ApplyFilterDomain(
|
||||
FILTER_REGISTRY, ApplyFilter, cg.optional.template(cg.float_), ((cg.float_, "x"),)
|
||||
)
|
||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
||||
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
|
||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
|
||||
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
@@ -321,11 +320,8 @@ HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter)
|
||||
DeltaFilter = sensor_ns.class_("DeltaFilter", Filter)
|
||||
OrFilter = sensor_ns.class_("OrFilter", Filter)
|
||||
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
|
||||
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
|
||||
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
|
||||
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
|
||||
RoundSignificantDigitsFilter = sensor_ns.class_("RoundSignificantDigitsFilter", Filter)
|
||||
@@ -424,16 +420,16 @@ def sensor_schema(
|
||||
return _SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
|
||||
async def offset_filter_to_code(config, filter_id):
|
||||
template_ = await cg.templatable(config, [], cg.float_)
|
||||
return cg.new_Pvariable(filter_id, template_)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_))
|
||||
async def multiply_filter_to_code(config, filter_id):
|
||||
template_ = await cg.templatable(config, [], cg.float_)
|
||||
return cg.new_Pvariable(filter_id, template_)
|
||||
APPLY_FILTER.register(
|
||||
"offset",
|
||||
cv.templatable(cv.float_),
|
||||
automation.ApplyCall("return x + {};", ((automation.WHOLE_CONFIG, cg.float_),)),
|
||||
)
|
||||
APPLY_FILTER.register(
|
||||
"multiply",
|
||||
cv.templatable(cv.float_),
|
||||
automation.ApplyCall("return x * {};", ((automation.WHOLE_CONFIG, cg.float_),)),
|
||||
)
|
||||
|
||||
|
||||
TemplatableFloat = cg.esphome_ns.class_("TemplatableFn").template(cg.float_)
|
||||
@@ -838,6 +834,18 @@ def validate_calibrate_linear(config):
|
||||
LINEAR_SEGMENT = cg.std_ns.class_("array").template(cg.float_, 3)
|
||||
|
||||
|
||||
_LINEAR = automation.ApplyCall(
|
||||
"return x * {} + {};", (("k", cg.float_), ("b", cg.float_))
|
||||
)
|
||||
_LINEAR_SCHEMA = cv.Schema({cv.Required("k"): cv.float_, cv.Required("b"): cv.float_})
|
||||
|
||||
|
||||
async def _linear_filter(filter_id: ID, k: float, b: float) -> MockObj:
|
||||
return await APPLY_FILTER.build(
|
||||
"calibrate_linear", {"k": k, "b": b}, filter_id, _LINEAR, schema=_LINEAR_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"calibrate_linear",
|
||||
CalibrateLinearFilter,
|
||||
@@ -858,12 +866,12 @@ async def calibrate_linear_filter_to_code(config, filter_id):
|
||||
x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]]
|
||||
y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]]
|
||||
|
||||
linear_functions = []
|
||||
if config[CONF_METHOD] == "least_squares":
|
||||
k, b = fit_linear(x, y)
|
||||
linear_functions = [[k, b, float("NaN")]]
|
||||
elif config[CONF_METHOD] == "exact":
|
||||
linear_functions = map_linear(x, y)
|
||||
return await _linear_filter(filter_id, *fit_linear(x, y))
|
||||
linear_functions = map_linear(x, y)
|
||||
if len(linear_functions) == 1:
|
||||
# One segment needs no table
|
||||
return await _linear_filter(filter_id, *linear_functions[0][:2])
|
||||
# The last segment always has a NaN boundary, so the filter needs no count.
|
||||
table = cg.shared_progmem_array(
|
||||
"sensor_calibrate_linear", LINEAR_SEGMENT, linear_functions
|
||||
@@ -935,14 +943,31 @@ CLAMP_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("clamp", ClampFilter, CLAMP_SCHEMA)
|
||||
async def clamp_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_MIN_VALUE],
|
||||
config[CONF_MAX_VALUE],
|
||||
config[CONF_IGNORE_OUT_OF_RANGE],
|
||||
)
|
||||
def _clamp_bound(config: ConfigType, value: float) -> str:
|
||||
"""The bound as written, NAN for no bound.
|
||||
|
||||
A literal beyond float range was infinite in the old ClampFilter, so no bound; one that
|
||||
rounds to FLT_MAX stays a bound. The compiler rounds the literal, as it always did.
|
||||
"""
|
||||
try:
|
||||
bound = struct.unpack("f", struct.pack("f", value))[0]
|
||||
except OverflowError:
|
||||
return "NAN"
|
||||
return str(cg.safe_exp(value)) if math.isfinite(bound) else "NAN"
|
||||
|
||||
|
||||
APPLY_FILTER.register(
|
||||
"clamp",
|
||||
CLAMP_SCHEMA,
|
||||
automation.ApplyCall(
|
||||
"return sensor::clamp_filter_value(x, {}, {}, {});",
|
||||
(
|
||||
(CONF_MIN_VALUE, cg.float_, _clamp_bound),
|
||||
(CONF_MAX_VALUE, cg.float_, _clamp_bound),
|
||||
(CONF_IGNORE_OUT_OF_RANGE, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
@@ -1235,46 +1260,26 @@ def ntc_process_calibration(value):
|
||||
}
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
_NTC_SCHEMA = cv.Schema({cv.Required(CONF_CALIBRATION): ntc_process_calibration})
|
||||
_NTC_COEFFICIENTS = tuple(
|
||||
((CONF_CALIBRATION, key), cg.double) for key in (CONF_A, CONF_B, CONF_C)
|
||||
)
|
||||
APPLY_FILTER.register(
|
||||
"to_ntc_resistance",
|
||||
ToNTCResistanceFilter,
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CALIBRATION): ntc_process_calibration,
|
||||
}
|
||||
),
|
||||
_NTC_SCHEMA,
|
||||
automation.ApplyCall(
|
||||
"return sensor::steinhart_hart_resistance(x, {}, {}, {});",
|
||||
_NTC_COEFFICIENTS,
|
||||
),
|
||||
)
|
||||
async def calibrate_ntc_resistance_filter_to_code(config, filter_id):
|
||||
calib = config[CONF_CALIBRATION]
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
calib[CONF_A],
|
||||
calib[CONF_B],
|
||||
calib[CONF_C],
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
APPLY_FILTER.register(
|
||||
"to_ntc_temperature",
|
||||
ToNTCTemperatureFilter,
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CALIBRATION): ntc_process_calibration,
|
||||
}
|
||||
),
|
||||
_NTC_SCHEMA,
|
||||
automation.ApplyCall(
|
||||
"return sensor::steinhart_hart_temperature(x, {}, {}, {});",
|
||||
_NTC_COEFFICIENTS,
|
||||
),
|
||||
)
|
||||
async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
|
||||
calib = config[CONF_CALIBRATION]
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
calib[CONF_A],
|
||||
calib[CONF_B],
|
||||
calib[CONF_C],
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
|
||||
@@ -215,16 +215,6 @@ optional<float> LambdaFilter::new_value(float value) {
|
||||
return it;
|
||||
}
|
||||
|
||||
// OffsetFilter
|
||||
OffsetFilter::OffsetFilter(TemplatableFn<float> offset) : offset_(offset) {}
|
||||
|
||||
optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); }
|
||||
|
||||
// MultiplyFilter
|
||||
MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(multiplier) {}
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// ValueListFilter
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
@@ -433,25 +423,6 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||
return res;
|
||||
}
|
||||
|
||||
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
|
||||
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
|
||||
optional<float> ClampFilter::new_value(float value) {
|
||||
if (std::isfinite(this->min_) && !(value >= this->min_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
}
|
||||
return this->min_;
|
||||
}
|
||||
|
||||
if (std::isfinite(this->max_) && !(value <= this->max_)) {
|
||||
if (this->ignore_out_of_range_) {
|
||||
return {};
|
||||
}
|
||||
return this->max_;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {}
|
||||
optional<float> RoundFilter::new_value(float value) {
|
||||
if (std::isfinite(value)) {
|
||||
@@ -469,25 +440,33 @@ optional<float> RoundMultipleFilter::new_value(float value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
optional<float> ToNTCResistanceFilter::new_value(float value) {
|
||||
if (!std::isfinite(value)) {
|
||||
optional<float> clamp_filter_value(float value, float min, float max, bool ignore_out_of_range) {
|
||||
if (std::isfinite(min) && !(value >= min))
|
||||
return ignore_out_of_range ? optional<float>{} : min;
|
||||
if (std::isfinite(max) && !(value <= max))
|
||||
return ignore_out_of_range ? optional<float>{} : max;
|
||||
return value;
|
||||
}
|
||||
|
||||
float steinhart_hart_resistance(float temperature, double a, double b, double c) {
|
||||
if (!std::isfinite(temperature)) {
|
||||
return NAN;
|
||||
}
|
||||
double k = 273.15;
|
||||
// https://de.wikipedia.org/wiki/Steinhart-Hart-Gleichung#cite_note-stein2_s4-3
|
||||
double t = static_cast<double>(value) + k;
|
||||
double y = (this->a_ - 1 / (t)) / (2 * this->c_);
|
||||
double x = sqrt(pow(this->b_ / (3 * this->c_), 3) + y * y);
|
||||
double t = static_cast<double>(temperature) + k;
|
||||
double y = (a - 1 / (t)) / (2 * c);
|
||||
double x = sqrt(pow(b / (3 * c), 3) + y * y);
|
||||
double resistance = exp(pow(x - y, 1 / 3.0) - pow(x + y, 1 / 3.0));
|
||||
return resistance;
|
||||
}
|
||||
|
||||
optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
if (!std::isfinite(value)) {
|
||||
float steinhart_hart_temperature(float resistance, double a, double b, double c) {
|
||||
if (!std::isfinite(resistance)) {
|
||||
return NAN;
|
||||
}
|
||||
double lr = log(double(value));
|
||||
double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
|
||||
double lr = log(double(resistance));
|
||||
double v = a + b * lr + c * lr * lr * lr;
|
||||
double temp = float(1.0 / v - 273.15);
|
||||
return temp;
|
||||
}
|
||||
|
||||
@@ -300,27 +300,6 @@ template<auto Fn> class ApplyFilter final : public Filter {
|
||||
optional<float> new_value(float value) override { return Fn(value); }
|
||||
};
|
||||
|
||||
/// A simple filter that adds `offset` to each value it receives.
|
||||
class OffsetFilter : public Filter {
|
||||
public:
|
||||
explicit OffsetFilter(TemplatableFn<float> offset);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
TemplatableFn<float> offset_;
|
||||
};
|
||||
|
||||
/// A simple filter that multiplies to each value it receives by `multiplier`.
|
||||
class MultiplyFilter : public Filter {
|
||||
public:
|
||||
explicit MultiplyFilter(TemplatableFn<float> multiplier);
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
TemplatableFn<float> multiplier_;
|
||||
};
|
||||
|
||||
// Value tables are read in place from flash, which ESP8266 only allows for whole words.
|
||||
static_assert(sizeof(TemplatableFn<float>) == sizeof(void *), "TemplatableFn<float> must stay a single pointer");
|
||||
|
||||
@@ -574,17 +553,6 @@ class CalibratePolynomialFilter : public Filter {
|
||||
const float *coefficients_; // [count, c0, c1, ...]
|
||||
};
|
||||
|
||||
class ClampFilter : public Filter {
|
||||
public:
|
||||
ClampFilter(float min, float max, bool ignore_out_of_range);
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
float min_{NAN};
|
||||
float max_{NAN};
|
||||
bool ignore_out_of_range_;
|
||||
};
|
||||
|
||||
class RoundFilter : public Filter {
|
||||
public:
|
||||
explicit RoundFilter(uint8_t precision);
|
||||
@@ -616,27 +584,13 @@ template<uint8_t Digits> class RoundSignificantDigitsFilter : public Filter {
|
||||
}
|
||||
};
|
||||
|
||||
class ToNTCResistanceFilter : public Filter {
|
||||
public:
|
||||
ToNTCResistanceFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}
|
||||
optional<float> new_value(float value) override;
|
||||
/// Clamp to [min, max] for the clamp filter; a non-finite bound is no bound, and NaN input fails the first set bound.
|
||||
/// Out of range gives the bound, or empty to drop the value with `ignore_out_of_range`.
|
||||
optional<float> clamp_filter_value(float value, float min, float max, bool ignore_out_of_range);
|
||||
|
||||
protected:
|
||||
double a_;
|
||||
double b_;
|
||||
double c_;
|
||||
};
|
||||
|
||||
class ToNTCTemperatureFilter : public Filter {
|
||||
public:
|
||||
ToNTCTemperatureFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
double a_;
|
||||
double b_;
|
||||
double c_;
|
||||
};
|
||||
/// Steinhart-Hart conversions in double precision for the to_ntc_* filters; non-finite input gives NaN.
|
||||
float steinhart_hart_resistance(float temperature, double a, double b, double c);
|
||||
float steinhart_hart_temperature(float resistance, double a, double b, double c);
|
||||
|
||||
/** RH Temperature correction filter
|
||||
*
|
||||
|
||||
@@ -82,7 +82,6 @@ class Sensor : public EntityBase {
|
||||
*
|
||||
* sensor->add_filters({
|
||||
* LambdaFilter([&](float value) -> optional<float> { return 42/value; }),
|
||||
* OffsetFilter(1),
|
||||
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -337,6 +337,57 @@ size_t truncate_name_to(char *buffer, size_t buffer_size, const char *name, size
|
||||
|
||||
// Parsing & formatting
|
||||
|
||||
// A plain decimal converts exactly with one division: the digits accumulate
|
||||
// into an integer that stays below 2^24 and the fraction's scale is a power of
|
||||
// ten up to 1e9 (2^9 * 5^9), so both convert to float exactly and IEEE division
|
||||
// rounds the quotient correctly, the same result strtof produces.
|
||||
static constexpr uint32_t PARSE_FLOAT_MANTISSA_LIMIT = 1000000; // one more digit stays below 2^24
|
||||
static constexpr uint32_t PARSE_FLOAT_MAX_FRACTION_DIGITS = 9;
|
||||
|
||||
optional<float> parse_float(const char *str) {
|
||||
const char *p = str;
|
||||
const bool negative = *p == '-';
|
||||
if (negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *dot = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<unsigned char>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && dot == nullptr) {
|
||||
dot = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa >= PARSE_FLOAT_MANTISSA_LIMIT)
|
||||
break;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction_digits = dot != nullptr ? p - dot - 1 : 0;
|
||||
const bool any_digit = p - digits > (dot != nullptr ? 1 : 0);
|
||||
if (*p == '\0' && any_digit && fraction_digits <= PARSE_FLOAT_MAX_FRACTION_DIGITS) {
|
||||
// The mantissa is exact in a float, so the sign goes on before the one rounding step
|
||||
float value = static_cast<float>(mantissa);
|
||||
if (negative)
|
||||
value = -value;
|
||||
if (fraction_digits != 0) {
|
||||
uint32_t scale = 10;
|
||||
for (uint32_t i = 1; i < fraction_digits; i++)
|
||||
scale *= 10;
|
||||
value /= static_cast<float>(scale);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// Exponents, hex, inf, nan, leading whitespace and longer inputs take the libc path.
|
||||
char *end = nullptr;
|
||||
const float value = ::strtof(str, &end);
|
||||
if (end == str || *end != '\0' || value == HUGE_VALF)
|
||||
return {};
|
||||
return value;
|
||||
}
|
||||
|
||||
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
size_t chars = std::min(length, 2 * count);
|
||||
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
|
||||
|
||||
+2
-21
@@ -25,7 +25,6 @@
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -1034,26 +1033,6 @@ inline float pow10_int(int8_t exp) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 10^n, exact while it fits in T.
|
||||
template<typename T> constexpr T pow10_exact(uint32_t n) {
|
||||
T result = 1;
|
||||
T base = 10;
|
||||
for (;;) {
|
||||
if ((n & 1) != 0)
|
||||
result *= base;
|
||||
n >>= 1;
|
||||
if (n == 0)
|
||||
return result;
|
||||
base *= base;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether c is '0' to '9'.
|
||||
constexpr bool is_digit(char c) { return static_cast<uint8_t>(c - '0') <= 9; }
|
||||
|
||||
/// Whether c is C-locale white space, without isspace's ctype table.
|
||||
constexpr bool is_space_c(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
|
||||
|
||||
/// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out).
|
||||
template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max_out) {
|
||||
return (value - min) * (max_out - min_out) / (max - min) + min_out;
|
||||
@@ -1594,6 +1573,8 @@ template<typename T, enable_if_t<(std::is_integral<T>::value && std::is_signed<T
|
||||
optional<T> parse_number(const std::string &str) {
|
||||
return parse_number<T>(str.c_str());
|
||||
}
|
||||
/// Parse a decimal floating-point number; plain decimals skip the libc parser.
|
||||
optional<float> parse_float(const char *str);
|
||||
/// Parse a decimal floating-point number from a null-terminated string.
|
||||
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0> optional<T> parse_number(const char *str) {
|
||||
return parse_float(str);
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
// picolibc 1.8.10's strtof, bit for bit, without libc's strtod: decimals round to 9 digits, then to the nearest float.
|
||||
// Float bits are built from integers, so chips that flush denormals give the same result.
|
||||
|
||||
#include "esphome/core/parse_float.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t FLOAT_SIGN_BIT = 0x80000000, FLOAT_INF_BITS = 0x7F800000;
|
||||
constexpr uint32_t FLOAT_NAN_BITS = 0x7FC00000; // nanf(""), which strtof returns for every NaN
|
||||
|
||||
constexpr uint32_t DECIMAL_DIGITS = 9, HEX_DIGITS = 8; // digits kept before the rest only round
|
||||
constexpr int32_t EXPONENT_CAP = 224; // picolibc stops adding exponent digits once the exponent reaches this
|
||||
// 0.<digits> * 10^exp10 is zero at or below MIN_EXP10 and infinity at or above MAX_EXP10, as in picolibc
|
||||
constexpr int32_t MIN_EXP10 = -46, MAX_EXP10 = 40;
|
||||
// Past this, one more digit could pass 2^24
|
||||
constexpr uint32_t PLAIN_MANTISSA_LIMIT = 1677720;
|
||||
|
||||
// Bits of the float nearest to (m + sticky fraction) * 2^e, ties to even; m > 0
|
||||
uint32_t round_to_float_bits(uint64_t m, int32_t e, bool sticky) {
|
||||
const int32_t msb = e + static_cast<int32_t>(std::bit_width(m)) - 1; // the value is in [2^msb, 2^(msb + 1))
|
||||
if (msb >= 128)
|
||||
return FLOAT_INF_BITS;
|
||||
if (msb < -150) // below half the smallest float
|
||||
return 0;
|
||||
// The unit of the float's last bit: 2^(msb - 23), or 2^-149 for a denormal
|
||||
const int32_t shift = std::max<int32_t>(msb - 23, -149) - e; // at most 64
|
||||
uint64_t q;
|
||||
if (shift <= 0) {
|
||||
q = m << -shift;
|
||||
} else {
|
||||
q = shift < 64 ? m >> shift : 0;
|
||||
const uint64_t rem = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
|
||||
const uint64_t half = uint64_t{1} << (shift - 1);
|
||||
if (rem > half || (rem == half && (sticky || (q & 1) != 0)))
|
||||
q++;
|
||||
}
|
||||
if (msb < -126)
|
||||
return static_cast<uint32_t>(q); // 2^23 here is the smallest normal float, as it should be
|
||||
// Rounding up past the largest float gives exactly FLOAT_INF_BITS
|
||||
return (static_cast<uint32_t>(msb + 127) << 23) + static_cast<uint32_t>(q - (uint64_t{1} << 23));
|
||||
}
|
||||
|
||||
// The decimal exponents that reach decimal_to_float_bits: 1 to 9 digits and MIN_EXP10 < digits + e10 < MAX_EXP10
|
||||
constexpr int32_t MIN_E10 = MIN_EXP10 + 1 - static_cast<int32_t>(DECIMAL_DIGITS), MAX_E10 = MAX_EXP10 - 2;
|
||||
// Top 64 bits of 10^(8k), rounded down: 10^(8k) = entry * 2^(floor_log2_pow10(8k) - 63), exact for k = 0..3
|
||||
constexpr int32_t POW10_BY_8_FIRST = MIN_E10 >> 3, POW10_BY_8_LAST = MAX_E10 >> 3;
|
||||
constexpr uint64_t POW10_BY_8[] PROGMEM = {
|
||||
0xFB158592BE068D2E, 0xBB127C53B17EC159, 0x8B61313BBABCE2C6, 0xCFB11EAD453994BA,
|
||||
0x9ABE14CD44753B52, 0xE69594BEC44DE15B, 0xABCC77118461CEFC, 0x8000000000000000,
|
||||
0xBEBC200000000000, 0x8E1BC9BF04000000, 0xD3C21BCECCEDA100, 0x9DC5ADA82B70B59D,
|
||||
};
|
||||
static_assert(POW10_BY_8_FIRST == -7 && POW10_BY_8_LAST == 4 &&
|
||||
sizeof(POW10_BY_8) / sizeof(POW10_BY_8[0]) == POW10_BY_8_LAST - POW10_BY_8_FIRST + 1,
|
||||
"POW10_BY_8 must cover MIN_E10..MAX_E10");
|
||||
|
||||
// floor(q * log2(10)), for |q| < 100
|
||||
constexpr int32_t floor_log2_pow10(int32_t q) { return (q * 217706) >> 16; }
|
||||
|
||||
// The top 64 bits of a * b, rounded down
|
||||
uint64_t mul_high(uint64_t a, uint64_t b) {
|
||||
const uint64_t a_lo = static_cast<uint32_t>(a), a_hi = a >> 32;
|
||||
const uint64_t b_lo = static_cast<uint32_t>(b), b_hi = b >> 32;
|
||||
const uint64_t hi_lo = a_hi * b_lo, lo_hi = a_lo * b_hi;
|
||||
const uint64_t middle = ((a_lo * b_lo) >> 32) + static_cast<uint32_t>(hi_lo) + static_cast<uint32_t>(lo_hi);
|
||||
return a_hi * b_hi + (hi_lo >> 32) + (lo_hi >> 32) + (middle >> 32);
|
||||
}
|
||||
|
||||
// Cold: a decimal within 16 units of a midpoint is an exact tie when m10 * 5^e10 is an integer
|
||||
__attribute__((noinline, cold)) uint32_t tie_float_bits(uint32_t m10, int32_t e10, uint32_t bits) {
|
||||
if (e10 >= 0) {
|
||||
if (e10 <= 14) // m10 * 5^14 stays below 2^64
|
||||
return round_to_float_bits(m10 * (pow10_exact<uint64_t>(e10) >> e10), e10, false);
|
||||
} else if (e10 >= -13) {
|
||||
const auto pow5 = static_cast<uint32_t>(pow10_exact<uint64_t>(-e10) >> -e10);
|
||||
if (m10 % pow5 == 0)
|
||||
return round_to_float_bits(m10 / pow5, e10, false);
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
// The float nearest to m10 * 10^e10 (0 < m10 <= 10^9). The product is under 4 units low; checked for all 85 billion
|
||||
// (m10, e10), only exact ties and one non-tie (6 units off, keeps its float) come within 34 units of a midpoint.
|
||||
uint32_t decimal_to_float_bits(uint32_t m10, int32_t e10) {
|
||||
// Both operands are exact floats, so the one float operation gives the nearest float
|
||||
const uint32_t power = e10 < 0 ? -e10 : e10;
|
||||
if (m10 < (1u << 24) && power <= 10) {
|
||||
const auto value = static_cast<float>(m10);
|
||||
// 10^10 overflows uint32_t
|
||||
const float factor = power == 10 ? 1e10f : static_cast<float>(pow10_exact<uint32_t>(power));
|
||||
return bit_cast<uint32_t>(e10 < 0 ? value / factor : value * factor);
|
||||
}
|
||||
const int32_t k8 = e10 >> 3; // rounds down
|
||||
const uint64_t x = uint64_t{m10} * pow10_exact<uint32_t>(e10 & 7);
|
||||
const auto zeros = static_cast<int32_t>(std::countl_zero(x));
|
||||
uint64_t m = x << zeros;
|
||||
int32_t e = -zeros;
|
||||
if (k8 == 0)
|
||||
return round_to_float_bits(m, e, false); // exact
|
||||
m = mul_high(m, POW10_BY_8[k8 - POW10_BY_8_FIRST]);
|
||||
e += floor_log2_pow10(8 * k8) + 1;
|
||||
if ((m >> 63) == 0) {
|
||||
m <<= 1;
|
||||
e--;
|
||||
}
|
||||
const uint32_t bits = round_to_float_bits(m, e, false);
|
||||
// The bits below the float's last one, and the midpoint among them
|
||||
const int32_t shift = std::max<int32_t>(40, -149 - e);
|
||||
if (shift <= 64) {
|
||||
const uint64_t low = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
|
||||
const uint64_t half = uint64_t{1} << (shift - 1);
|
||||
if (low - half + 16 > 32) // |low - half| > 16
|
||||
return bits;
|
||||
}
|
||||
return tie_float_bits(m10, e10, bits);
|
||||
}
|
||||
|
||||
uint32_t no_number(const char *str, const char **end, bool *negative) {
|
||||
*end = str;
|
||||
*negative = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The rest of an "nan" or "inf" after its first letter at p; returns the end, or nullptr for no number
|
||||
const char *read_inf_nan(const char *p) {
|
||||
const auto lower = [](char c) { return static_cast<char>(c | 0x20); };
|
||||
if (lower(*p) == 'n') {
|
||||
if (lower(p[1]) != 'a' || lower(p[2]) != 'n')
|
||||
return nullptr;
|
||||
p += 3;
|
||||
if (*p != '(')
|
||||
return p;
|
||||
// "nan(" must be closed after letters, digits and '_'
|
||||
do {
|
||||
p++;
|
||||
} while (static_cast<uint8_t>(lower(*p) - 'a') < 26 || is_digit(*p) || *p == '_');
|
||||
return *p == ')' ? p + 1 : nullptr;
|
||||
}
|
||||
if (lower(p[1]) != 'n' || lower(p[2]) != 'f')
|
||||
return nullptr;
|
||||
p += 3;
|
||||
if (lower(*p) != 'i')
|
||||
return p;
|
||||
// Past "infi", only "infinity" is a number
|
||||
return lower(p[1]) == 'n' && lower(p[2]) == 'i' && lower(p[3]) == 't' && lower(p[4]) == 'y' ? p + 5 : nullptr;
|
||||
}
|
||||
|
||||
// Reads a binary exponent "p" or decimal exponent "e" at p into *exp; returns the end of the number
|
||||
const char *read_exponent(const char *p, char letter, int32_t *exp) {
|
||||
if ((*p | 0x20) != letter)
|
||||
return p;
|
||||
const char *q = p + 1;
|
||||
const bool negative = *q == '-';
|
||||
if (negative || *q == '+')
|
||||
q++;
|
||||
if (!is_digit(*q))
|
||||
return p;
|
||||
int32_t value = 0;
|
||||
for (; is_digit(*q); q++) {
|
||||
if (value < EXPONENT_CAP)
|
||||
value = value * 10 + (*q - '0');
|
||||
}
|
||||
*exp += negative ? -value : value;
|
||||
return q;
|
||||
}
|
||||
|
||||
// Hex digits after the "x" at p, with the point and decimal exponent read so far (no non-zero digit yet)
|
||||
__attribute__((noinline, cold)) uint32_t read_hex(const char *p, bool dot, int32_t exp, const char **end) {
|
||||
uint32_t m = 0, kept = 0; // kept: digits in m from its first non-zero one
|
||||
bool sticky = false;
|
||||
for (;; p++) {
|
||||
const char lower = static_cast<char>(*p | 0x20);
|
||||
uint32_t digit = static_cast<uint8_t>(*p - '0');
|
||||
if (static_cast<uint8_t>(lower - 'a') <= 5) {
|
||||
digit = static_cast<uint8_t>(lower - 'a') + 10;
|
||||
} else if (digit > 9) {
|
||||
digit = 16; // picolibc takes ':' to '?' as 10 to 15; we stop there like newlib
|
||||
}
|
||||
if (digit < 16) {
|
||||
if (kept < HEX_DIGITS) {
|
||||
if (dot)
|
||||
exp--;
|
||||
m = m * 16 + digit;
|
||||
kept += m != 0 ? 1 : 0;
|
||||
} else {
|
||||
if (!dot)
|
||||
exp++;
|
||||
sticky |= digit != 0;
|
||||
}
|
||||
} else if (*p == '.' && !dot) {
|
||||
dot = true;
|
||||
} else if (lower != 'x' || m != 0) { // picolibc takes another "x" while the value is still zero
|
||||
break;
|
||||
}
|
||||
}
|
||||
exp *= 4;
|
||||
*end = read_exponent(p, 'p', &exp);
|
||||
return m == 0 ? 0 : round_to_float_bits(m, exp, sticky);
|
||||
}
|
||||
|
||||
// picolibc's conv_flt for a float from the first digit or point at p: returns the bits; *end is str for no number
|
||||
__attribute__((noinline)) uint32_t read_decimal(const char *str, const char *p, const char **end) {
|
||||
uint32_t m = 0, kept = 0; // the first (up to 9) significant digits, and how many from the first non-zero one
|
||||
int32_t exp = 0; // the value is m * 10^exp
|
||||
bool dot = false, any = false;
|
||||
uint32_t overflow = 0; // digits past the kept ones: 0 none, 1 below half, 2 half, 3 above half
|
||||
bool full = false; // a digit past the kept ones was read
|
||||
for (;; p++) {
|
||||
const uint32_t digit = static_cast<uint8_t>(*p - '0');
|
||||
if (digit <= 9) {
|
||||
any = true;
|
||||
if (kept < DECIMAL_DIGITS) {
|
||||
if (dot)
|
||||
exp--;
|
||||
m = m * 10 + digit;
|
||||
kept += m != 0 ? 1 : 0;
|
||||
} else {
|
||||
if (!dot)
|
||||
exp++;
|
||||
if (!full) {
|
||||
full = true;
|
||||
overflow = digit > 5 ? 3 : digit == 5 ? 2 : digit != 0 ? 1 : 0; // the first one decides the half
|
||||
} else {
|
||||
overflow |= digit != 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
} else if (*p == '.' && !dot) {
|
||||
dot = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((*p | 0x20) == 'x' && any && m == 0)
|
||||
return read_hex(p + 1, dot, exp, end);
|
||||
if (!any) {
|
||||
*end = str;
|
||||
return 0;
|
||||
}
|
||||
*end = read_exponent(p, 'e', &exp);
|
||||
if (m == 0)
|
||||
return 0;
|
||||
// Ties to even on the decimal digits
|
||||
overflow |= m & 1;
|
||||
m += (overflow + 1) >> 2;
|
||||
const int32_t exp10 = static_cast<int32_t>(kept) + exp;
|
||||
if (exp10 <= MIN_EXP10)
|
||||
return 0;
|
||||
if (exp10 >= MAX_EXP10)
|
||||
return FLOAT_INF_BITS;
|
||||
return decimal_to_float_bits(m, exp);
|
||||
}
|
||||
|
||||
// picolibc's strtof from the start: returns the bits without the sign, which goes in *negative
|
||||
__attribute__((noinline)) uint32_t parse_float_full(const char *str, const char **end, bool *negative) {
|
||||
// No number starts with a letter but i (inf) or n (nan): "unavailable", "on" and "unknown" stop here
|
||||
const char first = static_cast<char>(*str | 0x20);
|
||||
if (first >= 'a' && first <= 'z' && first != 'i' && first != 'n')
|
||||
return no_number(str, end, negative);
|
||||
const char *p = str;
|
||||
while (is_space_c(*p))
|
||||
p++;
|
||||
*negative = *p == '-';
|
||||
if (*negative || *p == '+')
|
||||
p++;
|
||||
const char lower = static_cast<char>(*p | 0x20);
|
||||
if (lower == 'i' || lower == 'n') {
|
||||
const char *stop = read_inf_nan(p);
|
||||
if (stop == nullptr)
|
||||
return no_number(str, end, negative);
|
||||
*end = stop;
|
||||
return lower == 'i' ? FLOAT_INF_BITS : FLOAT_NAN_BITS;
|
||||
}
|
||||
const uint32_t bits = read_decimal(str, p, end);
|
||||
if (*end == str)
|
||||
*negative = false;
|
||||
return bits;
|
||||
}
|
||||
|
||||
// A plain decimal below 2^24 with up to 9 digits after the point, in one float operation; nullptr otherwise
|
||||
ESPHOME_ALWAYS_INLINE inline const char *parse_plain_decimal(const char *str, uint32_t *bits, bool *negative) {
|
||||
const char *p = str;
|
||||
*negative = *p == '-';
|
||||
if (*negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *point = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<uint8_t>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && point == nullptr) {
|
||||
point = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa > PLAIN_MANTISSA_LIMIT)
|
||||
return nullptr;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
|
||||
const char c = static_cast<char>(*p | 0x20);
|
||||
if (p - digits == (point != nullptr ? 1 : 0) || fraction > 9 || c == 'e' || c == 'x')
|
||||
return nullptr;
|
||||
float value = static_cast<float>(mantissa);
|
||||
if (fraction != 0)
|
||||
value /= static_cast<float>(pow10_exact<uint32_t>(fraction)); // 10^9 is an exact float
|
||||
*bits = bit_cast<uint32_t>(value);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
optional<float> parse_float(const char *str) {
|
||||
// Own copy of the plain-decimal loop: sharing parse_plain_decimal costs 12 to 18% more instructions
|
||||
const char *p = str;
|
||||
const bool negative = *p == '-';
|
||||
if (negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *point = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<uint8_t>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && point == nullptr) {
|
||||
point = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa > PLAIN_MANTISSA_LIMIT)
|
||||
break;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
|
||||
if (*p == '\0' && p - digits > (point != nullptr ? 1 : 0) && fraction <= 9) {
|
||||
float value = negative ? -static_cast<float>(mantissa) : static_cast<float>(mantissa);
|
||||
if (fraction != 0)
|
||||
value /= static_cast<float>(pow10_exact<uint32_t>(fraction));
|
||||
return value;
|
||||
}
|
||||
const char *end;
|
||||
bool full_negative;
|
||||
const uint32_t bits = parse_float_full(str, &end, &full_negative);
|
||||
// HUGE_VALF, positive overflow or "inf", is rejected like before
|
||||
if (end == str || *end != '\0' || (!full_negative && bits == FLOAT_INF_BITS))
|
||||
return {};
|
||||
return bit_cast<float>(full_negative ? bits | FLOAT_SIGN_BIT : bits);
|
||||
}
|
||||
|
||||
float str_to_float(const char *str, char **end) {
|
||||
const char *stop;
|
||||
bool negative;
|
||||
uint32_t bits;
|
||||
if (const char *plain_end = parse_plain_decimal(str, &bits, &negative)) {
|
||||
stop = plain_end;
|
||||
} else {
|
||||
bits = parse_float_full(str, &stop, &negative);
|
||||
}
|
||||
if (end != nullptr)
|
||||
*end = const_cast<char *>(stop); // strtof's signature
|
||||
return bit_cast<float>(negative ? bits | FLOAT_SIGN_BIT : bits);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// The whole string as str_to_float's syntax (leading space, hex, inf, nan); trailing characters, "inf" and positive
|
||||
/// overflow are rejected, while -inf, negative overflow and NaN are accepted.
|
||||
optional<float> parse_float(const char *str);
|
||||
|
||||
/// strtof with picolibc's results on every platform, but it never sets errno (overflow gives +-HUGE_VALF, underflow 0);
|
||||
/// use it instead of strtof, or strtod/atof for a float result.
|
||||
float str_to_float(const char *str, char **end);
|
||||
|
||||
} // namespace esphome
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
|
||||
#ifdef USE_JSON
|
||||
#include "esphome/components/json/json_util.h"
|
||||
@@ -294,7 +293,7 @@ inline bool operator!=(const std::string &lhs, const ProgmemStringRef &rhs) { re
|
||||
#endif
|
||||
|
||||
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
|
||||
// Must be in esphome namespace for ADL to find them. Uses strtol, strtod and str_to_float to avoid heap allocation.
|
||||
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
|
||||
namespace internal {
|
||||
// NOLINTBEGIN(google-runtime-int)
|
||||
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
|
||||
@@ -335,7 +334,7 @@ inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
|
||||
return internal::parse_number<long>(str, pos, base, std::strtol);
|
||||
}
|
||||
inline float stof(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<float>(str, pos, str_to_float);
|
||||
return internal::parse_number<float>(str, pos, std::strtof);
|
||||
}
|
||||
inline double stod(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<double>(str, pos, std::strtod);
|
||||
|
||||
@@ -51,13 +51,17 @@ static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(SensorFilter_ExponentialMovingAvg);
|
||||
|
||||
// Mirror the code generated for `offset: 1` and `multiply: 2`
|
||||
static optional<float> offset_1(float x) { return x + 1.0f; }
|
||||
static optional<float> multiply_2(float x) { return x * 2.0f; }
|
||||
|
||||
// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window)
|
||||
static void SensorFilter_Chain3(benchmark::State &state) {
|
||||
Sensor sensor;
|
||||
|
||||
sensor.add_filters({
|
||||
new OffsetFilter([]() -> float { return 1.0f; }),
|
||||
new MultiplyFilter([]() -> float { return 2.0f; }),
|
||||
new ApplyFilter<offset_1>(),
|
||||
new ApplyFilter<multiply_2>(),
|
||||
new SlidingWindowMovingAverageFilter(5, 1, 1),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
static const char *const PLAIN[] = {"0.5", "255", "-12.75", "21.3"};
|
||||
static const char *const EXPONENT[] = {"1e3", "2.5e-2", "-6.02E5", "1.5e+1"};
|
||||
static const char *const PRECISE[] = {"3.14159265358979", "0.000123456789", "123456789.125", "-0.1000000000000000055"};
|
||||
// Home Assistant states that are not numbers
|
||||
static const char *const REJECT[] = {"unavailable", "unknown", "on", "off"};
|
||||
|
||||
template<size_t N, typename F> static void run(benchmark::State &state, const char *const (&inputs)[N], F parse) {
|
||||
for (auto _ : state) {
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < kInnerIterations; i++)
|
||||
sum += parse(inputs[i % N]);
|
||||
benchmark::DoNotOptimize(sum);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
|
||||
static float parse_number_float(const char *str) { return parse_number<float>(str).value_or(0.0f); }
|
||||
|
||||
static void ParseNumberFloat_Plain(benchmark::State &state) { run(state, PLAIN, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Plain);
|
||||
|
||||
static void ParseNumberFloat_Exponent(benchmark::State &state) { run(state, EXPONENT, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Exponent);
|
||||
|
||||
static void ParseNumberFloat_Precise(benchmark::State &state) { run(state, PRECISE, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Precise);
|
||||
|
||||
static void ParseNumberFloat_Reject(benchmark::State &state) { run(state, REJECT, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Reject);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -33,6 +33,26 @@ sensor:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 1.0
|
||||
- 10.0 -> 21.0
|
||||
# Exact with one segment: two points, and collinear points that merge into one
|
||||
- platform: template
|
||||
id: exact_two_points
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: exact
|
||||
datapoints:
|
||||
- 0.0 -> 3.0
|
||||
- 10.0 -> 23.0
|
||||
- platform: template
|
||||
id: exact_collinear
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: exact
|
||||
datapoints:
|
||||
- 0.0 -> 5.0
|
||||
- 10.0 -> 35.0
|
||||
- 20.0 -> 65.0
|
||||
- platform: template
|
||||
id: polynomial
|
||||
lambda: return 1.0;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
sensor:
|
||||
# Bounds are compared as C++ floats: just beyond FLT_MAX is infinite there, so no bound
|
||||
- platform: template
|
||||
id: clamp_beyond_max
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- clamp:
|
||||
min_value: 1.25
|
||||
max_value: 3.4028236e38
|
||||
- platform: template
|
||||
id: clamp_beyond_min
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- clamp:
|
||||
min_value: -3.4028236e38
|
||||
max_value: 2.75
|
||||
ignore_out_of_range: true
|
||||
# 3.4028235e38 rounds to FLT_MAX, so it stays a bound
|
||||
- platform: template
|
||||
id: clamp_flt_max
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- clamp:
|
||||
min_value: 0.5
|
||||
max_value: 3.4028235e38
|
||||
- platform: template
|
||||
id: clamp_negative_flt_max
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- clamp:
|
||||
min_value: -3.4028235e38
|
||||
max_value: 5.5
|
||||
# Rounded by the compiler from the literal, not by Python first
|
||||
- platform: template
|
||||
id: clamp_float_midpoint
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- clamp:
|
||||
min_value: 1.0000000596046448
|
||||
ignore_out_of_range: true
|
||||
@@ -29,10 +29,14 @@ def test_calibration_tables_are_shared_progmem(
|
||||
linear = re.findall(
|
||||
r"static constexpr std::array<float, 3> (\w+)\[\] PROGMEM", main_cpp
|
||||
)
|
||||
assert len(linear) == 2 # the two exact filters share one table
|
||||
exact, least_squares = linear
|
||||
assert len(linear) == 1 # only the exact filters have a table, and they share it
|
||||
(exact,) = linear
|
||||
assert main_cpp.count(f"sensor::CalibrateLinearFilter({exact});") == 2
|
||||
assert f"sensor::CalibrateLinearFilter({least_squares});" in main_cpp
|
||||
# A single segment needs no table: its function is baked into an apply filter
|
||||
assert "return x * 2.0f + 1.0f;" in main_cpp
|
||||
# So does an exact one, from two points or from collinear points
|
||||
assert "return x * 2.0f + 3.0f;" in main_cpp
|
||||
assert "return x * 3.0f + 5.0f;" in main_cpp
|
||||
|
||||
poly = re.search(
|
||||
r"static constexpr float (\w+)\[\] PROGMEM = \{2\.0f, 1\.0f, 2\.0f\};", main_cpp
|
||||
@@ -41,4 +45,17 @@ def test_calibration_tables_are_shared_progmem(
|
||||
assert f"sensor::CalibratePolynomialFilter({poly.group(1)});" in main_cpp
|
||||
# Table names never reuse a user id.
|
||||
assert exact != "sensor_calibrate_linear"
|
||||
assert "sensor_calibratelinearfilter_id_data" not in (exact, least_squares)
|
||||
assert exact != "sensor_calibratelinearfilter_id_data"
|
||||
|
||||
|
||||
def test_clamp_bounds_are_compared_as_float(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""A bound beyond float range is no bound; one that rounds to FLT_MAX stays a bound."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_clamp.yaml")
|
||||
|
||||
assert "clamp_filter_value(x, 1.25f, NAN, false);" in main_cpp
|
||||
assert "clamp_filter_value(x, NAN, 2.75f, true);" in main_cpp
|
||||
assert "clamp_filter_value(x, 0.5f, 3.4028235e+38f, false);" in main_cpp
|
||||
assert "clamp_filter_value(x, -3.4028235e+38f, 5.5f, false);" in main_cpp
|
||||
assert "clamp_filter_value(x, 1.0000000596046448f, NAN, true);" in main_cpp
|
||||
@@ -212,9 +212,11 @@ TEST_F(ATM90E32ThresholdRegisters, RefreshesThresholdsAfterRestoringAndClearingS
|
||||
}
|
||||
#endif // USE_HOST
|
||||
|
||||
static optional<float> double_value(float value) { return value * 2.0f; }
|
||||
|
||||
TEST_F(ATM90E32ThresholdRegisters, CombinesNativeCurrentWithOtherPhaseAlarms) {
|
||||
sensor::Sensor current;
|
||||
sensor::MultiplyFilter multiply([]() { return 2.0f; });
|
||||
sensor::ApplyFilter<double_value> multiply;
|
||||
current.add_filter(&multiply);
|
||||
current.publish_state(6.0f);
|
||||
ASSERT_FLOAT_EQ(current.state, 12.0f);
|
||||
|
||||
@@ -1,74 +1,34 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static uint32_t bits(float value) { return bit_cast<uint32_t>(value); }
|
||||
|
||||
// picolibc rounds a decimal to 9 significant digits and converts those to the nearest float, so up to 9
|
||||
// significant digits its result is the correctly rounded float the host's strtof (glibc, macOS) gives. Longer
|
||||
// input and the syntax where libcs differ are checked against values taken from picolibc 1.8.10's own strtof.
|
||||
static float reference_strtof(const char *str, char **end) {
|
||||
const float value = ::strtof(str, end);
|
||||
if (std::isnan(value))
|
||||
return std::signbit(value) ? -NAN : NAN;
|
||||
return value;
|
||||
static uint32_t bits(float value) {
|
||||
uint32_t out;
|
||||
memcpy(&out, &value, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every accepted input must give the reference's bits.
|
||||
// strtof is the reference: every accepted input must give the same bits.
|
||||
static void expect_matches_strtof(const char *str) {
|
||||
const float expected = reference_strtof(str, nullptr);
|
||||
char *end = nullptr;
|
||||
const float expected = ::strtof(str, &end);
|
||||
const auto parsed = parse_number<float>(str);
|
||||
if (!parsed.has_value()) {
|
||||
ADD_FAILURE() << str << " was rejected";
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(bits(parsed.value()), bits(expected)) << str;
|
||||
const float actual = parsed.value();
|
||||
EXPECT_EQ(bits(actual), bits(expected)) << str << " parsed " << actual << " expected " << expected;
|
||||
}
|
||||
|
||||
// The same bits and end as the host's strtof; for decimals of up to 9 significant digits
|
||||
static void expect_str_to_float_matches(const char *str) {
|
||||
char *expected_end = nullptr;
|
||||
const float expected = reference_strtof(str, &expected_end);
|
||||
char *end = nullptr;
|
||||
const float actual = str_to_float(str, &end);
|
||||
EXPECT_EQ(bits(actual), bits(expected)) << '"' << str << '"';
|
||||
EXPECT_EQ(end - str, expected_end - str) << '"' << str << '"';
|
||||
}
|
||||
|
||||
struct PicolibcCase {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
int32_t end;
|
||||
};
|
||||
|
||||
// Bits and end from picolibc 1.8.10's strtof
|
||||
static void expect_picolibc(const PicolibcCase &c) {
|
||||
char *end = nullptr;
|
||||
const float value = str_to_float(c.str, &end);
|
||||
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
|
||||
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
|
||||
}
|
||||
|
||||
static void expect_picolibc(const std::string &str, uint32_t expected) {
|
||||
char *end = nullptr;
|
||||
EXPECT_EQ(bits(str_to_float(str.c_str(), &end)), expected) << str.substr(0, 40);
|
||||
EXPECT_EQ(static_cast<size_t>(end - str.c_str()), str.size()) << str.substr(0, 40);
|
||||
}
|
||||
|
||||
static float float_from(uint32_t value) { return bit_cast<float>(value); }
|
||||
|
||||
TEST(ParseFloat, PlainDecimalsMatchStrtof) {
|
||||
for (const char *str :
|
||||
{"0", "-0", "+0", "1", "-1", "100.50", "99.75", "0.1", "0.3",
|
||||
@@ -86,14 +46,6 @@ TEST(ParseFloat, SweepOfTwoDecimalValuesMatchesStrtof) {
|
||||
}
|
||||
}
|
||||
|
||||
// Eight-digit mantissas below 2^24 stay on the one-division path; the next digit past the limit falls back
|
||||
TEST(ParseFloat, EightDigitPlainDecimalsMatchStrtof) {
|
||||
for (const char *str : {"12345678", "-12345678", "1234.5678", "0.12345678", "16777209", "1677720.9", "0.16777209",
|
||||
"16777210", "16777215", "1677721.5", "99999999", "9999999.9", "16777209."}) {
|
||||
expect_matches_strtof(str);
|
||||
}
|
||||
}
|
||||
|
||||
// A fraction longer than the fast path accepts must fall back, however long it is.
|
||||
TEST(ParseFloat, LongFractionsMatchStrtof) {
|
||||
std::string str = "0.";
|
||||
@@ -116,293 +68,10 @@ TEST(ParseFloat, LibcPathInputsMatchStrtof) {
|
||||
// Positive overflow and a positive literal infinity both come back as HUGE_VALF from strtof, and the parser
|
||||
// rejects both; the negative forms pass through, as they did before the fast path.
|
||||
TEST(ParseFloat, RejectsInvalidInput) {
|
||||
for (const char *str : {"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf",
|
||||
"infinity", "infi", "nan(12 ab)", "nan(12"}) {
|
||||
for (const char *str :
|
||||
{"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf", "infinity"}) {
|
||||
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
|
||||
}
|
||||
}
|
||||
|
||||
// Above the midpoint 1 + 2^-24 by less than half a double: newlib's strtof rounds to the midpoint and then to
|
||||
// 1.0f; picolibc's 9 digits, 1.00000006, give the next float.
|
||||
TEST(ParseFloat, FullPathRoundsLikePicolibc) {
|
||||
EXPECT_EQ(bits(parse_number<float>("1.0000000596046448").value()), 0x3F800001u);
|
||||
EXPECT_EQ(bits(parse_number<float>("-1.0000000596046448").value()), 0xBF800001u);
|
||||
}
|
||||
|
||||
// picolibc takes "nan(" with letters, digits and '_' up to a ")", and nothing at all otherwise
|
||||
TEST(ParseFloat, NanForms) {
|
||||
EXPECT_TRUE(std::isnan(parse_number<float>("nan(12)").value()));
|
||||
EXPECT_TRUE(std::isnan(parse_number<float>("nan(xyz_1)").value()));
|
||||
EXPECT_FALSE(parse_number<float>("nan(x y)").has_value());
|
||||
}
|
||||
|
||||
// The callers' overflow check: a positive overflow is HUGE_VALF and rejected, a negative one is -inf and accepted
|
||||
TEST(ParseFloat, OverflowIsHugeValf) {
|
||||
EXPECT_FALSE(parse_number<float>("1e39").has_value());
|
||||
EXPECT_FALSE(parse_number<float>("3.40282357e38").has_value());
|
||||
EXPECT_FALSE(parse_number<float>("1e99999").has_value());
|
||||
EXPECT_EQ(bits(parse_number<float>("-1e39").value()), 0xFF800000u);
|
||||
EXPECT_EQ(bits(parse_number<float>("3.4028235e38").value()), 0x7F7FFFFFu);
|
||||
}
|
||||
|
||||
// Up to 9 significant digits, with leading and trailing zeros, a point anywhere and an exponent
|
||||
TEST(StrToFloat, RandomDecimalsMatchStrtof) {
|
||||
std::mt19937_64 rng(1); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
|
||||
for (int i = 0; i < 50000; i++) {
|
||||
std::string str;
|
||||
if (rng() % 3 == 0)
|
||||
str += (rng() & 1) ? "-" : "+";
|
||||
const int zeros = static_cast<int>(rng() % 4);
|
||||
const int significant = 1 + static_cast<int>(rng() % 9);
|
||||
const int trailing = static_cast<int>(rng() % 4);
|
||||
std::string digits(zeros, '0');
|
||||
digits += static_cast<char>('1' + rng() % 9);
|
||||
for (int j = 1; j < significant; j++)
|
||||
digits += static_cast<char>('0' + rng() % 10);
|
||||
digits.append(trailing, '0');
|
||||
const int point = static_cast<int>(rng() % (digits.size() + 2)) - 1;
|
||||
for (int j = 0; j < static_cast<int>(digits.size()); j++) {
|
||||
if (j == point)
|
||||
str += '.';
|
||||
str += digits[j];
|
||||
}
|
||||
if (rng() & 1) {
|
||||
str += (rng() & 1) ? 'e' : 'E';
|
||||
char exp[8];
|
||||
snprintf(exp, sizeof(exp), "%d", static_cast<int>(rng() % 110) - 60);
|
||||
str += exp;
|
||||
}
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Every 16411th float, printed with 6 and 9 digits
|
||||
TEST(StrToFloat, FloatsMatchStrtof) {
|
||||
char buf[64];
|
||||
for (uint32_t u = 0; u < 0x7F800000; u += 16411) {
|
||||
for (int precision : {6, 9}) {
|
||||
snprintf(buf, sizeof(buf), "%.*e", precision - 1, static_cast<double>(float_from(u)));
|
||||
expect_str_to_float_matches(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StrToFloat, SyntaxMatchesStrtof) {
|
||||
// Tokens whose combinations the libcs read alike: no "x", "nan(" or "infi"
|
||||
static const char *const TOKENS[] = {"0", "1", "9", ".", "e", "E", "+", "-", "a", "N", " ", "\t", "y", "00", "#", ""};
|
||||
for (const char *a : TOKENS) {
|
||||
for (const char *b : TOKENS) {
|
||||
for (const char *c : TOKENS) {
|
||||
for (const char *d : TOKENS) {
|
||||
const std::string str = std::string(a) + b + c + d;
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Where newlib and glibc read the text differently from picolibc
|
||||
TEST(StrToFloat, SyntaxMatchesPicolibc) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
// "x" after any zero digits starts hex, even after a point or a second "0x"
|
||||
{"000x", 0x00000000, 4},
|
||||
{"0x", 0x00000000, 2},
|
||||
{"-0x", 0x80000000, 3},
|
||||
{"00x1", 0x3F800000, 4},
|
||||
{"0x0x1", 0x3F800000, 5},
|
||||
{"0.0x1", 0x3B800000, 5},
|
||||
{".x1", 0x00000000, 0},
|
||||
{"0x.p1", 0x00000000, 5},
|
||||
{"0x.8", 0x3F000000, 4},
|
||||
{"0X1.8", 0x3FC00000, 5},
|
||||
{"0x1P-1", 0x3F000000, 6},
|
||||
{"0x1p+", 0x3F800000, 3},
|
||||
{"1x5", 0x3F800000, 1},
|
||||
// "inf" stops before anything but "i"; past "infi" only "infinity" is a number
|
||||
{"inf", 0x7F800000, 3},
|
||||
{"-inf", 0xFF800000, 4},
|
||||
{"INFINITY", 0x7F800000, 8},
|
||||
{"+infi0", 0x00000000, 0},
|
||||
{"infi", 0x00000000, 0},
|
||||
{"infinit", 0x00000000, 0},
|
||||
// "nan(" takes letters, digits and '_' up to ")", and is no number without the ")"
|
||||
{"NaN", 0x7FC00000, 3},
|
||||
{"nan()", 0x7FC00000, 5},
|
||||
{"nan(xyz)", 0x7FC00000, 8},
|
||||
{"nan(_a1)", 0x7FC00000, 8},
|
||||
{"-nan(1)", 0xFFC00000, 7},
|
||||
{"nan( )", 0x00000000, 0},
|
||||
{"nan(12 ab)", 0x00000000, 0},
|
||||
{"nan(12", 0x00000000, 0},
|
||||
// An exponent needs a digit
|
||||
{"1e", 0x3F800000, 1},
|
||||
{"1e+", 0x3F800000, 1},
|
||||
{"1e-x", 0x3F800000, 1},
|
||||
{"\t 12", 0x41400000, 4},
|
||||
{"- 1", 0x00000000, 0},
|
||||
{"1.2.3", 0x3F99999A, 3},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// The inputs where newlib's strtof (decimal to double to float) and picolibc's were found to differ; every platform
|
||||
// now gives picolibc's result, which is also the correctly rounded float for these
|
||||
TEST(StrToFloat, RoundsLikePicolibcWhereNewlibDoubleRounds) {
|
||||
expect_picolibc({"1.0000000596046448", 0x3F800001, 18});
|
||||
expect_picolibc({"0x1.100b0d0000000c", 0x3F880587, 18});
|
||||
expect_picolibc("1.000000059604644775390625" + std::string(800, '0') + "1", 0x3F800001);
|
||||
}
|
||||
|
||||
// picolibc rounds a decimal to 9 significant digits, ties to even, before converting it: past 9 digits its float can
|
||||
// be one away from the nearest, and an exact tie between two floats can round up. Values from picolibc.
|
||||
TEST(StrToFloat, NineDigitRoundingLikePicolibc) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"1.0856183295439e10", 0x5021C50E, 18}, // nearest 0x5021C50D
|
||||
{"3.97496754550e-4", 0x39D0671D, 16}, // nearest 0x39D0671C
|
||||
{"9.76178673657108930e23", 0x674EB6C1, 22}, // nearest 0x674EB6C0
|
||||
{"156.6562236e-26", 0x17F269ED, 15}, // nearest 0x17F269EC
|
||||
{"1.000000059604644775390625", 0x3F800001, 26}, // the tie 1 + 2^-24: nearest-even 0x3F800000
|
||||
{"1.000000059", 0x3F800001, 11}, // 1.00000006 after rounding
|
||||
{"7.0064923216240854e-46", 0x00000000, 22}, // 7.00649232e-46, below half the smallest float
|
||||
{"21.299999999999997", 0x41AA6666, 18},
|
||||
{"0.30000000000000004", 0x3E99999A, 19},
|
||||
{"0.1000000000000000055511151231257827", 0x3DCCCCCD, 36},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// Nine digits converted exactly: ties to even between floats, where the 64-bit product cannot decide alone
|
||||
TEST(StrToFloat, NineDigitTiesGoToEven) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"16777217", 0x4B800000, 8}, {"16777219", 0x4B800002, 8}, {"16777217.0", 0x4B800000, 10},
|
||||
{"8388608.5", 0x4B000000, 9}, {"8388609.5", 0x4B000002, 9}, {"3e10", 0x50DF8476, 4},
|
||||
{"5e10", 0x513A43B7, 4}, {"1.00000006", 0x3F800001, 10},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// RP2350 flushes float denormals in its double to float conversion; #22677 bench. The parser builds the bits
|
||||
// with integer operations, so every denormal parses to its own value.
|
||||
TEST(StrToFloat, DenormalsParseWithoutFlushing) {
|
||||
char buf[64];
|
||||
const auto check = [&buf](uint32_t k) {
|
||||
snprintf(buf, sizeof(buf), "%.8e", std::ldexp(static_cast<double>(k), -149));
|
||||
EXPECT_EQ(bits(str_to_float(buf, nullptr)), k) << buf;
|
||||
};
|
||||
for (uint32_t k = 0; k <= 0x800000; k += 4099)
|
||||
check(k);
|
||||
// The largest denormal and FLT_MIN, where rounding carries into the exponent
|
||||
for (uint32_t k : {1u, 2u, 0x7FFFFEu, 0x7FFFFFu, 0x800000u})
|
||||
check(k);
|
||||
}
|
||||
|
||||
// Pico W and Pico 2 W bench, #22677: the RP2040 and RP2350 libcs flush float denormals to 0 in their double to
|
||||
// float conversion, and the RP2040 one turns a NaN into +infinity
|
||||
TEST(StrToFloat, Rp2040LibcFlushesAndNanToInfinity) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"1e-40", 0x000116C2, 5}, {"1.4e-45", 0x00000001, 7}, {"1e-38", 0x006CE3EE, 5},
|
||||
{"1.1754942e-38", 0x007FFFFF, 13}, {"0x1p-149", 0x00000001, 8}, {"1.17549435e-38", 0x00800000, 14},
|
||||
{"nan(12)", 0x7FC00000, 7}, {"nan", 0x7FC00000, 3},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// RP2350 bench: libc returned 0 and the first parser stalled stepping up from a flushed guess; #22677 bench
|
||||
TEST(StrToFloat, DenormalsAndFltMinMatchStrtof) {
|
||||
for (const char *str : {"1.4e-45", "-1.4e-45", "1e-40", "1e-38", "1.17549435e-38", "1.1754942e-38", "1.17549421e-38",
|
||||
"1.1754945e-38", "7.0064923e-46", "7.0064924e-46"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
}
|
||||
}
|
||||
|
||||
// Home Assistant's non-numeric states; CodSpeed HomeassistantSensor_StateResponse_Unavailable on #22677
|
||||
TEST(StrToFloat, NonNumericStatesAreRejected) {
|
||||
for (const char *str : {"unavailable", "unknown", "on", "off", "", "-", ".", "none", "inactive"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
|
||||
}
|
||||
}
|
||||
|
||||
// The one float operation path takes up to 2^24 - 1 and 10^10; #22677 review
|
||||
TEST(StrToFloat, PlainPathBoundaries) {
|
||||
for (const char *top : {"16777215", "16777216", "16777217"}) {
|
||||
for (const char *exp : {"e10", "e-10", "e11", "e-11", "e9", "e-9", ".5", ".25"}) {
|
||||
const std::string str = std::string(top) + exp;
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The midpoint between FLT_MAX and 2^128 rounds to infinity; review
|
||||
TEST(StrToFloat, OverflowMidpoint) {
|
||||
expect_picolibc({"340282356779733661637539395458142568448", 0x7F800000, 39});
|
||||
expect_picolibc({"3.40282356e38", 0x7F7FFFFF, 13});
|
||||
expect_picolibc({"3.40282357e38", 0x7F800000, 13});
|
||||
EXPECT_EQ(bits(str_to_float("0x1.ffffffp127", nullptr)), 0x7F800000u);
|
||||
}
|
||||
|
||||
// picolibc stops adding exponent digits at 224, so the digits of the mantissa can move it no further
|
||||
TEST(StrToFloat, HugeExponents) {
|
||||
for (const char *str : {"1e-99999", "1e99999", "-1e99999", "0e99999", "1e9999999999999999999",
|
||||
"1e-9999999999999999999", "1e-999999999999", "0.000e99999", "1e224", "1e2240", "1e-2240"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
}
|
||||
// 10^-3000 * 10^3000 is 1, but the exponent counts only as 300
|
||||
expect_picolibc("0." + std::string(2999, '0') + "1e3000", 0x00000000);
|
||||
expect_picolibc("1" + std::string(300, '0') + "e-300", 0x3F800000);
|
||||
}
|
||||
|
||||
TEST(StrToFloat, LongMantissas) {
|
||||
expect_picolibc("1" + std::string(799, '0') + "e-799", 0x3F800000);
|
||||
expect_picolibc("1." + std::string(3000, '0') + "1e-3001", 0x00000000);
|
||||
expect_picolibc("0." + std::string(400, '0') + "34028235" + std::string(500, '7') + "e440", 0x7F800000);
|
||||
expect_picolibc(std::string(300, '9') + "e-261", 0x7F800000);
|
||||
expect_picolibc("3" + std::string(799, '7') + "e-761", 0x7F800000);
|
||||
}
|
||||
|
||||
// picolibc takes ':' to '?' as hex digits 10 to 15 ("0x1:" is 26); these stop at them, as newlib does. #22677 review
|
||||
TEST(StrToFloat, HexStopsAtPunctuationAfterNine) {
|
||||
struct Case {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
int32_t end;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{"0x1:", 0x3F800000, 3}, {"0x1;", 0x3F800000, 3}, {"0x1<", 0x3F800000, 3}, {"0x1=", 0x3F800000, 3},
|
||||
{"0x1>", 0x3F800000, 3}, {"0x1?", 0x3F800000, 3}, {"0x1.:", 0x3F800000, 4}, {"0x:", 0x00000000, 2},
|
||||
{"0x9?", 0x41100000, 3}, {"0xA:", 0x41200000, 3},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
char *end = nullptr;
|
||||
const float value = str_to_float(c.str, &end);
|
||||
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
|
||||
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
|
||||
EXPECT_FALSE(parse_number<float>(c.str).has_value()) << c.str;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StrToFloat, HexDenormalsAndOverflow) {
|
||||
struct Case {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
};
|
||||
static const Case CASES[] = {
|
||||
{"0x1p-149", 0x00000001}, {"0x1p-150", 0x00000000},
|
||||
{"0x1.8p-149", 0x00000002}, {"0x1.fffffcp-127", 0x007FFFFF},
|
||||
{"0x1.fffffep-127", 0x00800000}, {"-0x1p-149", 0x80000001},
|
||||
{"0x1p128", 0x7F800000}, {"0x1.fffffep127", 0x7F7FFFFF},
|
||||
{"0x1.fffffefp127", 0x7F7FFFFF}, {"0x1.0000000000001p-150", 0x00000001},
|
||||
{"0x1.000001p0", 0x3F800000}, {"0x1.000003p0", 0x3F800002},
|
||||
{"0x1p-99999999999", 0x00000000}, {"0x1p99999999999", 0x7F800000},
|
||||
};
|
||||
for (const Case &c : CASES)
|
||||
EXPECT_EQ(bits(str_to_float(c.str, nullptr)), c.bits) << c.str;
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
#endif // USE_HOST
|
||||
Reference in new issue
Block a user