Die Rechenuhr mit einem großen LCD 2004

Dieser Beitrag ist auch als PDF-Dokument erhältlich.

 

Nach der Abakus-Uhr mit LED-Streifen kommt mit diessener Episode eine weitere Uhrenvariante rechtzeitig zum Start des neuen Schuljahres. Sie liefert einen Beitrag zur Förderung des Kopfrechnens, indem sie die Stunden und Minuten nicht im Klartext darstellt, wie man es von Digitaluhren gewohnt ist, sondern in Form von kleinen Rechenaufgaben. Auf Knopfdruck wird die Lösung eingeblendet.

 

Jede Minute erscheinen neue Aufgaben. Und damit auch stets die Uhrzeit stimmt, wird die Uhr durch Zugriff auf einen NTP-Zeitserver im Internet zu jeder Stunde synchronisiert. Wie das funktioniert, das erfahren Sie im heutigen Beitrag aus der Reihe

 

MicroPython auf dem ESP8266, ESP32 und Raspberry Pi Pico

 

heute

 

Die Rechen-Uhr

Der Aufbau ist sehr übersichtlich. Er besteht aus grade mal vier Teilen und einem Controller. Dieser und ein weiteres Bauteil, ein SHT21-Sensor für Temperatur- und Luftfeuchte-Messung, wohnen zusammen auf zwei über die Längsseite zusammengesteckten Breadboards. Als Controller sind alle in der Hardwareliste aufgeführten Modelle einsetzbar. Das Programm erschnüffelt den verwendeten Typ selbst.

 

Eine große LCD-Anzeige stellt Datum, Wochentag, die Rechenaufgaben, auf Knopfdruck die Uhrzeit, die Raumtemperatur und die relative Luftfeuchte im Raum dar. Ein weiterer Druckknopf schaltet für die Hintergrundbeleuchtung an und aus. War die Beleuchtung beim Drücken der Lösungstaste aus, dann wird sie für 10 Sekunden eingeschaltet und geht danach von selbst wieder aus.

 

Hardware

1

ESP32 Dev Kit C unverlötet oder

ESP32 Dev Kit C V4 unverlötet oder

ESP32 NodeMCU Module WLAN WiFi Development Board mit CP2102 oder

NodeMCU-ESP-32S-Kit

oder

Raspberry Pi Pico W RP2040 Mikrocontroller-Board WiFi WLAN mit Stiftleisten

1

GY-21 HTU21 Feuchtigkeit und Temperatur Sensor / SHT21

1

HD44780 2004 LCD Display Bundle 4x20 Zeichen mit I2C Schnittstelle

1

Drucktaster bis 36V, wasserdicht für Hupenknopfschalter, Türklingelschalter, Netzschalter, elektrische Geräte 12mm Durchmesser

1

Breadboard Kit - 3 x 65Stk. Jumper Wire Kabel M2M und 3 x Mini Breadboard 400 Pins

 

Abbildung 1: Aufbau mit dem ESP32 DEV Kit C V4

Abbildung 1 bestätigt den simplen Aufbau der Schaltung mit einem ESP32. Die Nummern der Tastenanschlüsse sind bei allen Controllern dieselben, haben am Board aber verschiedene Positionen. Das betrifft auch die I2C-Bus-Pins, die beim ESP32 frei verändert werden können. Beim Raspberry Pi Pico W sind zwar auch verschiedene Belegungen möglich, aber stets nur in ganz bestimmten Kombinationen. Für die ESP8266-Familie wäre zwar der Aufbau auch problemlos hinzukriegen, a b e r der Speicher des ESP8266 ist für das Programm leider zu mickerig.

 

Hier zur näheren Information die Schaltbilder.

 

Abbildung 2: Schaltung mit ESP32

 

Abbildung 3: Schaltung mit Raspberry Pi Pico W

Da wir auf das WLAN zugreifen wollen, scheidet der Raspberry Pi Pico ohne "W" aus.

 

Die Software

Fürs Flashen und die Programmierung des ESP32:

Thonny oder

µPyCraft

 

Verwendete Firmware für den ESP32:

v1.19.1 (2022-06-18) .bin

 

Verwendete Firmware für den Raspberry Pi Pico (W):

RPI_PICO_W-20240602-v1.23.0.uf2

 

Die MicroPython-Programme zum Projekt:

rechenuhr.py: Betriebsprogramm der Uhr.

credentials_poly.py: WLAN-Zugangsdaten

hd44780u.py: Hardwaretreiber LCD

lcd.py: API LCD

sht21.py: Treibermodul

ntp_test.py: Test- und Demoprogramm für den NTP-Zugriff

sht-test.py: Test- und Demoprogramm für LCD und SHT21-Sensor

timeout.py: Set nichtblockierender Softwaretimer

 

MicroPython - Sprache - Module und Programme

