[EN] Draw an analog clock using MicroPython.

The previous article discussed how to improve the display speed by using the double buffer technique. It is applied for the analog clock display as shown in Figure 1. We used Trigonometric calculations to determine the coordinates (x,y) of the tips of the seconds, minutes and hours. Each second operation uses a timer to make the operation closer to real-time than the Loop or Delay.

Figure 1 Result of drawing Analog clock

Equipment

The experimental equipment consists of

  1. esp32
  2. TFT 0.96″ display

Draw an analog clock

The 0.96″ TFT display uses the ST7735 driver as a driver. And in this example, using a REDTAB board with a resolution of 80×160 dots, need to rotate the screen by 90 degrees with rotation(1).

Draw a watch

The watch body is divided into two parts: the bezel part and the dot mark portion representing the numbers 1 to 12, which are numbers on the watch face.

Draw a frame

With the library in use, there is no circle draw command. So we used a midpoint circle draw algorithm from the geeksforgeeks website has written as follows

def midPointCircleDraw(x_centre, y_centre, r):
    x = r
    y = 0
     
    # Printing the initial point the
    # axes after translation
    print("(", x + x_centre, ", ",
               y + y_centre, ")",
               sep = "", end = "")
     
    # When radius is zero only a single
    # point be printed
    if (r > 0) :
     
        print("(", x + x_centre, ", ",
                  -y + y_centre, ")",
                  sep = "", end = "")
        print("(", y + x_centre, ", ",
                   x + y_centre, ")",
                   sep = "", end = "")
        print("(", -y + x_centre, ", ",
                    x + y_centre, ")", sep = "")
     
    # Initialising the value of P
    P = 1 - r
 
    while x > y:
     
        y += 1
         
        # Mid-point inside or on the perimeter
        if P <= 0:
            P = P + 2 * y + 1
             
        # Mid-point outside the perimeter
        else:        
            x -= 1
            P = P + 2 * y - 2 * x + 1
         
        # All the perimeter points have
        # already been printed
        if (x < y):
            break
         
        # Printing the generated point its reflection
        # in the other octants after translation
        print("(", x + x_centre, ", ", y + y_centre,
                            ")", sep = "", end = "")
        print("(", -x + x_centre, ", ", y + y_centre,
                             ")", sep = "", end = "")
        print("(", x + x_centre, ", ", -y + y_centre,
                             ")", sep = "", end = "")
        print("(", -x + x_centre, ", ", -y + y_centre,
                                        ")", sep = "")
         
        # If the generated point on the line x = y then
        # the perimeter points have already been printed
        if x != y:
         
            print("(", y + x_centre, ", ", x + y_centre,
                                ")", sep = "", end = "")
            print("(", -y + x_centre, ", ", x + y_centre,
                                 ")", sep = "", end = "")
            print("(", y + x_centre, ", ", -x + y_centre,
                                 ")", sep = "", end = "")
            print("(", -y + x_centre, ", ", -x + y_centre,
                                            ")", sep = "")

From the above code, we modified it as follows:

def midPointCircleDraw(x_centre, y_centre, r, c):
    x = r
    y = 0

    # When radius is zero only a single
    # point be printed
    if (r > 0) :
        tft.setPixel(x + x_centre,-y + y_centre, c)
        tft.setPixel(y + x_centre,x + y_centre, c)
        tft.setPixel(-y + x_centre,x + y_centre, c)
     
    # Initialising the value of P
    P = 1 - r
 
    while x > y:
        y += 1
         
        # Mid-point inside or on the perimeter
        if P <= 0:
            P = P + 2 * y + 1
             
        # Mid-point outside the perimeter
        else:        
            x -= 1
            P = P + 2 * y - 2 * x + 1
         
        # All the perimeter points have
        # already been printed
        if (x < y):
            break
         
        # Printing the generated point its reflection
        # in the other octants after translation
        tft.setPixel(x + x_centre,y + y_centre, c)
        tft.setPixel(-x + x_centre, y + y_centre, c)
        tft.setPixel( x + x_centre,-y + y_centre, c)
        tft.setPixel( -x + x_centre,-y + y_centre, c)
         
        # If the generated point on the line x = y then
        # the perimeter points have already been printed
        if x != y:
            tft.setPixel(y + x_centre, x + y_centre, c)
            tft.setPixel(-y + x_centre, x + y_centre, c)
            tft.setPixel(y + x_centre, -x + y_centre, c)
            tft.setPixel(-y + x_centre, -x + y_centre, c)

When adjusting the display with pint to setPixel() and drawing midPointCircleDraw(80, 40, 39, tft.color(232,232,232)) the result is as shown in Figure 2.

Draw pins

To draw the hour pin, draw a circle at the 12 o’clock position as the starting circle as shown in Figure 3, written as follows.

n12x = 80
n12y = 10
midPointCircleDraw(n12x, n12y, 3, tft.color(232, 232, 64))
Figure 3 Drawing a pin

Other positions use the principle of rotating the circle according to the degree of placement of the hour pin. A circle has a degree of 360 degrees. A total of 12 points must be rotated, so each peg must be rotated as follows.

  • degree = 360/12
    = 30

That means you have to rotate 30 degrees and draw a pin, 12 times in total.

To recalculate the values (x,y) you can use the equation mentioned in the rotation of the rectangle:

screenCenterX = 80
screenCenterY = 40

def rotate(pX,pY,angle):
    rad = math.radians(angle)
    pX -= screenCenterX
    pY -= screenCenterY
    xCos = pX*math.cos(rad)
    ySin = pY*math.sin(rad)
    xSin = pX*math.sin(rad)
    yCos = pY*math.cos(rad)
    newX = xCos - ySin + screenCenterX
    newY = xSin + yCos + screenCenterY
    return (int(newX), int(newY))

When drawing with the following command, the result will be as shown in Figure 4.

n12x = 80
n12y = 10
for i in range(12):
    newX,newY = rotate( n12x, n12y, 30*i)
    midPointCircleDraw( newX, newY, 3, tft.color(232, 232, 64))
Figure 4 Hours’s pins

Draw second hand

Drawing the second hand works on the same principle as calculating the position of the hour pin, but with 1 hour having 60 seconds, this means that there must be 60 seconds’ positions in 360 degrees.

  • degree = 360 / 60
    = 6

Therefore, when including drawing a straight line from the center of the screen and the tip of the hand is at the second sec. The code can be written as follows:

def drawSecond( sec ):
    deg = sec * 6
    n12x = 80
    n12y = 10
    newX,newY = rotate( n12x, n12y, deg)
    print(i, deg, newX, newY)
    tft.line((screenCenterX, screenCenterY), (newX, newY), tft.color(232, 64, 232))

When writing the code to display the second hand as follows, you will get an example result as shown in Figure 5.

for i in range(60):
    tft.fill(tft.BLACK)
    drawClock()
    drawSecond(i)
    tft.swap()
Figure 5 Example of the result of drawing the second hand

Draw minute hand

The minute hand uses the same calculation as the second but moves every time the second hand completes a cycle, so to draw the minute hand it can be written as follows.

def drawMinute( minute ):
    deg = minute * 6
    n12x = 80
    n12y = 10
    newX,newY = rotate( n12x, n12y, deg)
    tft.line((screenCenterX, screenCenterY), (newX, newY), tft.color(232, 242, 232))

Determining the relationship between the seconds and minutes values is written as follows.

minute = 10
for nLoop in range(300):
    for sec in range(60):
        tft.fill(tft.BLACK)
        drawClock()
        drawSecond(sec)
        drawMinute(minute)
        tft.swap()
    minute += 1

Draw hour hand

The hour hand calculates its position like drawing a pin. And is related to the minute value, that is, when the minute completes a cycle, the hour value is increased by 1, and if the hour value is greater than 12, return to 1 and the example result is as shown in Figure 1.

Example Code

An example program displays a clock with a default time of 1:30.00 and uses the second increment from the timer (read the article), resulting in the following code: And an example of the display is as shown in Figure 1.

from ST7735 import TFT
import machine as mc
from machine import SPI,Pin, Timer
import time
import math

