An error occurred while loading the file. Please try again.
web_server.cpp 12.28 KiB
#include "web_server.h"
#include "web_config.h"
#include "util.h"
#include "ntp.h"
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
#include "csv_writer.h"
#include "mqtt.h"
#include "lorawan.h"
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
#elif defined(ESP32)
#  include <WebServer.h>
#endif
namespace web_server {
  const char *header_template;
  const char *body1_template;
  const char *body2_template;
  const char *script_template;
  void handleWebServerRoot();
  void handlePageNotFound();
  void handleWebServerCommand();
  void handleDeleteCSV();
  void handleWebServerCSV();
  void definePages() {
    header_template =
        PSTR("<!doctype html><html lang=en>"
            "<head>"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>"
            "<meta charset='UTF-8'>"
            // HfT Favicon
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
            // Responsive grid:
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/pure-min.css'>"
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css'>"
            // JS Graphs:
            "<script src='https://cdn.plot.ly/plotly-basic-2.9.0.min.js'></script>"
            // Fullscreen
            "<meta name='viewport' content='width=device-width, initial-scale=1'>"
            // Refresh after every measurement.
            // "<meta http-equiv='refresh' content='%d'>"
            "</head>"
            "<body>"
            "<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</p></div></div>"
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>"
            "<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>"
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>"
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>");
    body1_template = PSTR("<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>"
        "<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>"
        "<li class='pure-menu-item' id='led'>&#11044;</li>" // LED
        "</ul></div></div>"
        "<script>"
        // 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;"
        "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');"
        "</script>"
        "<div class='pure-g'>"
        "<div class='pure-u-1' id='graph'></div>"// Graph placeholder
        "</div>"
        "<div class='pure-g'>"
        "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>"
"<tr><th colspan='2'>%s</th></tr>" "<tr><td>CO<sub>2</sub></td><td>%5d ppm</td></tr>" "<tr><td>Temperature</td><td>%.1f&#8451;</td></tr>" "<tr><td>Humidity</td><td>%.1f%%</td></tr>" "<tr><td>Last measurement</td><td>%s</td></tr>" "<tr><td>Timestep</td><td>%5d s</td></tr>" "<tbody %s>" "<tr><th colspan='2'>CSV</th></tr>" "<tr><td>Last write</td><td>%s</td></tr>" "<tr><td>Interval</td><td>%5d s</td></tr>" "<tr><td>Available space</td><td>%d kB</td></tr>" "</tbody>" "<tbody %s>" "<tr><th colspan='2'>MQTT</th></tr>" "<tr><td>Connected?</td><td>%s</td></tr>" "<tr><td>Last publish</td><td>%s</td></tr>" "<tr><td>Interval</td><td>%5d s</td></tr>" "</tbody>"); body2_template = PSTR( #if defined(ESP32) "<tbody %s>" "<tr><th colspan='2'>LoRaWAN</th></tr>" "<tr><td>Connected?</td><td>%s</td></tr>" "<tr><td>Frequency</td><td>%s MHz</td></tr>" "<tr><td>Last transmission</td><td>%s</td></tr>" "<tr><td>Interval</td><td>%5d s</td></tr>" "</tbody>" #endif "<tr><th colspan='2'>Sensor</th></tr>" "<tr><td>Temperature offset</td><td>%.1fK</td></tr>" "<tr><td>Auto-calibration?</td><td>%s</td></tr>" "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>" "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>" "<tr><td>MAC</td><td>%s</td></tr>" "<tr><td>Free heap space</td><td>%6d bytes</td></tr>" "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>" "<tr><td>Frag</td><td>%3d%%</td></tr>" "<tr><td>Max loop duration</td><td>%5d ms</td></tr>" "<tr><td>Board</td><td>%s</td></tr>" "<tr><td>ID</td><td>%s</td></tr>" "<tr><td>Ampel firmware</td><td>%s</td></tr>" "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>" "</table>" "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>" "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>" "<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>" "</div>" "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>&nbsp;" "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>"); script_template = PSTR("<script>" "document.body.style.cursor = 'default';" "fetch('%s',{credentials:'include'})" ".then(r=>r.text())" ".then(c2t)" ".then(addLog)" ".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))" ".catch(console.error);" "xs=[];" "data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}}," "{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}}," "{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];" "layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0}," "xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200}," "yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3}," "yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}"
"};" "function c2t(t){" "t=t.trim();" "ls=t.split('\\n');" "tb=document.createElement('table');" "tb.className='pure-table-striped';" "n=ls.length;" "ls.forEach((l,i)=>{" "fs=l.split(';');" //Don't display points without time "if (!fs[0].includes('1970-')){" "xs.push(fs[0]);" "data[0]['y'].push(fs[1]);" "data[1]['y'].push(fs[2]);" "data[2]['y'].push(fs[3]);" "};" "if(i>4 && i<n-12){if(i==5){fs=['...','...','...','...']}else{return;}}" "r=document.createElement('tr');" "fs.forEach((f,_)=>{" "c=document.createElement(i<2?'th':'td');" "c.appendChild(document.createTextNode(f));" "r.appendChild(c);});" "tb.appendChild(r);});" "return tb;}" "function addLog(t){document.getElementById('log').appendChild(t);}" "</script>" "</body>" "</html>"); // Web-server web_config::http.on("/", handleWebServerRoot); web_config::http.on("/command", handleWebServerCommand); web_config::http.on(csv_writer::filename, handleWebServerCSV); web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV); } /* * Allow access if Ampel is in access point mode, * if http_user or http_password are empty, * or if provided credentials match */ bool shouldBeAllowed() { return wifi::isAccessPoint() || strcmp(config::http_user, "") == 0 || strcmp(config::ampel_password(), "") == 0 || web_config::http.authenticate(config::http_user, config::ampel_password()); } void handleWebServerRoot() { if (!shouldBeAllowed()) { return web_config::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. Higher than 2000 apparently crashes the ESP8266 char content[1700]; // Current size (with Lorawan, timesteps and long thing name): // INFO - Header size : 1269 - Body1 size : 1259 - Body2 size : 1620 - Script size : 1485 snprintf_P(content, sizeof(content), header_template, sensor::co2, config::ampel_name(), wifi::local_ip); Serial.print(F("INFO - Header size : ")); Serial.print(strlen(content)); web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN); web_config::http.send_P(200, PSTR("text/html"), content);
// Body snprintf_P(content, sizeof(content), body1_template, csv_writer::filename, config::ampel_name(), sensor::co2, sensor::temperature, sensor::humidity, sensor::timestamp, config::measurement_timestep, config::is_csv_active() ? "" : "hidden", csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024, config::is_mqtt_active() ? "" : "hidden", mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval); Serial.print(F(" - Body1 size : ")); Serial.print(strlen(content)); web_config::http.sendContent(content); snprintf_P(content, sizeof(content), body2_template, #if defined(ESP32) config::is_lorawan_active() ? "" : "hidden", lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission, config::lorawan_sending_interval, #endif config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", config::ampel_name(), config::ampel_name(), wifi::local_ip, wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(), esp_get_heap_fragmentation(), ampel.max_loop_duration, ampel.board, ampel.sensorId, ampel.version, dd, hh, mm, ss); Serial.print(F(" - Body2 size : ")); Serial.print(strlen(content)); web_config::http.sendContent(content); // Script snprintf_P(content, sizeof(content), script_template, csv_writer::filename, config::ampel_name()); Serial.print(F(" - Script size : ")); Serial.println(strlen(content)); web_config::http.sendContent(content); } void handleWebServerCSV() { if (!shouldBeAllowed()) { return web_config::http.requestAuthentication(DIGEST_AUTH); } if (FS_LIB.exists(csv_writer::filename)) { fs::File csv_file = FS_LIB.open(csv_writer::filename, "r"); char csv_size[10]; snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size()); web_config::http.sendHeader("Content-Length", csv_size); web_config::http.streamFile(csv_file, F("text/csv")); csv_file.close(); } else { web_config::http.send(204, F("text/html"), F("No data available.")); } } void handleDeleteCSV() { if (!shouldBeAllowed()) { return web_config::http.requestAuthentication(DIGEST_AUTH); } Serial.print(F("Removing CSV file...")); FS_LIB.remove(csv_writer::filename); Serial.println(F(" Done!")); web_config::http.sendHeader("Location", "/"); web_config::http.send(303); } void handleWebServerCommand() { if (!shouldBeAllowed()) { return web_config::http.requestAuthentication(DIGEST_AUTH); } web_config::http.sendHeader("Location", "/"); web_config::http.send(303); sensor_console::execute(web_config::http.arg("send").c_str()); }
void handlePageNotFound() { web_config::http.send(404, F("text/plain"), F("404: Not found")); } }