/*************************************************************************/ /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */ /*************************************************************************/ // Removing comment for debugging over the first serial port. // #define DEBUG 1 // The AP to connect to via Wifi. #define STA_SSID "" // The AP Wifi password. #define STA_PSK "" // The MQTT broker to connect to. #define MQTT_HOST "" // The MQTT broker username. #define MQTT_USERNAME "" // The MQTT broker password. #define MQTT_PASSWORD "" // The MQTT broker port. #define MQTT_PORT 1883 // The default MQTT client ID is "esp-CHIPID" where CHIPID is the ESP8266 // or ESP32 chip identifier. #define MQTT_CLIENT_ID() String("esp-" + String(GET_CHIP_ID(), HEX)) // The authentication password to use for OTA updates. #define OTA_PASSWORD "" // The OTA port on which updates take place. #define OTA_PORT 8266 // The default topic that the sketch subscribes to is "esp/CHIPID" where // CHIPID is the ESP8266 or ESP32 chip identifier. #define MQTT_TOPIC() String("esp/" + String(GET_CHIP_ID(), HEX)) // Platform specific defines. #if defined(ARDUINO_ARCH_ESP8266) #define GET_CHIP_ID() (ESP.getChipId()) #elif defined(ARDUINO_ARCH_ESP32) #define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32)) #endif // Miscellaneous defines. //#define CHIP_ID_HEX (String(GET_CHIP_ID()).c_str()) #define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX)) // Platform specific libraries. #if defined(ARDUINO_ARCH_ESP8266) #include #include #elif defined(ARDUINO_ARCH_ESP32) #include #include #endif // General libraries. #include #include #include #include #if defined(ARDUINO_ARCH_ESP32) #include #include #endif WiFiClient espClient; PubSubClient mqttClient(espClient); // 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 String mqttSerialize(StaticJsonDocument<256> msg) { char output[256]; serializeJson(msg, output, 256); return String(output); } void mqttCallback(char *topic, byte *payload, unsigned int length) { String msgTopic = String(topic); // payload is not null terminated and casting will not work char msgPayload[length + 1]; snprintf(msgPayload, length + 1, "%s", payload); #ifdef DEBUG Serial.println("Message received on topic: " + String(topic) + " with payload: " + String(msgPayload)); #endif // Parse the payload sent to the MQTT topic as a JSON document. StaticJsonDocument<256> doc; #ifdef DEBUG Serial.println("Deserializing message...."); #endif DeserializationError error = deserializeJson(doc, msgPayload); if (error) { #ifdef DEBUG Serial.println("Failed to parse MQTT payload as JSON: " + String(error.c_str())); #endif return; } // Do not process messages without an action key. if (!doc.containsKey("action")) { return; } String action = (const char *)doc["action"]; if (action == "set") { String state = (const char *)doc["state"]; const int pin = (const int)doc["pin"]; #ifdef DEBUG Serial.println("Setting pin: " + String(pin) + " to state: " + String(state)); #endif pinMode(PINS[pin], OUTPUT); if (state == "on") { digitalWrite(PINS[pin], HIGH); int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif return; } digitalWrite(PINS[pin], LOW); int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif return; } if (action == "get") { const int pin = (const int)doc["pin"]; #ifdef DEBUG Serial.println("Getting pin: " + String(pin) + " state."); #endif int status = digitalRead(PINS[pin]); #ifdef DEBUG Serial.println("Pin " + String(pin) + " state is now: " + String(status)); #endif // Announce the action. StaticJsonDocument<256> msg; msg["pin"] = pin; switch (status) { case 1: msg["state"] = "on"; break; case 0: msg["state"] = "off"; break; default: msg["state"] = "unknown"; break; } mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); return; } } bool mqttConnect() { #ifdef DEBUG Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST)); #endif mqttClient.setServer(MQTT_HOST, MQTT_PORT); StaticJsonDocument<256> msg; if (mqttClient.connect(MQTT_CLIENT_ID().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) { #ifdef DEBUG Serial.println("Established connection with MQTT broker using client ID: " + MQTT_CLIENT_ID()); #endif mqttClient.setCallback(mqttCallback); msg["action"] = "connected"; mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); #ifdef DEBUG Serial.println("Attempting to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif if (!mqttClient.subscribe(MQTT_TOPIC().c_str())) { #ifdef DEBUG Serial.println("Failed to subscribe to MQTT topic: " + MQTT_TOPIC()); #endif return false; } #ifdef DEBUG Serial.println("Subscribed to MQTT topic: " + MQTT_TOPIC()); #endif msg.clear(); msg["action"] = "subscribed"; mqttClient.publish(MQTT_TOPIC().c_str(), mqttSerialize(msg).c_str()); return true; } #ifdef DEBUG Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state())); #endif return false; } bool loopWifiConnected() { // Process OTA loop first since emergency OTA updates might be needed. ArduinoOTA.handle(); // Process MQTT client loop. if (!mqttClient.connected()) { // If the connection to the MQTT broker has failed then sleep before carrying on. if (!mqttConnect()) { return false; } } mqttClient.loop(); return true; } void setup() { Serial.begin(115200); #ifdef DEBUG Serial.println("Booted, setting up Wifi in 10s..."); #endif delay(10000); WiFi.mode(WIFI_STA); #if defined(ARDUINO_ARCH_ESP8266) WiFi.hostname(HOSTNAME().c_str()); #elif defined(ARDUINO_ARCH_ESP32) WiFi.setHostname(HOSTNAME().c_str()); #endif WiFi.begin(STA_SSID, STA_PSK); while (WiFi.waitForConnectResult() != WL_CONNECTED) { #ifdef DEBUG Serial.println("Failed to connect to Wifi, rebooting in 5s..."); #endif delay(5000); ESP.restart(); } #ifdef DEBUG Serial.print("Connected to Wifi: "); #endif Serial.println(WiFi.localIP()); #ifdef DEBUG Serial.println("Setting up OTA in 10s..."); #endif delay(10000); // Port defaults to 8266 ArduinoOTA.setPort(OTA_PORT); // Hostname defaults to esp-[ChipID] ArduinoOTA.setHostname(HOSTNAME().c_str()); // Set the OTA password ArduinoOTA.setPassword(OTA_PASSWORD); ArduinoOTA.onStart([]() { switch (ArduinoOTA.getCommand()) { case U_FLASH: // Sketch #ifdef DEBUG Serial.println("OTA start updating sketch."); #endif break; #if defined(ARDUINO_ARCH_ESP8266) case U_FS: #elif defined(ARDUINO_ARCH_ESP32) case U_SPIFFS: #endif #ifdef DEBUG Serial.println("OTA start updating filesystem."); #endif SPIFFS.end(); break; default: #ifdef DEBUG Serial.println("Unknown OTA update type."); #endif break; } }); ArduinoOTA.onEnd([]() { #ifdef DEBUG Serial.println("OTA update complete."); #endif SPIFFS.begin(); #if defined(ARDUINO_ARCH_ESP8266) // For what it's worth, check the filesystem on ESP8266. SPIFFS.check(); #endif ESP.restart(); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { #ifdef DEBUG Serial.printf("OTA update progress: %u%%\r", (progress / (total / 100))); #endif }); ArduinoOTA.onError([](ota_error_t error) { #ifdef DEBUG Serial.printf("OTA update error [%u]: ", error); #endif switch (error) { case OTA_AUTH_ERROR: #ifdef DEBUG Serial.println("OTA authentication failed"); #endif break; case OTA_BEGIN_ERROR: #ifdef DEBUG Serial.println("OTA begin failed"); #endif break; case OTA_CONNECT_ERROR: #ifdef DEBUG Serial.println("OTA connect failed"); #endif break; case OTA_RECEIVE_ERROR: #ifdef DEBUG Serial.println("OTA receive failed"); #endif break; case OTA_END_ERROR: #ifdef DEBUG Serial.println("OTA end failed"); #endif break; default: #ifdef DEBUG Serial.println("Unknown OTA failure"); #endif break; } ESP.restart(); }); ArduinoOTA.begin(); // Set up MQTT client. mqttClient.setServer(MQTT_HOST, MQTT_PORT); mqttClient.setCallback(mqttCallback); // Touchdown. #ifdef DEBUG Serial.println("Setup complete."); #endif } void loop() { // Check the Wifi connection status. int wifiStatus = WiFi.status(); switch (wifiStatus) { case WL_CONNECTED: if (!loopWifiConnected()) { delay(1000); break; } delay(1); break; case WL_NO_SHIELD: #ifdef DEBUG Serial.println("No Wifi shield present."); #endif goto DEFAULT_CASE; break; case WL_NO_SSID_AVAIL: #ifdef DEBUG Serial.println("Configured SSID not found."); #endif goto DEFAULT_CASE; break; // Temporary statuses indicating transitional states. case WL_IDLE_STATUS: case WL_SCAN_COMPLETED: delay(1000); break; // Fatal Wifi statuses trigger a delayed ESP restart. case WL_CONNECT_FAILED: case WL_CONNECTION_LOST: case WL_DISCONNECTED: default: #ifdef DEBUG Serial.println("Wifi connection failed with status: " + String(wifiStatus)); #endif DEFAULT_CASE: delay(10000); ESP.restart(); break; } }