2024年9月23日 星期一

MicroPython 學習筆記 : 解決 xtools 函式庫在 v1.23 韌體無法傳送 Line 訊息問題

這兩天測試 MicroPython 發現最新版的 v1.23 韌體取消了 ussl 模組改用 tls 模組, 這會讓 "超簡單 Python/MicroPython 物聯網應用" 這本書所提供的好用函式庫 xtools 破功, 導致傳送 Line Notify 訊息的 line_msg() 函式出錯, 這是因為 xtools 會用到另一個 xrequests 模組, 而 xrequests 會用到 ussl 模組之故, 所以只好刷回到 v1.23 之前的韌體才能繼續使用 xtools.  

但是當我使用 v1.19 或 v1.22 版韌體的 MicroPython 串接 OpenAI API 時都會出現 OSError: -40 的系統錯誤, 經查這是舊版 MicroPython 對 SSL/TLS 支援不足所致, 改用 v1.23 版韌體就可順利串接 OpenAI API, 這變成兩難問題, 即退回使用 v1.23 前的版本 xtools 可傳 Line 但無法串接 OpenAI API; 反之, 使用 v1.23 韌體 xtools 無法傳 Line 卻能串接 OpenAI API. 解決之道應該是要要想辦法修改 xtools 函式庫讓它能在 v1.23 版韌體下兩全其美. 

昨天下了一整天雨, 我待在鄉下家測試了一整個下午, 借助 ChatGPT 協助終於搞定此問題, 測試過程紀錄如下. 

首先我將一顆 ESP8266 刷到最新的 v1.23 韌體, 利用 xtools 連上網路後不使用它的 line_msg() 含試發送 Line Notify 訊息, 而是從以前在 Cythone 上測試 Line Notify 時的筆記複製了一個自訂函式 notify() 函式過來, 然後用 MicroPython 內建的 urequest.post() 來發送 Line Notify 的 POST 請求, 參考這篇文章 :


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

>>> import urequests  
>>> def line_msg(msg, token):    
    url="https://notify-api.line.me/api/notify"   
    headers={"Authorization": "Bearer " + token}     
    payload={"message": msg}     
    r=urequests.post(url, headers=headers, params=payload)         
    return "訊息發送成功!"   

呼叫 line_msg() 
>>> msg='test'    
>>> token="在此輸入 Line Notify token"   
>>> xtools.line_msg(msg, token)     
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in notify
  File "requests/__init__.py", line 186, in post
TypeError: unexpected keyword argument 'params'    

在 xtools.py 中 line_msg() 是呼叫自訂的 xrequests.py 模組的 post() 發出請求, 所以不會有問題, 此處改用內建的 urequests 會出現問題是因為它的 post() 方法沒有 params 參數, 我將程式與錯誤訊息拿去問 ChatGPT 得知原來 urequests.post() 要用 data 關鍵字傳遞參數而非 params, 順此思路測試下去居然就解決問題了, AI 真是好用啊!  




ChatGPT 答覆如下 : 




原來問題是出在 post() 函式應該用 data 關鍵字傳遞參數, params 是 get() 方法用的. ChatGPT 提供的建議程式碼如下 (其實就是將 params 改成 data 而已) :

def line_msg(msg, token):
    url = "https://notify-api.line.me/api/notify"
    headers = {"Authorization": "Bearer " + token}
    payload = {"message": msg}
    r = urequests.post(url, headers=headers, data=payload)  # 修改為 data
    r.close()  # 記得關閉連線
    return "訊息發送成功!"

但執行仍出現錯誤 "object with buffer protocol required" :

>>> xtools.line_msg(msg, token)    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in notify
  File "requests/__init__.py", line 186, in post
  File "requests/__init__.py", line 125, in request
TypeError: object with buffer protocol required

再次詢問 ChatGPT 才知道原來 data 參數的值必須是 byte 類型 : 




但它建議的解決方法是用 urllib.parse.urlencode() 函式將 payload 字典轉換為 URL 編碼的表單格式, 例如 {"message": "test"} 會轉換為 "message=test" : 

import urequests
import urllib.parse

def line_msg(msg, token):
    url = "https://notify-api.line.me/api/notify"
    headers = {"Authorization": "Bearer " + token, "Content-Type": "application/x-www-form-urlencoded"}    
    # 將 payload 轉換為 application/x-www-form-urlencoded 格式
    payload = {"message": msg}
    encoded_payload = urllib.parse.urlencode(payload)  # 編碼成 URL 格式
    encoded_payload = encoded_payload.encode('utf-8')  # 轉換為 bytes
    # 發送 POST 請求
    r = urequests.post(url, headers=headers, data=encoded_payload)  # 使用編碼後的 payload
    r.close()  # 記得關閉連線    
    return "訊息發送成功!"

其中 encode('utf-8') 會將編碼後的字串轉換為 bytes 類型, 因為 urequests 需要以這種格式發送 POST 資料. 但 ChatGPT 建議的程式無法執行, 因為 MicroPython 沒有支援 urllib 模組啊! 怎麼辦? 
ChatGPT 最後提供一個自訂的小函式 urlencode() 來模擬 urllib.parse.urlencode() 的功能, 它會將 params 字典轉換為 application/x-www-form-urlencoded 格式 (即 'k1=v1&k2=v2& ...' 字串) : 

def urlencode(params):
    # 將字典的鍵值對轉換為 URL 編碼的字串 (k=v) 並以 & 連接多個鍵值對
    kv=['{}={}'.format(k, v) for k, v in params.items()]
    return '&'.join(kv) 

例如 :

>>> params={'a':'1', 'b':'2', 'c':'3'}   
>>> urlencode(params)     
'b=2&c=3&a=1'   

呼叫字串的 encode() 方法即可將 URL 字串轉成 data 參數要求的 bytes 類型 : 

>>> urlencode(params).encode('utf-8')    
b'b=2&c=3&a=1'

參數字典經過這樣編碼為 bytes 類型後便可傳給 data 參數了 : 

def line_msg(token, message):
    url="https://notify-api.line.me/api/notify"
    headers={"Authorization": "Bearer " + token,
             "Content-Type": "application/x-www-form-urlencoded"}     
    params={"message": message}  # 參數字典
    # 呼叫自訂的 URL 編碼函式將字典轉成 URL 字串, 再轉成 utf-8 編碼的 bytes 
    payload=urlencode(params).encode('utf-8')
    # 用編碼後的 payload 傳給 data 參數發送 POST 請求
    r=urequests.post(url, headers=headers, data=payload)  
    if r is not None and r.status_code == 200:
        print("Message has been sent.")
    else:
        print("Error! Failed to send notification message.")  
    r.close()  # 關閉連線

>>> import urequests   
>>> def urlencode(params):   
    # 將字典的鍵值對轉換為 URL 編碼的字串 (k=v) 並以 & 連接多個鍵值對
    kv=['{}={}'.format(k, v) for k, v in params.items()]
    return '&'.join(kv) 
>>> def line_msg(token, message):   
    url="https://notify-api.line.me/api/notify"
    headers={"Authorization": "Bearer " + token,
             "Content-Type": "application/x-www-form-urlencoded"}     
    params={"message": message}  # 參數字典
    # 呼叫自訂的 URL 編碼函式將字典轉成 URL 字串, 再轉成 utf-8 編碼的 bytes 
    payload=urlencode(params).encode('utf-8')
    # 用編碼後的 payload 傳給 data 參數發送 POST 請求
    r=urequests.post(url, headers=headers, data=payload)  
    if r is not None and r.status_code == 200:
        print("Message has been sent.")
    else:
        print("Error! Failed to send notification message.")  
    r.close()  # 關閉連線
>>> msg='test'    
>>> token='在此輸入 Line Notify Token'   
>>> xtools.line_msg(token, msg)   
Message has been sent.
>>> xtools.line_msg(token, msg)   
Message has been sent.
>>> msg='哈囉'    
>>> xtools.line_msg(token, msg)   
Message has been sent.

哈哈可以了, 結果如下 :




ChatGPT 真是程式員的好朋友啊! 

接下來用同樣方法改寫傳送貼圖的 line_sticker() 函式如下 : 

def line_sticker(token, message, stickerPackageId, stickerId):
    url="https://notify-api.line.me/api/notify"
    headers={ # 加入正確的 Content-Type
        "Authorization": "Bearer " + token,
        "Content-Type": "application/x-www-form-urlencoded"  
        }
    # 設定正確的 payload
    params={
        "message": message,
        "stickerPackageId": stickerPackageId,
        "stickerId": stickerId
        }
    # 使用自訂的 urlencode 函數將參數編碼,並轉換成 UTF-8 的字節串
    payload=urlencode(params).encode('utf-8')
    # 發送 POST 請求
    r=urequests.post(url, headers=headers, data=payload)
    # 判斷是否成功
    if r is not None and r.status_code == 200:
        return "The sticker has been sent."
    else:
        return "Error! Failed to send the sticker."

測試也 OK :

>>> xtools.line_sticker(token, message, 1, 4)   
'The sticker has been sent.'




但改寫 line_image() 卻不順利, 經詢問 ChatGPT, 原來 LINE Notify API 傳送圖片需要使用 multipart/form-data, 而不是 application/x-www-form-urlencoded, 圖檔須用 imageFile 參數作為文件上傳, 而不能直接使用 URL 編碼處理. ChatGPT 建議的程式碼如下 :

def line_image(token, message, image_path):
    url = "https://notify-api.line.me/api/notify"
    headers = {
        "Authorization": "Bearer " + token
        # 這裡不需要手動設定 Content-Type,urequests 會自動處理 multipart/form-data
        }
    # 打開圖片文件並發送請求
    with open(image_path, 'rb') as image_file:
        # 構造 multipart/form-data 資料
        payload = {"message": message}
        files = {"imageFile": image_file}  # 圖片文件必須在這裡作為二進制數據
        # 使用 urequests.post 發送包含文件的 multipart 請求
        r = urequests.post(url, headers=headers, data=payload, files=files)
        # 判斷是否成功
        if r is not None and r.status_code == 200:
            return "The image has been sent."
        else:
            return "Error! Failed to send the image."

但是呼叫這函式會出現如下錯誤 :

>>> xtools.line_image(token, message, 'kitten.jpg')    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 19, in line_image
  File "requests/__init__.py", line 186, in post
TypeError: unexpected keyword argument 'files'   

ChatGPT 回答錯誤原因如下 :




原來 MicroPython 的 urequests 模組不支援標準 Python 中的 files 參數, ChatGPT 提出建議程式碼改用手動建構 multipart/form-data 請求來解決 :

def line_image(token, message, image_path):
    url = "https://notify-api.line.me/api/notify"
    boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'  # 自訂 boundary
    headers = {
        "Authorization": "Bearer " + token,
        "Content-Type": "multipart/form-data; boundary={}".format(boundary)
       }
    # 打開圖片文件
    with open(image_path, 'rb') as image_file:
        image_data = image_file.read()
    # 構造 multipart/form-data 請求體
    payload = (
        '--{}\r\n'
        'Content-Disposition: form-data; name="message"\r\n\r\n'
        '{}\r\n'
        '--{}\r\n'
        'Content-Disposition: form-data; name="imageFile"; filename="{}"\r\n'
        'Content-Type: image/jpeg\r\n\r\n'
        '{}\r\n'
        '--{}--\r\n'
    ).format(boundary, message, boundary, image_path, image_data.decode('latin-1'), boundary)
    # 發送 POST 請求
    r = urequests.post(url, headers=headers, data=payload)
    # 判斷是否成功
    if r is not None and r.status_code == 200:
        return "The image has been sent."
    else:
        return "Error! Failed to send the image."

但執行結果卻出現如下錯誤:
>>> xtools.line_image(token, message, 'kitten.jpg')    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in line_image
MemoryError: memory allocation failed, allocating 7168 bytes

所以最根本的問題是, ESP32 記憶體不大, 要處理圖片上傳力有未逮, 所以只好放棄. 雖然如此, 但傳送雲端圖片還是可以的, 程式如下 :

def line_image_url(token, message, image_url):
    # 透過 LINE Notify 發送雲端圖片
    url="https://notify-api.line.me/api/notify"
    headers={
        "Authorization": "Bearer " + token,
        "Content-Type": "application/x-www-form-urlencoded"
        }
    # 構造請求的數據,包含圖片的 URL
    params={
        "message": message,
        "imageFullsize": image_url,  # 完整圖片的 URL
        "imageThumbnail": image_url  # 縮略圖圖片 URL,可與完整圖片相同
        }
    # 轉成 URL 字串並用 utf-8 編碼為 bytes 
    payload=urlencode(params).encode('utf-8')
    # 發送 POST 請求
    r=urequests.post(url, headers=headers, data=payload)
    # 判斷是否成功
    if r is not None and r.status_code == 200:
        return "The image URL has been sent."
    else:
        return "Error! Failed to send the image URL."

我使用的測試圖片網址如下 :


>>> import urequests   
>>> message='test'    
>>> image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'    
>>> xtools.line_image_url(token, message, image_url)    
'The image URL has been sent.'

測試結果 OK :



沒有留言 :