🇩🇪

Source file: /~heha/mb-iwp/Kamera/esp32cam/cam-221216.zip/Kamera.ino

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-cam-video-streaming-web-server-camera-home-assistant/
  
  IMPORTANT!!! 
   - Select Board "AI Thinker ESP32-CAM"
   - GPIO 0 must be connected to GND to upload a sketch
   - After connecting GPIO 0 to GND, press the ESP32-CAM on-board RESET button to put your board in flashing mode
Anpassung an tuc-special sowie Allgemeinfall
(nicht so'n Murks mit dem Eincompilieren von SSID+Passphrase):
-Videotyp an URL
*********/

#include <esp_camera.h>
#include <WiFi.h>
#include <esp_timer.h>
#include <img_converters.h>
#include <Arduino.h>
#include <fb_gfx.h>
#include <soc/soc.h> //disable brownout problems
#include <soc/rtc_cntl_reg.h>  //disable brownout problems
#include <esp_http_server.h>
#include <Preferences.h>
#include <WiFiMulti.h>
#include "input.h"

struct POINTS{
 int16_t x,y;
};
static const POINTS framesizes[]={      // Zuordnung Enum-Nummer zu Größe im esp32
 {96,96},	//0	1:1	ISDN?	   9216
 {160,120},	//1	4:3	QQVGA	  19200
 {176,144},	//2	11:9	QCIF	  25344
 {240,176},	//3	15:11	HQVGA	  42240
 {240,240},	//4	1:1	GSM?	  57600
 {320,240},	//5	4:3	QVGA	  76800
 {400,296},	//6	50:37	CIF	 118400
 {480,320},	//7	3:2	HVGA	 153600
 {640,480},	//8	4:3	VGA	 307200	OV7670
 {800,600},	//9	4:3	SVGA	 480000
 {1024,768},	//10	4:3	XGA	 786432
 {1280,720},	//11	16:9	HD	 921600
 {1280,1024},	//12	5:4	SXGA	1310720
 {1600,1200},	//13	4:3	UXGA	1920000	OV2640
 {1920,1080},	//14	16:9	FHD	2073600
 {720,1280},	//15	9:16	P_HD	 921600
 {864,1536},	//16	9:16	P_3MP	1327104
 {2048,1536},	//17	4:3	QXGA	3145728
 {2560,1440},	//18	16:9	QHD	3686400
 {2560,1600},	//19	8:5	WQXGA	4096000
 {1080,1920},	//20	9:16	P_FHD	2073600
 {2560,1920},	//21	4:3	QSXGA	4915200
};

static bool setupCam(framesize_t framesize, unsigned lossness) {
// framesize = 0..13, lossness = 0..63
 camera_config_t config;
 config.ledc_channel = LEDC_CHANNEL_0;
 config.ledc_timer = LEDC_TIMER_0;
 config.pin_d0 = 5;
 config.pin_d1 = 18;
 config.pin_d2 = 19;
 config.pin_d3 = 21;
 config.pin_d4 = 36;
 config.pin_d5 = 39;
 config.pin_d6 = 34;
 config.pin_d7 = 35;
 config.pin_xclk = 0;
 config.pin_pclk = 22;
 config.pin_vsync = 25;
 config.pin_href = 23;
 config.pin_sccb_sda = 26;
 config.pin_sccb_scl = 27;
 config.pin_pwdn = 32;
 config.pin_reset = -1;
 config.xclk_freq_hz = 20000000;
 config.pixel_format = PIXFORMAT_JPEG; 
 const char*mitohne;  
 if (psramFound()){
  if (framesize>FRAMESIZE_UXGA) framesize=FRAMESIZE_UXGA;
  mitohne="mit";
  config.frame_size = framesize;
  config.jpeg_quality = lossness;
  config.fb_count = 2;
 }else{
  if (framesize>FRAMESIZE_SVGA) framesize=FRAMESIZE_SVGA;
  mitohne="ohne";
  if (lossness<12) lossness=12;
  config.frame_size = framesize;
  config.jpeg_quality = lossness;
  config.fb_count = 1;
 }
 Serial.printf("Kamerainitialisierung %s PSRAM, %ux%u\n",mitohne,framesizes[framesize].x,framesizes[framesize].y);
  
  // Camera init
 esp_err_t err = esp_camera_init(&config);
 if (err != ESP_OK) {
  Serial.printf("Kamerainitialisierungs-Fehlerkode 0x%X!\n", err);
  return false;
 }
 return true;
}

#define PART_BOUNDARY "Omega"

static httpd_handle_t stream_httpd;

static esp_err_t stream_handler(httpd_req_t *req) {
 char s[32];
 *s=0;
 httpd_req_get_url_query_str(req,s,sizeof s);
 unsigned fmt=13,ms=0,qual=80;       // Bildgröße (13 = maximal), Millisekunden (von Bild zu Bild um WLAN-Bandbreite zu sparen) ~ Bildrate, JPEG-Qualität (in Prozent? 0..100)
 sscanf(s,"%u,%u,%u",&fmt,&ms,&qual);
 Serial.println("Stream-Handler startet.");
 setupCam(framesize_t(fmt),(100U-qual)>>1);        // widersprüchliche Qualitätsangaben
 esp_err_t res = httpd_resp_set_type(req,"multipart/x-mixed-replace;boundary=" PART_BOUNDARY);
 bool msgdone=false;
 while (res == ESP_OK){
  unsigned tic=millis();
  camera_fb_t*fb = esp_camera_fb_get();
  if (!fb) {
   Serial.println("Camera capture failed");
   res = ESP_FAIL;
   break;
  }
  size_t jpg_buf_len = 0;
  byte*jpg_buf = 0;
  if (fb->width > 400 && fb->format != PIXFORMAT_JPEG) {        // Größere Bilder kommen wohl manchmal unkomprimiert?
   if (!msgdone) {
    Serial.println("Manuelle JPEG-Kompression, Bilddaten kommen als Bixmap vom Sensor");
    msgdone=true;
   }
   bool jpeg_converted = frame2jpg(fb, qual, &jpg_buf, &jpg_buf_len);
   esp_camera_fb_return(fb);
   fb = 0;
   if (!jpeg_converted){
    Serial.println("JPEG compression failed");
    res = ESP_FAIL;
    break;
   }
  }
  jpg_buf_len = fb->len;
  jpg_buf = fb->buf;
  char part_buf[64];
  size_t hlen = snprintf(part_buf,sizeof part_buf,
    "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n",
    jpg_buf_len);
  if (res == ESP_OK) res=httpd_resp_send_chunk(req,part_buf,hlen);
  if (res == ESP_OK) res=httpd_resp_send_chunk(req,reinterpret_cast<char*>(jpg_buf),jpg_buf_len);       // Binärkram durchwursteln
  static const char STREAM_BOUNDARY[] = "\r\n--" PART_BOUNDARY "\r\n";                // (Zusätzliche) Ende-Kennung
  if (res == ESP_OK) res=httpd_resp_send_chunk(req,STREAM_BOUNDARY,strlen(STREAM_BOUNDARY));
  if (fb) esp_camera_fb_return(fb);
  else free(jpg_buf);
  while (millis()-tic<ms) ;     // warten für konstante, reduzierte Frame-Rate (igitt! Geht das nicht besser?)
 };
 Serial.println("Stream-Handler beendet.");
 return res;
}

static void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;

  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = 0
  };
  
  //Serial.printf("Starting web server on port: '%d'\n", config.server_port);
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &index_uri);
  }
}

static WiFiMulti wifiMulti;
typedef WifiAPlist_t Cred;
static auto&creds=wifiMulti.APlist;
static auto curcred=creds.begin();

struct CbDat{
 char sbuf[64];
 static KeyCbRet cb(int chr,void*p) {return (*(CbDat*)p)(chr);}
 KeyCbRet operator()(int);
};

// Lambdas als Funktionszeiger sind möglicherweise zu plump
// Daher hier ein gebasteltes Lambda
KeyCbRet CbDat::operator()(int chr) {
 switch (chr) {
  case 'P'-'@': curcred = curcred==creds.begin() ? creds.end()-1 : curcred-1; return clear_call_again;
  case 'N'-'@': curcred = curcred==creds.end()-1 ? creds.begin() : curcred+1; return clear_call_again;
  case -1: strcpy(sbuf,curcred->ssid); return string_replaced;
 }
 return not_handled;
}

// Eigene Präferenzen mit load() und save()
static struct MyPref:public Preferences{
 void load();
 void save();
}preferences;

void MyPref::load() {
 begin("credentials");
 for(int i=0;;i++) {
  char buf1[12],buf2[12];
  snprintf(buf1,sizeof buf1,"ssid%u",i);
  snprintf(buf2,sizeof buf2,"pass%u",i);
  Cred cred={
   getString(buf1).c_str(),
   getString(buf2).c_str()
  };
  if (!cred.ssid) break;
  if (*cred.ssid) break;
  if (cred.pass && !*cred.pass) cred.pass=0;
  creds.push_back(cred);
 }
}

void MyPref::save() {
 clear();
 for(size_t i=0; i<creds.size(); i++) {
  auto&cred=creds[i];
  char buf1[12],buf2[12];
  snprintf(buf1,sizeof buf1,"ssid%u",i);
  snprintf(buf2,sizeof buf2,"pass%u",i);
  putString(buf1,cred.ssid);
  putString(buf2,cred.pass);
 }
}


