Merge pull request #3 from rzeldent/feature/html_template

Feature/html template
This commit is contained in:
rzeldent
2022-09-10 23:56:39 +02:00
committed by GitHub
11 changed files with 365 additions and 78 deletions

View File

@@ -1,6 +1,6 @@
# ESP32CAM-RTSP
![example event parameter](https://github.com/rzeldent/esp32cam-rtsp/actions/workflows/main.yml/badge.svg?event=push)
![status badge](https://github.com/rzeldent/esp32cam-rtsp/actions/workflows/main.yml/badge.svg?event=push)
Simple [RTSP](https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol) server.
Easy configuration through the web interface. Stable.
@@ -63,7 +63,7 @@ When using an FTDI adapter, make sure the adapter is set to 3.3 volt before conn
After programming remove the wire to tge GPIO00 pin to exit the download mode.
## Compiling the software
## Compiling and deploying the software
Open a command line or terminal window and clone this repository from GitHub.
@@ -75,14 +75,22 @@ go into the folder
cd esp32cam-rtsp
```
Next, the software has to be compiled. Type:
Next, the firmware has to be build and deployed to the ESP32.
There are to flavours to do this; using the command line or the graphical interface of Visual Studio Code. I recommend to use VIsual Studio Code as it is free to use and offers more insight.
### Using the command line
First the source code and SPIFF partition (data) has to be compiled. Type:
```
pio run
pio run
pio run buildfs
```
When finished, make sure the ESP32-CAM is in download mode (see previous section) and type:
When finished, the SPIFF partition and software have to be uploaded.
Make sure the ESP32-CAM is in download mode (see previous section) and type:
```
pio run -t upload
pio run -t uploadfs
```
When done remove the jumper when using a FTDI adapter or press the reset button on the ESP32-CAM.
@@ -91,6 +99,18 @@ To monitor the output, start a terminal using:
pio device monitor
```
### Using Visual studio
Open the project in a new window. Run the following tasks using the ```Terminal -> Run Task``` or CTRL+ALT+T command in the menu. Make sure the ESP32-CAM is in download mode during the uploads.
- PlatformIO: Build Filesystem Image (esp32cam)
- PlatformIO: Build (esp32cam)
- PlatformIO: Upload Filesystem Image (esp32cam)
- PlatformIO: Upload (esp32cam)
To monitor the behavior run the task, run:
- PlatformIO: Monitor (esp32cam)
## Setting up the ESP32CAM-RTSP
After the programming of the ESP32, there is no configuration present. This needs to be added.
To connect initially to the device open the WiFi connections and select the WiFi network / accesspoint called **ESP32CAM-RTSP**.
@@ -100,30 +120,37 @@ After connecting, the browser should automatically open the status page.
In case this does not happens automatically, connect to [http://192.168.4.1](http://192.168.4.1).
This page will display the current settings and status. On the bottom, there is a link to the config. Click on this link.
This link brings up the configuration screen.
This link brings up the configuration screen when connecting fot the first time.
![Configuration screen](assets/Configuration.png)
Configure at least:
- The WiFi network settings. No dropdown is present to show available networks! Enter the Access point name manually.
- The access point to connect to. No dropdown is present to show available networks!
- A password for accessing the Access point (AP) when starting. (required)
- the type of the ESP32-CAM board
- Type of the ESP32-CAM board
When finished press Apply to save the configuration. The screen will redirect to the status screen.
When finished press ```Apply``` to save the configuration. The screen will redirect to the status screen.
Here it is possible to reboot the device so the settings take effect.
It is also possible to restart manually by pressing the reset button.
## Connecting to the configuration
After the initial configuration and the device is connected to an Access point, the device can be configured over http.
When a connection is made to [http://esp32cam-rtsp](http://esp32cam-rtsp) the status screen is shown.
![Status screen](assets/index.png)
In case changes have been made to the configuration, this is shown and the possibility to restart is given.
Clicking on the ```change configuration``` button will open the configuration. It is possible that a password dialog is shown before entering.
If this happens, for the user enter 'admin' and for the password the value that has been configured as the Access Point password.
## Connecting to the RTSP stream
RTSP stream is available at: [rtsp://esp32cam-rtsp.local:554/mjpeg/1](rtsp://esp32cam-rtsp.local:554/mjpeg/1).
This link can be opened with for example [VLC](https://www.videolan.org/vlc/).
**Please be aware that there is no password present on the stream!**
## Connecting to the configuration
When a connection is made to [http://esp32cam-rtsp](http://esp32cam-rtsp) the status screen is shown.
Clicking on the configuration link will open the configuration. It is possible that a password dialog is shown.
For the user enter 'admin' and for the password the value that has been configured as the Access point password.
:warning: **Please be aware that there is no password present on the stream!**
## Credits
esp32cam-ready depends on PlatformIO and Micro-RTSP by Kevin Hester.
esp32cam-ready depends on PlatformIO, Bootstap5 and Micro-RTSP by Kevin Hester.

BIN
assets/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

7
data/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

82
data/index.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css">
<title>{{AppTitle}} v{{AppVersion}}</title>
</head>
<body>
<div class="container">
<h1>{{ThingName}}</h1>
<hr>
{{#ConfigChanged}}
<div class="alert alert-danger" role="alert">
<p>The configuration has been changed. It is recommended to restart the device</p>
<p>This can be done by pressing the restart button below</p>
<button type="button" class="btn btn-danger" onclick="location.href='restart'">Restart</button>
</div>
{{/ConfigChanged}}
<div class="card bg-light mb-3">
<h5 class="card-header">ESP32</h5>
<div class="card-body">
<p>CPU model: {{ChipModel}}</p>
<p>CPU speed: {{CpuFreqMHz}}Mhz</p>
<p>Mac address: {{MacAddress}}</p>
<p>IPv4 address: {{IpV4}}</p>
<p>IPv6 address: {{IpV6}}</p>
</ul>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Settings</h5>
<div class="card-body">
<p>Camera type: {{CameraType}}</p>
<p>Frame size: {{FrameSize}}</p>
<p>Frame rate: {{FrameDuration}} ms ({{FrameFrequency}} f/s)</p>
<p>JPEG quality: {{JpegQuality}} (0-100)</p>
{{#CameraInitialized}}
<div class="alert alert-success" role="alert">
Camera was initialized successfully!
</div>
{{/CameraInitialized}}
{{^CameraInitialized}}
<div class="alert alert-danger" role="alert">
Failed to initialize the camera! No streaming possible. Please change the camera settings and
restart
</div>
{{/CameraInitialized}}
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Diagnostics</h5>
<div class="card-body">
<p>Uptime: {{Uptime}}</p>
<p>Free heap: {{FreeHeap}}b</p>
<p>Max free block: {{MaxAllocHeap}}b</p>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Camera stream</h5>
<div class="card-body">
The camera stream can be found at the following location:
<a
href="rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1">rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1</a>
</div>
</div>
<button type="button" class="btn btn-lg btn-warning" onclick="location.href='config'">Change
configuration</button>
</div>
</body>
</html>

29
data/restart.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css">
<title>{{AppTitle}} v{{AppVersion}}</title>
</head>
<body>
<div class="container">
<h1>{{ThingName}}</h1>
<hr>
<div class="jumbotron bg-light">
<h1 class="display-4">Restart</h1>
<p class="lead">The device is currently restarting. Please stand by...</p>
<hr class="my-4">
<p>In some cases, the device requires a hard reset (power cycle).</p>
<div class="d-flex justify-content-center">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Restarting...</span>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -11,11 +11,86 @@ typedef struct camera_config_entry
const camera_config_t config;
} camera_config_entry_t;
constexpr camera_config_t esp32cam_settings = {
.pin_pwdn = -1,
.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_SVGA,
.jpeg_quality = 12,
.fb_count = 2};
constexpr camera_config_t esp32cam_aithinker_settings = {
.pin_pwdn = 32,
.pin_reset = -1,
.pin_xclk = 0,
.pin_sscb_sda = 26,
.pin_sscb_scl = 27,
.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_SVGA,
.jpeg_quality = 12,
.fb_count = 2};
constexpr camera_config_t esp32cam_ttgo_t_settings = {
.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,
.fb_count = 2};
constexpr const camera_config_entry_t camera_configs[] = {
{"ESP32CAM", {.pin_pwdn = -1, .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_SVGA, .jpeg_quality = 12, .fb_count = 2}},
{"AI THINKER", {.pin_pwdn = 32, .pin_reset = -1, .pin_xclk = 0, .pin_sscb_sda = 26, .pin_sscb_scl = 27, .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_SVGA, .jpeg_quality = 12, .fb_count = 2}},
{"TTGO T-CAM", {.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, .fb_count = 2}}};
{"ESP32CAM", esp32cam_settings},
{"AI THINKER", esp32cam_aithinker_settings},
{"TTGO T-CAM", esp32cam_ttgo_t_settings}};
const camera_config_t lookup_camera_config(const char *pin)
{

View File

@@ -9,6 +9,6 @@
#define RTSP_PORT 554
#define DEFAULT_CAMERA_CONFIG "AI THINKER"
#define DEFAULT_FRAMERATE "20"
#define DEFAULT_FRAMEDURATION "20"
#define DEFAULT_FRAMESIZE "SVGA (800x600)"
#define DEFAULT_JPEG_QUALITY "12"

View File

@@ -0,0 +1,5 @@
{
"name": "template_render",
"version": "1.0.0",
"description": "A mini template renderer"
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <Arduino.h>
typedef struct
{
const char *key;
const String value;
} template_variable_t;
template <typename T, size_t n>
inline String template_render(const String& format, T (&values)[n])
{
auto s = String(format);
// Conditional sections
for (size_t i = 0; i < n; i++)
{
// Include Section {{#expr}}
auto match_section_begin = "{{#" + String(values[i].key) + "}}";
// Inverted section {{^expr}}
auto match_section_inverted_begin = "{{^" + String(values[i].key) + "}}";
// End section {{/expr}}
auto match_section_end = "{{/" + String(values[i].key) + "}}";
while (true)
{
bool inverted = false;
auto first = s.indexOf(match_section_begin);
if (first < 0)
{
inverted = true;
first = s.indexOf(match_section_inverted_begin);
if (first < 0)
break;
}
auto second = s.indexOf(match_section_end, first + match_section_begin.length());
if (second < 0)
break;
// Arduino returns 0 and 1 for bool.toString()
if ((!inverted && (values[i].value == "1")) || (inverted && (values[i].value == "0")))
s = s.substring(0, first) + s.substring(first + match_section_begin.length(), second) + s.substring(second + match_section_end.length());
else
s = s.substring(0, first) + s.substring(second + match_section_end.length());
}
}
// Replace variables {{variable}}
for (size_t i = 0; i < n; i++)
s.replace("{{" + String(values[i].key) + "}}", values[i].value);
return s;
}

View File

@@ -14,6 +14,8 @@ board = esp32cam
framework = arduino
monitor_speed = 115200
monitor_rts = 0
monitor_dtr = 0
monitor_filters = log2file, time, default
build_flags =

View File

@@ -9,16 +9,18 @@
#include <camera_config.h>
#include <format_duration.h>
#include <format_si.h>
#include <SPIFFS.h>
#include <template_render.h>
#include <settings.h>
char camera_config_val[sizeof(camera_config_entry)];
char frame_rate_val[6];
char frame_duration_val[6];
char frame_size_val[sizeof(frame_size_entry_t)];
char jpeg_quality_val[4];
auto config_group_stream_settings = iotwebconf::ParameterGroup("settings", "Streaming settings");
auto config_camera_config = iotwebconf::SelectParameter("Camera config", "config", camera_config_val, sizeof(camera_config_val), (const char *)camera_configs, (const char *)camera_configs, sizeof(camera_configs) / sizeof(camera_configs[0]), sizeof(camera_configs[0]), DEFAULT_CAMERA_CONFIG);
auto config_frame_rate = iotwebconf::NumberParameter("Frame rate (ms)", "fr", frame_rate_val, sizeof(frame_rate_val), DEFAULT_FRAMERATE, nullptr, "min=\"10\"");
auto config_frame_rate = iotwebconf::NumberParameter("Frame duration (ms)", "fd", frame_duration_val, sizeof(frame_duration_val), DEFAULT_FRAMEDURATION, nullptr, "min=\"10\"");
auto config_frame_size = iotwebconf::SelectParameter("Frame size", "fs", frame_size_val, sizeof(frame_size_val), (const char *)frame_sizes, (const char *)frame_sizes, sizeof(frame_sizes) / sizeof(frame_sizes[0]), sizeof(frame_sizes[0]), DEFAULT_FRAMESIZE);
auto config_jpg_quality = iotwebconf::NumberParameter("JPEG quality", "q", jpeg_quality_val, sizeof(jpeg_quality_val), DEFAULT_JPEG_QUALITY, nullptr, "min=\"1\" max=\"100\"");
@@ -34,6 +36,17 @@ IotWebConf iotWebConf(WIFI_SSID, &dnsServer, &web_server, WIFI_PASSWORD, CONFIG_
// Keep track of config changes. This will allow a reset of the device
bool config_changed = false;
// Check if camera is initialized
bool camera_initialized = false;
void stream_file(const char *spiffs_file, const char *mime_type)
{
// Cache for 86400 seconds (one day)
web_server.sendHeader("Cache-Control", "max-age=86400");
auto file = SPIFFS.open(spiffs_file);
web_server.streamFile(file, mime_type);
file.close();
}
void handle_root()
{
@@ -42,51 +55,31 @@ void handle_root()
if (iotWebConf.handleCaptivePortal())
return;
auto url = "rtsp://" + String(iotWebConf.getThingName()) + ".local:" + String(RTSP_PORT) + "/mjpeg/1";
const template_variable_t substitutions[] = {
{"AppTitle", APP_TITLE},
{"AppVersion", APP_VERSION},
{"ThingName", iotWebConf.getThingName()},
{"ChipModel", ESP.getChipModel()},
{"CpuFreqMHz", String(ESP.getCpuFreqMHz())},
{"MacAddress", WiFi.macAddress()},
{"IpV4", WiFi.localIP().toString()},
{"IpV6", WiFi.localIPv6().toString()},
{"CameraType", camera_config_val},
{"FrameSize", frame_size_val},
{"FrameDuration", frame_duration_val},
{"FrameFrequency", String(1000.0 / atol(frame_duration_val), 1)},
{"JpegQuality", jpeg_quality_val},
{"Uptime", String(format_duration(millis() / 1000))},
{"FreeHeap", format_si(ESP.getFreeHeap())},
{"MaxAllocHeap", format_si(ESP.getMaxAllocHeap())},
{"RtspPort", String(RTSP_PORT)},
{"ConfigChanged", String(config_changed)},
{"CameraInitialized", String(camera_initialized)}};
String html;
html += "<!DOCTYPE html><html lang=\"en\">"
"<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/>"
"<head><title>" APP_TITLE " v" APP_VERSION "</title></head>"
"<body>";
html += "<h2>Status page for " + String(iotWebConf.getThingName()) + "</h2><hr />";
html += "<h3>ESP32</h3>";
html += "<ul>";
html += "<li>CPU model: " + String(ESP.getChipModel()) + "</li>";
html += "<li>CPU speed: " + String(ESP.getCpuFreqMHz()) + "Mhz</li>";
html += "<li>Mac address: " + WiFi.macAddress() + "</li>";
html += "<li>IPv4 address: " + WiFi.localIP().toString() + "</li>";
html += "<li>IPv6 address: " + WiFi.localIPv6().toString() + "</li>";
html += "</ul>";
html += "<h3>Settings</h3>";
html += "<ul>";
html += "<li>Camera type: " + String(camera_config_val) + "</li>";
html += "<li>Frame size: " + String(frame_size_val) + "</li>";
html += "<li>Frame rate: " + String(frame_rate_val) + " ms (" + String(1000.0 / atol(frame_rate_val), 1) + " f/s)</li>";
html += "<li>JPEG quality: " + String(jpeg_quality_val) + " (0-100)</li>";
html += "</ul>";
html += "<h3>Diagnostics</h3>";
html += "<ul>";
html += "<li>Uptime: " + String(format_duration(millis() / 1000)) + "</li>";
html += "<li>Free heap: " + format_si(ESP.getFreeHeap()) + "b</li>";
html += "<li>Max free block: " + format_si(ESP.getMaxAllocHeap()) + "b</li>";
html += "</ul>";
html += "<br/>camera stream: <a href=\"" + url + "\">" + url + "</a>";
html += "<br />";
html += "<br/>Go to <a href=\"config\">configure page</a> to change settings.";
if (config_changed)
{
html += "<br />";
html += "<br/><h3 style=\"color:red\">Configuration has changed. Please <a href=\"restart\">restart</a> the device.</h3>";
}
html += "</body></html>";
web_server.sendHeader("Cache-Control", "no-cache");
auto file = SPIFFS.open("/index.html");
auto html = template_render(file.readString(), substitutions);
file.close();
web_server.send(200, "text/html", html);
}
@@ -95,20 +88,24 @@ void handle_restart()
log_v("Handle restart");
if (!config_changed)
{
// Redirect to root page.
// Redirect to root page
web_server.sendHeader("Location", "/", true);
web_server.send(302, "text/plain", "");
return;
}
String html;
html += "<h2>Restarting...</h2>";
html += "<!DOCTYPE html><html lang=\"en\"><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/>";
html += "<head><title>" APP_TITLE " v" APP_VERSION "</title></head>";
html += "<body>";
const template_variable_t substitutions[] = {
{"AppTitle", APP_TITLE},
{"AppVersion", APP_VERSION},
{"ThingName", iotWebConf.getThingName()}};
web_server.sendHeader("Cache-Control", "no-cache");
auto file = SPIFFS.open("/restart.html");
auto html = template_render(file.readString(), substitutions);
file.close();
web_server.send(200, "text/html", html);
log_v("Restarting... Press refresh to connect again");
sleep(250);
sleep(1000);
ESP.restart();
}
@@ -127,7 +124,7 @@ bool initialize_camera()
auto frame_size = lookup_frame_size(frame_size_val);
log_i("JPEG quality: %s", jpeg_quality_val);
auto jpeg_quality = atoi(jpeg_quality_val);
log_i("Framerate: %s ms", frame_rate_val);
log_i("Frame rate: %s ms", frame_duration_val);
camera_config.frame_size = frame_size;
camera_config.jpeg_quality = jpeg_quality;
@@ -137,13 +134,15 @@ bool initialize_camera()
void start_rtsp_server()
{
log_v("start_rtsp_server");
if (!initialize_camera())
camera_initialized = initialize_camera();
if (!camera_initialized)
{
log_e("Failed to initialize camera. Type: %s, frame size: %s, frame rate: %s ms, jpeg quality: %s", camera_config_val, frame_size_val, frame_rate_val, jpeg_quality_val);
log_e("Failed to initialize camera. Type: %s, frame size: %s, frame rate: %s ms, jpeg quality: %s", camera_config_val, frame_size_val, frame_duration_val, jpeg_quality_val);
return;
}
auto frame_rate = atol(frame_rate_val);
log_i("Camera initialized");
auto frame_rate = atol(frame_duration_val);
camera_server = std::unique_ptr<rtsp_server>(new rtsp_server(cam, frame_rate, RTSP_PORT));
// Add service to mDNS - rtsp
MDNS.addService("rtsp", "tcp", 554);
@@ -168,10 +167,13 @@ void setup()
Serial.setDebugOutput(true);
#endif
log_i("CPU Freq = %d Mhz", getCpuFrequencyMhz());
log_i("CPU Freq: %d Mhz", getCpuFrequencyMhz());
log_i("Free heap: %d bytes", ESP.getFreeHeap());
log_i("Starting " APP_TITLE "...");
if (!SPIFFS.begin())
log_e("Error while mounting SPIFFS. Please upload the filesystem");
config_group_stream_settings.addItem(&config_camera_config);
config_group_stream_settings.addItem(&config_frame_rate);
config_group_stream_settings.addItem(&config_frame_size);
@@ -187,6 +189,11 @@ void setup()
web_server.on("/config", []
{ iotWebConf.handleConfig(); });
web_server.on("/restart", HTTP_GET, handle_restart);
// bootstrap
web_server.on("/bootstrap.min.css", HTTP_GET, []()
{ stream_file("/bootstrap.min.css", "text/css"); });
web_server.onNotFound([]()
{ iotWebConf.handleNotFound(); });