Removed face detection code, refactored to allow for multiple clients on streams
This commit is contained in:
parent
e2dfb773d7
commit
3570a9cfac
1015
app_httpd.cpp
1015
app_httpd.cpp
File diff suppressed because it is too large
Load Diff
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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_
|
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in New Issue