diff --git a/LICENSE b/LICENSE index 4dba869..e43d7b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Peter Göthager - -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. +MIT License + +Copyright (c) 2018 Peter Göthager + +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. diff --git a/NidayandHelper.cpp b/NidayandHelper.cpp index 9b6383e..8d7a3bb 100644 --- a/NidayandHelper.cpp +++ b/NidayandHelper.cpp @@ -1,35 +1,43 @@ #include "Arduino.h" #include "NidayandHelper.h" -NidayandHelper::NidayandHelper(){ +NidayandHelper::NidayandHelper() { this->_configfile = "/config.json"; this->_mqttclientid = ("ESPClient-" + String(ESP.getChipId())); - } -boolean NidayandHelper::loadconfig(){ +boolean NidayandHelper::loadconfig() { File configFile = SPIFFS.open(this->_configfile, "r"); if (!configFile) { - Serial.println("Failed to open config file"); + Serial.println(F("Failed to open config file")); return false; } size_t size = configFile.size(); if (size > 1024) { - Serial.println("Config file size is too large"); + Serial.println(F("Config file size is too large")); return false; } // Allocate a buffer to store contents of the file. - std::unique_ptr buf(new char[size]); + //std::unique_ptr buf(new char[size]); // We don't use String here because ArduinoJson library requires the input // buffer to be mutable. If you don't use ArduinoJson, you may as well // use configFile.readString instead. - configFile.readBytes(buf.get(), size); + //configFile.readBytes(buf.get(), size); + + //No more memory leaks + DynamicJsonBuffer jsonBuffer(300); + + //Reading from buffer breaks first key + //this->_config = jsonBuffer.parseObject(buf.get()); - StaticJsonBuffer<200> jsonBuffer; - this->_config = jsonBuffer.parseObject(buf.get()); + //Reading directly from file DOES NOT cause currentPosition to break + this->_config = jsonBuffer.parseObject(configFile); + + //Avoid leaving opened files + configFile.close(); if (!this->_config.success()) { Serial.println("Failed to parse config file"); @@ -38,11 +46,11 @@ boolean NidayandHelper::loadconfig(){ return true; } -JsonVariant NidayandHelper::getconfig(){ +JsonVariant NidayandHelper::getconfig() { return this->_config; } -boolean NidayandHelper::saveconfig(JsonVariant json){ +boolean NidayandHelper::saveconfig(JsonVariant json) { File configFile = SPIFFS.open(this->_configfile, "w"); if (!configFile) { Serial.println("Failed to open config file for writing"); @@ -50,7 +58,8 @@ boolean NidayandHelper::saveconfig(JsonVariant json){ } json.printTo(configFile); - + configFile.flush(); //Making sure it's saved + Serial.println("Saved JSON to SPIFFS"); json.printTo(Serial); Serial.println(); @@ -61,21 +70,23 @@ String NidayandHelper::mqtt_gettopic(String type) { return "/raw/esp8266/" + String(ESP.getChipId()) + "/" + type; } - -void NidayandHelper::mqtt_reconnect(PubSubClient& psclient){ +void NidayandHelper::mqtt_reconnect(PubSubClient& psclient) { return mqtt_reconnect(psclient, String(NULL), String(NULL)); } -void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, std::list topics){ + +void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, std::list topics) { return mqtt_reconnect(psclient, String(NULL), String(NULL), topics); } -void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String pwd){ + +void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String pwd) { std::list mylist; return mqtt_reconnect(psclient, uid, pwd, mylist); } -void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String pwd, std::list topics){ + +void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String pwd, std::list topics) { // Loop until we're reconnected boolean mqttLogon = false; - if (uid!=NULL and pwd != NULL){ + if (uid != NULL and pwd != NULL) { mqttLogon = true; } while (!psclient.connected()) { @@ -85,13 +96,13 @@ void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String p Serial.println("connected"); //Send register MQTT message with JSON of chipid and ip-address - this->mqtt_publish(psclient, "/raw/esp8266/register", "{ \"id\": \"" + String(ESP.getChipId()) + "\", \"ip\":\"" + WiFi.localIP().toString() +"\"}"); + this->mqtt_publish(psclient, "/raw/esp8266/register", "{ \"id\": \"" + String(ESP.getChipId()) + "\", \"ip\":\"" + WiFi.localIP().toString() + "\"}"); //Setup subscription - if (!topics.empty()){ - for (const char* t : topics){ - psclient.subscribe(t); - Serial.println("Subscribed to "+String(t)); + if (!topics.empty()) { + for (const char* t : topics) { + psclient.subscribe(t); + Serial.println("Subscribed to " + String(t)); } } @@ -104,22 +115,22 @@ void NidayandHelper::mqtt_reconnect(PubSubClient& psclient, String uid, String p delay(5000); } } - if (psclient.connected()){ + if (psclient.connected()) { psclient.loop(); } } -void NidayandHelper::mqtt_publish(PubSubClient& psclient, String topic, String payload){ - Serial.println("Trying to send msg..."+topic+":"+payload); +void NidayandHelper::mqtt_publish(PubSubClient& psclient, String topic, String payload) { + Serial.println("Trying to send msg..." + topic + ":" + payload); //Send status to MQTT bus if connected if (psclient.connected()) { psclient.publish(topic.c_str(), payload.c_str()); } else { - Serial.println("PubSub client is not connected..."); + Serial.println(F("PubSub client is not connected...")); } } -void NidayandHelper::resetsettings(WiFiManager& wifim){ +void NidayandHelper::resetsettings(WiFiManager& wifim) { SPIFFS.format(); wifim.resetSettings(); -} +} diff --git a/NidayandHelper.h b/NidayandHelper.h index 1aaad62..7e93d7e 100644 --- a/NidayandHelper.h +++ b/NidayandHelper.h @@ -1,36 +1,36 @@ -#ifndef NidayandHelper_h -#define NidayandHelper_h - -#include "Arduino.h" -#include -#include "FS.h" -#include -#include -#include -#include - -class NidayandHelper { - public: - NidayandHelper(); - boolean loadconfig(); - JsonVariant getconfig(); - boolean saveconfig(JsonVariant json); - - String mqtt_gettopic(String type); - - void mqtt_reconnect(PubSubClient& psclient); - void mqtt_reconnect(PubSubClient& psclient, std::list topics); - void mqtt_reconnect(PubSubClient& psclient, String uid, String pwd); - void mqtt_reconnect(PubSubClient& psclient, String uid, String pwd, std::list topics); - - void mqtt_publish(PubSubClient& psclient, String topic, String payload); - - void resetsettings(WiFiManager& wifim); - - private: - JsonVariant _config; - String _configfile; - String _mqttclientid; -}; - -#endif +#ifndef NidayandHelper_h +#define NidayandHelper_h + +#include "Arduino.h" +#include +#include "FS.h" +#include +#include +#include +#include + +class NidayandHelper { + public: + NidayandHelper(); + boolean loadconfig(); + JsonVariant getconfig(); + boolean saveconfig(JsonVariant json); + + String mqtt_gettopic(String type); + + void mqtt_reconnect(PubSubClient& psclient); + void mqtt_reconnect(PubSubClient& psclient, std::list topics); + void mqtt_reconnect(PubSubClient& psclient, String uid, String pwd); + void mqtt_reconnect(PubSubClient& psclient, String uid, String pwd, std::list topics); + + void mqtt_publish(PubSubClient& psclient, String topic, String payload); + + void resetsettings(WiFiManager& wifim); + + private: + JsonVariant _config; + String _configfile; + String _mqttclientid; +}; + +#endif diff --git a/README.md b/README.md index 2dd7570..079a839 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,62 @@ -# motor-on-roller-blind-ws -WebSocket based version of [motor-on-roller-blind](https://github.com/nidayand/motor-on-roller-blind). I.e. there is no need of an MQTT server but MQTT is supported as well - you can control it with WebSockets and/or with MQTT messages. - -3d parts for printing are available on Thingiverse.com: ["motor on a roller blind"](https://www.thingiverse.com/thing:2392856) - - 1. A tiny webserver is setup on the esp8266 that will serve one page to the client - 2. Upon powering on the first time WIFI credentials, a hostname and - optional - MQTT server details is to be configured. You can specify if you want **clockwise (CW) rotation** to close the blind and you can also specify **MQTT authentication** if required. Connect your computer to a new WIFI hotspot named **BlindsConnectAP**. Password = **nidayand** - 3. Connect to your normal WIFI with your client and go to the IP address of the device - or if you have an mDNS supported device (e.g. iOS, OSX or have Bonjour installed) you can go to http://{hostname}.local. If you don't know the IP-address of the device check your router for the leases (or check the serial console in the Arduino IDE or check the `/raw/esp8266/register` MQTT message if you are using an MQTT server) - 4. As the webpage is loaded it will connect through a websocket directly to the device to progress updates and to control the device. If any other client connects the updates will be in sync. - 5. Go to the Settings page to calibrate the motor with the start and end positions of the roller blind. Follow the instructions on the page - -# MQTT -- When it connects to WIFI and MQTT it will send a "register" message to topic `/raw/esp8266/register` with a payload containing chip-id and IP-address -- A message to `/raw/esp8266/[chip-id]/in` will steer the blind according to the "payload actions" below -- Updates from the device will be sent to topic `/raw/esp8266/[chip-id]/out` - -### If you don't want to use MQTT -Simply do not enter any string in the MQTT server form field upon WIFI configuration of the device (step 3 above) - -## Payload options -- `(start)` - (calibrate) Sets the current position as top position -- `(max)` - (calibrate) Sets the current position as max position. Set `start` before you define `max` as `max` is a relative position to `start` -- `(0)` - (manual mode) Will stop the curtain -- `(-1)` - (manual mode) Will open the curtain. Requires `(0)` to stop the motor -- `(1)`- (manual mode) Will close the curtain. Requires `(0)` to stop the motor -- `0-100` - (auto mode) A number between 0-100 to set % of opened blind. Requires calibration before use. E.g. `50` will open it to 50% - -# Required libraries (3rd party) -- Stepper_28BYJ_48: https://github.com/thomasfredericks/Stepper_28BYJ_48/ -- PubSubClient: https://github.com/knolleary/pubsubclient/ -- ArduinoJson: https://github.com/bblanchon/ArduinoJson -- WIFIManager: https://github.com/tzapu/WiFiManager -- WbSocketsServer: https://github.com/Links2004/arduinoWebSockets - -# Screenshots - -## Control -![Control](https://user-images.githubusercontent.com/2181965/31178217-a5351678-a918-11e7-9611-3e8256c873a4.png) - -## Calibrate -![Settings](https://user-images.githubusercontent.com/2181965/31178216-a4f7194a-a918-11e7-85dd-8e189cfc031c.png) - -## Communication settings - ![WIFI Manager](https://user-images.githubusercontent.com/2181965/37288794-75244c84-2608-11e8-8c27-a17e1e854761.jpg) +# motor-on-roller-blind-ws +WebSocket based version of [motor-on-roller-blind](https://github.com/nidayand/motor-on-roller-blind). I.e. there is no need of an MQTT server but MQTT is supported as well - you can control it with WebSockets and/or with MQTT messages. + +3d parts for printing are available on Thingiverse.com: ["motor on a roller blind"](https://www.thingiverse.com/thing:2392856) + + 1. A tiny webserver is setup on the esp8266 that will serve one page to the client + 2. Upon powering on the first time WIFI credentials, a hostname and - optional - MQTT server details is to be configured. You can specify if you want **clockwise (CW) rotation** to close the blind and you can also specify **MQTT authentication** if required. Connect your computer to a new WIFI hotspot named **BlindsConnectAP**. Password = **nidayand** + 3. Connect to your normal WIFI with your client and go to the IP address of the device - or if you have an mDNS supported device (e.g. iOS, OSX or have Bonjour installed) you can go to http://{hostname}.local. If you don't know the IP-address of the device check your router for the leases (or check the serial console in the Arduino IDE or check the `/raw/esp8266/register` MQTT message if you are using an MQTT server) + 4. As the webpage is loaded it will connect through a websocket directly to the device to progress updates and to control the device. If any other client connects the updates will be in sync. + 5. Go to the Settings page to calibrate the motor with the start and end positions of the roller blind. Follow the instructions on the page + +# MQTT +- When it connects to WIFI and MQTT it will send a "register" message to topic `/raw/esp8266/register` with a payload containing chip-id and IP-address +- A message to `/raw/esp8266/[chip-id]/in` will steer the blind according to the "payload actions" below +- Updates from the device will be sent to topic `/raw/esp8266/[chip-id]/out` + +### If you don't want to use MQTT +Simply do not enter any string in the MQTT server form field upon WIFI configuration of the device (step 3 above) + +## Payload options +- `(start)` - (calibrate) Sets the current position as top position +- `(max)` - (calibrate) Sets the current position as max position. Set `start` before you define `max` as `max` is a relative position to `start` +- `(0)` - (manual mode) Will stop the curtain +- `(-1)` - (manual mode) Will open the curtain. Requires `(0)` to stop the motor +- `(1)`- (manual mode) Will close the curtain. Requires `(0)` to stop the motor +- `0-100` - (auto mode) A number between 0-100 to set % of opened blind. Requires calibration before use. E.g. `50` will open it to 50% + +# Manual operation +For those who do not want to carry smartphone all the time, device supports manual operation using buttons - up and down. +Additional reset button is added to clean configuration, device will be just like freshly flashed. +Calibration using webpage is still needed though. + +## Moving up and down +Buttons are active low using internal pullup resistors. +If you plan to use buttons _very_ far from device please use external pullup resistors and `INPUT` mode instead of `INPUT_PULLUP`. + +## Resetting +1. Press and hold reset button +2. Press and hold up and down buttons +3. Hold for 5 seconds +4. Release buttons and wait for device to reboot + +NOTE: After flashing - remove power source, otherwise device will hang after reset. +This is one-time-only issue so next resets will be done properly. + +# Required libraries (3rd party) +- Stepper_28BYJ_48: https://github.com/thomasfredericks/Stepper_28BYJ_48/ +- PubSubClient: https://github.com/knolleary/pubsubclient/ +- ArduinoJson v5: https://github.com/bblanchon/ArduinoJson +- WIFIManager: https://github.com/tzapu/WiFiManager +- WbSocketsServer: https://github.com/Links2004/arduinoWebSockets + +# Screenshots + +## Control +![Control](https://user-images.githubusercontent.com/2181965/31178217-a5351678-a918-11e7-9611-3e8256c873a4.png) + +## Calibrate +![Settings](https://user-images.githubusercontent.com/2181965/31178216-a4f7194a-a918-11e7-85dd-8e189cfc031c.png) + +## Communication settings + ![WIFI Manager](https://user-images.githubusercontent.com/2181965/37288794-75244c84-2608-11e8-8c27-a17e1e854761.jpg) diff --git a/index_html.h b/index_html.h index f941446..2687c74 100644 --- a/index_html.h +++ b/index_html.h @@ -253,4 +253,4 @@ String INDEX_HTML = R"( -)"; +)"; diff --git a/motor_on_a_roller_blind-ws.ino b/motor_on_a_roller_blind-ws.ino index 744c869..51b5d1b 100644 --- a/motor_on_a_roller_blind-ws.ino +++ b/motor_on_a_roller_blind-ws.ino @@ -1,480 +1,528 @@ -#include -#include -#include -#include -#include -#include -#include -#include "FS.h" -#include -#include -#include -#include -#include "NidayandHelper.h" -#include "index_html.h" - -//--------------- CHANGE PARAMETERS ------------------ -//Configure Default Settings for Access Point logon -String APid = "BlindsConnectAP"; //Name of access point -String APpw = "nidayand"; //Hardcoded password for access point - -//---------------------------------------------------- - -// Version number for checking if there are new code releases and notifying the user -String version = "1.3.1"; - -NidayandHelper helper = NidayandHelper(); - -//Fixed settings for WIFI -WiFiClient espClient; -PubSubClient psclient(espClient); //MQTT client -char mqtt_server[40]; //WIFI config: MQTT server config (optional) -char mqtt_port[6] = "1883"; //WIFI config: MQTT port config (optional) -char mqtt_uid[40]; //WIFI config: MQTT server username (optional) -char mqtt_pwd[40]; //WIFI config: MQTT server password (optional) - -String outputTopic; //MQTT topic for sending messages -String inputTopic; //MQTT topic for listening -boolean mqttActive = true; -char config_name[40]; //WIFI config: Bonjour name of device -char config_rotation[40] = "false"; //WIFI config: Detault rotation is CCW - -String action; //Action manual/auto -int path = 0; //Direction of blind (1 = down, 0 = stop, -1 = up) -int setPos = 0; //The set position 0-100% by the client -long currentPosition = 0; //Current position of the blind -long maxPosition = 2000000; //Max position of the blind. Initial value -boolean loadDataSuccess = false; -boolean saveItNow = false; //If true will store positions to SPIFFS -bool shouldSaveConfig = false; //Used for WIFI Manager callback to save parameters -boolean initLoop = true; //To enable actions first time the loop is run -boolean ccw = true; //Turns counter clockwise to lower the curtain - -Stepper_28BYJ_48 small_stepper(D1, D3, D2, D4); //Initiate stepper driver - -ESP8266WebServer server(80); // TCP server at port 80 will respond to HTTP requests -WebSocketsServer webSocket = WebSocketsServer(81); // WebSockets will respond on port 81 - -bool loadConfig() { - if (!helper.loadconfig()){ - return false; - } - JsonVariant json = helper.getconfig(); - - //Store variables locally - currentPosition = long(json["currentPosition"]); - maxPosition = long(json["maxPosition"]); - strcpy(config_name, json["config_name"]); - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_port, json["mqtt_port"]); - strcpy(mqtt_uid, json["mqtt_uid"]); - strcpy(mqtt_pwd, json["mqtt_pwd"]); - strcpy(config_rotation, json["config_rotation"]); - - return true; -} - -/** - Save configuration data to a JSON file - on SPIFFS -*/ -bool saveConfig() { - StaticJsonBuffer<200> jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); - json["currentPosition"] = currentPosition; - json["maxPosition"] = maxPosition; - json["config_name"] = config_name; - json["mqtt_server"] = mqtt_server; - json["mqtt_port"] = mqtt_port; - json["mqtt_uid"] = mqtt_uid; - json["mqtt_pwd"] = mqtt_pwd; - json["config_rotation"] = config_rotation; - - return helper.saveconfig(json); -} - -/* - Connect to MQTT server and publish a message on the bus. - Finally, close down the connection and radio -*/ -void sendmsg(String topic, String payload) { - if (!mqttActive) - return; - - helper.mqtt_publish(psclient, topic, payload); -} - - -/**************************************************************************************** -*/ -void processMsg(String res, uint8_t clientnum){ - /* - Check if calibration is running and if stop is received. Store the location - */ - if (action == "set" && res == "(0)") { - maxPosition = currentPosition; - saveItNow = true; - } - - /* - Below are actions based on inbound MQTT payload - */ - if (res == "(start)") { - /* - Store the current position as the start position - */ - currentPosition = 0; - path = 0; - saveItNow = true; - action = "manual"; - } else if (res == "(max)") { - /* - Store the max position of a closed blind - */ - maxPosition = currentPosition; - path = 0; - saveItNow = true; - action = "manual"; - } else if (res == "(0)") { - /* - Stop - */ - path = 0; - saveItNow = true; - action = "manual"; - } else if (res == "(1)") { - /* - Move down without limit to max position - */ - path = 1; - action = "manual"; - } else if (res == "(-1)") { - /* - Move up without limit to top position - */ - path = -1; - action = "manual"; - } else if (res == "(update)") { - //Send position details to client - int set = (setPos * 100)/maxPosition; - int pos = (currentPosition * 100)/maxPosition; - sendmsg(outputTopic, "{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - webSocket.sendTXT(clientnum, "{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - } else if (res == "(ping)") { - //Do nothing - } else { - /* - Any other message will take the blind to a position - Incoming value = 0-100 - path is now the position - */ - path = maxPosition * res.toInt() / 100; - setPos = path; //Copy path for responding to updates - action = "auto"; - - int set = (setPos * 100)/maxPosition; - int pos = (currentPosition * 100)/maxPosition; - - //Send the instruction to all connected devices - sendmsg(outputTopic, "{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - webSocket.broadcastTXT("{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - } -} - -void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { - switch(type) { - case WStype_TEXT: - Serial.printf("[%u] get Text: %s\n", num, payload); - - String res = (char*)payload; - - //Send to common MQTT and websocket function - processMsg(res, num); - break; - } -} -void mqttCallback(char* topic, byte* payload, unsigned int length) { - Serial.print("Message arrived ["); - Serial.print(topic); - Serial.print("] "); - String res = ""; - for (int i = 0; i < length; i++) { - res += String((char) payload[i]); - } - processMsg(res, NULL); -} - -/** - Turn of power to coils whenever the blind - is not moving -*/ -void stopPowerToCoils() { - digitalWrite(D1, LOW); - digitalWrite(D2, LOW); - digitalWrite(D3, LOW); - digitalWrite(D4, LOW); -} - -/* - Callback from WIFI Manager for saving configuration -*/ -void saveConfigCallback () { - shouldSaveConfig = true; -} - -void handleRoot() { - server.send(200, "text/html", INDEX_HTML); -} -void handleNotFound(){ - String message = "File Not Found\n\n"; - message += "URI: "; - message += server.uri(); - message += "\nMethod: "; - message += (server.method() == HTTP_GET)?"GET":"POST"; - message += "\nArguments: "; - message += server.args(); - message += "\n"; - for (uint8_t i=0; iOptional MQTT server parameters:

