気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP32-CAM/deep sleep/PIR/タクトスイッチ/ext1復帰で映像表示

ホーム前へ次へ
ESP8266って?

ESP32-CAM/deep sleep/PIR/タクトスイッチ/ext1復帰で映像表示

ESP32-CAM/deep sleep/PIR/タクトスイッチ/ext1復帰で映像表示

2021/10/11
ESP32-CAM+PIRセンサー+タクトスイッチext1ディープスリープ復帰回路

 Wi-Fi(wifi)モジュールESP8266/ESP32のバリエーションでカメラ付きESP32-SベースのESP32-CAMボードで電池駆動も見据えディープスリープしつつ、PIR/人感センサーHC-SR501による動体検知とタクトスイッチ(タクタイルスイッチ)押下双方を検知時に人感センサー検知中の間、ストリーミング再生してみました。

 昨日、これが上手くいかず、人感センサーのみの検知の記事を書いたのですが、各種エラーやマイコン再起動の連発など上手く行かなかった原因が全て配線の接触不良であることが判明したところでタクトスイッチを追加した複数GPIOを条件とした外部復帰となる、元々予定していた、この記事を書くことに。

 これは、ドアフォンやそれに類する機器の自作を目的とした途中経過に過ぎず、タクトスイッチは、その際のボタンスイッチをイメージしています。

 余計な配線が写っていますが、実行時は、ESP32-CAMの配線は、5V/GNDと信号線2本の4本のみでOKです。

材料・段取り・回路から実行まで

 材料と準備、プログラムのアップ、実行については、ESP32-CAM/deep sleep/PIRセンサー/ext0復帰で映像表示にある通り。

スケッチ

 今回は、PIR・人感・焦電赤外線センサーとタクトスイッチの複数のGPIOが復帰条件となるのでext1を使いました。

 RAMの内、グローバル変数の使用は18%である一方、フラッシュメモリの内、スケッチが85%を占めているのが気にかかりますが、ちゃんと機能はしています。

// ESP32-CAM+PIRセンサー+タクトスイッチでディープスリープから復帰
#include "esp_camera.h"
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <ESPmDNS.h>
#include <Arduino.h>
#include <FS.h>
#include "soc/soc.h"     // Disable brownout problems
#include "soc/rtc_cntl_reg.h" // Disable brownout problems
 
RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保
 
// https://randomnerdtutorials.com/esp32-external-wake-up-deep-sleep/
// 2^13+2^2 in hex GPIO13 + GPIO2
#define BUTTON_PIN_BITMASK 0x2004
 
//
// WARNING!!! Make sure that you have either selected ESP32 Wrover Module,
//     or another board which has PSRAM enabled
//
 
// Select camera model
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_ESP_EYE
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER
 
#include "camera_pins.h"
 
const char* ssid = "Wi-Fi_SSID";
const char* password = "Wi-Fi_PASSPHRASE";
 
void startCameraServer();
 
const char common_name[40] = "esp32cam";
const char *OTAName = common_name;    // A name and a password for the OTA service
const char *mdnsName = common_name;    // Domain name for the mDNS responder
 
void print_wakeup_reason(){
 esp_sleep_wakeup_cause_t wakeup_reason;
 
 wakeup_reason = esp_sleep_get_wakeup_cause();
 
 switch(wakeup_reason)
 {
  case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
  case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
  case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
  case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
  case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
  default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
 }
}
 
void startWiFi() { // Start a Wi-Fi access point, and try to connect to some given access points. Then wait for either an AP or STA connection
 // WiFi.softAP(ssid, password);      // Start the access point
 WiFi.mode(WIFI_STA);      // Start the access point
 WiFi.begin(ssid, password);      // Start the access point
 while ( WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
 }
 Serial.println("");
 Serial.print("SSID \"");
 Serial.print(ssid);
 Serial.println("\" started\r\n");
 
 Serial.print("Connected to ");
 Serial.println(ssid);
 
 Serial.print("IP address: ");
 Serial.println(WiFi.localIP());
 
 Serial.print("hostname : ");
 // Serial.println(WiFi.hostname());
 Serial.println("");
}
 
