pico-watch
auf rundem 1.28-Zoll-IPS-LCD-Display 240 x 240 Pixel
- Waveshare WS-28986 (RP 2350)
getestet mit der Firmware CircuitPython 10.0.0-beta
Hardware
- Rundes 1,28-Zoll-IPS-LCD-Display (WS-28986)
- USB-A zu USB-C Kabel bzw. USB-C Kabel
pico-watch
auf rundem 1.28-Zoll-IPS-LCD-Display 240 x 240 Pixel- Waveshare WS-28986 (RP 2350)
getestet mit der Firmware CircuitPython 10.0.0-beta

Diese Anleitung baut auf der Analoguhr aus der vorhergehenden Anleitung auf, die aber nicht zwingend erforderlich ist. Es wird ebenfalls eine
Analoguhr mit Minuten-, Stunden- und Sekundenzeiger mit einem individuell wählbaren Zifferblatt realisiert (siehe Foto oben). Eine Besonderheit
ist das durchbrochene Zifferblatt mit der sich bewegenden Unruhe. Das Display wird wieder in einem vom 3D-Drucker hergestellten Gehäuse mit
einem 3,7V/320mAh Akku untergebracht. Damit wird eine Laufzeit von ca. 4,5 Stunden erreicht. Danach kann das Display jetzt ohne PC neu gestartet
werden und mit Hilfe des ACC-Sensors (mit etwas Übung) gestellt
werden.
Los gehts
Als erstes wird die Firmware 'CircuitPython' für den Pico 2 benötigt. Der bei der Weiterentwicklung stets etwas erweiterte Speicherplatz
stellt hier jetzt kein Problem dar. Der RP 2350 hat ja im Vergleich zum RP 2040 doppelten RAM. Eine aktuelle Firmware können Sie
hier herunterladen,
und auf dem Board installieren.
Weiterhin brauchen Sie im 'lib-Ordner' die Bibliotheken adafruit_imageload, adafruit_display_shapes, adafruit_display_text sowie den
Displaytreiber gc9a01.py und den Treiber my_qmi8658.py für den ACC-Sensor. Die können
Sie bei mir als
zip-Datei
(für die Version 10.x.x) herunterladen, entpacken und in den 'lib_Ordner' kopieren. In der zip-Datei ist auch ein Ordner 'images'. Darin sind die Bitmaps mit
dem Zifferblatt und den Zeigern und die Unruhe. Ziehen Sie diesen Ordner ins Stammverzeichnis von 'CIRCUITPY'. Ebenso die Datei 'boot.py', welche beim
Speichern der Uhrzeit auf dem Displayboard eine Rolle spielt, auf die ich später noch eingehe.
Jetzt gehts richtig los.
Im unteren Kasten sehen Sie, wie das Display initialisiert wird, die Zeiger und die Unruhe angelegt werden. Für die Unruhe wird eine TileGrid
(Zeilen 56 bis 62) erzeugt, die aus zwei Sprites besteht, welche später die Bewegung simulieren. Die Unruhe und die Brücke
müssen unbedingt vor den Zeigern definiert werden, damit die Zeiger darüber liegend dargestellt werden. Kopieren Sie die Zeilen 1 bis 86
ins Thonny. Sie können auch schon mal 'start' drücken. Es erscheint das Zifferblatt (mehr noch nicht). Speichern Sie die Datei
als 'code.py'. Die Rückfrage zum Überschreiben der vorhanden 'code.py' beantworten Sie mit 'ja'.
1 # SPDX-FileCopyrightText : 2025 Detlef Gebhardt, written for CircuitPython 10.0.0-beta 2 # SPDX-FileCopyrightText : Copyright (c) 2025 Detlef Gebhardt 3 # SPDX-Filename : Analoguhr 4 # SPDX-License-Identifier: GEBMEDIA 5 import time 6 import rtc 7 import gc 8 import board 9 import busio 10 import displayio 11 import fourwire 12 import digitalio 13 import terminalio 14 import bitmaptools 15 import math 16 import adafruit_imageload 17 from adafruit_display_text import label 18 from adafruit_display_shapes.circle import Circle 19 from adafruit_display_shapes.roundrect import RoundRect 20 import gc9a01 21 import my_qmi8658 22 23 # Bitmap-Dateien fuer Hintergrund und Zeiger 24 zifferblatt = "/images/zifferblatt.bmp" 25 second_zeiger = "/images/second.bmp" 26 minute_zeiger = "/images/minute.bmp" 27 hour_zeiger = "/images/hour.bmp" 28 unruhe = "images/unruhe.bmp" 29 bruecke = "images/bruecke.bmp" 30 # 31 # Display initialisieren 32 # 33 # Ressourcen freigeben fuer das Display 34 displayio.release_displays() 35 36 # displayio SPI-Bus und GC9A01 Display 37 spi = busio.SPI(clock=board.GP10, MOSI=board.GP11) 38 display_bus = fourwire.FourWire(spi, command=board.GP8, chip_select=board.GP9, reset=board.GP12) 39 display = gc9a01.GC9A01(display_bus, width=240, height=240, rotation=90, backlight_pin=board.GP25) 40 41 main = displayio.Group() 42 group1 = displayio.Group() 43 display.root_group = main 44 45 # Sensor initialisieren 46 sensor=my_qmi8658.QMI8658() 47 48 # Ziffernblatt als Hintergrund 49 bg_bitmap,bg_pal = adafruit_imageload.load(zifferblatt) 50 bg_tile_grid = displayio.TileGrid(bg_bitmap, pixel_shader=bg_pal) 51 main.append(bg_tile_grid) 52 53 # Load the sprite sheet (Unruhe) 54 sprite_sheet, palette = adafruit_imageload.load("/images/unruhe_2.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) 55 palette.make_transparent(1) 56 #Create the TileGrid for unruhe 57 unruh = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=58, tile_height=58, default_tile=0) 58 unruh_group = displayio.Group(scale=1) 59 unruh_group.append(unruh) 60 main.append(unruh_group) 61 unruh.x = 88 62 unruh.y = 143 63 # Bruecke 64 bruecke1,bg_pal = adafruit_imageload.load(bruecke,bitmap=displayio.Bitmap,palette=displayio.Palette) 65 bg_pal.make_transparent(1) 66 bg_tile_grid = displayio.TileGrid(bruecke1, pixel_shader=bg_pal, x=85,y=140) 67 main.append(bg_tile_grid) 68 69 # Stundenzeiger 30x140 70 bitmap_pointer_hour, palette_pointer = adafruit_imageload.load(hour_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette) 71 palette_pointer.make_transparent(0) 72 # Blank bitmap vom Stundenzeiger 73 bitmap_pointer_blank_hour = displayio.Bitmap(bitmap_pointer_hour.width, bitmap_pointer_hour.height, 1) 74 75 # Minutenzeiger 30x140 76 bitmap_pointer_min, palette_pointer = adafruit_imageload.load(minute_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette) 77 palette_pointer.make_transparent(0) 78 # Blank bitmap vom Minutenzeiger 79 bitmap_pointer_blank_min = displayio.Bitmap(bitmap_pointer_min.width, bitmap_pointer_min.height, 1) 80 81 # Sekundenzeiger 30x140 pointer 82 bitmap_pointer_sec, palette_pointer = adafruit_imageload.load(second_zeiger, bitmap=displayio.Bitmap,palette=displayio.Palette) 83 palette_pointer.make_transparent(0) 84 # Blank bitmap vom Sekundenzeiger 85 bitmap_pointer_blank_sec = displayio.Bitmap(bitmap_pointer_sec.width, bitmap_pointer_sec.height, 1) 86
Im nächsten Schritt werden die transparenten Overlays für die zu drehenden Zeiger angelegt. Schließlich werden zwei kleine Kreise als zentraler Mittelpunkt definiert. Fügen Sie auch diese Zeilen (87 bis 102) im Thonny ein.
87 # Transparentes Overlay fuer 'rotozoom' 88 # Zeiger fuer die Drehung 89 bitmap_scribble_hour = displayio.Bitmap(display.width, display.height, len(palette_pointer)) 90 tile_grid = displayio.TileGrid(bitmap_scribble_hour, pixel_shader=palette_pointer) 91 main.append(tile_grid) 92 bitmap_scribble_min = displayio.Bitmap(display.width, display.height, len(palette_pointer)) 93 tile_grid = displayio.TileGrid(bitmap_scribble_min, pixel_shader=palette_pointer) 94 main.append(tile_grid) 95 bitmap_scribble_sec = displayio.Bitmap(display.width, display.height, len(palette_pointer)) 96 tile_grid = displayio.TileGrid(bitmap_scribble_sec, pixel_shader=palette_pointer) 97 main.append(tile_grid) 98 circle1 = Circle(120, 120, 10, fill=0xff0000, outline=None) 99 main.append(circle1) 100 circle2 = Circle(120, 120, 5, fill=0x3D4CD2, outline=0x0) 101 main.append(circle2) 102
Es folgen ein Teil mit der Gruppe 'group1' zur Anzeige im Stellmodus und die Funktion zum Stellen der Uhrzeit (Zeilen 118 bis157).
103 ## Group -group1- zum Stellen der Uhr 104 # Rechteck mit abgerundeten Ecken 105 roundrect1 = RoundRect(60, 132, 120, 40, 10, fill=0x0, outline=0xffffff, stroke=3) 106 group1.append(roundrect1) 107 # create the label 108 updating_label0 = label.Label(font=terminalio.FONT, text="Uhr stellen", scale=2, color=0xffffff, line_spacing=1) 109 updating_label0.anchor_point = (0, 0) 110 updating_label0.anchored_position = (60, 80) 111 group1.append(updating_label0) 112 113 updating_label1 = label.Label(font=terminalio.FONT, text="", scale=2, color=0xffffff, line_spacing=1) 114 updating_label1.anchor_point = (0, 0) 115 updating_label1.anchored_position = (70, 140) 116 group1.append(updating_label1) 117 118 def zeit_stellen(hour, minute, second): 119 stellen = True 120 start = time.monotonic() 121 while stellen == True: 122 # Uhrzeit einstellen 123 reading=sensor.Read_XYZ() 124 wert_y = (10)*reading[0] 125 wert_x = (10)*reading[1] 126 if wert_y < -6: 127 display.rotation = 0 128 updating_label0.text = "Minuten" 129 start = time.monotonic() 130 if minute < 59: 131 minute += 1 132 time.sleep(0.6) 133 else: 134 minute = 0 135 time.sleep(0.6) 136 if wert_y > 6: 137 display.rotation = 180 138 updating_label0.text = "Stunden" 139 start = time.monotonic() 140 if hour < 23: 141 hour += 1 142 time.sleep(0.6) 143 else: 144 hour = 0 145 time.sleep(0.6) 146 # Uhrzeit einstellen abschliessen 147 if wert_y > -2 and wert_y < 2 and wert_x > -8 and wert_x < 8: 148 display.rotation = 90 149 updating_label0.text = "Zeit speichern" 150 if (time.monotonic() - start) > 2: 151 stellen = False 152 updating_label1.text = "fertig" 153 time.sleep(0.5) 154 r = rtc.RTC() 155 r.datetime = time.struct_time((2000, 1, 1, hour, minute, 0, 0, 1, -1)) 156 #eingestellte Zeit holen und anzeigen 157 updating_label1.text = "{:02}:{:02}:{:02}".format(hour,minute,second) 158
Ein Test der Einstellfunktion ist erst nach dem nächsten Schritt möglich. Kopieren Sie deshalb auch die Zeilen159 bis 190 aus dem unteren Kasten ins Thonny.
159 current_time = time.localtime() 160 hour = current_time.tm_hour 161 minute = current_time.tm_min 162 second = current_time.tm_sec 163 164 if current_time.tm_year > 2000: 165 # Start am PC 166 with open("/time.txt", "w") as f: 167 f.write(str(hour)+"\n") 168 f.write(str(minute)+"\n") 169 f.close() 170 if current_time.tm_year == 2000: 171 # Start ohne PC 172 with open("time.txt", "r") as f: 173 hour = int(f.readline()) 174 minute = int(f.readline()) 175 f.close() 176 second = current_time.tm_sec 177 display.root_group = group1 178 zeit_stellen(hour, minute, second) 179 display.refresh() 180 display.root_group = main 181 182 current_time = time.localtime() 183 hour = current_time.tm_hour 184 minute = current_time.tm_min 185 second = current_time.tm_sec 186 187 tick = time.monotonic() 188 setz1 = True 189 setz2 = False 190
Für den ersten Test speichern Sie den gesamten bisherigen Programmcode unter 'code.py'. Schließen Sie den Thonnyinterpreter und
entfernen die Stromversorgung am USB-C Anschluss.
Anschließend schließen Sie das Board wieder an. Hier kommt jetzt die Rolle der Datei 'boot.py' zum Tragen.
Das Displayboard sucht beim Bootvorgang nach dieser Datei.
Ist dort der Befehl storage.remount("/", False) vorhanden, dann wird der interne Speicher zum Beschreiben aktiviert.
Ansonsten ist das Pico-Laufwerk schreibgeschützt. So wird im Normalfall verhindert, dass ein anderer Rechner auf den Pico schreibt.
Jede Änderung in der Datei 'boot.py', wird erst nach einem erneuten Bootvorgang wirksam (also z.B. Strom unterbrechen und wieder
anschließen).
Wenn das Board jetzt ohne am PC angeschlossen zu sein startet, dann beginnt der interne Counter bei 'Null' und 'localtime()' holt als ersten
Wert den 1.1.2000 um 00:00 Uhr. Das wird in der Zeile 170 abgefragt und dadurch zur Funktion
'zeit_stellen()' (Zeile 178) weitergeleitet. Andernfalls wird 'localtime()' mit der Zeit vom PC synchronisiert und es geht weiter zur
'while-Schleife'.
Wir testen das Einstellen der Uhrzeit jetzt:
Halten Sie das Display senkrecht mit dem USB-Anschluss nach rechts. Danach verbinden Sie das Display mit einer Spannungsquelle
(z.B. Powerbank) und ändern dann die Position der Reihe nach, wie in den Bildern zu sehen:
Bildbox 2 (klick hier)
Um den Vorgang beim Einstellen der Minuten und Stunden zu unterbrechen, halten Sie das Display immer wieder in der Ausgangsstellung (senkrecht
und USB-Anschluss
nach rechts). Damit der Vorgang nicht zu lange dauert, ist zwischen dem Weiterzählen der Minuten und Stunden nur eine relativ kurze Pause
vorhanden. Deshalb müssen Sie sicher einige Male üben, um die gewünschten Endpositionen zu erhalten. Zum Abschluss halten Sie das Display ca.
2 Sekunden waagerecht. Dabei wird die eingestellte Zeit gespeichert und der Vorgang abgeschlossen. Danach beginnt die Anzeige der Uhrzeit
(noch nicht bei diesem Test).
Weiter geht es mit dem Programm.
In der 'while'-Schleife ab Zeile 191 wird jetzt bei jedem Durchlauf die aktuelle Sekunde bestimmt. Die Stunde und die
Minute wird aus dem Bereich vor der Schleife bzw. den Zeilen 214 u. 215 verwendet. In den Zeilen 205, 207 und 209 werden die Winkel der Zeiger
in Abhängigkeit von den Sekunden, Minuten und Stunden (im Bogenmaß) berechnet. Dazu finden Sie Erklärungen
in der Anleitung 6
(hier). Die Zeilen
206, 208 und 210 stellen die Bitmaps der Zeiger in entsprechend gedrehter Position dar. Ab Zeile 212 werden immer bei Sekunde 0 die Werte
für 'Minute' und 'Stunde' in der Datei 'time.txt' aktualisiert (Zeilen 217 bis 220). Dabei hat die Zeit des Speichervorgangs keinen Einfluss auf die Ganggenauigkeit, denn
die Sekunden werden ja vom MC unabhängig weitergezählt.





Bildbox 2 (klick hier)
191 while True: 192 # Bewegung der Unruhe 193 if (time.monotonic() - tick) >= 0.2 and setz1 == True: 194 unruh[0] = 1 195 setz1 = False 196 setz2 = True 197 if ( time.monotonic()- tick) >= 0.4 and setz2 == True: 198 unruh[0] = 0 199 setz2 = False 200 setz1 = True 201 tick = time.monotonic() 202 # Anzeige der Analogzeit 203 current_time = time.localtime() 204 second = current_time.tm_sec 205 alpha_rad_sec = math.pi/30 * second 206 bitmaptools.rotozoom( bitmap_scribble_sec, bitmap_pointer_sec, angle = alpha_rad_sec, px=15,py=107) 207 alpha_rad_hour = math.pi/6 * hour + math.pi/180 * minute/2 208 bitmaptools.rotozoom( bitmap_scribble_hour, bitmap_pointer_hour, angle = alpha_rad_hour, px=15,py=107) 209 alpha_rad_min = math.pi/30 * minute 210 bitmaptools.rotozoom( bitmap_scribble_min, bitmap_pointer_min, angle = alpha_rad_min, px=15,py=105) 211 # einmal pro Minute speichern 212 if second == 0: 213 current_time = time.localtime() 214 hour = current_time.tm_hour 215 minute = current_time.tm_min 216 second = current_time.tm_sec 217 with open("/time.txt", "w") as f: 218 f.write(str(hour)+"\n") 219 f.write(str(minute)+"\n") 220 f.close() 221 gc.collect() 222 #print(gc.mem_free())
Worauf ich noch nicht eingegangen bin, ist die Bewegung der Unruhe. In den Zeilen 53 bis 62 ist dafür ein bereits erwähntes
TileGrid für die Unruhe angelegt und in den Zeilen 63 bis 67 wird die Brücke darüber dargestellt (siehe Bild). Der Hintergrund
von beiden ist weiss und wird transparent angezeigt.
Durch den schnellen Wechsel der beiden Sprites für die Unruhe in den Zeilen 193 bis 201 entsteht der Eindruck einer Bewegung, wie bei einer
echten Analoguhr.
Falls Sie an Infos zum Gehäuse und zum Litium-Ionen Akku Interesse haben, schauen Sie sich bitte
diese Anleitung bei mir
an.
Viel Spass und Erfolg beim Ausprobieren.




Viel Spass und Erfolg beim Ausprobieren.