Commit 983003b4 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'develop'

parents a200c6f8 86309b4a
......@@ -2,16 +2,17 @@
#define AMPEL_UTIL_H_INCLUDED
#include <Arduino.h>
#include "config.h"
#include "wifi_util.h" // To get MAC
#include <WiFiUdp.h> //required for NTP
#include <WiFiUdp.h> // required for NTP
#include "src/lib/NTPClient-master/NTPClient.h" // NTP
#if defined(ESP8266)
# define BOARD "ESP8266"
# include <ESP8266WiFi.h> // required to get MAC address
# define get_free_heap_size() system_get_free_heap_size()
#elif defined(ESP32)
# define BOARD "ESP32"
# include <WiFi.h> // required to get MAC address
# define get_free_heap_size() esp_get_free_heap_size()
#else
# define BOARD "Unknown"
......@@ -23,6 +24,18 @@ namespace ntp {
String getLocalTime();
}
namespace util {
template<typename Tpa, typename Tpb>
inline auto min(const Tpa &a, const Tpb &b) -> decltype(a < b ? a : b) {
return b < a ? b : a;
}
template<typename Tpa, typename Tpb>
inline auto max(const Tpa &a, const Tpb &b) -> decltype(b > a ? b : a) {
return b > a ? b : a;
}
}
#define seconds() (millis() / 1000UL)
extern uint32_t max_loop_duration;
const extern String SENSOR_ID;
......
......@@ -21,14 +21,16 @@ namespace web_server {
const char *body_template;
const char *script_template;
void handleWebServerRoot();
void handleWebServerCSV();
void handlePageNotFound();
#ifdef AMPEL_CSV
void handleDeleteCSV();
void handleWebServerCSV();
#endif
#if defined(ESP8266)
ESP8266WebServer http(80); // Create a webserver object that listens for HTTP request on port 80
#endif
#if defined(ESP32)
#elif defined(ESP32)
WebServer http(80);
#endif
......@@ -41,9 +43,9 @@ namespace web_server {
PSTR("<!doctype html><html lang=en>"
"<head>\n"
"<title>%d ppm - CO2 SENSOR - %s - %s</title>\n"
"<meta charset='UTF-8'>"
"<meta charset='UTF-8'>\n"
// HfT Favicon
"<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>"
"<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>\n"
// 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"
......@@ -56,13 +58,21 @@ namespace web_server {
"</head>\n"
"<body>\n"
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> sensor</p></div></div>\n"
"<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"
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
#ifdef AMPEL_CSV
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
"<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"
"</ul></div></div>\n");
#endif
"<li class='pure-menu-item' id='led'>&#11044;</li>\n" // LED
"</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");
body_template =
PSTR("<div class='pure-g'>\n"
......@@ -71,81 +81,102 @@ namespace web_server {
"<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><th colspan='2'>%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"
"<tr><td>Last CSV write</td><td>%s</td></tr>\n"
"<tr><td>CSV timestep</td><td>%5d s</td></tr>\n"
#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"
#ifdef AMPEL_CSV
"<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"
"<tr><td>Available drive space</td><td>%d kB</td></tr>\n"
#endif
#ifdef AMPEL_MQTT
"<tr><th colspan='2'>MQTT</th></tr>\n"
"<tr><td>Connected?</td><td>%s</td></tr>\n"
"<tr><td>Last publish</td><td>%s</td></tr>\n"
"<tr><td>Timestep</td><td>%5d s</td></tr>\n"
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
"<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"
#endif
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n"
"<tr><th colspan='2'>Sensor</th></tr>\n"
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
"<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"
// CSV placeholder
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
#ifdef AMPEL_CSV
"<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"
#endif
"</div>\n");
script_template = PSTR("<script>\n"
"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"
"</body>\n"
"</html>");
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"
#ifdef AMPEL_CSV
"<script>\n"
"document.body.style.cursor = 'default';\n"
"fetch('./%s',{credentials:'include'})\n"
".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"
#endif
"</body>\n"
"</html>");
// Web-server
http.on("/", handleWebServerRoot);
#ifdef AMPEL_CSV
http.on("/" + csv_writer::filename, handleWebServerCSV);
http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
#endif
http.onNotFound(handlePageNotFound);
http.begin();
......@@ -171,14 +202,18 @@ namespace web_server {
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
// Header size : 1383 - Body size : 1246 - Script size : 1648
// Header size : 1611 - Body size : 1800 - Script size : 1920
// Header
snprintf_P(content, sizeof(content), header_template, sensor::co2, SENSOR_ID.c_str(),
WiFi.localIP().toString().c_str(), csv_writer::filename.c_str());
WiFi.localIP().toString().c_str()
#ifdef AMPEL_CSV
, csv_writer::filename.c_str()
#endif
);
http.setContentLength(CONTENT_LENGTH_UNKNOWN);
http.send_P(200, PSTR("text/html"), content);
......@@ -186,22 +221,32 @@ namespace web_server {
// 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,
csv_writer::last_successful_write.c_str(), config::csv_interval,
#ifdef MQTT
mqtt::last_successful_publish.c_str(), config::sending_interval,
#ifdef AMPEL_CSV
csv_writer::last_successful_write.c_str(), config::csv_interval, csv_writer::getAvailableSpace() / 1024,
#endif
#ifdef AMPEL_MQTT
mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish.c_str(), config::sending_interval,
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan::connected ? "Yes" : "No", LMIC_FREQUENCY_PLAN, lorawan::last_transmission.c_str(),
config::lorawan_sending_interval,
#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);
WiFi.localIP().toString().c_str(), get_free_heap_size(), 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());
snprintf_P(content, sizeof(content), script_template
#ifdef AMPEL_CSV
, csv_writer::filename.c_str(), SENSOR_ID.c_str()
#endif
);
http.sendContent(content);
}
#ifdef AMPEL_CSV
void handleWebServerCSV() {
if (!shouldBeAllowed()) {
return http.requestAuthentication(DIGEST_AUTH);
......@@ -226,6 +271,7 @@ namespace web_server {
http.sendHeader("Location", "/");
http.send(303);
}
#endif
void handlePageNotFound() {
http.send(404, F("text/plain"), F("404: Not found"));
......
......@@ -2,18 +2,22 @@
#define WEB_SERVER_H_
#if defined(ESP8266)
# include <ESP8266WebServer.h>
#endif
#if defined(ESP32)
#elif defined(ESP32)
# include <WebServer.h>
#endif
#include "config.h"
#include "util.h"
#include "co2_sensor.h"
#include "csv_writer.h"
#ifdef MQTT
#ifdef AMPEL_CSV
# include "csv_writer.h"
#endif
#ifdef AMPEL_MQTT
# include "mqtt.h"
#endif
#ifdef AMPEL_LORAWAN
# include "lorawan.h"
#endif
namespace web_server {
void initialize();
......
......@@ -2,16 +2,11 @@
namespace config {
// WiFi config. See 'config.h' if you want to modify those values.
#ifdef WIFI_SSID
const char *wifi_ssid = WIFI_SSID;
const char *wifi_password = WIFI_PASSWORD;
#else
const char *wifi_ssid = "NO_WIFI";
const char *wifi_password = "";
#endif
#ifdef WIFI_TIMEOUT
const uint8_t wifi_timeout = WIFI_TIMEOUT; // [s] Will try to connect during wifi_timeout seconds before failing.
const uint8_t wifi_timeout = WIFI_TIMEOUT; // [s] Will try to connect during wifi_timeout seconds before failing.
#else
const uint8_t wifi_timeout = 60; // [s] Will try to connect during wifi_timeout seconds before failing.
#endif
......@@ -20,18 +15,11 @@ namespace config {
// Initialize Wi-Fi
void WiFiConnect(const String &hostname) {
//NOTE: WiFi Multi could allow multiple SSID and passwords.
if (strcmp(config::wifi_ssid, "NO_WIFI") == 0) {
Serial.println("Please change WIFI_SSID in config.h if you want to connect.");
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
return;
}
WiFi.persistent(false); // Don't write user & password to Flash.
WiFi.mode(WIFI_STA); // Set ESP8266 to be a WiFi-client only
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
#if defined(ESP8266)
WiFi.hostname(hostname);
#endif
#if defined(ESP32)
#elif defined(ESP32)
WiFi.setHostname(hostname.c_str());
#endif
......@@ -41,16 +29,17 @@ void WiFiConnect(const String &hostname) {
// Wait for connection, at most wifi_timeout seconds
for (int i = 0; i <= config::wifi_timeout && (WiFi.status() != WL_CONNECTED); i++) {
LedEffects::showRainbowWheel();
led_effects::showRainbowWheel();
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
LedEffects::showKITTWheel(color::green);
led_effects::showKITTWheel(color::green);
Serial.println();
Serial.print("\nWiFi connected, IP address: ");
Serial.println(WiFi.localIP());
} else {
LedEffects::showKITTWheel(color::red);
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
led_effects::showKITTWheel(color::red);
Serial.println("\nConnection to WiFi failed");
}
}
#ifndef WIFI_UTIL_H_INCLUDED
# define WIFI_UTIL_H_INCLUDED
# if defined(ESP8266)
# include <ESP8266WiFi.h>
# elif defined(ESP32)
# include <WiFi.h>
# endif
#define WIFI_UTIL_H_INCLUDED
#include "led_effects.h"
#include "config.h"
#include "util.h"
#include "led_effects.h"
void WiFiConnect(const String &hostname);
#endif
......@@ -8,7 +8,7 @@
; http://docs.platformio.org/page/projectconf.html
[platformio]
src_dir = ./
src_dir = ampel-firmware
[env:esp8266]
platform = espressif8266
......@@ -21,3 +21,11 @@ platform = espressif32
board = ttgo-lora32-v1
framework = arduino
monitor_speed = 115200
lib_deps =
MCCI LoRaWAN LMIC library
build_flags =
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-D CFG_eu868=1
-D CFG_sx1276_radio=1
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment