From 8e0f2df9b1bb0ef4941fad2d04c964a4369d1c51 Mon Sep 17 00:00:00 2001 From: Dirk Jahnke Date: Mon, 28 Jan 2019 12:22:08 +0100 Subject: [PATCH] Added robot eye animation. Made some cleanups. --- src/MD_RobotEyes.cpp | 274 +++++++++++++++++++++++++++ src/MD_RobotEyes.h | 214 +++++++++++++++++++++ src/MD_RobotEyes_Data.h | 406 ++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 193 ++++++++++++------- 4 files changed, 1020 insertions(+), 67 deletions(-) create mode 100644 src/MD_RobotEyes.cpp create mode 100644 src/MD_RobotEyes.h create mode 100644 src/MD_RobotEyes_Data.h diff --git a/src/MD_RobotEyes.cpp b/src/MD_RobotEyes.cpp new file mode 100644 index 0000000..edfa844 --- /dev/null +++ b/src/MD_RobotEyes.cpp @@ -0,0 +1,274 @@ +#include "MD_RobotEyes.h" +#include "MD_RobotEyes_Data.h" + +// Debugging macros +#define DEBUG 0 + +#if DEBUG +#define PRINTS(s) { Serial.print(F(s)); } +#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); } +#define PRINTX(s, v) { Serial.print(F(s)); Serial.print(F("0x")); Serial.print(v, HEX); } +#else +#define PRINTS(s) +#define PRINT(s, v) +#define PRINTX(s, v) +#endif + +MD_RobotEyes::MD_RobotEyes(void) : +_nextEmotion(E_NEUTRAL), _animState(S_IDLE), +_autoBlink(true), _timeBlinkMinimum(5000) +{ +}; + +void MD_RobotEyes::loadEye(uint8_t module, uint8_t ch) +{ + uint8_t buf[EYE_COL_SIZE]; + uint8_t size; + + size = _M->getChar(ch, EYE_COL_SIZE, buf); + + for (uint8_t i = 0; i < EYE_COL_SIZE; i++) + { + _M->setColumn(module, i, buf[i]); + } +} + +void MD_RobotEyes::drawEyes(uint8_t L, uint8_t R) +// Draw the left and right eyes +{ + MD_MAX72XX::fontType_t *savedFont = _M->getFont(); + + _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF); + _M->setFont(_RobotEyes_Font); + + _M->clear(_sd, _sd+1); // clear out display modules + + // Load the data and show it + loadEye(_sd+LEFT_MODULE_OFFSET, L); + loadEye(_sd+RIGHT_MODULE_OFFSET, R); + + _M->setFont(savedFont); + _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON); +} + +#if DEBUG +void MD_RobotEyes::dumpSequence(const animFrame_t* pBuf, uint8_t numElements) +// Debugging routine to display an animation table in PROGMEM +{ + for (uint8_t i = 0; i < numElements; i++) + { + animFrame_t f; + + //memcpy_P(&f, &pBuf[i], sizeof(animFrame_t)); + memcpy(&f, &pBuf[i], sizeof(animFrame_t)); + PRINT("\n[", i); + PRINT("]: L:", f.eyeData[LEFT_EYE_INDEX]); + PRINT(" R:", f.eyeData[RIGHT_EYE_INDEX]); + PRINT(" T:", f.timeFrame); + } +} +#endif + +uint8_t MD_RobotEyes::loadSequence(emotion_t e) +// Load the next emotion from the static data. +// Set global variables to the required values +{ + // run through the lookup table to find the sequence data + for (uint8_t i = 0; i < ARRAY_SIZE(lookupTable); i++) + { + //memcpy_P(&_animEntry, &lookupTable[i], sizeof(animTable_t)); + memcpy(&_animEntry, &lookupTable[i], sizeof(animTable_t)); + if (_animEntry.e == e) + { +#if DEBUG + dumpSequence(_animEntry.seq, _animEntry.size); +#endif + break; + } + } + + // set up the current index depending on direction of animation + if (_animReverse) _animIndex = _animEntry.size - 1; else _animIndex = 0; + + return(_animEntry.size); +} + +void MD_RobotEyes::loadFrame(animFrame_t* pBuf) +// Load the idx'th frame from the frame sequence PROGMEM to normal memory pBuf +{ + //memcpy_P(pBuf, &_animEntry.seq[_animIndex], sizeof(animFrame_t)); + memcpy(pBuf, &_animEntry.seq[_animIndex], sizeof(animFrame_t)); +} + +void MD_RobotEyes::showText(bool bInit) +// Print the text string to the LED matrix modules specified. +// Message area is padded with blank columns after printing. +{ + static enum { S_LOAD, S_SHOW, S_SPACE } state; + static uint8_t curLen, showLen; + static uint8_t cBuf[EYE_COL_SIZE]; + + if (bInit) + { + PRINT("\nText: ", _pText); + _timeLastAnimation = millis(); + _M->clear(_sd, _sd + 1); + state = S_LOAD; + } + + // Is it time to scroll the text? + if (millis() - _timeLastAnimation < FRAME_TIME/2) + return; + + _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF); + + // Now scroll the text + _M->transform(_sd, _sd+1, MD_MAX72XX::TSL); // scroll along by one place + _timeLastAnimation = millis(); // starting time for next scroll + + // Now work out what's next using finite state machine to control what we do + switch (state) + { + case S_LOAD: // Load the next character from the font table + // if we reached end of message or empty string, reset the message pointer + if (*_pText == '\0') + { + _pText = nullptr; + break; + } + + // otherwise load the character + showLen = _M->getChar(*_pText++, ARRAY_SIZE(cBuf), cBuf); + curLen = 0; + state = S_SHOW; + // fall through to the next state + + case S_SHOW: // display the next part of the character + _M->setColumn(_sd, 0, cBuf[curLen++]); + if (curLen == showLen) + { + showLen = (*_pText == '\0' ? 2*EYE_COL_SIZE : 1); // either 1 space or pad to the end of the display if finished + curLen = 0; + state = S_SPACE; + } + break; + + case S_SPACE: // display inter-character spacing (blank columns) + _M->setColumn(_sd, 0, 0); + curLen++; + if (curLen >= showLen) + state = S_LOAD; + break; + + default: + state = S_LOAD; + } + + _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON); +} + +void MD_RobotEyes::begin(MD_MAX72XX *M, uint8_t moduleStart) +// initialize other stuff after libraries have started +{ +#if DEBUG + Serial.begin(57600); +#endif + PRINTS("\n[MD_RobotEyes Debug]"); + + _M = M; + _sd = moduleStart; + + setAnimation(E_NEUTRAL, false); +}; + +bool MD_RobotEyes::runAnimation(void) +// Animate the eyes +// Return true if there is no animation happening +{ + static animFrame_t thisFrame; + + switch (_animState) + { + case S_IDLE: // no animation running - wait for a new one or blink if time to do so + if (_pText != nullptr) // there is some text to show + { + PRINTS("\nIDLE: showing text"); + showText(true); + _animState = S_TEXT; + break; + } + // otherwise fall through and try for an animation + + case S_RESTART: // back to start of current animation + if (_nextEmotion != E_NONE) // check if we have an animation in the queue + { + PRINTS("\nRESRT: showing animation"); + _timeLastAnimation = millis(); + + // set up the next animation + loadSequence(_nextEmotion); + _nextEmotion = E_NONE; + _animState = S_ANIMATE; + } + else if (_autoBlink) // check if we should be blinking + { + if (((millis() - _timeLastAnimation) >= _timeBlinkMinimum) && (random(1000) > 700)) + { + PRINTS("\nRESRT: forcing blink"); + setAnimation(E_BLINK, true); + _animState = S_RESTART; + } + } + break; + + case S_ANIMATE: // process the next frame for this sequence + PRINT("\nPROCESS: Frame:", _animIndex); + loadFrame(&thisFrame); + drawEyes(thisFrame.eyeData[LEFT_EYE_INDEX], thisFrame.eyeData[RIGHT_EYE_INDEX]); + if (_animReverse) _animIndex--; else _animIndex++; + + _timeStartPause = millis(); + _animState = S_PAUSE; + break; + + case S_PAUSE: // pause this frame for the required time + { + if ((millis() - _timeStartPause) < thisFrame.timeFrame) + break; + + // check if this is the end of animation + if ((!_animReverse && _animIndex >= _animEntry.size) || + (_animReverse && _animIndex < 0)) + { + PRINTS("\nPAUSE: Animation end") + if (_autoReverse) // set up the same emotion but in reverse + { + PRINTS(" & auto reverse"); + _nextEmotion = _animEntry.e; + _animReverse = true; // set this flag for the restart state + _autoReverse = false; // clear the flag for this animation sequence + _animState = S_RESTART; + } + else + _animState = S_IDLE; + } + else + _animState = S_ANIMATE; + } + break; + + case S_TEXT: // currently displaying text + { + showText(); + if (_pText == nullptr) + _animState = S_IDLE; + } + break; + + default: // something is wrong - reset the FSM + _animState = S_IDLE; + break; + } + + return(_animState == S_IDLE); +}; diff --git a/src/MD_RobotEyes.h b/src/MD_RobotEyes.h new file mode 100644 index 0000000..49a2ca0 --- /dev/null +++ b/src/MD_RobotEyes.h @@ -0,0 +1,214 @@ +// Implements a class to draw and animate a pair of 'emotive' eyes for a robot +// +#pragma once + +#include + +// Misc defines +#if !defined(ARRAY_SIZE) +#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) ///< number of elements in an array +#endif + +#define EYE_COL_SIZE 8 ///< number of columns in one eye + +// Module offsets from first module specified +#define LEFT_MODULE_OFFSET 1 ///< offset from the base LED module for the left eye +#define RIGHT_MODULE_OFFSET 0 ///< offset from the base LED module for the right eye + +// Array references for eyeData array below +#define LEFT_EYE_INDEX 1 ///< array reference in the eye data for the left eye +#define RIGHT_EYE_INDEX 0 ///< array reference in the eye data for the right eye + +// Basic unit of time a frame is displayed +#define FRAME_TIME 100 ///< minimum animation time + +/** +* Robot Eyes Class. +* This class manages the displayed of animated eyes using LED matrices using the functions +* provided by the MD_MAX72xx library. +*/ +class MD_RobotEyes +{ +public: + /** + * Emotions enumerated type. + * + * This enumerated type defines the emotion animations + * available in the class for the eyes display + */ + // + typedef enum + { + E_NONE, ///< placeholder for no emotions, not user selectable + E_NEUTRAL, ///< eyes in neutral position (no animation) + E_BLINK, ///< both eyes blink + E_WINK, ///< one eye blink + E_LOOK_L, ///< both eyes look left + E_LOOK_R, ///< both eyes look right + E_LOOK_U, ///< both eyes look up + E_LOOK_D, ///< both eyes look down + E_ANGRY, ///< eyes look angry (symmetrical) + E_SAD, ///< eyes look sad (symmetrical) + E_EVIL, ///< eyes look evil (symmetrical) + E_EVIL2, ///< eyes look evil (asymmetrical) + E_SQUINT, ///< both eye squint + E_DEAD, ///< eyes indicate dead (different) + E_SCAN_UD, ///< both eyes scanning Up/Down + E_SCAN_LR, ///< both eyes scanning Left/Right + } emotion_t; + + /** + * Class Constructor. + * + * Instantiate a new instance of the class. + */ + MD_RobotEyes(void); + + /** + * Class Destructor. + * + * Released any allocated memory and does the necessary to clean + * up once the object is no longer required. + */ + ~MD_RobotEyes(void) { }; + + /** + * Initialize the object. + * + * Initialize the object data. This needs to be called during setup() to initialize new + * data for the class that cannot be done during the object creation. + * + * Outside of the class, the MD_MAX72xx library should be initialized and the pointer + * to the MD_MAX72xx object passed to the parameter. Also, as the eyes could be in the + * middle of a string of LED modules, the first 'eye' module can be specified. + * + * /param M pointer to the MD_MAX72xx library object. + * /param moduleStart the first 'eye' LED module. Defaults to 0 if not specified. + */ + void begin(MD_MAX72XX *M, uint8_t moduleStart = 0); + + /** + * Set the animation type and parameters. + * + * Set the next animations to the specified. Additionally, set whether the animation should + * auto reverse the action (eg, blink down then back up again) and whether the animation + * should be run in reverse. + * + * Animations are generally symmetric, so only half the animation needs to be specified. + * If an animated expression needs to be held, the animation should be run without auto + * reverse, which holds the animation at the end point, and then later run the animation + * in reverse from the last position to return to the idle state. + * + * \param e the type of emotion to be displayed, one of the emotion_T enumerated values. + * \param r if true, run auto reverse. + * \param b if true, start the animation from the end of the sequence. + */ + inline void setAnimation(emotion_t e, bool r, bool b = false) { _nextEmotion = e; _autoReverse = r; _animReverse = b; }; + + /** + * Set the blink time. + * + * When no animation is running and AutoBlink is set, the eyes will occasionally blink. + * Set the minimum time period between blinks. A blink will occur a random time after this. + * + * \param t the minimum time between blinking actions in milliseconds. + */ + inline void setBlinkTime(uint16_t t) { _timeBlinkMinimum = t; }; + + /** + * Set or reset auto blink mode. + * + * When no animation is running and AutoBlink is set, the eyes will occasionally blink. + * + * \param b set auto blink if true, reset auto blink if false. + */ + inline void setAutoBlink(bool b) { _autoBlink = b; }; + + /** + * Display a text message. + * + * At the end of the current animation, the text will be scrolled across the 'eyes' + * and then the eyes are returned to the neutral expression + * + * \param p a pointer to a char array containing a nul terminated string. + The string must remain in scope while the message is being displayed. + */ + inline bool setText(const char *pText) { if (_pText != nullptr) return(false); else _pText = pText; return(true); }; + + /** + * Animate the display. + * + * This method needs to be invoked as often as possible to ensure smooth animation. + * + * The calling program should monitor the return value for 'true' in order to know when + * the animation has concluded. A 'true' return value means that the animation is complete. + * + * \return bool true if the animation has completed, false otherwise. + */ + bool runAnimation(void); + +protected: + // Animations FSM state + typedef enum + { + S_IDLE, + S_RESTART, + S_ANIMATE, + S_PAUSE, + S_TEXT, + } animState_t; + + // Define an animation frame + typedef struct + { + uint8_t eyeData[2]; // [LEFT_MODULE_OFFSET] and [RIGHT_MODULE_OFFSET] eye character from font data + uint16_t timeFrame; // time for this frame in milliseconds + } animFrame_t; + + // Define an entry in the animation sequence lookup table + typedef struct + { + emotion_t e; + const animFrame_t *seq; + uint8_t size; + } animTable_t; + + // Display parameters + MD_MAX72XX *_M; + uint16_t _sd; // start module for the display + + // Animation parameters + uint32_t _timeStartPause; + uint32_t _timeLastAnimation; + uint16_t _timeBlinkMinimum; + animState_t _animState; + bool _autoBlink; + uint16_t _scrollDelay; + + // Animation control data + animTable_t _animEntry; // record with animation sequence parameters + int8_t _animIndex; // current index in the animation sequence + bool _animReverse; // true = reverse sequence, false = normal sequence + bool _autoReverse; // true = always play the reverse, false = selected direction only + emotion_t _nextEmotion; // the next emotion to display + const char * _pText; // pointer to text data in user code. Not null means there is text to print + + // Methods + void loadEye(uint8_t module, uint8_t ch); + void drawEyes(uint8_t L, uint8_t R); + uint8_t loadSequence(emotion_t e); // return the size of the sequence + void loadFrame(animFrame_t* pBuf); + void showText(bool bInit = false); + + void dumpSequence(const animFrame_t* pBuf, uint8_t numElements); // debugging routine only + + // Static data tables + static const animFrame_t seqBlink[], seqWink[]; + static const animFrame_t seqLeft[], seqRight[], seqUp[], seqDown[]; + static const animFrame_t seqAngry[], seqSad[], seqEvil[], seqEvil2[]; + static const animFrame_t seqSquint[], seqDead[]; + static const animFrame_t seqScanUpDown[], seqScanLeftRight[]; + + // Lookup table to find animation + static const animTable_t lookupTable[]; +}; diff --git a/src/MD_RobotEyes_Data.h b/src/MD_RobotEyes_Data.h new file mode 100644 index 0000000..2fbc6c2 --- /dev/null +++ b/src/MD_RobotEyes_Data.h @@ -0,0 +1,406 @@ +// EmotiveEye class static variables +#pragma once + +#include "MD_RobotEyes.h" + +// Sequences for animations +// Note: must add this to the lookupTable below as well so that the animation +// can be found by the animation engine. +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqBlink[] /*PROGMEM*/ = +{ + { { 0, 0 }, FRAME_TIME/2 }, + { { 1, 1 }, FRAME_TIME/2 }, + { { 2, 2 }, FRAME_TIME/2 }, + { { 3, 3 }, FRAME_TIME/2 }, + { { 4, 4 }, FRAME_TIME/2 }, + { { 5, 5 }, FRAME_TIME }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqWink[] /*PROGMEM*/ = +{ + { { 0, 0 }, FRAME_TIME/2 }, + { { 1, 0 }, FRAME_TIME/2 }, + { { 2, 0 }, FRAME_TIME/2 }, + { { 3, 0 }, FRAME_TIME/2 }, + { { 4, 0 }, FRAME_TIME/2 }, + { { 5, 0 }, FRAME_TIME * 2 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqRight[] /*PROGMEM*/ = +{ + { { 0, 0 }, FRAME_TIME }, + { { 6, 6 }, FRAME_TIME }, + { { 7, 7 }, FRAME_TIME * 5 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqLeft[] /*PROGMEM*/ = +{ + { { 0, 0 }, FRAME_TIME }, + { { 8, 8 }, FRAME_TIME }, + { { 9, 9 }, FRAME_TIME * 5 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqUp[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 11, 11 }, FRAME_TIME }, + { { 12, 12 }, FRAME_TIME }, + { { 13, 13 }, FRAME_TIME * 5 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqDown[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 14, 14 }, FRAME_TIME }, + { { 15, 15 }, FRAME_TIME }, + { { 16, 16 }, FRAME_TIME * 5 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqAngry[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 22, 17 }, FRAME_TIME }, + { { 23, 18 }, FRAME_TIME }, + { { 24, 19 }, FRAME_TIME }, + { { 25, 20 }, 2000 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqSad[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 32, 27 }, FRAME_TIME }, + { { 33, 28 }, FRAME_TIME }, + { { 34, 29 }, 2000 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqEvil[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 39, 37 }, FRAME_TIME }, + { { 40, 38 }, 2000 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqEvil2[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 54, 17 }, FRAME_TIME }, + { { 55, 18 }, FRAME_TIME }, + { { 56, 19 }, FRAME_TIME }, + { { 57, 20 }, 2000 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqSquint[] /*PROGMEM*/ = +{ + { { 00, 00 }, FRAME_TIME }, + { { 54, 54 }, FRAME_TIME }, + { { 55, 55 }, FRAME_TIME }, + { { 56, 56 }, FRAME_TIME }, + { { 57, 57 }, 2000 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqDead[] /*PROGMEM*/ = +{ + { { 52, 52 }, FRAME_TIME * 4 }, + { { 53, 53 }, FRAME_TIME * 4 }, + { { 52, 52 }, FRAME_TIME * 2 }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqScanLeftRight[] /*PROGMEM*/ = +{ + { { 41, 41 }, FRAME_TIME * 2 }, + { { 42, 42 }, FRAME_TIME }, + { { 43, 43 }, FRAME_TIME }, + { { 44, 44 }, FRAME_TIME }, +}; + +const MD_RobotEyes::animFrame_t MD_RobotEyes::seqScanUpDown[] /*PROGMEM*/ = +{ + { { 46, 46 }, FRAME_TIME * 2 }, + { { 47, 47 }, FRAME_TIME }, + { { 48, 48 }, FRAME_TIME }, + { { 49, 49 }, FRAME_TIME }, + { { 50, 50 }, FRAME_TIME }, + { { 51, 51 }, FRAME_TIME }, +}; + +// Lookup table to find animation sequences +// Table associates the data for an emotion with the sequence table and it's size +const MD_RobotEyes::animTable_t MD_RobotEyes::lookupTable[] /*PROGMEM*/ = +{ + { MD_RobotEyes::E_NEUTRAL, MD_RobotEyes::seqBlink, 1 }, // special case, fixed neutral stare + { MD_RobotEyes::E_BLINK, MD_RobotEyes::seqBlink, ARRAY_SIZE(MD_RobotEyes::seqBlink) }, + { MD_RobotEyes::E_WINK, MD_RobotEyes::seqWink, ARRAY_SIZE(MD_RobotEyes::seqWink) }, + { MD_RobotEyes::E_LOOK_L, MD_RobotEyes::seqLeft, ARRAY_SIZE(MD_RobotEyes::seqLeft) }, + { MD_RobotEyes::E_LOOK_R, MD_RobotEyes::seqRight, ARRAY_SIZE(MD_RobotEyes::seqRight) }, + { MD_RobotEyes::E_LOOK_U, MD_RobotEyes::seqUp, ARRAY_SIZE(MD_RobotEyes::seqUp) }, + { MD_RobotEyes::E_LOOK_D, MD_RobotEyes::seqDown, ARRAY_SIZE(MD_RobotEyes::seqDown) }, + { MD_RobotEyes::E_ANGRY, MD_RobotEyes::seqAngry, ARRAY_SIZE(MD_RobotEyes::seqAngry) }, + { MD_RobotEyes::E_SAD, MD_RobotEyes::seqSad, ARRAY_SIZE(MD_RobotEyes::seqSad) }, + { MD_RobotEyes::E_EVIL, MD_RobotEyes::seqEvil, ARRAY_SIZE(MD_RobotEyes::seqEvil) }, + { MD_RobotEyes::E_EVIL2, MD_RobotEyes::seqEvil2, ARRAY_SIZE(MD_RobotEyes::seqEvil2) }, + { MD_RobotEyes::E_SQUINT, MD_RobotEyes::seqSquint, ARRAY_SIZE(MD_RobotEyes::seqSquint) }, + { MD_RobotEyes::E_DEAD, MD_RobotEyes::seqDead, ARRAY_SIZE(MD_RobotEyes::seqDead) }, + { MD_RobotEyes::E_SCAN_LR, MD_RobotEyes::seqScanLeftRight, ARRAY_SIZE(MD_RobotEyes::seqScanLeftRight) }, + { MD_RobotEyes::E_SCAN_UD, MD_RobotEyes::seqScanUpDown, ARRAY_SIZE(MD_RobotEyes::seqScanUpDown) }, +}; + +// Font file (bitmaps for emotion animation frames) +MD_MAX72XX::fontType_t _RobotEyes_Font[] /*PROGMEM*/ = +{ + 8, 0, 126, 129, 177, 177, 129, 126, 0, // 0 - 'Rest Position' + 8, 0, 124, 130, 178, 178, 130, 124, 0, // 1 - 'Blink 1' + 8, 0, 120, 132, 180, 180, 132, 120, 0, // 2 - 'Blink 2' + 8, 0, 48, 72, 120, 120, 72, 48, 0, // 3 - 'Blink 3' + 8, 0, 32, 80, 112, 112, 80, 32, 0, // 4 - 'Blink 4' + 8, 0, 32, 96, 96, 96, 96, 32, 0, // 5 - 'Blink 5' + 8, 0, 126, 129, 129, 177, 177, 126, 0, // 6 - 'Right 1' + 8, 0, 0, 126, 129, 129, 177, 177, 126, // 7 - 'Right 2' + 8, 0, 126, 177, 177, 129, 129, 126, 0, // 8 - 'Left 1' + 8, 126, 177, 177, 129, 129, 126, 0, 0, // 9 - 'Left 2' + 0, // 10 + 8, 0, 126, 129, 153, 153, 129, 126, 0, // 11 - 'Up 1' + 8, 0, 126, 129, 141, 141, 129, 126, 0, // 12 - 'Up 2' + 8, 0, 126, 129, 135, 135, 129, 126, 0, // 13 - 'Up 3' + 8, 0, 126, 129, 225, 225, 129, 126, 0, // 14 - 'Down 1' + 8, 0, 126, 129, 193, 193, 129, 126, 0, // 15 - 'Down 2' + 8, 0, 124, 130, 194, 194, 130, 124, 0, // 16 - 'Down 3' + 8, 0, 124, 130, 177, 177, 129, 126, 0, // 17 - 'Angry L 1' + 8, 0, 120, 132, 178, 177, 129, 126, 0, // 18 - 'Angry L 2' + 8, 0, 112, 136, 164, 178, 129, 126, 0, // 19 - 'Angry L 3' + 8, 0, 96, 144, 168, 180, 130, 127, 0, // 20 - 'Angry L 4' + 0, // 21 + 8, 0, 126, 129, 177, 177, 130, 124, 0, // 22 - 'Angry R 1' + 8, 0, 126, 129, 177, 178, 132, 120, 0, // 23 - 'Angry R 2' + 8, 0, 126, 129, 178, 164, 136, 112, 0, // 24 - 'Angry R 3' + 8, 0, 127, 130, 180, 168, 144, 96, 0, // 25 - 'Angry R 4' + 0, // 26 + 8, 0, 62, 65, 153, 153, 130, 124, 0, // 27 - 'Sad L 1' + 8, 0, 30, 33, 89, 154, 132, 120, 0, // 28 - 'Sad L 2' + 8, 0, 14, 17, 41, 90, 132, 120, 0, // 29 - 'Sad L 3' + 0, // 30 + 0, // 31 + 8, 0, 124, 130, 153, 153, 65, 62, 0, // 32 - 'Sad R 1' + 8, 0, 120, 132, 154, 89, 33, 30, 0, // 33 - 'Sad R 2' + 8, 0, 120, 132, 90, 41, 17, 14, 0, // 34 - 'Sad R 3' + 0, // 35 + 0, // 36 + 8, 0, 124, 194, 177, 177, 193, 126, 0, // 37 - 'Evil L 1' + 8, 0, 56, 68, 178, 177, 66, 60, 0, // 38 - 'Evil L 2' + 8, 0, 126, 193, 177, 177, 194, 124, 0, // 39 - 'Evil R 1' + 8, 0, 60, 66, 177, 178, 68, 56, 0, // 40 - 'Evil R 2' + 8, 0, 126, 129, 129, 129, 189, 126, 0, // 41 - 'Scan H 1' + 8, 0, 126, 129, 129, 189, 129, 126, 0, // 42 - 'Scan H 2' + 8, 0, 126, 129, 189, 129, 129, 126, 0, // 43 - 'Scan H 3' + 8, 0, 126, 189, 129, 129, 129, 126, 0, // 44 - 'Scan H 4' + 0, // 45 + 8, 0, 126, 129, 131, 131, 129, 126, 0, // 46 - 'Scan V 1' + 8, 0, 126, 129, 133, 133, 129, 126, 0, // 47 - 'Scan V 2' + 8, 0, 126, 129, 137, 137, 129, 126, 0, // 48 - 'Scan V 3' + 8, 0, 126, 129, 145, 145, 129, 126, 0, // 49 - 'Scan V 4' + 8, 0, 126, 129, 161, 161, 129, 126, 0, // 50 - 'Scan V 5' + 8, 0, 126, 129, 193, 193, 129, 126, 0, // 51 - 'Scan V 6' + 8, 0, 126, 137, 157, 137, 129, 126, 0, // 52 - 'RIP 1' + 8, 0, 126, 129, 145, 185, 145, 126, 0, // 53 - 'RIP 2' + 8, 0, 60, 66, 114, 114, 66, 60, 0, // 54 - 'Peering 1' + 8, 0, 56, 68, 116, 116, 68, 56, 0, // 55 - 'Peering 2' + 8, 0, 48, 72, 120, 120, 72, 48, 0, // 56 - 'Peering 3' + 8, 0, 32, 80, 112, 112, 80, 32, 0, // 57 - 'Peering 4' + 0, // 58 + 0, // 59 - 'Unused' + 0, // 60 - 'Unused' + 0, // 61 - 'Unused' + 0, // 62 - 'Unused' + 0, // 63 - 'Unused' + 0, // 64 - 'Unused' + 0, // 65 - 'Unused' + 0, // 66 - 'Unused' + 0, // 67 - 'Unused' + 0, // 68 - 'Unused' + 0, // 69 - 'Unused' + 0, // 70 - 'Unused' + 0, // 71 - 'Unused' + 0, // 72 - 'Unused' + 0, // 73 - 'Unused' + 0, // 74 - 'Unused' + 0, // 75 - 'Unused' + 0, // 76 - 'Unused' + 0, // 77 - 'Unused' + 0, // 78 - 'Unused' + 0, // 79 - 'Unused' + 0, // 80 - 'Unused' + 0, // 81 - 'Unused' + 0, // 82 - 'Unused' + 0, // 83 - 'Unused' + 0, // 84 - 'Unused' + 0, // 85 - 'Unused' + 0, // 86 - 'Unused' + 0, // 87 - 'Unused' + 0, // 88 - 'Unused' + 0, // 89 - 'Unused' + 0, // 90 - 'Unused' + 0, // 91 - 'Unused' + 0, // 92 - 'Unused' + 0, // 93 - 'Unused' + 0, // 94 - 'Unused' + 0, // 95 - 'Unused' + 0, // 96 - 'Unused' + 0, // 97 - 'Unused' + 0, // 98 - 'Unused' + 0, // 99 - 'Unused' + 0, // 100 - 'Unused' + 0, // 101 - 'Unused' + 0, // 102 - 'Unused' + 0, // 103 - 'Unused' + 0, // 104 - 'Unused' + 0, // 105 - 'Unused' + 0, // 106 - 'Unused' + 0, // 107 - 'Unused' + 0, // 108 - 'Unused' + 0, // 109 - 'Unused' + 0, // 110 - 'Unused' + 0, // 111 - 'Unused' + 0, // 112 - 'Unused' + 0, // 113 - 'Unused' + 0, // 114 - 'Unused' + 0, // 115 - 'Unused' + 0, // 116 - 'Unused' + 0, // 117 - 'Unused' + 0, // 118 - 'Unused' + 0, // 119 - 'Unused' + 0, // 120 - 'Unused' + 0, // 121 - 'Unused' + 0, // 122 - 'Unused' + 0, // 123 - 'Unused' + 0, // 124 - 'Unused' + 0, // 125 - 'Unused' + 0, // 126 - 'Unused' + 0, // 127 - 'Unused' + 0, // 128 - 'Unused' + 0, // 129 - 'Unused' + 0, // 130 - 'Unused' + 0, // 131 - 'Unused' + 0, // 132 - 'Unused' + 0, // 133 - 'Unused' + 0, // 134 - 'Unused' + 0, // 135 - 'Unused' + 0, // 136 - 'Unused' + 0, // 137 - 'Unused' + 0, // 138 - 'Unused' + 0, // 139 - 'Unused' + 0, // 140 - 'Unused' + 0, // 141 - 'Unused' + 0, // 142 - 'Unused' + 0, // 143 - 'Unused' + 0, // 144 - 'Unused' + 0, // 145 - 'Unused' + 0, // 146 - 'Unused' + 0, // 147 - 'Unused' + 0, // 148 - 'Unused' + 0, // 149 - 'Unused' + 0, // 150 - 'Unused' + 0, // 151 - 'Unused' + 0, // 152 - 'Unused' + 0, // 153 - 'Unused' + 0, // 154 - 'Unused' + 0, // 155 - 'Unused' + 0, // 156 - 'Unused' + 0, // 157 - 'Unused' + 0, // 158 - 'Unused' + 0, // 159 - 'Unused' + 0, // 160 - 'Unused' + 0, // 161 - 'Unused' + 0, // 162 - 'Unused' + 0, // 163 - 'Unused' + 0, // 164 - 'Unused' + 0, // 165 - 'Unused' + 0, // 166 - 'Unused' + 0, // 167 - 'Unused' + 0, // 168 - 'Unused' + 0, // 169 - 'Unused' + 0, // 170 - 'Unused' + 0, // 171 - 'Unused' + 0, // 172 - 'Unused' + 0, // 173 - 'Unused' + 0, // 174 - 'Unused' + 0, // 175 - 'Unused' + 0, // 176 - 'Unused' + 0, // 177 - 'Unused' + 0, // 178 - 'Unused' + 0, // 179 - 'Unused' + 0, // 180 - 'Unused' + 0, // 181 - 'Unused' + 0, // 182 - 'Unused' + 0, // 183 - 'Unused' + 0, // 184 - 'Unused' + 0, // 185 - 'Unused' + 0, // 186 - 'Unused' + 0, // 187 - 'Unused' + 0, // 188 - 'Unused' + 0, // 189 - 'Unused' + 0, // 190 - 'Unused' + 0, // 191 - 'Unused' + 0, // 192 - 'Unused' + 0, // 193 - 'Unused' + 0, // 194 - 'Unused' + 0, // 195 - 'Unused' + 0, // 196 - 'Unused' + 0, // 197 - 'Unused' + 0, // 198 - 'Unused' + 0, // 199 - 'Unused' + 0, // 200 - 'Unused' + 0, // 201 - 'Unused' + 0, // 202 - 'Unused' + 0, // 203 - 'Unused' + 0, // 204 - 'Unused' + 0, // 205 - 'Unused' + 0, // 206 - 'Unused' + 0, // 207 - 'Unused' + 0, // 208 - 'Unused' + 0, // 209 - 'Unused' + 0, // 210 - 'Unused' + 0, // 211 - 'Unused' + 0, // 212 - 'Unused' + 0, // 213 - 'Unused' + 0, // 214 - 'Unused' + 0, // 215 - 'Unused' + 0, // 216 - 'Unused' + 0, // 217 - 'Unused' + 0, // 218 - 'Unused' + 0, // 219 - 'Unused' + 0, // 220 - 'Unused' + 0, // 221 - 'Unused' + 0, // 222 - 'Unused' + 0, // 223 - 'Unused' + 0, // 224 - 'Unused' + 0, // 225 - 'Unused' + 0, // 226 - 'Unused' + 0, // 227 - 'Unused' + 0, // 228 - 'Unused' + 0, // 229 - 'Unused' + 0, // 230 - 'Unused' + 0, // 231 - 'Unused' + 0, // 232 - 'Unused' + 0, // 233 - 'Unused' + 0, // 234 - 'Unused' + 0, // 235 - 'Unused' + 0, // 236 - 'Unused' + 0, // 237 - 'Unused' + 0, // 238 - 'Unused' + 0, // 239 - 'Unused' + 0, // 240 - 'Unused' + 0, // 241 - 'Unused' + 0, // 242 - 'Unused' + 0, // 243 - 'Unused' + 0, // 244 - 'Unused' + 0, // 245 - 'Unused' + 0, // 246 - 'Unused' + 0, // 247 - 'Unused' + 0, // 248 - 'Unused' + 0, // 249 - 'Unused' + 0, // 250 - 'Unused' + 0, // 251 - 'Unused' + 0, // 252 - 'Unused' + 0, // 253 - 'Unused' + 0, // 254 - 'Unused' + 0, // 255 - 'Unused' +}; diff --git a/src/main.cpp b/src/main.cpp index 3609a5b..8d1bccc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,6 @@ #define COMPDATE __DATE__ __TIME__ // 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 @@ -13,7 +12,8 @@ #define VERTICAL_BAR_STARTS_TOP false #define DEBUG_RELAYS false #define DEBUG_DISPLAY false -#define DEBUG_LVL 0 +#define STARTUP1_ANIMATION_DURATION_ms 15000 +#define STARTUP2_ANIMATION_DURATION_ms 45000 #include #include @@ -23,12 +23,11 @@ #include #include #include - +#include "MD_RobotEyes.h" IOTAppStory IAS(COMPDATE, MODEBUTTON); String deviceName = "wemosMatrixDisplay"; String chipId; -const uint16_t WAIT_TIME = 1000; // 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 @@ -47,14 +46,12 @@ Your hardware matches the setting for FC-16 modules. Please set FC16_HW. #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 -#define CLK_PIN DISPLAY_CLK_PIN -#define DATA_PIN DISPLAY_DATA_PIN -#define CS_PIN DISPLAY_CS_PIN // 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; WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 3600, 60000); @@ -67,10 +64,10 @@ char *relay1Pin_String = "D1"; char *relay2Pin_String = "D2"; int relay1Pin = D1, relay2Pin = D2; char *relayHoldTime_ms_String = "200"; -int relayHoldTime_ms = 200; +unsigned int relayHoldTime_ms = 200; char *relayMinOffTime_ms_String = "100"; -int relayMinOffTime_ms = 100; -int displayRefresh_ms = 200; +unsigned int relayMinOffTime_ms = 100; +unsigned int displayRefresh_ms = 200; // Clock Display Config Parameter static char * displayClockNameEvery_ms_String = "16000"; @@ -90,7 +87,7 @@ void setupIAS(void) { // preset deviceName this is also your MDNS responder: http://deviceName.local IAS.preSetDeviceName(deviceName); IAS.preSetAppName(F("Wemos2RelaysMatrixDisplays")); - IAS.preSetAppVersion(F("0.0.1")); + IAS.preSetAppVersion(F("0.2.1")); IAS.preSetAutoUpdate(true); // define fields @@ -104,43 +101,43 @@ void setupIAS(void) { 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 in firmware update mode.")); - Serial.println(F("*-------------------------------------------------------------------------*")); + 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 in configuration mode.")); - Serial.println(F("*-------------------------------------------------------------------------*")); + 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("*-------------------------------------------------------------------------*")); + Serial.println(F("*----------------------------------------------------------------------*")); P.print("|rst"); ESP.reset(); }); IAS.onConfigMode([]() { - P.print("WiFi"); + P.print(" WiFi"); delay(400); - P.print("*" + chipId); + P.print(":" + chipId); Serial.print(F("Entered config mode for Wifi, device=")); Serial.println(chipId); }); IAS.onFirmwareUpdateCheck([]() { - P.print("chk upd"); + // P.print("chk upd"); Serial.println(F("Firmware update check")); }); IAS.onFirmwareUpdateDownload([]() { - P.print("dl&instl"); + P.print("dl+instl"); Serial.println(F("Download and install new firmware")); }); IAS.onFirmwareUpdateError([]() { - P.print("Err fwu"); + // P.print("Err fwu"); Serial.println(F("Firmware update error")); }); @@ -180,6 +177,8 @@ void setupRelays(int relay1Pin, int relay2Pin) { digitalWrite(relay2Pin, LOW); } +static MD_MAX72XX *graphicDisplay = NULL; + void setupDisplay() { int charCode; #if VERTICAL_BAR_STARTS_TOP @@ -211,6 +210,9 @@ void setupDisplay() { 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)); @@ -221,14 +223,15 @@ void setupDisplay() { P.addChar('0', newZero); P.print(intro); } + + void setup(void) { Serial.println(F("setup():")); setupDisplay(); setupIAS(); - delay(500); + delay(200); setupRelays(relay1Pin, relay2Pin); - delay(500); timeClient.begin(); Serial.println(F("setup() finished")); @@ -237,7 +240,7 @@ void setup(void) static bool timeClientInitialized = false; static unsigned long lastTimeOutput_ms = 0; -#define TIME_BETWEEN_TIME_REPORTS_ms 60000 +#define TIME_BETWEEN_REALTIME_UPDATE_ms 60000 static unsigned long last_relay_off_ts=0, last_relay_hold_ts=0; enum RelayState { RELAY_STATE_OFF=0, RELAY_STATE_ON_EVEN_MINUTE, RELAY_STATE_ON_ODD_MINUTE }; @@ -285,6 +288,80 @@ void loopRelays(void) { } } +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; + #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 loop(void) { @@ -298,17 +375,32 @@ void loop(void) 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; - } - IAS.loop(); - - if (timeClientInitialized && millis()-lastTimeOutput_ms > TIME_BETWEEN_TIME_REPORTS_ms) { 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(); @@ -323,6 +415,8 @@ void loop(void) if (minuteProgressIndicator > 9) minuteProgressIndicator = 9; snprintf(timeBuffer, 10, "%c %2d:%02d", minuteProgressIndicator, hours, minutes); + //P.displayAnimate(); + //P.displayClear(); /* DEBUG */ #if DEBUG_RELAYS if (seconds != lastSeconds) { @@ -346,44 +440,7 @@ void loop(void) #endif /* END DEBUG */ - currentDisplayState = seconds / 5; // Value 0..11 -#if DEBUG_DISPLAY - static bool executed = false; - if (recentDisplayState != currentDisplayState) executed = false; - - #define ExecOnce(p) {if (!executed) {executed=true; p;}} - - switch (currentDisplayState) { - case 0: ExecOnce(P.print(timeBuffer)); break; - case 1: - snprintf(debugMsg, MsgSize, "%c t%cr%c", minuteProgressIndicator, timeClientInitialized ? 'x':'-', (relaysState == RELAY_STATE_OFF)?'-':(relaysState == RELAY_STATE_ON_EVEN_MINUTE?'e':'o')); - ExecOnce(P.print(debugMsg)); - break; - case 2: ExecOnce(P.print(timeBuffer)); break; - case 3: // if (lastSeconds != seconds) P.print(seconds); break; - case 4: ExecOnce(P.print(timeBuffer)); break; - case 5: ExecOnce(P.print(timeBuffer)); break; - case 6: - switch (minutes % 3) { - case 0: snprintf(debugMsg, MsgSize, "%c s%d", minuteProgressIndicator, clockSpeed_modelMsPerRealSec); break; - case 1: snprintf(debugMsg, MsgSize, "%c h%d", minuteProgressIndicator, relayHoldTime_ms); break; - case 2: snprintf(debugMsg, MsgSize, "%c o%d", minuteProgressIndicator, relayMinOffTime_ms); break; - } - ExecOnce(P.print(debugMsg)); - break; - case 7: ExecOnce(P.print(timeBuffer)); break; - case 8: - snprintf(debugMsg, MsgSize, "t%cr%c", timeClientInitialized ? 'x':'-', (relaysState == RELAY_STATE_OFF)?'-':(relaysState == RELAY_STATE_ON_EVEN_MINUTE?'e':'o')); - ExecOnce(P.print(debugMsg)); - break; - case 9: ExecOnce(P.print(timeBuffer)); break; - case 10: if (lastSeconds != seconds) P.printf("%c %d", minuteProgressIndicator, seconds); break; - case 11: ExecOnce(P.print(timeBuffer)); break; - default: ExecOnce(P.print("default")); break; - } - recentDisplayState = currentDisplayState; -#else // standard procedure to display static uint32_t last_clock_refresh = 0; static uint32_t lastTimeClockNameShown = 0; @@ -398,20 +455,22 @@ void loop(void) 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 + P.setIntensity(2); P.print(clockName); lastTimeClockNameShown = millis(); showingClockName = true; } else { // showing clock if (millis() - last_clock_refresh > displayRefresh_ms) { + //P.displayClear(); + P.setIntensity(1); P.print(timeBuffer); last_clock_refresh = millis(); } } } -#endif - // toggle relays if (lastMinutes != minutes) { toggleRelays();