Zur Installation von Thonny finden Sie hier eine ausführliche Anleitung (english version). Darin gibt es auch eine Beschreibung, wie die Micropython-Firmware (Stand 25.01.2024) auf den ESP-Chip gebrannt wird. Wie Sie den Raspberry Pi Pico einsatzbereit kriegen, finden Sie hier.

 

MicroPython ist eine Interpretersprache. Der Hauptunterschied zur Arduino-IDE, wo Sie stets und ausschließlich ganze Programme flashen, ist der, dass Sie die MicroPython-Firmware nur einmal zu Beginn auf den ESP32 flashen müssen, damit der Controller MicroPython-Anweisungen versteht. Sie können dazu Thonny, µPyCraft oder esptool.py benutzen. Für Thonny habe ich den Vorgang hier beschrieben.

 

Sobald die Firmware geflasht ist, können Sie sich zwanglos mit Ihrem Controller im Zwiegespräch unterhalten, einzelne Befehle testen und sofort die Antwort sehen, ohne vorher ein ganzes Programm kompilieren und übertragen zu müssen. Genau das stört mich nämlich an der Arduino-IDE. Man spart einfach enorm Zeit, wenn man einfache Tests der Syntax und der Hardware bis hin zum Ausprobieren und Verfeinern von Funktionen und ganzen Programmteilen über die Kommandozeile vorab prüfen kann, bevor man ein Programm daraus strickt. Zu diesem Zweck erstelle ich auch gerne immer wieder kleine Testprogramme. Als eine Art Makro fassen sie wiederkehrende Befehle zusammen. Aus solchen Programmfragmenten entwickeln sich dann mitunter ganze Anwendungen.

 

 

Autostart

Soll das Programm autonom mit dem Einschalten des Controllers starten, kopieren Sie den Programmtext in eine neu angelegte Blankodatei. Speichern Sie diese Datei unter main.py im Workspace ab und laden Sie sie zum ESP-Chip hoch. Beim nächsten Reset oder Einschalten startet das Programm automatisch.

 

 

Programme testen

Manuell werden Programme aus dem aktuellen Editorfenster in der Thonny-IDE über die Taste F5 gestartet. Das geht schneller als der Mausklick auf den Startbutton, oder über das Menü Run. Lediglich die im Programm verwendeten Module müssen sich im Flash des ESP32 befinden.

 

 

Zwischendurch doch mal wieder Arduino-IDE?

Sollten Sie den Controller später wieder zusammen mit der Arduino-IDE verwenden wollen, flashen Sie das Programm einfach in gewohnter Weise. Allerdings hat der ESP32/ESP8266 dann vergessen, dass er jemals MicroPython gesprochen hat. Umgekehrt kann jeder Espressif-Chip, der ein kompiliertes Programm aus der Arduino-IDE oder die AT-Firmware oder LUA oder … enthält, problemlos mit der MicroPython-Firmware versehen werden. Der Vorgang ist immer so, wie hier beschrieben.

 

 

Ein paar Tests im Vorfeld

Tasten, SHT21 und Display

Der SHT21 = HTU21 liegt zusammen mit dem Adapter des Displays am I2C-Bus. Das verringert die Anzahl benötigter GPIO-Pins. Zusammen mit den Tastenanschlüssen brauch wir also vier. Wir fangen ganz langsam und einfach an.

 

Das mit den Tasten ist nicht wirklich schlimm, einer der Pole geht an den GPIO (GPIO14 und GPIO13), der andere Pol wird auf GND gelegt. Einen externen Pull-Up-Widerstand können wir uns sparen, weil wir den internen nutzen

 

>>> from machine import Pin

>>> licht=Pin(14,Pin.IN,Pin.PULL_UP)

>>> loesung=Pin(13,Pin.IN,Pin.PULL_UP)

 

Taste nicht gedrückt:

>>> licht()

1

 

Taste gedrückt und gehalten:

>>> licht()

0

 

Dann wird es schon ein wenig komplexer. Wir verbinden das Controller-Board mit dem SHT21 und mit dem Display. Die GPIOs für die drei Familien sind unterschiedlich. Ich gehe hier vom Einsatz eines ESP32 aus.

 

ESP32: 5V – LCD: VCC (5V)

ESP32: 3V3 – GY-21: Vin

ESP32: GND – GY-21: GND – LCD: GND

ESP32: SCL (Pin 21) – GY21: SCL - LCD: SCL

ESP32: SDA (Pin 22) – GY21: SDA – LCD: SDA

 

Wenn die Anschlüsse fertig sind, laden wir die Treiber für das Display, hd44780u.py und lcd.py, herunter und dann in den Flash des Controllers hoch.

 

Abbildung 4: Dateien in den Flash hochladen

 

Das Gleiche machen wir mit der Datei sht21.py. Die Datei sht-test.py speichern wir im Arbeitsverzeichnis des Projekts und öffnen sie in einem Editorfenster. Nach dem Import der benötigten Objekte lassen wir uns von der Variablen platform den Typ des Controllers flüstern und stellen danach die Pins für die Peripherie ein. Die Frequenz für den I2C-Bus müssen wir wegen des Display-Adapters mit dem Seriell-Parallel-Umsetzer PCF7485 auf 100000Hz begrenzen.

 

