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

Example - my approach to including wifimanager into the code #801

Open TinajaLabs opened 5 years ago

TinajaLabs commented 5 years ago

Showing this here for feedback. I love wifimanager but I find it difficult to intersperse the various pieces of wifiManager code into my own code.

This is an example that I use for a tip-cup rain gauge. The wifiManager part allows a user to set the local wifi station as well as the mqtt server (node-red) and part of the mqtt topic for location info.

The main parts of wifimanager have been broken into the following methods which make it easier for me to set up for other sensors. I'm including features like On Demand button, reset with long press down, SPIFFS. I would like to know if I'm covering all the bases properly or if there's a better way to do this. Not an expert with the Arduino C-like code:

The thought here is that perhaps there could be some examples that show these pieces as methods rather than large chunks of code in the setup or loop methods.

Also, as I mentioned, I also collect some location info in the wifiManager page which is to be used to create the appropriate mqtt topics when using the sensors in different locations. Typical topics might look like this:

The first part (farm/barn/fenceline) is collected by a wifiManager parameter and then constructed into the final topic string in a separate method, setRainTopics. I would like to know if this is a reasonable approach or maybe a better way.

Thanks for any tips, Chris.

/*
   TinajaLabs.com
  ---------------------------------------------------------------
   wfmt-rainguage

   Attributes:
   - WifiManager
   - FS
   - MQTT

   - RSSI: dbm
   - davis rain guage: tip bucket rain guage
     outputs triggers indicating the bucket has tipped out 0.01 inches of rain

   Arduino IDE settings:

    Board: Feather HUZZAH ESP8266
    Flash Size: 1M (64k SPIFFS)
    lwIPVariant: v2 Lower Memory
    VTables: Flash
    CPU Frequency: 80MHz
    Upload Speed: 230400
    Erase Flash: Only Sketch
    Port: dev/ttyUSB0, dev/ttyUSB1

    Board: LOLIN (WEMOS) D1 mini Lite
    Flash Size: 1M (64k SPIFFS)
    lwIPVariant: v2 Lower Memory
    VTables: Flash
    CPU Frequency: 80MHz
    Upload Speed: 230400
    Erase Flash: Only Sketch
    Port: dev/ttyUSB0, dev/ttyUSB1

  2018.12.02 - updated new soldered rain sensor board hooked up to a Davis 6465 Rain guage
  https://www.davisinstruments.com/product_documents/weather/spec_sheets/6463_6465_SS.pdf

  ---------------------------------------------------------------
*/

#include <FS.h>           //this needs to be first, or it all crashes and burns...
#include <ESP8266WiFi.h>  //https://github.com/esp8266/Arduino
#include <DNSServer.h>
#include <WiFiManager.h>  //https://github.com/tzapu/WiFiManager
#include <ESP8266WebServer.h> //Local WebServer used to serve the configuration portal
#include <ArduinoJson.h>  //https://github.com/bblanchon/ArduinoJson
#include <Adafruit_Sensor.h>  // Adafruit Unified Sensor Driver - https://github.com/adafruit/Adafruit_Sensor

// -------------------------- pubsub for mqtt
#include <PubSubClient.h>
#ifdef ESP32
#include <SPIFFS.h>
#endif

// ========================== for this system device

// -------------------------- for wemos d1 mini - WeMos.cc
//static const uint8_t D0   = 16;
//static const uint8_t D1   = 5;
//static const uint8_t D2   = 4;
//static const uint8_t D3   = 0;
//static const uint8_t D4   = 2;
//static const uint8_t D5   = 14;
//static const uint8_t D6   = 12;
//static const uint8_t D7   = 13;
//static const uint8_t D8   = 15;
//static const uint8_t D9   = 3;
//static const uint8_t D10  = 1;

#define TRIGGER_PIN   0                 // for wifimanager, D3, GPIO0, on wemos d1 mini
#define INTERRUPT_PIN 4                 // for rain gauge, D2, GPIO04, on wemos d1 mini
#define MQTT_LED      2                 // D4, GPIO2, on wemos d1 mini
char topic_device[20] = "WemosD1Mini/"; // defines specific device

// ========================== specific sensor configs

// -------------------------- RSSI sensor value
long rssiPreviousMillis = 0;
long rssiInterval = 5000; // every 5 seconds
char chipID[10];
char rssi_base[60] = "rssi/state/value";
char rssi_topic[100];

// -------------------------- for rain guage
const unsigned int DEBOUNCE_TIME = 50;
const float BUCKET_AMT = 0.01;

volatile unsigned long rainTips = 0;
volatile unsigned long rainInches = 0;
volatile unsigned long last_interrupt_time = 0;

long lastPubSensorRain = 0;
unsigned long delayTimeSensorRain = 5000;

char rain_base[60] = "rain/state/value";
char rain_topic[100];

// -------------------------- for mqtt
WiFiClient espClient;
PubSubClient mqttClient(espClient);

long lastMqttReconnectAttempt = 0;
char mqtt_server[40] = "192.168.0.254"; // for variables we'll edit in the wifimanager

// -------------------------- for wifiManager
bool shouldSaveConfig = false;

// to save settings, Spiffs, FS
const char* CONFIG_FILE = "/config.json";

// -------------------------- generic topic strings

// topic_loc is for the location prefix for all of the topics - it is configurable
// the middle is injected with the chipid
// the rest are suffixes for the various pub/sub entities

char topic_loc[30] = "farm/house/livingroom/";  // configurable in wifiManager

char publ_base[60] = "publish/state/value";
char subs_base[60] = "subscribe/state/value";
char subs_topic[100];
char publ_topic[100];

// Function Prototypes
bool loadConfigFile();
bool saveConfigFile();

// ==========================================================
// SETUP ====================================================
void setup() {

  Serial.begin(57600);
  Serial.println("\n\n=== Starting Tinaja Device ===");
  Serial.print("=== "); Serial.println(topic_loc);
  Serial.print("=== "); Serial.println(topic_device);

  // set the pins to be used for buttons, LEDs, etc.
  pinMode(INTERRUPT_PIN, INPUT_PULLUP);
  pinMode(TRIGGER_PIN, INPUT);  // set the pin to trigger wifiManager
  pinMode(MQTT_LED, OUTPUT);    // Initialize the MQTT_LED pin as an output
  digitalWrite(MQTT_LED, LOW);  // turn on LED during setup

  runWifiManager("auto");

  // ----------------------------- set mqtt topics
  setChipId();

  setRainTopics();

  // ----------------------------- initialize sensors

  // set up rain guage Interrupt
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), rainInterrupt, FALLING);
  // attachInterrupt(INTERRUPT_PIN, rainInterrupt, FALLING);

  // ----------------------------- for mqtt client
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(mqttCallback);

  Serial.println("=== Finished setup ===");
  Serial.println("");
  digitalWrite(MQTT_LED, HIGH);  // turn off LED after setup

}

// ==========================================================
// LOOP =====================================================
void loop() {

  if (!mqttClient.connected()) {
    Serial.println("mqttClient not connected...");
    digitalWrite(MQTT_LED, LOW);  // turn on LED to indicate disconnected
    long now = millis();
    if (now - lastMqttReconnectAttempt > 10000) {
      lastMqttReconnectAttempt = now;
      // Attempt to reconnect
      if (mqttReconnect()) {
        lastMqttReconnectAttempt = 0;
      }
    }
    digitalWrite(MQTT_LED, HIGH);  // turn off LED
  } else {
    // Client connected
    mqttClient.loop();
  }

  // if config reset pin has been pushed down
  if ( digitalRead(TRIGGER_PIN) == LOW ) {
    delay(50); // poor mans debounce, not recommended for production code
    if ( digitalRead(TRIGGER_PIN) == LOW ) {
      delay(3000);
      // if config reset pin has been pushed down for 3 seconds, reset/restart
      if ( digitalRead(TRIGGER_PIN) == LOW ) {
        Serial.println("Trigger pin low for 3+ seconds. Resetting device...");
        runWifiManager("reset");     // < <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< RESET CAN HAPPEN
      } else {
        Serial.println("Starting config portal...");
        runWifiManager("auto");
      }
    }
  }

  // ----------------------------------------------------------
  // put the main code below, to run repeatedly:

  // publish msgs about the sensor stats
  rssiCheck();
  publishRainfall();

  delay(50); // longer than 50 if mqttClient is often not connected...
  digitalWrite(MQTT_LED, HIGH);  // turn off LED

}

