A Raspberry Pico Bird Camera

Hot on the heels of, if not success, then at least lack of failure in my rather simple project to build a Raspberry Pico e-paper weather display, what I’d really like to have is a camera to photograph the very infrequent visits to the feeder in the garden.

There are plenty of cameras on the market but I have something quite specific in mind. It would upload images to a server, so that I didn’t have to go and check a memory card, and ideally be solar powered so I didn’t have to go and recharge or swap batteries. Many of the available cameras seem to use disposable batteries and that just seems like a bad thing in this day and age.

At least I have a clear idea of what I want, but I don’t have a clear idea of whether it’s achievable. But I have a plan.

  1. Check I can get a Pico and camera combo working. Like my first step with e-paper, given that I don’t really know what I’m doing I might not even get a basic image out
  2. Post the photos to a Wifi-connected server, which will probably just be my desktop in the first instance
  3. Get a motion detector working
  4. Put the two together to take photos based on my own movement
  5. Test to see if bird movements trigger the motion detector, and if they do see if the image quality is good enough. I’m more interested in knowing what kinds of birds come to visit and how often than winning Wildlife Photographer Of The Year
  6. Assess power consumption and feasibility of a solar supply
  7. If solar is reasonable, decide on whether to get a retail package with a USB micro, or to go with something more home-spun, that could potentially power multiple devices in the future (because I have many more plans!)
  8. Make some kind of weatherproof case, possibly just for the “camera” part, but possibly also the solar panel (and battery) depending on size requirement. I’m as useless with my hands as I am with electronics so this might be the hardest part.

For components, as well as another Pico W, I’m going for an Arducam Mini 2MP Plus OV2640 SPI camera and a Mini PIR sensor, although since they’re so cheap I’ll give the slightly larger (yet strangely cheaper) regular PIR sensor a try too. I have a very small garden, at only 9m long, and a mesh transmitter right near the door already so Wifi range shouldn’t be an issue.

A Arducam Mini 2MP Plus OV2640 SPI camera
The Arducam Mini 2MP Plus OV2640 SPI camera
The smaller Mini PIR sensor

And so a new adventure begins.

Pico E-ink Weather Display: Version 2 (or 2.9?)

For my first Pico experiments I bought the smallest e-ink display I could, because it was the cheapest and who knows if I’d even pass the first hurdle of making a prototype. I’d also bought the wrong type, with a cable connection instead of a nice and easy Pico-compatible HAT. So I ended up with a little cable nest like this:

The display looked decent, barring some pixel tweaking on the icons, but I still thought that a slightly bigger display would be better at a glance. This time I bought the correct HAT type too, and 2.9inches seemed just a bit bigger, but not too big. The display also has red as well as black, but I didn’t plan to make use of it this time around. It crossed my mind that weather warnings might be good in red, but they might also be harder to see than black on white – which kind of defeats the purpose.

To cut lots of medium-length stories short, I also discovered I had a bit more space on the 2.9 inch display, so I added sunrise and sunset to the display, as well as spacing things out a bit more. The display is 296×128 pixels, which isn’t a lot more than 250×120, but I think the pixels are also a little bit bigger. It also helps that the “height” (128 pixels, or 128 bits) is a whole number of bytes, so my rotation fudge doesn’t result in the top left of the screen being at coordinate (0,6). The 2.9b “driver” file worked straight out of the box this time too. On the other hand, if I’d bought the right display the first time I wouldn’t have learnt as much.

The HAT fits pretty nicely too:

And I’m quite happy with the end result (even if I’m not happy with the weather). I hand-tweaked a few of the images, such as the sunrise and sunset (as they’re only 16×16 and every pixel counts), but for the most part converting to 1-bit image at a 50% gray threshold looks good enough. Even with some of the text at exactly the same pixel height as on the 2.13 inch display it just looks a lot crisper:

The only thing left to do now is to try and frame it somehow so it doesn’t look like a scruffy bit of tech hanging by a USB cable in the hallway.