# sht-test.py

from sys import exit, platform

from time import sleep_ms,sleep

from machine import Pin, SoftI2C, Timer

from sht21 import SHT21

from lcd import LCD

 

if platform == "esp32":

    i2c=SoftI2C(scl=Pin(21),sda=Pin(22),freq=100000)

    licht=Pin(14,Pin.IN,Pin.PULL_UP)

    loesung=Pin(13,Pin.IN,Pin.PULL_UP)

elif platform == "rp2":

    i2c=SoftI2C(scl=Pin(21),sda=Pin(20),freq=100000)

    licht=Pin(14,Pin.IN,Pin.PULL_UP)

    loesung=Pin(13,Pin.IN,Pin.PULL_UP)

else:

    print("Nicht unterstuetzter Controller")

    exit()

   

 

Wir warten 15 ms bis sich der SHT21 aus dem Schlaf geräkelt hat und instanziieren ein SHT21-Objekt. Danach erzeugen wir ein LDC-Objekt. Die Hardware-Adresse des Adapterboards ist auf 0x27 eingestellt, wir haben 20 Spalten und vier Zeilen. Den Cursor schalten wir aus und die Hintergrundbeleuchtung an.

 

sleep_ms(15) # for booting SHT-Device

sh=SHT21(i2c)

 

d=LCD(i2c,adr=0x27,cols=20,lines=4)

d.cursorBlink(0)

d.cursor(0)

d.backLight(1)

 

Zwei Funktionen, getEnvironment() und showEnvironment() sorgen für das Requirieren der SHT21-Messwerte und deren Ausgabe in REPL und auf dem Display. Die Berechnungsroutinen legen die aus den Rohdaten berechneten Werte in den Attributen sh.Temp und sh.Hum ab.

 

def getEnvironment():

    sh.readTemperatureRaw()

    sh.calcTemperature()

    sh.readHumidityRaw()

    sh.calcHumidity()

 

def showEnvironment():

    print(tempString.format(sh.Temp))

    print(humString.format(sh.Hum))

    d.writeAt(tempString.format(sh.Temp),2,3)

    d.writeAt(humString.format(sh.Hum),12,3)

 

Wir vermelden die Fertigstellung der Vorbereitungen, setzen eine zeitliche Verzögerung zwischen den Wertausgaben und bereiten die Formatierungsstrings für die Ausgabe vor, 4-stellig mit einer Nachkommastelle. Die Werte erscheinen nach dem Programmstart in REPL und in der untersten Zeile des Displays.

 

print("Install done")

 

delay=3

tempString="{0:4.1f} C"

humString="{0:4.1f} %"

 

while 1:

    getEnvironment()

    showEnvironment()

    sleep(delay)

    d.clearAll()

 

SHT21 initialized @ 0X40

this is the constructor of HD44780U class

Size:20x4

Construktor of PCF8574U class

HWADR=0X27, Size=20x4

this is the constructor of LCD class

HWADR=0X27, Size:20x4

Install done

21.8 C

43.4 %

21.8 C

43.3 %

 

Jetzt können wir auch den Kontrast am Trimmpoti des Display-Adapters nachjustieren.

 

 

WLAN-Verbindung, NTP-Serverabfrage und Timestamps

Das Programm ntp_test.py zeigt den Umgang mit dem Modul network, das die Verbindung zu einem WLAN-Router/Accesspoint herstellen kann. Die Arbeitsweise des Programms und seine Objekte und Funktionen sind in dem PDF-Dokument Funkverbindungen mit einem WLAN im Kapitel Schnittstellen-Objekte und Funktionen genau beschrieben, weshalb ich die Lektüre dieses Tutorials empfehle. Informationen zu dem Programm ntp_test.py finden Sie ebenfalls in dem genannten PDF-Dokument im Kapitel Kontakt zu einem NTP-Server aufnehmen.

Für die WLAN-Anmeldung verwende ich eine Datei mit dem Namen credentials_poly.py. Sie enthält die Zugangsdaten zu einem oder mehreren WLAN-Knoten. Der Controller versucht sie der Reihe nach zu kontakten. Die Suche wird abgebrochen, sobald eine Verbindung zustande kam. Der Aufbau der Datei credentials_poly.py ist im Kapitel Die Credentials-Datei im PDF-Dokument beschrieben. Natürlich müssen Sie dort Ihre eigenen Zugangsdaten statt der Dummy-Werte eintragen. Die Vorgänge beim Verbindungsaufbau im Hauptprogramm finden Sie im Kapitel Vorbereitungen im Hauptprogramm, die Beschreibung der Hauptschleife selbst ist im Kapitel Die Hauptschleife zu finden.

 

# ntp_test.py

