PyQt로 구현한 아날로그 시계 1편

이번에 만들어본 예제는 아날로그 시계 (Analog Clock) 입니다.

 
먼저 완성된 모습을 살펴보겠습니다.

[아날로그 시계 화면]

 

 

Python, PyQt5를 이용해 구현되어 있습니다.

Pyinstaller 로 제작된 실행 파일(exe) : Analog Clock

개요


세계 시간을 모두 표시할 수 있는 아날로그 시계를 파이썬으로 만들어 보았습니다.

Network Time Protocol 을 이용, 온라인으로 타임서버에 접속해 시간을 읽어와 표시하는 프로그램 입니다.

[인터넷 연결시, On-line 표시]

타임서버에 접속 불가 시(오프라인)는 운영체제에서 시간을 읽어와 표시합니다.

[인터넷 연결불가시, Off-line 표시]

윈도우 시간이 맞지 않을때 time.windows.com 이라는 타임서버에 접속해 시간을 설정(동기화)하는 원리와 같습니다.

시침, 분침, 초침의 변화하는 각도와 원(시계)의 반지름 정보를 기반으로 삼각함수로 좌표를 찾고 선을 그려주는 방식으로 구현되어 있습니다.

UTC (협정 세계시)를 적용해 각 나라별 세계 시간이 모두 구현되어 있습니다.

아래는 주요 국가별 UTC 지도 입니다.
[협정 세계시 지도, 출처 : 위키백과]

주요 기능

1. NTP (Network Time Protocol) 를 이용한 온라인 시간 표시

2. 인터넷 비 연결시 Off-line 시간 표시 (시스템 시간 이용)

3. On-line or Off-line 자동 감지

4. UTC 기반 세계 시계 표시 (UTC-12~UTC+14)

5. 시계 주요 부분 개별 그리기 (위 동영상 참조)
 

소스 코드 분석

총 2개의 파일로 구성. (main.py , NTP.py)

NTP.py

먼저 타입서버에 접속해 시간정보를 읽어오는 부분입니다.

설명은 소스코드 뒤에서 이어 가도록 하고, 전체 코드를 살펴보겠습니다.
from datetime import datetime, timezone, timedelta
from socket import socket, AF_INET, SOCK_DGRAM
import struct

class NTP:

    time = datetime.now(timezone.utc)

    @classmethod
    def getNTPtime(cls, host ='time.google.com', utc_std=9):                
        # NTP query (48 bypte)
        query = '\x1b' + 47 * '\0'        
        # Unix standard time, 1970 00:00:00
        # NTP standart time, 1900 00:00:00
        # difference time 70 years (sec) 
        STD1970 = 2208988800        
        
        try:
            # crate socket(IPv4, UDP)
            sock = socket(AF_INET, SOCK_DGRAM)      
            # send NTP query
            sock.sendto(query.encode(), (host, 123))            
            # receive from NTP     
            sock.settimeout(1)
            recv, addr = sock.recvfrom(1024)
           
            if recv and len(recv)==48:
                # struct : python byte <-> C struct
                # !:Big endian, I:unsigned int(4byte) = 48byte
                # [10]: tuple[10] index, unpack returned a tuple type
                ts = struct.unpack('!12I', recv)[10]          
                ts -= STD1970            
            else:
                raise NameError('recv error')
            
        except Exception as e:            
            sock.close()
            utc = datetime.now(timezone.utc)   
            NTP.time = utc.astimezone(timezone(timedelta(hours=utc_std)))          
            #print(e)
            return False
        else:
            # from timestamp to datetime
            utc = datetime.fromtimestamp(ts, tz=timezone.utc)            
            # convert utc to local time
            #NTP.time = utc.astimezone()
            # convert utc to other utc time
            NTP.time = utc.astimezone(timezone(timedelta(hours=utc_std)))          
            
        return True

전체 코드는 NTP라는 하나의 클래스로 구성되어 있습니다.

time 이라는 파이썬 클래스 변수와 getNTPtime() 이라는 클래스 함수가 전부입니다.

time은 python의 datatime 클래스에서 인스턴스화 시킨 객체입니다.
time = datetime.now(timezone.utc)
datetime.now(timezone.utc) 함수는 현재 컴퓨터의 로컬 시간을 얻어오며, 전달된 timezone.utc는 리턴되는 datetime의 타입을 aware datetime 으로 생성되도록 합니다.

파이썬에서 날짜와 시간을 다루는 datetime 클래스는 naiveaware 두가지 타입이 존재하며, 차이는 naive는 단순 날짜,시간만 저장하는 반면 aware 타입은 UTC, daylight saving(일광시간절약, 흔히 썸머타임) 정보도 같이 저장합니다.

파이썬 공식 문서(3.7.4)에 아래와 같이 설명되어 있습니다.
There are two kinds of date and time objects: “naive” and “aware”.

An aware object has sufficient knowledge of applicable algorithmic and political time adjustments, such as time zone and daylight saving time information, to locate itself relative to other aware objects. An aware object is used to represent a specific moment in time that is not open to interpretation.

A naive object does not contain enough information to unambiguously locate itself relative to other date/time objects. Whether a naive object represents Coordinated Universal Time (UTC), local time, or time in some other timezone is purely up to the program, just like it is up to the program whether a particular number represents metres, miles, or mass. Naive objects are easy to understand and to work with, at the cost of ignoring some aspects of reality.
UTC를 구현하기 위해서는 당연히 timezone 개념이 포함된 aware datetime을 사용해야 합니다.

10번 라인의 getNTPtime() 는 클래스 메소드 이므로 아래와 같이 호출하는 사실을 참조바랍니다.

올바른 호출 예) NTP.getNTPtime()

잘못된 호출 예) 객체이름.getNTPtime()
@classmethod
    def getNTPtime(cls, host ='time.google.com', utc_std=9):  
이 함수의 목적은 타임서버(time.google.com)에 시간정보를 요구하는 요청(query)을 전송하고 시간정보를 수신한 후 위에서 설명한 time이라는 datetime class에 저장하는 용도입니다.

12번 라인에서 NTP에서 규정한 요청 쿼리(48 byte)를 생성합니다.
query = '\x1b' + 47 * '\0'
16번 라인은 타임서버에서 제공하는 시간기준(1900년 0시 0분 0초)Unix에서 시간기준(1970년 0시 0분 0초)이 서로 다르기 때문에 70년이라는 시간을 초단위로 바꾼 값입니다.

70년 = 2208988800 초 입니다.

(3600*24)*365*70 + (3600*24)*17(윤달) = 2,208,988,800 (second)

20 번 라인에서 소켓을 생성합니다. (AF_INET : IPv4, SOCK_DGRAM : 비연결지향형)
# crate socket(IPv4, UDP)
sock = socket(AF_INET, SOCK_DGRAM)      
# send NTP query
sock.sendto(query.encode(), (host, 123))            
# receive from NTP     
sock.settimeout(1)
recv, addr = sock.recvfrom(1024)
소켓을 생성하고, 데이터를 송수신하는 과정에서 발생되는 오류처리를 위해 try 구문으로 감싸서 처리합니다.

22번 라인은 위에서 생성된 query (str type)를 byte로 변환(encoding)하고, 해당 host(time.google.com)으로 123번 포트(NTP 기본)를 이용해 쿼리를 전송합니다.

비 연결지향형 소켓이므로, connect는 불필요합니다.

25번 라인에서 타입서버로부터 수신된 시간자료를 받아서 처리하는 코드입니다.
if recv and len(recv)==48:
    # struct : python byte <-> C struct
    # !:Big endian, I:unsigned int(4byte) = 48byte
    # [10]: tuple[10] index, unpack returned a tuple type
    ts = struct.unpack('!12I', recv)[10]          
    ts -= STD1970            
else:
    raise NameError('recv error')
ts = struct.unpack('!12I', recv)[10]  코드는 잘 살펴보아야 합니다.

struct는 C의 구조체와 Python 의 타입간 데이터 변환(Binary 수준)을 위해 사용되며, '!' 는 Byte ording의 Big Endian을 의미합니다.

struct의 반환값은 python tuple 입니다.

'12I'는 12개의 정수형(4byte)를 의미하므로 총 48 byte의 데이터를 수신해 튜플로 변환하고 그중 10번째 인덱스의 값을 ts(time stamp, int type)에 저장하라는 의미입니다.

이제 여기서 위에서 구한 70년을 빼서 현재 시간을 보정합니다.

36번 라인의 except 구문은 위 단계에서 NTP 서버와 통신에 문제가 발생한 경우 이 곳으로 들어오게 되며, 온라인이 아닌 컴퓨터 로컬 시간을 기준으로 정합니다.
except Exception as e:            
    sock.close()
    utc = datetime.now(timezone.utc)   
    NTP.time = utc.astimezone(timezone(timedelta(hours=utc_std)))          
    #print(e)
    return False
42번 라인은 NTP 에서 시간을 잘 읽어온 경우에 들어오며 (try에 문제가 없었다면), ts라는 timestamp(int type) 를 datetime 클래스로 변환해 시간 설정을 완료합니다.
else:
    # from timestamp to datetime
    utc = datetime.fromtimestamp(ts, tz=timezone.utc)            
    # convert utc to local time
    #NTP.time = utc.astimezone()
    # convert utc to other utc time
    NTP.time = utc.astimezone(timezone(timedelta(hours=utc_std)))
48번 라인은 NTP에서 읽어온 시간정보가 UTC 0 즉 영국의 그리니치 천문대 기준 시간이므로 이를 함수의 전달인자로 들어오는 UTC 정보를 이용해 timezone을 설정합니다.

다 적고 보니 꽤나 복잡합니다.

시간을 얻어 오는 과정을 정리해보면 다음과 같습니다.

1. 네트워크 통신을 위한 소켓 생성

2. NTP 서버로 시간정보 요청 쿼리(48byte) 전송

3. NTP 서버로부터 시간정보(48 byte) 수신

4. Unix Time(1970년 기준), NTP Time(1900 기준) 시간 보정

5. 타임서버로부터 시간 얻기 완료

6. 타임서버 연결불가시, 로컬시간 적용

여기까지가 NTP.py의 설명이며, 이 클래스는 다른 코드에서 시간을 얻기위해 아래와 같이 사용될 수 있습니다.
======================================
import NTP
NTP.getNTPtime(utc=9) #UTC+9는 우리나라 시간입니다.
======================================

만약 인터넷 연결이 정상이라면 타임서버에서 시간을 얻고 True 를 리턴,
아니면 로컬 컴퓨터 시간을 얻고 False 리턴.

이후, NTP.time.hour, NTP.time.year 등을 이용해 시간, 날짜 등 정보를 사용가능합니다.

실제 시계의 외형을 구성하는 코드인 main.py는 2탄에서 설명하도록 하겠습니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기