Adventskranz-Kalender mit ESP8266, zwei RGB-LED-Ringen und OLED-Display in MicroPython - AZ-Delivery

These instructions are also available as PDF document.

Apfent, Apfent, the Bärwurz burns.
First drink one, then two, three, four,
then you hit the door with your brain.

In this quatrain from the book "Weihnachtsgeschichten von Toni Lauerer - Apfent" (Christmas stories by Toni Lauerer - Apfent) something is burning, but what should burn with us is not a Bärwurz and also not just the four candles on the Advent wreath. It should be already a few lights more, how would it be with 28 pieces? In fact, in August the first discounters already start to fill the shelves with gingerbread and speculoos so that we don't have to wait 28 weeks for Christmas, but what about our 28 lights now? I'll be happy to tell you in a new episode of

MicroPython on the ESP32 and ESP8266

today

The Advent Wreath Calendar with the ESP8266

Figure 1: Advent wreath calendar

Image 1: Advent wreath calendar

If you have leafed through the brochure from the discounter at the beginning of December, you will certainly have stumbled across various Advent calendars with dubious contents such as toys, beer, wine, schnapps, and quite a few other delicate things, depending on the supplier. Our calendar is not of this kind. It only glows, but powerfully. Two neopixel rings, one small (37mm Ø) and one large (50mm Ø), make sure of that. Placed one behind the other, they form the wreath. Of course, we also need candles, which are replaced by flashing LEDs. This is for safety and protects against fire hazards due to the unattended burning of wax candles. Besides, you can hang this thing on the wall without any problems. Try this with a standard Advent wreath. To show the date and time at any time, I added a small OLED display to the Advent wreath calendar, which is placed behind the inner small ring. Together with the ESP8266, which takes over the command, we have the hardware already together.

Hardware

1

NodeMCU Lua Amica Module V2 ESP8266 ESP-12F WIFI or

D1 Mini NodeMcu with ESP8266-12F WLAN module or

NodeMCU Lua Lolin V3 Module ESP8266 ESP-12F WIFI

1

0.91 inch OLED I2C display 128 x 32 pixels

1

Neopixel ring 50mm

1

Neopixel ring 37mm

4

Resistor 1 kΩ

1

LED light emitting diodes assortment kit, 350 pieces, 3mm & 5mm, 5 colors - 1x set

1

Breadboard Kit - 3x Jumper Wire m2m/f2m/f2f + Set of 3 MB102 Breadbord compatible with Arduino and Raspberry Pi - 1x Set

Possibly

Base board 17cm x 17cm

The software

For flashing and programming the ESP32:

Thonny or

µPyCraft

Used firmware for the ESP8266:

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

The MicroPython programs for the project:

ssd1306.py Hardware driver to the OLED display

oled.py API for the OLED display

MicroPython - Language - Modules and programs

For the installation of Thonny you find here a detailed manual (english version). In it there is also a description of how the Micropython firmware (state 05.02.2022) on the ESP chip. burned is burned.

MicroPython is an interpreter language. The main difference to the Arduino IDE, where you always and only flash whole programs, is that you only have to flash the MicroPython firmware once at the beginning to the ESP32, so that the controller understands MicroPython instructions. You can use Thonny, µPyCraft, or esptool.py to do this. For Thonny, I have described the process here.

Once the firmware is flashed, you can casually talk to your controller one-on-one, test individual commands, and immediately see the response without having to compile and transfer an entire program first. In fact, that's what bothers me about the Arduino IDE. You simply save an enormous amount of time if you can do simple tests of the syntax and the hardware up to trying out and refining functions and whole program parts via the command line in advance before you knit a program out of it. For this purpose, I also like to create small test programs from time to time. As a kind of macro, they summarize recurring commands. From such program fragments sometimes whole applications are developed.

Autostart

If you want the program to start autonomously when the controller is switched on, copy the program text into a newly created blank file. Save this file as boot.py in the workspace and upload it to the ESP chip. The program will start automatically at the next reset or power-on.

Test programs

Manually, programs are started from the current editor window in the Thonny IDE via the F5 key. This is quicker than clicking on the Start button, or via the menu Run. Only the modules used in the program must be in the flash of the ESP32.

In between times Arduino IDE again?

If you want to use the controller together with the Arduino IDE again later, simply flash the program in the usual way. However, the ESP32/ESP8266 will then have forgotten that it ever spoke MicroPython. Conversely, any Espressif chip that contains a compiled program from the Arduino IDE or the AT firmware or LUA or ... can easily be flashed with the MicroPython firmware. The process is always here described.

Signals on the I2C bus

How a transmission on the I2C bus works and what the signal sequence looks like, you can read in my article mammutmatrix_2_ger.pdf to read. I use there a interesting little tool, with which you can get the I2C bus signals to your PC and analyze them.

Now it goes round with the Advent wreath

Yes, the parts are just not square, even if there is a calendar behind them. Surely you noticed that the two rings together bring 24 LEDs to the table. What could be more obvious than to assign a day from 1.12. to 24.12. to each of them and then switch on the "candles" at the appropriate time. Here is the clear circuit diagram.

Figure 2: Advent wreath calendar - circuitry

Image 2Advent wreath calendar circuit diagram

For the neopixel rings, they are cascaded, I only need one GPIO pin, which is D3 on the ESP8266 board. MicroPython-technically the pin GPIO0 is behind it.

The four blinking LEDs from the set are served by the GPIO2 (D4), GPIO14 (D5), GPIO12(D6), and GPIO13 (D7) pins.

That leaves the OLED display, which is driven by the I2C bus. The bus lines are SCL=GPIO5 (D1) and SDA=GPIO4 (D2).

Figure 3: Advent wreath calendar - test circuit

Image 3: Advent wreath calendar - test circuit

Figure 2 shows the arrangement of the two rings and behind them the display. The four individual LEDs are used as "candles" and distributed around the rings in the final setup, just like the real Advent wreath.

A list of mappings between the Arduino designations of the IO pins and the native one of MicroPython, is shown in the following table from the listing.

# Pin translator for ESP8266 boards
# ARDUINO pins D0 D1 D2 D3 D4 D5 D6 D7 D8
# ESP8266 pins 16 5 4 0 2 14 12 13 15
# SC SD

That's it with the hardware. Let's get to the program. It comes up with some nice features and details, which are not the least peculiarities of the ESP8266.

What would an Advent wreath be without a program?

Oh no, I don't want to deny the nurseries that they don't have a creative or even artistic ulterior motive when composing Advent wreaths. The same applies, of course, to objects that have been carefully prepared in the home. Often there is a motto or a program behind it.

Even our hardware does not get along without a program but of a different kind, namely a control by a MicroPython program.

The operation offers three different modes:

  1. Test mode

  2. synchronized by an NTP server

  3. RTC based in offline mode

All this can be done with one program. Admittedly, this has become a bit more extensive than originally planned. But - the selection of the operating mode is only done via a variable (test mode) or automatically via whether a WLAN is reachable or not.

In test mode, the variable debugvariable, which you will learn more about later, is set to True later. If debug = Falsethen the program tries to establish a connection to an access point you specified. If this does not succeed, then it resorts to specifying a start condition in the variable rtcTag which contains the day values in an 8-tuple with the following meaning.

(year, month, day, weekday, hour, minute, second, milliseconds).

Tuples are compilations of different data into a compound. They are noted in round brackets. A tuple is an immutable data type in MicroPython. This means that you cannot change the fields of such a data structure afterward. However, changes are possible before the program starts and, of course, when you change from a tuple to the Runtime (= during the program run) you create a new one.

Unfortunately, due to the interfaces between the three data formats of the date management, there are problems with the modules used, but I was able to break them down to a common track by using a function. My standard, therefore, looks as follows. I have followed here the format of localtime().

(Year, month, day, hour, minute, second, day of the week, day of the year)

Peculiarities of the ESP8266

In the kernel of the ESP8266 it is anchored that the controller, if it has already established a connection with an access point once, tries to establish this connection again when restarting. Most of the time this is annoying because you can't influence it. First, it delays the startup if the access point is not (anymore) available. Second, the ESP8266 tries to establish a connection in which it plays an accesspoint itself. This can lead to annoying constant restarts.

The first step to stop this behavior is to enter the following command at the command line, in the terminal window of Thonny for example, after re-flashing the firmware:

>>> import webrepl_setup

After that the line appears:

> d for disable

Enter here d and then reboot the ESP8266 with the RST key. This will at least stop the ESP8266 from trying to start webREPL, the radio command line.