void startMDNS() { // Start the mDNS responder
 MDNS.begin(mdnsName);         // start the multicast domain name server
 Serial.print("mDNS responder started: http://");
 Serial.print(mdnsName);
 Serial.println(".local");
}
 
void startOTA() { // Start the OTA service
 ArduinoOTA.setHostname(OTAName);
 // ArduinoOTA.setPassword(OTAPassword);
 
 ArduinoOTA.onStart([]() {
  Serial.println("Start");
  // turn off the LEDs
  for (int i = 0; i < 6; i++) {
   //digitalWrite(LED_BUILTIN, HIGH);
   //digitalWrite(LED_BUILTIN, LOW);
  }
 });
 ArduinoOTA.onEnd([]() {
  Serial.println("\r\nEnd");
 });
 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
  Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
 });
 ArduinoOTA.onError([](ota_error_t error) {
  Serial.printf("Error[%u]: ", error);
  if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
  else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
  else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
  else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
  else if (error == OTA_END_ERROR) Serial.println("End Failed");
 });
 ArduinoOTA.begin();
 Serial.println("OTA ready\r\n");
}
 
//Method to print the GPIO that triggered the wakeup
void print_GPIO_wake_up(){
 uint64_t GPIO_reason = esp_sleep_get_ext1_wakeup_status();
 Serial.print("GPIO that triggered the wake up: GPIO ");
 Serial.println((log(GPIO_reason))/log(2), 0);
}
 
void setup() {
 WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
 Serial.begin(115200);
 Serial.setDebugOutput(true);
 Serial.println();
 
 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;
 //init with high specs to pre-allocate larger buffers
 if(psramFound()){
  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;
 }
 
#if defined(CAMERA_MODEL_ESP_EYE)
 pinMode(13, INPUT_PULLUP);
 pinMode(14, INPUT_PULLUP);
#endif
 
 // camera init
 esp_err_t err = esp_camera_init(&config);
 if (err != ESP_OK) {
  Serial.printf("Camera init failed with error 0x%x", err);
  return;
 }
 
 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 blightness 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_QVGA);
 s->set_framesize(s, FRAMESIZE_SVGA);
 s->set_vflip(s, 1);//flip it back
 
#if defined(CAMERA_MODEL_M5STACK_WIDE)
 s->set_vflip(s, 1);
 s->set_hmirror(s, 1);
#endif
 
 startWiFi();
 startMDNS();
 startOTA();
 startCameraServer();
 
 ++bootCount;
 Serial.println("Boot number: " + String(bootCount));
 print_wakeup_reason();
 
 print_GPIO_wake_up();
 
 pinMode(GPIO_NUM_13, INPUT_PULLUP);
 pinMode(GPIO_NUM_2, INPUT_PULLUP); // GPIO2はmicroSDを使う場合、必須のピンなので使えない
 esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK, ESP_EXT1_WAKEUP_ALL_LOW);
 
 while (!digitalRead(GPIO_NUM_13)) { }
 
 Serial.println("Going to sleep now");
 delay(1000);
 esp_deep_sleep_start();
 Serial.println("This will never be printed");
}
 
