Self-watering plant with Pico + ESP01 + Rpi4

Hi everyone,

Just a disclaimer: I am by no means an expert in electronics. I only started doing this recently, so please forget me if the code isn't very tidy.

So, I have been wanting to create the classic self-watering plant. The idea would be to learn from the experience so I can create other things in the future (i.e., irrigation system or weather station). For this project I have used code I found from two people (code one, code two) and tried combining them as best as I could. I have used a Pico for controlling the sensors (temperature & soil moisture), an ESP01 to give WiFi to the Pico and a Raspberry Pi 4 as the central machine (server). I have used MQTT and made the ESP the client and the Rpi4 the server. I also wanted something that utilised the ESP's deep sleep mode.

In general and with the help of tutorials, I found everything fairly straight forward except from the WiFi implementation with the ESP01 - that was a real steep learning curve. Here was my previous post on how to flash and program an ESP01.

So, the idea would be that Pico gets the readings from sensors, then Pico sends that to the ESP, then the ESP sends the readings to the Rpi4 server. The Pico also tells the ESP to deep sleep and wake up. The Pico is written in Micropython and the ESP in C. I would have loved to have everything in Micropython (i.e., the ESP) because my knowledge of C is extremely limited but the examples that I found that best fitted the project were written in C.

I have now two problems and one question for anyone who would be willing to help:

  • Problem #1: I figured how to send a string all the way from the Pico to the Rpi4 but I am struggling to know how to send a float number. The best I could do was to str() the temperature readings... but it's not ideal (you can see the code at the bottom of this post in the function called 'esp01_temp'). I am also unsure how to properly send data from the ESP01 to the Rpi4. Do you know a better way to do it?
    #This is what I have done
    if (memcmp(msg, "sleep", 5) != 0) {
      client.publish("esp/temp", msg);
    }

    #And this is what I tried doing but did not work... maybe that's just the way I would have done it in Python?
    if (msg != "sleep") {
      client.publish("esp/temp", msg);
  • Problem #2: When I run the Pico+ESP and the Rpi4 server, the Rpi4 server gets every so often a string of the reading plus 'msg' (see screenshot). How could I get rid of that?
  • The question: so far I can get the Rpi4 to listen but is there a simple way to store the readings in the Rpi4? Maybe via a running python script that collects the readings? Then I can process and store them appropriately.

Thank you.

Below is the code for the ESP01:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ESP8266HTTPClient.h>

#define GPIO_STATUS 2

const char* ssid = "MySSID";
const char* password =  "MyPassword";
const char* mqttServer = "192.168.0.27";
const int mqttPort = 1883;
 
WiFiClient espClient;
PubSubClient client(espClient);
 
void setup() {
 
  Serial.begin(115200);

  pinMode(GPIO_STATUS, OUTPUT);
  digitalWrite(GPIO_STATUS, LOW);
  pinMode(LED_BUILTIN, OUTPUT);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println("Connecting to WiFi..");
  }

  digitalWrite(GPIO_STATUS, HIGH); // set status to say we are online
  Serial.flush();

  Serial.println("Connected to the WiFi network");
 
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
 
  while (!client.connected()) {
    Serial.println("Connecting to MQTT...");
 
    if (client.connect("ESP8266Client")) {
 
      Serial.println("connected");  
 
    } else {
 
      Serial.print("failed with state ");
      Serial.print(client.state());
      delay(2000);
 
    }
  }
 
  //client.publish("esp/temp", "Hello from ESP8266");
  client.subscribe("esp/temp");
 
}
 
void callback(char* topic, byte* payload, unsigned int length) {
 
  Serial.print("Message arrived in topic: ");
  Serial.println(topic);
 
  Serial.print("Message:");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
 
  Serial.println();
  Serial.println("-----------------------");
 
}

void send(const char* data) {
  if(data){
    unsigned int len = htonl(strlen(data));
    Serial.write("msg");
    Serial.write((char*)&len, 4);
    Serial.write(data);
    Serial.flush();
  }
}

