Weblight

The completed unit

The Weblight-project consist of an power supply for LEDs which can be controlled over the local network. The firmware also has the option to turn the unit into a alarm clock which wakes you up using light.

Features

  • Network-controllable, dimmable LED power supply.
  • Easy configuration using a webbrowser.
  • Supports Switching on/off the light when certain IP address is reachable (this is the alarm clock feature).
  • Supports both HTTP and HTTPS.
  • Safe, password-protected control.
The main landing page
The configuration page
The components of the unit

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

The circuit diagram is still work-in-progress.