HTML to SPIFF template bootstrap 5

This commit is contained in:
Rene Zeldenthuis
2022-09-10 23:27:41 +02:00
parent 4381af77e9
commit ac8064f20c
8 changed files with 285 additions and 91 deletions

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,38 +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

@@ -6,12 +6,46 @@ typedef struct
{
const char *key;
const String value;
} template_substitution_t;
} template_variable_t;
template <typename T, size_t n>
inline String template_render(const char *format, T (&values)[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);

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,6 +9,7 @@
#include <camera_config.h>
#include <format_duration.h>
#include <format_si.h>
#include <SPIFFS.h>
#include <template_render.h>
#include <settings.h>
@@ -35,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()
{
@@ -43,42 +55,9 @@ void handle_root()
if (iotWebConf.handleCaptivePortal())
return;
const char *root_page_template =
"<!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>"
"<h2>Status page for {{ThingName}}</h2><hr />"
"<h3>ESP32</h3>"
"<ul>"
"<li>CPU model: {{ChipModel}}</li>"
"<li>CPU speed: {{CpuFreqMHz}}Mhz</li>"
"<li>Mac address: {{MacAddress}}</li>"
"<li>IPv4 address: {{IpV4}}</li>"
"<li>IPv6 address: {{IpV6}}</li>"
"</ul>"
"<h3>Settings</h3>"
"<ul>"
"<li>Camera type: {{CameraType}}</li>"
"<li>Frame size: {{FrameSize}}</li>"
"<li>Frame rate: {{FrameDuration}} ms ({{FrameFrequency}} f/s)</li>"
"<li>JPEG quality: {{JpegQuality}} (0-100)</li>"
"</ul>"
"<h3>Diagnostics</h3>"
"<ul>"
"<li>Uptime: {{Uptime}}</li>"
"<li>Free heap: {{FreeHeap}}b</li>"
"<li>Max free block: {{MaxAllocHeap}}b</li>"
"</ul>"
"<br/>camera stream: <a href=\"rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1\">rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1</a>"
"<br />"
"<br/>Go to <a href=\"config\">configure page</a> to change settings.";
const template_substitution_t root_page_substitutions[] = {
const template_variable_t substitutions[] = {
{"AppTitle", APP_TITLE},
{"AppVersion", APP_VERSION},
{"ThingName", iotWebConf.getThingName()},
{"ChipModel", ESP.getChipModel()},
{"CpuFreqMHz", String(ESP.getCpuFreqMHz())},
@@ -93,38 +72,41 @@ void handle_root()
{"Uptime", String(format_duration(millis() / 1000))},
{"FreeHeap", format_si(ESP.getFreeHeap())},
{"MaxAllocHeap", format_si(ESP.getMaxAllocHeap())},
{"RtspPort", String(RTSP_PORT)}};
{"RtspPort", String(RTSP_PORT)},
{"ConfigChanged", String(config_changed)},
{"CameraInitialized", String(camera_initialized)}};
auto html = template_render(root_page_template, root_page_substitutions);
if (config_changed)
html += "<br />"
"<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);
}
void handle_restart()
{
log_v("Handle restart");
if (!config_changed)
{
// Redirect to root page.
web_server.sendHeader("Location", "/", true);
web_server.send(302, "text/plain", "");
return;
}
// if (!config_changed)
// {
// Redirect to root page.
// web_server.sendHeader("Location", "/", true);
// web_server.send(302, "text/plain", "");
// return;
// }
const char *html =
"<h2>Restarting...</h2>"
"<!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>";
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);
ESP.restart();
sleep(1000);
// ESP.restart();
}
void on_config_saved()
@@ -152,12 +134,14 @@ 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_duration_val, jpeg_quality_val);
return;
}
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
@@ -187,6 +171,9 @@ void setup()
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);
@@ -202,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(); });