Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Virtual Analog Ports #2620

Open
JavierAder opened this issue Sep 19, 2024 · 37 comments
Open

Multiple Virtual Analog Ports #2620

JavierAder opened this issue Sep 19, 2024 · 37 comments
Labels
enhancement New feature or request

Comments

@JavierAder
Copy link

Description

Hello. It's been a while since I wrote something. Greetings to everyone.

Motivation: The ESP8266 has only one analog port, which does not allow measuring more than one analog sensor. Personally, I need to measure multiple temperatures using NTC sensors (the digital sensors supported by Espurna haven't worked well for me); but beyond that, this modification would allow the use of multiple analog sensors of any kind (pressure sensors, humidity sensors, etc.).

Proposal: Allow Espurna to use "multiple virtual analog ports" with the help of an external analog multiplexer and the use of digital ports.

The general idea can be seen here:
https://www.youtube.com/watch?v=OgaeEiHemU4
or here (9.2 Typical Application):
https://www.ti.com/lit/ds/symlink/cd4052b.pdf?ts=1726728167254

I suppose the main modification should be made in AnalogSensor, allowing several instances of it, each associated with different "virtual analog ports." What AnalogSensor should do is: before reading the real analog input (pin 0), set the corresponding input of the multiplexer, wait for a small delay, and then read the analog value (see _rawRead() and analogRead(pin)).

At the configuration level, to enable this functionality, one should set something like:

#EnableVAP=1 // enables virtual analog ports
#DVAP= 1 or 2 or 3 // Number of digital ports connected to the multiplexer
#DVAP0= digital port to use for specifying bit 0 of the multiplexer port address
#DVAP1= digital port to use for specifying bit 1 of the multiplexer port address
#DVAP2= digital port to use for specifying bit 2 of the multiplexer port address

With these modifications, it would be possible to read from up to 8 analog devices.

Solution

No response

Alternatives

No response

Additional context

No response

@JavierAder JavierAder added the enhancement New feature or request label Sep 19, 2024
@mcspr
Copy link
Collaborator

mcspr commented Sep 23, 2024

I suppose the main modification should be made in AnalogSensor, allowing several instances of it, each associated with different "virtual analog ports." What AnalogSensor should do is: before reading the real analog input (pin 0), set the corresponding input of the multiplexer, wait for a small delay, and then read the analog value (see _rawRead() and analogRead(pin)).

So, from the sensor configuration side, it would be enough to just provide it with TYPE of analog source and PIN / CHANNEL / some-kind-of-a-name-for-ID ?

Right now something similar is happening with GPIO pins and. e.g., relays w/ mcp pins, rfb pins, sonoff pins. Relay config asks for a certain TYPE of a pin, provider gives out a pin handler that API consumer promises to then use for reads & writes.
TYPE is configured externally, at boot or on-demand, and separate from relay side of things

I do not really like the word 'virtual' here, as it does continue to use real hardware channel and just requires some extra work before doing so :) Source does change, though.

With these modifications, it would be possible to read from up to 8 analog devices.

fwiw cd4052b pdf linked describes a general purpose multiplexer, so generic pin read & write can also use those as inputs & outputs and not just analog.

@JavierAder
Copy link
Author

Hi;
my idea is something like this.
sensor.cpp


#ifdef NTC_VIRTUAL_SUPPORT

NTCSensor createVirtualNTCSensor(int NTCx,size_t NTCx_SAMPLES,
      Delay NTCx_DELAY,unsigned long NTCx_R_UP,unsigned long NTCx_R_DOWN, double NTCx_INPUT_VOLTAGE,
        unsigned long NTCx_BETA ,unsigned long NTCx_R0,double NTCx_T0)
{

        auto* sensor = new NTCSensor();
        sensor->setSamples(NTCx_SAMPLES);
        sensor->setDelay(NTCx_DELAY) ;
        sensor->setUpstreamResistor(NTCx_R_UP);
        sensor->setDownstreamResistor(NTCx_R_DOWN);
        sensor->setInputVoltage(NTCx_INPUT_VOLTAGE);
        sensor->setBeta(NTCx_BETA);
        sensor->setR0(NTCx_R0);
        sensor->setT0(NTCx_T0);
        sensor->setVirtualPort(NTCx);
        return sensor;

}
#endif

#if NTC_SUPPORT
    #ifndef NTC_VIRTUAL_SUPPORT
    {
        auto* sensor = new NTCSensor();
        sensor->setSamples(NTC_SAMPLES);
        sensor->setDelay(NTC_DELAY) ;
        sensor->setUpstreamResistor(NTC_R_UP);
        sensor->setDownstreamResistor(NTC_R_DOWN);
        sensor->setInputVoltage(NTC_INPUT_VOLTAGE);
        sensor->setBeta(NTC_BETA);
        sensor->setR0(NTC_R0);
        sensor->setT0(NTC_T0);
        add(sensor);
    }
    #else
     {
        #if NTC0 
        {add(createVirtualNTCSensor(NTC0,NTC0_SAMPLES,NTC0_DELAY,NTC0_R_UP,NTC0_R_DOWN,NTC0_INPUT_VOLTAGE,
        NTC0_BETA ,NTC0_R0,NTC0_T0)); 
         }
        #endif
        #if NTC1 
        {add(createVirtualNTCSensor(NTC1,NTC1_SAMPLES,NTC1_DELAY,NTC1_R_UP,NTC1_R_DOWN,NTC1_INPUT_VOLTAGE,
        NTC1_BETA ,NTC1_R0,NTC1_T0)); 
         }
        #endif
        //TODO: do the same for NTC2...NTC7 or use macros while.....
        
        
    }
   
    #endif
#endif

AnalogSensor.h


       void  setVirtualPort(int  vport)
        {
            _vport = vport;
        }

......


   protected:
        int _vport= -1;

        static unsigned int _rawRead(uint8_t pin, size_t samples, Delay delay) {
            // TODO: system_adc_read_fast()? current implementation is using system_adc_read()
            // (which is even more sampling on top of ours)
            unsigned int last { 0 };
            unsigned int result { 0 };
            for (size_t sample = 0; sample < samples; ++sample) {
                const auto value = ::analogRead(pin);
                result = result + value - last;
                last = value;
                if (sample > 0) {
                    espurna::time::critical::delay(delay);
                    yield();
                }
            }

            return result;
        }

        unsigned int _rawRead() const {
             #if VAP_SUPPORT
             {
                if (_vport>=0)
                {s
                    setVirtualPort();
                    //TODO: delay for multiplexer?

                }
            } 
             #endif

            return _rawRead(0, _samples, _delay);
        }

        #if VAP_SUPPORT
        void setVirtualPort() const{
            //TODO: using _vport and #DVAP0,#DVAP1 and #DVAP2 set digital outputs

        }
        #endif

Names are tentative

@JavierAder
Copy link
Author

fwiw cd4052b pdf linked describes a general purpose multiplexer, so generic pin read & write can also use those as inputs & outputs and not just analog.

Yes, multiplexing is general, but my main restriction is that there is only one analog port.

@mcspr
Copy link
Collaborator

mcspr commented Sep 24, 2024

Right, sensor specific code is possible. What I mean is to separate things ever so slightly.

Just to play around with this... You can already override analogRead(uint8_t) definition

// in any .cpp file, global namespace
int multiplexer_read(uint8_t pin) {
  ???
}

extern "C" int analogRead(uint8_t pin) {
  switch (pin) {
  case A0: // 17
    return system_adc_read();

  case 0 ... 7: // or some other unused numbers in the 0...255 range
    return multiplexer_read(pin);
  }

  return 0;
}

Extend NTCSensor code to allow PIN value changes, make a setup() code to instantiate multiplexer and then change analogRead implementation to access the multiplexer pin. Any implementation details related to PIN switching timing would be apparent, e.g. is there a need for delayMicroseconds / delay from our side or not, etc.

What I meant is to integrate multiplexer system-wide, not sensor specifically.
Have you seen relay and button code related to providers?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

@JavierAder
Copy link
Author

Right, sensor specific code is possible. What I mean is to separate things ever so slightly.

Just to play around with this... You can already override analogRead(uint8_t) definition

// in any .cpp file, global namespace
int multiplexer_read(uint8_t pin) {
  ???
}

extern "C" int analogRead(uint8_t pin) {
  switch (pin) {
  case A0: // 17
    return system_adc_read();

  case 0 ... 7: // or some other unused numbers in the 0...255 range
    return multiplexer_read(pin);
  }

  return 0;
}

Extend NTCSensor code to allow PIN value changes, make a setup() code to instantiate multiplexer and then change analogRead implementation to access the multiplexer pin. Any implementation details related to PIN switching timing would be apparent, e.g. is there a need for delayMicroseconds / delay from our side or not, etc.

Nice. It's not really necessary modify AnalogSensor, extending and overriding NTCSensor is enough; anyway I like modify AnalogSensor because my idea was support multiple analog sensor in general, not only ntc sensors (MICS2710 sensor for example)

What I meant is to integrate multiplexer system-wide, not sensor specifically. Have you seen relay and button code related to providers?

No much. It is related to the class DigitalPin?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

I think I understand your idea, but it seems to me that making this extension for the entire system is too complex. The problem is that there are sensors that use more than one port, but for relays, buttons, and analog sensor, yes, because they use only one port.
With this extension many more buttons, relays and analog sensors could be supported.

@JavierAder
Copy link
Author

What I meant is to integrate multiplexer system-wide, not sensor specifically. Have you seen relay and button code related to providers?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

Now I think I'm understanding your idea; I didn't know about providers support. To multiplex buttons I propose two new types of providers
BUTTON_PROVIDER_GPIO_MUX = 3
BUTTON_PROVIDER_ANALOG_MUX = 4
Also, in configuration, the following keys
muxAddress0= GPIO connected to bit 0 of mux address
muxAddress1= GPIO connected to bit 1 of mux address
....
muxAddressN= GPIO connected to bit N of mux address

Then, to define a digital multiplexed button, say, number 5, you would specify the following entries in the configuration

btnProv5=3
btnGpio5= (the 'real' port; the output of the mux)
btnMuxAddress5= (the address to set in muxAddress0, muxAddress1... muxAdressN before reading the 'real' port)
(everything else keys, the same)

Ok, but what code should be modified/extended? button.cpp?

@JavierAder
Copy link
Author

As a concrete example; using CD405xB and only 4 GPIO for support 8 buttons.
ButtonMUX

In runtime config:

