Badger 2040W Birthday calendar time.sleep loop?

Hi,

I wrote a little script for the Badger 2040W to function as a birthday calendar. Basicly, it show the days untill the next persons birthday from a dictionary of people and birthdays. Something like an offline Facebook Reminder, without the need of Facebook.

The first time I run this, the code runs great. Then, I make the device sleep for 8 hours (30 seconds right now for testing purpose), then check the date again to see if a day has passed. When someone has a birthday, the screen colors are inverted, the LED turns on and the display reads “Happy Birthday!”

import time
import machine
import badger2040

# Define the birthdays dictionary
birthdays = {
    "Person 1": (1, 9),
    "Person 2": (1, 20),
    "Person 3": (2, 4),
}
badger = badger2040.Badger2040()

# Function to get the current date from the onboard Wi-Fi chip
def get_current_date_wifi():
    display = badger2040.Badger2040()
    display.set_update_speed(2)
    display.set_thickness(4)
    WIDTH, HEIGHT = display.get_bounds()

    if badger2040.is_wireless():
        import ntptime
        try:
            display.connect()
            if display.isconnected():
                ntptime.settime()
                badger2040.pico_rtc_to_pcf()
        except (RuntimeError, OSError) as e:
            print(f"Wireless Error: {e.value}")

    # Thonny overwrites the Pico RTC, so re-sync from the physical RTC if we can
    try:
        badger2040.pcf_to_pico_rtc()
    except RuntimeError:
        pass

    rtc = machine.RTC()
    return list(rtc.datetime())[1:3]

# Function to calculate days in a month
def days_in_month(month, year):
    if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
        return 29
    return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]

# Function to display birthday message
def display_birthday_message(display, next_birthday_person, days_until_birthday):
    display.clear()  # White background
    display.set_pen(15)  # Black text

    display.set_font("bitmap8")

    text_width = display.measure_text(next_birthday_person, scale=6)
    display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

    if days_until_birthday == 0:
        # Display "Gelukkige verjaardag!" centered and larger
        birthday_message = "Happy Birthday"
        badger.led(255)
        text_width = display.measure_text(birthday_message, scale=3)
        display.text(birthday_message, (296 - text_width) // 2, 95, scale=3)
    else:
        # Display remaining days larger and centered
        days_text = f"Still {days_until_birthday} days"
        badger.led(0)
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)  # Updated height to 95

    display.update()
    time.sleep(5)  # Display the message for 5 seconds

# Display Setup
display = badger2040.Badger2040()

# Run the script every 8 hours
while True:
    current_month, current_day = get_current_date_wifi()

    next_birthday_person = None
    days_until_birthday = float('inf')

    for person, birthday in birthdays.items():
        birthday_month, birthday_day = birthday

        current_days = current_day + sum(days_in_month(m, 2023) for m in range(1, current_month))
        birthday_days = birthday_day + sum(days_in_month(m, 2023) for m in range(1, birthday_month))

        days_until = birthday_days - current_days

        if days_until < 0:
            # Adjust for the next year
            days_until += sum(days_in_month(m, 2023) for m in range(current_month, 13))

        if days_until < days_until_birthday:
            days_until_birthday = days_until
            next_birthday_person = person

    display.clear()
    display.set_pen(15)  # Black text
    display.set_font("bitmap8")

    text_width = display.measure_text(next_birthday_person, scale=6)
    display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

    days_text = f"Still {days_until_birthday} days"
    badger.led(0)
    text_width = display.measure_text(days_text, scale=4)
    display.text(days_text, (296 - text_width) // 2, 95, scale=4)

    display.update()

    if days_until_birthday == 0:
        display_birthday_message(display, next_birthday_person, days_until_birthday)
        
time.sleep(30)

The second time this code runs, it shows me a white display. I get the “connected” promt and my IP adress, then white. Is this a pen color/background color issue? Is my code incorrect?

Yes, it’s a pen colour issue.

First off, despite your comments, set_pen(15) selects a white pen, not black (see badger2040/docs/reference.md at 9b6acf25c2e79f5821f38ae7dcf881d2433c86f6 · pimoroni/badger2040 · GitHub)

Secondly, you only ever set the pen to 15 (white), so everything ends up white on white.

clear() clears the display to the current pen colour; so if you were to want (for example) a white background and black text, you would need to do:

...
display.set_pen(15)  # White pen
display.clear()
display.set_pen(0)  # Black pen
display.set_font("bitmap8")
...

It works the first time because the system will default to pen 0 (black) until the first time you change it.

So sorry, stupid mistake. this fixes it!

import time
import machine
import badger2040

# Define the birthdays dictionary
birthdays = {
    "Person 1": (1, 12),
    "Person 2": (1, 20),
    "Person 3": (2, 4),
}

badger = badger2040.Badger2040()

def connect_and_sync_time():
    display = badger2040.Badger2040()
    display.set_update_speed(2)
    display.set_thickness(4)
    WIDTH, HEIGHT = display.get_bounds()

    if badger2040.is_wireless():
        import ntptime
        try:
            display.connect()
            if display.isconnected():
                ntptime.settime()
                badger2040.pico_rtc_to_pcf()
        except (RuntimeError, OSError) as e:
            print(f"Wireless Error: {e.value}")

    try:
        badger2040.pcf_to_pico_rtc()
    except RuntimeError:
        pass

def get_current_date_wifi():
    connect_and_sync_time()
    rtc = machine.RTC()
    return list(rtc.datetime())[1:3]

def days_in_month(month, year):
    if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
        return 29
    return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]

def display_birthday_message(display, next_birthday_person, days_until_birthday):
    display.set_pen(15)  # White pen
    display.clear()
    display.set_pen(0)  # Black pen
    display.set_font("bitmap8")

    text_width = display.measure_text(next_birthday_person, scale=6)
    display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

    if days_until_birthday == 0:
        birthday_message = "Happy Birthday"
        badger.led(255)
        text_width = display.measure_text(birthday_message, scale=3)
        display.text(birthday_message, (296 - text_width) // 2, 95, scale=3)
    else:
        days_text = f"Still {days_until_birthday} days"
        badger.led(0)
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)

    display.update()
    time.sleep(5)

def main():
    display = badger2040.Badger2040()

    while True:
        current_month, current_day = get_current_date_wifi()

        next_birthday_person = None
        days_until_birthday = float('inf')

        for person, birthday in birthdays.items():
            birthday_month, birthday_day = birthday

            current_days = current_day + sum(days_in_month(m, 2023) for m in range(1, current_month))
            birthday_days = birthday_day + sum(days_in_month(m, 2023) for m in range(1, birthday_month))

            days_until = birthday_days - current_days

            if days_until < 0:
                days_until += sum(days_in_month(m, 2023) for m in range(current_month, 13))

            if days_until < days_until_birthday:
                days_until_birthday = days_until
                next_birthday_person = person

        display.set_pen(0)  # White pen
        display.clear()
        display.set_pen(15)  # Black pen
        display.set_font("bitmap8")

        text_width = display.measure_text(next_birthday_person, scale=6)
        display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

        days_text = f"Still {days_until_birthday} days"
        badger.led(0)
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)

        display.update()

        if days_until_birthday == 0:
            display_birthday_message(display, next_birthday_person, days_until_birthday)

        time.sleep(30)

if __name__ == "__main__":
    main()

Very nice project!

Some suggestions:

  • order your entries by date. Then you don’t need to sleep eight hours, you could just sleep until the the next birthday is due (maybe minus some days so you have time to buy some gifts). This requires running on USB-power. As an alternative:
  • The badger2040w has a sophisticated power-down and wake-up circuit. You could probably run for years on a set of batteries. The only thing to keep in mind is that the RTC the badger uses has a time horizon of one month, so you would have to wake-up at least once a month.

These are great ideas! I will continue to work on it, I’ll let you know!

Changed it a bit for Tufty 2040 (as there is no built-in WiFi) and added day of week calculations, so you know where you stand…

import time
import machine
from picographics import PicoGraphics, DISPLAY_TUFTY_2040, PEN_RGB332

# Define the birthdays dictionary
birthdays = {
    "L": (2, 10),
    "M": (2, 28),
    "A": (4, 17),
    "T": (6,10),
    "S": (10,17),
    "C": (12,8),
}

display = PicoGraphics(DISPLAY_TUFTY_2040, pen_type=PEN_RGB332)
WIDTH, HEIGHT = display.get_bounds()

def days_in_month(month, year):
    if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
        return 29
    return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]

def display_birthday_message(display, next_birthday_person, days_until_birthday):
    display.set_pen(15)  # White pen
    display.clear()
    display.set_pen(0)  # Black pen
    display.set_font("bitmap8")

    text_width = display.measure_text(next_birthday_person, scale=6)
    display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

    if days_until_birthday == 0:
        birthday_message = "Happy Birthday"
        text_width = display.measure_text(birthday_message, scale=3)
        display.text(birthday_message, (296 - text_width) // 2, 95, scale=3)
    else:
        days_text = f"Still {days_until_birthday} days"
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)

    display.update()
    time.sleep(5)

while True:
        year, month, day, hour, minute, second, seconds, subseconds = time.localtime()
         
        # Day of Week
        day_of_week = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
    
        # Common Year Offsets
        month_offset = [0,3,3,6,1,4,6,2,5,0,3,5]
        
        # Leap Year Offsets
        if ((year%4)==0) and ((year%100)==0) and ((year%400)==0):
           month_offset=[0,3,4,0,2,5,0,3,6,1,4,6]
           
        gregor_weekday = (((day+month_offset[month-1]) + 5*((year-1)%4) + 4*((year-1)%100) + 6*((year-1)%400))%7)

        next_birthday_person = None
        days_until_birthday = float('inf')

        for person, birthday in birthdays.items():
            birthday_month, birthday_day = birthday

            current_moment = day + sum(days_in_month(m, year) for m in range(1, month))
            birthday_days = birthday_day + sum(days_in_month(m, year) for m in range(1, birthday_month))

            days_until = birthday_days - current_moment

            if days_until < 0:
                days_until += sum(days_in_month(m, year) for m in range(month, 13))

            if days_until < days_until_birthday:
                days_until_birthday = days_until
                next_birthday_person = person

        display.set_pen(0)  # White pen
        display.clear()
        display.set_pen(15)  # Black pen
        display.set_font("bitmap8")

        text_width = display.measure_text(next_birthday_person, scale=6)
        display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)

        days_text = f"Still {days_until_birthday} days"
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)

        #Display current date (day of week: year, month, day)
        current_date = (f'{day_of_week[gregor_weekday]} + {year} + {month} + {day}')
        text_width = display.measure_text(current_date, scale=2)
        display.text(current_date, (296 - text_width) // 2, 195, scale=2)

        display.update()

        if days_until_birthday == 0:
            display_birthday_message(display, next_birthday_person, days_until_birthday)

        time.sleep(30)

To calculate day of week, there is a much simpler solution if you base it on seconds since 01/01/1970 (epoch-time):

now_epoch = time.time()                          # seconds since 01/01/1970
now_day   = (int(now_epoch/86400)+3) % 7         # 01/01/1970 is Thursday

Note: my code uses Monday as day-zero (that is how struct_time defines it), so you will need +4 instead of +3.

Lovely! I’m very happy some people are using and adapting this idea. I intend to use it in elementary school, my students love looking forward to festivities.

I was also wondering:

  • Is there a way to try different Wifi networks? For example, can I load the wificonfig file with 3 different networks, then let the device try to connect ten times with each. If no connection can be made, wait an hour then try again all 3? Is there a known way?

  • Also, sometimes people share a birthday. Quite often, it would seem!
    Birthday problem - Wikipedia
    Multiple people dont fit quite well onscreen (Person1/Person2/Person3 : 20 january). Is there a way to automaticaly resize the names when they don’t fit onscreen?

lol…

Yeah… epoch-time. Thanks for that one. Some how I got lost in the weeds. Ended up using the sledgehammer method vs a nice and simple mallet.

Working on the sorting functionality now.

@PJS:

I was thinking of using alternating screens every x minutes with the words “(shared birthday)” below the ‘Still x days’… so you know that day there are more then just one. I have a few friends [3] born the same day, so will use them as test subjects…

Also an interesting approach! I’m still try some things whenever I find the time on my end here, will you posted!

Ok… here is my stab at dealing with shared birthdays both in the future and day of.

The script will sorted the data via birthdays and then names. So the user is free to enter in any order their birthdays. The script will note any shared days with a 10 second delay between each member.

This script should work on most devices, as I used basic colours/fonts. Feel free to modify to your heart’s content.

Keep me posted if you find something I can tighten or overlooked. Cheers and thanks!!!

import time
import machine
from pimoroni import Button
from picographics import PicoGraphics, DISPLAY_TUFTY_2040, PEN_RGB332

display = PicoGraphics(DISPLAY_TUFTY_2040, pen_type=PEN_RGB332)
WIDTH, HEIGHT = display.get_bounds()

# Define the birthdays dictionary
birthdays = {
    "S": (1,12),
    "R": (1,21),
    "L": (2,11),
    "M": (2,21),
    "K": (2,21),
    "P": (2,21),
    "N": (2,23),
    "A": (3,5),
    "I": (4,27),
    "J": (8,12),
    "E": (9,3),
    "G": (9,16),
    "T": (10,31),
    "Y": (11,7),
    "C": (12,25),
    "N_Y": (1,1),
}

# Sort Dictionary By: Birthdays, then Names
sorted_birthdays = dict()
sorted_birthdays = sorted (birthdays.items(), key=lambda item: (item[1] , item[0]))
dictionary_count=len(sorted_birthdays)
birthdays.clear()

# Identify Shared Birthdays
for n in range (dictionary_count):
        person,birthday = sorted_birthdays[n]
        sorted_birthdays[n] = person,birthday,0

for n in range (dictionary_count-1):
        person, birthday, shared = sorted_birthdays[n]
        comp_person, comp_birthday , comp_shared = sorted_birthdays[n+1]
        if (birthday) == (comp_birthday):
            sorted_birthdays[n] = person,birthday,1
            sorted_birthdays[n+1] = comp_person,comp_birthday,1

# Functions for Main Program
def days_in_month(month, year):
        if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
            return 29
        return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1]