Optimising RAW Images For An E-ink Display

Whilst building the e-ink weather display for the Pico I’ve had to make a number of RAW images. I’ve been doing this by opening PNGs in Photoshop, converting them to grayscale, changing them to Bitmap (or 1-bit images) and then, because Photoshop won’t let me save a 1-bit image as RAW, I convert them back to grayscale and save as RAW.

For the 65×65 image for the daily weather summary this comes out to 4,224 bytes. Not a lot in the modern world, but when the Pico has such small storage space, and as I was planning to do day and night versions of each thumbnail, I needed two copies of fifteen different icons, making 123kb in total.

It was a waste of space as the e-ink display only displays 1-bit data, and therefore every image was taking eight times more storage space than necessary.

There are scripts available for creating 1-bit RAW images from PNGs, but I decided to experiment with another very clever thing that somebody else (Lucky Resistor) had written: the Micropython Bitmap Tool.

Essentially, it takes an image and turns it into a Python (or Micropython) byte string, so the image of the sun (first one above) becomes this:

bytearray(
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7c'
        b'\xfe\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x0f\x1f\x1f\x3e\x1c\x08\x00\x80\x80\xc0\xe0\xe0\xe0'
        b'\xf0\xf1\xf1\xf1\xf0\xe0\xe0\xe0\xc0\x80\x80\x00\x08\x1c\x3e\x1f\x0f\x0f\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x80\x00\x00\x00\xf0\xfc\xfe\xff\x1f\x07\x03\x01'
        b'\x01\x01\x00\x00\x00\x00\x01\x01\x03\x07\x1f\xff\xfe\xfc\xe0\x00\x00\x00\x80\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x80\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x03\x03\x03\x03\x01\x00\x00\x00\x00\x07\x1f\x7f\xff\xf8\xf0\xe0'
        b'\xc0\x80\x80\x80\x80\x80\x80\x80\xc0\xe0\xf0\xfc\xff\x7f\x1f\x07\x00\x00\x00\x00\x01\x03\x03\x03\x03\x01\x01\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\xe0\xf0\xf8\xfc\x7c\x3c\x08\x00\x00\x01\x01'
        b'\x03\x03\x07\x07\x87\x87\x87\x07\x07\x03\x03\x01\x01\x00\x00\x08\x3c\x7c\xfc\xf8\xf0\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x1f\x7f\x7f\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        b'\x00\x00\x00\x00\x00\x00\x00\x00\x00')

The advantage of this is that it takes a lot less space.

The disadvantage, as I found, is that loading images and exporting them and pasting into an array to do image lookups was really boring. And in the end, once I’d written a bit of code to look up which bytearray to use, instead of which image file to load, I had a 77kb file i.e. I’d only saved about 40% versus the grayscale versions.

The lesson, I guess, is that maybe next time I should learn how to make 1-bit RAW images with something like ImageMagick. The saving was so negligible for the effort it took that for the tiny images on the display I just stuck with my 8-bit RAW files – all 46.8kb of them.

Customising The E-ink Weather Display

With all the pieces in place to allow me to draw to the e-ink screen, I made some 8-bit RAW images and wrote values to the display. It worked, but it looked a bit crusty, not least because the Framebuffer.text method for putting text onto the screen is a fixed size, and a fixed font. Remember that I’m aiming for something like this, which uses a few different font sizes:

As with so many things, this has been solved before, with a piece of work that I think is absolute genius going by the catchy title of “font_to_py“. Basically, it’s a Python command line tool that inputs a font file (like a ttf) and some parameters, such as pixel size, and outputs a Python file containing all the data necessary to render the font as byte data. The files are very small too, with a 20 pixel font “compiling” down to 17kb.

Then you just import the Python files, which in my case were three different sizes of Calibri, and the functions to draw the font to a Framebuffer:

import calibri_36, calibri_15, calibri_12
from writer import Writer

And to draw to the display something like:

wri = Writer(display_buffer, calibri_36)
Writer.set_textpos(display_buffer, text_top, text_left)
wri.printstring(max_daily, True)

To print the value of the maximum daily temperature to the screen (via the Framebuffer). Somewhat strangely, the coordinates are passed in as (top, left) rather than (x, y). I also tweaked writer.py slightly to make sure it handled transparency (i.e. 0 pixels), by changing:

self.device.blit(fbc, s.text_col, s.text_row)

to

self.device.blit(fbc, s.text_col, s.text_row, 1 if invert else 0)

in the _printchar method.

The fonts still looked a little weedy, though, and rather than make a bold version of Calibri my own slightly hacky method was just to print the same characters four times over, offset by 1 pixel each time:

Writer.set_textpos(display_buffer, text_top, text_left)
wri.printstring(max_daily, True)
Writer.set_textpos(display_buffer, text_top, text_left - 1)
wri.printstring(max_daily, True)
Writer.set_textpos(display_buffer, text_top + 1, text_left)
wri.printstring(max_daily, True)
Writer.set_textpos(display_buffer, text_top + 1, text_left - 1)
wri.printstring(max_daily, True)

The results were good enough.

For degree symbols I used Framebuffer.ellipse, and to calculate the position to draw the symbol in writer.py even provides a very useful “how wide is this piece of text?” function:

text_width = get_string_display_width(wri, max_daily)
display_buffer.ellipse(text_left + text_width + 4, text_top + 5, 4, 4, 0)
display_buffer.ellipse(text_left + text_width + 4, text_top + 5, 3, 3, 0)
display_buffer.ellipse(text_left + text_width + 4, text_top + 5, 2, 2, 0)

Note that the ellipse (circle) is also drawn three times to create a faux bold effect.

The last part was some trigonometry as I wanted to display wind direction as an arrow in a circle.

Putting it all together resulted in something usable, albeit with some pixel tweaking required to sharpen some of the smaller images:

An epaper display showing temperature, weather summary, wind direction and rainfall duration

The last thing was to schedule it to update every hour. I experimented with lightsleep and deepsleep, which are supposedly low power modes, but the Pico just never seemed to wake up (and from Googling that appears to be a known issue), so I just took to a sleep function:

HOURS_SLEEP = 1
sleep_time = 60 * 60 * HOURS_SLEEP

Having made something functional, I now had a list of things I wanted to improve:

  1. The size. It’s a little bit too small. I decided to order a 2.9inch e-paper display, and this time get one with the right HAT
  2. The four hour summary is useful, but the weather at 7am is irrelevant once that time of day is passed. I wanted to change it to a rolling twenty-hour period
  3. What happens if I hit an error? I might think the weather’s the same every day and not notice the program has crashed. I needed to show the date/time of last update, and the “feels like” temperature is probably the least useful metric to replace it with
  4. The sleep function updated after an hour, but every update took maybe twenty seconds. If I was going to show update time, I wanted it to be on the hour, not to drift by a couple of minutes per day

I placed an order for another Pico (W, with headers, of course) and the larger e-ink display and waited.

Show Me The Weather

After connecting the e-ink display to the Pico and managing to get it to display in landscape mode I felt I’d solved the unknowns in making this e-ink weather display. Now it was time to decide what I was going to show, and how I was going to show it.

One Python example I’d found used a weather API called OpenWeather, so I went and signed up. Yet no matter what I seemed to do in terms of requesting API keys, and even waiting a day for activation, I was never able to make a valid request. In the meantime, with a little more Googling I found Meteo’s API. It turned out to be very easy to work with and had everything I’d need.

I chose some data fields I wanted to display and opened Photoshop to start a basic layout.

Design for an e-ink weather display showing maximum and minimum daily temperature, overall weather, wind gusts, and a summary for every 4 hours

I wanted minimum and maximum temperatures during the day, a nice summary icon, average wind speed, and then a bit more info about how it would feel which is where the 9 degrees (“feels like” temperature), 20mph (wind gusts), and 2hrs (duration of rainfall during the day) came from. Below that is a little summary of what the weather looks like at four hour intervals..

The API call is just a request to a URL, such as this:

https://api.open-meteo.com/v1/forecast?latitude=XXXXX&longitude=XXXXX&hourly=temperature_2m,windspeed_10m,winddirection_10m,windgusts_10m,weathercode&daily=temperature_2m_min,temperature_2m_max,sunrise,sunset,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant,precipitation_hours,apparent_temperature_max,weathercode&timezone=GMT&windspeed_unit=mph

You can request more fields in either the hourly or daily summary, and the documentation was pretty clear too.

The return is JSON and easily parsed with Micropython. I wrote a small module to do it all:

import network, rp2, time
import urequests
import json
import sys
from io import BytesIO

LATITUDE = 'XXXXX'
LONGITUDE = 'XXXXX'

BASE_URL = 'https://api.open-meteo.com/v1/forecast?' 
URL = BASE_URL + 'latitude=' + LATITUDE + '&longitude=' + LONGITUDE + '&hourly=temperature_2m,weathercode&daily=temperature_2m_min,temperature_2m_max,sunrise,sunset,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant,precipitation_hours,weathercode&timezone=GMT&windspeed_unit=mph'

# API documentation at https://open-meteo.com/en/docs
class Meteo:
    def __init__(self):
        self.json = None
        return
    
    def get_data(self):
        # Make GET request
        response = urequests.get(URL)
        if response.status_code != 200:
            print("Error")
            print(response.status_code)
            print(response.content);
            response.close();
            raise Exception("Error getting weather data " + response.status_code)
        else:
            self.json = response.json();
            response.close();
            return self.json;

I’d call it from a function, just so it was easier to test:

def getWeatherData():
    weather = meteo.Meteo();
    weather.get_data();
    return weather;

And accessing the data is just like accessing any Python object e.g. to get the maximum daily temperature:

max_daily = str(round(weather.json["daily"]["temperature_2m_max"][0]))

So far so simple, and I was quickly on to drawing all of this to the display, which is the subject of the next post.

Micropython E-ink Display Rotation

By default, sending pixels from a Raspberry Pico to a Waveshare e-ink display will result in a portrait image. In the case of the 2.13inch display I started with, this means a screen of 122 pixels wide by 250 pixels high.

But what if you want to display in portrait mode? As someone who’s used to the simple world of CSS it was actually fun to get back into the world of bits and pixels again.

Just like the ZX Spectrum, where I started programming, the display is stored as a list of bytes where each byte is eight consecutive pixels, either set to 1 to display a dark pixel or 0 for white (or clear).

In Micropython, this is stored as a byte array. So the code that sets up the display looks like this:

self.buffer_black = bytearray(self.height * self.width // 8)

Thankfully, there’s a helper module in the shape of Framebuffer which makes changing the value of these pixels much easier. It contains methods to write text, lines, rectangles and circles and, most usefully, to draw images at arbitrary coordinates using the blit command.

Not only does blit make it easier to draw, say, a 50×50 image onto the 122×250 display, but it also makes it much easier to draw an 8-bit image onto the 1-bit display. You start by setting a Framebuffer object with its data source as the byte array, such as:

self.imageblack = framebuf.FrameBuffer(self.buffer_black, self.width, self.height, framebuf.MONO_HLSB)

Where the first argument is the byte array, with and height are self explanatory, and framebuf.MONO_HLSB means that this framebuffer is a mono (1-bit) image, with horizontal bits stored as a byte.

To draw an image on top, assuming you have the pixel data in a variable called image_buffer, you can do something like:

self.imageblack.blit(framebuf.FrameBuffer(image_buffer, width, height, framebuf.GS8), x, y, key)

Where width and height are the width and height of the image, framebuf.GS8 signifies an 8-bit grayscale image, x and y are the coordinates to draw to, and key tells the blit method which pixels in the image it should treat as transparent. This allows you to draw just the black pixels onto the screen without the “white” pixels (or 0 bits) overwriting anything that’s already on the screen. Because the frame buffer takes the image_buffer bytearray as a parameter but changes the pixels in it by reference, any changes to self.imageblack (which is a Framebuffer object) are applied to the bytearay in self.buffer_black – which represents the actual pixel data.

So far so simple as far as drawing an image to the screen goes, assuming you’re still working in portrait mode. I made myself a helper function to load raw files from the on-board memory:

def drawImage(display_buffer, name, x, y, width, height, background=0xFF):
    image_data = get_image_data(name)
    if image_data:
        display_buffer.blit(image_data, x, y, background)
    else:
        image_buffer = bytearray(width * height)
        filename = 'pic/' + name + '.raw'
        with open (filename, "rb") as file:
            position = 0
            while position < (width * height):
                current_byte = file.read(1)
                # if eof
                if len(current_byte) == 0:
                    break
                # copy to buffer
                image_buffer[position] = ord(current_byte)
                position += 1
        file.close()
        display_buffer.blit(framebuf.FrameBuffer(image_buffer, width, height, framebuf.GS8), x, y, background)

Drawing a landscape image instead of portrait should be as simple as:

  1. Set up a Framebuffer with a width equal to the e-ink display height, and a width equal to the e-ink display width
  2. Draw to the Framebuffer
  3. Transpose columns of pixels into rows in a new bytearray
  4. Send to the e-ink display

It seemed like exactly the sort of thing that someone would already have tackled, and so it proved. So with some Googling and adjustment of width/height variables to suit the display I was working with I came to this:

def rotateDisplay(display):
    epaper_display = bytearray(epd.height * epd.width // 8)
    x=0; y=-1; n=0; R=0
    for i in range(0, epd.width//8):
        for j in range(0, epd.height):
            R = (n-x)+(n-y)*((epd.width//8)-1)
            pixel = display[n]
            epaper_display[R] = pixel
            n +=1
        x = n+i+1
        y = n-1
    epaper_buffer = framebuf.FrameBuffer(epaper_display, epd.width, epd.height, framebuf.MONO_HLSB)
    return epaper_display;

And I have to confess I didn’t worry too much about the details. Not until I ran it and the output was complete garbage.

It took a while to work out what I was doing wrong, but once I found the problem the solution made complete sense: I was inputting a Framebuffer stored as MONO_HLSB but the function above transposed the position of bytes, not individual bits. In other words, chunks of 8 pixels were being moved to the right place on the screen, but forming a column rather than a row. No wonder it looked bad.

The solution was simple: start with a Framebuffer of the type MONO_VLSB, where the bytes represent a column of 8 pixels, not a row. Then the transposition naturally mapped to MONO_HLSB.

I ended up with something like this:

display = bytearray(epd.height * epd.width // 8)
    display_buffer = Screen(display, epd.height, epd.width, framebuf.MONO_VLSB)
    display_buffer.fill(0xff)
    --- 
      Draw somethings here
    ---
    epaper_display = rotateDisplay(display)

where Screen was an extension of the Framebuffer class, necessary because the width and height parameters weren’t accessible in a native Framebuffer object. It just felt nicer to store them “in place” rather than pass another parameter around.

class Screen(framebuf.FrameBuffer):
    def __init__(self, display, width, height, encoding):
        self.display = display
        self.width = width
        self.height = height
        self.encoding = encoding
        return

The final wrinkles was that the e-ink display was 122 pixels “wide” (or now 122 pixels high, as I was trying to work in landscape mode), and therefore not a number that formed a whole number of bytes. Because of the way I was treating the screen, it meant my top left origin was at the coordinates (0, 6), which felt a bit nasty but was something I could live with. The alternative would have been to transpose the whole thing down 6 pixels using another Framebuffer, but frankly I couldn’t be bothered.

Writing To The E-ink Display With Micropython

I’d gotten as far as running a few lines of code on the Pico, so the next thing was to work out if I could display anything at all on the e-ink display. Since I’d already messed up by buying the wrong HAT type, I still had a small doubt about whether I would get the two to work together at all.

Waveshare has its own Github respository so I headed there to track down the correct Python file. I knew I needed one of the files that started with “Pico_ePaper-2.13-“, but which one?

There was a little stick on the display telling me I had a “v3”, but working out whether I had an A, B, C or D model was a mixture of guesswork and deduction. The file for both B and C appeared to support multiple colours, which is not what I had, so it wasn’t those. I had nothing else to go on, so I tried both A and D. Neither seemed to work properly, although I did get the screen to flicker a bit so something was happening.

I can’t even remember how I finally got the thing working, but it appears to be through a hybrid of files that started as Pico_ePaper-2.13-B_V4.py with all the code that referred to the red image data removed. The whole process was made more difficult by the fact that I seemed to have messed up connecting two of the pins, having not spotted that there was a pin labelled “GND” right between “GP21” and “GP22” on the Pico board.

This was my “driver” module:


from machine import Pin, SPI
import framebuf
import utime

EPD_WIDTH       = 122
EPD_HEIGHT      = 250

RST_PIN         = 12
DC_PIN          = 8
CS_PIN          = 9
BUSY_PIN        = 13

class EPD:
    def __init__(self):
        self.reset_pin = Pin(RST_PIN, Pin.OUT)
        
        self.busy_pin = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP)
        self.cs_pin = Pin(CS_PIN, Pin.OUT)
        if EPD_WIDTH % 8 == 0:
            self.width = EPD_WIDTH
        else :
            self.width = (EPD_WIDTH // 8) * 8 + 8
        self.height = EPD_HEIGHT
        
        self.spi = SPI(1)
        self.spi.init(baudrate=4000_000)
        self.dc_pin = Pin(DC_PIN, Pin.OUT)
        
        
        self.buffer_black = bytearray(self.height * self.width // 8)
        self.init()

    def digital_write(self, pin, value):
        pin.value(value)

    def digital_read(self, pin):
        return pin.value()

    def delay_ms(self, delaytime):
        utime.sleep(delaytime / 1000.0)

    def spi_writebyte(self, data):
        self.spi.write(bytearray(data))

    def module_exit(self):
        self.digital_write(self.reset_pin, 0)

    # Hardware reset
    def reset(self):
        self.digital_write(self.reset_pin, 1)
        self.delay_ms(50)
        self.digital_write(self.reset_pin, 0)
        self.delay_ms(2)
        self.digital_write(self.reset_pin, 1)
        self.delay_ms(50)


    def send_command(self, command):
        self.digital_write(self.dc_pin, 0)
        self.digital_write(self.cs_pin, 0)
        self.spi_writebyte([command])
        self.digital_write(self.cs_pin, 1)

    def send_data(self, data):
        self.digital_write(self.dc_pin, 1)
        self.digital_write(self.cs_pin, 0)
        self.spi_writebyte([data])
        self.digital_write(self.cs_pin, 1)
        
    def ReadBusy(self):
        print('busy')
        while(self.digital_read(self.busy_pin) == 1): 
            self.delay_ms(10) 
        print('busy release')
        self.delay_ms(20)
        
    def TurnOnDisplay(self):
        self.send_command(0x20)  # Activate Display Update Sequence
        self.ReadBusy()
        
    def TurnOnDisplayPart(self):
        self.send_command(0x20)        
        self.ReadBusy()

    def SetWindows(self, Xstart, Ystart, Xend, Yend):
        self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
        self.send_data((Xstart>>3) & 0xFF)
        self.send_data((Xend>>3) & 0xFF)

        self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
        self.send_data(Ystart & 0xFF)
        self.send_data((Ystart >> 8) & 0xFF)
        self.send_data(Yend & 0xFF)
        self.send_data((Yend >> 8) & 0xFF)
        
    def SetCursor(self, Xstart, Ystart):
        self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
        self.send_data(Xstart & 0xFF)

        self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
        self.send_data(Ystart & 0xFF)
        self.send_data((Ystart >> 8) & 0xFF)
    

    def init(self):
        print('init')
        self.reset()
        
        self.ReadBusy()   
        self.send_command(0x12)  #SWRESET
        self.ReadBusy()   

        self.send_command(0x01) #Driver output control      
        self.send_data(0xf9)
        self.send_data(0x00)
        self.send_data(0x00)

        self.send_command(0x11) #data entry mode       
        self.send_data(0x03)

        self.SetWindows(0, 0, self.width-1, self.height-1)
        self.SetCursor(0, 0)

        self.send_command(0x3C) #BorderWavefrom
        self.send_data(0x05)

        self.send_command(0x18) #Read built-in temperature sensor
        self.send_data(0x80)

        self.send_command(0x21) #  Display update control
        self.send_data(0x80)
        self.send_data(0x80)

        self.ReadBusy()
        
        return 0       
        
    def display(self):
        self.send_command(0x24)
        for j in range(0, self.height):
            for i in range(0, int(self.width / 8)):
                self.send_data(self.buffer_black[i + j * int(self.width / 8)])   
        self.TurnOnDisplay()

    
    def Clear(self, colorblack):
        self.send_command(0x24)
        for j in range(0, self.height):
            for i in range(0, int(self.width / 8)):
                self.send_data(colorblack)
        self.TurnOnDisplay()

    def sleep(self):
        self.send_command(0x10) 
        self.send_data(0x01)
        
        self.delay_ms(2000)
        self.module_exit()
        

I haven’t yet managed to track down what all the send_command statements actually do, but I feel sure there must be some documentation for them somewhere.

Anyway, finally, I had the equivalent of “hello world” on the display, without really knowing how. Also, I had discovered that the epaper display naturally preferred to display in portrait mode, which wouldn’t work for the layout I had in mind for my weather display.

    epd = EPD()
    epd.Clear(0xff)
    epd.imageblack = framebuf.FrameBuffer(epd.buffer_black, epd.width, epd.height, framebuf.MONO_HLSB)
    
    epd.imageblack.fill(0xff)
    #epd.imagered.fill(0xff)
    epd.imageblack.text("Waveshare", 0, 10, 0x00)
    epd.imageblack.text("ePaper-2.13-B", 0, 25, 0x00)
    epd.imageblack.text("RPi Pico", 0, 40, 0x00)
    #epd.imagered.text("Hello World", 0, 55, 0x00)
    epd.display()
    epd.delay_ms(2000)
    
    epd.imageblack.vline(10, 90, 40, 0x00)
    epd.imageblack.vline(90, 90, 40, 0x00)
    epd.imageblack.hline(10, 90, 80, 0x00)
    epd.imageblack.hline(10, 130, 80, 0x00)
    epd.imageblack.line(10, 90, 90, 130, 0x00)
    epd.imageblack.line(90, 90, 10, 130, 0x00)
    epd.display()
    epd.delay_ms(2000)

It felt like I’d conquered most of the unknowns anyway, so now onto showing some actual weather.

Getting Started With The Raspberry Pi Pico

Raspberry Pi Pico

The whole Raspberry Pi craze has passed me by before. Mainly because I have no competency whatsoever in electronics, and when it comes to using it just as a small computer I’ve had no need for one.

I don’t really know what drew me to the Raspberry Pi Pico, but it just looked like such a fun little thing to experiment with. I think it’s partly that it’s not just a small computer, because there’s no keyboard or mouse input, no monitor, and the only way to program it (that I know of so far) is through an attached computer. And with a very low power consumption it comes across more as a component to be used in something else than a mini-computer. Plus it can be programmed in Micropython, which is more-or-less a subset of the Python language with which I’m familiar (although no expert, but it looks pretty much the same as every other language to be honest).

But given my previously mentioned lack of electronics competency that alone wouldn’t be enough to lure me in, but the more I started to look at it the more it appeared that competency in electronics really wasn’t al that important. So many components, whether e-ink or LCD displays, cameras or motion sensors seem to be almost plug-and-play. Even I couldn’t mess that up.

Finally, I’d recently installed Hive Smart Radiators, and although the app is okay for controlling them, I’d been thinking how nice it would be to have small, unobtrusive controls for them in key rooms so it’s harder to forget to turn heating off in rooms that aren’t going to be used for most of the day, or turning them back on again if someone comes home early. I’d looked at cheap Android tablets that I could then build an app for, but for a kind of “at a glance” display I’d either have to have the screens always on, or look at something more expensive like OLED. Android, or cheap old model Apple tablets, just seemed like overkill, but also lacking in flexibility.

So a Pico looked fun and an avenue to explore, but where to start?

Starting Small: An E-ink Weather Display

The thing that piqued my interest was tiny e-ink displays, like the ones from Waveshare. I knew they’d be low power and ideal for infrequently updated displays, and although maybe not quite what I wanted for smart radiator controls (but then maybe they will be), I couldn’t get the idea of building something with one out of my head.

Waveshare 2.13 inch e-ink display

I decided I’d start with something smaller than a heating controller and settled on a weather display. My partner is always asking me if she’s going to need an umbrella that day, despite having a smartphone sitting in front of her, so I could kid myself it would be useful to install by the front door. Plus, some of the first search results I’d pulled up when looking at how hard it might be to program an e-ink were weather displays. (Albeit for a Raspberry Pi, not the Pico, and with a larger screen than I was planning. But it was some working code to give me direction.)

I had my project, and it looked achievable, so I just needed to buy some kit and start playing.

Buying The Right Equipment

In trying to buy just a Pico and an e-ink display my lack of knowledge almost set me back. Just in time, I realised that I needed a Pico W, which is a Pico with an onboard wifi controller, otherwise my weather display wouldn’t be able to retrieve weather information. (Although it’s possible to add an external controller, it’s much easier just to have it there at the start.)

Secondly, I needed a Pico with soldered headers to make the connection to the display much easier. In fact, as I don’t (yet) own a soldering iron, it was the only way to make the connection possible. The Pico with soldered headers costs a few pounds more, but easily worth the extra convenience. As shown in the image below, the headers are the “spikes” that make it easy to attach cables or other devices.

Raspberry Pi Pico with soldered headers

I knew then that if I bought an e-ink display with a HAT connection (which apparently stands for “hardware attached on top” and is easy to remember because it really does look lik you’re putting a hat on it) all I’d have to do is drop it on top.

The first confusion came when I tried to attach the two. The Pico had two rows of pins, but the display only had a single connection row. It had me stumped for a while, but then I worked out that I’d bought the wrong type of HAT. Apparently the e-ink display I’d bought hat a HAT configuration for a standard Raspberry Pi, not for a Pico.

This is what not to buy for a Pico:

Waveshare e-ink display with Raspberry Pi HAT connection

Whereas what I wanted would attach like this:

Waveshare e-ink display attached via HAT to Raspberry Pi PIco

It wasn’t a disaster, though as it also came with a cable connection. It just came down to working out how on earth to join the two things together.

A cable for a Waveshare e-ink display

Googling for the connections wasn’t too hard, as it’s all on the Waveshare site, but connecting them was made harder than it should be due to failing close-up eyesight which meant that matching microscopic pin numbers on the Pico to coloured cables from the display wasn’t as easy as it could have been.

Cable connections from a Raspberry Pi Pico to Waveshare e-ink display

Installing the Micropython firmware and connecting to the Pico with the Thonny IDE was straightforward and I was able to run an example piece of code to turn the onboard LED on and off within a few minutes. But I had a feeling that getting the e-ink display to show anything was going to be a little harder. For various reasons, so it proved, but that’s going to be covered in the next post.