import ntptime as ntp

import network

from time import sleep, localtime, gmtime, mktime

from credentials_poly import  *

from timeout import TimeOutMs

from sys import exit

from machine import Pin, RTC

 

displayPresent=False

timeZone=1

 

connectStatus = {

    1000: "STAT_IDLE",

    1001: "STAT_CONNECTING",

    1010: "STAT_GOT_IP",

    202:  "STAT_WRONG_PASSWORD",

    201:  "NO AP FOUND",

    5:    "UNKNOWN",

    0: "STAT_IDLE",

    1: "STAT_CONNECTING",

    5: "STAT_GOT_IP",

    2:  "STAT_WRONG_PASSWORD",

    3:  "NO AP FOUND",

    4:  "STAT_CONNECT_FAIL",

    }

 

def hexMac(byteMac):

    """

    Die Funktion hexMAC nimmt die MAC-Adresse im Bytecode und

    bildet daraus einen String fuer die Rueckgabe

    """

    macString =""

    for i in range(0,len(byteMac)):   # Fuer alle Bytewerte

        val="{:02X}".format(byteMac[i])

        macString += val

        if i <len(byteMac)-1 :        # Trennzeichen

            macString +="-"           # bis auf letztes Byte

    return macString

 

def connect2router(entry): # n = Connection-Set-Nummer

    # ************** Zum Router verbinden *******************

    nic=network.WLAN(network.AP_IF)

    nic.active(False)

    nic = network.WLAN(network.STA_IF)  # erzeugt WiFi-Objekt

    nic.active(True)                    # nic einschalten

    MAC = nic.config('mac')# binaere MAC-Adresse abrufen und 

    myMac=hexMac(MAC)      # in Hexziffernfolge umwandeln

    print("STATION MAC: \t"+myMac+"\n") # ausgeben

    sleep(2)

    nic.ifconfig((creds[entry]["myIP"],

                  creds[entry]["myMask"],

                  creds[entry]["myGW"],

                  creds[entry]["myDNS"]))

    if not nic.isconnected():

      nic.connect(creds[entry]["mySSID"],

                  creds[entry]["myPass"])

      print("Status: ", nic.isconnected())

      if displayPresent:

          d.clearAll()

          d.writeAt(creds[entry]["mySSID"],0,0)

      points="." * 10

      n=1

      while nic.status() != network.STAT_GOT_IP:

        print(".",end='')

        if displayPresent: d.writeAt(points[0:n],0,1)

        n+=1

        sleep(1)

        if n >= 10:

            if displayPresent:

                d.writeAt("  NOT CONNECTED  ",0,1)

            print("\nNot connected")

            nic.active(False)

            nic=None

            return None

    print("\nStatus: ",connectStatus[nic.status()])

    if displayPresent: d.clearAll()

    STAconf = nic.ifconfig()

    print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",\

          STAconf[1], "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')

    print()

    if displayPresent:

        d.writeAt(STAconf[0]+":"+str(creds[entry]["myPort"]),0,0)

        d.writeAt(creds[entry]["mySSID"],0,1)

    return nic

 

def inTheSummertime(jahr,monat,tag):

    if monat < 3 or monat > 10:

        return timeZone

    if 3 < monat < 10:

        return timeZone + 1

    letzterMaerzSonntag = max([mtag for mtag in range(25,32) \

        if gmtime(mktime((jahr, 3, mtag, 2, 0, 0, 0, 0, 0)))[6] == 6])

    letzterOktSonntag = max([mtag for mtag in range(25,32) \

        if gmtime(mktime((jahr, 10, mtag, 2, 0, 0, 0, 0, 0)))[6] == 6])

    if monat == 3 and tag >= letzterMaerzSonntag:

        return timeZone + 1

    if monat == 10 and tag < letzterOktSonntag:

        return timeZone + 1

    return timeZone

 

entry=None

for i in range(len(creds)):

    print("\n\n",creds[i]["mySSID"])

    if creds[i]["myIF"] == "STA":

        nic = connect2router(i)

        if nic is not None:

            entry=i

            break

 

 

taste=Pin(0,Pin.IN, Pin.PULL_UP)

r=RTC()

 

 

 

while 1:

    try:

        sekunden=ntp.time()

        year,month,day=gmtime(sekunden)[0:3]

        offset=inTheSummertime(year,month,day)

        # ntp.time() liefert Anzahl Sekunden seit

        # Epochenbeginn (01.01.1900)

        tag=localtime(sekunden+offset*3600)

        print("NTP-Time       ",tag)

        # NTP-Format

        # year,month,day,hor,minute,second,dow,_

       

    except:

        print("Uebertragungsfehler")

    sleep(1)

   

    if taste() == 0:

        exit()

   

 

Wir starten jetzt das Programm ntp_test.py in einem Editorfenster.

 

>>> %Run -c $EDITOR_CONTENT

 

 

 ARCTURUS

