2024年11月4日 星期一

MicroPython 學習筆記 : SSD1306 指針時鐘 (二)

在前一篇測試中已經完成 SSD1306 指針時鐘的改寫, 後續我想在此基礎上添加顯示秒針與日期資訊, 由於篇幅太長, 所以紀錄在此篇. 

本系列之前文章參考 :


1. 加上秒針 : 

如果要加上秒針, 則原本的分針長度拿來給秒針用, 所以時針與分針的長度要相對縮短一些才行, 修改後的程式碼如下 :

# ssd1306_clock_3.py 
import config   
import xtools
from gfx import GFX   
from machine import I2C, Pin, RTC  
import ssd1306
import time
import math

ip=xtools.connect_wifi(led=5) 
if ip:
    # 建立 SSD1306_I2C 與 GFX 物件
    i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C   
    oled=ssd1306.SSD1306_I2C(128, 64, i2c) # 0.96 吋解析度 128*64
    gfx=GFX(oled.width, oled.height, oled.pixel)
    oled.fill(0)  # 清除螢幕
    oled.show()
    # 繪製圓心與圓形外框
    gfx.circle(31, 31, 1, 1)
    oled.show()
    gfx.circle(31, 31, 29, 1)
    oled.show()
    # 繪製小時刻度
    for i in range(1, 13):
        angle=i * 30
        x=31 + math.trunc(27 * math.sin(math.radians(angle)))
        y=31 - math.trunc(27 * math.cos(math.radians(angle)))
        # 判斷是否為 3, 6, 9 或 12 點的刻度
        if i in [3, 6, 9, 12]:
            gfx.fill_rect(x-1, y-1, 2, 2, 1)  # 繪製 2x2 像素的粗圓點
        else:
            gfx.line(x, y,
                31 + math.trunc(29 * math.sin(math.radians(angle))),
                31 - math.trunc(29 * math.cos(math.radians(angle))), 1)
        oled.show()
    # 繪製時針, 分針與秒針    
    prev_s_angle=None   # 紀錄前一秒針的角度
    while True:
        dt=RTC().datetime() 
        time.sleep_ms(950)  # 延遲 0.95 秒
        gfx.circle(31, 31, 29, 1)  # 繪製時鐘外框
        # 計算並繪製時針
        h_angle=(dt[4] % 12 + dt[5] / 60) * 30  # 計算時針角度
        gfx.line(31, 31,
                 31 + math.trunc(15 * math.sin(math.radians(h_angle))),
                 31 - math.trunc(15 * math.cos(math.radians(h_angle))), 1)
        oled.show()  # 繪製時針

        # 計算並繪製分針 (縮短至 20 像素) 
        m_angle=dt[5] * 360 / 60
        gfx.line(31, 31,
                 31 + math.trunc(20 * math.sin(math.radians(m_angle))),  # 修改分針長度為 20
                 31 - math.trunc(20 * math.cos(math.radians(m_angle))), 1)
        oled.show()  # 繪製分針
        # 計算並繪製秒針 (使用原來的分針長度 25)
        s_angle=dt[6] * 360 / 60            
        if prev_s_angle is not None:  # 清除舊的秒針線條
            gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(prev_s_angle))),
                 31 - math.trunc(25 * math.cos(math.radians(prev_s_angle))), 0) 
        gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(s_angle))),  # 秒針長度設為 25
                 31 - math.trunc(25 * math.cos(math.radians(s_angle))), 1)   
        oled.show()  # 繪製新的秒針
        prev_s_angle=s_angle  # 更新前一秒針的角度
        gfx.fill_rect(53, 56, 17, 10, 0)  # 繪製填滿的長方形框
        oled.text(str(dt[6]), 53, 56, 1)  # 繪製秒數 (文字)
        oled.show()      
        if dt[6] == 59: # 59 秒時清空錶面重繪時鐘
            gfx.fill_circle(31, 31, 25, 0) # 填滿 0 清空錶面
            gfx.circle(31, 31, 1, 1)  # 重繪時鐘圓心
            gfx.circle(31, 31, 29, 1) # 重繪時鐘外框
            xtools.tw_now()  # RTC 時鐘同步 NTP 
