Using Enviro Grow (Pico W Abroad) to water my plants.

Introduction

I’d like to have a watering system for the Raspberry Pi Pico. I already have the hardware: Enviro Grow (Pico W Aboard). Yes I even have the accessory kit. If I’m honest, I have two of them. But the software is a bit simplistic, and I’m not sure it’s good for plants: It checks the moisture level, and if it goes below a threshold, it waters the plant. But that means that the soil stays wet all the time, because as soon as it dries up a bit, it gets watered again.

Anyway, I’m not a botanist, so I’d like some more control over this system. It’s going to run while I’m on holiday, so I want it to be resilient.

There is no plan yet, just trying to get the hardware to work with my code. Unfortunately, it’s not easy to debug directly on the hardware, because there is no way to solder pins to the board. So instead I’m going to code on another Raspberry Pi Pico and then use load the uf2 file and see what happens. Pretty terrible. But at least I’ll document whatever I find.

Once I have the pieces in place, I’ll write something to drive everything.

HTTP POST

The only way I can talk to the outside world is using HTTP. I don’t think that I can log anything from the board (as far as I can see). A bit of a limitation. But in any case I want to call a web server with data, so POST seems reasonable.

The first step is to get something working. HTTP is fairly straight-forward, using (roughly):

    struct altcp_pcb *conn = altcp_tcp_new();

    altcp_arg(conn, &context);
    altcp_sent(conn, tcp_sent_cb);
    altcp_recv(conn, tcp_recv_cb);

And then to connect (to my local machine):

    ip_addr_t ip_addr = IPADDR4_INIT_BYTES(192, 168, 4, 67);
    err_t err = altcp_connect(conn, &ip_addr, PORT, tcp_connected_cb);

To send some data:

    strcpy(buffer, "GET / HTTP/1.1\r\n\r\n");
    uint16_t sent = strlen(buffer);
    altcp_write(conn, buffer, sent, TCP_WRITE_FLAG_COPY);
    altcp_output(conn);

Then I can see the callbacks being called with reasonable values.

All pretty straight-forward, but I’m using 192.168.4.67 with HTTP. What I really want is to call an API - that would require some work.

Note that this section is called “HTTP POST” but didn’t use POST. I’m running a simple HTTP server locally (using python3 -m http.server), and it doesn’t support POST.

HTTPS GET

Time to try HTTPS again. Last time wasn’t a great success, but it’s been a couple of years and things have moved on.

Well, I have to say, things have changed. It is much, much easier to get TLS going. The answer is to simply follow the examples! Check out https://github.com/raspberrypi/pico-examples/tree/master/pico_w/wifi/tls_client, there’s not much else to say.

Briefly, though, in CMakeLists.txt:

target_link_libraries(<project>
        pico_cyw43_arch_lwip_poll
        pico_lwip_mbedtls
        pico_mbedtls
        )

Copied over mbedtls_config.h from the examples, and added to lwipopts.h:

/* TCP WND must be at least 16 kb to match TLS record size
   or you will get a warning "altcp_tls: TCP_WND is smaller than the RX decrypion buffer, connection RX might stall!" */
#undef TCP_WND
#define TCP_WND  16384

#define LWIP_ALTCP               1
#define LWIP_ALTCP_TLS           1
#define LWIP_ALTCP_TLS_MBEDTLS   1

#define ALTCP_MBEDTLS_DEBUG  LWIP_DBG_ON

(should be “decryption”, but I just copied and pasted).

Then in my code:

    struct altcp_tls_config *conf = altcp_tls_create_config_client(certificate, strlen(certificate) + 1);
    altcp_allocator_t allocator = {
        .alloc = altcp_tls_alloc,
        .arg = conf,
    };

    context.pcb = altcp_new(&allocator);
    if (context.pcb == NULL) {
    	/* error handling */
        return;
    }

    altcp_arg(context.pcb, &context);
    altcp_sent(context.pcb, altcp_sent_cb);
    altcp_recv(context.pcb, altcp_recv_cb);

where context holds struct altcp_pcb *pcb as well as other stuff.

The callbacks:

static err_t altcp_connected_cb(void *arg, struct altcp_pcb *conn, err_t err);
static err_t altcp_sent_cb(void *arg, struct altcp_pcb *conn, u16_t len);
static err_t altcp_recv_cb(void *arg, struct altcp_pcb *conn, struct pbuf *p, err_t err);

Then to make a connection:

        ip_addr_t ip_addr = IPADDR4_INIT_BYTES(18, 154, 78, 95);
        mbedtls_ssl_set_hostname(altcp_tls_context(context.pcb), "d1.awsstatic.com");
        err_t err = altcp_connect(context.pcb, &ip_addr, PORT, altcp_connected_cb);
        /* error handling */

Yes, this is very rough-and-ready, just a proof of concept. The actual request:

        strcpy(buffer, "GET /about-aws/About-aws_header.d9a81e96ac9d1bd04438be8db995bec7d2816b9f.jpg HTTP/1.1\r\n"
                       "Host: https://d1.awsstatic.com\r\n"
                       "Accept: image/*\n\n"
                       "\r\n");
        altcp_write(context.pcb, buffer, strlen(buffer), TCP_WRITE_FLAG_COPY);
        altcp_output(context.pcb);

The certificate was downloaded in the browser, just a long string in PEM format.

That’s about it, really. It just worked!

Well, I’m lying, it didn’t really work, I got an error page from AWS, but I don’t really care (probably missing User-Agent or something). The point is that the TLS handshake was successful.

Device Exploration

Before I continue with HTTPS POST (which I really just want for “debugging”), I can explore the board with Python, to avoid wasting too much time. What I’m interested in:

  1. RTC
  2. Moisture sensors
  3. Pumps
  4. LEDs (of course)

Temperature, light, humidity - they can wait (but maybe it’s all easy).

Action LED

The LED is connected to GPIO 6. Some simple code to make it blink:

import board
import digitalio
import time

pin = digitalio.DigitalInOut(board.GP6)
pin.direction = digitalio.Direction.OUTPUT
while True:
    pin.value = True
    time.sleep(0.5)
    pin.value = False
    time.sleep(0.5)

Status Switch

The switch is connected to GPIO 7. Some simple code to turn the LED on when the button is pressed:

import board
import digitalio
import time

led = digitalio.DigitalInOut(board.GP6)
led.direction = digitalio.Direction.OUTPUT

button = digitalio.DigitalInOut(board.GP7)
button.direction = digitalio.Direction.INPUT

while True:
    led.value = button.value
    time.sleep(0.01)

RTC (PCF85063A)

I found the datasheet at https://www.nxp.com/docs/en/data-sheet/PCF85063A.pdf

Making the Alarm LED blink once a second:

import busio
import board
from adafruit_bus_device.i2c_device import I2CDevice

SDA = board.GP4
SCL = board.GP5

with busio.I2C(SCL, SDA) as i2c:
    device = I2CDevice(i2c, 0x51)
    bytes = bytearray(2)
    bytes[0] = 0x01
    bytes[1] = 0x06
    with device:
        device.write(bytes)

Writing 0x07 instead of 0x06 turns the Alarm LED off. Writing 0x00 turns it on (I think that it actually makes it blink 32768 times a second, which looks “on” to me).

So it should be pretty straight-forward: write the address, then the value(s). Write the address, then read the values.

Let’s see. I’ll try to set the time, pause for 5 seconds, then read the time back.

(Later)

Water Pumps

That was simple:

import board
import digitalio
import time

pump = digitalio.DigitalInOut(board.GP12)
pump.direction = digitalio.Direction.OUTPUT

pump.value = True
time.sleep(1)
pump.value = False

Sadly “PUMP3” is actually the one marked as “A”, but that’s OK.

Pretty straight-forward. I’ll just make sure that “PUMP2” (GPIO 11) is “B” and “PUMP1” (GPIO 10) is “C”, and indeed that’s the case.

Moisture Sensors

This is a bit of a riddle, and I’ll probably have to check the code from Pimoroni. According to this, the sensors work using pulse-frequency modulation. Whether that’s what the RP2040 actually sees, or something between the sensor and the input pin converts it to an analogue signal - I don’t know.

According the code, it’s the former. We sample the digital value (0 or 1), and based on the number of changes per second, it’s possible to come up with some number. The website mentions “Hz value between about 0 and 30”. 30 Hz means 30 times a second, or about once every 33ms. Sampling at more than 60 Hz should be enough to determine the frequency (apparently, I’m not an expert). So checking once every 10ms should be OK.

Very crude method:

import board
import digitalio
import time

sensor = digitalio.DigitalInOut(board.GP15)
sensor.direction = digitalio.Direction.INPUT
sensor.pull = digitalio.Pull.DOWN

last_value = sensor.value
changes = 0
for i in range(99):
    time.sleep(0.01)
    current_value = sensor.value
    if current_value != last_value:
        changes += 1
        last_value = current_value

print("changes: {}".format(changes))

I get 21 when the sensor is in the soil (which is quite wet), and 43 when it’s not touching anything. The number of changes is roughly twice the frequency, so about 10 Hz when it’s in the soil, 22 Hz when it’s not. “A smaller number is wetter, and a larger number is drier”.

I have a feeling that PIO could be used here, but that’s another complication.

I’ve also confirmed that SENSOR_3 is “A”, so this is similar to the pumps.

Other Things

There’s a light sensor, an environmental sensor and a Piezo buzzer. I don’t know how much I want to play with them. It’s nice to know the temperature and check whether the soil gets dry faster when it’s hotter (hint: it does, I don’t think I need a sensor to tell me that).

Conclusion

This is becoming a long post, so I’ll continue in a separate post.