Boilerplate WiFi Access Point, Captive Portal and MQTT Subscriber Sketch for ESP8266 and ESP32

This project is based on Hieromon Ikasamo's standard AutoConnect that is meant to publish the current wireless signal level (RSSI) to a configurable MQTT server.

The sketch has been modified to subscribe instead to an MQTT topic on a server and to set a GPIO pin to HIGH, respectively LOW, whenever a specifically formatted message is published by a client on the MQTT bus.

The following changes and additions have been made:

  • adds a page to subscribe to a configurable MQTT topic in the captive portal itself - with retention of the MQTT settings over reboot,
    • makes sure to reload the MQTT settings once they have been changed in the interface (previously, changes would not apply and would require a controller reset).
  • makes the ESP set various pins HIGH or LOW depending on a message received on the MQTT topic via JSON formatted messages,
  • ties the AP BSSID to the hardware (CPU serial) such that the broadcasted name is unique and will not interfere with other devices running the same sketch.
  • allows the user to specify the MQTT client ID or by default a hardware-(CPU serial, just like the AP BSSID) based client ID is assumed - contrasted to the original solution which involves randomly generating a string as the MQTT client ID every time the hardware is restarted,
  • uses ESP8266 specific GPIO pin definitions instead of raw values that do not correspond to ESP8266 (for instance: pin 0 is, in fact, defined as 16).

The code is written for ESP8266 and ESP32.

Usage

Once an MQTT server is configured to subscribe to a certain topic, say esp/door, then whenever a message is received on the MQTT topic in the folllwing JSON format:

{
  "pin": PIN,
  "state": STATE
}

where:

  • PIN is a GPIO pin number (as printed on the board, since the WeMoS may use a different numbering internally),
  • STATE is either the string on or off

then the board will set the state of the GPIO pin.

Whenever the WeMoS looses and regains power, the board will re-connect to the configured WiFi network and also subscribe to the configured MQTT server. In case the wireless network ever changes, then the WeMoS will fail to connect and will fall back to AP mode such that a wireless client such as a PC can connect to the WeMoS and reconfigure the board appropriately without having to reprogram the board.

Code

File: http://svn.grimore.org/arduino-sketches/arduinoPinToggle/arduinoPinToggle.ino -

/*
This Arduino script is a boilerplate AP / connect and reconnect portal
that is used to subscribe to a configured MTT server and listen for
JSON messages in order to set the state of various GPIO pins.
 
Based on the documentation for AutoConnect by 2018 Hieromon Ikasamo
and modified by 2019 Wizardry and Steamworks.
*/
 
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266mDNS.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
#define GET_CHIPID()  (ESP.getChipId())
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <SPIFFS.h>
#include <HTTPClient.h>
#include <ESP32WebServer.h>
#include <ESPmDNS.h>
#define GET_CHIPID()  ((uint16_t)(ESP.getEfuseMac()>>32))
#endif
#include <FS.h>
#include <PubSubClient.h>
#include <AutoConnect.h>
#include <ArduinoOTA.h>
 
#define MQTT_SETTINGS_FILE "/mqtt_settings.json"
#define OTA_SETTINGS_FILE "/ota_settings.json"
#define MQTT_SETTINGS_URI "/mqtt_settings"
#define MQTT_SAVE_URI "/mqtt_save"
#define OTA_SETTINGS_URI "/update_settings"
#define OTA_SAVE_URI "/update_save"
 
// MQTT settings for portal interface.
static const char AUX_MQTT_SETTINGS[] PROGMEM = R"raw(
[
  {
    "title": "MQTT Settings",
    "uri": "/mqtt_settings",
    "menu": true,
    "element": [
      {
        "name": "style",
        "type": "ACStyle",
        "value": "label+input,label+select{position:sticky;left:120px;width:230px!important;box-sizing:border-box;}"
      },
      {
        "name": "header",
        "type": "ACText",
        "value": "<h2>MQTT broker settings</h2>",
        "style": "text-align:center;color:#2f4f4f;padding:10px;"
      },
      {
        "name": "caption",
        "type": "ACText",
        "value": "Subscribe to MQTT for pin toggling.",
        "style": "font-family:serif;color:#4682b4;"
      },
      {
        "name": "mqttServer",
        "type": "ACInput",
        "label": "Server",
        "placeholder": "MQTT server"
      },
      {
        "name": "mqttPort",
        "type": "ACInput",
        "label": "Port",
        "value": "1883",
        "placeholder": "MQTT port",
        "pattern": "^[0-9]+?$"
      },
      {
        "name": "mqttUsername",
        "type": "ACInput",
        "label": "MQTT username"
      },
      {
        "name": "mqttPassword",
        "type": "ACInput",
        "label": "MQTT password"
      },
      {
        "name": "mqttTopic",
        "type": "ACInput",
        "label": "MQTT topic"
      },
      {
        "name": "mqttClientId",
        "type": "ACInput",
        "label": "MQTT client ID"
      },
      {
        "name": "newline",
        "type": "ACElement",
        "value": "<hr>"
      },
      {
        "name": "save",
        "type": "ACSubmit",
        "value": "Save",
        "uri": "/mqtt_save"
      },
      {
        "name": "discard",
        "type": "ACSubmit",
        "value": "Discard",
        "uri": "/"
      }
    ]
  },
  {
    "title": "MQTT Settings",
    "uri": "/mqtt_save",
    "menu": false,
    "element": [
      {
        "name": "caption",
        "type": "ACText",
        "value": "<h4>Parameters saved as:</h4>",
        "style": "text-align:center;color:#2f4f4f;padding:10px;"
      },
      {
        "name": "parameters",
        "type": "ACText"
      }
    ]
  }
]
)raw";
 
