[TH] Draw an analog clock using MicroPython.

บทความก่อนหน้านี้ได้กล่าวถึงการปรับปรุงปรับปรุงความเร็วในการแสดงผลด้วยการใช้เทคนิคดับเบิลบัฟเฟอร์ (double buffer) จึงนำมาประยุกต์ใช้สำหรับการแสดงผลเป็นนาฬิกาแบบแอนาล็อกดังภาพที่ 1 ซึ่งวิธีการวาดนั้นใช้การคำนวณตรีโกณมิติเพื่อหาค่าพิกัด (x,y) ของปลายเข็มวินาที นาที และชั่วโมง โดยการทำงานของแต่ละวินาทีจะใช้ตัวตั้งเวลาหรือไทเมอร์ (Timer) เพื่อให้การทำงานนั้นใกล้เคียงกับเวลาจริงมากกว่าการวนรอบหรือการหน่วงเวลา

ภาพที่ 1 ตัวอย่างผลลัพธ์ของการวาดนาฬิกาแบบแอนาล็อก

อุปกรณ์

อุปกรณ์การทดลองประกอบด้วย

  1. บอร์ด esp32
  2. จอแสดงผล TFT 0.96″

วาดนาฬิกาแอนาล็อก

จอแสดงผล TFT 0.96″ ใช้ไดรเวอร์ ST7735 เป็นตัวขับการทำงาน และในตัวอย่างนี้ใช้บอร์ดที่เป็น REDTAB ที่มีความละเอียด 80×160 จุด ทำให้ต้องสั่งหมุนจอ 90 องศาด้วยการสั่ง rotation(1)

วาดเรือนนาฬิกา

ตัวเรือนนาฬิกาแบ่งการวาดเป็น 2 ส่วน คือ ส่วนของตัวกรอบ และส่วนของเครื่องหมายจุดที่เป็นตัวแทนของตัวเลข 1 ถึง 12 ซึ่งเป็นตัวเลขบนหน้าปัดนาฬิกา

วาดกรอบ

ด้วยไลบรารีที่ใช้งานไม่มีคำสั่งวาดวงกลม จึงได้ใช้ขั้นตอนวิธีการวาดวงกลมแบบจุดกลาง (midpoint circle draw) จากเว็บไซต์ geeksforgeeks ได้เขียนไว้ดังนี้

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 = "")

จากโค้ดข้างต้นปรับแก้ใหม่ได้ดังนี้

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)

เมื่อปรับการแสดงผลด้วย pint เป็นคำสั่ง setPixel() และสั่งวาด midPointCircleDraw(80, 40, 39, tft.color(232,232,232)) ได้ผลลัพธ์ดังภาพที่ 2

วาดหมุด

การวาดหมุดชั่วโมงใช้การวาดวงกลมที่ตำแหน่ง 12 นาฬิกาเป็นวงกลมตำแหน่งเริ่มต้นดังภาพที่ 3 ซึ่งเขียนดังนี้

n12x = 80
n12y = 10
midPointCircleDraw(n12x, n12y, 3, tft.color(232, 232, 64))
ภาพที่ 3 การวาดหมุดจรง 12 นาฬิกา

ส่วนตำแหน่งอื่น ๆ ใช้หลักการหมุนวงกลมไปตามองศาของการวางหมุดชั่วโมง นั่นหมายความว่า วงกลมมีองศาเป็น 360 องศา ต้องวาดหมุนจำนวน 12 จุด ดังนั้นแต่ละหมุดจะต้องหมุนองศาเป็นดังนี้

  • องศา = 360/12
    = 30

นั่นหมายความว่าจะต้องหมุน 30 องศา แล้ววาด 1 หมุด ทำทั้งหมด 12 ครั้ง

การตำนวณค่า (x,y) ใหม่สามารถใช้สมการคำนวณดังที่เคยกล่าวถึงในเรื่องของการหมุนสี่เหลี่ยมดังนี้

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))

เมื่อสั่งวาดด้วยคำสั่งต่อไปนี้จะได้ผลลัพธ์ดังภาพที่ 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))
ภาพที่ 4 หมุดทั้ง 12 ตำแหน่งของชั่วโมง

วาดเข็มวินาที

การวาดเข็มวินาทีใช้หลักการเดียวกับการคำนวณตำแหน่งของหมุดของตัวเลขชั่วโมง แต่ด้วย 1 ชั่วโมงมี 60 วินาที หมายความว่าจะต้องมีตำแหน่งของวินาที 60 ตำแหน่งใน 360 องศา ดังนั้น แต่ละตำแหน่งห่างกันตามสมการต่อไปนี้

  • องศา = 360 / 60
    = 6

ดังนั้น เมื่อรวมการวาดเส้นตรงจากตำแหน่งกึ่งกลางของจอ และปลายเข็มอยู่ที่วินาทีที่ sec สามารถเขียนโค้ดได้ดังนี้

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))

เมื่อเขียนโค้ดให้แสดงเข็มวินาทีดังต่อไปนี้จะได้ตัวอย่างผลลัพธ์ดังภาพที่ 5

for i in range(60):
    tft.fill(tft.BLACK)
    drawClock()
    drawSecond(i)
    tft.swap()
ภาพที่ 5 ตัวอย่างผลลัพธ์ของการวาดเข็มวินาที

วาดเข็มนาที

เข็มนาทีใช้การคำนวณเหมือนวินาที แต่ขยับทุกครั้งที่เข็มวินาทีวนครบ 1 รอบ ดังนั้น การวาดเข็มนาทีเขียนได้ดังนี้

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))

การกำหนดความสัมพันธ์ระหว่างค่าวินาทีกับนาทีเขียนดังนี้

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

วาดเข็มชั่วโมง

เข็มชั่วโมงคำนวณตำแหน่งเหมือนการวาดหมุด และมีความสัพธ์กับค่านาที คือ เมื่อนาทีครบ 1 รอบจะเพิ่มค่าชั่วโมงขึ้น 1 และถ้าค่าชั่วโมงมากกว่า12 ให้กลับไป 1 และตัวอย่างผลลัพธ์เป็นดังภาพที่ 1

ตัวอย่างโปรแกรม

ตัวอย่างโปรแกรมทำการแสดงนาฬิกาโดยเริ่มตั้งเวลาไว้ที่ 1:30.00 และใช้การเพิ่มค่าวินาทีจากการใช้การตั้งเวลา 1 วินาที (อ่านบทความ) จึงได้โค้ดผลลัพธ์เป็นดังนี้ และตัวอย่างของการแสดงผลเป็นดังภาพที่ 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

สรุป

จากบทความนี้ จะพบว่า การแสดงผลอาศัยการคำนวณเพื่อใช้ในการวาดวงกลม และตำแหน่งต่าง ๆ ของหมุดชั่วโมง เข็มวินาที เข็มนาที และเข็มชั่วโมง พร้อมทั้งได้ใช้งานตัวตั้งเวลาเป็นตัวเพิ่มค่าววินาที ซึ่งมีความสัมพันธ์กับการแสดงค่านาทีและชั่วโมงที่ขึ้นต่อกัน โดยทีมพวกเราหวังว่า ผู้อ่านจะสามารถนำไปปรับปรุงเพื่อใช้งานต่อไป สุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

แหล่งอ้างอิง

  1. geeksforgeeks : mid-point circle drawing algorithm

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-03, 2022-01-03