muxAddress0= 5 //GPIO connected to Pin A of multiplexer
muxAddress1= 4 //GPIO connected to Pin B of multiplexer
muxAddress2= 0 //GPIO connected to Pin C of multiplexer

For BTN1

btnProv1=3 //BUTTON_PROVIDER_GPIO_MUX
btnGpio1= 2 //GPIO connected to the output mulltiplexer, Pin COM
btnMuxAddress1 = 0 //Multiplexer channel to which the button is connected
.....

For BTN2

btnProv2=3 //BUTTON_PROVIDER_GPIO_MUX
btnGpio2= 2 //GPIO connected to the output mulltiplexer, Pin COM,SAME of BTN1
btnMuxAddress1 = 1 //Multiplexer channel to which the button is connected
.....

The same for BTN3... BTN8 changing btnMuxAddressX.

@mcspr
Copy link
Collaborator

mcspr commented Sep 28, 2024

Now I think I'm understanding your idea; I didn't know about providers support. To multiplex buttons I propose two new types of providers
BUTTON_PROVIDER_GPIO_MUX = 3
BUTTON_PROVIDER_ANALOG_MUX = 4
Also, in configuration, the following keys
muxAddress0= GPIO connected to bit 0 of mux address
muxAddress1= GPIO connected to bit 1 of mux address
....
muxAddressN= GPIO connected to bit N of mux address

Not quite the same as e.g. MCP support flag. It adds extra type for pin, but button continues to use GPIO provider and digital reads.

#define MCP23S08_SUPPORT 1

#define RELAY1_PIN 4
#define RELAY1_PIN_TYPE GPIO_TYPE_MCP23S08

#define BUTTON1_PIN 0
#define BUTTON1_PIN_TYPE GPIO_TYPE_MCP23S08

btnMuxAddress aka 'Multiplexer channel to which the hardware is connected' is btnGpio. Since the main use-case is digital access
Meaning, BUTTON config only knows about the MUX pin and only MUX config knowns about the hardware pins it controls. There is a limitation of available keywords, though, but I presumed it would be enough of a an abstraction.

muxType => cb450xb
muxComGpio => 2
muxGpio0 => 5
muxGpio1 => 4
muxGpio2 => 0
btnGpioType1 => cd450xb
btnGpio1 => 0
btnGpioType2 => cd450xb
btnGpio1 => 1

Analog buttons in this case also re-use the same config, changing btnProv to analog would still be able to access type and pin number which in turn would use a different proxy class to read specific MUX channel on ADC

@JavierAder
Copy link
Author

Nice.
Possible problems I see

constexpr size_t ButtonsMax { 32ul };

With multiplexers the number of buttons can potentially exceed 32

inline bool gpioLock(GpioBase& base, unsigned char pin, bool value,

Port conflict logic changes when there is a multiplexer

@JavierAder
Copy link
Author

On the other hand, to extend not only buttons, but also relays or LEDs (in general, to use a multiplexer as output), I think it is necessary to use not only a multiplexer, but also a buffer; this buffer is enabled using the additional port
(I'll upload a schematic later).
Say
muxBufferGPIO= //GPIO connected to the pin ENABLE of ouput buffer

@JavierAder
Copy link
Author

Hi. Apologies, but I've been rethinking these ideas and I think:

  • for digital input or output expansion, using multiplexers/demultiplexers seems unnecessarily complex to me. It seems much more elegant and simple to do it by supporting shift registers; that has the added advantage that only 3 ports need to be used. For example
    https://resources.altium.com/p/how-expand-input-and-output-microcontroller
    "A more elegant solution is to use serial clocking shift registers like the 74HC595 for output and 74HC165 for input. These ICs can be cascaded to each other with the limitation being the latency to shift the bytes to all the ICs. Using shift registers only involves three I/O pins on the microcontroller, regardless of the number of ICs."

  • for what I do think is almost insurmountable to use a multiplexer is to expand the number of analog inputs, which was my original problem.

Following up on this last idea, a perhaps simple way for future analog sensors to make use of multiplexing is to use a special encoding to specify their "pin" when calling analogRead(uint8_t pin):
pin=0 (0x00) analogRead works the same as now
pin=0x1pppppppp=128+ Address in the analog multiplexer
That is, the most significant bit is used to distinguish the standard analog reading from the reading using the multiplexer (ok, this is the same as you proposed before, but differentiating 0 as the normal pin; the current code uses analogRead(0) not analogRead(A0)).

To configure the analog multiplexer as a whole, the keys you proposed can be used, except muxComGpio (the analog multiplexer output must always be connected to pin A0 of the microcontroller).
Later I will try to define a sensor called NTCMuxSensor that simply extends NTCSensor by modifying the code that performs the reading to exemplify these ideas (obviously, many instances of NTCMuxSensor will be allowed).

@mcspr
Copy link
Collaborator

mcspr commented Oct 2, 2024

for digital input or output expansion, using multiplexers/demultiplexers seems unnecessarily complex to me. It seems much more elegant and simple to do it by supporting shift registers; that has the added advantage that only 3 ports need to be used. For example
https://resources.altium.com/p/how-expand-input-and-output-microcontroller
"A more elegant solution is to use serial clocking shift registers like the 74HC595 for output and 74HC165 for input. These ICs can be cascaded to each other with the limitation being the latency to shift the bytes to all the ICs. Using shift registers only involves three I/O pins on the microcontroller, regardless of the number of ICs."

Also true. Still, generic input support is a possibility? It does run into the case of not-the-best-tool-for-the-job, yes. Suppose, such multiplexer api can be cut out from allowing OUTPUTs, limiting pin abstraction to INPUTs only. Expander, shift registers, etc. can be allowed to support both.

pin=0 (0x00) analogRead works the same as now
pin=0x1pppppppp=128+ Address in the analog multiplexer

Ah. So, the Arduino side supports both by checking whether input is pin == 0 || pin == 17 (aka A0). I was reading espurna analog button code at that time, where I incidentally only added the A0 check.

Note that analogRead replacement is intended for 'variant' / 'board' / 'only-works-on-this-hw' override. e.g. https://github.com/esp8266/Arduino/blob/ccea72823ac50290bc05c67350d2be6626e65547/variants/wifi_slot/analogRead.cpp#L6

I do still lean to the idea of separating mux + analog and just analog through gpio type... But, still have to think about it some more.

@JavierAder
Copy link
Author

Well, here is my first attempt. Note that I added not only support for multiple NTCs but also for multiple Emon sensors (current support for multiple emon sensors requires additional hardware more complex than a simple multiplexer). I think this would be a good general scheme to support any other type of analog sensor in which more instances are required.
Hardwired in the code, using a single multiplexer, 3 NTC sensors and 2 Emon sensors would be supported, using a single multiplexer.
Obviously, feel totally free of any type of correction or suggestion.

New variables of preprocessor:

AnalogMux_SUPPORT
NTCMuxSensor_SUPPORT
EmonAnalogMuxSensor_SUPPORT

New settings:
TODO

Files:
sensor.cpp modified
AnalogMux.h added
NTCMuxSensor.h added
EmonAnalogMuxSensor.h added

AnalogMux.h

#include "espurna.h"

class AnalogMux{
    private:
    //TODO: use an array/vector for GPIOs associated with address pins of Multiplexer
    static uint8_t _muxGPIO0;
    static uint8_t _muxGPIO1;
    static uint8_t _muxGPIO2;

    public:
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      _muxGPIO0=5;
      _muxGPIO1=4;
      _muxGPIO2=0;

      
    }

    static void setAddressMux(uint8_t pin)
    {
        //TODO: decode pin as 1's and 0's, use that for set GPIOs, and then use the analogRead of system
        
        //Digital write to GPI0
        //Digital write to GPI1
        //...

        //delay before analog Read?
        //delay(?)

        
    }
};

NTCMuxSensor.h

#pragma once

#include "AnalogMux.h"
#include "NTCSensor.h"

class NTCMuxSensor : public NTCSensor {
    private:
    //Instances of all NTCMux created in setup()
    static  std::vector<NTCMuxSensor> _insts;


    public:
    static  std::vector<NTCMuxSensor> getSensors()
    {
      return _insts;
    }
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 3 ntc sensor
      NTCMuxSensor* sensor = new NTCMuxSensor();
      sensor->setAnalogPin(0);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);
      sensor = new NTCMuxSensor();
      sensor->setAnalogPin(1);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);
      sensor = new NTCMuxSensor();
      sensor->setAnalogPin(2);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);


    }

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    // Descriptive name of the sensor
    String description() const override {
      //TODO, use _analogPin for description?
      return F("NTCMux @ TOUT");
    } 

    void pre() override {
      //Before read trhow A0 set the multiplexer 
      AnalogMux::setAddressMux(_analogPin);
      
      NTCSensor::pre();
    }
    protected:

    uint8_t _analogPin;  

};

EmonAnalogMuxSensor.h


#pragma once

#include "AnalogMux.h"

#include "EmonAnalogSensor.h"

class EmonAnalogMuxSensor : public EmonAnalogSensor{

    private:
    //Instances of all EmonAnalogMuxSensor created in setup()
    static  std::vector<EmonAnalogMuxSensor> _insts;


    public:
    static  std::vector<EmonAnalogMuxSensor> getSensors()
    {
      return _insts;
    }
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 2 sensor, one for current and one for voltage
      //
      EmonAnalogMuxSensor* sensor = new EmonAnalogMuxSensor();
      sensor->setAnalogPin(3); //0, 1 and 2 is used for NTC sensors exsmples
        //TODO: se values for real sensors
      _insts.push_back(*sensor);

      sensor = new EmonAnalogMuxSensor();
      sensor->setAnalogPin(4);

      _insts.push_back(*sensor);
    }

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    unsigned int analogRead() override {
    
         //Before read trhow A0 set the multiplexer 
        AnalogMux::setAddressMux(_analogPin);


        return EmonAnalogSensor::analogRead();
    }
 
    protected:

    uint8_t _analogPin;  



};

@JavierAder
Copy link
Author

Ah...
sensor.cpp:225

#if AnalogMux_SUPPORT
    #include "sensors/AnalogMux.h"
#endif

#if NTCMuxSensor_SUPPORT
    #include "sensors/NTCMuxSensor.h"
#endif

#if EmonAnalogMuxSensor_SUPPORT
    #include "sensors/EmonAnalogMuxSensor.h"
#endif

sensor.cpp:2619

#if AnalogMux_SUPPORT
    {
       AnalogMux::setup();
    }
#endif

