mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[sensor] Share calibrate_linear and calibrate_polynomial tables in flash (#20074)
This commit is contained in:
@@ -806,6 +806,9 @@ def validate_calibrate_linear(config):
|
||||
return config
|
||||
|
||||
|
||||
LINEAR_SEGMENT = cg.std_ns.class_("array").template(cg.float_, 3)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"calibrate_linear",
|
||||
CalibrateLinearFilter,
|
||||
@@ -832,9 +835,11 @@ async def calibrate_linear_filter_to_code(config, filter_id):
|
||||
linear_functions = [[k, b, float("NaN")]]
|
||||
elif config[CONF_METHOD] == "exact":
|
||||
linear_functions = map_linear(x, y)
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(linear_functions)), linear_functions
|
||||
# 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
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, table)
|
||||
|
||||
|
||||
CONF_DEGREE = "degree"
|
||||
@@ -872,7 +877,11 @@ async def calibrate_polynomial_filter_to_code(config, filter_id):
|
||||
# Column vector
|
||||
b = [[v] for v in y]
|
||||
res = [v[0] for v in _lstsq(a, b)]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(res)), res)
|
||||
# The first entry holds the coefficient count, so the filter needs no count field.
|
||||
table = cg.shared_progmem_array(
|
||||
"sensor_calibrate_polynomial", cg.float_, [float(len(res)), *res]
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, table)
|
||||
|
||||
|
||||
def validate_clamp(config):
|
||||
|
||||
@@ -415,19 +415,21 @@ void HeartbeatFilter::initialize(Sensor *parent, Filter *next) {
|
||||
});
|
||||
}
|
||||
|
||||
optional<float> calibrate_linear_compute(const std::array<float, 3> *functions, size_t count, float value) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (!std::isfinite(functions[i][2]) || value < functions[i][2])
|
||||
return (value * functions[i][0]) + functions[i][1];
|
||||
// The calibration tables hold only floats, so aligned 32-bit loads from PROGMEM are safe on ESP8266.
|
||||
optional<float> CalibrateLinearFilter::new_value(float value) {
|
||||
// Always returns: codegen ends the table with a segment whose boundary is NaN.
|
||||
for (const auto *function = this->functions_;; function++) {
|
||||
if (!std::isfinite((*function)[2]) || value < (*function)[2])
|
||||
return (value * (*function)[0]) + (*function)[1];
|
||||
}
|
||||
return NAN;
|
||||
}
|
||||
|
||||
optional<float> calibrate_polynomial_compute(const float *coefficients, size_t count, float value) {
|
||||
optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||
float res = 0.0f;
|
||||
float x = 1.0f;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
res += x * coefficients[i];
|
||||
const auto count = static_cast<size_t>(this->coefficients_[0]);
|
||||
for (size_t i = 1; i <= count; i++) {
|
||||
res += x * this->coefficients_[i];
|
||||
x *= value;
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -579,38 +579,24 @@ template<size_t N> class OrFilter : public Filter {
|
||||
bool has_value_{false};
|
||||
};
|
||||
|
||||
/// Non-template helper for linear calibration (implementation in filter.cpp)
|
||||
optional<float> calibrate_linear_compute(const std::array<float, 3> *functions, size_t count, float value);
|
||||
|
||||
/// N is set by code generation to match the exact number of calibration segments.
|
||||
template<size_t N> class CalibrateLinearFilter : public Filter {
|
||||
/// Piecewise linear calibration; the segments are a PROGMEM table shared by filters with the same data.
|
||||
class CalibrateLinearFilter : public Filter {
|
||||
public:
|
||||
explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions) {
|
||||
init_array_from(this->linear_functions_, linear_functions);
|
||||
}
|
||||
optional<float> new_value(float value) override {
|
||||
return calibrate_linear_compute(this->linear_functions_.data(), N, value);
|
||||
}
|
||||
explicit CalibrateLinearFilter(const std::array<float, 3> *functions) : functions_(functions) {}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
std::array<std::array<float, 3>, N> linear_functions_{};
|
||||
const std::array<float, 3> *functions_; // Last segment has a NaN boundary, which ends the table
|
||||
};
|
||||
|
||||
/// Non-template helper for polynomial calibration (implementation in filter.cpp)
|
||||
optional<float> calibrate_polynomial_compute(const float *coefficients, size_t count, float value);
|
||||
|
||||
/// N is set by code generation to match the exact number of polynomial coefficients.
|
||||
template<size_t N> class CalibratePolynomialFilter : public Filter {
|
||||
/// Polynomial calibration; the coefficients are a PROGMEM table shared by filters with the same data.
|
||||
class CalibratePolynomialFilter : public Filter {
|
||||
public:
|
||||
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients) {
|
||||
init_array_from(this->coefficients_, coefficients);
|
||||
}
|
||||
optional<float> new_value(float value) override {
|
||||
return calibrate_polynomial_compute(this->coefficients_.data(), N, value);
|
||||
}
|
||||
explicit CalibratePolynomialFilter(const float *coefficients) : coefficients_(coefficients) {}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
std::array<float, N> coefficients_{};
|
||||
const float *coefficients_; // [count, c0, c1, ...]
|
||||
};
|
||||
|
||||
class ClampFilter : public Filter {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: exact_a
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: exact
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 10.0 -> 100.0
|
||||
- 20.0 -> 150.0
|
||||
- platform: template
|
||||
id: exact_b
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: exact
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 10.0 -> 100.0
|
||||
- 20.0 -> 150.0
|
||||
- platform: template
|
||||
id: least_squares
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 1.0
|
||||
- 10.0 -> 21.0
|
||||
- platform: template
|
||||
id: polynomial
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 1
|
||||
datapoints:
|
||||
- 0.0 -> 1.0
|
||||
- 10.0 -> 21.0
|
||||
# User ids equal to the old derived table name and to the helper's base name.
|
||||
- platform: template
|
||||
id: sensor_calibratelinearfilter_id_data
|
||||
lambda: return 1.0;
|
||||
- platform: template
|
||||
id: sensor_calibrate_linear
|
||||
lambda: return 1.0;
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Tests for the sensor component."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
from tests.component_tests.helpers import extract_packed_value
|
||||
|
||||
|
||||
@@ -15,3 +18,27 @@ def test_sensor_device_class_set(generate_main):
|
||||
# Then: device_class: voltage means packed value must be non-zero
|
||||
packed = extract_packed_value(main_cpp, "s_1")
|
||||
assert packed != 0
|
||||
|
||||
|
||||
def test_calibration_tables_are_shared_progmem(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Calibration data is a PROGMEM table, shared by filters with identical data."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_calibrate.yaml")
|
||||
|
||||
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 main_cpp.count(f"sensor::CalibrateLinearFilter({exact});") == 2
|
||||
assert f"sensor::CalibrateLinearFilter({least_squares});" in main_cpp
|
||||
|
||||
poly = re.search(
|
||||
r"static constexpr float (\w+)\[\] PROGMEM = \{2\.0f, 1\.0f, 2\.0f\};", main_cpp
|
||||
)
|
||||
assert poly is not None
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user