web_server.cpp 12.5 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
            // "<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>"
Eric Duminil's avatar
Eric Duminil committed
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>");

    body1_template = PSTR("<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>"
        // 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;"
        "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');"
        "</script>"
        "<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
        "<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>");
Eric Duminil's avatar
Eric Duminil committed
89

Eric Duminil's avatar
Eric Duminil committed
90
91
    body2_template =
        PSTR(
Eric Duminil's avatar
Eric Duminil committed
92
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
93
            "<tbody %s>"
Eric Duminil's avatar
Eric Duminil committed
94
95
96
97
98
99
                "<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>"
100
#endif
Eric Duminil's avatar
Eric Duminil committed
101
            "<tr><th colspan='2'>Sensor</th></tr>"
Eric Duminil's avatar
Eric Duminil committed
102
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>"
Eric Duminil's avatar
Eric Duminil committed
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
            "<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>"
118
119
            "<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
120
            "</form>"
Eric Duminil's avatar
Eric Duminil committed
121
122
123
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
            "</div>"
            "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>&nbsp;"
            "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>");

    script_template = PSTR("<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(';');"
        //Don't display points without time
        "if (!fields[0].includes('1970-')){"
        "xs.push(fields[0]);"
        "data[0]['y'].push(fields[1]);"
        "data[1]['y'].push(fields[2]);"
        "data[2]['y'].push(fields[3]);"
        "};"
        "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>"
        "</html>");
169
170

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

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

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

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

Eric Duminil's avatar
Eric Duminil committed
200
201
    //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
202
    // Current size (with Lorawan):
Eric Duminil's avatar
Eric Duminil committed
203
    //    INFO - Header size : 1685 - Body1 size : 843 - Body2 size : 1395 - Script size : 1918
204

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

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

    // Body
Eric Duminil's avatar
Eric Duminil committed
213
214
215
216
217
    snprintf_P(content, sizeof(content), body1_template, csv_writer::filename, config::ampel_name(), sensor::co2,
        sensor::temperature, 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
218
219
220
221
222
223

    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
224
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
225
226
        config::is_lorawan_active() ? "" : "hidden", lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan,
        lorawan::last_transmission, config::lorawan_sending_interval,
227
#endif
228
229
230
231
        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);
232

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

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

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

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

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

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

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