STATION MAC:        10-52-1C-02-50-24

 

 

Status:  STAT_GOT_IP

STA-IP:                      10.0.5.199

STA-NETMASK:      255.255.255.0

STA-GATEWAY:      10.0.5.20

 

NTP-Time        (2025, 9, 27, 19, 15, 31, 5, 270)

NTP-Time        (2025, 9, 27, 19, 15, 32, 5, 270)

NTP-Time        (2025, 9, 27, 19, 15, 33, 5, 270)

 

Mit den letzten beiden Programmen haben wir bereits einen Großteil des Projekts behandelt. Die Teile werden wir gleich im Hauptprogramm wiederfinden, bei der Besprechung jetzt aber übergehen.

 

Das Uhrenprogramm

Was uns noch fehlt, sind ein paar Kleinigkeiten zur Ablaufsteuerung und natürlich der Professor, der dynamisch die Rechenaufgaben stellt. Immer dieselben Aufgaben wäre ja öde! Laden Sie sich am besten gleich einmal das Programm rechenuhr.py herunter und öffnen Sie es in einem Editorfenster von Thonny. So können Sie leichter die nun zu besprechenden Teile orten und einordnen. Die Importliste ist ein bisschen gewachsen. Am wesentlichsten sind die ersten drei Zeilen. Die erste dient dem Erzeugen von Zufallszahlen. Dann holen wir uns Unterstützung für einen Hardwaretimer und die interne RTC des Controllers. Schließlich geht es noch um einige Funktionen zur Zeitformatumwandlung.

 

from random import *

from machine import Pin,SoftI2C,Timer,RTC

from time import sleep,sleep_ms,localtime, gmtime, mktime

import network

import ntptime as ntp

from credentials_poly import creds

from sys import exit, platform

from timeout import TimeOut

from sht21 import SHT21

 

Die Auswahl der Plattform ist bereits bekannt, ebenso die Erzeugung des SHT21-Objekts. Neu dabei ist aber die Erzeugung eines Hardwaretimer-Objekts t0.

 

if platform == "esp32":

    t0=Timer(0)

    i2c=SoftI2C(scl=Pin(21),sda=Pin(22),freq=100000)

    licht=Pin(14,Pin.IN,Pin.PULL_UP)

    loesung=Pin(13,Pin.IN,Pin.PULL_UP)

elif platform == "rp2":

    t0=Timer()

    i2c=SoftI2C(scl=Pin(21),sda=Pin(20),freq=100000)

    licht=Pin(14,Pin.IN,Pin.PULL_UP)

    loesung=Pin(13,Pin.IN,Pin.PULL_UP)

else:

    print("Nicht unterstuetzter Controller")

    exit()

 

 

Neu ist auch die Instanziierung einer RTC und das Abmelken der lokalen Zeit. Das Tupel enthält Datum und Uhrzeit sowie den Wochentag. Die Indizes in das Tupel werden als Konstanten definiert. In der Liste wochentag halten wir die "Namen" der Wochentage fest. Wir befinden uns in der Zeitzone 1. Circa alle 60 Minuten synchronisieren wir unsere RTC. Den Zähler setzen wir schon mal auf 0.

 

rtc=RTC()

dt=rtc.datetime()

anno=const(0)

mon=const(1)

mday=const(2)

wday=const(3)

hor=const(4)

mnt=const(5)

sec=const(6)

wochentag=[

    "Mont",

    "Dien",

    "Mitt",

    "Donn",

    "Frei",

    "Sams",

    "Sonn"

    ]

 

timeZone=1

refreshPoint=60 # Minuten zur naechsten Synchronisation

refreshCnt=0

 

Jetzt geht es in großen Sprüngen über bekannte Teile hinweg. Wir landen in Zeile 149. tick() ist die ISR des Hardwaretimers t0 und die wird aufgerufen, wenn der Timer abgelaufen ist. Hier wird nur das Flag ticked auf True gesetzt. ticked wird in der Hauptschleife zur Ausgabe der Uhrzeit benutzt.

 

def tick(t0):

    global ticked

    ticked = True

 

inTheSummertime() kennen wir schon von ntp-test.py. Die Funktion erhöht den Wert der Zeitzone gegenüber GMT während der Sommerzeit um 1.

 

def inTheSummertime(jahr,monat,tag):

    if monat < 3 or monat > 10:

        return timeZone

    if 3 < monat < 10:

        return timeZone + 1

    letzterMaerzSonntag = max([mtag for mtag in range(25,32) \

        if gmtime(mktime((jahr, 3, mtag, 2, 0, 0, 0, 0, 0)))[6] == 6])

    letzterOktSonntag = max([mtag for mtag in range(25,32) \

        if gmtime(mktime((jahr, 10, mtag, 3, 0, 0, 0, 0, 0)))[6] == 6])

    if monat == 3 and tag >= letzterMaerzSonntag:

        return timeZone + 1

    if monat == 10 and tag < letzterOktSonntag:

        return timeZone + 1

    return timeZone

 

