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

Generated by: LCOV version 1.15