在前一篇測試中已經完成 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 :