These instructions are also available as PDF document.
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
Image : 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.
Resistor 1 kΩ
Base board 17cm x 17cm
For flashing and programming the ESP32:
Used firmware for the ESP8266:
The MicroPython programs for the project:
ssd1306.py Hardware driver to the OLED display
oled.py API for the OLED display
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.
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.
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.
Image Advent 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).
Image : 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.
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:
synchronized by an NTP server
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:
After that the line appears:
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.
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.
An import like by
binds all lines of the module with the prefix network or socket into the namespace of the program. On the other hand imports
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.
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.
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.
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 thus addresses k3, because the index count starts at 0.
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.
(year, month, day, hour, minute, second, day of the week, day of the year)
(year, month, day, weekday, hour, minute, second, milliseconds)
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 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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
Image Advent 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 I determine the daily data of the Advent Sundays of the current year.
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
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.
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.
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.
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.
The next step is to set up WLAN access. The message in the display informs us about this.
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.
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.
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.
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.
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.
I try to reach a NTP server. If this succeeds, then the system time is synchronized with it.
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.
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.
We enter the main loop:
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.
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!
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.
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.
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.