#if NTCMuxSensor_SUPPORT
    {
       NTCMuxSensor::setup();
             //Add all sensors NTC 
       std::vector<NTCMuxSensor> sensors= NTCMuxSensor::getSensors();
       for (std::size_t i = 0; i < sensors.size(); i++) {
            NTCMuxSensor sensor = sensors[i];
            add(sensor);
        }
 
    }
#endif

#if EmonAnalogMuxSensor_SUPPORT
    {
       EmonAnalogMuxSensor::setup();
        //Add all sensors NTC 
       std::vector<EmonAnalogMuxSensor> sensors= NTCMuxSensor::getSensors();
       for (std::size_t i = 0; i < sensors.size(); i++) {
            NTCMuxSensor sensor = sensors[i];
            add(sensor);
        }

    }
#endif


@JavierAder
Copy link
Author

Hi. a more concrete version:
-Code was added to specify the NTC sensor data in a compact way in runtime settings; only one string for sensor
-Code was added to manage the multiplexer before the analog reading through the virtual pins (setAddressBeforeReading)
-TODO: get config strings from espurna

AnalogMux.h

#include "espurna.h"
#include "../gpio.h"

class AnalogMux
{
private:

    using Delay = espurna::duration::critical::Microseconds;
    static Delay _delay;
    // TODO: use an array/vector for GPIOs associated with address pins of Multiplexer
    static uint8_t _muxGPIO0;
    static uint8_t _muxGPIO1;
    static uint8_t _muxGPIO2;

    static std::vector<uint8_t> gpios;

    static int _error;

public:
    static void setup()
    {
        // TODO
        // Using settings or defines, set GPIOs for addressing the multiplexer and configure
        //  and locks pins as digital outs, and set delay
        _delay = Delay{ 100 };
        _muxGPIO0 = 5;
        _muxGPIO1 = 4;
        _muxGPIO2 = 0;

        // error, TODO debug print
        _error = SENSOR_ERROR_OK;

        // locks gpio and mode
        if (!gpioLock(_muxGPIO0))
        {
            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO0, OUTPUT);
        gpios.push_back(_muxGPIO0);

        if (!gpioLock(_muxGPIO1))
        {
            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO1, OUTPUT);
        gpios.push_back(_muxGPIO1);

        if (!gpioLock(_muxGPIO2))
        {

            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO2, OUTPUT);
        gpios.push_back(_muxGPIO2);
    }

    static void setAddressMuxBeforeRead(uint8_t pin)
    {
        if (_error)
            return;

        for (int i = 0; i < gpios.size(); i++)
        {
            uint8_t gpio = gpios[i];
            int bit = pin & 0x01;
            if (bit){
                digitalWrite(gpio, HIGH);
            }else{
                digitalWrite(gpio, LOW);

            }
            pin = pin >>1;
        }
        // TODO: 
        //If pin >0 -> error

        // delay before analog Read?
        espurna::time::critical::delay(_delay);
    }
};

NTCMuxSensor.h


#pragma once

#include "AnalogMux.h"
#include "NTCSensor.h"
#include "../settings_convert.h"

class NTCMuxSensor : public NTCSensor {
    private:
    //Instances of all NTCMux created in setup()
    static  std::vector<NTCMuxSensor> _insts;
    //helper

    // Helper
    static std::vector<String> splitConfig(String str)
    {
      std::vector<String> strings;
      char separator = ',';
      int startIndex = 0, endIndex = 0;
      for (int i = 0; i <= str.length(); i++)
      {

        // If we reached the end of the word or the end of the input.
        if (str[i] == separator || i == str.length())
        {
          endIndex = i;
          String temp;
          temp = str.substring(startIndex, endIndex);
          strings.push_back(temp);
          startIndex = endIndex + 1;
        }
      }
      return strings;
    }

    public:
    static  std::vector<NTCMuxSensor> getSensors()
    {
      return _insts;
    }



    static void setup(){
     //TODO: get allConfigs from settings

      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 3 ntc sensor
      //FORMAT: 9 items separated by commas
      //AnalogPin:int,samples:int,delay:int,upResistor:long,downResistor:long,inputVoltage:double,beta:long,r0:long,t0:long
      //Examples: 10 k upResistor, voltage 1.0, beta 3799,R0 10000,T0 298.15 (25 C in Kelvin)
      String s1="0,1,0,10000,0,1.0,3799,10000,298.15";
      String s2="1,1,0,10000,0,1.0,3799,10000,298.15";
      String s3="2,1,0,10000,0,1.0,3799,10000,298.15";
      std::vector<String> allConfigs;
      allConfigs.push_back(s1);
      allConfigs.push_back(s2);
      allConfigs.push_back(s3);

      using namespace espurna::settings::internal;

      for (auto config: allConfigs) {
        std::vector<String> dataSensor = splitConfig(config);
        if (dataSensor.size() != 9)
        {
          //error:wrong number of items in config string
          //TODO: print debug?
          continue; 
        }
        NTCMuxSensor* sensor = new NTCMuxSensor();
        //TODO:check values
        int pin = convert<int>(dataSensor[0]);
        int samples = convert<int>(dataSensor[1]);
        int delay = convert<int>(dataSensor[2]);
        long rUp = convert<long>(dataSensor[3]);
        long rDown =convert<long>(dataSensor[4]);
        double voltage = convert<double>(dataSensor[5]);
        long beta = convert<long>(dataSensor[6]);
        long r0 = convert<long>(dataSensor[7]);
        double t0 = convert<double>(dataSensor[8]);

        sensor->setAnalogPin(pin);
        sensor->setSamples(samples); 
        sensor->setDelay(delay);
        sensor->setUpstreamResistor(rUp);
        sensor->setDownstreamResistor(rDown);
        sensor->setInputVoltage(voltage);
        sensor->setBeta(beta);
        sensor->setR0(r0);
        sensor->setT0(t0);
        _insts.push_back(*sensor);
      }


    }

    //API sensor

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    // Descriptive name of the sensor
    String description() const override {
      //TODO, use _analogPin for description?
      return F("NTCMux @ TOUT");
    } 

    void pre() override {
      //Before read trhow A0 set the multiplexer 
      AnalogMux::setAddressMuxBeforeRead(_analogPin);
      
      NTCSensor::pre();
    }
    protected:

    uint8_t _analogPin;  

};


I think the code is almost usable, although I'm not sure if I'm using the APIs correctly. Any suggestion is appreciated.

@JavierAder
Copy link
Author

Ok, a running version:
PrimerBuildAndando
In terminal:

set analogMux 50,14,15,16
set ntcMux1 0,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux2 1,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux3 2,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux4 3,1,0,10000,0,1.0,3799,10000,298.15

config\custom.h


// ------------------------------------------------------------------------------
// Example file for custom.h
// Either copy and paste this file then rename removing the .example or create your
// own file: 'custom.h'
// This file allows users to create their own configurations.
// See 'code/espurna/config/general.h' for default settings.
//
// See: https://github.com/xoseperez/espurna/wiki/Software-features#enabling-features
// and 'code/platformio_override.ini.example' for more details.
// ------------------------------------------------------------------------------

//LOLIN with AnalogMux support
#if defined(NODEMCU_LOLIN_AM)

    // Info
    #define MANUFACTURER        "NODEMCU"
    #define DEVICE              "LOLIN_AM"

    // Buttons
    #define BUTTON1_PIN         0
    #define BUTTON1_CONFIG      BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
    #define BUTTON1_RELAY       1

    // Hidden button will enter AP mode if dblclick and reset the device when long-long-clicked
    #define RELAY1_PIN          12
    #define RELAY1_TYPE         RELAY_TYPE_NORMAL

    // Light
    #define LED1_PIN            2
    #define LED1_PIN_INVERSE    1
	
	//AnalogMux and NTCMux
	#define SENSOR_SUPPORT     	1
	#define AnalogMux_SUPPORT 	1
	#define NTCMuxSensor_SUPPORT 1
	

#endif

And finally, diff

diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h
index 727183e7..66babd7b 100644
--- a/code/espurna/config/sensors.h
+++ b/code/espurna/config/sensors.h
@@ -1510,7 +1510,8 @@
     MICS2710_SUPPORT || \
     MICS5525_SUPPORT || \
     NTC_SUPPORT || \
-    TMP3X_SUPPORT \
+    TMP3X_SUPPORT || \
+    AnalogMux_SUPPORT \
 )
 #undef ADC_MODE_VALUE
 #define ADC_MODE_VALUE ADC_TOUT
diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h
index 81c30b8d..1725913c 100644
--- a/code/espurna/config/types.h
+++ b/code/espurna/config/types.h
@@ -332,6 +332,10 @@
 #define SENSOR_SM300D2_ID           43
 #define SENSOR_PM1006_ID            44
 #define SENSOR_INA219_ID            45
+#define SENSOR_ANALOG_MUX_ID        46
+#define SENSOR_NTC_MUX_ID           47
+
+
 
 //--------------------------------------------------------------------------------
 // Magnitudes
diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp
index a2c4a188..6acfecff 100644
--- a/code/espurna/sensor.cpp
+++ b/code/espurna/sensor.cpp
@@ -222,6 +222,16 @@ Copyright (C) 2020-2022 by Maxim Prokhorov <prokhorov dot max at outlook dot com
     #include "sensors/PZEM004TV30Sensor.h"
 #endif
 
+#if AnalogMux_SUPPORT
+    #include "sensors/AnalogMux.h"
+#endif
+
+#if NTCMuxSensor_SUPPORT
+    #include "sensors/NTCMuxSensor.h"
+#endif
+
+
+
 #include "filters/LastFilter.h"
 #include "filters/MaxFilter.h"
 #include "filters/MedianFilter.h"
@@ -2601,6 +2611,32 @@ void load() {
         add(sensor);
     }
 #endif
