Removed face detection code, refactored to allow for multiple clients on streams

This commit is contained in:
Sam 2020-10-25 11:38:29 -05:00
parent e2dfb773d7
commit 3570a9cfac
6 changed files with 1722 additions and 678 deletions

File diff suppressed because it is too large Load Diff

681
esp32-cam-webserver.cpp Normal file
View File

@ -0,0 +1,681 @@
#include <Arduino.h>
#include "esp_camera.h"
#include <WiFi.h>
#include <DNSServer.h>
#define APP_CPU 1
#define PRO_CPU 0
/* This sketch is a extension/expansion/reork of the 'official' ESP32 Camera example
* sketch from Expressif:
* https://github.com/espressif/arduino-esp32/tree/master/libraries/ESP32/examples/Camera/CameraWebServer
*
* It is modified to allow control of Illumination LED Lamps's (present on some modules),
* greater feedback via a status LED, and the HTML contents are present in plain text
* for easy modification.
*
* A camera name can now be configured, and wifi details can be stored in an optional
* header file to allow easier updated of the repo.
*
* The web UI has had changes to add the lamp control, rotation, a standalone viewer,
* more feeedback, new controls and other tweaks and changes,
* note: Make sure that you have either selected ESP32 AI Thinker,
* or another board which has PSRAM enabled to use high resolution camera modes
*/
/*
* FOR NETWORK AND HARDWARE SETTINGS COPY OR RENAME 'myconfig.sample.h' TO 'myconfig.h' AND EDIT THAT.
*
* By default this sketch will assume an AI-THINKER ESP-CAM and create
* an accesspoint called "ESP32-CAM-CONNECT" (password: "InsecurePassword")
*
*/
// Primary config, or defaults.
#if __has_include("myconfig.h")
#include "myconfig.h"
#else
#warning "Using Defaults: Copy myconfig.sample.h to myconfig.h and edit that to use your own settings"
#define WIFI_AP_ENABLE
#define CAMERA_MODEL_AI_THINKER
struct station
{
const char ssid[64];
const char password[64];
const bool dhcp;
} stationList[] = {{"ESP32-CAM-CONNECT", "InsecurePassword", true}};
#endif
// Upstream version string
#include "src/version.h"
// Pin Mappings
#include "camera_pins.h"
// Internal filesystem (SPIFFS)
// used for non-volatile camera settings and face DB store
#include "storage.h"
// Sketch Info
int sketchSize;
int sketchSpace;
String sketchMD5;
// Start with accesspoint mode disabled, wifi setup will activate it if
// no known networks are found, and WIFI_AP_ENABLE has been defined
bool accesspoint = false;
// IP address, Netmask and Gateway, populated when connected
IPAddress ip;
IPAddress net;
IPAddress gw;
// Declare external function from app_httpd.cpp
extern void startCameraServer();
// Declare external function from esp32-streamserver.cpp
extern void startStreamServer();
// A Name for the Camera. (set in myconfig.h)
#if defined(CAM_NAME)
char myName[] = CAM_NAME;
#else
char myName[] = "ESP32 camera server";
#endif
#if !defined(WIFI_WATCHDOG)
#define WIFI_WATCHDOG 5000
#endif
// Number of known networks in stationList[]
int stationCount = sizeof(stationList) / sizeof(stationList[0]);
// If we have AP mode enabled, ignore first entry in the stationList[]
#if defined(WIFI_AP_ENABLE)
int firstStation = 1;
#else
int firstStation = 0;
#endif
// Select bvetween full and simple index as the default.
#if defined(DEFAULT_INDEX_FULL)
char default_index[] = "full";
#else
char default_index[] = "simple";
#endif
// DNS server
const byte DNS_PORT = 53;
DNSServer dnsServer;
bool captivePortal = false;
char apName[64] = "Undefined";
// The app and stream URLs
char httpURL[64] = {"Undefined"};
char streamURL[64] = {"Undefined"};
// This will be displayed to identify the firmware
char myVer[] PROGMEM = __DATE__ " @ " __TIME__;
// initial rotation
// can be set in myconfig.h
#if !defined(CAM_ROTATION)
#define CAM_ROTATION 0
#endif
int myRotation = CAM_ROTATION;
// Illumination LAMP/LED
#if defined(LAMP_DISABLE)
int lampVal = -1; // lamp is disabled in config
#elif defined(LAMP_PIN)
#if defined(LAMP_DEFAULT)
int lampVal = constrain(LAMP_DEFAULT, 0, 100); // initial lamp value, range 0-100
#else
int lampVal = 0; //default to off
#endif
#else
int lampVal = -1; // no lamp pin assigned
#endif
int lampChannel = 7; // a free PWM channel (some channels used by camera)
const int pwmfreq = 50000; // 50K pwm frequency
const int pwmresolution = 9; // duty cycle bit range
const int pwmMax = pow(2, pwmresolution) - 1;
#if defined(NO_FS)
bool filesystem = false;
#else
bool filesystem = true;
#endif
#if defined(FACE_DETECTION)
int8_t detection_enabled = 1;
#if defined(FACE_RECOGNITION)
int8_t recognition_enabled = 1;
#else
int8_t recognition_enabled = 0;
#endif
#else
int8_t detection_enabled = 0;
int8_t recognition_enabled = 0;
#endif
// Debug Data for stream and capture
#if defined(DEBUG_DEFAULT_ON)
bool debugData = true;
#else
bool debugData = false;
#endif
//defs
void flashLED(int flashtime);
void setLamp(int newVal);
void WifiSetup();
// Notification LED
void flashLED(int flashtime)
{
#ifdef LED_PIN // If we have it; flash it.
digitalWrite(LED_PIN, LED_ON); // On at full power.
delay(flashtime); // delay
digitalWrite(LED_PIN, LED_OFF); // turn Off
#else
return; // No notifcation LED, do nothing, no delay
#endif
}
// Lamp Control
void setLamp(int newVal)
{
if (newVal != -1)
{
// Apply a logarithmic function to the scale.
int brightness = round((pow(2, (1 + (newVal * 0.02))) - 2) / 6 * pwmMax);
ledcWrite(lampChannel, brightness);
Serial.print("Lamp: ");
Serial.print(newVal);
Serial.print("%, pwm = ");
Serial.println(brightness);
}
}
void WifiSetup()
{
// Feedback that we are now attempting to connect
flashLED(300);
delay(100);
flashLED(300);
Serial.println("Starting WiFi");
Serial.print("Known external SSIDs: ");
if (stationCount > firstStation)
{
for (int i = firstStation; i < stationCount; i++)
Serial.printf(" '%s'", stationList[i].ssid);
}
else
{
Serial.print("None");
}
Serial.println();
byte mac[6];
WiFi.macAddress(mac);
Serial.printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
int bestStation = -1;
long bestRSSI = -1024;
if (stationCount > firstStation)
{
// We have a list to scan
Serial.printf("Scanning local Wifi Networks\n");
int stationsFound = WiFi.scanNetworks();
Serial.printf("%i networks found\n", stationsFound);
if (stationsFound > 0)
{
for (int i = 0; i < stationsFound; ++i)
{
// Print SSID and RSSI for each network found
String thisSSID = WiFi.SSID(i);
int thisRSSI = WiFi.RSSI(i);
Serial.printf("%3i : %s (%i)", i + 1, thisSSID.c_str(), thisRSSI);
// Scan our list of known external stations
for (int sta = firstStation; sta < stationCount; sta++)
{
if (strcmp(stationList[sta].ssid, thisSSID.c_str()) == 0)
{
Serial.print(" - Known!");
// Chose the strongest RSSI seen
if (thisRSSI > bestRSSI)
{
bestStation = sta;
bestRSSI = thisRSSI;
}
}
}
Serial.println();
}
}
}
else
{
// No list to scan, therefore we are an accesspoint
accesspoint = true;
}
if (bestStation == -1)
{
if (!accesspoint)
{
#if defined(WIFI_AP_ENABLE)
Serial.println("No known networks found, entering AccessPoint fallback mode");
accesspoint = true;
#else
Serial.println("No known networks found");
#endif
}
else
{
Serial.println("AccessPoint mode selected in config");
}
}
else
{
Serial.printf("Connecting to Wifi Network: %s\n", stationList[bestStation].ssid);
if (stationList[bestStation].dhcp == false)
{
#if defined(ST_IP)
Serial.println("Applying static IP settings");
#if !defined(ST_GATEWAY) || !defined(ST_NETMASK)
#error "You must supply both Gateway and NetMask when specifying a static IP address"
#endif
IPAddress staticIP(ST_IP);
IPAddress gateway(ST_GATEWAY);
IPAddress subnet(ST_NETMASK);
#if !defined(ST_DNS1)
WiFi.config(staticIP, gateway, subnet);
#else
IPAddress dns1(ST_DNS1);
#if !defined(ST_DNS2)
WiFi.config(staticIP, gateway, subnet, dns1);
#else
IPAddress dns2(ST_DNS2);
WiFi.config(staticIP, gateway, subnet, dns1, dns2);
#endif
#endif
#else
Serial.println("Static IP settings requested but not defined in config, falling back to dhcp");
#endif
}
// Initiate network connection request
WiFi.begin(stationList[bestStation].ssid, stationList[bestStation].password);
// Wait to connect, or timeout
unsigned long start = millis();
while ((millis() - start <= WIFI_WATCHDOG) && (WiFi.status() != WL_CONNECTED))
{
delay(500);
Serial.print('.');
}
// If we have connected, inform user
if (WiFi.status() == WL_CONNECTED)
{
Serial.println("Client connection succeeded");
accesspoint = false;
// Note IP details
ip = WiFi.localIP();
net = WiFi.subnetMask();
gw = WiFi.gatewayIP();
Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]);
Serial.printf("Netmask : %d.%d.%d.%d\n", net[0], net[1], net[2], net[3]);
Serial.printf("Gateway : %d.%d.%d.%d\n", gw[0], gw[1], gw[2], gw[3]);
// Flash the LED to show we are connected
for (int i = 0; i < 5; i++)
{
flashLED(50);
delay(150);
}
}
else
{
Serial.println("Client connection Failed");
WiFi.disconnect(); // (resets the WiFi scan)
}
}
if (accesspoint && (WiFi.status() != WL_CONNECTED))
{
// The accesspoint has been enabled, and we have not connected to any existing networks
#if defined(AP_CHAN)
Serial.println("Setting up Fixed Channel AccessPoint");
Serial.print(" SSID : ");
Serial.println(stationList[0].ssid);
Serial.print(" Password : ");
Serial.println(stationList[0].password);
Serial.print(" Channel : ");
Serial.println(AP_CHAN);
WiFi.softAP(stationList[0].ssid, stationList[0].password, AP_CHAN);
#else
Serial.println("Setting up AccessPoint");
Serial.print(" SSID : ");
Serial.println(stationList[0].ssid);
Serial.print(" Password : ");
Serial.println(stationList[0].password);
WiFi.softAP(stationList[0].ssid, stationList[0].password);
#endif
#if defined(AP_ADDRESS)
// User has specified the AP details; apply them after a short delay
// (https://github.com/espressif/arduino-esp32/issues/985#issuecomment-359157428)
delay(100);
IPAddress local_IP(AP_ADDRESS);
IPAddress gateway(AP_ADDRESS);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_IP, gateway, subnet);
#endif
// Note AP details
ip = WiFi.softAPIP();
net = WiFi.subnetMask();
gw = WiFi.gatewayIP();
strcpy(apName, stationList[0].ssid);
Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]);
// Flash the LED to show we are connected
for (int i = 0; i < 5; i++)
{
flashLED(150);
delay(50);
}
// Start the DNS captive portal if requested
if (stationList[0].dhcp == true)
{
Serial.println("Starting Captive Portal");
dnsServer.start(DNS_PORT, "*", ip);
captivePortal = true;
}
}
}
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
Serial.println("====");
Serial.print("esp32-cam-webserver: ");
Serial.println(myName);
Serial.print("Code Built: ");
Serial.println(myVer);
Serial.print("Base Release: ");
Serial.println(baseVersion);
if (stationCount == 0)
{
Serial.println("\nFatal Error; Halting");
Serial.println("No wifi ssid details have been configured; we cannot connect to WiFi or start our own AccessPoint");
while (true)
delay(1000);
}
#if defined(LED_PIN) // If we have a notification LED, set it to output
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LED_ON);
#endif
// Create camera config structure; and populate with hardware and other defaults
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 highest supported specs to pre-allocate large buffers
if (psramFound())
{
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
}
else
{
config.frame_size = FRAMESIZE_XGA;
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.println("Camera init succeeded");
}
else
{
delay(100); // need a delay here or the next serial o/p gets missed
Serial.println("Halted: Camera sensor failed to initialise");
Serial.println("Will reboot to try again in 10s\n");
delay(10000);
ESP.restart();
}
sensor_t *s = esp_camera_sensor_get();
// Dump camera module, warn for unsupported modules.
switch (s->id.PID)
{
case OV9650_PID:
Serial.println("WARNING: OV9650 camera module is not properly supported, will fallback to OV2640 operation");
break;
case OV7725_PID:
Serial.println("WARNING: OV7725 camera module is not properly supported, will fallback to OV2640 operation");
break;
case OV2640_PID:
Serial.println("OV2640 camera module detected");
break;
case OV3660_PID:
Serial.println("OV3660 camera module detected");
break;
default:
Serial.println("WARNING: Camera module is unknown and not properly supported, will fallback to OV2640 operation");
}
// OV3660 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
}
// M5 Stack Wide has special needs
#if defined(CAMERA_MODEL_M5STACK_WIDE)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#endif
// Config can override mirror and flip
#if defined(H_MIRROR)
s->set_hmirror(s, H_MIRROR);
#endif
#if defined(V_FLIP)
s->set_vflip(s, V_FLIP);
#endif
// set initial frame rate
#if defined(DEFAULT_RESOLUTION)
s->set_framesize(s, DEFAULT_RESOLUTION);
#else
s->set_framesize(s, FRAMESIZE_SVGA);
#endif
/*
* Add any other defaults you want to apply at startup here:
* uncomment the line and set the value as desired (see the comments)
*
* these are defined in the esp headers here:
* https://github.com/espressif/esp32-camera/blob/master/driver/include/sensor.h#L149
*/
//s->set_framesize(s, FRAMESIZE_SVGA); // FRAMESIZE_[QQVGA|HQVGA|QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA|QXGA(ov3660)]);
//s->set_quality(s, val); // 10 to 63
//s->set_brightness(s, 0); // -2 to 2
//s->set_contrast(s, 0); // -2 to 2
//s->set_saturation(s, 0); // -2 to 2
//s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
//s->set_whitebal(s, 1); // aka 'awb' in the UI; 0 = disable , 1 = enable
//s->set_awb_gain(s, 1); // 0 = disable , 1 = enable
//s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
//s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable
//s->set_aec2(s, 0); // 0 = disable , 1 = enable
//s->set_ae_level(s, 0); // -2 to 2
//s->set_aec_value(s, 300); // 0 to 1200
//s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable
//s->set_agc_gain(s, 0); // 0 to 30
//s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6
//s->set_bpc(s, 0); // 0 = disable , 1 = enable
//s->set_wpc(s, 1); // 0 = disable , 1 = enable
//s->set_raw_gma(s, 1); // 0 = disable , 1 = enable
//s->set_lenc(s, 1); // 0 = disable , 1 = enable
//s->set_hmirror(s, 0); // 0 = disable , 1 = enable
//s->set_vflip(s, 0); // 0 = disable , 1 = enable
//s->set_dcw(s, 1); // 0 = disable , 1 = enable
//s->set_colorbar(s, 0); // 0 = disable , 1 = enable
// We now have camera with default init
// check for saved preferences and apply them
if (filesystem)
{
filesystemStart();
loadPrefs(SPIFFS);
loadFaceDB(SPIFFS);
}
else
{
Serial.println("No Internal Filesystem, cannot save preferences or face DB");
}
/*
* Camera setup complete; initialise the rest of the hardware.
*/
// Initialise and set the lamp
if (lampVal != -1)
{
ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel
setLamp(lampVal); // set default value
ledcAttachPin(LAMP_PIN, lampChannel); // attach the GPIO pin to the channel
}
else
{
Serial.println("No lamp, or lamp disabled in config");
}
// Having got this far; start Wifi and loop until we are connected or have started an AccessPoint
while ((WiFi.status() != WL_CONNECTED) && !accesspoint)
{
WifiSetup();
delay(1000);
}
// Now we have a network we can start the two http handlers for the UI and Stream.
startCameraServer();
startStreamServer();
// Construct the app and stream URLs
sprintf(httpURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], HTTP_PORT);
Serial.printf("\nCamera Ready!\nUse '%s' to connect\n", httpURL);
if (debugData)
Serial.println("Camera debug data is enabled (send any char to disable)");
else
Serial.println("Camera debug data is disabled (send any char to enable)");
// Used when dumping status; these are slow functions, so just do them once during startup
sketchSize = ESP.getSketchSize();
sketchSpace = ESP.getFreeSketchSpace();
sketchMD5 = ESP.getSketchMD5();
}
void loop()
{
/*
* Just loop forever, reconnecting Wifi As necesscary in client mode
* The stream and URI handler processes initiated by the startCameraServer() call at the
* end of setup() will handle the camera and UI processing from now on.
*/
if (accesspoint)
{
// Accespoint is permanently up, so just loop, servicing the captive portal as needed
unsigned long start = millis();
while (millis() - start < WIFI_WATCHDOG)
{
delay(100);
if (captivePortal)
dnsServer.processNextRequest();
}
}
else
{
// client mode can fail; so reconnect as appropriate
static bool warned = false;
if (WiFi.status() == WL_CONNECTED)
{
// We are connected, wait a bit and re-check
if (warned)
{
// Tell the user if we have just reconnected
Serial.println("WiFi reconnected");
warned = false;
}
// loop here for WIFI_WATCHDOG, turning debugData true/false depending on serial input..
unsigned long start = millis();
while (millis() - start < WIFI_WATCHDOG)
{
delay(100);
if (Serial.available())
{
// Toggle debug output on serial input
if (debugData)
{
debugData = false;
Serial.println("Camera debug data is disabled (send any char to enable)");
}
else
{
debugData = true;
Serial.println("Camera debug data is enabled (send any char to disable)");
}
}
while (Serial.available())
Serial.read(); // chomp the buffer
}
}
else
{
// disconnected; attempt to reconnect
if (!warned)
{
// Tell the user if we just disconnected
WiFi.disconnect(); // ensures disconnect is complete, wifi scan cleared
Serial.println("WiFi disconnected, retrying");
warned = true;
}
WifiSetup();
}
}
}

