昨天借助 ChatGPT 對 ESP8266 版的 xtools.py 進行了記憶體耗用上的優化, 參考 :
雖然 ESP32 開發板基本上有較大的 SRAM (520KB), 但寫程式不浪費記憶體仍是基本要求, 所以今天把 ESP32 版的 xtools.py 也丟給 ChatGPT 去檢視看看哪裡可以改進, 前一篇 ESP8266 重疊的部分就不列了, ChatGPT 修改建議如下 :
1. 將 set_ap() 中不再使用的變數 (例如 data, request 等) 在使用完後手動設定為 None 以協助垃圾回收進行內存釋放.
def set_ap(led=2):
html='''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
%s
</body>
</html>
'''
form='''
<form method='get' action='/update_ap'
style='width: max-content; margin: 10px auto'>
<h2 style='text-align: center; font-size: 20px'>設定 WiFi 基地台</h2>
<div style='display: block; margin-bottom: 20px'>
<label for='ssid' style='display: block; font-size: 16px'>SSID</label>
<input type='text' id='ssid' name='ssid'
style='padding: 10px 8px; width: 100%; font-size: 16px'>
</div>
<div style='display: block; margin-bottom: 20px'>
<label for='pwd' style='display: block; font-size: 16px'>Password</label>
<input type='text' id='pwd' name='pwd'
style='padding: 10px 8px; width: 100%; font-size: 16px'>
</div>
<button type="submit" style='width:100%;font-size: 16px'>連線</button>
</form>
'''
ok='''
<h2>WiFi 連線成功<br>IP : <a href={0}>{0}</a></h2>
<a href=192.168.4.1>
<button style="width:100%;font-size: 16px">重新設定</button>
</a>
'''
ng='''
<h2 style="text-align: center;">WiFi 基地台連線失敗<br>
按 Reset 鈕後重新設定</h2>
<a href="192.168.4.1">
<button style="width:100%;font-size: 16px">重新設定</button>
</a>
'''
wifi_led=Pin(led, Pin.OUT, value=1) # 預設熄滅板上 LED
ap=network.WLAN(network.AP_IF) # 開啟 AP 模式
ap.active(True)
sta=network.WLAN(network.STA_IF) # 開啟 STA 模式
sta.active(True)
import socket
addr=socket.getaddrinfo('192.168.4.1', 80)[0][-1] # 傳回 (ip, port)
s=socket.socket() # 建立伺服端 TCP socket
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 網址可重複請求
s.bind(addr) # 綁定 192.168.4.1 的 80 埠
s.listen(5) # 最多同時 5 個連線
print('網頁伺服器正在監聽 : ', addr)
while True: # 監聽 192.168.4.1 的 80 埠
cs, addr=s.accept()
print('發現來自客戶端的連線 : ', addr)
data=cs.recv(1024)
request=str(data, 'utf8')
print(request, end='\n')
del data, addr
if request.find('update_ap?') == 5: # 檢查是否為更新之 URL
# 擷取請求參數中的 SSID 與密碼
para=request[request.find('ssid='):request.find(' HTTP/')]
ssid=para.split('&')[0].split('=')[1]
pwd=para.split('&')[1].split('=')[1]
sta.connect(ssid, pwd) # 連線 WiFi 基地台
start_time=time.time() # 紀錄起始時間
while not sta.isconnected(): # 連線 WiFi (15 秒)
wifi_led.value(0) # 讓板載 LED 閃爍
time.sleep_ms(300)
wifi_led.value(1)
time.sleep_ms(300)
if time.time()-start_time > 15: # 是否超過連線秒數
print('WiFi 連線逾時!')
break # 逾時跳出無限迴圈
# 確認是否連線成功
if sta.isconnected(): # WiFi 連線成功
print('WiFi 連線成功 : ', sta.ifconfig())
ip=sta.ifconfig()[0] # 取得 ip
print('取得 IP : ' + ip)
with open('config.py', 'w', encoding='utf-8') as f:
f.write(f'SSID="{ssid}"\nPASSWORD="{pwd}"') # 更新設定檔
cs.send(html % ok.format(ip)) # 回應連線成功頁面
for i in range(25): # 連線成功 : 快閃 5 秒
wifi_led.value(0)
time.sleep_ms(100)
wifi_led.value(1)
time.sleep_ms(100)
cs.close()
s.close()
return ip
else:
print('WiFi 連線失敗 : 請按 Reset 鈕後重設.')
wifi_led.value(1) # 連線失敗 : 熄滅 LED
cs.send(html % ng) # 回應連線失敗頁面
cs.close()
s.close()
return None
else: # 顯示設定 WiFi 頁面
cs.send(html % form) # 回應設定 WiFi 頁面
cs.close()
del cs, request
更動的部分是將 del data, addr 向上移入 while 迴圈內而已.
2. 將 set_ap() 函式中的 html、form、ok 和 ng 的 HTML 内容拆出來成為網頁模板, 然後於 set_ap() 中呼叫新增的 load_template() 載入網頁內容來用 :
例如:
例如:
(1). template.html:包含主要的 HTML 框架
(2). form_template.html:包含 WiFi 設定的表單
(3). ok_template.html:包含 WiFi 連線成功的內容
(4). ng_template.html:包含 WiFi 連線失敗的內容
set_ap() 修改如下 :
def load_template(filename):
"""從文件讀取 HTML 模板"""
with open(filename, 'r', encoding='utf-8') as file:
return file.read()
def set_ap(led=2):
# 載入 HTML 模板文件
html_template = load_template('template.html')
form_template = load_template('form_template.html')
ok_template = load_template('ok_template.html')
ng_template = load_template('ng_template.html')
wifi_led = Pin(led, Pin.OUT, value=1) # 預設熄滅板上 LED
ap = network.WLAN(network.AP_IF) # 開啟 AP 模式
ap.active(True)
sta = network.WLAN(network.STA_IF) # 開啟 STA 模式
sta.active(True)
import socket
addr = socket.getaddrinfo('192.168.4.1', 80)[0][-1] # 傳回 (ip, port)
s = socket.socket() # 建立伺服端 TCP socket
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 網址可重複請求
s.bind(addr) # 綁定 192.168.4.1 的 80 埠
s.listen(5) # 最多同時 5 個連線
print('網頁伺服器正在監聽 : ', addr)
while True:
cs, addr = s.accept()
print('發現來自客戶端的連線 : ', addr)
data = cs.recv(1024)
request = str(data, 'utf8')
print(request, end='\n')
del data, addr
if request.find('update_ap?') == 5: # 檢查是否為更新之 URL
para = request[request.find('ssid='):request.find(' HTTP/')]
ssid = para.split('&')[0].split('=')[1]
pwd = para.split('&')[1].split('=')[1]
sta.connect(ssid, pwd) # 連線 WiFi 基地台
start_time = time.time() # 紀錄起始時間
while not sta.isconnected(): # 連線 WiFi (15 秒)
wifi_led.value(0) # 讓板載 LED 閃爍
time.sleep_ms(300)
wifi_led.value(1)
time.sleep_ms(300)
if time.time() - start_time > 15: # 是否超過連線秒數
print('WiFi 連線逾時!')
break # 逾時跳出無限迴圈
if sta.isconnected(): # WiFi 連線成功
print('WiFi 連線成功 : ', sta.ifconfig())
ip = sta.ifconfig()[0]
print('取得 IP : ' + ip)
with open('config.py', 'w', encoding='utf-8') as f:
f.write(f'SSID="{ssid}"\nPASSWORD="{pwd}"') # 更新設定檔
# 回應連線成功頁面
cs.send(html_template % ok_template.format(ip))
for i in range(25): # 快閃 5 秒
wifi_led.value(0)
time.sleep_ms(100)
wifi_led.value(1)
time.sleep_ms(100)
cs.close()
s.close()
return ip
else: # WiFi 連線失敗
print('WiFi 連線失敗 : 請按 Reset 鈕後重設.')
wifi_led.value(1) # 熄滅 LED
cs.send(html_template % ng_template) # 回應連線失敗頁面
cs.close()
s.close()
return None
else: # 顯示設定 WiFi 頁面
cs.send(html_template % form_template) # 回應設定 WiFi 頁面
cs.close()
del cs, request
不過這個建議我沒採納, 因為這樣就多出四個網頁檔出來, 還不如內建在 xtools 中方便.
3. 在line_msg(), line_sticker(), line_image_url(), 和 ask_gpt() 等函式中, 重複的 HTTP header 可以提取到共用函數中統一處理.
修改後的程式碼如下 :
def get_headers(service, token=None, api_key=None):
headers={
"Content-Type": "application/x-www-form-urlencoded"
} # 須有初始值
if service == 'line' and token: # 根據服務類型返回標頭
headers["Authorization"]="Bearer " + token
elif service == 'openai' and api_key:
headers["Authorization"]="Bearer " + api_key
headers["Content-Type"]="application/json"
return headers
def line_msg(token, message):
url="https://notify-api.line.me/api/notify"
headers=get_headers('line', token=token)
params={"message": message} # 參數字典
# 將參數字典轉成 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() # 關閉連線
def line_sticker(token, message, stickerPackageId, stickerId):
url="https://notify-api.line.me/api/notify"
headers=get_headers('line', token=token)
# 設定正確的 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."
def line_image_url(token, message, image_url):
# 透過 LINE Notify 發送雲端圖片
url="https://notify-api.line.me/api/notify"
headers=get_headers('line', token=token)
# 構造請求的數據,包含圖片的 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."
def ask_gpt(prompt, api_key, model='gpt-4o-mini'):
url='https://api.openai.com/v1/chat/completions'
headers=get_headers('openai', api_key=api_key)
# 建立 data 參數字典
data={
'model': model,
'messages': [{'role': 'user', 'content': prompt}]
}
# 將字典轉成字串後再編碼成 UTF-8
payload=ujson.dumps(data).encode('utf-8')
# 發送 POST 請求
response=urequests.post(url, headers=headers, data=payload)
if response.status_code == 200:
reply=response.json() # 轉成字典
return reply['choices'][0]['message']['content']
else:
return response.json() # 返回錯誤信息
測試結果如下 :
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(led=5)
network config: ('192.168.192.189', '255.255.255.0', '192.168.192.92', '192.168.192.92')
>>> line_token=config.LINE_NOTIFY_TOKEN
>>> openai_api_key=config.OPENAI_API_KEY
>>> message='test'
>>> image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'
>>> xtools.line_msg(line_token, message)
Message has been sent.
>>> xtools.line_sticker(line_token, message, 1, 4)
'The sticker has been sent.'
>>> xtools.line_image_url(line_token, message, image_url)
'The image URL has been sent.'
>>> prompt='Who are you'
>>> print(xtools.ask_gpt(prompt, openai_api_key))
I am an AI language model created by OpenAI, designed to assist with a wide range of questions and topics by providing information and generating text-based responses. How can I help you today?
結果如下 :
程式碼 :
import config
import xtools
ip=xtools.connect_wifi(led=5)
line_token=config.LINE_NOTIFY_TOKEN
openai_api_key=config.OPENAI_API_KEY
message='test'
image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'
xtools.line_msg(line_token, message)
xtools.line_sticker(line_token, message, 1, 4)
xtools.line_image_url(line_token, message, image_url)
prompt='Who are you'
print(xtools.ask_gpt(prompt, openai_api_key))
優化後的 xtools (for ESP32) 已上傳 GitHub 更新 :
沒有留言:
張貼留言