web_server.cpp 13.3 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
10
11
12
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
#ifdef AMPEL_CSV
#  include "csv_writer.h"
#endif
Eric Duminil's avatar
Eric Duminil committed
13
#include "mqtt.h"
Eric Duminil's avatar
Eric Duminil committed
14
15
16
#ifdef AMPEL_LORAWAN
#  include "lorawan.h"
#endif
Eric Duminil's avatar
Eric Duminil committed
17

Eric Duminil's avatar
Eric Duminil committed
18
#include <IotWebConf.h>
Eric Duminil's avatar
Eric Duminil committed
19
#include <IotWebConfUsing.h> // This loads aliases for easier class names.
20
#include <IotWebConfTParameter.h>
Eric Duminil's avatar
Eric Duminil committed
21
22
23
24
25
26
27
#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
28

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

#ifdef AMPEL_CSV
54
  void handleDeleteCSV();
55
56
  void handleWebServerCSV();
#endif
57

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

Eric Duminil's avatar
Eric Duminil committed
147
148
149
150
    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
151
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
152
153
            "<script>\n"
            "document.body.style.cursor = 'default';\n"
Eric Duminil's avatar
Eric Duminil committed
154
            "fetch('%s',{credentials:'include'})\n"
Eric Duminil's avatar
Eric Duminil committed
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
180
181
182
183
184
185
186
187
188
189
190
            ".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"
191
#endif
Eric Duminil's avatar
Eric Duminil committed
192
193
            "</body>\n"
            "</html>");
194
195

    // Web-server
Eric Duminil's avatar
Eric Duminil committed
196
197
    web_config::http.on("/", handleWebServerRoot);
    web_config::http.on("/command", handleWebServerCommand);
Eric Duminil's avatar
Eric Duminil committed
198
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
199
200
    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);
201
#endif
202
203
204
205
206
  }

  // 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
207
        || web_config::http.authenticate(config::http_user, config::http_password);
208
209
210
  }

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

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

Eric Duminil's avatar
Eric Duminil committed
231
    snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
232
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
233
        , csv_writer::filename
234
235
#endif
        );
236

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

    // Body
Eric Duminil's avatar
Eric Duminil committed
243
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
244
        sensor::humidity, sensor::timestamp, config::measurement_timestep,
Eric Duminil's avatar
Eric Duminil committed
245
#ifdef AMPEL_CSV
246
        csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
247
#endif
248
        mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval,
Eric Duminil's avatar
Eric Duminil committed
249
#if defined(AMPEL_LORAWAN) && defined(ESP32)
250
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
251
        config::lorawan_sending_interval,
252
#endif
253
254
255
        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);
256

257
258
    //    Serial.print(F(" - Body size : "));
    //    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
259
    web_config::http.sendContent(content);
260
261

    // Script
262
263
    snprintf_P(content, sizeof(content), script_template
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
264
        , csv_writer::filename, ampel.sensorId
265
266
#endif
        );
267

268
269
    //    Serial.print(F(" - Script size : "));
    //    Serial.println(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
270
    web_config::http.sendContent(content);
271
272
  }

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

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

Eric Duminil's avatar
Eric Duminil committed
302
303
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
304
      return web_config::http.requestAuthentication(DIGEST_AUTH);
Eric Duminil's avatar
Eric Duminil committed
305
    }
Eric Duminil's avatar
Eric Duminil committed
306
307
308
    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
309
310
  }

311
  void handlePageNotFound() {
Eric Duminil's avatar
Eric Duminil committed
312
    web_config::http.send(404, F("text/plain"), F("404: Not found"));
313
314
  }
}