2023年10月19日 星期四

ESP32CAM Four-in-one function

Purpose:

結合之前利用ESP32CAM所做的一些功能, 如Line notify, PIR detect, MQTT, Web Stream等等. 把這些ESP32CAM的code寫在一起方便以後maintain.

Combined with some functions previously project done using ESP32CAM, such as Line notify, PIR detect, MQTT, Web Stream, etc. Write these ESP32CAM codes together for easy maintenance in the future.

之前的文章列表(previously project list):

ESP32CAM PIR DETECTION WITH LINE Notify

ESP32-CAM video streaming with Node Red

ESP32 MQTT Publish/Subscribe DS18B20 Temperature to Node-Red

ESP32-CAM Video Stream by C#

Fundamental:

ESP32CAM circuit




Arduino IDE2

MQTT

請參照之前文章ESP32 MQTT Publish/Subscribe DS18B20 Temperature to Node-Red



Web-Stream Function:

請參照之前文章 ESP32-CAM Video Stream by C#

PIR Detect to Linenotify:

請參照之前文章ESP32CAM PIR DETECTION WITH LINE Notify

YouTubeDemo:


ESP32CAM Code:

#include <PubSubClient.h>
#include <HTTPClient.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "soc/soc.h"             //用於電源不穩不重開機
#include "soc/rtc_cntl_reg.h"    //用於電源不穩不重開機
#include "esp_camera.h"          //視訊函式

//ESP32-CAM 安信可模組腳位設定
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22
// Enter your WiFi ssid and password
const char* ssid     = "your network SSID";   //your network SSID
const char* password = "your network password";   //your network password

String lineNotifyToken = "Your Totken";    //Line Notify Token
//----MQTT--------
//------ 以下修改成你MQTT設定 ------
char* MQTTServer = "mqttgo.io";
int MQTTPort = 1883;//MQTT Port
char* MQTTUser = "";//
char* MQTTPassword = "";//
//-----publish-----
char* MQTTTopicPic1  = "yourpublish/pic";  
//subscribe燈號
char* MQTTSubTopic1 = "yoursubscribe/led";
char* MQTTSubTopic2 = "yoursubscribe/flash";
long MQTTLastPublishTime;//此變數用來記錄推播時間
long MQTTPublishInterval = 300;//每0.5秒推撥一次

WiFiClient WifiClient;
PubSubClient MQTTClient(WifiClient);
//--------------------------------------------------------------------------
#define LED_IN 33
const int Led_Flash = 4;
const int Led_run = 13;
int PIR_Sensor = 12;
boolean startTimer = false;
unsigned long time_now=0;
int time_capture=0;
int wifi_Flag=0;
int wifi_cnt=0;
int MqttFlag=0;
int LinenotifyFlag=0;

#define CAMERA_MODEL_AI_THINKER // Has PSRAM
void startCameraServer();

void setup()
{
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  //關閉電源不穩就重開機的設定
   
  Serial.begin(9600);
  Serial.setDebugOutput(true);  //開啟診斷輸出
  Serial.println();
  setupCam();

  pinMode(Led_Flash, OUTPUT);
  pinMode(Led_run, OUTPUT);
  pinMode(LED_IN, OUTPUT);
  pinMode(PIR_Sensor, INPUT_PULLDOWN);

  //閃光燈(GPIO4)
  ledcAttachPin(Led_Flash, 4);  
  ledcSetup(Led_Flash, 5000, 8);
 
  WiFi.mode(WIFI_AP_STA);  //其他模式 WiFi.mode(WIFI_AP); WiFi.mode(WIFI_STA);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
    wifi_cnt++;
    Serial.println(wifi_cnt);
    if(wifi_cnt==20)
    {
      //change another AP
      WiFi.begin("ras_2.4", "181415181415");
      while (WiFi.status() != WL_CONNECTED)
      {
        delay(500);
        Serial.print(".");
      }
    }
    if (WiFi.status() == WL_CONNECTED) {    //若連線成功
      Serial.println("");
      Serial.println("STAIP address: ");
      Serial.println(WiFi.localIP());
      Serial.println("");

      wifi_Flag=1;
      startCameraServer();
      break;
    }
  }
 
  digitalWrite(Led_Flash, LOW);
 
}

void loop()
{
  int v = digitalRead(PIR_Sensor);
  if (v == 1) {
    //傳照片
    String result = SendImageMQTT();
    Serial.println(result);
    ledcWrite(4,10);
    delay(1000);
    Serial.println("starting to Line");
    sendCapturedImage2LineNotify(lineNotifyToken);
    ledcWrite(4,0);
  }
  if(wifi_Flag==1)
  {
    digitalWrite(LED_IN, LOW);
    delay(200);
    digitalWrite(LED_IN, HIGH);
    delay(200);
  }
  if (!MQTTClient.connected())
  {
    MQTTConnecte();
  }
  if ((millis() - MQTTLastPublishTime) >= MQTTPublishInterval )
  {
    //String result = SendImageMQTT();
    //Serial.println(result);
    MQTTLastPublishTime = millis(); //更新最後傳輸時間
  }
  MQTTClient.loop();//更新訂閱狀態
  delay(100);  //You could only send up to 50 images to Line Notify in one hour.
}