Starten Sie für die folgenden Experimente noch einmal ntp-test.py und brechen Sie mit STRG + C ab. Damit haben Sie die Umgebung in der die folgenden Eingaben funktionieren, eine Verbindung zum Zeitserver, die Funktion inTheSummertime() und die Hilfsmittel vom Modul time. Zuerst ermitteln wir den Beginn der Epoche. Dann rufen wir die Anzahl von Sekunden seit Epochenbeginn ab und wandeln den Wert in ein besser lesbares Format um.

 

>>> gmtime(0)

(2000, 1, 1, 0, 0, 0, 5, 1)

>>> ntp.time()

812355151

>>> gmtime(812355151)

(2025, 9, 28, 6, 12, 31, 6, 271)

>>> gmtime(812355151)[0:3]

(2025, 9, 28)

 

Mit Slicing holen wir uns die ersten drei Elemente aus dem Tupel, das uns gmtime() liefert, nachdem die Sekunden umgerechnet wurden, Jahr, Monat und Tag. Dieses Tupel übergeben wir an inTheSummertime()

 

Diese Funktion bestimmt nun die Tagesnummern des letzten Sonntags im März und des letzten Sonntags im Oktober, wenn nicht eh schon die Monatsnummer zwischen 4 und 9 inclusive liegt. Wenn sonst das Tagesdatum nach dem letzten Sonntag im März (incl.) und vor dem letzten Sonntag im Oktober liegt, wird in all diesen Fällen der Wert von timezone um 1 erhöht.

 

>>> inTheSummertime(2025, 9, 28)

2

>>> inTheSummertime(2025, 11, 3)

1

 

Aber was macht synchronize()? Mit jedem Aufruf von synchronize() wird die RTC des ESP32 auf die aktuelle Zeit gesetzt, natürlich unter Berücksichtigung der Zeitzone und des Sommerzeitversatzes.

 

Wir holen die Sekunden seit Beginn der Epoche. Das ist der 01.01.2000, wie wir oben festgestellt haben. gmtime(sekunden) wandelt den Sekundenwert in einen Zeitstempel in Form eines Tupels um:

 

(Jahr,Monat,Tag,Stunden,Minuten,Sekunden,Wochentag,Jahrestag)

 

Das durch Slicing erhaltene Teil-Tupel (Jahr, Monat, Tag) entpacken wir und übergeben die drei Werte an inTheSummertime(). Dann korrigieren wir die Sekunden um den Offset-Wert in Sekunden. gmtime(sekunden) liefert den korrigierten Zeitstempel für unsere Zeitzone, den wir sofort in die Einzelwerte zerpflücken. Das ist zum Umsortieren nötig, denn das RTC-Format weicht vom GMT-Format ab.

 

(Jahr, Monat, Tag, Wochentag, Stunden, Minuten, Sekunden, Microsekunden)

 

Mit der geänderten Reihenfolge setzen wir die RTC-Zeit. Das alles passiert innerhalb des try-Blocks, um etwaige Fehler abzufangen, was mit except OSError geschieht.

 

refreshCnt haben wir eingangs als global deklariert, damit der auf 0 geänderte Wert im Hauptprogramm bekannt wird.

 

def synchronize():

    global refreshCnt

    try:

        sekunden=ntp.time()

        year,month,day=gmtime(sekunden)[0:3]

        offset=inTheSummertime(year,month,day) # offset=zeitzone+0/1

        sekunden += offset*3600 # Zeitzone und Sommer/Winterzeit

        Y,M,D,h,m,s,dow,doy=gmtime(sekunden)

        rtc.datetime((Y,M,D,dow,h,m,s,0))

        print("synchronized: ",rtc.datetime())

    except OSError as e:

        print("Synchron-Fehler",e)

    refreshCnt=0

 

Neben den Environment-Werten müssen auch Datum und Uhrzeit ausgegeben werden, wobei Stunden und Minuten als Rechenaufgabe codiert werden sollen. Die Anzeige selbst erledigt showDateTime(). Der Funktion übergeben wir ein Datum-Zeit-Tupel.

 

def showDateTime(dt):

    d.clearFT(0,0,19,1)

    d.writeAt("{:0>2}.{:0>2}".format(dt[mday],dt[mon]),0,0,False)

    d.writeAt(wochentag[dt[wday]],0,1)

    i=randint(0,len(terme)-1)

    s=terme[i](dt[hor])

    print("Stunde"," index",i,"Term:",s,"Stunde:",dt[hor])

    pos=d.columns - len(s)

    d.writeAt(s,pos,0)

    i=randint(0,len(terme)-1)

    s=terme[i](dt[mnt])

    print("Minute"," index",i,"Term:",s,"Stunde:",dt[mnt])

    pos=d.columns - len(s)

    d.writeAt(s,pos,1)

 

