SdFat-beta icon indicating copy to clipboard operation
SdFat-beta copied to clipboard

'SdFat {aka class SdFs}' has no member named 'vwd'

Open plumped opened this issue 2 years ago • 7 comments

Hey Guys,

Im trying to run the GameFrame Code from https://github.com/Jerware/GameFrameV2 with a Teensy 4.1

Any idea why the SdFat always yelling at me that there is no 'vwd' member? The code was written in the past for compiling with teensy 3.x

The connected SD card doesn't matter. I have tried different cards from different manufacturers and types & sizes.

The files on the SD card are written to root -> Drive:\ extracted from the files from https://github.com/Jerware/GameFrameV2/tree/master/sd

Errorlog_verbose.txt

Thank you!

My Code:


#include <SPI.h>
#include <EEPROM.h>
#include <Wire.h>
#include <Time.h>
#include <DS1307RTC.h>
#include <SdFatBeta.h>
#include <IniFileLite.h>
#include <FastLED.h>
#include <Entropy.h>
#include <IRremote.h>

/***************************************************
  Game Frame V2 Source Code
  Jeremy Williams, 2-26-2016

  Game Frame is available at LEDSEQ.COM

  NOTE: Altering your firmware voids your warranty. Have fun.

  BMP parsing code based on example sketch for the Adafruit
  1.8" SPI display library by Adafruit.
  http://www.adafruit.com/products/358

  In the SD card, place 24 or 32 bit color BMP files
  (Alpha channel is ignored in 32-bit files.)

  There are examples included
 ****************************************************/

#define SD_CS    10  // Chip select line for SD card
#define SD_FAT_TYPE 2
SdFat sd; // set filesystem
SdFile myFile; // set filesystem

#define BUFFPIXEL 16 // number of pixels to buffer when reading BMP files

// Time
tmElements_t tm;

// IR setup
const byte RECV_PIN = 4;
IRrecv irrecv(RECV_PIN);
decode_results results;
char irCommand;
char irLastCommand;
boolean understood = false;


// LED setup
#define DATA_PIN 2
#define CLOCK_PIN 3
#define NUM_LEDS    256
#define LED_TYPE    SK9822
#define COLOR_ORDER BGR
CRGB leds[NUM_LEDS];

//Button setup
const uint8_t buttonNextPin = 16; // "Next" button
const uint8_t buttonMenuPin = 17;  // "Menu" button

#define STATUS_LED 6

//Enable verbose prints?
const boolean debugMode = false;

//Global variables
boolean
clockIniRead = false,
framePowered = true, // virtual screen power, used w/remote power button
irMenuRepeat = false,
irNextRepeat = false,
irPowerRepeat = false,
abc = false, // auto brightness control toggle
logoPlayed = false, // plays logo animation correctly reardless of playMode
folderLoop = true, // animation looping
moveLoop = false, // translation/pan looping
buttonPressed = false, // control button check
buttonEnabled = true, // debounce guard
menuActive = false, // showing menus (set brightness, playback mode, etc.)
panoff = true, // movement scrolls off screen
singleGraphic = false, // single BMP file
abortImage = false, // image is corrupt; abort, retry, fail?
displayFolderCount = false, // output extra info to LEDs
statusLedState = false, // flicker tech
clockShown = false, // clock mode?
clockSet = false, // have we set the time?
clockSetBlink = false, // flicker digit
clockDigitSet = false, // is hours/minutes set?
enableSecondHand = true,
clockAnimationActive = false, // currently showing clock anim
clockAdjustState = false, // is clock in an adjustment state?
hour12 = true, // 12-hour clock?
finishBeforeProgressing = false, // finish the animation before progressing?
timerLapsed = false, // timer lapsed, queue next animation when current one finishes
breakout = false, // breakout playing?
ballMoving = false,
gameInitialized = false;

byte
abcMinute, // last minute ABC was checked
playMode = 0, // 0 = sequential, 1 = random, 2 = pause animations
displayMode = 0, // 0 = slideshow, 1 = clock
brightness = 4, // LED brightness
brightnessMultiplier = 8, // DO NOT CHANGE THIS!
cycleTimeSetting = 2, // time before next animation: 1=10 secs, 2=30 secs, 3=1 min... 8=infinity
menuMode = 0, // 0 = brightmess, 1 = play mode, 2 = cycle time
clockAnimationLength = 5, // seconds to play clock animations
secondHandX = 0,
secondHandY = 0,
secondOffset = 0,
lastSecond = 255, // a moment ago
currentHour = 12,
currentMinute = 0,
currentSecond = 255, // current second
paddleIndex = 230,
ballX = 112,
ballY = 208,
ballIndex = 216;

int16_t 
folderIndex = 0; // current folder

int
secondCounter = 0, // counts up every second
fileIndex = 0, // current frame
chainIndex = -1, // for chaining multiple folders
numFolders = 0, // number of folders on sd
cycleTime = 30, // seconds to wait before progressing to next folder
clockAdjust = 0, // seconds to adjust clock every 24 hours
offsetBufferX = 0, // for storing offset when entering menu
offsetBufferY = 0, // for storing offset when entering menu
offsetSpeedX = 0, // number of pixels to translate each frame
offsetSpeedY = 0, // number of pixels to translate each frame
offsetX = 0, // for translating images x pixels
offsetY = 0, // for translating images y pixels
imageWidth = 0,
imageHeight = 0,
ballAngle;

unsigned long
prevRemoteMillis = 0, // last time a valid remote code was received
colorCorrection = 0xFFFFFF, // color correction setting
colorTemperature = 0xFFFFFF, // color Temperature
remoteCodeMenu = 0,
remoteCodeNext = 0,
remoteCodePower = 0,
menuPowerCounter = 0, // counter for holding menu button to turn off power
lastTime = 0, // used to calculate draw time
drawTime = 0, // time to read from sd
holdTime = 200, // millisecods to hold each .bmp frame
swapTime = 0, // system time to advance to next frame
baseTime = 0, // system time logged at start of each new image sequence
buttonTime = 0, // time the last button was pressed (debounce code)
menuEndTime = 0, // pause animation while in menu mode
menuEnterTime = 0; // time we enter menu

char
chainRootFolder[9], // chain game
nextFolder[21]; // dictated next animation

CRGB secondHandColor = 0; // color grabbed from digits.bmp for second hand

// globals for automatic brightness control
struct abc {
  int m; // minute
  byte b; // brightness
};

typedef struct abc Abc;
Abc abc0 { -1, 0};
Abc abc1 { -1, 0};
Abc abc2 { -1, 0};
Abc abc3 { -1, 0};
Abc abc4 { -1, 0};
Abc abc5 { -1, 0};
Abc abc6 { -1, 0};
Abc abc7 { -1, 0};
Abc abc8 { -1, 0};
Abc abc9 { -1, 0};

void setup(void) {
  // debug LED setup
  pinMode(STATUS_LED, OUTPUT);
  analogWrite(STATUS_LED, 100);

  pinMode(buttonNextPin, INPUT);    // button as input
  pinMode(buttonMenuPin, INPUT);    // button as input
  digitalWrite(buttonNextPin, HIGH); // turns on pull-up resistor after input
  digitalWrite(buttonMenuPin, HIGH); // turns on pull-up resistor after input

  // teensy LED flash
  pinMode(13, OUTPUT);
  teensyLEDFlash();
  teensyLEDFlash();
  teensyLEDFlash();

  Entropy.Initialize();

  if (debugMode == true)
  {
    Serial.begin(57600);
    delay(2000);
    Serial.println("Hello there!");
  }

  // init clock and begin counting seconds

  setSyncProvider(RTC.get);
  setSyncInterval(300);

  // DS3231 EEPROM check & DS1307 hardware check
  if (EEPROM.read(4) == 1) clockSet = true;
  if (timeStatus() != timeSet) clockSet = false;

  // IR enable
  irrecv.enableIRIn();

  byte output = 0;

  // load last settings
  // read brightness setting from EEPROM
  output = EEPROM.read(0);
  if (output >= 1 && output <= 7) brightness = output;

  // read playMode setting from EEPROM
  output = EEPROM.read(1);
  if (output >= 0 && output <= 2) playMode = output;

  // read cycleTimeSetting setting from EEPROM
  output = EEPROM.read(2);
  if (output >= 1 && output <= 8) cycleTimeSetting = output;
  setCycleTime();

  // read displayMode setting from EEPROM
  output = EEPROM.read(3);
  if (output >= 0 && output <= 1) displayMode = output;

  // LED Init
  FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, COLOR_ORDER>(leds, NUM_LEDS).setDither(0);
  stripSetBrightness();
  clearStripBuffer();
  FastLED.show();

  // run burn in test if both tactile buttons held on cold boot
  if ((digitalRead(buttonNextPin) == LOW) && (digitalRead(buttonMenuPin) == LOW))
  {
    brightness = 7;
    stripSetBrightness();
    while (true)
    {
      testScreen();
    }
  }

  // revert to these values if MENU tactile button held on cold boot
  if (digitalRead(buttonMenuPin) == LOW)
  {
    brightness = 1;
    stripSetBrightness();
    playMode = 0;
    cycleTimeSetting = 2;
    setCycleTime();
    displayMode = 0;
  }

  // SD Init
  Serial.print("Init SD: ");
  if (!sd.begin(SD_CS, SPI_FULL_SPEED)) {
    Serial.println("fail");
    // SD error message
    sdErrorMessage();
    return;
  }
  Serial.println("OK!");

  // load automatic brightness control settings
  readABC();

  FastLED.setCorrection(colorCorrection);
  FastLED.setTemperature(colorTemperature);

  // load IR codes
  readRemoteIni();
  
  initGameFrame();
}

// This runs every time power is restored (cold or warm boot)
void initGameFrame()
{
  closeMyFile();
  // apply automatic brightness if enabled
  if (abc) applyCurrentABC();
  // reset vars
  displayFolderCount = false;
  logoPlayed = false;
  fileIndex = 0;
  offsetX = 0;
  offsetY = 0;
  folderIndex = 0;
  singleGraphic = false;
  clockAnimationActive = false;
  clockShown = false;
  nextFolder[0] = '\0';
  sd.chdir("/");

  // show test screens and folder count if NEXT tactile button held on boot
  if (digitalRead(buttonNextPin) == LOW)
  {
    displayFolderCount = true;
    testScreen();
  }

  char folder[13];

  if (numFolders == 0 || displayFolderCount)
  {
    numFolders = 0;
    // file indexes appear to loop after 2048
    for (int fileIndex = 0; fileIndex < 2048; fileIndex++)
    {
      myFile.open(sd.vwd(), fileIndex, O_READ);
      if (myFile.isDir()) {
        Serial.println("---");
        if (displayFolderCount == true)
        {
          leds[numFolders] = CRGB(128, 255, 0);
          FastLED.show();
        }
        numFolders++;
        Serial.print("File Index: ");
        Serial.println(fileIndex);
        myFile.getName(folder, 13);
        Serial.print("Folder: ");
        Serial.println(folder);
        closeMyFile();
      }
      else closeMyFile();
    }
    Serial.print(numFolders);
    Serial.println(" folders found.");
  }
  if (displayFolderCount == true)
  {
    delay(5000);
    irrecv.resume(); // Receive the next value
    remoteTest();
  }
  
  // play logo animation
  sd.chdir("/00system/logo");
  readIniFile();
  drawFrame();
}

void applyCurrentABC()
{
  getCurrentTime();
  int dayInMinutes = minuteCounter(currentHour, currentMinute);
  int newBrightness = -1;
  int triggerTimes[10] = {abc0.m, abc1.m, abc2.m, abc3.m, abc4.m, abc5.m, abc6.m, abc7.m, abc8.m, abc9.m};
  byte BrightnessSettings[10] = {abc0.b, abc1.b, abc2.b, abc3.b, abc4.b, abc5.b, abc6.b, abc7.b, abc8.b, abc9.b};
  for (byte x = 0; x < 9; x++)
  {
    if (dayInMinutes > triggerTimes[x] && triggerTimes[x] != -1) newBrightness = BrightnessSettings[x];
  }
  if (newBrightness == -1)
  {
    // current time is earlier than any triggers; use latest trigger before midnight
    newBrightness = BrightnessSettings[0];
    for (byte x = 1; x < 9; x++)
    {
      if (triggerTimes[x] > triggerTimes[x - 1]) newBrightness = BrightnessSettings[x];
    }
  }
  if (newBrightness > -1)
  {
    if (newBrightness == 0) newBrightness = 1;
    brightness = newBrightness;
    stripSetBrightness();
    saveSettingsToEEPROM();
  }
}

void stripSetBrightness()
{
  if (brightness > 7) brightness = 7;
  else if (brightness < 0) brightness = 0;
  FastLED.setBrightness(brightness * brightnessMultiplier);
  //  FastLED.setBrightness(2);
}

void remoteTest()
{
  clearStripBuffer();
  FastLED.show();
  sd.chdir("/00system");
  int graphicShown = 0;
  bmpDraw("irZapr.bmp", 0, 0);
  long nextIRCheck = 0;
  while (true)
  {
    irReceiver();
    if (irCommand == 'P' || irCommand == 'M' || irCommand == 'N' || irPowerRepeat || irMenuRepeat || irNextRepeat)
    {
      nextIRCheck = millis() + 125;
      if (irCommand == 'P' || irPowerRepeat && graphicShown != 1)
      {
        bmpDraw("irPowr.bmp", 0, 0);
        graphicShown = 1;
      }
      else if (irCommand == 'M' || irMenuRepeat && graphicShown != 2)
      {
        bmpDraw("irMenu.bmp", 0, 0);
        graphicShown = 2;
      }
      else if (irCommand == 'N' || irNextRepeat && graphicShown != 3)
      {
        bmpDraw("irNext.bmp", 0, 0);
        graphicShown = 3;
      }
    }
    // no remote button pressed
    if (millis() > nextIRCheck && graphicShown != 0)
    {
      bmpDraw("irZapr.bmp", 0, 0);
      graphicShown = 0;
    }
    // record new IR codes if NEXT tactile button is presssed
    if (digitalRead(buttonNextPin) == LOW)
    {
      recordIRCodes();
      bmpDraw("irZapr.bmp", 0, 0);
    }
    // exit if MENU tactile button is presssed
    if (digitalRead(buttonMenuPin) == LOW)
    {
      return;
    }
  }
}

void teensyLEDFlash()
{
  // teensy LED flash
  digitalWrite(13, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(100);               // wait for a second
  digitalWrite(13, LOW);    // turn the LED off by making the voltage LOW
  delay(100);               // wait for a second
}

// draw a red box around the screen to indicate recording active
void redBox()
{
  for (int i = 0; i < 15; i++)
  {
    leds[i] = CRGB(255, 0, 0);
  }
  for (int i = 0; i < 16; i++)
  {
    leds[getIndex(16, i)] = CRGB(255, 0, 0);
  }
  for (int i = 0; i < 15; i++)
  {
    leds[getIndex(i, 15)] = CRGB(255, 0, 0);
  }
  for (int i = 15; i > 0; i--)
  {
    leds[getIndex(0, i)] = CRGB(255, 0, 0);
  }
  FastLED.show();
}

void printIRCode()
{
  Serial.print("IR code: ");
  Serial.println(results.value, DEC);
}

void recordIRCodes()
{
  remoteCodeMenu = 0;
  remoteCodeNext = 0;
  remoteCodePower = 0;

  // record POWR
  bmpDraw("irPowr.bmp", 0, 0);
  redBox();
  while (remoteCodePower == 0)
  {
    if (irrecv.decode(&results)) {
      if (results.decode_type == NEC && results.value != 4294967295) // ignore repeat code (0xFFFFFFFF)
      {
        printIRCode();
        remoteCodePower = results.value;
      }
      irrecv.resume(); // Receive the next value
    }
  }

  // record MENU
  bmpDraw("irMenu.bmp", 0, 0);
  redBox();
  while (remoteCodeMenu == 0)
  {
    if (irrecv.decode(&results)) {
      if (results.decode_type == NEC && results.value != 4294967295 && results.value != remoteCodePower) // ignore repeat code (0xFFFFFFFF)
      {
        printIRCode();
        remoteCodeMenu = results.value;
      }
      irrecv.resume(); // Receive the next value
    }
  }

  // record NEXT
  bmpDraw("irNext.bmp", 0, 0);
  redBox();
  while (remoteCodeNext == 0)
  {
    if (irrecv.decode(&results)) {
      if (results.decode_type == NEC && results.value != 4294967295 && results.value != remoteCodePower && results.value != remoteCodeMenu) // ignore repeat code (0xFFFFFFFF)
      {
        printIRCode();
        remoteCodeNext = results.value;
      }
      irrecv.resume(); // Receive the next value
    }
  }

  // save codes to SD
  if (!myFile.open("remote.ini", O_CREAT | O_RDWR)) {
    Serial.println("File open failed");
    sdErrorMessage();
    return;
  }
  myFile.rewind();
  myFile.seekSet(352);
  myFile.println("[remote]");
  myFile.println("");
  myFile.println("# Learned Codes");
  myFile.print("power = ");
  myFile.println(remoteCodePower);
  myFile.print("menu = ");
  myFile.println(remoteCodeMenu);
  myFile.print("next = ");
  myFile.println(remoteCodeNext);
  closeMyFile();
}

void testScreen()
{
  // white
  for (int i = 0; i < 256; i++)
  {
    leds[i] = CRGB(255, 255, 255);
  }
  FastLED.show();
  delay(5000);

  // red
  for (int i = 0; i < 256; i++)
  {
    leds[i] = CRGB(255, 0, 0);
  }
  FastLED.show();
  delay(1000);

  // green
  for (int i = 0; i < 256; i++)
  {
    leds[i] = CRGB(0, 255, 0);
  }
  FastLED.show();
  delay(1000);

  // blue
  for (int i = 0; i < 256; i++)
  {
    leds[i] = CRGB(0, 0, 255);
  }
  FastLED.show();
  delay(1000);
}

void sdErrorMessage()
{
  // red bars
  for (int index = 64; index < 80; index++)
  {
    leds[index] = CRGB(255, 0, 0);
  }
  for (int index = 80; index < 192; index++)
  {
    leds[index] = CRGB(0, 0, 0);
  }
  for (int index = 192; index < 208; index++)
  {
    leds[index] = CRGB(255, 0, 0);
  }
  // S
  yellowDot(7, 6);
  yellowDot(6, 6);
  yellowDot(5, 6);
  yellowDot(4, 7);
  yellowDot(5, 8);
  yellowDot(6, 8);
  yellowDot(7, 9);
  yellowDot(6, 10);
  yellowDot(5, 10);
  yellowDot(4, 10);

  // D
  yellowDot(9, 6);
  yellowDot(10, 6);
  yellowDot(11, 7);
  yellowDot(11, 8);
  yellowDot(11, 9);
  yellowDot(10, 10);
  yellowDot(9, 10);
  yellowDot(9, 7);
  yellowDot(9, 8);
  yellowDot(9, 9);

  stripSetBrightness();
  FastLED.show();

  while (true)
  {
    for (int i = 255; i >= 0; i--)
    {
      analogWrite(STATUS_LED, i);
      printRemoteCode();
      delay(1);
    }
    for (int i = 0; i <= 254; i++)
    {
      analogWrite(STATUS_LED, i);
      printRemoteCode();
      delay(1);
    }
  }
}

void printRemoteCode()
{
  if (irrecv.decode(&results)) {
    printIRCode();
    irrecv.resume(); // Receive the next value
  }
}

void yellowDot(byte x, byte y)
{
  leds[getIndex(x, y)] = CRGB(255, 255, 0);
}

void setCycleTime()
{
  if (cycleTimeSetting == 2)
  {
    cycleTime = 30;
  }
  else if (cycleTimeSetting == 3)
  {
    cycleTime = 60;
  }
  else if (cycleTimeSetting == 4)
  {
    cycleTime = 300;
  }
  else if (cycleTimeSetting == 5)
  {
    cycleTime = 900;
  }
  else if (cycleTimeSetting == 6)
  {
    cycleTime = 1800;
  }
  else if (cycleTimeSetting == 7)
  {
    cycleTime = 3600;
  }
  else if (cycleTimeSetting == 8)
  {
    cycleTime = -1;
  }
  else
  {
    cycleTime = 10;
  }
}

void statusLedFlicker()
{
  if (statusLedState == false)
  {
    analogWrite(STATUS_LED, 200);
  }
  else
  {
    analogWrite(STATUS_LED, 255);
  }
  statusLedState = !statusLedState;
}

/* teensy 3 clock code */
time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}

/*  code to process time sync messages from the serial port   */
#define TIME_HEADER  "T"   // Header tag for serial time sync message

  unsigned long processSyncMessage() {
  unsigned long pctime = 0L;
  const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013

  if (Serial.find(TIME_HEADER)) {
    pctime = Serial.parseInt();
    return pctime;
    if ( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
      pctime = 0L; // return 0 to indicate that the time is not valid
    }
  }
  return pctime;
}

void saveSettingsToEEPROM()
{
  // save any new settings to EEPROM
  EEPROM.update(0, brightness);
  EEPROM.update(1, playMode);
  EEPROM.update(2, cycleTimeSetting);
  EEPROM.update(3, displayMode);
}

void irReceiver()
{
  irMenuRepeat = false;
  irNextRepeat = false;
  irPowerRepeat = false;
  irCommand = 'Z';
  if (irrecv.decode(&results)) {
    Serial.print("IR code received: ");
    Serial.println(results.value);
    understood = true;
    // menu
    if (results.value == remoteCodeMenu)
    {
      irCommand = 'M';
      prevRemoteMillis = millis();
    }
    // next
    else if (results.value == remoteCodeNext)
    {
      irCommand = 'N';
      prevRemoteMillis = millis();
    }
    // power
    else if (results.value == remoteCodePower)
    {
      irCommand = 'P';
      prevRemoteMillis = millis();
    }
    // repeat/held
    else if (results.value == 4294967295 && millis() - prevRemoteMillis < 250) // require recent prime signal to assume repeat 
    {
      irCommand = 'R';
      prevRemoteMillis = millis();
      if (irLastCommand == 'M') irMenuRepeat = true;
      else if (irLastCommand == 'N') irNextRepeat = true;
      else if (irLastCommand == 'P') irPowerRepeat = true;
    }
    else understood = false;
    if (understood)
    {
      Serial.print("Interpreted as: ");
      Serial.println(irCommand);
      if (irCommand != 'R') irLastCommand = irCommand;
    }

    irrecv.resume(); // Receive the next value
  }
}

void powerControl()
{
  if (irCommand == 'P')
  {
    // abc brightness set to zero; turn display on
    if (brightness == 0)
    {
      brightness = 1;
      stripSetBrightness();
      if (displayMode == 0) drawFrame(); // refrech the screen
      else initClock();
    }
    // toggle system power
    else
    {
      framePowered = !framePowered;
      // power down
      if (!framePowered) framePowerDown();
      // power restored
      else initGameFrame();
    }
  }
  // allow power toggle by holding down physical Menu button on PCB
  if (framePowered)
  {
    if (millis() > menuPowerCounter && digitalRead(buttonMenuPin) == LOW && menuActive)
    {
      framePowered = false;
      framePowerDown();
    }
  }
  // reset MENU button after power off
  else if (digitalRead(buttonMenuPin) == HIGH && buttonPressed == true) buttonPressed = false;
  // power on
  else if (digitalRead(buttonMenuPin) == LOW && buttonPressed == false)
  {
    framePowered = true;
  }
}

void framePowerDown()
{
  menuActive = false;
  if (breakout == true)
  {
    breakout = false;
    ballMoving = false;
  }
  clearStripBuffer();
  FastLED.show();
}

void loop() {
  irReceiver();
  powerControl();
  getCurrentTime();
  clockDriftAdjustment();

  if (framePowered)
  {
    runABC();
    if (brightness > 0)
    {
      if (breakout == false)
      {
        mainLoop();
      }

      else
      {
        breakoutLoop();
        if (breakout == false)
        {
          if (displayMode == 0)
          {
            nextImage();
            drawFrame();
          }
          else initClock();
        }
      }
    }
  }
}

void mainLoop()
{
  buttonDebounce();

  // next button
  if ((digitalRead(buttonNextPin) == LOW || irCommand == 'N') && buttonPressed == false && buttonEnabled == true && !clockShown)
  {
    buttonPressed = true;
    if (menuActive == false)
    {
      // exit chaining if necessary
      if (chainIndex > -1)
      {
        chainIndex = -1;
        chainRootFolder[0] = '\0';
        sd.chdir("/");
      }
      if (displayMode == 0)
      {
        nextImage();
        drawFrame();
      }
      else if (displayMode == 1)
      {
        // just displayed logo, enter clock mode
        initClock();
        return;
      }
    }
    else
    {
      menuEndTime = millis() + 3000;

      // adjust brightness
      if (menuMode == 0)
      {
        brightness += 1;
        if (brightness > 7) brightness = 1;
        char brightChar[2];
        char brightFile[23];
        strcpy_P(brightFile, PSTR("/00system/bright_"));
        itoa(brightness, brightChar, 10);
        strcat(brightFile, brightChar);
        strcat(brightFile, ".bmp");
        stripSetBrightness();
        bmpDraw(brightFile, 0, 0);
      }

      // adjust play mode
      else if (menuMode == 1)
      {
        playMode++;
        if (playMode > 2) playMode = 0;
        char playChar[2];
        char playFile[21];
        strcpy_P(playFile, PSTR("/00system/play_"));
        itoa(playMode, playChar, 10);
        strcat(playFile, playChar);
        strcat(playFile, ".bmp");
        bmpDraw(playFile, 0, 0);
      }

      // adjust cycle time
      else if (menuMode == 2)
      {
        cycleTimeSetting++;
        if (cycleTimeSetting > 8) cycleTimeSetting = 1;
        setCycleTime();
        char timeChar[2];
        char timeFile[21];
        strcpy_P(timeFile, PSTR("/00system/time_"));
        itoa(cycleTimeSetting, timeChar, 10);
        strcat(timeFile, timeChar);
        strcat(timeFile, ".bmp");
        bmpDraw(timeFile, 0, 0);
      }

      // clock/gallery mode
      else if (menuMode == 3)
      {
        displayMode++;
        if (displayMode > 1) displayMode = 0;
        char modeChar[2];
        char modeFile[21];
        strcpy_P(modeFile, PSTR("/00system/mode_"));
        itoa(displayMode, modeChar, 10);
        strcat(modeFile, modeChar);
        strcat(modeFile, ".bmp");
        bmpDraw(modeFile, 0, 0);
      }

      // breakout time
      else if (menuMode == 4)
      {
        menuActive = false;
        saveSettingsToEEPROM();

        // return to brightness menu next time
        menuMode = 0;

        buttonTime = millis();
        breakout = true;
        gameInitialized = false;
        buttonEnabled = false;

        char tmp[23];
        strcpy_P(tmp, PSTR("/00system/breakout.bmp"));
        bmpDraw(tmp, 0, 0);

        paddleIndex = 230,
        ballX = 112,
        ballY = 208,
        ballIndex = 216;
        holdTime = 0;
        fileIndex = 0;
        leds[ballIndex] = CRGB(175, 255, 15);
        leds[paddleIndex] = CRGB(200, 200, 200);
        leds[paddleIndex + 1] = CRGB(200, 200, 200);
        leds[paddleIndex + 2] = CRGB(200, 200, 200);
        FastLED.show();
        return;
      }
    }
  }

  // reset clock
  if ((digitalRead(buttonNextPin) == LOW || irCommand == 'N') && buttonPressed == false && buttonEnabled == true && clockShown)
  {
    buttonPressed = true;
    clockSet = false;
    clockSetBlink = false;
    getCurrentTime();
    setClock();
  }

  // menu button
  else if ((digitalRead(buttonMenuPin) == LOW || irCommand == 'M') && buttonPressed == false && buttonEnabled == true)
  {
    buttonPressed = true;
    menuEndTime = millis() + 3000;
    menuPowerCounter = millis() + 1500;

    if (menuActive == false)
    {
      menuActive = true;
      menuEnterTime = millis();
      offsetBufferX = offsetX;
      offsetBufferY = offsetY;
      offsetX = 0;
      offsetY = 0;
      closeMyFile();
      if (clockShown || clockAnimationActive)
      {
        clockAnimationActive = false;
        clockShown = false;
        abortImage = true;
        nextFolder[0] = '\0';
      }
    }
    else
    {
      menuMode++;
      if (menuMode > 4) menuMode = 0;
    }
    if (menuMode == 0)
    {
      char brightChar[2];
      char brightFile[23];
      strcpy_P(brightFile, PSTR("/00system/bright_"));
      itoa(brightness, brightChar, 10);
      strcat(brightFile, brightChar);
      strcat(brightFile, ".bmp");
      bmpDraw(brightFile, 0, 0);
    }
    else if (menuMode == 1)
    {
      char playChar[2];
      char playFile[21];
      strcpy_P(playFile, PSTR("/00system/play_"));
      itoa(playMode, playChar, 10);
      strcat(playFile, playChar);
      strcat(playFile, ".bmp");
      bmpDraw(playFile, 0, 0);
    }
    else if (menuMode == 2)
    {
      char timeChar[2];
      char timeFile[21];
      strcpy_P(timeFile, PSTR("/00system/time_"));
      itoa(cycleTimeSetting, timeChar, 10);
      strcat(timeFile, timeChar);
      strcat(timeFile, ".bmp");
      bmpDraw(timeFile, 0, 0);
    }
    else if (menuMode == 3)
    {
      char modeChar[2];
      char modeFile[21];
      strcpy_P(modeFile, PSTR("/00system/mode_"));
      itoa(displayMode, modeChar, 10);
      strcat(modeFile, modeChar);
      strcat(modeFile, ".bmp");
      bmpDraw(modeFile, 0, 0);
    }
    else if (menuMode == 4)
    {
      char gameFile[21];
      strcpy_P(gameFile, PSTR("/00system/game.bmp"));
      bmpDraw(gameFile, 0, 0);
    }
  }

  // time to exit menu mode?
  if (menuActive == true)
  {
    if (millis() > menuEndTime)
    {
      if (displayMode == 1)
      {
        initClock();
        return;
      }

      menuActive = false;

      saveSettingsToEEPROM();

      // return to brightness menu next time
      menuMode = 0;

      offsetX = offsetBufferX;
      offsetY = offsetBufferY;
      if (playMode == 2)
      {
        offsetX = imageWidth / -2 + 8;
        offsetY = imageHeight / 2 - 8;
      }
      swapTime = swapTime + (millis() - menuEnterTime);
      baseTime = baseTime + (millis() - menuEnterTime);
      if (abortImage == false)
      {
        drawFrame();
      }
    }
  }

  // currently playing images?
  if (menuActive == false && breakout == false)
  {
    if (clockShown == false || clockAnimationActive == true)
    {
      // advance counter
      if (currentSecond != lastSecond)
      {
        lastSecond = currentSecond;
        secondCounter++;
        // revert to clock display if animation played for 5 seconds
        if (clockAnimationActive == true && secondCounter >= clockAnimationLength)
        {
          initClock();
        }
      }

      // did image load fail?
      if (abortImage == true && clockShown == false && logoPlayed == true)
      {
        abortImage = false;
        nextImage();
        drawFrame();
      }

      // progress to next folder if cycleTime is up
      // check for infinite mode
      else if (cycleTimeSetting != 8  && clockShown == false && clockAnimationActive == false)
      {
        if (secondCounter >= cycleTime)
        {
          if (finishBeforeProgressing == true)
          {
            if (timerLapsed == false) timerLapsed = true;
          }
          else
          {
            nextImage();
            drawFrame();
          }
        }
      }

      // animate if not a single-frame & animations are on
      if (holdTime != -1 && playMode != 2 || logoPlayed == false)
      {
        if (millis() >= swapTime && clockShown == false)
        {
          statusLedFlicker();
          swapTime = millis() + holdTime;
          fileIndex++;
          drawFrame();
        }
      }
    }

    // show clock
    else if (clockShown == true && clockAnimationActive == false)
    {
      showClock();
    }
  }
}

void nextImage()
{
  Serial.println(F("---"));
  Serial.println(F("Next Folder..."));
  closeMyFile();
  boolean foundNewFolder = false;
  // reset secondCounter if not playing clock animations
  if (!clockAnimationActive) secondCounter = 0;
  baseTime = millis();
  holdTime = 0;
  char folder[9];
  sd.chdir("/");
  fileIndex = 0;
  offsetX = 0;
  offsetY = 0;
  singleGraphic = false;
  finishBeforeProgressing = false;
  timerLapsed = false;
  if (!logoPlayed) logoPlayed = true;

  // are we chaining folders?
  if (chainIndex > -1)
  {
    char chainChar[6];
    char chainDir[23];
    strcpy_P(chainDir, PSTR("/"));
    strcat(chainDir, chainRootFolder);
    strcat(chainDir, "/");
    itoa(chainIndex, chainChar, 10);
    strcat(chainDir, chainChar);
    if (sd.exists(chainDir))
    {
      Serial.print(F("Chaining: "));
      Serial.println(chainDir);
      sd.chdir(chainDir);
      chainIndex++;
    }
    else
    {
      // chaining concluded
      chainIndex = -1;
      chainRootFolder[0] = '\0';
      sd.chdir("/");
    }
  }

  // has the next animation has been dictated by the previous .INI file?
  if (nextFolder[0] != '\0' && chainIndex == -1)
  {
    Serial.print(F("Forcing next: "));
    Serial.println(nextFolder);
    if (sd.exists(nextFolder))
    {
      sd.chdir(nextFolder);
    }
    else
    {
      nextFolder[0] = '\0';
      Serial.println(F("Not exists!"));
    }
  }

  // next folder not assigned by .INI
  if (nextFolder[0] == '\0' && chainIndex == -1)
  {
    // Getting next folder
    // shuffle playback using random number
    if (playMode != 0) // check we're not in a sequential play mode
    {
      int targetFolder = Entropy.random(0, numFolders);

      // don't repeat the same image, please.
      if (targetFolder <= 0 or targetFolder == numFolders or targetFolder == numFolders - 1)
      {
        // Repeat image detected! Incrementing targetFolder.
        targetFolder = targetFolder + 2;
      }

      Serial.print(F("Randomly advancing "));
      Serial.print(targetFolder);
      Serial.println(F(" folder(s)."));
      int i = 1;
      while (i < targetFolder)
      {
        foundNewFolder = false;
        while (foundNewFolder == false)
        {
          myFile.open(sd.vwd(), folderIndex, O_READ);
          if (myFile.isDir()) {
            foundNewFolder = true;
            i++;
          }
          closeMyFile();
          folderIndex++;
        }
      }
    }

    foundNewFolder = false;

    while (foundNewFolder == false)
    {
      myFile.open(sd.vwd(), folderIndex, O_READ);
      myFile.getName(folder, 13);

      // ignore system folders that start with "00"
      if (myFile.isDir() && folder[0] != 48 && folder[1] != 48) {
        foundNewFolder = true;
        Serial.print(F("Folder Index: "));
        Serial.println(folderIndex);
        Serial.print(F("Opening Folder: "));
        Serial.println(folder);

        sd.chdir(folder);
        closeMyFile();
      }
      else closeMyFile();
      folderIndex++;
    }
  }

  // is this the start of a folder chain?
  char chainDir[2];
  strcpy_P(chainDir, PSTR("0"));
  if (sd.exists(chainDir))
  {
    Serial.print(F("Chaining detected: "));
    Serial.println(folder);
    memcpy(chainRootFolder, folder, 8);
    sd.chdir(chainDir);
    chainIndex = 1;
  }

  char firstImage[6];
  strcpy_P(firstImage, PSTR("0.bmp"));
  if (sd.exists(firstImage))
  {
    Serial.print(F("Opening File: "));
    Serial.print(folder);
    Serial.println(F("/config.ini"));
    readIniFile();

    char tmp[6];
    strcpy_P(tmp, PSTR("0.bmp"));
    refreshImageDimensions(tmp);

    Serial.print(F("Hold (in ms): "));
    Serial.println(holdTime);
    swapTime = millis() + holdTime;

    // setup image for x/y translation as needed if animations aren't paused
    if (playMode != 2)
    {
      if (offsetSpeedX > 0)
      {
        if (panoff == true) offsetX = (imageWidth * -1);
        else offsetX = (imageWidth * -1 + 16);
      }
      else if (offsetSpeedX < 0)
      {
        if (panoff == true) offsetX = 16;
        else offsetX = 0;
      }
      if (offsetSpeedY > 0)
      {
        if (panoff == true) offsetY = -16;
        else offsetY = 0;
      }
      else if (offsetSpeedY < 0)
      {
        if (panoff == true) offsetY = imageHeight;
        else offsetY = imageHeight - 16;
      }
    }
    // center image if animations are paused
    else
    {
      offsetX = imageWidth / -2 + 8;
      offsetY = imageHeight / 2 - 8;
    }

    // test for single frame

    char tmp_0[6];
    char tmp_1[6];
    strcpy_P(tmp_0, PSTR("0.bmp"));
    strcpy_P(tmp_1, PSTR("1.bmp"));
    if (sd.exists(tmp_0) && (!sd.exists(tmp_1)))
    {
      singleGraphic = true;
      // check for pan settings
      if (offsetSpeedX == 0 && offsetSpeedY == 0)
      {
        // single frame still
        holdTime = -1;
      }
    }
  }

  // empty folder
  else
  {
    Serial.println(F("Empty folder!"));
    nextImage();
  }
}

void drawFrame()
{
  if (panoff == true)
  {
    if (offsetX > 16 || offsetX < (imageWidth * -1) || offsetY > imageHeight || offsetY < -16)
    {
      if (moveLoop == false || finishBeforeProgressing == true)
      {
        fileIndex = 0;
        nextImage();
      }
      else
      {
        if (offsetSpeedX > 0 && offsetX >= 16)
        {
          offsetX = (imageWidth * -1);
        }
        else if (offsetSpeedX < 0 && offsetX <= imageWidth * -1)
        {
          offsetX = 16;
        }
        if (offsetSpeedY > 0 && offsetY >= imageHeight)
        {
          offsetY = -16;
        }
        else if (offsetSpeedY < 0 && offsetY <= -16)
        {
          offsetY = imageHeight;
        }
      }
    }
  }
  else
  {
    if (offsetX > 0 || offsetX < (imageWidth * -1 + 16) || offsetY > imageHeight - 16 || offsetY < 0)
    {
      if (moveLoop == false || finishBeforeProgressing == true)
      {
        fileIndex = 0;
        nextImage();
      }
      else
      {
        if (offsetSpeedX > 0 && offsetX >= 0)
        {
          offsetX = (imageWidth * -1 + 16);
        }
        else if (offsetSpeedX < 0 && offsetX <= imageWidth - 16)
        {
          offsetX = 0;
        }
        if (offsetSpeedY > 0 && offsetY >= imageHeight - 16)
        {
          offsetY = 0;
        }
        else if (offsetSpeedY < 0 && offsetY <= 0)
        {
          offsetY = imageHeight - 16;
        }
      }
    }
  }

  if (singleGraphic == false)
  {
    char bmpFile[13]; // 8-digit number + .bmp + null byte
    itoa(fileIndex, bmpFile, 10);
    strcat(bmpFile, ".bmp");
    if (!sd.exists(bmpFile))
    {
      fileIndex = 0;
      itoa(fileIndex, bmpFile, 10);
      strcat(bmpFile, ".bmp");
      if (finishBeforeProgressing && (offsetSpeedX != 0 || offsetSpeedY != 0)); // translating image - continue animating until moved off screen
      else if (folderLoop == false || timerLapsed == true)
      {
        if (displayMode == 0) nextImage();
        else if (displayMode == 1)
        {
          // just displayed logo, enter clock mode
          initClock();
          return;
        }
      }
    }
    bmpDraw(bmpFile, 0, 0);
  }
  else bmpDraw("0.bmp", 0, 0);

  // print draw time in milliseconds
  drawTime = millis() - lastTime;
  lastTime = millis();
  Serial.print(F("ttd: "));
  Serial.println(drawTime);

  if (offsetSpeedX != 0) offsetX += offsetSpeedX;
  if (offsetSpeedY != 0) offsetY += offsetSpeedY;
}

void refreshImageDimensions(char *filename) {

  const uint8_t  gridWidth = 16;
  const uint8_t  gridHeight = 16;

  if ((0 >= gridWidth) || (0 >= gridHeight)) {
    Serial.print(F("Abort."));
    return;
  }

  // storing dimentions for image
  // Open requested file on SD card
  if (!myFile.open(filename, O_READ)) {
    Serial.println(F("File open failed"));
    sdErrorMessage();
    return;
  }

  // Parse BMP header
  if (read16(myFile) == 0x4D42) { // BMP signature
    (void)read32(myFile); // Read & ignore file size
    (void)read32(myFile); // Read & ignore creator bytes
    (void)read32(myFile); // skip data
    // Read DIB header
    (void)read32(myFile); // Read & ignore Header size
    imageWidth  = read32(myFile);
    imageHeight = read32(myFile);
    Serial.print(F("Image resolution: "));
    Serial.print(imageWidth);
    Serial.print(F("x"));
    Serial.println(imageHeight);
  }
  closeMyFile();
}

// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates.  It's sped up
// by reading many pixels worth of data at a time
// (rather than pixel by pixel).  Increasing the buffer
// size takes more of the Arduino's precious RAM but
// makes loading a little faster.  20 pixels seems a
// good balance.

