web_server.cpp 12.6 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
22
23
24
25
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
26
  void handleWebServerCommand();
27

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

Eric Duminil's avatar
Eric Duminil committed
31
  void definePages() {
32
33
    header_template =
        PSTR("<!doctype html><html lang=en>"
Eric Duminil's avatar
Eric Duminil committed
34
35
36
            "<head>"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>"
            "<meta charset='UTF-8'>"
37
            // HfT Favicon
Eric Duminil's avatar
Eric Duminil committed
38
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
39
            // Responsive grid:
Eric Duminil's avatar
Eric Duminil committed
40
41
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/pure-min.css'>"
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css'>"
42
            // JS Graphs:
Eric Duminil's avatar
Eric Duminil committed
43
            "<script src='https://cdn.plot.ly/plotly-basic-1.58.2.min.js'></script>"
44
            // Fullscreen
Eric Duminil's avatar
Eric Duminil committed
45
            "<meta name='viewport' content='width=device-width, initial-scale=1'>"
46
            // Refresh after every measurement.
Eric Duminil's avatar
Eric Duminil committed
47
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
64
65
66
67
68
69
            "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
70
71

    body_template =
Eric Duminil's avatar
Eric Duminil committed
72
73
74
75
76
77
78
79
        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>"
            "<tr><th colspan='2'>CSV</th></tr>" //TODO: Gray out if !config::csv_active
            "<tr><td>Last write</td><td>%s</td></tr>"
80
            "<tr><td>Interval</td><td>%5d s</td></tr>"
Eric Duminil's avatar
Eric Duminil committed
81
82
83
84
            "<tr><td>Available space</td><td>%d kB</td></tr>"
            "<tr><th colspan='2'>MQTT</th></tr>"//TODO: Gray out if !config::mqtt_active
            "<tr><td>Connected?</td><td>%s</td></tr>"
            "<tr><td>Last publish</td><td>%s</td></tr>"
85
            "<tr><td>Interval</td><td>%5d s</td></tr>"
Eric Duminil's avatar
Eric Duminil committed
86
#if defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
87
88
89
90
            "<tr><th colspan='2'>LoRaWAN</th></tr>" //TODO: Gray out if !config::lora_active
            "<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>"
91
            "<tr><td>Interval</td><td>%5d s</td></tr>"
92
#endif
Eric Duminil's avatar
Eric Duminil committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
            "<tr><th colspan='2'>Sensor</th></tr>"
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>" //TODO: Read it from sensor?
            "<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>"
110
111
            "<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
112
113
            "</form>"
            "</div>");
114

Eric Duminil's avatar
Eric Duminil committed
115
116
    script_template =
        PSTR(
Eric Duminil's avatar
Done    
Eric Duminil committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
            "<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
144
145
                //Don't display points without time
                "if (!fields[0].includes('1970-')){"
Eric Duminil's avatar
Done    
Eric Duminil committed
146
147
148
149
                "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
150
                "};"
Eric Duminil's avatar
Done    
Eric Duminil committed
151
152
153
154
155
156
157
158
159
160
161
                "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>"
162
                "</html>");
163
164

    // Web-server
Eric Duminil's avatar
Eric Duminil committed
165
166
    web_config::http.on("/", handleWebServerRoot);
    web_config::http.on("/command", handleWebServerCommand);
167
    web_config::http.on(csv_writer::filename, handleWebServerCSV);
Eric Duminil's avatar
Eric Duminil committed
168
    web_config::http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
169
170
  }

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

  void handleWebServerRoot() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
183
      return web_config::http.requestAuthentication(DIGEST_AUTH);
184
185
186
    }

    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
187
188
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
189
190
191
192
193
    unsigned int hh = ss / 3600;
    ss -= hh * 3600;
    uint8_t mm = ss / 60;
    ss -= mm * 60;

Eric Duminil's avatar
Eric Duminil committed
194
195
    //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
196
    // Current size (with Lorawan):
Eric Duminil's avatar
Done    
Eric Duminil committed
197
    //  INFO - Header size : 1826 - Body size : 2005 - Script size : 1904
Eric Duminil's avatar
Eric Duminil committed
198
    //FIXME: Body too long with Lorawan! Split in 4 parts?
199

Eric Duminil's avatar
Eric Duminil committed
200
    snprintf_P(content, sizeof(content), header_template, sensor::co2, config::ampel_name(), wifi::local_ip,
201
        csv_writer::filename);
202

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

    // Body
Eric Duminil's avatar
Eric Duminil committed
209
    snprintf_P(content, sizeof(content), body_template, config::ampel_name(), sensor::co2, sensor::temperature,
210
        sensor::humidity, sensor::timestamp, config::measurement_timestep, csv_writer::last_successful_write,
Eric Duminil's avatar
Eric Duminil committed
211
212
        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
213
#if defined(ESP32)
214
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
215
        config::lorawan_sending_interval,
216
#endif
217
218
219
220
        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);
221

222
223
    Serial.print(F(" - Body size : "));
    Serial.print(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
224
    web_config::http.sendContent(content);
225
226

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

229
230
    Serial.print(F(" - Script size : "));
    Serial.println(strlen(content));
Eric Duminil's avatar
Eric Duminil committed
231
    web_config::http.sendContent(content);
232
233
234
235
  }

  void handleWebServerCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
236
      return web_config::http.requestAuthentication(DIGEST_AUTH);
237
238
239
    }
    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
240
241
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
Eric Duminil's avatar
Eric Duminil committed
242
243
      web_config::http.sendHeader("Content-Length", csv_size);
      web_config::http.streamFile(csv_file, F("text/csv"));
244
245
      csv_file.close();
    } else {
Eric Duminil's avatar
Eric Duminil committed
246
      web_config::http.send(204, F("text/html"), F("No data available."));
247
248
249
250
251
    }
  }

  void handleDeleteCSV() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
252
      return web_config::http.requestAuthentication(DIGEST_AUTH);
253
    }
Eric Duminil's avatar
Eric Duminil committed
254
    Serial.print(F("Removing CSV file..."));
255
    FS_LIB.remove(csv_writer::filename);
Eric Duminil's avatar
Eric Duminil committed
256
    Serial.println(F(" Done!"));
Eric Duminil's avatar
Eric Duminil committed
257
258
    web_config::http.sendHeader("Location", "/");
    web_config::http.send(303);
259
260
  }

Eric Duminil's avatar
Eric Duminil committed
261
262
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
Eric Duminil's avatar
Eric Duminil committed
263
      return web_config::http.requestAuthentication(DIGEST_AUTH);
Eric Duminil's avatar
Eric Duminil committed
264
    }
Eric Duminil's avatar
Eric Duminil committed
265
266
267
    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
268
269
  }

270
  void handlePageNotFound() {
Eric Duminil's avatar
Eric Duminil committed
271
    web_config::http.send(404, F("text/plain"), F("404: Not found"));
272
273
  }
}