Developing a low cost EC tester for hydroponics that can stay in the fluid all the time

Develop an EC probe that can remain in the nutrient fluid all the time with a local display of the EC value that is also published as a web page on my local WIFI network.

Use a four element probe feeding an ADS1115 four input 16 bit A to D converter
linked to an ESP32 microprocessor with WIFI output. A Dallas DS18B20 thermometer reads the water temperature in order to correct the EC value to 25 degrees C.
A SSD1306 128 x 64 display shows EC and temperature

The ESP32 web page is just a comma-delimited text string of data that is then received on a Raspberry Pi by a curl script and then used to build a graph of EC versus time of day using gnuplot.

The probe tube
An STL file can be made available for a 3D printer. The electric motor brushes are 13 x 8 x 6 mm from eBay. (20 for £4.36 from China). 8 x 5 mm also seem standard and they are easy to obtain.
The DS18B20 thermometer is positioned in the trough with all wires to the carbon brushes. Then all is covered in hot glue.
I may use an epoxy potting mix for the final probe.
A longer tube version is currently under test.
I have a large flood and drain hydroponic system (top right picture). The trays are cut down old IBC 1000 litre tanks and I may place the tube so that the contents flush out as the tank level goes up and down.
In new 3D printed design a 1/2 inch hose is connected to the top of the probe and a small 4mm pipe will be connected to this and to the output pipe of one of the flood and drain pumps. A small flow from the pump will refresh the liquid in the probe tube. The hose will allow the probes to be deep in the tank and also ensure that the top of the assembly connects to air. There must be no tank fluid connection to the top probe or currents will flow outside the probe assembly.

The circuit board
The testing has so far only been done with 500 gm salt per litre - see bottom left picture. Th brown material on the probe is the hot glue insulation for the wires. It also contains the "one wire" Dallas DS18B20 thermometer. The display is an SSD1306 128 x 64 OLED.

The small circuit on the right is an ADS1115 for input A to D connected by i2c

Early Results
The graph (bottom right) shows how the Raw EC moves substantially with temperature. It is corrected for temperature in the blue curve. That data is multiplied by a factor to give a value of 1 for the chosen fluid. It is sodium chloride in this experiment.

I will probably mix the recommended mixture for tomatoes given by Masterblend and keep a large vessel of that at the side of the main supply tank as my standard. The probe will be set to 1 for that mix and then moved to the main tank. It should then read 1 if the tank is set for tomatoes or a lower value for strawberries. (I may mix a half strength standard for those)

The important measurement is that of what chemical feed has been added. Masterblend do not suggest an EC value since tap water etc varies between customers. I expect to use rain water for strawberries (and for making my standard calibration mix) but in mid summer may have to use tap water - and change the reference mix as well to be tap water with measured Masterblend additions.

Arduino IDE code for ESP32 - crude first attempt - it works!

#include <Adafruit_ADS1X15.h>
Adafruit_ADS1115 ads; /* Use this for the 16-bit version */
// default i2c address is 0x48
// Adafruit_ADS1015 ads; /* Use this for the 12-bit version
// from Dropbox/ArduinoIDE/EC_meter/pulse_and_measure_3/pulse_and_measure_8.
// see https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/
// see https://www.luisllamas.es/en/esp32-preferences/
// see https://www.masterblend.com/wp-content/uploads/2022/09/Masterblend-4-18-38-Mixing-Instructions-092922.pdf
// see https://hydroponicseuro.com/mixing-instructions/

const int ledPin = 5; //drive pulse
const int ADCorange = 19; //LED to show measurement period on scope
const int CALswitch = 18; // push a button to trigger the calibration sequence
int CALval = 1; // 1 means no button press 0 means pressed so pin 18 is earthed LOW

//----------------------------------------------------------------------------------------------for WIFI Yemperature and OLED display
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
//Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// the callibration multiplier has to be saved in the ESP32 and be available after power off.
#include <Preferences.h> //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE
Preferences preferences; //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE

#define SENSOR_PIN 4 // ESP32 pin GPIO17 connected to DS18B20 thermometer sensor DATA pin

const char* ssid = "deco"; // CHANGE IT
const char* password = "password"; // CHANGE IT

OneWire oneWire(SENSOR_PIN); // setup a oneWire instance
DallasTemperature DS18B20(&oneWire); // pass oneWire to DallasTemperature library

AsyncWebServer server(80);

float temperature;
float volts, volts0, volts1, volts2, volts3 ,current, EC;
float TemperatureCoef = 0.019; //this changes depending on what chemical we are measuring - seems OK for salt and Masterblend mix
float EC25 =0;
float seriesR = 1000;

float realEC25 = 0;
// to get real EC25 use realEC25 = EC25/cal1000_EC2

float getTemperature() {
DS18B20.requestTemperatures(); // send the command to get temperatures
float tempC = DS18B20.getTempCByIndex(0); // read temperature in °C
return tempC;

//----------------------------------------------------------------------------------------------------------- SETUP

void setup(void)

// setup pin 5 as a digital output pin to drive the EC probe pulses
pinMode (ledPin, OUTPUT);
// setup pin 18 as a digital output pin to pulse the orange LED to indicate the duration of the ADC read period
pinMode (ADCorange, OUTPUT);
pinMode(CALswitch, INPUT);


//--------------------------------------------------------------------------------------for SSD1306 display
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));

display.setCursor(0, 0);
// Display static text
display.setCursor(0, 33);
Serial.println("startup done");

//----------------------------------------------------------------------------------------SSD1306 display end

DS18B20.begin(); // initialize the DS18B20 sensor

// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.println("Connecting to WiFi...");
Serial.println("Connected to WiFi");

