web_server.cpp 11 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "web_server.h"

namespace config {
  // Values should be defined in config.h
#ifdef HTTP_USER
  const char *http_user = HTTP_USER;
#else
  const char *http_user = "";
#endif
#ifdef HTTP_PASSWORD
  const char *http_password = HTTP_PASSWORD;
#else
  const char *http_password = "";
#endif

}

namespace web_server {

  const char *header_template;
  const char *body_template;
  const char *script_template;
  void handleWebServerRoot();
  void handleWebServerCSV();
  void handlePageNotFound();
  void handleDeleteCSV();

#if defined(ESP8266)
  ESP8266WebServer http(80); // Create a webserver object that listens for HTTP request on port 80
Eric Duminil's avatar
Eric Duminil committed
30
#elif defined(ESP32)
31
32
33
34
35
36
37
38
39
40
41
42
  WebServer http(80);
#endif

  void update() {
    http.handleClient(); // Listen for HTTP requests from clients
  }

  void initialize() {
    header_template =
        PSTR("<!doctype html><html lang=en>"
            "<head>\n"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
43
            "<meta charset='UTF-8'>\n"
44
            // HfT Favicon
45
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
46
47
48
49
50
51
52
53
54
55
56
57
            // Responsive grid:
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/pure-min.css'>\n"
            "<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css'>\n"
            // JS Graphs:
            "<script src='https://cdn.plot.ly/plotly-basic-1.58.2.min.js'></script>\n"
            // Fullscreen
            "<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
            // Refresh after every measurement.
            // "<meta http-equiv='refresh' content='%d'>\n"
            "</head>\n"
            "<body>\n"

Eric Duminil's avatar
Eric Duminil committed
58
            "<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>\n"
59
60
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
61
62
#ifdef CSV_WRITER
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
63
64
            "<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
            "<li class='pure-menu-item'><a href='./%s' class='pure-menu-link'>Download CSV</a></li>\n"
65
66
#endif
            "<li class='pure-menu-item' id='led'>&#11044;</li>\n" // LED
Eric Duminil's avatar
Eric Duminil committed
67
68
69
70
71
72
            "</ul></div></div>\n"
            "<script>\n"
            // 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;\n"
            "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n"
            "</script>\n");
73
74
75
76
77
78
79
80
81
82
83
84
85
86

    body_template =
        PSTR("<div class='pure-g'>\n"
            "<div class='pure-u-1' id='graph'></div>\n" // Graph placeholder
            "</div>\n"
            "<div class='pure-g'>\n"
            //Sensor table
            "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n"
            "<tr><th>Sensor</th><th>%s</th></tr>\n"
            "<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
            "<tr><td>Temperature</td><td>%.1f&#8451;</td></tr>\n"
            "<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
            "<tr><td>Last measurement</td><td>%s</td></tr>\n"
            "<tr><td>Measurement timestep</td><td>%5d s</td></tr>\n"
87
#ifdef CSV_WRITER
88
89
            "<tr><td>Last CSV write</td><td>%s</td></tr>\n"
            "<tr><td>CSV timestep</td><td>%5d s</td></tr>\n"
90
#endif
91
92
93
#ifdef MQTT
            "<tr><td>Last MQTT publish</td><td>%s</td></tr>\n"
            "<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
94
95
96
97
98
#endif
#if defined(LORAWAN) && defined(ESP32)
            "<tr><td>Connected to LoRaWAN?</td><td>%s</td></tr>\n"
            "<tr><td>Last LoRaWAN transmission</td><td>%s</td></tr>\n"
            "<tr><td>LoRaWAN publish timestep</td><td>%5d s</td></tr>\n"
99
#endif
Eric Duminil's avatar
TODO    
Eric Duminil committed
100
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
101
102
103
104
105
106
107
108
109
            "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
            "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
            "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
            "<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
            "<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
            "<tr><td>Board</td><td>%s</td></tr>\n"
            "<tr><td>Uptime</td><td>%4d h %02d min %02d s</td></tr>\n"
            "</table>\n"
            "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
110
#ifdef CSV_WRITER
111
112
113
            "<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>\n"
114
#endif
115
116
117
118
            "</div>\n"
            "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>\n"
            "<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>\n");

119
120
121
    script_template = PSTR(
#ifdef CSV_WRITER
        "<script>\n"
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
        "document.body.style.cursor = 'default';\n"
        "fetch('./%s',{credentials:'include'})\n"
        // Get CSV, fill table and fill diagram
        ".then(response=>response.text())\n"
        ".then(csvText=>csvToTable(csvText))\n"
        ".then(htmlTable=>addLogTableToPage(htmlTable))\n"
        ".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))\n"
        ".catch(e=>console.error(e));\n"
        "xs=[];\n"
        "data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}},\n"
        "{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}},\n"
        "{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];\n"
        "layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},\n"
        "xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},\n"
        "yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},\n"
        "yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}\n"
        "};\n"
        "function csvToTable(csvText) {\n"
        "csvText=csvText.trim();\n"
        "lines=csvText.split('\\n');\n"
        "table=document.createElement('table');\n"
        "table.className='pure-table-striped';\n"
        "n=lines.length;\n"
        "lines.forEach((line,i)=>{\n"
        "fields=line.split(';');\n"
        "xs.push(fields[0]);\n"
        "data[0]['y'].push(fields[1]);\n"
        "data[1]['y'].push(fields[2]);\n"
        "data[2]['y'].push(fields[3]);\n"
        "if(i>4 && i<n-12){if(i==5){fields=['...','...','...','...']}else{return;}}\n"
        "row=document.createElement('tr');\n"
        "fields.forEach((field,index)=>{\n"
        "cell=document.createElement(i<2?'th':'td');\n"
        "cell.appendChild(document.createTextNode(field));\n"
        "row.appendChild(cell);});\n"
        "table.appendChild(row);});\n"
        "return table;}\n"
        "function addLogTableToPage(table){document.getElementById('log').appendChild(table);}\n"
        "</script>\n"
161
162
#endif
        "</body>\n" "</html>");
163
164
165

    // Web-server
    http.on("/", handleWebServerRoot);
166
#ifdef CSV_WRITER
167
168
    http.on("/" + csv_writer::filename, handleWebServerCSV);
    http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
169
#endif
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
    http.onNotFound(handlePageNotFound);
    http.begin();

    Serial.print(F("You can access this sensor via http://"));
    Serial.print(SENSOR_ID);
    Serial.print(F(".local (might be unstable) or http://"));
    Serial.println(WiFi.localIP());
  }

  // Allow access if http_user or http_password are empty, or if provided credentials match
  bool shouldBeAllowed() {
    return strcmp(config::http_user, "") == 0 || strcmp(config::http_password, "") == 0
        || http.authenticate(config::http_user, config::http_password);
  }

  void handleWebServerRoot() {
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }

    unsigned long ss = seconds();
    unsigned int hh = ss / 3600;
    ss -= hh * 3600;
    uint8_t mm = ss / 60;
    ss -= mm * 60;
    uint16_t available_fs_space = csv_writer::getAvailableSpace() / 1024;

    //NOTE: Splitting in multiple parts in order to use less RAM
    char content[2000]; // Update if needed
Eric Duminil's avatar
Eric Duminil committed
199
    // Header size : 1611 - Body size : 1652 - Script size : 1688
200

201
202
203
204
205
206
207
208
209
210
    // Header
    snprintf_P(content, sizeof(content), header_template, sensor::co2, SENSOR_ID.c_str(),
        WiFi.localIP().toString().c_str(), csv_writer::filename.c_str());

    http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    http.send_P(200, PSTR("text/html"), content);

    // Body
    snprintf_P(content, sizeof(content), body_template, SENSOR_ID.c_str(), sensor::co2, sensor::temperature,
        sensor::humidity, sensor::timestamp.c_str(), config::measurement_timestep,
211
#ifdef CSV_WRITER
212
        csv_writer::last_successful_write.c_str(), config::csv_interval,
213
#endif
214
215
#ifdef MQTT
        mqtt::last_successful_publish.c_str(), config::sending_interval,
Eric Duminil's avatar
Eric Duminil committed
216
217
218
#endif
#if defined(LORAWAN) && defined(ESP32)
        lorawan::connected ? "Yes" : "No", lorawan::last_transmission.c_str(), config::lorawan_sending_interval,
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#endif
        config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(),
        WiFi.localIP().toString().c_str(), get_free_heap_size(), available_fs_space, max_loop_duration, BOARD, hh, mm,
        ss);

    http.sendContent(content);

    // Script
    snprintf_P(content, sizeof(content), script_template, csv_writer::filename.c_str(), SENSOR_ID.c_str());

    http.sendContent(content);
  }

  void handleWebServerCSV() {
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }
    if (FS_LIB.exists(csv_writer::filename)) {
      fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
      http.sendHeader("Content-Length", String(csv_file.size()));
      http.streamFile(csv_file, F("text/csv"));
      csv_file.close();
    } else {
      http.send(204, F("text/html"), F("No data available."));
    }
  }

  void handleDeleteCSV() {
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }
    Serial.print("Removing CSV file...");
    FS_LIB.remove(csv_writer::filename);
    Serial.println(" Done!");
    http.sendHeader("Location", "/");
    http.send(303);
  }

  void handlePageNotFound() {
    http.send(404, F("text/plain"), F("404: Not found"));
  }
}