web_server.cpp 18.1 KB
Newer Older
1
2
#include "web_server.h"

Eric Duminil's avatar
Eric Duminil committed
3
4
#if defined(ESP8266)
#  include <ESP8266WebServer.h>
Eric Duminil's avatar
Eric Duminil committed
5
#  include <ESP8266mDNS.h> //allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
Eric Duminil's avatar
Eric Duminil committed
6
7
#elif defined(ESP32)
#  include <WebServer.h>
Eric Duminil's avatar
Eric Duminil committed
8
#  include <ESPmDNS.h>
Eric Duminil's avatar
Eric Duminil committed
9
10
11
12
#endif

#include "config.h"
#include "util.h"
Eric Duminil's avatar
Eric Duminil committed
13
#include "ntp.h"
Eric Duminil's avatar
Eric Duminil committed
14
15
16
17
18
19
20
21
22
23
24
25
#include "wifi_util.h"
#include "co2_sensor.h"
#include "sensor_console.h"
#ifdef AMPEL_CSV
#  include "csv_writer.h"
#endif
#ifdef AMPEL_MQTT
#  include "mqtt.h"
#endif
#ifdef AMPEL_LORAWAN
#  include "lorawan.h"
#endif
Eric Duminil's avatar
Eric Duminil committed
26
#include <IotWebConf.h>
Eric Duminil's avatar
Eric Duminil committed
27
#include <IotWebConfUsing.h> // This loads aliases for easier class names.
28
#include <IotWebConfTParameter.h>
Eric Duminil's avatar
Eric Duminil committed
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 handlePageNotFound();
Eric Duminil's avatar
Eric Duminil committed
52
  void handleWebServerCommand();
53
54

#ifdef AMPEL_CSV
55
  void handleDeleteCSV();
56
57
  void handleWebServerCSV();
#endif
58
59
60

#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
61
#elif defined(ESP32)
62
63
64
  WebServer http(80);
#endif

Eric Duminil's avatar
Eric Duminil committed
65
66
  DNSServer dnsServer;

Eric Duminil's avatar
Eric Duminil committed
67
  IotWebConf *iotWebConf;
Eric Duminil's avatar
Eric Duminil committed
68

69
#define STRING_LEN 64
Eric Duminil's avatar
Eric Duminil committed
70
71

// -- Configuration specific key. The value should be modified if config structure was changed.
72
73
74
75
76
77
78
79
80
81
82
  const char config_version[] = "ampel_test_v3";

  static const char chooserValues[][STRING_LEN] = { "red", "blue", "darkYellow" };
  static const char chooserNames[][STRING_LEN] = { "Red", "Blue", "Dark yellow" };

  iotwebconf::TextTParameter<STRING_LEN> stringParam = iotwebconf::Builder<iotwebconf::TextTParameter< STRING_LEN>>(
      "stringParam").label("String param").build();
  iotwebconf::ParameterGroup group1 = iotwebconf::ParameterGroup("group1", "");
  iotwebconf::IntTParameter<int16_t> intParam =
      iotwebconf::Builder<iotwebconf::IntTParameter<int16_t>>("intParam").label("Int param").defaultValue(30).min(1).max(
          100).step(1).placeholder("1..100").build();
Eric Duminil's avatar
Eric Duminil committed
83
// -- We can add a legend to the separator
84
85
86
87
88
89
90
91
92
  iotwebconf::ParameterGroup group2 = iotwebconf::ParameterGroup("c_factor", "Calibration factor");
  iotwebconf::FloatTParameter floatParam = iotwebconf::Builder<iotwebconf::FloatTParameter>("floatParam").label(
      "Float param").defaultValue(0.0).step(0.1).placeholder("e.g. 23.4").build();
  iotwebconf::CheckboxTParameter checkboxParam =
      iotwebconf::Builder<iotwebconf::CheckboxTParameter>("checkParam").label("Check param").defaultValue(true).build();
  iotwebconf::SelectTParameter<STRING_LEN> chooserParam =
      iotwebconf::Builder<iotwebconf::SelectTParameter< STRING_LEN>>("chooseParam").label("Choose param").optionValues(
          (const char*) chooserValues).optionNames((const char*) chooserNames).optionCount(
          sizeof(chooserValues) / STRING_LEN).nameLength(STRING_LEN).build();
