2021-05-09 18:45:59 +00:00
# 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 ( ) ;
2021-11-15 12:45:36 +00:00
void handleWebServerCommand ( ) ;
2021-05-09 18:45:59 +00:00
# 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 "
2021-11-15 12:45:36 +00:00
" <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> \n "
2021-05-09 18:45:59 +00:00
" <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 "
2021-11-15 12:45:36 +00:00
" <li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li> \n "
2021-05-09 18:45:59 +00:00
# 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 "
2021-11-15 12:45:36 +00:00
" </script> \n "
" <div class='pure-g'> \n "
" <div class='pure-u-1' id='graph'></div> \n " // Graph placeholder
2021-05-09 18:45:59 +00:00
" </div> \n "
" <div class='pure-g'> \n "
2021-11-15 12:45:36 +00:00
" <table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'> \n " ) ;
body_template =
PSTR ( " <tr><th colspan='2'>%s</th></tr> \n "
2021-05-09 18:45:59 +00:00
" <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?
2021-11-15 12:45:36 +00:00
" <tr><td>Auto-calibration?</td><td>%s</td></tr> \n "
2021-05-09 18:45:59 +00:00
" <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 "
2021-11-15 12:45:36 +00:00
" <tr><td>MAC</td><td>%s</td></tr> \n "
2021-05-09 18:45:59 +00:00
" <tr><td>Free heap space</td><td>%6d bytes</td></tr> \n "
2021-11-15 12:45:36 +00:00
" <tr><td>Largest heap block</td><td>%6d bytes</td></tr> \n "
2021-05-09 18:45:59 +00:00
" <tr><td>Max loop duration</td><td>%5d ms</td></tr> \n "
" <tr><td>Board</td><td>%s</td></tr> \n "
2021-11-15 12:45:36 +00:00
" <tr><td>Ampel firmware</td><td>%s</td></tr> \n "
2021-05-09 18:45:59 +00:00
" <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 "
2021-11-15 12:45:36 +00:00
" <form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form> \n "
2021-05-09 18:45:59 +00:00
# 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 "
2021-11-15 12:45:36 +00:00
" fetch('%s',{credentials:'include'}) \n "
2021-05-09 18:45:59 +00:00
" .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 ) ;
2021-11-15 12:45:36 +00:00
http . on ( " /command " , handleWebServerCommand ) ;
2021-05-09 18:45:59 +00:00
# ifdef AMPEL_CSV
2021-11-15 12:45:36 +00:00
http . on ( csv_writer : : filename , handleWebServerCSV ) ; //NOTE: csv_writer should have been initialized first.
2021-05-09 18:45:59 +00:00
http . on ( " /delete_csv " , HTTP_POST , handleDeleteCSV ) ;
# endif
http . onNotFound ( handlePageNotFound ) ;
http . begin ( ) ;
Serial . print ( F ( " You can access this sensor via http:// " ) ) ;
2021-11-15 12:45:36 +00:00
Serial . print ( ampel . sensorId ) ;
2021-05-09 18:45:59 +00:00
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
2021-11-15 12:45:36 +00:00
// INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
2021-05-09 18:45:59 +00:00
2021-11-15 12:45:36 +00:00
snprintf_P ( content , sizeof ( content ) , header_template , sensor : : co2 , ampel . sensorId , wifi : : local_ip
2021-05-09 18:45:59 +00:00
# ifdef AMPEL_CSV
2021-11-15 12:45:36 +00:00
, csv_writer : : filename
2021-05-09 18:45:59 +00:00
# endif
) ;
2021-11-15 12:45:36 +00:00
// Serial.print(F("INFO - Header size : "));
// Serial.print(strlen(content));
2021-05-09 18:45:59 +00:00
http . setContentLength ( CONTENT_LENGTH_UNKNOWN ) ;
http . send_P ( 200 , PSTR ( " text/html " ) , content ) ;
// Body
2021-11-15 12:45:36 +00:00
snprintf_P ( content , sizeof ( content ) , body_template , ampel . sensorId , sensor : : co2 , sensor : : temperature ,
sensor : : humidity , sensor : : timestamp , config : : measurement_timestep ,
2021-05-09 18:45:59 +00:00
# ifdef AMPEL_CSV
2021-11-15 12:45:36 +00:00
csv_writer : : last_successful_write , config : : csv_interval , csv_writer : : getAvailableSpace ( ) / 1024 ,
2021-05-09 18:45:59 +00:00
# endif
# ifdef AMPEL_MQTT
2021-11-15 12:45:36 +00:00
mqtt : : connected ? " Yes " : " No " , mqtt : : last_successful_publish , config : : mqtt_sending_interval ,
2021-05-09 18:45:59 +00:00
# endif
# if defined(AMPEL_LORAWAN) && defined(ESP32)
2021-11-15 12:45:36 +00:00
lorawan : : connected ? " Yes " : " No " , LMIC_FREQUENCY_PLAN , lorawan : : last_transmission ,
2021-05-09 18:45:59 +00:00
config : : lorawan_sending_interval ,
# endif
2021-11-15 12:45:36 +00:00
config : : temperature_offset , config : : auto_calibrate_sensor ? " Yes " : " No " , ampel . sensorId , ampel . sensorId ,
wifi : : local_ip , wifi : : local_ip , ampel . macAddress , ESP . getFreeHeap ( ) , esp_get_max_free_block_size ( ) ,
ampel . max_loop_duration , ampel . board , ampel . version , dd , hh , mm , ss ) ;
2021-05-09 18:45:59 +00:00
2021-11-15 12:45:36 +00:00
// Serial.print(F(" - Body size : "));
// Serial.print(strlen(content));
2021-05-09 18:45:59 +00:00
http . sendContent ( content ) ;
// Script
snprintf_P ( content , sizeof ( content ) , script_template
# ifdef AMPEL_CSV
2021-11-15 12:45:36 +00:00
, csv_writer : : filename , ampel . sensorId
2021-05-09 18:45:59 +00:00
# endif
) ;
2021-11-15 12:45:36 +00:00
// Serial.print(F(" - Script size : "));
// Serial.println(strlen(content));
2021-05-09 18:45:59 +00:00
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 " ) ;
2021-11-15 12:45:36 +00:00
char csv_size [ 10 ] ;
snprintf ( csv_size , sizeof ( csv_size ) , " %d " , csv_file . size ( ) ) ;
http . sendHeader ( " Content-Length " , csv_size ) ;
2021-05-09 18:45:59 +00:00
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 ) ;
}
2021-11-15 12:45:36 +00:00
Serial . print ( F ( " Removing CSV file... " ) ) ;
2021-05-09 18:45:59 +00:00
FS_LIB . remove ( csv_writer : : filename ) ;
2021-11-15 12:45:36 +00:00
Serial . println ( F ( " Done! " ) ) ;
2021-05-09 18:45:59 +00:00
http . sendHeader ( " Location " , " / " ) ;
http . send ( 303 ) ;
}
# endif
2021-11-15 12:45:36 +00:00
void handleWebServerCommand ( ) {
if ( ! shouldBeAllowed ( ) ) {
return http . requestAuthentication ( DIGEST_AUTH ) ;
}
http . sendHeader ( " Location " , " / " ) ;
http . send ( 303 ) ;
sensor_console : : execute ( http . arg ( " send " ) . c_str ( ) ) ;
}
2021-05-09 18:45:59 +00:00
void handlePageNotFound ( ) {
http . send ( 404 , F ( " text/plain " ) , F ( " 404: Not found " ) ) ;
}
}