這兩天測試 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 答覆如下 :
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 :
沒有留言:
張貼留言