void bmpDraw(char *filename, uint8_t x, uint8_t y) {

  int  bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24 or 32)
  uint8_t  sdbuffer[3 * BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t  rowSize;               // Not always = bmpWidth; may have padding
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip    = true;        // BMP is stored bottom-to-top
  int  w, h, row, col;
  uint8_t  r, g, b;
  uint32_t pos = 0;
  const uint8_t  gridWidth = 16;
  const uint8_t  gridHeight = 16;

  if ((x >= gridWidth) || (y >= gridHeight)) {
    Serial.print(F("Abort."));
    return;
  }

  if (!myFile.isOpen())
  {
    Serial.println();
    Serial.print(F("Loading image '"));
    Serial.print(filename);
    Serial.println('\'');
    // Open requested file on SD card
    if (!myFile.open(filename, O_READ)) {
      Serial.println(F("File open failed"));
      sdErrorMessage();
      return;
    }
  }
  else myFile.rewind();

  // Parse BMP header
  if (read16(myFile) == 0x4D42) { // BMP signature
    if (debugMode)
    {
      Serial.print(F("File size: ")); Serial.println(read32(myFile));
    }
    else (void)read32(myFile);
    (void)read32(myFile); // Read & ignore creator bytes
    bmpImageoffset = read32(myFile); // Start of image data
    //    Serial.print(F("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    (void)read32(myFile); // Read & ignore Header size
    bmpWidth  = read32(myFile);
    bmpHeight = read32(myFile);
    if (read16(myFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(myFile); // bits per pixel
      //      Serial.print(F("Bit Depth: ")); Serial.println(bmpDepth);
      if ((bmpDepth == 24 || bmpDepth == 32) && (read32(myFile) == 0)) { // 0 = uncompressed
        goodBmp = true; // Supported BMP format -- proceed!
        if (debugMode)
        {
          Serial.print(F("Image size: "));
          Serial.print(bmpWidth);
          Serial.print('x');
          Serial.println(bmpHeight);
        }

        // image smaller than 16x16?
        if ((bmpWidth < 16 && bmpWidth > -16) || (bmpHeight < 16 && bmpHeight > -16))
        {
          clearStripBuffer();
        }

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * bmpDepth/8 + bmpDepth/8) & ~(bmpDepth/8); // 32-bit BMP support
        //        Serial.print(F("Row size: "));
        //        Serial.println(rowSize);

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if (bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip      = false;
        }

        // initialize our pixel index
        byte index = 0; // a byte is perfect for a 16x16 grid

        // Crop area to be loaded
        w = bmpWidth;
        h = bmpHeight;
        if ((x + w - 1) >= gridWidth)  w = gridWidth - x;
        if ((y + h - 1) >= gridHeight) h = gridHeight - y;

        for (row = 0; row < h; row++) { // For each scanline...

          // Seek to start of scan line.  It might seem labor-
          // intensive to be doing this on every line, but this
          // method covers a lot of gritty details like cropping
          // and scanline padding.  Also, the seek only takes
          // place if the file position actually needs to change
          // (avoids a lot of cluster math in SD library).

          if (flip) // Bitmap is stored bottom-to-top order (normal BMP)
            pos = (bmpImageoffset + (offsetX * -3) + (bmpHeight - 1 - (row + offsetY)) * rowSize);
          else     // Bitmap is stored top-to-bottom
            pos = bmpImageoffset + row * rowSize;
          if (myFile.curPosition() != pos) { // Need seek?
            myFile.seekSet(pos);
            buffidx = sizeof(sdbuffer); // Force buffer reload
          }

          for (col = 0; col < w; col++) { // For each pixel...
            // Time to read more pixel data?
            
            if (bmpDepth == 24)
            {
              if (buffidx >= sizeof(sdbuffer)) { // Indeed
                myFile.read(sdbuffer, sizeof(sdbuffer));
                buffidx = 0; // Set index to beginning
              }
            }
            else if (bmpDepth == 32)
            {
              if (buffidx >= 3) { // 32-bit file compatibility forces buffer to 1 pixel at a time
                myFile.read(sdbuffer, 3); 
                myFile.read(); // eat the alpha channel
                buffidx = 0; // Set index to beginning
              }
            }

            // push to LED buffer
            b = sdbuffer[buffidx++];
            g = sdbuffer[buffidx++];
            r = sdbuffer[buffidx++];

            // apply contrast
            r = dim8_jer(r);
            g = dim8_jer(g);
            b = dim8_jer(b);

            // offsetY is beyond bmpHeight
            if (row >= bmpHeight - offsetY)
            {
              // black pixel
              leds[getIndex(col, row)] = CRGB(0, 0, 0);
            }
            // offsetY is negative
            else if (row < offsetY * -1)
            {
              // black pixel
              leds[getIndex(col, row)] = CRGB(0, 0, 0);
            }
            // offserX is beyond bmpWidth
            else if (col >= bmpWidth + offsetX)
            {
              // black pixel
              leds[getIndex(col, row)] = CRGB(0, 0, 0);
            }
            // offsetX is positive
            else if (col < offsetX)
            {
              // black pixel
              leds[getIndex(col, row)] = CRGB(0, 0, 0);
            }
            // all good
            else leds[getIndex(col + x, row)] = CRGB(r, g, b);
            // paint pixel color
          } // end pixel
        } // end scanline
      } // end goodBmp
    }
  }
  if (!clockShown || breakout == true)
  {
    FastLED.show();
  }
  if (singleGraphic == false || menuActive == true || breakout == true)
  {
    closeMyFile();
  }
  if (!goodBmp) Serial.println(F("Format unrecognized"));
}

uint8_t dim8_jer( uint8_t x )
{
  return ((uint16_t)x * (uint16_t)(x) ) >> 8;
}

void closeMyFile()
{
  if (myFile.isOpen()) 
  {
    if (debugMode) Serial.println(F("Closing Image..."));
    myFile.close();
  }
}

byte getIndex(byte x, byte y)
{
  byte index;
  if (y == 0)
  {
    index = x;
  }
  else if (y % 2 == 0)
  {
    index = y * 16 + x;
  }
  else
  {
    index = (y * 16 + 15) - x;
  }
  return index;
}

void clearStripBuffer()
{
  for (int i = 0; i < 256; i++)
  {
    leds[i] = CRGB(0, 0, 0);
  }
}

void buttonDebounce()
{
  // button debounce -- no false positives
  if (((digitalRead(buttonMenuPin) == HIGH) && digitalRead(buttonNextPin) == HIGH) && buttonPressed == true)
  {
    buttonPressed = false;
    buttonEnabled = false;
    buttonTime = millis();
  }
  if ((buttonEnabled == false) && buttonPressed == false)
  {
    if (millis() > buttonTime + 50) buttonEnabled = true;
  }
}

// automatic brightness control
void runABC()
{
  if (currentSecond == 0)
  {
    if (abc && clockSet && currentMinute != abcMinute)
    {
      abcMinute = currentMinute;
      int dayInMinutes = minuteCounter(currentHour, currentMinute);
      int triggerTimes[10] = {abc0.m, abc1.m, abc2.m, abc3.m, abc4.m, abc5.m, abc6.m, abc7.m, abc8.m, abc9.m};
      byte brightnessSettings[10] = {abc0.b, abc1.b, abc2.b, abc3.b, abc4.b, abc5.b, abc6.b, abc7.b, abc8.b, abc9.b};
      for (byte x = 0; x < 9; x++)
      {
        if (dayInMinutes == triggerTimes[x])
        {
          brightness = brightnessSettings[x];
          stripSetBrightness();
          if (brightness == 0)
          {
            clearStripBuffer();
            FastLED.show();
          }
          else if (displayMode == 1)
          {
            initClock();
          }
          // refresh the screen
          else
          {
            drawFrame();
          }
        }
      }
    }
  }
}

// Clock

void debugClockDisplay()
{
  // digital clock display of the time
  Serial.print(currentHour);
  printDigits(currentMinute);
  printDigits(currentSecond);
  Serial.println();
}