// OTA Settings
static const char AUX_OTA_SETTINGS[] PROGMEM = R"raw(
[
  {
    "title": "OTA Settings",
    "uri": "/update_settings",
    "menu": true,
    "element": [
      {
        "name": "style",
        "type": "ACStyle",
        "value": "label+input,label+select{position:sticky;left:120px;width:230px!important;box-sizing:border-box;}"
      },
      {
        "name": "header",
        "type": "ACText",
        "value": "<h2>OTA Update Settings</h2>",
        "style": "text-align:center;color:#2f4f4f;padding:10px;"
      },
      {
        "name": "caption",
        "type": "ACText",
        "value": "Settings for OTA updates.",
        "style": "font-family:serif;color:#4682b4;"
      },
      {
        "name": "otaPassword",
        "type": "ACInput",
        "label": "Password",
        "placeholder": "esp"
      },
      {
        "name": "newline",
        "type": "ACElement",
        "value": "<hr>"
      },
      {
        "name": "save",
        "type": "ACSubmit",
        "value": "Save",
        "uri": "/update_save"
      },
      {
        "name": "discard",
        "type": "ACSubmit",
        "value": "Discard",
        "uri": "/"
      }
    ]
  },
  {
    "title": "OTA Settings",
    "uri": "/update_save",
    "menu": false,
    "element": [
      {
        "name": "caption",
        "type": "ACText",
        "value": "<h4>Parameters saved as:</h4>",
        "style": "text-align:center;color:#2f4f4f;padding:10px;"
      },
      {
        "name": "parameters",
        "type": "ACText"
      }
    ]
  }
]
)raw";
 
// Define GPIO pins for supported architectures.
#if defined(ARDUINO_ARCH_ESP8266)
int PINS[] = { D0, D1, D2, D3, D4, D5, D6, D7, D8 };
#elif defined(ARDUINO_ARCH_ESP32)
int PINS[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 
              12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 
              23, 25, 26, 27, 32, 33, 34, 35, 36, 37, 
              38, 39 };
#endif
 
 
#if defined(ARDUINO_ARCH_ESP8266)
ESP8266WebServer httpServer; 
ESP8266HTTPUpdateServer httpUpdate;
#endif
 
AutoConnect portal(httpServer);
AutoConnectConfig config;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
 
// Wifi
wl_status_t WIFIStatus;
unsigned long WIFIUptime;
 
// MQTT
String mqttServerName;
String mqttUsername;
String mqttPassword;
String mqttPort;
String mqttTopic;
String mqttClientId;
 
// OTA
String otaPassword;
 
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String msgTopic = String(topic);
  String msgPayload = (const char *) payload;
  Serial.println("Message received on topic: " + msgTopic + " with payload: " + msgPayload);
 
  // Do not process messages for topics that have not been subscribed to.
  if(msgTopic != mqttTopic) {
    return;
  }
 
  StaticJsonDocument<255> doc;
  DeserializationError error = deserializeJson(doc, msgPayload);
  if(error) {
    Serial.println("Failed to parse message as JSON: " + String(error.c_str()));
    return;
  }
 
  const int pin = (const int) doc["pin"];
  String state = (const char*) doc["state"];
 
  if(pin == NULL || state == NULL) {
    Serial.println("Unknown parameters received.");
    return;
  }
 
  Serial.println("Setting pin: " + String(pin) + " to state: " + String(state));
 
  pinMode(PINS[pin], OUTPUT);
  if(state == "on") {
    digitalWrite(PINS[pin], HIGH);
    return;
  }
 
  digitalWrite(PINS[pin], LOW);
}
 
bool mqttConnect() {
  uint8_t retry = 3;
  while (!mqttClient.connected()) {
    if (mqttServerName.length() <= 0) {
      break;
    }
 
    mqttClient.setServer(mqttServerName.c_str(), mqttPort.toInt());
    Serial.println("Attempting MQTT broker: " + mqttServerName);
 
    if(mqttClientId == NULL || mqttClientId.isEmpty()) {
      mqttClientId = "ESP-" + String(GET_CHIPID(), HEX);
    }
 
    if (mqttClient.connect(mqttClientId.c_str(), mqttUsername.c_str(), mqttPassword.c_str())) {
      Serial.println("Established: " + String(mqttClientId));
      mqttClient.setCallback(mqttCallback);
      if(mqttClient.subscribe(mqttTopic.c_str())) {
        Serial.println("Subscribed to topic: " + mqttTopic);
      }
      return true;
    }
    else {
      Serial.println("Connection failed:" + String(mqttClient.state()));
      if (!--retry) {
        break;
      }
      delay(3000);
    }
  }
  return false;
}
 
String loadOTASettings(AutoConnectAux& aux, PageArgument& args) {
  Serial.println("Loading OTA settings.");
 
  if (SPIFFS.exists(OTA_SETTINGS_FILE)) {
    File param = SPIFFS.open(OTA_SETTINGS_FILE, "r");
    if (aux.loadElement(param)) {
      otaPassword = aux["otaPassword"].value;
      Serial.println("Loaded settings file: " OTA_SETTINGS_FILE);
    }
    else {
      Serial.println(OTA_SETTINGS_FILE " failed to load");
    }
    param.close();
  }
  else {
    Serial.println("Failed to open parameters file: " OTA_SETTINGS_FILE);
#ifdef ARDUINO_ARCH_ESP32
    Serial.println("If you get error as 'SPIFFS: mount failed, -10025', Please modify with 'SPIFFS.begin(true)'.");
#endif
  }
 
  return String("");
}
 
String loadMQTTSettings(AutoConnectAux& aux, PageArgument& args) {
  Serial.println("Loading MQTT settings.");
 
  if (SPIFFS.exists(MQTT_SETTINGS_FILE)) {
    File param = SPIFFS.open(MQTT_SETTINGS_FILE, "r");
    if (aux.loadElement(param)) {
      mqttServerName = aux["mqttServer"].value;
      mqttPort = aux["mqttPort"].value;
      mqttUsername = aux["mqttUsername"].value;
      mqttPassword = aux["mqttPassword"].value;
      mqttTopic = aux["mqttTopic"].value;
      mqttClientId = aux["mqttClientId"].value;
      Serial.println("Loaded parameters file: " MQTT_SETTINGS_FILE);
    }
    else {
      Serial.println(MQTT_SETTINGS_FILE " failed to load");
    }
    param.close();
  }
  else {
    Serial.println("Failed to open settings file: " MQTT_SETTINGS_FILE);
#ifdef ARDUINO_ARCH_ESP32
    Serial.println("If you get error as 'SPIFFS: mount failed, -10025', Please modify with 'SPIFFS.begin(true)'.");
#endif
  }
 
  return String("");
}
 
String saveOTASettings(AutoConnectAux& aux, PageArgument& args) {
  AutoConnectAux& update_settings = *portal.aux("/update_settings");
  otaPassword = update_settings["otaPassword"].value;
 
  File param = SPIFFS.open(OTA_SETTINGS_FILE, "w");
  update_settings.saveElement(param, { "otaPassword" });
  param.close();
 
  AutoConnectText& echo = aux["parameters"].as<AutoConnectText>();
  echo.value = "Password: " + otaPassword + " ";
  echo.value += "<br>";
 
  return String("");
}
 
