17. Februar 2024
DIY: Tisch-Uhr mit e-Ink-Display (Waveshare Pico e-Paper 3.7) und Raspberry Pi Pico
In diesem Artikel beschreibe ich, wie ich eine Tisch-Uhr mit e-Ink-Display programmiert & gebaut habe. Neben der Uhrzeit wird auf dem Display auch noch die aktuelle Außentemperatur, der Tageshöchstwert, die aktuelle Leistung meines Balkonkraftwerks und der Tagesertrag des BKW angezeigt.
Material
- Raspberry Pi Pico WH (ca. 8 € – z.B. von Berrybase)
- 3,7 Zoll E-Paper-/E-Ink- Display von Waveshare (ca. 25 € – z.B. von Berrybase)
- ein Gehäuse aus Holz (ca. 3 €), Pappe oder aus dem 3D-Drucker
- eine Holzleiste aus dem Baumarkt (ca. 2 €)
- ein Micro-USB-Kabel + Netzteil
- Cuttermesser, (Säge,) Heißkleber, Sprühlack
Ich habe mich bei diesem Projekt für das 3,7 Zoll E-Paper-/E-Ink- Display von Waveshare entschieden, da es dieses Display in einer speziellen Ausführung für den Raspberry Pi Pico WH gibt. Da der Pico WH bereits über eine eingelötete Stiftleiste verfügt, kann er direkt auf das Display aufgesteckt werden. Löten ist somit zu keiner Zeit erforderlich.
Ursprünglich wollte ich die Uhr mit 3 oder 4 AA-Batterien betreiben. Die Kabel des Batteriefachs kann man wahlweise am Pico anlöten oder man benutzt ein Batteriefach mit USB-Ausgang und betreibt das Gerät per USB. Warum ich mich gegen beide Lösungen entschieden habe, werde ich später noch erläutern.
Als Gehäuse habe ich mich für ein kleines Holzkästchen aus dem „TEDI“ entschieden. Das Kästchen hatte zufällig genau die gewünschten Maße (10 x 15 cm) und das weiche Kieferholz ließ sich leicht mit einem Cuttermesser schneiden. Doch zunächst zur Programmierung:
Programmierung
Die Programmierung hatte ich mir vor dem Start dieses Projektes etwas einfacher vorgestellt. In der Annahme, dass der Pico und die Waveshare-Displays in der „Maker-Szene“ weit verbreitet sind, dachte ich, dass es bestimmt viele Tutorials und Beispiele gibt, wie das Display zu bespielen ist. Dem ist allerdings nicht so. Und auch Chat GPT konnte micht bei meinem Einstieg in MicroPython in vielen Fällen nur sehr bedingt unterstützen.
Ganz entscheidend weitergeholfen, hat mir dieses Modul auf GitHub, das auch Teil meines Codes ist: https://github.com/phoreglad/pico-epaper
Basierend auf den Beipielcodes habe ich folgenden Code entwickelt:
from Pico_ePaper import Eink
import framebuf
from writer import Writer
import utime
from utime import sleep
from machine import RTC
import gc
import time
from fonts import inter_bold_44
import machine
import network
import urequests
import network
import sys
import time
import usocket as socket
import ustruct as struct
import img.n0 as n0
import img.n1 as n1
import img.n2 as n2
import img.n3 as n3
import img.n4 as n4
import img.n5 as n5
import img.n6 as n6
import img.n7 as n7
import img.n8 as n8
import img.n9 as n9
import img.space as space
import img.line1 as line1
import img.line2 as line2
ssid = 'YOUR_SSID'
password = 'YOUR_PASSWORD'
class DummyDevice(framebuf.FrameBuffer):
def __init__(self, width, height, buf_format):
self.width = width
self.height = height
self._buf = bytearray(self.width * self.height // 8)
super().__init__(self._buf, self.width, self.height, buf_format)
self.fill(1)
def display_digit(image, x, y):
img_tmp = framebuf.FrameBuffer(image.img_bw, image.width, image.height, framebuf.MONO_HLSB)
epd.blit(img_tmp, x, y)
def fetch_weather_data():
url = "https://api.open-meteo.com/v1/forecast?latitude=52.028953201377355&longitude=8.533312390444614¤t=temperature_2m&daily=temperature_2m_max&timezone=Europe%2FBerlin&forecast_days=1"
response = urequests.get(url)
if response.status_code == 200:
weather_data = response.json()
response.close()
return weather_data
else:
print("Fehler bei der API-Anfrage. Status-Code:", response.status_code)
response.close()
return None
def fetch_power_data():
url = "YOUR_API_URL"
response = urequests.get(url)
if response.status_code == 200:
power_data = response.json()
response.close()
return power_data
else:
print("Fehler bei der API-Anfrage. Status-Code:", response.status_code)
response.close()
return None
def display_text(writer, text, x, y):
Writer.set_textpos(dummy, x, y)
writer.set_clip(row_clip=True, col_clip=True, wrap=True)
writer.printstring(text, invert=True)
# Winterzeit / Sommerzeit
GMT_OFFSET = 3600 * 1 # 3600 = 1 h (Winterzeit)
#GMT_OFFSET = 3600 * 2 # 3600 = 1 h (Sommerzeit)
# NTP-Host
NTP_HOST = 'pool.ntp.org'
# Funktion: WLAN-Verbindung
def wlanConnect():
wlan = network.WLAN(network.STA_IF)
if not wlan.isconnected():
print('WLAN-Verbindung herstellen')
wlan.active(True)
wlan.connect(ssid, password)
for i in range(10):
if wlan.status() < 0 or wlan.status() >= 3:
break
print('.')
time.sleep(1)
if wlan.isconnected():
print('WLAN-Verbindung hergestellt / WLAN-Status:', wlan.status())
else:
print('Keine WLAN-Verbindung')
print('WLAN-Status:', wlan.status())
# Funktion: Zeit per NTP holen
def getTimeNTP():
NTP_DELTA = 2208988800
NTP_QUERY = bytearray(48)
NTP_QUERY[0] = 0x1B
addr = socket.getaddrinfo(NTP_HOST, 123)[0][-1]
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.settimeout(1)
res = s.sendto(NTP_QUERY, addr)
msg = s.recv(48)
finally:
s.close()
ntp_time = struct.unpack("!I", msg[40:44])[0]
return time.gmtime(ntp_time - NTP_DELTA + GMT_OFFSET)
# Funktion: RTC-Zeit setzen
def setTimeRTC():
# NTP-Zeit holen
tm = getTimeNTP()
machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
# WLAN-Verbindung herstellen
wlanConnect()
# Zeit setzen
setTimeRTC()
epd = Eink(rotation=180)
# Zeitstempel für die Aktualisierung
last_update = 0
while True:
rtc = RTC()
datetime = rtc.datetime()
gc.collect()
gc.mem_free()
# Überprüfen, ob seit der letzten Aktualisierung 5 Minuten vergangen sind
current_time = utime.time()
if current_time - last_update >= 300:
img_tmp = framebuf.FrameBuffer(space.img_bw, space.width, space.height, framebuf.MONO_HLSB)
epd.blit(img_tmp, 30, 340)
epd.blit(img_tmp, 143, 340)
# WLAN-Verbindung herstellen
wlanConnect()
# Wetterdaten abrufen
weather_data = fetch_weather_data()
if weather_data:
current_temperature_2m = weather_data["current"]["temperature_2m"]
daily_temperature_2m_max = weather_data["daily"]["temperature_2m_max"][0]
print("Aktuelle Temperatur_2m:", current_temperature_2m, "°C")
print("Tägliche Temperatur_2m_max:", daily_temperature_2m_max, "°C")
gc.collect()
gc.mem_free()
print("Speicherverbrauch nach manueller GC:", gc.mem_alloc() / 1024, "KiB")
dummy = DummyDevice(epd.width, epd.height, framebuf.MONO_HLSB)
wri = Writer(dummy, inter_bold_44)
display_text(wri, f'{current_temperature_2m}', 346, 35)
display_text(wri, f'{daily_temperature_2m_max}', 346, 141)
epd.blit(dummy, 0, 0, key=1, ram=epd.RAM_BW)
gc.collect()
gc.mem_free()
print("Speicherverbrauch nach manueller GC:", gc.mem_alloc() / 1024, "KiB")
# Zeitstempel für die letzte Aktualisierung aktualisieren
last_update = current_time
'''
# Stromdaten abrufen
power_data = fetch_power_data()
if power_data:
now = power_data["now"]
total_sun_day = power_data["total_sun_day"]
gc.collect()
gc.mem_free()
print("Speicherverbrauch nach manueller GC:", gc.mem_alloc() / 1024, "KiB")
dummy = DummyDevice(epd.width, epd.height, framebuf.MONO_HLSB)
wri = Writer(dummy, inter_bold_44)
display_text(wri, f'{now}', 406, 35)
display_text(wri, f'{total_sun_day}', 406, 141)
epd.blit(dummy, 0, 0, key=1, ram=epd.RAM_BW)
gc.collect()
gc.mem_free()
print("Speicherverbrauch nach manueller GC:", gc.mem_alloc() / 1024, "KiB")
'''
hour_first_digit = datetime[4] // 10
hour_second_digit = datetime[4] % 10
display_digit(eval(f'n{hour_first_digit}'), 30, 50)
display_digit(eval(f'n{hour_second_digit}'), 30 + eval(f'n{hour_first_digit}').width, 50)
display_digit(space, 30 + eval(f'n{hour_first_digit}').width + eval(f'n{hour_second_digit}').width, 50)
minute_first_digit = datetime[5] // 10
minute_second_digit = datetime[5] % 10
display_digit(eval(f'n{minute_first_digit}'), 30, 50 + eval(f'n{hour_first_digit}').height)
display_digit(eval(f'n{minute_second_digit}'), 30 + eval(f'n{minute_first_digit}').width, 50 + eval(f'n{hour_first_digit}').height)
display_digit(space, 30 + eval(f'n{minute_first_digit}').width + eval(f'n{minute_second_digit}').width, 50 + eval(f'n{hour_first_digit}').height)
img_tmp = framebuf.FrameBuffer(line1.img_bw, line1.width, line1.height, framebuf.MONO_HLSB)
epd.blit(img_tmp, 10, 340)
'''
img_tmp = framebuf.FrameBuffer(line2.img_bw, line2.width, line2.height, framebuf.MONO_HLSB)
epd.blit(img_tmp, 10, 400)
'''
gc.collect()
gc.mem_free()
print("Speicherverbrauch nach manueller GC:", gc.mem_alloc() / 1024, "KiB")
epd.show()
epd.sleep()
sleep(60)
epd.reinit()
Code-Sprache: HTML, XML (xml)
Hierzu einige Erläuterungen:
Uhrzeit
Während der Entwicklung am USB-Port des Rechners wird der Pico vom Rechner mit der aktuellen Uhrzeit versorgt. Da der Pico keine interne Batterie hat, muss die Uhrzeit im Standalone-Betrieb von einem Zeitserver abgerufen werden (s. Z. 80-125)
Bilder
Die großen Ziffern und die Labels unten habe ich als Grafik eingebaut. Da das Display normale Bilddaten nicht verarbeiten kann, müssen alle Bilder vorher in Bytearrays konvertiert werden. Hierfür habe ich das Skript „convertimage.py“ geschrieben, das lokal im Terminal mit folgendem Aufruf ausgeführt werden kann
python3 convertimage.py
Code-Sprache: CSS (css)
Fonts
Die kleinen Ziffern unten sind Text, der mit einem Custom-Font (inter-bold.ttf) dargestellt wird. Da das Display keine TrueType-Fonts darstellen kann, müssen diese ebenfalls vorher konvertiert werden. Hierfür gibt es das Skript „font_to_py.py“, das lokal im Terminal mit folgendem Aufruf ausgeführt werden kann:
python3 font_to_py.py Inter-Bold.ttf 23 inter_bold_23.py -x
Code-Sprache: CSS (css)
Wetterdaten
Die Wetterdaten beziehe ich von der kostenlosen Wetter-Api von Open Meteo. Die Konfiguration sollte selbsterklärend sein.
Daten des Balkonkraftwerks
Für die Daten des Balkonkraftwerks habe ich eine eigene API geschrieben. Weitere Erläuterungen dazu findest du hier: www.fbnfrtg.de/so-kannst-die-lokale-api-des-apsystems-ez-1-auch-mit-php-ohne-python-nutzen/
Raspberry Pi Pico „Autostart“
Wenn das Programm automatisch auf dem Pico starten soll, musst du die Datei in „main.py“ umbenennen.
Probleme bei der Entwicklung
Beim Entwickeln bin auf einige Probleme gestoßen, mit denen ich vorher so nicht gerechnet habe.
Zu wenig Strom
Mein ursprünglicher Plan war, die Uhr mit 4 AA-Batterien zu betreiben. Offenbar benötigt der WLAN-Chip des Pico aber beim Verbindungsaufbau mehr Strom, als die Batterien bereit stellen können. Das funktioniert also nicht. Demnach läuft die Uhr nur via USB-Netzteil.
Zu wenig RAM
Bei meinen ersten Versuchen ist der Pico regelmäßig abgestürzt. Durch den Blick in die Konsole in Thonny war schnell zu erkennen, dass der Speicher (RAM) „voll läuft“. Angeblich soll es in MicroPython nicht notwendig sein, den Speicher manuell freizugeben, in der Praxis habe ich aber andere Erfahrungen gemacht. Durch den Einsatz des „Garbage Collectors“ an mehreren Stellen im Code („gc.collect() gc.mem_free()“), konnte dieses Problem auf jeden Fall gelöst werden.
Das Gehäuse
Als Gehäuse habe ich mich für ein kleines Holzkästchen entschieden. In das weiche Kiefernholz habe ich mit einem Cuttermesser eine 5 x 8 cm große Öffnung für das Display geschnitten. Die Kanten habe ich mit Schleifpapier etwas geglättet. Anschließend habe ich das Kästchen mit Sprühlack lackiert.
Den Pico habe ich mit zwei auf Maß geschnittenen Holzleisten fixiert. Die Holzleisten können mit etwas Heißkleber fixiert werden.
Bei den Maßen des Gehäuses ist zu Bedenken, dass der USB-Stecker recht viel Platz in Anspruch nimmt.
Hier geht´s zum Download des kompletten Codes. Bitte beachte, dass der Code an einigen Stellen noch angepasst werden muss (WLAN-Zugangsdaten, Wetter-API, ggf. BKW-API). Und: Nicht vergessen, die Datei in „main.py“ umzubenennen.
Hi,
danke für die Anleitung, ich möchte die lässige Uhr gerne nachbauen, habe aber bisher nur Erfahrung mit einem Raspberry 3. Welches OS muss wie auf dem Pi Zero installiert werden?
Lassen sich die Werte „Now“ und „Max“ für die Wetterdaten auf Deutsch umbenennen, oder sind das Bezeichnungen die so über die API reinkommen?
Kann sie im Code nämlich nicht finden.
Könntest du bitte kurz die ersten Schritte / Befehle in der Konsole beschreiben? Wo muss ich anfangen?
Danke und beste Grüße,
Micha
Hi,
hier findest du die ersten Schritte mit dem Pico:
https://www.fbnfrtg.de/erste-schritte-mit-dem-raspberry-pi-pico-und-micropython/
Im Prinzip kannst du den fertigen Code aus dem zip einfach auf den Pico kopieren und die Anpassungen mit Thonny direkt auf dem Pico vornehmen.
„Now“ und „Max“ sind Bilder, die in Bytearrays konvertiert sind (line1.py & line2.py).
Wenn´s geklappt hat, schick doch gerne mal ein Bild.
Danke nochmal für deine Tips.
Es hat alles soweit geklappt, auch mit dem Konvertieren in Bildarrays, ich habe nun „Now“ und „Max“ gegen „Jetzt“ und „Maximal“ ersetzt.
Das hat vor 2 Wochen wunderbar funktioniert und alles wurde auf dem Display wie gewünscht angezeigt.
Jetzt habe ich alles in das Kästchen verbaut und soweit verklebt, schliesse nun alles wieder am Rechner an um das Skript anzustossen, jetzt geht irgendwie gar Nichts mehr.
Auf dem Display wird immer noch dieselbe Uhrzeit wie vor 2 Wochen angezeigt, nichts bewegt sich mehr sobald ich das „Main.py“-Skript starte, es erscheint nur noch
++
MPY: soft reboot
WLAN-Verbindung hergestellt / WLAN-Status: 3
++
Auf dem Display bewegt sich nichts…
Habe Thonny schon neu installiert und Micro-Python 1.22.1 wie in deiner Anleitung unter
https://www.fbnfrtg.de/erste-schritte-mit-dem-raspberry-pi-pico-und-micropython/
ebenso erneut auf dem Pico installiert.
Was mache ich falsch? Ich habe am zuvor funktionierenden Skript nichts geändert.
Im Wlan ist der Pico auch soweit frei gegeben, die API-Adressen werden definitiv nicht blockiert.
Danke für jegliche Tips!
WLAN-Status 3 bedeutet „Authentifizierung fehlgeschlagen“. Überprüf mal das Passwort. Möglicherweise hat sich ein Leerzeichen eingschlichen oder ähnliches?
Also bei mir sieht es jetzt so aus:
+++
MPY: soft reboot
WLAN-Verbindung hergestellt / WLAN-Status: 3
WLAN-Verbindung hergestellt / WLAN-Status: 3
Aktuelle Temperatur_2m: 24.5 °C
Tägliche Temperatur_2m_max: 27.6 °C
Speicherverbrauch nach manueller GC: 94.125 KiB
Orientation: Horizontal. Reversal: False. Width: 280. Height: 480.
Start row = 0 col = 0
Speicherverbrauch nach manueller GC: 110.6406 KiB
Speicherverbrauch nach manueller GC: 110.6406 KiB
Speicherverbrauch nach manueller GC: 110.6719 KiB
++
Laut PiHole wird auf jeden Fall eine Verbindung zu ntp.org sowie zu open-meteo.com hergestellt, also steht die Verbindung. Trotzdem passiert auf dem Display einfach gar nichts mehr…
Ich vermute langsam dass das Display plötzlich defekt ist..
EDIT: Laut PiHole stellt der Pico die Verbindung zu ntp.org sowie open-meteo.com her.
Ich werde wahrscheinlich das Display zu berryb*se einsenden, doof ist nur dass das Display dort nicht mehr verfügbar ist und in anderen Shops mittlerweile mindestens das Doppelte kostet 🙁
Noch Ideen?
Grüße
Versuch doch mal ein anderes Skript auf dem Pico auszuführen. Bei Github gibt es ein paar. D.h. z.B.: https://github.com/CoenTempelaars/raspi-pico-epaper