#define CHAR_TABLE_ASCII //Na LCD jsou jen ENG znaky takze ASCII
#define WOKWI //Kód fungující na simulátoru https://wokwi.com/projects/400945750495226881
//#define SERIALDEBUG
#ifdef WOKWI
#include "TimeForStoriesCore.h"
#else
#include "src/TimeForStoriesCore.h"
#endif
TimeForStoriesCore _tfsCore;
#ifdef WOKWI
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C _lcd1(0x27, 20, 4); // set the LCD address to 0x27 for a 20 chars and 4 line display
LiquidCrystal_I2C _lcd2(0x28, 20, 4);
LiquidCrystal_I2C _lcd3(0x29, 20, 4);
#endif
#define BRIGHTNESS_TIMER_MS 50 //Jak často se aktualizuje celkový jas
#define PHOTORES_MAX_VAL 1015 //Maximální hodnota kterou zle dosáhnout výstupu Photoresistoru
#define PHOTORES_MIN_VAL 8 //Minimální
#define PIN_LEDS_RGB 11 //Pin RGB pasek
#define PIN_PHOTORES A7 //Pin Photoresistoru
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif
Adafruit_NeoPixel _pixelsRGB = Adafruit_NeoPixel(TimeForStoriesCore::CLOCK_TABLE_SIZE, PIN_LEDS_RGB, NEO_GRB + NEO_KHZ800); // 5V a (12V 1LED na kanál)
//------ RTC (address 0x68, RAM address 0x57)
#ifdef WOKWI
#include <RTClib.h>
RTC_DS1307 _rtc;
#else
#include <DS3232RTC.h>
DS3232RTC _rtc;
#endif
//------ RTC - timezone
#include <Timezone.h> //#include "TimeLib.h" je součástí timezone
Timezone _timeZone(0);
//Australia Eastern Time Zone (Sydney, Melbourne)
//TimeChangeRule DST = { "AEDT", First, Sun, Oct, 2, 660 }; //UTC + 11 hours
//TimeChangeRule STD = { "AEST", First, Sun, Apr, 3, 600 }; //UTC + 10 hours
//US Eastern Time Zone (New York, Detroit)
//TimeChangeRule DST = { "EDT", Second, Sun, Mar, 2, -240 }; //Eastern Daylight Time = UTC - 4 hours
//TimeChangeRule STD = { "EST", First, Sun, Nov, 2, -300 }; //Eastern Standard Time = UTC - 5 hours
//------ RTC
//------ EEPROM
#include <EEPROM.h>
#define EEPROM_SETTINGS_MAGICNUMBER 80
//------ EEPROM
struct CoreSettings
{
//True pokud má každou sec blikat X
uint8_t secBlink = true;
//Čas animace změny textu
uint32_t milisAnim = 3000;
//Barva podsvícení Hodin
uint32_t colorHour = 0;
//Barva podsvícení Minut
uint32_t colorMinute = 0;
//Barva podsvícení Sekund
uint32_t colorSecond = 0;
uint8_t brightnessStep = 2; //1-10
uint8_t brightnessMin = 60; //50
uint8_t brightnessMax = 255;
//Pravidlo pro letni cas
TimeChangeRule dstRule;
//Pravidlo pro normalni cas
TimeChangeRule stdRule;
};
struct ActualValues
{
uint8_t hours = 0;
uint8_t minutes = 0;
uint8_t seconds = 0;
uint16_t millis = 0;
//Výpočet milisekund (RTC má rozlišení jen na vteřiny)
uint32_t lastMillis = 0; //Výpočet milisekund
uint32_t nextBrightness = 0; //Detekce aktualizace podsvícení
boolean forceLEDUpdate = false;
uint32_t lastLightCode = 0; //Detekce aktualizace LCD
uint8_t lastBrightness = 255; //Podsvícení LED(LCD)
void ResetTimers()
{
lastMillis = 0;
nextBrightness = 0;
}
};
//Nastavení aplikace (+ ukládání do EEPROM)
CoreSettings _settings;
ActualValues _actVal;
void setup()
{
#ifdef SERIALDEBUG
Serial.begin(115200);
#endif
#ifdef WOKWI
#ifdef SERIALDEBUG
Serial.println("Init LCD ");
#endif
// initialize the 20x4 lcd module + enable backlight
_lcd1.init(); _lcd1.backlight();
_lcd2.init(); _lcd2.backlight();
_lcd3.init(); _lcd3.backlight();
#endif
//Načtení konfigurace
if (EEPROM.read(0) != EEPROM_SETTINGS_MAGICNUMBER)
{
#ifdef SERIALDEBUG
Serial.println("EEPROM - default");
#endif
//Nastavit default
//Central European Time (Frankfurt, Paris, Prague)
//CEST plati od 1:00 UTC (2:00 CET, resp. 3:00 CEST) posledni nedele v breznu do 1:00 UTC (3:00 CEST, resp. 2:00 CET) posledni nedele v rijnu.
const TimeChangeRule dst PROGMEM = { "CEST", Last, Sun, Mar, 2, 120 }; //Central European Summer Time +1h +1h
const TimeChangeRule std PROGMEM = { "CET", Last, Sun, Oct, 3, 60 }; //Central European Standard Time +1h
_settings.dstRule = dst;
_settings.stdRule = std;
_settings.colorHour = _pixelsRGB.Color(0, 0, 255);
_settings.colorMinute = _pixelsRGB.Color(0, 255, 0);
_settings.colorSecond = _pixelsRGB.Color(255, 255, 255);
EEPROM.put(0, EEPROM_SETTINGS_MAGICNUMBER);
EEPROM.put(1, _settings);
}
else
{
EEPROM.get(1, _settings);
};
//Aplikování pravidel pro letní a zimní čas
_timeZone.setRules(_settings.dstRule, _settings.stdRule);
//Aplikování nastavení TimeForStories
_tfsCore.Init(_settings.secBlink);
//Inicializace RTC
_rtc.begin();
#ifdef WOKWI
_rtc.adjust(_timeZone.toUTC(_rtc.now().unixtime())); //Natavení RTC na UTC čas.
#endif
SetRTCTime(2024, 1, 1, 23, 59, 55); //Debug možnost nastavení jakéhokoliv času
//SetRTCTime(2024, 1, 1, 1, 59, 55);
//SetRTCTime(2024, 1, 1, 4, 9, 55);
//SetRTCTime(2024, 1, 1, 10, 29, 50);
// Inicializace NeoPixel.
_pixelsRGB.begin();
_pixelsRGB.fill(_pixelsRGB.Color(0, 0, 0), 0, TimeForStoriesCore::CLOCK_TABLE_SIZE);
_pixelsRGB.show();
UpdateActualTime();
// #ifdef SERIALDEBUG
// Serial.println("Init Test");
// #endif
// Test(0);
// Test(1);
//Prvotní vykreslení (pokud není zapnuto blikání sec, LED se můžou aktualizovat až za 1min.)
_actVal.lastLightCode = _tfsCore.GetLightCode(_actVal.hours, _actVal.minutes, _actVal.seconds);
uint8_t *ledArray = _tfsCore.GetCharacterArray(_actVal.lastLightCode);
ShowBytesToLED(ledArray, _settings.colorHour, _settings.colorMinute, _settings.colorSecond);
ShowBytesToLCD(ledArray);
#ifdef SERIALDEBUG
Serial.println("Init Copleted - Start");
#endif
}
void loop()
{
uint32_t loopMillis = millis();
UpdateActualTime(); //Aktuální Lokální čas včetně milisekund.
if(loopMillis - _actVal.nextBrightness > BRIGHTNESS_TIMER_MS) //Bezpečné millis() rollover
{
_actVal.nextBrightness = loopMillis;
SetLEDBrightness();
}
// ledArray = viditelnost jednotlivých znaků 0=skrytý až 255=viditelný, 110 hodnot
uint8_t *ledAniArray = _tfsCore.UpdateClockWhenNeeded(_actVal.hours, _actVal.minutes, _actVal.seconds, _actVal.millis, _settings.milisAnim, _actVal.forceLEDUpdate);
if (ledAniArray != nullptr)
{ //Je čas na aktualizaci
ShowBytesToLED(ledAniArray, _settings.colorHour, _settings.colorMinute, _settings.colorSecond);
//Detekce změny pro LCD (Bez animace)
uint32_t lightCode = _tfsCore.GetLightCode(_actVal.hours, _actVal.minutes); //bez ..,sec) LCD se překresluje pomalu
if (lightCode != _actVal.lastLightCode)
{ //Je jiné zobrazení na
_actVal.lastLightCode = lightCode;
ShowBytesToLCD(_tfsCore.GetCharacterArray(lightCode)); //Bez animace jen 0/255
#ifdef SERIALDEBUG
Serial.println(String(lightCode));
#endif
}
_actVal.forceLEDUpdate = false;
}
#ifdef WOKWI
_lcd1.setCursor(0, 0);
_lcd1.print(FormatPadRight(FormatTime(_actVal.hours, _actVal.minutes, _actVal.seconds, _actVal.millis, F(":")), 15, F(" ")) + FormatPadLeft(String(_actVal.lastBrightness),5,F(" ")));
#endif
//Serial.println(String(millis() - loopMillis));
}
void SetLEDBrightness()
{
//Podle intezity světla? https://docs.wokwi.com/parts/wokwi-photoresistor-sensor
int av = analogRead(PIN_PHOTORES);
int16_t brightness = (int16_t)map(av, PHOTORES_MAX_VAL, PHOTORES_MIN_VAL, 0, 255); //0-255
//Tolerance +/- Step
uint8_t tolr = _settings.brightnessStep;
int16_t lstB = _actVal.lastBrightness; //Přetypování pro bezpečné odečítání
int16_t minB = max(lstB - tolr, 0 + tolr);
int16_t maxB = min(lstB + tolr, 255 - tolr);
//Korekce pokud jsem mimo toleranci
if (brightness < minB) { lstB = max(lstB - _settings.brightnessStep, _settings.brightnessMin); }
if (brightness > maxB) { lstB = min(lstB + _settings.brightnessStep, _settings.brightnessMax); }
//Případný zápis
if (_actVal.lastBrightness != lstB)
{
_actVal.lastBrightness = (uint8_t)lstB;
_pixelsRGB.setBrightness(_actVal.lastBrightness); //Nastavení na LED
_pixelsRGB.show();
//_actVal.forceLEDUpdate = true;
}
}
void UpdateActualTime()
{
#ifdef WOKWI
time_t dtNow = _rtc.now().unixtime();
#else
time_t dtNow = _rtc.get(); //Nahradit interním časovačem s korekcí driftu, a sync s RTC cca každou minutu
#endif
time_t t = _timeZone.toLocal(dtNow); //Převod z UTC na lokální čas
//Výpočet Milisekund (počítají se od detekce změny sec) (RTC pracuje s přesností na SEC)
uint8_t sec = second(t);
if (_actVal.seconds != sec) { _actVal.lastMillis = millis(); }
_actVal.hours = hour(t);
_actVal.minutes = minute(t);
_actVal.seconds = sec;
_actVal.millis = millis() - _actVal.lastMillis; //Bezpečené na rollaout
}
void SetRTCTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second)
{
_actVal.ResetTimers();
tmElements_t dtTime;
dtTime.Year = CalendarYrToTm(year); //Do elements se to uklada jako rozdíl od 1970 :(
dtTime.Month = month;
dtTime.Day = day;
dtTime.Hour = hour;
dtTime.Minute = minute;
dtTime.Second = second;
time_t t = _timeZone.toUTC(makeTime(dtTime));
#ifdef WOKWI
_rtc.adjust(t);
#else
_rtc.set(t);
#endif
}
void ShowBytesToLED(uint8_t* ledidx, uint32_t colorHour, uint32_t colorMinute, uint32_t colorSecond)
{
if (ledidx != nullptr)
{
uint32_t col = colorHour;
for (uint8_t idxLed = 0; idxLed < TimeForStoriesCore::CLOCK_TABLE_SIZE; idxLed++)
{
if (idxLed == 60) { col = colorSecond; }
if (idxLed == 61) { col = colorMinute; }
_pixelsRGB.setPixelColor(idxLed, ColorBrightness(col, ledidx[idxLed]));
}
_pixelsRGB.show(); // This sends the updated pixel color to the hardware.
}
}
uint32_t ColorBrightness(uint32_t color, uint8_t brightness)
{
if (brightness == 255) //Maximální jas není co počítat
{
return color;
}
else if (brightness == 0) //Žádný jas není co počítat
{
return _pixelsRGB.Color(0, 0, 0);
}
else
{
//Brightness je počítán 8-bit aritmetikou s následujícím významem
// 0 = Maximální jas
// 1 = Minimální jas
//255 = Těsně pod maximálním jasem
brightness = brightness + 1; //tedy 0=1 a 255=0
//Rozbalím barvu
uint8_t r = (uint8_t)(color >> 16), g = (uint8_t)(color >> 8), b = (uint8_t)color;
//Aplikuji jas
r = (r * brightness) >> 8;
g = (g * brightness) >> 8;
b = (b * brightness) >> 8;
//Zabalím barvu
return _pixelsRGB.Color(r, g, b);
}
}
void ShowBytesToLCD(uint8_t* ledidx)
{
if (ledidx != NULL)
{
const String empty = F(" ");
String disp;
for (uint8_t idxLed = 0; idxLed < TimeForStoriesCore::CLOCK_TABLE_SIZE; idxLed++) //01- (1-počet LED)
{
if (ledidx[idxLed] > 0)
{ //Pixel svítí
disp.concat(_tfsCore.charactersTable[idxLed]); //Pixel na znak
}
else
{ //Pixel je zhasnutý
disp.concat(empty);
}
//Zobrazení řádku
if ((idxLed + 1) % 11 == 0)
{
PrintRowToLCD(idxLed, disp);
disp = "";
}
}
}
}
void PrintRowToLCD(const uint8_t idx, String row)
{
#ifdef WOKWI
if (idx < 20) { _lcd1.setCursor(0, 1); _lcd1.print(row); } //10
else if (idx < 30) { _lcd1.setCursor(0, 2); _lcd1.print(row); } //21
else if (idx < 40) { _lcd1.setCursor(0, 3); _lcd1.print(row); } //32
else if (idx < 50) { _lcd2.setCursor(0, 0); _lcd2.print(row); } //43
else if (idx < 60) { _lcd2.setCursor(0, 1); _lcd2.print(row); } //54
else if (idx < 70) { _lcd2.setCursor(0, 2); _lcd2.print(row); } //65
else if (idx < 80) { _lcd2.setCursor(0, 3); _lcd2.print(row); } //76
else if (idx < 90) { _lcd3.setCursor(0, 0); _lcd3.print(row); } //87
else if (idx < 100){ _lcd3.setCursor(0, 1); _lcd3.print(row); } //98
else { _lcd3.setCursor(0, 2); _lcd3.print(row); } //109
#endif
}
void Test(uint8_t test)
{
uint32_t lightCode = 1;
uint8_t *ledAniArray;
for (uint8_t t = 0; t < 32; t++)
{
if (test == 0)
{
lightCode = lightCode << 1;
}
else
{
bitSet(lightCode,t);
}
#ifdef WOKWI
_lcd1.setCursor(0, 0);
_lcd1.print(FormatPadLeft(String(t+1),2,F(" ")));
#endif
ledAniArray = _tfsCore.GetCharacterArray(lightCode);
ShowBytesToLCD(ledAniArray); //Bez animace jen 0/255
ShowBytesToLED(ledAniArray, _settings.colorHour, _settings.colorMinute, _settings.colorSecond);
delay((100));
}
}
//01.01.2000 10:00:00
// String FormatDateTime_t(const time_t time)
// {
// return FormatDate(day(time), month(time), year(time)) + String(F(" ")) + FormatTime(hour(time), minute(time), second(time), 0, F(":"));
// }
//10:00:00
// String FormatTime_t(const time_t time)
// {
// return FormatTime_t(time, F(":"), false);
// }
//10:00:00 nebo 34:00:00 ":"=zadany oddelovac
// String FormatTime_t(const time_t time, const String separator, const boolean plusDays)
// {
// uint16_t hours = hour(time);
// if (plusDays) { hours += elapsedDays(time) * 24; }
// return FormatTime(hours, minute(time), second(time), 0, separator);
// }
//10:00:00:0000 ":"=zadany oddelovac
String FormatTime(const uint8_t hour, const uint8_t minute, const uint8_t second, const uint16_t millisec, const String separator)
{
const String zero = F("0");
return FormatPadLeft(String(hour), 2, zero) + separator + FormatPadLeft(String(minute), 2, zero) + separator + FormatPadLeft(String(second), 2, zero) + separator + FormatPadLeft(String(millisec), 3, zero);
}
//20.01.2000
// String FormatDate(const uint8_t day, const uint8_t month, const uint16_t year)
// {
// const String zero = F("0");
// const String sep = F(".");
// return FormatPadLeft(String(day), 2, zero) + sep + FormatPadLeft(String(month), 2, zero) + sep + String(year);
// }
//Z leva doplni zadanym znakem (případně ořízne na požadovanou délku)
String FormatPadLeft(const String value, const uint8_t len, const String padChar)
{
String padString;
for (uint8_t i = 0; i < len; i++)
{
padString.concat(padChar);
}
padString.concat(value);
return padString.substring(padString.length() - len);
}
//Z prava doplni zadanym znakem (případně ořízne na požadovanou délku)
String FormatPadRight(const String value, const uint8_t len, const String padChar)
{
String padString = value;
for (uint8_t i = 0; i < len; i++)
{
padString.concat(padChar);
}
return padString.substring(0, len);
}