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
11
12
13
14
15
16
17
18
19
20
21
22
23
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
#elif defined(ESP32)
#  include <WebServer.h>
#endif

#include "config.h"
#include "util.h"
#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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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