void fetch(char* url) {
  WiFiClient client;
  HTTPClient http;

  digitalWrite(LED_BUILTIN, LOW); // turn on the LED
  if (http.begin(url)) {  // HTTP
    int httpCode = http.GET();
    if (httpCode > 0) { // httpCode will be negative on error
      if (httpCode == HTTP_CODE_OK) {
        send(http.getString().c_str());
      }
    } else {
      send(http.errorToString(httpCode).c_str());
    }
    http.end();
  } else {
    send("[HTTP] Unable to connect\n");
  }
  digitalWrite(LED_BUILTIN, HIGH); // turn off the LED
}

char buf[128];
char* read_message() {
  if(!Serial.available()) {
    return NULL;
  }
  
  int idx = 0;
  memset(buf, 0, 128);
  while(idx < 7) {
    while(Serial.available() > 0) {    // Checks is there any data in buffer (holds 64 bytes max)
      buf[idx++] = char(Serial.read());           // Read serial data byte
    }
  }
  
  if(memcmp(buf, "msg", 3) == 0) {
    unsigned int size = ntohl(*(unsigned int*)(buf + 3)); 
    while(idx < size + 7 && idx < 120) {
      while(Serial.available() > 0) {   
        buf[idx++] = char(Serial.read());
      }
    }
  } else {
    return NULL;
  }

  return buf + 7;
}

void loop() {
  client.loop();

  char* msg = read_message();
  if(msg) {
    if (memcmp(msg, "sleep", 5) != 0) {
      client.publish("esp/temp", msg);
    }
    else if(memcmp(msg, "sleep", 5) == 0) {
      send("ok");
      digitalWrite(2, LOW);
      ESP.deepSleep(0, WAKE_RF_DEFAULT); // go to sleep
    } else {
      fetch(msg);
    }
  }
}

And here the code for the Pico:

from machine import I2C, Pin, UART
import utime
from seesaw import *
from stemma_soil_sensor import *

from ssd1306 import SSD1306_I2C
import framebuf

from time import sleep
import struct

#led
led = Pin(25, Pin.OUT)
led.value(1)

#Stemma
i2c = I2C(1, scl=Pin(3), sda=Pin(2))
utime.sleep_ms(100)
seesaw = StemmaSoilSensor(i2c)


uart0 = UART(1, baudrate=115200, tx=Pin(4), rx=Pin(5)) # UART tx/rx
ready = Pin(7, Pin.IN, Pin.PULL_DOWN) # used to check the ESP01 status
reset = Pin(6, Pin.OUT) # used to wake the ESP01 from deep sleep

reset.value(1) # ESP01 reset pin, high in normal mode, low to reset  

def send_message(msg):
    print("Send msg ....")
    uart0.write(b'msg')
    uart0.write(struct.pack('!I', len(msg)))
    uart0.write(msg)

    # read the header
    rxData = bytes()
    while len(rxData) < 7: # add timeout ...
        while uart0.any() > 0:
            rxData += uart0.read(1)
            
    # decode the header
    if rxData[0:3] == b'msg':
        size = struct.unpack_from('!I', rxData, 3)[0]
    else:
        return '' # we dont understand the message
        
    # read the full message
    while len(rxData) < size + 7:
        while uart0.any() > 0:
            rxData += uart0.read(1)
            
    return rxData[7:].decode('utf-8')

def esp01_ready():
    while ready.value() == 0:
        print('.', end ="")
        sleep(0.1)
        
    for i in range(4):
        while uart0.any():
            uart0.read(1) # empty any boot up data
        sleep(0.1)
    
    print("Ready")


def esp01_sleep():
    print('sleep')
    send_message('sleep')
    while ready.value() == 1:
        print('.', end="")
        sleep(0.1)
    print('ok')
    

def esp01_wake():
    print("wake")
    reset.value(0)
    sleep(0.1)
    reset.value(1)
    esp01_ready()

def esp01_temp():
    print('temp')
    send_message(str(seesaw.get_temp()))

esp01_ready()

while True:
    sleep(10)
    esp01_temp()
    esp01_sleep()
    sleep(10)
    esp01_wake()```