2024年10月22日 星期二

MicroPython 學習筆記 : 用 SSD1306 顯示氣象資訊 (四)

在前一篇測試中我們已經將從 OpenWeatherMap 取得的氣象資料用自訂像素圖顯示在 SSD1306 顯示器上, 本篇則是要來複製趙英傑老師 "超圖解 Python 物聯網實作入門" 這本書第 18-2 節的做法, 利用 OpenWeatherMap 傳回值中的 icon 編號來顯示天氣概況圖, 溫濕度, 與氣壓等氣象資訊 (氣壓是我添加的, 書上範例僅顯示溫溼度). 

氣象爬蟲做法參考 :

 
本實驗所需的 icon 圖檔可從該書範例壓縮檔中取得, 網址如下 : 


本篇測試所需的向量圖示 .bin 檔放在壓縮檔解開後的 ch18/icons 資料夾內 (共 17 個) :




其中最後一個 na.bin 不是 OpenWeatherMap 定義的圖示名稱, 而是此書作者特製, 用在萬一找不到天氣的圖示時顯示用的. 這些 .bin 檔都是用 wb 模式將圖示的 bytes 類型資料寫入二進位檔而成. 可用 Thonny 將整個 icons 資料夾上傳到開發板 (全部約 4.78KB) :






注意, 這些點陣像素圖檔轉成 byte 陣列時都是用 MVLSB 方式排列的, 用 framebuf 讀取時須指定為此格式. 

首先匯入要用的模組與類別 : 

MPY: soft reboot
MicroPython v1.23.0 on 2024-06-02; Generic ESP32 module with ESP32
Type "help()" for more information.

>>> import framebuf  
>>> from machine import I2C, Pin   
>>> import ssd1306      

其中 framebuf 用來操控顯示緩衝區以便顯示點陣像素圖檔, 這在上一篇測試中已熟悉其用法. 接著是建立 SSD1306_I2C 物件, 同樣是使用 LOLIN D32 的 ESP32 開發板 :

>>> i2c=I2C(0, scl=Pin(22), sda=Pin(21))    
>>> oled=ssd1306.SSD1306_I2C(128, 64, i2c)      

先來顯示 02d (晴有雲) 這張圖檔, 以 rb 模式讀取後傳給 bytearray() 轉成 bytearray 類型, 然後以MVLSB 格式寫入緩衝顯示區, 然後呼叫 FrameBuffer 類別的靜態方法 blit() 顯示於螢幕 : 

>>> icon_file='/icons/02d.bin'   
>>> with open(icon_file, 'rb') as f:    
    icon=f.read()
    oled.fill(0)
    fb=framebuf.FrameBuffer(bytearray(icon), 48, 48, framebuf.MVLSB)
    oled.framebuf.blit(fb, 0, 0)
    oled.show()

結果如下 : 




接下來要用之前寫好的 OpenWeatherMap 氣象爬蟲來取得高雄的即時天氣資料 : 

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:
            return None
    except Exception as e:
        return None

先匯入 xtools 函式庫, 氣象爬蟲需要的 urequests 與 ujson 模組, 以及前一篇測試中用來顯示 16*16 大字型數字圖檔的自訂 big_simbol 模組 :

>>> import config   
>>> import xtools    
>>> import urequests 
>>> import ujson     
>>> import big_symbol    

連上網路 :

>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)    
network config: ('192.168.50.164', '255.255.255.0', '192.168.50.1', '192.168.50.1')

定義呼叫爬蟲函式須要的變數 :

>>> weather_api_key=config.WEATHER_API_KEY   
>>> city='Kaohsiung'     
>>> country='TW'   

呼叫 get_weather() : 

>>> data=get_weather(country, city, weather_api_key)   
>>> data    
{'icon': '04d', 'temperature': 28.32, 'geocode': 1673820, 'pressure': 1007, 'humidity': 79}

其中 icon 為 04d (多雲), 我們要利用此屬性來載入 /icons 目錄下的 04d.bin 圖檔於顯示緩衝區後顯示於螢幕上. 先清除螢幕 :

>>> oled.fill(0)  # (填滿 0 熄滅畫素)   
>>> oled.show()    

用 f 字串製作圖檔路徑 : 

>>> icon_file=f'/icons/{data["icon"]}.bin'   
>>> icon_file     
'/icons/04d.bin'   

在座標 (0, 15) 處開始顯示此天氣概況圖示 (保留兩列, 第一列用來顯示城市名稱) : 

>>> with open(icon_file, 'rb') as f:    
    icon=f.read()
    oled.fill(0)
    fb=framebuf.FrameBuffer(bytearray(icon), 48, 48, framebuf.MVLSB)
    oled.framebuf.blit(fb, 0, 15)
    
注意此處要指定 icon 點陣圖的排列方式為 MVLSB. 

接下來要用大字型模組 big_symbol 以圖檔方式來顯示氣象資料, 由於天氣概況圖示寬度為 48px, 因此氣象資料會從 X 座標 50 開始顯示, 溫度 Y 座標從 10 開始, 因每個大字型是 16*16 解析度, 因此濕度從 Y=28 開始顯示 (10+16+2, 留 2px 間隔), 氣壓從 Y=46 開始顯示 (28+16+2, 留 2px 間隔). 

先建立一個 Symbol 物件 : 

>>> sb=big_symbol.Symbol(oled)     # 建立 Symbol 物件   

因傳回之溫度是小數點後兩位之浮點數, 占用寬度太寬, 所以用 round() 取整數 : 

>>> temperature=round(data["temperature"])     
>>> temperature   
28   
>>> sb.text(f'{temperature}c', 50, 10)   
>>> sb.text(f'{data["humidity"]}%', 50, 28)      
>>> sb.text(f'{data["pressure"]}hPa', 50, 46)    

呼叫 show() 將顯示緩衝區資料輸出至螢幕 : 

>>> oled.show()    

最後在預留的前兩列顯示區的第一列開頭之 (0, 0) 座標顯示城市 'Kaohsiung' : 

>>> oled.text(city, 0, 0, 1)    # 在第一列顯示城市名稱
>>> oled.show()   

注意, 此處文字要放在最後才輸出, 如果放在前面不會隨圖檔輸出一起顯示. 結果如下 : 




可見氣壓因字串太長, 後面的單位 hPa 僅顯示 h 而已, 若值為 3 位數就會顯示 hP. 

完整程式碼如下 : 

# weather_ssd1306_4.py 
import config  
import xtools   
import urequests  
import ujson
from machine import I2C, Pin
import ssd1306
import framebuf
import big_symbol
import time

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:
            return None
    except Exception as e:
        return None

ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
if ip:
    # 設定氣象爬蟲參數
    weather_api_key=config.WEATHER_API_KEY
    city='Kaohsiung'   
    country='TW'
    # 建立 I2C 與 SSD1306_I2C 物件
    i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C
    oled=ssd1306.SSD1306_I2C(128, 64, i2c)
    sb=big_symbol.Symbol(oled)
    while True:  # 每分鐘抓一次更新 OLED 顯示器
        data=get_weather(country, city, weather_api_key)
        if data:
            sb.clear()
            icon_file=f'/icons/{data["icon"]}.bin'
            with open(icon_file, 'rb') as f:    
                icon=f.read()
                oled.fill(0)
                fb=framebuf.FrameBuffer(bytearray(icon), 48, 48, framebuf.MVLSB)
                oled.framebuf.blit(fb, 0, 15)
            temperature=round(data["temperature"])
            sb.text(f'{temperature}c', 50, 10)
            sb.text(f'{data["humidity"]}%', 50, 28)
            sb.text(f'{data["pressure"]}hPa', 50, 46)                
            oled.show()
            oled.text(city, 0, 0, 1)
            oled.show()
        time.sleep(60)
else:
    print('無法連線 WiFi')

以上實驗所有檔案 zip 壓縮檔可在 GitHub 下載 :


注意, 上傳開發板之前須先編輯 config.py 設定 SSID, PASSWORD, 以及 OpenWeatherMap 的 API Key 等資料. 

2024-10-23 補充 :

上面的程式碼在 OpenWeatherMap 傳回的 icon 萬一不存在時會出現檔案讀取錯誤, 原作者為了避免此問題發生製作了一個 na.bin (找不到圖檔之圖示), 可以用 os.stat() 先檢查檔案路徑存不存在, 存在就傳回該天氣概況之圖檔路徑, 否則傳回 na.bin 的路徑, 程式碼修改如下 :

import config  
import xtools   
import urequests  
import ujson
from machine import I2C, Pin
import ssd1306
import framebuf
import big_symbol
import time
import os  

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:
            return None
    except Exception as e:
        return None

def check_icon(icon):  
    try:
        os.stat(icon)   # 檢查圖檔是否存在
        return icon
    except:
        return '/icons/na.bin'   # 圖

ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
if ip:
    # 設定氣象爬蟲參數
    weather_api_key=config.WEATHER_API_KEY
    city='Kaohsiung'   
    country='TW'
    # 建立 I2C 與 SSD1306_I2C 物件
    i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C
    oled=ssd1306.SSD1306_I2C(128, 64, i2c)
    sb=big_symbol.Symbol(oled)
    while True:  # 每分鐘抓一次更新 OLED 顯示器
        data=get_weather(country, city, weather_api_key)
        if data:
            sb.clear()
            icon_file=check_icon(f'/icons/{data["icon"]}.bin')  
            with open(icon_file, 'rb') as f:    
                icon=f.read()
                oled.fill(0)
                fb=framebuf.FrameBuffer(bytearray(icon), 48, 48, framebuf.MVLSB)
                oled.framebuf.blit(fb, 0, 15)
            temperature=round(data["temperature"])
            sb.text(f'{temperature}c', 50, 10)
            sb.text(f'{data["humidity"]}%', 50, 28)
            sb.text(f'{data["pressure"]}hPa', 50, 46)                
            oled.show()
            oled.text(city, 0, 0, 1)
            oled.show()
        time.sleep(60)
else:
    print('無法連線 WiFi')

黃底色為修改或新增的部分 (注意新增 import os). 另外溫度顯示也可以放寬到小數點後一位, 只要將 round(data["temperature"]) 改為 round(data["temperature"], 1) 即可. 

沒有留言 :