SEN0193 Capacitive Soil Moisture Sensor auto-calibration

Hello. I'm building a project for school using an R4 Wifi and 6 sen0193 capacitive soil moisture sensors.

I'm using these exact ones: Capacitive_Soil_Moisture_Sensor_SKU_SEN0193-DFRobot

I'll try to explain my setup, but please feel free to ask any questions about what I might have missed.

The sensors are wired to pins A0-A5 through 6 x 3.5" audio jack cables (The sensors each have a 1.5m cable ending in a male 3.5" audio jack. The audio jack is connected to a female audio jack connector which is then wired to the Arduino inside my project box).

The sensors are powered through the arduino 5v pin.

Since the R4 Wifi supports up to 14bit Analog Resolution, I've set the resolution to 14 bits in my setup() function using analogReadResolution(14).

I've also set the analog Reference to AR_EXTERNAL and connected the 3.3v pin to AREF (I've done this to get more usable values out of the sensors that output 3v max according to their specifications).

Now let's move on to my question.

The problem I was having was that each sensor has its own value range. This requires each sensor to be calibrated manually (in water and in air) to get the usable range. So in my graphs, I get slightly different results from each sensor even in the same exact conditions (water vs air).

What I thought I would do was make it so the sensors auto-calibrate after a few readings.

