web_server.cpp 13 KB
Newer Older
1
2
#include "web_server.h"

Eric Duminil's avatar
Eric Duminil committed
3
#include "web_config.h"
Eric Duminil's avatar
Eric Duminil committed
4
5
#include "config.h"
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
6
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
7
8
9
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
10
#include "csv_writer.h"
Eric Duminil's avatar
Eric Duminil committed
11
#include "mqtt.h"
Eric Duminil's avatar
Eric Duminil committed
12
#include "lorawan.h"
Eric Duminil's avatar
Eric Duminil committed
13

Eric Duminil's avatar
Eric Duminil committed
14
#include <IotWebConf.h>
Eric Duminil's avatar
Eric Duminil committed
15
#include <IotWebConfUsing.h> // This loads aliases for easier class names.
16
#include <IotWebConfTParameter.h>
Eric Duminil's avatar
Eric Duminil committed
17
18
19
20
21
22
23
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
#  include <ESP8266mDNS.h> //allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
#elif defined(ESP32)
#  include <WebServer.h>
#  include <ESPmDNS.h>
#endif
Eric Duminil's avatar
Eric Duminil committed
24

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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();
Eric Duminil's avatar
Eric Duminil committed
47
  void handleWebServerCommand();
48

49
  void handleDeleteCSV();
50
  void handleWebServerCSV();
51

Eric Duminil's avatar
Eric Duminil committed
52
  void definePages() {
53
54
55
56
    header_template =
        PSTR("<!doctype html><html lang=en>"
            "<head>\n"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
57
            "<meta charset='UTF-8'>\n"
58
            // HfT Favicon
59
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
60
61
62
63
64
65
66
67
68
69
70
71
            // 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"

Eric Duminil's avatar
Eric Duminil committed
72
            "<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"
73
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
Eric Duminil's avatar
Better    
Eric Duminil committed
74
            "<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>\n"
75
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
76
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
77
            "<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
Eric Duminil's avatar
Eric Duminil committed
78
            "<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>\n"
79
            "<li class='pure-menu-item' id='led'>&#11044;</li>\n"// LED
Eric Duminil's avatar
Eric Duminil committed
80
81
82
83
84
            "</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"
Eric Duminil's avatar
Eric Duminil committed
85
86
87
            "</script>\n"
            "<div class='pure-g'>\n"
            "<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
88
89
            "</div>\n"
            "<div class='pure-g'>\n"
Eric Duminil's avatar
Eric Duminil committed
90
91
92
93
            "<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"
94
95
96
97
98
            "<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
            "<tr><td>Temperature</td><td>%.1f&#8451;</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"
Eric Duminil's avatar
Eric Duminil committed
99
100
101
            "<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"
Eric Duminil's avatar
FIXMEs    
Eric Duminil committed
102
            "<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
103
            "<tr><th colspan='2'>MQTT</th></tr>\n"
104
            "<tr><td>Connected?</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
105
106
            "<tr><td>Last publish</td><td>%s</td></tr>\n"
            "<tr><td>Timestep</td><td>%5d s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
107
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
108
109
110
111
112
            "<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"
113
#endif
Eric Duminil's avatar
Eric Duminil committed
114
            "<tr><th colspan='2'>Sensor</th></tr>\n"
Eric Duminil's avatar
TODO    
Eric Duminil committed
115
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
116
            "<tr><td>Auto-calibration?</td><td>%s</td></tr>\n"
117
118
            "<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"
119
            "<tr><td>MAC</td><td>%s</td></tr>\n"
120
            "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
121
            "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
122
            "<tr><td>Frag</td><td>%3d%%</td></tr>\n"
123
124
            "<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
            "<tr><td>Board</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
125
            "<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
126
            "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
127
128
            "</table>\n"
            "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
Eric Duminil's avatar
Eric Duminil committed
129
            "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n"
130
131
132
            "<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"
Eric Duminil's avatar
Eric Duminil committed
133
            "</div>\n");
134

Eric Duminil's avatar
Eric Duminil committed
135
136
137
138
    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"
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
                "<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"
                "</body>\n"
                "</html>");
180
181

    // Web-server
