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

Eric Duminil's avatar
Eric Duminil committed
3
4
5
6
7
8
9
10
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
#elif defined(ESP32)
#  include <WebServer.h>
#endif

#include "config.h"
#include "util.h"
11
#include "time_util.h"
Eric Duminil's avatar
Eric Duminil committed
12
13
14
15
16
17
18
19
20
21
22
23
24
#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

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

#ifdef AMPEL_CSV
50
  void handleDeleteCSV();
51
52
  void handleWebServerCSV();
#endif
53
54
55

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

Eric Duminil's avatar
Eric Duminil committed
153
154
155
156
    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
157
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
158
159
            "<script>\n"
            "document.body.style.cursor = 'default';\n"
Eric Duminil's avatar
Eric Duminil committed
160
            "fetch('%s',{credentials:'include'})\n"
Eric Duminil's avatar
Eric Duminil committed
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
195
196
            ".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"
197
#endif
Eric Duminil's avatar
Eric Duminil committed
198
199
            "</body>\n"
            "</html>");
200
201
202

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

    Serial.print(F("You can access this sensor via http://"));
212
    Serial.print(ampel.sensorId);
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    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();
Eric Duminil's avatar
Eric Duminil committed
229
230
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
231
232
233
234
235
236
237
    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
238
    // INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
239

Eric Duminil's avatar
Eric Duminil committed
240
    snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
241
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
242
        , csv_writer::filename
243
244
#endif
        );
245

246
247
    //    Serial.print(F("INFO - Header size : "));
    //    Serial.print(strlen(content));
248
249
250
251
    http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    http.send_P(200, PSTR("text/html"), content);

    // Body
Eric Duminil's avatar
Eric Duminil committed
252
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
253
        sensor::humidity, sensor::timestamp, config::measurement_timestep,
Eric Duminil's avatar
Eric Duminil committed
254
#ifdef AMPEL_CSV
255
        csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
256
#endif
Eric Duminil's avatar
Eric Duminil committed
257
#ifdef AMPEL_MQTT
258
        mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval,
Eric Duminil's avatar
Eric Duminil committed
259
#endif
Eric Duminil's avatar
Eric Duminil committed
260
#if defined(AMPEL_LORAWAN) && defined(ESP32)
261
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
262
        config::lorawan_sending_interval,
263
#endif
Eric Duminil's avatar
Eric Duminil committed
264
        config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
265
266
        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);
267

268
269
    //    Serial.print(F(" - Body size : "));
    //    Serial.print(strlen(content));
270
271
272
    http.sendContent(content);

    // Script
273
274
    snprintf_P(content, sizeof(content), script_template
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
275
        , csv_writer::filename, ampel.sensorId
276
277
#endif
        );
278

279
280
    //    Serial.print(F(" - Script size : "));
    //    Serial.println(strlen(content));
281
282
283
    http.sendContent(content);
  }

284
#ifdef AMPEL_CSV
285
286
287
288
289
290
  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");
Eric Duminil's avatar
Eric Duminil committed
291
292
293
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
      http.sendHeader("Content-Length", csv_size);
294
295
296
297
298
299
300
301
302
303
304
      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);
    }
Eric Duminil's avatar
Eric Duminil committed
305
    Serial.print(F("Removing CSV file..."));
306
    FS_LIB.remove(csv_writer::filename);
Eric Duminil's avatar
Eric Duminil committed
307
    Serial.println(F(" Done!"));
308
309
310
    http.sendHeader("Location", "/");
    http.send(303);
  }
311
#endif
312

Eric Duminil's avatar
Eric Duminil committed
313
314
315
316
317
318
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }
    http.sendHeader("Location", "/");
    http.send(303);
319
    sensor_console::execute(http.arg("send").c_str());
Eric Duminil's avatar
Eric Duminil committed
320
321
  }

322
323
324
325
  void handlePageNotFound() {
    http.send(404, F("text/plain"), F("404: Not found"));
  }
}