本文將編寫一個讓ESP32-CAM定時拍照並上傳影像到網站伺服器的前端應用程式,並且說明背後的HTTP POST標頭訊息格式。你可以自行加上感測電路,讓ESP32-CAM開發板在某個情況下自動觸發、上傳影像,例如,感應到人體紅外線。
開發Arduino HTTP前端應用,例如,對某個網站服務發起GET或POST請求,通常會採用HTTPClient.h程式庫來簡化程式碼,像《超圖解ESP32深度實作》第七章的讀取氣象網站的JSON格式資料應用,我們不用自己編寫HTTP標頭訊息,也無需自行解析HTTP回應。
然而,HTTPClient.h程式庫的POST方法不具備上傳二進位檔案的功能,所以要用底層的WiFiClient.h程式庫傳送自行編寫HTTP標頭訊息。也因此,開發程式之前,必須要先認識夾帶二進位檔案的HTTP POST標頭訊息。
傳送二進位檔的HTTP POST標頭訊息格式
假設從用戶端(ESP32-CAM控制板)上傳影像檔給位於192.168.0.13的網站伺服器,接收與處理上傳影像的路由是“/esp32cam”,其HTTP標頭可大致分成兩個部分,首先,Content-Type欄位後面要附帶boundary(直譯為「分界」)參數,其值為自訂的識別名稱。
透過網頁表單上傳檔案時,「分界」識別名稱由瀏覽器自動產生,名稱最長不超過70個US-ASCII字元(也就是只能用ASCII編碼的前127個英數字和符號)。
緊接著的HTTP標頭第二部分,是夾帶上傳檔案的分界內容,這個部分的位元組大小要附加在上面的Content-Length欄位(此處假設為567890位元組)。
ESP32-CAM的上傳影像程式,將要送出如上述的HTTP POST標頭訊息給網站伺服器。
建立WiFiClient前端物件
Arduino官方的Ethernet.h標頭檔定義了乙太有線網路連線的物件方法(參閱《超圖解Arduino互動設計入門》第17章),而WiFi.h標頭檔則定義了Wi-Fi無線網路連線的物件方法,兩種不同物件的基本操作方法名稱都一樣,例如,初始連線的方法叫做begin()、輸出訊息的方法叫print()。
若用Arduino UNO搭配乙太有線網路擴展板,要透過Ethernet.h程式庫的EthernetClient類別建立前端物件,執行connect()方法即連線遠端伺服器:
#include <Ethernet.h> // 在Arduino UNO引用乙太網路程式庫 : 略 EthernetClient client; // 宣告乙太網路前端物件 : 略 client.connect(server, 80); // 連線到server變數指定的伺服器、80埠 : 略 client.print("hello"); // 透過網路送出訊息給伺服器
在ESP32上使用Wi-Fi連線遠端HTTP伺服器的操作邏輯相同,只是換了程式庫:
#include <WiFi.h> // 內含Wi-Fi網路物件方法以及WiFiClient.h程式庫 : 略 WiFiClient client; // 宣告WiFi網路前端物件 : 略 client.connect(server, 80); // 連線到server變數指定的伺服器、80埠 : 略 client.print("hello"); // 透過網路送出訊息給伺服器
從ESP32開發板傳送自訂的HTTP POST標頭
ESP32 Arduino程式開頭先引用程式庫並且定義幾個常數:
#include <WiFi.h> // 引用Wi-Fi程式庫 #include "esp_camera.h" // 操作ESP32攝像頭的程式庫 #define SERVER "192.168.0.119" // 連線伺服器的IP位址或域名 #define UPLOAD_URL "/esp32cam" // 接收上傳檔的表單處理程式路徑 #define PORT 80 // 網站伺服器的埠號
HTTP標頭是純文字訊息,底下的boundBegin和boundEnd字串變數分別定義了「分界」內容的起始欄位和結尾,上傳影像檔名固定為pict.jpg。
String boundBegin = "--ESP32CAM\r\n"; // 分界內容的開頭 boundBegin += "Content-Disposition: form-data; name=\"filename\"; filename=\"pict.jpg\"\r\n"; boundBegin += "Content-Type: image/jpeg\r\n"; boundBegin += "\r\n"; String boundEnd = "\r\n--ESP32CAM--\r\n"; // 分界內容的結尾
假設ESP32攝像頭的影像儲存在叫做“fb”的結構體,底下的playloadSize變數將紀錄整個「分界」內容的位元組大小。
uint32_t imgSize = fb->len; // 取得影像檔的大小 uint32_t payloadSize = boundBegin.length() + imgSize + boundEnd.length();
取得「分界」內容的大小,就能組成HTTP POST訊息的第一部分:
String httpMsg = String("POST ") + UPLOAD_URL + " HTTP/1.1\r\n"; httpMsg += String("Host: ") + SERVER + "\r\n"; httpMsg += "User-Agent: Arduino/ESP32CAM\r\n"; httpMsg += "Content-Length: " + String(payloadSize) + "\r\n"; httpMsg += "Content-Type: multipart/form-data; boundary=ESP32CAM\r\n"; httpMsg += "\r\n"; httpMsg += boundBegin; // 加上「分界」內容的起始欄位
然後透過前端物件的print()方法傳給伺服器:
client.print(httpMsg.c_str()); // 送出第一部分的HTTP標頭
從ESP32上傳影像檔
實際要上傳的檔案夾在「分界」訊息中間,由於上傳檔的大小可能超過ESP32的記憶體容量,因此檔案以1024位元組(1KB)為單位分批上傳:
// 宣告指向影像檔的指標變數buf uint8_t *buf = fb->buf; // 從影像檔的開頭,每次最多讀取、傳送1024位元組 for (uint32_t i=0; i<imgSize; i+=1024) { if (i+1024 < imgSize) { client.write(buf, 1024); // 傳送1024位元組大小的資料 buf += 1024; // 若剩下不到1024位元組,則一次全部送出。 } else if (imgSize%1024>0) { uint32_t remainder = imgSize%1024; client.write(buf, remainder); } }
檔案上傳完畢,最後再送出「分界」訊息結尾:
client.print(boundEnd.c_str());
接收HTTP網站伺服器的回應
送出HTTP標頭給網站伺服器之後,便能刪除記憶體裡的影像檔:
esp_camera_fb_return(fb);
接著等候來自伺服器的回應,此處設定等待時間最長不超過10秒;若10秒內沒收到伺服器的回應,代表連線有問題或者網站伺服器掛了。
若在等待期間收到伺服器的回應,則讀取第一行回應之後關閉連線,完成上傳檔案。
// 等待伺服器的回應(最長等待10秒) long timout = 10000L + millis(); while (timout > millis()) { // 若尚未超過等待時間… Serial.print("."); // 持續顯示"." delay(100); if (client.available()){ // 若收到伺服器的回應… Serial.println("\n伺服器回應:"); // 讀取一行回應 String line = client.readStringUntil('\r'); Serial.println(line); break; // 跳出while回圈 } } Serial.println("關閉連線"); client.stop();
底下是ESP32CAM控制板連線Wi-Fi網路、拍照、上傳網站伺服器的過程中,在Arduino IDE的「序列埠監控視窗」顯示的訊息:
ESP32CAM定時拍照與上傳網站伺服器的完整前端程式碼
筆者把連線遠端伺服器、上傳影像檔的程式寫在postImage()自訂函式裡面,設定每10秒鐘拍攝、上傳影像。接收上傳影像的HTTP網站伺服器程式碼,請參閱《使用Python Flask建置影像圖檔上傳網站服務(五)》這篇貼文。
#include <WiFi.h> #include "esp_camera.h" #define SERVER "192.168.0.13" // 請改成你的網站伺服器位址或域名 #define UPLOAD_URL "/esp32cam" #define PORT 80 const char* ssid = "你的Wi-Fi網路名稱"; const char* password = "網路密碼"; WiFiClient client; const int timerInterval = 10000; // 上傳影像的間隔毫秒數 unsigned long previousMillis = 0; bool initCamera() { // 設定攝像頭的接腳和影像格式與尺寸 static camera_config_t camera_config = { .pin_pwdn = 32, // 斷電腳 .pin_reset = -1, // 重置腳 .pin_xclk = 0, // 外部時脈腳 .pin_sscb_sda = 26, // I2C資料腳 .pin_sscb_scl = 27, // I2C時脈腳 .pin_d7 = 35, // 資料腳 .pin_d6 = 34, .pin_d5 = 39, .pin_d4 = 36, .pin_d3 = 21, .pin_d2 = 19, .pin_d1 = 18, .pin_d0 = 5, .pin_vsync = 25, // 垂直同步腳 .pin_href = 23, // 水平同步腳 .pin_pclk = 22, // 像素時脈腳 .xclk_freq_hz = 20000000, // 設定外部時脈:20MHz .ledc_timer = LEDC_TIMER_0, // 指定產生XCLK時脈的計時器 .ledc_channel = LEDC_CHANNEL_0, // 指定產生XCLM時脈的通道 .pixel_format = PIXFORMAT_JPEG, // 設定影像格式:JPEG .frame_size = FRAMESIZE_SVGA, // 設定影像大小:SVGA .jpeg_quality = 10, // 設定JPEG影像畫質,有效值介於0-63,數字越低畫質越高。 .fb_count = 1 // 影像緩衝記憶區數量 }; // 初始化攝像頭 esp_err_t err = esp_camera_init(&camera_config); if (err != ESP_OK) { Serial.printf("攝像頭出錯了,錯誤碼:0x%x", err); return false; } return true; } void postImage() { camera_fb_t *fb = NULL; // 宣告儲存影像結構資料的變數 fb = esp_camera_fb_get(); // 拍照 if(!fb) { Serial.println("無法取得影像資料…"); delay(1000); ESP.restart(); // 重新啟動 } Serial.printf("連接伺服器:%s\n", SERVER); if (client.connect(SERVER, PORT)) { Serial.println("開始上傳影像…"); String boundBegin = "--ESP32CAM\r\n"; boundBegin += "Content-Disposition: form-data; name=\"filename\"; filename=\"pict.jpg\"\r\n"; boundBegin += "Content-Type: image/jpeg\r\n"; boundBegin += "\r\n"; String boundEnd = "\r\n--ESP32CAM--\r\n"; uint32_t imgSize = fb->len; // 取得影像檔的大小 uint32_t payloadSize = boundBegin.length() + imgSize + boundEnd.length(); String httpMsg = String("POST ") + UPLOAD_URL + " HTTP/1.1\r\n"; httpMsg += String("Host: ") + SERVER + "\r\n"; httpMsg += "User-Agent: Arduino/ESP32CAM\r\n"; httpMsg += "Content-Length: " + String(payloadSize) + "\r\n"; httpMsg += "Content-Type: multipart/form-data; boundary=ESP32CAM\r\n"; httpMsg += "\r\n"; httpMsg += boundBegin; // 送出HTTP標頭訊息 client.print(httpMsg.c_str()); // 上傳檔案 uint8_t *buf = fb->buf; for (uint32_t i=0; i<imgSize; i+=1024) { if (i+1024 < imgSize) { client.write(buf, 1024); buf += 1024; } else if (imgSize%1024>0) { uint32_t remainder = imgSize%1024; client.write(buf, remainder); } } client.print(boundEnd.c_str()); esp_camera_fb_return(fb); // 等待伺服器的回應(10秒) long timout = 10000L + millis(); while (timout > millis()) { Serial.print("."); delay(100); if (client.available()){ // 讀取伺服器的回應 Serial.println("\n伺服器回應:"); String line = client.readStringUntil('\r'); Serial.println(line); break; } } Serial.println("關閉連線"); } else { Serial.printf("無法連接伺服器:%s\n", SERVER); } client.stop(); // 關閉用戶端 } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.println("\n\n連接Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.print("\nIP位址:"); Serial.println(WiFi.localIP()); bool cameraReady = initCamera(); if (!cameraReady) { Serial.println("攝像頭出錯了…"); delay(1000); ESP.restart(); // 重新啟動ESP32 } postImage(); // 上傳影像 } void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= timerInterval) { postImage(); // 上傳影像 previousMillis = currentMillis; } }
Hi,作者您好,參考您的文章成功在linux上架出flask去接收esp32cam的圖片,但因專案需求需同時post esp32cam上外接的DHT11之溫溼度,修改了一下flask並用html測試,flask可正常接收圖片+其他變數,但嘗試修改esp32cam的code後,flask一直無法成功接收esp32cam post過去的東西(顯示Invalid request from ip=xxxx: Invalid HTTP request line: ‘\x03\x00\x00/*à\x00\x00\x00\x00\x00Cookie: mstshash=Administr’
ImmutableMultiDict([])
{‘msg’: ‘no_file’}
),想請問您有沒有做過同時post照片和變數的做法呢?
如果是在Flask接收單一欄位,要用request.form[“欄位名稱”],
若是接收包含多個選項的欄位值,要用request.form.getlist(“欄位名稱”)。
承上篇多變數post問題,老師我的flask是這樣寫沒錯!也可正確從html接收圖片檔案+其他變數,問題是我不知道esp32cam這邊的code要如何修改~
直接把你要傳給後端的資料值加入POST訊息字串即可,假設傳遞data參數,其值為”hello”,POST訊息可這樣改寫: