延續上一篇貼文,本文將介紹觸發ESP32-CAM開發板拍照之後,把影像附加在e-mail中傳遞的程式寫法。
筆者把寄送郵件的程式寫成名叫mailPhoto()的函式,底下的版本將寄送HTML格式的訊息,函式開頭是令ESP32-CAM執行拍攝影像的敘述,相關說明請參閱「ESP32-CAM開發板(二):esp32-camera程式庫與縮時攝影程式」貼文。
void mailPhoto() { camera_fb_t *fb = esp_camera_fb_get(); // 拍照、將影像存入變數fb if (!fb) { Serial.println("無法取得影像資料…"); delay(1000); ESP.restart(); // 重新啟動 } Serial.println("準備寄送郵件…"); ESP_Mail_Session session; // 宣告SMTP郵件伺服器連線物件 /* 設定SMTP郵件伺服器連線物件 */ session.server.host_name = SMTP_HOST; // 設定寄信的郵件伺服器名稱 session.server.port = SMTP_PORT; // 郵件伺服器的埠號 session.login.email = AUTHOR_EMAIL; // 你的帳號 session.login.password = AUTHOR_PASSWORD; // 密碼 /* 設置時區 */ session.time.ntp_server = F("pool.ntp.org,time.nist.gov"); session.time.gmt_offset = 8; // 台北時區 session.time.day_light_offset = 0; // 無日光節約時間 SMTP_Message message; // 宣告SMTP訊息物件 /* 設定郵件標頭 */ message.sender.name = SENDER_NAME; // 寄信人的名字 message.sender.email = AUTHOR_EMAIL; // 寄信人的e-mail message.subject = MAIL_SUBJECT; // 信件主旨 message.addRecipient(RECIPIENT, RECIPIENT_EMAIL); // "收信人的名字", "收信人的e-mail" // 設定郵件內容(HTML格式訊息) String htmlMsg = "<div style=\"color:#ff3300\"">"<h1>鐵證如山!"</h1>"<p>- 從ESP32開發板傳送"</p>"</div>"; message.html.content = htmlMsg.c_str(); // 設定信件內容 message.html.charSet = "utf-8"; // 設定訊息文字的編碼 // 這裡將加入附加影像檔的敘述 if (!smtp.connect(&session)) // 連線到郵件伺服器 return; if (!MailClient.sendMail(&smtp, &message)) // 開始寄信 Serial.println("寄信時出錯了:" + smtp.errorReason()); }
透過SMTP_Attachment物件設定信件的附件
“ESP Mail Client”(ESP郵件用戶端)程式庫定義的SMTP_Attachment類別,用於設定附件檔案的名稱、類型和檔案內容。底下的程式片段宣告一個名叫att的「附件物件」:
SMTP_Attachment att; // 宣告附件物件 att.descr.filename = "photo.jpg"; // 設定附件的檔名 att.descr.mime = "image/jpeg"; // 設定附件的MIME類型(JPEG影像) att.blob.data = fb->buf; // 取得影像檔的資料內容 att.blob.size = fb->len; // 取得影像檔的位元組大小 message.addAttachment(att); // 把附件檔加入信件
如「ESP32-CAM開發板(二):esp32-camera程式庫與縮時攝影程式」貼文的說明,ESP32-CAM拍攝的影像資料和大小,分別存在fb結構體的buf和len成員,所以上面的程式將把拍攝影像附加到信件。
把ESP32-CAM拍攝照片附加到e-mail並傳送的完整程式碼
完整的程式碼如下,請自行修改其中的一些變數值:
#include <WiFi.h> #include <ESP_Mail_Client.h> // 負責寄信的程式庫 #include <esp_camera.h> // ESP32-CAM拍攝影像的程式庫 // 採用Gmail的郵件伺服器 #define SMTP_HOST "smtp.gmail.com" #define SMTP_PORT 587 #define AUTHOR_EMAIL "你的帳號@gmail.com" #define AUTHOR_PASSWORD "上文產生的應用程式密碼" #define SENDER_NAME "寄信人的名字" #define MAIL_SUBJECT "信件的主旨" #define RECIPIENT "收信人的名字" #define RECIPIENT_EMAIL "收信人的e-amil" const char* ssid = "你的網路名稱"; const char* password = "網路密碼"; /* 宣告用於寄信的SMTP Session物件 */ SMTPSession smtp; // 宣告寄信的回呼函式 void smtpCallback(SMTP_Status status); WiFiClient client; 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 mailPhoto() { camera_fb_t *fb = NULL; // 宣告儲存影像結構資料的變數 fb = esp_camera_fb_get(); // 拍照 if (!fb) { Serial.println("無法取得影像資料…"); delay(1000); ESP.restart(); // 重新啟動 } Serial.println("準備寄送郵件…"); ESP_Mail_Session session; // 宣告SMTP郵件伺服器連線物件 /* 設定SMTP郵件伺服器連線物件 */ session.server.host_name = SMTP_HOST; // 設定寄信的郵件伺服器名稱 session.server.port = SMTP_PORT; // 郵件伺服器的埠號 session.login.email = AUTHOR_EMAIL; // 你的帳號 session.login.password = AUTHOR_PASSWORD; // 密碼 /* 設置時區 */ session.time.ntp_server = F("pool.ntp.org,time.nist.gov"); session.time.gmt_offset = 8; // 台北時區 session.time.day_light_offset = 0; // 無日光節約時間 SMTP_Message message; // 宣告SMTP訊息物件 /* 設定郵件標頭 */ message.sender.name = SENDER_NAME; // 寄信人的名字 message.sender.email = AUTHOR_EMAIL; // 寄信人的e-mail message.subject = MAIL_SUBJECT; // 信件主旨 message.addRecipient(RECIPIENT, RECIPIENT_EMAIL); // "收信人的名字", "收信人的e-mail" /* 設定郵件內容(HTML格式訊息) */ String htmlMsg = "<div style=\"color:#ff3300\"><h1>鐵證如山!</h1><p>- 從ESP32開發板傳送</p></div>"; message.html.content = htmlMsg.c_str(); // 設定信件內容 message.html.charSet = "utf-8"; // 設定訊息文字的編碼 SMTP_Attachment att; // 宣告附件物件 att.descr.filename = "photo.jpg"; // 設定附件的檔名 att.descr.mime = "image/jpeg"; // 設定附件的MIME類型(JPEG影像) att.blob.data = fb->buf; // 取得影像檔的資料內容 att.blob.size = fb->len; // 取得影像檔的位元組大小 message.addAttachment(att); // 把附件檔加入信件 if (!smtp.connect(&session)) // 連線到郵件伺服器 return; if (!MailClient.sendMail(&smtp, &message)) // 開始寄信 Serial.println("寄信時出錯了:" + smtp.errorReason()); } 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 } /** 是否在序列埠監控視窗顯示除錯訊息 0代表不要 1代表顯示簡單除錯訊息 */ smtp.debug(1); /* 設定取得寄信狀態的回呼函式 */ smtp.callback(smtpCallback); mailPhoto(); // 寄送拍攝影像 } void loop() { } /* 取得信件寄送狀態的回呼函式本體 */ void smtpCallback(SMTP_Status status) { Serial.println(status.info()); // 顯示目前的狀態 if (status.success()) { // 顯示傳送結果 Serial.println("----------------"); ESP_MAIL_PRINTF("訊息傳送成功:%d\n", status.completedCount()); ESP_MAIL_PRINTF("訊息傳送失敗:%d\n", status.failedCount()); Serial.println("----------------\n"); struct tm dt; for (int i = 0; i < smtp.sendingResult.size(); i++) { SMTP_Result result = smtp.sendingResult.getItem(i); time_t ts = (time_t)result.timestamp; ESP_MAIL_PRINTF("訊息編號:%d\n", i + 1); ESP_MAIL_PRINTF("狀態:%s\n", result.completed ? "成功" : "失敗"); ESP_MAIL_PRINTF("日期/時間:%s\n", asctime(localtime(&ts))); ESP_MAIL_PRINTF("收信人:%s\n", result.recipients.c_str()); ESP_MAIL_PRINTF("主旨:%s\n", result.subject.c_str()); } Serial.println("----------------\n"); smtp.sendingResult.clear(); } }
編譯並上傳程式碼到ESP32-CAM開發板,在Yahoo信箱收到的訊息內容如下:
將ESP32-CAM拍攝的影像插入信件內文
上一節的程式透過message(SMTP訊息)物件addAttachment(直譯為「加入附件」)方法,把影像檔附加在信件末尾,本節將透過「訊息」物件的另一個addInlineImage(直譯為「新增行內影像」)方法,把拍攝到的影像直接顯示在訊息內文,像這樣:
首先要修改訊息內文,在其中加入<img>影像標籤並且選擇性地設定影像的替代(說明)文字以及影像的顯示大小(此例設成400 × 300像素),重點是,影像檔的來源(src)屬性要用“cid:識別名稱”的格式,指定影像來源。
附加檔案的att物件程式,要透過content_id設置自訂的影像識別名稱,此例設成“espCAM”。最後,透過addInlineImage()方法附加「行內影像」。
這裡有一個小細節,e-mail的附件檔案預設採用”7bit”編碼,嵌入信件內文的影像要用“base64”編碼(“base64”字串值可改寫成Content_Transfer_Encoding::enc_base64)。
修改之後的e-mail訊息內容以及附加檔案的程式片段如下:
/* 設定郵件內容(HTML格式訊息) */ String htmlMsg = "<div style=\"color:#ff3300\"><h1>鐵證如山!</h1><img src=\"cid:espCAM\" alt=\"拍到鹹魚犯~\" width=\"400\" height=\"300\"><p>- 從ESP32開發板傳送</p></div>"; message.html.content = htmlMsg.c_str(); // 設定信件內容 message.html.charSet = "utf-8"; // 設定訊息文字的編碼 SMTP_Attachment att; // 宣告附件物件 // 加上附件 att.descr.content_id = "espCAM"; // 設定影像的識別名稱 att.descr.filename = "photo.jpg"; att.descr.mime = "image/jpeg"; att.descr.transfer_encoding = "base64"; // 內文影像要用“base64”編碼 att.blob.data = fb->buf; // 取得影像檔的內容 att.blob.size = fb->len; // 取得影像檔的位元組大小 /* 把影像檔加入信件內文 */ message.addInlineImage(att);
解決Brownout detector was triggered(電力不足)錯誤
筆者在ESP32-CAM開發板上測試寄送相片郵件的程式碼時,發生如下的錯誤訊息,代表ESP32的電力不足,無法正常運作。
總結網路上的各個前輩的經驗,根本的解決方法是確保電力充足,換USB線、換電源供應板、測試時改接電腦的Type-C介面(因為Type-C介面的輸出電流遠大於USB 2.0介面的0.5A,詳閱維基百科的《USB-C條目》)。
另外,根據ESP32晶片原廠樂鑫科技的ESP32硬體瑕疵修正文件指出,第一批生產的ESP32晶片的Brown-out Reset(直譯為「電源低壓重置」)功能不正常,無法在發生電力不足錯誤之後重新啟動。
筆者採用FT232RL USB序列埠轉換板燒錄ESP32-CAM程式(參閱「ESP32-CAM開發板(一):簡介與燒錄程式」貼文),原本使用3.3V供電,後來改在USB插座的5V接點焊接一個排針,提供5V電源給ESP32-CAM開發板,並將它插在筆電的Type-C插孔。
在程式開發測試階段,也可以先關閉ESP32晶片內部的電源低壓偵測功能,方法是引用這兩個程式庫:
#include <soc/soc.h> #include <soc/rtc_cntl_reg.h>
然後在setup()函式開頭輸入「停止低電壓偵測」的敘述,這樣也可以暫時避免引發「電力不足」的錯誤。
void setup() { WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // 停止ESP32晶片內部的低壓偵測功能 : 略 }