+
+
+#if AnalogMux_SUPPORT
+    {
+       AnalogMux* am = AnalogMux::createInst();
+       add(am);
+       
+    }
+#endif
+
+#if NTCMuxSensor_SUPPORT
+    {
+       
+       //Add all sensors NTC 
+       NTCMuxSensorConfig c;
+       std::vector<NTCMuxSensor *> ntcSensors = c.getSensors();
+       for (std::size_t i = 0; i < ntcSensors.size(); i++) {
+            NTCMuxSensor* sensor = ntcSensors[i];
+            add(sensor);
+        }
+ 
+    }
+#endif
+
+
+
 }
 
 namespace units {
diff --git a/code/espurna/sensors/AnalogMux.h b/code/espurna/sensors/AnalogMux.h
new file mode 100644
index 00000000..4d9b7ed2
--- /dev/null
+++ b/code/espurna/sensors/AnalogMux.h
@@ -0,0 +1,178 @@
+
+#pragma once
+
+#include "espurna.h"
+#include "../gpio.h"
+#include "../settings.h"
+#include "../settings_convert.h"
+#include "BaseSensor.h"
+
+class AnalogMux : public BaseSensor
+{
+private:
+    static AnalogMux *_inst;
+
+    using Delay = espurna::duration::critical::Microseconds;
+    // static
+    Delay _delay;
+    int _access;
+
+    std::vector<uint8_t> _gpios;
+
+    // Helper
+    std::vector<String> splitConfig(String str)
+    {
+        std::vector<String> strings;
+        char separator = ',';
+        uint startIndex = 0, endIndex = 0;
+        for (uint i = 0; i <= str.length(); i++)
+        {
+
+            // If we reached the end of the word or the end of the input.
+            if (str[i] == separator || i == str.length())
+            {
+                endIndex = i;
+                String temp;
+                temp = str.substring(startIndex, endIndex);
+                strings.push_back(temp);
+                startIndex = endIndex + 1;
+            }
+        }
+        return strings;
+    }
+
+    void unlockGPIOs()
+    {
+        for (uint i = 0; i < _gpios.size(); i++)
+        {
+            uint8_t gpio = _gpios[i];
+            gpioUnlock(gpio);
+        }
+        _gpios.clear();
+    }
+
+public:
+    static AnalogMux *createInst();
+
+    static AnalogMux *Inst();
+
+    // static void setup()
+    //{
+    //  Sensor ID, must be unique
+    unsigned char id() const override
+    {
+        return SENSOR_ANALOG_MUX_ID;
+    }
+
+    // Number of available value slots
+    unsigned char count() const override
+    {
+        return 1;
+    }
+    // Descriptive name of the sensor
+    String description() const override
+    {
+        return "AnalogMux";
+    }
+    // Address of the sensor (it could be the GPIO or I2C address)
+    String address(unsigned char) const override
+    {
+        return "TODO";
+    }
+    // Type for slot # index
+    unsigned char type(unsigned char index) const override
+    {
+        if (index == 0)
+            return MAGNITUDE_COUNT;
+
+        return MAGNITUDE_NONE;
+    }
+
+    // Current value for slot # index
+    double value(unsigned char index) override
+    {
+        if (index == 0)
+            return _access;
+        return 0;
+    }
+    void begin() override
+    {
+        _access = 0;
+
+        if (_gpios.size() > 0)
+            unlockGPIOs();
+
+        using namespace espurna::settings::internal;
+        // FORMAT:analogMux=DelayBeforeRead(Microsecs),GPIO0,GPIO1....
+        String config = getSetting("analogMux");
+        // TODO:check in getSetting can return null
+        if (config == nullptr)
+            config = "";
+
+        std::vector<String> configs = splitConfig(config);
+
+        if (configs.size() < 2)
+        {
+            _error = SENSOR_ERROR_CONFIG;
+            return;
+        }
+        _delay = Delay{convert<int>(configs[0])};
+
+        for (uint i = 1; i < configs.size(); i++)
+        {
+
+            uint8_t muxGPIO = convert<int>(configs[i]);
+            // locks gpio and mode
+            if (!gpioLock(muxGPIO))
+            {
+                // error, TODO debug print
+                _error = SENSOR_ERROR_GPIO_USED;
+                return;
+            }
+            pinMode(muxGPIO, OUTPUT);
+            _gpios.push_back(muxGPIO);
+        }
+
+        _error = SENSOR_ERROR_OK;
+        _ready = true;
+    }
+
+    void setAddressMuxBeforeRead(uint8_t pin)
+    {
+        if (_error)
+            return;
+
+        for (uint i = 0; i < _gpios.size(); i++)
+        {
+            uint8_t gpio = _gpios[i];
+            int bit = pin & 0x01;
+            if (bit)
+            {
+                digitalWrite(gpio, HIGH);
+            }
+            else
+            {
+                digitalWrite(gpio, LOW);
+            }
+            pin = pin >> 1;
+        }
+        _access++;
+        // TODO:
+        // If pin >0 -> error
+
+        // delay before analog Read?
+        espurna::time::critical::delay(_delay);
+    }
+};
+
+AnalogMux* AnalogMux::_inst = nullptr;
+
+AnalogMux* AnalogMux::createInst()
+{
+    AnalogMux::_inst = new AnalogMux();
+    return _inst;
+}
+AnalogMux* AnalogMux::Inst()
+{
+    return _inst;
+}
\ No newline at end of file
diff --git a/code/espurna/sensors/AnalogSensor.h b/code/espurna/sensors/AnalogSensor.h
index c8ccbd63..b06d5107 100644
--- a/code/espurna/sensors/AnalogSensor.h
+++ b/code/espurna/sensors/AnalogSensor.h
@@ -160,8 +160,13 @@ class AnalogSensor : public BaseAnalogSensor {
         double _offset { 0.0 };
 };
 
+constexpr int AnalogSensor::RawBits;
+
 constexpr double AnalogSensor::RawMin;
 constexpr double AnalogSensor::RawMax;
 
 constexpr AnalogSensor::Delay AnalogSensor::DelayMin;
 constexpr AnalogSensor::Delay AnalogSensor::DelayMax;
+
+constexpr size_t AnalogSensor::SamplesMin; 
+constexpr size_t AnalogSensor::SamplesMax;
diff --git a/code/espurna/sensors/NTCMuxSensor.h b/code/espurna/sensors/NTCMuxSensor.h
new file mode 100644
index 00000000..c0c68fbf
--- /dev/null
+++ b/code/espurna/sensors/NTCMuxSensor.h
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "AnalogMux.h"
+#include "NTCSensor.h"
+#include "AnalogSensor.h"
+#include "../settings.h"
+#include "../settings_convert.h"
+
+class NTCMuxSensor : public NTCSensor
+{
+private:
+  // static std::vector<NTCMuxSensor *> *_ntcSensors;
+  //  Helper
+  // static std::vector<String> splitConfig(String str);
+
+public:
+  // static std::size_t getSensors();
+  // static NTCMuxSensor *getSensor(std::size_t);
+
+  NTCMuxSensor()
+  {
+  }
+  void setAnalogPin(uint8_t analogPin)
+  {
+    _analogPin = analogPin;
+  }
+  unsigned char id() const override
+  {
+    return SENSOR_NTC_MUX_ID;
+  }
+
+  // Descriptive name of the sensor
+  String description() const override
+  {
+    // TODO, use _analogPin for description?
+    return "NTCMux "+String( _analogPin)+ " "+String(_input_voltage);
+  }
+
+  // Current value for slot # index
+  double value(unsigned char index) override
+  {
+    if (index == 0)
+    {
+      return _value;
+    }
+
+    return 0.0;
+  }
+  void pre() override
+  {
+    // Before read trhow A0 set the multiplexer
+    AnalogMux *am = AnalogMux::Inst();
+    if (am == nullptr)
+      return;
+    if (am->error())
+      return;
+    am->setAddressMuxBeforeRead(_analogPin);
+
+    NTCSensor::pre();
+  }
+
+protected:
+  uint8_t _analogPin;
+};
+
+class NTCMuxSensorConfig
+{
+public:
+  NTCMuxSensorConfig()
+  {
+  }
+
+  std::vector<String> splitConfig(String str)
+  {
+    std::vector<String> strings;
+    char separator = ',';
+    uint startIndex = 0, endIndex = 0;
+    for (uint i = 0; i <= str.length(); i++)
+    {
+
+      // If we reached the end of the word or the end of the input.
+      if (str[i] == separator || i == str.length())
+      {
+        endIndex = i;
+        String temp;
+        temp = str.substring(startIndex, endIndex);
+        strings.push_back(temp);
+        startIndex = endIndex + 1;
+      }
+    }
+    return strings;
+  }
+
+  std::vector<NTCMuxSensor *> getSensors()
+  {
+    std::vector<NTCMuxSensor *> ntcSensors;
+    // TODO: get allConfigs from settings
+
+    // Using settings or defines, set GPIOs for addressing the multiplexer and configure
+    //  and locks pins as digital outs
+    // For now, for we create 3 ntc sensor
+    // FORMAT: 9 items separated by commas
+    // AnalogPin:int,samples:int,delay:int,upResistor:long,downResistor:long,inputVoltage:double,beta:long,r0:long,t0:long
+    // Examples: 10 k upResistor, voltage 1.0, beta 3799,R0 10000,T0 298.15 (25 C in Kelvin)
+    // String s1="0,1,0,10000,0,1.0,3799,10000,298.15";
+    // String s2="1,1,0,10000,0,1.0,3799,10000,298.15";
+    // String s3="2,1,0,10000,0,1.0,3799,10000,298.15";
+
+    using namespace espurna::settings::internal;
+    // TODO: get all settings with prefix ntcMux
+    String s1 = getSetting("ntcMux1");
+    String s2 = getSetting("ntcMux2");
+    String s3 = getSetting("ntcMux3");
+    String s4 = getSetting("ntcMux4");
+
+    // TODO check if getSetting can return null
+    if (s1 == nullptr)
+      s1 = "";
+    if (s2 == nullptr)
+      s2 = "";
+    if (s3 == nullptr)
+      s3 = "";
+    if (s4 == nullptr)
+      s4 = "";
+
+    std::vector<String> allConfigs;
+    allConfigs.push_back(s1);
+    allConfigs.push_back(s2);
+    allConfigs.push_back(s3);
+    allConfigs.push_back(s4);
+
+    for (auto config : allConfigs)
+    {
+      std::vector<String> dataSensor = splitConfig(config);
+      if (dataSensor.size() != 9)
+      {
+        // error:wrong number of items in config string
+        // TODO: print debug?
+        continue;
+      }
+      NTCMuxSensor *sensor = new NTCMuxSensor();
+      // TODO:check values
+      int pin = convert<int>(dataSensor[0]);
+      int samples = convert<int>(dataSensor[1]);
+      int delay = convert<int>(dataSensor[2]);
+      long rUp = convert<long>(dataSensor[3]);
+      long rDown = convert<long>(dataSensor[4]);
+      double voltage = convert<double>(dataSensor[5]);
+      long beta = convert<long>(dataSensor[6]);
+      long r0 = convert<long>(dataSensor[7]);
+      double t0 = convert<double>(dataSensor[8]);
+
+      sensor->setAnalogPin(pin);
+      sensor->setSamples(samples);
+      sensor->setDelay(delay);
+      sensor->setUpstreamResistor(rUp);
+      sensor->setDownstreamResistor(rDown);
+      sensor->setInputVoltage(voltage);
+      sensor->setBeta(beta);
+      sensor->setR0(r0);
+      sensor->setT0(t0);
+      ntcSensors.push_back(sensor);
+    }
+    return ntcSensors;
+  }
+};
+
+// std::vector<NTCMuxSensor *> *NTCMuxSensor::_ntcSensors = nullptr;
+//  STATIC METHODS
+//   Helper
diff --git a/code/platformio_override.ini b/code/platformio_override.ini
new file mode 100644
index 00000000..1b9424f3
--- /dev/null
+++ b/code/platformio_override.ini
@@ -0,0 +1,4 @@
+[env:nodemcu-lolin-analog-mux]
+extends = env:esp8266-4m-base
+build_src_flags = -DNODEMCU_LOLIN_AM -DNOWSAUTH -DUSE_CUSTOM_H
+

TODO: get multiples config with prefix ntcMux

@JavierAder
Copy link
Author

Well, these days I realized two things:

  • the ESP8266 ADC besides having low resolution is very noisy (consecutively read temperatures could vary several degrees)
  • there is a way to support both multiple analog ports and a better ADC: ADS1115. It has a 16-bit ADC and an internal multiplexer that supports up to 4 channels (I think they can also be cascaded to support even more)
    https://www.ti.com/product/es-mx/ADS1115
    There is already code that uses that chip (EmonADS1X15Sensor.h) although not related to NTCs.

I think using ADS1115 should be the standard way to expand analog inputs, possibly at AnalogSensor level.

@mcspr
Copy link
Collaborator

mcspr commented Nov 29, 2024

ADC noise not related to power / specific board / lack-of stable power input?
Anything changes w/ longer delay between analogRead()s, not just 2ms but 100ms+, 200ms+, ...?

Does current ADS1115 work?:) I do remember tweaking read loops at some point, and non-working bitmask for old configs, but i don't really remember whether it was tested. Plus, not sure I have the part around rn

