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


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.

  
  
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.