else:
    print('無法連線 WiFi 網路!')

黃底色部分即添加的顯示秒針功能, 由於 while 迴圈每秒跑一圈, 秒針每秒要重繪, 在畫新的秒針前必須先將舊的秒針線條清除, 否則秒針線條會一直不斷地在錶面累積, 這裡用 prev_s_angle 來記錄前一個秒針的角度 (分針與時針變動較緩慢不須每秒重繪, 迴圈末尾在 59 秒要跳下一分鐘時會重繪錶面, 時針與分針是在跳分時重繪的).

結果如下 : 




可見秒針每秒都會隨秒數跳動而旋轉. 


2. 在右半邊上半部顯示日期時間文字 : 

接下來我想在螢幕右半邊的上半部以文字顯示日期, 星期, 與時間等四列資訊. 右半部寬度 64px. 每個字元 8px, 因此每列只能顯示 8 個字元, 規劃如下 :

2024
Nov.4
Monday
10:16:38

以下是從 RTC 時鐘取得的日期時間 tuple : 

>>> from machine import RTC      
>>> rtc=RTC()    
>>> dt=rtc.datetime()    
>>> dt    
(2024, 11, 4, 0, 11, 33, 24, 909053)     # (年, 月, 日, 星期, 時, 分, 秒, 微秒)
   
前三個元素是日期, 第四個是星期 (0=Monday), 第 5~7 個元素是時間, 我們可以從 datetime() 傳回的元組來製作要顯示的日期時間資訊 . 

首先來處理月份, 先定義一個 months 串列 : 

>>> months=['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.']    
製作月份日期字串 : 

>>> md=f'{months[dt[1]-1]}{dt[2]}'     
>>> md   
'Nov.4'   

製作年份字串 : 

>>> year=str(dt[0])     # 轉成字串
>>> year   
'2024'

定義星期串列來製作星期字串 :

>>> days=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']     
>>> day=days[dt[3]]     
>>> day       
'Monday'

利用格式化字串製作時分秒字串 :  

>>> hms='%02d:%02d:%02d' % (dt[4], dt[5], dt[6])    
>>> hms     
'11:39:27'

依據上面測試修改程式碼如下 :

# ssd1306_clock_4.py
import config   
import xtools
from gfx import GFX   
from machine import I2C, Pin, RTC  
import ssd1306
import time
import math

ip=xtools.connect_wifi(led=5) 
if ip:
    # 建立 SSD1306_I2C 與 GFX 物件
    i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C   
    oled=ssd1306.SSD1306_I2C(128, 64, i2c) # 0.96 吋解析度 128*64
    gfx=GFX(oled.width, oled.height, oled.pixel)
    oled.fill(0)  # 清除螢幕
    oled.show()
    # 繪製圓心與圓形外框
    gfx.circle(31, 31, 1, 1)
    oled.show()
    gfx.circle(31, 31, 29, 1)
    oled.show()
    # 繪製小時刻度
    for i in range(1, 13):
        angle=i * 30
        x=31 + math.trunc(27 * math.sin(math.radians(angle)))
        y=31 - math.trunc(27 * math.cos(math.radians(angle)))
        # 判斷是否為 3, 6, 9 或 12 點的刻度
        if i in [3, 6, 9, 12]:
            gfx.fill_rect(x-1, y-1, 2, 2, 1)  # 繪製 2x2 像素的粗圓點
        else:
            gfx.line(x, y,
                31 + math.trunc(29 * math.sin(math.radians(angle))),
                31 - math.trunc(29 * math.cos(math.radians(angle))), 1)
        oled.show()
    # 繪製時針, 分針與秒針    
    prev_s_angle=None   # 紀錄前一秒針的角度
    while True:
        dt=RTC().datetime() 
        time.sleep_ms(950)  # 延遲 0.95 秒
        gfx.circle(31, 31, 29, 1)  # 繪製時鐘外框
        # 計算並繪製時針
        h_angle=(dt[4] % 12 + dt[5] / 60) * 30  # 計算時針角度
        gfx.line(31, 31,
                 31 + math.trunc(15 * math.sin(math.radians(h_angle))),
                 31 - math.trunc(15 * math.cos(math.radians(h_angle))), 1)
        oled.show()  # 繪製時針

        # 計算並繪製分針 (縮短至 20 像素) 
        m_angle=dt[5] * 360 / 60
        gfx.line(31, 31,
                 31 + math.trunc(20 * math.sin(math.radians(m_angle))),  # 修改分針長度為 20
                 31 - math.trunc(20 * math.cos(math.radians(m_angle))), 1)
        oled.show()  # 繪製分針
        # 計算並繪製秒針 (使用原來的分針長度 25)
        s_angle=dt[6] * 360 / 60            
        if prev_s_angle is not None:  # 清除舊的秒針線條
            gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(prev_s_angle))),
                 31 - math.trunc(25 * math.cos(math.radians(prev_s_angle))), 0)
        gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(s_angle))),  # 秒針長度設為 25
                 31 - math.trunc(25 * math.cos(math.radians(s_angle))), 1)
        oled.show()  # 繪製新的秒針
        prev_s_angle=s_angle  # 更新前一秒針的角度
        if dt[6] == 59: # 59 秒時清空錶面重繪時鐘
            gfx.fill_circle(31, 31, 25, 0) # 填滿 0 清空錶面
            gfx.circle(31, 31, 1, 1)  # 重繪時鐘圓心
            gfx.circle(31, 31, 29, 1) # 重繪時鐘外框
            xtools.tw_now()  # RTC 時鐘同步 NTP
        # 在右半部顯示日期時間資訊
        months=['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.',
                'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.']
        days=['Monday', 'Tuesday', 'Wednesday', 'Thursday',
              'Friday', 'Saturday', 'Sunday']
        # 在螢幕右半邊上半部四列顯示日期時間資訊
        gfx.fill_rect(64, 0, 64, 64, 0)  # 清除右半邊的顯示區域
        # 更新日期與時間顯示
        oled.text(str(dt[0]), 64, 0, 1)  # 年
        oled.text(f'{months[dt[1] - 1]}{dt[2]}', 64, 8, 1)  # 月.日
        oled.text(days[dt[3]], 64, 16, 1)  # 星期
        oled.text('%02d:%02d:%02d' % (dt[4], dt[5], dt[6]), 64, 24, 1)  # 時分秒
        oled.show()
else:
    print('無法連線 WiFi 網路!')

黃底色部分為此次添加的程式碼. 注意, 由時日期時間資訊中已包含跳動的秒數, 所以原本在時鐘右下角的秒數顯示已被去除. 結果如下 :




3. 在螢幕右下半部顯示氣象資訊 : 

上面的範例程式只使用了螢幕的右上半部, 我們可以在右下半部顯示從 OpenWeatherMap 取得的氣象資料, 關於 OpenWeatherMap 的 API 用法參考 :


在之前的測試中我們已經將查詢 OpenWeatherMap API 的方法寫成如下的爬蟲函式 :

def get_weather(country, city, api_key):
    url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'
    try:
        res=urequests.get(url)
        data=ujson.loads(res.text)
        if data['cod']==200:    # 注意是數值
            ret={'geocode': data['id'],
                 'icon': data['weather'][0]['icon'],
                 'temperature': data['main']['temp'],
                 'pressure': data['main']['pressure'],
                 'humidity': data['main']['humidity']}
            return ret
        else:
            print(f'code:{data['cod']}')
            return None
    except Exception as e:
        print(e)
        return None 

使用 OpenWeatherMap API 前須先申請 API Key, 然後把它寫入 config.py 裡的一個變數中 (例如 WEATHER_API_KEY) 以備程式取用. 