@JavierAder
Copy link
Author

ADC noise not related to power / specific board / lack-of stable power input? Anything changes w/ longer delay between analogRead()s, not just 2ms but 100ms+, 200ms+, ...?

I tested it with the code where AnalogSensor calculated the averages wrong; with your latest modifications and using several samples per access it will probably generate less variations.
As for the delays, I will try them as soon as I can, but I suspect that the problem does not come from there.

Does current ADS1115 work?:) I do remember tweaking read loops at some point, and non-working bitmask for old configs, but i don't really remember whether it was tested. Plus, not sure I have the part around rn

The code I read is related to an Emon sensor; I don't know if it works well, but accessing ADS1115 seems relatively simple.

@JavierAder
Copy link
Author

Well, this is my last idea: delegate all the analog reads to a separate class, which is independently configured (in runtime) and potentially supports several devices that generate analog reads (one of which is ESP8266 itself), each of which may have several channels/pins. On the other hand, an additional field must be added to AnalogSensor, input_device_id; When you want to do the analog reading, AnalogSensor delegates it to the new class (AnalogInputs).
I think it is the most versatile and expandable form; In the future one can support not only ADS1115, but even communication with other controllers via i2c or another protocol; and all this just by modifying a class.
espurna/analog_inputs.h:

#pragma once

#include "types.h"

// Device to read from
#define IN_DEVICE_ESP8266_ID 0     // standar ESP8266 analog input (pin A0)
#define IN_DEVICE_ADS1115_GND_ID 1 // ADS1115 via i2c with address 1001000 /four channels)
#define IN_DEVICE_ADS1115_VDD_ID 2 // ADS1115 via i2c with address 1001001 (four channels)
#define IN_DEVICE_ADS1115_SDA_ID 3 // ADS1115 via i2c with address 1001010 (four channels)
#define IN_DEVICE_ADS1115_SCL_ID 4 // ADS1115 via i2c with address 1001011 (four channels)

struct AnalogInputResult
{
    int raw_value;
    double voltage; // conversion form raw_value to voltage is device dependent
    bool error;     // overflow of maximum value, connection error, etc
};
// Singleton, configurated in boot time (or runtime)

struct ADS1115Config

{

    uint8_t datarate;
    uint8_t mode;
    uint8_t gain;
    bool configured;
    // TODO: delay? current channel?
};
//Singleton
class AnalogInputs
{
public:
    static AnalogInputs *createInst();

    //singleton: global instance
    static AnalogInputs *Inst();

    //hook for possible adaptation via subclasing
    static void setCustomAnalogInputs(AnalogInputs* custom)
    {
        _inst = custom;


    }
    AnalogInputs()
    {
        ads1115gnd.configured = false;
        ads1115vdd.configured = false;
        ads1115sda.configured = false;
        ads1115scl.configured = false;
    }
    // called from main with runtime config

    void setADS1115ConfigGND(ADS1115Config config)
    {
        ads1115gnd = config;
    }

    void setADS1115ConfigVDD(ADS1115Config config)
    {
        ads1115vdd = config;
    }
    void setADS1115ConfigSDA(ADS1115Config config)
    {
        ads1115sda = config;
    }
    void setADS1115ConfigSCL(ADS1115Config config)
    {
        ads1115scl = config;
    }

    // standard analog read of ESP8266
    AnalogInputResult analogRead()
    {

        return analogRead(IN_DEVICE_ESP8266_ID, 0);
    }
    AnalogInputResult analogRead(uint8_t device_id, uint8_t pin)
    {
        AnalogInputResult result;
        // TODO:Depending on the device ID, reading is forwarded to pin A0, i2c (ADS115), etc...
	//IMPORTANT: also calculate the voltage 
	//if (device_id == IN_DEVICE_ESP8266_ID)
	//	normal read via ::analogRead
	//else if (device_id == IN_DEVICE_ADS1115_GND_ID )
	//	read via i2c using pin and ads1115gnd
	//etc

        return result;
    }

protected:
    static AnalogInputs *_inst;
    ADS1115Config ads1115gnd;
    ADS1115Config ads1115vdd;
    ADS1115Config ads1115sda;
    ADS1115Config ads1115scl;
};

AnalogInputs *AnalogInputs::_inst = nullptr;

AnalogInputs *AnalogInputs::createInst()
{
    AnalogInputs::_inst = new AnalogInputs();
    return _inst;
}
AnalogInputs *AnalogInputs::Inst()
{
    return AnalogInputs::_inst;
}

AnalogInputs* _i = AnalogInputs::createInst();

Next week I'm going to buy an ADS1115 and I'm going to try to make a usable version.

@mcspr
Copy link
Collaborator

mcspr commented Dec 5, 2024

input_device_id

Note that most of the time in the current code, object handle is passed directly and consumer only cares about the interface not the detail that there is some kind of 'object' / 'device' id included. But not to repeat myself, I am returning to the 'port' analogy from above

Next week I'm going to buy an ADS1115 and I'm going to try to make a usable version.

I do also wonder how much of a difference there is between the genuine part and cheaper CJ-..., GY-... , etc. variants

@JavierAder
Copy link
Author

I don't understand what you mean by "the genuine part and cheaper CJ-..., GY-... , etc. variants"
My first idea was to expand Esp8266 with an analog multiplexer to allow multiple analog sensors to share the Esp8266 ADC; my current idea is to expand Esp8266 so that analog sensors can use any analog source, in particular, that they can use an external ADC (ADS1115), and not be restricted to the Esp8266 ADC (which is noisy and low resolution).
If instead of using ADS1115 you want to use an analog multiplexer and the Esp8266 ADC (as in my original idea) you can simply define a new device id, say:
IN_DEVICE_MUX_ID = 5
and add the necessary code in AnalogInputs

@mcspr
Copy link
Collaborator

mcspr commented Dec 7, 2024

I don't understand what you mean by "the genuine part and cheaper CJ-..., GY-... , etc. variants"

Try searching e.g. aliexpress, chips are ADS1x15 analogues

@mcspr
Copy link
Collaborator

mcspr commented Dec 7, 2024

If instead of using ADS1115 you want to use an analog multiplexer and the Esp8266 ADC (as in my original idea) you can simply define a new device id, say:
IN_DEVICE_MUX_ID = 5
and add the necessary code in AnalogInputs

Right, this is the implementation. Should the reader remember the id internally to pass it along to the analoginputs? In sensor class, I mean. Or terminal adc. Or the buttons.
There is also resolution difference btw

@JavierAder
Copy link
Author

Hello, just recently I had time to advance the code.
In summary: AnalogSensor delegates the analog readings to another class, AnalogInputs; to do the reading, you only have to pass the ID of the reading device (the same Esp8266, a generic multiplexer, or an ADS1115) and the pin/channel within it.
AnalogInputs is configured independently and at runtime.
As an example of use I added a RawAnalogSensor.
The ADS1115 access code is based on the Adafruit library.
Modified files:
AnalogSensor.h
sensor.cpp
Files added:
analog_inputs.h
RawAnalogSensor.h

TODO:
-finalize details and compile (see TODOs in code)
-modify NTCSensor to support many ntc sensors using the same scheme that RawAnalogSensor uses

@JavierAder
Copy link
Author

analog_inputs.h

#pragma once

#include "types.h"
#include "gpio.h"
#include "settings.h"
#include "settings_convert.h"
#include "espurna.h"
#include "i2c.h"

// Device to read from
#define IN_DEVICE_ESP8266_ID 0     // standar ESP8266 analog input (pin A0)
#define IN_DEVICE_MUX_ID 1         // generic mux using ESP8266 ADC
#define IN_DEVICE_ADS1115_GND_ID 2 // ADS1115 via i2c with address 1001000 /four channels)
#define IN_DEVICE_ADS1115_VDD_ID 3 // ADS1115 via i2c with address 1001001 (four channels)
#define IN_DEVICE_ADS1115_SDA_ID 4 // ADS1115 via i2c with address 1001010 (four channels)
#define IN_DEVICE_ADS1115_SCL_ID 5 // ADS1115 via i2c with address 1001011 (four channels)

#define ADS1115_GND_ADDRESS 0b1001000
#define ADS1115_VDD_ADDRESS 0b1001001
#define ADS1115_SDA_ADDRESS 0b1001010
#define ADS1115_SCL_ADDRESS 0b1001011

// Based in Adafruit

/*=========================================================================
    POINTER REGISTER
    -----------------------------------------------------------------------*/
