282 lines
12 KiB
C++
282 lines
12 KiB
C++
|
#include "web_server.h"
|
||
|
|
||
|
namespace config {
|
||
|
// Values should be defined in config.h
|
||
|
#ifdef HTTP_USER
|
||
|
const char *http_user = HTTP_USER;
|
||
|
#else
|
||
|
const char *http_user = "";
|
||
|
#endif
|
||
|
#ifdef HTTP_PASSWORD
|
||
|
const char *http_password = HTTP_PASSWORD;
|
||
|
#else
|
||
|
const char *http_password = "";
|
||
|
#endif
|
||
|
|
||
|
}
|
||
|
|
||
|
namespace web_server {
|
||
|
|
||
|
const char *header_template;
|
||
|
const char *body_template;
|
||
|
const char *script_template;
|
||
|
void handleWebServerRoot();
|
||
|
void handlePageNotFound();
|
||
|
|
||
|
#ifdef AMPEL_CSV
|
||
|
void handleDeleteCSV();
|
||
|
void handleWebServerCSV();
|
||
|
#endif
|
||
|
|
||
|
#if defined(ESP8266)
|
||
|
ESP8266WebServer http(80); // Create a webserver object that listens for HTTP request on port 80
|
||
|
#elif defined(ESP32)
|
||
|
WebServer http(80);
|
||
|
#endif
|
||
|
|
||
|
void update() {
|
||
|
http.handleClient(); // Listen for HTTP requests from clients
|
||
|
}
|
||
|
|
||
|
void initialize() {
|
||
|
header_template =
|
||
|
PSTR("<!doctype html><html lang=en>"
|
||
|
"<head>\n"
|
||
|
"<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
|
||
|
"<meta charset='UTF-8'>\n"
|
||
|
// HfT Favicon
|
||
|
"<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
|
||
|
// Responsive grid:
|
||
|
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/pure-min.css'>\n"
|
||
|
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css'>\n"
|
||
|
// JS Graphs:
|
||
|
"<script src='https://cdn.plot.ly/plotly-basic-1.58.2.min.js'></script>\n"
|
||
|
// Fullscreen
|
||
|
"<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
|
||
|
// Refresh after every measurement.
|
||
|
// "<meta http-equiv='refresh' content='%d'>\n"
|
||
|
"</head>\n"
|
||
|
"<body>\n"
|
||
|
|
||
|
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>MakerLab Murnau e.V. CO<sub>2</sub> Ampel</p></div></div>\n"
|
||
|
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
|
||
|
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
|
||
|
#ifdef AMPEL_CSV
|
||
|
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
|
||
|
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
|
||
|
"<li class='pure-menu-item'><a href='./%s' class='pure-menu-link'>Download CSV</a></li>\n"
|
||
|
#endif
|
||
|
"<li class='pure-menu-item' id='led'>⬤</li>\n" // LED
|
||
|
"</ul></div></div>\n"
|
||
|
"<script>\n"
|
||
|
// Show a colored dot on the webpage, with a similar color than on LED Ring.
|
||
|
"hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;\n"
|
||
|
"document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n"
|
||
|
"</script>\n");
|
||
|
|
||
|
body_template =
|
||
|
PSTR("<div class='pure-g'>\n"
|
||
|
"<div class='pure-u-1' id='graph'></div>\n" // Graph placeholder
|
||
|
"</div>\n"
|
||
|
"<div class='pure-g'>\n"
|
||
|
//Sensor table
|
||
|
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n"
|
||
|
"<tr><th colspan='2'>%s</th></tr>\n"
|
||
|
"<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
|
||
|
"<tr><td>Temperature</td><td>%.1f℃</td></tr>\n"
|
||
|
"<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
|
||
|
"<tr><td>Last measurement</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Measurement timestep</td><td>%5d s</td></tr>\n"
|
||
|
#ifdef AMPEL_CSV
|
||
|
"<tr><th colspan='2'>CSV</th></tr>\n"
|
||
|
"<tr><td>Last write</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
|
||
|
"<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
|
||
|
#endif
|
||
|
#ifdef AMPEL_MQTT
|
||
|
"<tr><th colspan='2'>MQTT</th></tr>\n"
|
||
|
"<tr><td>Connected?</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Last publish</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
|
||
|
#endif
|
||
|
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||
|
"<tr><th colspan='2'>LoRaWAN</th></tr>\n"
|
||
|
"<tr><td>Connected?</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Frequency</td><td>%s MHz</td></tr>\n"
|
||
|
"<tr><td>Last transmission</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
|
||
|
#endif
|
||
|
"<tr><th colspan='2'>Sensor</th></tr>\n"
|
||
|
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
|
||
|
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
|
||
|
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
|
||
|
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
|
||
|
"<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
|
||
|
"<tr><td>Board</td><td>%s</td></tr>\n"
|
||
|
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
|
||
|
"</table>\n"
|
||
|
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
|
||
|
#ifdef AMPEL_CSV
|
||
|
"<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">"
|
||
|
"<input type='submit' value='Delete CSV'/>"
|
||
|
"</form>\n"
|
||
|
#endif
|
||
|
"</div>\n");
|
||
|
|
||
|
script_template =
|
||
|
PSTR(
|
||
|
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>\n"
|
||
|
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>\n"
|
||
|
#ifdef AMPEL_CSV
|
||
|
"<script>\n"
|
||
|
"document.body.style.cursor = 'default';\n"
|
||
|
"fetch('./%s',{credentials:'include'})\n"
|
||
|
".then(response=>response.text())\n"
|
||
|
".then(csvText=>csvToTable(csvText))\n"
|
||
|
".then(htmlTable=>addLogTableToPage(htmlTable))\n"
|
||
|
".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))\n"
|
||
|
".catch(e=>console.error(e));\n"
|
||
|
"xs=[];\n"
|
||
|
"data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}},\n"
|
||
|
"{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}},\n"
|
||
|
"{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];\n"
|
||
|
"layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},\n"
|
||
|
"xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},\n"
|
||
|
"yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},\n"
|
||
|
"yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}\n"
|
||
|
"};\n"
|
||
|
"function csvToTable(csvText) {\n"
|
||
|
"csvText=csvText.trim();\n"
|
||
|
"lines=csvText.split('\\n');\n"
|
||
|
"table=document.createElement('table');\n"
|
||
|
"table.className='pure-table-striped';\n"
|
||
|
"n=lines.length;\n"
|
||
|
"lines.forEach((line,i)=>{\n"
|
||
|
"fields=line.split(';');\n"
|
||
|
"xs.push(fields[0]);\n"
|
||
|
"data[0]['y'].push(fields[1]);\n"
|
||
|
"data[1]['y'].push(fields[2]);\n"
|
||
|
"data[2]['y'].push(fields[3]);\n"
|
||
|
"if(i>4 && i<n-12){if(i==5){fields=['...','...','...','...']}else{return;}}\n"
|
||
|
"row=document.createElement('tr');\n"
|
||
|
"fields.forEach((field,index)=>{\n"
|
||
|
"cell=document.createElement(i<2?'th':'td');\n"
|
||
|
"cell.appendChild(document.createTextNode(field));\n"
|
||
|
"row.appendChild(cell);});\n"
|
||
|
"table.appendChild(row);});\n"
|
||
|
"return table;}\n"
|
||
|
"function addLogTableToPage(table){document.getElementById('log').appendChild(table);}\n"
|
||
|
"</script>\n"
|
||
|
#endif
|
||
|
"</body>\n"
|
||
|
"</html>");
|
||
|
|
||
|
// Web-server
|
||
|
http.on("/", handleWebServerRoot);
|
||
|
#ifdef AMPEL_CSV
|
||
|
http.on("/" + csv_writer::filename, handleWebServerCSV);
|
||
|
http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
|
||
|
#endif
|
||
|
http.onNotFound(handlePageNotFound);
|
||
|
http.begin();
|
||
|
|
||
|
Serial.print(F("You can access this sensor via http://"));
|
||
|
Serial.print(SENSOR_ID);
|
||
|
Serial.print(F(".local (might be unstable) or http://"));
|
||
|
Serial.println(WiFi.localIP());
|
||
|
}
|
||
|
|
||
|
// Allow access if http_user or http_password are empty, or if provided credentials match
|
||
|
bool shouldBeAllowed() {
|
||
|
return strcmp(config::http_user, "") == 0 || strcmp(config::http_password, "") == 0
|
||
|
|| http.authenticate(config::http_user, config::http_password);
|
||
|
}
|
||
|
|
||
|
void handleWebServerRoot() {
|
||
|
if (!shouldBeAllowed()) {
|
||
|
return http.requestAuthentication(DIGEST_AUTH);
|
||
|
}
|
||
|
|
||
|
unsigned long ss = seconds();
|
||
|
uint8_t dd = ss / 86400;
|
||
|
ss -= dd * 86400;
|
||
|
unsigned int hh = ss / 3600;
|
||
|
ss -= hh * 3600;
|
||
|
uint8_t mm = ss / 60;
|
||
|
ss -= mm * 60;
|
||
|
|
||
|
//NOTE: Splitting in multiple parts in order to use less RAM
|
||
|
char content[2000]; // Update if needed
|
||
|
// Header size : 1611 - Body size : 1800 - Script size : 1920
|
||
|
|
||
|
// Header
|
||
|
snprintf_P(content, sizeof(content), header_template, sensor::co2, SENSOR_ID.c_str(),
|
||
|
WiFi.localIP().toString().c_str()
|
||
|
#ifdef AMPEL_CSV
|
||
|
, csv_writer::filename.c_str()
|
||
|
#endif
|
||
|
);
|
||
|
|
||
|
http.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||
|
http.send_P(200, PSTR("text/html"), content);
|
||
|
|
||
|
// Body
|
||
|
snprintf_P(content, sizeof(content), body_template, SENSOR_ID.c_str(), sensor::co2, sensor::temperature,
|
||
|
sensor::humidity, sensor::timestamp.c_str(), config::measurement_timestep,
|
||
|
#ifdef AMPEL_CSV
|
||
|
csv_writer::last_successful_write.c_str(), config::csv_interval, csv_writer::getAvailableSpace() / 1024,
|
||
|
#endif
|
||
|
#ifdef AMPEL_MQTT
|
||
|
mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish.c_str(), config::sending_interval,
|
||
|
#endif
|
||
|
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||
|
lorawan::connected ? "Yes" : "No", LMIC_FREQUENCY_PLAN, lorawan::last_transmission.c_str(),
|
||
|
config::lorawan_sending_interval,
|
||
|
#endif
|
||
|
config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(),
|
||
|
WiFi.localIP().toString().c_str(), get_free_heap_size(), max_loop_duration, BOARD, dd, hh, mm, ss);
|
||
|
|
||
|
http.sendContent(content);
|
||
|
|
||
|
// Script
|
||
|
snprintf_P(content, sizeof(content), script_template
|
||
|
#ifdef AMPEL_CSV
|
||
|
, csv_writer::filename.c_str(), SENSOR_ID.c_str()
|
||
|
#endif
|
||
|
);
|
||
|
|
||
|
http.sendContent(content);
|
||
|
}
|
||
|
|
||
|
#ifdef AMPEL_CSV
|
||
|
void handleWebServerCSV() {
|
||
|
if (!shouldBeAllowed()) {
|
||
|
return http.requestAuthentication(DIGEST_AUTH);
|
||
|
}
|
||
|
if (FS_LIB.exists(csv_writer::filename)) {
|
||
|
fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
|
||
|
http.sendHeader("Content-Length", String(csv_file.size()));
|
||
|
http.streamFile(csv_file, F("text/csv"));
|
||
|
csv_file.close();
|
||
|
} else {
|
||
|
http.send(204, F("text/html"), F("No data available."));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void handleDeleteCSV() {
|
||
|
if (!shouldBeAllowed()) {
|
||
|
return http.requestAuthentication(DIGEST_AUTH);
|
||
|
}
|
||
|
Serial.print("Removing CSV file...");
|
||
|
FS_LIB.remove(csv_writer::filename);
|
||
|
Serial.println(" Done!");
|
||
|
http.sendHeader("Location", "/");
|
||
|
http.send(303);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
void handlePageNotFound() {
|
||
|
http.send(404, F("text/plain"), F("404: Not found"));
|
||
|
}
|
||
|
}
|