View File

@ -15,3 +15,8 @@ src_dir = ./
platform = espressif32 platform = espressif32
board = esp32cam board = esp32cam
framework = arduino framework = arduino
monitor_speed = 115200
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue

193
src/OV2640.cpp Normal file
View File

@ -0,0 +1,193 @@
#include "OV2640.h"
#define TAG "OV2640"
// definitions appropriate for the ESP32-CAM devboard (and most clones)
camera_config_t esp32cam_config{
.pin_pwdn = -1, // FIXME: on the TTGO T-Journal I think this is GPIO 0
.pin_reset = 15,
.pin_xclk = 27,
.pin_sscb_sda = 25,
.pin_sscb_scl = 23,
.pin_d7 = 19,
.pin_d6 = 36,
.pin_d5 = 18,
.pin_d4 = 39,
.pin_d3 = 5,
.pin_d2 = 34,
.pin_d1 = 35,
.pin_d0 = 17,
.pin_vsync = 22,
.pin_href = 26,
.pin_pclk = 21,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
// .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space
// .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer
// .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12, //0-63 lower numbers are higher quality
.fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg
};
camera_config_t esp32cam_aithinker_config{
.pin_pwdn = 32,
.pin_reset = -1,
.pin_xclk = 0,
.pin_sscb_sda = 26,
.pin_sscb_scl = 27,
// Note: LED GPIO is apparently 4 not sure where that goes
// per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c
.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,
.ledc_timer = LEDC_TIMER_1,
.ledc_channel = LEDC_CHANNEL_1,
.pixel_format = PIXFORMAT_JPEG,
// .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space
// .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer
// .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12, //0-63 lower numbers are higher quality
.fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg
};
camera_config_t esp32cam_ttgo_t_config{
.pin_pwdn = 26,
.pin_reset = -1,
.pin_xclk = 32,
.pin_sscb_sda = 13,
.pin_sscb_scl = 12,
.pin_d7 = 39,
.pin_d6 = 36,
.pin_d5 = 23,
.pin_d4 = 18,
.pin_d3 = 15,
.pin_d2 = 4,
.pin_d1 = 14,
.pin_d0 = 5,
.pin_vsync = 27,
.pin_href = 25,
.pin_pclk = 19,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12, //0-63 lower numbers are higher quality
.fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg
};
void OV2640::run(void)
{
if (fb)
//return the frame buffer back to the driver for reuse
esp_camera_fb_return(fb);
fb = esp_camera_fb_get();
}
void OV2640::runIfNeeded(void)
{
if (!fb)
run();
}
int OV2640::getWidth(void)
{
runIfNeeded();
return fb->width;
}
int OV2640::getHeight(void)
{
runIfNeeded();
return fb->height;
}
size_t OV2640::getSize(void)
{
runIfNeeded();
if (!fb)
return 0; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes?
return fb->len;
}
uint8_t *OV2640::getfb(void)
{
runIfNeeded();
if (!fb)
return NULL; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes?
return fb->buf;
}
framesize_t OV2640::getFrameSize(void)
{
return _cam_config.frame_size;
}
void OV2640::setFrameSize(framesize_t size)
{
_cam_config.frame_size = size;
}
pixformat_t OV2640::getPixelFormat(void)
{
return _cam_config.pixel_format;
}
void OV2640::setPixelFormat(pixformat_t format)
{
switch (format)
{
case PIXFORMAT_RGB565:
case PIXFORMAT_YUV422:
case PIXFORMAT_GRAYSCALE:
case PIXFORMAT_JPEG:
_cam_config.pixel_format = format;
break;
default:
_cam_config.pixel_format = PIXFORMAT_GRAYSCALE;
break;
}
}
esp_err_t OV2640::init(camera_config_t config)
{
memset(&_cam_config, 0, sizeof(_cam_config));
memcpy(&_cam_config, &config, sizeof(config));
esp_err_t err = esp_camera_init(&_cam_config);
if (err != ESP_OK)
{
printf("Camera probe failed with error 0x%x", err);
return err;
}
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
return ESP_OK;
}