void loop() {
 ArduinoOTA.handle();         // listen for OTA events
}

 これでPIRセンサーが検知中にタクトスイッチを押下(またはその逆)でWi-Fiに接続、IPが配布され、mDNS名が振られ、mDNS.localでブラウザからアクセス、映像が表示され、PIRセンサーが検知している間、映像を表示し続けられるようになります。

 PIRセンサーHC-SR501は、検知後の遅延時間を設定でき、ジャンパピンを挿す位置で検知中の再検知をするかしないか設定できるので再検知するようにしておけば、遅延時間に加え、その間、ストリーミング表示されるようになります。

 但し、ここでは深く考えずにGPIO2を使いました。

 が、ESP32-CAMの極々限られた少ないGPIOピンにおいて更にmicroSDを使う際は、下記のような情報を参考にADC対応、また、ディープスリープを使う際は、併せてRTC対応のGPIO14/15/2/4/12/13を使用できること、SD.h/SD_MMC.hの別、SD.hでSPI用の4ピンを指定できること、ただ、GPIO14/GPIO15はCLK/CMDなのでGPIO2/GPIO4/GPIO12/GPIO13の何れか2つが使えること、GPIO4はフラッシュLEDにも割り当てられているため、これを使うとLEDも光ってしまうことなどに注意。

 とは言え、今のところ、複数GPIOピン復帰対応のext1においてmicroSDの利用はできていません(やり方によるのか、そもそもできないのかは不明)。

 ESP32 External Wake Up from Deep Sleepにある通り、[esp_sleep_enable_ext1_wakeup()]関数の第一引数BUTTON_PIN_BITMASKは、複数のGPIO番号による2の累乗をそれぞれ足した10進数を16進数にしたものという変わり種。

 計算にあたっては、同ページでWebツールも紹介されていますが、例えば、MATE電卓でも[モード(M)]、[プログラミング(P)]を選択すると計算できます。

 スケッチには、この結果を確認できる関数も用意されているので使わせていただきましたが、[GPIO inf]としか表示されない...何か間違えたか...。

 第二引数は、ここで使った全てのGPIOがLOWであることを表すESP_EXT1_WAKEUP_ALL_LOWの他、何れかのGPIOがHIGHであることを表すESP_EXT1_WAKEUP_ANY_HIGHもあるようです。

 尚、デフォルトでメニューを隠して映像のみ表示するようにしてありますが、ハンバーガー(横三本線)メニューボタン押下でメニューを表示することもできます。

 ちなみにESP32-CAMのサンプルスケッチだとESP32-CAMを横向きにした状態で天地方向が合ってしまい、以前なら、V-Flipでカメラ(ESP32-CAM)を縦にした状態にすることができたのですが、AI Thinker ESP32-CAMボードが追加されてから、V-Flipをいじっても反転はするものの、縦横変換はできなくなりましたが、V-Flip機能としては、こっちの方が正常なのかも。

 他に方法あるのだろうか?

 画面については、ext0のケースと同様にJavaScript/CSS/HTMLを適宜編集後、gzip、これをcamera_index.h同様となるようにxxd -iでCヘッダファイル出力、配列名を変更、これに伴い、app_httpd.cppでも返す配列名を変更したりしてアクセス時に映像表示したり、デフォルトでメニューを隠したりと見栄えなどに手を加えました。

備考

 ext0よりもext1は、電流を多く消費するようで、とは言え、PCのUSBポートでも、また、QC3.0対応5V/2A USB接続ACアダプタなどならいける(たぶん100均の5V/1Aのも大丈夫じゃないかと思う)一方、ちょっと怪しい中華製くらいならいけても、超怪しい中華製USB接続ACアダプタ超激安品などでは、ext0はいけたとしてもext1だと映像を表示できないといったケースもあるので注意。

 また、改めて書いておくとジャンパワイヤなど配線は超重要、ちょっとでも接触不良があろうものなら、シリアルモニタ上でリブートを繰り返したり、正常なのにカメラを認識できないと言われてみたり、原因不明なあらゆるエラーに見舞われることになるので要注意。

 特に5V/GNDは、最初ジャストフィットでも、しばらく挿している間にとろけてしまうのか、ゆるくなるケースがある気がしています(ESP32-CAMに限らず)。

2022/06/26

 ESP32-CAMと同程度の価格でESP32 WROVER IE + OV2460カメラを購入...、してみたら、他ESP32ボードの技適番号が刻印された偽技適...。

2023/08/04

 実用品自作用にと技適番号201-220052が刻印されているカメラ付きESP32-S3-WROOM-1開発ボードを購入。

ホーム前へ次へ