//鏡頭設定
void setupCam() {  
  // #define CAMERA_MODEL_AI_THINKER
  //視訊組態設定  https://github.com/espressif/esp32-camera/blob/master/driver/include/esp_camera.h
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  if(psramFound()){  //是否有PSRAM(Psuedo SRAM)記憶體IC
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  //視訊初始化
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    ESP.restart();
  }

  //可自訂視訊框架預設大小(解析度大小)
  sensor_t * s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1); // flip it back
    s->set_brightness(s, 1); // up the brightness just a bit
    s->set_saturation(s, -2); // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_SVGA);    //解析度 UXGA(1600x1200), SXGA(1280x1024), XGA(1024x768), SVGA(800x600), VGA(640x480), CIF(400x296), QVGA(320x240), HQVGA(240x176), QQVGA(160x120), QXGA(2048x1564 for OV3660)

  //s->set_vflip(s, 1);  //垂直翻轉
  //s->set_hmirror(s, 1);  //水平鏡像
  //s->set_pRotation(s, 1);  //0=不旋轉 1-轉90度 2- 轉180度 3-轉270
}

String sendCapturedImage2LineNotify(String token) {
  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
    delay(1000);
    ESP.restart();
    return "Camera capture failed";
  }
   
  WiFiClientSecure client_tcp;  //啟動SSL wificlient
  client_tcp.setInsecure();   //run version 1.0.5 or above
  Serial.println("Connect to notify-api.line.me");
  if (client_tcp.connect("notify-api.line.me", 443)) {
    Serial.println("Connection successful");
   
    String message = "ESP32-CAM";
    String head = "--Taiwan\r\nContent-Disposition: form-data; name=\"message\"; \r\n\r\n" + message + "\r\n--Taiwan\r\nContent-Disposition: form-data; name=\"imageFile\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
    String tail = "\r\n--Taiwan--\r\n";

    uint16_t imageLen = fb->len;
    uint16_t extraLen = head.length() + tail.length();
    uint16_t totalLen = imageLen + extraLen;
    //開始POST傳送訊息
    client_tcp.println("POST /api/notify HTTP/1.1");
    client_tcp.println("Connection: close");
    client_tcp.println("Host: notify-api.line.me");
    client_tcp.println("Authorization: Bearer " + token);
    client_tcp.println("Content-Length: " + String(totalLen));
    client_tcp.println("Content-Type: multipart/form-data; boundary=Taiwan");
    client_tcp.println();
    client_tcp.print(head);
   
    uint8_t *fbBuf = fb->buf;
    size_t fbLen = fb->len;
    Serial.println("Data Sending....");
    //檔案太大,分段傳送
    for (size_t n=0;n<fbLen;n=n+1024) {
      if (n+1024<fbLen) {
        client_tcp.write(fbBuf, 1024);
        fbBuf += 1024;
      }
      else if (fbLen%1024>0) {
        size_t remainder = fbLen%1024;
        client_tcp.write(fbBuf, remainder);
      }
    }  
   
    client_tcp.print(tail);
    client_tcp.println();
    esp_camera_fb_return(fb);

    String getResponse="",Feedback="";
    int waitTime = 10000;   // timeout 10 seconds
    long startTime = millis();
    boolean state = false;
   
    while ((startTime + waitTime) > millis()) {
      Serial.print(".");
      delay(100);      
      while (client_tcp.available()) {  //當有收到回覆資料時
          char c = client_tcp.read();
          if (state==true) Feedback += String(c);        
          if (c == '\n') {
            if (getResponse.length()==0) state=true;
            getResponse = "";
          }
          else if (c != '\r')
            getResponse += String(c);
          startTime = millis();
       }
       if (Feedback.length()>0) break;
    }
    Serial.println();
    client_tcp.stop();
    return Feedback;
  }
  else {
    return "Connected to notify-api.line.me failed.";
  }
}