#define ADS1X15_REG_POINTER_MASK (0x03)      ///< Point mask
#define ADS1X15_REG_POINTER_CONVERT (0x00)   ///< Conversion
#define ADS1X15_REG_POINTER_CONFIG (0x01)    ///< Configuration
#define ADS1X15_REG_POINTER_LOWTHRESH (0x02) ///< Low threshold
#define ADS1X15_REG_POINTER_HITHRESH (0x03)  ///< High threshold
/*=========================================================================*/

/*=========================================================================
    CONFIG REGISTER
    -----------------------------------------------------------------------*/
#define ADS1X15_REG_CONFIG_OS_MASK (0x8000) ///< OS Mask
#define ADS1X15_REG_CONFIG_OS_SINGLE \
    (0x8000) ///< Write: Set to start a single-conversion
#define ADS1X15_REG_CONFIG_OS_BUSY \
    (0x0000) ///< Read: Bit = 0 when conversion is in progress
#define ADS1X15_REG_CONFIG_OS_NOTBUSY \
    (0x8000) ///< Read: Bit = 1 when device is not performing a conversion

#define ADS1X15_REG_CONFIG_MUX_MASK (0x7000) ///< Mux Mask
#define ADS1X15_REG_CONFIG_MUX_DIFF_0_1 \
    (0x0000) ///< Differential P = AIN0, N = AIN1 (default)
#define ADS1X15_REG_CONFIG_MUX_DIFF_0_3 \
    (0x1000) ///< Differential P = AIN0, N = AIN3
#define ADS1X15_REG_CONFIG_MUX_DIFF_1_3 \
    (0x2000) ///< Differential P = AIN1, N = AIN3
#define ADS1X15_REG_CONFIG_MUX_DIFF_2_3 \
    (0x3000)                                     ///< Differential P = AIN2, N = AIN3
#define ADS1X15_REG_CONFIG_MUX_SINGLE_0 (0x4000) ///< Single-ended AIN0 100 : AINP = AIN0 and AINN = GND
#define ADS1X15_REG_CONFIG_MUX_SINGLE_1 (0x5000) ///< Single-ended AIN1 101 : AINP = AIN1 and AINN = GND
#define ADS1X15_REG_CONFIG_MUX_SINGLE_2 (0x6000) ///< Single-ended AIN2 110 : AINP = AIN2 and AINN = GND
#define ADS1X15_REG_CONFIG_MUX_SINGLE_3 (0x7000) ///< Single-ended AIN3 111 : AINP = AIN3 and AINN = GND

constexpr uint16_t MUX_BY_CHANNEL[] = {
    ADS1X15_REG_CONFIG_MUX_SINGLE_0, ///< Single-ended AIN0
    ADS1X15_REG_CONFIG_MUX_SINGLE_1, ///< Single-ended AIN1
    ADS1X15_REG_CONFIG_MUX_SINGLE_2, ///< Single-ended AIN2
    ADS1X15_REG_CONFIG_MUX_SINGLE_3  ///< Single-ended AIN3
}; ///< MUX config by channel

#define ADS1X15_REG_CONFIG_PGA_MASK (0x0E00)   ///< PGA Mask
#define ADS1X15_REG_CONFIG_PGA_6_144V (0x0000) ///< +/-6.144V range = Gain 2/3
#define ADS1X15_REG_CONFIG_PGA_4_096V (0x0200) ///< +/-4.096V range = Gain 1
#define ADS1X15_REG_CONFIG_PGA_2_048V \
    (0x0400)                                   ///< +/-2.048V range = Gain 2 (default)
#define ADS1X15_REG_CONFIG_PGA_1_024V (0x0600) ///< +/-1.024V range = Gain 4
#define ADS1X15_REG_CONFIG_PGA_0_512V (0x0800) ///< +/-0.512V range = Gain 8
#define ADS1X15_REG_CONFIG_PGA_0_256V (0x0A00) ///< +/-0.256V range = Gain 16

#define ADS1X15_REG_CONFIG_MODE_MASK (0x0100)   ///< Mode Mask
#define ADS1X15_REG_CONFIG_MODE_CONTIN (0x0000) ///< Continuous conversion mode
#define ADS1X15_REG_CONFIG_MODE_SINGLE \
    (0x0100) ///< Power-down single-shot mode (default)

#define ADS1X15_REG_CONFIG_RATE_MASK (0x00E0) ///< Data Rate Mask

#define ADS1X15_REG_CONFIG_CMODE_MASK (0x0010) ///< CMode Mask
#define ADS1X15_REG_CONFIG_CMODE_TRAD \
    (0x0000)                                     ///< Traditional comparator with hysteresis (default)
#define ADS1X15_REG_CONFIG_CMODE_WINDOW (0x0010) ///< Window comparator

#define ADS1X15_REG_CONFIG_CPOL_MASK (0x0008) ///< CPol Mask
#define ADS1X15_REG_CONFIG_CPOL_ACTVLOW \
    (0x0000) ///< ALERT/RDY pin is low when active (default)
#define ADS1X15_REG_CONFIG_CPOL_ACTVHI \
    (0x0008) ///< ALERT/RDY pin is high when active

#define ADS1X15_REG_CONFIG_CLAT_MASK \
    (0x0004) ///< Determines if ALERT/RDY pin latches once asserted
#define ADS1X15_REG_CONFIG_CLAT_NONLAT \
    (0x0000)                                   ///< Non-latching comparator (default)
#define ADS1X15_REG_CONFIG_CLAT_LATCH (0x0004) ///< Latching comparator

#define ADS1X15_REG_CONFIG_CQUE_MASK (0x0003) ///< CQue Mask
#define ADS1X15_REG_CONFIG_CQUE_1CONV \
    (0x0000) ///< Assert ALERT/RDY after one conversions
#define ADS1X15_REG_CONFIG_CQUE_2CONV \
    (0x0001) ///< Assert ALERT/RDY after two conversions
#define ADS1X15_REG_CONFIG_CQUE_4CONV \
    (0x0002) ///< Assert ALERT/RDY after four conversions
#define ADS1X15_REG_CONFIG_CQUE_NONE \
    (0x0003) ///< Disable the comparator and put ALERT/RDY in high state (default)
/*=========================================================================*/

/** Gain settings */
typedef enum
{
    GAIN_TWOTHIRDS = ADS1X15_REG_CONFIG_PGA_6_144V,
    GAIN_ONE = ADS1X15_REG_CONFIG_PGA_4_096V,
    GAIN_TWO = ADS1X15_REG_CONFIG_PGA_2_048V,
    GAIN_FOUR = ADS1X15_REG_CONFIG_PGA_1_024V,
    GAIN_EIGHT = ADS1X15_REG_CONFIG_PGA_0_512V,
    GAIN_SIXTEEN = ADS1X15_REG_CONFIG_PGA_0_256V
} adsGain_t;

#define RATE_ADS1115_8SPS (0x0000)   ///< 8 samples per second
#define RATE_ADS1115_16SPS (0x0020)  ///< 16 samples per second
#define RATE_ADS1115_32SPS (0x0040)  ///< 32 samples per second
#define RATE_ADS1115_64SPS (0x0060)  ///< 64 samples per second
#define RATE_ADS1115_128SPS (0x0080) ///< 128 samples per second (default)
#define RATE_ADS1115_250SPS (0x00A0) ///< 250 samples per second
#define RATE_ADS1115_475SPS (0x00C0) ///< 475 samples per second
#define RATE_ADS1115_860SPS (0x00E0) ///< 860 samples per second

using Delay = espurna::duration::critical::Microseconds;

struct AnalogInputResult
{
    int raw_value;
    double voltage; // conversion form raw_value to voltage is device dependent
    uint8_t error;  // overflow of maximum value, connection error, etc
};
// Singleton, configurated in boot time (or runtime)

struct ADS1115Config

{
    uint8_t address; // i2c address

    uint16_t datarate;
    uint8_t mode;
    uint8_t gain;
    Delay delay;
    uint8_t error;

    bool configured;
    // TODO: delay? current channel?
};

struct MUXConfig

{

    std::vector<uint8_t> gpios;
    Delay delay;
    uint8_t error;
    bool configured;
    // TODO: delay? current channel?
};
// Singleton
class AnalogInputs
{
public:
    static constexpr int RawBits8266{10};

    static constexpr double RawMin8266{0.0};
    static constexpr double RawMax8266{(1 << RawBits8266) - 1};

    static AnalogInputs *createInst();

    // singleton: global instance
    static AnalogInputs *Inst();

    // hook for possible adaptation via subclasing
    static void setCustomAnalogInputs(AnalogInputs *custom)
    {
        _inst = custom;
    }
    
    // Helper ; TODO find a better place for it
    std::vector<String> splitConfig(String str)
    {
        return splitConfig(str,',');
    }
    std::vector<String> splitConfig(String str,char separator)
    {
        if (str == nullptr)
            str = "";
        std::vector<String> strings;
        //char separator = ',';
        uint startIndex = 0, endIndex = 0;
        for (uint i = 0; i <= str.length(); i++)
        {

            // If we reached the end of the word or the end of the input.
            if (str[i] == separator || i == str.length())
            {
                endIndex = i;
                String temp;
                temp = str.substring(startIndex, endIndex);
                strings.push_back(temp);
                startIndex = endIndex + 1;
            }
        }
        return strings;
    }
    // called from main
    void setup()
    {
        using namespace espurna::settings::internal;
        String config;
        // FORMAT:analogMux=DelayBeforeRead(Microsecs),GPIO0,GPIO1....
        config = getSetting("analogMux");
        setupMUX(splitConfig(config));

        config = getSetting("ADS1115GND");
        ads1115gnd = createADS1115Config(splitConfig(config));
        ads1115gnd.address = ADS1115_GND_ADDRESS;

        config = getSetting("ADS1115VDD");
        ads1115vdd = createADS1115Config(splitConfig(config));
        ads1115vdd.address = ADS1115_VDD_ADDRESS;

        config = getSetting("ADS1115SDA");
        ads1115sda = createADS1115Config(splitConfig(config));
        ads1115sda.address = ADS1115_SDA_ADDRESS;

        config = getSetting("ADS1115SCL");
        ads1115scl = createADS1115Config(splitConfig(config));
        ads1115scl.address = ADS1115_SCL_ADDRESS;
    }

    void setupMUX(std::vector<String> configs)
    {
        using namespace espurna::settings::internal;
        if (configs.size() == 0)
            return;
        mux.configured = true;
        if (configs.size() < 2)
        {
            mux.error = SENSOR_ERROR_CONFIG;
            return;
        }
        mux.delay = Delay{convert<int>(configs[0])};
        for (uint i = 1; i < configs.size(); i++)
        {

            uint8_t muxGPIO = convert<int>(configs[i]);
            // locks gpio and mode
            if (!gpioLock(muxGPIO))
            {
                // error, TODO debug print
                mux.error = SENSOR_ERROR_GPIO_USED;
                return;
            }
            pinMode(muxGPIO, OUTPUT);
            mux.gpios.push_back(muxGPIO);
        }
    }