// ==========================================================
void rainInterrupt() {
  // runs in the Interrupt as defined in setup

  if ((millis() - last_interrupt_time) > DEBOUNCE_TIME ) { // debounce of sensor signal
    rainTips++;
    last_interrupt_time = millis();
  }

}

void publishRainfall() {

  Serial.print(" rain tips: "); Serial.println(rainTips);

  long now = millis();
  if (now - lastPubSensorRain > delayTimeSensorRain) {
    digitalWrite(MQTT_LED, LOW);  // turn on LED during setup

    lastPubSensorRain = now;
    float thisTotal = rainTips * BUCKET_AMT;
    Serial.print("mqtt pub - Rainfall inches: "); Serial.println(thisTotal);
    mqttClient.publish(rain_topic, String(thisTotal).c_str());
    rainTips = 0;

    digitalWrite(MQTT_LED, HIGH);  // turn on LED during setup
  }
}

// ==========================================================
void rssiCheck() {
  if (millis() - rssiPreviousMillis > rssiInterval) {
    digitalWrite(MQTT_LED, LOW);  // turn on LED during setup

    int32_t rssiValue = WiFi.RSSI();
    char rssiMsg[50];
    snprintf (rssiMsg, 75, "%ld", WiFi.RSSI());
    Serial.print("mqtt pub - signal strength (RSSI): "); Serial.println(rssiValue);
    mqttClient.publish(rssi_topic, rssiMsg);
    rssiPreviousMillis = millis();

    digitalWrite(MQTT_LED, HIGH);  // turn on LED during setup
  }
}

// ==========================================================
void saveConfigCallback () {
  //callback notifying us of the need to save config
  Serial.println(" --- Should save config");
  shouldSaveConfig = true;
}

// ==========================================================
void runWifiManager(String mode) {

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

  wifiManager.setSaveConfigCallback(saveConfigCallback);

  if (mode == "reset") {
    Serial.println("Resetting/restarting the current configuration...");
    wifiManager.resetSettings(); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< RESET CAN HAPPEN
    ESP.restart();
  }

  // load the settings from the file system (FS), from setupSpiffs
  loadConfigFile();

  Serial.println("starting wifiManager");

  // set up some additional parameters
  WiFiManagerParameter custom_mqtt_server("mqtt_server", "MQTT server addr", mqtt_server, 40);
  WiFiManagerParameter custom_topic_loc("topic_loc", "Topic Location", topic_loc, 30);
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_topic_loc);

  wifiManager.setConfigPortalTimeout(60); // sets timeout until configuration portal gets turned off
  wifiManager.setConnectTimeout(5); // how long to try to connect for before continuing

  // TODO: add password to wifimanager login
  if (mode == "auto") {
    Serial.println("    Attempting automatic connection");
    if (!wifiManager.autoConnect("TinajaAutoAP")) {
      Serial.println("failed to connect and hit timeout");
      delay(3000);
      //reset and try again, or maybe put it to deep sleep
      ESP.reset();
      delay(5000);
    }
  } else {
    Serial.println("Attempting on demand connection");
    if (!wifiManager.startConfigPortal("TinajaOnDemandAP")) {
      Serial.println("failed to connect and hit timeout");
      delay(3000);
      //reset and try again, or maybe put it to deep sleep
      ESP.reset();
      delay(5000);
    }
  }

  strcpy(mqtt_server, custom_mqtt_server.getValue());
  strcpy(topic_loc, custom_topic_loc.getValue());

  // ----------------------------- save the custom parameters to FS
  if (shouldSaveConfig) {
    saveConfigFile();
    shouldSaveConfig = false;
  }

  //if you get here you have connected to the WiFi
  Serial.print("    OK. Device is running with IP address: ");
  Serial.println(WiFi.localIP());
  // Serial.println(WiFi.SSID());
  // Serial.println(WiFi.psk());
}