String sendRequest2LineNotify(String token, String request) {
  request.replace("%","%25");
  request.replace(" ","%20");
  //request.replace("&","%20");
  request.replace("#","%20");
  //request.replace("\'","%27");
  request.replace("\"","%22");
  request.replace("\n","%0D%0A");
  request.replace("%3Cbr%3E","%0D%0A");
  request.replace("%3Cbr/%3E","%0D%0A");
  request.replace("%3Cbr%20/%3E","%0D%0A");
  request.replace("%3CBR%3E","%0D%0A");
  request.replace("%3CBR/%3E","%0D%0A");
  request.replace("%3CBR%20/%3E","%0D%0A");
  request.replace("%20stickerPackageId","&stickerPackageId");
  request.replace("%20stickerId","&stickerId");    

  WiFiClientSecure client_tcp;
  client_tcp.setInsecure();   //run version 1.0.5 or above
 
  Serial.println("Connect to notify-api.line.me");  
 
  if (client_tcp.connect("notify-api.line.me", 443)) {
    Serial.println("Connection successful");
       
    Serial.println(request);    
    client_tcp.println("POST /api/notify HTTP/1.1");
    client_tcp.println("Connection: close");
    client_tcp.println("Host: notify-api.line.me");
    client_tcp.println("User-Agent: ESP8266/1.0");
    client_tcp.println("Authorization: Bearer " + token);
    client_tcp.println("Content-Type: application/x-www-form-urlencoded");
    client_tcp.println("Content-Length: " + String(request.length()));
    client_tcp.println();
    client_tcp.println(request);
    client_tcp.println();
   
    String getResponse="",Feedback="";
    boolean state = false;
    int waitTime = 3000;   // timeout 3 seconds
    long startTime = millis();
    while ((startTime + waitTime) > millis()) {
      Serial.print(".");
      delay(100);      
      while (client_tcp.available()) {
          char c = client_tcp.read();
          if (state==true) Feedback += String(c);        
          if (c == '\n') {
            if (getResponse.length()==0) state=true;
            getResponse = "";
          }
          else if (c != '\r')
            getResponse += String(c);
          startTime = millis();
       }
       if (getResponse.length()>0) break;
    }
    Serial.println();
    client_tcp.stop();
    return Feedback;
  }
  else
    return "Connection failed";  
}

//開始MQTT連線伺服器
void MQTTConnecte()
{
  MQTTClient.setServer(MQTTServer, MQTTPort);
  MQTTClient.setCallback(MQTTCallback);
  while (!MQTTClient.connected()) {
    //以亂數為ClientID
    String  MQTTClientid = "esp32-" + String(random(1000000, 9999999));
    if (MQTTClient.connect(MQTTClientid.c_str(), MQTTUser, MQTTPassword)) {
      //連結成功,顯示「已連線」。
      Serial.println("MQTT已連線");
      //訂閱SubTopic1主題
      MQTTClient.subscribe(MQTTSubTopic1);
      MQTTClient.subscribe(MQTTSubTopic2);
    } else {
      //若連線不成功,則顯示錯誤訊息,並重新連線
      Serial.print("MQTT連線失敗,狀態碼=");
      Serial.println(MQTTClient.state());
      Serial.println("五秒後重新連線");
      delay(5000);
    }
  }
}

//拍照傳送到MQTT
String SendImageMQTT()
{
  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("MQTT capture failed");
    delay(1000);
    ESP.restart();
    return "MQTT capture failed";
  }
  size_t fbLen = fb->len;
  //int ps = 512;
  //開始傳遞影像檔,批次傳黨
  MQTTClient.beginPublish(MQTTTopicPic1, fbLen, false);
  uint8_t *fbBuf = fb->buf;
  for (size_t n = 0; n < fbLen; n = n + 2048) {
    if (n + 2048 < fbLen) {
      MQTTClient.write(fbBuf, 2048);
      fbBuf += 2048;
    } else if (fbLen % 2048 > 0) {
      size_t remainder = fbLen % 2048;
      MQTTClient.write(fbBuf, remainder);
    }
  }
  boolean isPublished = MQTTClient.endPublish();
  esp_camera_fb_return(fb);//清除緩衝區
  if (isPublished) {
    return "MQTT傳輸成功";
  }
  else {
    return "MQTT傳輸失敗,請檢查網路設定";
  }
}

//接收到訂閱時
void MQTTCallback(char* topic, byte* payload, unsigned int length)
{
  Serial.print(topic); Serial.print("訂閱通知:");
  String payloadString;//將接收的payload轉成字串
  //顯示訂閱內容
  for (int i = 0; i < length; i++) {
    payloadString = payloadString + (char)payload[i];
  }
  Serial.println(payloadString);
  //比對主題是否為訂閱主題1
  if (strcmp(topic, MQTTSubTopic1) == 0)
  {
    Serial.println("LED:" + payloadString);
    if (payloadString == "ON")
    {
      digitalWrite(16, HIGH);
    }
    if (payloadString == "OFF")
    {
      digitalWrite(16, LOW);
    }
  }
  //比對主題是否為訂閱主題1
  if (strcmp(topic, MQTTSubTopic2) == 0) {
    Serial.println("Flash:" + payloadString);
    if (payloadString == "ON")
    {
      ledcWrite(4,10);
      delay(1000);
      String result = SendImageMQTT();
      Serial.println(result);
      delay(1000);
      Serial.println("starting to Line");
      sendCapturedImage2LineNotify(lineNotifyToken);
      for (int i=0;i<3;i++) {  
        ledcWrite(4,10);
        delay(200);
        ledcWrite(4,0);
        delay(200);    
      }
    }
    if (payloadString == "OFF")
    {
      ledcWrite(4,0);
    }
  }
}


沒有留言:

張貼留言