"); - WiFiManagerParameter custom_mqtt_server("server", "MQTT server", mqtt_server, 40); - WiFiManagerParameter custom_mqtt_port("port", "MQTT port", mqtt_port, 6); - WiFiManagerParameter custom_mqtt_uid("uid", "MQTT username", mqtt_server, 40); - WiFiManagerParameter custom_mqtt_pwd("pwd", "MQTT password", mqtt_server, 40); - WiFiManagerParameter custom_text2(""); - //Setup WIFI Manager - WiFiManager wifiManager; - - //reset settings - for testing - //clean FS, for testing - //helper.resetsettings(wifiManager); - - wifiManager.setSaveConfigCallback(saveConfigCallback); - //add all your parameters here - wifiManager.addParameter(&custom_config_name); - wifiManager.addParameter(&custom_rotation); - wifiManager.addParameter(&custom_text); - wifiManager.addParameter(&custom_mqtt_server); - wifiManager.addParameter(&custom_mqtt_port); - wifiManager.addParameter(&custom_mqtt_uid); - wifiManager.addParameter(&custom_mqtt_pwd); - wifiManager.addParameter(&custom_text2); - - wifiManager.autoConnect(APid.c_str(), APpw.c_str()); - - //Load config upon start - if (!SPIFFS.begin()) { - Serial.println("Failed to mount file system"); - return; - } - - /* Save the config back from WIFI Manager. - This is only called after configuration - when in AP mode - */ - if (shouldSaveConfig) { - //read updated parameters - strcpy(config_name, custom_config_name.getValue()); - strcpy(mqtt_server, custom_mqtt_server.getValue()); - strcpy(mqtt_port, custom_mqtt_port.getValue()); - strcpy(mqtt_uid, custom_mqtt_uid.getValue()); - strcpy(mqtt_pwd, custom_mqtt_pwd.getValue()); - strcpy(config_rotation, custom_rotation.getValue()); - - //Save the data - saveConfig(); - } - - /* - Try to load FS data configuration every time when - booting up. If loading does not work, set the default - positions - */ - loadDataSuccess = loadConfig(); - if (!loadDataSuccess) { - currentPosition = 0; - maxPosition = 2000000; - } - - /* - Setup multi DNS (Bonjour) - */ - if (MDNS.begin(config_name)) { - Serial.println("MDNS responder started"); - MDNS.addService("http", "tcp", 80); - MDNS.addService("ws", "tcp", 81); - - } else { - Serial.println("Error setting up MDNS responder!"); - while(1) { - delay(1000); - } - } - Serial.print("Connect to http://"+String(config_name)+".local or http://"); - Serial.println(WiFi.localIP()); - - //Start HTTP server - server.on("/", handleRoot); - server.onNotFound(handleNotFound); - server.begin(); - - //Start websocket - webSocket.begin(); - webSocket.onEvent(webSocketEvent); - - /* Setup connection for MQTT and for subscribed - messages IF a server address has been entered - */ - if (String(mqtt_server) != ""){ - Serial.println("Registering MQTT server"); - psclient.setServer(mqtt_server, String(mqtt_port).toInt()); - psclient.setCallback(mqttCallback); - - } else { - mqttActive = false; - Serial.println("NOTE: No MQTT server address has been registered. Only using websockets"); - } - - /* Set rotation direction of the blinds */ - if (String(config_rotation) == "false"){ - ccw = true; - } else { - ccw = false; - } - - //Update webpage - INDEX_HTML.replace("{VERSION}","V"+version); - INDEX_HTML.replace("{NAME}",String(config_name)); - - - //Setup OTA - //helper.ota_setup(config_name); - { - // Authentication to avoid unauthorized updates - //ArduinoOTA.setPassword(OTA_PWD); - - ArduinoOTA.setHostname(config_name); - - ArduinoOTA.onStart([]() { - Serial.println("Start"); - }); - ArduinoOTA.onEnd([]() { - Serial.println("\nEnd"); - }); - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }); - ArduinoOTA.onError([](ota_error_t error) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - ArduinoOTA.begin(); - } -} - -void loop(void) -{ - //OTA client code - ArduinoOTA.handle(); - - //Websocket listner - webSocket.loop(); - - /** - Serving the webpage - */ - server.handleClient(); - - //MQTT client - if (mqttActive){ - helper.mqtt_reconnect(psclient, mqtt_uid, mqtt_pwd, { inputTopic.c_str() }); - } - - - /** - Storing positioning data and turns off the power to the coils - */ - if (saveItNow) { - saveConfig(); - saveItNow = false; - - /* - If no action is required by the motor make sure to - turn off all coils to avoid overheating and less energy - consumption - */ - stopPowerToCoils(); - - } - - /** - Manage actions. Steering of the blind - */ - if (action == "auto") { - /* - Automatically open or close blind - */ - if (currentPosition > path){ - small_stepper.step(ccw ? -1: 1); - currentPosition = currentPosition - 1; - } else if (currentPosition < path){ - small_stepper.step(ccw ? 1 : -1); - currentPosition = currentPosition + 1; - } else { - path = 0; - action = ""; - int set = (setPos * 100)/maxPosition; - int pos = (currentPosition * 100)/maxPosition; - webSocket.broadcastTXT("{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - sendmsg(outputTopic, "{ \"set\":"+String(set)+", \"position\":"+String(pos)+" }"); - Serial.println("Stopped. Reached wanted position"); - saveItNow = true; - } - - } else if (action == "manual" && path != 0) { - /* - Manually running the blind - */ - small_stepper.step(ccw ? path : -path); - currentPosition = currentPosition + path; - } - - /* - After running setup() the motor might still have - power on some of the coils. This is making sure that - power is off the first time loop() has been executed - to avoid heating the stepper motor draining - unnecessary current - */ - if (initLoop) { - initLoop = false; - stopPowerToCoils(); - } -} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "FS.h" +#include "index_html.h" +#include "NidayandHelper.h" + +//--------------- CHANGE PARAMETERS ------------------ +//Configure Default Settings for Access Point logon +String APid = "BlindsConnectAP"; //Name of access point +String APpw = "nidayand"; //Hardcoded password for access point + +//Set up buttons +const uint8_t btnup = D5; //Up button +const uint8_t btndn = D6; //Down button +const uint8_t btnres = D7; //Reset button + +//---------------------------------------------------- + +// Version number for checking if there are new code releases and notifying the user +String version = "1.3.3"; + +NidayandHelper helper = NidayandHelper(); + +//Fixed settings for WIFI +WiFiClient espClient; +PubSubClient psclient(espClient); //MQTT client +char mqtt_server[40]; //WIFI config: MQTT server config (optional) +char mqtt_port[6] = "1883"; //WIFI config: MQTT port config (optional) +char mqtt_uid[40]; //WIFI config: MQTT server username (optional) +char mqtt_pwd[40]; //WIFI config: MQTT server password (optional) + +String outputTopic; //MQTT topic for sending messages +String inputTopic; //MQTT topic for listening +boolean mqttActive = true; +char config_name[40]; //WIFI config: Bonjour name of device +char config_rotation[40] = "false"; //WIFI config: Detault rotation is CCW + +String action; //Action manual/auto +int path = 0; //Direction of blind (1 = down, 0 = stop, -1 = up) +int setPos = 0; //The set position 0-100% by the client +long currentPosition = 0; //Current position of the blind +long maxPosition = 2000000; //Max position of the blind. Initial value +boolean loadDataSuccess = false; +boolean saveItNow = false; //If true will store positions to SPIFFS +bool shouldSaveConfig = false; //Used for WIFI Manager callback to save parameters +boolean initLoop = true; //To enable actions first time the loop is run +boolean ccw = true; //Turns counter clockwise to lower the curtain + +Stepper_28BYJ_48 small_stepper(D1, D3, D2, D4); //Initiate stepper driver + +ESP8266WebServer server(80); // TCP server at port 80 will respond to HTTP requests +WebSocketsServer webSocket = WebSocketsServer(81); // WebSockets will respond on port 81 + +bool loadConfig() { + if (!helper.loadconfig()) + return false; + + JsonVariant json = helper.getconfig(); + + //Useful if you need to see why confing is read incorrectly + json.printTo(Serial); + + //Store variables locally + currentPosition = json["currentPosition"].as(); + maxPosition = json["maxPosition"].as(); + + strcpy(config_name, json["config_name"]); + strcpy(mqtt_server, json["mqtt_server"]); + strcpy(mqtt_port, json["mqtt_port"]); + strcpy(mqtt_uid, json["mqtt_uid"]); + strcpy(mqtt_pwd, json["mqtt_pwd"]); + strcpy(config_rotation, json["config_rotation"]); + + return true; +} + +/** + Save configuration data to a JSON file + on SPIFFS +*/ +bool saveConfig() { + DynamicJsonBuffer jsonBuffer(300); + JsonObject& json = jsonBuffer.createObject(); + json["currentPosition"] = currentPosition; + json["maxPosition"] = maxPosition; + json["config_name"] = config_name; + json["mqtt_server"] = mqtt_server; + json["mqtt_port"] = mqtt_port; + json["mqtt_uid"] = mqtt_uid; + json["mqtt_pwd"] = mqtt_pwd; + json["config_rotation"] = config_rotation; + + return helper.saveconfig(json); +} + +/* + Connect to MQTT server and publish a message on the bus. + Finally, close down the connection and radio +*/ +void sendmsg(String topic, String payload) { + if (!mqttActive) + return; + + helper.mqtt_publish(psclient, topic, payload); +} + + +/**************************************************************************************** +*/ +void processMsg(String res, uint8_t clientnum) { + /* + Check if calibration is running and if stop is received. Store the location + */ + if (action == "set" && res == "(0)") { + maxPosition = currentPosition; + saveItNow = true; + } + + //Below are actions based on inbound MQTT payload + if (res == "(start)") { + + //Store the current position as the start position + currentPosition = 0; + path = 0; + saveItNow = true; + action = "manual"; + } else if (res == "(max)") { + + //Store the max position of a closed blind + maxPosition = currentPosition; + path = 0; + saveItNow = true; + action = "manual"; + } else if (res == "(0)") { + + //Stop + path = 0; + saveItNow = true; + action = "manual"; + } else if (res == "(1)") { + + //Move down without limit to max position + path = 1; + action = "manual"; + } else if (res == "(-1)") { + + //Move up without limit to top position + path = -1; + action = "manual"; + } else if (res == "(update)") { + //Send position details to client + int set = (setPos * 100) / maxPosition; + int pos = (currentPosition * 100) / maxPosition; + sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + webSocket.sendTXT(clientnum, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + } else if (res == "(ping)") { + //Do nothing + } else { + /* + Any other message will take the blind to a position + Incoming value = 0-100 + path is now the position + */ + path = maxPosition * res.toInt() / 100; + setPos = path; //Copy path for responding to updates + action = "auto"; + + int set = (setPos * 100) / maxPosition; + int pos = (currentPosition * 100) / maxPosition; + + //Send the instruction to all connected devices + sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + webSocket.broadcastTXT("{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + } +} + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + switch (type) { + case WStype_TEXT: + Serial.printf("[%u] get Text: %s\n", num, payload); + + String res = (char*)payload; + + //Send to common MQTT and websocket function + processMsg(res, num); + break; + } +} +void mqttCallback(char* topic, byte* payload, unsigned int length) { + Serial.print(F("Message arrived [")); + Serial.print(topic); + Serial.print(F("] ")); + String res = ""; + for (int i = 0; i < length; i++) { + res += String((char) payload[i]); + } + processMsg(res, NULL); +} + +/** + Turn of power to coils whenever the blind + is not moving +*/ +void stopPowerToCoils() { + digitalWrite(D1, LOW); + digitalWrite(D2, LOW); + digitalWrite(D3, LOW); + digitalWrite(D4, LOW); + Serial.println(F("Motor stopped")); +} + +/* + Callback from WIFI Manager for saving configuration +*/ +void saveConfigCallback () { + shouldSaveConfig = true; +} + +void handleRoot() { + server.send(200, "text/html", INDEX_HTML); +} +void handleNotFound() { + String message = "File Not Found\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} + +void setup(void) +{ + Serial.begin(115200); + delay(100); + Serial.print(F("Starting now\n")); + + pinMode(btnup, INPUT_PULLUP); + pinMode(btndn, INPUT_PULLUP); + pinMode(btnres, INPUT_PULLUP); + + //Reset the action + action = ""; + + //Set MQTT properties + outputTopic = helper.mqtt_gettopic("out"); + inputTopic = helper.mqtt_gettopic("in"); + + //Set the WIFI hostname + WiFi.hostname(config_name); + + //Define customer parameters for WIFI Manager + WiFiManagerParameter custom_config_name("Name", "Bonjour name", config_name, 40); + WiFiManagerParameter custom_rotation("Rotation", "Clockwise rotation", config_rotation, 40); + WiFiManagerParameter custom_text("

