web_server.cpp 13.1 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
13
14
#ifdef AMPEL_LORAWAN
#  include "lorawan.h"
#endif
Eric Duminil's avatar
Eric Duminil committed
15

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

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

51
  void handleDeleteCSV();
52
  void handleWebServerCSV();
53

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

Eric Duminil's avatar
Eric Duminil committed
137
138
139
140
    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"
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
180
181
                "<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>");
182
183

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

  // 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
193
        || web_config::http.authenticate(config::http_user, config::http_password);
194
195
196
  }

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

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

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

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

    // Body
Eric Duminil's avatar
Eric Duminil committed
226
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
227
228
229
        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
230
#if defined(AMPEL_LORAWAN) && defined(ESP32)
231
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
232
        config::lorawan_sending_interval,
233
#endif
234
235
236
        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);
237

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

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

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

  void handleWebServerCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
252
      return web_config::http.requestAuthentication(DIGEST_AUTH);
253
254
255
    }
    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
256
257
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
Eric Duminil's avatar
Eric Duminil committed
258
259
      web_config::http.sendHeader("Content-Length", csv_size);
      web_config::http.streamFile(csv_file, F("text/csv"));
260
261
      csv_file.close();
    } else {
Eric Duminil's avatar
Eric Duminil committed
262
      web_config::http.send(204, F("text/html"), F("No data available."));
263
264
265
266
267
    }
  }

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

Eric Duminil's avatar
Eric Duminil committed
277
278
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
279
      return web_config::http.requestAuthentication(DIGEST_AUTH);
Eric Duminil's avatar
Eric Duminil committed
280
    }
Eric Duminil's avatar
Eric Duminil committed
281
282
283
    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
284
285
  }

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