Leila

Leila

We made a clock with Python

Last week we were at DjangoCon Europe in Heidelberg and I gave a lightning talk about a clock that we made with Python.

we made this clock because ours was broken, one of the segment was dead, like in this picture:

After lot of research to find a new one with the right requirements (it had to be blue and it had to be visible when it was dark) and without finding what we were looking for we finally decided to make one ourselves.

To make this clock we had to use:

  • a breadbord
  • lots of wires
  • an RTC module
  • a blue quad 7-segment display and its driver
  • an M0 trinket
  • and few other stuff

Here is plan A

Plan a

This didn't work because we used a 3.3V to 5V 1-way logic converter (the data sheet said the display drivers needed a 5V signal and the trinket outputs a 3.3V signal) but we should have used a 2-way logic converter (as the display driver "talks back" to the micro-controller) and we didn't have one.

Plan b

We had to make a plan B, after all the display driver works fine with a 3.3V signal, so we got rid of the logic converter all together and the whole circuit is that much simpler.

We also decided to use a single medium breadboard and therefore had to re-do the whole circuit.

After the circuit the code

To make this clock we wrote 2 different files

  • full_clock.py
  • utils.py

full_clock.py

full_clock.py is the main file of the project, it's used to read the time and date from the RTC module, display the time or what we want to display on the display and it's also used to manage buttons.

from board import SCL, SDA, D1, D3, D4  
from busio import I2C

import digitalio

from time import sleep

from adafruit_ht16k33.segments import Seg7x4

from pcf8523_light import Rtc  
from utils import bcd_to_int, int_to_bcd, get_dst


i2c = I2C(SCL, SDA)  
rtc = Rtc(i2c)  
display = Seg7x4(i2c)

# Initialize buttons
mode_btn = digitalio.DigitalInOut(D1)  
mode_btn.direction = digitalio.Direction.INPUT  
mode_btn.pull = digitalio.Pull.DOWN

left_btn = digitalio.DigitalInOut(D3)  
left_btn.direction = digitalio.Direction.INPUT  
left_btn.pull = digitalio.Pull.UP

right_btn = digitalio.DigitalInOut(D4)  
right_btn.direction = digitalio.Direction.INPUT  
right_btn.pull = digitalio.Pull.DOWN

debug = False

mode = 0  
mode_ct = 0  
max_ct = 100

read_mode_mapping = (  
    'read_time',
    'read_date',
    'read_year',
)

write_mode_mapping = (  
    ((Rtc.HOURS,  24, 0), (Rtc.MINUTES, 60, 0)),
    ((Rtc.MONTHS, 13, 1), (Rtc.DAYS,    32, 1)),
    (None,                (Rtc.YEARS,  100, 0)),
)


old_r_val = 0  
old_mode = 0

is_dst = False


while True:  
    cur = getattr(rtc, read_mode_mapping[mode])(debug=debug)
    if mode != 0:
        mode_ct += 1

    if cur is None:
        continue

    new_r_val = bcd_to_int(cur[1])
    if old_r_val != new_r_val or old_mode != mode:
        old_r_val = new_r_val
        old_mode = mode

        new_dst = get_dst(rtc)
        if new_dst is not None:
            is_dst = new_dst

        # We only care about dst if displaying the time
        if mode == 0 and is_dst:
            cur[0] = int_to_bcd((bcd_to_int(cur[0]) + 1) % 24)

        display.fill(0)

        for i in range(2):
            display.put('{}'.format(cur[i] & 0x0f), i * 2 + 1)
            left = (cur[i] & 0xf0) >> 4
            if left != 0 or (mode != 1 and i == 1):
                # put expects a string
                display.put('{}'.format(left), i * 2)

        if mode == 0:
            display.put(':')
        elif mode == 1:
            display.put('.', 1)

        display.show()

    if mode_btn.value:
        mode = (mode + 1) % len(read_mode_mapping)
        mode_ct = 0
        sleep(.15)
    else:
        btn_index = 0 if left_btn.value == 0 else 1 if right_btn.value else -1
        if btn_index != -1:
            mode_ct = 0
            write_operation = write_mode_mapping[mode][btn_index]

            if write_operation is None:
                continue

            new_val = int_to_bcd((bcd_to_int(cur[btn_index]) + 1) % write_operation[1])

            if new_val == 0:
                new_val += write_operation[2]

            if debug:
                print(cur[btn_index], bcd_to_int(cur[btn_index]), '->', new_val)

            rtc.write(write_operation[0], new_val, debug)
            old_r_val = 0xff
            sleep(.15)

    if mode_ct > max_ct:
        mode = 0

    sleep(.1)

utils.py

utils.py is used to pass from binary to integer and back it's also used to determine if it's summer or winter time.

get_dst is the daylight-saving-time rule for Western Europe because we are limited in memory and the clock is located in Belgium and we have no intention of moving it in the near future.

def int_to_bcd(val):  
    return ((val // 10) << 4) + (val % 10)


def bcd_to_int(val):  
    return ((val & 0xf0) >> 4) * 10 + (val & 0x0f)


def get_dow(day, month, year, debug=False):  
    Y = year if month > 2 else year - 1
    y = Y % 100
    c = Y // 100
    m = (month - 2) if month > 2 else month + 10

    if debug:
        print(Y, y, c, m)

    rv = int(day + (2.6 * m - .2) + y + (1.0 * y) / 4 + (1.0 * c) / 4 - 2 * c) % 7

    if debug:
        print('DOW for {}-{}-{} is {}'.format(year, month, day, rv))

    return rv


def get_dst(rtc, debug=False):  
    today = rtc.read_date(debug=debug)

    if not all(today):
        return None

    today = [bcd_to_int(item) for item in today]
    if today[0] > 10 or today[0] < 3:
        if debug:
            print('Winter')
        return False
    if today[0] > 3 and today[0] < 10:
        if debug:
            print('Not winter')
        return True
    if today[1] < 25:
        print('Early in month')
        return today[0] == 10

    year = rtc.read(rtc.YEARS, debug=debug)
    if year[0] is None:
        return None

    year = 2000 + bcd_to_int(year[0])
    dow = get_dow(today[1], today[0], year)

    if dow == 0:
        if debug:
            print('Sunday')
        hour = rtc.read(rtc.HOURS)[0]
        return (today[0] == 3 and hour >= 2) or (today[0] == 10 and hour < 2)

    dow_25 = get_dow(25, today[0], year)
    if dow_25 == 0:
        dow_25 = 7

    if debug:
        print(dow, dow_25)

    return (dow < dow_25) ^ (today[0] == 10)