def day_of_week(year,month,day):
        named_day = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
        month_offset = [0,3,3,6,1,4,6,2,5,0,3,5]
        if ((year%4)==0) and ((year%100)==0) and ((year%400)==0):
           month_offset=[0,3,4,0,2,5,0,3,6,1,4,6]
        gregor_weekday = (((day+month_offset[month-1]) + 5*((year-1)%4) + 4*((year-1)%100) + 6*((year-1)%400))%7)
        return named_day[gregor_weekday] #Gauss's algorithm
    
def completed_display():
        current_date = (f'{day_of_week(year,month,day)} + {year} + {month} + {day}')
        text_width = display.measure_text(current_date, scale=2)
        display.text(current_date, (296 - text_width) // 2, 195, scale=2)
        display.update()
        time.sleep(10)

def birthday_message():
        display.set_pen(15)  # White pen
        display.clear()
        display.set_pen(0)  # Black pen
        display.set_font("bitmap8")
        text_width = display.measure_text(next_birthday_person, scale=6)
        display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)
        text_width = display.measure_text("Happy Birthday", scale=3)
        display.text("Happy Birthday", (296 - text_width) // 2, 95, scale=3)

def days_to_birthday_message():
        display.set_pen(0)  # White pen
        display.clear()
        display.set_pen(15)  # Black pen
        display.set_font("bitmap8")
        text_width = display.measure_text(next_birthday_person, scale=6)
        display.text(next_birthday_person, (296 - text_width) // 2, 30, scale=6)
        days_text = f"Still {days_until_birthday} days"
        text_width = display.measure_text(days_text, scale=4)
        display.text(days_text, (296 - text_width) // 2, 95, scale=4)

def shared_birthday_message():
        shared_text =f"(shared birthday)"
        text_width = display.measure_text(shared_text, scale=2)
        display.text(shared_text, (296 - text_width) // 2, 130, scale=2)


while True:
        days_until_birthday = float('inf')
        year, month, day, hour, minute, second, seconds, subseconds = time.localtime()

        # Search for various birthday scenarios
        for n in range (dictionary_count):
            person, birthday, shared = sorted_birthdays[n]
            birthday_month, birthday_day = birthday
            
            current_moment = day + sum(days_in_month(m, year) for m in range(1, month))
            birthday_days = birthday_day + sum(days_in_month(m, year) for m in range(1, birthday_month))
            days_until = birthday_days - current_moment
  
            if days_until < 0:
                days_until += sum(days_in_month(m, year) for m in range(month, 13))

            if days_until < days_until_birthday:
                days_until_birthday = days_until
                next_birthday_person = person

            # Actual Birthday
            if (days_until_birthday == 0):
                if (shared == 0) and (person == next_birthday_person):
                    birthday_message()
                    completed_display()
                
                if (shared == 1) and (person == next_birthday_person):
                    current_birthday_person,current_birthday_date,current_birthday_shared = sorted_birthdays[n]
                    for s in range (dictionary_count-1):
                         person_a,birthday_a,shared_a = sorted_birthdays[s+1]
                         if birthday_a == current_birthday_date:
                            next_birthday_person = person_a
                            birthday_message()
                            shared_birthday_message()
                            completed_display()
                         

            # Shared & Unshared Day of Birth
            if (days_until_birthday !=0):
                if (shared == 1) and (person == next_birthday_person):
                    days_to_birthday_message()
                    shared_birthday_message()
                    completed_display()
                    
                    current_birthday_person,current_birthday_date,current_birthday_shared = sorted_birthdays[n]
                    for s in range (dictionary_count-1):
                        person_a,birthday_a,shared_a = sorted_birthdays[s+1]
                        if birthday_a == current_birthday_date:
                            next_birthday_person = person_a
                            days_to_birthday_message()
                            shared_birthday_message()
                            completed_display()
                    
                if (shared == 0) and (person == next_birthday_person):
                    days_to_birthday_message()
                
        completed_display()    
    

Mornin’ P J S!! –

How is your [elementary school] birthday calendar programming coming along? - curious if you come up with any new angles.

Cheers,
Tomas

Hi Tomas, still working on it! Right now, it is sitting on my desk, and as my work days move along I check the code somedays. The leap year is calculated wrong for example, as I noticed recently. Progress is slow, but I don’t really mind, this way, my kids can see that working on something can be a steady slow process. "Don’t give up’ and ‘Mistakes are okay’ types of messages!

Hello PJS!!

My apologies, thank you for pointing out my coding error, I’ve fixed it as noted in the screenshot

That should correct for the leap year miscalculations. Tested with several years ahead and with 2100 as a sample…

Cheers,
Tomas