web_server.cpp 12.2 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
  const __FlashStringHelper* showHTMLIf(bool is_active) {
Eric Duminil's avatar
Eric Duminil committed
33
34
35
    return is_active ? F("") : F("hidden");
  }

Eric Duminil's avatar
Eric Duminil committed
36
  const __FlashStringHelper* yesOrNo(bool is_active) {
Eric Duminil's avatar
Eric Duminil committed
37
38
39
    return is_active ? F("Yes") : F("No");
  }

Eric Duminil's avatar
Eric Duminil committed
40
  void definePages() {
41
42
    header_template =
        PSTR("<!doctype html><html lang=en>"
Eric Duminil's avatar
Eric Duminil committed
43
44
45
            "<head>"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>"
            "<meta charset='UTF-8'>"
46
            // HfT Favicon
Eric Duminil's avatar
Eric Duminil committed
47
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
48
            // Responsive grid:
Eric Duminil's avatar
Eric Duminil committed
49
50
            "<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'>"
51
            // JS Graphs:
Eric Duminil's avatar
Eric Duminil committed
52
            "<script src='https://cdn.plot.ly/plotly-basic-2.9.0.min.js'></script>"
53
            // Fullscreen
Eric Duminil's avatar
Eric Duminil committed
54
            "<meta name='viewport' content='width=device-width, initial-scale=1'>"
55
            // Refresh after every measurement.
Eric Duminil's avatar
Eric Duminil committed
56
57
58
            // "<meta http-equiv='refresh' content='%d'>"
            "</head>"
            "<body>"
Eric Duminil's avatar
H2    
Eric Duminil committed
59
            "<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><h2 class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</h2></div></div>"
Eric Duminil's avatar
Eric Duminil committed
60
61
62
            "<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
63
64
            "<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>");
Eric Duminil's avatar
Eric Duminil committed
65

Eric Duminil's avatar
Eric Duminil committed
66
67
    body1_template =
        PSTR(
68
69
70
71
            "<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
72
            // Show a colored dot on the webpage, with a similar color than on LED Ring.
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
            "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>"
            "<tr><td>CO<sub>2</sub></td><td>%5d ppm</td></tr>"
            "<tr><td>Last measurement</td><td>%s</td></tr>"
            "<tr><td>Timestep</td><td>%5d s</td></tr>"
            "<tbody %s>"
            "<tr><th colspan='2'>CSV</th></tr>"
            "<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 %s>"
            "<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
97
98
99
100
101
102
103
104
105
#if defined(ESP32)
        "<tbody %s>"
        "<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>"
#endif
Eric Duminil's avatar
Eric Duminil committed
106
            );
Eric Duminil's avatar
Eric Duminil committed
107

Eric Duminil's avatar
Eric Duminil committed
108
109
    body2_template =
        PSTR(
Eric Duminil's avatar
Eric Duminil committed
110
            "<tr><th colspan='2'>Sensor</th></tr>"
Eric Duminil's avatar
Eric Duminil committed
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
                "<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>"
                "<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>"
                "<button onclick=\"fetch('/command?send=set_time '+Math.floor(Date.now()/1000))\" %s>Set time!</button>" // Can be useful in AP mode
                "</div>"
                "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>&nbsp;"
132
133
134
                "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>"
                "<script>"
                "document.body.style.cursor='default';");
Eric Duminil's avatar
Eric Duminil committed
135

136
    script_template = PSTR("fetch('%s',{credentials:'include'})"
Eric Duminil's avatar
Eric Duminil committed
137
138
        ".then(r=>r.text())"
        ".then(c2t)"
139
        ".then(t=>document.getElementById('log').appendChild(t))"
Eric Duminil's avatar
Eric Duminil committed
140
        ".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))"
Eric Duminil's avatar
Eric Duminil committed
141
        ".catch(console.error);"
142
        "xs=[];y1=[];"
Eric Duminil's avatar
Eric Duminil committed
143
        "d={x:xs,type:'scatter',mode:'lines+markers',marker:{size:3}};" // circle marker (symbol 0), from https://plotly.com/python/marker-style/
144
        "data=["
Eric Duminil's avatar
Eric Duminil committed
145
        "{...d,...{y:y1,name:'CO<sub>2</sub>',line:{color:'#2ca02c'}}},"
146
        "];"
Eric Duminil's avatar
Eric Duminil committed
147
148
149
        "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},"
        "};"
Eric Duminil's avatar
Eric Duminil committed
150
151
152
153
154
155
        "function c2t(t){"
        "t=t.trim();"
        "ls=t.split('\\n');"
        "tb=document.createElement('table');"
        "tb.className='pure-table-striped';"
        "n=ls.length;"
156
        "ld=NaN;"
Eric Duminil's avatar
Eric Duminil committed
157
158
        "ls.forEach((l,i)=>{"
        "fs=l.split(';');"
Eric Duminil's avatar
Eric Duminil committed
159
        //Don't display points without time
160
161
162
163
164
165
166
167
        "if(fs[0].includes('1970-')){return};"
        "d=Date.parse(fs[0]);"
        //Split curves when points are more than 1h apart
        "if(d-ld>36e5){"
        "xs.push(NaN);"
        "y1.push(NaN);"
        "}"
        "ld=d;"
Eric Duminil's avatar
Eric Duminil committed
168
        "xs.push(fs[0]);"
Eric Duminil's avatar
Eric Duminil committed
169
        "y1.push(fs[1]);"
170
        "if(i>4&&i<n-12){if(i==5){fs=['...','...','...','...']}else{return;}}"
Eric Duminil's avatar
Eric Duminil committed
171
172
173
174
175
176
177
        "r=document.createElement('tr');"
        "fs.forEach((f,_)=>{"
        "c=document.createElement(i<2?'th':'td');"
        "c.appendChild(document.createTextNode(f));"
        "r.appendChild(c);});"
        "tb.appendChild(r);});"
        "return tb;}"
Eric Duminil's avatar
Eric Duminil committed
178
179
180
        "</script>"
        "</body>"
        "</html>");
181
182

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

189
190
191
192
193
  /*
   * Allow access if Ampel is in access point mode,
   * if http_user or http_password are empty,
   * or if provided credentials match
   */
194
  bool shouldBeAllowed() {
Eric Duminil's avatar
Eric Duminil committed
195
196
    return wifi::isAccessPoint() || strcmp(config::http_user, "") == 0 || strcmp(config::ampel_password(), "") == 0
        || web_config::http.authenticate(config::http_user, config::ampel_password());
197
198
199
200
  }

  void handleWebServerRoot() {
    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
201
202
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
203
204
205
206
207
    unsigned int hh = ss / 3600;
    ss -= hh * 3600;
    uint8_t mm = ss / 60;
    ss -= mm * 60;

Eric Duminil's avatar
Eric Duminil committed
208
    //NOTE: Splitting in multiple parts in order to use less RAM. Higher than 2000 apparently crashes the ESP8266
Eric Duminil's avatar
Eric Duminil committed
209
    char content[1600];
Eric Duminil's avatar
Eric Duminil committed
210
    // Current size (with Lorawan, timesteps and long thing name):
Eric Duminil's avatar
Eric Duminil committed
211
    //    INFO - Header size : 1347 - Body1 size : 1448 - Body2 size : 1475 - Script size : 1507
212

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

215
216
    Serial.print(F("INFO - Header size : "));
    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
217
218
    web_config::http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    web_config::http.send_P(200, PSTR("text/html"), content);
219
220

    // Body
Eric Duminil's avatar
Eric Duminil committed
221
    snprintf_P(content, sizeof(content), body1_template, csv_writer::filename, config::ampel_name(), sensor::co2,
222
        sensor::timestamp, config::measurement_timestep,
Eric Duminil's avatar
Eric Duminil committed
223
        showHTMLIf(config::is_csv_active()), csv_writer::last_successful_write, config::csv_interval,
Eric Duminil's avatar
Eric Duminil committed
224
225
        csv_writer::getAvailableSpace() / 1024, showHTMLIf(config::is_mqtt_active()), yesOrNo(mqtt::connected),
        mqtt::last_successful_publish, config::mqtt_sending_interval
Eric Duminil's avatar
Eric Duminil committed
226
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
227
        , showHTMLIf(config::is_lorawan_active()), yesOrNo(lorawan::connected),
Eric Duminil's avatar
Eric Duminil committed
228
229
230
        config::lorawan_frequency_plan, lorawan::last_transmission, config::lorawan_sending_interval
#endif
        );
Eric Duminil's avatar
Eric Duminil committed
231
232
233
234
235

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

236
    snprintf_P(content, sizeof(content), body2_template,
Eric Duminil's avatar
Eric Duminil committed
237
        yesOrNo(config::auto_calibrate_sensor), config::ampel_name(), config::ampel_name(), wifi::local_ip,
Eric Duminil's avatar
Eric Duminil committed
238
239
        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,
Eric Duminil's avatar
Eric Duminil committed
240
        ss, showHTMLIf(!ntp::connected_at_least_once));
241

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

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

249
250
    Serial.print(F(" - Script size : "));
    Serial.println(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
251
    web_config::http.sendContent(content);
252
253
254
255
256
  }

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

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

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

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