// ==========================================================
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // Switch on the LED if an 1 was received as first character
  if ((char)payload[0] == '1') {
    digitalWrite(MQTT_LED, LOW);   // Turn the LED on (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is acive low on the ESP-01)
  } else {
    digitalWrite(MQTT_LED, HIGH);  // Turn the LED off by making the voltage HIGH
  }
}

// ==========================================================
bool mqttReconnect() {

  // Loop until we're reconnected
  Serial.print("Attempting connection to MQTT server: " ); Serial.print(mqtt_server); Serial.print("... ");

  // Create a random client ID
  String clientId = "tinajaMQTTClient-";
  clientId += String(random(0xffff), HEX);

  // Attempt to connect
  if (mqttClient.connect(clientId.c_str())) {
    Serial.println(" Connected.");

    // Once connected, publish an announcement...
    mqttClient.publish(publ_topic, chipID);
    Serial.print("  published to topic: "); Serial.print(publ_topic); Serial.print(".");
    Serial.print(" Payload is chip Id: "); Serial.println(chipID);

    // ... and resubscribe
    mqttClient.subscribe(subs_topic);
    Serial.print(" subscribed to topic: "); Serial.println(subs_topic);
  } else {
    Serial.println(" Not connected.");
  }

  Serial.println();
  Serial.println("=========================================================");
  Serial.println();

  return mqttClient.connected();
}

// ==========================================================
bool loadConfigFile() {

  //clean FS, for testing
  // SPIFFS.format(); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  //read configuration from FS json
  Serial.println("");
  Serial.print("mounting FS...");

  if (SPIFFS.begin()) {
    Serial.println(" mounted.");
    if (SPIFFS.exists(CONFIG_FILE)) {
      //file exists, reading and loading
      Serial.print("reading config file... ");
      File configFile = SPIFFS.open(CONFIG_FILE, "r");
      if (configFile) {
        Serial.print(" opened and retrieved data: ");
        Serial.println("");
        size_t size = configFile.size();

        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        configFile.readBytes(buf.get(), size);
        DynamicJsonBuffer jsonBuffer;
        JsonObject& json = jsonBuffer.parseObject(buf.get());
        json.printTo(Serial);

        if (json.success()) {

          // set up the extra parameters
          if (json.containsKey("mqtt_server")) {
            strcpy(mqtt_server, json["mqtt_server"]);
          }
          if (json.containsKey("topic_loc")) {
            strcpy(topic_loc, json["topic_loc"]);
          }

          Serial.println("");
          Serial.println("successfully loaded json config");
          Serial.println("");

        } else {
          Serial.println(" >>> failed to load json config <<<");
        }
      }
    }
  } else {
    Serial.println(" >>> failed to mount FS <<<");
  }
  //end read
}

// ==========================================================
bool saveConfigFile() {

  // write configs to local file store

  Serial.println("Saving config...");
  DynamicJsonBuffer jsonBuffer;
  JsonObject& json = jsonBuffer.createObject();

  json["mqtt_server"] = mqtt_server;
  json["topic_loc"] = topic_loc;

  // Open file for writing
  File f = SPIFFS.open(CONFIG_FILE, "w");
  if (!f) {
    Serial.println("Failed to open config file for saving");
    return false;
  }

  json.prettyPrintTo(Serial);
  // Write data to file and close it
  json.printTo(f);
  f.close();

  Serial.println("\nConfig file was successfully saved");
  return true;
}

// ========================== set up the topics for this device