呼叫 get_weather() 前需要先匯入 ujson 與 urequests 模組 :

>>> import ujson   
>>> import urequests   

傳入 API key, 城市, 與國家代號會傳回一個字典, 我們要將其中的 temperature, humidity, 與 temperature 三個鍵的值顯示在螢幕的右下半部. 

>>> weather_api_key=config.WEATHER_API_KEY      
>>> city='Kaohsiung'       
>>> country='TW'   
>>> data=get_weather(country, city, weather_api_key)      
>>> data   
{'icon': '03d', 'temperature': 28.94, 'geocode': 1673820, 'pressure': 1013, 'humidity': 71}

修改上面的程式碼, 在秒數為 59 秒時呼叫 get_weather() 取得氣象資料:

        if dt[6] == 59: # 59 秒時清空錶面重繪時鐘
            gfx.fill_circle(31, 31, 25, 0) # 填滿 0 清空錶面
            gfx.circle(31, 31, 1, 1)  # 重繪時鐘圓心
            gfx.circle(31, 31, 29, 1) # 重繪時鐘外框
            xtools.tw_now()  # RTC 時鐘同步 NTP
            data=get_weather('TW', 'Kaohsiung', config.WEATHER_API_KEY)  # 查詢氣象資料

 然後顯示在 (64, 32) 開始的右下邊螢幕 :

        if data:
            degree=[0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00]  # 溫度符號點陣
            buf=bytearray(degree)
            fb=framebuf.FrameBuffer(buf, 8, 8, framebuf.MONO_VLSB)
            oled.text(str(data['temperature']), 64, 32, 1)  # 溫度
            oled.framebuf.blit(fb, 104, 32) # degree symbol
            oled.text(str(data['humidity']) + '%', 64, 40, 1)  # 濕度
            oled.text(str(data['pressure']) + 'mPh', 64, 48, 1)  # 氣壓
            oled.text('Kaohsiung', 64, 56, 1)  # 濕度

注意, 此處使用 framebuf 來顯示右上角小圈圈的溫度度數符號, 做法參考 : 


完整程式碼如下 :

# ssd1306_clock_5.py
import config   
import xtools
from gfx import GFX   
from machine import I2C, Pin, RTC  
import ssd1306
import time
import math
import urequests  
import ujson  
import framebuf   

def get_weather(country, city, api_key):
    url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'
    try:
        res=urequests.get(url)
        data=ujson.loads(res.text)
        if data['cod']==200:    # 注意是數值
            ret={'geocode': data['id'],
                 'icon': data['weather'][0]['icon'],
                 'temperature': data['main']['temp'],
                 'pressure': data['main']['pressure'],
                 'humidity': data['main']['humidity']}
            return ret
        else:
            print(f'code:{data['cod']}')
            return None
    except Exception as e:
        print(e)
        return None 