    ADS1115Config createADS1115Config(std::vector<String> configs)
    {
        using namespace espurna::settings::internal;
        ADS1115Config config;
        config.configured = false;
        config.error = 0;
        if (configs.size() == 0)
            return config;

        config.configured = true;
        if (configs.size() != 4)
        {
            config.error = SENSOR_ERROR_CONFIG;
            return config;
        }
        config.delay = Delay{convert<int>(configs[0])};
        config.datarate = convert<int>(configs[1]);
        config.gain = convert<int>(configs[2]);
        config.mode = convert<int>(configs[3]);
        config.error = checkADS111Config(config);
        return config;
    }
    uint8_t checkADS111Config(ADS1115Config config)
    {
        // TODO
        return 0;
    }

    AnalogInputs()
    {
        mux.configured = false;
        ads1115gnd.configured = false;
        ads1115vdd.configured = false;
        ads1115sda.configured = false;
        ads1115scl.configured = false;
        mux.error = 0;
        ads1115gnd.error = 0;
        ads1115vdd.error = 0;
        ads1115sda.error = 0;
        ads1115scl.error = 0;
    }

    // standard analog read of ESP8266
    AnalogInputResult analogRead()
    {

        return analogRead(IN_DEVICE_ESP8266_ID, 0);
    }
    AnalogInputResult analogRead(uint8_t device_id, uint8_t pin)
    {
        AnalogInputResult result;
        // TODO:Depending on the device ID, reading is forwarded to pin A0, i2c (ADS115), etc...
        // IMPORTANT: also calculate the voltage
        // if (device_id == IN_DEVICE_ESP8266_ID)
        //	normal read via ::analogRead
        // else if (device_id == IN_DEVICE_ADS1115_GND_ID )
        //	read via i2c using pin and ads1115gnd
        // etc
        if (device_id == IN_DEVICE_ESP8266_ID)
        {
            return analogRead8266(pin);
        }

        if (device_id == IN_DEVICE_MUX_ID)
        {
            return analogReadMUX(pin);
        }

        if (device_id == IN_DEVICE_ADS1115_GND_ID)
        {
            return analogReadADS1115(ads1115gnd, pin);
        }

        if (device_id == IN_DEVICE_ADS1115_VDD_ID)
        {
            return analogReadADS1115(ads1115vdd, pin);
        }
        if (device_id == IN_DEVICE_ADS1115_SDA_ID)
        {
            return analogReadADS1115(ads1115sda, pin);
        }
        if (device_id == IN_DEVICE_ADS1115_SCL_ID)
        {
            return analogReadADS1115(ads1115scl, pin);
        }

        result.error = 1; // TODO: analog device not defined

        return result;
    }

protected:
    AnalogInputResult analogRead8266(uint8_t pin)
    {

        AnalogInputResult result;
        result.error = 0;
        result.raw_value = ::analogRead(pin);
        result.voltage = result.raw_value / AnalogInputs::RawMax8266;
    }
    AnalogInputResult analogReadMUX(uint8_t pin)
    {

        AnalogInputResult result;
        result.error = 0;
        if (!mux.configured)
        {
            result.error = 1; // TODO: error mux not configured
            return result;
        }
        if (mux.error)
        {
            result.error = mux.error; // configuration error in mux
            return result;
        }

        for (uint i = 0; i < mux.gpios.size(); i++)
        {
            uint8_t gpio = mux.gpios[i];
            int bit = pin & 0x01;
            if (bit)
            {
                digitalWrite(gpio, HIGH);
            }
            else
            {
                digitalWrite(gpio, LOW);
            }
            pin = pin >> 1;
        }
        // TODO:
        // If pin >0 -> error

        // delay before analog Read?
        espurna::time::critical::delay(mux.delay);

        result.raw_value = ::analogRead(0); // 0 or A0?
        result.voltage = result.raw_value / AnalogInputs::RawMax8266;
        return result;
    }

    AnalogInputResult analogReadADS1115(ADS1115Config config, uint8 pin)
    {
        AnalogInputResult result;

        if (!config.configured)
        {
            result.error = 1; // TODO: error ads1115 not configured
            return result;
        }
        if (config.error)
        {
            result.error = config.error; // configuration error in ADS1115
            return result;
        }
        if (pin > 3)
        {
            result.error = 1; // TODO: error ads1115 channel incorrect
            return result;
        }

        startADCReadingADS1115(config, pin, /*continuous=*/false);

        // Wait for the conversion to complete
        while (!conversionCompleteADS1115(config))
        {
            ; // TODO; How long to wait before detecting a read error?
        }

        // Read the conversion results
        int16_t raw = getLastConversionResultsADS1115(config);

        result.error = 0;
        result.raw_value = raw;
        result.voltage = computeVolts(gainToBits(config.gain),raw);

        return result;
    }

    void startADCReadingADS1115(ADS1115Config configADS, uint8_t channel, bool continuous)
    {
        // Start with default values
        uint16_t config =
            ADS1X15_REG_CONFIG_CQUE_1CONV |   // Set CQUE to any value other than-- 1:0
                                              // None so we can use it in RDY mode
            ADS1X15_REG_CONFIG_CLAT_NONLAT |  // Non-latching (default val)-- 2
            ADS1X15_REG_CONFIG_CPOL_ACTVLOW | // Alert/Rdy active low   (default val)-- 3
            ADS1X15_REG_CONFIG_CMODE_TRAD;    // Traditional comparator (default val)-- 4

        // Set data rate -- 7:5
        uint16_t m_dataRate = dataRateToBits(configADS.datarate);
        config |= m_dataRate;

        // continuous o single-shot -- 8
        if (continuous)
        {
            config |= ADS1X15_REG_CONFIG_MODE_CONTIN;
        }
        else
        {
            config |= ADS1X15_REG_CONFIG_MODE_SINGLE;
        }

        // Set PGA/voltage range -- 11:9
        adsGain_t m_gain = gainToBits(configADS.gain);
        config |= m_gain;

        // Set channels --14:12
        uint16_t mux = MUX_BY_CHANNEL[channel];
        config |= mux;

        // Set 'start single-conversion' bit -- 15
        config |= ADS1X15_REG_CONFIG_OS_SINGLE;

        // Write config register to the ADC
        // writeRegister(ADS1X15_REG_POINTER_CONFIG, config);
        i2c_write_uint16(configADS.address, ADS1X15_REG_POINTER_CONFIG, config);

        // Set ALERT/RDY to RDY mode.
        // writeRegister(ADS1X15_REG_POINTER_HITHRESH, 0x8000);
        // writeRegister(ADS1X15_REG_POINTER_LOWTHRESH, 0x0000);
        i2c_write_uint16(configADS.address, ADS1X15_REG_POINTER_HITHRESH, 0x8000);
        i2c_write_uint16(configADS.address, ADS1X15_REG_POINTER_LOWTHRESH, 0x0000);
    }

    bool conversionCompleteADS1115(ADS1115Config config)
    {
        // return (readRegister(ADS1X15_REG_POINTER_CONFIG) & 0x8000) != 0;
        return (i2c_read_uint16(config.address, ADS1X15_REG_POINTER_CONFIG) & 0x8000) != 0;
    }

    int16_t getLastConversionResultsADS1115(ADS1115Config config)
    {
        // Read the conversion results
        // uint16_t res = readRegister(ADS1X15_REG_POINTER_CONVERT) >> m_bitShift;
        uint16_t res = i2c_read_uint16(config.address, ADS1X15_REG_POINTER_CONVERT);
        return (int16_t)res;
    }

    float computeVolts(adsGain_t gain, int16_t counts)
    {
        // see data sheet Table 3
        float fsRange;
        switch (gain)
        {
        case GAIN_TWOTHIRDS:
            fsRange = 6.144f;
            break;
        case GAIN_ONE:
            fsRange = 4.096f;
            break;
        case GAIN_TWO:
            fsRange = 2.048f;
            break;
        case GAIN_FOUR:
            fsRange = 1.024f;
            break;
        case GAIN_EIGHT:
            fsRange = 0.512f;
            break;
        case GAIN_SIXTEEN:
            fsRange = 0.256f;
            break;
        default:
            fsRange = 0.0f;
        }
        return counts * (fsRange / (32768));
    }

    adsGain_t gainToBits(uint8_t gain)
    {

        switch (gain)
        {
        case 0:
            return GAIN_TWOTHIRDS;
        case 1:
            return GAIN_ONE;
        case 2:
            return GAIN_TWO;
        case 3:
            return GAIN_FOUR;
        case 4:
            return GAIN_EIGHT;
        case 5:
            return GAIN_SIXTEEN;
        }
        return GAIN_ONE;
    }

    uint16_t dataRateToBits(uint8_t dataRate)
    {
        switch (dataRate)
        {
        case 0:
            return RATE_ADS1115_8SPS;
        case 1:
            return RATE_ADS1115_16SPS;
        case 2:
            return RATE_ADS1115_32SPS;
        case 3:
            return RATE_ADS1115_64SPS;
        case 4:
            return RATE_ADS1115_128SPS;
        case 5:
            return RATE_ADS1115_250SPS;
        case 6:
            return RATE_ADS1115_475SPS;
        case 7:
            return RATE_ADS1115_860SPS;
        }
        return RATE_ADS1115_128SPS;
    }

    static AnalogInputs *_inst;
    MUXConfig mux;
    ADS1115Config ads1115gnd;
    ADS1115Config ads1115vdd;
    ADS1115Config ads1115sda;
    ADS1115Config ads1115scl;

};

AnalogInputs *AnalogInputs::_inst = nullptr;

AnalogInputs *AnalogInputs::createInst()
{
    AnalogInputs::_inst = new AnalogInputs();
    return _inst;
}
AnalogInputs *AnalogInputs::Inst()
{
    return AnalogInputs::_inst;
}

//AnalogInputs *_i = AnalogInputs::createInst();

#ifndef __cpp_inline_variables
constexpr int AnalogInputs::RawBits8266;

constexpr double AnalogInputs::RawMin8266;
constexpr double AnalogInputs::RawMax8266;
#endif

@JavierAder
Copy link
Author

sensor.cpp in load()

