[sensor] Share calibrate_linear and calibrate_polynomial tables in flash (#20074)

This commit is contained in:
J. Nick Koston
2026-10-12 13:39:38 -10:00
committed by GitHub
parent 331d12af2f
commit b66cde0270
5 changed files with 110 additions and 35 deletions
+12 -3
View File
@@ -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):
+10 -8
View File
@@ -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;
+10 -24
View File
@@ -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)