#################################################################
###### setting ##################################################
#################################################################
mc.freq(240000000)
spi = SPI(2, baudrate=26000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
# dc, rst, cs
tft=TFT(spi,15,13,2)
screenCenterX = 80
screenCenterY = 40
second = 0
minute = 30
hour = 1

#################################################################
###### sub modules ##############################################
#################################################################
def midPointCircleDraw(x_centre, y_centre, r, c):
    x = r
    y = 0

    tft.setPixel(x + x_centre,y + y_centre, c)

    # When radius is zero only a single
    # point be printed
    if (r > 0) :
        tft.setPixel(x + x_centre,-y + y_centre, c)
        tft.setPixel(y + x_centre,x + y_centre, c)
        tft.setPixel(-y + x_centre,x + y_centre, c)
     
    # Initialising the value of P
    P = 1 - r
 
    while x > y:
        y += 1
         
        # Mid-point inside or on the perimeter
        if P <= 0:
            P = P + 2 * y + 1
             
        # Mid-point outside the perimeter
        else:        
            x -= 1
            P = P + 2 * y - 2 * x + 1
         
        # All the perimeter points have
        # already been printed
        if (x < y):
            break
         
        # Printing the generated point its reflection
        # in the other octants after translation
        tft.setPixel(x + x_centre,y + y_centre, c)
        tft.setPixel(-x + x_centre, y + y_centre, c)
        tft.setPixel( x + x_centre,-y + y_centre, c)
        tft.setPixel( -x + x_centre,-y + y_centre, c)
         
        # If the generated point on the line x = y then
        # the perimeter points have already been printed
        if x != y:
            tft.setPixel(y + x_centre, x + y_centre, c)
            tft.setPixel(-y + x_centre, x + y_centre, c)
            tft.setPixel(y + x_centre, -x + y_centre, c)
            tft.setPixel(-y + x_centre, -x + y_centre, c)

def rotate(pX,pY,angle):
    rad = math.radians(angle)
    pX -= screenCenterX
    pY -= screenCenterY
    xCos = pX*math.cos(rad)
    ySin = pY*math.sin(rad)
    xSin = pX*math.sin(rad)
    yCos = pY*math.cos(rad)
    newX = xCos - ySin + screenCenterX
    newY = xSin + yCos + screenCenterY
    return (int(newX), int(newY))

def drawClock():
    midPointCircleDraw(80, 40, 39, tft.color(232,232,232))
    n12x = 80
    n12y = 10
    for i in range(12):
        newX,newY = rotate( n12x, n12y, 30*i)
        midPointCircleDraw( newX, newY, 3, tft.color(232, 232, 64))
        
def drawSecond( sec ):
    deg = sec * 6
    n12x = 80
    n12y = 10
    newX,newY = rotate( n12x, n12y, deg)
    tft.line((screenCenterX, screenCenterY), (newX, newY), tft.color(232, 64, 232))

def drawMinute( minute ):
    deg = minute * 6
    n12x = 80
    n12y = 10
    newX,newY = rotate( n12x, n12y, deg)
    tft.line((screenCenterX, screenCenterY), (newX, newY), tft.color(232, 242, 232))
    
def drawHour( hour ):
    deg = hour * 30
    n12x = 80
    n12y = 12
    newX,newY = rotate( n12x, n12y, deg)
    tft.line((screenCenterX, screenCenterY), (newX, newY), tft.color(232, 242, 64))

def cbSecond(x):
    global second, minute, hour
    second += 1
    if (second == 60):
        second = 0
        minute += 1
        if (minute == 60):
            minute = 0
            hour += 1
            if (hour == 12):
                hour = 0
    tft.fill(tft.BLACK)
    drawClock()
    drawSecond(second)
    drawMinute(minute)
    drawHour(hour)
    tft.swap()
    
#################################################################
###### main program #############################################
#################################################################
secTmr = Timer(0)
secTmr.init( period=1000, mode=Timer.PERIODIC, callback=cbSecond)
while True:
    pass

Conclusion

From this article, it can be seen that the display relies on calculations for drawing circles and the positions of the hour pins, second hand, minute hand and hour hand as well as using the timer as an increment of seconds which correlates with the display of dependent minutes and hours. We hope you will be able to improve them for further use. Finally, have fun with programming.

Refrerence

  1. geeksforgeeks : mid-point circle drawing algorithm

(C) 2020-2022, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2022-01-13