void load() {

#if ANALOG_INPUTS_SUPPORT
    {
        AnalogInputs * ai = AnalogInputs::createInst();
        ai->setup();
    }
#endif

#if RAW_ANALOG_SENSOR_SUPPORT
    {
        RawAnalogSensorConfig* rac = new RawAnalogSensorConfig();
        std::vector<RawAnalogSensor *> rass = rac->getSensors();
        for (RawAnalogSensor* ras : rass)
        {
            add(ras);
        }
    }
#endif
......


@JavierAder
Copy link
Author

RawAnalogSensor.h

#pragma once

#include "../analog_inputs.h"
#include "../settings.h"
#include "../settings_convert.h"
#include "AnalogSensor.h"

class RawAnalogSensor : public AnalogSensor
{
public:
    RawAnalogSensor()
    {
    }

    unsigned char id() const override
    {
        return 123; // TODO
    }

    unsigned char count() const override
    {
        return 3;
    }
    // Descriptive name of the sensor
    String description() const override
    {
        return F("Raw Analog Sensor");
    }


    // Type for slot # index
    unsigned char type(unsigned char index) const override
    {
        if (index == 0)
        {
            return MAGNITUDE_COUNT;
        }

        if (index == 1)
        {
            return MAGNITUDE_VOLTAGE;
        }
        if (index == 2)
        {
            return MAGNITUDE_ANALOG;
        }

        return MAGNITUDE_NONE;
    }

    // Current value for slot # index
    double value(unsigned char index) override
    {
        if (index == 0)
        {
            return _sampledValue();
        }
        if (index == 1)
        {
            return _sampledVoltageValue();
        }
        if (index == 2)
        {
            return _sampledVoltageValue() * getMult();
        }

        return 0;
    }

    double getMult()
    {
        return _mult;
    }
    void setMult(double mult)
    {
        _mult = mult;
    }

protected:
    double _mult{1.0};
};

class RawAnalogSensorConfig
{

public:
    RawAnalogSensorConfig()
    {
    }
    std::vector<RawAnalogSensor *> getSensors()
    {
        std::vector<RawAnalogSensor *> rass;
        using namespace espurna::settings::internal;
        String configSensors;
        // FORMAT:rawAnalogSensors=analog_devide_id1,channel1,mult1;analog_devide_id2,channel2,mult2;...
        configSensors = getSetting("rawAnalogSensors");
        std::vector<String> configs = AnalogInputs::Inst()->splitConfig(configSensors, ';');

        for (auto config : configs)
        {
            std::vector<String> dataSensor = AnalogInputs::Inst()->splitConfig(config);
            if (dataSensor.size() != 3)
            {
                // error:wrong number of items in config string
                // TODO: print debug?
                continue;
            }
            int device_id = convert<int>(dataSensor[0]);
            int channel = convert<int>(dataSensor[1]);
            double mult = convert<double>(dataSensor[2]);
            RawAnalogSensor* ras = new RawAnalogSensor();
            ras->setInDevice(device_id);
            ras->setPin(channel);
            ras->setMult(mult);
            rass.push_back(ras);
        }

        return rass;
    }
};

@JavierAder
Copy link
Author

AnalogSensor.h modifcations

   protected:
        double _sampledVoltageValue() const {
            return _valueVolt;
        }

        void _sampledVoltageValue(double value) {
            _valueVolt = value;
        }

        double _sampledValue() const {
            return _value;
        }

        void _sampledValue(double value) {
            _value = value;
        }

        void _readNext(uint8_t pin) {
            if (_sample >= _samples) {
                return;
            }

            const auto now = TimeSource::now();
            if (now - _last < _delay) {
                return;
            }
            
            _error = 0;
           AnalogInputResult result = AnalogInputs::Inst()->analogRead(_in_device,_pin);
           if (result.error){
                _error = result.error;
                return;
           }

           ++_sample;
           _last = now;
           _sum += result.raw_value;
           _sumVolt += result.voltage;
           

           if (_sample >= _samples) {
               const double sum = _sum;
               const double samples = _samples;
               const double sumVolt = _sumVolt;
               _sampledValue(sum / samples);
               _sampledVoltageValue(sumVolt/samples);
               _sum = 0;
               _sumVolt = 0.0;
               _sample = 0;
           }
        }

public methods


        void setPin(uint8_t pin) {
            _pin = pin;
        }

        void setInDevice(uint8_t in_device){
            _in_device = in_device;
        }


new members


        //NOTE: unsigneds? certain sensors/adc can generate negative values
        uint32_t _sum { 0 };
        double _sumVolt { 0 };
        

        double _value { 0.0 };
        double _valueVolt { 0.0 };

        double _factor { 1.0 };
        double _offset { 0.0 };


        uint8_t _pin { A0 };
        uint8_t _in_device { 0};

@JavierAder
Copy link
Author

My current problem is what to do in

    // Wait for the conversion to complete
        while (!conversionCompleteADS1115(config))
        {
            ; // TODO; How long to wait before detecting a read error?
        }

How to implement a time out?

@mcspr
Copy link
Collaborator

mcspr commented Feb 3, 2025

My current problem is what to do in

    // Wait for the conversion to complete
        while (!conversionCompleteADS1115(config))
        {
            ; // TODO; How long to wait before detecting a read error?
        }

How to implement a time out?

As a guess, 1000 / samples-per-second (datarate) is duration-per-sample in milliseconds? For 128, 8ms. For 826, roughly 2ms. And etc.

For non-blocking... just check the clock? Otherwise, see blockingDelay template and pass it in_progress? func as the last argument.

bool blockingDelay(CoreClock::duration timeout, CoreClock::duration interval, T&& blocked) {

time::blockingDelay(
timeout,
duration::Milliseconds{ 10 },
[&]() {
return ptr->err == ERR_INPROGRESS;
});

@JavierAder
Copy link
Author

JavierAder commented Feb 4, 2025

As a guess, 1000 / samples-per-second (datarate) is duration-per-sample in milliseconds? For 128, 8ms. For 826, roughly 2ms. And etc.

I think so, in one shot mode the reading time is inversely proportional to the datarate plus a small additional time needed to exit sleep mode (although I would have to investigate this further).

For non-blocking... just check the clock? Otherwise, see blockingDelay template and pass it in_progress? func as the last argument.

Would the following code work?

	// Wait for the conversion to complete
	const auto timeout = TimeSource::now() + getReadTimeout(config.datarate);
	//getReadTimeout returns, say, 16 mls for datarate 128, 4 mls for datarate 826
        while (!conversionCompleteADS1115(config))
        {
		
            if (TimeSource::now() > timeout)
	   {
		result.error = SENSOR_ERROR_TIMEOUT;
		return result;
	  }
        }

Although I find it not very energy efficient... Using blockingDelay is probably a better solution.

@mcspr
Copy link
Collaborator

mcspr commented Feb 4, 2025

const auto timeout = TimeSource::now() + getReadTimeout(config.datarate);
...
  if (TimeSource::now() > timeout)
...

If TimeSource is CoreClock and thus duration is duration::Milliseconds with uint32_t as underlying type, one should be aware that 'now() + offset' can overflow at some point. Subtraction result compared to timeout usually works better here.

template <typename T>
bool PolledFlag<T>::wait(typename T::duration interval) {
const auto now = T::now();
if (now - _last > interval) {
_last = now;
return true;
}
return false;
}

(and note that duration:: types are all unsigned underneath; delta between durations / time points is always a positive number)

@JavierAder
Copy link
Author

JavierAder commented Feb 6, 2025

Nice. As for energy efficiency (the loop is continuously executing i2c reads...), I looked at blockingDelay and it seems to me that it simply ends up executing delay(); wouldn't the following code be more or less equivalent (and simpler):

	// Wait for the conversion to complete
	const auto timeout = TimeSource::now() + getReadTimeout(config.datarate);
	//getReadTimeout returns, say, 16 mls for datarate 128, 4 mls for datarate 826
	auto delayBetweenReads = timeout/4; 
	auto last = TimeSource::now();
        while (!conversionCompleteADS1115(config))
        {
	    auto now = TimeSource::now();
            if (now - last > timeout)
	  {

	    result.error = SENSOR_ERROR_TIMEOUT;
	    return result;
	  }else
	  {
	     last = now;
	    delay(delayBetweenReads);  

	  }
      }

@mcspr
Copy link
Collaborator

mcspr commented Feb 6, 2025

I looked at blockingDelay and it seems to me that it simply ends up executing delay(); wouldn't the following code be more or less equivalent (and simpler):

Correct, but it also depends what you would prefer more - readability of the name blockingDelay, or readability of the variables around and before the loop and the loop itself :)

The reason for the blockingDelay was the first, replacing the code in several sensors (and having some 2.x.x<->3.x.x Core compatible code). But you have to watch out for public funcs in the header to be aware that they exist in the first place

@JavierAder
Copy link
Author

Okay. Incidentally, I was reading that the delay should only be used in the main loop (and not, for example, interrupt routines). Does the sensor code always run in the main loop? I think in general yes, but I have my doubts if it cannot be executed in the interrupt routine or TCP/IP stack code from the execution of commands in the debug terminal (I seem to remember that there was a command to, for example, read the ADC of Esp8266).

Btw, the previous code was wrong, I think this one would work:

  // Wait for the conversion to complete
   const auto timeout = getReadTimeout(config.datarate);
   //getReadTimeout returns, say, 16 mls for datarate 128, 4 mls for datarate 826
   auto delayBetweenChecks = timeout/4; 
   auto firstCheck = TimeSource::now();
   while (!conversionCompleteADS1115(config))
   {
     auto now = TimeSource::now();
     if (now - firstCheck > timeout)
     {
   	 result.error = SENSOR_ERROR_TIMEOUT;
        return result;
     }else
     {
       delay(delayBetweenChecks);  
     }
   }

@mcspr
Copy link
Collaborator

mcspr commented Feb 7, 2025

Sensor methods are ok for delay()s, so are terminal commands possibly calling something with delay()s
Only worry if you expect something to happen in some other loop() func in the mean time. delay() schedules sdk / system tasks and suspends loop() at that point in code; for example, no other sensor methods would execute until the delay is done.

Both yield() and delay() are indeed made to only work within setup() & loop(). Though, calling them outside of that context would crash the board, so I'd expect that occurrence would be pretty easy to spot.

fwiw, unless you are manually writing tcp / udp servers or clients with espasynctcp or using lwip callbacks, you won't happen within networking task
another way to call something in non-delay()-able context are SystemTimer callbacks (os_timer_...)
interrupts are also explicit; e.g. attachInterrupt callbacks in some sensors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants