2024年9月30日 星期一

2024 年第 39 周記事

不知不覺又來到月底了, 如果沒有記下一些生活大小事的話, 真的會渾渾噩噩不知道怎麼走到今天, 這樣的人生會像是一團迷霧. 不過偉人似乎都有寫日記習慣, 前幾天看到國史館關於蔣經國日記的新聞, 說蔣經國在 1941 年的日記封面上記載說他發現該年日記被撕掉 77 頁之多, "實為奇事", 他只記覺得奇怪而未記是否有調查誰撕的, 這不是更奇怪嗎? 這一年正是情婦章亞若懷雙胞胎之時, 可能是被蔣介石撕掉, 也可能是自己撕掉而故弄玄虛. 愚以為若是難堪事乾脆就不要寫, 何必寫了又撕. 

週六載菁菁回楠梓工作室, 我順路回鄉下, 途經燕巢交流道附近的果苗園, 下車買了三株珍珠芭樂, 一株檸檬, 一株白草莓, 共花了 420 元 :





但目前菜園無適合處所種芭樂, 要等小舅的玉米收成後再種. 

AI 自動製作簡報工具 Gamma

今天益師傅來我司授課, 傳授了好幾個 AI 簡報工具, 我在課堂中只能勉強跟上 Gamma, 覺得它的介面簡潔且功能強大, 免費帳戶可以製作 10 張簡報, 如果不夠可以付費成為 Pro 帳戶, 或者將內容切割分次產生即可 (要設定相同主題版面以免風格突兀). 


1. 註冊 Gamma 免費帳戶 : 

先到 Gamma 官網 : 


按右上角的 "Try for free" 鈕 : 




可以用 Google 帳號快速註冊, 但我習慣使用 Email :




填寫 Email 後按 "Continue with email" 鈕 :



收 email 認證信, 按下信中的 "Verify your email address" 鈕會開啟建立帳戶頁面 : 




輸入姓名選擇語言, 設定密碼 (須至少 8 個字元, 含一個大寫字母與符號) :




建立工作區, 選擇 "個人" :




自我介紹任選, 按 "繼續" 完成註冊 :



完成後會進入 Gamma 主頁面 :




可以貼上文字內容, 提示詞, 上傳檔案或指定網址等方式讓 Gamma 的 AI 自動生成簡報. 


2. 貼上文字內容自動升成簡報 : 

在登入後的主頁面按最左邊的 "貼上文字" 會顯示一個文字輸入框, 我將詢問 ChatGPT "請比較 Framer Motion 與其他競爭之函式庫之優缺點" 的答覆貼進去, 點選 "簡報內容" 後按 "繼續" :




左邊是生成簡報時之文字格式設定, 底下是生成張數設定, 可按 + 增加張數, 但免費帳戶最多可生成時十張, 設定好後按 "繼續" :




此頁是要選擇簡報版面主題, 可捲動右方卷軸選取一個適合的主題, 按右上方的 "產生" 鈕即開始自動產生簡報頁面 : 





生成結果如下 : 




按右上角的 "分享" 會彈出一個分享視窗 :




按 "分享" 會產生此文件的 URL, 可按 "複製連結" 分享給團隊成員共同編輯 (注意, 此連結是指向可編輯的簡報文件), 也可以按 "匯出" 將簡報文件匯出為 pdf, png, 或 pptx 檔 : 




下面是匯出頁面 : 




如果要在網站上公開展示可按 "發布到站點" 鈕, 這會在右邊出現站點設定頁面, 按 "Convert to site" 鈕 :




按 "發布站點" 會發布到 Gamma 提供的免費網站 : 




可見上面 Gamma 生成之簡報發布到 Gamma 網站後之網址如下 : 

可見 Gamma 真是簡報的懶人包工具啊! 

2024年9月29日 星期日

使用 Make 的免費 Webhook

早上花了一點時間找出很久以前註冊的 IFTTT 帳號, 想要製作一個 Webhook 來測試 xtools 函式庫中的 webhook_get() 與 webhook_post() 函式, 結果設定了老半天到後面才發現白忙了, 因為 IFTTT 把 webhook 也列入收費項目, 已經無法免費使用 : 



 
但我找到替代方案 :


原來幾周前維元老師來講課時介紹的流程自動化 Make 也有提供免費的 Webhook 功能, 關於註冊 Make 帳號參考前一篇文章 :


以下就照這篇文章的介紹來建立一個 Webhook, 首先登入 Make 後按右上角的 "Create a new scenario" 鈕 :




這時右邊會出現一張 App 選單, 點選其中的 Webhook :




接著在 Triggers 選單中點選 "Custom Webhook" :





然後按 "Create a webhook" 鈕 :




在彈出視窗中填寫 Webhook 名稱 (120 個字元內隨意取名), 也可以在底下框框輸入自己的 IP 避免別人存取此 Webhook (如果是要公開存取就不用輸入 IP) : 




我是要做 ESP32 測試用, 所以填寫如下 : 




這時 Webhook 名稱底下會出現此 Webhook 的網址, 按下方的 "Copy address to clipboard" 鈕將此 Webhook 網址複製到剪貼簿 : 




先將此網址儲存在記事本, 因為後續要在此 Webhook 網址後面傳送 URL 參數 msg=Hello, 所以我先在此網址後面添加 '?msg=Hello", 例如 :


https://hook.eu2.make.com/bff8xph3.........wxr2qxdkkr4u8tii?msg=Hello   

將此添加參數後的網址貼到瀏覽器網址列按 Enter, 網頁會出現 Accepted : 




這時 Webhook 名稱底下就會出現 "Successfully determined" 表示 Webhook 網址設定完成, 確定 Webhook 網址已儲存在記事本後可以按右下角 OK 鈕關閉彈出視窗 :




接下來按  "Add another module" 鈕來串接一個應用服務, 例如 Line Notify : 




在彈出選單中往下捲動找到 LINE 後點選 (也可以在底下搜尋框中輸入 LINE 搜尋) :




接著點選 Notify API (Send a notification) : 




這會彈出一個新增連線的視窗, 按右方的 Add 鈕新增連線名稱 :




在彈出視窗中填寫連線名稱后按 Save 鈕  :




這時會彈出另一個視窗要求登入 LINE 帳戶 : 




登入 LINE 帳號後選擇指定要傳送的對象, 再按底下的 "同意連動" 鈕 : 




連動完成後自己的 LINE 會收到一個與 Make 連動完成的訊息 : 




接下來回到連線視窗設定串接 LINE 時要傳送的訊息, 點一下 Message 筐會出現一個訊息項目視窗, 它會列出 Webhook 網址參數的項目, 因為在上面確定網址時我們有在後面添加一個 msg 參數, 所以這裡就會列出 msg, 如果添加兩個參數例如 ?msg=Hello&name=Tony 的話這裡就會出現msg 與 name 這兩個項目. 如果要更改參數項目要回到上面確定網址那一步貼上新網址. 

點一下 msg 這項目它就會出現在 Message 框, 這樣就完成串接 LINE Notify 設定了, 按底下 OK 鈕關閉串接 LINE 的連線視窗 : 




這樣這個簡單的 Senerio 便全部設定完成, 按底下的磁碟圖示將 Senerio 存檔, 然後將左下角的滑動開關設置到 ON 啟用此 Sernerio : 




參考 :


2024年9月28日 星期六

MicroPython 學習筆記 : ESP32-S3 開發板燒錄 v1.23 韌體

前幾天到貨的兩片 ESP32-S3 今天下午終於有時間拆開來測試, 此板規格參見賣家商品頁面 :


模組名稱為 ESP32-S3-WROOM-1-N16R8, 其中 N16 表示 Nand Flash 容量為 16MB, 而 R8 則表示 PSRAM 記憶體容量是 8MB, PSRAM 是 ESP32-S3 模組內的周邊擴充 SRAM 晶片, 當所執行的程式需要的記憶體超過內建的 512K SRAM 時就會自動透過 Octal SPI 介面調用 PSRAM 來擴充, 但這這需要支援 PSRAM 的韌體才能啟用. 

首先來燒錄不使用 PSRAM 的韌體. 請注意此板有兩個 Type C 插槽, 左邊那個是 OTG USB 插槽, 右邊那個才是連接 PC COM 埠用來燒錄韌體傳送檔案的, 不要插錯了 :


Source : 露天



板上有兩個按鈕, 上方是 RST 重置鈕用來重開機, 下方的是 BOOT 按鈕用來燒錄韌體用, 但實測發現現在的板子用 esptool 燒錄或某除韌體時都不需要再按下 BOOT 鈕 (可能是新版 esptool 會自動去控制). 另外板上還有一顆全彩 RGB LED (GPIO48). 





ESP32-S3 的韌體的下載頁面如下 :  


最上面的韌體是不支援 PSRAM 的, 按其中的 .bin 連結下載 :




或直接按下列網址 : 


將開發板 COM 埠 USB 連接 PC USB 槽, 從裝置管理員找出連接埠編號, 此處為 COM4, 先檢查 Flash 資訊 : 

D:\ESP32>esptool --port COM4 flash_id    
esptool.py v4.6.2
Serial port COM4
Connecting....
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 3c:84:27:c9:67:1c
Uploading stub...
Running stub...
Stub running...
Manufacturer: 85
Device: 2018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Hard resetting via RTS pin...

可見此開發板確實有 16MB Flash 記憶體 (硬碟).

然後將 Flash 裡面的韌體抹除, 注意, ESP32-S3 的指令與 ESP32 的不同, --chip 參數要改用 esp32s3 或 esp32-s3 : 

D:\ESP32>esptool --chip esp32s3 --port COM4 erase_flash   
esptool.py v4.6.2
Serial port COM4
Connecting...
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 3c:84:27:c9:67:1c
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 29.8s
Hard resetting via RTS pin...

然後燒錄韌體 : 

D:\ESP32>esptool --chip esp32s3 --port COM4 write_flash -z 0 ESP32_GENERIC_S3-20240602-v1.23.0.bin   
esptool.py v4.6.2
Serial port COM4
Connecting....
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 3c:84:27:c9:67:1c
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x0018dfff...
Compressed 1628144 bytes to 1067824...
Wrote 1628144 bytes (1067824 compressed) at 0x00000000 in 94.1 seconds (effective 138.4 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

重開機後上傳 xtools 函式庫測試傳 Line Notify 與查詢 ChatGPT 均正常 : 

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

>>> import xtools    
import config
ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
token=config.LINE_NOTIFY_TOKEN
openai_api_key=config.OPENAI_API_KEY
message='test'
prompt='Who are you'
image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'   

network config: ('192.168.50.124', '255.255.255.0', '192.168.50.1', '192.168.50.1')
>>> xtools.line_msg(token, message)    
Message has been sent.
>>> xtools.line_sticker(token, message, 1, 4)     
'The sticker has been sent.'
>>> xtools.line_image_url(token, message, image_url)    
'The image URL has been sent.'
>>> 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 tasks by providing information and generating text. How can I help you today?'
>>> prompt='你是誰?'    
>>> print(xtools.ask_gpt(prompt, openai_api_key))       
我是一个人工智能助手,旨在回答问题、提供信息和帮助您解决问题。有任何需要了解的内容或者想要讨论的话题吗?

此韌體是不支援 SPI PSRAM 的, 所以查詢記憶體顯示的是 SRAM 的大小 :

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

>>> import gc   
>>> print(gc.mem_free())      
242576  

512 KB 的 SRAM 只剩下一半不到. 用 micropython.mem_info() 會顯示動態記憶體配置情形 :

>>> import micropython  
>>> micropython.mem_info()      
stack: 736 out of 15360
GC: total: 64000, used: 13008, free: 50992, max new split: 188416
 No. of 1-blocks: 299, 2-blocks: 39, max blk sz: 32, max free sz: 3175

此開發板若要用在大量計算 (例如 AI/ML 方面) 就需要用到 PSRAM, 因此接下來改燒錄支援 PSRAM 的韌體, 將下載頁面向下捲動到 Support for Octal-SPIRAM 項目下點選 .bin 即可下載支援 PSRAM 的韌體 : 



或按下列網址 : 


先將上面燒錄的原韌體抹除 : 

D:\ESP32>esptool --chip esp32s3 --port COM4 erase_flash   
esptool.py v4.6.2
Serial port COM4
Connecting....
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 3c:84:27:c9:67:1c
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 29.3s
Hard resetting via RTS pin...

然後燒錄這個有支援 PSRAM 的韌體 : 

D:\ESP32>esptool --chip esp32s3 --port COM4 write_flash -z 0 ESP32_GENERIC_S3-SPIRAM_OCT-20240602-v1.23.0.bin   
esptool.py v4.6.2
Serial port COM4
Connecting...
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 3c:84:27:c9:67:1c
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x0018efff...
Compressed 1631424 bytes to 1069649...
Wrote 1631424 bytes (1069649 compressed) at 0x00000000 in 94.2 seconds (effective 138.5 kbit/s)...
Hash of data verified.

檢查動態記憶體 : 

MicroPython v1.23.0 on 2024-06-02; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import gc     
>>> print(gc.mem_free())       
8302160

總共有 8.3 MB, 其中 8MB 為 PSRAM. 用 micropython.mem_info() 顯示較詳細配置 : 

>>> import micropython    
>>> micropython.mem_info()      
stack: 736 out of 15360
GC: total: 64000, used: 23520, free: 40480, max new split: 8257536  
 No. of 1-blocks: 548, 2-blocks: 57, max blk sz: 32, max free sz: 2518

當然, 此支援 PSRAM 的韌體執行 Line Notify 傳訊息與詢問 OpenAI API 均可正常執行. 總之, 此 ESP32-S3 開發板既然內建 PSRAM, 當然要充分利用, 雖然 PSRAM 效能比不上 SRAM, 但不用也是浪費. 

2024年9月27日 星期五

React 的動畫函式庫 Framer Motion

最近在網路上看到 Framer Motion 這個基於 React 的動畫函式庫, 雖然我沒學過 React (因為我偏愛 Vue), 但基於好奇, 就透過 ChatGPT 來快速了解其特性 :

問 : 請說明 Framer Motion 是甚麼? 
答 : 
Framer Motion 是一個用於 React 應用程式的動畫庫,提供簡單且強大的 API 來實現各種動畫效果。它是 Framer 團隊開發的,專注於流暢、高效的動畫,並支持許多動態交互,例如過渡效果、彈跳、拖動、滾動等。

Framer Motion 的特色包括:

  1. 簡單易用的 API:使用直觀的屬性(如 animateinitialexit)來輕鬆為元件添加動畫。
  2. 性能優化:內建使用動畫關鍵幀、GPU 加速,確保動畫流暢、性能高效。
  3. 支援複雜的動畫:可以輕鬆地定義多階段動畫、拖動行為和手勢。
  4. 易於整合:可以與 React 框架中的其他元件或第三方庫(如 styled-components)無縫整合。
  5. 動態交互:支持互動性動畫,如當元素被拖動、點擊或滾動時的動畫效果。
可以用 GPU 加速真不賴ㄟ. 

問 : 需要先安裝甚麼執行環境嗎?   
答 : 
所需的環境:
  1. 安裝 Node.js 和 npm。
  2. React 應用程式(如 create-react-app)。
    命令列指令 :
    npx create-react-app my-app
    cd my-app
  3. 安裝 Framer Motion 後,即可在專案中使用它來添加動畫效果。
    npm install framer-motion
問 : 請比較 Framer Motion 與其他競爭之函式庫之優缺點 
答 : 

Framer Motion 在動畫和互動效果方面與其他動畫庫相比有獨特的優勢和挑戰。以下是它與幾個常見的競爭動畫函式庫之比較:

1. Framer Motion

優點:

  • 簡單且直觀的 API:使用者可以通過少量代碼快速建立動畫,API 設計對 React 開發者十分友好。
  • 強大的動態交互功能:支援拖曳(drag)、手勢、滾動動畫等複雜行為,適合現代互動式應用。
  • 自動管理動畫:動畫自動化過程(如布局過渡 layout transition),減少手動處理 DOM 的繁瑣工作。
  • React 專用優化:針對 React 環境進行深度優化,並與 React 16.8+ 的 Hooks 機制無縫集成。
  • 高性能:使用 requestAnimationFrame 和 GPU 加速技術來優化動畫性能,動畫流暢。

缺點:

  • 局限於 React:Framer Motion 是專為 React 應用設計的,因此無法在非 React 框架中使用。
  • 文件與學習曲線:儘管 API 簡單,但某些進階功能(如變換動畫、路徑動畫等)可能需要更多學習與摸索。

2. GSAP (GreenSock Animation Platform)

優點:

  • 功能強大且靈活:GSAP 支援所有類型的動畫效果,從簡單過渡到複雜的時序動畫,並且可用於任何 JavaScript 環境(不限於 React)。
  • 極高的瀏覽器相容性:GSAP 支援各種瀏覽器,包括 IE9+,且在不同的環境中保持一致的表現。
  • 複雜的時序控制:GSAP 提供功能豐富的時間軸(timeline)系統,可以精確控制動畫的執行順序和時間。

缺點:

  • 學習曲線較陡:GSAP 的功能多樣,對初學者來說學習成本較高,尤其是其複雜的 API 和動畫時序系統。
  • 相對於 React 整合性弱:雖然 GSAP 可以用於 React,但它並不像 Framer Motion 那樣針對 React 進行專門的優化,整合上稍微繁瑣。

3. Anime.js

優點:

  • 輕量且靈活:Anime.js 是一個體積小巧的動畫庫,適合實現精簡的動畫效果,尤其是在 DOM 操作和 SVG 路徑動畫上表現優異。
  • 多類型動畫支持:Anime.js 支援動畫對象包括 CSS 屬性、DOM 屬性、SVG、JSON、JavaScript 物件等,非常靈活。

缺點:

  • React 整合性不足:Anime.js 不是專為 React 設計的,與 React 整合時需要額外處理元件生命周期管理,導致開發體驗不如 Framer Motion 流暢。
  • 進階功能較弱:相比 GSAP 和 Framer Motion,Anime.js 在時間軸控制和交互性動畫上功能不如前兩者強大。

4. React Spring

優點:

  • 物理模擬動畫:React Spring 以物理模型為基礎,用彈簧物理模擬來構建動畫,提供比線性時間函數更自然的動畫效果。
  • React 整合良好:它也是專為 React 應用設計,與 React 的 Hook 和動畫狀態的管理深度集成,容易控制狀態變化中的動畫。
  • 靈活的 API:可以根據不同場景靈活選擇基於物理的動畫模型或傳統過渡動畫。

缺點:

  • 學習成本:對於需要精確控制的動畫,React Spring 的設置和配置相對複雜,對初學者來說學習成本較高。
  • 缺乏拖動、手勢等功能:相比 Framer Motion,React Spring 對於複雜的互動和手勢控制沒有專門的支援。
Framer Motion 的優勢在於它與 React 的深度整合、易於使用的 API 以及對複雜交互動畫的支持,非常適合需要打造高度互動、動態 UI 的 React 應用。GSAP 則適合需要實現複雜動畫且希望跨平台使用的場景,而 Anime.js 和 React Spring 則適合輕量或物理模擬動畫需求的項目。

總結比較如下表 : 




學 Framer Motion 得先學 React 累累, 似乎輕量的 Anime.js 較合我胃口. 不過我也只是偶然看到透過 AI 了解一下而已, 還真沒時間學, 先記著. 


2024年9月26日 星期四

露天購買 Lolin ESP32 開發板 x 3

今天找到另一款 ESP32 開發板 : LOLIN D32, 此板特點是有鋰電池插座且內建鋰電池充電電路, 當 Micro USB 插上電源時會自動向鋰電池充電, 無電源時則由鋰電池供電, 故可搭配太陽能板在有陽光時由太陽能板供電, 無陽光時則吃鋰電池, 適合用在自供電專案. 





全家取貨付款免運 477 元

買完才發現有更便宜的賣家哈哈 : 


LOLIN 也有 mini 板 :


2024年9月25日 星期三

MicroPython 學習筆記 : xtools 模組改版備忘錄

今天總結過去幾天的測試結果修改 xtools 函式庫, 主要是修改了 line_msg() 函式, 它原本依賴於自訂的 xrequests 模組的 post() 函式來傳送 Line Notify 訊息, 由於此 post() 函式又依賴於 ussl 模組, 但是 ussl 已經在 MicroPython v1.23 版韌體中被移除, 導致在 v1.23 版中呼叫 line_msg() 函式會出現找不到 ussl 之錯誤.

為了解決 MicroPython v1.23 版移除 ussl 所造成之問題, 我借助 ChatGPT 幫忙修改程式碼, 不再依賴自訂的 xrequests 模組, 而是改用內建 urequests.post() 傳遞以經過 utf-8 編碼之 URL 參數, 測試可順利傳送 Line Notify 訊息, 參考 :


主要修改項目如下 : 


1. 新增 urlencode(params) 函式 : 

此函式是 ChatGPT 提供, 用來將參數字典轉成 URL 字串, 以便 line_msg() 呼叫 urequests.post() 時傳入 data 參數內 (需用 utf-8 編碼為 bytes 類型). 

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


2. 修改 line_msg() 函式 :     

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()  # 關閉連線


3. 新增了傳送貼圖的 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."


4. 新增傳送雲端圖片的 line_image_url() 函式 :     

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."


5. 新增 ask_gpt() 函式 :     

此函式需要添加匯入 ujson 模組 :

import ujson 

def ask_gpt(prompt, api_key, model='gpt-4o-mini'):
    url='https://api.openai.com/v1/chat/completions'
    headers = {
        'Content-Type': 'application/json',  
        'Authorization': f'Bearer {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()  # 返回錯誤信息


6. 修改 connect_wifi_led() 為 connect_wifi() 函式 :     

此函式名稱太長, 所以我把最後面的 led 拿掉, 函式內容不變. 


這樣修改後 xtools.py 中只剩下 webhook_post() 函式有用到 xrequests.py 模組的 post() 函式, 因為還未測試其用途, 不知是否能用 urequests.post() 取代, 故仍保留不改, 但把原先的 from xrequests import post 寫法改成 import xrequests :

import xrequests

def webhook_post(url, value):
    print("invoking webhook")
    r = xrequests.post(url, data=value)
    if r is not None and r.status_code == 200:
        print("Webhook invoked")
    else:
        print("Webhook failed")
        show_error()

這樣比較清楚這個 post() 函式是來自 xrequests. 


7. 修改 config.py :     

除了連線 WiFi 基地台的帳密外, 添加了 LINE_NOTIFY_TOKEN 與 OPENAI_API_KEY : 

SSID = "TonyS24U"          
PASSWORD = "123456"
LINE_NOTIFY_TOKEN="在此填入 Line Notify 權杖"
OPENAI_API_KEY="在此填入 OpenAI 金鑰"

測試結果 :

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

>>> import xtools   
>>> import config   
>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)   
>>> token=config.LINE_NOTIFY_TOKEN   
>>> message='test'   
Connecting to network...
network config: ('192.168.192.58', '255.255.255.0', '192.168.192.92', '192.168.192.92')
>>> xtools.line_msg(token, message)   
Message has been sent.
>>> xtools.line_sticker(token, message, 1, 4)   
'The sticker has been sent.'
>>> 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.'

結果如下 :




以下是呼叫 ask_gpt() 測試 : 

>>> prompt='Who are you'   
>>> 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 tasks by providing information and generating text based on the input I receive. How can I help you today?'

中文提示詞時傳回值是 \u 開頭的 unicode :

>>> prompt='你是誰?'    
>>> xtools.ask_gpt(prompt, openai_api_key)    
'\u6211\u662f\u4e00\u4e2a\u4eba\u5de5\u667a\u80fd\u52a9\u624b\uff0c\u65e8\u5728\u56de\u7b54\u95ee\u9898\u3001\u63d0\u4f9b\u4fe1\u606f\u548c\u5e2e\u52a9\u7528\u6237\u89e3\u51b3\u95ee\u9898\u3002\u6709\u4ec0\u9ebc\u6211\u53ef\u4ee5\u5e6b\u52a9\u4f60\u7684\u55ce\uff1f'

用 print() 輸出就會解碼為中文字元 :

>>> print(xtools.ask_gpt(prompt, openai_api_key))      
我是ChatGPT,一個由OpenAI開發的人工智慧語言模型。我的目的是幫助回答問題、提供資訊和進行對話。你有什麼想知道的嗎?

Unicode 可直接傳給 Line Notify : 

>>> reply=xtools.ask_gpt(prompt, openai_api_key)      
>>> xtools.line_msg(token, reply)     
Message has been sent.




更新後的 xtools.py (v1.23 韌體適用) 網址不變 :


更新後的函式庫壓縮檔網址不變 :


舊的函式庫壓縮檔 (v1.23 前版本之韌體適用) 網址如下 : 



2024-10-05 補充 :

以上針對 v1.23 韌體改版的 xtools 只能用在 ESP32 開發板, 在 ESP8266 板載入 OK, 但執行 line_msg() 等呼叫 urequests.post() 之函式時都會自動 Reset 而無法使用, ESP8266 開發板只能在 v1.23 版之前的韌體上用 xrequests.post() 呼叫 line_msg() 而已. 如果要在 v1.23 韌體上呼叫 line_msg(), line_sticker(), line_image_url() 與 ask_gpt(), 必須使用 ESP32 開發板. 


2024-10-08 補充 :

昨天測試 LOLIN D32 ESP32 開發板時發現此板的板上藍色 LED 是 GPIO5, 因此呼叫 connect_wifi() 時 LED 不會閃, 為了適應各開發板 LED 接腳不同, 修改 connect_wifi() 加入一個關鍵字參數 led 預設為 2 : 

def connect_wifi(ssid=config.SSID, passwd=config.PASSWORD, led=2, timeout=20):
    wifi_led=Pin(led, Pin.OUT, value=1)
    sta=network.WLAN(network.STA_IF)
    sta.active(True)
    start_time=time.time() # 記錄時間判斷是否超時
    if not sta.isconnected():
        print("Connecting to network...")
        sta.connect(ssid, passwd)
        while not sta.isconnected():
            wifi_led.value(0)
            time.sleep_ms(300)
            wifi_led.value(1)
            time.sleep_ms(300)
            # 判斷是否超過timeout秒數
            if time.time()-start_time > timeout:
                print("Wifi connecting timeout!")
                break
    if sta.isconnected():
        for i in range(25):   # 連線成功 : 快閃 5 秒
            wifi_led.value(0)
            time.sleep_ms(100)
            wifi_led.value(1)
            time.sleep_ms(100)
        print("network config:", sta.ifconfig())
        return sta.ifconfig()[0] 

若 LED 不是預設 GPIO2, 則呼叫時傳入 led 參數指定即可 :

MicroPython v1.23.0 on 2024-06-02; Generic ESP32 module with ESP32

Type "help()" for more information.

>>> import config   
>>> import xtools   
>>> xtools.connect_wifi(led=5)   
Connecting to network...
network config: ('192.168.192.225', '255.255.255.0', '192.168.192.92', '192.168.192.92')
'192.168.192.225'

測試 OK, 已更新 GitHub. 

MicroPython 學習筆記 : 非 ASCII 字元自動轉成 Unicode 問題

這幾天在修改 xtools 模組以便能在 v1.23 版韌體中能正常發送 Line 訊息時, 發現在 MicroPython 中中文字元一律會被轉成 \u 開頭的 Unicode 字元, 例如 : 

MicroPython v1.23.0 on 2024-06-02; Generic ESP32 module with ESP32
Type "help()" for more information.
>>> v1=100   
>>> v2='您好'    
>>> params={'v1': v1, 'v2': v2}   
>>> ['{}={}'.format(k, v) for k, v in params.items()]    
['v1=100', 'v2=\u60a8\u597d']    

但是我在 PC 的標準 Python 上測試卻不會被轉成 Unicode, 而是顯示原來的中文 :

Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> v1=100   
>>> v2='您好'    
>>> params={'v1': v1, 'v2': v2}   
>>> ['{}={}'.format(k, v) for k, v in params.items()] 
['v1=100', 'v2=您好']      

好奇怪, 詢問 ChatGPT 才知道這其實是 Python 執行環境版本的問題, 不同的 Python 的內建字串顯示函式 repr() 會考量裝置的資源限制 (主要是記憶體) 而有不同的實作 : MicroPython 是在資源有限的嵌入式裝置上運行, 它會將非 ASCII 字元自動轉換為 Unicode 以保字串的輸出能一致; 但在標準 Python 上則因為有豐富的 Unicode 顯示支援會直接顯示這些非 ASCII 字元而無須轉成 Unicode. 總之, 在 MicroPython 和標準 CPython 的 repr() 函數的行為差異與它們的設計目標和資源限制有關。

以下摘錄自 ChatGPT 回覆 : 

問 : 為何標準的 CPython 的 repr() 不轉成 unicode, 而 MicroPython 的卻自動轉換?  

1. MicroPython 的設計考量

  • 資源受限的環境:MicroPython 專為資源有限的嵌入式系統(如 ESP32、ESP8266)設計,內存、存儲等資源相對較少。因此,MicroPython 對字串處理的實現更加簡化。
  • repr() 的簡化處理:在 MicroPython 中,repr() 的行為被簡化,非 ASCII 字符會自動被轉換為 Unicode 轉義序列(\uXXXX),以便在資源受限的環境中正確處理和顯示非 ASCII 字符。這樣做的好處是確保在不同的終端環境中,即使終端不支援中文或其他特殊字符,也能看到唯一標識符來代表這些字符。
  • 一致性與兼容性:通過將所有非 ASCII 字符轉換為 Unicode 編碼,MicroPython 保證在嵌入式系統中的輸出行為更加一致。這樣的設計可以避免一些特殊字符在不同終端或顯示器上產生不預期的行為。

2. CPython 的設計考量

  • 桌面和服務器環境:標準 CPython 針對的是桌面、服務器等更高資源的環境。這些環境中,內存、存儲、顯示器等資源通常不會像嵌入式系統那樣受限,因此 CPython 可以直接顯示 Unicode 字符而不需要轉義。
  • repr() 的行為:在 CPython 中,repr() 的主要目的是提供一個開發者可以識別並重現的物件表示,但它不會自動將所有非 ASCII 字符轉換為 Unicode 編碼。當字串中包含可打印的非 ASCII 字符(如中文),CPython 的 repr() 會直接顯示這些字符,而不是將它們轉換為轉義序列。
  • Python 3 的 Unicode 支持:Python 3 本身對 Unicode 有更好的支持,所有字串都是 Unicode 字符串。CPython 的環境資源豐富,因此可以直接顯示包含非 ASCII 字符的字串,並且這樣做在大多數情況下更符合開發者的期望。

差異總結:

  • MicroPython:由於內存和處理能力受限,對於字串的表示會更保守,因此會將非 ASCII 字符自動轉換為 Unicode 轉義序列。這樣可以保證嵌入式系統上字串的輸出更加一致,並且避免潛在的顯示或處理問題。
  • CPython:資源充裕,並且對於字串處理有強大的 Unicode 支持。因此,當使用 repr() 時,如果字串包含可顯示的非 ASCII 字符(如中文),它會直接顯示這些字符,而不進行轉義。
原來如此, 真是長知識了. 

2024年9月23日 星期一

露天購買 ESP32E 開發板 (4MB Flash) x 3

因為零件箱只找到兩片 4MB 的 ESP32, 為了補庫存上露天找到下面這個賣家, 本來想買一片 179 元的便宜 ESP32-WROOM, 最後還是買了標示品質佳但貴了 50 元的 ESP32E 3 片 : 





全家取貨付款免運 687 元. 


2024-09-28 補充 :

今天拿一片來燒錄 MicroPython v1.23 測試正常 (晶片是 ESP32-D0WD-V3) : 

D:\ESP32>esptool --port COM6 flash_id    
esptool.py v4.6.2
Serial port COM6
Connecting.......
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting......
Detecting chip type... ESP32
Chip is ESP32-D0WD-V3 (revision v3.0)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: c0:49:ef:b2:ba:60
Uploading stub...
Running stub...
Stub running...
Manufacturer: 0b
Device: 4016
Detected flash size: 4MB
Hard resetting via RTS pin...

D:\ESP32>esptool --chip esp32 --port COM6 erase_flash    
esptool.py v4.6.2
Serial port COM6
Connecting....
Chip is ESP32-D0WD-V3 (revision v3.0)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: c0:49:ef:b2:ba:60
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 9.9s
Hard resetting via RTS pin...

D:\ESP32>esptool --chip esp32 --port COM6 write_flash -z 0x1000 ESP32_GENERIC-20240602-v1.23.0.bin   
esptool.py v4.6.2
Serial port COM6
Connecting.......
Chip is ESP32-D0WD-V3 (revision v3.0)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: c0:49:ef:b2:ba:60
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00001000 to 0x001a8fff...
Compressed 1734240 bytes to 1142447...
Wrote 1734240 bytes (1142447 compressed) at 0x00001000 in 101.2 seconds (effective 137.1 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

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)
token=config.LINE_NOTIFY_TOKEN
openai_api_key=config.OPENAI_API_KEY
message='test'
prompt='Who are you'
Connecting to network...
network config: ('192.168.50.3', '255.255.255.0', '192.168.50.1', '192.168.50.1')
>>> xtools.line_msg(token, message)      
Message has been sent.
>>> xtools.line_sticker(token, message, 1, 4)    
'The sticker has been sent.'
>>> 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.'
>>> xtools.ask_gpt(prompt, openai_api_key)      
[object_link_for_thonny=1073573008]'I am an AI language model created by OpenAI, designed to assist with a wide range of questions and tasks. My purpose is to provide information, answer questions, generate text based on prompts, and engage in conversation on various topics. How can I assist you today?'
>>> prompt='你是誰?'   
>>> print(xtools.ask_gpt(prompt, openai_api_key))    
我是一個人工智慧助手,旨在幫助您解答問題和提供資訊。如果您有任何疑問或需要幫助的地方,隨時可以問我!

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 :