I will explain the second step of the problem-solving in the program discussion.

Not all GPIOs are fully usable by the programmer. This concerns especially the pins GPIO16 (D0) and GPIO15 (D8), which are therefore not used in the program.

Modules

The language scope of MicroPython includes a large number of modules. These are libraries that perform special tasks like input/output of data via GPIO pins, timers, bus lines like I2C or RS232, analog input, etc. Other hardware is also served by modules, but they have to be uploaded extra, at program start, as external files to the ESP8266. In our case these are the files oled.py and ssd1306.py. After downloading, copy these files into your working directory (_workspace) in the project directory that you create at any location on your hard disk. In Thonny, navigate to your working directory and call up the context menu by right-clicking on the file to be uploaded. Then select from the context menu the item Upload to /.

Internal as well as external modules are imported when the program is started and are thus brought to the attention of the MicroPython interpreter.

from machine import Pin, SoftI2C, RTC
from time import sleep, time, localtime, ticks_ms, mktime
import ntptime
from neopixel import NeoPixel
from oled import OLED
import network, socket
from sys import exit

An import like by

import network, socket

binds all lines of the module with the prefix network or socket into the namespace of the program. On the other hand imports

from time import sleep, time, localtime, ticks_ms, mktime

only the listed methods, but then without the prefix time are to be used.

By specifying the name of a classonly the contents of this class will be imported, but not what may be outside of it in the module of definitions of objects.

from oled import OLED

Details, also about this, can be found when working through the MicroPython series.

How does the program work

After importing the utilities, I define if I want to do a test run or if it is already serious. The boolean variable debug takes care of that. With debug = False I declare the serious case. With debug = True I start the test mode. Then I start to set up the necessary objects.

debug=False

# ************** declare objects *******************
#
neo=0 # D3
neoPin=Pin(neo,Pin.OUT)
neoCnt=24
np = NeoPixel(neoPin, neoCnt) #

i2c=SoftI2C(scl=Pin(5),sda=Pin(4))
d=OLED(i2c,heightw=32) # 128x32 pixel display
d.clearAll()
d.writeAt("WELCOME TO THE",0,0)
d.writeAt("ADVENT WREATH",0,1)
d.writeAt("CALENDAR",3,2)
sleep(3)

The object np will control the 24 neopixel LEDs via pin GPIO0. It is clear that GPIO0 must act as an output. The functionality of neopixel LEDs I have described in Bandit - Games with the ESP32 in MicroPython described.

Then I create an I2C bus instance and pass it to the display object d which works with 32 pixels display height. The 128 pixels width are set as default value in the module OLED module. You can load the module in Thonny by double-clicking on the file name in the editor to study the inner workings of the module.

I delete the display completely and output a welcome message. After this, the program snores for a whole 3 seconds.

The definition of the "candles" follows. So that they are also addressable in the compound, I fill a list with the objects. Now I can for example delete or turn on all "candles" by means of a for-loop.

k1 = Pin(2,  Pin.OUT, value = 1) # D4
k2 = Pin(14, Pin.OUT, value = 1) # D5
k3 = Pin(12, Pin.OUT, value = 1) # D6
k4 = Pin(13, Pin.OUT, value = 1) # D7
candle=[k1,k2,k3,k4] # Candle list

The container sundays takes the dates of the four Sundays of Advent and with timeZone I set the time zone with which I convert the UTC (Coordinated Universal Time) of a NTP server into the local time (CET Central European Time). "sundays" is a so-called list. This sequential data type is mutable. This means that the fields are mutable even during the program run. The fields are addressed by their place number, the so-called Indexwhich is appended to the list name in square brackets. Candle[2] thus addresses k3, because the index count starts at 0.

sundays=[0,0,0,0]# List of Advent Sundays
timeZone=+1 # Berlin time zone

If no time server is available because of missing WLAN access, I use the RTC module (Real Time Clock) built into the ESP8266. Its accuracy leaves a lot to be desired, but with our time resolution in days it is OK. While the access to a time server allows to switch on our circuit at any time of the year, it synchronizes itself, the setup has to be done with RTC-access has to be started at the right time. The same is true for the test mode. The two 8-tuples tag and rtcTag define the respective start time. The fields are arranged as follows.

tag:

(year, month, day, hour, minute, second, day of the week, day of the year)

rtcDay:

