web_server.cpp 12.8 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-1.58.2.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
61
            // "<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
62
            // Show a colored dot on the webpage, with a similar color than on LED Ring.
Eric Duminil's avatar
Eric Duminil committed
63
64
65
66
67
68
69
70
            "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'>");
Eric Duminil's avatar
Eric Duminil committed
71

Eric Duminil's avatar
Eric Duminil committed
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    body1_template = PSTR("<tr><th colspan='2'>%s</th></tr>"
        "<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>"
        "<tbody>"
        "<tr><th colspan='2'>CSV</th></tr>" //TODO: Gray out if !config::csv_active
        "<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>"
        "<tbody>"
        "<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
92
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
93
94
95
96
97
98
99
            "<tbody>"
                "<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
121
            "</form>"
            "</div>");
122

Eric Duminil's avatar
Eric Duminil committed
123
124
    script_template =
        PSTR(
Eric Duminil's avatar
Done    
Eric Duminil committed
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
            "<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
152
153
                //Don't display points without time
                "if (!fields[0].includes('1970-')){"
Eric Duminil's avatar
Done    
Eric Duminil committed
154
155
156
157
                "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
158
                "};"
Eric Duminil's avatar
Done    
Eric Duminil committed
159
160
161
162
163
164
165
166
167
168
169
                "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>"
170
                "</html>");
171
172

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

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

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

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

Eric Duminil's avatar
Eric Duminil committed
202
203
    //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
204
    // Current size (with Lorawan):
Eric Duminil's avatar
Done    
Eric Duminil committed
205
    //  INFO - Header size : 1826 - Body size : 2005 - Script size : 1904
206

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

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

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

    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)
227
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
228
        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
  }
}