tzapu / WiFiManager

ESP8266 WiFi Connection manager with web captive portal
http://tzapu.com/esp8266-wifi-connection-manager-library-arduino-ide/
MIT License
6.63k stars 1.98k forks source link

A more complete example-file of "AutoConnectNonBlocking" - possible? #1559

Open Jagakatt opened 1 year ago

Jagakatt commented 1 year ago

Basic Infos

Hi! If it would be possible for someone with knowledge (which I do not have) to create a better AutoConnectNonBlocking-example-file ? It wold be really appreciated and helpful with an example where some code is executed during the "Portal Mode" and also shown how to know when exiting the portal.

I know there are some info here: https://github.com/tzapu/WiFiManager#save-settings But I do not manage to get it working. Example files are the best!

Would be really nice to reach the goal to urge someone to connect to WiFi: x with Password: y with a simple message on a display. And Finally a message "Device connected!" or something similar.. screenshot(1)

screenshot(2)

Searched the web for hours, and all I can find is others who not succeed either. So no code to "borrow" from someone. :-( And the "NonBlocking" is needed to be able to toggle between messages if I got it right.

I supply my sketchy sketch which almost gets me there. And no, I do not consider me as a programmer. Haha!. It must be a smarter way?

Hardware

WiFimanager Branch/Release: Master

Esp8266/Esp32:

Hardware: ESP-12e, esp01, esp25

Core Version: 2.4.0, staging

Description

Problem description

Settings in IDE

Module: NodeMcu, Wemos D1

Additional libraries:

Sketch

// WiFiManager in non blocking mode to toggle between messages on display. (Not made by a programmer.....)
// Original sketch: https://github.com/tzapu/WiFiManager/blob/master/examples/NonBlocking/AutoConnectNonBlocking/AutoConnectNonBlocking.ino

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C address 0x27, 16 column and 2 rows connected to SCL --> D1, SDA -> D2
#include <WiFiManager.h>             // https://github.com/tzapu/WiFiManager
WiFiManager wm;

void setup() {
  WiFi.mode(WIFI_STA);  // explicitly set mode, esp defaults to STA+AP
  // put your setup code here, to run once:
  Serial.begin(115200);
  lcd.init();  // initialize the lcd
  lcd.backlight();

  //reset settings - wipe credentials for testing
  //wm.resetSettings();

  wm.setConfigPortalBlocking(false);
  std::vector<const char *> menu = { "wifi", "restart", "exit" };  // Added by me...
  wm.setMenu(menu);
  wm.setConfigPortalTimeout(60);
  //automatically connect using saved credentials if they exist
  //If connection fails it starts an access point with the specified name
  if (wm.autoConnect("MyPortal", "password")) {
    Serial.println("connected...yeey)");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Connected!!!");
    // } else {
    //   Serial.println("Configportal running");    // Not possible to see when
    //   runningConfigPortal = true;
  }
}

unsigned int currentMillis;
unsigned int startMillis;
unsigned int WiFiErrorCount;
bool portalMode;
bool startingUp = true;

void loop() {
  wm.process();
  // put your main code here, to run repeatedly:

  currentMillis = millis();
  if (currentMillis - startMillis > 2000) {  //test whether the period has elapsed
    String checkMode;
    checkMode = WiFi.softAPIP().toString();  // See if this returns 192.168.4.1 = Portal Mode. Did not manage to find any other way to get notice
    Serial.println(checkMode);               // of when exiting from "PortalMod".
    if (checkMode == "192.168.4.1") {
      Serial.println("###################################### IN PORTAL MODE ################### ");
      portalMode = true;
      startingUp = true;
    } else {
      portalMode = false;
    }
    Serial.print("Every second - WiFi.status is : ");
    Serial.println(WiFi.status());
    if (WiFi.status() == WL_CONNECTED) {  //
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Connected!!!");
      WiFiErrorCount = 0;
      startingUp = false;
    } else if ((WiFi.status() != WL_CONNECTED) && (portalMode == false)) {
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("NOT Connected!!!");
      lcd.setCursor(0, 1);
      lcd.print("Restarting.");
      lcd.print(WiFiErrorCount);
      WiFiErrorCount++;
      if ((WiFiErrorCount > 100) && (startingUp == false)) {  // Restart device after about 200 sec if no WiFi.
        ESP.restart();
      }
    }
    startMillis = currentMillis;
  }

  if ((WiFi.status() != WL_CONNECTED) && (portalMode == true)) {  // Show configuration messages on display if no WiFi and in PortalMode.
    portalDisplayMessages();
  }

  if (WiFi.status() == WL_CONNECTED) {
    // futureCrappyCode();      // Code dependent on an active WiFi connection.
  }
}

void portalDisplayMessages() {
  int currentMillis;
  static unsigned int startMillis;
  static unsigned int messageCount = 0;

  currentMillis = millis();
  if (currentMillis - startMillis > 10000) {  //test whether the period has elapsed
    Serial.println("Running portal,  10 seconds message switch");

    switch (messageCount) {
      case 0:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("AccessPoint:");
        lcd.setCursor(0, 1);
        lcd.print("MyPortal");
        messageCount++;
        break;

      case 1:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Password:");
        lcd.setCursor(0, 1);
        lcd.print("gggggggg");
        messageCount = 0;
    }
    startMillis = currentMillis;
  }
}

Debug Messages

messages here
tablatronix commented 1 year ago

Can you layout an overview of how you want it to flow? We might need to add additional callouts, there are some open issues etc about similar

Jagakatt commented 1 year ago

Thanks for asking! Do almost get a little Christmas feeling when I can which for something! :-) It would be nice for normal non nerdy people to complete the "ConfigPortal" which I have realized watching normal people in action can be a bit confusing. So be able get some trigger to react upon when:

To be able to get some trigger of the above listed situation would at least enable me to create some nice assistance for the enduser. And I assume the NonLocking mode is the way be able to run code to toggle between messages on small displays when in portal-mode. Thanks!

tablatronix commented 1 year ago

I assume you know about the existingones already ?

//callbacks
  // called after AP mode and config portal has started
  //  setAPCallback( std::function<void(WiFiManager*)> func );
  //  
  // called after webserver has started
  //  setWebServerCallback( std::function<void()> func );
  //  
  // called when settings reset have been triggered
  //  setConfigResetCallback( std::function<void()> func );
  //  
  // called when wifi settings have been changed and connection was successful ( or setBreakAfterConfig(true) )
  //  setSaveConfigCallback( std::function<void()> func );
  //  
  // called when saving either params-in-wifi or params page
  //  setSaveParamsCallback( std::function<void()> func );
  //  
  // called when saving params-in-wifi or params before anything else happens (eg wifi)
  //  setPreSaveConfigCallback( std::function<void()> func );
  //  
  // called just before doing OTA update
  //  setPreOtaUpdateCallback( std::function<void()> func );
tablatronix commented 1 year ago

heres a crappy demo, it works, but do not use this code permanantly, just an example. I just added some callbacks into your example. in reality you should be setting states in these callbacks and NOT actually doing stuff.

Then do stuff based on those states in loop etc, for example DO NOT actually delay or print inside callbacks but keep them fast as possible. But this is quick and dirty for concept. I will work on a proper example to add to the lib ro lcd and oled maybe

// Liquidcrystal demo
// WiFiManager in non blocking mode to toggle between messages on display. (Not made by a programmer.....)
// Original sketch: https://github.com/tzapu/WiFiManager/blob/master/examples/NonBlocking/AutoConnectNonBlocking/AutoConnectNonBlocking.ino

#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C address 0x27, 16 column and 2 rows connected to SCL --> D1, SDA -> D2
#include <WiFiManager.h>             // https://github.com/tzapu/WiFiManager
WiFiManager wm;

void setup() {
  WiFi.mode(WIFI_STA);  // explicitly set mode, esp defaults to STA+AP
  // put your setup code here, to run oQnce:
  Serial.begin(115200);
  Serial.setDebugOutput(false);

  Wire.setClock(400000L);  // set i2c speed 400khz
  Wire.begin();  
  lcd.init();  // initialize the lcd
  lcd.backlight();

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Connecting to wifi .....");

  //reset settings - wipe credentials for testing
  wm.resetSettings();

  wm.setAPCallback(configModeCallback);
  // wm.setWebServerCallback(bindServerCallback);
  wm.setSaveConfigCallback(saveWifiCallback);
  // wm.setSaveParamsCallback(saveParamCallback);
  // wm.setPreOtaUpdateCallback(handlePreOtaUpdateCallback);

  wm.setConfigPortalBlocking(false);
  std::vector<const char *> menu = { "wifi", "restart", "exit" };  // Added by me...
  wm.setMenu(menu);
  wm.setConfigPortalTimeout(60);
  //automatically connect using saved credentials if they exist
  //If connection fails it starts an access point with the specified name
  if (wm.autoConnect("MyPortal", "password")) {
    Serial.println("connected...yeey)");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Connected!!!");
    // } else {
    //   Serial.println("Configportal running");    // Not possible to see when
    //   runningConfigPortal = true;
  }
}

void saveWifiCallback(){
  Serial.println("[CALLBACK] saveCallback fired");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Saving new wifi....");
      delay(2000);
}

//gets called when WiFiManager enters configuration mode
void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("[CALLBACK] configModeCallback fired");
  // myWiFiManager->setAPStaticIPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0)); 
  // Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  // Serial.println(myWiFiManager->getConfigPortalSSID());
  // 
  // esp_wifi_set_bandwidth(WIFI_IF_AP, WIFI_BW_HT20);
  lcd.clear();
  lcd.setCursor(0, 0); 
  lcd.print("NO WiFi, Connect to AP: " + wm.getConfigPortalSSID());
  delay(1000);
}

void checkPortalState(){
    bool cp = wm.getConfigPortalActive();
    bool wp = wm.getWebPortalActive();
      lcd.clear();
      lcd.setCursor(0, 0);      
    if(cp){
      lcd.print("Portal Running...");
      delay(1000);
    } else {
      lcd.print("Portal Closed!");
      delay(1000);
    }
}

unsigned int currentMillis;
unsigned int startMillis;
unsigned int WiFiErrorCount;
bool portalMode;
bool startingUp = true;

void loop() {
  wm.process();
  // put your main code here, to run repeatedly:

  currentMillis = millis();
  if (currentMillis - startMillis > 2000) {  //test whether the period has elapsed
    String checkMode;
    checkMode = WiFi.softAPIP().toString();  // See if this returns 192.168.4.1 = Portal Mode. Did not manage to find any other way to get notice
    Serial.println(checkMode);               // of when exiting from "PortalMod".
    if (checkMode == "192.168.4.1") {
      Serial.println("###################################### IN PORTAL MODE ################### ");
      portalMode = true;
      startingUp = true;
    } else {
      portalMode = false;
    }
    Serial.print("Every second - WiFi.status is : ");
    Serial.println(WiFi.status());
    if (WiFi.status() == WL_CONNECTED) {  //
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Connected!!!");
      WiFiErrorCount = 0;
      startingUp = false;
    } else if ((WiFi.status() != WL_CONNECTED) && (portalMode == false)) {
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("NOT Connected!!!");
      lcd.setCursor(0, 1);
      lcd.print("Restarting.");
      lcd.print(WiFiErrorCount);
      WiFiErrorCount++;
      if ((WiFiErrorCount > 100) && (startingUp == false)) {  // Restart device after about 200 sec if no WiFi.
        ESP.restart();
      }
    }
    startMillis = currentMillis;
  }

  checkPortalState();

  if ((WiFi.status() != WL_CONNECTED) && (portalMode == true)) {  // Show configuration messages on display if no WiFi and in PortalMode.
    portalDisplayMessages();
  }

  if (WiFi.status() == WL_CONNECTED) {
    // futureCrappyCode();      // Code dependent on an active WiFi connection.
  }
  delay(1000);
}

void portalDisplayMessages() {
  int currentMillis;
  static unsigned int startMillis;
  static unsigned int messageCount = 0;

  currentMillis = millis();
  if (currentMillis - startMillis > 10000) {  //test whether the period has elapsed
    Serial.println("Running portal,  10 seconds message switch");

    switch (messageCount) {
      case 0:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("AccessPoint:");
        lcd.setCursor(0, 1);
        lcd.print("MyPortal");
        messageCount++;
        break;

      case 1:
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Password:");
        lcd.setCursor(0, 1);
        lcd.print("gggggggg");
        messageCount = 0;
    }
    startMillis = currentMillis;
  }
}
tablatronix commented 1 year ago

63A863CB-7B37-4655-AFCF-0FFF3517692B

Jagakatt commented 1 year ago

I assume you know about the existingones already ?

//callbacks
  // called after AP mode and config portal has started
  //  setAPCallback( std::function<void(WiFiManager*)> func );
  //  
  // called after webserver has started
  //  setWebServerCallback( std::function<void()> func );
  //  
  // called when settings reset have been triggered
  //  setConfigResetCallback( std::function<void()> func );
  //  
  // called when wifi settings have been changed and connection was successful ( or setBreakAfterConfig(true) )
  //  setSaveConfigCallback( std::function<void()> func );
  //  
  // called when saving either params-in-wifi or params page
  //  setSaveParamsCallback( std::function<void()> func );
  //  
  // called when saving params-in-wifi or params before anything else happens (eg wifi)
  //  setPreSaveConfigCallback( std::function<void()> func );
  //  
  // called just before doing OTA update
  //  setPreOtaUpdateCallback( std::function<void()> func );

Sorry, I did not know of the existing ones. And even less how to put them in action. It's just a lot of magic lines of code that unfortunately makes no sense to me. Played around earlier with what's found under "Callbacks" on the WiFiManager main page here, but without success. I have only programmed for a few months so I am still at the level where I get totally chocked every time the compilation goes through without a few pages of red text...

But I definitely think I will manage to progress further thanks to the pieces of code you added to my sketch!! I will upload a cleaner version here in a few days. THANK YOU SO MUCH!!!

Jagakatt commented 1 year ago

This was obviously written on a monochrome monitor...


// A sketchy sketch to show messages on a LCD1602 when WiFiManager is in "non blocking mode"
// Messages about network status and WiFimanager configuration portal.
// And nope, I do not consider myself as a skilled programmer, but after scanning the Internet I realized I am not the only one not managing to execute code when the portal was active.
// So hopefully it can help someone.
// At least what's found below fulfilled my requirements, even though there are probably a zillion ways to do it in a better way, so feel free to modify! :-)
// Fredrik

// To be added somehow: A message when wrong network credentials were entered.
// Looks like "saveWifiCallback" only is executed when proper network credentials were entered.
// But no big deal. :-)

// Setup LCD1602 with I2C-interface
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C address 0x27, 16 column and 2 rows connected to SCL --> D1, SDA -> D2
#include <WiFiManager.h>             // https://github.com/tzapu/WiFiManager

WiFiManager wm;

bool starting;
bool newWIFIsuccess = false;

void setup() {
  WiFi.mode(WIFI_STA);  // explicitly set mode, esp defaults to STA+AP
  // put your setup code here, to run oQnce:
  Serial.begin(115200);
  Serial.setDebugOutput(true);  //
  lcd.init();                   // initialize the lcd
  lcd.backlight();

  starting = true;
  displayWiFimanagerMessages();  // Just to show the initial message.

  //reset settings - wipe credentials for testing
  //wm.resetSettings();

  wm.setAPCallback(configModeCallback);
  // wm.setWebServerCallback(bindServerCallback);
  wm.setSaveConfigCallback(saveWifiCallback);
  // wm.setSaveParamsCallback(saveParamCallback);
  // wm.setPreOtaUpdateCallback(handlePreOtaUpdateCallback);

  wm.setConfigPortalBlocking(false);
  std::vector<const char *> menu = { "wifi", "restart", "exit" };  // Limit the portal buttuns to these three.
  wm.setMenu(menu);
  wm.setConfigPortalTimeout(120);
  //automatically connect using saved credentials if they exist
  //If connection fails it starts an access point with the specified name
  if (wm.autoConnect("MyPortal", "MyPassword")) {
    Serial.println("connected...yeey)");
    // lcd.clear();
    // lcd.setCursor(0, 0);
    // lcd.print("Connected!!!");
  } else {
  }
}

void loop() {
  wm.process();  // WiFimanager to be executed frequently.
  displayWiFimanagerMessages();
}

void displayWiFimanagerMessages() {
  static unsigned int messageCount = 0;
  static unsigned long previousMillis = 0;                        // To run directly (== 0) when called from "Setup"
  if (millis() - previousMillis > 3000 || previousMillis == 0) {  // Toggle between messages every 3s.
    previousMillis = millis();

    //Serial.println(wm.getWiFiPass());  //  <-- Will get you the stored Passwd
    //Serial.println(wm.getWiFiSSID());  //  <-- Will get you the stored SSID

    if (starting) {  // Displayed when starting
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Connecting to...");
      lcd.setCursor(0, 1);
      lcd.print(wm.getWiFiSSID());
      starting = false;
      return;
    }

    if (WiFi.status() == WL_CONNECTED && !wm.getConfigPortalActive()) {  // Displayed when successfully connedted to network.
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Connected to:");
      lcd.setCursor(0, 1);
      lcd.print(wm.getWiFiSSID());
      Serial.println("Connected to : ");
      Serial.println(wm.getWiFiSSID());
    } else {
      // lcd.clear();
      // lcd.setCursor(0, 0);
      // lcd.print("NOT Connected!!!");
      // Serial.println("NOT Connected!!!");
    }

    if (WiFi.status() != WL_CONNECTED) {  // Displayed after loosing connection, and after Config portal timeout.
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("No WiFi :-\(\ ");
      lcd.setCursor(0, 1);
      lcd.print("Not good...");
    }

    if (newWIFIsuccess) {  // Displayed after proper network credentials have been enterd in portal.
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("WiFi Config");
      lcd.setCursor(0, 1);
      lcd.print("Success!!");
      newWIFIsuccess = false;
    }

    if (wm.getConfigPortalActive()) {  // Toggle between help messages on display as long as the Config Portal is active.
      switch (messageCount) {

        case 0:
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("No Internet!");
          lcd.setCursor(0, 1);
          lcd.print("Starting config.");
          messageCount++;
          break;

        case 1:
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Connect to Wifi:");
          lcd.setCursor(0, 1);
          lcd.print(wm.getConfigPortalSSID());
          messageCount++;
          break;

        case 2:
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Password:");
          lcd.setCursor(0, 1);
          lcd.print("MyPassword");
          messageCount++;
          break;

        case 3:
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("--> Conf. WiFi:");
          lcd.setCursor(0, 1);
          lcd.print("Select Network");
          messageCount++;
          break;

        case 4:
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("--> Save");
          lcd.setCursor(0, 1);

          messageCount = 0;
      }
    }

    if (wm.getWebPortalActive()) {
      //Serial.println("Web Portal is Active");
    }
  }
}

void configModeCallback(WiFiManager *myWiFiManager) {  //gets called when WiFiManager enters configuration mode
  Serial.println("[CALLBACK] configModeCallback fired");
  // myWiFiManager->setAPStaticIPConfig(IPAddress(10,0,1,1), IPAddress(10,0,1,1), IPAddress(255,255,255,0));
  // Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  // Serial.println(myWiFiManager->getConfigPortalSSID());
  //
  // esp_wifi_set_bandwidth(WIFI_IF_AP, WIFI_BW_HT20);

  // lcd.clear();
  // lcd.setCursor(0, 0);
  // lcd.print("NO WiFi, Connect to AP: " + wm.getConfigPortalSSID());
  // delay(1000);
}

void saveWifiCallback() {                                                            // Executed when saving new network credentials.
  Serial.println("[CALLBACK] saveCallback fired. Saving new network credentials.");  /// Only seam to be execuded when saving valid network credentials.

  newWIFIsuccess = true;  // To trigger message when proper network credentials have been entered.

  // lcd.clear();
  // lcd.setCursor(0, 0);
  // lcd.print("Saving new wifi....");
  // delay(2000);
}

void checkPortalState() {
  // bool cp = wm.getConfigPortalActive();
  // bool wp = wm.getWebPortalActive();
  // lcd.clear();
  // lcd.setCursor(0, 0);
  // if (cp) {
  //   lcd.print("Portal Running...");
  //   delay(1000);
  // } else {
  //   lcd.print("Portal Closed!");
  //   delay(1000);
  // }
}
tablatronix commented 1 year ago

Is this a working update?

Jagakatt commented 1 year ago

Yes, it does work. But keep in mind that I'm new to coding.

tzapu commented 1 year ago

maybe it helps, here s a sketch i ve used long ago that uses a tiny oled and displays connection prompt/statuses, ota statuses and so on. it illustrates callback usage. credits to all the people that made the original

/**The MIT License (MIT)

Copyright (c) 2016 by Daniel Eichhorn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

See more at http://blog.squix.ch
*/

/* Customizations by Neptune (NeptuneEng on Twitter, Neptune2 on Github)
 *  
 *  Added Wifi Splash screen and credit to Squix78
 *  Modified progress bar to a thicker and symmetrical shape
 *  Replaced TimeClient with built-in lwip sntp client (no need for external ntp client library)
 *  Added Daylight Saving Time Auto adjuster with DST rules using simpleDSTadjust library
 *  https://github.com/neptune2/simpleDSTadjust
 *  Added Setting examples for Boston, Zurich and Sydney
  *  Selectable NTP servers for each locale
  *  DST rules and timezone settings customizable for each locale
   *  See https://www.timeanddate.com/time/change/ for DST rules
  *  Added AM/PM or 24-hour option for each locale
 *  Changed to 7-segment Clock font from http://www.keshikan.net/fonts-e.html
 *  Added Forecast screen for days 4-6 (requires 1.1.3 or later version of esp8266_Weather_Station library)
 *  Added support for DHT22, DHT21 and DHT11 Indoor Temperature and Humidity Sensors
 *  Fixed bug preventing display.flipScreenVertically() from working
 *  Slight adjustment to overlay
 */

#include <ESP8266WiFi.h>
#include <Ticker.h>
#include "settings.h"
#include <JsonListener.h>
#include <ArduinoOTA.h>
#include <ESP8266mDNS.h>
#include <time.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <PubSubClient.h>        //https://github.com/Imroy/pubsubclient

#include "WundergroundClient.h"
#include "WeatherStationFonts.h"
#include "WeatherStationImages.h"
#include "DSEG7Classic-BoldFont.h"
#include "ThingspeakClient.h"

Adafruit_BMP280 bme;

WiFiClient wclient;
PubSubClient mqttClient(wclient);

int         lastMQTTConnectionAttempt = 0;

// Initialize Wunderground client with METRIC setting
WundergroundClient wunderground(IS_METRIC);

// Initialize the temperature/ humidity sensor
float humidity = 0.0;
float temperature = 0.0;

ThingspeakClient thingspeak;

// flag changed in the ticker function every 10 minutes
bool readyForWeatherUpdate = false;
// flag changed in the ticker function every 1 minute
bool readyForIndoorUpdate = false;

String lastUpdate = "--";

Ticker ticker;

//declaring prototypes
void configModeCallback (WiFiManager *myWiFiManager);
void drawProgress(OLEDDisplay *display, int percentage, String label);
void drawOtaProgress(unsigned int, unsigned int);
void updateData(OLEDDisplay *display);
void drawDateTime(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawCurrentWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawForecast(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawForecast2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawIndoor(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawThingspeak(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawForecastDetails(OLEDDisplay *display, int x, int y, int dayIndex);
void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state);
void setReadyForWeatherUpdate();
int8_t getWifiQuality();

// Add frames
// this array keeps function pointers to all frames
// frames are the single views that slide from right to left
FrameCallback frames[] = { drawDateTime, drawIndoor, drawCurrentWeather, drawForecast, drawForecast2 };
int numberOfFrames = 5;
//FrameCallback frames[] = { drawDateTime, drawIndoor, drawCurrentWeather };
//int numberOfFrames = 3;

OverlayCallback overlays[] = { drawHeaderOverlay };
int numberOfOverlays = 1;

void setup() {
  Serial.begin(115200);
  //power display - wemos d1 mini quick plug
  pinMode(5, OUTPUT);
  digitalWrite(5, HIGH);
  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);

  // initialize display
  display.init();
  display.clear();
  display.display();

  display.flipScreenVertically();  // Comment out to flip display 180deg
  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.setContrast(255);

  // Credit where credit is due
  display.drawXbm(34, 14, WiFi_Logo_width, WiFi_Logo_height, WiFi_Logo_bits);
  display.display();

  //BMP280
  if (!bme.begin(0x76)) {  
    Serial.println("Could not find a valid BMP280 sensor, check wiring!");
  }

  //WiFiManager
  //Local intialization. Once its business is done, there is no need to keep it around
  WiFiManager wifiManager;

  // Uncomment for testing wifi manager
  // wifiManager.resetSettings();
  wifiManager.setAPCallback(configModeCallback);

  //or use this for auto generated name ESP + ChipID
  wifiManager.autoConnect();

  //Manual Wifi
  // WiFi.begin(SSID, PASSWORD);
  String hostname(HOSTNAME);
  WiFi.hostname(hostname);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    display.clear();
    display.drawString(64, 10, "Connecting to WiFi");
    display.drawXbm(46, 30, 8, 8, counter % 3 == 0 ? activeSymbol : inactiveSymbol);
    display.drawXbm(60, 30, 8, 8, counter % 3 == 1 ? activeSymbol : inactiveSymbol);
    display.drawXbm(74, 30, 8, 8, counter % 3 == 2 ? activeSymbol : inactiveSymbol);
    display.display();

    counter++;
  }

  ui.setTargetFPS(60);
  ui.setTimePerFrame(8*1000); // Setup frame display time to 10 sec

  //Hack until disableIndicator works:
  //Set an empty symbol
  ui.setActiveSymbol(emptySymbol);
  ui.setInactiveSymbol(emptySymbol);

  ui.disableIndicator();

  // You can change the transition that is used
  // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN
  ui.setFrameAnimation(SLIDE_LEFT);

  ui.setFrames(frames, numberOfFrames);

  ui.setOverlays(overlays, numberOfOverlays);

  // Setup OTA
  Serial.println("Hostname: " + hostname);
  ArduinoOTA.setHostname((const char *)hostname.c_str());
  ArduinoOTA.onProgress(drawOtaProgress);
  ArduinoOTA.begin();

  updateData(&display);

  ticker.attach(UPDATE_INTERVAL_SECS, setReadyForWeatherUpdate);
  ticker.attach(10, setReadyForIndoorUpdate);

  mqttClient.set_server("server.com", 1883);
}

void loop() {

  if (readyForWeatherUpdate && ui.getUiState()->frameState == FIXED) {
    updateData(&display);
  }

  if (readyForIndoorUpdate && ui.getUiState()->frameState == FIXED) {
    updateIndoor();
  }

  int remainingTimeBudget = ui.update();

  if (remainingTimeBudget > 0) {
    // You can do some work here
    // Don't do stuff if you are below your
    // time budget.
    ArduinoOTA.handle();
    if (!mqttClient.connected()) {
      if(lastMQTTConnectionAttempt == 0 || millis() > lastMQTTConnectionAttempt + 10 * 60 * 1000) {
        lastMQTTConnectionAttempt = millis();
        Serial.println(millis());
        Serial.println("Trying to connect to mqtt");
        if (mqttClient.connect("spk-sauron")) {
          Serial.println("connected");
        } else {
          Serial.println("failed");
        }
      }
    } else {
      mqttClient.loop();
    }
    delay(remainingTimeBudget);
  }

}

void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("Entered config mode");
  Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  Serial.println(myWiFiManager->getConfigPortalSSID());
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.setFont(ArialMT_Plain_10);
  display.drawString(64, 10, "Wifi Manager");
  display.drawString(64, 20, "Please connect to AP");
  display.drawString(64, 30, myWiFiManager->getConfigPortalSSID());
  display.drawString(64, 40, "To setup Wifi Configuration");
  display.display();
}

void drawProgress(OLEDDisplay *display, int percentage, String label) {
  display->clear();
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64, 10, label);
  display->drawProgressBar(2, 28, 124, 12, percentage);
  display->display();
}

void drawOtaProgress(unsigned int progress, unsigned int total) {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.setFont(ArialMT_Plain_10);
  display.drawString(64, 10, "OTA Update");
  display.drawProgressBar(2, 28, 124, 12, progress / (total / 100));
  display.display();
}

void updateData(OLEDDisplay *display) {
  drawProgress(display, 10, "Updating time...");
  configTime(UTC_OFFSET * 3600, 0, NTP_SERVERS);
  drawProgress(display, 30, "Updating conditions...");
  wunderground.updateConditions(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
  drawProgress(display, 50, "Updating forecasts...");
  wunderground.updateForecast(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);

//drawProgress(display, 70, "Updating DHT Sensor");
//  humidity = dht.readHumidity();
  drawProgress(display, 80, "Updating Sensors...");
  temperature = bme.readTemperature();
  delay(500);

  drawProgress(display, 90, "Updating thingspeak...");
//  thingspeak.getLastChannelItem(THINGSPEAK_CHANNEL_ID, THINGSPEAK_API_READ_KEY);
  readyForWeatherUpdate = false;
  drawProgress(display, 100, "Done...");
  delay(1000);
}

// Called every 1 minute
void updateIndoor() {
  temperature = bme.readTemperature();
  char topic[50];
  sprintf(topic, "homie/sauron/temperature/degrees");
  String stateString = String(temperature);
  mqttClient.publish(topic, stateString);
  readyForIndoorUpdate = false;
}

void drawDateTime(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  char *dstAbbrev;
  char time_str[11];
  time_t now = dstAdjusted.time(&dstAbbrev);
  struct tm * timeinfo = localtime (&now);

  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  String date = ctime(&now);
  date = date.substring(0,11) + String(1900+timeinfo->tm_year);
  int textWidth = display->getStringWidth(date);
  display->drawString(64 + x, 5 + y, date);
  display->setFont(DSEG7_Classic_Bold_21);
  display->setTextAlignment(TEXT_ALIGN_RIGHT);

#ifdef STYLE_24HR
  sprintf(time_str, "%02d:%02d:%02d\n",timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
  display->drawString(108 + x, 19 + y, time_str);
#else
  int hour = (timeinfo->tm_hour+11)%12+1;  // take care of noon and midnight
  sprintf(time_str, "%2d:%02d:%02d\n",hour, timeinfo->tm_min, timeinfo->tm_sec);
  display->drawString(101 + x, 19 + y, time_str);
#endif

  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_10);
#ifdef STYLE_24HR
  sprintf(time_str, "%s", dstAbbrev);
  display->drawString(108 + x, 27 + y, time_str);  // Known bug: Cuts off 4th character of timezone abbreviation
#else
  sprintf(time_str, "%s\n%s", dstAbbrev, timeinfo->tm_hour>=12?"pm":"am");
  display->drawString(102 + x, 18 + y, time_str);
#endif

}

void drawCurrentWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setFont(ArialMT_Plain_10);
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->drawString(60 + x, 5 + y, wunderground.getWeatherText());

  display->setFont(ArialMT_Plain_24);
  String temp = wunderground.getCurrentTemp() + (IS_METRIC ? "°C": "°F");

  display->drawString(60 + x, 15 + y, temp);
  int tempWidth = display->getStringWidth(temp);

  display->setFont(Meteocons_Plain_42);
  String weatherIcon = wunderground.getTodayIcon();
  int weatherIconWidth = display->getStringWidth(weatherIcon);
  display->drawString(32 + x - weatherIconWidth / 2, 05 + y, weatherIcon);
}

void drawForecast(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  drawForecastDetails(display, x, y, 0);
  drawForecastDetails(display, x + 44, y, 2);
  drawForecastDetails(display, x + 88, y, 4);
}

void drawForecast2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  drawForecastDetails(display, x, y, 6);
  drawForecastDetails(display, x + 44, y, 8);
  drawForecastDetails(display, x + 88, y, 10);
}

void drawIndoor(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64 + x, y + 5, "Indoor" );
  display->setFont(ArialMT_Plain_24);
  dtostrf(temperature, 4, 1, FormattedTemperature);
  display->drawString(64 + x, y + 19, "" + String(FormattedTemperature) + (IS_METRIC ? "°C": "°F"));
  //dtostrf(humidity,4, 1, FormattedHumidity);
  //display->drawString(64+x, 30, "Humidity: " + String(FormattedHumidity) + "%");

}

void drawThingspeak(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64 + x, 0 + y, "Thingspeak Sensor");
  display->setFont(ArialMT_Plain_16);
  display->drawString(64 + x, 12 + y, thingspeak.getFieldValue(0) + "°C");
  // display->drawString(64 + x, 12 + y, thingspeak.getFieldValue(0) + (IS_METRIC ? "°C": "°F"));  // Needs code to convert Thingspeak temperature string
  display->drawString(64 + x, 30 + y, thingspeak.getFieldValue(1) + "%");
}

void drawForecastDetails(OLEDDisplay *display, int x, int y, int dayIndex) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  String day = wunderground.getForecastTitle(dayIndex).substring(0, 3);
  day.toUpperCase();
  display->drawString(x + 20, y, day);

  display->setFont(Meteocons_Plain_21);
  display->drawString(x + 20, y + 12, wunderground.getForecastIcon(dayIndex));

  display->setFont(ArialMT_Plain_10);
  display->drawString(x + 20, y + 34, wunderground.getForecastLowTemp(dayIndex) + "|" + wunderground.getForecastHighTemp(dayIndex));
  display->setTextAlignment(TEXT_ALIGN_LEFT);
}

void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) {
  int y = 55;

  display->setColor(WHITE);
  display->fillRect(0, y, 128, 9);

  display->setColor(BLACK);
  display->setFont(ArialMT_Plain_10);

  char time_str[11];
  time_t now = dstAdjusted.time(nullptr);
  struct tm * timeinfo = localtime (&now);

  display->setFont(ArialMT_Plain_10);

#ifdef STYLE_24HR
  sprintf(time_str, "%02d:%02d:%02d\n",timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
#else
  int hour = (timeinfo->tm_hour+11)%12+1;  // take care of noon and midnight
  sprintf(time_str, "%2d:%02d:%02d%s\n",hour, timeinfo->tm_min, timeinfo->tm_sec, timeinfo->tm_hour>=12?"pm":"am");
#endif

  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->drawString(64, y - 2, time_str);

  display->setTextAlignment(TEXT_ALIGN_RIGHT);
  dtostrf(temperature, 4, 1, FormattedTemperature);
  display->drawString(128, y - 2, "" + String(FormattedTemperature) + (IS_METRIC ? "°C": "°F"));

  int8_t quality = getWifiQuality();
  for (int8_t i = 0; i < 4; i++) {
    if (quality > i * 25) {
      display->drawCircle(4 + 8 * i, y + 4, 3);
    } else {
      display->fillCircle(4 + 8 * i, y + 4, 3);
    }
  }

  display->setColor(WHITE);
}

/*
void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) {
  char time_str[11];
  time_t now = dstAdjusted.time(nullptr);
  struct tm * timeinfo = localtime (&now);

  display->setFont(ArialMT_Plain_10);

#ifdef STYLE_24HR
  sprintf(time_str, "%02d:%02d:%02d\n",timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
#else
  int hour = (timeinfo->tm_hour+11)%12+1;  // take care of noon and midnight
  sprintf(time_str, "%2d:%02d:%02d%s\n",hour, timeinfo->tm_min, timeinfo->tm_sec, timeinfo->tm_hour>=12?"pm":"am");
#endif

  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->drawString(5, 52, time_str);

  display->setTextAlignment(TEXT_ALIGN_CENTER);
  String temp = wunderground.getCurrentTemp() + (IS_METRIC ? "°C": "°F");
  display->drawString(101, 52, temp);

  int8_t quality = getWifiQuality();
  for (int8_t i = 0; i < 4; i++) {
    for (int8_t j = 0; j < 2 * (i + 1); j++) {
      if (quality > i * 25 || j == 0) {
        display->setPixel(120 + 2 * i, 61 - j);
      }
    }
  }

  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(Meteocons_Plain_10);
  String weatherIcon = wunderground.getTodayIcon();
  int weatherIconWidth = display->getStringWidth(weatherIcon);
  // display->drawString(64, 55, weatherIcon);
  display->drawString(77, 53, weatherIcon);

  display->drawHorizontalLine(0, 51, 128);
}
*/

// converts the dBm to a range between 0 and 100%
int8_t getWifiQuality() {
  int32_t dbm = WiFi.RSSI();
  if(dbm <= -100) {
      return 0;
  } else if(dbm >= -50) {
      return 100;
  } else {
      return 2 * (dbm + 100);
  }
}

void setReadyForWeatherUpdate() {
  Serial.println("Setting readyForUpdate to true");
  readyForWeatherUpdate = true;
}

void setReadyForIndoorUpdate() {
  Serial.println("Setting setReadyForIndoorUpdate to true");
  readyForIndoorUpdate = true;
}