web_server.cpp 12.9 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
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
5
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
6
7
8
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
9
#include "csv_writer.h"
Eric Duminil's avatar
Eric Duminil committed
10
#include "mqtt.h"
Eric Duminil's avatar
Eric Duminil committed
11
#include "lorawan.h"
Eric Duminil's avatar
Eric Duminil committed
12
13
14
15
16
17

#if defined(ESP8266)
#  include <ESP8266WebServer.h>
#elif defined(ESP32)
#  include <WebServer.h>
#endif
Eric Duminil's avatar
Eric Duminil committed
18

19
20
21
namespace web_server {

  const char *header_template;
Eric Duminil's avatar
Eric Duminil committed
22
23
  const char *body1_template;
  const char *body2_template;
24
25
26
  const char *script_template;
  void handleWebServerRoot();
  void handlePageNotFound();
Eric Duminil's avatar
Eric Duminil committed
27
  void handleWebServerCommand();
28

29
  void handleDeleteCSV();
30
  void handleWebServerCSV();
31

Eric Duminil's avatar
Eric Duminil committed
32
  void definePages() {
33
34
    header_template =
        PSTR("<!doctype html><html lang=en>"
Eric Duminil's avatar
Eric Duminil committed
35
36
37
            "<head>"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>"
            "<meta charset='UTF-8'>"
38
            // HfT Favicon
Eric Duminil's avatar
Eric Duminil committed
39
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
40
            // Responsive grid:
Eric Duminil's avatar
Eric Duminil committed
41
42
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/pure-min.css'>"
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css'>"
43
            // JS Graphs:
Eric Duminil's avatar
Eric Duminil committed
44
            "<script src='https://cdn.plot.ly/plotly-basic-2.9.0.min.js'></script>"
45
            // Fullscreen
Eric Duminil's avatar
Eric Duminil committed
46
            "<meta name='viewport' content='width=device-width, initial-scale=1'>"
47
            // Refresh after every measurement.
Eric Duminil's avatar
Eric Duminil committed
48
49
50
51
52
53
54
55
56
57
58
59
60
            // "<meta http-equiv='refresh' content='%d'>"
            "</head>"
            "<body>"
            "<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>"
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>"
            "<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>"
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>"
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>"
            "<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>"
            "<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>"
            "<li class='pure-menu-item' id='led'>&#11044;</li>"// LED
            "</ul></div></div>"
            "<script>"
Eric Duminil's avatar
Eric Duminil committed
61
            // Show a colored dot on the webpage, with a similar color than on LED Ring.
Eric Duminil's avatar
Eric Duminil committed
62
63
            "hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;"
            "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');"
Eric Duminil's avatar
Eric Duminil committed
64
65
66
67
68
69
70
71
            "</script>");

    body1_template = PSTR("<div class='pure-g'>"
        "<div class='pure-u-1' id='graph'></div>" // Graph placeholder
        "</div>"
        "<div class='pure-g'>"
        "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>"
        "<tr><th colspan='2'>%s</th></tr>"
Eric Duminil's avatar
Eric Duminil committed
72
73
74
75
76
        "<tr><td>CO<sub>2</sub></td><td>%5d ppm</td></tr>"
        "<tr><td>Temperature</td><td>%.1f&#8451;</td></tr>"
        "<tr><td>Humidity</td><td>%.1f%%</td></tr>"
        "<tr><td>Last measurement</td><td>%s</td></tr>"
        "<tr><td>Timestep</td><td>%5d s</td></tr>"
Eric Duminil's avatar
Eric Duminil committed
77
78
        "<tbody %s>"
        "<tr><th colspan='2'>CSV</th></tr>"//TODO: Gray out if !config::csv_active
Eric Duminil's avatar
Eric Duminil committed
79
80
81
82
        "<tr><td>Last write</td><td>%s</td></tr>"
        "<tr><td>Interval</td><td>%5d s</td></tr>"
        "<tr><td>Available space</td><td>%d kB</td></tr>"
        "</tbody>"
Eric Duminil's avatar
Eric Duminil committed
83
        "<tbody %s>"
Eric Duminil's avatar
Eric Duminil committed
84
85
86
87
88
89
90
        "<tr><th colspan='2'>MQTT</th></tr>"
        "<tr><td>Connected?</td><td>%s</td></tr>"
        "<tr><td>Last publish</td><td>%s</td></tr>"
        "<tr><td>Interval</td><td>%5d s</td></tr>"
        "</tbody>");
    body2_template =
        PSTR(
Eric Duminil's avatar
Eric Duminil committed
91
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
92
            "<tbody %s>"
Eric Duminil's avatar
Eric Duminil committed
93
94
95
96
97
98
                "<tr><th colspan='2'>LoRaWAN</th></tr>"
                "<tr><td>Connected?</td><td>%s</td></tr>"
                "<tr><td>Frequency</td><td>%s MHz</td></tr>"
                "<tr><td>Last transmission</td><td>%s</td></tr>"
                "<tr><td>Interval</td><td>%5d s</td></tr>"
                "</tbody>"
99
#endif
Eric Duminil's avatar
Eric Duminil committed
100
            "<tr><th colspan='2'>Sensor</th></tr>"
Eric Duminil's avatar
Eric Duminil committed
101
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>"
Eric Duminil's avatar
Eric Duminil committed
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
            "<tr><td>Auto-calibration?</td><td>%s</td></tr>"
            "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>"
            "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>"
            "<tr><td>MAC</td><td>%s</td></tr>"
            "<tr><td>Free heap space</td><td>%6d bytes</td></tr>"
            "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>"
            "<tr><td>Frag</td><td>%3d%%</td></tr>"
            "<tr><td>Max loop duration</td><td>%5d ms</td></tr>"
            "<tr><td>Board</td><td>%s</td></tr>"
            "<tr><td>ID</td><td>%s</td></tr>"
            "<tr><td>Ampel firmware</td><td>%s</td></tr>"
            "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>"
            "</table>"
            "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>"
            "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>"
117
118
            "<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'/>"
Eric Duminil's avatar
Eric Duminil committed
119
120
            "</form>"
            "</div>");
121

Eric Duminil's avatar
Eric Duminil committed
122
123
    script_template =
        PSTR(
Eric Duminil's avatar
Done    
Eric Duminil committed
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
            "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>"
                "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>"
                "<script>"
                "document.body.style.cursor = 'default';"
                "fetch('%s',{credentials:'include'})"
                ".then(response=>response.text())"
                ".then(csvText=>csvToTable(csvText))"
                ".then(htmlTable=>addLogTableToPage(htmlTable))"
                ".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))"
                ".catch(e=>console.error(e));"
                "xs=[];"
                "data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}},"
                "{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}},"
                "{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];"
                "layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},"
                "xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},"
                "yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},"
                "yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}"
                "};"
                "function csvToTable(csvText){"
                "csvText=csvText.trim();"
                "lines=csvText.split('\\n');"
                "table=document.createElement('table');"
                "table.className='pure-table-striped';"
                "n=lines.length;"
                "lines.forEach((line,i)=>{"
                "fields=line.split(';');"
Eric Duminil's avatar
Debug    
Eric Duminil committed
151
152
                //Don't display points without time
                "if (!fields[0].includes('1970-')){"
Eric Duminil's avatar
Done    
Eric Duminil committed
153
154
155
156
                "xs.push(fields[0]);"
                "data[0]['y'].push(fields[1]);"
                "data[1]['y'].push(fields[2]);"
                "data[2]['y'].push(fields[3]);"
Eric Duminil's avatar
Debug    
Eric Duminil committed
157
                "};"
Eric Duminil's avatar
Done    
Eric Duminil committed
158
159
160
161
162
163
164
165
166
167
168
                "if(i>4 && i<n-12){if(i==5){fields=['...','...','...','...']}else{return;}}"
                "row=document.createElement('tr');"
                "fields.forEach((field,index)=>{"
                "cell=document.createElement(i<2?'th':'td');"
                "cell.appendChild(document.createTextNode(field));"
                "row.appendChild(cell);});"
                "table.appendChild(row);});"
                "return table;}"
                "function addLogTableToPage(table){document.getElementById('log').appendChild(table);}"
                "</script>"
                "</body>"
169
                "</html>");
170
171

    // Web-server
Eric Duminil's avatar
Eric Duminil committed
172
173
    web_config::http.on("/", handleWebServerRoot);
    web_config::http.on("/command", handleWebServerCommand);
174
    web_config::http.on(csv_writer::filename, handleWebServerCSV);
Eric Duminil's avatar
Eric Duminil committed
175
    web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
176
177
  }

178
179
180
181
182
  /*
   * Allow access if Ampel is in access point mode,
   * if http_user or http_password are empty,
   * or if provided credentials match
   */
183
  bool shouldBeAllowed() {
Eric Duminil's avatar
Eric Duminil committed
184
185
    return wifi::isAccessPoint() || strcmp(config::http_user, "") == 0 || strcmp(config::ampel_password(), "") == 0
        || web_config::http.authenticate(config::http_user, config::ampel_password());
186
187
188
189
  }

  void handleWebServerRoot() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
190
      return web_config::http.requestAuthentication(DIGEST_AUTH);
191
192
193
    }

    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
194
195
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
196
197
198
199
200
    unsigned int hh = ss / 3600;
    ss -= hh * 3600;
    uint8_t mm = ss / 60;
    ss -= mm * 60;

Eric Duminil's avatar
Eric Duminil committed
201
202
    //NOTE: Splitting in multiple parts in order to use less RAM. Higher than 2000 apparently crashes the ESP8266
    char content[2000];
Eric Duminil's avatar
Eric Duminil committed
203
    // Current size (with Lorawan):
Eric Duminil's avatar
Eric Duminil committed
204
    //    INFO - Header size : 1685 - Body1 size : 843 - Body2 size : 1395 - Script size : 1918
205

Eric Duminil's avatar
Eric Duminil committed
206
    snprintf_P(content, sizeof(content), header_template, sensor::co2, config::ampel_name(), wifi::local_ip,
207
        csv_writer::filename);
208

209
210
    Serial.print(F("INFO - Header size : "));
    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
211
212
    web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    web_config::http.send_P(200, PSTR("text/html"), content);
213
214

    // Body
Eric Duminil's avatar
Eric Duminil committed
215
    snprintf_P(content, sizeof(content), body1_template, config::ampel_name(), sensor::co2, sensor::temperature,
Eric Duminil's avatar
Eric Duminil committed
216
217
218
219
        sensor::humidity, sensor::timestamp, config::measurement_timestep, config::is_csv_active() ? "" : "hidden",
        csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
        config::is_mqtt_active() ? "" : "hidden", mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish,
        config::mqtt_sending_interval);
Eric Duminil's avatar
Eric Duminil committed
220
221
222
223
224
225

    Serial.print(F(" - Body1 size : "));
    Serial.print(strlen(content));
    web_config::http.sendContent(content);

    snprintf_P(content, sizeof(content), body2_template,
Eric Duminil's avatar
Eric Duminil committed
226
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
227
228
        config::is_lorawan_active() ? "" : "hidden", lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan,
        lorawan::last_transmission, config::lorawan_sending_interval,
229
#endif
230
231
232
233
        config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", config::ampel_name(),
        config::ampel_name(), 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.sensorId, ampel.version, dd, hh, mm, ss);
234

Eric Duminil's avatar
Eric Duminil committed
235
    Serial.print(F(" - Body2 size : "));
236
    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
237
    web_config::http.sendContent(content);
238
239

    // Script
Eric Duminil's avatar
Eric Duminil committed
240
    snprintf_P(content, sizeof(content), script_template, csv_writer::filename, config::ampel_name());
241

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

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

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

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

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