void printDigits(int digits)
{
  // utility function for clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

void initClock()
{
  saveSettingsToEEPROM();

  secondCounter = 0;
  menuActive = false;
  clockShown = true;
  clockAnimationActive = false;
  buttonTime = millis();
  menuMode = 0;
  lastSecond = 255;

  closeMyFile();
  holdTime = 0;
  sd.chdir("/");
  fileIndex = 0;
  singleGraphic = true;
  if (!logoPlayed) logoPlayed = true;

  if (!clockIniRead) readClockIni();

  if (!clockSet)
  {
    currentHour = 12;
    currentMinute = 0;
    setClock();
  }
  if (!enableSecondHand)
  {
    // 24 hour conversion
    if (hour12 && currentHour > 12) currentHour -= 12;
    if (hour12 && currentHour == 0) currentHour = 12;
    drawDigitsAndShow();
  }
}

void setClock()
{
  clockSet = false;
  clockAdjustState = false;
  currentSecond = 0;
  setClockHour();
  setClockMinute();
  adjustClock();
  lastSecond = 255;
  if (EEPROM.read(4) != 1) EEPROM.write(4, 1);
  // don't adjust second hand at midnight if set manually AT midnight
  if (currentHour == 0 && currentMinute == 0) clockAdjustState = true;
}

void drawDigitsAndShow()
{
  drawDigits();
  FastLED.show();
  debugClockDisplay();
}

void setClockHour()
{
  // set hour
  long lastButtonCheck = 0;
  long lastDigitFlash = 0;
  while (clockDigitSet == false)
  {
    buttonDebounce();
    irReceiver();

    // menu button
    if ((digitalRead(buttonMenuPin) == LOW || irCommand == 'M' || irMenuRepeat == true) && buttonPressed == false && buttonEnabled == true && millis() - lastButtonCheck > 100)
    {
      clockSetBlink = true;
      currentHour++;
      if (currentHour > 23) currentHour = 0;
      lastButtonCheck = millis();
      lastDigitFlash = millis();
      drawDigitsAndShow();
    }

    // flash digit
    if (millis() - lastDigitFlash > 250)
    {
      lastDigitFlash = millis();
      drawDigitsAndShow();
    }

    // next button
    if ((digitalRead(buttonNextPin) == LOW || irCommand == 'N') && buttonPressed == false && buttonEnabled == true)
    {
      buttonPressed = true;
      clockDigitSet = true;
    }
  }
}

void setClockMinute()
{
  // set minutes
  long lastButtonCheck = 0;
  long lastDigitFlash = 0;
  while (clockDigitSet == true)
  {
    buttonDebounce();
    irReceiver();
    // menu button
    if ((digitalRead(buttonMenuPin) == LOW  || irCommand == 'M' || irMenuRepeat == true) && buttonPressed == false && buttonEnabled == true && millis() - lastButtonCheck > 100)
    {
      clockSetBlink = true;
      currentMinute++;
      if (currentMinute > 59) currentMinute = 0;
      lastButtonCheck = millis();
      lastDigitFlash = millis();
      drawDigitsAndShow();
    }

    // flash digit
    if (millis() - lastDigitFlash > 250)
    {
      lastDigitFlash = millis();
      drawDigitsAndShow();
    }

    // next button
    if ((digitalRead(buttonNextPin) == LOW || irCommand == 'N') && buttonPressed == false && buttonEnabled == true)
    {
      buttonPressed = true;
      clockDigitSet = false;
      clockSetBlink = true;
    }
  }
}

void adjustClock()
{
  tm.Hour = currentHour;
  tm.Minute = currentMinute;
  tm.Second = currentSecond;
  RTC.write(tm);
  setTime(RTC.get());
}

// adjust the clock at midnight by the number of seconds indicated in clock.ini file
void clockDriftAdjustment()
{
  if (clockAdjust < 0)
  {
    if (hour() == 0 && minute() == 0 && second() == 0 && !clockAdjustState)
    {
      currentHour = 23;
      currentMinute = 59;
      currentSecond = 60 + clockAdjust;
      adjustClock();
      lastSecond = 255;
      clockAdjustState = true;
    }
  }
  else if (clockAdjust > 0)
  {
    if (hour() == 0 && minute() == 0 && second() == 0)
    {
      currentHour = 0;
      currentMinute = 0;
      currentSecond = clockAdjust;
      adjustClock();
      lastSecond = 255;
    }
  }
  if (hour() == 0 && minute() == 0 && second() > 0 && clockAdjustState) clockAdjustState = false;
}

void getCurrentTime()
{
  currentSecond = second();
  currentMinute = minute();
  currentHour = hour();
}

void showClock()
{
  if (currentSecond != lastSecond)
  {
    secondCounter++;
    lastSecond = currentSecond;
    statusLedFlicker();
    debugClockDisplay();

    // 24 hour conversion
    if (hour12 && currentHour > 12) currentHour -= 12;
    if (hour12 && currentHour == 0) currentHour = 12;

    // draw time
    if (enableSecondHand)
    {
      // offset second hand if required
      currentSecond = currentSecond + secondOffset;
      if (currentSecond >= 60) currentSecond -= 60;
      storeSecondHandColor();
      drawDigits();
      secondHand();
      FastLED.show();
    }
    // second hand disabled, so only draw time on new minute
    else if (currentSecond == 0)
    {
      drawDigitsAndShow();
    }

//    debugClockDisplay();

    // show an animation
    if (cycleTime != -1 && clockAnimationLength > 0 && (secondsIntoHour() % cycleTime) == 0 && clockSet == true)
    {
      secondCounter = 0;
      currentSecond = second();
      clockAnimationActive = true;
      clockShown = false;
      closeMyFile();
      abortImage = true;
      nextFolder[0] = '\0';
    }
    // this boolean is set here to avoid showing an animation immediately after clock being set
    else if (!clockSet) clockSet = true;
  }
}

int secondsIntoHour()
{
  return (currentMinute * 60) + currentSecond;
}

void drawDigits()
{
  clockDigit_1();
  clockDigit_2();
  clockDigit_3();
  clockDigit_4();
  if (!clockSet) clockSetBlink = !clockSetBlink;
  closeMyFile();
}

void storeSecondHandColor()
{
  getSecondHandIndex();
  offsetX = 0;
  offsetY = 176;
  strcpy_P(nextFolder, PSTR("/00system/digits.bmp"));
  // max brightness in order to store correct color value
  FastLED.setBrightness(255);
  bmpDraw(nextFolder, 0, 0);
  secondHandColor = leds[getIndex(secondHandX, secondHandY)];
  // restore brightness
  stripSetBrightness();
}

void clockDigit_1()
{
  char numChar[3];
  itoa(currentHour, numChar, 10);
  byte singleDigit = numChar[0] - '0';
  offsetX = -1;
  if (currentHour >= 10)
  {
    offsetY = singleDigit * 16;
  }
  else offsetY = 160;
  if (!clockSet && !clockDigitSet && !clockSetBlink)
  {
    offsetY = 160;
  }
  strcpy_P(nextFolder, PSTR("/00system/digits.bmp"));
  bmpDraw(nextFolder, 0, 0);
}

void clockDigit_2()
{
  char numChar[3];
  itoa(currentHour, numChar, 10);
  byte singleDigit;
  if (currentHour >= 10)
  {
    singleDigit = numChar[1] - '0';
  }
  else singleDigit = numChar[0] - '0';
  offsetX = 0;
  offsetY = singleDigit * 16;
  if (!clockSet && !clockDigitSet && !clockSetBlink)
  {
    offsetY = 160;
  }
  strcpy_P(nextFolder, PSTR("/00system/digits.bmp"));
  bmpDraw(nextFolder, 3, 0);
}

void clockDigit_3()
{
  char numChar[3];
  itoa(currentMinute, numChar, 10);
  byte singleDigit;
  if (currentMinute >= 10)
  {
    singleDigit = numChar[0] - '0';
  }
  else singleDigit = 0;
  offsetY = singleDigit * 16;
  if (!clockSet && clockDigitSet && !clockSetBlink)
  {
    offsetY = 160;
  }
  strcpy_P(nextFolder, PSTR("/00system/digits.bmp"));
  bmpDraw(nextFolder, 8, 0);
}

void clockDigit_4()
{
  char numChar[3];
  itoa(currentMinute, numChar, 10);
  byte singleDigit;
  if (currentMinute >= 10)
  {
    singleDigit = numChar[1] - '0';
  }
  else singleDigit = numChar[0] - '0';
  offsetY = singleDigit * 16;
  if (!clockSet && clockDigitSet && !clockSetBlink)
  {
    offsetY = 160;
  }
  strcpy_P(nextFolder, PSTR("/00system/digits.bmp"));
  bmpDraw(nextFolder, 12, 0);
}

void getSecondHandIndex()
{
  if (currentSecond < 16)
  {
    secondHandX = currentSecond;
    secondHandY = 0;
  }
  else if (currentSecond >= 16 && currentSecond < 30)
  {
    secondHandX = 15;
    secondHandY = (currentSecond - 15);
  }
  else if (currentSecond >= 30 && currentSecond < 46)
  {
    secondHandX = (15 - (currentSecond - 30));
    secondHandY = 15;
  }
  else if (currentSecond >= 46)
  {
    secondHandX = 0;
    secondHandY = (15 - (currentSecond - 45));
  }
}

void secondHand()
{
  getSecondHandIndex();
  leds[getIndex(secondHandX, secondHandY)] = secondHandColor;
}

// .INI file support

void printErrorMessage(uint8_t e, bool eol = true)
{
  switch (e) {
    case IniFile::errorNoError:
      Serial.print(F("no error"));
      break;
    case IniFile::errorFileNotFound:
      Serial.print(F("fnf"));
      break;
    case IniFile::errorFileNotOpen:
      Serial.print(F("fno"));
      break;
    case IniFile::errorBufferTooSmall:
      Serial.print(F("bts"));
      break;
    case IniFile::errorSeekError:
      Serial.print(F("se"));
      break;
    case IniFile::errorSectionNotFound:
      Serial.print(F("snf"));
      break;
    case IniFile::errorKeyNotFound:
      Serial.print(F("knf"));
      break;
    case IniFile::errorEndOfFile:
      Serial.print(F("eof"));
      break;
    case IniFile::errorUnknownError:
      Serial.print(F("unknown"));
      break;
    default:
      Serial.print(F("unknown error value"));
      break;
  }
  if (eol)
    Serial.println();
}

void readIniFile()
{
  const size_t bufferLen = 50;
  char buffer[bufferLen];
  char configFile[11];
  strcpy_P(configFile, PSTR("config.ini"));
  const char *filename = configFile;
  IniFile ini(filename);
  if (!ini.open()) {
    Serial.print(filename);
    Serial.println(F(" does not exist"));
    // Cannot do anything else
  }
  else
  {
    Serial.println(F("Ini file exists"));
  }

  // Check the file is valid. This can be used to warn if any lines
  // are longer than the buffer.
  if (!ini.validate(buffer, bufferLen)) {
    Serial.print(F("ini file "));
    Serial.print(ini.getFilename());
    Serial.print(F(" not valid: "));
    printErrorMessage(ini.getError());
    // Cannot do anything else
  }
  char section[10];
  strcpy_P(section, PSTR("animation"));
  char entry[11];
  strcpy_P(entry, PSTR("hold"));

  // Fetch a value from a key which is present
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("hold value: "));
    Serial.println(buffer);
    holdTime = atol(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    holdTime = 200;
  }

  strcpy_P(entry, PSTR("loop"));

  // Fetch a boolean value
  bool loopCheck;
  bool found = ini.getValue(section, entry, buffer, bufferLen, loopCheck);
  if (found) {
    Serial.print(F("animation loop value: "));
    // Print value, converting boolean to a string
    Serial.println(loopCheck ? F("TRUE") : F("FALSE"));
    folderLoop = loopCheck;
  }
  else {
    printErrorMessage(ini.getError());
    folderLoop = true;
  }

  strcpy_P(entry, PSTR("finish"));

  // Fetch a boolean value
  bool finishCheck;
  bool found4 = ini.getValue(section, entry, buffer, bufferLen, finishCheck);
  if (found4) {
    Serial.print(F("finish value: "));
    // Print value, converting boolean to a string
    Serial.println(finishCheck ? F("TRUE") : F("FALSE"));
    finishBeforeProgressing = finishCheck;
  }
  else {
    printErrorMessage(ini.getError());
    finishBeforeProgressing = false;
  }

  strcpy_P(section, PSTR("translate"));
  strcpy_P(entry, PSTR("moveX"));

  // Fetch a value from a key which is present
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("moveX value: "));
    Serial.println(buffer);
    offsetSpeedX = atoi(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    offsetSpeedX = 0;
  }

  strcpy_P(entry, PSTR("moveY"));

  // Fetch a value from a key which is present
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("moveY value: "));
    Serial.println(buffer);
    offsetSpeedY = atoi(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    offsetSpeedY = 0;
  }

  strcpy_P(entry, PSTR("loop"));

  // Fetch a boolean value
  bool loopCheck2;
  bool found2 = ini.getValue(section, entry, buffer, bufferLen, loopCheck2);
  if (found2) {
    Serial.print(F("translate loop value: "));
    // Print value, converting boolean to a string
    Serial.println(loopCheck2 ? F("TRUE") : F("FALSE"));
    moveLoop = loopCheck2;
  }
  else {
    printErrorMessage(ini.getError());
    moveLoop = false;
  }

  strcpy_P(entry, PSTR("panoff"));

  // Fetch a boolean value
  bool loopCheck3;
  bool found3 = ini.getValue(section, entry, buffer, bufferLen, loopCheck3);
  if (found3) {
    Serial.print(F("panoff value: "));
    // Print value, converting boolean to a string
    Serial.println(loopCheck3 ? F("TRUE") : F("FALSE"));
    panoff = loopCheck3;
  }
  else {
    printErrorMessage(ini.getError());
    panoff = true;
  }

  strcpy_P(entry, PSTR("nextFolder"));

  // Fetch a value from a key which is present
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("nextFolder value: "));
    Serial.println(buffer);
    memcpy(nextFolder, buffer, 8);
  }
  else {
    printErrorMessage(ini.getError());
    nextFolder[0] = '\0';
  }

  if (ini.isOpen()) ini.close();
}

