LCOV - code coverage report
Current view: top level - src/AH/Hardware - FilteredAnalog.hpp (source / functions) Coverage Total Hit
Test: 73449d9b107c772cf65493691543348214e5d5eb Lines: 100.0 % 39 39
Test Date: 2026-06-06 17:44:35 Functions: 100.0 % 51 51
Legend: Lines:     hit not hit

            Line data    Source code
       1              : #pragma once
       2              : 
       3              : #include <AH/Filters/EMA.hpp>
       4              : #include <AH/Filters/Hysteresis.hpp>
       5              : #include <AH/Hardware/ExtendedInputOutput/ExtendedInputOutput.hpp>
       6              : #include <AH/Hardware/Hardware-Types.hpp>
       7              : #include <AH/Math/IncreaseBitDepth.hpp>
       8              : #include <AH/Math/MinMaxFix.hpp>
       9              : #include <AH/STL/type_traits> // std::enable_if, std::is_constructible
      10              : #include <AH/STL/utility> // std::forward
      11              : #include <AH/Settings/SettingsWrapper.hpp>
      12              : 
      13              : BEGIN_AH_NAMESPACE
      14              : 
      15              : /**
      16              :  * @brief   Helper to determine how many of the remaining bits of the filter 
      17              :  *          data types can be used to achieve higher precision.
      18              :  */
      19              : template <uint8_t FilterShiftFactor, class FilterType, class AnalogType>
      20              : struct MaximumFilteredAnalogIncRes {
      21              :     constexpr static uint8_t value =
      22              :         min(sizeof(FilterType) * CHAR_BIT - ADC_BITS - FilterShiftFactor,
      23              :             sizeof(AnalogType) * CHAR_BIT - ADC_BITS);
      24              : };
      25              : 
      26              : /**
      27              :  * @brief   FilteredAnalog base class with generic MappingFunction.
      28              :  * 
      29              :  * @see FilteredAnalog
      30              :  */
      31              : template <class MappingFunction, uint8_t Precision = 10,
      32              :           uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
      33              :           class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
      34              :           uint8_t IncRes = MaximumFilteredAnalogIncRes<
      35              :               FilterShiftFactor, FilterType, AnalogType>::value>
      36              : class GenericFilteredAnalog {
      37              :   public:
      38              :     /**
      39              :      * @brief   Construct a new GenericFilteredAnalog object.
      40              :      *
      41              :      * @param   analogPin
      42              :      *          The analog pin to read from.
      43              :      * @param   mapFn
      44              :      *          The mapping function
      45              :      * @param   initial
      46              :      *          The initial value of the filter.
      47              :      */
      48           21 :     GenericFilteredAnalog(pin_t analogPin, MappingFunction mapFn,
      49              :                           AnalogType initial = 0)
      50           21 :         : analogPin(analogPin), mapFn(std::forward<MappingFunction>(mapFn)),
      51           21 :           filter(increaseBitDepth<ADC_BITS + IncRes, Precision, AnalogType,
      52           21 :                                   AnalogType>(initial)) {}
      53              : 
      54              :     /// @copydoc GenericFilteredAnalog::GenericFilteredAnalog(pin_t,MappingFunction,AnalogType)
      55              :     GenericFilteredAnalog(ArduinoPin_t analogPin, MappingFunction mapFn,
      56              :                           AnalogType initial = 0)
      57              :         : GenericFilteredAnalog(pin_t(analogPin),
      58              :                                 std::forward<MappingFunction>(mapFn), initial) {
      59              :     }
      60              : 
      61              :     /**
      62              :      * @brief   Reset the filter to the given value.
      63              :      * 
      64              :      * @param   value 
      65              :      *          The value to reset the filter state to.
      66              :      * 
      67              :      * @todo    Should the filter be initialized to the first value that is read
      68              :      *          instead of to zero? This would require adding a `begin` method.
      69              :      */
      70            1 :     void reset(AnalogType value = 0) {
      71              :         AnalogType widevalue = increaseBitDepth<ADC_BITS + IncRes, Precision,
      72            1 :                                                 AnalogType, AnalogType>(value);
      73            1 :         filter.reset(widevalue);
      74            1 :         hysteresis.setValue(widevalue);
      75            1 :     }
      76              : 
      77              :     /**
      78              :      * @brief   Reset the filtered value to the value that's currently being
      79              :      *          measured at the analog input.
      80              :      * 
      81              :      * This is useful to avoid transient effects upon initialization.
      82              :      */
      83            7 :     void resetToCurrentValue() {
      84            7 :         AnalogType widevalue = getRawValue();
      85            7 :         filter.reset(widevalue);
      86            7 :         hysteresis.setValue(widevalue);
      87            7 :     }
      88              : 
      89              :     /**
      90              :      * @brief   Specify a mapping function/functor that is applied to the analog
      91              :      *          value after filtering and before applying hysteresis.
      92              :      *
      93              :      * @param   fn
      94              :      *          This functor should have a call operator that takes the filtered
      95              :      *          value (of ADC_BITS + IncRes bits wide) as a parameter, 
      96              :      *          and returns a value of ADC_BITS + IncRes bits wide.
      97              :      * 
      98              :      * @note    Applying the mapping function before filtering could result in
      99              :      *          the noise being amplified to such an extent that filtering it
     100              :      *          afterwards would be ineffective.  
     101              :      *          Applying it after hysteresis would result in a lower resolution.  
     102              :      *          That's why the mapping function is applied after filtering and
     103              :      *          before hysteresis.
     104              :      */
     105            7 :     void map(MappingFunction fn) { mapFn = std::forward<MappingFunction>(fn); }
     106              : 
     107              :     /**
     108              :      * @brief   Get a reference to the mapping function.
     109              :      */
     110              :     MappingFunction &getMappingFunction() { return mapFn; }
     111              :     /**
     112              :      * @brief   Get a reference to the mapping function.
     113              :      */
     114              :     const MappingFunction &getMappingFunction() const { return mapFn; }
     115              : 
     116              :     /**
     117              :      * @brief   Read the analog input value, apply the mapping function, and
     118              :      *          update the average.
     119              :      *
     120              :      * @retval  true
     121              :      *          The value changed since last time it was updated.
     122              :      * @retval  false
     123              :      *          The value is still the same.
     124              :      */
     125           42 :     bool update() {
     126           42 :         AnalogType input = getRawValue(); // read the raw analog input value
     127           42 :         input = filter.filter(input);     // apply a low-pass EMA filter
     128           42 :         input = mapFnHelper(input);       // apply the mapping function
     129           42 :         return hysteresis.update(input);  // apply hysteresis, and return true
     130              :         // if the value changed since last time
     131              :     }
     132              : 
     133              :     /**
     134              :      * @brief   Get the filtered value of the analog input (with the mapping 
     135              :      *          function applied).
     136              :      * 
     137              :      * @note    This function just returns the value from the last call to
     138              :      *          @ref update, it doesn't read the analog input again.
     139              :      *
     140              :      * @return  The filtered value of the analog input, as a number
     141              :      *          of `Precision` bits wide.
     142              :      */
     143           65 :     AnalogType getValue() const { return hysteresis.getValue(); }
     144              : 
     145              :     /**
     146              :      * @brief   Get the filtered value of the analog input with the mapping 
     147              :      *          function applied as a floating point number from 0.0 to 1.0.
     148              :      * 
     149              :      * @return  The filtered value of the analog input, as a number
     150              :      *          from 0.0 to 1.0.
     151              :      */
     152           21 :     float getFloatValue() const {
     153           21 :         return getValue() * (1.0f / (ldexpf(1.0f, Precision) - 1.0f));
     154              :     }
     155              : 
     156              :     /**
     157              :      * @brief   Read the raw value of the analog input without any filtering or
     158              :      *          mapping applied, but with its bit depth increased by @c IncRes.
     159              :      */
     160           49 :     AnalogType getRawValue() const {
     161           49 :         AnalogType value = ExtIO::analogRead(analogPin);
     162              : #ifdef ESP8266
     163              :         if (value > 1023)
     164              :             value = 1023;
     165              : #endif
     166           49 :         return increaseBitDepth<ADC_BITS + IncRes, ADC_BITS, AnalogType>(value);
     167              :     }
     168              : 
     169              :     /**
     170              :      * @brief   Get the maximum value that can be returned from @ref getRawValue.
     171              :      */
     172              :     constexpr static AnalogType getMaxRawValue() {
     173              :         return (1ul << (ADC_BITS + IncRes)) - 1ul;
     174              :     }
     175              : 
     176              :     /**
     177              :      * @brief   Select the configured ADC resolution. By default, it is set to
     178              :      *          the maximum resolution supported by the hardware.
     179              :      * 
     180              :      * @see     @ref ADC_BITS "ADC_BITS"
     181              :      * @see     @ref ADCConfig.hpp "ADCConfig.hpp"
     182              :      */
     183            7 :     static void setupADC() {
     184              : #if HAS_ANALOG_READ_RESOLUTION
     185            7 :         analogReadResolution(ADC_BITS);
     186              : #endif
     187            7 :     }
     188              : 
     189              :   private:
     190              :     /// Helper function that applies the mapping function if it's enabled.
     191              :     /// This function is only enabled if MappingFunction is explicitly
     192              :     /// convertible to bool.
     193              :     template <typename M = MappingFunction>
     194              :     typename std::enable_if<std::is_constructible<bool, M>::value,
     195              :                             AnalogType>::type
     196           41 :     mapFnHelper(AnalogType input) {
     197           41 :         return bool(mapFn) ? mapFn(input) : input;
     198              :     }
     199              : 
     200              :     /// Helper function that applies the mapping function without checking if
     201              :     /// it's enabled.
     202              :     /// This function is only enabled if MappingFunction is not convertible to
     203              :     /// bool.
     204              :     template <typename M = MappingFunction>
     205              :     typename std::enable_if<!std::is_constructible<bool, M>::value,
     206              :                             AnalogType>::type
     207            1 :     mapFnHelper(AnalogType input) {
     208            1 :         return mapFn(input);
     209              :     }
     210              : 
     211              :   private:
     212              :     pin_t analogPin;
     213              :     MappingFunction mapFn;
     214              : 
     215              :     using EMA_t = EMA<FilterShiftFactor, AnalogType, FilterType>;
     216              : 
     217              :     static_assert(
     218              :         ADC_BITS + IncRes + FilterShiftFactor <= sizeof(FilterType) * CHAR_BIT,
     219              :         "Error: FilterType is not wide enough to hold the maximum value");
     220              :     static_assert(
     221              :         ADC_BITS + IncRes <= sizeof(AnalogType) * CHAR_BIT,
     222              :         "Error: AnalogType is not wide enough to hold the maximum value");
     223              :     static_assert(
     224              :         Precision <= ADC_BITS + IncRes,
     225              :         "Error: Precision is larger than the increased ADC precision");
     226              :     static_assert(EMA_t::supports_range(AnalogType(0), getMaxRawValue()),
     227              :                   "Error: EMA filter type doesn't support full ADC range");
     228              : 
     229              :     EMA_t filter;
     230              :     Hysteresis<ADC_BITS + IncRes - Precision, AnalogType, AnalogType>
     231              :         hysteresis;
     232              : };
     233              : 
     234              : /**
     235              :  * @brief   A class that reads and filters an analog input.
     236              :  *
     237              :  * A map function can be applied to the analog value (e.g. to compensate for
     238              :  * logarithmic taper potentiometers or to calibrate the range). The analog input
     239              :  * value is filtered using an exponential moving average filter. The default
     240              :  * settings for this filter can be changed in Settings.hpp.  
     241              :  * After filtering, hysteresis is applied to prevent flipping back and forth 
     242              :  * between two values when the input is not changing.
     243              :  * 
     244              :  * @tparam  Precision
     245              :  *          The number of bits of precision the output should have.
     246              :  * @tparam  FilterShiftFactor
     247              :  *          The number of bits used for the EMA filter.
     248              :  *          The pole location is
     249              :  *          @f$ 1 - \left(\frac{1}{2}\right)^{\text{FilterShiftFactor}} @f$.  
     250              :  *          A lower shift factor means less filtering (@f$0@f$ is no filtering),
     251              :  *          and a higher shift factor means more filtering (and more latency).
     252              :  * @tparam  FilterType
     253              :  *          The type to use for the intermediate types of the filter.  
     254              :  *          Should be at least 
     255              :  *          @f$ \text{ADC_BITS} + \text{IncRes} + 
     256              :  *          \text{FilterShiftFactor} @f$ bits wide.
     257              :  * @tparam  AnalogType
     258              :  *          The type to use for the analog values.  
     259              :  *          Should be at least @f$ \text{ADC_BITS} + \text{IncRes} @f$ 
     260              :  *          bits wide.
     261              :  * @tparam  IncRes
     262              :  *          The number of bits to increase the resolution of the analog reading
     263              :  *          by.
     264              :  * 
     265              :  * @ingroup AH_HardwareUtils
     266              :  */
     267              : template <uint8_t Precision = 10,
     268              :           uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
     269              :           class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
     270              :           uint8_t IncRes = MaximumFilteredAnalogIncRes<
     271              :               FilterShiftFactor, FilterType, AnalogType>::value>
     272              : class FilteredAnalog
     273              :     : public GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
     274              :                                    FilterShiftFactor, FilterType, AnalogType,
     275              :                                    IncRes> {
     276              :   public:
     277              :     /**
     278              :      * @brief   Construct a new FilteredAnalog object.
     279              :      *
     280              :      * @param   analogPin
     281              :      *          The analog pin to read from.
     282              :      * @param   initial 
     283              :      *          The initial value of the filter.
     284              :      */
     285           17 :     FilteredAnalog(pin_t analogPin, AnalogType initial = 0)
     286              :         : GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
     287              :                                 FilterShiftFactor, FilterType, AnalogType,
     288           17 :                                 IncRes>(analogPin, nullptr, initial) {}
     289              : 
     290              :     /// @copydoc FilteredAnalog(pin_t,AnalogType)
     291              :     FilteredAnalog(ArduinoPin_t analogPin, AnalogType initial = 0)
     292              :         : FilteredAnalog(pin_t(analogPin), initial) {}
     293              : 
     294              :     /**
     295              :      * @brief   Construct a new FilteredAnalog object.
     296              :      * 
     297              :      * **This constructor should not be used.**  
     298              :      * It is just a way to easily create arrays of FilteredAnalog objects, and
     299              :      * initializing them later. Trying to update a default-constructed or 
     300              :      * uninitialized FilteredAnalog object will result in a fatal runtime error.
     301              :      */
     302              :     FilteredAnalog() : FilteredAnalog(NO_PIN) {}
     303              : 
     304              :     /// A function pointer to a mapping function to map analog values.
     305              :     /// @see    map()
     306              :     using MappingFunction = AnalogType (*)(AnalogType);
     307              : 
     308              :     /**
     309              :      * @brief   Invert the analog value. For example, if the precision is 10 
     310              :      *          bits, when the analog input measures 1023, the output will be 0,
     311              :      *          and when the analog input measures 0, the output will be 1023.
     312              :      * 
     313              :      * @note    This overrides the mapping function set by the `map` method.
     314              :      */
     315            3 :     void invert() {
     316            3 :         constexpr AnalogType maxval = FilteredAnalog::getMaxRawValue();
     317           16 :         this->map([](AnalogType val) -> AnalogType { return maxval - val; });
     318            3 :     }
     319              : };
     320              : 
     321              : END_AH_NAMESPACE
        

Generated by: LCOV version 2.4-beta