Eric Duminil's avatar
Eric Duminil committed
93

94
  void update() {
Eric Duminil's avatar
Better    
Eric Duminil committed
95
    iotWebConf->doLoop(); // Listen for HTTP requests from clients
96
97
98
  }

  void initialize() {
Eric Duminil's avatar
Eric Duminil committed
99
    iotWebConf = new IotWebConf(ampel.sensorId, &dnsServer, &http, HTTP_PASSWORD, config_version);
Eric Duminil's avatar
Eric Duminil committed
100
101
102
103
104
105
106

    const int ONBOARD_LED_PIN = 2;
# ifdef ESP8266
    iotWebConf->setStatusPin(ONBOARD_LED_PIN, LOW);
# else
    iotWebConf->setStatusPin(ONBOARD_LED_PIN, HIGH);
# endif
Eric Duminil's avatar
Eric Duminil committed
107
    iotWebConf->setWifiConnectionTimeoutMs(1000UL * WIFI_TIMEOUT);
Eric Duminil's avatar
Eric Duminil committed
108

Eric Duminil's avatar
Eric Duminil committed
109
110
111
#if defined(ESP8266)
    WiFi.hostname(ampel.sensorId);
#elif defined(ESP32)
Eric Duminil's avatar
Notes    
Eric Duminil committed
112
    WiFi.setHostname(ampel.sensorId);
Eric Duminil's avatar
Eric Duminil committed
113
114
115
116
117
118
119
120
121
122
#endif

    group1.addItem(&intParam);
    group2.addItem(&floatParam);
    group2.addItem(&checkboxParam);
    group2.addItem(&chooserParam);

    iotWebConf->addSystemParameter(&stringParam);
    iotWebConf->addParameterGroup(&group1);
    iotWebConf->addParameterGroup(&group2);
Eric Duminil's avatar
Notes    
Eric Duminil committed
123

Eric Duminil's avatar
Eric Duminil committed
124
125
126
127
128
129
    iotWebConf->setWifiConnectionCallback([]() {
      led_effects::showKITTWheel(color::green);
      Serial.println();
      Serial.print(F("WiFi - Connected! IP address: "));
      IPAddress address = WiFi.localIP();
      snprintf(wifi::local_ip, sizeof(wifi::local_ip), "%d.%d.%d.%d", address[0], address[1], address[2], address[3]);
Eric Duminil's avatar
Eric Duminil committed
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

      ntp::initialize();

      //FIXME: Somehow already started
//      if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local
//        MDNS.addService("http", "tcp", 80);
//        Serial.println(F("mDNS responder started"));
//      } else {
//        Serial.println(F("Error setting up MDNS responder!"));
//      }

#  ifdef AMPEL_MQTT
      mqtt::initialize(ampel.sensorId);
#  endif

Eric Duminil's avatar
Eric Duminil committed
145
      Serial.println(wifi::local_ip);
Eric Duminil's avatar
Eric Duminil committed
146
147
148
149
      Serial.print(F("You can access this sensor via http://"));
      Serial.print(ampel.sensorId);
      Serial.print(F(".local (might be unstable) or http://"));
      Serial.println(WiFi.localIP());
Eric Duminil's avatar
Eric Duminil committed
150
151
    });

Eric Duminil's avatar
Eric Duminil committed
152
    iotWebConf->setWifiConnectionFailedHandler([]() -> iotwebconf::WifiAuthInfo* {
Eric Duminil's avatar
Eric Duminil committed
153
154
155
156
157
      led_effects::showKITTWheel(color::red);
      Serial.println(F("Connection to WiFi failed"));
      return NULL;
    });

Eric Duminil's avatar
Eric Duminil committed
158
    iotWebConf->skipApStartup();
Eric Duminil's avatar
Better    
Eric Duminil committed
159
160
    //TODO: Add callbacks
    //TODO: Add LED effects
Eric Duminil's avatar
Eric Duminil committed
161
162
    //TODO: Allow offline config loading
    //TODO: Add default values for SSID/password
Eric Duminil's avatar
Better    
Eric Duminil committed
163
164
    //TODO: Add other params
    //TODO: Use HTTP_USER / HTTP_PASSWORD for config
Eric Duminil's avatar
Eric Duminil committed
165
166
    //TODO: Move to own class
    //TODO: Remove AP Password config?
Eric Duminil's avatar
Notes    
Eric Duminil committed
167
    //TODO: Save LoRaWAN key if possible?
Eric Duminil's avatar
Eric Duminil committed
168
169
170
    //FIXME: Why does MQTT fail? (on ESP32)

//    iotWebConf->loadConfig();
171
172
173
174
    Serial.println("<<<<<<<<<<<<<<<");
    Serial.println(stringParam.value());
    Serial.println(intParam.value());
    Serial.println(floatParam.value());
Eric Duminil's avatar
Eric Duminil committed
175
    iotWebConf->init();
176
177
178
179
    Serial.println(stringParam.value());
    Serial.println(intParam.value());
    Serial.println(floatParam.value());
    Serial.println(">>>>>>>>>>>>>>>");
Eric Duminil's avatar
Eric Duminil committed
180

Eric Duminil's avatar
Eric Duminil committed
181
    sensor_console::defineCommand("reset_config", []() {
Eric Duminil's avatar
Better    
Eric Duminil committed
182
      Serial.println(F("Resetting config..."));
Eric Duminil's avatar
Eric Duminil committed
183
184
      iotWebConf->getSystemParameterGroup()->applyDefaultValue();
      iotWebConf->saveConfig();
Eric Duminil's avatar
Better    
Eric Duminil committed
185
      Serial.println(F("Done!"));
Eric Duminil's avatar
Eric Duminil committed
186
187
    }, F("(resets the complete IotWeb config)"));

188
189
190
191
    header_template =
        PSTR("<!doctype html><html lang=en>"
            "<head>\n"
            "<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
192
            "<meta charset='UTF-8'>\n"
193
            // HfT Favicon
194
            "<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
195
196
197
198
199
200
201
202
203
204
205
206
            // 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
207
            "<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"
208
            "<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
Eric Duminil's avatar
Better    
Eric Duminil committed
209
            "<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>\n"
210
            "<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
Eric Duminil's avatar
Eric Duminil committed
211
#ifdef AMPEL_CSV
212
            "<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
213
            "<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
Eric Duminil's avatar
Eric Duminil committed
214
            "<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>\n"
215
216
#endif
            "<li class='pure-menu-item' id='led'>&#11044;</li>\n" // LED
Eric Duminil's avatar
Eric Duminil committed
217
218
219
220
221
            "</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"
Eric Duminil's avatar
Eric Duminil committed
222
223
224
            "</script>\n"
            "<div class='pure-g'>\n"
            "<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
225
226
            "</div>\n"
            "<div class='pure-g'>\n"
Eric Duminil's avatar
Eric Duminil committed
227
228
229
230
            "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n");

    body_template =
        PSTR("<tr><th colspan='2'>%s</th></tr>\n"
231
232
233
234
235
            "<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"
Eric Duminil's avatar
Eric Duminil committed
236
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
237
238
239
            "<tr><th colspan='2'>CSV</th></tr>\n"
            "<tr><td>Last write</td><td>%s</td></tr>\n"
            "<tr><td>Timestep</td><td>%5d s</td></tr>\n"
Eric Duminil's avatar
FIXMEs    
Eric Duminil committed
240
            "<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
241
#endif
Eric Duminil's avatar
Eric Duminil committed
242
#ifdef AMPEL_MQTT
Eric Duminil's avatar
Eric Duminil committed
243
            "<tr><th colspan='2'>MQTT</th></tr>\n"
244
            "<tr><td>Connected?</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
245
246
            "<tr><td>Last publish</td><td>%s</td></tr>\n"
            "<tr><td>Timestep</td><td>%5d s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
247
#endif
Eric Duminil's avatar
Eric Duminil committed
248
#if defined(AMPEL_LORAWAN) && defined(ESP32)
Eric Duminil's avatar
Eric Duminil committed
249
250
251
252
253
            "<tr><th colspan='2'>LoRaWAN</th></tr>\n"
            "<tr><td>Connected?</td><td>%s</td></tr>\n"
            "<tr><td>Frequency</td><td>%s MHz</td></tr>\n"
            "<tr><td>Last transmission</td><td>%s</td></tr>\n"
            "<tr><td>Timestep</td><td>%5d s</td></tr>\n"
254
#endif
Eric Duminil's avatar
Eric Duminil committed
255
            "<tr><th colspan='2'>Sensor</th></tr>\n"
Eric Duminil's avatar
TODO    
Eric Duminil committed
256
            "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
257
            "<tr><td>Auto-calibration?</td><td>%s</td></tr>\n"
258
259
            "<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"
260
            "<tr><td>MAC</td><td>%s</td></tr>\n"
261
            "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
262
            "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
263
264
            "<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
            "<tr><td>Board</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
265
            "<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
Eric Duminil's avatar
Eric Duminil committed
266
            "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
267
268
            "</table>\n"
            "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
Eric Duminil's avatar
Eric Duminil committed
269
            "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n"
Eric Duminil's avatar
Eric Duminil committed
270
#ifdef AMPEL_CSV
271
272
273
            "<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"
274
#endif
Eric Duminil's avatar
Eric Duminil committed
275
            "</div>\n");
276

Eric Duminil's avatar
Eric Duminil committed
277
278
279
280
    script_template =
        PSTR(
            "<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"
Eric Duminil's avatar
Eric Duminil committed
281
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
282
283
            "<script>\n"
            "document.body.style.cursor = 'default';\n"
Eric Duminil's avatar
Eric Duminil committed
284
            "fetch('%s',{credentials:'include'})\n"
Eric Duminil's avatar
Eric Duminil committed
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
            ".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"
321
#endif
Eric Duminil's avatar
Eric Duminil committed
322
323
            "</body>\n"
            "</html>");
324
325
326

    // Web-server
    http.on("/", handleWebServerRoot);
Eric Duminil's avatar
Eric Duminil committed
327
    http.on("/command", handleWebServerCommand);
Eric Duminil's avatar
Eric Duminil committed
328
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
329
    http.on(csv_writer::filename, handleWebServerCSV); //NOTE: csv_writer should have been initialized first.
330
    http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
331
#endif
Eric Duminil's avatar
Eric Duminil committed
332
333

    http.on("/config", [] {
Eric Duminil's avatar
Eric Duminil committed
334
      iotWebConf->handleConfig();
Eric Duminil's avatar
Eric Duminil committed
335
336
    });
    http.onNotFound([]() {
Eric Duminil's avatar
Eric Duminil committed
337
      iotWebConf->handleNotFound();
Eric Duminil's avatar
Eric Duminil committed
338
339
    });

Eric Duminil's avatar
Better    
Eric Duminil committed
340
    //TODO: Only once wifi connected
341
342
343
344
345
346
347
348
349
  }

  // 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() {
Eric Duminil's avatar
Eric Duminil committed
350
    if (iotWebConf->handleCaptivePortal()) {
Eric Duminil's avatar
Better    
Eric Duminil committed
351
      // -- Captive portal requests were already served.
Eric Duminil's avatar
Eric Duminil committed
352
353
      return;
    }
354
355
356
357
358
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }

    unsigned long ss = seconds();
Eric Duminil's avatar
Eric Duminil committed
359
360
    uint8_t dd = ss / 86400;
    ss -= dd * 86400;
361
362
363
364
365
366
367
    unsigned int hh = ss / 3600;
    ss -= hh * 3600;
    uint8_t mm = ss / 60;
    ss -= mm * 60;

    //NOTE: Splitting in multiple parts in order to use less RAM
    char content[2000]; // Update if needed
Eric Duminil's avatar
Eric Duminil committed
368
    // INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
369

Eric Duminil's avatar
Eric Duminil committed
370
    snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
371
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
372
        , csv_writer::filename
373
374
#endif
        );
375

376
377
    //    Serial.print(F("INFO - Header size : "));
    //    Serial.print(strlen(content));
378
379
380
381
    http.setContentLength(CONTENT_LENGTH_UNKNOWN);
    http.send_P(200, PSTR("text/html"), content);

    // Body
Eric Duminil's avatar
Eric Duminil committed
382
    snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
383
        sensor::humidity, sensor::timestamp, config::measurement_timestep,
Eric Duminil's avatar
Eric Duminil committed
384
#ifdef AMPEL_CSV
385
        csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
386
#endif
Eric Duminil's avatar
Eric Duminil committed
387
#ifdef AMPEL_MQTT
388
        mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval,
Eric Duminil's avatar
Eric Duminil committed
389
#endif
Eric Duminil's avatar
Eric Duminil committed
390
#if defined(AMPEL_LORAWAN) && defined(ESP32)
391
        lorawan::connected ? "Yes" : "No", config::lorawan_frequency_plan, lorawan::last_transmission,
Eric Duminil's avatar
Eric Duminil committed
392
        config::lorawan_sending_interval,
393
#endif
Eric Duminil's avatar
Eric Duminil committed
394
        config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
395
396
        wifi::local_ip, wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(),
        ampel.max_loop_duration, ampel.board, ampel.version, dd, hh, mm, ss);
397

398
399
    //    Serial.print(F(" - Body size : "));
    //    Serial.print(strlen(content));
400
401
402
    http.sendContent(content);

    // Script
403
404
    snprintf_P(content, sizeof(content), script_template
#ifdef AMPEL_CSV
Eric Duminil's avatar
Eric Duminil committed
405
        , csv_writer::filename, ampel.sensorId
406
407
#endif
        );
408

409
410
    //    Serial.print(F(" - Script size : "));
    //    Serial.println(strlen(content));
411
412
413
    http.sendContent(content);
  }

414
#ifdef AMPEL_CSV
415
416
417
418
419
420
  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");
Eric Duminil's avatar
Eric Duminil committed
421
422
423
      char csv_size[10];
      snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
      http.sendHeader("Content-Length", csv_size);
424
425
426
427
428
429
430
431
432
433
434
      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);
    }
Eric Duminil's avatar
Eric Duminil committed
435
    Serial.print(F("Removing CSV file..."));
436
    FS_LIB.remove(csv_writer::filename);
Eric Duminil's avatar
Eric Duminil committed
437
    Serial.println(F(" Done!"));
438
439
440
    http.sendHeader("Location", "/");
    http.send(303);
  }
441
#endif
442

Eric Duminil's avatar
Eric Duminil committed
443
444
445
446
447
448
  void handleWebServerCommand() {
    if (!shouldBeAllowed()) {
      return http.requestAuthentication(DIGEST_AUTH);
    }
    http.sendHeader("Location", "/");
    http.send(303);
449
    sensor_console::execute(http.arg("send").c_str());
Eric Duminil's avatar
Eric Duminil committed
450
451
  }

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