void readClockIni()
{
  clockIniRead = true;
  const size_t bufferLen = 50;
  char buffer[bufferLen];
  char configFile[11];
  strcpy_P(configFile, PSTR("clock.ini"));
  const char *filename = configFile;
  IniFile ini(filename);
  sd.chdir("/00system");
  if (!ini.open()) {
    Serial.print(filename);
    Serial.println(F(" does not exist"));
    // Cannot do anything else
  }
  else
  {
    Serial.println(F("Ini file exists"));
  }

  // Check the file is valid. This can be used to warn if any lines
  // are longer than the buffer.
  if (!ini.validate(buffer, bufferLen)) {
    Serial.print(F("ini file "));
    Serial.print(ini.getFilename());
    Serial.print(F(" not valid: "));
    printErrorMessage(ini.getError());
    // Cannot do anything else
  }
  char section[10];
  strcpy_P(section, PSTR("clock"));
  char entry[11];
  strcpy_P(entry, PSTR("hour12"));

  // Fetch a boolean value
  // 12/24 hour mode
  bool loopCheck;
  bool found = ini.getValue(section, entry, buffer, bufferLen, loopCheck);
  if (found) {
    Serial.print(F("hour12 value: "));
    // Print value, converting boolean to a string
    Serial.println(loopCheck ? F("TRUE") : F("FALSE"));
    hour12 = loopCheck;
  }
  else {
    printErrorMessage(ini.getError());
    hour12 = true;
  }

  strcpy_P(entry, PSTR("second"));

  // Fetch a boolean value
  // show second hand
  found = ini.getValue(section, entry, buffer, bufferLen, loopCheck);
  if (found) {
    Serial.print(F("second value: "));
    // Print value, converting boolean to a string
    Serial.println(loopCheck ? F("TRUE") : F("FALSE"));
    enableSecondHand = loopCheck;
  }
  else {
    printErrorMessage(ini.getError());
    enableSecondHand = true;
  }

  strcpy_P(entry, PSTR("offset"));

  // Fetch a value from a key which is present
  // second hand position offset
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("offset value: "));
    Serial.println(buffer);
    secondOffset = atoi(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    secondOffset = 0;
  }

  strcpy_P(entry, PSTR("anim"));

  // Fetch a value from a key which is present
  // clock animation length in seconds
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("anim value: "));
    Serial.println(buffer);
    clockAnimationLength = atoi(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    clockAnimationLength = 5;
  }

  strcpy_P(entry, PSTR("adjust"));

  // Fetch a value from a key which is present
  // daily midnight offset in seconds
  if (ini.getValue(section, entry, buffer, bufferLen)) {
    Serial.print(F("adjust value: "));
    Serial.println(buffer);
    clockAdjust = atoi(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    clockAdjust = 0;
  }

  if (ini.isOpen()) ini.close();
  sd.chdir("/");
}

