Initial
This commit is contained in:
693
src/connectivity/ntp_client_plus.cpp
Normal file
693
src/connectivity/ntp_client_plus.cpp
Normal file
@@ -0,0 +1,693 @@
|
||||
#include <Arduino.h>
|
||||
#include "ntp_client_plus.h"
|
||||
|
||||
/**
|
||||
* @brief Construct a new NTPClientPlus::NTPClientPlus object
|
||||
*
|
||||
* @param udp UDP client
|
||||
* @param poolServerName time server name
|
||||
* @param utcx UTC offset (in 1h)
|
||||
* @param _swChange should summer/winter time be considered
|
||||
*/
|
||||
NTPClientPlus::NTPClientPlus(UDP &udp, const char *poolServerName, int utcx, bool _swChange)
|
||||
{
|
||||
this->_udp = &udp;
|
||||
this->_utcx = utcx;
|
||||
this->_timeOffset = this->secondperhour * this->_utcx;
|
||||
this->_poolServerName = poolServerName;
|
||||
this->_swChange = _swChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Starts the underlying UDP client, get first NTP timestamp and calc date
|
||||
*
|
||||
*/
|
||||
void NTPClientPlus::setupNTPClient()
|
||||
{
|
||||
this->_udp->begin(this->_port);
|
||||
this->_udpSetup = true;
|
||||
this->updateNTP();
|
||||
this->calcDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get new update from NTP
|
||||
*
|
||||
* @return NTP_UPDATE_TIMEOUT timeout after 500 ms
|
||||
* @return NTP_UPDATE_SUCCESS after successful update
|
||||
* @return NTP_UPDATE_DIFFTOOHIGH too much difference to previous received time (try again)
|
||||
* @return NTP_UPDATE_TIME_INVALID time value is invalid
|
||||
*/
|
||||
int NTPClientPlus::updateNTP()
|
||||
{
|
||||
|
||||
// flush any existing packets
|
||||
while (this->_udp->parsePacket() != 0)
|
||||
{
|
||||
this->_udp->flush();
|
||||
}
|
||||
|
||||
this->sendNTPPacket();
|
||||
|
||||
// Wait till data is there or timeout...
|
||||
uint8_t conn_tries = 0;
|
||||
int received_bytes = 0;
|
||||
|
||||
while ((received_bytes == 0) && (conn_tries++ <= MAX_NTP_CONN_TRIES))
|
||||
{
|
||||
received_bytes = this->_udp->parsePacket();
|
||||
wait(NTP_RECEIVE_WAIT_TIME_MS);
|
||||
}
|
||||
|
||||
if (conn_tries >= MAX_NTP_CONN_TRIES)
|
||||
{
|
||||
return NTP_UPDATE_TIMEOUT;
|
||||
}
|
||||
|
||||
this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE);
|
||||
|
||||
unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]);
|
||||
unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]);
|
||||
// combine the four bytes (two words) into a long integer
|
||||
// this is NTP time (seconds since Jan 1 1900):
|
||||
unsigned long tempSecsSince1900 = highWord << 16 | lowWord;
|
||||
|
||||
if (tempSecsSince1900 < UNIX_TIMESTAMP_1900) // NTP time is not valid
|
||||
{
|
||||
return NTP_UPDATE_TIME_INVALID;
|
||||
}
|
||||
|
||||
// check if time off last ntp update is roughly in the same range: 100sec apart (validation check)
|
||||
if (this->_lastSecsSince1900 == 0 || tempSecsSince1900 - this->_lastSecsSince1900 < 100000)
|
||||
{
|
||||
// Only update time then
|
||||
this->_lastUpdate = millis() - (NTP_RECEIVE_WAIT_TIME_MS * (conn_tries + 1)); // Account for delay in reading the time
|
||||
this->_secsSince1900 = tempSecsSince1900;
|
||||
this->_currentEpoc = this->_secsSince1900 - UNIX_TIMESTAMP_1900;
|
||||
|
||||
// Remember time of last update
|
||||
this->_lastSecsSince1900 = tempSecsSince1900;
|
||||
return NTP_UPDATE_SUCCESS; // return 0 after successful update
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remember time of last update
|
||||
this->_lastSecsSince1900 = tempSecsSince1900;
|
||||
return NTP_UPDATE_DIFFTOOHIGH;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops the underlying UDP client
|
||||
*
|
||||
*/
|
||||
void NTPClientPlus::end()
|
||||
{
|
||||
this->_udp->stop();
|
||||
this->_udpSetup = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Setter TimeOffset
|
||||
*
|
||||
* @param timeOffset offset from UTC in seconds
|
||||
*/
|
||||
void NTPClientPlus::setTimeOffset(int timeOffset)
|
||||
{
|
||||
this->_timeOffset = timeOffset;
|
||||
}
|
||||
|
||||
long NTPClientPlus::getTimeOffset()
|
||||
{
|
||||
return this->_timeOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set time server name
|
||||
*
|
||||
* @param poolServerName
|
||||
*/
|
||||
void NTPClientPlus::setPoolServerName(const char *poolServerName)
|
||||
{
|
||||
this->_poolServerName = poolServerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calc seconds since 1. Jan. 1900
|
||||
*
|
||||
* @return unsigned long seconds since 1. Jan. 1900
|
||||
*/
|
||||
unsigned long NTPClientPlus::getSecsSince1900() const
|
||||
{
|
||||
return this->_timeOffset + // User offset
|
||||
this->_secsSince1900 + // seconds returned by the NTP server
|
||||
((millis() - this->_lastUpdate) / 1000); // Time since last update
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get UNIX Epoch time since 1. Jan. 1970
|
||||
*
|
||||
* @return unsigned long UNIX Epoch time since 1. Jan. 1970 in seconds
|
||||
*/
|
||||
unsigned long NTPClientPlus::getEpochTime() const
|
||||
{
|
||||
return this->getSecsSince1900() - UNIX_TIMESTAMP_1900;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current hours in 24h format
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int NTPClientPlus::getHours24() const
|
||||
{
|
||||
int hours = ((this->getEpochTime() % 86400L) / 3600);
|
||||
return hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current hours in 12h format
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int NTPClientPlus::getHours12() const
|
||||
{
|
||||
int hours = this->getHours24();
|
||||
if (hours >= 12)
|
||||
{
|
||||
hours = hours - 12;
|
||||
}
|
||||
return hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current minutes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int NTPClientPlus::getMinutes() const
|
||||
{
|
||||
return ((this->getEpochTime() % 3600) / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current seconds
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int NTPClientPlus::getSeconds() const
|
||||
{
|
||||
return (this->getEpochTime() % 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @return String time formatted like `hh:mm:ss`
|
||||
*/
|
||||
String NTPClientPlus::getFormattedTime() const
|
||||
{
|
||||
unsigned long rawTime = this->getEpochTime();
|
||||
unsigned long hours = (rawTime % 86400L) / 3600;
|
||||
String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);
|
||||
|
||||
unsigned long minutes = (rawTime % 3600) / 60;
|
||||
String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);
|
||||
|
||||
unsigned long seconds = rawTime % 60;
|
||||
String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);
|
||||
|
||||
return hoursStr + ":" + minuteStr + ":" + secondStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @return String date formatted like `dd.mm.yyyy`
|
||||
*/
|
||||
String NTPClientPlus::getFormattedDate()
|
||||
{
|
||||
this->calcDate();
|
||||
unsigned int dateDay = this->_dateDay;
|
||||
unsigned int dateMonth = this->_dateMonth;
|
||||
unsigned int dateYear = this->_dateYear;
|
||||
|
||||
String dayStr = dateDay < 10 ? "0" + String(dateDay) : String(dateDay);
|
||||
String monthStr = dateMonth < 10 ? "0" + String(dateMonth) : String(dateMonth);
|
||||
String yearStr = dateYear < 10 ? "0" + String(dateYear) : String(dateYear);
|
||||
|
||||
return dayStr + "." + monthStr + "." + yearStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calc date from seconds since 1900
|
||||
*
|
||||
*/
|
||||
void NTPClientPlus::calcDate()
|
||||
{
|
||||
// Start: Calc date
|
||||
|
||||
// get days since 1900
|
||||
unsigned long days1900 = this->getSecsSince1900() / secondperday;
|
||||
|
||||
// calc current year
|
||||
this->_dateYear = this->getYear();
|
||||
|
||||
// calc how many leap days since 1.Jan 1900
|
||||
int leapDays = 0;
|
||||
for (unsigned int i = 1900; i < this->_dateYear; i++)
|
||||
{
|
||||
// check if leap year
|
||||
if (this->isLeapYear(i))
|
||||
{
|
||||
leapDays++;
|
||||
}
|
||||
}
|
||||
leapDays = leapDays - 1;
|
||||
|
||||
// check if current year is leap year
|
||||
if (this->isLeapYear(this->_dateYear))
|
||||
{
|
||||
daysInMonth[2] = 29;
|
||||
}
|
||||
else
|
||||
{
|
||||
daysInMonth[2] = 28;
|
||||
}
|
||||
|
||||
unsigned int dayOfYear = (days1900 - ((this->_dateYear - 1900) * 365) - leapDays);
|
||||
|
||||
// calc current month
|
||||
this->_dateMonth = this->getMonth(dayOfYear);
|
||||
|
||||
this->_dateDay = 0;
|
||||
|
||||
// calc day of month
|
||||
for (unsigned int i = 0; i < this->_dateMonth; i++)
|
||||
{
|
||||
this->_dateDay = this->_dateDay + daysInMonth[i];
|
||||
}
|
||||
this->_dateDay = dayOfYear - this->_dateDay;
|
||||
|
||||
// calc day of week:
|
||||
// Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7
|
||||
// 1. Januar 1900 was a monday
|
||||
this->_dayOfWeek = 1;
|
||||
|
||||
for (unsigned int i = 0; i < days1900; i++)
|
||||
{
|
||||
if (this->_dayOfWeek < 7)
|
||||
{
|
||||
this->_dayOfWeek = this->_dayOfWeek + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->_dayOfWeek = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// End: Calc date (dateDay, dateMonth, dateYear)
|
||||
|
||||
// calc if summer time active
|
||||
|
||||
this->updateSWChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Getter for day of the week
|
||||
*
|
||||
* @return unsigned int
|
||||
*/
|
||||
unsigned int NTPClientPlus::getDayOfWeek()
|
||||
{
|
||||
return this->_dayOfWeek;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Function to calc current year
|
||||
*
|
||||
* @return unsigned int
|
||||
*/
|
||||
unsigned int NTPClientPlus::getYear()
|
||||
{
|
||||
|
||||
unsigned long sec1900 = this->getSecsSince1900();
|
||||
|
||||
// NTP starts at 1. Jan 1900
|
||||
unsigned int result = 1900;
|
||||
unsigned int dayInYear = 0;
|
||||
unsigned int days = 0;
|
||||
unsigned int days1900 = 0;
|
||||
|
||||
unsigned int for_i = 0;
|
||||
bool leapYear = LOW;
|
||||
|
||||
days1900 = sec1900 / this->secondperday;
|
||||
|
||||
for (for_i = 0; for_i < days1900; for_i++)
|
||||
{
|
||||
|
||||
leapYear = this->isLeapYear(result);
|
||||
|
||||
if (leapYear)
|
||||
{
|
||||
dayInYear = 366;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
dayInYear = 365;
|
||||
}
|
||||
|
||||
days++;
|
||||
|
||||
if (days >= dayInYear)
|
||||
{
|
||||
result++;
|
||||
days = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Function to check if given year is leap year
|
||||
*
|
||||
* @param year
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
bool NTPClientPlus::isLeapYear(unsigned int year)
|
||||
{
|
||||
|
||||
bool result = LOW;
|
||||
|
||||
// check for leap year
|
||||
if ((year % 4) == 0)
|
||||
{
|
||||
|
||||
result = HIGH;
|
||||
|
||||
if ((year % 100) == 0)
|
||||
{
|
||||
|
||||
result = LOW;
|
||||
|
||||
if ((year % 400) == 0)
|
||||
{
|
||||
|
||||
result = HIGH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
result = LOW;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get Month of given day of year
|
||||
*
|
||||
* @param dayOfYear
|
||||
* @return int
|
||||
*/
|
||||
int NTPClientPlus::getMonth(int dayOfYear)
|
||||
{
|
||||
|
||||
bool leapYear = this->isLeapYear(this->getYear());
|
||||
|
||||
// Month beginnings
|
||||
int monthMin[13] = {0, 1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};
|
||||
// Month endings
|
||||
int monthMax[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
|
||||
|
||||
int month = 0;
|
||||
|
||||
int y = 0;
|
||||
|
||||
// Calculation of the beginning and end of each month in the leap year
|
||||
if (leapYear == HIGH)
|
||||
{
|
||||
|
||||
for (y = 3; y < 13; y++)
|
||||
{
|
||||
monthMin[y] = monthMin[y] + 1;
|
||||
}
|
||||
|
||||
for (y = 2; y < 13; y++)
|
||||
{
|
||||
monthMax[y] = monthMax[y] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// January
|
||||
if (dayOfYear >= monthMin[1] && dayOfYear <= monthMax[1])
|
||||
{
|
||||
month = 1;
|
||||
}
|
||||
|
||||
// February
|
||||
if (dayOfYear >= monthMin[2] && dayOfYear <= monthMax[2])
|
||||
{
|
||||
month = 2;
|
||||
}
|
||||
|
||||
// March
|
||||
if (dayOfYear >= monthMin[3] && dayOfYear <= monthMax[3])
|
||||
{
|
||||
month = 3;
|
||||
}
|
||||
|
||||
// April
|
||||
if (dayOfYear >= monthMin[4] && dayOfYear <= monthMax[4])
|
||||
{
|
||||
month = 4;
|
||||
}
|
||||
|
||||
// May
|
||||
if (dayOfYear >= monthMin[5] && dayOfYear <= monthMax[5])
|
||||
{
|
||||
month = 5;
|
||||
}
|
||||
|
||||
// June
|
||||
if (dayOfYear >= monthMin[6] && dayOfYear <= monthMax[6])
|
||||
{
|
||||
month = 6;
|
||||
}
|
||||
|
||||
// July
|
||||
if (dayOfYear >= monthMin[7] && dayOfYear <= monthMax[7])
|
||||
{
|
||||
month = 7;
|
||||
}
|
||||
|
||||
// August
|
||||
if (dayOfYear >= monthMin[8] && dayOfYear <= monthMax[8])
|
||||
{
|
||||
month = 8;
|
||||
}
|
||||
|
||||
// September
|
||||
if (dayOfYear >= monthMin[9] && dayOfYear <= monthMax[9])
|
||||
{
|
||||
month = 9;
|
||||
}
|
||||
|
||||
// October
|
||||
if (dayOfYear >= monthMin[10] && dayOfYear <= monthMax[10])
|
||||
{
|
||||
month = 10;
|
||||
}
|
||||
|
||||
// November
|
||||
if (dayOfYear >= monthMin[11] && dayOfYear <= monthMax[11])
|
||||
{
|
||||
month = 11;
|
||||
}
|
||||
|
||||
// December
|
||||
if (dayOfYear >= monthMin[12] && dayOfYear <= monthMax[12])
|
||||
{
|
||||
month = 12;
|
||||
}
|
||||
|
||||
return month;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief (private) Send NTP Packet to NTP server
|
||||
*
|
||||
*/
|
||||
void NTPClientPlus::sendNTPPacket()
|
||||
{
|
||||
// set all bytes in the buffer to 0
|
||||
memset(this->_packetBuffer, 0, NTP_PACKET_SIZE);
|
||||
// Initialize values needed to form NTP request
|
||||
this->_packetBuffer[0] = 0b11100011; // LI, Version, Mode
|
||||
this->_packetBuffer[1] = 0; // Stratum, or type of clock
|
||||
this->_packetBuffer[2] = 6; // Polling Interval
|
||||
this->_packetBuffer[3] = 0xEC; // Peer Clock Precision
|
||||
// 8 bytes of zero for Root Delay & Root Dispersion
|
||||
this->_packetBuffer[12] = 49;
|
||||
this->_packetBuffer[13] = 0x4E;
|
||||
this->_packetBuffer[14] = 49;
|
||||
this->_packetBuffer[15] = 52;
|
||||
|
||||
// all NTP fields have been given values, now
|
||||
// you can send a packet requesting a timestamp:
|
||||
if (this->_poolServerName)
|
||||
{
|
||||
this->_udp->beginPacket(this->_poolServerName, 123);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->_udp->beginPacket(this->_poolServerIP, 123);
|
||||
}
|
||||
this->_udp->write(this->_packetBuffer, NTP_PACKET_SIZE);
|
||||
this->_udp->endPacket();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief (private) Set time offset accordance to summer time
|
||||
*
|
||||
* @param summertime
|
||||
*/
|
||||
void NTPClientPlus::setSummertime(bool summertime)
|
||||
{
|
||||
if (summertime)
|
||||
{
|
||||
this->_timeOffset = this->secondperhour * (this->_utcx + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->_timeOffset = this->secondperhour * (this->_utcx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief (private) Update Summer/Winter time change
|
||||
*
|
||||
* @returns bool summertime active
|
||||
*/
|
||||
bool NTPClientPlus::updateSWChange()
|
||||
{
|
||||
unsigned int dayOfWeek = this->_dayOfWeek;
|
||||
unsigned int dateDay = this->_dateDay;
|
||||
unsigned int dateMonth = this->_dateMonth;
|
||||
|
||||
bool summertimeActive = false;
|
||||
|
||||
if (this->_swChange)
|
||||
{
|
||||
// Start: Set summer-/ winter time
|
||||
|
||||
// current month is march
|
||||
if (dateMonth == 3)
|
||||
{
|
||||
|
||||
// it is last week in march
|
||||
if ((this->daysInMonth[3] - dateDay) < 7)
|
||||
{
|
||||
|
||||
// Example year 2020: March 31 days; Restart March 26, 2020 (Thursday = weekday = 4); 5 days remaining; Last Sunday March 29, 2020
|
||||
// Calculation: 31 - 26 = 5; 5 + 4 = 9;
|
||||
// Result: Last day in March is a Tuesday. There follows another Sunday in October => set winter time
|
||||
|
||||
// Example year 2021: March 31 days; Restart March 30, 2021 (Tuesday = weekday = 2); 1 days remaining; Last Sunday March 28, 2021
|
||||
// Calculation: 31 - 30 = 1; 1 + 2 = 3;
|
||||
// Result: Last day in March is a Wednesday. Changeover to summer time already done => set summer time
|
||||
|
||||
// There follows within the last week in March one more Sunday => set winter time
|
||||
if (((this->daysInMonth[3] - dateDay) + dayOfWeek) >= 7)
|
||||
{
|
||||
this->setSummertime(0);
|
||||
summertimeActive = false;
|
||||
}
|
||||
|
||||
// last sunday in march already over -> summer time
|
||||
else
|
||||
{
|
||||
this->setSummertime(1);
|
||||
summertimeActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// restart in first three weeks of march -> winter time
|
||||
else
|
||||
{
|
||||
this->setSummertime(0);
|
||||
summertimeActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// current month is october
|
||||
else if (dateMonth == 10)
|
||||
{
|
||||
|
||||
// restart last week of october
|
||||
if ((this->daysInMonth[10] - dateDay) < 7)
|
||||
{
|
||||
|
||||
// Example year 2020: October 31 days; restart October 26, 2020 (Monday = weekday = 1); 5 days remaining; last Sunday October 25, 2020
|
||||
// Calculation: 31 - 26 = 5; 5 + 1 = 6;
|
||||
// Result: Last day in October is a Saturday. Changeover to winter time already done => set winter time
|
||||
|
||||
// Example year 2021: October 31 days; Restart 26. October 2021 (Tuesday = weekday = 2); 5 days remaining; Last Sunday 31. October 2021
|
||||
// Calculation: 31 - 26 = 5; 5 + 2 = 7;
|
||||
// Result: Last day in October is a Sunday. There follows another Sunday in October => set summer time
|
||||
|
||||
// There follows within the last week in October one more Sunday => summer time
|
||||
if (((this->daysInMonth[10] - dateDay) + dayOfWeek) >= 7)
|
||||
{
|
||||
this->setSummertime(1);
|
||||
summertimeActive = true;
|
||||
}
|
||||
|
||||
// last sunday in october already over -> winter time
|
||||
else
|
||||
{
|
||||
this->setSummertime(0);
|
||||
summertimeActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// restart in first three weeks of october -> summer time
|
||||
else
|
||||
{
|
||||
this->setSummertime(1);
|
||||
summertimeActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// restart in summer time
|
||||
else if (dateMonth > 3 && dateMonth < 10)
|
||||
{
|
||||
this->setSummertime(1);
|
||||
summertimeActive = true;
|
||||
}
|
||||
|
||||
// restart in winter time
|
||||
else if (dateMonth < 3 || dateMonth > 10)
|
||||
{
|
||||
this->setSummertime(0);
|
||||
summertimeActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
return summertimeActive;
|
||||
}
|
||||
|
||||
void wait(unsigned long time)
|
||||
{
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < time)
|
||||
{
|
||||
yield();
|
||||
};
|
||||
}
|
||||
65
src/connectivity/otafunctions.cpp
Normal file
65
src/connectivity/otafunctions.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include "otafunctions.h"
|
||||
|
||||
// setup Arduino OTA
|
||||
void setupOTA(String hostname)
|
||||
{
|
||||
// Port defaults to 8266
|
||||
// ArduinoOTA.setPort(8266);
|
||||
|
||||
// Hostname defaults to esp8266-[ChipID]
|
||||
ArduinoOTA.setHostname(hostname.c_str());
|
||||
|
||||
// No authentication by default
|
||||
// ArduinoOTA.setPassword("admin");
|
||||
|
||||
// Password can be set with it's md5 value as well
|
||||
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
|
||||
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
|
||||
|
||||
ArduinoOTA.onStart([]()
|
||||
{
|
||||
String type;
|
||||
if (ArduinoOTA.getCommand() == U_FLASH)
|
||||
{
|
||||
type = "sketch";
|
||||
}
|
||||
else
|
||||
{ // U_FS
|
||||
type = "filesystem";
|
||||
}
|
||||
|
||||
// NOTE: if updating FS this would be the place to unmount FS using FS.end()
|
||||
// Serial.println("Start updating " + type);
|
||||
});
|
||||
ArduinoOTA.onEnd([]()
|
||||
{
|
||||
// Serial.println("\nEnd");
|
||||
});
|
||||
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total)
|
||||
{
|
||||
// Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
|
||||
});
|
||||
ArduinoOTA.onError([](ota_error_t error)
|
||||
{
|
||||
//Serial.printf("Error[%u]: ", error);
|
||||
if (error == OTA_AUTH_ERROR) {
|
||||
//Serial.println("Auth Failed");
|
||||
} else if (error == OTA_BEGIN_ERROR) {
|
||||
//Serial.println("Begin Failed");
|
||||
} else if (error == OTA_CONNECT_ERROR) {
|
||||
//Serial.println("Connect Failed");
|
||||
} else if (error == OTA_RECEIVE_ERROR) {
|
||||
//Serial.println("Receive Failed");
|
||||
} else if (error == OTA_END_ERROR) {
|
||||
//Serial.println("End Failed");
|
||||
} });
|
||||
ArduinoOTA.begin();
|
||||
}
|
||||
|
||||
void handleOTA()
|
||||
{
|
||||
// handle OTA
|
||||
ArduinoOTA.handle();
|
||||
}
|
||||
43
src/connectivity/udp_logger.cpp
Normal file
43
src/connectivity/udp_logger.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "udp_logger.h"
|
||||
|
||||
UDPLogger::UDPLogger()
|
||||
{
|
||||
}
|
||||
|
||||
UDPLogger::UDPLogger(IPAddress interface_addr, IPAddress multicast_addr, int port, String name)
|
||||
{
|
||||
_interfaceAddr = interface_addr;
|
||||
_multicastAddr = multicast_addr;
|
||||
_name = name;
|
||||
_port = port;
|
||||
_udp.beginMulticast(_interfaceAddr, _multicastAddr, _port);
|
||||
}
|
||||
|
||||
void UDPLogger::set_name(String name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
void UDPLogger::log_string(String message)
|
||||
{
|
||||
// wait 5 milliseconds if last send was less than 5 milliseconds before
|
||||
if (millis() < (_lastSend + 5))
|
||||
{
|
||||
delay(5);
|
||||
}
|
||||
message = _name + ": " + message;
|
||||
Serial.println(message);
|
||||
_udp.beginPacketMulticast(_multicastAddr, _port, _interfaceAddr);
|
||||
message.toCharArray(_packetBuffer, 100);
|
||||
_udp.print(_packetBuffer);
|
||||
_udp.endPacket();
|
||||
_lastSend = millis();
|
||||
}
|
||||
|
||||
void UDPLogger::log_color_24bit(uint32_t color)
|
||||
{
|
||||
uint8_t result_red = color >> 16 & 0xff;
|
||||
uint8_t result_green = color >> 8 & 0xff;
|
||||
uint8_t result_blue = color & 0xff;
|
||||
log_string(String(result_red) + ", " + String(result_green) + ", " + String(result_blue));
|
||||
}
|
||||
332
src/games/pong.cpp
Normal file
332
src/games/pong.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* @file pong.cpp
|
||||
* @author techniccontroller (mail[at]techniccontroller.com)
|
||||
* @brief Class implementation for pong game
|
||||
* @version 0.1
|
||||
* @date 2022-03-06
|
||||
*
|
||||
* @copyright Copyright (c) 2022
|
||||
*
|
||||
* main code from https://elektro.turanis.de/html/prj041/index.html
|
||||
*
|
||||
*/
|
||||
#include "pong.h"
|
||||
|
||||
/**
|
||||
* @brief Construct a new Pong:: Pong object
|
||||
*
|
||||
*/
|
||||
Pong::Pong()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a new Pong:: Pong object
|
||||
*
|
||||
* @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush()
|
||||
* @param mylogger pointer to UDPLogger object, need to provide a function log_string(message)
|
||||
*/
|
||||
Pong::Pong(LEDMatrix * matrix, UDPLogger * logger)
|
||||
{
|
||||
_ledmatrix = matrix;
|
||||
_logger = logger;
|
||||
_gameState = GAME_STATE_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run main loop for one cycle
|
||||
*
|
||||
*/
|
||||
void Pong::loopCycle()
|
||||
{
|
||||
switch (_gameState)
|
||||
{
|
||||
case GAME_STATE_INIT:
|
||||
initGame(2);
|
||||
break;
|
||||
case GAME_STATE_RUNNING:
|
||||
updateBall();
|
||||
updateGame();
|
||||
break;
|
||||
case GAME_STATE_END:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: UP for given player
|
||||
*
|
||||
* @param playerid id of player {0, 1}
|
||||
*/
|
||||
void Pong::ctrlUp(uint8_t playerid)
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
|
||||
{
|
||||
_playerMovement[playerid] = PADDLE_MOVE_DOWN; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: DOWN for given player
|
||||
*
|
||||
* @param playerid id of player {0, 1}
|
||||
*/
|
||||
void Pong::ctrlDown(uint8_t playerid)
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
|
||||
{
|
||||
_playerMovement[playerid] = PADDLE_MOVE_UP; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: NONE for given player
|
||||
*
|
||||
* @param playerid id of player {0, 1}
|
||||
*/
|
||||
void Pong::ctrlNone(uint8_t playerid)
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
|
||||
{
|
||||
_playerMovement[playerid] = PADDLE_MOVE_NONE;
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize a new game
|
||||
*
|
||||
* @param numBots number of bots {0, 1, 2} -> two bots results in animation
|
||||
*/
|
||||
void Pong::initGame(uint8_t numBots)
|
||||
{
|
||||
(*_logger).log_string("Pong: init with " + String(numBots) + " Bots");
|
||||
resetLEDs();
|
||||
_lastButtonClick = millis();
|
||||
|
||||
_numBots = numBots;
|
||||
|
||||
_ball.x = 1;
|
||||
_ball.y = (Y_MAX / 2) - (PADDLE_WIDTH / 2) + 1;
|
||||
_ball_old.x = _ball.x;
|
||||
_ball_old.y = _ball.y;
|
||||
_ballMovement[0] = 1;
|
||||
_ballMovement[1] = -1;
|
||||
_ballDelay = BALL_DELAY_MAX;
|
||||
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
_paddles[PLAYER_1][i].x = 0;
|
||||
_paddles[PLAYER_1][i].y = (Y_MAX / 2) - (PADDLE_WIDTH / 2) + i;
|
||||
_paddles[PLAYER_2][i].x = X_MAX - 1;
|
||||
_paddles[PLAYER_2][i].y = _paddles[PLAYER_1][i].y;
|
||||
}
|
||||
|
||||
_gameState = GAME_STATE_RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update ball position
|
||||
*
|
||||
*/
|
||||
void Pong::updateBall()
|
||||
{
|
||||
bool hitBall = false;
|
||||
if ((millis() - _lastBallUpdate) < _ballDelay)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastBallUpdate = millis();
|
||||
toggleLed(_ball.x, _ball.y, LED_TYPE_OFF);
|
||||
|
||||
// collision detection for player 1
|
||||
if (_ballMovement[0] == -1 && _ball.x == 1)
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
if (_paddles[PLAYER_1][i].y == _ball.y)
|
||||
{
|
||||
hitBall = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collision detection for player 2
|
||||
if (_ballMovement[0] == 1 && _ball.x == X_MAX - 2)
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
if (_paddles[PLAYER_2][i].y == _ball.y)
|
||||
{
|
||||
hitBall = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitBall == true)
|
||||
{
|
||||
_ballMovement[0] *= -1;
|
||||
if (_ballDelay > BALL_DELAY_MIN)
|
||||
{
|
||||
_ballDelay -= BALL_DELAY_STEP;
|
||||
}
|
||||
}
|
||||
|
||||
_ball.x += _ballMovement[0];
|
||||
_ball.y += _ballMovement[1];
|
||||
|
||||
if (_ball.x <= 0 || _ball.x >= X_MAX - 1)
|
||||
{
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ball.y <= 0 || _ball.y >= Y_MAX - 1)
|
||||
{
|
||||
_ballMovement[1] *= -1;
|
||||
}
|
||||
|
||||
toggleLed(_ball.x, _ball.y, LED_TYPE_BALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game over, draw ball red
|
||||
*
|
||||
*/
|
||||
void Pong::endGame()
|
||||
{
|
||||
(*_logger).log_string("Pong: Game ended");
|
||||
_gameState = GAME_STATE_END;
|
||||
toggleLed(_ball.x, _ball.y, LED_TYPE_BALL_RED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update paddle position and check for game over
|
||||
*
|
||||
*/
|
||||
void Pong::updateGame()
|
||||
{
|
||||
if ((millis() - _lastDrawUpdate) < GAME_DELAY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastDrawUpdate = millis();
|
||||
|
||||
// turn off paddle LEDs
|
||||
for (uint8_t p = 0; p < PLAYER_AMOUNT; p++)
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
toggleLed(_paddles[p][i].x, _paddles[p][i].y, LED_TYPE_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
// move _paddles
|
||||
for (uint8_t p = 0; p < PLAYER_AMOUNT; p++)
|
||||
{
|
||||
uint8_t movement = getPlayerMovement(p);
|
||||
if (movement == PADDLE_MOVE_UP && _paddles[p][PADDLE_WIDTH - 1].y < (Y_MAX - 1))
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
_paddles[p][i].y++;
|
||||
}
|
||||
}
|
||||
if (movement == PADDLE_MOVE_DOWN && _paddles[p][0].y > 0)
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
_paddles[p][i].y--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// show paddle LEDs
|
||||
for (uint8_t p = 0; p < PLAYER_AMOUNT; p++)
|
||||
{
|
||||
for (uint8_t i = 0; i < PADDLE_WIDTH; i++)
|
||||
{
|
||||
toggleLed(_paddles[p][i].x, _paddles[p][i].y, LED_TYPE_PADDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the next movement of paddle from given player
|
||||
*
|
||||
* @param playerId id of player {0, 1}
|
||||
* @return uint8_t movement {UP, DOWN, NONE}
|
||||
*/
|
||||
uint8_t Pong::getPlayerMovement(uint8_t playerId)
|
||||
{
|
||||
uint8_t action = PADDLE_MOVE_NONE;
|
||||
if (playerId < _numBots)
|
||||
{
|
||||
// bot moves paddle
|
||||
int8_t ydir = _ball_old.y - _ball.y;
|
||||
int8_t diff = _paddles[playerId][PADDLE_WIDTH / 2].y - _ball.y + ydir * 0.5;
|
||||
// no movement if ball moves away from paddle or no difference between ball and paddle
|
||||
if (diff == 0 || (_ballMovement[0] > 0 && playerId == 0) || (_ballMovement[0] < 0 && playerId == 1))
|
||||
{
|
||||
action = PADDLE_MOVE_NONE;
|
||||
}
|
||||
else if (diff > 0)
|
||||
{
|
||||
action = PADDLE_MOVE_DOWN;
|
||||
}
|
||||
else
|
||||
{
|
||||
action = PADDLE_MOVE_UP;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
action = _playerMovement[playerId];
|
||||
_playerMovement[playerId] = PADDLE_MOVE_NONE;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the led matrix (turn all leds off)
|
||||
*
|
||||
*/
|
||||
void Pong::resetLEDs()
|
||||
{
|
||||
(*_ledmatrix).flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Turn on LED on matrix
|
||||
*
|
||||
* @param x x position of led
|
||||
* @param y y position of led
|
||||
* @param type type of pixel {PADDLE, BALL_RED, BALL, OFF}
|
||||
*/
|
||||
void Pong::toggleLed(uint8_t x, uint8_t y, uint8_t type)
|
||||
{
|
||||
uint32_t color;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case LED_TYPE_PADDLE:
|
||||
color = LEDMatrix::color_24bit(0, 80, 80);
|
||||
break;
|
||||
case LED_TYPE_BALL_RED:
|
||||
color = LEDMatrix::color_24bit(120, 0, 0);
|
||||
break;
|
||||
case LED_TYPE_BALL:
|
||||
color = LEDMatrix::color_24bit(0, 100, 0);
|
||||
break;
|
||||
case LED_TYPE_OFF:
|
||||
default:
|
||||
color = LEDMatrix::color_24bit(0, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
(*_ledmatrix).grid_add_pixel(x, y, color);
|
||||
}
|
||||
315
src/games/snake.cpp
Normal file
315
src/games/snake.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @file snake.cpp
|
||||
* @author techniccontroller (mail[at]techniccontroller.com)
|
||||
* @brief Class implementation of snake game
|
||||
* @version 0.1
|
||||
* @date 2022-03-05
|
||||
*
|
||||
* @copyright Copyright (c) 2022
|
||||
*
|
||||
* main code from https://elektro.turanis.de/html/prj099/index.html
|
||||
*
|
||||
*/
|
||||
#include "snake.h"
|
||||
|
||||
/**
|
||||
* @brief Construct a new Snake:: Snake object
|
||||
*
|
||||
*/
|
||||
Snake::Snake()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a new Snake:: Snake object
|
||||
*
|
||||
* @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), gridFlush()
|
||||
* @param mylogger pointer to UDPLogger object, need to provide a function logString(message)
|
||||
*/
|
||||
Snake::Snake(LEDMatrix * matrix, UDPLogger * logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_ledmatrix = matrix;
|
||||
_gameState = GAME_STATE_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run main loop for one cycle
|
||||
*
|
||||
*/
|
||||
void Snake::loopCycle()
|
||||
{
|
||||
switch (_gameState)
|
||||
{
|
||||
case GAME_STATE_INIT:
|
||||
initGame();
|
||||
break;
|
||||
case GAME_STATE_RUNNING:
|
||||
updateGame();
|
||||
break;
|
||||
case GAME_STATE_END:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: UP
|
||||
*
|
||||
*/
|
||||
void Snake::ctrlUp()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING)
|
||||
{
|
||||
(*_logger).log_string("Snake: UP");
|
||||
_userDirection = DIRECTION_DOWN; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: DOWN
|
||||
*
|
||||
*/
|
||||
void Snake::ctrlDown()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING)
|
||||
{
|
||||
_logger->log_string("Snake: DOWN");
|
||||
_userDirection = DIRECTION_UP; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: RIGHT
|
||||
*
|
||||
*/
|
||||
void Snake::ctrlRight()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING)
|
||||
{
|
||||
_logger->log_string("Snake: RIGHT");
|
||||
_userDirection = DIRECTION_LEFT; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: LEFT
|
||||
*
|
||||
*/
|
||||
void Snake::ctrlLeft()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameState == GAME_STATE_RUNNING)
|
||||
{
|
||||
_logger->log_string("Snake: LEFT");
|
||||
_userDirection = DIRECTION_RIGHT; // need to swap direction as field is rotated 180deg
|
||||
_lastButtonClick = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the led matrix (turn all leds off)
|
||||
*
|
||||
*/
|
||||
void Snake::resetLEDs()
|
||||
{
|
||||
_ledmatrix->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize a new game
|
||||
*
|
||||
*/
|
||||
void Snake::initGame()
|
||||
{
|
||||
_logger->log_string("Snake: init");
|
||||
resetLEDs();
|
||||
_head.x = 0;
|
||||
_head.y = 0;
|
||||
_food.x = -1;
|
||||
_food.y = -1;
|
||||
_wormLength = MIN_TAIL_LENGTH;
|
||||
_userDirection = DIRECTION_LEFT;
|
||||
_lastButtonClick = millis();
|
||||
|
||||
for (int i = 0; i < MAX_TAIL_LENGTH; i++)
|
||||
{
|
||||
_tail[i].x = -1;
|
||||
_tail[i].y = -1;
|
||||
}
|
||||
updateFood();
|
||||
_gameState = GAME_STATE_RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update game representation
|
||||
*
|
||||
*/
|
||||
void Snake::updateGame()
|
||||
{
|
||||
if ((millis() - _lastDrawUpdate) > GAME_DELAY)
|
||||
{
|
||||
_logger->log_string("Snake: update game");
|
||||
toggleLed(_tail[_wormLength - 1].x, _tail[_wormLength - 1].y, LED_TYPE_OFF);
|
||||
switch (_userDirection)
|
||||
{
|
||||
case DIRECTION_RIGHT:
|
||||
if (_head.x > 0)
|
||||
{
|
||||
_head.x--;
|
||||
}
|
||||
break;
|
||||
case DIRECTION_LEFT:
|
||||
if (_head.x < X_MAX - 1)
|
||||
{
|
||||
_head.x++;
|
||||
}
|
||||
break;
|
||||
case DIRECTION_DOWN:
|
||||
if (_head.y > 0)
|
||||
{
|
||||
_head.y--;
|
||||
}
|
||||
break;
|
||||
case DIRECTION_UP:
|
||||
if (_head.y < Y_MAX - 1)
|
||||
{
|
||||
_head.y++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCollision() == true)
|
||||
{
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
updateTail();
|
||||
|
||||
if (_head.x == _food.x && _head.y == _food.y)
|
||||
{
|
||||
if (_wormLength < MAX_TAIL_LENGTH)
|
||||
{
|
||||
_wormLength++;
|
||||
}
|
||||
updateFood();
|
||||
}
|
||||
|
||||
_lastDrawUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game over, draw _head red
|
||||
*
|
||||
*/
|
||||
void Snake::endGame()
|
||||
{
|
||||
_gameState = GAME_STATE_END;
|
||||
toggleLed(_head.x, _head.y, LED_TYPE_BLOOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update _tail led positions
|
||||
*
|
||||
*/
|
||||
void Snake::updateTail()
|
||||
{
|
||||
for (unsigned int i = _wormLength - 1; i > 0; i--)
|
||||
{
|
||||
_tail[i].x = _tail[i - 1].x;
|
||||
_tail[i].y = _tail[i - 1].y;
|
||||
}
|
||||
_tail[0].x = _head.x;
|
||||
_tail[0].y = _head.y;
|
||||
|
||||
for (unsigned int i = 0; i < _wormLength; i++)
|
||||
{
|
||||
if (_tail[i].x > -1)
|
||||
{
|
||||
toggleLed(_tail[i].x, _tail[i].y, LED_TYPE_SNAKE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update _food position (generate new one if found)
|
||||
*
|
||||
*/
|
||||
void Snake::updateFood()
|
||||
{
|
||||
bool found = true;
|
||||
do
|
||||
{
|
||||
found = true;
|
||||
_food.x = random(0, X_MAX);
|
||||
_food.y = random(0, Y_MAX);
|
||||
for (unsigned int i = 0; i < _wormLength; i++)
|
||||
{
|
||||
if (_tail[i].x == _food.x && _tail[i].y == _food.y)
|
||||
{
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
} while (found == false);
|
||||
toggleLed(_food.x, _food.y, LED_TYPE_FOOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check for collisison between snake and border or itself
|
||||
*
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
bool Snake::isCollision()
|
||||
{
|
||||
if (_head.x < 0 || _head.x >= X_MAX)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_head.y < 0 || _head.y >= Y_MAX)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for (unsigned int i = 1; i < _wormLength; i++)
|
||||
{
|
||||
if (_tail[i].x == _head.x && _tail[i].y == _head.y)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Turn on LED on matrix
|
||||
*
|
||||
* @param x x position of led
|
||||
* @param y y position of led
|
||||
* @param type type of pixel {SNAKE, OFF, FOOD, BLOOD}
|
||||
*/
|
||||
void Snake::toggleLed(uint8_t x, uint8_t y, uint8_t type)
|
||||
{
|
||||
uint32_t color;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case LED_TYPE_SNAKE:
|
||||
color = LEDMatrix::color_24bit(0, 100, 100);
|
||||
break;
|
||||
case LED_TYPE_OFF:
|
||||
color = LEDMatrix::color_24bit(0, 0, 0);
|
||||
break;
|
||||
case LED_TYPE_FOOD:
|
||||
color = LEDMatrix::color_24bit(0, 150, 0);
|
||||
break;
|
||||
case LED_TYPE_BLOOD:
|
||||
default:
|
||||
color = LEDMatrix::color_24bit(150, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
_ledmatrix->grid_add_pixel(x, y, color);
|
||||
}
|
||||
680
src/games/tetris.cpp
Normal file
680
src/games/tetris.cpp
Normal file
@@ -0,0 +1,680 @@
|
||||
/**
|
||||
* @file tetris.cpp
|
||||
* @author techniccontroller (mail[at]techniccontroller.com)
|
||||
* @brief Class implementation for tetris game
|
||||
* @version 0.1
|
||||
* @date 2022-03-05
|
||||
*
|
||||
* @copyright Copyright (c) 2022
|
||||
*
|
||||
* main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty
|
||||
*
|
||||
*/
|
||||
#include "tetris.h"
|
||||
|
||||
Tetris::Tetris()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a new Tetris:: Tetris object
|
||||
*
|
||||
* @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), draw_on_matrix(), gridFlush() and printNumber(x,y,n,col)
|
||||
* @param mylogger pointer to UDPLogger object, need to provide a function log_string(message)
|
||||
*/
|
||||
Tetris::Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger)
|
||||
{
|
||||
_logger = mylogger;
|
||||
_ledmatrix = myledmatrix;
|
||||
_gameStatet = GAME_STATE_READY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run main loop for one cycle
|
||||
*
|
||||
*/
|
||||
void Tetris::loopCycle()
|
||||
{
|
||||
switch (_gameStatet)
|
||||
{
|
||||
case GAME_STATE_READY:
|
||||
|
||||
break;
|
||||
case GAME_STATE_INIT:
|
||||
tetrisInit();
|
||||
|
||||
break;
|
||||
case GAME_STATE_RUNNING:
|
||||
// If brick is still "on the loose", then move it down by one
|
||||
if (_activeBrick.enabled)
|
||||
{
|
||||
// move faster down when allow drop
|
||||
if (_allowdrop)
|
||||
{
|
||||
if (millis() > _droptime + 50)
|
||||
{
|
||||
_droptime = millis();
|
||||
shiftActiveBrick(DIR_DOWN);
|
||||
printField();
|
||||
}
|
||||
}
|
||||
|
||||
// move down with regular speed
|
||||
if ((millis() - _prevUpdateTime) > (_brickSpeed * _speedtetris / 100))
|
||||
{
|
||||
_prevUpdateTime = millis();
|
||||
shiftActiveBrick(DIR_DOWN);
|
||||
printField();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_allowdrop = false;
|
||||
// Active brick has "crashed", check for full lines
|
||||
// and create new brick at top of field
|
||||
checkFullLines();
|
||||
newActiveBrick();
|
||||
_prevUpdateTime = millis(); // Reset update time to avoid brick dropping two spaces
|
||||
}
|
||||
break;
|
||||
case GAME_STATE_PAUSED:
|
||||
|
||||
break;
|
||||
case GAME_STATE_END:
|
||||
// at game end show all bricks on field in red color for 1.5 seconds, then show score
|
||||
if (_tetrisGameOver == true)
|
||||
{
|
||||
_tetrisGameOver = false;
|
||||
(*_logger).log_string("Tetris: end");
|
||||
everythingRed();
|
||||
_tetrisshowscore = millis();
|
||||
}
|
||||
|
||||
if (millis() > (_tetrisshowscore + RED_END_TIME))
|
||||
{
|
||||
resetLEDs();
|
||||
_score = _nbRowsTotal;
|
||||
showscore();
|
||||
_gameStatet = GAME_STATE_READY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: START (& restart)
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlStart()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
|
||||
{
|
||||
_lastButtonClick = millis();
|
||||
_gameStatet = GAME_STATE_INIT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: PAUSE/PLAY
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlPlayPause()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
|
||||
{
|
||||
_lastButtonClick = millis();
|
||||
if (_gameStatet == GAME_STATE_PAUSED)
|
||||
{
|
||||
(*_logger).log_string("Tetris: continue");
|
||||
|
||||
_gameStatet = GAME_STATE_RUNNING;
|
||||
}
|
||||
else if (_gameStatet == GAME_STATE_RUNNING)
|
||||
{
|
||||
(*_logger).log_string("Tetris: pause");
|
||||
|
||||
_gameStatet = GAME_STATE_PAUSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: RIGHT
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlRight()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
|
||||
{
|
||||
_lastButtonClick = millis();
|
||||
shiftActiveBrick(DIR_RIGHT);
|
||||
printField();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: LEFT
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlLeft()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
|
||||
{
|
||||
_lastButtonClick = millis();
|
||||
shiftActiveBrick(DIR_LEFT);
|
||||
printField();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: UP (rotate)
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlUp()
|
||||
{
|
||||
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
|
||||
{
|
||||
_lastButtonClick = millis();
|
||||
rotateActiveBrick();
|
||||
printField();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger control: DOWN (drop)
|
||||
*
|
||||
*/
|
||||
void Tetris::ctrlDown()
|
||||
{
|
||||
// longer debounce time, to prevent immediate drop
|
||||
if (millis() > _lastButtonClickr + DEBOUNCE_TIME * 5 && _gameStatet == GAME_STATE_RUNNING)
|
||||
{
|
||||
_allowdrop = true;
|
||||
_lastButtonClickr = millis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set game speed
|
||||
*
|
||||
* @param i new speed value
|
||||
*/
|
||||
void Tetris::setSpeed(int32_t i)
|
||||
{
|
||||
_logger->log_string("setSpeed: " + String(i));
|
||||
_speedtetris = -10 * i + 150;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the led matrix (turn all leds off)
|
||||
*
|
||||
*/
|
||||
void Tetris::resetLEDs()
|
||||
{
|
||||
_ledmatrix->flush();
|
||||
_ledmatrix->draw_on_matrix_instant();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the tetris game
|
||||
*
|
||||
*/
|
||||
void Tetris::tetrisInit()
|
||||
{
|
||||
(*_logger).log_string("Tetris: init");
|
||||
|
||||
clearField();
|
||||
_brickSpeed = INIT_SPEED;
|
||||
_nbRowsThisLevel = 0;
|
||||
_nbRowsTotal = 0;
|
||||
_tetrisGameOver = false;
|
||||
|
||||
newActiveBrick();
|
||||
_prevUpdateTime = millis();
|
||||
|
||||
_gameStatet = GAME_STATE_RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw current field representation to led matrix
|
||||
*
|
||||
*/
|
||||
void Tetris::printField()
|
||||
{
|
||||
int x, y;
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
for (y = 0; y < MATRIX_HEIGHT; y++)
|
||||
{
|
||||
uint8_t activeBrickPix = 0;
|
||||
if (_activeBrick.enabled)
|
||||
{ // Only draw brick if it is enabled
|
||||
// Now check if brick is "in view"
|
||||
if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz))))
|
||||
{
|
||||
activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos];
|
||||
}
|
||||
}
|
||||
if (_field.pix[x][y] == 1)
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, _field.color[x][y]);
|
||||
}
|
||||
else if (activeBrickPix == 1)
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, _activeBrick.col);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, 0x000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ledmatrix->draw_on_matrix_instant();
|
||||
}
|
||||
|
||||
/* *** Game functions *** */
|
||||
/**
|
||||
* @brief Spawn new (random) brick
|
||||
*
|
||||
*/
|
||||
void Tetris::newActiveBrick()
|
||||
{
|
||||
uint8_t selectedBrick = 0;
|
||||
static uint8_t lastselectedBrick = 0;
|
||||
|
||||
// choose random next brick, but not the same as before
|
||||
do
|
||||
{
|
||||
selectedBrick = random(7);
|
||||
} while (lastselectedBrick == selectedBrick);
|
||||
|
||||
// Save selected brick for next round
|
||||
lastselectedBrick = selectedBrick;
|
||||
|
||||
// every brick has its color, select corresponding color
|
||||
uint32_t selectedCol = _brickLib[selectedBrick].col;
|
||||
// Set properties of brick
|
||||
_activeBrick.siz = _brickLib[selectedBrick].siz;
|
||||
_activeBrick.yOffset = _brickLib[selectedBrick].yOffset;
|
||||
_activeBrick.xpos = MATRIX_WIDTH / 2 - _activeBrick.siz / 2;
|
||||
_activeBrick.ypos = BRICKOFFSET - _activeBrick.yOffset;
|
||||
_activeBrick.enabled = true;
|
||||
|
||||
// Set color of brick
|
||||
_activeBrick.col = selectedCol;
|
||||
// _activeBrick.color = _colorLib[1];
|
||||
|
||||
// Copy pix array of selected Brick
|
||||
uint8_t x, y;
|
||||
for (y = 0; y < MAX_BRICK_SIZE; y++)
|
||||
{
|
||||
for (x = 0; x < MAX_BRICK_SIZE; x++)
|
||||
{
|
||||
_activeBrick.pix[x][y] = (_brickLib[selectedBrick]).pix[x][y];
|
||||
}
|
||||
}
|
||||
|
||||
// Check collision, if already, then game is over
|
||||
if (checkFieldCollision(&_activeBrick))
|
||||
{
|
||||
_tetrisGameOver = true;
|
||||
_gameStatet = GAME_STATE_END;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check collision between bricks in the field and the specified brick
|
||||
*
|
||||
* @param brick brick to be checked for collision
|
||||
* @return boolean true if collision occured
|
||||
*/
|
||||
boolean Tetris::checkFieldCollision(struct Brick *brick)
|
||||
{
|
||||
uint8_t bx, by;
|
||||
uint8_t fx, fy;
|
||||
for (by = 0; by < MAX_BRICK_SIZE; by++)
|
||||
{
|
||||
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
|
||||
{
|
||||
fx = brick->xpos + bx;
|
||||
fy = brick->ypos + by;
|
||||
if ((brick->pix[bx][by] == 1) && (_field.pix[fx][fy] == 1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check collision between specified brick and all sides of the playing field
|
||||
*
|
||||
* @param brick brick to be checked for collision
|
||||
* @return boolean true if collision occured
|
||||
*/
|
||||
boolean Tetris::checkSidesCollision(struct Brick *brick)
|
||||
{
|
||||
// Check vertical collision with sides of field
|
||||
uint8_t bx, by;
|
||||
uint8_t fx; //, fy; /* Patch */
|
||||
for (by = 0; by < MAX_BRICK_SIZE; by++)
|
||||
{
|
||||
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
|
||||
{
|
||||
if (brick->pix[bx][by] == 1)
|
||||
{
|
||||
fx = brick->xpos + bx; // Determine actual position in the field of the current pix of the brick
|
||||
// fy = brick->ypos + by; /* Patch */
|
||||
if (fx < 0 || fx >= MATRIX_WIDTH)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Rotate current active brick
|
||||
*
|
||||
*/
|
||||
void Tetris::rotateActiveBrick()
|
||||
{
|
||||
// Copy active brick pix array to temporary pix array
|
||||
uint8_t x, y;
|
||||
Brick tmpBrick;
|
||||
for (y = 0; y < MAX_BRICK_SIZE; y++)
|
||||
{
|
||||
for (x = 0; x < MAX_BRICK_SIZE; x++)
|
||||
{
|
||||
tmpBrick.pix[x][y] = _activeBrick.pix[x][y];
|
||||
}
|
||||
}
|
||||
tmpBrick.xpos = _activeBrick.xpos;
|
||||
tmpBrick.ypos = _activeBrick.ypos;
|
||||
tmpBrick.siz = _activeBrick.siz;
|
||||
|
||||
// Depending on size of the active brick, we will rotate differently
|
||||
if (_activeBrick.siz == 3)
|
||||
{
|
||||
// Perform rotation around center pix
|
||||
tmpBrick.pix[0][0] = _activeBrick.pix[0][2];
|
||||
tmpBrick.pix[0][1] = _activeBrick.pix[1][2];
|
||||
tmpBrick.pix[0][2] = _activeBrick.pix[2][2];
|
||||
tmpBrick.pix[1][0] = _activeBrick.pix[0][1];
|
||||
tmpBrick.pix[1][1] = _activeBrick.pix[1][1];
|
||||
tmpBrick.pix[1][2] = _activeBrick.pix[2][1];
|
||||
tmpBrick.pix[2][0] = _activeBrick.pix[0][0];
|
||||
tmpBrick.pix[2][1] = _activeBrick.pix[1][0];
|
||||
tmpBrick.pix[2][2] = _activeBrick.pix[2][0];
|
||||
// Keep other parts of temporary block clear
|
||||
tmpBrick.pix[0][3] = 0;
|
||||
tmpBrick.pix[1][3] = 0;
|
||||
tmpBrick.pix[2][3] = 0;
|
||||
tmpBrick.pix[3][3] = 0;
|
||||
tmpBrick.pix[3][2] = 0;
|
||||
tmpBrick.pix[3][1] = 0;
|
||||
tmpBrick.pix[3][0] = 0;
|
||||
}
|
||||
else if (_activeBrick.siz == 4)
|
||||
{
|
||||
// Perform rotation around center "cross"
|
||||
tmpBrick.pix[0][0] = _activeBrick.pix[0][3];
|
||||
tmpBrick.pix[0][1] = _activeBrick.pix[1][3];
|
||||
tmpBrick.pix[0][2] = _activeBrick.pix[2][3];
|
||||
tmpBrick.pix[0][3] = _activeBrick.pix[3][3];
|
||||
tmpBrick.pix[1][0] = _activeBrick.pix[0][2];
|
||||
tmpBrick.pix[1][1] = _activeBrick.pix[1][2];
|
||||
tmpBrick.pix[1][2] = _activeBrick.pix[2][2];
|
||||
tmpBrick.pix[1][3] = _activeBrick.pix[3][2];
|
||||
tmpBrick.pix[2][0] = _activeBrick.pix[0][1];
|
||||
tmpBrick.pix[2][1] = _activeBrick.pix[1][1];
|
||||
tmpBrick.pix[2][2] = _activeBrick.pix[2][1];
|
||||
tmpBrick.pix[2][3] = _activeBrick.pix[3][1];
|
||||
tmpBrick.pix[3][0] = _activeBrick.pix[0][0];
|
||||
tmpBrick.pix[3][1] = _activeBrick.pix[1][0];
|
||||
tmpBrick.pix[3][2] = _activeBrick.pix[2][0];
|
||||
tmpBrick.pix[3][3] = _activeBrick.pix[3][0];
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger->log_string("Tetris: Brick size error");
|
||||
}
|
||||
|
||||
// Now validate by checking collision.
|
||||
// Collision possibilities:
|
||||
// - Brick now sticks outside field
|
||||
// - Brick now sticks inside fixed bricks of field
|
||||
// In case of collision, we just discard the rotated temporary brick
|
||||
if ((!checkSidesCollision(&tmpBrick)) && (!checkFieldCollision(&tmpBrick)))
|
||||
{
|
||||
// Copy temporary brick pix array to active pix array
|
||||
for (y = 0; y < MAX_BRICK_SIZE; y++)
|
||||
{
|
||||
for (x = 0; x < MAX_BRICK_SIZE; x++)
|
||||
{
|
||||
_activeBrick.pix[x][y] = tmpBrick.pix[x][y];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shift brick left/right/down by one if possible
|
||||
*
|
||||
* @param dir direction to be shifted
|
||||
*/
|
||||
void Tetris::shiftActiveBrick(int dir)
|
||||
{
|
||||
// Change position of active brick (no copy to temporary needed)
|
||||
if (dir == DIR_LEFT)
|
||||
{
|
||||
_activeBrick.xpos--;
|
||||
}
|
||||
else if (dir == DIR_RIGHT)
|
||||
{
|
||||
_activeBrick.xpos++;
|
||||
}
|
||||
else if (dir == DIR_DOWN)
|
||||
{
|
||||
_activeBrick.ypos++;
|
||||
}
|
||||
|
||||
// Check position of active brick
|
||||
// Two possibilities when collision is detected:
|
||||
// - Direction was LEFT/RIGHT, just revert position back
|
||||
// - Direction was DOWN, revert position and fix block to field on collision
|
||||
// When no collision, keep _activeBrick coordinates
|
||||
if ((checkSidesCollision(&_activeBrick)) || (checkFieldCollision(&_activeBrick)))
|
||||
{
|
||||
if (dir == DIR_LEFT)
|
||||
{
|
||||
_activeBrick.xpos++;
|
||||
}
|
||||
else if (dir == DIR_RIGHT)
|
||||
{
|
||||
_activeBrick.xpos--;
|
||||
}
|
||||
else if (dir == DIR_DOWN)
|
||||
{
|
||||
_activeBrick.ypos--; // Go back up one
|
||||
addActiveBrickToField();
|
||||
_activeBrick.enabled = false; // Disable brick, it is no longer moving
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy active pixels to field, including color
|
||||
*
|
||||
*/
|
||||
void Tetris::addActiveBrickToField()
|
||||
{
|
||||
uint8_t bx, by;
|
||||
uint8_t fx, fy;
|
||||
for (by = 0; by < MAX_BRICK_SIZE; by++)
|
||||
{
|
||||
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
|
||||
{
|
||||
fx = _activeBrick.xpos + bx;
|
||||
fy = _activeBrick.ypos + by;
|
||||
|
||||
if (fx >= 0 && fy >= 0 && fx < MATRIX_WIDTH && fy < MATRIX_HEIGHT && _activeBrick.pix[bx][by])
|
||||
{ // Check if inside playing field
|
||||
// _field.pix[fx][fy] = _field.pix[fx][fy] || _activeBrick.pix[bx][by];
|
||||
_field.pix[fx][fy] = _activeBrick.pix[bx][by];
|
||||
_field.color[fx][fy] = _activeBrick.col;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move all pix from the field above startRow down by one. startRow is overwritten
|
||||
*
|
||||
* @param startRow
|
||||
*/
|
||||
void Tetris::moveFieldDownOne(uint8_t startRow)
|
||||
{
|
||||
if (startRow == 0)
|
||||
{ // Topmost row has nothing on top to move...
|
||||
return;
|
||||
}
|
||||
uint8_t x, y;
|
||||
for (y = startRow - 1; y > 0; y--)
|
||||
{
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
_field.pix[x][y + 1] = _field.pix[x][y];
|
||||
_field.color[x][y + 1] = _field.color[x][y];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a line is complete
|
||||
*
|
||||
*/
|
||||
void Tetris::checkFullLines()
|
||||
{
|
||||
int x, y;
|
||||
int minY = 0;
|
||||
for (y = (MATRIX_HEIGHT - 1); y >= minY; y--)
|
||||
{
|
||||
uint8_t rowSum = 0;
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
rowSum = rowSum + (_field.pix[x][y]);
|
||||
}
|
||||
if (rowSum >= MATRIX_WIDTH)
|
||||
{
|
||||
// Found full row, animate its removal
|
||||
_activeBrick.enabled = false;
|
||||
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
_field.pix[x][y] = 0;
|
||||
printField();
|
||||
delay(100);
|
||||
}
|
||||
// Move all upper rows down by one
|
||||
moveFieldDownOne(y);
|
||||
y++;
|
||||
minY++;
|
||||
printField();
|
||||
delay(100);
|
||||
|
||||
_nbRowsThisLevel++;
|
||||
_nbRowsTotal++;
|
||||
if (_nbRowsThisLevel >= LEVELUP)
|
||||
{
|
||||
_nbRowsThisLevel = 0;
|
||||
_brickSpeed = _brickSpeed - SPEED_STEP;
|
||||
if (_brickSpeed < 200)
|
||||
{
|
||||
_brickSpeed = 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear field
|
||||
*
|
||||
*/
|
||||
void Tetris::clearField()
|
||||
{
|
||||
uint8_t x, y;
|
||||
for (y = 0; y < MATRIX_HEIGHT; y++)
|
||||
{
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
_field.pix[x][y] = 0;
|
||||
_field.color[x][y] = 0;
|
||||
}
|
||||
}
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{ // This last row is invisible to the player and only used for the collision detection routine
|
||||
_field.pix[x][MATRIX_HEIGHT] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Color all bricks on the field red
|
||||
*
|
||||
*/
|
||||
void Tetris::everythingRed()
|
||||
{
|
||||
int x, y;
|
||||
for (x = 0; x < MATRIX_WIDTH; x++)
|
||||
{
|
||||
for (y = 0; y < MATRIX_HEIGHT; y++)
|
||||
{
|
||||
uint8_t activeBrickPix = 0;
|
||||
if (_activeBrick.enabled)
|
||||
{ // Only draw brick if it is enabled
|
||||
// Now check if brick is "in view"
|
||||
if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz))))
|
||||
{
|
||||
activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos];
|
||||
}
|
||||
}
|
||||
if (_field.pix[x][y] == 1)
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, RED);
|
||||
}
|
||||
else if (activeBrickPix == 1)
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, RED);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ledmatrix->grid_add_pixel(x, y, 0x000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ledmatrix->draw_on_matrix_instant();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw score to led matrix
|
||||
*
|
||||
*/
|
||||
void Tetris::showscore()
|
||||
{
|
||||
uint32_t color = LEDMatrix::color_24bit(255, 170, 0);
|
||||
_ledmatrix->flush();
|
||||
if (_score > 9)
|
||||
{
|
||||
_ledmatrix->print_number(2, 3, _score / 10, color);
|
||||
_ledmatrix->print_number(6, 3, _score % 10, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ledmatrix->print_number(4, 3, _score, color);
|
||||
}
|
||||
_ledmatrix->draw_on_matrix_instant();
|
||||
}
|
||||
439
src/matrix/animationfunctions.cpp
Normal file
439
src/matrix/animationfunctions.cpp
Normal file
@@ -0,0 +1,439 @@
|
||||
#include <Arduino.h>
|
||||
#include "animationfunctions.h"
|
||||
#include "wordclock_constants.h"
|
||||
#include "udp_logger.h"
|
||||
#include "ledmatrix.h"
|
||||
|
||||
extern UDPLogger logger;
|
||||
extern LEDMatrix led_matrix;
|
||||
|
||||
const int8_t dx[] = {1, -1, 0, 0};
|
||||
const int8_t dy[] = {0, 0, -1, 1};
|
||||
|
||||
/**
|
||||
* @brief Function to draw a spiral step (from center)
|
||||
*
|
||||
* @param init marks if call is the initial step of the spiral
|
||||
* @param empty marks if the spiral should 'draw' empty leds
|
||||
* @param size the size of the spiral in leds
|
||||
* @return int - 1 if end is reached, else 0
|
||||
*/
|
||||
int draw_spiral(bool init, bool empty, uint8_t size)
|
||||
{
|
||||
static Direction dir; // current direction
|
||||
static int x;
|
||||
static int y;
|
||||
static int counter;
|
||||
static int count_step;
|
||||
static int count_edge;
|
||||
static int count_corner;
|
||||
static bool expand;
|
||||
static int random_num;
|
||||
|
||||
if (init)
|
||||
{
|
||||
logger.log_string("Init Spiral with empty=" + String(empty));
|
||||
dir = DOWN; // current direction
|
||||
x = MATRIX_WIDTH / 2;
|
||||
y = MATRIX_WIDTH / 2;
|
||||
if (!empty)
|
||||
{
|
||||
led_matrix.flush();
|
||||
}
|
||||
counter = 0;
|
||||
count_step = 0;
|
||||
count_edge = 1;
|
||||
count_corner = 0;
|
||||
expand = true;
|
||||
random_num = random(UINT8_MAX);
|
||||
}
|
||||
|
||||
if (count_step == size * size)
|
||||
{
|
||||
// End reached return 1
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// calc color from colorwheel
|
||||
uint32_t color = LEDMatrix::wheel((random_num + count_step * 6) % UINT8_MAX);
|
||||
// if draw mode is empty, set color to zero
|
||||
if (empty)
|
||||
{
|
||||
color = 0;
|
||||
}
|
||||
led_matrix.grid_add_pixel(x, y, color);
|
||||
if (count_corner == 2 && expand)
|
||||
{
|
||||
count_edge += 1;
|
||||
expand = false;
|
||||
}
|
||||
if (counter >= count_edge)
|
||||
{
|
||||
dir = next_direction(dir, LEFT);
|
||||
counter = 0;
|
||||
count_corner++;
|
||||
}
|
||||
if (count_corner >= 4)
|
||||
{
|
||||
count_corner = 0;
|
||||
count_edge += 1;
|
||||
expand = true;
|
||||
}
|
||||
|
||||
x += dx[dir];
|
||||
y += dy[dir];
|
||||
// logger.log_string("x: " + String(x) + ", y: " + String(y) + "c: " + String(color) + "\n");
|
||||
counter++;
|
||||
count_step++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run random snake animation
|
||||
*
|
||||
* @param init marks if call is the initial step of the animation
|
||||
* @param len length of the snake
|
||||
* @param color color of the snake
|
||||
* @param numSteps number of animation steps
|
||||
* @return int - 1 when animation is finished, else 0
|
||||
*/
|
||||
int random_snake(bool init, const uint8_t len, const uint32_t color, int numSteps)
|
||||
{
|
||||
static Direction dir;
|
||||
static int snake[2][10];
|
||||
static int random_y;
|
||||
static int random_x;
|
||||
static int e;
|
||||
static int countStep;
|
||||
if (init)
|
||||
{
|
||||
dir = DOWN; // current direction
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
snake[0][i] = 3;
|
||||
snake[1][i] = i;
|
||||
}
|
||||
|
||||
random_y = random(1, 8); // Random variable for y-direction
|
||||
random_x = random(1, 4); // Random variable for x-direction
|
||||
e = LEFT; // next turn
|
||||
countStep = 0;
|
||||
}
|
||||
if (countStep == numSteps)
|
||||
{
|
||||
// End reached return 1
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// move one step forward
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (i < len - 1)
|
||||
{
|
||||
snake[0][i] = snake[0][i + 1];
|
||||
snake[1][i] = snake[1][i + 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
snake[0][i] = snake[0][i] + dx[dir];
|
||||
snake[1][i] = snake[1][i] + dy[dir];
|
||||
}
|
||||
}
|
||||
// collision with wall?
|
||||
if ((dir == DOWN && snake[1][len - 1] >= MATRIX_HEIGHT - 1) ||
|
||||
(dir == UP && snake[1][len - 1] <= 0) ||
|
||||
(dir == RIGHT && snake[0][len - 1] >= MATRIX_WIDTH - 1) ||
|
||||
(dir == LEFT && snake[0][len - 1] <= 0))
|
||||
{
|
||||
dir = next_direction(dir, e);
|
||||
}
|
||||
// Random branching at the side edges
|
||||
else if ((dir == UP && snake[1][len - 1] == random_y && snake[0][len - 1] >= MATRIX_WIDTH - 1) || (dir == DOWN && snake[1][len - 1] == random_y && snake[0][len - 1] <= 0))
|
||||
{
|
||||
dir = next_direction(dir, LEFT);
|
||||
e = (e + 2) % 2 + 1;
|
||||
}
|
||||
else if ((dir == DOWN && snake[1][len - 1] == random_y && snake[0][len - 1] >= MATRIX_WIDTH - 1) || (dir == UP && snake[1][len - 1] == random_y && snake[0][len - 1] <= 0))
|
||||
{
|
||||
dir = next_direction(dir, RIGHT);
|
||||
e = (e + 2) % 2 + 1;
|
||||
}
|
||||
else if ((dir == LEFT && snake[0][len - 1] == random_x && snake[1][len - 1] <= 0) || (dir == RIGHT && snake[0][len - 1] == random_x && snake[1][len - 1] >= MATRIX_HEIGHT - 1))
|
||||
{
|
||||
dir = next_direction(dir, LEFT);
|
||||
e = (e + 2) % 2 + 1;
|
||||
}
|
||||
else if ((dir == RIGHT && snake[0][len - 1] == random_x && snake[1][len - 1] <= 0) || (dir == LEFT && snake[0][len - 1] == random_x && snake[1][len - 1] >= MATRIX_HEIGHT - 1))
|
||||
{
|
||||
dir = next_direction(dir, RIGHT);
|
||||
e = (e + 2) % 2 + 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
// draw the snake
|
||||
led_matrix.grid_add_pixel(snake[0][i], snake[1][i], color);
|
||||
}
|
||||
|
||||
// calc new random variables after every 20 steps
|
||||
if (countStep % 20 == 0)
|
||||
{
|
||||
random_y = random(1, 8);
|
||||
random_x = random(1, 4);
|
||||
}
|
||||
countStep++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calc the next direction for led movement (snake and spiral)
|
||||
*
|
||||
* @param dir direction of the current led movement
|
||||
* @param d action to be executed
|
||||
* @return direction - next direction
|
||||
*/
|
||||
Direction next_direction(Direction dir, int d)
|
||||
{
|
||||
// d = 0 -> continue straight on
|
||||
// d = 1 -> turn LEFT
|
||||
// d = 2 -> turn RIGHT
|
||||
Direction selection[3];
|
||||
switch (dir)
|
||||
{
|
||||
case RIGHT:
|
||||
{
|
||||
selection[0] = RIGHT;
|
||||
selection[1] = UP;
|
||||
selection[2] = DOWN;
|
||||
break;
|
||||
}
|
||||
case LEFT:
|
||||
{
|
||||
selection[0] = LEFT;
|
||||
selection[1] = DOWN;
|
||||
selection[2] = UP;
|
||||
break;
|
||||
}
|
||||
case UP:
|
||||
{
|
||||
selection[0] = UP;
|
||||
selection[1] = LEFT;
|
||||
selection[2] = RIGHT;
|
||||
break;
|
||||
}
|
||||
case DOWN:
|
||||
{
|
||||
selection[0] = DOWN;
|
||||
selection[1] = RIGHT;
|
||||
selection[2] = LEFT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Direction next = selection[d];
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show the time as digits on the wordclock
|
||||
*
|
||||
* @param hours hours of time to display
|
||||
* @param minutes minutes of time to display
|
||||
* @param color color to display (24bit)
|
||||
*/
|
||||
void show_digital_clock(uint8_t hours, uint8_t minutes, uint32_t color)
|
||||
{
|
||||
led_matrix.flush();
|
||||
uint8_t fstDigitH = hours / 10;
|
||||
uint8_t sndDigitH = hours % 10;
|
||||
uint8_t fstDigitM = minutes / 10;
|
||||
uint8_t sndDigitM = minutes % 10;
|
||||
led_matrix.print_number(2, 0, fstDigitH, color);
|
||||
led_matrix.print_number(6, 0, sndDigitH, color);
|
||||
led_matrix.print_number(2, 6, fstDigitM, color);
|
||||
led_matrix.print_number(6, 6, sndDigitM, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run random tetris animation
|
||||
*
|
||||
* @param init marks if call is the initial step of the animation
|
||||
* @return int - 1 when animation is finished, else 0
|
||||
*/
|
||||
int random_tetris(bool init)
|
||||
{
|
||||
// total number of blocks which can be displayed
|
||||
const static uint8_t numBlocks = 30;
|
||||
// all different block shapes
|
||||
const static bool blockshapes[9][3][3] = {{{0, 0, 0},
|
||||
{0, 0, 0},
|
||||
{0, 0, 0}},
|
||||
{{1, 0, 0},
|
||||
{1, 0, 0},
|
||||
{1, 0, 0}},
|
||||
{{0, 0, 0},
|
||||
{1, 0, 0},
|
||||
{1, 0, 0}},
|
||||
{{0, 0, 0},
|
||||
{1, 1, 0},
|
||||
{1, 0, 0}},
|
||||
{{0, 0, 0},
|
||||
{0, 0, 0},
|
||||
{1, 1, 0}},
|
||||
{{0, 0, 0},
|
||||
{1, 1, 0},
|
||||
{1, 1, 0}},
|
||||
{{0, 0, 0},
|
||||
{0, 0, 0},
|
||||
{1, 1, 1}},
|
||||
{{0, 0, 0},
|
||||
{1, 1, 1},
|
||||
{1, 0, 0}},
|
||||
{{0, 0, 0},
|
||||
{0, 0, 1},
|
||||
{1, 1, 1}}};
|
||||
// local game screen buffer
|
||||
static uint8_t screen[MATRIX_HEIGHT + 3][MATRIX_WIDTH];
|
||||
// current number of blocks on the screen
|
||||
static int counterID;
|
||||
// indicate if the game was lost
|
||||
static bool gameover = false;
|
||||
|
||||
if (init || gameover)
|
||||
{
|
||||
logger.log_string("Init Tetris: init=" + String(init) + ", gameover=" + String(gameover));
|
||||
// clear local game screen
|
||||
for (int h = 0; h < MATRIX_HEIGHT + 3; h++)
|
||||
{
|
||||
for (int w = 0; w < MATRIX_WIDTH; w++)
|
||||
{
|
||||
screen[h][w] = 0;
|
||||
}
|
||||
}
|
||||
counterID = 0;
|
||||
gameover = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
led_matrix.flush();
|
||||
|
||||
// list of all blocks in game, indicating which are moving
|
||||
// set every block on the screen as a potentially mover
|
||||
bool to_move[numBlocks + 1];
|
||||
for (int i = 0; i < numBlocks; i++)
|
||||
to_move[i + 1] = i < counterID;
|
||||
|
||||
// identify tiles which can move DOWN (no collision below)
|
||||
for (int c = 0; c < MATRIX_WIDTH; c++)
|
||||
{ // columns
|
||||
for (int r = 0; r < MATRIX_HEIGHT + 3; r++)
|
||||
{ // rows
|
||||
// only check pixels which are occupied
|
||||
if (screen[r][c] != 0)
|
||||
{
|
||||
// every tile which has a pixel in last row -> no mover
|
||||
if (r == MATRIX_HEIGHT + 2)
|
||||
{
|
||||
to_move[screen[r][c]] = false;
|
||||
}
|
||||
// or every pixel
|
||||
else if (screen[r + 1][c] != 0 && screen[r + 1][c] != screen[r][c])
|
||||
{
|
||||
to_move[screen[r][c]] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// indicate if there is no moving block
|
||||
// assume first there are no more moving block
|
||||
bool no_more_mover = true;
|
||||
// loop over existing block and ask if they can move
|
||||
for (int i = 0; i < counterID; i++)
|
||||
{
|
||||
if (to_move[i + 1])
|
||||
{
|
||||
no_more_mover = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_more_mover)
|
||||
{
|
||||
// no more moving blocks -> check if game over or spawn new block
|
||||
logger.log_string("Tetris: No more Mover");
|
||||
gameover = false;
|
||||
// check if game was lost -> one pixel active in 4rd row (top row on the led grid)
|
||||
for (int s = 0; s < MATRIX_WIDTH; s++)
|
||||
{
|
||||
if (screen[3][s] != 0)
|
||||
gameover = true;
|
||||
}
|
||||
if (gameover || counterID >= (numBlocks - 1))
|
||||
{
|
||||
logger.log_string("Tetris: Gameover");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Create new block
|
||||
// increment counter
|
||||
counterID++;
|
||||
// select random shape for new block
|
||||
uint8_t randShape = random(1, 9);
|
||||
// select random position (column) for spawn of new block
|
||||
uint8_t randx = random(0, MATRIX_WIDTH - 3);
|
||||
// copy shape to screen (c1 - column of block, c2 - column of screen)
|
||||
// write the id of block on the screen
|
||||
for (int c1 = 0, c2 = randx; c1 < 3; c1++, c2++)
|
||||
{
|
||||
for (int r = 0; r < 3; r++)
|
||||
{
|
||||
if (blockshapes[randShape][r][c1])
|
||||
screen[r][c2] = counterID;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// uint8_t tempscreen[MATRIX_HEIGHT + 3][MATRIX_WIDTH] = {0};
|
||||
uint8_t moveX = MATRIX_WIDTH - 1;
|
||||
uint8_t moveY = MATRIX_HEIGHT + 2;
|
||||
// moving blocks exists -> move them one pixel DOWN
|
||||
// loop over pixels and move every pixel DOWN, which belongs to a moving block
|
||||
for (int c = MATRIX_WIDTH - 1; c >= 0; c--)
|
||||
{
|
||||
for (int r = MATRIX_HEIGHT + 1; r >= 0; r--)
|
||||
{
|
||||
if ((screen[r][c] != 0) && to_move[screen[r][c]])
|
||||
{
|
||||
// tempscreen[r + 1][c] = screen[r][c];
|
||||
screen[r + 1][c] = screen[r][c];
|
||||
screen[r][c] = 0;
|
||||
// save top LEFT corner of block
|
||||
if (moveX > c)
|
||||
moveX = c;
|
||||
if (moveY > r)
|
||||
moveY = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw/copy screen values to led grid (r - row, c - column)
|
||||
for (int c = 0; c < MATRIX_WIDTH; c++)
|
||||
{
|
||||
for (int r = 0; r < MATRIX_HEIGHT; r++)
|
||||
{
|
||||
if (screen[r + 3][c] != 0)
|
||||
{
|
||||
// screen is 3 pixels higher than led grid, so drop the upper three lines
|
||||
led_matrix.grid_add_pixel(c, r, colors_24bit[(screen[r + 3][c] % NUM_COLORS)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
343
src/matrix/ledmatrix.cpp
Normal file
343
src/matrix/ledmatrix.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "ledmatrix.h"
|
||||
#include "own_font.h"
|
||||
#include "wordclock_constants.h"
|
||||
|
||||
#define MAX_LED_CURRENT_MA 20 // 20mA for full brightness per LED
|
||||
|
||||
// seven predefined colors24bit (green, red, yellow, purple, orange, lightgreen, blue)
|
||||
const uint32_t colors_24bit[NUM_COLORS] = {
|
||||
LEDMatrix::color_24bit(0, 255, 0),
|
||||
LEDMatrix::color_24bit(255, 0, 0),
|
||||
LEDMatrix::color_24bit(200, 200, 0),
|
||||
LEDMatrix::color_24bit(255, 0, 200),
|
||||
LEDMatrix::color_24bit(255, 128, 0),
|
||||
LEDMatrix::color_24bit(0, 128, 0),
|
||||
LEDMatrix::color_24bit(0, 0, 255)};
|
||||
|
||||
/**
|
||||
* @brief Construct a new LEDMatrix::LEDMatrix object
|
||||
*
|
||||
* @param mymatrix pointer to Adafruit_NeoMatrix object
|
||||
* @param mybrightness the initial brightness of the leds
|
||||
* @param mylogger pointer to the UDPLogger object
|
||||
*/
|
||||
LEDMatrix::LEDMatrix(Adafruit_NeoMatrix * matrix, uint8_t brightness, UDPLogger * logger)
|
||||
{
|
||||
_neomatrix = matrix;
|
||||
_brightness = brightness;
|
||||
_logger = logger;
|
||||
_current_limit = DEFAULT_CURRENT_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert RGB value to 24bit color value
|
||||
*
|
||||
* @param r red value (0-255)
|
||||
* @param g green value (0-255)
|
||||
* @param b blue value (0-255)
|
||||
* @return uint32_t 24bit color value
|
||||
*/
|
||||
uint32_t LEDMatrix::color_24bit(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert 24bit color to 16bit color
|
||||
*
|
||||
* @param color24bit 24bit color value
|
||||
* @return uint16_t 16bit color value
|
||||
*/
|
||||
uint16_t LEDMatrix::color_24_to_16bit(uint32_t color_24bit)
|
||||
{
|
||||
uint8_t r = color_24bit >> 16 & 0xff;
|
||||
uint8_t g = color_24bit >> 8 & 0xff;
|
||||
uint8_t b = color_24bit & 0xff;
|
||||
return ((uint16_t)(r & 0xF8) << 8) |
|
||||
((uint16_t)(g & 0xFC) << 3) |
|
||||
(b >> 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Input a value 0 to 255 to get a color value. The colors are a transition r - g - b - back to r.
|
||||
*
|
||||
* @param WheelPos Value between 0 and 255
|
||||
* @return uint32_t return 24bit color of colorwheel
|
||||
*/
|
||||
uint32_t LEDMatrix::wheel(uint8_t WheelPos)
|
||||
{
|
||||
WheelPos = UINT8_MAX - WheelPos;
|
||||
if (WheelPos < 85)
|
||||
{
|
||||
return color_24bit(UINT8_MAX - WheelPos * 3, 0, WheelPos * 3);
|
||||
}
|
||||
if (WheelPos < 170)
|
||||
{
|
||||
WheelPos -= 85;
|
||||
return color_24bit(0, WheelPos * 3, UINT8_MAX - WheelPos * 3);
|
||||
}
|
||||
WheelPos -= 170;
|
||||
return color_24bit(WheelPos * 3, UINT8_MAX - WheelPos * 3, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Interpolates two colors24bit and returns an color of the result
|
||||
*
|
||||
* @param color1 startcolor for interpolation
|
||||
* @param color2 endcolor for interpolatio
|
||||
* @param factor which color is wanted on the path from start to end color
|
||||
* @return uint32_t interpolated color
|
||||
*/
|
||||
uint32_t LEDMatrix::interpolate_color_24bit(uint32_t color1, uint32_t color2, float factor)
|
||||
{
|
||||
uint8_t resultRed = color1 >> 16 & 0xff;
|
||||
uint8_t resultGreen = color1 >> 8 & 0xff;
|
||||
uint8_t resultBlue = color1 & 0xff;
|
||||
resultRed = (uint8_t)(resultRed + (int16_t)(factor * ((int16_t)(color2 >> 16 & 0xff) - (int16_t)resultRed)));
|
||||
resultGreen = (uint8_t)(resultGreen + (int16_t)(factor * ((int16_t)(color2 >> 8 & 0xff) - (int16_t)resultGreen)));
|
||||
resultBlue = (uint8_t)(resultBlue + (int16_t)(factor * ((int16_t)(color2 & 0xff) - (int16_t)resultBlue)));
|
||||
return color_24bit(resultRed, resultGreen, resultBlue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Setup function for LED matrix
|
||||
*
|
||||
*/
|
||||
void LEDMatrix::setup_matrix()
|
||||
{
|
||||
_neomatrix->begin();
|
||||
_neomatrix->setTextWrap(false);
|
||||
_neomatrix->setBrightness(_brightness);
|
||||
randomSeed(analogRead(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Turn on the minutes indicator leds with the provided pattern (binary encoded)
|
||||
*
|
||||
* @param pattern the binary encoded pattern of the minute indicator
|
||||
* @param color color to be displayed
|
||||
*/
|
||||
void LEDMatrix::set_min_indicator(uint8_t pattern, uint32_t color)
|
||||
{
|
||||
// pattern:
|
||||
// 15 -> 1111
|
||||
// 14 -> 1110
|
||||
// (...)
|
||||
// 2 -> 0010
|
||||
// 1 -> 0001
|
||||
// 0 -> 0000
|
||||
if (pattern & 1)
|
||||
{
|
||||
_target_indicators[0] = color;
|
||||
}
|
||||
if (pattern >> 1 & 1)
|
||||
{
|
||||
_target_indicators[1] = color;
|
||||
}
|
||||
if (pattern >> 2 & 1)
|
||||
{
|
||||
_target_indicators[2] = color;
|
||||
}
|
||||
if (pattern >> 3 & 1)
|
||||
{
|
||||
_target_indicators[3] = color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "Activates" a pixel in targetgrid with color
|
||||
*
|
||||
* @param x x-position of pixel
|
||||
* @param y y-position of pixel
|
||||
* @param color color of pixel
|
||||
*/
|
||||
void LEDMatrix::grid_add_pixel(uint8_t x, uint8_t y, uint32_t color)
|
||||
{
|
||||
// limit ranges of x and y
|
||||
if (x >= 0 && x < MATRIX_WIDTH && y >= 0 && y < MATRIX_HEIGHT)
|
||||
{
|
||||
_target_grid[y][x] = color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// logger->log_string("Index out of Range: " + String(x) + ", " + String(y));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief "Deactivates" all pixels in targetgrid
|
||||
*
|
||||
*/
|
||||
void LEDMatrix::flush(void)
|
||||
{
|
||||
// set a zero to each pixel
|
||||
for (uint8_t i = 0; i < MATRIX_HEIGHT; i++)
|
||||
{
|
||||
for (uint8_t j = 0; j < MATRIX_WIDTH; j++)
|
||||
{
|
||||
_target_grid[i][j] = 0;
|
||||
}
|
||||
}
|
||||
// set every minutes indicator led to 0
|
||||
_target_indicators[0] = 0;
|
||||
_target_indicators[1] = 0;
|
||||
_target_indicators[2] = 0;
|
||||
_target_indicators[3] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write target pixels directly to leds
|
||||
*
|
||||
*/
|
||||
void LEDMatrix::draw_on_matrix_instant()
|
||||
{
|
||||
_draw_on_matrix(1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write target pixels with low pass filter to leds
|
||||
*
|
||||
* @param factor factor between 0 and 1 (1.0 = hard, 0.1 = smooth)
|
||||
*/
|
||||
void LEDMatrix::draw_on_matrix_smooth(float factor)
|
||||
{
|
||||
_draw_on_matrix(factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draws the targetgrid to the ledmatrix
|
||||
*
|
||||
* @param factor factor between 0 and 1 (1.0 = hard, 0.1 = smooth)
|
||||
*/
|
||||
void LEDMatrix::_draw_on_matrix(float factor)
|
||||
{
|
||||
uint16_t total_current = 0;
|
||||
uint32_t filtered_color = 0;
|
||||
|
||||
// loop over all leds in matrix
|
||||
for (int s = 0; s < MATRIX_WIDTH; s++)
|
||||
{
|
||||
for (int z = 0; z < MATRIX_HEIGHT; z++)
|
||||
{
|
||||
// inplement momentum as smooth transistion function
|
||||
uint32_t filtered_color = interpolate_color_24bit(_current_grid[z][s], _target_grid[z][s], factor);
|
||||
_neomatrix->drawPixel(s, z, color_24_to_16bit(filtered_color));
|
||||
_current_grid[z][s] = filtered_color;
|
||||
total_current += _calc_estimated_led_current(filtered_color);
|
||||
}
|
||||
}
|
||||
|
||||
// loop over all minute indicator leds
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
filtered_color = interpolate_color_24bit(_current_indicators[i], _target_indicators[i], factor);
|
||||
_neomatrix->drawPixel(MATRIX_WIDTH - (1 + i), MATRIX_HEIGHT, color_24_to_16bit(filtered_color));
|
||||
_current_indicators[i] = filtered_color;
|
||||
total_current += _calc_estimated_led_current(filtered_color);
|
||||
}
|
||||
|
||||
// Check if totalCurrent reaches CURRENTLIMIT -> if yes reduce brightness
|
||||
if (total_current > _current_limit)
|
||||
{
|
||||
uint8_t new_brightness = _brightness * float(_current_limit) / float(total_current);
|
||||
_neomatrix->setBrightness(new_brightness);
|
||||
}
|
||||
_neomatrix->show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows a 1-digit number on LED matrix (5x3)
|
||||
*
|
||||
* @param xpos x of left top corner of digit
|
||||
* @param ypos y of left top corner of digit
|
||||
* @param number number to display
|
||||
* @param color color to display (24bit)
|
||||
*/
|
||||
void LEDMatrix::print_number(uint8_t xpos, uint8_t ypos, uint8_t number, uint32_t color)
|
||||
{
|
||||
for (int y = ypos, i = 0; y < (ypos + 5); y++, i++)
|
||||
{
|
||||
for (int x = xpos, k = 2; x < (xpos + 3); x++, k--)
|
||||
{
|
||||
if ((numbers_font[number][i] >> k) & 0x1)
|
||||
{
|
||||
grid_add_pixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shows a character on LED matrix (5x3), supports currently only 'I' and 'P'
|
||||
*
|
||||
* @param xpos x of left top corner of character
|
||||
* @param ypos y of left top corner of character
|
||||
* @param character character to display
|
||||
* @param color color to display (24bit)
|
||||
*/
|
||||
void LEDMatrix::print_char(uint8_t xpos, uint8_t ypos, char character, uint32_t color)
|
||||
{
|
||||
int id = 0;
|
||||
if (character == 'I')
|
||||
{
|
||||
id = 0;
|
||||
}
|
||||
else if (character == 'P')
|
||||
{
|
||||
id = 1;
|
||||
}
|
||||
|
||||
for (int y = ypos, i = 0; y < (ypos + 5); y++, i++)
|
||||
{
|
||||
for (int x = xpos, k = 2; x < (xpos + 3); x++, k--)
|
||||
{
|
||||
if ((chars_font[id][i] >> k) & 0x1)
|
||||
{
|
||||
grid_add_pixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set Brightness
|
||||
*
|
||||
* @param mybrightness brightness to be set [0..255]
|
||||
*/
|
||||
void LEDMatrix::set_brightness(uint8_t brightness)
|
||||
{
|
||||
_brightness = brightness;
|
||||
_neomatrix->setBrightness(_brightness);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calc estimated current (mA) for one pixel with the given color and brightness
|
||||
*
|
||||
* @param color 24bit color value of the pixel for which the current should be calculated
|
||||
* @return the current in mA
|
||||
*/
|
||||
uint16_t LEDMatrix::_calc_estimated_led_current(uint32_t color)
|
||||
{
|
||||
// extract rgb values
|
||||
uint8_t red = color >> 16 & 0xff;
|
||||
uint8_t green = color >> 8 & 0xff;
|
||||
uint8_t blue = color & 0xff;
|
||||
|
||||
// Linear estimation: 20mA for full brightness per LED
|
||||
// (calculation avoids float numbers)
|
||||
uint32_t estimated_current = (MAX_LED_CURRENT_MA * red) + (MAX_LED_CURRENT_MA * green) + (MAX_LED_CURRENT_MA * blue);
|
||||
estimated_current /= UINT8_MAX;
|
||||
estimated_current = (estimated_current * _brightness) / UINT8_MAX;
|
||||
|
||||
return estimated_current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the current limit
|
||||
*
|
||||
* @param new_current_limit the total current limit for whole matrix
|
||||
*/
|
||||
void LEDMatrix::set_current_limit(uint16_t new_current_limit)
|
||||
{
|
||||
_current_limit = new_current_limit;
|
||||
}
|
||||
1064
src/wordclock_esp8266.cpp
Normal file
1064
src/wordclock_esp8266.cpp
Normal file
File diff suppressed because it is too large
Load Diff
238
src/wordclock_functions.cpp
Normal file
238
src/wordclock_functions.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#include <Arduino.h>
|
||||
#include "wordclock_functions.h"
|
||||
#include "ledmatrix.h"
|
||||
#include "udp_logger.h"
|
||||
|
||||
|
||||
extern LEDMatrix led_matrix;
|
||||
extern String split(String s, char parser, int index); // TODO cleanup
|
||||
|
||||
const String clockStringGerman = "ESPISTAFUNFVIERTELZEHNZWANZIGUVORTECHNICNACHHALBMELFUNFXCONTROLLEREINSEAWZWEIDREITUMVIERSECHSQYACHTSIEBENZWOLFZEHNEUNJUHR";
|
||||
|
||||
/**
|
||||
* @brief control the four minute indicator LEDs
|
||||
*
|
||||
* @param minutes minutes to be displayed [0 ... 59]
|
||||
* @param color 24bit color value
|
||||
*/
|
||||
void draw_minute_indicator(uint8_t minutes, uint32_t color)
|
||||
{
|
||||
// separate LEDs for minutes in an additional row
|
||||
{
|
||||
switch (minutes % 5)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
led_matrix.set_min_indicator(0b1000, color);
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
led_matrix.set_min_indicator(0b1100, color);
|
||||
break;
|
||||
}
|
||||
|
||||
case 3:
|
||||
{
|
||||
led_matrix.set_min_indicator(0b1110, color);
|
||||
break;
|
||||
}
|
||||
|
||||
case 4:
|
||||
{
|
||||
led_matrix.set_min_indicator(0b1111, color);
|
||||
break;
|
||||
}
|
||||
case 0:
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Draw the given sentence to the word clock
|
||||
*
|
||||
* @param message sentence to be displayed
|
||||
* @param color 24bit color value
|
||||
* @return int: 0 if successful, -1 if sentence not possible to display
|
||||
*/
|
||||
int show_string_on_clock(String message, uint32_t color)
|
||||
{
|
||||
String word = "";
|
||||
int last_letter_clock = 0;
|
||||
int word_position = 0;
|
||||
int idx = 0;
|
||||
|
||||
// add space on the end of message for splitting
|
||||
message = message + " ";
|
||||
|
||||
// empty the target grid
|
||||
led_matrix.flush();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// extract next word from message
|
||||
word = split(message, ' ', idx);
|
||||
idx++;
|
||||
|
||||
if (word.length() > 0)
|
||||
{
|
||||
// find word in clock string
|
||||
word_position = clockStringGerman.indexOf(word, last_letter_clock);
|
||||
|
||||
if (word_position >= 0)
|
||||
{
|
||||
// word found on clock -> enable leds in targetgrid
|
||||
for (unsigned int i = 0; i < word.length(); i++)
|
||||
{
|
||||
unsigned int x = (word_position + i) % MATRIX_WIDTH;
|
||||
unsigned int y = (word_position + i) / MATRIX_WIDTH;
|
||||
led_matrix.grid_add_pixel(x, y, color);
|
||||
}
|
||||
// remember end of the word on clock
|
||||
last_letter_clock = word_position + word.length();
|
||||
}
|
||||
else
|
||||
{
|
||||
// word is not possible to show on clock
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else // end - no more word in message
|
||||
{
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // return success
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts the given time as sentence (String)
|
||||
*
|
||||
* @param hours hours of the time value
|
||||
* @param minutes minutes of the time value
|
||||
* @return String time as sentence
|
||||
*/
|
||||
String time_to_string(uint8_t hours, uint8_t minutes)
|
||||
{
|
||||
String message = "ES IST "; // first two words
|
||||
|
||||
// show minutes
|
||||
if (minutes >= 5 && minutes < 10)
|
||||
{
|
||||
message += "FUNF NACH ";
|
||||
}
|
||||
else if (minutes >= 10 && minutes < 15)
|
||||
{
|
||||
message += "ZEHN NACH ";
|
||||
}
|
||||
else if (minutes >= 15 && minutes < 20)
|
||||
{
|
||||
message += "VIERTEL NACH ";
|
||||
}
|
||||
else if (minutes >= 20 && minutes < 25)
|
||||
{
|
||||
message += "ZEHN VOR HALB ";
|
||||
}
|
||||
else if (minutes >= 25 && minutes < 30)
|
||||
{
|
||||
message += "FUNF VOR HALB ";
|
||||
}
|
||||
else if (minutes >= 30 && minutes < 35)
|
||||
{
|
||||
message += "HALB ";
|
||||
}
|
||||
else if (minutes >= 35 && minutes < 40)
|
||||
{
|
||||
message += "FUNF NACH HALB ";
|
||||
}
|
||||
else if (minutes >= 40 && minutes < 45)
|
||||
{
|
||||
message += "ZEHN NACH HALB ";
|
||||
}
|
||||
else if (minutes >= 45 && minutes < 50)
|
||||
{
|
||||
message += "VIERTEL VOR ";
|
||||
}
|
||||
else if (minutes >= 50 && minutes < 55)
|
||||
{
|
||||
message += "ZEHN VOR ";
|
||||
}
|
||||
else if (minutes >= 55 && minutes < 60)
|
||||
{
|
||||
message += "FUNF VOR ";
|
||||
}
|
||||
|
||||
// convert hours to 12h format
|
||||
if (hours >= 12)
|
||||
{
|
||||
hours -= 12;
|
||||
}
|
||||
if (minutes >= 20)
|
||||
{
|
||||
hours++;
|
||||
}
|
||||
if (hours == 12)
|
||||
{
|
||||
hours = 0;
|
||||
}
|
||||
|
||||
// show hours
|
||||
switch (hours)
|
||||
{
|
||||
case 0:
|
||||
message += "ZWOLF ";
|
||||
break;
|
||||
case 1:
|
||||
message += "EIN";
|
||||
// EIN(S)
|
||||
if (minutes > 4)
|
||||
{
|
||||
message += "S";
|
||||
}
|
||||
message += " ";
|
||||
break;
|
||||
case 2:
|
||||
message += "ZWEI ";
|
||||
break;
|
||||
case 3:
|
||||
message += "DREI ";
|
||||
break;
|
||||
case 4:
|
||||
message += "VIER ";
|
||||
break;
|
||||
case 5:
|
||||
message += "FUNF ";
|
||||
break;
|
||||
case 6:
|
||||
message += "SECHS ";
|
||||
break;
|
||||
case 7:
|
||||
message += "SIEBEN ";
|
||||
break;
|
||||
case 8:
|
||||
message += "ACHT ";
|
||||
break;
|
||||
case 9:
|
||||
message += "NEUN ";
|
||||
break;
|
||||
case 10:
|
||||
message += "ZEHN ";
|
||||
break;
|
||||
case 11:
|
||||
message += "ELF ";
|
||||
break;
|
||||
}
|
||||
if (minutes < 5)
|
||||
{
|
||||
message += "UHR ";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
143
src/wrapper/base64_wrapper.cpp
Normal file
143
src/wrapper/base64_wrapper.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright (C) 2016 Arturo Guadalupi. All right reserved.
|
||||
|
||||
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "base64_wrapper.h"
|
||||
#if (defined(__AVR__))
|
||||
#include <avr/pgmspace.h>
|
||||
#else
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
const char PROGMEM _Base64AlphabetTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
int Base64Class::encode(char *output, char *input, int inputLength) {
|
||||
int i = 0, j = 0;
|
||||
int encodedLength = 0;
|
||||
unsigned char A3[3];
|
||||
unsigned char A4[4];
|
||||
|
||||
while(inputLength--) {
|
||||
A3[i++] = *(input++);
|
||||
if(i == 3) {
|
||||
fromA3ToA4(A4, A3);
|
||||
|
||||
for(i = 0; i < 4; i++) {
|
||||
output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[i]]);
|
||||
}
|
||||
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(i) {
|
||||
for(j = i; j < 3; j++) {
|
||||
A3[j] = '\0';
|
||||
}
|
||||
|
||||
fromA3ToA4(A4, A3);
|
||||
|
||||
for(j = 0; j < i + 1; j++) {
|
||||
output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[j]]);
|
||||
}
|
||||
|
||||
while((i++ < 3)) {
|
||||
output[encodedLength++] = '=';
|
||||
}
|
||||
}
|
||||
output[encodedLength] = '\0';
|
||||
return encodedLength;
|
||||
}
|
||||
|
||||
int Base64Class::decode(char * output, char * input, int inputLength) {
|
||||
int i = 0, j = 0;
|
||||
int decodedLength = 0;
|
||||
unsigned char A3[3];
|
||||
unsigned char A4[4];
|
||||
|
||||
|
||||
while (inputLength--) {
|
||||
if(*input == '=') {
|
||||
break;
|
||||
}
|
||||
|
||||
A4[i++] = *(input++);
|
||||
if (i == 4) {
|
||||
for (i = 0; i <4; i++) {
|
||||
A4[i] = lookupTable(A4[i]);
|
||||
}
|
||||
|
||||
fromA4ToA3(A3,A4);
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
output[decodedLength++] = A3[i];
|
||||
}
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (j = i; j < 4; j++) {
|
||||
A4[j] = '\0';
|
||||
}
|
||||
|
||||
for (j = 0; j <4; j++) {
|
||||
A4[j] = lookupTable(A4[j]);
|
||||
}
|
||||
|
||||
fromA4ToA3(A3,A4);
|
||||
|
||||
for (j = 0; j < i - 1; j++) {
|
||||
output[decodedLength++] = A3[j];
|
||||
}
|
||||
}
|
||||
output[decodedLength] = '\0';
|
||||
return decodedLength;
|
||||
}
|
||||
|
||||
int Base64Class::encodedLength(int plainLength) {
|
||||
int n = plainLength;
|
||||
return (n + 2 - ((n + 2) % 3)) / 3 * 4;
|
||||
}
|
||||
|
||||
int Base64Class::decodedLength(char * input, int inputLength) {
|
||||
int i = 0;
|
||||
int numEq = 0;
|
||||
for(i = inputLength - 1; input[i] == '='; i--) {
|
||||
numEq++;
|
||||
}
|
||||
|
||||
return ((6 * inputLength) / 8) - numEq;
|
||||
}
|
||||
|
||||
//Private utility functions
|
||||
inline void Base64Class::fromA3ToA4(unsigned char * A4, unsigned char * A3) {
|
||||
A4[0] = (A3[0] & 0xfc) >> 2;
|
||||
A4[1] = ((A3[0] & 0x03) << 4) + ((A3[1] & 0xf0) >> 4);
|
||||
A4[2] = ((A3[1] & 0x0f) << 2) + ((A3[2] & 0xc0) >> 6);
|
||||
A4[3] = (A3[2] & 0x3f);
|
||||
}
|
||||
|
||||
inline void Base64Class::fromA4ToA3(unsigned char * A3, unsigned char * A4) {
|
||||
A3[0] = (A4[0] << 2) + ((A4[1] & 0x30) >> 4);
|
||||
A3[1] = ((A4[1] & 0xf) << 4) + ((A4[2] & 0x3c) >> 2);
|
||||
A3[2] = ((A4[2] & 0x3) << 6) + A4[3];
|
||||
}
|
||||
|
||||
inline unsigned char Base64Class::lookupTable(char c) {
|
||||
if(c >='A' && c <='Z') return c - 'A';
|
||||
if(c >='a' && c <='z') return c - 71;
|
||||
if(c >='0' && c <='9') return c + 4;
|
||||
if(c == '+') return 62;
|
||||
if(c == '/') return 63;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Base64Class Base64;
|
||||
217
src/wrapper/littlefs_wrapper.cpp
Normal file
217
src/wrapper/littlefs_wrapper.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
// ****************************************************************
|
||||
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
|
||||
// created: Jens Fleischer, 2020-06-08
|
||||
// last mod: Jens Fleischer, 2020-12-19
|
||||
// For more information visit: https://fipsok.de
|
||||
// ****************************************************************
|
||||
// Hardware: Esp8266
|
||||
// Software: Esp8266 Arduino Core 2.7.0 - 3.0.2
|
||||
// Geprueft: von 1MB bis 2MB Flash
|
||||
// Getestet auf: Nodemcu
|
||||
/******************************************************************
|
||||
Copyright (c) 2020 Jens Fleischer. All rights reserved.
|
||||
|
||||
This file is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
This file is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
*******************************************************************/
|
||||
// Diese Version von LittleFS sollte als Tab eingebunden werden.
|
||||
// #include <LittleFS.h> #include <ESP8266WebServer.h> muessen im Haupttab aufgerufen werden
|
||||
// Die Funktionalitaet des ESP8266 Webservers ist erforderlich.
|
||||
// "webserver.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
|
||||
// Die Funktion "setup_filesystem();" muss im Setup aufgerufen werden.
|
||||
/**************************************************************************************/
|
||||
#include <Arduino.h>
|
||||
#include "littlefs_wrapper.h"
|
||||
#include "LittleFS.h"
|
||||
#include <list>
|
||||
#include <tuple>
|
||||
#include <ESP8266WebServer.h>
|
||||
|
||||
extern ESP8266WebServer webserver;
|
||||
|
||||
const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "FS:none" kompiliert!)";
|
||||
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
|
||||
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die fs.html hoch.)";
|
||||
|
||||
void setup_filesystem()
|
||||
{ // Funktionsaufruf "setup_filesystem();" muss im Setup eingebunden werden
|
||||
LittleFS.begin();
|
||||
webserver.on("/format", format_filesystem);
|
||||
webserver.on("/upload", HTTP_POST, send_response, handle_upload);
|
||||
webserver.onNotFound([]()
|
||||
{
|
||||
if (!handle_file(webserver.urlDecode(webserver.uri())))
|
||||
webserver.send(404, "text/plain", "FileNotFound"); });
|
||||
}
|
||||
|
||||
bool handle_list()
|
||||
{ // Senden aller Daten an den Client
|
||||
FSInfo fs_info;
|
||||
LittleFS.info(fs_info); // Fuellt FSInfo Struktur mit Informationen ueber das Dateisystem
|
||||
Dir dir = LittleFS.openDir("/");
|
||||
using namespace std;
|
||||
using records = tuple<String, String, int>;
|
||||
list<records> dirList;
|
||||
while (dir.next()) // Ordner und Dateien zur Liste hinzufuegen
|
||||
{
|
||||
if (dir.isDirectory())
|
||||
{
|
||||
uint8_t ran{0};
|
||||
Dir fold = LittleFS.openDir(dir.fileName());
|
||||
while (fold.next())
|
||||
{
|
||||
ran++;
|
||||
dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize());
|
||||
}
|
||||
if (!ran)
|
||||
dirList.emplace_back(dir.fileName(), "", 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
dirList.emplace_back("", dir.fileName(), dir.fileSize());
|
||||
}
|
||||
}
|
||||
dirList.sort([](const records &f, const records &l) { // Dateien sortieren
|
||||
if (webserver.arg(0) == "1")
|
||||
{
|
||||
return get<2>(f) > get<2>(l);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint8_t i = 0; i < 31; i++)
|
||||
{
|
||||
if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i]))
|
||||
return true;
|
||||
else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i]))
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
dirList.sort([](const records &f, const records &l) { // Ordner sortieren
|
||||
if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00)
|
||||
{
|
||||
for (uint8_t i = 0; i < 31; i++)
|
||||
{
|
||||
if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i]))
|
||||
return true;
|
||||
else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i]))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
String temp = "[";
|
||||
for (auto &t : dirList)
|
||||
{
|
||||
if (temp != "[")
|
||||
temp += ',';
|
||||
temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + format_bytes(get<2>(t)) + "\"}";
|
||||
}
|
||||
temp += ",{\"usedBytes\":\"" + format_bytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz
|
||||
"\",\"totalBytes\":\"" + format_bytes(fs_info.totalBytes) + // Zeigt die Groeße des Speichers
|
||||
"\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz
|
||||
webserver.send(200, "application/json", temp);
|
||||
return true;
|
||||
}
|
||||
|
||||
void delete_recursive(const String &path)
|
||||
{
|
||||
if (LittleFS.remove(path))
|
||||
{
|
||||
LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w");
|
||||
return;
|
||||
}
|
||||
Dir dir = LittleFS.openDir(path);
|
||||
while (dir.next())
|
||||
{
|
||||
delete_recursive(path + '/' + dir.fileName());
|
||||
}
|
||||
LittleFS.rmdir(path);
|
||||
}
|
||||
|
||||
bool handle_file(String &&path)
|
||||
{
|
||||
if (webserver.hasArg("new"))
|
||||
{
|
||||
String folderName{webserver.arg("new")};
|
||||
for (auto &c : {34, 37, 38, 47, 58, 59, 92})
|
||||
for (auto &e : folderName)
|
||||
if (e == c)
|
||||
{
|
||||
e = 95; // Ersetzen der nicht erlaubten Zeichen
|
||||
}
|
||||
LittleFS.mkdir(folderName);
|
||||
}
|
||||
if (webserver.hasArg("sort"))
|
||||
{
|
||||
return handle_list();
|
||||
}
|
||||
if (webserver.hasArg("delete"))
|
||||
{
|
||||
delete_recursive(webserver.arg("delete"));
|
||||
send_response();
|
||||
return true;
|
||||
}
|
||||
if (!LittleFS.exists("fs.html"))
|
||||
webserver.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermoeglicht das hochladen der fs.html
|
||||
if (path.endsWith("/"))
|
||||
{
|
||||
path += "index.html";
|
||||
}
|
||||
if (path == "/spiffs.html")
|
||||
{
|
||||
send_response(); // Vorruebergehend fuer den Admin Tab
|
||||
}
|
||||
return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); webserver.streamFile(f, mime::getContentType(path)); f.close(); true; }) : false;
|
||||
}
|
||||
|
||||
void handle_upload() // Dateien ins Filesystem schreiben
|
||||
{
|
||||
static File fsUploadFile;
|
||||
HTTPUpload &upload = webserver.upload();
|
||||
if (upload.status == UPLOAD_FILE_START)
|
||||
{
|
||||
if (upload.filename.length() > 31)
|
||||
{ // Dateinamen kuerzen
|
||||
upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
|
||||
}
|
||||
printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
|
||||
fsUploadFile = LittleFS.open(webserver.arg(0) + "/" + webserver.urlDecode(upload.filename), "w");
|
||||
}
|
||||
else if (upload.status == UPLOAD_FILE_WRITE)
|
||||
{
|
||||
printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize);
|
||||
fsUploadFile.write(upload.buf, upload.currentSize);
|
||||
}
|
||||
else if (upload.status == UPLOAD_FILE_END)
|
||||
{
|
||||
printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize);
|
||||
fsUploadFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
void format_filesystem() // Formatiert das Filesystem
|
||||
{
|
||||
LittleFS.format();
|
||||
send_response();
|
||||
}
|
||||
|
||||
void send_response()
|
||||
{
|
||||
webserver.sendHeader("Location", "fs.html");
|
||||
webserver.send(303, "message/http");
|
||||
}
|
||||
|
||||
const String format_bytes(size_t const &bytes)
|
||||
{ // lesbare Anzeige der Speichergroeßen
|
||||
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB"
|
||||
: static_cast<String>(bytes / 1048576.0) + " MB";
|
||||
}
|
||||
Reference in New Issue
Block a user