/*********
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();
}
Vorgefundene Kodierung: UTF-8 | 0
|