/* Wemos8266RelaysLedDisplay/main.cpp */ #define COMPDATE __DATE__ __TIME__ #define APP_VERSION "0.2.18" // Button pin on the esp for selecting modes. 0 for Generic devices! #define MODEBUTTON D3 #define RELAY1_PIN D1 #define RELAY2_PIN D2 #define DISPLAY_CLK_PIN D5 #define DISPLAY_DATA_PIN D7 #define DISPLAY_CS_PIN D6 #define VERTICAL_BAR_STARTS_TOP false #define DEBUG_RELAYS false #define DEBUG_DISPLAY false #define STARTUP1_ANIMATION_DURATION_ms 2000 #define STARTUP2_ANIMATION_DURATION_ms 33000 #include #include #include #include #include #include #include #include #include "MD_RobotEyes.h" #include #include // https://github.com/me-no-dev/AsyncTCP #include #include #include "Relays.h" IOTAppStory IAS(COMPDATE, MODEBUTTON); String deviceName = "wemosMatrixDisplay"; String chipId; // Define the number of devices we have in the chain and the hardware interface // NOTE: These pin numbers will probably not work with your hardware and may // need to be adapted /* Mapper result, connector to ESP is at right (backside): Your responses produce these hardware parameters HW_DIG_ROWS 1 HW_REV_COLS 0 HW_REV_ROWS 0 Your hardware matches the setting for FC-16 modules. Please set FC16_HW. */ #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 // Hardware SPI connection // MD_Parola P = MD_Parola(HARDWARE_TYPE, DISPLAY_CS_PIN, MAX_DEVICES); // Arbitrary output pins MD_Parola P = MD_Parola(HARDWARE_TYPE, DISPLAY_DATA_PIN, DISPLAY_CLK_PIN, DISPLAY_CS_PIN, MAX_DEVICES); MD_RobotEyes E; Relays R; WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 3600, 60000); AsyncWebServer server(80); // Field default values char *clockName = "FREMO"; char *clockSpeed_modelMsPerRealSec_String = "250"; int clockSpeed_modelMsPerRealSec = 250; char *relay1Pin_String = "D1"; char *relay2Pin_String = "D2"; int relay1Pin = D1, relay2Pin = D2; char *relayHoldTime_ms_String = "200"; char *relayMinOffTime_ms_String = "100"; unsigned int displayRefresh_ms = 200; // Clock Display Config Parameter static char * displayClockNameEvery_ms_String = "16000"; static char * displayClockNameDuration_ms_String = "1200"; static uint32_t displayClockNameEvery_ms = 16000; static uint32_t displayClockNameDuration_ms = 1200; static uint16_t doNotShowClockNameBeforeAndAfterMinuteChange_s = 2; static uint16_t hours, minutes, seconds; static char minuteProgressIndicator; static char timeBuffer[10]; void setupIAS(void) { #if defined ESP8266 // creat a unique deviceName for classroom situations (deviceName-123) chipId = String(ESP.getChipId()); chipId = "-"+chipId.substring(chipId.length()-3); deviceName += chipId; #endif // preset deviceName this is also your MDNS responder: http://deviceName.local IAS.preSetDeviceName(deviceName); IAS.preSetAppName(F("Wemos2RelaysMatrixDisplays")); IAS.preSetAppVersion(F(APP_VERSION)); IAS.preSetAutoUpdate(true); // define fields IAS.addField(clockName, "Clock Name", 8, 'T'); IAS.addField(clockSpeed_modelMsPerRealSec_String, "Model MilliSec per Real Sec", 8, 'N'); IAS.addField(displayClockNameEvery_ms_String, "Display clock name every (ms)", 5, 'N'); IAS.addField(displayClockNameDuration_ms_String, "Display clock name duration (ms)", 5, 'N'); IAS.addField(relay1Pin_String, "Pin Relay 1", 2, 'P'); IAS.addField(relay2Pin_String, "Pin Relay 2", 2, 'P'); IAS.addField(relayHoldTime_ms_String, "Relay hold time (ms)", 3, 'N'); IAS.addField(relayMinOffTime_ms_String, "Relay min off time (ms)", 3, 'N'); IAS.onModeButtonShortPress([]() { Serial.println(F(" If mode button is released, I will enter firmware update mode.")); Serial.println(F("*----------------------------------------------------------------------*")); P.print("|updt"); }); IAS.onModeButtonLongPress([]() { Serial.println(F(" If mode button is released, I will enter configuration mode.")); Serial.println(F("*----------------------------------------------------------------------*")); P.print("|cfg"); }); IAS.onFirstBoot([]() { Serial.println(F(" Manual reset necessary after serial upload!")); Serial.println(F("*----------------------------------------------------------------------*")); P.print("|rst"); ESP.reset(); }); IAS.onConfigMode([]() { P.print(" WiFi"); delay(400); P.print(":" + chipId); Serial.print(F("Entered config mode for Wifi, device=")); Serial.println(chipId); }); IAS.onFirmwareUpdateCheck([]() { // P.print("chk upd"); Serial.println(F("Firmware update check")); }); IAS.onFirmwareUpdateDownload([]() { P.print("dl+instl"); Serial.println(F("Download and install new firmware")); }); IAS.onFirmwareUpdateError([]() { // P.print("Err fwu"); Serial.println(F("Firmware update error")); }); // Optional parameter: What to do with EEPROM on First boot of the app? // 'F' Fully erase | 'P' Partial erase(default) | 'L' Leave intact IAS.begin('L'); delay(500); // Set to true to enable calling home frequently (disabled by default) IAS.setCallHome(true); // Call home interval in seconds, use 60s only for development. // Please change it to at least 2 hours in production IAS.setCallHomeInterval(120); IAS.callHome(true /*SPIFFS-check*/); clockSpeed_modelMsPerRealSec = atoi(clockSpeed_modelMsPerRealSec_String); relay1Pin = IAS.dPinConv(relay1Pin_String); relay2Pin = IAS.dPinConv(relay2Pin_String); R.setHoldTime_ms(atoi(relayHoldTime_ms_String)); R.setMinOffTime_ms(atoi(relayMinOffTime_ms_String)); displayClockNameEvery_ms = atoi(displayClockNameEvery_ms_String); displayClockNameDuration_ms = atoi(displayClockNameDuration_ms_String); Serial.println(F("Configuration used:")); Serial.print(F("Relay1 Pin: ")); Serial.println(relay1Pin); Serial.print(F("Relay2 Pin: ")); Serial.println(relay2Pin); Serial.print(F("Clock speed: ")); Serial.print(clockSpeed_modelMsPerRealSec); Serial.println(F(" model ms per real sec")); Serial.print(F("Relay hold time (ms): ")); Serial.println(relayHoldTime_ms_String); Serial.print(F("Relay min off time (ms): ")); Serial.println(relayMinOffTime_ms_String); Serial.print(F("Clock speed (model ms per real time s): ")); Serial.println(clockSpeed_modelMsPerRealSec); Serial.print(F("Show clock name every (ms): ")); Serial.println(displayClockNameEvery_ms); Serial.print(F("Show clock name for (ms): ")); Serial.println(displayClockNameDuration_ms); } static MD_MAX72XX *graphicDisplay = NULL; void setupDisplay() { int charCode; #if VERTICAL_BAR_STARTS_TOP static uint8_t verticalBarFont[] = { 1, 0x00, /* blank */ 1, 0x01, /* 1 dot */ 1, 0x03, /* 2 dots */ 1, 0x07, 1, 0x0f, 1, 0x1f, 1, 0x3f, 1, 0x7f, 1, 0xff, /* vertical bar completely set */ }; // columns from right to left, each byte is a single column #else static uint8_t verticalBarFont[] = { 1, 0x00, /* blank */ 1, 0x80, /* 1 dot */ 1, 0xc0, /* 2 dots */ 1, 0xe0, 1, 0xf0, 1, 0xf8, 1, 0xfc, 1, 0xfe, 1, 0xff, /* vertical bar completely set */ }; // columns from right to left, each byte is a single column #endif static uint8_t newZero[] = {0x05, 0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00}; P.begin(); // P.setZoneEffect(0, true, PA_FLIP_LR); graphicDisplay = P.getGraphicObject(); E.begin(graphicDisplay); P.setIntensity(1); for (charCode=1; charCode<=9; ++charCode) { P.addChar(charCode, verticalBarFont+2*(charCode-1)); } char intro[] = {':', '-', ')', ' ', 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00}; // replace the 0 characters, we do not like the "slash" P.addChar('0', newZero); P.print(intro); } //called when the url is not defined here return 404 void onRequest(AsyncWebServerRequest *request){ //Handle Unknown Request request->send(404); } void setupFS() { if(!SPIFFS.begin()){ Serial.println(F(" SPIFFS Mount Failed")); return; } } void setupWebServer() { server.on("/fwd", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println(F("\n WebApp button pressed -- move clock forward")); R.toggle(); // create json return String json = "{"; json += "\"result\":\"OK\","; json += "\"clockName\":\"" + String(clockName) + "\","; json += "\"hours\":\"" + String(hours) + "\","; json += "\"minutes\":\"" + String(minutes) + "\","; json += "\"seconds\":\"" + String(seconds) + "\""; json += "}"; // return json to WebApp request->send(200, F("text/json"), json); json = String(); }); server.on("/clock", HTTP_GET, [](AsyncWebServerRequest *request){ // create json return String json = "{"; json += "\"clockName\":\""+String(clockName)+"\","; json += "\"clockSpeed\":\""+String(clockSpeed_modelMsPerRealSec)+"\","; json += "\"relayHoldTime_ms\":\""+String(R.getHoldTime_ms())+"\","; json += "\"relayMinOffTime_ms\":\""+String(R.getMinOffTime_ms())+"\","; json += "\"displayRefresh_ms\":\""+String(displayRefresh_ms)+"\","; json += "\"displayClockNameEvery_ms\":\""+String(displayClockNameEvery_ms)+"\","; json += "\"displayClockNameDuration_ms\":\""+String(displayClockNameDuration_ms)+"\","; json += "\"doNotShowClockNameBeforeAndAfterMinuteChange_s\":\""+String(doNotShowClockNameBeforeAndAfterMinuteChange_s)+"\","; json += "\"real_hours\":\""+String(hours)+"\","; json += "\"real_minutes\":\""+String(minutes)+"\","; json += "\"real_seconds\":\""+String(seconds)+"\","; json += "\"model_hours\":\""+String(hours)+"\","; json += "\"model_minutes\":\""+String(minutes)+"\","; json += "\"model_seconds\":\""+String(seconds)+"\""; json += "}"; // return json to WebApp request->send(200, F("text/json"), json); json = String(); }); server.on("/setDT", HTTP_GET, [](AsyncWebServerRequest *request){ String h, m, message; Serial.println(F("\n Setting displayed time of clock")); message = ""; if (request->hasParam("h")) { h = request->getParam("h")->value(); } else { message += "Parameter h for hours missing. "; } if (request->hasParam("m")) { m = request->getParam("m")->value(); } else { message += "Parameter m for minutes missing. "; } R.setDisplayedTime(h.toInt(), m.toInt()); h = String(); m = String(); // create json return String json = "{"; if (message.length() > 0) { json += "\"result\":\"Error\","; json += "\"message\": \"" + message + "\""; } else { json += "\"result\":\"OK\""; } json += "}"; // return json to WebApp request->send(200, F("text/json"), json); json = String(); R.fwdToTime(hours, minutes); }); server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ Serial.println(F("\n Directory of FS requested")); FSInfo fs_info; String message = ""; if (!SPIFFS.info(fs_info)) { message += "Cannot get info about file system! "; } // create json return String json = "{"; if (message.length() > 0) { json += "\"result\":\"Error\","; json += "\"message\":\"" + message + "\""; } else { json += "\"result\":\"OK\","; json += "\"files\":\"["; Dir dir = SPIFFS.openDir("/"); boolean isFirstEntry = true; while (dir.next()) { if (isFirstEntry) { isFirstEntry = false; } else { json += ","; } json += "{\"filename\":\"" + dir.fileName() + "\",\"size\":" + dir.fileSize() + "}"; } json += "]"; } json += "}"; // return json to WebApp request->send(200, F("text/json"), json); json = String(); }); server.serveStatic("/", SPIFFS, "/"); server.onNotFound(onRequest); // start the HTTP server server.begin(); Serial.print(F("HTTP server started at: ")); Serial.println(WiFi.localIP()); Serial.println(""); } void setup(void) { Serial.println(F("setup():")); setupDisplay(); setupFS(); setupIAS(); setupWebServer(); delay(200); R.begin(relay1Pin, relay2Pin); timeClient.begin(); Serial.println(F("setup() finished")); } static bool timeClientInitialized = false; static unsigned long lastTimeOutput_ms = 0; #define TIME_BETWEEN_REALTIME_UPDATE_ms 60000 typedef struct { char name[7]; MD_RobotEyes::emotion_t e; uint16_t timePause; // in milliseconds } sampleItem_t; static const sampleItem_t eSeq[] = { { "Nutral", MD_RobotEyes::E_NEUTRAL, 1000 }, { "Blink" , MD_RobotEyes::E_BLINK, 1000 }, { "Wink" , MD_RobotEyes::E_WINK, 1000 }, { "Left" , MD_RobotEyes::E_LOOK_L, 1000 }, { "Right" , MD_RobotEyes::E_LOOK_R, 1000 }, { "Up" , MD_RobotEyes::E_LOOK_U, 1000 }, { "Down" , MD_RobotEyes::E_LOOK_D, 1000 }, { "Angry" , MD_RobotEyes::E_ANGRY, 1000 }, { "Sad" , MD_RobotEyes::E_SAD, 1000 }, { "Evil" , MD_RobotEyes::E_EVIL, 1000 }, { "Evil2" , MD_RobotEyes::E_EVIL2, 1000 }, { "Squint", MD_RobotEyes::E_SQUINT, 1000 }, { "Dead" , MD_RobotEyes::E_DEAD, 1000 }, { "ScanV" , MD_RobotEyes::E_SCAN_UD, 1000 }, { "ScanH" , MD_RobotEyes::E_SCAN_LR, 1000 }, }; #define DISPLAY_ANIM_NAME false void loopStartupAnimation() { // show startup animation boolean animationFinished = false; static uint32_t timeStartDelay; static uint8_t index = ARRAY_SIZE(eSeq); static enum { S_IDLE, S_TEXT, S_ANIM, S_PAUSE } state = S_IDLE; animationFinished = E.runAnimation(); switch (state) { case S_IDLE: index++; if (index >= ARRAY_SIZE(eSeq)) index = 0; P.displayClear(); #if DISPLAY_ANIM_NAME E.setText(eSeq[index].name); #endif state = S_TEXT; break; case S_TEXT: // wait for the text to finish if (animationFinished) // text animation is finished { E.setAnimation(eSeq[index].e, true); state = S_ANIM; } break; case S_ANIM: // checking animation is completed if (animationFinished) // animation is finished { timeStartDelay = millis(); state = S_PAUSE; } break; case S_PAUSE: // non blocking waiting for a period between animations if (millis() - timeStartDelay >= eSeq[index].timePause) state = S_IDLE; break; default: state = S_IDLE; break; } } void reInitializeDisplay() { static unsigned long last_reinit_ts = 0; #define REINIT_AFTER_ms 5000 #define AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s 3 if (last_reinit_ts == 0) last_reinit_ts = millis(); if (millis() - last_reinit_ts > REINIT_AFTER_ms && seconds < 60 - AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s && seconds > AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s) { P.begin(); last_reinit_ts = millis(); } } void loop(void) { int currentDisplayState; static int lastMinutes = 0; static int lastSeconds = 0; #define MsgSize 10 static char debugMsg[MsgSize+1]; static int recentDisplayState = -1; static unsigned long firstLoop_ts = 0; if (firstLoop_ts == 0) firstLoop_ts = millis(); if (!timeClientInitialized && WiFi.status() == WL_CONNECTED) { timeClient.begin(); timeClientInitialized = true; timeClient.update(); Serial.println(timeClient.getFormattedTime()); lastTimeOutput_ms = millis(); } IAS.loop(); if (timeClientInitialized && millis()-lastTimeOutput_ms > TIME_BETWEEN_REALTIME_UPDATE_ms) { timeClient.update(); Serial.println(timeClient.getFormattedTime()); lastTimeOutput_ms = millis(); } if (millis() < firstLoop_ts + STARTUP1_ANIMATION_DURATION_ms) { // Startup phase 1: Constant display of content, created during setup() return; } if (millis() < firstLoop_ts + STARTUP1_ANIMATION_DURATION_ms + STARTUP2_ANIMATION_DURATION_ms) { loopStartupAnimation(); return; } if (timeClientInitialized) { hours = timeClient.getHours(); minutes = timeClient.getMinutes(); seconds = timeClient.getSeconds(); } else { hours = (millis() / 60 * 60 * 1000) % 24; minutes = (millis() / 60 * 1000) % 60; seconds = (millis() / 1000) % 60; } minuteProgressIndicator = seconds/6.7 + 1; // char code 1-8 show vertical bar if (minuteProgressIndicator > 9) minuteProgressIndicator = 9; snprintf(timeBuffer, 10, "%c %2d:%02d", minuteProgressIndicator, hours, minutes); // standard procedure to display static uint32_t last_clock_refresh = 0; static uint32_t lastTimeClockNameShown = 0; static boolean showingClockName = false; if (showingClockName) { if (millis() - lastTimeClockNameShown > displayClockNameDuration_ms) { // stop showingClockName showingClockName = false; } } else { if ((millis() - lastTimeClockNameShown > displayClockNameEvery_ms) && (seconds < 60-doNotShowClockNameBeforeAndAfterMinuteChange_s) && (seconds > doNotShowClockNameBeforeAndAfterMinuteChange_s)) { //P.begin(); // re-initialize, that fixes display problems due to electrical relais feedbacks reInitializeDisplay(); P.setIntensity(2); P.print(clockName); lastTimeClockNameShown = millis(); showingClockName = true; } else { // showing clock if (millis() - last_clock_refresh > displayRefresh_ms) { // P.begin(); // re-initialize, that fixes display problems due to electrical relais feedbacks reInitializeDisplay(); P.setIntensity(1); P.print(timeBuffer); last_clock_refresh = millis(); } } } // toggle relays if (lastMinutes != minutes) { R.toggle(); lastMinutes = minutes; } lastSeconds = seconds; R.loop(); }