(year, month, day, weekday, hour, minute, second, milliseconds)

rtc=RTC()
tag=(2022,12,17,8,0,0,6,0)  # debug tag start
rtcTag=(2022,11,27,6,8,0,0,0) # RTC day stamp start
weekday=[
   "Monday",
   "Tuesday",
   "Wednesday",
   "Thursday",
   "Friday",
   "Saturday",
   "Sunday"
  ]
syncTime=60000 # ms
refreshTime =20000 # ms

In order to be able to output the weekdays in plain text, their names must be entered in the list weekday summarized. The index runs from 0 for Monday to 6 for Sunday, according to the value in the tuples. Weekday[4] therefore returns Friday.

syncTime is the time period after which a synchronization with a timeserver via NTP is performed, while refreshTime sets the interval for a refresh of the illumination of the rings and the "candle" LEDs.

I control the brightness of the neopixels via the variable factor, the colors are set by the tuples in the list color list. You see that lists need not contain only simple data types. Each color tuple is addressable by its index.

factor=0.3 # brightness factor
color=[(160,0,0), # red
      (120,40,0),
      (80,80,0), # yellow
      (40,120,0),
      (0,160,0), # green
      (0,120,40),
      (0,80,80), # cyan
      (0,40,120),
      (0,0,160), # blue
      (40,0,120),
      (80,0,80), # magenta
      (120,0,40),
    ]

To access an NTP server we need the WLAN. The access parameters for the SSID and the password depend on the defaults of your WLAN router. So be sure to enter your own credentials here. The port number is almost freely selectable and may be between 1024 and 65535.

# **************Define WLAN access*******************
#
mySSID="Here goes your SSID"
myPass="Here goes your password"
myPort=9009

The network interface (NIC) of the ESP8266 returns various status messages when a connection is established. The Dictionary (Dict for short) connectStatus translates the numeric codes into plain text.

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",
  }

Like the values in a tuple, the keys in a dict are immutable; they cannot be subsequently changed. While keys must additionally be unique, values may occur more than once. Dicts are enclosed by curly braces. The key-value pairs are separated by a colon. There is a comma between the pairs.

A large part of the program work is done by functions. The function hexMac() translates a bytes object supplied by the WLAN module into a human-readable string. This string consists of the usual hexadecimal digits 0-9 and A-F, through which the MAC address of the station interface of the ESP8266 is defined.

This six-pack must be made known to the WLAN router so that it grants access to the ESP8266. To do this, the address must be added to the list of authorized devices. You can usually find this in the maintenance menu of your router under the item WLAN - Security. For the exact procedure please consult the manual of your device. By the way, for security reasons, it is not a good idea to allow access to all devices that log in by turning off MAC filtering. The next hacker will be very happy if you keep all doors open for him.

All other functions deal with lighting control. This is how setPixel() controls the LED with the number num with the color pattern in r, g and b where the factor f influences the brightness. However, the final values must not exceed 255.

def setPixel(num,r,g,b,f):
   r=int(r*f)
   g=int(g*f)
   b=int(b*f)
   np[num]=(r,g,b)
   np.write()

The functions ringTest() and candleTest() allow to check the health status of the LEDs in the ring and the "candles". In both functions all objects of the group are passed through with a for loop. pause defines the delay between the LEDs in the rings. In MicroPython, the upper limit of a range is always excluded. The run index of the for loop therefore takes values from 0 to 23. The loop is thus run 24 times.

def ringTest(pause,factor):
   for i in range(24):
       setPixel(i,160,160,160,factor)
       sleep(pause)
   sleep(2)
   clearCalender()

def candleTest():
   for i in range(4):
       candle[i].value(0)
       sleep(0.5)
   sleep(2)
   clearCandles()

Unlike Easter and Pentecost, Christmas is not determined by a day of the week, but by a date of the month. Therefore, the calendar position of Advent Sundays shifts from year to year. The task of the function adventSundays() is to determine the day data from the weekday of Christmas Eve.

def adventSundays(year):
   global sundays
   z=mktime((year,12,24,0,0,0,0,0))
   dateTime=localtime(z)
   wt=dateTime[6] # weekday of the 24.12.
   if wt==6:
       sundays=[3,10,17,24]
   else:
       s4=24-(wt+1)
       s1=s4-21 if wt <= 1 else (s4-21)+30
       sundays=[s1,s4-14,s4-7,s4]
   # print (sundays)

global sundays makes changes to the list inside the function available outside. A local variable like zdeclared inside a function is not referenceable outside the function, it does not exist. As soon as the function is exited, nobody knows this z. It is possible to read values from outside a function at any time.

z=mktime((year,12,24,0,0,0,0,0))

The function mktime() is a method from the class time. It creates a timestamp in seconds from the 8-tuple of a given time since 01/01/2000, 00:00. With the argument year the current Christmas Eve of this year is defined. From the timestamp I can now inversely easily determine the weekday of 12/24. This makes

dateTime=localtime(z)
wt=dateTime[6]

If wt has the value 6 has, the soup is already eaten, then the 24.12.is a Sunday and the Advent takes place certainly within the month of December. We will have that in 2023.

if wt==6:
   sundays=[3,10,17,24]

Otherwise, the first Sunday of Advent can be already in November, as it is the case this year (2022). So we have to find out the date of the last Sunday before Christmas Eve. We subtract the number of the weekday increased by 1 from 24. This year the 24.12. is a Saturday with the number 5. 24 - 6 = 18, so the last Sunday before Christmas is the 18.12.

else:
   s4=24-(wt+1)
   s1=s4-21 if wt <= 1 else (s4-21)+30
   sundays=[s1,s4-14,s4-7,s4]

From there we go back 21 days = 3 Advent weeks. If the result is positive, then the first Advent is immediately fixed. If the result is negative, then we are in November. If now 30 is added, we have caught the first Advent, it is the 27.11.

Ahh, wait a minute, -21 + 30 = 9 and 18 + 9 = 27, then you could write s4 + 9, instead of (s4 - 21) +30. Yes, you could, arithmetically it is correct, but the procedure is then not so easily comprehensible. Where does the 9 come from? In the end, this has something to do with number theory, namely with the Modulo-calculation. If I were to carry out an analogous calculation from January back to December, then I would have to calculate with modulo 31 instead of modulo 30, and then the 9 would no longer be correct.

Figure 4: Advent Sundays

Image 4Advent Sundays

18 + 9 is purely logically the 27th of December, but not the 27th of November.

Now it is about lighting the candles. The function lightCandles() I pass a default date tuple. If it doesn't get one, then the function gets one itself from the system time. Here I have to take the timezone into account. With the year in the field dateTime[0] I determine the daily data of the Advent Sundays of the current year.

def lightCandles(dt=None):
   if dt is not None:
       dateTime=dt
   else:
       dateTime=localtime(time()+timeZone*3600)
   adventSundays(dateTime[0])
   if (sundays[0] <= dateTime[2] and dateTime[1] == 11) or\
      (sundays[0] >=27 and dateTime[2]<=24 \
       and dateTime[1]==12):
       candle[0].value(0)
       print(0,sundays[0], dateTime[2])
   for i in range(1,4):
       if (sundays[i] <= dateTime[2]) and dateTime[1]==12:
           print(i,sundays[i], dateTime[2])
           candle[i].value(0)
           candle[0].value(0)

If the current date is on or after the first Sunday of Advent and we are in November, or if the first Advent is in November and the current date is between 12/1 and 12/24 inclusive, then the first candle must be lit. The instruction

candle[0].value(0)

sets the cathode of candle k1 to GND potential, the LED turns on and starts flickering, as candles do.

If the day's date is on or after the second, third or fourth Advent Sunday, the respective candle must also be lit when the time comes. To be on the safe side, we also light k1. The list candle and the for-loop save us from having to write a similar sequence three times.

Similar to the test routines work the functions that make the rings and "candles" go out.

def clearCandles():
   for i in range(4):
       candle[i].value(1)
       
def clearCalender():
   for i in range(neoCnt):
       np[i]=(0,0,0)
   np.write()

Before the rings are turned off, they must first be on. This is done by the function setCalender(). It is also usually passed a dateTime tuple.

def setCalender(dt=None):
   if dt is not None:
       dateTime=dt
   else:
       dateTime=localtime(time()+timeZone*3600)
   dom=dateTime[2]
   month=dateTime[1]
   if month==12 and dom <= 24:
       for tag in range(dom):
           r,g,b=color[tag % 12]
           setPixel(tag,r,g,b,factor)
           sleep(0.5)

