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.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.