Nach dem Löschen der oberen beiden Zeilen im Display geben wir links oben das Datum aus und darunter die Abkürzung des Wochentags. randint() bestimmt eine "zufällige", ganzzahlige Zahl zwischen den übergebenen Werten (incl.). Die Funktion benutze ich zweimal, um aus einer Reihe von Funktionen eine auszuwürfeln. Die dahintersteckende Magie von MicroPython lernen wir später kennen. Das Ziel dieser zufällig ausgewählten Funktion ist es, einen Term zu erzeugen, der für den Stunden- und Minutenwert am rechten Rand des Displays rechtsbündig auszugeben ist. Der Term kommt als String zurück und wird der Variablen s zugewiesen. Die Startposition für die Ausgabe ergibt sich als Differenz aus der Spaltenzahl des Displays, d.columns, und der Länge von s. Die print-Anweisungen helfen bei der Programmentwicklung und können auch weggelassen werden.

 

Abbildung 5: Rechenaufgaben

 

Abbildung 6: Rechenaufgaben mit Lösung

 

getEnvironment() und showEnvironment() sind schon bekannt, daher gehen wir gleich zu Zeile 210 weiter. Ab hier kommen die Definitionen der Funktionen, welche die Terme erzeugen. Auch hier spielt die Funktion randint() wieder mehrfach eine wichtige Rolle. In jedem Fall muss mindestens eine Zufallszahl (x oder x und y) bestimmt werden. Andererseits ist der zu übergebende Wert für z jeweils fest durch die Stunde oder die Minute, die sich ergeben soll, vorgegeben. Weil sich durch die verschiedenen Rechenoperationen +, -, • und : allein durch Zufallswerte selten der Wert von z ergeben wird, brauchen wir überall einen variablen, anpassbaren Wert, den ich a genannt habe. Beispiel:

 

Term: x + a = z mit dem Zufallswert x, der zwischen 0 und z liegen soll.

x=randint(0,z)

 

Daraus berechnen wir a.

 

a=z-x

 

Schließlich bauen wir alles zu einem String zusammen, den wir zurückgeben.

s="{}+{}".format(x,a)

return s

 

Alles zusammen:

 

def term0(z):

    x=randint(0,z)

    a=z-x

    s="{}+{}".format(x,a)

    return s

 

Stunden laufen von 0 bis 23, Minuten von 0 bis 59. Die Null müssen wir als Sonderfall einstufen, wenn sie Probleme erwarten lässt.

 

def term1(z):

    if z == 0:

        return"0"

    x=randint(z,z*2)

    a=x-z

    s="{}-{}".format(x,a)

    return s

 

Term: x – a = z ó a = x – z; mit x > z

x = randint(z,2*z); aber randint(0,0) liefert einen Laufzeitfehler

 

Also sortieren wir den Fall z = 0 durch das if-Konstrukt einfach aus und geben 0 zurück. Ähnlich sieht es bei den weiteren 13 Funktionen aus.

 

Jetzt kommen wir zur Magie von MicroPython. Wie wähle ich eine Funktion aus mehreren aus? Die plumpe Lösung ist ein if-elif-elif…-Konstrukt, welches jeweils auf eine zufällig gewürfelte Nummer abgleicht und die entsprechende Funktion aufruft. Diese Lösung ist schwerfällig, umfangreich und schlecht skalierbar. Die elegantere Lösung läuft über eine Liste, deren Elemente die Bezeichner der termx-Funktionen sind. Wir finden die Definition ab Zeile 357.

 

terme = [term0,

         term1,

         term2,

         term3,

         term4,

         term5,

         term6,

         term7,

         term8,

         term9,

         term10,

         term11,

         term12,

         term13,

         term14,

         term15,

         ]

 

Somit ruft terme[5](z) die Funktion term5(z) mit dem Argument z auf. Funktionen sind in MicroPython Objekte. Und eine Liste kann sogar Elemente von ganz unterschiedlichem Typ enthalten. Die Indizierung ersetzt also dynamisch mit einer Zeile eine starre Struktur von ca. 45 Zeilen. Das nutzen wir in showDateTime().

 

Ob die termx-Funktionen funktionieren und die Terme den korrekten Wert z liefern, das können wir mit Hilfe der Funktion testTerme() prüfen. Für jede Zahl zwischen 0 und 23 inclusive wird jede termx-Funktion aufgerufen. Wir ermitteln die Funktion und damit den Term als String.

 

            term=terme[i]

            s=term(val)

 

Dann geben wir die Termnummer, den String s und den Termwert aus, den wir durch eval() aus dem String berechnen lassen. Letzteres ist auch wieder ein Stück MicroPython-Magie.

 

>>> val=17

>>> i=4

>>> term=terme[i]

>>> s=term(val)

>>> i

4

>>> s

'-7+8*3'

>>> eval(s)

17

 

Stimmen val und eval(s) nicht überein, bricht die Funktion ab und meldet den Fehler. Läuft die Funktion bis zum Ende durch, können wir sicher sein, dass die Terme korrekt erstellt werden.

 