The month date is extracted, and if we are in December, all LEDs are illuminated from the first to the current date. The for loop does this with the help of the function setPixel(). The brightness factor that we defined at the beginning is also taken into account here. So that we can enjoy the colors individually, there's a small purple pause of 0.5 seconds in between. So the whole spectacle can last up to 12 seconds. Vary this value as you like, just make sure that the total duration does not exceed the refresh interval.

So that for each operation mode the retrieval of the time tuple is done in the correct way, I have added the function getDayTime() function. It recognizes by the status values what has to be done.

def getDayTime():
   if debug:
       return tag
       #print("debugging")
   if nicStatus != 4:
       return localtime(time()+timeZone*3600)
       #print("Local time")
   yr,mon,day,dow,hor,minute,sec,ms=rtc.datetime()
   #print("RTC-Time")
   return(yr,mon,day,hor,minute,sec,dow)

Became debug at True then the corresponding sequences in the main loop themselves take care of the timing in tag.

If the nicStatus is not equal to 4, then there is most likely a WLAN connection and the system time is synchronized via NTP. We include the time zone and return the standardized 8-tuple.

If neither is the case, then the timekeeping is based on the RTC. However, the field order in its timestamp must be converted to the standard format.

The gold tinsel on the Advent wreath is the function TimeOut(), small but nice and full of finesse. The function does not return a value, but the function compare(). Strictly speaking, it is not the function that is returned, but a reference to it. I already wrote above that objects defined inside a function are not visible outside, they are local. If the function is exited, all locally defined objects die. This will be the case with TimeOut() is bypassed by using the function defined within compare() is applied to the parameter t and the outside of compare(), defined to TimeOut() local variable start accesses. From the function TimeOut() becomes a so-called Closure.

This pull-up allows me to include as many easy-to-manage software timers in my programs as I want. Each timer works independently from the others in the background, so it does not block the program flow in any way. Only when calling the reference to the returned function, the function wakes up from its slumber and returns the information whether the timer has expired or not.

def TimeOut(t):
   start=ticks_ms()
   def compare():
       return int(ticks_ms()-start) >= t
   return compare

The next step is to set up WLAN access. The message in the display informs us about this.

d.clearAll()
d.writeAt("CONNECTING TO",1,0)
d.writeAt(mySSID,4,1)
sleep(3)

First of all, I deliberately disable the AP interface, because this tends to cause irritations in connection with the ESP8266. This is the second step after disabling webREPL, which I already mentioned at the beginning.

nic = network.WLAN(network.AP_IF)  # AP interface object
nic.active(False)                  # Switch off safely

Then I turn on the station interface (STA) and enable it. The method config() with the argument 'mac' (as a string!), returns the MAC address I got from hexMac() translates it into plain text.

nic = network.WLAN(network.STA_IF) # Create WiFi object
nic.active(True)                   # Switch on STA object nic

MAC = nic.config('mac')   # get binary MAC address and
myMac=hexMac(MAC)         Convert # to a hex digit sequence
print("STATION MAC: \t"+myMac+"\n") # output

At this point, an ESP8266 has already automatically established a WLAN connection if it has previously connected to this access point. If not, then the following sequence will try to establish a connection. For this the credentials mySSID and myPass needed.

if not nic.isconnected():
 # Connect to AP in local network and show status
 nic.connect(mySSID, myPass)
 # wait until the connection to the access point is established
 print("connection status: ", nic.isconnected())
 n=0
 line="..........."
 while (nic.status() != network.STAT_GOT_IP) and (n < 10):
   n+=1
   print(".",end='')
   d.writeAt(line[0:n],0,2)
   sleep(1)

The game will now be started between accesspoint and ESP8266, which may take a few seconds. As long as the ESP8266 is being serviced by the DHCP of the router, dots in the terminal and in the display show the progress of the negotiations.

If the whole process takes longer than 10 seconds, then the ESP8266 probably cannot reach the router or does not receive access permission from the router. Have you entered the MAC address at the router and used the credentials without errors at the beginning?

We get and note the status and announce the state in the terminal and on the display.

nicStatus=nic.status()
print("\nConnection status: ",connectStatus[nicStatus])

STAconf = nic.ifconfig()
print("STA-IP:\t\t",STAconf[0],"\nSTA-NETMASK:\t",STAconf[1],\
     "\nSTA-GATEWAY:\t",STAconf[2] ,sep='')

d.clearAll()
if nicStatus == 5: # got IP
   d.writeAt(STAconf[0],0,0)
   d.writeAt(STAconf[1],0,1)
   d.writeAt(STAconf[2],0,2)
else:
   d.writeAt("STATION CONNECT",0,0)
   d.writeAt("FAILED",4,1)
   d.writeAt("USING RTC",2,2)
sleep(3)

The timer for synchronizing date and time is set, as well as the one for renewing the display of the rings and "candles". Then we turn off both groups of LEDs.

syncIt=TimeOut(syncTime) # Clock synchronization
renew =TimeOut(refreshTime)    # Refresh display
clearCandles()
clearCalender()

I try to reach a NTP server. If this succeeds, then the system time is synchronized with it.

try:
   ntptime.settime()
   print("Synchonized",localtime(time()+timeZone*3600))

In the other case I set the time of the RTC to the value specified at the beginning in rtcTag. The tuple should of course contain the current day and time.

except:
   rtc.datetime(rtcTag)
   print("RTC time set",rtc.datetime())#localtime(time()+timeZone*3600))

Now I fetch with getDayTime() to get the time data in standard format and initialize calendar and "candles" with it. After that, clear the display.

dayTime=getDayTime()
setCalender(dayTime)
lightCandles(dayTime)
d.clearAll()

We enter the main loop:

while 1:
   dayTime=getDayTime()
   if renew():
       clearCandles()
       clearCalender()
       setCalender(dayTime)
       lightCandles(dayTime)
       if debug:
           year=tag[0]
           day=day[2]+1
           day= 1 if day == 31 else day
           sec=day[5]+1
           hor=tag[3]
           mn =tag[4]
           month= 12 if 1 <= day <= 25 else 11
           dow=tag[6]
           tag=(2022,month,day,hor,mn,sec,dow,0)
           dayTime=tag
           print(tag)
       renew=TimeOut(refreshTime)

Again, we get a standardized time tuple.

Has the refresh timer expired? Via the identifier renew I actually call the function compare() function, which calls True if the values specified in t to TimeOut() was exceeded in milliseconds.

Then the "candles" and the rings must be deleted and set with the new timestamp.

In the debug mode now in the timestamp in tag the current date is incremented. We take into account a possible change of month. Finally, we reset the timer.

The synchronization timer is also queried. If the remembered nicStatus equals 5 there is a WLAN connection and the system timer can be synchronized with the NTP server. If there is no connection, there is nothing to do. Reset the synchronization timer and that's it.

In debug mode, the seconds entry in tag must be updated.

    if debug:
       year=tag[0]
       mon=day[1]
       day=day[2]
       sec=tag[5]+1
       if sec == 60:
           sec = 0
           mn=tag[4]+1
       else:
           mn=tag[4]
       hor=tag[3]
       dow=tag[6]
       tag=(2022,mon,day,hor,mn,sec,dow,0)
       dayTime=tag
       print(tag)

The program can be ended when the 25.12. is reached. Well then: Merry Christmas, Veselé Vánoce, Merry Christmas, Feliz Navidad, Boldog Karácsonyt, Joyeux noël!

    if dayTime[2]==25 and dayTime[1] == 12:
       print("Program finished")
       d.clearAll()
       d.writeAt("HAPPY",5,0)
       d.writeAt("CHRISTMAS",2,1)
       sleep(5)
       exit()

The only thing left to do is to display the current day of the week including date and time. For this I use formatting stringswhich output the day, month and time in two digits, if necessary with a leading 0. The year remains four-digit. Pause for one second, then go to the next loop pass.

    d.clearAll(False)
   d.writeAt(weekday[dayTime[6]],4,0, False)
d.writeAt("{:02}.{:02}.{:04}".format(dayTime[2],dayTime[1],\
                               dayTime[0]),3,1,False)
d.writeAt("{:02}:{:02}:{:02}".format(dayTime[3],dayTime[4],\
                               dayTime[5]),4,2, True)
sleep(1)

Function test

Of course you can't wait until 12/25 to find out if the circuit and program work. That's why I included the debug mode. You can set any date around Advent in the time tuple tag and code debug at True set. Then, when the program starts, the current date is incremented with each new refresh interval and you can check if the LEDs turn on at the right time.

Normal operation

For normal operation, synchronization with a time server via WLAN is the very simplest option. You can switch it on and off at any time. The connection with the time server sets the daily lighting within a short time, always on the pulse of the world time.

If no WLAN is available, then you proceed similarly with the function test. Only enter a whole time stamp with year, month, day, hour, minute, second, weekday and a final 0 in the tuple rtcTag tuple. Then immediately start the program. The display will then react with a deviation of a few seconds. The inaccuracy of the Real Time Clock can of course lead to further deviations from the standard time. I have no further knowledge about this at the moment. I have also considered the use of an external RTC, but in the short time available I have not yet come to a conclusion. Only one thing is certain, that an ESP8266 in connection with the DS1302-BOB (Break Out Board) is overtaxed for this circuit, concerning digital inputs and outputs. An alternative would be a DS3231 with I2C interface. But this was not available at the moment.

Well, maybe there will be an article about RTC in the near future. The story does not necessarily have to be about Advent.

By the way, the story by Toni Lauerer I also found on the Internet. When you have finished building your Advent wreath calendar, programmed it, festively decorated it with fir trees and started it, you will have enough quiet hours until Christmas. I recommend you then to take a look at this story. Believe me, it's worth it! And what can not happen to you in any case with the Advent wreath calendar:

If the fifth candle burns, then you have missed Christmas!

Have a great Advent and enjoy the project.

DisplaysEsp-8266Specials

6 comments

Mario Wegner

Mario Wegner

Danke Jürgen, das hat geholfen.
Jetzt ergeben sich für mich noch zwei Fragen:
1. Nach Skript werden die Pins für LED – Kerzen als Out definiert. Damit liefern sie beim Einschalten Strom (Plus-Pol). Im Schaltbild werden aber alle LED`s mit dem Minus-Pol an den Pins angeschlossen und alle Plus-Pole auf den 3v – Pin? Ist das so? Ein Einschalten der Pins mfehlt doch dann der Minus-Pol?
2. Gilt der Ermittlung der Sonntage nur für 2022 oder muss das Skript für 20323 umgeschrieben werden. Dem Kalender nach kann ich ja noch Nichts sehen.

Danke für die Hilfe
Gruß MWR

Jürgen

Jürgen

@MWR
Aus welchem Pogramm heraus hast du die Fehlermeldung bekommen? Any way, um sicher zu gehen, habe ich die beiden Module oled.py und ssd1306.py über die Links im Blog heruntergeladen und auf einen ESP32 gespielt. Die folgenden Zeilen sollten nun den Fehler verifizieren, tun es aber nicht.
from machine import SoftI2C,Pin
from oled import OLED
i2c=SoftI2C(Pin(22),Pin(23))
d=OLED
Das Display wird wie gewünscht initialisiert – ohne Fehlermeldung.

Idee zur Lösung: Ist denn das Modul ssd1306 in den Flash des Controllers hochgeladen worden? Ist die Datei unbeschädigt über den Link im Blog heruntergeladen worden? SSD1306_I2C ist eine Klasse, die den Hardwaretreiber darstellt und im Modul ssd1306.py zu finden. SSD1306_I2C wird von OLED importiert und beerbt. Wenn der Import nicht funktioniert (Zeile 27), dann gehe ich davon aus, dass die erste Lösungsidee zutrifft.

MWR

MWR

Leider gibt es Importprobleme:
Traceback (most recent call last):
File “”, line 18, in
File “oled.py”, line 27, in
File “ssd1306.py”, line 27, in
ImportError: can’t import name SSD1306_I2C

Hast Du eine Lösung dafür?

Kurt Hennig

Kurt Hennig

Danke, das ging aber schnell,
Grüße

Andreas Wolter

Andreas Wolter

@Kurt Henning: tatsächlich hatten wir den Link zur advent.py nicht im Text. Das haben wir ergänzt. Sorry dafür. Jetzt sollte es passen.

Grüße
Andreas Wolter
AZ-Delivery Blog

Kurt Hennig

Kurt Hennig

Hallo und einen guten Tag,
das liest sich alles sehr gut. Gibt es das Programm auch als ganzes zum Download?

Für eine Antwort wäre ich dankbar
Kurt Hennig

Leave a comment

All comments are moderated before being published

Recommended blog posts

  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