Eric Duminil's avatar
Eric Duminil committed
182
183
    web_config::http.on("/", handleWebServerRoot);
    web_config::http.on("/command", handleWebServerCommand);
184
    web_config::http.on(csv_writer::filename, handleWebServerCSV);
Eric Duminil's avatar
Eric Duminil committed
185
    web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
186
187
188
189
190
  }

  // 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
Eric Duminil's avatar
Eric Duminil committed
191
        || web_config::http.authenticate(config::http_user, config::http_password);
192
193
194
  }

  void handleWebServerRoot() {
Eric Duminil's avatar
Eric Duminil committed
195
196
197
198
//    if (web_config::handleCaptivePortal()) {
//      // -- Captive portal requests were already served.
//      return;
//    }
199
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
200
      return web_config::http.requestAuthentication(DIGEST_AUTH);
201
202
203
    }

    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
204
205
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
206
207
208
209
210
211
212
    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
Eric Duminil's avatar
Eric Duminil committed
213
    // INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
214

215
216
    snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip,
        csv_writer::filename);
217

218
219
    //    Serial.print(F("INFO - Header size : "));
    //    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
220
221
    web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    web_config::http.send_P(200, PSTR("text/html"), content);
222
223

    // Body
Eric Duminil's avatar
Eric Duminil committed
224
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
225
226
227
        sensor::humidity, sensor::timestamp, config::measurement_timestep, csv_writer::last_successful_write,
        config::csv_interval, csv_writer::getAvailableSpace() / 1024, mqtt::connected ? "Yes" : "No",
        mqtt::last_successful_publish, config::mqtt_sending_interval,
Eric Duminil's avatar
Eric Duminil committed
228
#if defined(ESP32)
229
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
230
        config::lorawan_sending_interval,
231
#endif
232
233
234
        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(),
        esp_get_heap_fragmentation(), ampel.max_loop_duration, ampel.board, ampel.version, dd, hh, mm, ss);
235

236
237
    //    Serial.print(F(" - Body size : "));
    //    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
238
    web_config::http.sendContent(content);
239
240

    // Script
241
    snprintf_P(content, sizeof(content), script_template, csv_writer::filename, ampel.sensorId);
242

243
244
    //    Serial.print(F(" - Script size : "));
    //    Serial.println(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
245
    web_config::http.sendContent(content);
246
247
248
249
  }

  void handleWebServerCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
250
      return web_config::http.requestAuthentication(DIGEST_AUTH);
251
252
253
    }
    if (FS_LIB.exists(csv_writer::filename)) {
      fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
Eric Duminil's avatar
Eric Duminil committed
254
255
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
Eric Duminil's avatar
Eric Duminil committed
256
257
      web_config::http.sendHeader("Content-Length", csv_size);
      web_config::http.streamFile(csv_file, F("text/csv"));
258
259
      csv_file.close();
    } else {
Eric Duminil's avatar
Eric Duminil committed
260
      web_config::http.send(204, F("text/html"), F("No data available."));
261
262
263
264
265
    }
  }

  void handleDeleteCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
266
      return web_config::http.requestAuthentication(DIGEST_AUTH);
267
    }
Eric Duminil's avatar
Eric Duminil committed
268
    Serial.print(F("Removing CSV file..."));
269
    FS_LIB.remove(csv_writer::filename);
Eric Duminil's avatar
Eric Duminil committed
270
    Serial.println(F(" Done!"));
Eric Duminil's avatar
Eric Duminil committed
271
272
    web_config::http.sendHeader("Location", "/");
    web_config::http.send(303);
273
274
  }

Eric Duminil's avatar
Eric Duminil committed
275
276
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
277
      return web_config::http.requestAuthentication(DIGEST_AUTH);
Eric Duminil's avatar
Eric Duminil committed
278
    }
Eric Duminil's avatar
Eric Duminil committed
279
280
281
    web_config::http.sendHeader("Location", "/");
    web_config::http.send(303);
    sensor_console::execute(web_config::http.arg("send").c_str());
Eric Duminil's avatar
Eric Duminil committed
282
283
  }

284
  void handlePageNotFound() {
Eric Duminil's avatar
Eric Duminil committed
285
    web_config::http.send(404, F("text/plain"), F("404: Not found"));
286
287
  }
}