// Print the ESP32's IP address
Serial.print("ESP32 Web Server's IP address: ");

// Define a route to serve the HTML page
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
Serial.println("ESP32 Web Server: New request received:"); // for debugging
Serial.println("GET /"); // for debugging

// get temperature from sensor
float temperature = getTemperature();
// Format the temperature with two decimal places
String temperatureStr = String(temperature, 2);

// put the temperature into the string html to send as a text item for curl from Pi on network
// send a text string to be read using a Curl script on a Raspberry Pi
String html = temperatureStr;
html += ",";
html += EC;
html += ",";
html += EC25;
html += ",";
html += realEC25;
request->send(200, "text/html", html);

// Start the server


Serial.println("Getting single-ended readings from AIN0..3");
Serial.println("ADC Range: +/- 6.144V (1 bit = 3mV/ADS1015, 0.1875mV/ADS1115)");
if (!ads.begin())
Serial.println("Failed to initialize ADS.");
while (1);
//---------------------------------------------------------------------------------------------------------------LOOP START
void loop(void)

// cal1000_EC25 multiply the EC25 number by this to give EC compared to EC chosen to be 1 for the cal liquid - perhaps use the Multiblend suggested mix as "1"

preferences.begin("my_variables", false); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§
float cal1000_EC25 = preferences.getFloat("float_value", 0.0); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§

int16_t adc0, adc1, adc2, adc3;
//float volts, volts0, volts1, volts2, volts3 ,current, EC;

Serial.print (" ==========================retrieved value of cal1000_EC25 = ");

//================================================================== EC drive Pulse HIGH
digitalWrite (ledPin, HIGH); // turn on the white LED
delay(10); // measure this long after pulse rises

digitalWrite (ADCorange, HIGH); // turn on the green LED to indicate start of adc measurements
adc0 = ads.readADC_SingleEnded(0);
adc1 = ads.readADC_SingleEnded(1);
adc2 = ads.readADC_SingleEnded(2);
adc3 = ads.readADC_SingleEnded(3);
digitalWrite (ADCorange, LOW); // turn off the green LED to indicate end of adc measurements

delay(500); // pulse length is compute time plus 300 ms make shorter and use 1000 mfd???
digitalWrite (ledPin, LOW); // turn off the LED
//===================================================================== EC drive Pulse LOW

volts0 = ads.computeVolts(adc0);
volts1 = ads.computeVolts(adc1);
volts2 = ads.computeVolts(adc2);
volts3 = ads.computeVolts(adc3); //spare
current = (volts3 - volts2) / seriesR; //amps for 1000 ohms series R = I=V/R
volts = volts1 - volts0; // volts across two central floating probes
EC = current * 1000/volts; // R=V/I conductivity = 1/R = I/V

// get temperature from sensor
temperature = getTemperature();
// Format the temperature with one decimal place
// String temperatureStr = String(temperature, 1);

EC25 = EC / (1+ TemperatureCoef*(temperature-25.0)); // calculate "EC25"

// Print the ESP32's IP address
Serial.print(" IP : ");

Serial.print("temperature = ");Serial.println(temperature);
Serial.print("(temperature-25.0) = "); Serial.println((temperature-25.0));
Serial.print("(1+ TemperatureCoef*(temperature-25.0)) = ");
Serial.println((1+ TemperatureCoef*(temperature-25.0)));

Serial.print("AIN0: "); Serial.print(adc0);
Serial.print(" AIN1: "); Serial.print(adc1);
Serial.print(" AIN2: "); Serial.print(adc2);
Serial.print(" AIN3: "); Serial.println(adc3);

Serial.print(volts0); Serial.print(" V ______");
Serial.print(volts1); Serial.print(" V ______ ");
Serial.print(volts2); Serial.print(" V ______ ");
Serial.print(volts3); Serial.println(" V");

Serial.print("current = ");Serial.print(current*1000);Serial.print(" mA volts = ");Serial.println(volts);
Serial.print(" EC raw = "); Serial.print(EC); Serial.print(" EC25 = "); Serial.println(EC25);
realEC25 = EC25/cal1000_EC25;
Serial.print(" The calibration multiplier in use (cal1000 EC25 = )"); Serial.println(cal1000_EC25);
Serial.print(" real EC25 = "); Serial.println(realEC25);
Serial.println(" ");
Serial.println(" ");
Serial.println(" ");
//-------------------------------------------------------------------------feed the SSD1306 OLED display
display.setCursor(0, 0);
//Display measured data
display.setCursor(0, 20);
display.print("CL ");

//------------------------------------------------------------------------------------------calibration routine

// Store a floating point value
//preferences.putFloat("float_value", 3.14);
// Retrieve a floating point value
//float myFloat = preferences.getFloat("float_value", 0.0);

// preferences.begin("my_variables", false);
// preferences.putString("network_name", ssid);
// preferences.putString("network_password", password);
// cal1000_EC25

CALval = digitalRead(CALswitch);
Serial.print("CALval = "); Serial.println(CALval);
if (CALval==0) {
digitalWrite (ADCorange, HIGH);

cal1000_EC25 = EC25; // this will make EC=1 since realEC25 = EC25/cal1000_EC25 and then probe the ank and EC25 will no longer be 1.0
preferences.putFloat("float_value", cal1000_EC25); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE
preferences.end(); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE

Serial.println("display CAL DONE");
display.setCursor(0, 0);
display.setCursor(0, 33);

delay (4000);
digitalWrite (ADCorange, LOW);

delay(400); // wait


//---------------------------------------------------------------------------------------------------------------LOOP END

The "web page" just shows  21.00,0.42,0.45,1.01  

Circuit and Pulses

Mixture EC Tests

If you build this or similar please - email me