void queryCredentials() {
 termcap.setattr(4);   // unterstreichen
 Serial.print("WLAN-Infrastruktur-Zugang muss konfiguriert werden!");
 termcap.setattr(); // unterstreichen aus
 Serial.println();
 int n;
 do n=WiFi.scanComplete();
 while (n==-1);        // Returnwert für "Scannen in Arbeit" (sollte vorbei sein)
 Serial.print(n);
 Serial.println(" Hotspots gefunden");
//  if (n==-2) WiFi.
 
 struct Hotspot{
  char encr,rssi;
  String ssid;
  int compare(Hotspot const&r)          {return strcmp(ssid.c_str(),r.ssid.c_str());}
  bool operator==(Hotspot const&r)      {return compare(r)==0;}
  bool operator<(Hotspot const&r)       {return compare(r)<0;}
 };
 std::vector<Hotspot> hotspots;
// Dusseligerweise werden (in der Uni) viele Hotspots mit gleichem Namen gefunden.
// Daher wäre die Liste theoretisch zu sortieren und Duplikate zu filtern.
// Hier werden Mehrfachnennungen per Doppelschleife verhindert: Langsam aber schnell genug und RAM sparend.
 if (n>0) hotspots.reserve(n);
 for (size_t i=0; i<hotspots.size(); i++) {
  auto&hotspot=hotspots[i];
  hotspot.encr=WiFi.encryptionType(i);
  hotspot.rssi=WiFi.RSSI(i);
  hotspot.ssid=WiFi.SSID(i);
 }
 std::sort(hotspots.begin(),hotspots.end());
 hotspots.erase(std::unique(hotspots.begin(),hotspots.end()),hotspots.end());
 for (auto const&hotspot:hotspots) {
  String lock="🔒";
  if (hotspot.encr == WIFI_AUTH_OPEN) lock="🔓";
  else{
   auto f=std::find(creds.begin(),creds.end(),hotspot.ssid.c_str());
   if (f!=creds.end()) {
    curcred=f;  // der letzte Match wird genommen
    lock="🔐";  // Schlüssel vorhanden
   }
  }
  Serial.print(lock);
  Serial.println(hotspot.ssid);
 }
// Die MAC-Adresse auszuspucken ist für tuc-special der TU Chemnitz gedacht.
// Mit dieser kann man im IdM-Portal (früher: mouse.hrz) einen Zugang beantragen
// und bekommt innerhalb von Sekunden auch eine Freischaltung mitsamt Passwort.
 Serial.print("Diese MAC-Adresse: ");
 Serial.println(WiFi.macAddress());
// Mit den vertikalen Kursortasten kann eine der Vorgaben aus der Liste der gefundenen WLANs ausgewählt werden
 CbDat cbdat;
 cbdat.sbuf[0]=0;
 if (curcred!=creds.end()) strcpy(cbdat.sbuf,curcred->ssid);     // voraus ausfüllen
 for(;;){
  input("WLAN-Netzwerkname",cbdat.sbuf,sizeof cbdat.sbuf,n ? CbDat::cb : 0, &cbdat);
  if (cbdat.sbuf[0]) break;
 }
 Cred cred={strdup(cbdat.sbuf),0};
 input(" Zugangsschlüssel",cbdat.sbuf,sizeof cbdat.sbuf);
 if (*cbdat.sbuf) cred.pass=strdup(cbdat.sbuf);
 creds.push_back(cred);
 Serial.println("Angaben werden gespeichert.");
 preferences.save();
};

void setup() {
// Wer/was auch immer noch etws VORHER in die Konsole kritzelt ...
 WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
 
 Serial.begin(115200);
 Serial.setDebugOutput(false);
  
// Serielle Verbindung auf Vorhandensein von PuTTY testen
 termcap.init();
// WLAN-Verbindung aufbauen
 preferences.load();

 WiFi.mode(WIFI_STA);
 WiFi.scanNetworks(true,false); // Im Hintergrund bereits anfangen zu suchen
 for (auto const&cred:creds) wifiMulti.addAP(cred.ssid,cred.pass);
 }

void loop() {
 switch (Serial.read()) {
  case 'C'-'@': preferences.clear(); [[fallthrough]];
  case 'R'-'@': setup(); // ^R wirkt wie Reset
 }
 if (wifiMulti.run()!=WL_CONNECTED) queryCredentials();
 for(;;) {
//  WiFi.begin(curcred->ssid,curcred->pass);
  char intime=20; // 10 Sekunden
  do{
   if (WiFi.status() == WL_CONNECTED) break;
   Serial.print(".");
   delay(500);
  }while(--intime);
  if (intime) break;
  Serial.println(" Zeitüberschreitung!");
  queryCredentials();
 }
 Serial.println();
 Serial.println("Verbindung mit WLAN-Router hergestellt.");
  
 Serial.print("Kamera bereit! Bild im Browser durch Aufruf von: ");
 termcap.setattr(1); // fett
 Serial.print("http://");
 Serial.print(WiFi.localIP());
 Serial.print("/");
 termcap.setattr();  // fett aus
 Serial.println("Größe(0..13),Qualität(80-90),Wiederholrate(ms)");
  // Start streaming web server
 startCameraServer();
}
Detected encoding: UTF-80