Weblight

Die Haupteinheit

Das Weblight-Projekt besteht aus einem übers lokale Netzwerk kontrollierbaren Netzteil für LEDs. Die Features der von mir dazu entwickelten Firmware erlauben zudem eine Nutzung der Einheit als Lichtwecker. Die Firmware läuft auf einem ESP8266 und unterstützt unter anderem HTTPS.

Features

  • Über Netzwerk kontrollierbares Netzteil mit Dimming-Funktion.
  • Einfache Konfiguration via Webbrowser.
  • Unterstützt An- sowie Ausschalten bei Erreichbarkeit einer bestimmten IP-Adresse.
  • Frei wählbares Protokoll: HTTP oder HTTPS.
  • Passwortgeschützte Steuerungsseite.
Die Hauptseite
Die Konfigurationsseite
Die Komponenten der Einheit

Firmware

/*****************************************************************************
Weblight Sketch

Copyright (c) 2020 Robin Lux  <info[guesswhat]robinlux[ismissinghere]de>

Requires Pinger Library
https://github.com/bluemurder/esp8266-ping
*****************************************************************************/


#include <Pinger.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266WebServerSecure.h>
#include <EEPROM.h>
#include <ESP8266mDNS.h>
#include "myTypes.h"

//#define TESTSUITE
#define HTTPS
//#define TEMP_SENSORS



#ifdef TESTSUITE
  #define OUT_PWM LED_BUILTIN
  #define OUT_PIN D1
#else
  #define OUT_PIN D1
  #define OUT_PWM D2
#endif

static const char serverCert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
INSERT CERTIFICATE STRING HERE!
-----END CERTIFICATE-----
)EOF"
;

static const char serverKey[] PROGMEM =  R"EOF(
-----BEGIN PRIVATE KEY-----
INSERT CERTIFICATE STRING HERE!
-----END PRIVATE KEY-----
)EOF"
;


const char* www_username = "user interface user";
const char* www_password = "user password";
const char* fallback_ssid = "weblight";
volatile bool last_reachable = false;
volatile bool enableOnPing = true;
volatile bool enableOffPing = true;
volatile unsigned long lastPingTime = 0;
volatile bool pinState = false;
bool pingSuccess[3] = {false, false, false};
int pingIntervall = 30;
int ledBrigthness = 500;


extern "C"
{
  #include <lwip/icmp.h> // needed for icmp packet definitions
}

// Set global to avoid object removing after setup() routine
Pinger pinger;
IPAddress ip(192,168,1,31);
IPAddress mask(255,255,255,0);
IPAddress gate(192,168,1,1);
IPAddress device_ip(192,168,1,128);


#ifdef TESTSUITE
uint32_t freev;
uint16_t maxv;
uint8_t fragv;
#endif

#ifdef TEMP_SENSORS
#include <OneWire.h>
#include <DallasTemperature.h>
#define TEMPFETCHINTERVALL 1000*2
#define TEMP_SENSORS 10
#define ONE_WIRE_BUS D5
#define TEMPERATURE_PRECISION 9

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
DeviceAddress TemperatureProbe;

unsigned long lastTempFetch = 0;
short temperatures[TEMP_SENSORS][144] = {0};
int temperatures_i[TEMP_SENSORS] = {0};
int found_sensors = 0;
#endif


#ifdef HTTPS
  BearSSL::ESP8266WebServerSecure server(443);
#else
  ESP8266WebServer server(80);
#endif

configData_t eeprom_cfg;

void setup()
{  
  // Begin serial connection at 9600 baud
  Serial.begin(115200);
  pinMode(OUT_PIN, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.print("Loading Config ... ");

  //eraseConfig();eeprom_cfg.valid == 1
  loadConfig();
  if(eeprom_cfg.valid == 1){
    pinState = (eeprom_cfg.defaultLED == 1);
    enableOnPing = (eeprom_cfg.defaultOnPing == 1);
    enableOffPing = (eeprom_cfg.defaultOffPing == 1);
    updatePin();

    ip.fromString(eeprom_cfg.ip);
    gate.fromString(eeprom_cfg.gate);
    mask.fromString(eeprom_cfg.mask);
   
    WiFi.disconnect();
    delay(500);
    WiFi.begin(String(eeprom_cfg.SSID),String(eeprom_cfg.password));
    delay(500);
    WiFi.config(ip, gate, mask);
    device_ip.fromString(eeprom_cfg.device_ip);
    delay(500);
    Serial.print("Address : ");
    Serial.println(ip);
    Serial.print("Mask : ");
    Serial.println(mask);
    Serial.print("Gate : ");
    Serial.println(gate);
    Serial.print("Ping Address : ");
    Serial.println(device_ip);
    Serial.print("SSID : ");
    Serial.println(eeprom_cfg.SSID);
    Serial.print("Password : ");
    Serial.println(eeprom_cfg.password);
    Serial.println("Ok!");
   
  } else {
    pinState = false;
    updatePin();
    enableOnPing = true;
    eeprom_cfg.connectionTimeout = 60;
    WiFi.disconnect();
    delay(500);
    WiFi.softAP(fallback_ssid);
    delay(500);
    WiFi.config(ip, gate, mask);
    Serial.println("Failed! Used fallbacks");
  }



  configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov");
 
  Serial.print("Starting Webserver...");
#ifdef HTTPS
  server.setRSACert(new BearSSL::X509List(serverCert), new BearSSL::PrivateKey(serverKey));
#endif
  server.onNotFound(handleRoot);
  server.on("/", handleRoot);
  server.on("/settings", handleUpdatePage);
  server.on("/update", HTTP_POST, updateSettings);
  server.on("/updatedefaults", HTTP_POST, updateDefaultSettings);
  server.on("/reset", HTTP_POST, resetController);
  server.on("/resetsettings", HTTP_POST, resetSettings);
  #ifdef TEMP_SENSORS
  server.on("/temp", handleTempPage);
  #endif
  server.begin();
  Serial.print("Ok\n");

  if (MDNS.begin("weblight")) {
    Serial.println("MDNS responder started");
  }
 
  // Wait connection completed
  Serial.print("Connecting to AP...");
  int ticks_since_connect = 0;
  while((WiFi.status() != WL_CONNECTED) and (ticks_since_connect < eeprom_cfg.connectionTimeout))
  {
    delay(500);
    Serial.print(".");
    ticks_since_connect++;
  }
  if(ticks_since_connect == eeprom_cfg.connectionTimeout){
    Serial.println("Can't connect to AP");
    WiFi.disconnect();
    WiFi.softAP("ESP8266", "82668266");
    Serial.print("Config interface at: ");
    Serial.println(WiFi.softAPIP());
  }
  Serial.println("Ok\n");
  lastPingTime = millis();

  #ifdef TEMP_SENSORS
  lastTempFetch = millis();
  sensors.begin();
  #endif

 
  pinger.OnReceive([](const PingerResponse& response)
  {
     if(response.ReceivedResponse){
       pingSuccess[0] = true;
     }

     pingSuccess[0] = response.ReceivedResponse;
    return false;
  });
}



void updatePin(){
#ifdef TESTSUITE
  digitalWrite(OUT_PIN, !pinState);
  analogWrite(OUT_PWM, ledBrigthness);
#else
  digitalWrite(OUT_PIN, pinState);
  analogWrite(OUT_PWM, ledBrigthness);
#endif
 
}

#ifdef TEMP_SENSORS
void handleTempPage(){
  char status_String[576] = {};
  char partString[4] = {0};
  int probe_id = 0;
 
  if(server.hasArg("probeId")){
    probe_id = server.arg("probeId").toInt();
  }

  if((probe_id > 9) || (probe_id < 0)){
    server.send(400, "text/html", "Invalid Sensor ID!");
    return;
  }
  int temp_index = temperatures_i[0];
 
  Serial.printf("Sending Temperatures of probe %d ...", probe_id);
  for(int status_i = 0; status_i < 144; ++status_i){
   
    sprintf(partString, "%03d,", temperatures[0][temp_index]/2);
    for(int i = 0; i < 4; i++){
      status_String[i + status_i*4] = partString[i];
    }
    //memcpy((status_String + status_i*4), &partString, 4);
    temp_index = (temp_index+1)%144;
  }
  Serial.println("done");
  server.send(200, "text/html", status_String);
}
#endif

void handleRoot(){
  // Simple Auth
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  String status_string;
  char timeString[50];
  sprintf(timeString, "Time since last device Ping: %d", (long) ((millis() - lastPingTime) / 1000));
  // Give the device some time to leave WiFi
  status_string = "<title>LED Lamp</title><center><font size='20'>Manage LED Lamp</font><font size='5'>Copyright (c) 2020 Robin Lux</font><br><br>";
  status_string += String(timeString);
  status_string += "<br><br><form action='/update' method='POST'>LED &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='led' value='1' ";
  if(pinState){
    status_string += "checked>ON    <input type='radio' name='led' value='0'>OFF";
  } else {
    status_string += ">ON    <input type='radio' name='led' value='0' checked>OFF";
  }
  status_string += "<br><br>Activation Ping Mode &nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='pingon' value='1' ";
  if(enableOnPing){
    status_string += "checked>ON    <input type='radio' name='pingon' value='0'>OFF";
  } else {
    status_string += ">ON    <input type='radio' name='pingon' value='0' checked>OFF";
  }
  status_string += "<br>Deactivation Ping Mode &nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='pingoff' value='1' ";
  if(enableOffPing){
    status_string += "checked>ON    <input type='radio' name='pingoff' value='0'>OFF";
  } else {
    status_string += ">ON    <input type='radio' name='pingoff' value='0' checked>OFF";
  }
  status_string += "<br><br>LED Brightness: <input type='text' value='" + String(ledBrigthness) + "' name='brigthness'>";
  status_string += "<br><br><input type='submit' value='Send'></form><a href='/settings'>Settings</a></center>";


  status_string += "<center>Link History: ";
  for(int i = 0; i < 3; i++){
    if(pingSuccess[i]){
      status_string += "-";
    } else {
      status_string += "_";
    }
  }
  status_string += "</center>";
  server.send(200, "text/html", status_string);
}


void handleUpdatePage(){
  // Simple Auth
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  String status_string;
  char timeString[50];
  long millisecs = millis();
  sprintf(timeString, "Uptime: %02d:%02d:%02d", int((millisecs / (1000 * 60 * 60 * 24)) % 365), int((millisecs / (1000 * 60 * 60)) % 24), int((millisecs / (1000 * 60)) % 60));
  // Give the device some time to leave WiFi
  status_string = "<title>Settings</title><center>" + String(timeString);
  status_string += "<br><br><form action='/updatedefaults' method='POST'>Default LED &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='led' value='1' ";
  if(eeprom_cfg.defaultLED){
    status_string += "checked>ON    <input type='radio' name='led' value='0'>OFF";
  } else {
    status_string += ">ON    <input type='radio' name='led' value='0' checked>OFF";
  }
  status_string += "<br><br>Default Activation Ping Mode &nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='pingon' value='1' ";
  if(eeprom_cfg.defaultOnPing){
    status_string += "checked>ON    <input type='radio' name='pingon' value='0'>OFF<br>";
  } else {
    status_string += ">ON    <input type='radio' name='pingon' value='0' checked>OFF";
  }
  status_string += "<br>Default Deactivation Ping Mode &nbsp;&nbsp;&nbsp;&nbsp;:<input type='radio' name='pingoff' value='1' ";
  if(eeprom_cfg.defaultOffPing){
    status_string += "checked>ON    <input type='radio' name='pingoff' value='0'>OFF<br><br>";
  } else {
    status_string += ">ON    <input type='radio' name='pingoff' value='0' checked>OFF<br><br>";
  }
  status_string += "Ping Intervall &nbsp;&nbsp;&nbsp;:<input type='text' name='defaultintervall' value='" + String(eeprom_cfg.pingIntervall) + "' ><br>";
  status_string += "Device Address :<input type='text' name='defaultdaddr' value='" + String(eeprom_cfg.ip) + "' ><br>";
  status_string += "Subnet Mask &nbsp;&nbsp;&nbsp;&nbsp;:<input type='text' name='defaultmask' value='" + String(eeprom_cfg.mask) + "' ><br>";
  status_string += "Gate Address &nbsp;&nbsp;&nbsp;:<input type='text' name='defaultgate' value='" + String(eeprom_cfg.gate) + "' ><br>";
  status_string += "Ping Address &nbsp;&nbsp;&nbsp;:<input type='text' name='defaultpaddr' value='" + String(eeprom_cfg.device_ip) + "' ><br><br>";
  status_string += "SSID &nbsp;&nbsp;&nbsp;:<input type='text' name='wifiName' value='" + String(eeprom_cfg.SSID) + "' ><br>";
  status_string += "Password &nbsp;&nbsp;&nbsp;:<input type='text' name='wifiPasswd' value='" + String(eeprom_cfg.password) + "' ><br>";
  status_string += "AP Connection Timeout &nbsp;&nbsp;&nbsp;:<input type='text' name='connecttimeout' value='" + String(eeprom_cfg.connectionTimeout) + "' ><br>";
  status_string += "<br><br><input type='submit' value='Save'></form><form action='/reset' method='POST'><input type='submit' value='Reset'></form><form action='/resetsettings' method='POST'><input type='submit' value='Reset Settings'></form><br><a href='/'>Main Page</a>Copyright (c) 2020 Robin Lux";

  // Display build info

  status_string += "<br><br><br> Build Time :";
  status_string += __DATE__ " " __TIME__;
 
  #ifdef TEMP_SENSORS
  status_string += "<br>Build Option: Temperature Sensors";
  #endif

  #ifdef TESTSUITE
  status_string += "<br>Build Option: Test Suite";
  #endif

  #ifdef HTTPS
  status_string += "<br>Build Option: HTTPS Server";
  #endif

  status_string += "</center>";
  server.send(200, "text/html", status_string);
 
}

void updateSettings(){

  last_reachable = false;
  if(server.hasArg("led")){
    pinState = (server.arg("led") == "1");
  }

  if(server.hasArg("brigthness")){
    ledBrigthness = server.arg("brigthness").toInt();
  }

  if(server.hasArg("pingon")){
    enableOnPing = (server.arg("pingon") == "1");
  }

  if(server.hasArg("pingoff")){
    enableOffPing = (server.arg("pingoff") == "1");
  }

  lastPingTime = millis();
  server.sendHeader("Location","/");        // Add a header to respond with a new location for the browser to go to the home page again
  server.send(303);
}



void updateDefaultSettings(){
  eeprom_cfg.valid = 1;
  server.arg("wifiName").toCharArray(eeprom_cfg.SSID, 31);
  server.arg("wifiPasswd").toCharArray(eeprom_cfg.password, 31);
  server.arg("defaultdaddr").toCharArray(eeprom_cfg.ip, 16);
  server.arg("defaultmask").toCharArray(eeprom_cfg.mask, 16);
  server.arg("defaultgate").toCharArray(eeprom_cfg.gate, 16);
  server.arg("defaultpaddr").toCharArray(eeprom_cfg.device_ip, 16);
  eeprom_cfg.pingIntervall = server.arg("defaultintervall").toInt();
  eeprom_cfg.defaultLED = server.arg("led").toInt();
  eeprom_cfg.defaultOnPing = server.arg("pingon").toInt();
  eeprom_cfg.defaultOffPing = server.arg("pingoff").toInt();
  eeprom_cfg.connectionTimeout = server.arg("connecttimeout").toInt();

 
 


  server.sendHeader("Location","/settings");        // Add a header to respond with a new location for the browser to go to the home page again
  server.send(303);

  saveConfig();
 
}

void resetController(){
  server.sendHeader("Location","/settings");        // Add a header to respond with a new location for the browser to go to the home page again
  server.send(303);

  delay(1000);
  ESP.restart();
 
}

void resetSettings(){
  eraseConfig();
  resetController();
}


void eraseConfig() {
  // Reset EEPROM bytes to '0' for the length of the data structure
  EEPROM.begin(512);
  for (int i = 0 ; i < sizeof(eeprom_cfg) ; i++) {
    EEPROM.write(i, 0);
  }
  delay(200);
  EEPROM.commit();
  EEPROM.end();
}

void saveConfig() {
  // Save configuration from RAM into EEPROM
  EEPROM.begin(512);
  EEPROM.put(0, eeprom_cfg);
  delay(200);
  EEPROM.commit();                      // Only needed for ESP8266 to get data written
  EEPROM.end();                         // Free RAM copy of structure
}

void loadConfig() {
  EEPROM.begin(512);
  EEPROM.get(0, eeprom_cfg);
  EEPROM.end();                       // Free RAM copy of structure
}



void loop()
{

  server.handleClient();
  MDNS.update();
  updatePin();

  //analogWrite(OUT_PWM, (millis()/10)%1000);

  if((millis() - lastPingTime) > 333*eeprom_cfg.pingIntervall){
    lastPingTime = millis();

    // Use buffer to determine if device is up or down and toggle light
    if(enableOnPing and pingSuccess[0] and pingSuccess[1] and pingSuccess[2]){
      pinState = true;
    }

    if(enableOffPing and not pingSuccess[0] and not pingSuccess[1] and not pingSuccess[2]){
      pinState = false;
    }
   
    #ifdef TESTSUITE
    ESP.getHeapStats(&freev, &maxv, &fragv);
    Serial.printf("DEBUG: Free RAM: %d", freev);
    Serial.printf("Ping state: %d %d\n", pingSuccess[0], pingSuccess[1]);
    #endif

    // update sample buffer
    pingSuccess[2] = pingSuccess[1];
    pingSuccess[1] = pingSuccess[0];
    pingSuccess[0] = false;
    pinger.Ping(device_ip);
    pinger.Ping(device_ip);
    pinger.Ping(device_ip);
    pinger.Ping(device_ip);

    lastPingTime = millis();
  }

  #ifdef TEMPSENSORS
  if(millis() - lastTempFetch > TEMPFETCHINTERVALL){
   
    sensors.requestTemperatures();
    for(int i = 0; i < TEMP_SENSORS; ++i){
      if(!sensors.getAddress(TemperatureProbe, i)){
        if(i == 0){
          found_sensors = 0;
          Serial.println("No OneWire Temperature Sensors!");
        }
        break; // no more temperature sensors to read
      }
      found_sensors = (i+1);
      sensors.setResolution(TemperatureProbe, TEMPERATURE_PRECISION);
      temperatures[i][temperatures_i[i]] = (int) sensors.getTempC(TemperatureProbe)*2;
      Serial.printf("Got Temp: %f for Sensor %d\n", sensors.getTempC(TemperatureProbe), i);
      temperatures_i[i]++;
      if(temperatures_i[i] > 143) temperatures_i[i] = 0;
    }

    lastTempFetch = millis();
  }
  #endif

}

#ifndef myTypes_h
#define myTypes_h

#include <WString.h>

// Types 'byte' und 'word' doesn't work!
typedef struct {
  int valid;                        // 0=no configuration, 1=valid configuration
  char SSID[31];                    // SSID of WiFi
  char password[31];                // Password of WiFi
  char ip[16];
  char mask[16];
  char gate[16];
  char device_ip[16];
  int defaultLED;
  int defaultOnPing;
  int defaultOffPing;
  int pingIntervall;
  int connectionTimeout;
  int fadeOnMax;
  int fadeOnTime;
  int fadeOffMin;
  int fadeOffTime;
} configData_t;

#endif

Der Schaltplan des Netzteil ist noch in Arbeit.