--- /dev/null
+/*
+ * 4 Channel Switcher, IM Dendrite module (for ESP8266)
+ * Created for Interplaymedium™ project (https://interplaymedium.org)
+ * Copyright © 2016 Dmitry Shalnov [interplaymedium.org]
+ * Licensed under the Apache License, Version 2.0
+*/
+
+#define SERIAL 0
+
+#include "../../info"
+#include "version"
+
+#include <NTPClient.h>
+
+#include <ESP8266WiFi.h>
+#include <WiFiClient.h>
+#include <ESP8266WebServer.h>
+#include <ESP8266mDNS.h>
+#include <EEPROM.h>
+
+#define EEPROM_HOST_MAX_LEN 57 // Host name max length, can't be > 57 (could be 64, buth there is a bug in avahi https://github.com/lathiat/avahi/issues/273)
+#define EEPROM_OTHER_LEN 9 // length of EEPROM excepting EEPROM_HOST_MAX_LEN (count below)
+
+#define EEPROM_HOST 0
+#define EEPROM_FLAG EEPROM_HOST_MAX_LEN + 1
+#define EEPROM_R EEPROM_HOST_MAX_LEN + 2
+#define EEPROM_G EEPROM_HOST_MAX_LEN + 3
+#define EEPROM_B EEPROM_HOST_MAX_LEN + 4
+#define EEPROM_W EEPROM_HOST_MAX_LEN + 5
+#define EEPROM_ROTATE_DELAY EEPROM_HOST_MAX_LEN + 6 // uint16_t
+#define EEPROM_MAIN_DELAY EEPROM_HOST_MAX_LEN + 8 // uint16_t
+
+#define FULL_OUTPUT 2 // default quiet level
+
+// -------------------- main settings --------------------
+
+#define LED1 0
+#define LED2 2
+#define LED3 3
+#define LED4 1
+
+#define DEF_MAIN_DELAY 600;
+
+uint8_t switchState = 0;
+uint8_t quiet = FULL_OUTPUT;
+uint8_t paramCnt = 0;
+uint8_t JSONon = 0;
+
+int R=0, G=0, B=0, W=0;
+uint16_t rotateTimer = 0, mainTimer = 0;
+uint16_t rotateDelay = 0;
+uint16_t mainDelay = DEF_MAIN_DELAY;
+String rgbw = "";
+
+unsigned long timeStamp = 0;
+
+// ------------------- server settings -------------------
+
+#define HOST_DEFAULT "im_switch_"
+#define NTP_SERVER "europe.pool.ntp.org"
+
+char host[ EEPROM_HOST_MAX_LEN ];
+
+const char* ssid = IM_WIFI_SSID;
+const char* password = IM_WIFI_PASS;
+
+ESP8266WebServer server(80);
+
+String hostDefURI;
+String HTTPOut;
+String HTTPErr;
+
+WiFiUDP ntpUDP;
+NTPClient timeClient(ntpUDP, NTP_SERVER , 0, 60000);
+
+// ---------------- EEPROM String r/w -------------------
+
+void EEPROMStrRead( unsigned char addr, char * str ){
+ for (unsigned char a = addr; a < addr + EEPROM_HOST_MAX_LEN; a++ ){
+ str[ a ] = EEPROM.read( a );
+ if (str[ a ] == 0) break;
+ }
+}
+
+void EEPROMStrWrite( unsigned char addr, char * str ){
+ unsigned char a = 0;
+ for (a = addr; a < addr + EEPROM_HOST_MAX_LEN; a++ ){
+ EEPROM.write( a, str[ a ] );
+ if (str[ a ] == 0) break;
+ }
+ EEPROM.write( a, 0 );
+}
+
+// ---------------- misc ---------------------------------
+
+int str2HEX( const String str ) {
+ return strtol( str.c_str(), 0, 16 );
+}
+
+String printNum( unsigned long num, int base, char sign ) {
+
+ char outbuf[12];
+
+ int i = 12;
+ int j = 0;
+ int n;
+
+ do {
+ outbuf[i] = "0123456789ABCDEF"[num % base];
+ i--;
+ num = num/base;
+ } while ( num > 0 );
+
+ if ( sign != ' ' ){
+ outbuf[0] = sign;
+ ++j;
+ }
+
+ while ( ++i < 13 ){
+ outbuf[j++] = outbuf[i];
+ }
+
+ outbuf[j] = 0;
+
+ return outbuf;
+}
+
+uint8_t is_number( const char *s ){
+
+ while (*s) {
+ Serial.printf("s: %c\n", *s);
+ if ( *s++ <48 || *s >57 ) return 0;
+ }
+ return true;
+}
+
+// ----------- explode for selected substring -----------
+
+String expld( String str, unsigned int numb, char delimiter ){
+
+ unsigned int cnt = 0, a = 0, p2 = 0, p1 = 0, lng = 0;
+
+ lng = str.length();
+
+ for ( a = 0; a < lng; a++ ){
+ if ( str.charAt( a ) == delimiter ) {
+ p2 = p1;
+ p1 = a;
+ if ( cnt == numb ) break;
+ cnt ++;
+ }
+ }
+
+ if ( cnt < numb ) return "";
+
+ if ( a == lng ) {
+ p2 = p1;
+ p1 = lng;
+ }
+
+ if ( numb > 0 ) p2 ++;
+
+ return str.substring(p2, p1);
+
+}
+
+// --------------- Init ---------------------------------
+
+void setup(void) {
+
+ HTTPOut.reserve(800); // lenght of Help message generally
+
+ // init LED pins
+
+ pinMode(LED1, OUTPUT);
+ pinMode(LED2, OUTPUT);
+ pinMode(LED3, OUTPUT);
+ pinMode(LED4, OUTPUT);
+
+ digitalWrite(LED1, LOW);
+ digitalWrite(LED2, LOW);
+ digitalWrite(LED3, LOW);
+ digitalWrite(LED4, LOW);
+
+#if SERIAL == 1
+
+ // Serial init
+
+ Serial.begin(115200);
+ Serial.println();
+ Serial.println("Booting...");
+#endif
+
+ // default hostname of the unit
+
+ hostDefURI.reserve( EEPROM_HOST_MAX_LEN );
+ hostDefURI = HOST_DEFAULT + WiFi.macAddress().substring(12, 14) + WiFi.macAddress().substring(15, 17);
+
+ // read parameters from EEPROM (if it has been saved early)
+
+ EEPROM.begin( EEPROM_HOST_MAX_LEN + EEPROM_OTHER_LEN );
+
+ if ( EEPROM.read( EEPROM_FLAG ) == 1 ){
+
+ EEPROMStrRead( EEPROM_HOST, host );
+
+ mainDelay = EEPROM.read( EEPROM_MAIN_DELAY );
+ rotateDelay = EEPROM.read( EEPROM_ROTATE_DELAY );
+
+ R = EEPROM.read( EEPROM_R );
+ G = EEPROM.read( EEPROM_G );
+ B = EEPROM.read( EEPROM_B );
+ W = EEPROM.read( EEPROM_W );
+
+ } else {
+ mainDelay = DEF_MAIN_DELAY;
+ strcpy( host, (char*)hostDefURI.c_str() ); // default URI and host name
+ }
+
+ WiFi.hostname( host );
+
+// WiFi.softAP(APssid, APpassword);
+// WiFi.mode(WIFI_AP);
+// WiFi.mode(WIFI_AP_STA);
+
+ WiFi.mode(WIFI_STA);
+ WiFi.setAutoReconnect( true );
+ WiFi.begin(ssid, password);
+
+#if SERIAL == 1
+ Serial.printf("Ready. Try \"curl %s/help\" in terminal\n", host);
+#endif
+
+
+ if (WiFi.waitForConnectResult() == WL_CONNECTED) {
+
+ MDNS.begin( host );
+
+ // get time
+
+// timeClient.setTimeOffset(0);
+ timeClient.begin();
+ timeClient.update();
+
+ // server begin
+
+ server.begin();
+ MDNS.addService("http", "tcp", 80);
+
+ // -------------------- handlers of parameters ----------------------
+
+ server.onNotFound( []() {
+
+ server.sendHeader("Connection", "close");
+
+ // no command given (see handlers of commands below)
+
+ if ( server.uri() != "/" ) HTTPErr += "Command '" + server.uri() + "' not exist.\n";
+
+ // check parameters
+
+ for ( uint8_t i=0; i < server.args(); i++ ){
+
+ paramCnt = 0;
+
+ // ------------- specific parameters -----------------
+
+ // GPIO switch
+
+ if ( server.argName(i) == "switch" ){
+
+ rgbw = server.arg("switch");
+
+ R = !!str2HEX( rgbw.substring(0, 1) );
+ G = !!str2HEX( rgbw.substring(1, 2) );
+ B = !!str2HEX( rgbw.substring(2, 3) );
+ W = !!str2HEX( rgbw.substring(3, 4) );
+
+ switchState = 1;
+
+ paramCnt++;
+ }
+
+ // GPIO rotate
+
+ if ( server.argName(i) == "rotate" ){
+
+ rotateDelay = server.arg("rotate").toInt();
+
+ paramCnt++;
+ }
+
+ // ------------- common parameters -------------------
+
+ // set time
+
+ if ( server.argName(i) == "time" ){ // || param != 0
+
+ if ( is_number( server.arg("time").c_str() ) ) {
+ timeClient.update();
+ timeStamp = strtol( server.arg("time").c_str(), 0, 10 );
+ } else {
+ HTTPErr += "Parameter 'time' must be specified in UNIX timestamp format.\n"; // 1631848103
+ }
+
+ paramCnt++;
+ }
+
+ // rename host
+
+ if ( server.argName(i) == "host" ) {
+
+ if ( server.arg("host") == "" ) {
+
+ HTTPErr += "Please specify new unit hostname.\n";
+
+ } else if ( server.arg("host").length() > EEPROM_HOST_MAX_LEN ) {
+
+ HTTPErr += "Unit hostname can not exceed " + String( EEPROM_HOST_MAX_LEN ) + " symbols.\n";
+
+ } else {
+
+ strcpy( host, (char*)server.arg("host").c_str() ); // host = (char *)server.arg("host").c_str();
+ WiFi.hostname( host );
+ MDNS.begin( host );
+
+ HTTPOut += "The host name has been changed to '" + String( host ) + "'. Do not forget to save current settings using the '?save' command.\n";
+ }
+
+ paramCnt++;
+ }
+
+ // save all parameters to EEPROM
+
+ if (server.argName(i) == "save" ){
+
+ EEPROMStrWrite( EEPROM_HOST, host );
+
+ EEPROM.write( EEPROM_MAIN_DELAY, mainDelay );
+ EEPROM.write( EEPROM_ROTATE_DELAY, rotateDelay );
+
+ EEPROM.write( EEPROM_R, R );
+ EEPROM.write( EEPROM_G, G );
+ EEPROM.write( EEPROM_B, B );
+ EEPROM.write( EEPROM_W, W );
+
+ EEPROM.write( EEPROM_FLAG, 1 ); // EEPROM changing flag
+ EEPROM.commit();
+
+ HTTPOut += "The current settings are saved.\n";
+
+ paramCnt++;
+ }
+
+ // reset to default parameters
+
+ if ( server.argName(i) == "reset" ){
+
+ EEPROMStrWrite( EEPROM_HOST, (char*)hostDefURI.c_str() );
+
+ EEPROM.write( EEPROM_MAIN_DELAY, 0 );
+ EEPROM.write( EEPROM_ROTATE_DELAY, 0 );
+
+ EEPROM.write( EEPROM_R, 0 );
+ EEPROM.write( EEPROM_G, 0 );
+ EEPROM.write( EEPROM_B, 0 );
+ EEPROM.write( EEPROM_W, 0 );
+
+ EEPROM.write( EEPROM_FLAG, 0 ); // EEPROM changing flag
+ EEPROM.commit();
+
+ if ( quiet > 1 ) {
+
+ server.send_P(200, "text/plain", PSTR("All parameters are set to default values. Unit rebooting...\n") );
+ server.begin();
+ }
+
+ paramCnt++;
+
+ delay(1000);
+ ESP.restart();
+ }
+
+ // reboot
+
+ if ( server.argName(i) == "reboot" ){
+
+ if ( quiet > 1 ) {
+
+ server.send_P(200, "text/plain", PSTR("Unit rebooting...\n") );
+ server.begin();
+ }
+
+ paramCnt++;
+
+ delay(1000);
+ ESP.restart();
+ }
+
+ // quiet, level of output
+
+ if ( server.argName(i) == "quiet" ) {
+
+ quiet = str2HEX( server.arg("quiet") );
+ paramCnt++;
+ } else {
+ quiet = FULL_OUTPUT;
+ }
+
+ // json on
+
+ if ( server.argName(i) == "json" ) { // server.hasArg("json") ) {
+ JSONon = true;
+ paramCnt++;
+ }
+
+ if ( !paramCnt ) HTTPErr += "Parameter '" + server.argName(i) + "' not exist.\n";
+ }
+
+ // host default return
+
+ if ( HTTPErr != "" ) {
+
+ String Err = "";
+
+ paramCnt = 0;
+
+ if (quiet > 0) {
+
+ if ( JSONon ) {
+
+ HTTPOut += "{[";
+
+ while ( 1 ){
+ Err = expld( HTTPErr, paramCnt, '\n' );
+ if ( Err != "" ) HTTPOut += "\"" + Err + "\""; else break;
+ if ( expld( HTTPErr, paramCnt+1, '\n' ) != "" ) HTTPOut += ",";
+ paramCnt ++;
+ }
+
+ HTTPOut += "]}";
+
+ } else {
+
+ while ( 1 ){
+ Err = expld( HTTPErr, paramCnt, '\n' );
+ if ( Err != "" ) HTTPOut += "Error: " + Err + "\n"; else break;
+ paramCnt ++;
+ }
+// HTTPOut += "See '/help' for details\n";
+ }
+ }
+
+ } else {
+
+ if ( quiet > 1 && HTTPOut == "" ) {
+
+ if ( JSONon ){
+
+ HTTPOut = "{";
+ HTTPOut += "\"MAC\":\"" + String( WiFi.macAddress() ) + "\",\n";
+ HTTPOut += "\"Host\":\"" + String( host ) + "\",\n";
+ HTTPOut += "\"Timestamp\":\"" + String( timeClient.getEpochTime() ) + "\",\n";
+ HTTPOut += "\"Target_timestamp\":\"" + String( timeStamp ) + "\",\n";
+ HTTPOut += "\"Switchers\":\"" + String( R ) + String( G ) + String( B ) + String( W ) + "\",\n";
+ HTTPOut += "\"Rotate\":\"" + printNum( rotateDelay, 16, ' ' ) + "\",\n";
+// HTTPOut += "\"Delay\":\"" + printNum( mainDelay, 16, ' ' ) + "\"\n";
+ HTTPOut += "}";
+
+ } else {
+
+ HTTPOut = "MAC:" + String( WiFi.macAddress() ) + "\n";
+ HTTPOut += "Host:" + String( host ) + "\n";
+ HTTPOut += "Timestamp:" + String( timeClient.getEpochTime() ) + "\n";
+ HTTPOut += "Target timestamp:" + String( timeStamp ) + "\n";
+ HTTPOut += "Switchers:" + String( R ) + " " + String( G ) + " " + String( B ) + " " + String( W ) + "\n";
+ HTTPOut += "Rotate:" + printNum( rotateDelay, 16, ' ' ) + "\n";
+// HTTPOut += "Delay:" + printNum( mainDelay, 16, ' ' ) + "\n";
+
+ }
+ }
+ }
+
+ server.send(200, "text/plain", HTTPOut );
+
+ HTTPOut = "";
+ HTTPErr = "";
+ JSONon = 0;
+
+ });
+
+ // -------------------- handlers of commands ----------------------
+
+ // ui interface TODO xml inteface
+
+ server.on("/ui", HTTP_GET, []() {
+ HTTPOut = "User interface. Not yet implemented.\n";
+ server.sendHeader("Connection", "close");
+ server.send(200, "text/plain", HTTPOut);
+ });
+
+ // help
+
+ server.on("/help", HTTP_GET, []() {
+
+ HTTPOut = "Interplay Medium ESP8266 4-channel binary switcher. Version: " + String(VERSION) + "\n";
+ HTTPOut += "Created by Dmitry Shalnov (c) 2017. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. \n\n";
+
+ HTTPOut += " ?switch= four-bit representation of the switches status, 1 symbol per switch (1111)\n";
+// HTTPOut += " ?blink= Red Green Blue and White components, hex 32 bit, 8 bit/color (ff00ff00)\n";
+ HTTPOut += " ?rotate= Switches rotation delay (ffff, set 0 to stop)\n";
+// HTTPOut += " ?delay= Delay of state changing, hex 16 bit (ffff)\n";
+ HTTPOut += " ?time= Target timestamp (when the changes will take effect, good for synch), decimal UNIX timestamp\n";
+
+ HTTPOut += " \n";
+ HTTPOut += " ?host= Rename the unit\n";
+ HTTPOut += " ?quiet= Level of output (0 -- no output, 1 -- errors only)\n";
+ HTTPOut += " ?save Save current settings\n";
+ HTTPOut += " ?reset Reset to initial settings\n";
+ HTTPOut += " ?reboot Reboot unit\n";
+ HTTPOut += " ?json Output in JSON format\n";
+
+ HTTPOut += " \n";
+ HTTPOut += " /ui Output in XML UI format\n";
+ HTTPOut += " /update Wireless update of firmware (see example below)\n";
+ HTTPOut += " /help This help\n\n";
+
+ HTTPOut += " \n";
+ HTTPOut += "Usage: curl " + String(host) + "?<parameter>=<value>\n";
+ HTTPOut += " curl " + String(host) + "/<command>\n";
+ HTTPOut += "Examples: curl \"" + String(host) + "?switch=1010\" \n";
+ HTTPOut += " curl -F image=@firmware.bin " + String(host) + "/update \n";
+
+ server.sendHeader("Connection", "close");
+ server.send( 200, "text/plain", HTTPOut );
+
+ });
+
+ // firmware update
+
+ server.on("/update", HTTP_POST, []() {
+
+ server.sendHeader("Connection", "close");
+ server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
+
+ ESP.restart();
+
+ }, []() {
+
+ HTTPUpload& upload = server.upload();
+
+ if (upload.status == UPLOAD_FILE_START) {
+
+ rotateDelay = 0;
+
+#if SERIAL == 1
+ Serial.setDebugOutput(true);
+#endif
+ WiFiUDP::stopAll();
+
+#if SERIAL == 1
+ Serial.printf("Update: %s\n", upload.filename.c_str());
+#endif
+ uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
+
+ if (!Update.begin(maxSketchSpace)) { //start with max available size
+#if SERIAL == 1
+ Update.printError(Serial);
+#endif
+ }
+
+ } else if (upload.status == UPLOAD_FILE_WRITE) {
+
+ if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
+#if SERIAL == 1
+ Update.printError(Serial);
+#endif
+ }
+ } else if (upload.status == UPLOAD_FILE_END) {
+
+ if (Update.end(true)) { //true to set the size to the current progress
+
+ server.sendHeader("Connection", "close");
+ server.send_P(200, "text/plain", PSTR("Success. Please wait until device replace firmware and boot up...\n") );
+#if SERIAL == 1
+ Serial.printf("Update Success: %u\nRebooting....\n", upload.totalSize);
+#endif
+ } else {
+ server.sendHeader("Connection", "close");
+ server.send_P(200, "text/plain", PSTR("Error: Something went wrong. Please reset device and try again.\n") );
+#if SERIAL == 1
+ Update.printError(Serial);
+#endif
+ }
+#if SERIAL == 1
+ Serial.setDebugOutput(false);
+#endif
+ }
+
+ yield();
+ });
+
+ } else {
+#if SERIAL == 1
+ Serial.println("WiFi failed");
+#endif
+ }
+}
+
+// ---------------------------------------------------------
+// ========================== MAIN =========================
+// ---------------------------------------------------------
+
+void loop(void) {
+
+ server.handleClient();
+ MDNS.update();
+
+ rotateTimer ++;
+ mainTimer ++;
+
+ // reconnect
+
+ if ( WiFi.status() != WL_CONNECTED ) {
+#if SERIAL == 1
+ Serial.printf(".");
+#endif
+
+ if (WiFi.waitForConnectResult() == WL_CONNECTED) {
+#if SERIAL == 1
+ Serial.println("Reconnected");
+#endif
+ MDNS.begin( host );
+
+ timeClient.begin();
+ timeClient.update();
+ }
+
+ }
+
+ // main routine with synch
+
+ if ( timeClient.getEpochTime() > timeStamp ) {
+
+ // change state
+
+ if ( switchState == 1 ) {
+
+ digitalWrite(LED1, R);
+ digitalWrite(LED2, G);
+ digitalWrite(LED3, B);
+ digitalWrite(LED4, W);
+
+ switchState = 0;
+ }
+
+ // blink switchers
+
+ // TODO blinking all of them and each with different freq
+
+ // rotate switchers
+
+ if ( rotateTimer >= rotateDelay && rotateDelay != 0 && switchState == 0 ) {
+
+ uint8_t TMP = R;
+
+ R = G;
+ G = B;
+ B = W;
+ W = TMP;
+
+ digitalWrite(LED1, R);
+ digitalWrite(LED2, G);
+ digitalWrite(LED3, B);
+ digitalWrite(LED4, W);
+
+ rotateTimer = 0;
+
+ // TODO rotate switchers
+ }
+ }
+}