/********* ESP32 based 4 probe EC meter that can stay in the feed water 24/7. A web page allows you to calibrate the probe against an EC wand that has been calibrated using a reference solution Lesson one C code for Arduino IDE documented at http://www.sunspot.co.uk/ - with many thanks to :- Rui Santos Complete project details at https://RandomNerdTutorials.com/esp32-esp8266-input-data-html-form/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. AND :- https://www.dfrobot.com/blog-1103.html Adafruit_ADS1015 ads; /* Use this for the 12-bit version 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/ see https://randomnerdtutorials.com/esp32-esp8266-input-data-html-form/ definitions :- setEC25 = the EC of the fluid as measured by a wand (or EC value of a calibtation fluid) at calibration time cal_EC25 - multiply EC25 (the number from the electronics) by cal_EC25 to obtain the true EC25 - cal_EC25 is saved to ROM at calibration time. realEC25now = EC25 just measured by the electronics in normal use multiplied by cal_EC25 retrieved from ROM *********/ const int ledPin = 5; //3 volt drive pulse const int LED_orange = 19; //LED to show the measurement period on a 'scope as a pulse - it also comes on and stays on to confirm ready for manual recalibration const int CALswitch = 18; // push a button to take pin 18 low and trigger the manual recalibration sequence // ONLY USE BUTTON AFTER A RECENT UPLOAD OF A NEW REFERENCE EC FROM A WAND OR A CALIBRATION FLUID int CAL_LED = 1; // 1 means button not pressed 0 means pressed so pin 18 is earthed LOW // (if a fluid has just been measured to have a an EC of 1.5 (say) by a wand then CAL_LED set to 1.5 will make the 4 probe read like the wand) //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR int cycle = 0; int trigger_pin = 25; int echo_pin = 26; int distance_cm = 0; //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR float setEC25; float cal_EC25; float EC25 = 0; float realEC25now; #include <Adafruit_ADS1X15.h> Adafruit_ADS1115 ads; /* Use this for the 16-bit version -- default i2c address is 0x48 */ #include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> //--------------------------------------------------------------------forTemperature and SSD1306 OLED display #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); //-------------------------------------------------------------------------------------------------------end // the callibration multiplier has to be saved in the ESP32 ROM and be available after power off. #include <Preferences.h> //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE Preferences preferences; //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE #define SENSOR_PIN 4 // ESP32 pin GPIO17 connected to one wire DS18B20 thermometer sensor DATA pin OneWire oneWire(SENSOR_PIN); // setup a oneWire instance DallasTemperature DS18B20(&oneWire); // pass oneWire to DallasTemperature library String inputMessage; AsyncWebServer server(80); // REPLACE WITH YOUR NETWORK CREDENTIALS const char* ssid = "deco"; const char* password = "uu8diode"; const char* PARAM_INPUT_1 = "input1"; float temperature; // =====================================================HTML web page to handle 1 input field (input1) START const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE HTML><html><head> <title>ESP Input Form</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head><body> <font size="5" face="arial" color="red"> <p>Current EC value: %PLACEHOLDER_EC_NOW% mS/m</p> <p>Current Temperature: %PLACEHOLDER_TEMPERATURE% C</p> <p>Sonar: %PLACEHOLDER_SONAR% cm</p> </font> <font size="5" face="arial" color="blue"> <hr> RESET CALIBRATION<br> <p>cal_EC25 from ROM: %PLACEHOLDER_CALECROM% </p> <form action="/get"> Enter the EC value of this fluid <BR>(e.g. 1.4): <BR><input type="text" name="input1" style="font-size:18pt;height:33px;width:55px;"> <input type="submit" value="Submit" id="Submit" value="Submit" style="height:40px; width:55px" /> </form> <p>Calibration now = %PLACEHOLDER_EC_SET% mS/m</p> <hr> </font> </body></html>)rawliteral"; // ==========================================================HTML web page to handle 1 input field (input1) END //------------------------------------------ data for placeholders in index_html code START String processor(const String& var) { Serial.println(var); if(var == "PLACEHOLDER_TEMPERATURE"){ return String(temperature); } else if(var == "PLACEHOLDER_EC_SET"){ return inputMessage; } else if(var == "PLACEHOLDER_EC_NOW"){ return String(EC25*cal_EC25); } else if(var == "PLACEHOLDER_SONAR"){ return String(distance_cm); } else if(var == "PLACEHOLDER_CALECROM"){ return String(cal_EC25); } return String(); } //------------------------------------------ data for placeholders in index_html code END void notFound(AsyncWebServerRequest *request) { request->send(404, "text/plain", "Not found"); } float volts, volts0, volts1, volts2, volts3 ,current, EC; float voltslow, volts0low, volts1low, volts2low, volts3low; float TemperatureCoef = 0.019; //this changes depending on what chemical we are measuring - seems OK for salt and Masterblend mix float seriesR = 1000; float getTemperature() { DS18B20.requestTemperatures(); // send the command to get temperatures float tempC = DS18B20.getTempCByIndex(0); // read temperature in °C return tempC; } //-----------------------–––––––-------------------------------------------------------------------------------------------SETUP START void setup(void) { Serial.begin(115200); // 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 (LED_orange, OUTPUT); pinMode(CALswitch, INPUT); //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR - no include needed pinMode(trigger_pin, OUTPUT); pinMode(echo_pin, INPUT); //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR //------------------------------------------------------------------------------------for SSD1306 display start if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println(F("SSD1306 allocation failed")); for(;;); } delay(2000); display.clearDisplay(); display.setTextSize(3); //3 lines of text squeezed in display.setTextColor(WHITE); display.setCursor(0, 0); // Display static text display.println("DISPLAY"); display.setCursor(0, 33); display.println("START"); display.display(); Serial.println("startup done"); //----------------------------------------------------------------------------------------SSD1306 display end DS18B20.begin(); // initialize the DS18B20 sensor WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); if (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println("WiFi Failed!"); return; } Serial.println(); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); // for curl from raspberry pi - example - my curl data is a comma delimited string at http://192.168.1.182/curldata.html // Define a route to serve the HTML page server.on("/curldata.html", HTTP_GET, [](AsyncWebServerRequest *request) { // get temperature from sensor float temperature = getTemperature(); // Format the temperature with one decimal place String temperatureStr = String(temperature, 1); Serial.println(temperatureStr); Serial.println(temperature); // put the temperature into the string curlhtml to send as a text item for curl from Pi on my network String curlhtml = temperatureStr; curlhtml += ","; curlhtml += EC; curlhtml += ","; curlhtml += EC25; curlhtml += ","; curlhtml += realEC25now; curlhtml += ","; curlhtml += distance_cm; Serial.print("data for curl = "); Serial.println(curlhtml); request->send(200, "text/html", curlhtml); }); // Send web page with input fields to client server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, processor); //NB I added processor and changed " to % }); // Send a GET request to <ESP_IP>/get?input1=<inputMessage> - in my example - http://192.168.1.182/get?input1=1.4 for fluid of EC 1.4 server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) { String inputParam; // GET input1 value on <ESP_IP>/get?input1=<inputMessage> if (request->hasParam(PARAM_INPUT_1)) { inputMessage = request->getParam(PARAM_INPUT_1)->value(); inputParam = PARAM_INPUT_1; setEC25 = inputMessage.toFloat(); // tis is the value we keyed in on the we page - the known EC of the fluid from a wand cal_EC25 = setEC25/EC25; // the multiplier that turns EC25 into trueEC (= EC25*cal_EC25) and then later probe the tank and trueEC25 will slowly change preferences.putFloat("float_value", cal_EC25); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE save cal_EC25 in ROM preferences.end(); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE } else { inputMessage = "No message sent"; inputParam = "none"; } Serial.print("inputMessage = "); Serial.println(inputMessage); Serial.print("EC25 = "); Serial.println(EC25); Serial.print("new cal_EC25 = "); Serial.println(cal_EC25); request->send(200, "text/html", "<!DOCTYPE HTML><html><head><title>Confirm</title><meta name='viewport' content='width=device-width, initial-scale=1'></head><body><font size='5' face='arial' color='red'> The EC value you sent was " + inputMessage + "<br><a href=\"/\">Return to Home Page</a></font></body></html>"); }); server.onNotFound(notFound); server.begin(); 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); } } //-----------------------–––––––-------------------------------------------------------------------------------SETUP END void loop(void) //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>. LOOP START { // multiply the EC25 number by cal_EC25 to give real EC25 for the cal liquid as measured by a wand preferences.begin("my_variables", false); //§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE - get cal_EC25 from ROM (saved even if power goes off) cal_EC25 = preferences.getFloat("float_value", 0.0); //§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE Serial.print("cal_EC25 from ROM = "); Serial.println(cal_EC25); int16_t adc0, adc1, adc2, adc3; //use for the high pulse int16_t adc0low, adc1low, adc2low, adc3low; // use to check the adc inputs are zero just before the next pulse rises - use 1 megohms to drain any leakage to 0 Serial.print (" =====retrieved value of cal_EC25 - multiply EC25 by this for realEC25now"); Serial.println(cal_EC25); //=============================================================================================== EC drive Pulse HIGH digitalWrite (ledPin, HIGH); // turn on the white LED delay(10); // measure this long after pulse rises digitalWrite (LED_orange, HIGH); // turn on the orange 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 (LED_orange, LOW); // turn off the green LED to indicate end of adc measurements delay(10); // 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); 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.println(WiFi.localIP()); 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.println("----------------------------------------------"); 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.println("----------------------------------------------"); 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 across cntral probe pair = ");Serial.println(volts); Serial.print(" EC raw = "); Serial.print(EC); Serial.print(" EC25 = "); Serial.println(EC25); realEC25now = EC25*cal_EC25; Serial.print(" The calibration multiplier in use (cal_EC25 = )"); Serial.println(cal_EC25); Serial.print(" realEC25now = "); Serial.println(realEC25now); Serial.println(" "); Serial.println(" "); Serial.println(" "); //-----------------------------------------------------------------------feed the SSD1306 OLED display START display.clearDisplay(); display.setTextSize(3); display.setTextColor(WHITE); display.setCursor(0, 0); //Display measured data display.print("EC="); display.println(realEC25now); display.setCursor(0, 22); display.print("T="); display.println(temperature); display.setCursor(0, 44); display.print("D="); display.println(distance_cm); display.display(); //-------------------------------------------------------------------------feed the SSD1306 OLED display END //----------------------------------------------------------------------------------------------------------------------------------------------calibration routine start // "setEC25" is the number you key in on the webpage - it is held in RAM while the ESP32 calculates the "cal_EC25" multiplier // that is stored in ROM - it turns the electronics measurement into a genuine EC value. CAL_LED = digitalRead(CALswitch); // value 1 for button not pressed Serial.print("CAL_LED = "); Serial.println(CAL_LED); // If you hold the cal button down CAL_LED will be 0 for the next line and we enter this manual routine to make the EC value reported for // this fluid equal to the number keyed in from the web page. If you key in 1.4 (say) the probe will report 1.4 but if you leave the probe in the fluid // for a while it may drift a little as the temperature of the probe becomes more equal to the fluid temperature. // so press it again to make the reading 1.4 (say) again - no need to key in the wand value again unless the power went off. // A calibration multiplier, cal_EC25, will be created and stored in ROM // Then the reading will follow changes in the liquid EC and report them continuously to the web. if (CAL_LED==0) { digitalWrite (LED_orange, HIGH); // now in the manual re-calibration routine // EC25 is just a number given by the electronics from the voltage and current measurements. It tracks up and down as the true EC goes up and down. // EC25 might perhaps go from 0.5 to 1.0 when the setEC25 goes from 2 to 4 - so in this case we would multiply EC25 by (setEC25/EC25) = 2 // setEC25 is the EC of the fluid as measured by a calibrated wand or the known EC if instead we are using a standard salt solution of known EC. // this calibration factor must be (setEC25 as we key it in)/(EC25 measured at calibration time) // So at calibration time we key in the true fluid EC and then press the cal button and cal_EC25 is saved to ROM // we save setEC25/EC25 in ROM as cal_EC25 - ths is then the factor we multiply future EC25 readings by. // We saved the cal_EC25 multiplier first by keying in setEC25 (the known EC of he liquid) from the web page. // shortly after with the probe in the same solution we repeat the calculation having waited a while for readings to settle cal_EC25 = setEC25/EC25; // the multiplier that turns EC25 into trueEC (= EC25*cal_EC25) and then later probe the tank and trueEC25 will slowly change preferences.putFloat("float_value", cal_EC25); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE save cal_EC25 in ROM preferences.end(); //§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ PREFERENCES CODE Serial.println("display CAL DONE"); display.clearDisplay(); display.setTextSize(3); display.setTextColor(WHITE); display.setCursor(0, 0); display.print("CAL"); display.setCursor(0, 33); display.print("DONE"); display.display(); delay (4000); digitalWrite (LED_orange, LOW); } //----------------------------------------------------------------------------------------------------------------------------------------------------calibration routine end delay(5000); // wait // measure the base line to check that the next pulse starts clean at zero volts adc0low = ads.readADC_SingleEnded(0); adc1low = ads.readADC_SingleEnded(1); adc2low = ads.readADC_SingleEnded(2); adc3low = ads.readADC_SingleEnded(3); volts0low = ads.computeVolts(adc0low); volts1low = ads.computeVolts(adc1low); volts2low = ads.computeVolts(adc2low); volts3low = ads.computeVolts(adc3low); Serial.println("ADC samples taken just before drive pulse - all low?"); Serial.print(volts0low); Serial.print(" V ______"); Serial.print(volts1low); Serial.print(" V ______ "); Serial.print(volts2low); Serial.print(" V ______ "); Serial.print(volts3low); Serial.println(" V"); Serial.println(" "); //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR digitalWrite(trigger_pin, LOW); delayMicroseconds(2); digitalWrite(trigger_pin, HIGH); delayMicroseconds(10); digitalWrite(trigger_pin, LOW); long duration = pulseIn(echo_pin, HIGH); distance_cm = (duration / 2) / 29.09; Serial.print("SONAR - cm to water surface = "); Serial.println(distance_cm); //±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±± SONAR delay(10); // allow the next pulse to rise at least this long after measuring the base line } //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> LOOP END |
Home screen e.g.
http://192.168.1.182 |
Confirmation screen. I entered 1.4 into the home screen form and pressed submit. If I view http://192.168.1.182/curldata.html The page just shows a string like :- 19.2,0.69,0.77,1.40,154 (temperature,EC,EC25,trueEC25,sonar distance to surface) A raspberry pi on the network grabs this by curl and uses gnuplot to plot a graph of temperature and EC values as a function of the time of day. |
All on a board for a small box (a bit too
small!) A 3.3 to 5 volt converter board was added under the display for the ultrasonic ranger board. |
The A to D board, the bipolar capacitors, the
Zener diodes and the 1 Megohm resistors are under the ESP32 |
The version of the probe that fits a
15mm/half inch plastic pipe. |
Brushes and thermometer soldered in before
covering in hot glue. The cable goes up the pipe. |