43
src/OV2640.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef OV2640_H_
#define OV2640_H_
#include <Arduino.h>
#include <pgmspace.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_camera.h"
extern camera_config_t esp32cam_config, esp32cam_aithinker_config, esp32cam_ttgo_t_config;
class OV2640
{
public:
OV2640(){
fb = NULL;
};
~OV2640(){
};
esp_err_t init(camera_config_t config);
void run(void);
size_t getSize(void);
uint8_t *getfb(void);
int getWidth(void);
int getHeight(void);
framesize_t getFrameSize(void);
pixformat_t getPixelFormat(void);
void setFrameSize(framesize_t size);
void setPixelFormat(pixformat_t format);
private:
void runIfNeeded(); // grab a frame if we don't already have one
// camera_framesize_t _frame_size;
// camera_pixelformat_t _pixel_format;
camera_config_t _cam_config;
camera_fb_t *fb;
};
#endif //OV2640_H_

463
stream_httpd.cpp Normal file
View File

@ -0,0 +1,463 @@
/*
This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM
and ESP-EYE modules.
This is tested to work with VLC and Blynk video widget and can support up to 10
simultaneously connected streaming clients.
Simultaneous streaming is implemented with FreeRTOS tasks.
Inspired by and based on this Instructable: $9 RTSP Video Streamer Using the ESP32-CAM Board
(https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/)
Board: AI-Thinker ESP32-CAM or ESP-EYE
Compile as:
ESP32 Dev Module
CPU Freq: 240
Flash Freq: 80
Flash mode: QIO
Flash Size: 4Mb
Patrition: Minimal SPIFFS
PSRAM: Enabled
*/
// ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack)
#define APP_CPU 1
#define PRO_CPU 0
#include "Arduino.h"
#include "src/OV2640.h"
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiClient.h>
#include <esp_bt.h>
#include <esp_wifi.h>
#include <esp_sleep.h>
#include <driver/rtc_io.h>
// 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 // default
#include "camera_pins.h"
/*
Next one is an include with wifi credentials.
This is what you need to do:
1. Create a file called "home_wifi_multi.h" in the same folder OR under a separate subfolder of the "libraries" folder of Arduino IDE. (You are creating a "fake" library really - I called it "MySettings").
2. Place the following text in the file:
#define SSID1 "replace with your wifi ssid"
#define PWD1 "replace your wifi password"
3. Save.
Should work then
*/
#if !defined(STREAM_PORT)
#define STREAM_PORT 81
#endif
int streamPort = STREAM_PORT;
WebServer server(streamPort);
// ===== rtos task handles =========================
// Streaming is implemented with 3 tasks:
TaskHandle_t tMjpeg; // handles client connections to the webserver
TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally
TaskHandle_t tStream; // actually streaming frames to all connected clients
// frameSync semaphore is used to prevent streaming buffer as it is replaced with the next frame
SemaphoreHandle_t frameSync = NULL;
// Queue stores currently connected clients to whom we are streaming
QueueHandle_t streamingClients;
// We will try to achieve 25 FPS frame rate
const int FPS = 14;
// We will handle web client requests every 50 ms (20 Hz)
const int WSINTERVAL = 100;
// def
void mjpegCB(void *pvParameters);
void camCB(void *pvParameters);
char *allocateMemory(char *aPtr, size_t aSize);
void handleJPGSstream(void);
void streamCB(void *pvParameters);
void handleJPG(void);
void handleNotFound();
void startStreamServer(int sPort);
// ==== Memory allocator that takes advantage of PSRAM if present =======================
char *allocateMemory(char *aPtr, size_t aSize)
{
// Since current buffer is too smal, free it
if (aPtr != NULL)
free(aPtr);
size_t freeHeap = ESP.getFreeHeap();
char *ptr = NULL;
// If memory requested is more than 2/3 of the currently free heap, try PSRAM immediately
if (aSize > freeHeap * 2 / 3)
{
if (psramFound() && ESP.getFreePsram() > aSize)
{
ptr = (char *)ps_malloc(aSize);
}
}
else
{
// Enough free heap - let's try allocating fast RAM as a buffer
ptr = (char *)malloc(aSize);
// If allocation on the heap failed, let's give PSRAM one more chance:
if (ptr == NULL && psramFound() && ESP.getFreePsram() > aSize)
{
ptr = (char *)ps_malloc(aSize);
}
}
// Finally, if the memory pointer is NULL, we were not able to allocate any memory, and that is a terminal condition.
if (ptr == NULL)
{
ESP.restart();
}
return ptr;
}
// ======== Server Connection Handler Task ==========================
void mjpegCB(void *pvParameters)
{
TickType_t xLastWakeTime;
const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL);
// Creating frame synchronization semaphore and initializing it
frameSync = xSemaphoreCreateBinary();
xSemaphoreGive(frameSync);
// Creating a queue to track all connected clients
streamingClients = xQueueCreate(10, sizeof(WiFiClient *));
//=== setup section ==================
// Creating RTOS task for grabbing frames from the camera
xTaskCreatePinnedToCore(
camCB, // callback
"cam", // name
4096, // stacj size
NULL, // parameters
2, // priority
&tCam, // RTOS task handle
APP_CPU); // core
// Creating task to push the stream to all connected clients
xTaskCreatePinnedToCore(
streamCB,
"strmCB",
4 * 1024,
NULL, //(void*) handler,
2,
&tStream,
APP_CPU);
// Registering webserver handling routines
server.on("/mjpeg/1", HTTP_GET, handleJPGSstream);
server.on("/jpg", HTTP_GET, handleJPG);
server.onNotFound(handleNotFound);
// Starting webserver
server.begin();
//=== loop() section ===================
xLastWakeTime = xTaskGetTickCount();
for (;;)
{
server.handleClient();
// After every server client handling request, we let other tasks run and then pause
taskYIELD();
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
// Commonly used variables:
volatile size_t camSize; // size of the current frame, byte
volatile char *camBuf; // pointer to the current frame
// ==== RTOS task to grab frames from the camera =========================
void camCB(void *pvParameters)
{
TickType_t xLastWakeTime;
// A running interval associated with currently desired frame rate
const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS);
// Mutex for the critical section of swithing the active frames around
portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED;
//=== loop() section ===================
xLastWakeTime = xTaskGetTickCount();
// Pointers to the 2 frames, their respective sizes and index of the current frame
char *fbs[2] = {NULL, NULL};
size_t fSize[2] = {0, 0};
int ifb = 0;
camera_fb_t *fb = NULL;
size_t _jpg_buf_len = 0;
uint8_t *_jpg_buf = NULL;
for (;;)
{
// Grab a frame from the camera and query its size
// cam.run();
// size_t s = cam.getSize();
fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Camera capture failed");
}
else
{
if (fb->format != PIXFORMAT_JPEG)
{
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if (!jpeg_converted)
{
Serial.println("JPEG compression failed");
}
}
else
{
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
// If frame size is more that we have previously allocated - request 125% of the current frame space
if (_jpg_buf_len > fSize[ifb])
{
fSize[ifb] = _jpg_buf_len * 4 / 3;
fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]);
}
// Copy current frame into local buffer
memcpy(fbs[ifb], _jpg_buf, _jpg_buf_len);
}
if (fb)
{
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
}
else if (_jpg_buf)
{
free(_jpg_buf);
_jpg_buf = NULL;
}
// Let other tasks run and wait until the end of the current frame rate interval (if any time left)
taskYIELD();
vTaskDelayUntil(&xLastWakeTime, xFrequency);
// Only switch frames around if no frame is currently being streamed to a client
// Wait on a semaphore until client operation completes
xSemaphoreTake(frameSync, portMAX_DELAY);
// Do not allow interrupts while switching the current frame
portENTER_CRITICAL(&xSemaphore);
camBuf = fbs[ifb];
camSize = _jpg_buf_len;
ifb++;
ifb &= 1; // this should produce 1, 0, 1, 0, 1 ... sequence
portEXIT_CRITICAL(&xSemaphore);
// Let anyone waiting for a frame know that the frame is ready
xSemaphoreGive(frameSync);
// Technically only needed once: let the streaming task know that we have at least one frame
// and it could start sending frames to the clients, if any
xTaskNotifyGive(tStream);
// Immediately let other (streaming) tasks run
taskYIELD();
// If streaming task has suspended itself (no active clients to stream to)
// there is no need to grab frames from the camera. We can save some juice
// by suspedning the tasks
if (eTaskGetState(tStream) == eSuspended)
{
vTaskSuspend(NULL); // passing NULL means "suspend yourself"
}
}
}
// ==== STREAMING ======================================================
const char HEADER[] = "HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n";
const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n";
const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: ";
const int hdrLen = strlen(HEADER);
const int bdrLen = strlen(BOUNDARY);
const int cntLen = strlen(CTNTTYPE);
// ==== Handle connection request from clients ===============================
void handleJPGSstream(void)
{
Serial.println("Handle stream request");
// Can only acommodate 10 clients. The limit is a default for WiFi connections
if (!uxQueueSpacesAvailable(streamingClients))
return;
// Create a new WiFi Client object to keep track of this one
WiFiClient *client = new WiFiClient();
*client = server.client();
// Immediately send this client a header
client->write(HEADER, hdrLen);
client->write(BOUNDARY, bdrLen);
// Push the client to the streaming queue
xQueueSend(streamingClients, (void *)&client, 0);
// Wake up streaming tasks, if they were previously suspended:
if (eTaskGetState(tCam) == eSuspended)
vTaskResume(tCam);
if (eTaskGetState(tStream) == eSuspended)
vTaskResume(tStream);
}
// ==== Actually stream content to all connected clients ========================
void streamCB(void *pvParameters)
{
Serial.println("Stream request");
char buf[16];
TickType_t xLastWakeTime;
TickType_t xFrequency;
// Wait until the first frame is captured and there is something to send
// to clients
ulTaskNotifyTake(pdTRUE, /* Clear the notification value before exiting. */
portMAX_DELAY); /* Block indefinitely. */
xLastWakeTime = xTaskGetTickCount();
for (;;)
{
// Default assumption we are running according to the FPS
xFrequency = pdMS_TO_TICKS(1000 / FPS);
// Only bother to send anything if there is someone watching
UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients);
if (activeClients)
{
// Adjust the period to the number of connected clients
xFrequency /= activeClients;
Serial.printf("ActiveClients %d\n", activeClients);
// Since we are sending the same frame to everyone,
// pop a client from the the front of the queue
WiFiClient *client;
xQueueReceive(streamingClients, (void *)&client, 0);
// Check if this client is still connected.
if (!client->connected())
{
// delete this client reference if s/he has disconnected
// and don't put it back on the queue anymore. Bye!
delete client;
}
else
{
// Ok. This is an actively connected client.
// Let's grab a semaphore to prevent frame changes while we
// are serving this frame
xSemaphoreTake(frameSync, portMAX_DELAY);
client->write(CTNTTYPE, cntLen);
sprintf(buf, "%d\r\n\r\n", camSize);
client->write(buf, strlen(buf));
client->write((char *)camBuf, (size_t)camSize);
client->write(BOUNDARY, bdrLen);
// Since this client is still connected, push it to the end
// of the queue for further processing
xQueueSend(streamingClients, (void *)&client, 0);
// The frame has been served. Release the semaphore and let other tasks run.
// If there is a frame switch ready, it will happen now in between frames
xSemaphoreGive(frameSync);
taskYIELD();
}
}
else
{
// Since there are no connected clients, there is no reason to waste battery running
vTaskSuspend(NULL);
}
// Let other tasks run after serving every client
taskYIELD();
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
const char JHEADER[] = "HTTP/1.1 200 OK\r\n"
"Content-disposition: inline; filename=capture.jpg\r\n"
"Content-type: image/jpeg\r\n\r\n";
const int jhdLen = strlen(JHEADER);
// ==== Serve up one JPEG frame =============================================
void handleJPG(void)
{
WiFiClient client = server.client();
if (!client.connected())
return;
client.write(JHEADER, jhdLen);
client.write((char *)camBuf, (size_t)camSize);
}
// ==== Handle invalid URL requests ============================================
void handleNotFound()
{
String message = "Server is running!\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
server.send(200, "text / plain", message);
}
// ==== SETUP method ==================================================================
void startStreamServer()
{
Serial.printf("Starting web server on port: '%d'\n", streamPort);
// Start mainstreaming RTOS task
xTaskCreatePinnedToCore(
mjpegCB,
"mjpeg",
4 * 1024,
NULL,
2,
&tMjpeg,
APP_CPU);
}