The following is a template meant for controlling single stepper motors via a controller and a stepping motor such as a NEMA step-motor.
The block diagram shows a stepper motor connected to a TMC2208 controller and then the Arduino. Of course, the Arduino could also be an ESP8266 or an ESP32 by Expressif rather than an Arduino. The capacitor can be skipped, but it is there to prevent current spikes when the motor is engaged and starts pulling lots of current. In case the motor is used for high-torque jobs then the capacitor should definitely be there and it is not optional.
The MS1
and MS2
pins control the stepping style of the stepper motor and they are configurable within the template by adjusting STEP_CONTROLLER_MS1_PIN
respectively STEP_CONTROLLER_MS2_PIN
.
The controller board is powered from two distinct sources, the one connected to VS
and GND
, and the lower source connected to VIO
and GND
. Technically these can be one and the same source but they are separate because typically the controlling job itself is a low-energy application that can be powered using logic-levels like but the mechanical job such as the motor torque is also typically a tough job such that higher voltage and higher amperage is to be expected. Maybe for non-high-torque jobs the stereotype is to use a single
source that steps down to
to power the Arduino and then the Arduino's own step-own of
will power the controller board.
The API that the code implements follows a simple key-value pair grammar with keywords and specific values with the actual transmitted command being encoded using JSON.
parameter | sub-parameter | sub-sub-parameter | description |
---|---|---|---|
action | stop | ||
move | steps | the number of steps to move (positive clockwise, negative anti-clockwise) | |
delay | the delay between tiny steps (decimal) |
/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2025 - License: GNU MIT // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// // The following template is meant to control a stepper motor using MQTT // // by receving payloads on a subscribed topic and then moving the motor // // according to the message payload. // // // // The full documentation can be found on: // // * https://grimore.org/arduino/step_motors // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // configurable parameters // /////////////////////////////////////////////////////////////////////////// // comment out to enable debugging //#define DEBUG 1 // set the master password for OTA updates and access to the soft AP #define PREBOOT_MASTER_PASSWORD "" // the name and length of the cookie to use for authentication #define PREBOOT_COOKIE_NAME "ArduinoPrebootCookie" #define PREBOOT_COOKIE_MAX_LENGTH 256 // timeout to establish STA connection in milliseconds #define WIFI_RETRY_TIMEOUT 1000 * 10 // retries as multiples of WIFI_RETRY_TIMEOUT milliseconds #define WIFI_CONNECT_TRIES 60 // how much time to wait for a client to reconfigure before switching to client mode again #define WIFI_SERVER_TIMEOUT 1000 * 60 * 3 // the time between blinking a single digit #define BLINK_DIT_LENGTH 250 // the time between blinking the whole number #define BLINK_DAH_LENGTH 2500 // stepper application // The MQTT broker to connect to. #define MQTT_HOST "iot.internal" // The MQTT broker username. #define MQTT_USERNAME "" // The MQTT broker password. #define MQTT_PASSWORD "" // The MQTT broker port. #define MQTT_PORT 1883 // The MQTT topic #define MQTT_TOPIC "" #define MQTT_TOPIC_OUTBOX "messages" // Maximal size of an MQTT payload #define MQTT_PAYLOAD_MAX_LENGTH MQTT_MAX_PACKET_SIZE // These pins must correspond to the stepper motor control connection. #define STEP_MOTOR_DIRECTION_PIN D1 #define STEP_MOTOR_STEP_PIN D2 #define STEP_MOTOR_ENABLED_PIN D3 // wait time between steps (shorter for more fluid motion) #define STEP_MOTOR_STEP_DELAY 4000 // time between calling step motor updates (should not need changing) #define STEP_MOTOR_INTERRUPT_TIME 1 #define STEP_CONTROLLER_MS1_PIN D8 #define STEP_CONTROLLER_MS2_PIN D7 // 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 // The hostname to use. #define HOSTNAME() String("curtains") //String("esp-" + String(GET_CHIP_ID(), HEX)) #define CONFIGURATION_FILE_NAME "/config.json" #define CONFIGURATION_MAX_LENGTH 1024 /////////////////////////////////////////////////////////////////////////// // includes // /////////////////////////////////////////////////////////////////////////// #include <DNSServer.h> #if defined(ARDUINO_ARCH_ESP32) #include <WiFi.h> #include "esp_mac.h" #include <WebServer.h> #include <ESPmDNS.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #include <ESP8266WebServer.h> #endif #include <FS.h> #include <LittleFS.h> #include <ArduinoJson.h> // Arduino OTA #include <WiFiUdp.h> #include <ArduinoOTA.h> #include <TickTwo.h> // stepper application #include <PubSubClient.h> #include <SingleStepperLite.h> /////////////////////////////////////////////////////////////////////////// // function definitions // /////////////////////////////////////////////////////////////////////////// byte* getHardwareAddress(void); char* getHardwareAddress(char colon); String generateTemporarySSID(void); void arduinoOtaTickCallback(void); void blinkDigitsDahTickCallback(void); void blinkDigitsDitTickCallback(void); void blinkDigitsBlinkTickCallback(void); void clientWifiTickCallback(void); void serverWifiTickCallback(void); void handleServerWifi(void); void handleClientWifi(void); bool setConfiguration(const char* configurationFile, JsonDocument& configuration); int getConfiguration(const char* configurationFile, JsonDocument& configuration); void handleRootHttpRequest(void); void handleRootCssRequest(void); void handleSetupHttpRequest(void); void handleRootHttpGet(void); void handleSetupHttpGet(void); void handleRootHttpPost(void); void handleSetupHttpPost(void); void handleHttpNotFound(void); void rebootTickCallback(void); // stepper application void mqttTickCallback(void); void mqttStepperTickCallback(void); String getMqttTopic(void); String getMqttId(void); bool mqttConnect(void); void mqttCallback(char *topic, byte *payload, unsigned int length); constexpr unsigned int mqttActionHash(const char *s, int off = 0); /////////////////////////////////////////////////////////////////////////// // variable declarations // /////////////////////////////////////////////////////////////////////////// IPAddress softAPAddress(8, 8, 8, 8); IPAddress softAPNetmask(255, 255, 255, 0); DNSServer dnsServer; #if defined(ARDUINO_ARCH_ESP8266) ESP8266WebServer server(80); #elif defined(ARDUINO_ARCH_ESP32) WebServer server(80); #endif TickTwo arduinoOtaTick(arduinoOtaTickCallback, 1000); TickTwo rebootTick(rebootTickCallback, 1000); TickTwo clientWifiTick(clientWifiTickCallback, 25); TickTwo serverWifiTick(serverWifiTickCallback, 250); TickTwo blinkDigitsDahTick(blinkDigitsDahTickCallback, BLINK_DAH_LENGTH); TickTwo blinkDigitsDitTick(blinkDigitsDitTickCallback, BLINK_DIT_LENGTH); TickTwo blinkDigitsBlinkTick(blinkDigitsBlinkTickCallback, 25); enum bootMode : int { BOOT_MODE_NONE = 0, BOOT_MODE_CLIENT, BOOT_MODE_SERVER }; char* authenticationCookie = NULL; bool otaStarted; bool otaInProgress; bool networkConnected; int clientConnectionTries; bool rebootPending; int temporarySSIDLength; int temporarySSIDIndex; int* temporarySSIDNumbers; int blinkLedState; // stepper application WiFiClient espClient; PubSubClient mqttClient(espClient); TickTwo mqttTick(mqttTickCallback, 250); TickTwo mqttStepperTick(mqttStepperTickCallback, STEP_MOTOR_INTERRUPT_TIME); String mqttTopicCache; String mqttIdCache; SingleStepperLite stepper; /////////////////////////////////////////////////////////////////////////// // HTML & CSS templates // /////////////////////////////////////////////////////////////////////////// const char* GENERIC_CSS_TEMPLATE = R"html( * { box-sizing: border-box; } body { background-color: #3498db; font-family: "Arial", sans-serif; padding: 50px; } .container { margin: 20px auto; padding: 10px; width: 300px; height: 100%; background-color: #fff; border-radius: 5px; margin-left: auto; margin-right: auto; } h1 { width: 70%; color: #777; font-size: 32px; margin: 28px auto; text-align: center; } form { text-align: center; } input { padding: 12px 0; margin-bottom: 10px; border-radius: 3px; border: 2px solid transparent; text-align: center; width: 90%; font-size: 16px; transition: border 0.2s, background-color 0.2s; } form .field { background-color: #ecf0f1; } form .field:focus { border: 2px solid #3498db; } form .btn { background-color: #3498db; color: #fff; line-height: 25px; cursor: pointer; } form .btn:hover, form .btn:active { background-color: #1f78b4; border: 2px solid #1f78b4; } .pass-link { text-align: center; } .pass-link a:link, .pass-link a:visited { font-size: 12px; color: #777; } table { border: 1px solid #dededf; border-collapse: collapse; border-spacing: 1px; margin-left: auto; margin-right: auto; width: 80%; } td { border: 1px solid #dededf; background-color: #ffffff; color: #000000; padding: 1em; } )html"; const char* HTML_SETUP_TEMPLATE = R"html( <!DOCTYPE html> <html lang="en"> <head> <title>setup</title> <link rel="stylesheet" href="/style.css"> </head> <body> <div class="container"> <h1>setup</h1> <table> <tr> <td>AP</td> <td>%AP%</td> </tr> <tr> <td>MAC</td> <td>%MAC%</td> </tr> </table> <br> <form method="POST" action="/setup"> <label for="name">Name</label> <input id="name" type="text" name="name" value="%NAME%" class="field"> <label for="Ssid">SSID</label> <input id="Ssid" type="text" name="Ssid" class="field"> <label for="password">Password</label> <input id="password" type="password" name="password" class="field"> <input type="submit" value="login" class="btn"> </form> </div> </body> </html> )html"; const char* HTML_AUTH_TEMPLATE = R"html( <!DOCTYPE html> <html lang="en"> <head> <title>Preboot Access</title> <link rel="stylesheet" href="/style.css"> </head> <body> <div class="container"> <h1>admin</h1> <form method="POST"> <input id="password" type="password" name="password" class="field" placeholder="password"> <input type="submit" value="login" class="btn"> </form> </div> </body> </html> )html"; /////////////////////////////////////////////////////////////////////////// // begin Arduino // /////////////////////////////////////////////////////////////////////////// void setup() { #ifdef DEBUG Serial.begin(115200); // wait for serial while (!Serial) { delay(100); } Serial.println(); #else Serial.end(); #endif #ifdef DEBUG Serial.println("Mounting filesystem..."); #endif #if defined(ARDUINO_ARCH_ESP8266) if (!LittleFS.begin()) { #ifdef DEBUG Serial.println("LittleFS mount failed, formatting and rebooting..."); #endif LittleFS.format(); delay(1000); ESP.restart(); #elif defined(ARDUINO_ARCH_ESP32) if (!LittleFS.begin(true)) { #endif #ifdef DEBUG Serial.println("LittleFS mount & format failed..."); #endif return; } #ifdef DEBUG Serial.printf("Checking if WiFi server must be started...\n"); #endif // check if Ssid is set and start soft AP or STA mode DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) { #ifdef DEBUG Serial.println("Unable to retrieve configuration."); #endif delay(60000); ESP.restart(); return; } switch(configuration["boot"].as<int>()) { case BOOT_MODE_CLIENT: #ifdef DEBUG Serial.printf("Client connecting to WiFi...\n"); #endif clientWifiTick.start(); break; case BOOT_MODE_SERVER: case BOOT_MODE_NONE: default: #ifdef DEBUG Serial.printf("Server AP starting...\n"); #endif // start soft AP rebootTick.start(); serverWifiTick.start(); break; } // setup OTA ArduinoOTA.setHostname(configuration["name"].as<const char*>()); // allow flashing with the master password ArduinoOTA.setPassword(PREBOOT_MASTER_PASSWORD); ArduinoOTA.onStart([]() { // mark OTA as started otaInProgress = true; // stop LittleFS as per the documentation LittleFS.end(); String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; } else { // U_FS type = "filesystem"; } // NOTE: if updating FS this would be the place to unmount FS using FS.end() #ifdef DEBUG Serial.println("Start updating " + type); #endif }); ArduinoOTA.onEnd([]() { otaInProgress = false; #ifdef DEBUG Serial.println("\nEnd"); #endif // restart the device #ifdef DEBUG Serial.printf("Restarting ESP.\n"); #endif delay(1000); ESP.restart(); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { #ifdef DEBUG Serial.printf("Progress: %u%%\r", (progress / (total / 100))); #endif }); ArduinoOTA.onError([](ota_error_t error) { #ifdef DEBUG Serial.printf("Error[%u]: ", error); #endif if (error == OTA_AUTH_ERROR) { #ifdef DEBUG Serial.println("Auth Failed"); #endif } else if (error == OTA_BEGIN_ERROR) { #ifdef DEBUG Serial.println("Begin Failed"); #endif } else if (error == OTA_CONNECT_ERROR) { #ifdef DEBUG Serial.println("Connect Failed"); #endif } else if (error == OTA_RECEIVE_ERROR) { #ifdef DEBUG Serial.println("Receive Failed"); #endif } else if (error == OTA_END_ERROR) { #ifdef DEBUG Serial.println("End Failed"); #endif } }); // start timers / threads arduinoOtaTick.start(); rebootTick.start(); // stepper application mqttTick.start(); // set up motor pinMode(STEP_MOTOR_ENABLED_PIN, OUTPUT); pinMode(STEP_MOTOR_STEP_PIN, OUTPUT); pinMode(STEP_MOTOR_DIRECTION_PIN, OUTPUT); // disable motor for now digitalWrite(STEP_MOTOR_ENABLED_PIN, HIGH); } void loop() { arduinoOtaTick.update(); rebootTick.update(); clientWifiTick.update(); serverWifiTick.update(); blinkDigitsDitTick.update(); blinkDigitsDahTick.update(); blinkDigitsBlinkTick.update(); // stepper application mqttTick.update(); mqttStepperTick.update(); } /////////////////////////////////////////////////////////////////////////// // end Arduino // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // stepper-MQTT // /////////////////////////////////////////////////////////////////////////// constexpr unsigned int mqttActionHash(const char *s, int off) { return !s[off] ? 5381 : (mqttActionHash(s, off+1)*33) ^ s[off]; } String getMqttTopic(void) { // store the MQTT topic in a global varible to prevent redundant CPU usage if(mqttTopicCache == NULL || mqttTopicCache.length() == 0) { mqttTopicCache = String(MQTT_TOPIC); if(mqttTopicCache == NULL || mqttTopicCache.length() == 0) { DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) { #ifdef DEBUG Serial.println("Unable to retrieve configuration."); #endif return mqttTopicCache; } if(configuration.containsKey("name")) { mqttTopicCache = configuration["name"].as<String>(); } } } return mqttTopicCache; } String getMqttId(void) { // store the MQTT id in a global varible to prevent redundant CPU usage if(mqttIdCache == NULL || mqttIdCache.length() == 0) { mqttIdCache = String(HOSTNAME()); if(mqttIdCache == NULL || mqttIdCache.length() == 0) { DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) { #ifdef DEBUG Serial.println("Unable to retrieve configuration."); #endif return mqttIdCache; } if(configuration.containsKey("name")) { mqttIdCache = configuration["name"].as<String>(); } } } return mqttIdCache; } void mqttStepperTickCallback(void) { stepper.do_tasks(); //if motor 0 completed all the steps if (stepper.is_finished()) { mqttStepperTick.stop(); // decouple hold on motor in order to reduce consumption and usage digitalWrite(STEP_MOTOR_ENABLED_PIN, HIGH); DynamicJsonDocument doc(MQTT_PAYLOAD_MAX_LENGTH); doc["motion"] = "complete"; char buff[MQTT_PAYLOAD_MAX_LENGTH]; size_t payloadLength = serializeJson(doc, buff); mqttClient.publish((getMqttTopic() + "/" + MQTT_TOPIC_OUTBOX).c_str(), buff, payloadLength); } } void mqttCallback(char *topic, byte *payload, unsigned int length) { String msgTopic = String(topic); // do not listen on topics not subscribed to or on empty topics if(msgTopic.length() == 0) { return; } // only listen on the topic that has been subscribed to if(!msgTopic.equals(mqttTopicCache)) { return; } // Parse the payload sent to the MQTT topic as a JSON document. #ifdef DEBUG Serial.println("Deserializing message..."); #endif DynamicJsonDocument doc(MQTT_PAYLOAD_MAX_LENGTH); char payloadBuffer[MQTT_PAYLOAD_MAX_LENGTH]; size_t payloadLength = strlcpy(payloadBuffer, (const char *)payload, length + 1); DeserializationError error = deserializeJson(doc, payloadBuffer, payloadLength); if (error) { #ifdef DEBUG Serial.println("Failed to parse MQTT payload as JSON: " + String(error.c_str())); #endif return; } #ifdef DEBUG //Serial.printf("Message received on topic %s with payload %s...\n", topic, payload); #endif // Do not process messages without an action key. if (!doc.containsKey("action")) { #ifdef DEBUG Serial.printf("No action provided.\n"); #endif return; } String action = String(doc["action"].as<const char*>()); // normalize action action.trim(); action.toUpperCase(); if(action.length() == 0) { #ifdef DEBUG Serial.println("Empty action provided."); #endif return; } DynamicJsonDocument msg(MQTT_PAYLOAD_MAX_LENGTH); msg["execute"] = doc; switch(mqttActionHash(action.c_str())) { case mqttActionHash("STOP"): // stop any on-going timer mqttStepperTick.stop(); // stop the motor from rotating stepper.stop(); // release hold on motor digitalWrite(STEP_MOTOR_ENABLED_PIN, HIGH); break; case mqttActionHash("MOVE"): if(!doc.containsKey("steps")) { #ifdef DEBUG Serial.println("No position requested for moving."); #endif break; } int stepsTotal = doc["steps"].as<int>(); if(stepsTotal == 0) { #ifdef DEBUG Serial.println("Invalid number of steps specified."); #endif break; } // set the direction from the sign of the steps. int stepsDirection = signbit(stepsTotal); stepsTotal = abs(stepsTotal); int stepsDelay = STEP_MOTOR_STEP_DELAY; if(doc.containsKey("delay")) { stepsDelay = doc["delay"].as<int>(); } #ifdef DEBUG //Serial.printf("Moving %d with %dµs delay in direction %d.\n", stepsTotal, stepsDelay, stepsDirection); #endif // stop any on-going timer mqttStepperTick.stop(); // stop the motor from rotating stepper.stop(); // release hold on motor digitalWrite(STEP_MOTOR_ENABLED_PIN, HIGH); // microstep settings //pinMode(STEP_CONTROLLER_MS1_PIN, OUTPUT); //pinMode(STEP_CONTROLLER_MS2_PIN, OUTPUT); //digitalWrite(STEP_CONTROLLER_MS1_PIN, HIGH); //digitalWrite(STEP_CONTROLLER_MS2_PIN, LOW); // low is clockwise digitalWrite(STEP_MOTOR_DIRECTION_PIN, !stepsDirection); // enable is set to low digitalWrite(STEP_MOTOR_ENABLED_PIN, LOW); stepper.init_stepper(STEP_MOTOR_STEP_PIN); stepper.start_finite(stepsDelay, stepsTotal); mqttStepperTick.interval(STEP_MOTOR_INTERRUPT_TIME); mqttStepperTick.start(); break; } #ifdef DEBUG Serial.println("Sending feedback..."); #endif char buff[MQTT_PAYLOAD_MAX_LENGTH]; payloadLength = serializeJson(msg, buff); mqttClient.publish((getMqttTopic() + "/" + MQTT_TOPIC_OUTBOX).c_str(), buff, payloadLength); } bool mqttConnect(void) { #ifdef DEBUG Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST)); #endif mqttClient.setServer(MQTT_HOST, MQTT_PORT); if (mqttClient.connect(getMqttId().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) { mqttClient.setCallback(mqttCallback); #ifdef DEBUG Serial.print("Subscribing to MQTT topic " + getMqttTopic() + ": "); #endif if (!mqttClient.subscribe(getMqttTopic().c_str())) { #ifdef DEBUG Serial.println("fail"); #endif return false; } #ifdef DEBUG Serial.println("success"); #endif // Announce success at connecting and subscribing to MQTT broker. DynamicJsonDocument msg(MQTT_PAYLOAD_MAX_LENGTH); msg["client"] = getMqttId().c_str(); msg["topic"] = getMqttTopic().c_str(); msg["action"] = "hello"; char buff[MQTT_PAYLOAD_MAX_LENGTH]; size_t payloadLength = serializeJson(msg, buff); mqttClient.publish((getMqttTopic() + "/" + MQTT_TOPIC_OUTBOX).c_str(), buff, payloadLength); return true; } #ifdef DEBUG Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state())); #endif return false; } /////////////////////////////////////////////////////////////////////////// // Arduino loop // /////////////////////////////////////////////////////////////////////////// void mqttTickCallback(void) { if(!networkConnected) { return; } // Process MQTT client loop. if (mqttClient.connected()) { mqttClient.loop(); return; } if (!mqttConnect()) { #ifdef DEBUG Serial.printf("Unable to connect to MQTT\n"); #endif } } /////////////////////////////////////////////////////////////////////////// // OTA updates // /////////////////////////////////////////////////////////////////////////// void arduinoOtaTickCallback(void) { ArduinoOTA.handle(); if(!networkConnected) { return; } if(!otaStarted) { ArduinoOTA.begin(); otaStarted = true; } } /////////////////////////////////////////////////////////////////////////// // system-wide reboot // /////////////////////////////////////////////////////////////////////////// void rebootTickCallback(void) { // if not reboot hasbeen scheduled then just return if(!rebootPending) { return; } #ifdef DEBUG Serial.printf("Stopping filesystem...\n"); #endif #ifdef DEBUG LittleFS.end(); #endif #ifdef DEBUG Serial.printf("Rebooting...\n"); #endif ESP.restart(); } /////////////////////////////////////////////////////////////////////////// // HTTP route handling // /////////////////////////////////////////////////////////////////////////// void handleRootHttpPost(void) { String password; for(int i = 0; i < server.args(); ++i) { if(server.argName(i) == "password") { password = server.arg(i); continue; } } if(!password.equals(PREBOOT_MASTER_PASSWORD)) { server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } #ifdef DEBUG Serial.println("Authentication succeeded, setting cookie and redirecting."); #endif // clear old authentication cookie if(authenticationCookie != NULL) { free(authenticationCookie); authenticationCookie = NULL; } authenticationCookie = randomStringHex(8); char* buff = (char*) malloc(PREBOOT_COOKIE_MAX_LENGTH * sizeof(char)); snprintf(buff, PREBOOT_COOKIE_MAX_LENGTH, "%s=%s; Max-Age=600; SameSite=Strict", PREBOOT_COOKIE_NAME, authenticationCookie); #ifdef DEBUG Serial.printf("Preboot cookie set to: %s\n", buff); #endif server.sendHeader("Set-Cookie", buff); server.sendHeader("Location", "/setup"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); free(buff); } void handleSetupHttpPost(void) { String espName, staSsid, password; for(int i = 0; i < server.args(); ++i) { if(server.argName(i) == "name") { espName = server.arg(i); continue; } if(server.argName(i) == "Ssid") { staSsid = server.arg(i); continue; } if(server.argName(i) == "password") { password = server.arg(i); continue; } } if(espName == NULL || staSsid == NULL || password == NULL) { server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } #ifdef DEBUG Serial.printf("Ssid %s and password %s received from web application.\n", staSsid, password); #endif DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); configuration["name"] = espName; configuration["Ssid"] = staSsid; configuration["password"] = password; configuration["boot"] = BOOT_MODE_CLIENT; if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) { #ifdef DEBUG Serial.printf("Failed to write configuration.\n"); #endif server.sendHeader("Location", "/setup"); server.sendHeader("Cache-Control", "no-cache"); server.send(307); return; } server.send(200, "text/plain", "Parameters applied. Scheduling reboot..."); #ifdef DEBUG Serial.printf("Configuration applied...\n"); #endif rebootPending = true; } void handleRootHttpGet(void) { // send login form #ifdef DEBUG Serial.printf("Sending authentication webpage.\n"); #endif String processTemplate = String(HTML_AUTH_TEMPLATE); server.send(200, "text/html", processTemplate); } void handleSetupHttpGet(void) { DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) { #ifdef DEBUG Serial.println("Unable to retrieve configuration."); #endif server.sendHeader("Location", "/setup"); server.sendHeader("Cache-Control", "no-cache"); server.send(307); } String espName = HOSTNAME(); if(configuration.containsKey("name")) { espName = configuration["name"].as<const char*>(); } // send default boot webpage #ifdef DEBUG Serial.printf("Sending configuration form webpage.\n"); #endif String processTemplate = String(HTML_SETUP_TEMPLATE); processTemplate.replace("%AP%", generateTemporarySSID()); processTemplate.replace("%MAC%", getHardwareAddress(':')); processTemplate.replace("%NAME%", espName); server.send(200, "text/html", processTemplate); } void handleRootHttpRequest(void) { switch(server.method()) { case HTTP_GET: handleRootHttpGet(); break; case HTTP_POST: handleRootHttpPost(); break; } } void handleRootCssRequest(void) { if(server.method() != HTTP_GET) { handleHttpNotFound(); return; } #ifdef DEBUG Serial.println("Sending stylesheet..."); #endif String rootCss = String(GENERIC_CSS_TEMPLATE); server.send(200, "text/css", rootCss); } void handleSetupHttpRequest(void) { #ifdef DEBUG Serial.println("HTTP setup request received."); #endif if(!server.hasHeader("Cookie")) { #ifdef DEBUG Serial.println("No cookie header found."); #endif server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } String cookie = server.header("Cookie"); if(authenticationCookie == NULL || cookie.indexOf(authenticationCookie) == -1) { #ifdef DEBUG Serial.println("Authentication failed."); #endif server.sendHeader("Location", "/"); server.sendHeader("Cache-Control", "no-cache"); server.send(302); return; } switch(server.method()) { case HTTP_GET: #ifdef DEBUG Serial.printf("HTTP GET request received for setup.\n"); #endif handleSetupHttpGet(); break; case HTTP_POST: #ifdef DEBUG Serial.printf("HTTP POST request received for setup.\n"); #endif handleSetupHttpPost(); break; } } void handleHttpNotFound(void) { server.sendHeader("Cache-Control", "no-cache"); server.send(404); } /////////////////////////////////////////////////////////////////////////// // set the current configuration // /////////////////////////////////////////////////////////////////////////// bool setConfiguration(const char* configurationFile, JsonDocument& configuration) { #if defined(ARDUINO_ARCH_ESP8266) File file = LittleFS.open(configurationFile, "w"); #elif defined(ARDUINO_ARCH_ESP32) File file = LittleFS.open(configurationFile, FILE_WRITE); #endif if(!file) { #ifdef DEBUG Serial.println("Failed to open file for writing."); #endif return false; } size_t bytesWritten = serializeJson(configuration, file); file.close(); #ifdef DEBUG Serial.printf("Written bytes %d vs. document bytes %d\n", bytesWritten, measureJson(configuration)); #endif return bytesWritten == measureJson(configuration); } /////////////////////////////////////////////////////////////////////////// // get the current configuration // /////////////////////////////////////////////////////////////////////////// int getConfiguration(const char* configurationFile, JsonDocument& configuration) { #if defined(ARDUINO_ARCH_ESP8266) File file = LittleFS.open(configurationFile, "r"); #elif defined(ARDUINO_ARCH_ESP32) File file = LittleFS.open(configurationFile); #endif if (!file) { #ifdef DEBUG Serial.println("Failed to open file for reading."); #endif return false; } DeserializationError error = deserializeJson(configuration, file); file.close(); if(error) { #ifdef DEBUG Serial.printf("Deserialization failed with error %s\n", error.c_str()); #endif return -1; } return measureJson(configuration); } /////////////////////////////////////////////////////////////////////////// // generate random string // /////////////////////////////////////////////////////////////////////////// char* randomStringHex(int length) { const char alphabet[] = "0123456789abcdef"; char* payload = (char*) malloc(length * sizeof(char)); int i; for (i=0; i<length; ++i) { payload[i] = alphabet[random(16)]; } payload[i] = '\0'; return payload; } /////////////////////////////////////////////////////////////////////////// // get wireless status // /////////////////////////////////////////////////////////////////////////// const char* wl_status_to_string(wl_status_t status) { switch (status) { case WL_NO_SHIELD: return "WL_NO_SHIELD"; case WL_IDLE_STATUS: return "WL_IDLE_STATUS"; case WL_NO_SSID_AVAIL: return "WL_NO_SSID_AVAIL"; case WL_SCAN_COMPLETED: return "WL_SCAN_COMPLETED"; case WL_CONNECTED: return "WL_CONNECTED"; case WL_CONNECT_FAILED: return "WL_CONNECT_FAILED"; case WL_CONNECTION_LOST: return "WL_CONNECTION_LOST"; case WL_DISCONNECTED: return "WL_DISCONNECTED"; #if defined(ARDUINO_ARCH_ESP32) case WL_STOPPED: return "WL_STOPPED"; #endif } return "UNKNOWN"; } /////////////////////////////////////////////////////////////////////////// // get WiFi MAC address // /////////////////////////////////////////////////////////////////////////// byte* getHardwareAddress(void) { // get mac address byte* mac = (byte *)malloc(6 * sizeof(byte)); #if defined(ARDUINO_ARCH_ESP8266) WiFi.macAddress(mac); #elif defined(ARDUINO_ARCH_ESP32) esp_read_mac(mac, ESP_MAC_WIFI_STA); #endif return mac; } /////////////////////////////////////////////////////////////////////////// // convert MAC address to string // /////////////////////////////////////////////////////////////////////////// char* getHardwareAddress(char colon) { byte* mac = getHardwareAddress(); char* buff = (char *)malloc(18 * sizeof(char)); sprintf(buff, "%02x%c%02x%c%02x%c%02x%c%02x%c%02x", mac[0], colon, mac[1], colon, mac[2], colon, mac[3], colon, mac[4], colon, mac[5] ); free(mac); return buff; } /////////////////////////////////////////////////////////////////////////// // get WiFi soft AP // /////////////////////////////////////////////////////////////////////////// String generateTemporarySSID(void) { byte* mac = getHardwareAddress(); String ssid = String(mac[0] ^ mac[1] ^ mac[2] ^ mac[3] ^ mac[4] ^ mac[5], DEC); free(mac); return ssid; } /////////////////////////////////////////////////////////////////////////// // serve WiFi AP // /////////////////////////////////////////////////////////////////////////// void serverWifiTickCallback(void) { if(rebootPending || otaInProgress) { return; } unsigned long callbackTickTime = serverWifiTick.counter() * (serverWifiTick.interval() / 1000); if(callbackTickTime >= WIFI_SERVER_TIMEOUT) { #ifdef DEBUG Serial.println("Server timeout, rebooting...\n"); #endif DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); configuration["boot"] = BOOT_MODE_CLIENT; if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) { #ifdef DEBUG Serial.printf("Failed to write configuration.\n"); #endif } rebootPending = true; return; } #ifdef DEBUG /* if(callbackTickTime % 1000 == 0 ) { Serial.printf("Time till reboot %.0fs\n", (float)(WIFI_SERVER_TIMEOUT - callbackTickTime)/1000.0); } */ #endif // create the boot SSID String temporarySSID = generateTemporarySSID(); if(WiFi.softAPSSID().equals(temporarySSID)) { // run WiFi server loops dnsServer.processNextRequest(); server.handleClient(); if(blinkDigitsDahTick.state() == STOPPED) { temporarySSIDLength = temporarySSID.length(); temporarySSIDNumbers = (int *) malloc(temporarySSIDLength * sizeof(int)); for(int i = 0; i < temporarySSIDLength; ++i) { temporarySSIDNumbers[i] = temporarySSID[i] - '0'; } temporarySSIDIndex = 0; blinkDigitsDahTick.start(); } return; } #ifdef DEBUG Serial.println("Starting HTTP server for Wifi server."); #endif // handle HTTP REST requests server.on("/", handleRootHttpRequest); server.on("/setup", handleSetupHttpRequest); server.on("/style.css", handleRootCssRequest); // captive portal proprietary junk redirected to webserver root // connectivitycheck.gstatic.com/generate_204 // www.googe.com/gen_204 server.on("/generate_204", handleRootHttpRequest); server.on("/gen_204", handleRootHttpRequest); server.on("/fwlink", handleRootHttpRequest); server.onNotFound(handleHttpNotFound); #ifdef DEBUG Serial.println("Ensure HTTP headers are collected by the HTTP server."); #endif #if defined(ARDUINO_ARCH_ESP8266) server.collectHeaders("Cookie"); #elif defined(ARDUINO_ARCH_ESP32) const char* collectHeaders[] = { "Cookie" }; size_t headerkeyssize = sizeof(collectHeaders) / sizeof(char *); server.collectHeaders(collectHeaders, headerkeyssize); #endif // the soft AP (or WiFi) must be started before the HTTP server or it will result in a crash on ESP32 #ifdef DEBUG Serial.println("Starting temporary AP."); #endif DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) != -1) { if (!MDNS.begin(configuration["name"].as<const char*>())) { #ifdef DEBUG Serial.println("Error setting up MDNS responder."); #endif } } WiFi.softAPConfig(softAPAddress, softAPAddress, softAPNetmask); WiFi.softAP(temporarySSID, String(), 1, false, 1); dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(53, "*", softAPAddress); #ifdef DEBUG Serial.println("Starting HTTP server."); #endif server.begin(); } /////////////////////////////////////////////////////////////////////////// // connect to WiFi // /////////////////////////////////////////////////////////////////////////// void clientWifiTickCallback(void) { if(rebootPending || otaInProgress) { return; } unsigned long callbackCount = clientWifiTick.counter(); #ifdef DEBUG //Serial.printf("Client tick %lu\n", callbackCount); #endif if(callbackCount == 1) { #ifdef DEBUG Serial.printf("Rescheduling client WiFi to check mevery 10s...\n"); #endif clientWifiTick.interval(WIFI_RETRY_TIMEOUT); clientWifiTick.resume(); } // if WiFi is already connected or a reboot is pending just bail out wl_status_t wifiStatus = WiFi.status(); if(wifiStatus == WL_CONNECTED) { #ifdef DEBUG Serial.println("-- MARK --"); #endif clientConnectionTries = 0; networkConnected = true; return; } #ifdef DEBUG Serial.printf("Client WiFi not connected: %d\n", wl_status_to_string(wifiStatus)); #endif networkConnected = false; DynamicJsonDocument configuration(CONFIGURATION_MAX_LENGTH); if(getConfiguration(CONFIGURATION_FILE_NAME, configuration) == -1) { #ifdef DEBUG Serial.println("Unable to retrieve configuration."); #endif return; } // too many retries so reboot to soft AP if(++clientConnectionTries > WIFI_CONNECT_TRIES) { configuration["boot"] = BOOT_MODE_SERVER; if(!setConfiguration(CONFIGURATION_FILE_NAME, configuration)) { #ifdef DEBUG Serial.printf("Failed to write configuration.\n"); #endif } #ifdef DEBUG Serial.printf("Restarting in 1 second...\n"); #endif rebootPending = true; return; } #ifdef DEBUG Serial.printf("Attempting to establish WiFi STA connecton [%d/%d]\n", (WIFI_CONNECT_TRIES - clientConnectionTries) + 1, WIFI_CONNECT_TRIES); #endif #if defined(ARDUINO_ARCH_ESP8266) WiFi.hostname(configuration["name"].as<String>()); #elif defined(ARDUINO_ARCH_ESP32) WiFi.setHostname(configuration["name"].as<const char*>()); #endif if (!MDNS.begin(configuration["name"].as<const char*>())) { #ifdef DEBUG Serial.println("Error setting up MDNS responder."); #endif } String Ssid = configuration["Ssid"].as<String>(); String password = configuration["password"].as<String>(); #ifdef DEBUG Serial.printf("Trying connection to %s with %s...\n", Ssid, password); #endif //WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); WiFi.begin(Ssid, password); } /////////////////////////////////////////////////////////////////////////// // blink the temporary Ssid // /////////////////////////////////////////////////////////////////////////// void blinkDigitsDahTickCallback(void) { // wait for the dits to complete if(blinkDigitsDitTick.state() != STOPPED) { return; } if(temporarySSIDIndex >= temporarySSIDLength) { blinkDigitsDahTick.stop(); blinkDigitsDitTick.stop(); blinkDigitsBlinkTick.stop(); free(temporarySSIDNumbers); #ifdef DEBUG Serial.println(); Serial.println("Dah-dit blink sequence completed."); #endif return; } #ifdef DEBUG Serial.printf("Starting to blink %d times: ", temporarySSIDNumbers[temporarySSIDIndex]); #endif pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); blinkDigitsDitTick.start(); } void blinkDigitsDitTickCallback(void) { #ifdef DEBUG Serial.printf("Dit: %d/%d\n", blinkDigitsDitTick.counter(), temporarySSIDNumbers[temporarySSIDIndex]); #endif if(blinkDigitsDitTick.counter() > temporarySSIDNumbers[temporarySSIDIndex]) { blinkDigitsDitTick.stop(); ++temporarySSIDIndex; #ifdef DEBUG Serial.println("Dits completed..."); #endif return; } blinkDigitsDitTick.pause(); blinkDigitsBlinkTick.start(); } void blinkDigitsBlinkTickCallback(void) { if(blinkDigitsBlinkTick.counter() > 2) { blinkDigitsBlinkTick.stop(); blinkDigitsDitTick.resume(); return; } blinkLedState = !blinkLedState; digitalWrite(LED_BUILTIN, blinkLedState); }