/* * 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 #include #include #include #include #include #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 . \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) + "?=\n"; HTTPOut += " curl " + String(host) + "/\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 } } }