// Auto Brightness Control
void readABC()
{
  const size_t bufferLen = 50;
  char buffer[bufferLen];
  char configFile[11];
  strcpy_P(configFile, PSTR("clock.ini"));
  const char *filename = configFile;
  IniFile ini(filename);
  sd.chdir("/00system");
  if (!ini.open()) {
    Serial.print(filename);
    Serial.println(F(" does not exist"));
    // Cannot do anything else
  }
  else
  {
    Serial.println(F("Ini file exists"));
  }

  // Check the file is valid. This can be used to warn if any lines
  // are longer than the buffer.
  if (!ini.validate(buffer, bufferLen)) {
    Serial.print(F("ini file "));
    Serial.print(ini.getFilename());
    Serial.print(F(" not valid: "));
    printErrorMessage(ini.getError());
    // Cannot do anything else
  }
  char section[10];
  strcpy_P(section, PSTR("abc"));
  char entry[11];
  strcpy_P(entry, PSTR("abc"));

  // Fetch a boolean value
  // Use ABC?
  bool boolcheck;
  bool found = ini.getValue(section, entry, buffer, bufferLen, boolcheck);
  if (found) {
    Serial.print(F("abc value: "));
    // Print value, converting boolean to a string
    Serial.println(boolcheck ? F("TRUE") : F("FALSE"));
    abc = boolcheck;
  }
  else {
    printErrorMessage(ini.getError());
    abc = false;
  }

  //  check INI for 1440 time of day entries (takes too long)
  //  int minuteInDay = 0;
  //
  //  for (int h=0; h<24; h++)
  //  {
  //    for (int m=0; m<60; m++)
  //    {
  //      // construct time of day character array
  //      char timeOfDay[6];
  //      char minuteChar[3];
  //      itoa(h, timeOfDay, 10);
  //      strcat(timeOfDay, ":");
  //      itoa(m, minuteChar, 10);
  //      if (strlen(minuteChar) == 1) strcat(timeOfDay, "0");
  //      strcat(timeOfDay, minuteChar);
  //
  //      // Fetch a value from a key which is present
  //      // abc brightness level 0
  //      buffer[0] = '\0';
  //      if (ini.getValue(section, timeOfDay, buffer, bufferLen)) {
  //        if (strlen(buffer) > 0)
  //        {
  //          Abc abc0 = {minuteInDay, atoi(buffer)};
  //          Serial.print(timeOfDay);
  //          Serial.print(F(" brightness: "));
  //          Serial.println(atoi(buffer));
  //        }
  //      }
  //      minuteInDay++;
  //    }
  //  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc0", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc0.m = minuteCounter(h, m);
      abc0.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc0 value: ");
    Serial.print(abc0.m);
    Serial.print(", ");
    Serial.println(abc0.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc1", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc1.m = minuteCounter(h, m);
      abc1.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc1 value: ");
    Serial.print(abc1.m);
    Serial.print(", ");
    Serial.println(abc1.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc2", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc2.m = minuteCounter(h, m);
      abc2.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc2 value: ");
    Serial.print(abc2.m);
    Serial.print(", ");
    Serial.println(abc2.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc3", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc3.m = minuteCounter(h, m);
      abc3.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc3 value: ");
    Serial.print(abc3.m);
    Serial.print(", ");
    Serial.println(abc3.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc4", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc4.m = minuteCounter(h, m);
      abc4.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc4 value: ");
    Serial.print(abc4.m);
    Serial.print(", ");
    Serial.println(abc4.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc5", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc5.m = minuteCounter(h, m);
      abc5.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc5 value: ");
    Serial.print(abc5.m);
    Serial.print(", ");
    Serial.println(abc5.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc6", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc6.m = minuteCounter(h, m);
      abc6.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc6 value: ");
    Serial.print(abc6.m);
    Serial.print(", ");
    Serial.println(abc6.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc7", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc7.m = minuteCounter(h, m);
      abc7.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc7 value: ");
    Serial.print(abc7.m);
    Serial.print(", ");
    Serial.println(abc7.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc8", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc8.m = minuteCounter(h, m);
      abc8.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc8 value: ");
    Serial.print(abc8.m);
    Serial.print(", ");
    Serial.println(abc8.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "abc9", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      int h = atoi(strtok(buffer, ":"));
      int m = atoi(strtok(NULL, ":,"));
      abc9.m = minuteCounter(h, m);
      abc9.b = atoi(strtok(NULL, " ,"));
    }
    Serial.print("abc9 value: ");
    Serial.print(abc9.m);
    Serial.print(", ");
    Serial.println(abc9.b);
  }
  else {
    printErrorMessage(ini.getError());
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "colorCorrection", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      colorCorrection = strtoul(buffer, NULL, 16);
    }
    Serial.print("Color Correction: ");
    Serial.println(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    colorCorrection = 0xFFFFFF;
  }

  // Fetch a value from a key which is present
  if (ini.getValue(section, "colorTemperature", buffer, bufferLen)) {
    if (strlen(buffer) > 0)
    {
      colorTemperature = strtoul(buffer, NULL, 16);
    }
    Serial.print("Color Temperature: ");
    Serial.println(buffer);
  }
  else {
    printErrorMessage(ini.getError());
    colorTemperature = 0xFFFFFF;
  }

  if (ini.isOpen()) ini.close();
  sd.chdir("/");
}

void readRemoteIni()
{
  const size_t bufferLen = 50;
  char buffer[bufferLen];
  char configFile[11];
  strcpy_P(configFile, PSTR("remote.ini"));
  const char *filename = configFile;
  IniFile ini(filename);
  sd.chdir("/00system");
  if (!ini.open()) {
    Serial.print(filename);
    Serial.println(F(" does not exist"));
    // Cannot do anything else
  }
  else
  {
    Serial.println(F("Ini file exists"));
  }

  // Check the file is valid. This can be used to warn if any lines
  // are longer than the buffer.
  if (!ini.validate(buffer, bufferLen)) {
    Serial.print(F("ini file "));
    Serial.print(ini.getFilename());
    Serial.print(F(" not valid: "));
    printErrorMessage(ini.getError());
    // Cannot do anything else
  }
  char section[7];
  strcpy_P(section, PSTR("remote"));

  // Fetch a value from a key which is present
  // second hand position offset
  if (ini.getValue(section, "menu", buffer, bufferLen)) {
    Serial.print(F("IR Menu code: "));
    Serial.println(buffer);
    remoteCodeMenu = strtoull(buffer, NULL, 10);
  }
  else {
    printErrorMessage(ini.getError());
    remoteCodeMenu = 2155864095;
  }

  // Fetch a value from a key which is present
  // clock animation length in seconds
  if (ini.getValue(section, "next", buffer, bufferLen)) {
    Serial.print(F("IR Next code: "));
    Serial.println(buffer);
    remoteCodeNext = strtoull(buffer, NULL, 10);
  }
  else {
    printErrorMessage(ini.getError());
    remoteCodeNext = 2155831455;
  }

  // Fetch a value from a key which is present
  // daily midnight offset in seconds
  if (ini.getValue(section, "power", buffer, bufferLen)) {
    Serial.print(F("IR Power code: "));
    Serial.println(buffer);
    remoteCodePower = strtoull(buffer, NULL, 10);
  }
  else {
    printErrorMessage(ini.getError());
    remoteCodePower = 2155819215;
  }

  if (ini.isOpen()) ini.close();
  sd.chdir("/");
}

int minuteCounter(byte h, byte m)
{
  int minutes = 0;
  minutes = h * 60;
  minutes += m;
  return minutes;
}

// breakout code
void drawPaddle()
{
  leds[paddleIndex] = CRGB(200, 200, 200);
  leds[paddleIndex + 1] = CRGB(200, 200, 200);
  leds[paddleIndex + 2] = CRGB(200, 200, 200);
  FastLED.show();
}

void breakoutLoop()
{
  if (holdTime > 0) holdTime--;
  if (fileIndex > 0) fileIndex--;

  if (buttonEnabled == false)
  {
    if (millis() > buttonTime + 50) buttonEnabled = true;
  }

  // menu button
  if ((digitalRead(buttonNextPin) == LOW || irCommand == 'N' || irNextRepeat == true) && holdTime == 0 && paddleIndex < 237 && gameInitialized == true && buttonEnabled == true)
  {
    paddleIndex++;
    leds[paddleIndex - 1] = CRGB(0, 0, 0);
    drawPaddle();
    // paddle speed
    holdTime = 2500;
    if (ballMoving == false)
    {
      ballMoving = true;
      // ball speed
      swapTime = 5000;
      ballAngle = random8(190, 225);
    }
  }

  // next button
  else if ((digitalRead(buttonMenuPin) == LOW || irCommand == 'M' || irMenuRepeat == true) && holdTime == 0 && paddleIndex > 224 && buttonEnabled == true)
  {
    paddleIndex--;
    leds[paddleIndex + 3] = CRGB(0, 0, 0);
    drawPaddle();
    // paddle speed
    holdTime = 2500;
    if (ballMoving == false)
    {
      ballMoving = true;
      // ball speed
      swapTime = 5000;
      ballAngle = random8(135, 170);
    }
  }

  else if ((digitalRead(buttonNextPin) == HIGH || digitalRead(buttonMenuPin) == HIGH || irCommand == 'M' || irCommand == 'N') && gameInitialized == false) gameInitialized = true;

  // ball logic
  if (ballMoving == true && fileIndex == 0)
  {
    fileIndex = swapTime;
    leds[ballIndex] = CRGB(0, 0, 0);

    // did the player lose?
    if (ballIndex >= 239)
    {
      ballMoving = false;
      breakout = false;
      Serial.print(F("Lose!!!"));
      for (int c = 250; c >= 0; c = c - 10)
      {
        for (int i = 0; i < NUM_LEDS; i++)
        {
          byte r = 0;
          byte g = 0;
          byte b = 0;
          if (random8(0, 2) == 1)
          {
            r = c;
          }
          if (random8(0, 2) == 1)
          {
            g = c;
          }
          if (random8(0, 2) == 1)
          {
            b = c;
          }
          leds[i] = CRGB(r, g, b);
        }
        FastLED.show();
        delay(50);
      }
    }

    // ball still in play
    else
    {
      if (ballAngle < 180)
      {
        if ((ballX + sin(degToRad(ballAngle)) * 16) + .5 > 256) swapXdirection();
      }
      else
      {
        if ((ballX + sin(degToRad(ballAngle)) * 16) + .5 < 0) swapXdirection();
      }
      ballX = ballX + sin(degToRad(ballAngle)) * 16 + .5;

      if (ballAngle > 90 && ballAngle < 270)
      {
        if ((ballY + cos(degToRad(ballAngle)) * 16) + .5 < 0) swapYdirection();
      }
      else
      {
        if ((ballY + cos(degToRad(ballAngle)) * 16) + .5 > 256) swapYdirection();
      }
      ballY = ballY + cos(degToRad(ballAngle)) * 16 + .5;
      ballIndex = getScreenIndex(ballX, ballY);

      // paddle hit?
      if (ballIndex == paddleIndex or ballIndex == paddleIndex + 1 or ballIndex == paddleIndex + 2)
      {
        // move the ball back in time one step
        ballX = ballX + ((sin(degToRad(ballAngle)) * 16 + .5) * -1);
        ballY = ballY + ((cos(degToRad(ballAngle)) * 16 + .5) * -1);
        swapYdirection();
        if (ballIndex == paddleIndex)
        {
          ballAngle = random8(115, 170);
        }
        else if (ballIndex == paddleIndex + 2)
        {
          ballAngle = random8(190, 245);
        }
        ballIndex = getScreenIndex(ballX, ballY);
        leds[paddleIndex] = CRGB(200, 200, 200);
        leds[paddleIndex + 1] = CRGB(200, 200, 200);
        leds[paddleIndex + 2] = CRGB(200, 200, 200);
      }

      // brick hit?
      if (leds[ballIndex].r > 0 || leds[ballIndex].g > 0 || leds[ballIndex].b > 0)
      {
        // speed up and change direction
        swapTime -= 35;
        swapYdirection();
        if (winCheck())
        {
          Serial.print(F("Win!!!"));
          for (byte flashes=0; flashes < 30; flashes++)
          {
            leds[ballIndex] = CRGB(random8(), random8(), random8());
            FastLED.show();
            delay(50);
          }
          ballMoving = false;
          chdirFirework();
          char bmpFile[7]; // 2-digit number + .bmp + null byte
          for (byte fileIndex = 0; fileIndex < 37; fileIndex++)
          {
            itoa(fileIndex, bmpFile, 10);
            strcat(bmpFile, ".bmp");
            bmpDraw(bmpFile, 0, 0);
            delay(90);
          }
          breakout = false;
        }
      }

      // check for preceeding win
      if (breakout == true)
      {
        leds[ballIndex] = CRGB(175, 255, 15);
        FastLED.show();
      }
    }
  }
}

void chdirFirework()
{
  char tmp[20];
  strcpy_P(tmp, PSTR("/00system/firework"));
  sd.chdir(tmp);
}

boolean winCheck()
{
  byte numberOfLitPixels = 0;
  for (byte i = 0; i < 255; i++)
  {
    if (leds[i].r > 0 || leds[i].b > 0 || leds[i].g > 0)
    {
      numberOfLitPixels++;
    }
  }
  if (numberOfLitPixels <= 4)
  {
    // why is this delay needed?? Function always returns true without it.
    delay(1);
    return true;
  }
}

byte getScreenIndex(byte x, byte y)
{
  byte screenX = x / 16;
  byte screenY = y / 16;
  byte index;
  index = screenY * 16;
  if (screenY == 0)
  {
    index = 15 - screenX;
  }
  else if (screenY % 2 != 0)
  {
    index = (screenY * 16) + screenX;
  }
  else
  {
    index = (screenY * 16 + 15) - screenX;
  }
  return index;
}

void swapYdirection()
{
  if (ballAngle > 90 && ballAngle < 270)
  {
    if (ballAngle > 180)
    {
      ballAngle = 360 - (ballAngle - 180);
    }
    else
    {
      ballAngle = 90 - (ballAngle - 90);
    }
  }
  else
  {
    if (ballAngle < 90)
    {
      ballAngle = 90 + (90 - ballAngle);
    }
    else
    {
      ballAngle = 180 + (360 - ballAngle);
    }
  }
}

void swapXdirection()
{
  if (ballAngle < 180)
  {
    if (ballAngle < 90)
    {
      ballAngle = 270 + (90 - ballAngle);
    }
    else ballAngle = 270 - (ballAngle - 90);
  }
  else
  {
    if (ballAngle > 270)
    {
      ballAngle = 360 - ballAngle;
    }
    else ballAngle = 180 - (ballAngle - 180);
  }
}

float degToRad(float deg)
{
  float result;
  result = deg * PI / 180;
  return result;
}

// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

uint16_t read16(SdFile& f) {
  uint16_t result;
  f.read(&result, 2);
  return result;
}

uint32_t read32(SdFile& f) {
  uint32_t result;
  f.read(&result, 4);
  return result;
}

// clock debug function
void print2digits(int number) {
  if (number >= 0 && number < 10) {
    Serial.write('0');
  }
  Serial.print(number);
}

plumped avatar Nov 20 '21 13:11 plumped

There is no vwd() in SdFat V2.x. for SdFs since I could not find a reliable way to support vwd for FAT16/FAT32 and exFAT for types SdFat, SdExFat and SdFs. There is a private vwd() for classes SdFat and SdExFat that is not user accessible.

I Also don't want a user accessible vwd() since it has caused problems because of conflicts between internal use and application use.

For users up to now it has been easy to simply open a directory file when using open by index.

 FsFile cwd; 
 cwd.open(dirPath);
 open(&cwd, index, mode);

Looks like I need a easy way to open a file in the current volume working directory.

Maybe something like file.open(&sd, index, mode). I already have a file.open(&sd, path, mode) it is used in open(path, mode):

bool ExFatFile::open(const char* path, oflag_t oflag) {
  return open(ExFatVolume::cwv(), path, oflag);
}

Then I could also have an file.open(index, mode) that opened a file by index on the current working directory.

It will be simple to do but will be a while since I have lots of testing for other features in the next version.

greiman avatar Nov 20 '21 16:11 greiman

Thanks for your quick feedback! So is there any workarround known for the requested? Or is the easiest way to buy a teensy 3.x? xD

Im a complete noob with teensy btw.

plumped avatar Nov 20 '21 16:11 plumped

No easy workaround and you will have the problem with any V 2.x of SdFat. I guess Teensy 3.x still supports V 1.x.

A not so easy workaround is to add a cwd file: SdBaseFile cwd;

Add this function

void sdChdir(const char* path) {
 cwd.close();
 cwd.open(path);
 sd.chdir(path);
}

and replace sd.chdir(path) with sdChdir(path).

replace sd.vwd() with &cwd like this:

myFile.open(sd.vwd(), fileIndex, O_READ);

becomes

myFile.open(&cwd, fileIndex, O_READ);

greiman avatar Nov 20 '21 17:11 greiman

thank you for the explanations! I'll try out the whole thing again with your input, but have also ordered a 3.6 for current use in the LED-Matrix. You are now on my watchlist and I look forward to the new features.

For 4.x is still the beta variant is that correct so far?

Have a nice sunday!

plumped avatar Nov 21 '21 09:11 plumped

For 4.x is still the beta variant is that correct so far?

I don't know which version is used in Teensy. I don't develop the Teensy wrapper.

greiman avatar Nov 21 '21 14:11 greiman

I added a call in SdFat-beta that opens a file by index in the current working directory which is volume working directory, sd.vwd(), if you have only one SD.

That should only require replacing: myFile.open(sd.vwd(), fileIndex, O_READ); by myFile.open(fileIndex, O_READ);

myFile.open(filename, O_READ) opens a file in the current working directory by name.

greiman avatar Nov 25 '21 17:11 greiman

Brilliant! Thank you!

plumped avatar Nov 25 '21 22:11 plumped