My code which returns the data is as follows (it's a function that runs when the arduino receives an HTTP request from a server)

int bitHalf = (pow(2, ADC_BITS)-1)/2;
int sensorMin[] = {bitHalf, bitHalf, bitHalf, bitHalf, bitHalf, bitHalf};
int sensorMax[] = {bitHalf, bitHalf, bitHalf, bitHalf, bitHalf, bitHalf};


void getHumidityAll() {

  String valuesArr[6];
  String finalTextReturn = "";
  int i;
  int dummyAnalogRead;
  int realAnalogRead;
  

  for (i=0; i<=5; i++){

    dummyAnalogRead = analogRead(ain[i]);
    delay(10);
    realAnalogRead = analogRead(ain[i]);
    

    if(realAnalogRead < sensorMin[i]){
      sensorMin[i] = realAnalogRead;
    }

    if(realAnalogRead > sensorMax[i]){
      sensorMax[i] = realAnalogRead;
    } 

    realAnalogRead = map(realAnalogRead, sensorMin[i], sensorMax[i], 1000, 0);
    valuesArr[i] = String(realAnalogRead);

    if(i<5){
      finalTextReturn += valuesArr[i] + ",";
    } else {
      finalTextReturn += valuesArr[i];
    }

  }
  // Serial.println(bitHalf);
  webserver.send(200, "text/plain", finalTextReturn);
}

The variable ADC_BITS is there just in case I switch the analog resolution in the future. It just holds an int value of 10/12/14.

I then create two arrays that hold the half-point value depending on the ADC_BITS. So for 10bit resolution, the half value would be 511.

I set the values at that half-point to make sure they are in the useful range of the sensor.

Then, when an HTTP request reaches the Arduino, I take 2 readings for each sensor to "reset" the ADC (as I've seen in many such threads).

Here comes the auto calibrating bit (hopefully)

	if(realAnalogRead < sensorMin[i]){
      sensorMin[i] = realAnalogRead;
    }

    if(realAnalogRead > sensorMax[i]){
      sensorMax[i] = realAnalogRead;
    } 

As I understand it, each item of my sensorMin and sensorMax arrays will take the minimum and maximum possible value of its corresponding sensor.

These min/max values are the values of the sensors in water (sensorMin) and in air (sensorMax) respectively.

So each time the Arduino is reset, I will put the sensors in air and in water for a few readings and then the minimum and maximum values for each on will be set automatically.

After that, I map these values to a range of 0-1000 (1000 being in water) and return them.

What I hope to accomplish with this is to get a 0 value for all sensors when they are in air, and 1000 value for when they are in water, regardless of their unique values.

I've tried the above and it seems to be working. But since I don't have a lot of experience, I'd like to get a few more opinions in case I'm missing something.

So, is my approach correct?

Do you see any other potential issues with my setup?

it's not clear how long you give yourself to immerse the sensors in water (assuming they start out in air), so you probably want to enable a calibration mode as well as end it.

you probaly want to know when no further min/max values are being set, so you need prints.

you may also want to display the min/max values

it's not clear how your web server works. do you want to measure continually or only when there is a client request

and you probably don't want to re-calibrate each time, so you probably wnat to use the EEPROM to store calibrated values and read then on startup in setup().


const byte Ain [] = { A0, A1, A2, A3, A4, A5 };
const int Nsensor = sizeof(Ain);

const int ADC_BITS = 10;
const int BitHalf  = 1 << ADC_BITS /2;       // Capitalize Constants

int sensorMin [Nsensor];
int sensorMax [Nsensor];

enum { Idle, Measure, Calibrate };
int mode = Idle;

char s [90];

// -----------------------------------------------------------------------------
void
dump ()
{
    Serial.println ("dump:\n");
    for (int i = 0; i < Nsensor; i++) {
        sprintf (s, " %d %6d %6d", i, sensorMin [i], sensorMax [i]);
        Serial.println (s);
    }
}

// -----------------------------------------------------------------------------
void calibrate ()
{
    for (int i = 0; i < Nsensor; i++) {
        int val = analogRead(Ain[i]);
        delay(10);
        val = analogRead(Ain[i]);

        if (val < sensorMin[i])  {
            sensorMin[i] = val;
            sprintf (s,
                " %d %6d %6d %6d", i, sensorMin [i], val, sensorMax [i]);
            Serial.println (s);
        }

        if (val > sensorMax[i])  {
            sensorMax[i] = val;
            sprintf (s,
                " %d %6d %6d %6d", i, sensorMin [i], val, sensorMax [i]);
            Serial.println (s);
        }
    }
}

// -----------------------------------------------------------------------------
void
measure ()
{
    String text = "";

    for (int i = 0; i < Nsensor; i++) {
        int val = analogRead(Ain[i]);
        delay (10);
        val = analogRead(Ain[i]);

        val = map(val, sensorMin[i], sensorMax[i], 1000, 0);

        if (0 == i)
            text = String(val);
        else 
            text += ", " + String (val);
    }

    Serial.println (text);
}

// -----------------------------------------------------------------------------
void
loop (void)
{
    if (Serial.available ())  {
        switch (Serial.read ())  {
        case 'c':
            mode = Calibrate;
            break;

        case 'd':
            dump ();
            break;

        case 'i':
            mode = Idle;
            break;

        case 'm':
            mode = Measure;
            break;
        }
    }

    switch (mode) {
    case Measure:
        measure ();
     // webserver.send(200, "text/plAin", text);
        mode = Idle;
        break;

    case Calibrate:
        calibrate ();
        break;
    }
}

// -----------------------------------------------------------------------------
void
setup (void)
{
    Serial.begin (9600);

    for (int i = 0; i < Nsensor; i++)
        sensorMin [i] = sensorMax [i] = BitHalf;
}

Thank you for your answer.

it's not clear how long you give yourself to immerse the sensors in water (assuming they start out in air), so you probably want to enable a calibration mode as well as end it.

Right now it doesn't matter how long the calibration is run for. It runs on every request so on reset I would start by keeping all the sensors in the air for a few minutes, and then immerse them in water for a few more minutes.

But you are right that it should be a separate proccess that starts and ends.

To that end, I will create two new functions (calibrateAir, calibrateWater) that will be initiated through my server. The functions will take 10 readings each (is this enough?) and keep the values in the sensorMin/sensorMax variables.

I'm not sure I want to write them to the EEPROM because I want to be able to change which sensor is connected to each port, thus making the saved values quickly obsolete. I would be writing to EEPROM too much I think, using up the write cycles.

For now I'm ok with running the calibration on each startup.

The new functions are as follows:

void calibrateWater(){

  // Reset minimum sensor values (in water)
  sensorMin[] = {bitHalf, bitHalf, bitHalf, bitHalf, bitHalf, bitHalf};

  int i, j, dummyAnalogRead, realAnalogRead;

  for (i=0; i<=10; i++){

    for (j=0; j<=5; j++){

      dummyAnalogRead = analogRead(ain[j]);
      delay(10);
      realAnalogRead = analogRead(ain[j]);

      if(realAnalogRead < sensorMin[j]){
        sensorMin[j] = realAnalogRead;
      }
    
    }
  }
  webserver.send(200, "text/plain", "OK");
}

void calibrateAir(){

  // Reset maximum sensor values (in air)
  sensorMax[] = {bitHalf, bitHalf, bitHalf, bitHalf, bitHalf, bitHalf};

  int i, j, dummyAnalogRead, realAnalogRead;

  for (i=0; i<=10; i++){

    for (j=0; j<=5; j++){

      dummyAnalogRead = analogRead(ain[j]);
      delay(10);
      realAnalogRead = analogRead(ain[j]);

      if(realAnalogRead > sensorMax[j]){
        sensorMax[j] = realAnalogRead;
      }
    }
    delay(10);
  }
  webserver.send(200, "text/plain", "OK");
}

Do you think my delay (10ms) between readings (both for the dummy value and between the calibration data), is enough?

doubt that it matters much. ithink just 2 separate reads may be necessary. but why only 10 measurements? if 1 isnt' enough, how do you know that it won't take a 100 (which with a 10 ms delay is still only 5 second.) this is why i suggested using prints

it's not clear how you expect your students to do this project. will all the pieces be in a kit or have to be dismantled from previous use? after all is assembled, why not enter the calibration mode with the sensors lying on a table and then one by one put into water until there are no more prints. then exit the calibration mode and continue.

at this point power can't be removed unless the EEPROM were used. writing to the EEPROM is only necessary once and could be a separate command that would also exit calibration mode. can't imagine exceeding the 100k write limit

Hmm. I think I should be doing at least 100 reads as you suggest.

The arduino is not connected to a PC with a USB cable so I can't do Serial prints and I would like it to be as independent as possible as it will be used in an agriculture environment.

it's not clear how you expect your students to do this project. will all the pieces be in a kit or have to be dismantled from previous use? after all is assembled, why not enter the calibration mode with the sensors lying on a table and then one by one put into water until there are no more prints. then exit the calibration mode and continue.

I'm the student in this situation. This project will be in a plastic project box.

Basically there are two buttons on my web app that controls the whole thing. The buttons run the two functions on my previous comment respectively.

So on deployment, I would leave the sensors on a table and click the calibration button (for air). It will run the calibration function (I'll probably change it so it takes 100 readings).

Once its done (and the Ajax request is successful, which I will determine with a console.log on the browser), I will be putting the sensor in water and press the calibration button (for water).

This will also take 100 readings and return a success message through console.log on the browser.

After that the sensors would be considered calibrated and I can continue by inserting them into actual soil and getting (hopefully) correct readings.

I could also do what you suggested, meaning having a way to determine if new min/max values are being set and stopping the calibration once I get X readings with no change in min/max values.

Though this introduces another potential problem of the function running "indefinitely". The PHP function (CURL) that calls Arduino calibration functions could timeout.

I think I need to have a set amount of reads so I can correctly predict how long the calibration would take.

Do you think 100 reads would be enough?

have no idea. don't know the response time of these sesnsors

but apparently there's some device that is providing commands thru the web server ???

guessing this may be in a hot house with all sensors within 5 ft of an Arduino.

could flash the builtin LED on an arduino each time there's a min/max update. Just wait until the LED no longer flashes.

so with the sensors sitting on the bench, enter the calibration mode. LED flashes. wait until it stops flashing, then put sensors in water. wait until the LED stops flashing, exit calibration mode.

have no idea. don't know the response time of these sesnsors

Without the 10ms delay I could probably even do 1000 reads without worrying about the PHP Script timing out.

But I think maybe I should also keep calibrating for every reading. So if a new value is read that is bigger or lower than the initially calibrated value, it should become the new min/max value for the sensor.

So the function that returns the readings should stay like this maybe?

void getHumidityAll() {

  String valuesArr[6];
  String finalTextReturn = "";
  int i;
  int dummyAnalogRead;
  int realAnalogRead;
  

  for (i=0; i<=5; i++){

    dummyAnalogRead = analogRead(ain[i]);
    realAnalogRead = analogRead(ain[i]);
    
    if(realAnalogRead < sensorMin[i]){
      sensorMin[i] = realAnalogRead;
    }

    if(realAnalogRead > sensorMax[i]){
      sensorMax[i] = realAnalogRead;
    } 

    realAnalogRead = map(realAnalogRead, sensorMin[i], sensorMax[i], 1000, 0);
    valuesArr[i] = String(realAnalogRead);

    if(i<5){
      finalTextReturn += valuesArr[i] + ",";
    } else {
      finalTextReturn += valuesArr[i];
    }

  }
  webserver.send(200, "text/plain", finalTextReturn);
}

That way I would take into account any outliers that may come up as time goes on while having a good base after calibration.

The reason I'm thinking of doing this is because in testing, the same sensor returns a different min/max value for every calibration. That is even after 1000 reads.

if you exceed the min/max while making measurements, something is wrong! If you're measureing soil moisture, how could it be drier than air or wetter than water.

This is whyi suggested running calibration until no more updates occur and possibly for some time after. Each time there's a min/max update, reset a timer.