void setRainTopics() {

  strcat(rssi_topic, topic_loc); strcat(rssi_topic, topic_device); strcat(rssi_topic, chipID); strcat(rssi_topic, "/"); strcat(rssi_topic, "device/"); strcat(rssi_topic, rssi_base);
  strcat(publ_topic, topic_loc); strcat(publ_topic, topic_device); strcat(publ_topic, chipID); strcat(publ_topic, "/"); strcat(publ_topic, "device/"); strcat(publ_topic, publ_base);
  strcat(subs_topic, topic_loc); strcat(subs_topic, topic_device); strcat(subs_topic, chipID); strcat(subs_topic, "/"); strcat(subs_topic, "device/"); strcat(subs_topic, subs_base);

  strcat(rain_topic, topic_loc); strcat(rain_topic, topic_device); strcat(rain_topic, chipID); strcat(rain_topic, "/"); strcat(rain_topic, "rainguage/"); strcat(rain_topic, rain_base);

  Serial.println("concatenated topics: ");
  Serial.println(rssi_topic); // Serial.println(" = " + strlen(rssi_topic));
  Serial.println(publ_topic); // Serial.println(" = " + strlen(publ_topic));
  Serial.println(subs_topic); // Serial.println(" = " + strlen(subs_topic));
  Serial.println(rain_topic); // Serial.println(" = " + strlen(rain_topic));

  Serial.println("");
}

// ========================== set up the topics for this device
void setChipId() {
  snprintf(chipID, 10, "%ld", ESP.getChipId());
  Serial.println(""); Serial.print("Device chip id: "); Serial.println(chipID);
  // return chipID;
}
tablatronix commented 5 years ago

I use the development branch DEV example as my template for sample code, I have also added some better examples of use cases.

code is a bit to read, and not syntax highlighted

TinajaLabs commented 5 years ago

OK. Syntax highlighting added but I understand... it is a bit to read. ;)

The main point I was making was to come up with blocks of method code to call from setup() or loop(), with button interrupt, so as to minimize the detailed code into those methods.

I have existing hard wired sensor code into which I want to add wifiManager. Methods like runWifiManager, loadConfigFile, saveConfigFile make it easier for me to re-wire existing code.

Thanks, Chris.

tablatronix commented 5 years ago

It really all depends on flow, I would personally only start cp on demand or if creds empty

Jeppedy commented 4 years ago

I love that you worked to clean this up. I may do something similar in my code. I had a comment/suggestion/question... If "ParamSave callback" is not called, should you pull back the values from the portal? If the callback wasn't invoked, doesn't that mean the user did not intend the values to be stored? I'm still working on some mods to this library, but that's one of the changes I'm proving out within my application.

Your sketch code I'm referring to is:

//JAH: Should these two lines be moved within the IF? strcpy(mqtt_server, custom_mqtt_server.getValue()); strcpy(topic_loc, custom_topic_loc.getValue());

// ----------------------------- save the custom parameters to FS if (shouldSaveConfig) { saveConfigFile(); shouldSaveConfig = false; }

tablatronix commented 4 years ago

Avoid doing work inside a IF or callback, only set a flag etc.

That is why the shouldSaveConfig flag is used

Jeppedy commented 4 years ago

I'm unclear. I'm not familiar with this approach of doing work you don't need to do to keep logic out of an IF block.
If the config is not to be "saved", do you want to pull back the values from the portal? (That may be the real question here) If no, then this is logic that is only needed when a certain condition is true, use the IF block. It takes up cycles, and potentially causes undesirable results later... Wouldn't it?

ramanraja commented 4 years ago

I have made similar modification in my code as @Jeppedy has done, and so far it seems to work. if the flag shouldSaveConfig is not true, I just return from the function. Please let me know if there is any logical or practical flaw in my approach. That will help correct myself. (I may not fully understand memory management and the overheads of strcpy etc.) Thanks, Rajaraman India

On Fri, Sep 18, 2020 at 8:54 PM Jeppedy notifications@github.com wrote:

I'm unclear. I'm not familiar with this approach of doing work you don't need to do to keep logic out of an IF block. If the config is not to be "saved", do you want to pull back the values from the portal? (That may be the real question here) If no, then this is logic that is only needed when a certain condition is true, use the IF block. It takes up cycles, and potentially causes undesirable results later... Wouldn't it?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/tzapu/WiFiManager/issues/801#issuecomment-694932461, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABXAAH7MSZIFAORK34DIEADSGN3URANCNFSM4GOSQLSQ .

tablatronix commented 4 years ago

sorry I misunderstood, I thought you mean "interrupt funciton" not if statement