延續上一篇貼文,本文將介紹觸發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晶片內部的低壓偵測功能
: 略
}
