Raspberry Pi Pico Watering System
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:
- RTC
- Moisture sensors
- Pumps
- 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.