Optional MQTT server parameters:

"); + WiFiManagerParameter custom_mqtt_server("server", "MQTT server", mqtt_server, 40); + WiFiManagerParameter custom_mqtt_port("port", "MQTT port", mqtt_port, 6); + WiFiManagerParameter custom_mqtt_uid("uid", "MQTT username", mqtt_server, 40); + WiFiManagerParameter custom_mqtt_pwd("pwd", "MQTT password", mqtt_server, 40); + WiFiManagerParameter custom_text2(""); + //Setup WIFI Manager + WiFiManager wifiManager; + + //reset settings - for testing + //clean FS, for testing + //helper.resetsettings(wifiManager); + + wifiManager.setSaveConfigCallback(saveConfigCallback); + //add all your parameters here + wifiManager.addParameter(&custom_config_name); + wifiManager.addParameter(&custom_rotation); + wifiManager.addParameter(&custom_text); + wifiManager.addParameter(&custom_mqtt_server); + wifiManager.addParameter(&custom_mqtt_port); + wifiManager.addParameter(&custom_mqtt_uid); + wifiManager.addParameter(&custom_mqtt_pwd); + wifiManager.addParameter(&custom_text2); + + wifiManager.autoConnect(APid.c_str(), APpw.c_str()); + + //Load config upon start + if (!SPIFFS.begin()) { + Serial.println(F("Failed to mount file system")); + return; + } + + /* Save the config back from WIFI Manager. + This is only called after configuration + when in AP mode + */ + if (shouldSaveConfig) { + //read updated parameters + strcpy(config_name, custom_config_name.getValue()); + strcpy(mqtt_server, custom_mqtt_server.getValue()); + strcpy(mqtt_port, custom_mqtt_port.getValue()); + strcpy(mqtt_uid, custom_mqtt_uid.getValue()); + strcpy(mqtt_pwd, custom_mqtt_pwd.getValue()); + strcpy(config_rotation, custom_rotation.getValue()); + + //Save the data + saveConfig(); + } + + /* + Try to load FS data configuration every time when + booting up. If loading does not work, set the default + positions + */ + loadDataSuccess = loadConfig(); + if (!loadDataSuccess) { + Serial.println(F("Unable to load saved data")); + currentPosition = 0; + maxPosition = 2000000; + } + /* + Setup multi DNS (Bonjour) + */ + if (MDNS.begin(config_name)) { + Serial.println(F("MDNS responder started")); + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); + + } else { + Serial.println(F("Error setting up MDNS responder!")); + while (1) { + delay(1000); + } + } + Serial.print("Connect to http://" + String(config_name) + ".local or http://"); + Serial.println(WiFi.localIP()); + + //Start HTTP server + server.on("/", handleRoot); + server.onNotFound(handleNotFound); + server.begin(); + + //Start websocket + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + /* Setup connection for MQTT and for subscribed + messages IF a server address has been entered + */ + if (String(mqtt_server) != "") { + Serial.println(F("Registering MQTT server")); + psclient.setServer(mqtt_server, String(mqtt_port).toInt()); + psclient.setCallback(mqttCallback); + + } else { + mqttActive = false; + Serial.println(F("NOTE: No MQTT server address has been registered. Only using websockets")); + } + + //Set rotation direction of the blinds + if (String(config_rotation) == "false") + ccw = true; + else + ccw = false; + + + //Update webpage + INDEX_HTML.replace("{VERSION}", "V" + version); + INDEX_HTML.replace("{NAME}", String(config_name)); + + + //Setup OTA + //helper.ota_setup(config_name); + { + // Authentication to avoid unauthorized updates + //ArduinoOTA.setPassword(OTA_PWD); + + ArduinoOTA.setHostname(config_name); + + ArduinoOTA.onStart([]() { + Serial.println(F("Start")); + }); + ArduinoOTA.onEnd([]() { + Serial.println(F("\nEnd")); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println(F("Auth Failed")); + else if (error == OTA_BEGIN_ERROR) Serial.println(F("Begin Failed")); + else if (error == OTA_CONNECT_ERROR) Serial.println(F("Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) Serial.println(F("Receive Failed")); + else if (error == OTA_END_ERROR) Serial.println(F("End Failed")); + }); + ArduinoOTA.begin(); + } +} + +void loop(void) +{ + //OTA client code + ArduinoOTA.handle(); + + //Websocket listner + webSocket.loop(); + + //Serving the webpage + server.handleClient(); + + //MQTT client + if (mqttActive) + helper.mqtt_reconnect(psclient, mqtt_uid, mqtt_pwd, { inputTopic.c_str() }); + + if (digitalRead(btnres)) { + bool pres_cont = false; + while (!digitalRead(btndn) && currentPosition > 0) { + Serial.println(F("Moving down")); + small_stepper.step(ccw ? -1 : 1); + currentPosition = currentPosition - 1; + yield(); + delay(1); + pres_cont = true; + } + while (!digitalRead(btnup) && currentPosition < maxPosition) { + Serial.println(F("Moving up")); + small_stepper.step(ccw ? 1 : -1); + currentPosition = currentPosition + 1; + yield(); + delay(1); + pres_cont = true; + } + if (pres_cont) { + int set = (setPos * 100) / maxPosition; + int pos = (currentPosition * 100) / maxPosition; + webSocket.broadcastTXT("{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + Serial.println(F("Stopped. Reached wanted position")); + saveItNow = true; + } + } + + if (!(digitalRead(btnres) || digitalRead(btndn) || digitalRead(btnup))) { + Serial.println(F("Hold to reset...")); + uint32_t restime = millis(); + while (!(digitalRead(btnres) || digitalRead(btndn) || digitalRead(btnup))) + yield(); //Prevent watchdog trigger + + if (millis() - restime >= 2500) { + stopPowerToCoils(); + Serial.println(F("Removing configs...")); + + WiFi.disconnect(true); + WiFiManager wifiManager; + helper.resetsettings(wifiManager); + + Serial.println(F("Reboot")); + ESP.wdtFeed(); + yield(); + ESP.restart(); + } + } + + //Storing positioning data and turns off the power to the coils + if (saveItNow) { + saveConfig(); + saveItNow = false; + + /* + If no action is required by the motor make sure to + turn off all coils to avoid overheating and less energy + consumption + */ + stopPowerToCoils(); + } + + //Manage actions. Steering of the blind + if (action == "auto") { + + //Automatically open or close blind + if (currentPosition > path) { + Serial.println(F("Moving down")); + small_stepper.step(ccw ? -1 : 1); + currentPosition = currentPosition - 1; + } else if (currentPosition < path) { + Serial.println(F("Moving up")); + small_stepper.step(ccw ? 1 : -1); + currentPosition = currentPosition + 1; + } else { + path = 0; + action = ""; + int set = (setPos * 100) / maxPosition; + int pos = (currentPosition * 100) / maxPosition; + webSocket.broadcastTXT("{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }"); + Serial.println(F("Stopped. Reached wanted position")); + saveItNow = true; + } + + } else if (action == "manual" && path != 0) { + + //Manually running the blind + small_stepper.step(ccw ? path : -path); + currentPosition = currentPosition + path; + Serial.println(F("Moving motor manually")); + } + + /* + After running setup() the motor might still have + power on some of the coils. This is making sure that + power is off the first time loop() has been executed + to avoid heating the stepper motor draining + unnecessary current + */ + if (initLoop) { + initLoop = false; + stopPowerToCoils(); + } +}