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

Eric Duminil's avatar
Eric Duminil committed
3
4
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
Eric Duminil's avatar
Eric Duminil committed
5
#  include <ESP8266mDNS.h> //allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
Eric Duminil's avatar
Eric Duminil committed
6
7
#elif defined(ESP32)
#  include <WebServer.h>
Eric Duminil's avatar
Eric Duminil committed
8
#  include <ESPmDNS.h>
Eric Duminil's avatar
Eric Duminil committed
9
10
#endif

Eric Duminil's avatar
Eric Duminil committed
11
#include "web_config.h"
Eric Duminil's avatar
Eric Duminil committed
12
13
#include "config.h"
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
14
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
15
16
17
18
19
20
21
22
23
24
25
26
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
#ifdef AMPEL_CSV
#  include "csv_writer.h"
#endif
#ifdef AMPEL_MQTT
#  include "mqtt.h"
#endif
#ifdef AMPEL_LORAWAN
#  include "lorawan.h"
#endif
Eric Duminil's avatar
Eric Duminil committed
27
#include <IotWebConf.h>
Eric Duminil's avatar
Eric Duminil committed
28
#include <IotWebConfUsing.h> // This loads aliases for easier class names.
29
#include <IotWebConfTParameter.h>
Eric Duminil's avatar
Eric Duminil committed
30

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
53
  void handleWebServerCommand();
54
55

#ifdef AMPEL_CSV
56
  void handleDeleteCSV();
57
58
  void handleWebServerCSV();
#endif
59

Eric Duminil's avatar
Eric Duminil committed
60
  void definePages() {
61
62
63
64
    header_template =
        PSTR("<!doctype html><html lang=en>"
            "<head>\n"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
65
            "<meta charset='UTF-8'>\n"
66
            // HfT Favicon
67
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
68
69
70
71
72
73
74
75
76
77
78
79
            // 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
80
            "<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"
81
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
Eric Duminil's avatar
Better    
Eric Duminil committed
82
            "<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>\n"
83
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
Eric Duminil's avatar
Eric Duminil committed
84
#ifdef AMPEL_CSV
85
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
86
            "<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
Eric Duminil's avatar
Eric Duminil committed
87
            "<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>\n"
88
89
#endif
            "<li class='pure-menu-item' id='led'>&#11044;</li>\n" // LED
Eric Duminil's avatar
Eric Duminil committed
90
91
92
93
94
            "</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
95
96
97
            "</script>\n"
            "<div class='pure-g'>\n"
            "<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
98
99
            "</div>\n"
            "<div class='pure-g'>\n"
Eric Duminil's avatar
Eric Duminil committed
100
101
102
103
            "<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"
104
105
106
107
108
            "<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
109
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
110
111
112
            "<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
113
            "<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
114
#endif
Eric Duminil's avatar
Eric Duminil committed
115
#ifdef AMPEL_MQTT
Eric Duminil's avatar
Eric Duminil committed
116
            "<tr><th colspan='2'>MQTT</th></tr>\n"
117
            "<tr><td>Connected?</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
118
119
            "<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
120
#endif
Eric Duminil's avatar
Eric Duminil committed
121
#if defined(AMPEL_LORAWAN) && defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
122
123
124
125
126
            "<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"
127
#endif
Eric Duminil's avatar
Eric Duminil committed
128
            "<tr><th colspan='2'>Sensor</th></tr>\n"
Eric Duminil's avatar
TODO    
Eric Duminil committed
129
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
130
            "<tr><td>Auto-calibration?</td><td>%s</td></tr>\n"
131
132
            "<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"
133
            "<tr><td>MAC</td><td>%s</td></tr>\n"
134
            "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
135
            "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
136
            "<tr><td>Frag</td><td>%3d%%</td></tr>\n"
137
138
            "<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
139
            "<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
140
            "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
141
142
            "</table>\n"
            "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
Eric Duminil's avatar
Eric Duminil committed
143
            "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n"
Eric Duminil's avatar
Eric Duminil committed
144
#ifdef AMPEL_CSV
145
146
147
            "<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"
148
#endif
Eric Duminil's avatar
Eric Duminil committed
149
            "</div>\n");
150

Eric Duminil's avatar
Eric Duminil committed
151
152
153
154
    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"
Eric Duminil's avatar
Eric Duminil committed
155
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
156
157
            "<script>\n"
            "document.body.style.cursor = 'default';\n"
Eric Duminil's avatar
Eric Duminil committed
158
            "fetch('%s',{credentials:'include'})\n"
Eric Duminil's avatar
Eric Duminil committed
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
            ".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"
195
#endif
Eric Duminil's avatar
Eric Duminil committed
196
197
            "</body>\n"
            "</html>");
198
199

    // Web-server
Eric Duminil's avatar
Eric Duminil committed
200
201
    web_config::http.on("/", handleWebServerRoot);
    web_config::http.on("/command", handleWebServerCommand);
Eric Duminil's avatar
Eric Duminil committed
202
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
203
204
    web_config::http.on(csv_writer::filename, handleWebServerCSV); //NOTE: csv_writer should have been initialized first.
    web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
205
#endif
206
207
208
209
210
  }

  // 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
211
        || web_config::http.authenticate(config::http_user, config::http_password);
212
213
214
  }

  void handleWebServerRoot() {
Eric Duminil's avatar
Eric Duminil committed
215
216
217
218
//    if (web_config::handleCaptivePortal()) {
//      // -- Captive portal requests were already served.
//      return;
//    }
219
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
220
      return web_config::http.requestAuthentication(DIGEST_AUTH);
221
222
223
    }

    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
224
225
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
226
227
228
229
230
231
232
    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
233
    // INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
234

Eric Duminil's avatar
Eric Duminil committed
235
    snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
236
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
237
        , csv_writer::filename
238
239
#endif
        );
240

241
242
    //    Serial.print(F("INFO - Header size : "));
    //    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
243
244
    web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    web_config::http.send_P(200, PSTR("text/html"), content);
245
246

    // Body
Eric Duminil's avatar
Eric Duminil committed
247
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
248
        sensor::humidity, sensor::timestamp, config::measurement_timestep,
Eric Duminil's avatar
Eric Duminil committed
249
#ifdef AMPEL_CSV
250
        csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
251
#endif
Eric Duminil's avatar
Eric Duminil committed
252
#ifdef AMPEL_MQTT
253
        mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval,
Eric Duminil's avatar
Eric Duminil committed
254
#endif
Eric Duminil's avatar
Eric Duminil committed
255
#if defined(AMPEL_LORAWAN) && defined(ESP32)
256
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
257
        config::lorawan_sending_interval,
258
#endif
Eric Duminil's avatar
Eric Duminil committed
259
        config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
Eric Duminil's avatar
Eric Duminil committed
260
        wifi::local_ip, wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(), esp_get_heap_fragmentation(),
261
        ampel.max_loop_duration, ampel.board, ampel.version, dd, hh, mm, ss);
262

263
264
    //    Serial.print(F(" - Body size : "));
    //    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
265
    web_config::http.sendContent(content);
266
267

    // Script
268
269
    snprintf_P(content, sizeof(content), script_template
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
270
        , csv_writer::filename, ampel.sensorId
271
272
#endif
        );
273

274
275
    //    Serial.print(F(" - Script size : "));
    //    Serial.println(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
276
    web_config::http.sendContent(content);
277
278
  }

279
#ifdef AMPEL_CSV
280
281
  void handleWebServerCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
282
      return web_config::http.requestAuthentication(DIGEST_AUTH);
283
284
285
    }
    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
286
287
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
Eric Duminil's avatar
Eric Duminil committed
288
289
      web_config::http.sendHeader("Content-Length", csv_size);
      web_config::http.streamFile(csv_file, F("text/csv"));
290
291
      csv_file.close();
    } else {
Eric Duminil's avatar
Eric Duminil committed
292
      web_config::http.send(204, F("text/html"), F("No data available."));
293
294
295
296
297
    }
  }

  void handleDeleteCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
298
      return web_config::http.requestAuthentication(DIGEST_AUTH);
299
    }
Eric Duminil's avatar
Eric Duminil committed
300
    Serial.print(F("Removing CSV file..."));
301
    FS_LIB.remove(csv_writer::filename);
Eric Duminil's avatar
Eric Duminil committed
302
    Serial.println(F(" Done!"));
Eric Duminil's avatar
Eric Duminil committed
303
304
    web_config::http.sendHeader("Location", "/");
    web_config::http.send(303);
305
  }
306
#endif
307

Eric Duminil's avatar
Eric Duminil committed
308
309
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
310
      return web_config::http.requestAuthentication(DIGEST_AUTH);
Eric Duminil's avatar
Eric Duminil committed
311
    }
Eric Duminil's avatar
Eric Duminil committed
312
313
314
    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
315
316
  }

317
  void handlePageNotFound() {
Eric Duminil's avatar
Eric Duminil committed
318
    web_config::http.send(404, F("text/plain"), F("404: Not found"));
319
320
  }
}