Sense the water pH of a hydroponic plant with Arduino Nano 33 BLE Sense and determine if it's right using an Edge Impulse trained model.
Things used in this project
Hardware components
Story
Introduction
I’ve had a hydroponic plant for two years now, and everything went well until this summer. The new flowers, that were supposed to be pink, were green with a few shades of pink. So I searched on Google and I found out that the optimal water pH value for my plant is from 5.5 and 5.8. Well, guess what? I was using tap water for the plant and its pH is 7! So, I decided to buy a pH corrector solution and build my own system to monitor the pH of the water by exploiting the tiny machine learning.
Setup
Hardware
For this project I decided to use Arduino Nano 33 BLE Sense as the central unit, since I've used it in the HarvardX TinyML course. The circuit is simple: a pH sensor (with a water pH probe connected with it) is wired to an Arduino analog input and 3 leds are driven as digital outputs. After I uploaded the sketch to the Arduino, I had to calibrate the sensor. I dipped a pH indicator paper in a glass of tap water to know its pH, and it was 7. Then, I dipped the probe in the same glass and I adjusted the sensor trimmer close to the probe connector until I read values around 7 on the serial monitor. For the specifc sensor I've used, it was usefull adjust also the trimmer close to the sensor pins, to improve the sensibility. For this project I chose three pH edge values:
- pH 4: pH values lower then 5 is too low for the plant
- pH 5: pH values from 5 to 6 are optimal for the plant
- pH 7: pH values around 7 is too high for the plant
To have a solution with pH 4, I've put some lemon juice in the glass of tap water and then I've measured the pH with the indicator paper to verify that it was around 4, so I dipped the probe in the glass and it sensed values around 4, that made me sure that the sensor was well calibrated. For the pH 5 solution, I took another glass of tap water and I put in it a pH corrector to reduce its pH. Both the indicator paper and the pH sensor showed a value around 5, so I decided to use this solution for the hydroponic plant. The three leds in the circuits indicates the pH level:
- red led: turns on when pH value is around 4
- green led: turns on when pH value is around 5
- yellow led: turns on when pH value is around 7
TinyML model
To train the tinyML model I've used the Edge Impulse platform. I've trained the model based on the three edge values, for each of them I've acquired data for five minutes, in order to cover most of the little fluctuations around the value.
After the data collection I've proced to the impulse design, adding the functional blocks I needed.
The "Time series data" block splits the collected data in several windows of the selected size (in this case 2000ms). The "Spectral Analysis" block filters the signal and then performs the spectral analysis. The "Classification" block is the neural network that uses the results of the spectral analysis to learn how to discriminate between the three datasets. To create the impulse, I've clicked on "Save Impulse", then went to "Spectral Features" on the left menu to see the spectral analysis results and clicked on "Save Parameters".
After that, a new window appeared that allowed to generate the features for the neureal network.
Then, by clickink on "NN Classifier" on the left menu, I entered in the neural network configuration enviroment. I've chose to train the network for 500 epochs, in order to achieve a satisfactory level of model accuracy.
The neural network was able to distinguish between the datasets, so I could proced to the next step: add th "Anomaly Detection" block to the impulse design, that allows to classify as anomalies data that the neural netwotk can't identify.
The "Anomaly Detection" section on the left menu allowed me to select the features of which I wanted to detect the anomalies.
To test the model I've proced to the live classification and I've collected data for five minutes, from the ph 5 solution. The model was able to identify that the collected data belonged to the ph 5 dataset. Since the sensed values were included in the datasets, no anomaly was detected.
The model has proven reliable, so I've deployed it to the Arduino. The "Deployment" section of the left menu helped me to do that. I could chose to deploy the impulse as a library or as a firmware, I've decided to create an Arduino library, so I could included it into my sketch.
On the Arduino IDE
To include the model library in the Arduino IDE, I went on Sketch -> Include Library -> Add.ZIP, then I include the library in my sketch and add few lines to the code in order to run the model to the Arduino Nano 33 BLE Sense.
Schematics
Arduino Nano 33 BLE Sense pH sensor
Code
pH.ino
/******* This code is tailored for the specific pH sensor used in the project, you might need to modify some parts according to your sensor ****************/
#include <pHmodel_inferencing.h> //library deployed with Edge Impulse
#define K 1.84f //multiply the voltage value with this constant to obtain //the pH value
/****** for the data forwarder (to collect data with Edge Impulse) *******************/
#define FREQUENCY_HZ 50
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))
/**************************************************************************************/
float voltage;
float ph;
int i;
/******* Leds that indicate the pH level *************/
int ledR = D3; // pH 4
int ledG = D4; // pH between 5 and 6
int ledY = D5; // pH 7
/*******************************************************/
static unsigned long last_interval_ms = 0;
// to classify 1 frame of data you need EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE values
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
// keep track of where we are in the feature array
size_t feature_ix = 0;
void setup() {
voltage = 0.0;
ph = 7.0;
/******* initialize the output pins and turn off the leds **********/
pinMode(ledR, OUTPUT);
pinMode(ledG, OUTPUT);
pinMode(ledY, OUTPUT);
digitalWrite(ledR, LOW);
digitalWrite(ledG, LOW);
digitalWrite(ledY, LOW);
/****************************************************/
Serial.begin(115200);
Serial.println("Ph meter");
analogReadResolution(12); //set the ADC resolution to 12 bits
}
void loop() {
if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();
for (i = 0; i < 10; i++) //collect some analog values from the pH sensor
{
voltage = voltage + analogRead(A0);
delay(10);
}
voltage = voltage / 10.0; //median of the values
voltage = (float)voltage * (5.0 / 4095); //analog value converted in voltage value
ph = K * voltage; //ph value
if (ph <= 1.0f) //the probe is disconnected from the sensor -> all leds on
{
digitalWrite(ledR, HIGH);
digitalWrite(ledG, HIGH);
digitalWrite(ledY, HIGH);
}
if (ph >= 8.90f) //when the voltage increase, the sensed pH increased. But the real pH measured with the indicator paper decrease -> ph = ph - 5 to obtain the real value
//if the pH is on its maximum level
{
ph = ph - 5.0;
}
if ((ph >= 8.0f) && (ph < 8.90f)) //when the voltage increase, the sensed pH increased. But the real pH measured with the indicator paper decrease -> ph = ph - 3.0 to obtain the real value
//when the pH is lower then its maximum level
{
ph = ph - 3.0;
}
Serial.println(ph, 2);
Serial.print("\t"); //it allows the data forwarder to identify the variable ph as the variable with the sensed data
/************************* run the trained model to detected the pH level *******************************/
if (feature_ix == 0) //array index
{
features[feature_ix] = ph;
feature_ix++;
}
if ((feature_ix != 0) && (feature_ix < (EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE - 1)) ) features[feature_ix++] = ph;
ei_printf("Data collection. feature_ix %d EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE %d\n", feature_ix, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
if (feature_ix == (EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE - 1)) //if we are at the end of the array -> end data collection
{
ei_printf("End data collection\n");
ei_impulse_result_t result;
// create signal from features frame
signal_t signal;
numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
// run classifier
EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);
ei_printf("run_classifier returned: %d\n", res);
if (res != 0)
{
ei_printf("Cannot classify\n");
}
else
{
// print predictions
ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
// print the predictions
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf("%s:\t%.5f\n", result.classification[ix].label, result.classification[ix].value);
}
if ( (result.classification[0].value > result.classification[1].value) && (result.classification[0].value > result.classification[2].value) ) //ph 4 detected
{
digitalWrite(ledR, HIGH);
digitalWrite(ledG, LOW);
digitalWrite(ledY, LOW);
}
if ( (result.classification[1].value > result.classification[0].value) && (result.classification[1].value > result.classification[2].value) ) //ph 5 detected
{
digitalWrite(ledR, LOW);
digitalWrite(ledG, HIGH);
digitalWrite(ledY, LOW);
}
if ( (result.classification[2].value > result.classification[0].value) && (result.classification[2].value > result.classification[1].value) ) //ph 7 detected
{
digitalWrite(ledR, LOW);
digitalWrite(ledG, LOW);
digitalWrite(ledY, HIGH);
}
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf("anomaly:\t%.3f\n", result.anomaly);
#endif
}
// reset features frame
feature_ix = 0;
}
}
}
void ei_printf(const char *format, ...)
{
static char print_buf[1024] = { 0 };
va_list args;
va_start(args, format);
int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
va_end(args);
if (r > 0) {
Serial.write(print_buf);
}
}
The article was first published in hackster, September 1, 2021
cr: https://www.hackster.io/Janet89/water-ph-monitoring-for-hydroponic-plant-8f4b61
author: Janet N [https://www.hackster.io/Janet89]