String saveMQTTSettings(AutoConnectAux& aux, PageArgument& args) {    
  AutoConnectAux& mqtt_settings = *portal.aux("/mqtt_settings");
  mqttServerName = mqtt_settings["mqttServer"].value;
  mqttPort = mqtt_settings["mqttPort"].value;
  mqttUsername = mqtt_settings["mqttUsername"].value;
  mqttPassword = mqtt_settings["mqttPassword"].value;
  mqttTopic = mqtt_settings["mqttTopic"].value;
  mqttClientId = mqtt_settings["mqttClientId"].value;
 
  File param = SPIFFS.open(MQTT_SETTINGS_FILE, "w");
  mqtt_settings.saveElement(param, { "mqttServer", "mqttPort", "mqttUsername", "mqttPassword", "mqttTopic", "mqttClientId" });
  param.close();
 
  // Echo back saved parameters to AutoConnectAux page.
  AutoConnectText& echo = aux["parameters"].as<AutoConnectText>();
  echo.value = "Server: " + mqttServerName + " ";
 
  AutoConnectInput& mqttserver = mqtt_settings["mqttServer"].as<AutoConnectInput>();
  echo.value += mqttserver.isValid() ? String(" (OK)") : String(" (ERR)");
  echo.value += "<br>";
  echo.value += "MQTT username: " + mqttUsername + "<br>";
  echo.value += "MQTT password: " + mqttPassword + "<br>";
  echo.value += "MQTT topic: " + mqttTopic + "<br>";
  echo.value += "MQTT client ID: " + mqttClientId + "<br>";
 
  // Disconnect MQTT for reconfiguration.
  mqttClient.disconnect();
 
  return String("");
}
 
/*
 * Puts the board to sleep in case the connection to the AP cannot be established for some time.
 * Note: this requires physically bridging RST to D0 (GPIO 16 / WAKE) on an ESP8266.
 */
void maintainWifi() {
  wl_status_t currentWifiStatus = WiFi.status();
  if (WIFIStatus != currentWifiStatus && currentWifiStatus != WL_NO_SSID_AVAIL) {
    Serial.printf("Status changed: %d->%d, %ld\n", WIFIStatus, WiFi.status(), WIFIUptime);
    WIFIStatus = WiFi.status();
    WIFIUptime = millis();
  }
  else if (WIFIStatus == WL_DISCONNECTED) {
    // If disconnected state maintains while 60 seconds,
    // wait 3 minutes to reset the module
 
    if (millis() - WIFIUptime > (60 * 1000)) {
      Serial.println("WiFi disconnected, enter deep sleep.");
      // minutes x seconds x 1m
      ESP.deepSleep(5 * 60 * 1000 * 1000);
    }
  }
}
 
void redirectAutoConnect() {
  httpServer.send(308, "text/plain", "");
  httpServer.sendHeader("Location", "/_ac");
}
 
void setup() {
  SPIFFS.begin();
  Serial.begin(115200);
  Serial.println();
 
  if (portal.load(FPSTR(AUX_MQTT_SETTINGS))) {
    AutoConnectAux& mqtt_settings = *portal.aux(MQTT_SETTINGS_URI);
    PageArgument args;
    loadMQTTSettings(mqtt_settings, args);
    portal.on(MQTT_SETTINGS_URI, loadMQTTSettings);
    portal.on(MQTT_SAVE_URI, saveMQTTSettings);
  } else {
    Serial.println("Unable to load MQTT settings.");
  }
 
  if(portal.load(FPSTR(AUX_OTA_SETTINGS))) {
    AutoConnectAux& update_settings = *portal.aux(OTA_SETTINGS_URI);
    PageArgument args;
    loadOTASettings(update_settings, args);
    portal.on(OTA_SETTINGS_URI, loadOTASettings);
    portal.on(OTA_SAVE_URI, saveOTASettings);
 
    // Set up OTA update server.
    httpUpdate.setup(&httpServer, "/update", "ESP-" + String(GET_CHIPID(), HEX), update_settings["otaPassword"].value);
    if (MDNS.begin("esp-webupdate")) {
      MDNS.addService("http", "tcp", 80);
    }
    Serial.println("OTA update server started.");
  } else {
    Serial.println("Unable to load OTA settings.");
  }
 
  // Append the ESP chip identifier to the AP BSSID for uniqueness.
  config.apid = "ESP-" + String(GET_CHIPID(), HEX);
  config.bootUri = AC_ONBOOTURI_HOME;
  config.homeUri = "/_ac";
  httpServer.on("/", redirectAutoConnect);
  portal.config(config);
 
  Serial.print("WiFi starting up...");
  if (portal.begin()) {
    WIFIStatus = WiFi.status();
    WIFIUptime = millis();
    Serial.println("Connected to AP: " + WiFi.SSID());
    Serial.println("IP: " + WiFi.localIP().toString());
  }
  else {
    Serial.println("Could not connect to AP: " + String(WiFi.status()));
  }
}
 
void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    if (!mqttClient.connected()) {
        mqttConnect();
    }
    mqttClient.loop();
  }
  maintainWifi();
  MDNS.update();
  portal.handleClient();
}

fuss/arduino/start.txt ยท Last modified: 2019/12/13 06:19 by office

Access website using Tor Access website using i2p


For the copyright, license, warranty and privacy terms for the usage of this website please see the license, privacy and plagiarism pages.