ip=xtools.connect_wifi(led=5) 
if ip:
    # 建立 SSD1306_I2C 與 GFX 物件
    i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C   
    oled=ssd1306.SSD1306_I2C(128, 64, i2c) # 0.96 吋解析度 128*64
    gfx=GFX(oled.width, oled.height, oled.pixel)
    oled.fill(0)  # 清除螢幕
    oled.show()
    # 繪製圓心與圓形外框
    gfx.circle(31, 31, 1, 1)
    oled.show()
    gfx.circle(31, 31, 29, 1)
    oled.show()
    # 繪製小時刻度
    for i in range(1, 13):
        angle=i * 30
        x=31 + math.trunc(27 * math.sin(math.radians(angle)))
        y=31 - math.trunc(27 * math.cos(math.radians(angle)))
        # 判斷是否為 3, 6, 9 或 12 點的刻度
        if i in [3, 6, 9, 12]:
            gfx.fill_rect(x-1, y-1, 2, 2, 1)  # 繪製 2x2 像素的粗圓點
        else:
            gfx.line(x, y,
                31 + math.trunc(29 * math.sin(math.radians(angle))),
                31 - math.trunc(29 * math.cos(math.radians(angle))), 1)
        oled.show()
    # 繪製時針, 分針與秒針    
    prev_s_angle=None   # 紀錄前一秒針的角度
    data=None  # 氣象資料
    while True:
        dt=RTC().datetime() 
        time.sleep_ms(950)  # 延遲 0.95 秒
        gfx.circle(31, 31, 29, 1)  # 繪製時鐘外框
        # 計算並繪製時針
        h_angle=(dt[4] % 12 + dt[5] / 60) * 30  # 計算時針角度
        gfx.line(31, 31,
                 31 + math.trunc(15 * math.sin(math.radians(h_angle))),
                 31 - math.trunc(15 * math.cos(math.radians(h_angle))), 1)
        oled.show()  # 繪製時針

        # 計算並繪製分針 (縮短至 20 像素) 
        m_angle=dt[5] * 360 / 60
        gfx.line(31, 31,
                 31 + math.trunc(20 * math.sin(math.radians(m_angle))),  # 修改分針長度為 20
                 31 - math.trunc(20 * math.cos(math.radians(m_angle))), 1)
        oled.show()  # 繪製分針
        # 計算並繪製秒針 (使用原來的分針長度 25)
        s_angle=dt[6] * 360 / 60            
        if prev_s_angle is not None:  # 清除舊的秒針線條
            gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(prev_s_angle))),
                 31 - math.trunc(25 * math.cos(math.radians(prev_s_angle))), 0)
        gfx.line(31, 31,
                 31 + math.trunc(25 * math.sin(math.radians(s_angle))),  # 秒針長度設為 25
                 31 - math.trunc(25 * math.cos(math.radians(s_angle))), 1)
        oled.show()  # 繪製新的秒針
        prev_s_angle=s_angle  # 更新前一秒針的角度
        if dt[6] == 59: # 59 秒時清空錶面重繪時鐘
            gfx.fill_circle(31, 31, 25, 0) # 填滿 0 清空錶面
            gfx.circle(31, 31, 1, 1)  # 重繪時鐘圓心
            gfx.circle(31, 31, 29, 1) # 重繪時鐘外框
            xtools.tw_now()  # RTC 時鐘同步 NTP
            data=get_weather('TW', 'Kaohsiung', config.WEATHER_API_KEY)  # 查詢氣象資料
        # 在右半部顯示日期時間資訊
        months=['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.',
                'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.']
        days=['Monday', 'Tuesday', 'Wednesday', 'Thursday',
              'Friday', 'Saturday', 'Sunday']
        # 在螢幕右半邊上半部四列顯示日期時間資訊
        gfx.fill_rect(64, 0, 64, 64, 0)  # 清除右半邊的顯示區域
        # 更新日期與時間顯示
        oled.text(str(dt[0]), 64, 0, 1)  # 年
        oled.text(f'{months[dt[1] - 1]}{dt[2]}', 64, 8, 1)  # 月.日
        oled.text(days[dt[3]], 64, 16, 1)  # 星期
        oled.text('%02d:%02d:%02d' % (dt[4], dt[5], dt[6]), 64, 24, 1)  # 時分秒
        if data:  
            degree=[0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00] # 溫度符號點陣
            buf=bytearray(degree)   
            fb=framebuf.FrameBuffer(buf, 8, 8, framebuf.MONO_VLSB)    
            oled.text(str(data['temperature']), 64, 32, 1)  # 溫度
            oled.framebuf.blit(fb, 104, 32) # degree symbol
            oled.text(str(data['humidity']) + '%', 64, 40, 1)  # 濕度
            oled.text(str(data['pressure']) + 'mPh', 64, 48, 1)  # 氣壓
            oled.text('Kaohsiung', 64, 56, 1)  # 濕度
        oled.show()
else:
    print('無法連線 WiFi 網路!')

黃底色部分為新增的部分, 結果如下 : 




大功告成! 

最終範例與所需模組已打包上傳 GitHub :


沒有留言 :