2024年10月11日 星期五

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

昨天搞定 SSD1306 OLED 顯示器的驅動程式問題後, 想要用它來顯示從 OpenWeatherMap 抓取的高雄氣象資料, 氣象爬蟲做法參考 :


本系列之前的文章參考 :


MicroPython 相關文章索引 :


基於此項實驗使用較多記憶體, 因此使用 ESP32 開發板, 此次使用 LOLIN D32, 同時將連網 SSID 帳密與 OpenWeatherMap 的 API key 寫在 config.py 檔案內. 首先匯入 config 與 xtools 模組連網 : 

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

>>> import config   
>>> import xtools   
>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)   
Connecting to network...
network config: ('192.168.50.94', '255.255.255.0', '192.168.50.1', '192.168.50.1')

接著匯入 urequests 與 ujson 模組, 從 config.py 取出 OpenWeaherMap API key, 定義 city 與 country 變數將其嵌入 API 網址中, 然後呼叫 urequests.get() 傳入 API 網址即可取得氣象資料 :

>>> import urequests   
>>> import ujson   
>>> weather_api_key=config.WEATHER_API_KEY
>>> city='Kaohsiung'   
>>> country='TW'   
>>> url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={weather_api_key}'   
>>> r=urequests.get(url)   
>>> data=ujson.loads(r.text)    # 轉成字典
>>> data   
{'timezone': 28800, 'cod': 200, 'dt': 1728572708, 'base': 'stations', 'weather': [{'id': 803, 'icon': '04n', 'main': 'Clouds', 'description': '\u591a\u96f2'}], 'sys': {'country': 'TW', 'sunrise': 1728510789, 'sunset': 1728553082, 'id': 2002588, 'type': 2}, 'name': 'Kaohsiung City', 'clouds': {'all': 75}, 'coord': {'lon': 120.3133, 'lat': 22.6163}, 'visibility': 10000, 'wind': {'speed': 2.57, 'deg': 40}, 'id': 1673820, 'main': {'feels_like': 26.33, 'pressure': 1012, 'temp_max': 25.97, 'temp': 25.58, 'temp_min': 25.58, 'humidity': 82, 'sea_level': 1012, 'grnd_level': 1011}}

我們感興趣的氣象資料是溫溼度與氣壓 : 

>>> temperature=data['main']['temp']   
>>> temperature   
25.58
>>> humidity=data['main']['humidity']    
>>> humidity   
82 
>>> pressure=data['main']['pressure']   
>>> pressure   
1012

氣候狀況描述會傳回該地區語言的 Unicode, 用 print() 即可解碼 :

>>> data['weather'][0]['description']  
'\u591a\u96f2'
>>> print(data['weather'][0]['description'])  
多雲

但中文無法在 SSD1306 顯示, 但可以用天氣圖示表示, OpenWeatherMap 定義了 17 種天氣概況圖示並與已命名, 放在  data['weather'][0]['icon'] : 

>>> data['weather'][0]['icon']    
'04n'

這個編號後續測試會用到. 

接下來就可以將這些資料輸出至 SSD1306 顯示, 先匯入 machine 模組下的 Pin 與 I2C 類別, 以及 ssd1306 模組 (鑰先上傳開發板) : 

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

呼叫 I2C 類別之建構式 I2C() 並傳入 I2C 的兩個接腳 SDA 與 SCL 之 Pin 物件以建立 i2c 物件 : 

>>> i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C   

建立代表 OLED 顯示器的 SSD1306_I2C 物件 : 

>>> oled=ssd1306.SSD1306_I2C(128, 64, i2c)    # 0.96 吋解析度 128*64  

此物件有如下常用方法 : 


 SSD1306_I2C 物件的方法 說明
 fill(col) 將顯示記憶體全部畫素填入 col=1 (亮) 或 0 (暗)
 pixel(x, y, col) 在顯示記憶體指定畫素位置 (x, y) 填入 col=1 (亮) 或 0 (暗)
 text(string, x, y, col=1) 在顯示記憶體指定畫素位置 (x, y) 起填入預設 col=1 之字串 string
 show() 將顯示記憶體內容輸出於面板顯示內容
 scroll(dx, dy) 將顯示記憶體畫素內容向上下 (dy) 或向左右 (dx) 捲動
 invert(state) state=True (反白), False (取消反白)


先點亮 OLED 畫面 :

>>> oled.fill(1)      # 將 1 填滿顯示緩衝器 (點亮畫素)
>>> oled.show()  

然後呼叫 text(x, y, on) 在第一列顯示城市, 其中 x, y 為座標, on 表示畫素點亮與否, 1=點亮, 0= 熄滅, 因上面我們將 OLED 每個畫素都點亮, 故顯示文字時需將其熄滅 : 

>>> oled.text(city, 0, 0, 0)   

在第二列顯示縣在時間, 先呼叫 xtools.tw_now() 取得時間再傳給 oled.text() : 

>>> now=xtools.tw_now()   
Querying NTP server and set RTC time ... OK.
>>> now     
'2024-10-10 23:39:13'
>>> oled.text(now, 0, 8, 0)  

注意, SSD1306 解析度 128*64, text() 方法以 8*8 畫素繪製一個字元, 因此每列可顯示 128/8=16 個字元, 共可顯示 64/8=8 列文字, 往下移一列時 Y 座標加 8. 

第 3~5 列分別顯示溫溼度與氣壓 :

>>> oled.text(f'Temperature:{temperature}', 0, 16, 0)    
>>> oled.text(f'Humidity:{humidity}', 0, 24, 0)    
>>> oled.text(f'Pressure:{pressure}', 0, 32, 0)     

最後需呼叫 show() 方法才會顯示 : 

>>> oled.show()  

結果如下 : 




不過亮背景方式較耗電, 應改採用暗背景亮文字方式較省電, 這時要先熄滅所有畫素, 呼叫 text() 方法時第三參數需傳入 1 點亮畫素 :

>>> oled.fill(0)      # 將 0 填滿顯示緩衝器 (熄滅畫素)
>>> oled.text(city, 0, 0, 1)  
>>> oled.text(now, 0, 8, 1)  
>>> oled.text(f'Temperature:{temperature}', 0, 16, 1)    
>>> oled.text(f'Humidity:{humidity}', 0, 24, 1)    
>>> oled.text(f'Pressure:{pressure}', 0, 32, 1)    
>>> oled.show()   

結果如下 : 




SSD1306 只能顯示 ASCII 字元, 因此天氣狀況描述 (中文 Unicode) 在缺乏中文字元集解碼情況下無法正常顯示 :

>>> oled.text(f'Pressure:{pressure}', 0, 40, 1)        
>>> oled.show()     

結果顯示一個長直條 :




以上測試之完整程式碼如下 : 

# weather_ssd1306_1.py
import config   
import xtools
import urequests   
import ujson
from machine import I2C, Pin   
import ssd1306   

ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
weather_api_key=config.WEATHER_API_KEY
city='Kaohsiung'   
country='TW'   
url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={weather_api_key}'   
r=urequests.get(url)   
data=ujson.loads(r.text) # 轉成字典
temperature=data['main']['temp']   
humidity=data['main']['humidity']    
pressure=data['main']['pressure']
description=data['weather'][0]['description']
now=xtools.tw_now() 
i2c=I2C(0, scl=Pin(22), sda=Pin(21)) # ESP32 I2C
oled=ssd1306.SSD1306_I2C(128, 64, i2c) # 0.96 吋解析度 128*64
oled.fill(0)      # 將 0 填滿顯示緩衝器 (熄滅畫素)
oled.text(city, 0, 0, 1)  
oled.text(now, 0, 8, 1)  
oled.text(f'Temperature:{temperature}', 0, 16, 1)    
oled.text(f'Humidity:{humidity}', 0, 24, 1)    
oled.text(f'Pressure:{pressure}', 0, 32, 1)    
oled.show()  

但此程式只能顯示抓取一次氣象資料, 應該週期性地 (例如每分鐘) 去 OpenWeatherMap 抓資料後更新 SSD1306 才實用. 首先將之前寫的天氣爬蟲函式改寫如下 :

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

參考 :


先在互動環境測試此函式 : 

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

然後將輸出至顯示器的功能寫成一個 show_weather() 函式 :

def show_weather(oled, data):
    oled.fill(0)  # (填滿 0 熄滅畫素)
    oled.text(data['city'], 0, 0, 1)  
    oled.text(data['time'], 0, 8, 1)  
    oled.text(f"Temperature:{data['temperature']}", 0, 16, 1)    
    oled.text(f"Humidity:{data['humidity']}", 0, 24, 1)    
    oled.text(f"Pressure:{data['pressure']}", 0, 32, 1)    
    oled.show()

此函式要傳入一個 SSD1306_I2C 物件 oled 與要顯示之資料字典 data, 這主要是在上面呼叫 get_weather() 時取得的傳回值上添加城市 city 與時間 time 這兩個鍵 : 

>>> data['time']=xtools.tw_now()   
Querying NTP server and set RTC time ... OK.
>>> data['city']=city    
>>> data    
{'pressure': 1012, 'time': '2024-10-11 08:25:10', 'temperature': 26.68, 'city': 'Kaohsiung', 'humidity': 84, 'geocode': 1673820, 'icon': '04d'}

呼叫 show_weather() 測試 OK : 

>>> show_weather(oled, data)   

接下來只要匯入 time 模組, 然後用一個無窮迴圈每 60 秒去 OpenWeatherMap 抓一次資料來更新 OLED 顯示器即可 : 

>>> import time   
>>> while True:  # 每分鐘抓一次更新 OLED 顯示器
    data=get_weather(country, city, weather_api_key)   
    data['time']=xtools.tw_now()  # 新增或更新欄位
    data['city']=city # 新增或更新欄位
    show_weather(oled, data)    
    time.sleep(60)    

完整程式碼如下 : 

# weather_ssd1306_2.py
import config   
import xtools
import urequests   
import ujson
from machine import I2C, Pin   
import ssd1306
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

def show_weather(oled, data):
    oled.fill(0)  # (填滿 0 熄滅畫素)
    oled.text(data['city'], 0, 0, 1)  
    oled.text(data['time'], 0, 8, 1)  
    oled.text(f"Temperature:{data['temperature']}", 0, 16, 1)    
    oled.text(f"Humidity:{data['humidity']}", 0, 24, 1)    
    oled.text(f"Pressure:{data['pressure']}", 0, 32, 1)    
    oled.show()

def main():
    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)
        while True:  # 每分鐘抓一次更新 OLED 顯示器
            data=get_weather(country, city, weather_api_key)
            data['time']=xtools.tw_now()  # 新增或更新欄位
            data['city']=city # 新增或更新欄位
            show_weather(oled, data)
            time.sleep(60)
    else:
        print('無法連線 WiFi')

if __name__ == '__main__':
  main()

比較系統化的結構是把連線 WiFi 的功能寫在 main.py :

# main.py
import xtools    
import config
import weather_ssd1306      

ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
mac=xtools.get_id()
print('ip: ', ip)
print('mac: ', mac)
if not ip: # 連線失敗傳回 None
    print('無法連線 WiFi 基地台')
else:      # 連線成功傳回 ip 字串: 執行 app
    weather_ssd1306.main() # 連線成功才執行 App 程式

然後把上面的 weather_ssd1306_test_2.py 改寫為下面的 weather_ssd1306.py :

# weather_ssd1306.py 
import config   
import xtools
import urequests   
import ujson
from machine import I2C, Pin   
import ssd1306
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

def show_weather(oled, data):
    oled.fill(0)  # (填滿 0 熄滅畫素)
    oled.text(data['city'], 0, 0, 1)  
    oled.text(data['time'], 0, 8, 1)  
    oled.text(f"Temperature:{data['temperature']}", 0, 16, 1)    
    oled.text(f"Humidity:{data['humidity']}", 0, 24, 1)    
    oled.text(f"Pressure:{data['pressure']}", 0, 32, 1)    
    oled.show()

def main():
    # 設定氣象爬蟲參數
    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)
    while True:  # 每分鐘抓一次更新 OLED 顯示器
        data=get_weather(country, city, weather_api_key)
        data['time']=xtools.tw_now()  # 新增或更新欄位
        data['city']=city # 新增或更新欄位
        show_weather(oled, data)   # 更新 OLED 顯示
        time.sleep(60)

if __name__ == '__main__':
  main()

注意, 此結構須按開發板的 Reset 鈕硬啟動才會執行, Thonny 的 "執行/重新啟動後端程式" 只是軟啟動 (soft restart) 不會執行 boot.py, 所以也不會去執行 main.py. 這些程式壓縮檔可從 GitHub 下載 :


注意, 上傳到 ESP32 開發板之前須先編輯 config.py, 填入 WiFi 基地台的 SSID 與密碼, 以及向 OpenWeatherMap 取得的 API key. 

沒有留言 :