275 lines
7.1 KiB
C++
275 lines
7.1 KiB
C++
#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);
|
|
};
|