Wir bilden die Formatstrings in bekannter Weise, synchronisieren die RTC, stellen das Flag ticked zurück und löschen das Display.

 

tempString="{0:4.1f} *C"

humString="{0:4.1f} %"

 

synchronize()

ticked=False

d.clearAll()

 

Dann holen wir einen Timestamp und lassen Datum, die Rechenaufgaben und die Environmentwerte im Display ausgeben.

 

dt=rtc.datetime()

showDateTime(dt)

getEnvironment()

showEnvironment()

 

Das Display wird jede Minute upgedatet. Deshalb stellen wir den Updatetimer t0 auf 60000 Millisekunden = 1 Minute. Er läuft periodisch und startet bei Ablauf die ISR tick().

 

t0.init(period=60000,mode=Timer.PERIODIC,callback=tick)

 

weiter=TimeOut(0)

 

Ferner stellen wir den Softwaretimer weiter auf False. TimeOut() gibt eine Funktion zurück, die einen Timer darstellt. Dieser Funktion weisen wir den Bezeichner weiter zu. weiter() liefert False zurück so lange der Timer noch nicht abgelaufen ist, andernfalls True. Mit dem Aufruf weiter(0) bekommen wir bis zum St. Nimmerleins-Tag False zurück.

 

Kommen wir jetzt noch einmal auf den Timer t0 zurück. Er läuft nach 1 Minute ab und ruft dann die ISR tick(). Diese setzt ihrerseits das Flag ticked auf True. Und genau dieses Flag fragen wir in der Hauptschleife ab. Dort wird dann die Hauptarbeit verrichtet, weil eine ISR so kurz wie möglich gehalten werden soll, delegieren wir die Sequenz an die Mainloop.

 

Wenn ticked True ist, muss die Anzeige erneut aufgebaut werden. Dazu löschen wir das Display total und setzen ticked sofort wieder auf False. Dann holen wir einen Timestamp und die Umgebungswerte. Das Datum wird im Klartext, die Zeit in Form von zwei Rechenaufgaben ausgegeben. Es folgt die Ausgabe der Environmentwerte.

 

while 1:

    if ticked:

        d.clearAll()

        ticked = False

       

        dt=rtc.datetime()

        getEnvironment()

        showDateTime(dt)

        showEnvironment()

 

        refreshCnt += 1

        if refreshCnt == refreshPoint:

            synchronize()

 

Der Refresh-Zähler wird erhöht. Haben wir den Grenzwert erreicht, erfolgt die Synchronisation der RTC mit dem NTP-Server, das passiert nach jeweils 60 Minuten.

 

Mit der Lösungstaste wird in der Zeile 2 die aktuelle Uhrzeit im Klartext eingeblendet. Wenn die Hintergrundbeleuchtung aus war, wird sie jetzt eingeschaltet und der Ablauf-Timer wird auf 10 Sekunden gestellt. Das Licht geht aus, wenn der Timer abgelaufen ist.

 

    if loesung() == 0:

        if d.backLight() == 0:

            d.backLight(1)

            weiter=TimeOut(10)

        d.writeAt("{:0>2}:{:0>2}".format(dt[hor],dt[mnt]),8,2)

 

Mit der Taste licht können wir die Hintergrundbeleuchtung ein- und ausschalten.

 

    if licht() == 0:

        if d.backLight() == 0:

            d.backLight(1)

        else:

            d.backLight(0)

        sleep(0.2)

 

Der Timer weiter() ist das Kernstück für die automatische Abschaltung der Hintergrundbeleuchtung. Liefert die Closure, die hinter allem steckt True zurück, ist der Timer abgelaufen und wir löschen die Klartext-Uhrzeit und die Hintergrundbeleuchtung. Der Timer weiter muss wieder permanent auf False gesetzt werden, weil sonst das Licht nicht dauerhaft eingeschaltet werden kann.

 

    if weiter():

        d.clearFT(6,2,13,2)

        d.backLight(0)

        weiter=TimeOut(0)

 

 

Mit der Flashtaste des ESP32-Boards lässt sich das Programm abbrechen, wenn gar nix anderes mehr geht.

 

    if taste() == 0:

        d.backLight(0)

        exit()

 

Jetzt wünsche ich viel Freude beim Aufbau und der Programmierung. Sie können zum Beispiel neue Terme entwerfen, nach Schwierigkeitsgruppen sortieren oder einen Timer einbauen, der beim Erscheinen einer neuen Aufgabe automatisch nach 50 Sekunden die Lösung einblendet.

 

DisplaysEspEsp-32Esp32-dev-kitGrundlagen softwareProjekte für fortgeschritteneRaspberry esp32Raspberry piSensoren

Kommentar hinterlassen

Alle Kommentare werden von einem Moderator vor der Veröffentlichung überprüft

Empfohlene Blogbeiträge

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery