Inky Frame
Pimoroni in the UK sell a cute 5.7” e-paper photo frame, with an integrated Raspberry Pi Pico W. They’re pretty pricey, but colour e-paper is very cool. I’ve had it for a while, and had grand visions of building a customisable YAML driven home assistant front-end for it, to display stats about the house, weather, inside and outside temperatures, and other nerdery. Like all the best laid plans though, it never really got anywhere. Time and other priorities meant it was left languishing in a drawer and I never got the project past a barely functional prototype.
Well last weekend, while procrastinating building a greenhouse base (it was raining…), I remembered it and thought I could do something more simple. The Met Office publishes a few APIs, one of which provides images for map overlays.
I hacked together a couple of scripts, one which runs locally on my
network pulls a day’s worth of imagery from the Met Office API and
processes it to be easy to display on the Inky Frame. Those images are
then hosted on the same pi’s web server, where the Raspberry Pi Pico W
can grab them conveniently over Wi-Fi. It’s probably not so hard to do
this all in-device on the Pico W itself, but I wanted to experiment with
different dithering, resizing, and cropping – all of which are much
faster to do on a computer than a Pico.
#!/usr/bin/env python3
import json
import re
import datetime
import urllib.request
= "https://data.hub.api.metoffice.gov.uk/map-images/1.0.0"
API_URL = """YOUR KEY HERE"""
MET_OFFICE_API_KEY = "YOUR ORDER NUMBER HERE"
ORDER_NAME = ""
FILENAME_BASE
# Get a list of files in the job, filter out dupes.
= urllib.request.Request(API_URL + "/orders/" + ORDER_NAME + "/latest")
req "apikey", MET_OFFICE_API_KEY)
req.add_header("Accept", "application/json")
req.add_header(= urllib.request.urlopen(req).read()
resp = json.loads(resp.decode("utf-8"))
data
= []
files for file in data["orderDetails"]["files"]:
= file["fileId"]
fileId if "_+00" not in fileId and "ts" in fileId:
= datetime.datetime.strptime(file["runDateTime"], "%Y-%m-%dT%H:%M:%S%z")
timestamp = int(re.findall(r"ts\d\d?", fileId)[0][2:])
timestep = re.split(r"_", fileId)[0]
series = timestamp + datetime.timedelta(hours=timestep)
timestamp 'fileId' : fileId, 'timestamp': timestamp, 'series': series})
files.append({
# Download each file from the list of files.
for file in files:
= urllib.request.Request(API_URL + "/orders/" + ORDER_NAME + "/latest/" + file['fileId'] + "/data")
req "apikey", MET_OFFICE_API_KEY)
req.add_header("Accept", "image/png")
req.add_header(= urllib.request.urlopen(req).read()
resp = FILENAME_BASE + "{}-{}.png".format(file['series'], file['timestamp'].strftime("%Y-%m-%d-%H"))
filename with open(filename, 'wb') as f:
f.write(resp)print(filename)
This produces a bunch of PNG files with names like
temperature-2025-02-14-10.png
(the temperature forecast for
Valentine’s Day 2025 at 10:00). The Micro Python libraries available for
the Inky Frame only support JPEG compression, so I have written a bash
script which calls imagemagick to convert all the output PNGs into
JPEGs. The PNGs returned by the Met Office do not include a map
background layer (despite what their sample data shows…) so I composite
in a background image for the British Isles. I experimented with using
imagemagick’s dithering, but the automatic dithering done by the JPEG
library on the Inky Frame produced (to me) a more pleasing result. This
script also copies the files to the web server’s hosted directory.
#!/bin/bash
cd /home/simon/weather
# Delete old files
rm -f temperature-*.png
rm -f mean-*.png
rm -f total-*.png
rm -f *.jpeg
for f in `python3 /home/simon/weather/metoffice.py`; do
convert -composite -compose Multiply british-isles.png $f -crop 600x448+200+352 ${f%.*}.jpeg;
done
rm -f /var/www/html/weather/*.jpeg
cp *.jpeg /var/www/html/weather/
Then, on the Inky Frame itself I hacked together some Micro Python to download the correct file based on the setting of the RTC, and update the display accordingly.
import machine
import gc
import time
import asyncio
import ntptime
import jpegdec
import json
import sdcard
import uos
from urllib import urequest
from picographics import PicoGraphics, DISPLAY_INKY_FRAME as DISPLAY
import inky_helper as ih
= 16
IF_MISO = 18
IF_CLK = 19
IF_MOSI = 22
IF_SD_CS
= "http://raspberrypi/weather/"
BASE_URL = "/sd/current-weather.jpeg"
FILENAME =60
UPDATE_INTERVAL= ""
time_string
= [ {"url": "temperature", 'string': "Temperature" }, {"url": "mean", 'string': "Pressure" }, {"url": "total", 'string': "Precipitation" } ]
MODES = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "November", "December"]
MONTH
= { "url" : None }
state
gc.collect()
ih.led_warn.on()= None
sd while True:
try:
= machine.SPI(0, sck=machine.Pin(18, machine.Pin.OUT), mosi=machine.Pin(19, machine.Pin.OUT), miso=machine.Pin(16, machine.Pin.OUT))
sd_spi = sdcard.SDCard(sd_spi, machine.Pin(22))
sd break
except OSError:
= None
sd
gc.collect()
ih.inky_frame.button_e.led_on()1)
time.sleep(
ih.inky_frame.button_e.led_off()print("failed to connect to SD card, retrying")
ih.led_warn.off()"/sd")
uos.mount(sd,
gc.collect()
try:
from secrets import WIFI_SSID, WIFI_PASSWORD
ih.network_connect(WIFI_SSID, WIFI_PASSWORD)except ImportError:
print("Create secrets.py with your Wi-Fi credentials")
reset()
# Get some memory back, we really need it!
gc.collect()print("Wi-Fi Connected")
= PicoGraphics(DISPLAY)
graphics = graphics.get_bounds()
WIDTH, HEIGHT "bitmap8")
graphics.set_font(
# Turn any LEDs off that may still be on from last run.
ih.clear_button_leds()
ih.led_warn.off()
def time_to_url(current_t, mode):
global time_string
= BASE_URL + "{}-{:02d}-{:02d}-{:02d}-{:02d}.jpeg".format(MODES[mode]["url"], current_t[0], current_t[1], current_t[2], current_t[3])
url = "{} {} {} {} {:02d}:00".format(MODES[mode]["string"], current_t[2], MONTH[current_t[1]], current_t[0], current_t[3])
time_string return url
def set_time():
# Correct the time while we're at it
try:
ih.inky_frame.set_time()print(time.localtime())
except e:
print(e)
"Unable to set time: {}".format(e))
show_error(
ih.led_warn.on()return
def update(url):
print("Update")
# Grab the image
try:
ih.pulse_network_led()= urequest.urlopen(url)
socket
gc.collect()= bytearray(1024)
data with open(FILENAME, "wb") as f:
while True:
if socket.readinto(data) == 0:
break
f.write(data)
socket.close()del data
gc.collect()except OSError as e:
print(e)
"Unable to download image")
show_error(
ih.led_warn.on()finally:
ih.stop_network_led()
def show_error(text):
4)
graphics.set_pen(0, 10, WIDTH, 70)
graphics.rectangle(1)
graphics.set_pen(5, 16, 635, 2)
graphics.text(text,
def draw():
global state
try:
= jpegdec.JPEG(graphics)
jpeg 1)
graphics.set_pen(
graphics.clear()
gc.collect()
jpeg.open_file(FILENAME)
jpeg.decode()0)
graphics.set_pen(0, 0, WIDTH, 25)
graphics.rectangle(1)
graphics.set_pen(5, 5, WIDTH, 2)
graphics.text(time_string, except OSError as e:
"Unable to display image! OSError: {}".format(e))
show_error(
ih.inky_frame.button_b.led_on()except Exception as e:
"Unable to display image! Error: {}".format(e))
show_error(
ih.inky_frame.button_c.led_on()finally:
graphics.update()
def save_state(data):
with open("/state.json", "w") as f:
f.write(json.dumps(data))
f.flush()
def load_state():
global state
= json.loads(open("/state.json", "r").read())
data if type(data) is dict:
= data
state else:
= { "url" : None }
state
set_time()
while True:
# Disable deep sleep while we check for an update
ih.inky_frame.vsys.init(machine.Pin.OUT)
ih.inky_frame.vsys.on()
= state['url']
old_url = time_to_url(time.localtime(), 0)
url if old_url != url:
print("New URL: {}".format(url))
update(url)
draw()'url'] = url
state[
save_state(state)30)
time.sleep(
# Sleep for one minute
1) ih.inky_frame.sleep_for(
Battery, sleep, and Thonny
I had surprising friction trying to take my main.py
from
something that worked under Thonny. The update of the screen appears to
be asynchronous, but that seems to mean that calling deep sleep allows
the Pi Pico to sleep before the screen has updated causing meaning it
never draws the JPEG to the display. Thus far, still not working – I am
probably doing something bone-headed, but I can’t see what. For now, it
works when connected to USB (external power disables the sleep function)
so it can sit on my desk powered by my monitor.